a1254836a614abd3611e8d557d4c02fcf46d8f77
[usit-rt.git] / lib / RT / Search / Googleish.pm
1 # BEGIN BPS TAGGED BLOCK {{{
2 #
3 # COPYRIGHT:
4 #
5 # This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC
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
57 Use the argument passed in as a "Google-style" set of keywords
58
59 =head1 METHODS
60
61 =cut
62
63 package RT::Search::Googleish;
64
65 use strict;
66 use warnings;
67 use base qw(RT::Search);
68
69 use 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
73 our %AND = (
74     content => 1,
75     subject => 1,
76 );
77
78 sub _Init {
79     my $self = shift;
80     my %args = @_;
81
82     $self->{'Queues'} = delete( $args{'Queues'} ) || [];
83     $self->SUPER::_Init(%args);
84 }
85
86 sub Describe {
87     my $self = shift;
88     return ( $self->loc( "Keyword and intuition-based searching", ref $self ) );
89 }
90
91 sub 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
101 sub 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['"]}
113                                 |\w+
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
145 sub 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
157 sub 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
172 sub 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
196 our @GUESS = (
197     [ 10 => sub { return "subject" if $_[1] } ],
198     [ 20 => sub { return "id" if /^#?\d+$/ } ],
199     [ 30 => sub { return "requestor" if /\w+@\w+/} ],
200     [ 40 => sub {
201           return "status" if RT::Queue->new( $_[2] )->IsValidStatus( $_ )
202       }],
203     [ 40 => sub { return "status" if /^((in)?active|any)$/i } ],
204     [ 50 => sub {
205           my $q = RT::Queue->new( $_[2] );
206           return "queue" if $q->Load($_) and $q->Id
207       }],
208     [ 60 => sub {
209           my $u = RT::User->new( $_[2] );
210           return "owner" if $u->Load($_) and $u->Id and $u->Privileged
211       }],
212     [ 70 => sub { return "owner" if $_ eq "me" } ],
213 );
214
215 sub GuessType {
216     my $self = shift;
217     my ($val, $quoted) = @_;
218
219     my $cu = $self->TicketsObj->CurrentUser;
220     for my $sub (map $_->[1], sort {$a->[0] <=> $b->[0]} @GUESS) {
221         local $_ = $val;
222         my $ret = $sub->($val, $quoted, $cu);
223         return $ret if $ret;
224     }
225     return "default";
226 }
227
228 sub HandleDefault   { return subject   => "Subject LIKE '$_[1]'"; }
229 sub HandleSubject   { return subject   => "Subject LIKE '$_[1]'"; }
230 sub HandleFulltext  { return content   => "Content LIKE '$_[1]'"; }
231 sub HandleContent   { return content   => "Content LIKE '$_[1]'"; }
232 sub HandleId        { $_[1] =~ s/^#//; return id => "Id = $_[1]"; }
233 sub HandleStatus    {
234     if ($_[1] =~ /^active$/i and !$_[2]) {
235         return status => map {s/(['\\])/\\$1/g; "Status = '$_'"} RT::Queue->ActiveStatusArray();
236     } elsif ($_[1] =~ /^inactive$/i and !$_[2]) {
237         return status => map {s/(['\\])/\\$1/g; "Status = '$_'"} RT::Queue->InactiveStatusArray();
238     } elsif ($_[1] =~ /^any$/i and !$_[2]) {
239         return 'status';
240     } else {
241         return status => "Status = '$_[1]'";
242     }
243 }
244 sub HandleOwner     {
245     return owner  => (!$_[2] and $_[1] eq "me") ? "Owner.id = '__CurrentUser__'" : "Owner = '$_[1]'";
246 }
247 sub HandleWatcher     {
248     return watcher => (!$_[2] and $_[1] eq "me") ? "Watcher.id = '__CurrentUser__'" : "Watcher = '$_[1]'";
249 }
250 sub HandleRequestor { return requestor => "Requestor STARTSWITH '$_[1]'";  }
251 sub HandleQueue     { return queue     => "Queue = '$_[1]'";      }
252 sub HandleQ         { return queue     => "Queue = '$_[1]'";      }
253 sub HandleCf        { return "cf.$_[3]" => "'CF.{$_[3]}' LIKE '$_[1]'"; }
254
255 RT::Base->_ImportOverlays();
256
257 1;