[Rt-commit] rt branch, 5.0.0-releng, updated. rt-5.0.0beta2-86-g0d4c430084

? sunnavy sunnavy at bestpractical.com
Fri Jul 10 17:47:13 EDT 2020


The branch, 5.0.0-releng has been updated
       via  0d4c430084d2e04dabb9de1ce0255549c622fc95 (commit)
       via  055e579a91c62b880dff3ca77796ac0b2fe99fe6 (commit)
       via  1b478393a406edc424cebb6a452ad3e882f0a61b (commit)
       via  c531c1ffeb2283562ff95cf42b189f944986501e (commit)
       via  755f489c640b8c6a9d2735a9e5ee3c7cb63b50b1 (commit)
       via  24d65aed3c15e3313a968a054eb6071917592cae (commit)
       via  fd7a7a780cba1dff8a778e30b85eece1ec379e15 (commit)
       via  0eb975e896070d1da1f0fd31923a7bea81532f4d (commit)
       via  528b8d7defa1028f495ba57088f8017126dec1ac (commit)
       via  5e988bb37eb44d61760f8056671da404e83fb9ed (commit)
       via  a9ea99b27ef286813902b0f2be4fb0e269f59fe7 (commit)
       via  decc8769db1237675af7433ef52364731d26e37b (commit)
       via  9b4134f6dc83d4e4c72d3a758e620aca4f00294d (commit)
       via  ddecbcb998385c9ad7e3b0c1e555ebfb2062c03e (commit)
       via  aa5063c9f2a0b9936a9a117ac66bcf2e58a2fd5d (commit)
       via  42fce4092aed2020cf893912b2f73e60f8f8b0f8 (commit)
       via  a88446da3853bd9e60a6811803ba64c217f9806c (commit)
       via  24b4ef9b27ac65fb6c122e81e68f7ec3d9e2213e (commit)
       via  a59bdd9bf2d9277ddf3adc9a85e6753cc98cda8f (commit)
       via  ce833d0d0303b53ff8e35a2518bf2f3e794cd346 (commit)
       via  926c20ea994c1f0794e02d8c30aafdc4523721c3 (commit)
       via  1f67e8fa35e6fa2b8a4803981f47f39ad5bcc0c3 (commit)
       via  34860e5c0407e253cff8da555ce41137583dd532 (commit)
       via  1b7a58004491ca9f87e93f71465c0faae0c5dd50 (commit)
       via  7d9d841dbd2d3aa84ab3ea21be73eed3a4368d01 (commit)
      from  2b1b9f286c9f3822bcaabbf3d172812edcbcc70c (commit)

Summary of changes:
 docs/customizing/assets/tutorial.pod            |  14 +++++------
 docs/images/asset-cfs.png                       | Bin 0 -> 87310 bytes
 docs/images/asset-date-details.png              | Bin 0 -> 51456 bytes
 docs/images/asset-search.png                    | Bin 0 -> 114293 bytes
 docs/images/asset-ticket-create-selfservice.png | Bin 0 -> 53494 bytes
 docs/images/asset-ticket-create.png             | Bin 0 -> 107590 bytes
 docs/images/catalog-rights.png                  | Bin 0 -> 80320 bytes
 docs/images/edit-catalog.png                    | Bin 0 -> 47123 bytes
 docs/query_builder.pod                          |   3 +--
 docs/web_deployment.pod                         |  13 ++++++++++
 etc/RT_Config.pm.in                             |   8 ++++++
 etc/cpanfile                                    |   5 ++--
 lib/RT/CustomRole.pm                            |  16 ++++++------
 lib/RT/Interface/Web/QueryBuilder/Tree.pm       |  32 +++++++++++++++++++++---
 lib/RT/Queue.pm                                 |   3 +++
 lib/RT/Record/Role/Roles.pm                     |   7 ++++--
 lib/RT/Report/Tickets.pm                        |   2 +-
 lib/RT/Test/GnuPG.pm                            |   2 ++
 lib/RT/Tickets.pm                               |   2 +-
 lib/RT/Transactions.pm                          |   2 +-
 sbin/rt-email-digest.in                         |  10 +++++++-
 share/html/Search/Elements/PickBasics           |   2 +-
 t/customroles/basic.t                           |  30 +++++++++++++++++++---
 t/rest2/ticket-customroles.t                    |   2 +-
 24 files changed, 118 insertions(+), 35 deletions(-)
 create mode 100644 docs/images/asset-cfs.png
 create mode 100644 docs/images/asset-date-details.png
 create mode 100644 docs/images/asset-search.png
 create mode 100644 docs/images/asset-ticket-create-selfservice.png
 create mode 100644 docs/images/asset-ticket-create.png
 create mode 100644 docs/images/catalog-rights.png
 create mode 100644 docs/images/edit-catalog.png

- Log -----------------------------------------------------------------
commit c531c1ffeb2283562ff95cf42b189f944986501e
Merge: 2b1b9f286c 755f489c64
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Sat Jul 11 03:20:18 2020 +0800

    Merge branch '4.4-trunk' into 5.0.0-releng
    
    In 4.4, we require DBIx::SearchBuilder 1.68+ for mysql, 1.66+ for Pg and
    1.65 for others. For big version bump like RT 5, here we simplify it to
    be 1.68+ for all database types.

diff --cc docs/web_deployment.pod
index 3ba4f83e71,d4d6a43122..a36e40304d
--- a/docs/web_deployment.pod
+++ b/docs/web_deployment.pod
@@@ -32,6 -32,53 +32,19 @@@ spontaneously logged in as other users 
  See also L<authentication/Apache configuration>, in case you intend to
  use Apache to provide authentication.
  
+ =head3 prefork MPM
+ 
+ Apache can run with several different
+ L<Multi-Processing Modules (MPMs)|https://httpd.apache.org/docs/2.4/mpm.html>.
+ RT is designed to run only with the L<prefork MPM|https://httpd.apache.org/docs/2.4/mod/prefork.html>.
+ RT will run with the event MPM, but it will run very poorly
+ and you are likely to see issues like intermittent errors when loading
+ pages, slowness, and possible excessive memory usage.
+ 
+ In the past, Apache defaulted to the prefork MPM, but the packaged Apache
+ on many newer Linux systems defaults to event, so it is important to
+ make sure Apache is configured to use prefork on your RT server.
+ 
 -=head3 mod_fastcgi
 -
 -    # Tell FastCGI to put its temporary files somewhere sane; this may
 -    # be necessary if your distribution doesn't already set it
 -    #FastCgiIpcDir /tmp
 -
 -    FastCgiServer /opt/rt4/sbin/rt-server.fcgi -processes 5 -idle-timeout 300
 -
 -    <VirtualHost rt.example.com>
 -        ### Optional apache logs for RT
 -        # Ensure that your log rotation scripts know about these files
 -        # ErrorLog /opt/rt4/var/log/apache2.error
 -        # TransferLog /opt/rt4/var/log/apache2.access
 -        # LogLevel debug
 -
 -        AddDefaultCharset UTF-8
 -
 -        ScriptAlias / /opt/rt4/sbin/rt-server.fcgi/
 -
 -        DocumentRoot "/opt/rt4/share/html"
 -        <Location />
 -            <IfVersion >= 2.4> # For Apache 2.4
 -                Require all granted
 -            </IfVersion>
 -            <IfVersion < 2.4>  # For Apache 2.2
 -                Order allow,deny
 -                Allow from all
 -            </IfVersion>
 -
 -            Options +ExecCGI
 -            AddHandler fastcgi-script fcgi
 -        </Location>
 -    </VirtualHost>
 -
  =head3 mod_fcgid
  
  B<WARNING>: Before mod_fcgid 2.3.6, the maximum request size was 1GB.
diff --cc etc/RT_Config.pm.in
index 51fe3c13e3,44e8ce56d1..a7301b3436
--- a/etc/RT_Config.pm.in
+++ b/etc/RT_Config.pm.in
@@@ -3750,9 -3012,13 +3754,13 @@@ Set( %Crypt
          Encrypt => 0,
          Sign    => 0,
      },
+     DigestEmail => {
+         Encrypt => 0,
+         Sign    => 0,
+     },
  );
  
 -=head2 SMIME configuration
 +=head3 SMIME configuration
  
  A full description of the SMIME integration can be found in
  L<RT::Crypt::SMIME>.
diff --cc etc/cpanfile
index a4f903c1e9,0000000000..d9eb5d629a
mode 100644,000000..100644
--- a/etc/cpanfile
+++ b/etc/cpanfile
@@@ -1,222 -1,0 +1,221 @@@
 +requires 'perl', '5.10.1';
 +
 +# Core dependencies
 +requires 'Apache::Session', '>= 1.53';
 +requires 'Business::Hours';
 +requires 'CGI', ($] >= 5.019003 ? '>= 4.00' : '>= 3.38');
 +requires 'CGI::Cookie', '>= 1.20';
 +requires 'CGI::Emulate::PSGI';
 +requires 'CGI::PSGI', '>= 0.12';
 +requires 'Class::Accessor::Fast';
 +requires 'Clone';
 +requires 'Convert::Color';
 +requires 'Crypt::Eksblowfish';
 +requires 'CSS::Minifier::XS';
 +requires 'CSS::Squish', '>= 0.06';
 +requires 'Data::GUID';
 +requires 'Data::ICal';
 +requires 'Data::Page::Pageset';
 +requires 'Date::Extract', '>= 0.02';
 +requires 'Date::Manip';
 +requires 'DateTime', '>= 0.44';
 +requires 'DateTime::Format::Natural', '>= 0.67';
 +requires 'DateTime::Locale', '>= 0.40, != 1.00, != 1.01';
 +requires 'DBI', '>= 1.37';
- requires 'DBIx::SearchBuilder', '>= 1.65';
++requires 'DBIx::SearchBuilder', '>= 1.68';
 +requires 'Devel::GlobalDestruction';
 +requires 'Devel::StackTrace', '>= 1.19, != 1.28, != 1.29';
 +requires 'Digest::base';
 +requires 'Digest::MD5', '>= 2.27';
 +requires 'Digest::SHA';
 +requires 'Email::Address', '>= 1.912';
 +requires 'Email::Address::List', '>= 0.06';
 +requires 'Encode', '>= 2.64';
 +requires 'Encode::Detect::Detector';
 +requires 'Encode::HanExtra';
 +requires 'Errno';
 +requires 'File::Glob';
 +requires 'File::ShareDir';
 +requires 'File::Spec', '>= 0.8';
 +requires 'File::Temp', '>= 0.19';
 +requires 'HTML::Entities';
 +requires 'HTML::FormatExternal';
 +requires 'HTML::FormatText::WithLinks', '>= 0.14';
 +requires 'HTML::FormatText::WithLinks::AndTables', '>= 0.06';
 +requires 'HTML::Gumbo';
 +requires 'HTML::Mason', '>= 1.43';
 +requires 'HTML::Mason::PSGIHandler', '>= 0.52';
 +requires 'HTML::Quoted';
 +requires 'HTML::RewriteAttributes', '>= 0.05';
 +requires 'HTML::Scrubber', '>= 0.08';
 +requires 'HTTP::Message', '>= 6.0';
 +requires 'IPC::Run3';
 +requires 'JavaScript::Minifier::XS';
 +requires 'JSON';
 +requires 'List::MoreUtils';
 +requires 'Locale::Maketext', '>= 1.06';
 +requires 'Locale::Maketext::Fuzzy', '>= 0.11';
 +requires 'Locale::Maketext::Lexicon', '>= 0.32';
 +requires 'Log::Dispatch', '>= 2.30';
 +requires 'LWP::Simple';
 +requires 'Mail::Header', '>= 2.12';
 +requires 'Mail::Mailer', '>= 1.57';
 +requires 'MIME::Entity', '>= 5.504';
 +requires 'MIME::Types';
 +requires 'Module::Refresh', '>= 0.03';
 +requires 'Module::Versions::Report', '>= 1.05';
 +requires 'Net::CIDR';
 +requires 'Net::IP';
 +requires 'Plack', '>= 1.0002';
 +requires 'Plack::Handler::Starlet';
 +requires 'Pod::Select';
 +requires 'Regexp::Common';
 +requires 'Regexp::Common::net::CIDR';
 +requires 'Regexp::IPv6';
 +requires 'Role::Basic', '>= 0.12';
 +requires 'Scalar::Util';
 +requires 'Scope::Upper';
 +requires 'Storable', '>= 2.08';
 +requires 'Symbol::Global::Name', ($] >= 5.019003 ? '>= 0.05' : '>= 0.04');
 +requires 'Sys::Syslog', '>= 0.16';
 +requires 'Text::Password::Pronounceable';
 +requires 'Text::Quoted', '>= 2.07';
 +requires 'Text::Template', '>= 1.44';
 +requires 'Text::WikiFormat', '>= 0.76';
 +requires 'Text::Wrapper';
 +requires 'Time::HiRes';
 +requires 'Time::ParseDate';
 +requires 'Tree::Simple', '>= 1.04';
 +requires 'UNIVERSAL::require';
 +requires 'URI', '>= 1.59';
 +requires 'URI::QueryParam';
 +requires 'XML::RSS', '>= 1.05';
 +
 +# Mailgate
 +requires 'Getopt::Long';
 +requires 'LWP::Protocol::https';
 +requires 'LWP::UserAgent', '>= 6.02';
 +requires 'Mozilla::CA';
 +requires 'Pod::Usage';
 +
 +# CLI
 +requires 'Getopt::Long', '>= 2.24';
 +requires 'HTTP::Request::Common';
 +requires 'LWP', '>= 6.02';
 +requires 'Term::ReadKey';
 +requires 'Term::ReadLine';
 +requires 'Text::ParseWords';
 +
 +# REST2
 +requires 'Module::Runtime';
 +requires 'Moose';
 +requires 'MooseX::NonMoose';
 +requires 'MooseX::Role::Parameterized';
 +requires 'namespace::autoclean';
 +requires 'Sub::Exporter';
 +requires 'Web::Machine' => '>= 0.12';
 +requires 'Module::Path';
 +requires 'Path::Dispatcher' => '>= 1.07';
 +
 +on 'develop' => sub {
 +    requires 'Email::Abstract';
 +    requires 'File::Find';
 +    requires 'File::Which';
 +    requires 'HTML::Entities';
 +    requires 'Locale::PO';
 +    requires 'Log::Dispatch::Perl';
-     requires 'Mojo::DOM';
++    requires 'Mojolicious', '!= 8.54';
 +    requires 'Plack::Middleware::Test::StashWarnings', '>= 0.08';
 +    requires 'Pod::Simple', '>= 3.24';
 +    requires 'Set::Tiny';
 +    requires 'String::ShellQuote';
 +    requires 'Test::Builder', '>= 0.90';
 +    requires 'Test::Deep';
 +    requires 'Test::Email';
 +    requires 'Test::Expect', '>= 0.31';
 +    requires 'Test::LongString';
 +    requires 'Test::MockTime';
 +    requires 'Test::NoWarnings';
 +    requires 'Test::Pod';
 +    requires 'Test::Warn';
 +    requires 'Test::WWW::Mechanize', '>= 1.30';
 +    requires 'Test::WWW::Mechanize::PSGI';
 +    requires 'Try::Tiny';
 +    requires 'WWW::Mechanize', '>= 1.80';
 +    requires 'XML::Simple';
 +};
 +
 +
 +# Deployment options
 +feature 'standalone' => sub {};
 +
 +feature 'fastcgi' => sub {
 +    requires 'FCGI', '>= 0.74';
 +};
 +
 +feature 'modperl1' => sub {
 +    requires 'Apache::Request';
 +};
 +
 +feature 'modperl2' => sub {};
 +
 +
 +# Database options
 +feature 'mysql' => sub {
 +    requires 'DBD::mysql', '>= 2.1018, != 4.042';
 +};
 +
 +feature 'oracle' => sub {
 +    requires 'DBD::Oracle', '!= 1.23';
 +};
 +
 +feature 'pg' => sub {
-     requires 'DBIx::SearchBuilder', '>= 1.66';
 +    requires 'DBD::Pg', '>= 1.43';
 +};
 +
 +feature 'sqlite' => sub {
 +    requires 'DBD::SQLite', '>= 1.00';
 +};
 +
 +
 +# Optional features
 +feature 'gpg' => sub {
 +    requires 'File::Which';
 +    requires 'GnuPG::Interface', '>= 1.00';
 +    requires 'PerlIO::eol';
 +};
 +
 +feature 'smime' => sub {
 +    requires 'Crypt::X509';
 +    requires 'File::Which';
 +    requires 'String::ShellQuote';
 +};
 +
 +feature 'graphviz' => sub {
 +    requires 'GraphViz';
 +    requires 'IPC::Run', '>= 0.90';
 +};
 +
 +feature 'gd' => sub {
 +    requires 'GD';
 +    requires 'GD::Graph', '>= 1.47';
 +    requires 'GD::Text';
 +};
 +
 +feature 'externalauth' => sub {
 +    requires 'Net::SSLeay';
 +    requires 'Net::LDAP';
 +    on 'develop' => sub {
 +        requires 'Net::LDAP::Server::Test';
 +    };
 +};
 +
 +
 +# External attachment storage
 +feature 's3' => sub {
 +    requires 'Amazon::S3';
 +};
 +
 +feature 'dropbox' => sub {
 +    requires 'File::Dropbox', '>= 0.7';
 +};
diff --cc lib/RT/Interface/Web/QueryBuilder/Tree.pm
index d3f193d759,f9cdd6144f..62c9b34fd2
--- a/lib/RT/Interface/Web/QueryBuilder/Tree.pm
+++ b/lib/RT/Interface/Web/QueryBuilder/Tree.pm
@@@ -110,10 -114,30 +114,30 @@@ sub GetReferencedQueues 
              return unless $node->isLeaf;
  
              my $clause = $node->getNodeValue();
-             return unless $clause->{Key} =~ /^(?:Ticket)?Queue$/;
-             return unless $clause->{Op} eq '=';
- 
-             $queues->{ $clause->{Value} } = 1;
 -            if ( $clause->{Key} eq 'Queue' ) {
++            if ( $clause->{Key} =~ /^(?:Ticket)?Queue$/ ) {
+                 if ( $clause->{Op} eq '=' ) {
+                     $queues->{ $clause->{Value} } ||= 1;
+                 }
+                 elsif ( $clause->{Op} =~ /^LIKE$/i ) {
+                     my $qs = RT::Queues->new( $args{CurrentUser} || $HTML::Mason::Commands::session{CurrentUser} );
+                     $qs->Limit( FIELD => 'Name', VALUE => $clause->{Value}, OPERATOR => 'LIKE' );
+                     while ( my $q = $qs->Next ) {
+                         next unless $q->id;
+                         $queues->{ $q->id } ||= 1;
+                     }
+                 }
+             }
+             elsif ( $clause->{Key} eq 'Lifecycle' ) {
+                 if ( $clause->{Op} eq '=' ) {
+                     my $qs = RT::Queues->new( $args{CurrentUser} || $HTML::Mason::Commands::session{CurrentUser} );
+                     $qs->Limit( FIELD => 'Lifecycle', VALUE => $clause->{Value} );
+                     while ( my $q = $qs->Next ) {
+                         next unless $q->id;
+                         $queues->{ $q->id } ||= 1;
+                     }
+                 }
+             }
+             return;
          }
      );
  
diff --cc lib/RT/Tickets.pm
index 8110e061d2,e094c63ef6..290a0916f9
--- a/lib/RT/Tickets.pm
+++ b/lib/RT/Tickets.pm
@@@ -3133,45 -3133,6 +3133,45 @@@ sub _parser 
          }
      );
  
 +    if ( RT->Config->Get('EnablePriorityAsString') ) {
-         my $queues = $tree->GetReferencedQueues;
++        my $queues = $tree->GetReferencedQueues( CurrentUser => $self->CurrentUser );
 +        my %config = RT->Config->Get('PriorityAsString');
 +        my @names;
 +        if (%$queues) {
 +            for my $id ( keys %$queues ) {
 +                my $queue = RT::Queue->new( $self->CurrentUser );
 +                $queue->Load($id);
 +                if ( $queue->Id ) {
 +                    push @names, $queue->__Value('Name');    # Skip ACL check
 +                }
 +            }
 +        }
 +        else {
 +            @names = keys %config;
 +        }
 +
 +        my %map;
 +        for my $name (@names) {
 +            if ( my $value = exists $config{$name} ? $config{$name} : $config{Default} ) {
 +                my %hash = ref $value eq 'ARRAY' ? @$value : %$value;
 +                for my $label ( keys %hash ) {
 +                    $map{lc $label} //= $hash{$label};
 +                }
 +            }
 +        }
 +
 +        $tree->traverse(
 +            sub {
 +                my $node = shift;
 +                return unless $node->isLeaf;
 +                my $value = $node->getNodeValue;
 +                if ( $value->{Key} =~ /^(?:Initial|Final)?Priority$/i ) {
 +                    $value->{Value} = $map{ lc $value->{Value} } if defined $map{ lc $value->{Value} };
 +                }
 +            }
 +        );
 +    }
 +
      # Perform an optimization pass looking for watcher bundling
      $tree->traverse(
          sub {
diff --cc lib/RT/Transactions.pm
index eba3a04996,6c602b39db..43458aaaf9
--- a/lib/RT/Transactions.pm
+++ b/lib/RT/Transactions.pm
@@@ -146,986 -138,6 +146,986 @@@ sub AddRecord 
      return $self->SUPER::AddRecord($record);
  }
  
 +our %FIELD_METADATA = (
 +    id         => ['INT'],                 #loc_left_pair
 +    ObjectId   => ['ID'],                  #loc_left_pair
 +    ObjectType => ['STRING'],              #loc_left_pair
 +    Creator    => [ 'ENUM' => 'User' ],    #loc_left_pair
 +    TimeTaken  => ['INT'],                 #loc_left_pair
 +
 +    Type          => ['STRING'],                 #loc_left_pair
 +    Field         => ['STRING'],                 #loc_left_pair
 +    OldValue      => ['STRING'],                 #loc_left_pair
 +    NewValue      => ['STRING'],                 #loc_left_pair
 +    ReferenceType => ['STRING'],                 #loc_left_pair
 +    OldReference  => ['STRING'],                 #loc_left_pair
 +    NewReference  => ['STRING'],                 #loc_left_pair
 +    Data          => ['STRING'],                 #loc_left_pair
 +    Created       => [ 'DATE' => 'Created' ],    #loc_left_pair
 +
 +    Content     => ['ATTACHCONTENT'],            #loc_left_pair
 +    ContentType => ['ATTACHFIELD'],              #loc_left_pair
 +    Filename    => ['ATTACHFIELD'],              #loc_left_pair
 +    Subject     => ['ATTACHFIELD'],              #loc_left_pair
 +
 +    CustomFieldValue => [ 'CUSTOMFIELD' => 'Transaction' ],    #loc_left_pair
 +    CustomField      => [ 'CUSTOMFIELD' => 'Transaction' ],    #loc_left_pair
 +    CF               => [ 'CUSTOMFIELD' => 'Transaction' ],    #loc_left_pair
 +
 +    TicketId              => ['TICKETFIELD'],                  #loc_left_pair
 +    TicketSubject         => ['TICKETFIELD'],                  #loc_left_pair
 +    TicketQueue           => ['TICKETFIELD'],                  #loc_left_pair
 +    TicketStatus          => ['TICKETFIELD'],                  #loc_left_pair
 +    TicketOwner           => ['TICKETFIELD'],                  #loc_left_pair
 +    TicketCreator         => ['TICKETFIELD'],                  #loc_left_pair
 +    TicketLastUpdatedBy   => ['TICKETFIELD'],                  #loc_left_pair
 +    TicketCreated         => ['TICKETFIELD'],                  #loc_left_pair
 +    TicketStarted         => ['TICKETFIELD'],                  #loc_left_pair
 +    TicketResolved        => ['TICKETFIELD'],                  #loc_left_pair
 +    TicketTold            => ['TICKETFIELD'],                  #loc_left_pair
 +    TicketLastUpdated     => ['TICKETFIELD'],                  #loc_left_pair
 +    TicketStarts          => ['TICKETFIELD'],                  #loc_left_pair
 +    TicketDue             => ['TICKETFIELD'],                  #loc_left_pair
 +    TicketPriority        => ['TICKETFIELD'],                  #loc_left_pair
 +    TicketInitialPriority => ['TICKETFIELD'],                  #loc_left_pair
 +    TicketFinalPriority   => ['TICKETFIELD'],                  #loc_left_pair
 +    TicketType            => ['TICKETFIELD'],                  #loc_left_pair
 +    TicketQueueLifecycle  => ['TICKETQUEUEFIELD'],             #loc_left_pair
 +
 +    CustomFieldName => ['CUSTOMFIELDNAME'],                    #loc_left_pair
 +    CFName          => ['CUSTOMFIELDNAME'],                    #loc_left_pair
 +
 +    OldCFValue => ['OBJECTCUSTOMFIELDVALUE'],                  #loc_left_pair
 +    NewCFValue => ['OBJECTCUSTOMFIELDVALUE'],                  #loc_left_pair
 +);
 +
 +# Lower Case version of FIELDS, for case insensitivity
 +our %LOWER_CASE_FIELDS = map { ( lc($_) => $_ ) } (keys %FIELD_METADATA);
 +
 +our %dispatch = (
 +    INT                    => \&_IntLimit,
 +    ID                     => \&_IdLimit,
 +    ENUM                   => \&_EnumLimit,
 +    DATE                   => \&_DateLimit,
 +    STRING                 => \&_StringLimit,
 +    CUSTOMFIELD            => \&_CustomFieldLimit,
 +    ATTACHFIELD            => \&_AttachLimit,
 +    ATTACHCONTENT          => \&_AttachContentLimit,
 +    TICKETFIELD            => \&_TicketLimit,
 +    TICKETQUEUEFIELD       => \&_TicketQueueLimit,
 +    OBJECTCUSTOMFIELDVALUE => \&_ObjectCustomFieldValueLimit,
 +    CUSTOMFIELDNAME        => \&_CustomFieldNameLimit,
 +);
 +
 +sub FIELDS     { return \%FIELD_METADATA }
 +
 +our @SORTFIELDS = qw(id ObjectId Created);
 +
 +=head2 SortFields
 +
 +Returns the list of fields that lists of transactions can easily be sorted by
 +
 +=cut
 +
 +sub SortFields {
 +    my $self = shift;
 +    return (@SORTFIELDS);
 +}
 +
 +=head1 Limit Helper Routines
 +
 +These routines are the targets of a dispatch table depending on the
 +type of field.  They all share the same signature:
 +
 +  my ($self,$field,$op,$value, at rest) = @_;
 +
 +The values in @rest should be suitable for passing directly to
 +DBIx::SearchBuilder::Limit.
 +
 +Essentially they are an expanded/broken out (and much simplified)
 +version of what ProcessRestrictions used to do.  They're also much
 +more clearly delineated by the TYPE of field being processed.
 +
 +=head2 _IdLimit
 +
 +Handle ID field.
 +
 +=cut
 +
 +sub _IdLimit {
 +    my ( $sb, $field, $op, $value, @rest ) = @_;
 +
 +    if ( $value eq '__Bookmarked__' ) {
 +        return $sb->_BookmarkLimit( $field, $op, $value, @rest );
 +    } else {
 +        return $sb->_IntLimit( $field, $op, $value, @rest );
 +    }
 +}
 +
 +=head2 _EnumLimit
 +
 +Handle Fields which are limited to certain values, and potentially
 +need to be looked up from another class.
 +
 +This subroutine actually handles two different kinds of fields.  For
 +some the user is responsible for limiting the values.  (i.e. ObjectType).
 +
 +For others, the value specified by the user will be looked by via
 +specified class.
 +
 +Meta Data:
 +  name of class to lookup in (Optional)
 +
 +=cut
 +
 +sub _EnumLimit {
 +    my ( $sb, $field, $op, $value, @rest ) = @_;
 +
 +    # SQL::Statement changes != to <>.  (Can we remove this now?)
 +    $op = "!=" if $op eq "<>";
 +
 +    die "Invalid Operation: $op for $field"
 +        unless $op eq "="
 +        or $op     eq "!=";
 +
 +    my $meta = $FIELD_METADATA{$field};
 +    if ( defined $meta->[1] && defined $value && $value !~ /^\d+$/ ) {
 +        my $class = "RT::" . $meta->[1];
 +        my $o     = $class->new( $sb->CurrentUser );
 +        $o->Load($value);
 +        $value = $o->Id || 0;
 +    }
 +    $sb->Limit(
 +        FIELD    => $field,
 +        VALUE    => $value,
 +        OPERATOR => $op,
 +        @rest,
 +    );
 +}
 +
 +=head2 _IntLimit
 +
 +Handle fields where the values are limited to integers.  (id)
 +
 +Meta Data:
 +  None
 +
 +=cut
 +
 +sub _IntLimit {
 +    my ( $sb, $field, $op, $value, @rest ) = @_;
 +
 +    my $is_a_like = $op =~ /MATCHES|ENDSWITH|STARTSWITH|LIKE/i;
 +
 +    # We want to support <id LIKE '1%'>, but we need to explicitly typecast
 +    # on Postgres
 +
 +    if ( $is_a_like && RT->Config->Get('DatabaseType') eq 'Pg' ) {
 +        return $sb->Limit(
 +            FUNCTION => "CAST(main.$field AS TEXT)",
 +            OPERATOR => $op,
 +            VALUE    => $value,
 +            @rest,
 +        );
 +    }
 +
 +    $sb->Limit(
 +        FIELD    => $field,
 +        VALUE    => $value,
 +        OPERATOR => $op,
 +        @rest,
 +    );
 +}
 +
 +=head2 _DateLimit
 +
 +Handle date fields.  (Created)
 +
 +Meta Data:
 +  1: type of link.  (Probably not necessary.)
 +
 +=cut
 +
 +sub _DateLimit {
 +    my ( $sb, $field, $op, $value, %rest ) = @_;
 +
 +    die "Invalid Date Op: $op"
 +        unless $op =~ /^(=|>|<|>=|<=|IS(\s+NOT)?)$/i;
 +
 +    my $meta = $FIELD_METADATA{$field};
 +    die "Incorrect Meta Data for $field"
 +        unless ( defined $meta->[1] );
 +
 +    if ( $op =~ /^(IS(\s+NOT)?)$/i) {
 +        return $sb->Limit(
 +            FUNCTION => $sb->NotSetDateToNullFunction,
 +            FIELD    => $meta->[1],
 +            OPERATOR => $op,
 +            VALUE    => "NULL",
 +            %rest,
 +        );
 +    }
 +
 +    my $date = RT::Date->new( $sb->CurrentUser );
 +    $date->Set( Format => 'unknown', Value => $value );
 +
 +    if ( $op eq "=" ) {
 +
 +        # if we're specifying =, that means we want everything on a
 +        # particular single day.  in the database, we need to check for >
 +        # and < the edges of that day.
 +
 +        $date->SetToMidnight( Timezone => 'server' );
 +        my $daystart = $date->ISO;
 +        $date->AddDay;
 +        my $dayend = $date->ISO;
 +
 +        $sb->_OpenParen;
 +
 +        $sb->Limit(
 +            FIELD    => $meta->[1],
 +            OPERATOR => ">=",
 +            VALUE    => $daystart,
 +            %rest,
 +        );
 +
 +        $sb->Limit(
 +            FIELD    => $meta->[1],
 +            OPERATOR => "<",
 +            VALUE    => $dayend,
 +            %rest,
 +            ENTRYAGGREGATOR => 'AND',
 +        );
 +
 +        $sb->_CloseParen;
 +
 +    }
 +    else {
 +        $sb->Limit(
 +            FUNCTION => $sb->NotSetDateToNullFunction,
 +            FIELD    => $meta->[1],
 +            OPERATOR => $op,
 +            VALUE    => $date->ISO,
 +            %rest,
 +        );
 +    }
 +}
 +
 +=head2 _StringLimit
 +
 +Handle simple fields which are just strings.  (Type, Field, OldValue, NewValue, ReferenceType)
 +
 +Meta Data:
 +  None
 +
 +=cut
 +
 +sub _StringLimit {
 +    my ( $sb, $field, $op, $value, @rest ) = @_;
 +
 +    # FIXME:
 +    # Valid Operators:
 +    #  =, !=, LIKE, NOT LIKE
 +    if ( RT->Config->Get('DatabaseType') eq 'Oracle'
 +        && (!defined $value || !length $value)
 +        && lc($op) ne 'is' && lc($op) ne 'is not'
 +    ) {
 +        if ($op eq '!=' || $op =~ /^NOT\s/i) {
 +            $op = 'IS NOT';
 +        } else {
 +            $op = 'IS';
 +        }
 +        $value = 'NULL';
 +    }
 +
 +    $sb->Limit(
 +        FIELD         => $field,
 +        OPERATOR      => $op,
 +        VALUE         => $value,
 +        CASESENSITIVE => 0,
 +        @rest,
 +    );
 +}
 +
 +=head2 _ObjectCustomFieldValueLimit
 +
 +Handle object custom field values.  (OldReference, NewReference)
 +
 +Meta Data:
 +  None
 +
 +=cut
 +
 +sub _ObjectCustomFieldValueLimit {
 +    my ( $self, $field, $op, $value, @rest ) = @_;
 +
 +    my $alias_name = $field =~ /new/i ? 'newocfv' : 'oldocfv';
 +    $self->{_sql_aliases}{$alias_name} ||= $self->Join(
 +        TYPE   => 'LEFT',
 +        FIELD1 => $field =~ /new/i ? 'NewReference' : 'OldReference',
 +        TABLE2 => 'ObjectCustomFieldValues',
 +        FIELD2 => 'id',
 +    );
 +
 +    my $value_is_long = ( length( Encode::encode( "UTF-8", $value ) ) > 255 ) ? 1 : 0;
 +
 +    $self->Limit(
 +        @rest,
 +        ALIAS         => $self->{_sql_aliases}{$alias_name},
 +        FIELD         => $value_is_long ? 'LargeContent' : 'Content',
 +        OPERATOR      => $op,
 +        VALUE         => $value,
 +        CASESENSITIVE => 0,
 +        @rest,
 +    );
 +}
 +
 +=head2 _CustomFieldNameLimit
 +
 +Handle custom field name field.  (Field)
 +
 +Meta Data:
 +  None
 +
 +=cut
 +
 +sub _CustomFieldNameLimit {
 +    my ( $self, $_field, $op, $value, %rest ) = @_;
 +
 +    $self->Limit(
 +        FIELD         => 'Type',
 +        OPERATOR      => '=',
 +        VALUE         => 'CustomField',
 +        CASESENSITIVE => 0,
 +        ENTRYAGGREGATOR => 'AND',
 +    );
 +
 +    if ( $value =~ /\D/ ) {
 +        my $cfs = RT::CustomFields->new( RT->SystemUser );
 +        $cfs->Limit(
 +            FIELD         => 'Name',
 +            VALUE         => $value,
 +            CASESENSITIVE => 0,
 +        );
 +        $value = [ map { $_->id } @{ $cfs->ItemsArrayRef } ];
 +
 +        $self->Limit(
 +            FIELD         => 'Field',
 +            OPERATOR      => $op eq '!=' ? 'NOT IN' : 'IN',
 +            VALUE         => $value,
 +            CASESENSITIVE => 0,
 +            ENTRYAGGREGATOR => 'AND',
 +            %rest,
 +        );
 +    }
 +    else {
 +        $self->Limit(
 +            FIELD         => 'Field',
 +            OPERATOR      => $op,
 +            VALUE         => $value,
 +            CASESENSITIVE => 0,
 +            ENTRYAGGREGATOR => 'AND',
 +            %rest,
 +        );
 +    }
 +}
 +
 +=head2 _CustomFieldDecipher
 +
 +Try and turn a CF descriptor into (cfid, cfname) object pair.
 +
 +Takes an optional second parameter of the CF LookupType, defaults to Ticket CFs.
 +
 +=cut
 +
 +sub _CustomFieldDecipher {
 +    my ($self, $string, $lookuptype) = @_;
 +    $lookuptype ||= $self->_SingularClass->CustomFieldLookupType;
 +
 +    my ($object, $field, $column) = ($string =~ /^(?:(.+?)\.)?\{(.+)\}(?:\.(Content|LargeContent))?$/);
 +    $field ||= ($string =~ /^\{(.*?)\}$/)[0] || $string;
 +
 +    my ($cf, $applied_to);
 +
 +    if ( $object ) {
 +        my $record_class = RT::CustomField->RecordClassFromLookupType($lookuptype);
 +        $applied_to = $record_class->new( $self->CurrentUser );
 +        $applied_to->Load( $object );
 +
 +        if ( $applied_to->id ) {
 +            RT->Logger->debug("Limiting to CFs identified by '$field' applied to $record_class #@{[$applied_to->id]} (loaded via '$object')");
 +        }
 +        else {
 +            RT->Logger->warning("$record_class '$object' doesn't exist, parsed from '$string'");
 +            $object = 0;
 +            undef $applied_to;
 +        }
 +    }
 +
 +    if ( $field =~ /\D/ ) {
 +        $object ||= '';
 +        my $cfs = RT::CustomFields->new( $self->CurrentUser );
 +        $cfs->Limit( FIELD => 'Name', VALUE => $field, CASESENSITIVE => 0 );
 +        $cfs->LimitToLookupType($lookuptype);
 +
 +        if ($applied_to) {
 +            $cfs->SetContextObject($applied_to);
 +            $cfs->LimitToObjectId($applied_to->id);
 +        }
 +
 +        # if there is more then one field the current user can
 +        # see with the same name then we shouldn't return cf object
 +        # as we don't know which one to use
 +        $cf = $cfs->First;
 +        if ( $cf ) {
 +            $cf = undef if $cfs->Next;
 +        }
 +    }
 +    else {
 +        $cf = RT::CustomField->new( $self->CurrentUser );
 +        $cf->Load( $field );
 +        $cf->SetContextObject($applied_to)
 +            if $cf->id and $applied_to;
 +    }
 +
 +    return ($object, $field, $cf, $column);
 +}
 +
 +=head2 _CustomFieldLimit
 +
 +Limit based on CustomFields
 +
 +Meta Data:
 +  none
 +
 +=cut
 +
 +sub _CustomFieldLimit {
 +    my ( $self, $_field, $op, $value, %rest ) = @_;
 +
 +    my $meta  = $FIELD_METADATA{ $_field };
 +    my $class = $meta->[1] || 'Transaction';
 +    my $type  = "RT::$class"->CustomFieldLookupType;
 +
 +    my $field = $rest{'SUBKEY'} || die "No field specified";
 +
 +    # For our sanity, we can only limit on one object at a time
 +
 +    my ($object, $cfid, $cf, $column);
 +    ($object, $field, $cf, $column) = $self->_CustomFieldDecipher( $field, $type );
 +
 +
 +    $self->_LimitCustomField(
 +        %rest,
 +        LOOKUPTYPE  => $type,
 +        CUSTOMFIELD => $cf || $field,
 +        KEY      => $cf ? $cf->id : "$type-$object.$field",
 +        OPERATOR => $op,
 +        VALUE    => $value,
 +        COLUMN   => $column,
 +        SUBCLAUSE => "txnsql",
 +    );
 +}
 +
 +=head2 _AttachLimit
 +
 +Limit based on the ContentType or the Filename of an attachment.
 +
 +=cut
 +
 +sub _AttachLimit {
 +    my ( $self, $field, $op, $value, %rest ) = @_;
 +
 +    unless ( defined $self->{_sql_aliases}{attach} ) {
 +        $self->{_sql_aliases}{attach} = $self->Join(
 +            TYPE   => 'LEFT', # not all txns have an attachment
 +            FIELD1 => 'id',
 +            TABLE2 => 'Attachments',
 +            FIELD2 => 'TransactionId',
 +        );
 +    }
 +
 +    $self->Limit(
 +        %rest,
 +        ALIAS         => $self->{_sql_aliases}{attach},
 +        FIELD         => $field,
 +        OPERATOR      => $op,
 +        VALUE         => $value,
 +        CASESENSITIVE => 0,
 +    );
 +}
 +
 +=head2 _AttachContentLimit
 +
 +Limit based on the Content of a transaction.
 +
 +=cut
 +
 +sub _AttachContentLimit {
 +
 +    my ( $self, $field, $op, $value, %rest ) = @_;
 +    $field = 'Content' if $field =~ /\W/;
 +
 +    my $config = RT->Config->Get('FullTextSearch') || {};
 +    unless ( $config->{'Enable'} ) {
 +        $self->Limit( %rest, FIELD => 'id', VALUE => 0 );
 +        return;
 +    }
 +
 +    unless ( defined $self->{_sql_aliases}{attach} ) {
 +        $self->{_sql_aliases}{attach} = $self->Join(
 +            TYPE   => 'LEFT', # not all txns have an attachment
 +            FIELD1 => 'id',
 +            TABLE2 => 'Attachments',
 +            FIELD2 => 'TransactionId',
 +        );
 +    }
 +
 +    $self->_OpenParen;
 +    if ( $config->{'Indexed'} ) {
 +        my $db_type = RT->Config->Get('DatabaseType');
 +
 +        my $alias;
 +        if ( $config->{'Table'} and $config->{'Table'} ne "Attachments") {
 +            $alias = $self->{'_sql_aliases'}{'full_text'} ||= $self->Join(
 +                TYPE   => 'LEFT',
 +                ALIAS1 => $self->{_sql_aliases}{attach},
 +                FIELD1 => 'id',
 +                TABLE2 => $config->{'Table'},
 +                FIELD2 => 'id',
 +            );
 +        } else {
 +            $alias = $self->{_sql_aliases}{attach};
 +        }
 +
 +        #XXX: handle negative searches
 +        my $index = $config->{'Column'};
 +        if ( $db_type eq 'Oracle' ) {
 +            my $dbh = $RT::Handle->dbh;
 +            my $alias = $self->{_sql_aliases}{attach};
 +            $self->Limit(
 +                %rest,
 +                FUNCTION      => "CONTAINS( $alias.$field, ".$dbh->quote($value) .")",
 +                OPERATOR      => '>',
 +                VALUE         => 0,
 +                QUOTEVALUE    => 0,
 +                CASESENSITIVE => 1,
 +            );
 +            # this is required to trick DBIx::SB's LEFT JOINS optimizer
 +            # into deciding that join is redundant as it is
 +            $self->Limit(
 +                ENTRYAGGREGATOR => 'AND',
 +                ALIAS           => $self->{_sql_aliases}{attach},
 +                FIELD           => 'Content',
 +                OPERATOR        => 'IS NOT',
 +                VALUE           => 'NULL',
 +            );
 +        }
 +        elsif ( $db_type eq 'Pg' ) {
 +            my $dbh = $RT::Handle->dbh;
 +            $self->Limit(
 +                %rest,
 +                ALIAS       => $alias,
 +                FIELD       => $index,
 +                OPERATOR    => '@@',
 +                VALUE       => 'plainto_tsquery('. $dbh->quote($value) .')',
 +                QUOTEVALUE  => 0,
 +            );
 +        }
 +        elsif ( $db_type eq 'mysql' and not $config->{Sphinx}) {
 +            my $dbh = $RT::Handle->dbh;
 +            $self->Limit(
 +                %rest,
 +                FUNCTION    => "MATCH($alias.Content)",
 +                OPERATOR    => 'AGAINST',
 +                VALUE       => "(". $dbh->quote($value) ." IN BOOLEAN MODE)",
 +                QUOTEVALUE  => 0,
 +            );
 +            # As with Oracle, above, this forces the LEFT JOINs into
 +            # JOINS, which allows the FULLTEXT index to be used.
 +            # Orthogonally, the IS NOT NULL clause also helps the
 +            # optimizer decide to use the index.
 +            $self->Limit(
 +                ENTRYAGGREGATOR => 'AND',
 +                ALIAS           => $alias,
 +                FIELD           => "Content",
 +                OPERATOR        => 'IS NOT',
 +                VALUE           => 'NULL',
 +                QUOTEVALUE      => 0,
 +            );
 +        }
 +        elsif ( $db_type eq 'mysql' ) {
 +            # This is a special character.  Note that \ does not escape
 +            # itself (in Sphinx 2.1.0, at least), so 'foo\;bar' becoming
 +            # 'foo\\;bar' is not a vulnerability, and is still parsed as
 +            # "foo, \, ;, then bar".  Happily, the default mode is
 +            # "all", meaning that boolean operators are not special.
 +            $value =~ s/;/\\;/g;
 +
 +            my $max = $config->{'MaxMatches'};
 +            $self->Limit(
 +                %rest,
 +                ALIAS       => $alias,
 +                FIELD       => 'query',
 +                OPERATOR    => '=',
 +                VALUE       => "$value;limit=$max;maxmatches=$max",
 +            );
 +        }
 +    } else {
 +        # This is the main difference from ticket content search.
 +        # For transaction searches, it probably worths keeping emails.
 +        # $self->Limit(
 +        #     %rest,
 +        #     FIELD    => 'Type',
 +        #     OPERATOR => 'NOT IN',
 +        #     VALUE    => ['EmailRecord', 'CommentEmailRecord'],
 +        # );
 +
 +        $self->Limit(
 +            ENTRYAGGREGATOR => 'AND',
 +            ALIAS           => $self->{_sql_aliases}{attach},
 +            FIELD           => $field,
 +            OPERATOR        => $op,
 +            VALUE           => $value,
 +            CASESENSITIVE   => 0,
 +        );
 +    }
 +    if ( RT->Config->Get('DontSearchFileAttachments') ) {
 +        $self->Limit(
 +            ENTRYAGGREGATOR => 'AND',
 +            ALIAS           => $self->{_sql_aliases}{attach},
 +            FIELD           => 'Filename',
 +            OPERATOR        => 'IS',
 +            VALUE           => 'NULL',
 +        );
 +    }
 +    $self->_CloseParen;
 +}
 +
 +sub _TicketLimit {
 +    my ( $self, $field, $op, $value, %rest ) = @_;
 +    $field =~ s!^Ticket!!;
 +
 +    if ( $field eq 'Queue' && $value =~ /\D/ ) {
 +        my $queue = RT::Queue->new($self->CurrentUser);
 +        $queue->Load($value);
 +        $value = $queue->id if $queue->id;
 +    }
 +
 +    if ( $field =~ /^(?:Owner|Creator)$/ && $value =~ /\D/ ) {
 +        my $user = RT::User->new( $self->CurrentUser );
 +        $user->Load($value);
 +        $value = $user->id if $user->id;
 +    }
 +
 +    $self->Limit(
 +        %rest,
 +        ALIAS         => $self->_JoinTickets,
 +        FIELD         => $field,
 +        OPERATOR      => $op,
 +        VALUE         => $value,
 +        CASESENSITIVE => 0,
 +    );
 +}
 +
 +sub _TicketQueueLimit {
 +    my ( $self, $field, $op, $value, %rest ) = @_;
 +    $field =~ s!^TicketQueue!!;
 +
 +    my $queue = $self->{_sql_aliases}{ticket_queues} ||= $_[0]->Join(
 +        ALIAS1 => $self->_JoinTickets,
 +        FIELD1 => 'Queue',
 +        TABLE2 => 'Queues',
 +        FIELD2 => 'id',
 +    );
 +
 +    $self->Limit(
 +        ALIAS    => $queue,
 +        FIELD    => $field,
 +        OPERATOR => $op,
 +        VALUE    => $value,
 +        %rest,
 +    );
 +}
 +
 +sub PrepForSerialization {
 +    my $self = shift;
 +    delete $self->{'items'};
 +    delete $self->{'items_array'};
 +    $self->RedoSearch();
 +}
 +
 +sub _OpenParen {
 +    $_[0]->SUPER::_OpenParen( $_[1] || 'txnsql' );
 +}
 +sub _CloseParen {
 +    $_[0]->SUPER::_CloseParen( $_[1] || 'txnsql' );
 +}
 +
 +sub Limit {
 +    my $self = shift;
 +    my %args = @_;
 +    $self->{'must_redo_search'} = 1;
 +    delete $self->{'raw_rows'};
 +    delete $self->{'count_all'};
 +
 +    $args{SUBCLAUSE} ||= "txnsql"
 +        if $self->{parsing_txnsql} and not $args{LEFTJOIN};
 +
 +    $self->SUPER::Limit(%args);
 +}
 +
 +=head2 FromSQL
 +
 +Convert a RT-SQL string into a set of SearchBuilder restrictions.
 +
 +Returns (1, 'Status message') on success and (0, 'Error Message') on
 +failure.
 +
 +=cut
 +
 +sub _parser {
 +    my ($self,$string) = @_;
 +
 +    require RT::Interface::Web::QueryBuilder::Tree;
 +    my $tree = RT::Interface::Web::QueryBuilder::Tree->new;
 +    my @results = $tree->ParseSQL(
 +        Query => $string,
 +        CurrentUser => $self->CurrentUser,
 +        Class => ref $self || $self,
 +    );
 +    die join "; ", map { ref $_ eq 'ARRAY' ? $_->[ 0 ] : $_ } @results if @results;
 +
 +
 +    # To handle __Active__ and __InActive__ statuses, copied from
 +    # RT::Tickets::_parser with field name updates, i.e.
 +    #   Lifecycle => TicketQueueLifecycle
 +    #   Status => TicketStatus
 +
 +    my ( $active_status_node, $inactive_status_node );
 +    my $escape_quotes = sub {
 +        my $text = shift;
 +        $text =~ s{(['\\])}{\\$1}g;
 +        return $text;
 +    };
 +
 +    $tree->traverse(
 +        sub {
 +            my $node = shift;
 +            return unless $node->isLeaf and $node->getNodeValue;
 +            my ($key, $subkey, $meta, $op, $value, $bundle)
 +                = @{$node->getNodeValue}{qw/Key Subkey Meta Op Value Bundle/};
 +            return unless $key eq "TicketStatus" && $value =~ /^(?:__(?:in)?active__)$/i;
 +
 +            my $parent = $node->getParent;
 +            my $index = $node->getIndex;
 +
 +            if ( ( lc $value eq '__inactive__' && $op eq '=' ) || ( lc $value eq '__active__' && $op eq '!=' ) ) {
 +                unless ( $inactive_status_node ) {
 +                    my %lifecycle =
 +                      map { $_ => $RT::Lifecycle::LIFECYCLES{ $_ }{ inactive } }
 +                      grep { @{ $RT::Lifecycle::LIFECYCLES{ $_ }{ inactive } || [] } }
 +                      grep { $RT::Lifecycle::LIFECYCLES_CACHE{ $_ }{ type } eq 'ticket' }
 +                      keys %RT::Lifecycle::LIFECYCLES;
 +                    return unless %lifecycle;
 +
 +                    my $sql;
 +                    if ( keys %lifecycle == 1 ) {
 +                        $sql = join ' OR ', map { qq{ TicketStatus = '$_' } } map { $escape_quotes->($_) } map { @$_ } values %lifecycle;
 +                    }
 +                    else {
 +                        my @inactive_sql;
 +                        for my $name ( keys %lifecycle ) {
 +                            my $escaped_name = $escape_quotes->($name);
 +                            my $inactive_sql =
 +                                qq{TicketQueueLifecycle = '$escaped_name'}
 +                              . ' AND ('
 +                              . join( ' OR ', map { qq{ TicketStatus = '$_' } } map { $escape_quotes->($_) } @{ $lifecycle{ $name } } ) . ')';
 +                            push @inactive_sql, qq{($inactive_sql)};
 +                        }
 +                        $sql = join ' OR ', @inactive_sql;
 +                    }
 +                    $inactive_status_node = RT::Interface::Web::QueryBuilder::Tree->new;
 +                    $inactive_status_node->ParseSQL(
 +                        Class       => ref $self,
 +                        Query       => $sql,
 +                        CurrentUser => $self->CurrentUser,
 +                    );
 +                }
 +                $parent->removeChild( $node );
 +                $parent->insertChild( $index, $inactive_status_node );
 +            }
 +            else {
 +                unless ( $active_status_node ) {
 +                    my %lifecycle =
 +                      map {
 +                        $_ => [
 +                            @{ $RT::Lifecycle::LIFECYCLES{ $_ }{ initial } || [] },
 +                            @{ $RT::Lifecycle::LIFECYCLES{ $_ }{ active }  || [] },
 +                          ]
 +                      }
 +                      grep {
 +                             @{ $RT::Lifecycle::LIFECYCLES{ $_ }{ initial } || [] }
 +                          || @{ $RT::Lifecycle::LIFECYCLES{ $_ }{ active }  || [] }
 +                      }
 +                      grep { $RT::Lifecycle::LIFECYCLES_CACHE{ $_ }{ type } eq 'ticket' }
 +                      keys %RT::Lifecycle::LIFECYCLES;
 +                    return unless %lifecycle;
 +
 +                    my $sql;
 +                    if ( keys %lifecycle == 1 ) {
 +                        $sql = join ' OR ', map { qq{ TicketStatus = '$_' } } map { $escape_quotes->($_) } map { @$_ } values %lifecycle;
 +                    }
 +                    else {
 +                        my @active_sql;
 +                        for my $name ( keys %lifecycle ) {
 +                            my $escaped_name = $escape_quotes->($name);
 +                            my $active_sql =
 +                                qq{TicketQueueLifecycle = '$escaped_name'}
 +                              . ' AND ('
 +                              . join( ' OR ', map { qq{ TicketStatus = '$_' } } map { $escape_quotes->($_) } @{ $lifecycle{ $name } } ) . ')';
 +                            push @active_sql, qq{($active_sql)};
 +                        }
 +                        $sql = join ' OR ', @active_sql;
 +                    }
 +                    $active_status_node = RT::Interface::Web::QueryBuilder::Tree->new;
 +                    $active_status_node->ParseSQL(
 +                        Class       => ref $self,
 +                        Query       => $sql,
 +                        CurrentUser => $self->CurrentUser,
 +                    );
 +                }
 +                $parent->removeChild( $node );
 +                $parent->insertChild( $index, $active_status_node );
 +            }
 +        }
 +    );
 +
 +    if ( RT->Config->Get('EnablePriorityAsString') ) {
-         my $queues = $tree->GetReferencedQueues;
++        my $queues = $tree->GetReferencedQueues( CurrentUser => $self->CurrentUser );
 +        my %config = RT->Config->Get('PriorityAsString');
 +        my @names;
 +        if (%$queues) {
 +            for my $id ( keys %$queues ) {
 +                my $queue = RT::Queue->new( $self->CurrentUser );
 +                $queue->Load($id);
 +                if ( $queue->Id ) {
 +                    push @names, $queue->__Value('Name');    # Skip ACL check
 +                }
 +            }
 +        }
 +        else {
 +            @names = keys %config;
 +        }
 +
 +        my %map;
 +        for my $name (@names) {
 +            if ( my $value = exists $config{$name} ? $config{$name} : $config{Default} ) {
 +                my %hash = ref $value eq 'ARRAY' ? @$value : %$value;
 +                for my $label ( keys %hash ) {
 +                    $map{lc $label} //= $hash{$label};
 +                }
 +            }
 +        }
 +
 +        $tree->traverse(
 +            sub {
 +                my $node = shift;
 +                return unless $node->isLeaf;
 +                my $value = $node->getNodeValue;
 +                if ( $value->{Key} =~ /^Ticket(?:Initial|Final)?Priority$/i ) {
 +                    $value->{Value} = $map{ lc $value->{Value} } if defined $map{ lc $value->{Value} };
 +                }
 +            }
 +        );
 +    }
 +
 +    my $ea = '';
 +    $tree->traverse(
 +        sub {
 +            my $node = shift;
 +            $ea = $node->getParent->getNodeValue if $node->getIndex > 0;
 +            return $self->_OpenParen unless $node->isLeaf;
 +
 +            my ($key, $subkey, $meta, $op, $value, $bundle)
 +                = @{$node->getNodeValue}{qw/Key Subkey Meta Op Value Bundle/};
 +
 +            # normalize key and get class (type)
 +            my $class = $meta->[0];
 +
 +            # replace __CurrentUser__ with id
 +            $value = $self->CurrentUser->id if $value eq '__CurrentUser__';
 +
 +            # replace __CurrentUserName__ with the username
 +            $value = $self->CurrentUser->Name if $value eq '__CurrentUserName__';
 +
 +            my $sub = $dispatch{ $class }
 +                or die "No dispatch method for class '$class'";
 +
 +            # A reference to @res may be pushed onto $sub_tree{$key} from
 +            # above, and we fill it here.
 +            $sub->( $self, $key, $op, $value,
 +                    ENTRYAGGREGATOR => $ea,
 +                    SUBKEY          => $subkey,
 +                    BUNDLE          => $bundle,
 +                  );
 +        },
 +        sub {
 +            my $node = shift;
 +            return $self->_CloseParen unless $node->isLeaf;
 +        }
 +    );
 +}
 +
 +sub FromSQL {
 +    my ($self,$query) = @_;
 +
 +    $self->CleanSlate;
 +    $self->_InitSQL;
 +
 +    return (1, $self->loc("No Query")) unless $query;
 +
 +    $self->{_sql_query} = $query;
 +    eval {
 +        local $self->{parsing_txnsql} = 1;
 +        $self->_parser( $query );
 +    };
 +    if ( $@ ) {
 +        my $error = "$@";
 +        $RT::Logger->error("Couldn't parse query: $error");
 +        return (0, $error);
 +    }
 +
 +    # set SB's dirty flag
 +    $self->{'must_redo_search'} = 1;
 +
 +    return (1, $self->loc("Valid Query"));
 +}
 +
 +sub _JoinTickets {
 +    my $self = shift;
 +    unless ( defined $self->{_sql_aliases}{tickets} ) {
 +        $self->{_sql_aliases}{tickets} = $self->Join(
 +            TYPE   => 'LEFT',
 +            FIELD1 => 'ObjectId',
 +            TABLE2 => 'Tickets',
 +            FIELD2 => 'id',
 +        );
 +    }
 +    return $self->{_sql_aliases}{tickets};
 +}
 +
 +=head2 Query
 +
 +Returns the last string passed to L</FromSQL>.
 +
 +=cut
 +
 +sub Query {
 +    my $self = shift;
 +    return $self->{_sql_query};
 +}
 +
  RT::Base->_ImportOverlays();
  
  1;

commit 1b478393a406edc424cebb6a452ad3e882f0a61b
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Sat Jul 11 04:01:54 2020 +0800

    Fix "Case sensitive search by Queues.Name" warnings in GetReferencedQueues

diff --git a/lib/RT/Interface/Web/QueryBuilder/Tree.pm b/lib/RT/Interface/Web/QueryBuilder/Tree.pm
index 62c9b34fd2..34d47851dc 100644
--- a/lib/RT/Interface/Web/QueryBuilder/Tree.pm
+++ b/lib/RT/Interface/Web/QueryBuilder/Tree.pm
@@ -120,7 +120,7 @@ sub GetReferencedQueues {
                 }
                 elsif ( $clause->{Op} =~ /^LIKE$/i ) {
                     my $qs = RT::Queues->new( $args{CurrentUser} || $HTML::Mason::Commands::session{CurrentUser} );
-                    $qs->Limit( FIELD => 'Name', VALUE => $clause->{Value}, OPERATOR => 'LIKE' );
+                    $qs->Limit( FIELD => 'Name', VALUE => $clause->{Value}, OPERATOR => 'LIKE', CASESENSITIVE => 0 );
                     while ( my $q = $qs->Next ) {
                         next unless $q->id;
                         $queues->{ $q->id } ||= 1;

commit 055e579a91c62b880dff3ca77796ac0b2fe99fe6
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Sat Jul 11 04:12:03 2020 +0800

    Grant SeeQueue in REST2 custom role tests so current user can see custom roles
    
    The right change was made in 4.4/custom-role-check-right

diff --git a/t/rest2/ticket-customroles.t b/t/rest2/ticket-customroles.t
index 418d572537..cad2992266 100644
--- a/t/rest2/ticket-customroles.t
+++ b/t/rest2/ticket-customroles.t
@@ -40,7 +40,7 @@ for my $email (qw/multi at example.com test at localhost multi2 at example.com single2 at ex
 }
 
 $user->PrincipalObj->GrantRight( Right => $_ )
-    for qw/CreateTicket ShowTicket ModifyTicket OwnTicket AdminUsers SeeGroup/;
+    for qw/CreateTicket ShowTicket ModifyTicket OwnTicket AdminUsers SeeGroup SeeQueue/;
 
 # Create and view ticket with no watchers
 {

commit 0d4c430084d2e04dabb9de1ce0255549c622fc95
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Sat Jul 11 03:35:40 2020 +0800

    Apply bootstrap style to queue text input in search builder
    
    This text input shows up when the QueueOp is set to "match" or "doesn't
    match".

diff --git a/share/html/Search/Elements/PickBasics b/share/html/Search/Elements/PickBasics
index c549d0fe53..e9e0414986 100644
--- a/share/html/Search/Elements/PickBasics
+++ b/share/html/Search/Elements/PickBasics
@@ -557,7 +557,7 @@ $m->callback( Conditions => \@lines );
 
     // create a text input box and hide it for use with matches / doesn't match
     // NB: if you give text a name it will add an additional term to the query!
-    var text = jQuery('<input>').attr('type','text');
+    var text = jQuery('<input>').attr('type','text').attr('class', 'form-control');
     text.hide();
     text.bind('change',function() {
         hidden[0].value = text[0].value;

-----------------------------------------------------------------------


More information about the rt-commit mailing list