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