Upgrade 4.0.17 clean.
[usit-rt.git] / lib / RT / Search / Googleish.pm
CommitLineData
84fb5b46
MKG
1# BEGIN BPS TAGGED BLOCK {{{
2#
3# COPYRIGHT:
4#
403d7b0b 5# This software is Copyright (c) 1996-2013 Best Practical Solutions, LLC
84fb5b46
MKG
6# <sales@bestpractical.com>
7#
8# (Except where explicitly superseded by other copyright notices)
9#
10#
11# LICENSE:
12#
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
16# from www.gnu.org.
17#
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.
22#
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.
28#
29#
30# CONTRIBUTION SUBMISSION POLICY:
31#
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.)
37#
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.
46#
47# END BPS TAGGED BLOCK }}}
48
49=head1 NAME
50
51 RT::Search::Googleish
52
53=head1 SYNOPSIS
54
55=head1 DESCRIPTION
56
57Use the argument passed in as a "Google-style" set of keywords
58
59=head1 METHODS
60
61=cut
62
63package RT::Search::Googleish;
64
65use strict;
66use warnings;
67use base qw(RT::Search);
68
69use Regexp::Common qw/delimited/;
70
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
73our %AND = (
74 content => 1,
75 subject => 1,
76);
77
78sub _Init {
79 my $self = shift;
80 my %args = @_;
81
82 $self->{'Queues'} = delete( $args{'Queues'} ) || [];
83 $self->SUPER::_Init(%args);
84}
85
86sub Describe {
87 my $self = shift;
88 return ( $self->loc( "Keyword and intuition-based searching", ref $self ) );
89}
90
91sub Prepare {
92 my $self = shift;
93 my $tql = $self->QueryToSQL( $self->Argument );
94
95 $RT::Logger->debug($tql);
96
97 $self->TicketsObj->FromSQL($tql);
98 return (1);
99}
100
101sub QueryToSQL {
102 my $self = shift;
103 my $query = shift || $self->Argument;
104
105 my %limits;
106 $query =~ s/^\s*//;
107 while ($query =~ /^\S/) {
108 if ($query =~ s/^
109 (?:
110 (\w+) # A straight word
111 (?:\. # With an optional .foo
112 ($RE{delimited}{-delim=>q['"]}
dab09ea8 113 |[\w-]+ # Allow \w + dashes
84fb5b46
MKG
114 ) # Which could be ."foo bar", too
115 )?
116 )
117 : # Followed by a colon
118 ($RE{delimited}{-delim=>q['"]}
119 |\S+
120 ) # And a possibly-quoted foo:"bar baz"
121 \s*//ix) {
122 my ($type, $extra, $value) = ($1, $2, $3);
123 ($value, my ($quoted)) = $self->Unquote($value);
124 $extra = $self->Unquote($extra) if defined $extra;
125 $self->Dispatch(\%limits, $type, $value, $quoted, $extra);
126 } elsif ($query =~ s/^($RE{delimited}{-delim=>q['"]}|\S+)\s*//) {
127 # If there's no colon, it's just a word or quoted string
128 my($val, $quoted) = $self->Unquote($1);
129 $self->Dispatch(\%limits, $self->GuessType($val, $quoted), $val, $quoted);
130 }
131 }
132 $self->Finalize(\%limits);
133
134 my @clauses;
135 for my $subclause (sort keys %limits) {
136 next unless @{$limits{$subclause}};
137
138 my $op = $AND{lc $subclause} ? "AND" : "OR";
139 push @clauses, "( ".join(" $op ", @{$limits{$subclause}})." )";
140 }
141
142 return join " AND ", @clauses;
143}
144
145sub Dispatch {
146 my $self = shift;
147 my ($limits, $type, $contents, $quoted, $extra) = @_;
148 $contents =~ s/(['\\])/\\$1/g;
149 $extra =~ s/(['\\])/\\$1/g if defined $extra;
150
151 my $method = "Handle" . ucfirst(lc($type));
152 $method = "HandleDefault" unless $self->can($method);
153 my ($key, @tsql) = $self->$method($contents, $quoted, $extra);
154 push @{$limits->{$key}}, @tsql;
155}
156
157sub Unquote {
158 # Given a word or quoted string, unquote it if it is quoted,
159 # removing escaped quotes.
160 my $self = shift;
161 my ($token) = @_;
162 if ($token =~ /^$RE{delimited}{-delim=>q['"]}{-keep}$/) {
163 my $quote = $2 || $5;
164 my $value = $3 || $6;
165 $value =~ s/\\(\\|$quote)/$1/g;
166 return wantarray ? ($value, 1) : $value;
167 } else {
168 return wantarray ? ($token, 0) : $token;
169 }
170}
171
172sub Finalize {
173 my $self = shift;
174 my ($limits) = @_;
175
176 # Apply default "active status" limit if we don't have any status
177 # limits ourselves, and we're not limited by id
178 if (not $limits->{status} and not $limits->{id}
179 and RT::Config->Get('OnlySearchActiveTicketsInSimpleSearch', $self->TicketsObj->CurrentUser)) {
180 $limits->{status} = [map {s/(['\\])/\\$1/g; "Status = '$_'"} RT::Queue->ActiveStatusArray()];
181 }
182
183 # Respect the "only search these queues" limit if we didn't
184 # specify any queues ourselves
185 if (not $limits->{queue} and not $limits->{id}) {
186 for my $queue ( @{ $self->{'Queues'} } ) {
187 my $QueueObj = RT::Queue->new( $self->TicketsObj->CurrentUser );
188 next unless $QueueObj->Load($queue);
189 my $name = $QueueObj->Name;
190 $name =~ s/(['\\])/\\$1/g;
191 push @{$limits->{queue}}, "Queue = '$name'";
192 }
193 }
194}
195
196our @GUESS = (
197 [ 10 => sub { return "subject" if $_[1] } ],
198 [ 20 => sub { return "id" if /^#?\d+$/ } ],
199 [ 30 => sub { return "requestor" if /\w+@\w+/} ],
01e3b242 200 [ 35 => sub { return "domain" if /^@\w+/} ],
84fb5b46
MKG
201 [ 40 => sub {
202 return "status" if RT::Queue->new( $_[2] )->IsValidStatus( $_ )
203 }],
204 [ 40 => sub { return "status" if /^((in)?active|any)$/i } ],
205 [ 50 => sub {
206 my $q = RT::Queue->new( $_[2] );
403d7b0b 207 return "queue" if $q->Load($_) and $q->Id and not $q->Disabled
84fb5b46
MKG
208 }],
209 [ 60 => sub {
210 my $u = RT::User->new( $_[2] );
211 return "owner" if $u->Load($_) and $u->Id and $u->Privileged
212 }],
213 [ 70 => sub { return "owner" if $_ eq "me" } ],
214);
215
216sub GuessType {
217 my $self = shift;
218 my ($val, $quoted) = @_;
219
220 my $cu = $self->TicketsObj->CurrentUser;
221 for my $sub (map $_->[1], sort {$a->[0] <=> $b->[0]} @GUESS) {
222 local $_ = $val;
223 my $ret = $sub->($val, $quoted, $cu);
224 return $ret if $ret;
225 }
226 return "default";
227}
228
dab09ea8
MKG
229# $_[0] is $self
230# $_[1] is escaped value without surrounding single quotes
231# $_[2] is a boolean of "was quoted by the user?"
232# ensure this is false before you do smart matching like $_[1] eq "me"
233# $_[3] is escaped subkey, if any (see HandleCf)
84fb5b46
MKG
234sub HandleDefault { return subject => "Subject LIKE '$_[1]'"; }
235sub HandleSubject { return subject => "Subject LIKE '$_[1]'"; }
236sub HandleFulltext { return content => "Content LIKE '$_[1]'"; }
237sub HandleContent { return content => "Content LIKE '$_[1]'"; }
238sub HandleId { $_[1] =~ s/^#//; return id => "Id = $_[1]"; }
239sub HandleStatus {
240 if ($_[1] =~ /^active$/i and !$_[2]) {
241 return status => map {s/(['\\])/\\$1/g; "Status = '$_'"} RT::Queue->ActiveStatusArray();
242 } elsif ($_[1] =~ /^inactive$/i and !$_[2]) {
243 return status => map {s/(['\\])/\\$1/g; "Status = '$_'"} RT::Queue->InactiveStatusArray();
244 } elsif ($_[1] =~ /^any$/i and !$_[2]) {
245 return 'status';
246 } else {
247 return status => "Status = '$_[1]'";
248 }
249}
250sub HandleOwner {
dab09ea8
MKG
251 if (!$_[2] and $_[1] eq "me") {
252 return owner => "Owner.id = '__CurrentUser__'";
253 }
254 elsif (!$_[2] and $_[1] =~ /\w+@\w+/) {
255 return owner => "Owner.EmailAddress = '$_[1]'";
256 } else {
257 return owner => "Owner = '$_[1]'";
258 }
84fb5b46
MKG
259}
260sub HandleWatcher {
261 return watcher => (!$_[2] and $_[1] eq "me") ? "Watcher.id = '__CurrentUser__'" : "Watcher = '$_[1]'";
262}
263sub HandleRequestor { return requestor => "Requestor STARTSWITH '$_[1]'"; }
01e3b242 264sub HandleDomain { $_[1] =~ s/^@?/@/; return requestor => "Requestor ENDSWITH '$_[1]'"; }
84fb5b46
MKG
265sub HandleQueue { return queue => "Queue = '$_[1]'"; }
266sub HandleQ { return queue => "Queue = '$_[1]'"; }
267sub HandleCf { return "cf.$_[3]" => "'CF.{$_[3]}' LIKE '$_[1]'"; }
268
269RT::Base->_ImportOverlays();
270
2711;