Upgrade to 4.2.2
[usit-rt.git] / lib / RT / Tickets.pm
1 # BEGIN BPS TAGGED BLOCK {{{
2 #
3 # COPYRIGHT:
4 #
5 # This software is Copyright (c) 1996-2014 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::Tickets - A collection of Ticket objects
52
53
54 =head1 SYNOPSIS
55
56   use RT::Tickets;
57   my $tickets = RT::Tickets->new($CurrentUser);
58
59 =head1 DESCRIPTION
60
61    A collection of RT::Tickets.
62
63 =head1 METHODS
64
65
66 =cut
67
68 package RT::Tickets;
69
70 use strict;
71 use warnings;
72
73 use base 'RT::SearchBuilder';
74
75 use Role::Basic 'with';
76 with 'RT::SearchBuilder::Role::Roles';
77
78 use Scalar::Util qw/blessed/;
79
80 use RT::Ticket;
81 use RT::SQL;
82
83 sub Table { 'Tickets'}
84
85 use RT::CustomFields;
86
87 __PACKAGE__->RegisterCustomFieldJoin(@$_) for
88     [ "RT::Transaction" => sub { $_[0]->JoinTransactions } ],
89     [ "RT::Queue"       => sub {
90             # XXX: Could avoid join and use main.Queue with some refactoring?
91             return $_[0]->{_sql_aliases}{queues} ||= $_[0]->Join(
92                 ALIAS1 => 'main',
93                 FIELD1 => 'Queue',
94                 TABLE2 => 'Queues',
95                 FIELD2 => 'id',
96             );
97         }
98     ];
99
100 # Configuration Tables:
101
102 # FIELD_METADATA is a mapping of searchable Field name, to Type, and other
103 # metadata.
104
105 our %FIELD_METADATA = (
106     Status          => [ 'ENUM', ], #loc_left_pair
107     Queue           => [ 'ENUM' => 'Queue', ], #loc_left_pair
108     Type            => [ 'ENUM', ], #loc_left_pair
109     Creator         => [ 'ENUM' => 'User', ], #loc_left_pair
110     LastUpdatedBy   => [ 'ENUM' => 'User', ], #loc_left_pair
111     Owner           => [ 'WATCHERFIELD' => 'Owner', ], #loc_left_pair
112     EffectiveId     => [ 'INT', ], #loc_left_pair
113     id              => [ 'ID', ], #loc_left_pair
114     InitialPriority => [ 'INT', ], #loc_left_pair
115     FinalPriority   => [ 'INT', ], #loc_left_pair
116     Priority        => [ 'INT', ], #loc_left_pair
117     TimeLeft        => [ 'INT', ], #loc_left_pair
118     TimeWorked      => [ 'INT', ], #loc_left_pair
119     TimeEstimated   => [ 'INT', ], #loc_left_pair
120
121     Linked          => [ 'LINK' ], #loc_left_pair
122     LinkedTo        => [ 'LINK' => 'To' ], #loc_left_pair
123     LinkedFrom      => [ 'LINK' => 'From' ], #loc_left_pair
124     MemberOf        => [ 'LINK' => To => 'MemberOf', ], #loc_left_pair
125     DependsOn       => [ 'LINK' => To => 'DependsOn', ], #loc_left_pair
126     RefersTo        => [ 'LINK' => To => 'RefersTo', ], #loc_left_pair
127     HasMember       => [ 'LINK' => From => 'MemberOf', ], #loc_left_pair
128     DependentOn     => [ 'LINK' => From => 'DependsOn', ], #loc_left_pair
129     DependedOnBy    => [ 'LINK' => From => 'DependsOn', ], #loc_left_pair
130     ReferredToBy    => [ 'LINK' => From => 'RefersTo', ], #loc_left_pair
131     Told             => [ 'DATE'            => 'Told', ], #loc_left_pair
132     Starts           => [ 'DATE'            => 'Starts', ], #loc_left_pair
133     Started          => [ 'DATE'            => 'Started', ], #loc_left_pair
134     Due              => [ 'DATE'            => 'Due', ], #loc_left_pair
135     Resolved         => [ 'DATE'            => 'Resolved', ], #loc_left_pair
136     LastUpdated      => [ 'DATE'            => 'LastUpdated', ], #loc_left_pair
137     Created          => [ 'DATE'            => 'Created', ], #loc_left_pair
138     Subject          => [ 'STRING', ], #loc_left_pair
139     Content          => [ 'TRANSCONTENT', ], #loc_left_pair
140     ContentType      => [ 'TRANSFIELD', ], #loc_left_pair
141     Filename         => [ 'TRANSFIELD', ], #loc_left_pair
142     TransactionDate  => [ 'TRANSDATE', ], #loc_left_pair
143     Requestor        => [ 'WATCHERFIELD'    => 'Requestor', ], #loc_left_pair
144     Requestors       => [ 'WATCHERFIELD'    => 'Requestor', ], #loc_left_pair
145     Cc               => [ 'WATCHERFIELD'    => 'Cc', ], #loc_left_pair
146     AdminCc          => [ 'WATCHERFIELD'    => 'AdminCc', ], #loc_left_pair
147     Watcher          => [ 'WATCHERFIELD', ], #loc_left_pair
148     QueueCc          => [ 'WATCHERFIELD'    => 'Cc'      => 'Queue', ], #loc_left_pair
149     QueueAdminCc     => [ 'WATCHERFIELD'    => 'AdminCc' => 'Queue', ], #loc_left_pair
150     QueueWatcher     => [ 'WATCHERFIELD'    => undef     => 'Queue', ], #loc_left_pair
151     CustomFieldValue => [ 'CUSTOMFIELD' => 'Ticket' ], #loc_left_pair
152     CustomField      => [ 'CUSTOMFIELD' => 'Ticket' ], #loc_left_pair
153     CF               => [ 'CUSTOMFIELD' => 'Ticket' ], #loc_left_pair
154     TxnCF            => [ 'CUSTOMFIELD' => 'Transaction' ], #loc_left_pair
155     TransactionCF    => [ 'CUSTOMFIELD' => 'Transaction' ], #loc_left_pair
156     QueueCF          => [ 'CUSTOMFIELD' => 'Queue' ], #loc_left_pair
157     Updated          => [ 'TRANSDATE', ], #loc_left_pair
158     UpdatedBy        => [ 'TRANSCREATOR', ], #loc_left_pair
159     OwnerGroup       => [ 'MEMBERSHIPFIELD' => 'Owner', ], #loc_left_pair
160     RequestorGroup   => [ 'MEMBERSHIPFIELD' => 'Requestor', ], #loc_left_pair
161     CCGroup          => [ 'MEMBERSHIPFIELD' => 'Cc', ], #loc_left_pair
162     AdminCCGroup     => [ 'MEMBERSHIPFIELD' => 'AdminCc', ], #loc_left_pair
163     WatcherGroup     => [ 'MEMBERSHIPFIELD', ], #loc_left_pair
164     HasAttribute     => [ 'HASATTRIBUTE', 1 ],
165     HasNoAttribute     => [ 'HASATTRIBUTE', 0 ],
166 );
167
168 # Lower Case version of FIELDS, for case insensitivity
169 our %LOWER_CASE_FIELDS = map { ( lc($_) => $_ ) } (keys %FIELD_METADATA);
170
171 our %SEARCHABLE_SUBFIELDS = (
172     User => [qw(
173         EmailAddress Name RealName Nickname Organization Address1 Address2
174         WorkPhone HomePhone MobilePhone PagerPhone id
175     )],
176 );
177
178 # Mapping of Field Type to Function
179 our %dispatch = (
180     ENUM            => \&_EnumLimit,
181     INT             => \&_IntLimit,
182     ID              => \&_IdLimit,
183     LINK            => \&_LinkLimit,
184     DATE            => \&_DateLimit,
185     STRING          => \&_StringLimit,
186     TRANSFIELD      => \&_TransLimit,
187     TRANSCONTENT    => \&_TransContentLimit,
188     TRANSDATE       => \&_TransDateLimit,
189     TRANSCREATOR    => \&_TransCreatorLimit,
190     WATCHERFIELD    => \&_WatcherLimit,
191     MEMBERSHIPFIELD => \&_WatcherMembershipLimit,
192     CUSTOMFIELD     => \&_CustomFieldLimit,
193     HASATTRIBUTE    => \&_HasAttributeLimit,
194 );
195
196 # Default EntryAggregator per type
197 # if you specify OP, you must specify all valid OPs
198 my %DefaultEA = (
199     INT  => 'AND',
200     ENUM => {
201         '='  => 'OR',
202         '!=' => 'AND'
203     },
204     DATE => {
205         'IS' => 'OR',
206         'IS NOT' => 'OR',
207         '='  => 'OR',
208         '>=' => 'AND',
209         '<=' => 'AND',
210         '>'  => 'AND',
211         '<'  => 'AND'
212     },
213     STRING => {
214         '='        => 'OR',
215         '!='       => 'AND',
216         'LIKE'     => 'AND',
217         'NOT LIKE' => 'AND'
218     },
219     TRANSFIELD   => 'AND',
220     TRANSDATE    => 'AND',
221     LINK         => 'OR',
222     LINKFIELD    => 'AND',
223     TARGET       => 'AND',
224     BASE         => 'AND',
225     WATCHERFIELD => {
226         '='        => 'OR',
227         '!='       => 'AND',
228         'LIKE'     => 'OR',
229         'NOT LIKE' => 'AND'
230     },
231
232     HASATTRIBUTE => {
233         '='        => 'AND',
234         '!='       => 'AND',
235     },
236
237     CUSTOMFIELD => 'OR',
238 );
239
240 sub FIELDS     { return \%FIELD_METADATA }
241
242 our @SORTFIELDS = qw(id Status
243     Queue Subject
244     Owner Created Due Starts Started
245     Told
246     Resolved LastUpdated Priority TimeWorked TimeLeft);
247
248 =head2 SortFields
249
250 Returns the list of fields that lists of tickets can easily be sorted by
251
252 =cut
253
254 sub SortFields {
255     my $self = shift;
256     return (@SORTFIELDS);
257 }
258
259
260 # BEGIN SQL STUFF *********************************
261
262
263 sub CleanSlate {
264     my $self = shift;
265     $self->SUPER::CleanSlate( @_ );
266     delete $self->{$_} foreach qw(
267         _sql_cf_alias
268         _sql_group_members_aliases
269         _sql_object_cfv_alias
270         _sql_role_group_aliases
271         _sql_trattachalias
272         _sql_u_watchers_alias_for_sort
273         _sql_u_watchers_aliases
274         _sql_current_user_can_see_applied
275     );
276 }
277
278 =head1 Limit Helper Routines
279
280 These routines are the targets of a dispatch table depending on the
281 type of field.  They all share the same signature:
282
283   my ($self,$field,$op,$value,@rest) = @_;
284
285 The values in @rest should be suitable for passing directly to
286 DBIx::SearchBuilder::Limit.
287
288 Essentially they are an expanded/broken out (and much simplified)
289 version of what ProcessRestrictions used to do.  They're also much
290 more clearly delineated by the TYPE of field being processed.
291
292 =head2 _IdLimit
293
294 Handle ID field.
295
296 =cut
297
298 sub _IdLimit {
299     my ( $sb, $field, $op, $value, @rest ) = @_;
300
301     if ( $value eq '__Bookmarked__' ) {
302         return $sb->_BookmarkLimit( $field, $op, $value, @rest );
303     } else {
304         return $sb->_IntLimit( $field, $op, $value, @rest );
305     }
306 }
307
308 sub _BookmarkLimit {
309     my ( $sb, $field, $op, $value, @rest ) = @_;
310
311     die "Invalid operator $op for __Bookmarked__ search on $field"
312         unless $op =~ /^(=|!=)$/;
313
314     my @bookmarks = $sb->CurrentUser->UserObj->Bookmarks;
315
316     return $sb->Limit(
317         FIELD    => $field,
318         OPERATOR => $op,
319         VALUE    => 0,
320         @rest,
321     ) unless @bookmarks;
322
323     # as bookmarked tickets can be merged we have to use a join
324     # but it should be pretty lightweight
325     my $tickets_alias = $sb->Join(
326         TYPE   => 'LEFT',
327         ALIAS1 => 'main',
328         FIELD1 => 'id',
329         TABLE2 => 'Tickets',
330         FIELD2 => 'EffectiveId',
331     );
332     $sb->_OpenParen;
333     my $first = 1;
334     my $ea = $op eq '='? 'OR': 'AND';
335     foreach my $id ( sort @bookmarks ) {
336         $sb->Limit(
337             ALIAS    => $tickets_alias,
338             FIELD    => 'id',
339             OPERATOR => $op,
340             VALUE    => $id,
341             $first? (@rest): ( ENTRYAGGREGATOR => $ea )
342         );
343         $first = 0 if $first;
344     }
345     $sb->_CloseParen;
346 }
347
348 =head2 _EnumLimit
349
350 Handle Fields which are limited to certain values, and potentially
351 need to be looked up from another class.
352
353 This subroutine actually handles two different kinds of fields.  For
354 some the user is responsible for limiting the values.  (i.e. Status,
355 Type).
356
357 For others, the value specified by the user will be looked by via
358 specified class.
359
360 Meta Data:
361   name of class to lookup in (Optional)
362
363 =cut
364
365 sub _EnumLimit {
366     my ( $sb, $field, $op, $value, @rest ) = @_;
367
368     # SQL::Statement changes != to <>.  (Can we remove this now?)
369     $op = "!=" if $op eq "<>";
370
371     die "Invalid Operation: $op for $field"
372         unless $op eq "="
373         or $op     eq "!=";
374
375     my $meta = $FIELD_METADATA{$field};
376     if ( defined $meta->[1] && defined $value && $value !~ /^\d+$/ ) {
377         my $class = "RT::" . $meta->[1];
378         my $o     = $class->new( $sb->CurrentUser );
379         $o->Load($value);
380         $value = $o->Id || 0;
381     } elsif ( $field eq "Type" ) {
382         $value = lc $value if $value =~ /^(ticket|approval|reminder)$/i;
383     } elsif ($field eq "Status") {
384         $value = lc $value;
385     }
386     $sb->Limit(
387         FIELD    => $field,
388         VALUE    => $value,
389         OPERATOR => $op,
390         @rest,
391     );
392 }
393
394 =head2 _IntLimit
395
396 Handle fields where the values are limited to integers.  (For example,
397 Priority, TimeWorked.)
398
399 Meta Data:
400   None
401
402 =cut
403
404 sub _IntLimit {
405     my ( $sb, $field, $op, $value, @rest ) = @_;
406
407     my $is_a_like = $op =~ /MATCHES|ENDSWITH|STARTSWITH|LIKE/i;
408
409     # We want to support <id LIKE '1%'> for ticket autocomplete,
410     # but we need to explicitly typecast on Postgres
411     if ( $is_a_like && RT->Config->Get('DatabaseType') eq 'Pg' ) {
412         return $sb->Limit(
413             FUNCTION => "CAST(main.$field AS TEXT)",
414             OPERATOR => $op,
415             VALUE    => $value,
416             @rest,
417         );
418     }
419
420     $sb->Limit(
421         FIELD    => $field,
422         VALUE    => $value,
423         OPERATOR => $op,
424         @rest,
425     );
426 }
427
428 =head2 _LinkLimit
429
430 Handle fields which deal with links between tickets.  (MemberOf, DependsOn)
431
432 Meta Data:
433   1: Direction (From, To)
434   2: Link Type (MemberOf, DependsOn, RefersTo)
435
436 =cut
437
438 sub _LinkLimit {
439     my ( $sb, $field, $op, $value, @rest ) = @_;
440
441     my $meta = $FIELD_METADATA{$field};
442     die "Invalid Operator $op for $field" unless $op =~ /^(=|!=|IS|IS NOT)$/io;
443
444     my $is_negative = 0;
445     if ( $op eq '!=' || $op =~ /\bNOT\b/i ) {
446         $is_negative = 1;
447     }
448     my $is_null = 0;
449     $is_null = 1 if !$value || $value =~ /^null$/io;
450
451     my $direction = $meta->[1] || '';
452     my ($matchfield, $linkfield) = ('', '');
453     if ( $direction eq 'To' ) {
454         ($matchfield, $linkfield) = ("Target", "Base");
455     }
456     elsif ( $direction eq 'From' ) {
457         ($matchfield, $linkfield) = ("Base", "Target");
458     }
459     elsif ( $direction ) {
460         die "Invalid link direction '$direction' for $field\n";
461     } else {
462         $sb->_OpenParen;
463         $sb->_LinkLimit( 'LinkedTo', $op, $value, @rest );
464         $sb->_LinkLimit(
465             'LinkedFrom', $op, $value, @rest,
466             ENTRYAGGREGATOR => (($is_negative && $is_null) || (!$is_null && !$is_negative))? 'OR': 'AND',
467         );
468         $sb->_CloseParen;
469         return;
470     }
471
472     my $is_local = 1;
473     if ( $is_null ) {
474         $op = ($op =~ /^(=|IS)$/i)? 'IS': 'IS NOT';
475     }
476     elsif ( $value =~ /\D/ ) {
477         $value = RT::URI->new( $sb->CurrentUser )->CanonicalizeURI( $value );
478         $is_local = 0;
479     }
480     $matchfield = "Local$matchfield" if $is_local;
481
482 #For doing a left join to find "unlinked tickets" we want to generate a query that looks like this
483 #    SELECT main.* FROM Tickets main
484 #        LEFT JOIN Links Links_1 ON (     (Links_1.Type = 'MemberOf')
485 #                                      AND(main.id = Links_1.LocalTarget))
486 #        WHERE Links_1.LocalBase IS NULL;
487
488     if ( $is_null ) {
489         my $linkalias = $sb->Join(
490             TYPE   => 'LEFT',
491             ALIAS1 => 'main',
492             FIELD1 => 'id',
493             TABLE2 => 'Links',
494             FIELD2 => 'Local' . $linkfield
495         );
496         $sb->Limit(
497             LEFTJOIN => $linkalias,
498             FIELD    => 'Type',
499             OPERATOR => '=',
500             VALUE    => $meta->[2],
501         ) if $meta->[2];
502         $sb->Limit(
503             @rest,
504             ALIAS      => $linkalias,
505             FIELD      => $matchfield,
506             OPERATOR   => $op,
507             VALUE      => 'NULL',
508             QUOTEVALUE => 0,
509         );
510     }
511     else {
512         my $linkalias = $sb->Join(
513             TYPE   => 'LEFT',
514             ALIAS1 => 'main',
515             FIELD1 => 'id',
516             TABLE2 => 'Links',
517             FIELD2 => 'Local' . $linkfield
518         );
519         $sb->Limit(
520             LEFTJOIN => $linkalias,
521             FIELD    => 'Type',
522             OPERATOR => '=',
523             VALUE    => $meta->[2],
524         ) if $meta->[2];
525         $sb->Limit(
526             LEFTJOIN => $linkalias,
527             FIELD    => $matchfield,
528             OPERATOR => '=',
529             VALUE    => $value,
530         );
531         $sb->Limit(
532             @rest,
533             ALIAS      => $linkalias,
534             FIELD      => $matchfield,
535             OPERATOR   => $is_negative? 'IS': 'IS NOT',
536             VALUE      => 'NULL',
537             QUOTEVALUE => 0,
538         );
539     }
540 }
541
542 =head2 _DateLimit
543
544 Handle date fields.  (Created, LastTold..)
545
546 Meta Data:
547   1: type of link.  (Probably not necessary.)
548
549 =cut
550
551 sub _DateLimit {
552     my ( $sb, $field, $op, $value, %rest ) = @_;
553
554     die "Invalid Date Op: $op"
555         unless $op =~ /^(=|>|<|>=|<=|IS(\s+NOT)?)$/i;
556
557     my $meta = $FIELD_METADATA{$field};
558     die "Incorrect Meta Data for $field"
559         unless ( defined $meta->[1] );
560
561     if ( $op =~ /^(IS(\s+NOT)?)$/i) {
562         return $sb->Limit(
563             FUNCTION => $sb->NotSetDateToNullFunction,
564             FIELD    => $meta->[1],
565             OPERATOR => $op,
566             VALUE    => "NULL",
567             %rest,
568         );
569     }
570
571     if ( my $subkey = $rest{SUBKEY} ) {
572         if ( $subkey eq 'DayOfWeek' && $op !~ /IS/i && $value =~ /[^0-9]/ ) {
573             for ( my $i = 0; $i < @RT::Date::DAYS_OF_WEEK; $i++ ) {
574                 # Use a case-insensitive regex for better matching across
575                 # locales since we don't have fc() and lc() is worse.  Really
576                 # we should be doing Unicode normalization too, but we don't do
577                 # that elsewhere in RT.
578                 # 
579                 # XXX I18N: Replace the regex with fc() once we're guaranteed 5.16.
580                 next unless lc $RT::Date::DAYS_OF_WEEK[ $i ] eq lc $value
581                          or $sb->CurrentUser->loc($RT::Date::DAYS_OF_WEEK[ $i ]) =~ /^\Q$value\E$/i;
582
583                 $value = $i; last;
584             }
585             return $sb->Limit( FIELD => 'id', VALUE => 0, %rest )
586                 if $value =~ /[^0-9]/;
587         }
588         elsif ( $subkey eq 'Month' && $op !~ /IS/i && $value =~ /[^0-9]/ ) {
589             for ( my $i = 0; $i < @RT::Date::MONTHS; $i++ ) {
590                 # Use a case-insensitive regex for better matching across
591                 # locales since we don't have fc() and lc() is worse.  Really
592                 # we should be doing Unicode normalization too, but we don't do
593                 # that elsewhere in RT.
594                 # 
595                 # XXX I18N: Replace the regex with fc() once we're guaranteed 5.16.
596                 next unless lc $RT::Date::MONTHS[ $i ] eq lc $value
597                          or $sb->CurrentUser->loc($RT::Date::MONTHS[ $i ]) =~ /^\Q$value\E$/i;
598
599                 $value = $i + 1; last;
600             }
601             return $sb->Limit( FIELD => 'id', VALUE => 0, %rest )
602                 if $value =~ /[^0-9]/;
603         }
604
605         my $tz;
606         if ( RT->Config->Get('ChartsTimezonesInDB') ) {
607             my $to = $sb->CurrentUser->UserObj->Timezone
608                 || RT->Config->Get('Timezone');
609             $tz = { From => 'UTC', To => $to }
610                 if $to && lc $to ne 'utc';
611         }
612
613         # $subkey is validated by DateTimeFunction
614         my $function = $RT::Handle->DateTimeFunction(
615             Type     => $subkey,
616             Field    => $sb->NotSetDateToNullFunction,
617             Timezone => $tz,
618         );
619
620         return $sb->Limit(
621             FUNCTION => $function,
622             FIELD    => $meta->[1],
623             OPERATOR => $op,
624             VALUE    => $value,
625             %rest,
626         );
627     }
628
629     my $date = RT::Date->new( $sb->CurrentUser );
630     $date->Set( Format => 'unknown', Value => $value );
631
632     if ( $op eq "=" ) {
633
634         # if we're specifying =, that means we want everything on a
635         # particular single day.  in the database, we need to check for >
636         # and < the edges of that day.
637
638         $date->SetToMidnight( Timezone => 'server' );
639         my $daystart = $date->ISO;
640         $date->AddDay;
641         my $dayend = $date->ISO;
642
643         $sb->_OpenParen;
644
645         $sb->Limit(
646             FIELD    => $meta->[1],
647             OPERATOR => ">=",
648             VALUE    => $daystart,
649             %rest,
650         );
651
652         $sb->Limit(
653             FIELD    => $meta->[1],
654             OPERATOR => "<",
655             VALUE    => $dayend,
656             %rest,
657             ENTRYAGGREGATOR => 'AND',
658         );
659
660         $sb->_CloseParen;
661
662     }
663     else {
664         $sb->Limit(
665             FUNCTION => $sb->NotSetDateToNullFunction,
666             FIELD    => $meta->[1],
667             OPERATOR => $op,
668             VALUE    => $date->ISO,
669             %rest,
670         );
671     }
672 }
673
674 =head2 _StringLimit
675
676 Handle simple fields which are just strings.  (Subject,Type)
677
678 Meta Data:
679   None
680
681 =cut
682
683 sub _StringLimit {
684     my ( $sb, $field, $op, $value, @rest ) = @_;
685
686     # FIXME:
687     # Valid Operators:
688     #  =, !=, LIKE, NOT LIKE
689     if ( RT->Config->Get('DatabaseType') eq 'Oracle'
690         && (!defined $value || !length $value)
691         && lc($op) ne 'is' && lc($op) ne 'is not'
692     ) {
693         if ($op eq '!=' || $op =~ /^NOT\s/i) {
694             $op = 'IS NOT';
695         } else {
696             $op = 'IS';
697         }
698         $value = 'NULL';
699     }
700
701     $sb->Limit(
702         FIELD         => $field,
703         OPERATOR      => $op,
704         VALUE         => $value,
705         CASESENSITIVE => 0,
706         @rest,
707     );
708 }
709
710 =head2 _TransDateLimit
711
712 Handle fields limiting based on Transaction Date.
713
714 The inpupt value must be in a format parseable by Time::ParseDate
715
716 Meta Data:
717   None
718
719 =cut
720
721 # This routine should really be factored into translimit.
722 sub _TransDateLimit {
723     my ( $sb, $field, $op, $value, @rest ) = @_;
724
725     # See the comments for TransLimit, they apply here too
726
727     my $txn_alias = $sb->JoinTransactions;
728
729     my $date = RT::Date->new( $sb->CurrentUser );
730     $date->Set( Format => 'unknown', Value => $value );
731
732     $sb->_OpenParen;
733     if ( $op eq "=" ) {
734
735         # if we're specifying =, that means we want everything on a
736         # particular single day.  in the database, we need to check for >
737         # and < the edges of that day.
738
739         $date->SetToMidnight( Timezone => 'server' );
740         my $daystart = $date->ISO;
741         $date->AddDay;
742         my $dayend = $date->ISO;
743
744         $sb->Limit(
745             ALIAS         => $txn_alias,
746             FIELD         => 'Created',
747             OPERATOR      => ">=",
748             VALUE         => $daystart,
749             @rest
750         );
751         $sb->Limit(
752             ALIAS         => $txn_alias,
753             FIELD         => 'Created',
754             OPERATOR      => "<=",
755             VALUE         => $dayend,
756             @rest,
757             ENTRYAGGREGATOR => 'AND',
758         );
759
760     }
761
762     # not searching for a single day
763     else {
764
765         #Search for the right field
766         $sb->Limit(
767             ALIAS         => $txn_alias,
768             FIELD         => 'Created',
769             OPERATOR      => $op,
770             VALUE         => $date->ISO,
771             @rest
772         );
773     }
774
775     $sb->_CloseParen;
776 }
777
778 sub _TransCreatorLimit {
779     my ( $sb, $field, $op, $value, @rest ) = @_;
780     $op = "!=" if $op eq "<>";
781     die "Invalid Operation: $op for $field" unless $op eq "=" or $op eq "!=";
782
783     # See the comments for TransLimit, they apply here too
784     my $txn_alias = $sb->JoinTransactions;
785     if ( defined $value && $value !~ /^\d+$/ ) {
786         my $u = RT::User->new( $sb->CurrentUser );
787         $u->Load($value);
788         $value = $u->id || 0;
789     }
790     $sb->_SQLLimit( ALIAS => $txn_alias, FIELD => 'Creator', OPERATOR => $op, VALUE => $value, @rest );
791 }
792
793 =head2 _TransLimit
794
795 Limit based on the ContentType or the Filename of a transaction.
796
797 =cut
798
799 sub _TransLimit {
800     my ( $self, $field, $op, $value, %rest ) = @_;
801
802     my $txn_alias = $self->JoinTransactions;
803     unless ( defined $self->{_sql_trattachalias} ) {
804         $self->{_sql_trattachalias} = $self->Join(
805             TYPE   => 'LEFT', # not all txns have an attachment
806             ALIAS1 => $txn_alias,
807             FIELD1 => 'id',
808             TABLE2 => 'Attachments',
809             FIELD2 => 'TransactionId',
810         );
811     }
812
813     $self->Limit(
814         %rest,
815         ALIAS         => $self->{_sql_trattachalias},
816         FIELD         => $field,
817         OPERATOR      => $op,
818         VALUE         => $value,
819         CASESENSITIVE => 0,
820     );
821 }
822
823 =head2 _TransContentLimit
824
825 Limit based on the Content of a transaction.
826
827 =cut
828
829 sub _TransContentLimit {
830
831     # Content search
832
833     # If only this was this simple.  We've got to do something
834     # complicated here:
835
836     #Basically, we want to make sure that the limits apply to
837     #the same attachment, rather than just another attachment
838     #for the same ticket, no matter how many clauses we lump
839     #on.
840
841     # In the SQL, we might have
842     #       (( Content = foo ) or ( Content = bar AND Content = baz ))
843     # The AND group should share the same Alias.
844
845     # Actually, maybe it doesn't matter.  We use the same alias and it
846     # works itself out? (er.. different.)
847
848     # Steal more from _ProcessRestrictions
849
850     # FIXME: Maybe look at the previous FooLimit call, and if it was a
851     # TransLimit and EntryAggregator == AND, reuse the Aliases?
852
853     # Or better - store the aliases on a per subclause basis - since
854     # those are going to be the things we want to relate to each other,
855     # anyway.
856
857     # maybe we should not allow certain kinds of aggregation of these
858     # clauses and do a psuedo regex instead? - the problem is getting
859     # them all into the same subclause when you have (A op B op C) - the
860     # way they get parsed in the tree they're in different subclauses.
861
862     my ( $self, $field, $op, $value, %rest ) = @_;
863     $field = 'Content' if $field =~ /\W/;
864
865     my $config = RT->Config->Get('FullTextSearch') || {};
866     unless ( $config->{'Enable'} ) {
867         $self->Limit( %rest, FIELD => 'id', VALUE => 0 );
868         return;
869     }
870
871     my $txn_alias = $self->JoinTransactions;
872     unless ( defined $self->{_sql_trattachalias} ) {
873         $self->{_sql_trattachalias} = $self->Join(
874             TYPE   => 'LEFT', # not all txns have an attachment
875             ALIAS1 => $txn_alias,
876             FIELD1 => 'id',
877             TABLE2 => 'Attachments',
878             FIELD2 => 'TransactionId',
879         );
880     }
881
882     $self->_OpenParen;
883     if ( $config->{'Indexed'} ) {
884         my $db_type = RT->Config->Get('DatabaseType');
885
886         my $alias;
887         if ( $config->{'Table'} and $config->{'Table'} ne "Attachments") {
888             $alias = $self->{'_sql_aliases'}{'full_text'} ||= $self->Join(
889                 TYPE   => 'LEFT',
890                 ALIAS1 => $self->{'_sql_trattachalias'},
891                 FIELD1 => 'id',
892                 TABLE2 => $config->{'Table'},
893                 FIELD2 => 'id',
894             );
895         } else {
896             $alias = $self->{'_sql_trattachalias'};
897         }
898
899         #XXX: handle negative searches
900         my $index = $config->{'Column'};
901         if ( $db_type eq 'Oracle' ) {
902             my $dbh = $RT::Handle->dbh;
903             my $alias = $self->{_sql_trattachalias};
904             $self->Limit(
905                 %rest,
906                 FUNCTION      => "CONTAINS( $alias.$field, ".$dbh->quote($value) .")",
907                 OPERATOR      => '>',
908                 VALUE         => 0,
909                 QUOTEVALUE    => 0,
910                 CASESENSITIVE => 1,
911             );
912             # this is required to trick DBIx::SB's LEFT JOINS optimizer
913             # into deciding that join is redundant as it is
914             $self->Limit(
915                 ENTRYAGGREGATOR => 'AND',
916                 ALIAS           => $self->{_sql_trattachalias},
917                 FIELD           => 'Content',
918                 OPERATOR        => 'IS NOT',
919                 VALUE           => 'NULL',
920             );
921         }
922         elsif ( $db_type eq 'Pg' ) {
923             my $dbh = $RT::Handle->dbh;
924             $self->Limit(
925                 %rest,
926                 ALIAS       => $alias,
927                 FIELD       => $index,
928                 OPERATOR    => '@@',
929                 VALUE       => 'plainto_tsquery('. $dbh->quote($value) .')',
930                 QUOTEVALUE  => 0,
931             );
932         }
933         elsif ( $db_type eq 'mysql' ) {
934             # XXX: We could theoretically skip the join to Attachments,
935             # and have Sphinx simply index and group by the TicketId,
936             # and join Ticket.id to that attribute, which would be much
937             # more efficient -- however, this is only a possibility if
938             # there are no other transaction limits.
939
940             # This is a special character.  Note that \ does not escape
941             # itself (in Sphinx 2.1.0, at least), so 'foo\;bar' becoming
942             # 'foo\\;bar' is not a vulnerability, and is still parsed as
943             # "foo, \, ;, then bar".  Happily, the default mode is
944             # "all", meaning that boolean operators are not special.
945             $value =~ s/;/\\;/g;
946
947             my $max = $config->{'MaxMatches'};
948             $self->Limit(
949                 %rest,
950                 ALIAS       => $alias,
951                 FIELD       => 'query',
952                 OPERATOR    => '=',
953                 VALUE       => "$value;limit=$max;maxmatches=$max",
954             );
955         }
956     } else {
957         $self->Limit(
958             %rest,
959             ALIAS         => $self->{_sql_trattachalias},
960             FIELD         => $field,
961             OPERATOR      => $op,
962             VALUE         => $value,
963             CASESENSITIVE => 0,
964         );
965     }
966     if ( RT->Config->Get('DontSearchFileAttachments') ) {
967         $self->Limit(
968             ENTRYAGGREGATOR => 'AND',
969             ALIAS           => $self->{_sql_trattachalias},
970             FIELD           => 'Filename',
971             OPERATOR        => 'IS',
972             VALUE           => 'NULL',
973         );
974     }
975     $self->_CloseParen;
976 }
977
978 =head2 _WatcherLimit
979
980 Handle watcher limits.  (Requestor, CC, etc..)
981
982 Meta Data:
983   1: Field to query on
984
985
986
987 =cut
988
989 sub _WatcherLimit {
990     my $self  = shift;
991     my $field = shift;
992     my $op    = shift;
993     my $value = shift;
994     my %rest  = (@_);
995
996     my $meta = $FIELD_METADATA{ $field };
997     my $type = $meta->[1] || '';
998     my $class = $meta->[2] || 'Ticket';
999
1000     # Bail if the subfield is not allowed
1001     if (    $rest{SUBKEY}
1002         and not grep { $_ eq $rest{SUBKEY} } @{$SEARCHABLE_SUBFIELDS{'User'}})
1003     {
1004         die "Invalid watcher subfield: '$rest{SUBKEY}'";
1005     }
1006
1007     $self->RoleLimit(
1008         TYPE      => $type,
1009         CLASS     => "RT::$class",
1010         FIELD     => $rest{SUBKEY},
1011         OPERATOR  => $op,
1012         VALUE     => $value,
1013         SUBCLAUSE => "ticketsql",
1014         %rest,
1015     );
1016 }
1017
1018 =head2 _WatcherMembershipLimit
1019
1020 Handle watcher membership limits, i.e. whether the watcher belongs to a
1021 specific group or not.
1022
1023 Meta Data:
1024   1: Role to query on
1025
1026 =cut
1027
1028 sub _WatcherMembershipLimit {
1029     my ( $self, $field, $op, $value, %rest ) = @_;
1030
1031     # we don't support anything but '='
1032     die "Invalid $field Op: $op"
1033         unless $op =~ /^=$/;
1034
1035     unless ( $value =~ /^\d+$/ ) {
1036         my $group = RT::Group->new( $self->CurrentUser );
1037         $group->LoadUserDefinedGroup( $value );
1038         $value = $group->id || 0;
1039     }
1040
1041     my $meta = $FIELD_METADATA{$field};
1042     my $type = $meta->[1] || '';
1043
1044     my ($members_alias, $members_column);
1045     if ( $type eq 'Owner' ) {
1046         ($members_alias, $members_column) = ('main', 'Owner');
1047     } else {
1048         (undef, undef, $members_alias) = $self->_WatcherJoin( New => 1, Name => $type );
1049         $members_column = 'id';
1050     }
1051
1052     my $cgm_alias = $self->Join(
1053         ALIAS1          => $members_alias,
1054         FIELD1          => $members_column,
1055         TABLE2          => 'CachedGroupMembers',
1056         FIELD2          => 'MemberId',
1057     );
1058     $self->Limit(
1059         LEFTJOIN => $cgm_alias,
1060         ALIAS => $cgm_alias,
1061         FIELD => 'Disabled',
1062         VALUE => 0,
1063     );
1064
1065     $self->Limit(
1066         ALIAS    => $cgm_alias,
1067         FIELD    => 'GroupId',
1068         VALUE    => $value,
1069         OPERATOR => $op,
1070         %rest,
1071     );
1072 }
1073
1074 =head2 _CustomFieldDecipher
1075
1076 Try and turn a CF descriptor into (cfid, cfname) object pair.
1077
1078 Takes an optional second parameter of the CF LookupType, defaults to Ticket CFs.
1079
1080 =cut
1081
1082 sub _CustomFieldDecipher {
1083     my ($self, $string, $lookuptype) = @_;
1084     $lookuptype ||= $self->_SingularClass->CustomFieldLookupType;
1085
1086     my ($object, $field, $column) = ($string =~ /^(?:(.+?)\.)?\{(.+)\}(?:\.(Content|LargeContent))?$/);
1087     $field ||= ($string =~ /^\{(.*?)\}$/)[0] || $string;
1088
1089     my ($cf, $applied_to);
1090
1091     if ( $object ) {
1092         my $record_class = RT::CustomField->RecordClassFromLookupType($lookuptype);
1093         $applied_to = $record_class->new( $self->CurrentUser );
1094         $applied_to->Load( $object );
1095
1096         if ( $applied_to->id ) {
1097             RT->Logger->debug("Limiting to CFs identified by '$field' applied to $record_class #@{[$applied_to->id]} (loaded via '$object')");
1098         }
1099         else {
1100             RT->Logger->warning("$record_class '$object' doesn't exist, parsed from '$string'");
1101             $object = 0;
1102             undef $applied_to;
1103         }
1104     }
1105
1106     if ( $field =~ /\D/ ) {
1107         $object ||= '';
1108         my $cfs = RT::CustomFields->new( $self->CurrentUser );
1109         $cfs->Limit( FIELD => 'Name', VALUE => $field, CASESENSITIVE => 0 );
1110         $cfs->LimitToLookupType($lookuptype);
1111
1112         if ($applied_to) {
1113             $cfs->SetContextObject($applied_to);
1114             $cfs->LimitToObjectId($applied_to->id);
1115         }
1116
1117         # if there is more then one field the current user can
1118         # see with the same name then we shouldn't return cf object
1119         # as we don't know which one to use
1120         $cf = $cfs->First;
1121         if ( $cf ) {
1122             $cf = undef if $cfs->Next;
1123         }
1124     }
1125     else {
1126         $cf = RT::CustomField->new( $self->CurrentUser );
1127         $cf->Load( $field );
1128         $cf->SetContextObject($applied_to)
1129             if $cf->id and $applied_to;
1130     }
1131
1132     return ($object, $field, $cf, $column);
1133 }
1134
1135 =head2 _CustomFieldLimit
1136
1137 Limit based on CustomFields
1138
1139 Meta Data:
1140   none
1141
1142 =cut
1143
1144 sub _CustomFieldLimit {
1145     my ( $self, $_field, $op, $value, %rest ) = @_;
1146
1147     my $meta  = $FIELD_METADATA{ $_field };
1148     my $class = $meta->[1] || 'Ticket';
1149     my $type  = "RT::$class"->CustomFieldLookupType;
1150
1151     my $field = $rest{'SUBKEY'} || die "No field specified";
1152
1153     # For our sanity, we can only limit on one object at a time
1154
1155     my ($object, $cfid, $cf, $column);
1156     ($object, $field, $cf, $column) = $self->_CustomFieldDecipher( $field, $type );
1157
1158
1159     $self->_LimitCustomField(
1160         %rest,
1161         LOOKUPTYPE  => $type,
1162         CUSTOMFIELD => $cf || $field,
1163         KEY      => $cf ? $cf->id : "$type-$object.$field",
1164         OPERATOR => $op,
1165         VALUE    => $value,
1166         COLUMN   => $column,
1167         SUBCLAUSE => "ticketsql",
1168     );
1169 }
1170
1171 sub _CustomFieldJoinByName {
1172     my $self = shift;
1173     my ($ObjectAlias, $cf, $type) = @_;
1174
1175     my ($ocfvalias, $CFs, $ocfalias) = $self->SUPER::_CustomFieldJoinByName(@_);
1176     $self->Limit(
1177         LEFTJOIN        => $ocfalias,
1178         ENTRYAGGREGATOR => 'OR',
1179         FIELD           => 'ObjectId',
1180         VALUE           => 'main.Queue',
1181         QUOTEVALUE      => 0,
1182     );
1183     return ($ocfvalias, $CFs, $ocfalias);
1184 }
1185
1186 sub _HasAttributeLimit {
1187     my ( $self, $field, $op, $value, %rest ) = @_;
1188
1189     my $alias = $self->Join(
1190         TYPE   => 'LEFT',
1191         ALIAS1 => 'main',
1192         FIELD1 => 'id',
1193         TABLE2 => 'Attributes',
1194         FIELD2 => 'ObjectId',
1195     );
1196     $self->Limit(
1197         LEFTJOIN        => $alias,
1198         FIELD           => 'ObjectType',
1199         VALUE           => 'RT::Ticket',
1200         ENTRYAGGREGATOR => 'AND'
1201     );
1202     $self->Limit(
1203         LEFTJOIN        => $alias,
1204         FIELD           => 'Name',
1205         OPERATOR        => $op,
1206         VALUE           => $value,
1207         ENTRYAGGREGATOR => 'AND'
1208     );
1209     $self->Limit(
1210         %rest,
1211         ALIAS      => $alias,
1212         FIELD      => 'id',
1213         OPERATOR   => $FIELD_METADATA{$field}->[1]? 'IS NOT': 'IS',
1214         VALUE      => 'NULL',
1215         QUOTEVALUE => 0,
1216     );
1217 }
1218
1219
1220 # End Helper Functions
1221
1222 # End of SQL Stuff -------------------------------------------------
1223
1224
1225 =head2 OrderByCols ARRAY
1226
1227 A modified version of the OrderBy method which automatically joins where
1228 C<ALIAS> is set to the name of a watcher type.
1229
1230 =cut
1231
1232 sub OrderByCols {
1233     my $self = shift;
1234     my @args = @_;
1235     my $clause;
1236     my @res   = ();
1237     my $order = 0;
1238
1239     foreach my $row (@args) {
1240         if ( $row->{ALIAS} ) {
1241             push @res, $row;
1242             next;
1243         }
1244         if ( $row->{FIELD} !~ /\./ ) {
1245             my $meta = $FIELD_METADATA{ $row->{FIELD} };
1246             unless ( $meta ) {
1247                 push @res, $row;
1248                 next;
1249             }
1250
1251             if ( $meta->[0] eq 'ENUM' && ($meta->[1]||'') eq 'Queue' ) {
1252                 my $alias = $self->Join(
1253                     TYPE   => 'LEFT',
1254                     ALIAS1 => 'main',
1255                     FIELD1 => $row->{'FIELD'},
1256                     TABLE2 => 'Queues',
1257                     FIELD2 => 'id',
1258                 );
1259                 push @res, { %$row, ALIAS => $alias, FIELD => "Name", CASESENSITIVE => 0 };
1260             } elsif ( ( $meta->[0] eq 'ENUM' && ($meta->[1]||'') eq 'User' )
1261                 || ( $meta->[0] eq 'WATCHERFIELD' && ($meta->[1]||'') eq 'Owner' )
1262             ) {
1263                 my $alias = $self->Join(
1264                     TYPE   => 'LEFT',
1265                     ALIAS1 => 'main',
1266                     FIELD1 => $row->{'FIELD'},
1267                     TABLE2 => 'Users',
1268                     FIELD2 => 'id',
1269                 );
1270                 push @res, { %$row, ALIAS => $alias, FIELD => "Name", CASESENSITIVE => 0 };
1271             } else {
1272                 push @res, $row;
1273             }
1274             next;
1275         }
1276
1277         my ( $field, $subkey ) = split /\./, $row->{FIELD}, 2;
1278         my $meta = $FIELD_METADATA{$field};
1279         if ( defined $meta->[0] && $meta->[0] eq 'WATCHERFIELD' ) {
1280             # cache alias as we want to use one alias per watcher type for sorting
1281             my $cache_key = join "-", map { $_ || "" } @$meta[1,2];
1282             my $users = $self->{_sql_u_watchers_alias_for_sort}{ $cache_key };
1283             unless ( $users ) {
1284                 $self->{_sql_u_watchers_alias_for_sort}{ $cache_key }
1285                     = $users = ( $self->_WatcherJoin( Name => $meta->[1], Class => "RT::" . ($meta->[2] || 'Ticket') ) )[2];
1286             }
1287             push @res, { %$row, ALIAS => $users, FIELD => $subkey };
1288        } elsif ( defined $meta->[0] && $meta->[0] eq 'CUSTOMFIELD' ) {
1289            my ($object, $field, $cf, $column) = $self->_CustomFieldDecipher( $subkey );
1290            my $cfkey = $cf ? $cf->id : "$object.$field";
1291            push @res, $self->_OrderByCF( $row, $cfkey, ($cf || $field) );
1292        } elsif ( $field eq "Custom" && $subkey eq "Ownership") {
1293            # PAW logic is "reversed"
1294            my $order = "ASC";
1295            if (exists $row->{ORDER} ) {
1296                my $o = $row->{ORDER};
1297                delete $row->{ORDER};
1298                $order = "DESC" if $o =~ /asc/i;
1299            }
1300
1301            # Ticket.Owner    1 0 X
1302            # Unowned Tickets 0 1 X
1303            # Else            0 0 X
1304
1305            foreach my $uid ( $self->CurrentUser->Id, RT->Nobody->Id ) {
1306                if ( RT->Config->Get('DatabaseType') eq 'Oracle' ) {
1307                    my $f = ($row->{'ALIAS'} || 'main') .'.Owner';
1308                    push @res, {
1309                        %$row,
1310                        FIELD => undef,
1311                        ALIAS => '',
1312                        FUNCTION => "CASE WHEN $f=$uid THEN 1 ELSE 0 END",
1313                        ORDER => $order
1314                    };
1315                } else {
1316                    push @res, {
1317                        %$row,
1318                        FIELD => undef,
1319                        FUNCTION => "Owner=$uid",
1320                        ORDER => $order
1321                    };
1322                }
1323            }
1324
1325            push @res, { %$row, FIELD => "Priority", ORDER => $order } ;
1326        }
1327        else {
1328            push @res, $row;
1329        }
1330     }
1331     return $self->SUPER::OrderByCols(@res);
1332 }
1333
1334 sub _SQLLimit {
1335     my $self = shift;
1336     RT->Deprecated( Remove => "4.4", Instead => "Limit" );
1337     $self->Limit(@_);
1338 }
1339 sub _SQLJoin {
1340     my $self = shift;
1341     RT->Deprecated( Remove => "4.4", Instead => "Join" );
1342     $self->Join(@_);
1343 }
1344
1345 sub _OpenParen {
1346     $_[0]->SUPER::_OpenParen( $_[1] || 'ticketsql' );
1347 }
1348 sub _CloseParen {
1349     $_[0]->SUPER::_CloseParen( $_[1] || 'ticketsql' );
1350 }
1351
1352 sub Limit {
1353     my $self = shift;
1354     my %args = @_;
1355     $self->{'must_redo_search'} = 1;
1356     delete $self->{'raw_rows'};
1357     delete $self->{'count_all'};
1358
1359     if ($self->{'using_restrictions'}) {
1360         RT->Deprecated( Message => "Mixing old-style LimitFoo methods with Limit is deprecated" );
1361         $self->LimitField(@_);
1362     }
1363
1364     $args{SUBCLAUSE} ||= "ticketsql"
1365         if $self->{parsing_ticketsql} and not $args{LEFTJOIN};
1366
1367     $self->{_sql_looking_at}{ lc $args{FIELD} } = 1
1368         if $args{FIELD} and (not $args{ALIAS} or $args{ALIAS} eq "main");
1369
1370     $self->SUPER::Limit(%args);
1371 }
1372
1373
1374 =head2 LimitField
1375
1376 Takes a paramhash with the fields FIELD, OPERATOR, VALUE and DESCRIPTION
1377 Generally best called from LimitFoo methods
1378
1379 =cut
1380
1381 sub LimitField {
1382     my $self = shift;
1383     my %args = (
1384         FIELD       => undef,
1385         OPERATOR    => '=',
1386         VALUE       => undef,
1387         DESCRIPTION => undef,
1388         @_
1389     );
1390     $args{'DESCRIPTION'} = $self->loc(
1391         "[_1] [_2] [_3]",  $args{'FIELD'},
1392         $args{'OPERATOR'}, $args{'VALUE'}
1393         )
1394         if ( !defined $args{'DESCRIPTION'} );
1395
1396
1397     if ($self->_isLimited > 1) {
1398         RT->Deprecated( Message => "Mixing old-style LimitFoo methods with Limit is deprecated" );
1399     }
1400     $self->{using_restrictions} = 1;
1401
1402     my $index = $self->_NextIndex;
1403
1404 # make the TicketRestrictions hash the equivalent of whatever we just passed in;
1405
1406     %{ $self->{'TicketRestrictions'}{$index} } = %args;
1407
1408     $self->{'RecalcTicketLimits'} = 1;
1409
1410     return ($index);
1411 }
1412
1413
1414
1415
1416 =head2 LimitQueue
1417
1418 LimitQueue takes a paramhash with the fields OPERATOR and VALUE.
1419 OPERATOR is one of = or !=. (It defaults to =).
1420 VALUE is a queue id or Name.
1421
1422
1423 =cut
1424
1425 sub LimitQueue {
1426     my $self = shift;
1427     my %args = (
1428         VALUE    => undef,
1429         OPERATOR => '=',
1430         @_
1431     );
1432
1433     #TODO  VALUE should also take queue objects
1434     if ( defined $args{'VALUE'} && $args{'VALUE'} !~ /^\d+$/ ) {
1435         my $queue = RT::Queue->new( $self->CurrentUser );
1436         $queue->Load( $args{'VALUE'} );
1437         $args{'VALUE'} = $queue->Id;
1438     }
1439
1440     # What if they pass in an Id?  Check for isNum() and convert to
1441     # string.
1442
1443     #TODO check for a valid queue here
1444
1445     $self->LimitField(
1446         FIELD       => 'Queue',
1447         VALUE       => $args{'VALUE'},
1448         OPERATOR    => $args{'OPERATOR'},
1449         DESCRIPTION => join(
1450             ' ', $self->loc('Queue'), $args{'OPERATOR'}, $args{'VALUE'},
1451         ),
1452     );
1453
1454 }
1455
1456
1457
1458 =head2 LimitStatus
1459
1460 Takes a paramhash with the fields OPERATOR and VALUE.
1461 OPERATOR is one of = or !=.
1462 VALUE is a status.
1463
1464 RT adds Status != 'deleted' until object has
1465 allow_deleted_search internal property set.
1466 $tickets->{'allow_deleted_search'} = 1;
1467 $tickets->LimitStatus( VALUE => 'deleted' );
1468
1469 =cut
1470
1471 sub LimitStatus {
1472     my $self = shift;
1473     my %args = (
1474         OPERATOR => '=',
1475         @_
1476     );
1477     $self->LimitField(
1478         FIELD       => 'Status',
1479         VALUE       => $args{'VALUE'},
1480         OPERATOR    => $args{'OPERATOR'},
1481         DESCRIPTION => join( ' ',
1482             $self->loc('Status'), $args{'OPERATOR'},
1483             $self->loc( $args{'VALUE'} ) ),
1484     );
1485 }
1486
1487 =head2 LimitToActiveStatus
1488
1489 Limits the status to L<RT::Queue/ActiveStatusArray>
1490
1491 TODO: make this respect lifecycles for the queues associated with the search
1492
1493 =cut
1494
1495 sub LimitToActiveStatus {
1496     my $self = shift;
1497
1498     my @active = RT::Queue->ActiveStatusArray();
1499     for my $active (@active) {
1500         $self->LimitStatus(
1501             VALUE => $active,
1502         );
1503     }
1504 }
1505
1506 =head2 LimitToInactiveStatus
1507
1508 Limits the status to L<RT::Queue/InactiveStatusArray>
1509
1510 TODO: make this respect lifecycles for the queues associated with the search
1511
1512 =cut
1513
1514 sub LimitToInactiveStatus {
1515     my $self = shift;
1516
1517     my @active = RT::Queue->InactiveStatusArray();
1518     for my $active (@active) {
1519         $self->LimitStatus(
1520             VALUE => $active,
1521         );
1522     }
1523 }
1524
1525 =head2 IgnoreType
1526
1527 If called, this search will not automatically limit the set of results found
1528 to tickets of type "Ticket". Tickets of other types, such as "project" and
1529 "approval" will be found.
1530
1531 =cut
1532
1533 sub IgnoreType {
1534     my $self = shift;
1535
1536     # Instead of faking a Limit that later gets ignored, fake up the
1537     # fact that we're already looking at type, so that the check in
1538     # FromSQL goes down the right branch
1539
1540     #  $self->LimitType(VALUE => '__any');
1541     $self->{_sql_looking_at}{type} = 1;
1542 }
1543
1544
1545
1546 =head2 LimitType
1547
1548 Takes a paramhash with the fields OPERATOR and VALUE.
1549 OPERATOR is one of = or !=, it defaults to "=".
1550 VALUE is a string to search for in the type of the ticket.
1551
1552
1553
1554 =cut
1555
1556 sub LimitType {
1557     my $self = shift;
1558     my %args = (
1559         OPERATOR => '=',
1560         VALUE    => undef,
1561         @_
1562     );
1563     $self->LimitField(
1564         FIELD       => 'Type',
1565         VALUE       => $args{'VALUE'},
1566         OPERATOR    => $args{'OPERATOR'},
1567         DESCRIPTION => join( ' ',
1568             $self->loc('Type'), $args{'OPERATOR'}, $args{'VALUE'}, ),
1569     );
1570 }
1571
1572
1573
1574
1575
1576 =head2 LimitSubject
1577
1578 Takes a paramhash with the fields OPERATOR and VALUE.
1579 OPERATOR is one of = or !=.
1580 VALUE is a string to search for in the subject of the ticket.
1581
1582 =cut
1583
1584 sub LimitSubject {
1585     my $self = shift;
1586     my %args = (@_);
1587     $self->LimitField(
1588         FIELD       => 'Subject',
1589         VALUE       => $args{'VALUE'},
1590         OPERATOR    => $args{'OPERATOR'},
1591         DESCRIPTION => join( ' ',
1592             $self->loc('Subject'), $args{'OPERATOR'}, $args{'VALUE'}, ),
1593     );
1594 }
1595
1596
1597
1598 # Things that can be > < = !=
1599
1600
1601 =head2 LimitId
1602
1603 Takes a paramhash with the fields OPERATOR and VALUE.
1604 OPERATOR is one of =, >, < or !=.
1605 VALUE is a ticket Id to search for
1606
1607 =cut
1608
1609 sub LimitId {
1610     my $self = shift;
1611     my %args = (
1612         OPERATOR => '=',
1613         @_
1614     );
1615
1616     $self->LimitField(
1617         FIELD       => 'id',
1618         VALUE       => $args{'VALUE'},
1619         OPERATOR    => $args{'OPERATOR'},
1620         DESCRIPTION =>
1621             join( ' ', $self->loc('Id'), $args{'OPERATOR'}, $args{'VALUE'}, ),
1622     );
1623 }
1624
1625
1626
1627 =head2 LimitPriority
1628
1629 Takes a paramhash with the fields OPERATOR and VALUE.
1630 OPERATOR is one of =, >, < or !=.
1631 VALUE is a value to match the ticket's priority against
1632
1633 =cut
1634
1635 sub LimitPriority {
1636     my $self = shift;
1637     my %args = (@_);
1638     $self->LimitField(
1639         FIELD       => 'Priority',
1640         VALUE       => $args{'VALUE'},
1641         OPERATOR    => $args{'OPERATOR'},
1642         DESCRIPTION => join( ' ',
1643             $self->loc('Priority'),
1644             $args{'OPERATOR'}, $args{'VALUE'}, ),
1645     );
1646 }
1647
1648
1649
1650 =head2 LimitInitialPriority
1651
1652 Takes a paramhash with the fields OPERATOR and VALUE.
1653 OPERATOR is one of =, >, < or !=.
1654 VALUE is a value to match the ticket's initial priority against
1655
1656
1657 =cut
1658
1659 sub LimitInitialPriority {
1660     my $self = shift;
1661     my %args = (@_);
1662     $self->LimitField(
1663         FIELD       => 'InitialPriority',
1664         VALUE       => $args{'VALUE'},
1665         OPERATOR    => $args{'OPERATOR'},
1666         DESCRIPTION => join( ' ',
1667             $self->loc('Initial Priority'), $args{'OPERATOR'},
1668             $args{'VALUE'}, ),
1669     );
1670 }
1671
1672
1673
1674 =head2 LimitFinalPriority
1675
1676 Takes a paramhash with the fields OPERATOR and VALUE.
1677 OPERATOR is one of =, >, < or !=.
1678 VALUE is a value to match the ticket's final priority against
1679
1680 =cut
1681
1682 sub LimitFinalPriority {
1683     my $self = shift;
1684     my %args = (@_);
1685     $self->LimitField(
1686         FIELD       => 'FinalPriority',
1687         VALUE       => $args{'VALUE'},
1688         OPERATOR    => $args{'OPERATOR'},
1689         DESCRIPTION => join( ' ',
1690             $self->loc('Final Priority'), $args{'OPERATOR'},
1691             $args{'VALUE'}, ),
1692     );
1693 }
1694
1695
1696
1697 =head2 LimitTimeWorked
1698
1699 Takes a paramhash with the fields OPERATOR and VALUE.
1700 OPERATOR is one of =, >, < or !=.
1701 VALUE is a value to match the ticket's TimeWorked attribute
1702
1703 =cut
1704
1705 sub LimitTimeWorked {
1706     my $self = shift;
1707     my %args = (@_);
1708     $self->LimitField(
1709         FIELD       => 'TimeWorked',
1710         VALUE       => $args{'VALUE'},
1711         OPERATOR    => $args{'OPERATOR'},
1712         DESCRIPTION => join( ' ',
1713             $self->loc('Time Worked'),
1714             $args{'OPERATOR'}, $args{'VALUE'}, ),
1715     );
1716 }
1717
1718
1719
1720 =head2 LimitTimeLeft
1721
1722 Takes a paramhash with the fields OPERATOR and VALUE.
1723 OPERATOR is one of =, >, < or !=.
1724 VALUE is a value to match the ticket's TimeLeft attribute
1725
1726 =cut
1727
1728 sub LimitTimeLeft {
1729     my $self = shift;
1730     my %args = (@_);
1731     $self->LimitField(
1732         FIELD       => 'TimeLeft',
1733         VALUE       => $args{'VALUE'},
1734         OPERATOR    => $args{'OPERATOR'},
1735         DESCRIPTION => join( ' ',
1736             $self->loc('Time Left'),
1737             $args{'OPERATOR'}, $args{'VALUE'}, ),
1738     );
1739 }
1740
1741
1742
1743
1744
1745 =head2 LimitContent
1746
1747 Takes a paramhash with the fields OPERATOR and VALUE.
1748 OPERATOR is one of =, LIKE, NOT LIKE or !=.
1749 VALUE is a string to search for in the body of the ticket
1750
1751 =cut
1752
1753 sub LimitContent {
1754     my $self = shift;
1755     my %args = (@_);
1756     $self->LimitField(
1757         FIELD       => 'Content',
1758         VALUE       => $args{'VALUE'},
1759         OPERATOR    => $args{'OPERATOR'},
1760         DESCRIPTION => join( ' ',
1761             $self->loc('Ticket content'), $args{'OPERATOR'},
1762             $args{'VALUE'}, ),
1763     );
1764 }
1765
1766
1767
1768 =head2 LimitFilename
1769
1770 Takes a paramhash with the fields OPERATOR and VALUE.
1771 OPERATOR is one of =, LIKE, NOT LIKE or !=.
1772 VALUE is a string to search for in the body of the ticket
1773
1774 =cut
1775
1776 sub LimitFilename {
1777     my $self = shift;
1778     my %args = (@_);
1779     $self->LimitField(
1780         FIELD       => 'Filename',
1781         VALUE       => $args{'VALUE'},
1782         OPERATOR    => $args{'OPERATOR'},
1783         DESCRIPTION => join( ' ',
1784             $self->loc('Attachment filename'), $args{'OPERATOR'},
1785             $args{'VALUE'}, ),
1786     );
1787 }
1788
1789
1790 =head2 LimitContentType
1791
1792 Takes a paramhash with the fields OPERATOR and VALUE.
1793 OPERATOR is one of =, LIKE, NOT LIKE or !=.
1794 VALUE is a content type to search ticket attachments for
1795
1796 =cut
1797
1798 sub LimitContentType {
1799     my $self = shift;
1800     my %args = (@_);
1801     $self->LimitField(
1802         FIELD       => 'ContentType',
1803         VALUE       => $args{'VALUE'},
1804         OPERATOR    => $args{'OPERATOR'},
1805         DESCRIPTION => join( ' ',
1806             $self->loc('Ticket content type'), $args{'OPERATOR'},
1807             $args{'VALUE'}, ),
1808     );
1809 }
1810
1811
1812
1813
1814
1815 =head2 LimitOwner
1816
1817 Takes a paramhash with the fields OPERATOR and VALUE.
1818 OPERATOR is one of = or !=.
1819 VALUE is a user id.
1820
1821 =cut
1822
1823 sub LimitOwner {
1824     my $self = shift;
1825     my %args = (
1826         OPERATOR => '=',
1827         @_
1828     );
1829
1830     my $owner = RT::User->new( $self->CurrentUser );
1831     $owner->Load( $args{'VALUE'} );
1832
1833     # FIXME: check for a valid $owner
1834     $self->LimitField(
1835         FIELD       => 'Owner',
1836         VALUE       => $args{'VALUE'},
1837         OPERATOR    => $args{'OPERATOR'},
1838         DESCRIPTION => join( ' ',
1839             $self->loc('Owner'), $args{'OPERATOR'}, $owner->Name(), ),
1840     );
1841
1842 }
1843
1844
1845
1846
1847 =head2 LimitWatcher
1848
1849   Takes a paramhash with the fields OPERATOR, TYPE and VALUE.
1850   OPERATOR is one of =, LIKE, NOT LIKE or !=.
1851   VALUE is a value to match the ticket's watcher email addresses against
1852   TYPE is the sort of watchers you want to match against. Leave it undef if you want to search all of them
1853
1854
1855 =cut
1856
1857 sub LimitWatcher {
1858     my $self = shift;
1859     my %args = (
1860         OPERATOR => '=',
1861         VALUE    => undef,
1862         TYPE     => undef,
1863         @_
1864     );
1865
1866     #build us up a description
1867     my ( $watcher_type, $desc );
1868     if ( $args{'TYPE'} ) {
1869         $watcher_type = $args{'TYPE'};
1870     }
1871     else {
1872         $watcher_type = "Watcher";
1873     }
1874
1875     $self->LimitField(
1876         FIELD       => $watcher_type,
1877         VALUE       => $args{'VALUE'},
1878         OPERATOR    => $args{'OPERATOR'},
1879         TYPE        => $args{'TYPE'},
1880         DESCRIPTION => join( ' ',
1881             $self->loc($watcher_type),
1882             $args{'OPERATOR'}, $args{'VALUE'}, ),
1883     );
1884 }
1885
1886
1887
1888
1889
1890
1891 =head2 LimitLinkedTo
1892
1893 LimitLinkedTo takes a paramhash with two fields: TYPE and TARGET
1894 TYPE limits the sort of link we want to search on
1895
1896 TYPE = { RefersTo, MemberOf, DependsOn }
1897
1898 TARGET is the id or URI of the TARGET of the link
1899
1900 =cut
1901
1902 sub LimitLinkedTo {
1903     my $self = shift;
1904     my %args = (
1905         TARGET   => undef,
1906         TYPE     => undef,
1907         OPERATOR => '=',
1908         @_
1909     );
1910
1911     $self->LimitField(
1912         FIELD       => 'LinkedTo',
1913         BASE        => undef,
1914         TARGET      => $args{'TARGET'},
1915         TYPE        => $args{'TYPE'},
1916         DESCRIPTION => $self->loc(
1917             "Tickets [_1] by [_2]",
1918             $self->loc( $args{'TYPE'} ),
1919             $args{'TARGET'}
1920         ),
1921         OPERATOR    => $args{'OPERATOR'},
1922     );
1923 }
1924
1925
1926
1927 =head2 LimitLinkedFrom
1928
1929 LimitLinkedFrom takes a paramhash with two fields: TYPE and BASE
1930 TYPE limits the sort of link we want to search on
1931
1932
1933 BASE is the id or URI of the BASE of the link
1934
1935 =cut
1936
1937 sub LimitLinkedFrom {
1938     my $self = shift;
1939     my %args = (
1940         BASE     => undef,
1941         TYPE     => undef,
1942         OPERATOR => '=',
1943         @_
1944     );
1945
1946     # translate RT2 From/To naming to RT3 TicketSQL naming
1947     my %fromToMap = qw(DependsOn DependentOn
1948         MemberOf  HasMember
1949         RefersTo  ReferredToBy);
1950
1951     my $type = $args{'TYPE'};
1952     $type = $fromToMap{$type} if exists( $fromToMap{$type} );
1953
1954     $self->LimitField(
1955         FIELD       => 'LinkedTo',
1956         TARGET      => undef,
1957         BASE        => $args{'BASE'},
1958         TYPE        => $type,
1959         DESCRIPTION => $self->loc(
1960             "Tickets [_1] [_2]",
1961             $self->loc( $args{'TYPE'} ),
1962             $args{'BASE'},
1963         ),
1964         OPERATOR    => $args{'OPERATOR'},
1965     );
1966 }
1967
1968
1969 sub LimitMemberOf {
1970     my $self      = shift;
1971     my $ticket_id = shift;
1972     return $self->LimitLinkedTo(
1973         @_,
1974         TARGET => $ticket_id,
1975         TYPE   => 'MemberOf',
1976     );
1977 }
1978
1979
1980 sub LimitHasMember {
1981     my $self      = shift;
1982     my $ticket_id = shift;
1983     return $self->LimitLinkedFrom(
1984         @_,
1985         BASE => "$ticket_id",
1986         TYPE => 'HasMember',
1987     );
1988
1989 }
1990
1991
1992
1993 sub LimitDependsOn {
1994     my $self      = shift;
1995     my $ticket_id = shift;
1996     return $self->LimitLinkedTo(
1997         @_,
1998         TARGET => $ticket_id,
1999         TYPE   => 'DependsOn',
2000     );
2001
2002 }
2003
2004
2005
2006 sub LimitDependedOnBy {
2007     my $self      = shift;
2008     my $ticket_id = shift;
2009     return $self->LimitLinkedFrom(
2010         @_,
2011         BASE => $ticket_id,
2012         TYPE => 'DependentOn',
2013     );
2014
2015 }
2016
2017
2018
2019 sub LimitRefersTo {
2020     my $self      = shift;
2021     my $ticket_id = shift;
2022     return $self->LimitLinkedTo(
2023         @_,
2024         TARGET => $ticket_id,
2025         TYPE   => 'RefersTo',
2026     );
2027
2028 }
2029
2030
2031
2032 sub LimitReferredToBy {
2033     my $self      = shift;
2034     my $ticket_id = shift;
2035     return $self->LimitLinkedFrom(
2036         @_,
2037         BASE => $ticket_id,
2038         TYPE => 'ReferredToBy',
2039     );
2040 }
2041
2042
2043
2044
2045
2046 =head2 LimitDate (FIELD => 'DateField', OPERATOR => $oper, VALUE => $ISODate)
2047
2048 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
2049
2050 OPERATOR is one of > or <
2051 VALUE is a date and time in ISO format in GMT
2052 FIELD is one of Starts, Started, Told, Created, Resolved, LastUpdated
2053
2054 There are also helper functions of the form LimitFIELD that eliminate
2055 the need to pass in a FIELD argument.
2056
2057 =cut
2058
2059 sub LimitDate {
2060     my $self = shift;
2061     my %args = (
2062         FIELD    => undef,
2063         VALUE    => undef,
2064         OPERATOR => undef,
2065
2066         @_
2067     );
2068
2069     #Set the description if we didn't get handed it above
2070     unless ( $args{'DESCRIPTION'} ) {
2071         $args{'DESCRIPTION'} = $args{'FIELD'} . " "
2072             . $args{'OPERATOR'} . " "
2073             . $args{'VALUE'} . " GMT";
2074     }
2075
2076     $self->LimitField(%args);
2077
2078 }
2079
2080
2081 sub LimitCreated {
2082     my $self = shift;
2083     $self->LimitDate( FIELD => 'Created', @_ );
2084 }
2085
2086 sub LimitDue {
2087     my $self = shift;
2088     $self->LimitDate( FIELD => 'Due', @_ );
2089
2090 }
2091
2092 sub LimitStarts {
2093     my $self = shift;
2094     $self->LimitDate( FIELD => 'Starts', @_ );
2095
2096 }
2097
2098 sub LimitStarted {
2099     my $self = shift;
2100     $self->LimitDate( FIELD => 'Started', @_ );
2101 }
2102
2103 sub LimitResolved {
2104     my $self = shift;
2105     $self->LimitDate( FIELD => 'Resolved', @_ );
2106 }
2107
2108 sub LimitTold {
2109     my $self = shift;
2110     $self->LimitDate( FIELD => 'Told', @_ );
2111 }
2112
2113 sub LimitLastUpdated {
2114     my $self = shift;
2115     $self->LimitDate( FIELD => 'LastUpdated', @_ );
2116 }
2117
2118 #
2119
2120 =head2 LimitTransactionDate (OPERATOR => $oper, VALUE => $ISODate)
2121
2122 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
2123
2124 OPERATOR is one of > or <
2125 VALUE is a date and time in ISO format in GMT
2126
2127
2128 =cut
2129
2130 sub LimitTransactionDate {
2131     my $self = shift;
2132     my %args = (
2133         FIELD    => 'TransactionDate',
2134         VALUE    => undef,
2135         OPERATOR => undef,
2136
2137         @_
2138     );
2139
2140     #  <20021217042756.GK28744@pallas.fsck.com>
2141     #    "Kill It" - Jesse.
2142
2143     #Set the description if we didn't get handed it above
2144     unless ( $args{'DESCRIPTION'} ) {
2145         $args{'DESCRIPTION'} = $args{'FIELD'} . " "
2146             . $args{'OPERATOR'} . " "
2147             . $args{'VALUE'} . " GMT";
2148     }
2149
2150     $self->LimitField(%args);
2151
2152 }
2153
2154
2155
2156
2157 =head2 LimitCustomField
2158
2159 Takes a paramhash of key/value pairs with the following keys:
2160
2161 =over 4
2162
2163 =item CUSTOMFIELD - CustomField name or id.  If a name is passed, an additional parameter QUEUE may also be passed to distinguish the custom field.
2164
2165 =item OPERATOR - The usual Limit operators
2166
2167 =item VALUE - The value to compare against
2168
2169 =back
2170
2171 =cut
2172
2173 sub LimitCustomField {
2174     my $self = shift;
2175     my %args = (
2176         VALUE       => undef,
2177         CUSTOMFIELD => undef,
2178         OPERATOR    => '=',
2179         DESCRIPTION => undef,
2180         FIELD       => 'CustomFieldValue',
2181         QUOTEVALUE  => 1,
2182         @_
2183     );
2184
2185     my $CF = RT::CustomField->new( $self->CurrentUser );
2186     if ( $args{CUSTOMFIELD} =~ /^\d+$/ ) {
2187         $CF->Load( $args{CUSTOMFIELD} );
2188     }
2189     else {
2190         $CF->LoadByNameAndQueue(
2191             Name  => $args{CUSTOMFIELD},
2192             Queue => $args{QUEUE}
2193         );
2194         $args{CUSTOMFIELD} = $CF->Id;
2195     }
2196
2197     #If we are looking to compare with a null value.
2198     if ( $args{'OPERATOR'} =~ /^is$/i ) {
2199         $args{'DESCRIPTION'}
2200             ||= $self->loc( "Custom field [_1] has no value.", $CF->Name );
2201     }
2202     elsif ( $args{'OPERATOR'} =~ /^is not$/i ) {
2203         $args{'DESCRIPTION'}
2204             ||= $self->loc( "Custom field [_1] has a value.", $CF->Name );
2205     }
2206
2207     # if we're not looking to compare with a null value
2208     else {
2209         $args{'DESCRIPTION'} ||= $self->loc( "Custom field [_1] [_2] [_3]",
2210             $CF->Name, $args{OPERATOR}, $args{VALUE} );
2211     }
2212
2213     if ( defined $args{'QUEUE'} && $args{'QUEUE'} =~ /\D/ ) {
2214         my $QueueObj = RT::Queue->new( $self->CurrentUser );
2215         $QueueObj->Load( $args{'QUEUE'} );
2216         $args{'QUEUE'} = $QueueObj->Id;
2217     }
2218     delete $args{'QUEUE'} unless defined $args{'QUEUE'} && length $args{'QUEUE'};
2219
2220     my @rest;
2221     @rest = ( ENTRYAGGREGATOR => 'AND' )
2222         if ( $CF->Type eq 'SelectMultiple' );
2223
2224     $self->LimitField(
2225         VALUE => $args{VALUE},
2226         FIELD => "CF"
2227             .(defined $args{'QUEUE'}? ".$args{'QUEUE'}" : '' )
2228             .".{" . $CF->Name . "}",
2229         OPERATOR    => $args{OPERATOR},
2230         CUSTOMFIELD => 1,
2231         @rest,
2232     );
2233
2234     $self->{'RecalcTicketLimits'} = 1;
2235 }
2236
2237
2238
2239 =head2 _NextIndex
2240
2241 Keep track of the counter for the array of restrictions
2242
2243 =cut
2244
2245 sub _NextIndex {
2246     my $self = shift;
2247     return ( $self->{'restriction_index'}++ );
2248 }
2249
2250
2251
2252
2253 sub _Init {
2254     my $self = shift;
2255     $self->{'table'}                   = "Tickets";
2256     $self->{'RecalcTicketLimits'}      = 1;
2257     $self->{'restriction_index'}       = 1;
2258     $self->{'primary_key'}             = "id";
2259     delete $self->{'items_array'};
2260     delete $self->{'item_map'};
2261     delete $self->{'columns_to_display'};
2262     $self->SUPER::_Init(@_);
2263
2264     $self->_InitSQL();
2265 }
2266
2267 sub _InitSQL {
2268     my $self = shift;
2269     # Private Member Variables (which should get cleaned)
2270     $self->{'_sql_transalias'}    = undef;
2271     $self->{'_sql_trattachalias'} = undef;
2272     $self->{'_sql_cf_alias'}  = undef;
2273     $self->{'_sql_object_cfv_alias'}  = undef;
2274     $self->{'_sql_watcher_join_users_alias'} = undef;
2275     $self->{'_sql_query'}         = '';
2276     $self->{'_sql_looking_at'}    = {};
2277 }
2278
2279
2280 sub Count {
2281     my $self = shift;
2282     $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2283     return ( $self->SUPER::Count() );
2284 }
2285
2286
2287 sub CountAll {
2288     my $self = shift;
2289     $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2290     return ( $self->SUPER::CountAll() );
2291 }
2292
2293
2294
2295 =head2 ItemsArrayRef
2296
2297 Returns a reference to the set of all items found in this search
2298
2299 =cut
2300
2301 sub ItemsArrayRef {
2302     my $self = shift;
2303
2304     return $self->{'items_array'} if $self->{'items_array'};
2305
2306     my $placeholder = $self->_ItemsCounter;
2307     $self->GotoFirstItem();
2308     while ( my $item = $self->Next ) {
2309         push( @{ $self->{'items_array'} }, $item );
2310     }
2311     $self->GotoItem($placeholder);
2312     $self->{'items_array'}
2313         = $self->ItemsOrderBy( $self->{'items_array'} );
2314
2315     return $self->{'items_array'};
2316 }
2317
2318 sub ItemsArrayRefWindow {
2319     my $self = shift;
2320     my $window = shift;
2321
2322     my @old = ($self->_ItemsCounter, $self->RowsPerPage, $self->FirstRow+1);
2323
2324     $self->RowsPerPage( $window );
2325     $self->FirstRow(1);
2326     $self->GotoFirstItem;
2327
2328     my @res;
2329     while ( my $item = $self->Next ) {
2330         push @res, $item;
2331     }
2332
2333     $self->RowsPerPage( $old[1] );
2334     $self->FirstRow( $old[2] );
2335     $self->GotoItem( $old[0] );
2336
2337     return \@res;
2338 }
2339
2340
2341 sub Next {
2342     my $self = shift;
2343
2344     $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2345
2346     my $Ticket = $self->SUPER::Next;
2347     return $Ticket unless $Ticket;
2348
2349     if ( $Ticket->__Value('Status') eq 'deleted'
2350         && !$self->{'allow_deleted_search'} )
2351     {
2352         return $self->Next;
2353     }
2354     elsif ( RT->Config->Get('UseSQLForACLChecks') ) {
2355         # if we found a ticket with this option enabled then
2356         # all tickets we found are ACLed, cache this fact
2357         my $key = join ";:;", $self->CurrentUser->id, 'ShowTicket', 'RT::Ticket-'. $Ticket->id;
2358         $RT::Principal::_ACL_CACHE->{ $key } = 1;
2359         return $Ticket;
2360     }
2361     elsif ( $Ticket->CurrentUserHasRight('ShowTicket') ) {
2362         # has rights
2363         return $Ticket;
2364     }
2365     else {
2366         # If the user doesn't have the right to show this ticket
2367         return $self->Next;
2368     }
2369 }
2370
2371 sub _DoSearch {
2372     my $self = shift;
2373     $self->CurrentUserCanSee if RT->Config->Get('UseSQLForACLChecks');
2374     return $self->SUPER::_DoSearch( @_ );
2375 }
2376
2377 sub _DoCount {
2378     my $self = shift;
2379     $self->CurrentUserCanSee if RT->Config->Get('UseSQLForACLChecks');
2380     return $self->SUPER::_DoCount( @_ );
2381 }
2382
2383 sub _RolesCanSee {
2384     my $self = shift;
2385
2386     my $cache_key = 'RolesHasRight;:;ShowTicket';
2387  
2388     if ( my $cached = $RT::Principal::_ACL_CACHE->{ $cache_key } ) {
2389         return %$cached;
2390     }
2391
2392     my $ACL = RT::ACL->new( RT->SystemUser );
2393     $ACL->Limit( FIELD => 'RightName', VALUE => 'ShowTicket' );
2394     $ACL->Limit( FIELD => 'PrincipalType', OPERATOR => '!=', VALUE => 'Group' );
2395     my $principal_alias = $ACL->Join(
2396         ALIAS1 => 'main',
2397         FIELD1 => 'PrincipalId',
2398         TABLE2 => 'Principals',
2399         FIELD2 => 'id',
2400     );
2401     $ACL->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
2402
2403     my %res = ();
2404     foreach my $ACE ( @{ $ACL->ItemsArrayRef } ) {
2405         my $role = $ACE->__Value('PrincipalType');
2406         my $type = $ACE->__Value('ObjectType');
2407         if ( $type eq 'RT::System' ) {
2408             $res{ $role } = 1;
2409         }
2410         elsif ( $type eq 'RT::Queue' ) {
2411             next if $res{ $role } && !ref $res{ $role };
2412             push @{ $res{ $role } ||= [] }, $ACE->__Value('ObjectId');
2413         }
2414         else {
2415             $RT::Logger->error('ShowTicket right is granted on unsupported object');
2416         }
2417     }
2418     $RT::Principal::_ACL_CACHE->{ $cache_key } = \%res;
2419     return %res;
2420 }
2421
2422 sub _DirectlyCanSeeIn {
2423     my $self = shift;
2424     my $id = $self->CurrentUser->id;
2425
2426     my $cache_key = 'User-'. $id .';:;ShowTicket;:;DirectlyCanSeeIn';
2427     if ( my $cached = $RT::Principal::_ACL_CACHE->{ $cache_key } ) {
2428         return @$cached;
2429     }
2430
2431     my $ACL = RT::ACL->new( RT->SystemUser );
2432     $ACL->Limit( FIELD => 'RightName', VALUE => 'ShowTicket' );
2433     my $principal_alias = $ACL->Join(
2434         ALIAS1 => 'main',
2435         FIELD1 => 'PrincipalId',
2436         TABLE2 => 'Principals',
2437         FIELD2 => 'id',
2438     );
2439     $ACL->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
2440     my $cgm_alias = $ACL->Join(
2441         ALIAS1 => 'main',
2442         FIELD1 => 'PrincipalId',
2443         TABLE2 => 'CachedGroupMembers',
2444         FIELD2 => 'GroupId',
2445     );
2446     $ACL->Limit( ALIAS => $cgm_alias, FIELD => 'MemberId', VALUE => $id );
2447     $ACL->Limit( ALIAS => $cgm_alias, FIELD => 'Disabled', VALUE => 0 );
2448
2449     my @res = ();
2450     foreach my $ACE ( @{ $ACL->ItemsArrayRef } ) {
2451         my $type = $ACE->__Value('ObjectType');
2452         if ( $type eq 'RT::System' ) {
2453             # If user is direct member of a group that has the right
2454             # on the system then he can see any ticket
2455             $RT::Principal::_ACL_CACHE->{ $cache_key } = [-1];
2456             return (-1);
2457         }
2458         elsif ( $type eq 'RT::Queue' ) {
2459             push @res, $ACE->__Value('ObjectId');
2460         }
2461         else {
2462             $RT::Logger->error('ShowTicket right is granted on unsupported object');
2463         }
2464     }
2465     $RT::Principal::_ACL_CACHE->{ $cache_key } = \@res;
2466     return @res;
2467 }
2468
2469 sub CurrentUserCanSee {
2470     my $self = shift;
2471     return if $self->{'_sql_current_user_can_see_applied'};
2472
2473     return $self->{'_sql_current_user_can_see_applied'} = 1
2474         if $self->CurrentUser->UserObj->HasRight(
2475             Right => 'SuperUser', Object => $RT::System
2476         );
2477
2478     local $self->{using_restrictions};
2479
2480     my $id = $self->CurrentUser->id;
2481
2482     # directly can see in all queues then we have nothing to do
2483     my @direct_queues = $self->_DirectlyCanSeeIn;
2484     return $self->{'_sql_current_user_can_see_applied'} = 1
2485         if @direct_queues && $direct_queues[0] == -1;
2486
2487     my %roles = $self->_RolesCanSee;
2488     {
2489         my %skip = map { $_ => 1 } @direct_queues;
2490         foreach my $role ( keys %roles ) {
2491             next unless ref $roles{ $role };
2492
2493             my @queues = grep !$skip{$_}, @{ $roles{ $role } };
2494             if ( @queues ) {
2495                 $roles{ $role } = \@queues;
2496             } else {
2497                 delete $roles{ $role };
2498             }
2499         }
2500     }
2501
2502 # there is no global watchers, only queues and tickes, if at
2503 # some point we will add global roles then it's gonna blow
2504 # the idea here is that if the right is set globaly for a role
2505 # and user plays this role for a queue directly not a ticket
2506 # then we have to check in advance
2507     if ( my @tmp = grep $_ ne 'Owner' && !ref $roles{ $_ }, keys %roles ) {
2508
2509         my $groups = RT::Groups->new( RT->SystemUser );
2510         $groups->Limit( FIELD => 'Domain', VALUE => 'RT::Queue-Role', CASESENSITIVE => 0 );
2511         foreach ( @tmp ) {
2512             $groups->Limit( FIELD => 'Name', VALUE => $_, CASESENSITIVE => 0 );
2513         }
2514         my $principal_alias = $groups->Join(
2515             ALIAS1 => 'main',
2516             FIELD1 => 'id',
2517             TABLE2 => 'Principals',
2518             FIELD2 => 'id',
2519         );
2520         $groups->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
2521         my $cgm_alias = $groups->Join(
2522             ALIAS1 => 'main',
2523             FIELD1 => 'id',
2524             TABLE2 => 'CachedGroupMembers',
2525             FIELD2 => 'GroupId',
2526         );
2527         $groups->Limit( ALIAS => $cgm_alias, FIELD => 'MemberId', VALUE => $id );
2528         $groups->Limit( ALIAS => $cgm_alias, FIELD => 'Disabled', VALUE => 0 );
2529         while ( my $group = $groups->Next ) {
2530             push @direct_queues, $group->Instance;
2531         }
2532     }
2533
2534     unless ( @direct_queues || keys %roles ) {
2535         $self->Limit(
2536             SUBCLAUSE => 'ACL',
2537             ALIAS => 'main',
2538             FIELD => 'id',
2539             VALUE => 0,
2540             ENTRYAGGREGATOR => 'AND',
2541         );
2542         return $self->{'_sql_current_user_can_see_applied'} = 1;
2543     }
2544
2545     {
2546         my $join_roles = keys %roles;
2547         $join_roles = 0 if $join_roles == 1 && $roles{'Owner'};
2548         my ($role_group_alias, $cgm_alias);
2549         if ( $join_roles ) {
2550             $role_group_alias = $self->_RoleGroupsJoin( New => 1 );
2551             $cgm_alias = $self->_GroupMembersJoin( GroupsAlias => $role_group_alias );
2552             $self->Limit(
2553                 LEFTJOIN   => $cgm_alias,
2554                 FIELD      => 'MemberId',
2555                 OPERATOR   => '=',
2556                 VALUE      => $id,
2557             );
2558         }
2559         my $limit_queues = sub {
2560             my $ea = shift;
2561             my @queues = @_;
2562
2563             return unless @queues;
2564             if ( @queues == 1 ) {
2565                 $self->Limit(
2566                     SUBCLAUSE => 'ACL',
2567                     ALIAS => 'main',
2568                     FIELD => 'Queue',
2569                     VALUE => $_[0],
2570                     ENTRYAGGREGATOR => $ea,
2571                 );
2572             } else {
2573                 $self->SUPER::_OpenParen('ACL');
2574                 foreach my $q ( @queues ) {
2575                     $self->Limit(
2576                         SUBCLAUSE => 'ACL',
2577                         ALIAS => 'main',
2578                         FIELD => 'Queue',
2579                         VALUE => $q,
2580                         ENTRYAGGREGATOR => $ea,
2581                     );
2582                     $ea = 'OR';
2583                 }
2584                 $self->SUPER::_CloseParen('ACL');
2585             }
2586             return 1;
2587         };
2588
2589         $self->SUPER::_OpenParen('ACL');
2590         my $ea = 'AND';
2591         $ea = 'OR' if $limit_queues->( $ea, @direct_queues );
2592         while ( my ($role, $queues) = each %roles ) {
2593             $self->SUPER::_OpenParen('ACL');
2594             if ( $role eq 'Owner' ) {
2595                 $self->Limit(
2596                     SUBCLAUSE => 'ACL',
2597                     FIELD           => 'Owner',
2598                     VALUE           => $id,
2599                     ENTRYAGGREGATOR => $ea,
2600                 );
2601             }
2602             else {
2603                 $self->Limit(
2604                     SUBCLAUSE       => 'ACL',
2605                     ALIAS           => $cgm_alias,
2606                     FIELD           => 'MemberId',
2607                     OPERATOR        => 'IS NOT',
2608                     VALUE           => 'NULL',
2609                     QUOTEVALUE      => 0,
2610                     ENTRYAGGREGATOR => $ea,
2611                 );
2612                 $self->Limit(
2613                     SUBCLAUSE       => 'ACL',
2614                     ALIAS           => $role_group_alias,
2615                     FIELD           => 'Name',
2616                     VALUE           => $role,
2617                     ENTRYAGGREGATOR => 'AND',
2618                     CASESENSITIVE   => 0,
2619                 );
2620             }
2621             $limit_queues->( 'AND', @$queues ) if ref $queues;
2622             $ea = 'OR' if $ea eq 'AND';
2623             $self->SUPER::_CloseParen('ACL');
2624         }
2625         $self->SUPER::_CloseParen('ACL');
2626     }
2627     return $self->{'_sql_current_user_can_see_applied'} = 1;
2628 }
2629
2630
2631
2632 =head2 ClearRestrictions
2633
2634 Removes all restrictions irretrievably
2635
2636 =cut
2637
2638 sub ClearRestrictions {
2639     my $self = shift;
2640     delete $self->{'TicketRestrictions'};
2641     $self->{_sql_looking_at} = {};
2642     $self->{'RecalcTicketLimits'}      = 1;
2643 }
2644
2645 # Convert a set of oldstyle SB Restrictions to Clauses for RQL
2646
2647 sub _RestrictionsToClauses {
2648     my $self = shift;
2649
2650     my %clause;
2651     foreach my $row ( keys %{ $self->{'TicketRestrictions'} } ) {
2652         my $restriction = $self->{'TicketRestrictions'}{$row};
2653
2654         # We need to reimplement the subclause aggregation that SearchBuilder does.
2655         # Default Subclause is ALIAS.FIELD, and default ALIAS is 'main',
2656         # Then SB AND's the different Subclauses together.
2657
2658         # So, we want to group things into Subclauses, convert them to
2659         # SQL, and then join them with the appropriate DefaultEA.
2660         # Then join each subclause group with AND.
2661
2662         my $field = $restriction->{'FIELD'};
2663         my $realfield = $field;    # CustomFields fake up a fieldname, so
2664                                    # we need to figure that out
2665
2666         # One special case
2667         # Rewrite LinkedTo meta field to the real field
2668         if ( $field =~ /LinkedTo/ ) {
2669             $realfield = $field = $restriction->{'TYPE'};
2670         }
2671
2672         # Two special case
2673         # Handle subkey fields with a different real field
2674         if ( $field =~ /^(\w+)\./ ) {
2675             $realfield = $1;
2676         }
2677
2678         die "I don't know about $field yet"
2679             unless ( exists $FIELD_METADATA{$realfield}
2680                 or $restriction->{CUSTOMFIELD} );
2681
2682         my $type = $FIELD_METADATA{$realfield}->[0];
2683         my $op   = $restriction->{'OPERATOR'};
2684
2685         my $value = (
2686             grep    {defined}
2687                 map { $restriction->{$_} } qw(VALUE TICKET BASE TARGET)
2688         )[0];
2689
2690         # this performs the moral equivalent of defined or/dor/C<//>,
2691         # without the short circuiting.You need to use a 'defined or'
2692         # type thing instead of just checking for truth values, because
2693         # VALUE could be 0.(i.e. "false")
2694
2695         # You could also use this, but I find it less aesthetic:
2696         # (although it does short circuit)
2697         #( defined $restriction->{'VALUE'}? $restriction->{VALUE} :
2698         # defined $restriction->{'TICKET'} ?
2699         # $restriction->{TICKET} :
2700         # defined $restriction->{'BASE'} ?
2701         # $restriction->{BASE} :
2702         # defined $restriction->{'TARGET'} ?
2703         # $restriction->{TARGET} )
2704
2705         my $ea = $restriction->{ENTRYAGGREGATOR}
2706             || $DefaultEA{$type}
2707             || "AND";
2708         if ( ref $ea ) {
2709             die "Invalid operator $op for $field ($type)"
2710                 unless exists $ea->{$op};
2711             $ea = $ea->{$op};
2712         }
2713
2714         # Each CustomField should be put into a different Clause so they
2715         # are ANDed together.
2716         if ( $restriction->{CUSTOMFIELD} ) {
2717             $realfield = $field;
2718         }
2719
2720         exists $clause{$realfield} or $clause{$realfield} = [];
2721
2722         # Escape Quotes
2723         $field =~ s!(['\\])!\\$1!g;
2724         $value =~ s!(['\\])!\\$1!g;
2725         my $data = [ $ea, $type, $field, $op, $value ];
2726
2727         # here is where we store extra data, say if it's a keyword or
2728         # something.  (I.e. "TYPE SPECIFIC STUFF")
2729
2730         if (lc $ea eq 'none') {
2731             $clause{$realfield} = [ $data ];
2732         } else {
2733             push @{ $clause{$realfield} }, $data;
2734         }
2735     }
2736     return \%clause;
2737 }
2738
2739 =head2 ClausesToSQL
2740
2741 =cut
2742
2743 sub ClausesToSQL {
2744   my $self = shift;
2745   my $clauses = shift;
2746   my @sql;
2747
2748   for my $f (keys %{$clauses}) {
2749     my $sql;
2750     my $first = 1;
2751
2752     # Build SQL from the data hash
2753     for my $data ( @{ $clauses->{$f} } ) {
2754       $sql .= $data->[0] unless $first; $first=0; # ENTRYAGGREGATOR
2755       $sql .= " '". $data->[2] . "' ";            # FIELD
2756       $sql .= $data->[3] . " ";                   # OPERATOR
2757       $sql .= "'". $data->[4] . "' ";             # VALUE
2758     }
2759
2760     push @sql, " ( " . $sql . " ) ";
2761   }
2762
2763   return join("AND",@sql);
2764 }
2765
2766 sub _ProcessRestrictions {
2767     my $self = shift;
2768
2769     delete $self->{'items_array'};
2770     delete $self->{'item_map'};
2771     delete $self->{'raw_rows'};
2772     delete $self->{'count_all'};
2773
2774     my $sql = $self->Query;
2775     if ( !$sql || $self->{'RecalcTicketLimits'} ) {
2776
2777         local $self->{using_restrictions};
2778         #  "Restrictions to Clauses Branch\n";
2779         my $clauseRef = eval { $self->_RestrictionsToClauses; };
2780         if ($@) {
2781             $RT::Logger->error( "RestrictionsToClauses: " . $@ );
2782             $self->FromSQL("");
2783         }
2784         else {
2785             $sql = $self->ClausesToSQL($clauseRef);
2786             $self->FromSQL($sql) if $sql;
2787         }
2788     }
2789
2790     $self->{'RecalcTicketLimits'} = 0;
2791
2792 }
2793
2794 =head2 _BuildItemMap
2795
2796 Build up a L</ItemMap> of first/last/next/prev items, so that we can
2797 display search nav quickly.
2798
2799 =cut
2800
2801 sub _BuildItemMap {
2802     my $self = shift;
2803
2804     my $window = RT->Config->Get('TicketsItemMapSize');
2805
2806     $self->{'item_map'} = {};
2807
2808     my $items = $self->ItemsArrayRefWindow( $window );
2809     return unless $items && @$items;
2810
2811     my $prev = 0;
2812     $self->{'item_map'}{'first'} = $items->[0]->EffectiveId;
2813     for ( my $i = 0; $i < @$items; $i++ ) {
2814         my $item = $items->[$i];
2815         my $id = $item->EffectiveId;
2816         $self->{'item_map'}{$id}{'defined'} = 1;
2817         $self->{'item_map'}{$id}{'prev'}    = $prev;
2818         $self->{'item_map'}{$id}{'next'}    = $items->[$i+1]->EffectiveId
2819             if $items->[$i+1];
2820         $prev = $id;
2821     }
2822     $self->{'item_map'}{'last'} = $prev
2823         if !$window || @$items < $window;
2824 }
2825
2826 =head2 ItemMap
2827
2828 Returns an a map of all items found by this search. The map is a hash
2829 of the form:
2830
2831     {
2832         first => <first ticket id found>,
2833         last => <last ticket id found or undef>,
2834
2835         <ticket id> => {
2836             prev => <the ticket id found before>,
2837             next => <the ticket id found after>,
2838         },
2839         <ticket id> => {
2840             prev => ...,
2841             next => ...,
2842         },
2843     }
2844
2845 =cut
2846
2847 sub ItemMap {
2848     my $self = shift;
2849     $self->_BuildItemMap unless $self->{'item_map'};
2850     return $self->{'item_map'};
2851 }
2852
2853
2854
2855
2856 =head2 PrepForSerialization
2857
2858 You don't want to serialize a big tickets object, as
2859 the {items} hash will be instantly invalid _and_ eat
2860 lots of space
2861
2862 =cut
2863
2864 sub PrepForSerialization {
2865     my $self = shift;
2866     delete $self->{'items'};
2867     delete $self->{'items_array'};
2868     $self->RedoSearch();
2869 }
2870
2871 =head1 FLAGS
2872
2873 RT::Tickets supports several flags which alter search behavior:
2874
2875
2876 allow_deleted_search  (Otherwise never show deleted tickets in search results)
2877
2878 These flags are set by calling 
2879
2880 $tickets->{'flagname'} = 1;
2881
2882 BUG: There should be an API for this
2883
2884
2885
2886 =cut
2887
2888 =head2 FromSQL
2889
2890 Convert a RT-SQL string into a set of SearchBuilder restrictions.
2891
2892 Returns (1, 'Status message') on success and (0, 'Error Message') on
2893 failure.
2894
2895 =cut
2896
2897 sub _parser {
2898     my ($self,$string) = @_;
2899     my $ea = '';
2900
2901     # Bundling of joins is implemented by dynamically tracking a parallel query
2902     # tree in %sub_tree as the TicketSQL is parsed.
2903     #
2904     # Only positive, OR'd watcher conditions are bundled currently.  Each key
2905     # in %sub_tree is a watcher type (Requestor, Cc, AdminCc) or the generic
2906     # "Watcher" for any watcher type.  Owner is not bundled because it is
2907     # denormalized into a Tickets column and doesn't need a join.  AND'd
2908     # conditions are not bundled since a record may have multiple watchers
2909     # which independently match the conditions, thus necessitating two joins.
2910     #
2911     # The values of %sub_tree are arrayrefs made up of:
2912     #
2913     #   * Open parentheses "(" pushed on by the OpenParen callback
2914     #   * Arrayrefs of bundled join aliases pushed on by the Condition callback
2915     #   * Entry aggregators (AND/OR) pushed on by the EntryAggregator callback
2916     #
2917     # The CloseParen callback takes care of backing off the query trees until
2918     # outside of the just-closed parenthetical, thus restoring the tree state
2919     # an equivalent of before the parenthetical was entered.
2920     #
2921     # The Condition callback handles starting a new subtree or extending an
2922     # existing one, determining if bundling the current condition with any
2923     # subtree is possible, and pruning any dangling entry aggregators from
2924     # trees.
2925     #
2926
2927     my %sub_tree;
2928     my $depth = 0;
2929
2930     my %callback;
2931     $callback{'OpenParen'} = sub {
2932       $self->_OpenParen;
2933       $depth++;
2934       push @$_, '(' foreach values %sub_tree;
2935     };
2936     $callback{'CloseParen'} = sub {
2937       $self->_CloseParen;
2938       $depth--;
2939       foreach my $list ( values %sub_tree ) {
2940           if ( $list->[-1] eq '(' ) {
2941               pop @$list;
2942               pop @$list if $list->[-1] =~ /^(?:AND|OR)$/i;
2943           }
2944           else {
2945               pop @$list while $list->[-2] ne '(';
2946               $list->[-1] = pop @$list;
2947           }
2948       }
2949     };
2950     $callback{'EntryAggregator'} = sub {
2951       $ea = $_[0] || '';
2952       push @$_, $ea foreach grep @$_ && $_->[-1] ne '(', values %sub_tree;
2953     };
2954     $callback{'Condition'} = sub {
2955         my ($key, $op, $value) = @_;
2956
2957         my $negative_op = ($op eq '!=' || $op =~ /\bNOT\b/i);
2958         my $null_op = ( 'is not' eq lc($op) || 'is' eq lc($op) );
2959         # key has dot then it's compound variant and we have subkey
2960         my $subkey = '';
2961         ($key, $subkey) = ($1, $2) if $key =~ /^([^\.]+)\.(.+)$/;
2962
2963         # normalize key and get class (type)
2964         my $class;
2965         if (exists $LOWER_CASE_FIELDS{lc $key}) {
2966             $key = $LOWER_CASE_FIELDS{lc $key};
2967             $class = $FIELD_METADATA{$key}->[0];
2968         }
2969         die "Unknown field '$key' in '$string'" unless $class;
2970
2971         # replace __CurrentUser__ with id
2972         $value = $self->CurrentUser->id if $value eq '__CurrentUser__';
2973
2974
2975         unless( $dispatch{ $class } ) {
2976             die "No dispatch method for class '$class'"
2977         }
2978         my $sub = $dispatch{ $class };
2979
2980         my @res; my $bundle_with;
2981         if ( $class eq 'WATCHERFIELD' && $key ne 'Owner' && !$negative_op && (!$null_op || $subkey) ) {
2982             if ( !$sub_tree{$key} ) {
2983               $sub_tree{$key} = [ ('(')x$depth, \@res ];
2984             } else {
2985               $bundle_with = $self->_check_bundling_possibility( $string, @{ $sub_tree{$key} } );
2986               if ( $sub_tree{$key}[-1] eq '(' ) {
2987                     push @{ $sub_tree{$key} }, \@res;
2988               }
2989             }
2990         }
2991
2992         # Remove our aggregator from subtrees where our condition didn't get added
2993         pop @$_ foreach grep @$_ && $_->[-1] =~ /^(?:AND|OR)$/i, values %sub_tree;
2994
2995         # A reference to @res may be pushed onto $sub_tree{$key} from
2996         # above, and we fill it here.
2997         @res = $sub->( $self, $key, $op, $value,
2998                 SUBCLAUSE       => '',  # don't need anymore
2999                 ENTRYAGGREGATOR => $ea,
3000                 SUBKEY          => $subkey,
3001                 BUNDLE          => $bundle_with,
3002               );
3003         $ea = '';
3004     };
3005     RT::SQL::Parse($string, \%callback);
3006 }
3007
3008 sub FromSQL {
3009     my ($self,$query) = @_;
3010
3011     {
3012         # preserve first_row and show_rows across the CleanSlate
3013         local ($self->{'first_row'}, $self->{'show_rows'}, $self->{_sql_looking_at});
3014         $self->CleanSlate;
3015         $self->_InitSQL();
3016     }
3017
3018     return (1, $self->loc("No Query")) unless $query;
3019
3020     $self->{_sql_query} = $query;
3021     eval {
3022         local $self->{parsing_ticketsql} = 1;
3023         $self->_parser( $query );
3024     };
3025     if ( $@ ) {
3026         my $error = "$@";
3027         $RT::Logger->error("Couldn't parse query: $error");
3028         return (0, $error);
3029     }
3030
3031     # We only want to look at EffectiveId's (mostly) for these searches.
3032     unless ( $self->{_sql_looking_at}{effectiveid} ) {
3033         # instead of EffectiveId = id we do IsMerged IS NULL
3034         $self->Limit(
3035             FIELD           => 'IsMerged',
3036             OPERATOR        => 'IS',
3037             VALUE           => 'NULL',
3038             ENTRYAGGREGATOR => 'AND',
3039             QUOTEVALUE      => 0,
3040         );
3041     }
3042     unless ( $self->{_sql_looking_at}{type} ) {
3043         $self->Limit( FIELD => 'Type', VALUE => 'ticket' );
3044     }
3045
3046     # We don't want deleted tickets unless 'allow_deleted_search' is set
3047     unless( $self->{'allow_deleted_search'} ) {
3048         $self->Limit(
3049             FIELD    => 'Status',
3050             OPERATOR => '!=',
3051             VALUE => 'deleted',
3052         );
3053     }
3054
3055     # set SB's dirty flag
3056     $self->{'must_redo_search'} = 1;
3057     $self->{'RecalcTicketLimits'} = 0;
3058
3059     return (1, $self->loc("Valid Query"));
3060 }
3061
3062 =head2 Query
3063
3064 Returns the last string passed to L</FromSQL>.
3065
3066 =cut
3067
3068 sub Query {
3069     my $self = shift;
3070     return $self->{_sql_query};
3071 }
3072
3073 sub _check_bundling_possibility {
3074     my $self = shift;
3075     my $string = shift;
3076     my @list = reverse @_;
3077     while (my $e = shift @list) {
3078         next if $e eq '(';
3079         if ( lc($e) eq 'and' ) {
3080             return undef;
3081         }
3082         elsif ( lc($e) eq 'or' ) {
3083             return shift @list;
3084         }
3085         else {
3086             # should not happen
3087             $RT::Logger->error(
3088                 "Joins optimization failed when parsing '$string'. It's bug in RT, contact Best Practical"
3089             );
3090             die "Internal error. Contact your system administrator.";
3091         }
3092     }
3093     return undef;
3094 }
3095
3096 RT::Base->_ImportOverlays();
3097
3098 1;