1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2016 Best Practical Solutions, LLC
6 # <sales@bestpractical.com>
8 # (Except where explicitly superseded by other copyright notices)
13 # This work is made available to you under the terms of Version 2 of
14 # the GNU General Public License. A copy of that license should have
15 # been provided with this software, but in any event can be snarfed
18 # This work is distributed in the hope that it will be useful, but
19 # WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
21 # General Public License for more details.
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
26 # 02110-1301 or visit their web page on the internet at
27 # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
30 # CONTRIBUTION SUBMISSION POLICY:
32 # (The following paragraph is not intended to limit the rights granted
33 # to you to modify and distribute this software under the terms of
34 # the GNU General Public License and is only of importance to you if
35 # you choose to contribute your changes and enhancements to the
36 # community by submitting them to Best Practical Solutions, LLC.)
38 # By intentionally submitting any modifications, corrections or
39 # derivatives to this work, or any other work intended for use with
40 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
41 # you are the copyright holder for those contributions and you grant
42 # Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
43 # royalty-free, perpetual, license to use, copy, create derivative
44 # works based on those contributions, and sublicense and distribute
45 # those contributions and any derivatives thereof.
47 # END BPS TAGGED BLOCK }}}
57 Use the argument passed in as a simple set of keywords
63 package RT::Search::Simple;
67 use base qw(RT::Search);
69 use Regexp::Common qw/delimited/;
71 # Only a subset of limit types AND themselves together. "queue:foo
72 # queue:bar" is an OR, but "subject:foo subject:bar" is an AND
83 $self->{'Queues'} = delete( $args{'Queues'} ) || [];
84 $self->SUPER::_Init(%args);
89 return ( $self->loc( "Keyword and intuition-based searching", ref $self ) );
94 my $tql = $self->QueryToSQL( $self->Argument );
96 $RT::Logger->debug($tql);
98 $self->TicketsObj->FromSQL($tql);
104 my $query = shift || $self->Argument;
108 while ($query =~ /^\S/) {
111 (\w+) # A straight word
112 (?:\. # With an optional .foo
113 ($RE{delimited}{-delim=>q['"]}
114 |[\w-]+ # Allow \w + dashes
115 ) # Which could be ."foo bar", too
118 : # Followed by a colon
119 ($RE{delimited}{-delim=>q['"]}
121 ) # And a possibly-quoted foo:"bar baz"
123 my ($type, $extra, $value) = ($1, $2, $3);
124 ($value, my ($quoted)) = $self->Unquote($value);
125 $extra = $self->Unquote($extra) if defined $extra;
126 $self->Dispatch(\%limits, $type, $value, $quoted, $extra);
127 } elsif ($query =~ s/^($RE{delimited}{-delim=>q['"]}|\S+)\s*//) {
128 # If there's no colon, it's just a word or quoted string
129 my($val, $quoted) = $self->Unquote($1);
130 $self->Dispatch(\%limits, $self->GuessType($val, $quoted), $val, $quoted);
133 $self->Finalize(\%limits);
136 for my $subclause (sort keys %limits) {
137 next unless @{$limits{$subclause}};
139 my $op = $AND{lc $subclause} ? "AND" : "OR";
140 push @clauses, "( ".join(" $op ", @{$limits{$subclause}})." )";
143 return join " AND ", @clauses;
148 my ($limits, $type, $contents, $quoted, $extra) = @_;
149 $contents =~ s/(['\\])/\\$1/g;
150 $extra =~ s/(['\\])/\\$1/g if defined $extra;
152 my $method = "Handle" . ucfirst(lc($type));
153 $method = "HandleDefault" unless $self->can($method);
154 my ($key, @tsql) = $self->$method($contents, $quoted, $extra);
155 push @{$limits->{$key}}, @tsql;
159 # Given a word or quoted string, unquote it if it is quoted,
160 # removing escaped quotes.
163 if ($token =~ /^$RE{delimited}{-delim=>q['"]}{-keep}$/) {
164 my $quote = $2 || $5;
165 my $value = $3 || $6;
166 $value =~ s/\\(\\|$quote)/$1/g;
167 return wantarray ? ($value, 1) : $value;
169 return wantarray ? ($token, 0) : $token;
177 # Assume that numbers were actually "default"s if we have other limits
178 if ($limits->{id} and keys %{$limits} > 1) {
179 my $values = delete $limits->{id};
180 for my $value (@{$values}) {
181 $value =~ /(\d+)/ or next;
182 my ($key, @tsql) = $self->HandleDefault($1);
183 push @{$limits->{$key}}, @tsql;
187 # Apply default "active status" limit if we don't have any status
188 # limits ourselves, and we're not limited by id
189 if (not $limits->{status} and not $limits->{id}
190 and RT::Config->Get('OnlySearchActiveTicketsInSimpleSearch', $self->TicketsObj->CurrentUser)) {
191 $limits->{status} = [map {s/(['\\])/\\$1/g; "Status = '$_'"} RT::Queue->ActiveStatusArray()];
194 # Respect the "only search these queues" limit if we didn't
195 # specify any queues ourselves
196 if (not $limits->{queue} and not $limits->{id}) {
197 for my $queue ( @{ $self->{'Queues'} } ) {
198 my $QueueObj = RT::Queue->new( $self->TicketsObj->CurrentUser );
199 next unless $QueueObj->Load($queue);
200 my $name = $QueueObj->Name;
201 $name =~ s/(['\\])/\\$1/g;
202 push @{$limits->{queue}}, "Queue = '$name'";
208 [ 10 => sub { return "default" if $_[1] } ],
209 [ 20 => sub { return "id" if /^#?\d+$/ } ],
210 [ 30 => sub { return "requestor" if /\w+@\w+/} ],
211 [ 35 => sub { return "domain" if /^@\w+/} ],
213 return "status" if RT::Queue->new( $_[2] )->IsValidStatus( $_ )
215 [ 40 => sub { return "status" if /^((in)?active|any)$/i } ],
217 my $q = RT::Queue->new( $_[2] );
218 return "queue" if $q->Load($_) and $q->Id and not $q->Disabled
221 my $u = RT::User->new( $_[2] );
222 return "owner" if $u->Load($_) and $u->Id and $u->Privileged
224 [ 70 => sub { return "owner" if $_ eq "me" } ],
229 my ($val, $quoted) = @_;
231 my $cu = $self->TicketsObj->CurrentUser;
232 for my $sub (map $_->[1], sort {$a->[0] <=> $b->[0]} @GUESS) {
234 my $ret = $sub->($val, $quoted, $cu);
241 # $_[1] is escaped value without surrounding single quotes
242 # $_[2] is a boolean of "was quoted by the user?"
243 # ensure this is false before you do smart matching like $_[1] eq "me"
244 # $_[3] is escaped subkey, if any (see HandleCf)
246 my $fts = RT->Config->Get('FullTextSearch');
247 if ($fts->{Enable} and $fts->{Indexed}) {
248 return default => "Content LIKE '$_[1]'";
250 return default => "Subject LIKE '$_[1]'";
253 sub HandleSubject { return subject => "Subject LIKE '$_[1]'"; }
254 sub HandleFulltext { return content => "Content LIKE '$_[1]'"; }
255 sub HandleContent { return content => "Content LIKE '$_[1]'"; }
256 sub HandleId { $_[1] =~ s/^#//; return id => "Id = $_[1]"; }
258 if ($_[1] =~ /^active$/i and !$_[2]) {
259 return status => map {s/(['\\])/\\$1/g; "Status = '$_'"} RT::Queue->ActiveStatusArray();
260 } elsif ($_[1] =~ /^inactive$/i and !$_[2]) {
261 return status => map {s/(['\\])/\\$1/g; "Status = '$_'"} RT::Queue->InactiveStatusArray();
262 } elsif ($_[1] =~ /^any$/i and !$_[2]) {
265 return status => "Status = '$_[1]'";
269 if (!$_[2] and $_[1] eq "me") {
270 return owner => "Owner.id = '__CurrentUser__'";
272 elsif (!$_[2] and $_[1] =~ /\w+@\w+/) {
273 return owner => "Owner.EmailAddress = '$_[1]'";
275 return owner => "Owner = '$_[1]'";
279 return watcher => (!$_[2] and $_[1] eq "me") ? "Watcher.id = '__CurrentUser__'" : "Watcher = '$_[1]'";
281 sub HandleRequestor { return requestor => "Requestor STARTSWITH '$_[1]'"; }
282 sub HandleDomain { $_[1] =~ s/^@?/@/; return requestor => "Requestor ENDSWITH '$_[1]'"; }
283 sub HandleQueue { return queue => "Queue = '$_[1]'"; }
284 sub HandleQ { return queue => "Queue = '$_[1]'"; }
285 sub HandleCf { return "cf.$_[3]" => "'CF.{$_[3]}' LIKE '$_[1]'"; }
287 RT::Base->_ImportOverlays();