summaryrefslogtreecommitdiff
path: root/rt/lib/RT/Search/Simple.pm
diff options
context:
space:
mode:
Diffstat (limited to 'rt/lib/RT/Search/Simple.pm')
-rw-r--r--rt/lib/RT/Search/Simple.pm289
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;