diff options
Diffstat (limited to 'rt/lib/RT/Search/Simple.pm')
-rw-r--r-- | rt/lib/RT/Search/Simple.pm | 289 |
1 files changed, 289 insertions, 0 deletions
diff --git a/rt/lib/RT/Search/Simple.pm b/rt/lib/RT/Search/Simple.pm new file mode 100644 index 0000000..4cb2482 --- /dev/null +++ b/rt/lib/RT/Search/Simple.pm @@ -0,0 +1,289 @@ +# BEGIN BPS TAGGED BLOCK {{{ +# +# COPYRIGHT: +# +# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC +# <sales@bestpractical.com> +# +# (Except where explicitly superseded by other copyright notices) +# +# +# LICENSE: +# +# This work is made available to you under the terms of Version 2 of +# the GNU General Public License. A copy of that license should have +# been provided with this software, but in any event can be snarfed +# from www.gnu.org. +# +# This work is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 or visit their web page on the internet at +# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +# +# +# CONTRIBUTION SUBMISSION POLICY: +# +# (The following paragraph is not intended to limit the rights granted +# to you to modify and distribute this software under the terms of +# the GNU General Public License and is only of importance to you if +# you choose to contribute your changes and enhancements to the +# community by submitting them to Best Practical Solutions, LLC.) +# +# By intentionally submitting any modifications, corrections or +# derivatives to this work, or any other work intended for use with +# Request Tracker, to Best Practical Solutions, LLC, you confirm that +# you are the copyright holder for those contributions and you grant +# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +# royalty-free, perpetual, license to use, copy, create derivative +# works based on those contributions, and sublicense and distribute +# those contributions and any derivatives thereof. +# +# END BPS TAGGED BLOCK }}} + +=head1 NAME + + RT::Search::Simple + +=head1 SYNOPSIS + +=head1 DESCRIPTION + +Use the argument passed in as a simple set of keywords + +=head1 METHODS + +=cut + +package RT::Search::Simple; + +use strict; +use warnings; +use base qw(RT::Search); + +use Regexp::Common qw/delimited/; + +# Only a subset of limit types AND themselves together. "queue:foo +# queue:bar" is an OR, but "subject:foo subject:bar" is an AND +our %AND = ( + default => 1, + content => 1, + subject => 1, +); + +sub _Init { + my $self = shift; + my %args = @_; + + $self->{'Queues'} = delete( $args{'Queues'} ) || []; + $self->SUPER::_Init(%args); +} + +sub Describe { + my $self = shift; + return ( $self->loc( "Keyword and intuition-based searching", ref $self ) ); +} + +sub Prepare { + my $self = shift; + my $tql = $self->QueryToSQL( $self->Argument ); + + $RT::Logger->debug($tql); + + $self->TicketsObj->FromSQL($tql); + return (1); +} + +sub QueryToSQL { + my $self = shift; + my $query = shift || $self->Argument; + + my %limits; + $query =~ s/^\s*//; + while ($query =~ /^\S/) { + if ($query =~ s/^ + (?: + (\w+) # A straight word + (?:\. # With an optional .foo + ($RE{delimited}{-delim=>q['"]} + |[\w-]+ # Allow \w + dashes + ) # Which could be ."foo bar", too + )? + ) + : # Followed by a colon + ($RE{delimited}{-delim=>q['"]} + |\S+ + ) # And a possibly-quoted foo:"bar baz" + \s*//ix) { + my ($type, $extra, $value) = ($1, $2, $3); + ($value, my ($quoted)) = $self->Unquote($value); + $extra = $self->Unquote($extra) if defined $extra; + $self->Dispatch(\%limits, $type, $value, $quoted, $extra); + } elsif ($query =~ s/^($RE{delimited}{-delim=>q['"]}|\S+)\s*//) { + # If there's no colon, it's just a word or quoted string + my($val, $quoted) = $self->Unquote($1); + $self->Dispatch(\%limits, $self->GuessType($val, $quoted), $val, $quoted); + } + } + $self->Finalize(\%limits); + + my @clauses; + for my $subclause (sort keys %limits) { + next unless @{$limits{$subclause}}; + + my $op = $AND{lc $subclause} ? "AND" : "OR"; + push @clauses, "( ".join(" $op ", @{$limits{$subclause}})." )"; + } + + return join " AND ", @clauses; +} + +sub Dispatch { + my $self = shift; + my ($limits, $type, $contents, $quoted, $extra) = @_; + $contents =~ s/(['\\])/\\$1/g; + $extra =~ s/(['\\])/\\$1/g if defined $extra; + + my $method = "Handle" . ucfirst(lc($type)); + $method = "HandleDefault" unless $self->can($method); + my ($key, @tsql) = $self->$method($contents, $quoted, $extra); + push @{$limits->{$key}}, @tsql; +} + +sub Unquote { + # Given a word or quoted string, unquote it if it is quoted, + # removing escaped quotes. + my $self = shift; + my ($token) = @_; + if ($token =~ /^$RE{delimited}{-delim=>q['"]}{-keep}$/) { + my $quote = $2 || $5; + my $value = $3 || $6; + $value =~ s/\\(\\|$quote)/$1/g; + return wantarray ? ($value, 1) : $value; + } else { + return wantarray ? ($token, 0) : $token; + } +} + +sub Finalize { + my $self = shift; + my ($limits) = @_; + + # Assume that numbers were actually "default"s if we have other limits + if ($limits->{id} and keys %{$limits} > 1) { + my $values = delete $limits->{id}; + for my $value (@{$values}) { + $value =~ /(\d+)/ or next; + my ($key, @tsql) = $self->HandleDefault($1); + push @{$limits->{$key}}, @tsql; + } + } + + # Apply default "active status" limit if we don't have any status + # limits ourselves, and we're not limited by id + if (not $limits->{status} and not $limits->{id} + and RT::Config->Get('OnlySearchActiveTicketsInSimpleSearch', $self->TicketsObj->CurrentUser)) { + $limits->{status} = [map {s/(['\\])/\\$1/g; "Status = '$_'"} RT::Queue->ActiveStatusArray()]; + } + + # Respect the "only search these queues" limit if we didn't + # specify any queues ourselves + if (not $limits->{queue} and not $limits->{id}) { + for my $queue ( @{ $self->{'Queues'} } ) { + my $QueueObj = RT::Queue->new( $self->TicketsObj->CurrentUser ); + next unless $QueueObj->Load($queue); + my $name = $QueueObj->Name; + $name =~ s/(['\\])/\\$1/g; + push @{$limits->{queue}}, "Queue = '$name'"; + } + } +} + +our @GUESS = ( + [ 10 => sub { return "default" if $_[1] } ], + [ 20 => sub { return "id" if /^#?\d+$/ } ], + [ 30 => sub { return "requestor" if /\w+@\w+/} ], + [ 35 => sub { return "domain" if /^@\w+/} ], + [ 40 => sub { + return "status" if RT::Queue->new( $_[2] )->IsValidStatus( $_ ) + }], + [ 40 => sub { return "status" if /^((in)?active|any)$/i } ], + [ 50 => sub { + my $q = RT::Queue->new( $_[2] ); + return "queue" if $q->Load($_) and $q->Id and not $q->Disabled + }], + [ 60 => sub { + my $u = RT::User->new( $_[2] ); + return "owner" if $u->Load($_) and $u->Id and $u->Privileged + }], + [ 70 => sub { return "owner" if $_ eq "me" } ], +); + +sub GuessType { + my $self = shift; + my ($val, $quoted) = @_; + + my $cu = $self->TicketsObj->CurrentUser; + for my $sub (map $_->[1], sort {$a->[0] <=> $b->[0]} @GUESS) { + local $_ = $val; + my $ret = $sub->($val, $quoted, $cu); + return $ret if $ret; + } + return "default"; +} + +# $_[0] is $self +# $_[1] is escaped value without surrounding single quotes +# $_[2] is a boolean of "was quoted by the user?" +# ensure this is false before you do smart matching like $_[1] eq "me" +# $_[3] is escaped subkey, if any (see HandleCf) +sub HandleDefault { + my $fts = RT->Config->Get('FullTextSearch'); + if ($fts->{Enable} and $fts->{Indexed}) { + return default => "Content LIKE '$_[1]'"; + } else { + return default => "Subject LIKE '$_[1]'"; + } +} +sub HandleSubject { return subject => "Subject LIKE '$_[1]'"; } +sub HandleFulltext { return content => "Content LIKE '$_[1]'"; } +sub HandleContent { return content => "Content LIKE '$_[1]'"; } +sub HandleId { $_[1] =~ s/^#//; return id => "Id = $_[1]"; } +sub HandleStatus { + if ($_[1] =~ /^active$/i and !$_[2]) { + return status => map {s/(['\\])/\\$1/g; "Status = '$_'"} RT::Queue->ActiveStatusArray(); + } elsif ($_[1] =~ /^inactive$/i and !$_[2]) { + return status => map {s/(['\\])/\\$1/g; "Status = '$_'"} RT::Queue->InactiveStatusArray(); + } elsif ($_[1] =~ /^any$/i and !$_[2]) { + return 'status'; + } else { + return status => "Status = '$_[1]'"; + } +} +sub HandleOwner { + if (!$_[2] and $_[1] eq "me") { + return owner => "Owner.id = '__CurrentUser__'"; + } + elsif (!$_[2] and $_[1] =~ /\w+@\w+/) { + return owner => "Owner.EmailAddress = '$_[1]'"; + } else { + return owner => "Owner = '$_[1]'"; + } +} +sub HandleWatcher { + return watcher => (!$_[2] and $_[1] eq "me") ? "Watcher.id = '__CurrentUser__'" : "Watcher = '$_[1]'"; +} +sub HandleRequestor { return requestor => "Requestor STARTSWITH '$_[1]'"; } +sub HandleDomain { $_[1] =~ s/^@?/@/; return requestor => "Requestor ENDSWITH '$_[1]'"; } +sub HandleQueue { return queue => "Queue = '$_[1]'"; } +sub HandleQ { return queue => "Queue = '$_[1]'"; } +sub HandleCf { return "cf.$_[3]" => "'CF.{$_[3]}' LIKE '$_[1]'"; } + +RT::Base->_ImportOverlays(); + +1; |