import rt 3.8.7
[freeside.git] / rt / lib / RT / Tickets_Overlay_SQL.pm
1 # BEGIN BPS TAGGED BLOCK {{{
2
3 # COPYRIGHT:
4
5 # This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC
6 #                                          <jesse@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 package RT::Tickets;
50
51 use strict;
52 use warnings;
53
54 use RT::SQL;
55
56 # Import configuration data from the lexcial scope of __PACKAGE__ (or
57 # at least where those two Subroutines are defined.)
58
59 our (%FIELD_METADATA, %dispatch, %can_bundle);
60
61 # Lower Case version of FIELDS, for case insensitivity
62 my %lcfields = map { ( lc($_) => $_ ) } (keys %FIELD_METADATA);
63
64 sub _InitSQL {
65   my $self = shift;
66
67   # Private Member Variables (which should get cleaned)
68   $self->{'_sql_transalias'}    = undef;
69   $self->{'_sql_trattachalias'} = undef;
70   $self->{'_sql_cf_alias'}  = undef;
71   $self->{'_sql_object_cfv_alias'}  = undef;
72   $self->{'_sql_watcher_join_users_alias'} = undef;
73   $self->{'_sql_query'}         = '';
74   $self->{'_sql_looking_at'}    = {};
75 }
76
77 sub _SQLLimit {
78   my $self = shift;
79     my %args = (@_);
80     if ($args{'FIELD'} eq 'EffectiveId' &&
81          (!$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) ) {
82         $self->{'looking_at_effective_id'} = 1;
83     }      
84     
85     if ($args{'FIELD'} eq 'Type' &&
86          (!$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) ) {
87         $self->{'looking_at_type'} = 1;
88     }
89
90   # All SQL stuff goes into one SB subclause so we can deal with all
91   # the aggregation
92   $self->SUPER::Limit(%args,
93                       SUBCLAUSE => 'ticketsql');
94 }
95
96 sub _SQLJoin {
97   # All SQL stuff goes into one SB subclause so we can deal with all
98   # the aggregation
99   my $this = shift;
100
101   $this->SUPER::Join(@_,
102                      SUBCLAUSE => 'ticketsql');
103 }
104
105 # Helpers
106 sub _OpenParen {
107   $_[0]->SUPER::_OpenParen( 'ticketsql' );
108 }
109 sub _CloseParen {
110   $_[0]->SUPER::_CloseParen( 'ticketsql' );
111 }
112
113 =head1 SQL Functions
114
115 =cut
116
117 =head2 Robert's Simple SQL Parser
118
119 Documentation In Progress
120
121 The Parser/Tokenizer is a relatively simple state machine that scans through a SQL WHERE clause type string extracting a token at a time (where a token is:
122
123   VALUE -> quoted string or number
124   AGGREGator -> AND or OR
125   KEYWORD -> quoted string or single word
126   OPerator -> =,!=,LIKE,etc..
127   PARENthesis -> open or close.
128
129 And that stream of tokens is passed through the "machine" in order to build up a structure that looks like:
130
131        KEY OP VALUE
132   AND  KEY OP VALUE
133   OR   KEY OP VALUE
134
135 That also deals with parenthesis for nesting.  (The parentheses are
136 just handed off the SearchBuilder)
137
138 =cut
139
140 sub _close_bundle {
141     my ($self, @bundle) = @_;
142     return unless @bundle;
143
144     if ( @bundle == 1 ) {
145         $bundle[0]->{'dispatch'}->(
146             $self,
147             $bundle[0]->{'key'},
148             $bundle[0]->{'op'},
149             $bundle[0]->{'val'},
150             SUBCLAUSE       => '',
151             ENTRYAGGREGATOR => $bundle[0]->{ea},
152             SUBKEY          => $bundle[0]->{subkey},
153         );
154     }
155     else {
156         my @args;
157         foreach my $chunk (@bundle) {
158             push @args, [
159                 $chunk->{key},
160                 $chunk->{op},
161                 $chunk->{val},
162                 SUBCLAUSE       => '',
163                 ENTRYAGGREGATOR => $chunk->{ea},
164                 SUBKEY          => $chunk->{subkey},
165             ];
166         }
167         $bundle[0]->{dispatch}->( $self, \@args );
168     }
169 }
170
171 sub _parser {
172     my ($self,$string) = @_;
173     my @bundle;
174     my $ea = '';
175
176     my %callback;
177     $callback{'OpenParen'} = sub {
178       $self->_close_bundle(@bundle); @bundle = ();
179       $self->_OpenParen
180     };
181     $callback{'CloseParen'} = sub {
182       $self->_close_bundle(@bundle); @bundle = ();
183       $self->_CloseParen;
184     };
185     $callback{'EntryAggregator'} = sub { $ea = $_[0] || '' };
186     $callback{'Condition'} = sub {
187         my ($key, $op, $value) = @_;
188
189         # key has dot then it's compound variant and we have subkey
190         my $subkey = '';
191         ($key, $subkey) = ($1, $2) if $key =~ /^([^\.]+)\.(.+)$/;
192
193         # normalize key and get class (type)
194         my $class;
195         if (exists $lcfields{lc $key}) {
196             $key = $lcfields{lc $key};
197             $class = $FIELD_METADATA{$key}->[0];
198         }
199         die "Unknown field '$key' in '$string'" unless $class;
200
201         # replace __CurrentUser__ with id
202         $value = $self->CurrentUser->id if $value eq '__CurrentUser__';
203
204
205         unless( $dispatch{ $class } ) {
206             die "No dispatch method for class '$class'"
207         }
208         my $sub = $dispatch{ $class };
209
210         if ( $can_bundle{ $class }
211              && ( !@bundle
212                   || ( $bundle[-1]->{dispatch}  == $sub
213                        && $bundle[-1]->{key}    eq $key
214                        && $bundle[-1]->{subkey} eq $subkey
215                      )
216                 )
217            )
218         {
219             push @bundle, {
220                 dispatch => $sub,
221                 key      => $key,
222                 op       => $op,
223                 val      => $value,
224                 ea       => $ea,
225                 subkey   => $subkey,
226             };
227         }
228         else {
229             $self->_close_bundle(@bundle); @bundle = ();
230             $sub->( $self, $key, $op, $value,
231                     SUBCLAUSE       => '',  # don't need anymore
232                     ENTRYAGGREGATOR => $ea,
233                     SUBKEY          => $subkey,
234                   );
235         }
236         $self->{_sql_looking_at}{lc $key} = 1;
237         $ea = '';
238     };
239     RT::SQL::Parse($string, \%callback);
240     $self->_close_bundle(@bundle); @bundle = ();
241 }
242
243 =head2 ClausesToSQL
244
245 =cut
246
247 sub ClausesToSQL {
248   my $self = shift;
249   my $clauses = shift;
250   my @sql;
251
252   for my $f (keys %{$clauses}) {
253     my $sql;
254     my $first = 1;
255
256     # Build SQL from the data hash
257     for my $data ( @{ $clauses->{$f} } ) {
258       $sql .= $data->[0] unless $first; $first=0; # ENTRYAGGREGATOR
259       $sql .= " '". $data->[2] . "' ";            # FIELD
260       $sql .= $data->[3] . " ";                   # OPERATOR
261       $sql .= "'". $data->[4] . "' ";             # VALUE
262     }
263
264     push @sql, " ( " . $sql . " ) ";
265   }
266
267   return join("AND",@sql);
268 }
269
270 =head2 FromSQL
271
272 Convert a RT-SQL string into a set of SearchBuilder restrictions.
273
274 Returns (1, 'Status message') on success and (0, 'Error Message') on
275 failure.
276
277
278
279
280 =cut
281
282 sub FromSQL {
283     my ($self,$query) = @_;
284
285     {
286         # preserve first_row and show_rows across the CleanSlate
287         local ($self->{'first_row'}, $self->{'show_rows'});
288         $self->CleanSlate;
289     }
290     $self->_InitSQL();
291
292     return (1, $self->loc("No Query")) unless $query;
293
294     $self->{_sql_query} = $query;
295     eval { $self->_parser( $query ); };
296     if ( $@ ) {
297         $RT::Logger->error( $@ );
298         return (0, $@);
299     }
300
301     # We only want to look at EffectiveId's (mostly) for these searches.
302     unless ( exists $self->{_sql_looking_at}{'effectiveid'} ) {
303         #TODO, we shouldn't be hard #coding the tablename to main.
304         $self->SUPER::Limit( FIELD           => 'EffectiveId',
305                              VALUE           => 'main.id',
306                              ENTRYAGGREGATOR => 'AND',
307                              QUOTEVALUE      => 0,
308                            );
309     }
310     # FIXME: Need to bring this logic back in
311
312     #      if ($self->_isLimited && (! $self->{'looking_at_effective_id'})) {
313     #         $self->SUPER::Limit( FIELD => 'EffectiveId',
314     #               OPERATOR => '=',
315     #               QUOTEVALUE => 0,
316     #               VALUE => 'main.id');   #TODO, we shouldn't be hard coding the tablename to main.
317     #       }
318     # --- This is hardcoded above.  This comment block can probably go.
319     # Or, we need to reimplement the looking_at_effective_id toggle.
320
321     # Unless we've explicitly asked to look at a specific Type, we need
322     # to limit to it.
323     unless ( $self->{looking_at_type} ) {
324         $self->SUPER::Limit( FIELD => 'Type', VALUE => 'ticket' );
325     }
326
327     # We don't want deleted tickets unless 'allow_deleted_search' is set
328     unless( $self->{'allow_deleted_search'} ) {
329         $self->SUPER::Limit( FIELD    => 'Status',
330                              OPERATOR => '!=',
331                              VALUE => 'deleted',
332                            );
333     }
334
335     # set SB's dirty flag
336     $self->{'must_redo_search'} = 1;
337     $self->{'RecalcTicketLimits'} = 0;                                           
338
339     return (1, $self->loc("Valid Query"));
340 }
341
342 =head2 Query
343
344 Returns the query that this object was initialized with
345
346 =cut
347
348 sub Query {
349     return ($_[0]->{_sql_query});
350 }
351
352 {
353 my %inv = (
354     '=' => '!=', '!=' => '=', '<>' => '=',
355     '>' => '<=', '<' => '>=', '>=' => '<', '<=' => '>',
356     'is' => 'IS NOT', 'is not' => 'IS',
357     'like' => 'NOT LIKE', 'not like' => 'LIKE',
358     'matches' => 'NOT MATCHES', 'not matches' => 'MATCHES',
359     'startswith' => 'NOT STARTSWITH', 'not startswith' => 'STARTSWITH',
360     'endswith' => 'NOT ENDSWITH', 'not endswith' => 'ENDSWITH',
361 );
362
363 my %range = map { $_ => 1 } qw(> >= < <=);
364
365 sub ClassifySQLOperation {
366     my $self = shift;
367     my $op = shift;
368
369     my $is_negative = 0;
370     if ( $op eq '!=' || $op =~ /\bNOT\b/i ) {
371         $is_negative = 1;
372     }
373
374     my $is_null = 0;
375     if ( 'is not' eq lc($op) || 'is' eq lc($op) ) {
376         $is_null = 1;
377     }
378
379     return ($is_negative, $is_null, $inv{lc $op}, $range{lc $op});
380 } }
381
382 1;
383
384 =pod
385
386 =head2 Exceptions
387
388 Most of the RT code does not use Exceptions (die/eval) but it is used
389 in the TicketSQL code for simplicity and historical reasons.  Lest you
390 be worried that the dies will trigger user visible errors, all are
391 trapped via evals.
392
393 99% of the dies fall in subroutines called via FromSQL and then parse.
394 (This includes all of the _FooLimit routines in Tickets_Overlay.pm.)
395 The other 1% or so are via _ProcessRestrictions.
396
397 All dies are trapped by eval {}s, and will be logged at the 'error'
398 log level.  The general failure mode is to not display any tickets.
399
400 =head2 General Flow
401
402 Legacy Layer:
403
404    Legacy LimitFoo routines build up a RestrictionsHash
405
406    _ProcessRestrictions converts the Restrictions to Clauses
407    ([key,op,val,rest]).
408
409    Clauses are converted to RT-SQL (TicketSQL)
410
411 New RT-SQL Layer:
412
413    FromSQL calls the parser
414
415    The parser calls the _FooLimit routines to do DBIx::SearchBuilder
416    limits.
417
418 And then the normal SearchBuilder/Ticket routines are used for
419 display/navigation.
420
421 =cut
422