ticket search for "this month", RT#11057
[freeside.git] / rt / lib / RT / Tickets_Overlay.pm
1 # BEGIN BPS TAGGED BLOCK {{{
2
3 # COPYRIGHT:
4
5 # This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC
6 #                                          <jesse@bestpractical.com>
7
8 # (Except where explicitly superseded by other copyright notices)
9
10
11 # LICENSE:
12
13 # This work is made available to you under the terms of Version 2 of
14 # the GNU General Public License. A copy of that license should have
15 # been provided with this software, but in any event can be snarfed
16 # from www.gnu.org.
17
18 # This work is distributed in the hope that it will be useful, but
19 # WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
21 # General Public License for more details.
22
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
26 # 02110-1301 or visit their web page on the internet at
27 # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
28
29
30 # CONTRIBUTION SUBMISSION POLICY:
31
32 # (The following paragraph is not intended to limit the rights granted
33 # to you to modify and distribute this software under the terms of
34 # the GNU General Public License and is only of importance to you if
35 # you choose to contribute your changes and enhancements to the
36 # community by submitting them to Best Practical Solutions, LLC.)
37
38 # By intentionally submitting any modifications, corrections or
39 # derivatives to this work, or any other work intended for use with
40 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
41 # you are the copyright holder for those contributions and you grant
42 # Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
43 # royalty-free, perpetual, license to use, copy, create derivative
44 # works based on those contributions, and sublicense and distribute
45 # those contributions and any derivatives thereof.
46
47 # END BPS TAGGED BLOCK }}}
48
49 # Major Changes:
50
51 # - Decimated ProcessRestrictions and broke it into multiple
52 # functions joined by a LUT
53 # - Semi-Generic SQL stuff moved to another file
54
55 # Known Issues: FIXME!
56
57 # - ClearRestrictions and Reinitialization is messy and unclear.  The
58 # only good way to do it is to create a new RT::Tickets object.
59
60 =head1 NAME
61
62   RT::Tickets - A collection of Ticket objects
63
64
65 =head1 SYNOPSIS
66
67   use RT::Tickets;
68   my $tickets = new RT::Tickets($CurrentUser);
69
70 =head1 DESCRIPTION
71
72    A collection of RT::Tickets.
73
74 =head1 METHODS
75
76
77 =cut
78
79 package RT::Tickets;
80
81 use strict;
82 no warnings qw(redefine);
83
84 use RT::CustomFields;
85 use DBIx::SearchBuilder::Unique;
86
87 # Configuration Tables:
88
89 # FIELD_METADATA is a mapping of searchable Field name, to Type, and other
90 # metadata.
91
92 our %FIELD_METADATA = (
93     Status          => [ 'ENUM', ], #loc_left_pair
94     Queue           => [ 'ENUM' => 'Queue', ], #loc_left_pair
95     Type            => [ 'ENUM', ], #loc_left_pair
96     Creator         => [ 'ENUM' => 'User', ], #loc_left_pair
97     LastUpdatedBy   => [ 'ENUM' => 'User', ], #loc_left_pair
98     Owner           => [ 'WATCHERFIELD' => 'Owner', ], #loc_left_pair
99     EffectiveId     => [ 'INT', ], #loc_left_pair
100     id              => [ 'ID', ], #loc_left_pair
101     InitialPriority => [ 'INT', ], #loc_left_pair
102     FinalPriority   => [ 'INT', ], #loc_left_pair
103     Priority        => [ 'INT', ], #loc_left_pair
104     TimeLeft        => [ 'INT', ], #loc_left_pair
105     TimeWorked      => [ 'INT', ], #loc_left_pair
106     TimeEstimated   => [ 'INT', ], #loc_left_pair
107
108     Linked          => [ 'LINK' ], #loc_left_pair
109     LinkedTo        => [ 'LINK' => 'To' ], #loc_left_pair
110     LinkedFrom      => [ 'LINK' => 'From' ], #loc_left_pair
111     MemberOf        => [ 'LINK' => To => 'MemberOf', ], #loc_left_pair
112     DependsOn       => [ 'LINK' => To => 'DependsOn', ], #loc_left_pair
113     RefersTo        => [ 'LINK' => To => 'RefersTo', ], #loc_left_pair
114     HasMember       => [ 'LINK' => From => 'MemberOf', ], #loc_left_pair
115     DependentOn     => [ 'LINK' => From => 'DependsOn', ], #loc_left_pair
116     DependedOnBy    => [ 'LINK' => From => 'DependsOn', ], #loc_left_pair
117     ReferredToBy    => [ 'LINK' => From => 'RefersTo', ], #loc_left_pair
118     Told             => [ 'DATE'            => 'Told', ], #loc_left_pair
119     Starts           => [ 'DATE'            => 'Starts', ], #loc_left_pair
120     Started          => [ 'DATE'            => 'Started', ], #loc_left_pair
121     Due              => [ 'DATE'            => 'Due', ], #loc_left_pair
122     Resolved         => [ 'DATE'            => 'Resolved', ], #loc_left_pair
123     LastUpdated      => [ 'DATE'            => 'LastUpdated', ], #loc_left_pair
124     Created          => [ 'DATE'            => 'Created', ], #loc_left_pair
125     Subject          => [ 'STRING', ], #loc_left_pair
126     Content          => [ 'TRANSFIELD', ], #loc_left_pair
127     ContentType      => [ 'TRANSFIELD', ], #loc_left_pair
128     Filename         => [ 'TRANSFIELD', ], #loc_left_pair
129     TransactionDate  => [ 'TRANSDATE', ], #loc_left_pair
130     Requestor        => [ 'WATCHERFIELD'    => 'Requestor', ], #loc_left_pair
131     Requestors       => [ 'WATCHERFIELD'    => 'Requestor', ], #loc_left_pair
132     Cc               => [ 'WATCHERFIELD'    => 'Cc', ], #loc_left_pair
133     AdminCc          => [ 'WATCHERFIELD'    => 'AdminCc', ], #loc_left_pair
134     Watcher          => [ 'WATCHERFIELD', ], #loc_left_pair
135     QueueCc          => [ 'WATCHERFIELD'    => 'Cc'      => 'Queue', ], #loc_left_pair
136     QueueAdminCc     => [ 'WATCHERFIELD'    => 'AdminCc' => 'Queue', ], #loc_left_pair
137     QueueWatcher     => [ 'WATCHERFIELD'    => undef     => 'Queue', ], #loc_left_pair
138     CustomFieldValue => [ 'CUSTOMFIELD', ], #loc_left_pair
139     CustomField      => [ 'CUSTOMFIELD', ], #loc_left_pair
140     CF               => [ 'CUSTOMFIELD', ], #loc_left_pair
141     Updated          => [ 'TRANSDATE', ], #loc_left_pair
142     RequestorGroup   => [ 'MEMBERSHIPFIELD' => 'Requestor', ], #loc_left_pair
143     CCGroup          => [ 'MEMBERSHIPFIELD' => 'Cc', ], #loc_left_pair
144     AdminCCGroup     => [ 'MEMBERSHIPFIELD' => 'AdminCc', ], #loc_left_pair
145     WatcherGroup     => [ 'MEMBERSHIPFIELD', ], #loc_left_pair
146     HasAttribute     => [ 'HASATTRIBUTE', 1 ],
147     HasNoAttribute     => [ 'HASATTRIBUTE', 0 ],
148     Agentnum         => [ 'FREESIDEFIELD', ],
149     Classnum         => [ 'FREESIDEFIELD', ],
150     Tagnum           => [ 'FREESIDEFIELD', 'cust_tag' ],
151 );
152
153 # Mapping of Field Type to Function
154 our %dispatch = (
155     ENUM            => \&_EnumLimit,
156     INT             => \&_IntLimit,
157     ID              => \&_IdLimit,
158     LINK            => \&_LinkLimit,
159     DATE            => \&_DateLimit,
160     STRING          => \&_StringLimit,
161     TRANSFIELD      => \&_TransLimit,
162     TRANSDATE       => \&_TransDateLimit,
163     WATCHERFIELD    => \&_WatcherLimit,
164     MEMBERSHIPFIELD => \&_WatcherMembershipLimit,
165     CUSTOMFIELD     => \&_CustomFieldLimit,
166     HASATTRIBUTE    => \&_HasAttributeLimit,
167     FREESIDEFIELD   => \&_FreesideFieldLimit,
168 );
169 our %can_bundle = ();# WATCHERFIELD => "yes", );
170
171 # Default EntryAggregator per type
172 # if you specify OP, you must specify all valid OPs
173 my %DefaultEA = (
174     INT  => 'AND',
175     ENUM => {
176         '='  => 'OR',
177         '!=' => 'AND'
178     },
179     DATE => {
180         '='  => 'OR',
181         '>=' => 'AND',
182         '<=' => 'AND',
183         '>'  => 'AND',
184         '<'  => 'AND'
185     },
186     STRING => {
187         '='        => 'OR',
188         '!='       => 'AND',
189         'LIKE'     => 'AND',
190         'NOT LIKE' => 'AND'
191     },
192     TRANSFIELD   => 'AND',
193     TRANSDATE    => 'AND',
194     LINK         => 'OR',
195     LINKFIELD    => 'AND',
196     TARGET       => 'AND',
197     BASE         => 'AND',
198     WATCHERFIELD => {
199         '='        => 'OR',
200         '!='       => 'AND',
201         'LIKE'     => 'OR',
202         'NOT LIKE' => 'AND'
203     },
204
205     HASATTRIBUTE => {
206         '='        => 'AND',
207         '!='       => 'AND',
208     },
209
210     CUSTOMFIELD => 'OR',
211 );
212
213 # Helper functions for passing the above lexically scoped tables above
214 # into Tickets_Overlay_SQL.
215 sub FIELDS     { return \%FIELD_METADATA }
216 sub dispatch   { return \%dispatch }
217 sub can_bundle { return \%can_bundle }
218
219 # Bring in the clowns.
220 require RT::Tickets_Overlay_SQL;
221
222 # {{{ sub SortFields
223
224 our @SORTFIELDS = qw(id Status
225     Queue Subject
226     Owner Created Due Starts Started
227     Told
228     Resolved LastUpdated Priority TimeWorked TimeLeft);
229
230 =head2 SortFields
231
232 Returns the list of fields that lists of tickets can easily be sorted by
233
234 =cut
235
236 sub SortFields {
237     my $self = shift;
238     return (@SORTFIELDS);
239 }
240
241 # }}}
242
243 # BEGIN SQL STUFF *********************************
244
245
246 sub CleanSlate {
247     my $self = shift;
248     $self->SUPER::CleanSlate( @_ );
249     delete $self->{$_} foreach qw(
250         _sql_cf_alias
251         _sql_group_members_aliases
252         _sql_object_cfv_alias
253         _sql_role_group_aliases
254         _sql_transalias
255         _sql_trattachalias
256         _sql_u_watchers_alias_for_sort
257         _sql_u_watchers_aliases
258         _sql_current_user_can_see_applied
259     );
260 }
261
262 =head1 Limit Helper Routines
263
264 These routines are the targets of a dispatch table depending on the
265 type of field.  They all share the same signature:
266
267   my ($self,$field,$op,$value,@rest) = @_;
268
269 The values in @rest should be suitable for passing directly to
270 DBIx::SearchBuilder::Limit.
271
272 Essentially they are an expanded/broken out (and much simplified)
273 version of what ProcessRestrictions used to do.  They're also much
274 more clearly delineated by the TYPE of field being processed.
275
276 =head2 _IdLimit
277
278 Handle ID field.
279
280 =cut
281
282 sub _IdLimit {
283     my ( $sb, $field, $op, $value, @rest ) = @_;
284
285     return $sb->_IntLimit( $field, $op, $value, @rest ) unless $value eq '__Bookmarked__';
286
287     die "Invalid operator $op for __Bookmarked__ search on $field"
288         unless $op =~ /^(=|!=)$/;
289
290     my @bookmarks = do {
291         my $tmp = $sb->CurrentUser->UserObj->FirstAttribute('Bookmarks');
292         $tmp = $tmp->Content if $tmp;
293         $tmp ||= {};
294         grep $_, keys %$tmp;
295     };
296
297     return $sb->_SQLLimit(
298         FIELD    => $field,
299         OPERATOR => $op,
300         VALUE    => 0,
301         @rest,
302     ) unless @bookmarks;
303
304     # as bookmarked tickets can be merged we have to use a join
305     # but it should be pretty lightweight
306     my $tickets_alias = $sb->Join(
307         TYPE   => 'LEFT',
308         ALIAS1 => 'main',
309         FIELD1 => 'id',
310         TABLE2 => 'Tickets',
311         FIELD2 => 'EffectiveId',
312     );
313     $sb->_OpenParen;
314     my $first = 1;
315     my $ea = $op eq '='? 'OR': 'AND';
316     foreach my $id ( sort @bookmarks ) {
317         $sb->_SQLLimit(
318             ALIAS    => $tickets_alias,
319             FIELD    => 'id',
320             OPERATOR => $op,
321             VALUE    => $id,
322             $first? (@rest): ( ENTRYAGGREGATOR => $ea )
323         );
324     }
325     $sb->_CloseParen;
326 }
327
328 =head2 _EnumLimit
329
330 Handle Fields which are limited to certain values, and potentially
331 need to be looked up from another class.
332
333 This subroutine actually handles two different kinds of fields.  For
334 some the user is responsible for limiting the values.  (i.e. Status,
335 Type).
336
337 For others, the value specified by the user will be looked by via
338 specified class.
339
340 Meta Data:
341   name of class to lookup in (Optional)
342
343 =cut
344
345 sub _EnumLimit {
346     my ( $sb, $field, $op, $value, @rest ) = @_;
347
348     # SQL::Statement changes != to <>.  (Can we remove this now?)
349     $op = "!=" if $op eq "<>";
350
351     die "Invalid Operation: $op for $field"
352         unless $op eq "="
353         or $op     eq "!=";
354
355     my $meta = $FIELD_METADATA{$field};
356     if ( defined $meta->[1] && defined $value && $value !~ /^\d+$/ ) {
357         my $class = "RT::" . $meta->[1];
358         my $o     = $class->new( $sb->CurrentUser );
359         $o->Load($value);
360         $value = $o->Id;
361     }
362     $sb->_SQLLimit(
363         FIELD    => $field,
364         VALUE    => $value,
365         OPERATOR => $op,
366         @rest,
367     );
368 }
369
370 =head2 _IntLimit
371
372 Handle fields where the values are limited to integers.  (For example,
373 Priority, TimeWorked.)
374
375 Meta Data:
376   None
377
378 =cut
379
380 sub _IntLimit {
381     my ( $sb, $field, $op, $value, @rest ) = @_;
382
383     die "Invalid Operator $op for $field"
384         unless $op =~ /^(=|!=|>|<|>=|<=)$/;
385
386     $sb->_SQLLimit(
387         FIELD    => $field,
388         VALUE    => $value,
389         OPERATOR => $op,
390         @rest,
391     );
392 }
393
394 =head2 _LinkLimit
395
396 Handle fields which deal with links between tickets.  (MemberOf, DependsOn)
397
398 Meta Data:
399   1: Direction (From, To)
400   2: Link Type (MemberOf, DependsOn, RefersTo)
401
402 =cut
403
404 sub _LinkLimit {
405     my ( $sb, $field, $op, $value, @rest ) = @_;
406
407     my $meta = $FIELD_METADATA{$field};
408     die "Invalid Operator $op for $field" unless $op =~ /^(=|!=|IS|IS NOT)$/io;
409
410     my $is_negative = 0;
411     if ( $op eq '!=' || $op =~ /\bNOT\b/i ) {
412         $is_negative = 1;
413     }
414     my $is_null = 0;
415     $is_null = 1 if !$value || $value =~ /^null$/io;
416
417     my $direction = $meta->[1] || '';
418     my ($matchfield, $linkfield) = ('', '');
419     if ( $direction eq 'To' ) {
420         ($matchfield, $linkfield) = ("Target", "Base");
421     }
422     elsif ( $direction eq 'From' ) {
423         ($matchfield, $linkfield) = ("Base", "Target");
424     }
425     elsif ( $direction ) {
426         die "Invalid link direction '$direction' for $field\n";
427     } else {
428         $sb->_OpenParen;
429         $sb->_LinkLimit( 'LinkedTo', $op, $value, @rest );
430         $sb->_LinkLimit(
431             'LinkedFrom', $op, $value, @rest,
432             ENTRYAGGREGATOR => (($is_negative && $is_null) || (!$is_null && !$is_negative))? 'OR': 'AND',
433         );
434         $sb->_CloseParen;
435         return;
436     }
437
438     my $is_local = 1;
439     if ( $is_null ) {
440         $op = ($op =~ /^(=|IS)$/)? 'IS': 'IS NOT';
441     }
442     elsif ( $value =~ /\D/ ) {
443         $is_local = 0;
444     }
445     $matchfield = "Local$matchfield" if $is_local;
446
447 #For doing a left join to find "unlinked tickets" we want to generate a query that looks like this
448 #    SELECT main.* FROM Tickets main
449 #        LEFT JOIN Links Links_1 ON (     (Links_1.Type = 'MemberOf')
450 #                                      AND(main.id = Links_1.LocalTarget))
451 #        WHERE Links_1.LocalBase IS NULL;
452
453     if ( $is_null ) {
454         my $linkalias = $sb->Join(
455             TYPE   => 'LEFT',
456             ALIAS1 => 'main',
457             FIELD1 => 'id',
458             TABLE2 => 'Links',
459             FIELD2 => 'Local' . $linkfield
460         );
461         $sb->SUPER::Limit(
462             LEFTJOIN => $linkalias,
463             FIELD    => 'Type',
464             OPERATOR => '=',
465             VALUE    => $meta->[2],
466         ) if $meta->[2];
467         $sb->_SQLLimit(
468             @rest,
469             ALIAS      => $linkalias,
470             FIELD      => $matchfield,
471             OPERATOR   => $op,
472             VALUE      => 'NULL',
473             QUOTEVALUE => 0,
474         );
475     }
476     else {
477         my $linkalias = $sb->Join(
478             TYPE   => 'LEFT',
479             ALIAS1 => 'main',
480             FIELD1 => 'id',
481             TABLE2 => 'Links',
482             FIELD2 => 'Local' . $linkfield
483         );
484         $sb->SUPER::Limit(
485             LEFTJOIN => $linkalias,
486             FIELD    => 'Type',
487             OPERATOR => '=',
488             VALUE    => $meta->[2],
489         ) if $meta->[2];
490         $sb->SUPER::Limit(
491             LEFTJOIN => $linkalias,
492             FIELD    => $matchfield,
493             OPERATOR => '=',
494             VALUE    => $value,
495         );
496         $sb->_SQLLimit(
497             @rest,
498             ALIAS      => $linkalias,
499             FIELD      => $matchfield,
500             OPERATOR   => $is_negative? 'IS': 'IS NOT',
501             VALUE      => 'NULL',
502             QUOTEVALUE => 0,
503         );
504     }
505 }
506
507 =head2 _DateLimit
508
509 Handle date fields.  (Created, LastTold..)
510
511 Meta Data:
512   1: type of link.  (Probably not necessary.)
513
514 =cut
515
516 sub _DateLimit {
517     my ( $sb, $field, $op, $value, @rest ) = @_;
518
519     die "Invalid Date Op: $op"
520         unless $op =~ /^(=|>|<|>=|<=)$/;
521
522     my $meta = $FIELD_METADATA{$field};
523     die "Incorrect Meta Data for $field"
524         unless ( defined $meta->[1] );
525
526     $sb->_DateFieldLimit( $meta->[1], $op, $value, @rest );
527 }
528
529 # Factor this out for use by custom fields
530
531 sub _DateFieldLimit {
532     my ( $sb, $field, $op, $value, @rest ) = @_;
533
534     my $date = RT::Date->new( $sb->CurrentUser );
535     $date->Set( Format => 'unknown', Value => $value );
536
537     if ( $op eq "=" ) {
538
539         # if we're specifying =, that means we want everything on a
540         # particular single day.  in the database, we need to check for >
541         # and < the edges of that day.
542        
543         my ($daystart, $dayend);
544         if ( lc($value) eq 'this month' ) { 
545             # special case: > and < the edges of this month
546             $date->SetToNow;
547             $date->SetToStart('month');
548             $daystart = $date->ISO;
549             $date->AddMonth;
550             $dayend = $date->ISO;
551         }
552         else {
553             $date->SetToMidnight( Timezone => 'server' );
554             $daystart = $date->ISO;
555             $date->AddDay;
556             $dayend = $date->ISO;
557         }
558
559         $sb->_OpenParen;
560
561         $sb->_SQLLimit(
562             FIELD    => $field,
563             OPERATOR => ">=",
564             VALUE    => $daystart,
565             @rest,
566         );
567
568         $sb->_SQLLimit(
569             FIELD    => $field,
570             OPERATOR => "<",
571             VALUE    => $dayend,
572             @rest,
573             ENTRYAGGREGATOR => 'AND',
574         );
575
576         $sb->_CloseParen;
577
578     }
579     else {
580         $sb->_SQLLimit(
581             FIELD    => $field,
582             OPERATOR => $op,
583             VALUE    => $date->ISO,
584             @rest,
585         );
586     }
587 }
588
589 =head2 _StringLimit
590
591 Handle simple fields which are just strings.  (Subject,Type)
592
593 Meta Data:
594   None
595
596 =cut
597
598 sub _StringLimit {
599     my ( $sb, $field, $op, $value, @rest ) = @_;
600
601     # FIXME:
602     # Valid Operators:
603     #  =, !=, LIKE, NOT LIKE
604     if ( (!defined $value || !length $value)
605         && lc($op) ne 'is' && lc($op) ne 'is not'
606         && RT->Config->Get('DatabaseType') eq 'Oracle'
607     ) {
608         my $negative = 1 if $op eq '!=' || $op =~ /^NOT\s/;
609         $op = $negative? 'IS NOT': 'IS';
610         $value = 'NULL';
611     }
612
613     $sb->_SQLLimit(
614         FIELD         => $field,
615         OPERATOR      => $op,
616         VALUE         => $value,
617         CASESENSITIVE => 0,
618         @rest,
619     );
620 }
621
622 =head2 _TransDateLimit
623
624 Handle fields limiting based on Transaction Date.
625
626 The inpupt value must be in a format parseable by Time::ParseDate
627
628 Meta Data:
629   None
630
631 =cut
632
633 # This routine should really be factored into translimit.
634 sub _TransDateLimit {
635     my ( $sb, $field, $op, $value, @rest ) = @_;
636
637     # See the comments for TransLimit, they apply here too
638
639     unless ( $sb->{_sql_transalias} ) {
640         $sb->{_sql_transalias} = $sb->Join(
641             ALIAS1 => 'main',
642             FIELD1 => 'id',
643             TABLE2 => 'Transactions',
644             FIELD2 => 'ObjectId',
645         );
646         $sb->SUPER::Limit(
647             ALIAS           => $sb->{_sql_transalias},
648             FIELD           => 'ObjectType',
649             VALUE           => 'RT::Ticket',
650             ENTRYAGGREGATOR => 'AND',
651         );
652     }
653
654     my $date = RT::Date->new( $sb->CurrentUser );
655     $date->Set( Format => 'unknown', Value => $value );
656
657     $sb->_OpenParen;
658     if ( $op eq "=" ) {
659
660         # if we're specifying =, that means we want everything on a
661         # particular single day.  in the database, we need to check for >
662         # and < the edges of that day.
663
664         $date->SetToMidnight( Timezone => 'server' );
665         my $daystart = $date->ISO;
666         $date->AddDay;
667         my $dayend = $date->ISO;
668
669         $sb->_SQLLimit(
670             ALIAS         => $sb->{_sql_transalias},
671             FIELD         => 'Created',
672             OPERATOR      => ">=",
673             VALUE         => $daystart,
674             CASESENSITIVE => 0,
675             @rest
676         );
677         $sb->_SQLLimit(
678             ALIAS         => $sb->{_sql_transalias},
679             FIELD         => 'Created',
680             OPERATOR      => "<=",
681             VALUE         => $dayend,
682             CASESENSITIVE => 0,
683             @rest,
684             ENTRYAGGREGATOR => 'AND',
685         );
686
687     }
688
689     # not searching for a single day
690     else {
691
692         #Search for the right field
693         $sb->_SQLLimit(
694             ALIAS         => $sb->{_sql_transalias},
695             FIELD         => 'Created',
696             OPERATOR      => $op,
697             VALUE         => $date->ISO,
698             CASESENSITIVE => 0,
699             @rest
700         );
701     }
702
703     $sb->_CloseParen;
704 }
705
706 =head2 _TransLimit
707
708 Limit based on the Content of a transaction or the ContentType.
709
710 Meta Data:
711   none
712
713 =cut
714
715 sub _TransLimit {
716
717     # Content, ContentType, Filename
718
719     # If only this was this simple.  We've got to do something
720     # complicated here:
721
722     #Basically, we want to make sure that the limits apply to
723     #the same attachment, rather than just another attachment
724     #for the same ticket, no matter how many clauses we lump
725     #on. We put them in TicketAliases so that they get nuked
726     #when we redo the join.
727
728     # In the SQL, we might have
729     #       (( Content = foo ) or ( Content = bar AND Content = baz ))
730     # The AND group should share the same Alias.
731
732     # Actually, maybe it doesn't matter.  We use the same alias and it
733     # works itself out? (er.. different.)
734
735     # Steal more from _ProcessRestrictions
736
737     # FIXME: Maybe look at the previous FooLimit call, and if it was a
738     # TransLimit and EntryAggregator == AND, reuse the Aliases?
739
740     # Or better - store the aliases on a per subclause basis - since
741     # those are going to be the things we want to relate to each other,
742     # anyway.
743
744     # maybe we should not allow certain kinds of aggregation of these
745     # clauses and do a psuedo regex instead? - the problem is getting
746     # them all into the same subclause when you have (A op B op C) - the
747     # way they get parsed in the tree they're in different subclauses.
748
749     my ( $self, $field, $op, $value, %rest ) = @_;
750
751     unless ( $self->{_sql_transalias} ) {
752         $self->{_sql_transalias} = $self->Join(
753             ALIAS1 => 'main',
754             FIELD1 => 'id',
755             TABLE2 => 'Transactions',
756             FIELD2 => 'ObjectId',
757         );
758         $self->SUPER::Limit(
759             ALIAS           => $self->{_sql_transalias},
760             FIELD           => 'ObjectType',
761             VALUE           => 'RT::Ticket',
762             ENTRYAGGREGATOR => 'AND',
763         );
764     }
765     unless ( defined $self->{_sql_trattachalias} ) {
766         $self->{_sql_trattachalias} = $self->_SQLJoin(
767             TYPE   => 'LEFT', # not all txns have an attachment
768             ALIAS1 => $self->{_sql_transalias},
769             FIELD1 => 'id',
770             TABLE2 => 'Attachments',
771             FIELD2 => 'TransactionId',
772         );
773     }
774
775     #Search for the right field
776     if ( $field eq 'Content' and RT->Config->Get('DontSearchFileAttachments') ) {
777         $self->_OpenParen;
778         $self->_SQLLimit(
779                         %rest,
780                         ALIAS         => $self->{_sql_trattachalias},
781                         FIELD         => $field,
782                         OPERATOR      => $op,
783                         VALUE         => $value,
784                         CASESENSITIVE => 0,
785                        );
786         $self->_SQLLimit(
787                         ENTRYAGGREGATOR => 'AND',
788                         ALIAS           => $self->{_sql_trattachalias},
789                         FIELD           => 'Filename',
790                         OPERATOR        => 'IS',
791                         VALUE           => 'NULL',
792                        );
793         $self->_CloseParen;
794     } else {
795         $self->_SQLLimit(
796                         %rest,
797                         ALIAS         => $self->{_sql_trattachalias},
798                         FIELD         => $field,
799                         OPERATOR      => $op,
800                         VALUE         => $value,
801                         CASESENSITIVE => 0,
802         );
803     }
804
805
806 }
807
808 =head2 _WatcherLimit
809
810 Handle watcher limits.  (Requestor, CC, etc..)
811
812 Meta Data:
813   1: Field to query on
814
815
816
817 =cut
818
819 sub _WatcherLimit {
820     my $self  = shift;
821     my $field = shift;
822     my $op    = shift;
823     my $value = shift;
824     my %rest  = (@_);
825
826     my $meta = $FIELD_METADATA{ $field };
827     my $type = $meta->[1] || '';
828     my $class = $meta->[2] || 'Ticket';
829
830     # Owner was ENUM field, so "Owner = 'xxx'" allowed user to
831     # search by id and Name at the same time, this is workaround
832     # to preserve backward compatibility
833     if ( $field eq 'Owner' ) {
834         if ( $op =~ /^!?=$/ && (!$rest{'SUBKEY'} || $rest{'SUBKEY'} eq 'Name' || $rest{'SUBKEY'} eq 'EmailAddress') ) {
835             my $o = RT::User->new( $self->CurrentUser );
836             my $method = ($rest{'SUBKEY'}||'') eq 'EmailAddress' ? 'LoadByEmail': 'Load';
837             $o->$method( $value );
838             $self->_SQLLimit(
839                 FIELD    => 'Owner',
840                 OPERATOR => $op,
841                 VALUE    => $o->id,
842                 %rest,
843             );
844             return;
845         }
846         if ( ($rest{'SUBKEY'}||'') eq 'id' ) {
847             $self->_SQLLimit(
848                 FIELD    => 'Owner',
849                 OPERATOR => $op,
850                 VALUE    => $value,
851                 %rest,
852             );
853             return;
854         }
855     }
856     $rest{SUBKEY} ||= 'EmailAddress';
857
858     my $groups = $self->_RoleGroupsJoin( Type => $type, Class => $class );
859
860     $self->_OpenParen;
861     if ( $op =~ /^IS(?: NOT)?$/ ) {
862         my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
863         # to avoid joining the table Users into the query, we just join GM
864         # and make sure we don't match records where group is member of itself
865         $self->SUPER::Limit(
866             LEFTJOIN   => $group_members,
867             FIELD      => 'GroupId',
868             OPERATOR   => '!=',
869             VALUE      => "$group_members.MemberId",
870             QUOTEVALUE => 0,
871         );
872         $self->_SQLLimit(
873             ALIAS         => $group_members,
874             FIELD         => 'GroupId',
875             OPERATOR      => $op,
876             VALUE         => $value,
877             %rest,
878         );
879     }
880     elsif ( $op =~ /^!=$|^NOT\s+/i ) {
881         # reverse op
882         $op =~ s/!|NOT\s+//i;
883
884         # XXX: we have no way to build correct "Watcher.X != 'Y'" when condition
885         # "X = 'Y'" matches more then one user so we try to fetch two records and
886         # do the right thing when there is only one exist and semi-working solution
887         # otherwise.
888         my $users_obj = RT::Users->new( $self->CurrentUser );
889         $users_obj->Limit(
890             FIELD         => $rest{SUBKEY},
891             OPERATOR      => $op,
892             VALUE         => $value,
893         );
894         $users_obj->OrderBy;
895         $users_obj->RowsPerPage(2);
896         my @users = @{ $users_obj->ItemsArrayRef };
897
898         my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
899         if ( @users <= 1 ) {
900             my $uid = 0;
901             $uid = $users[0]->id if @users;
902             $self->SUPER::Limit(
903                 LEFTJOIN      => $group_members,
904                 ALIAS         => $group_members,
905                 FIELD         => 'MemberId',
906                 VALUE         => $uid,
907             );
908             $self->_SQLLimit(
909                 %rest,
910                 ALIAS           => $group_members,
911                 FIELD           => 'id',
912                 OPERATOR        => 'IS',
913                 VALUE           => 'NULL',
914             );
915         } else {
916             $self->SUPER::Limit(
917                 LEFTJOIN   => $group_members,
918                 FIELD      => 'GroupId',
919                 OPERATOR   => '!=',
920                 VALUE      => "$group_members.MemberId",
921                 QUOTEVALUE => 0,
922             );
923             my $users = $self->Join(
924                 TYPE            => 'LEFT',
925                 ALIAS1          => $group_members,
926                 FIELD1          => 'MemberId',
927                 TABLE2          => 'Users',
928                 FIELD2          => 'id',
929             );
930             $self->SUPER::Limit(
931                 LEFTJOIN      => $users,
932                 ALIAS         => $users,
933                 FIELD         => $rest{SUBKEY},
934                 OPERATOR      => $op,
935                 VALUE         => $value,
936                 CASESENSITIVE => 0,
937             );
938             $self->_SQLLimit(
939                 %rest,
940                 ALIAS         => $users,
941                 FIELD         => 'id',
942                 OPERATOR      => 'IS',
943                 VALUE         => 'NULL',
944             );
945         }
946     } else {
947         my $group_members = $self->_GroupMembersJoin(
948             GroupsAlias => $groups,
949             New => 0,
950         );
951
952         my $users = $self->{'_sql_u_watchers_aliases'}{$group_members};
953         unless ( $users ) {
954             $users = $self->{'_sql_u_watchers_aliases'}{$group_members} = 
955                 $self->NewAlias('Users');
956             $self->SUPER::Limit(
957                 LEFTJOIN      => $group_members,
958                 ALIAS         => $group_members,
959                 FIELD         => 'MemberId',
960                 VALUE         => "$users.id",
961                 QUOTEVALUE    => 0,
962             );
963         }
964
965         # we join users table without adding some join condition between tables,
966         # the only conditions we have are conditions on the table iteslf,
967         # for example Users.EmailAddress = 'x'. We should add this condition to
968         # the top level of the query and bundle it with another similar conditions,
969         # for example "Users.EmailAddress = 'x' OR Users.EmailAddress = 'Y'".
970         # To achive this goal we use own SUBCLAUSE for conditions on the users table.
971         $self->SUPER::Limit(
972             %rest,
973             SUBCLAUSE       => '_sql_u_watchers_'. $users,
974             ALIAS           => $users,
975             FIELD           => $rest{'SUBKEY'},
976             VALUE           => $value,
977             OPERATOR        => $op,
978             CASESENSITIVE   => 0,
979         );
980         # A condition which ties Users and Groups (role groups) is a left join condition
981         # of CachedGroupMembers table. To get correct results of the query we check
982         # if there are matches in CGM table or not using 'cgm.id IS NOT NULL'.
983         $self->_SQLLimit(
984             %rest,
985             ALIAS           => $group_members,
986             FIELD           => 'id',
987             OPERATOR        => 'IS NOT',
988             VALUE           => 'NULL',
989         );
990     }
991     $self->_CloseParen;
992 }
993
994 sub _RoleGroupsJoin {
995     my $self = shift;
996     my %args = (New => 0, Class => 'Ticket', Type => '', @_);
997     return $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $args{'Type'} }
998         if $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $args{'Type'} }
999            && !$args{'New'};
1000
1001     # we always have watcher groups for ticket, so we use INNER join
1002     my $groups = $self->Join(
1003         ALIAS1          => 'main',
1004         FIELD1          => $args{'Class'} eq 'Queue'? 'Queue': 'id',
1005         TABLE2          => 'Groups',
1006         FIELD2          => 'Instance',
1007         ENTRYAGGREGATOR => 'AND',
1008     );
1009     $self->SUPER::Limit(
1010         LEFTJOIN        => $groups,
1011         ALIAS           => $groups,
1012         FIELD           => 'Domain',
1013         VALUE           => 'RT::'. $args{'Class'} .'-Role',
1014     );
1015     $self->SUPER::Limit(
1016         LEFTJOIN        => $groups,
1017         ALIAS           => $groups,
1018         FIELD           => 'Type',
1019         VALUE           => $args{'Type'},
1020     ) if $args{'Type'};
1021
1022     $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $args{'Type'} } = $groups
1023         unless $args{'New'};
1024
1025     return $groups;
1026 }
1027
1028 sub _GroupMembersJoin {
1029     my $self = shift;
1030     my %args = (New => 1, GroupsAlias => undef, @_);
1031
1032     return $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} }
1033         if $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} }
1034             && !$args{'New'};
1035
1036     my $alias = $self->Join(
1037         TYPE            => 'LEFT',
1038         ALIAS1          => $args{'GroupsAlias'},
1039         FIELD1          => 'id',
1040         TABLE2          => 'CachedGroupMembers',
1041         FIELD2          => 'GroupId',
1042         ENTRYAGGREGATOR => 'AND',
1043     );
1044
1045     $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} } = $alias
1046         unless $args{'New'};
1047
1048     return $alias;
1049 }
1050
1051 =head2 _WatcherJoin
1052
1053 Helper function which provides joins to a watchers table both for limits
1054 and for ordering.
1055
1056 =cut
1057
1058 sub _WatcherJoin {
1059     my $self = shift;
1060     my $type = shift || '';
1061
1062
1063     my $groups = $self->_RoleGroupsJoin( Type => $type );
1064     my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
1065     # XXX: work around, we must hide groups that
1066     # are members of the role group we search in,
1067     # otherwise them result in wrong NULLs in Users
1068     # table and break ordering. Now, we know that
1069     # RT doesn't allow to add groups as members of the
1070     # ticket roles, so we just hide entries in CGM table
1071     # with MemberId == GroupId from results
1072     $self->SUPER::Limit(
1073         LEFTJOIN   => $group_members,
1074         FIELD      => 'GroupId',
1075         OPERATOR   => '!=',
1076         VALUE      => "$group_members.MemberId",
1077         QUOTEVALUE => 0,
1078     );
1079     my $users = $self->Join(
1080         TYPE            => 'LEFT',
1081         ALIAS1          => $group_members,
1082         FIELD1          => 'MemberId',
1083         TABLE2          => 'Users',
1084         FIELD2          => 'id',
1085     );
1086     return ($groups, $group_members, $users);
1087 }
1088
1089 =head2 _WatcherMembershipLimit
1090
1091 Handle watcher membership limits, i.e. whether the watcher belongs to a
1092 specific group or not.
1093
1094 Meta Data:
1095   1: Field to query on
1096
1097 SELECT DISTINCT main.*
1098 FROM
1099     Tickets main,
1100     Groups Groups_1,
1101     CachedGroupMembers CachedGroupMembers_2,
1102     Users Users_3
1103 WHERE (
1104     (main.EffectiveId = main.id)
1105 ) AND (
1106     (main.Status != 'deleted')
1107 ) AND (
1108     (main.Type = 'ticket')
1109 ) AND (
1110     (
1111         (Users_3.EmailAddress = '22')
1112             AND
1113         (Groups_1.Domain = 'RT::Ticket-Role')
1114             AND
1115         (Groups_1.Type = 'RequestorGroup')
1116     )
1117 ) AND
1118     Groups_1.Instance = main.id
1119 AND
1120     Groups_1.id = CachedGroupMembers_2.GroupId
1121 AND
1122     CachedGroupMembers_2.MemberId = Users_3.id
1123 ORDER BY main.id ASC
1124 LIMIT 25
1125
1126 =cut
1127
1128 sub _WatcherMembershipLimit {
1129     my ( $self, $field, $op, $value, @rest ) = @_;
1130     my %rest = @rest;
1131
1132     $self->_OpenParen;
1133
1134     my $groups       = $self->NewAlias('Groups');
1135     my $groupmembers = $self->NewAlias('CachedGroupMembers');
1136     my $users        = $self->NewAlias('Users');
1137     my $memberships  = $self->NewAlias('CachedGroupMembers');
1138
1139     if ( ref $field ) {    # gross hack
1140         my @bundle = @$field;
1141         $self->_OpenParen;
1142         for my $chunk (@bundle) {
1143             ( $field, $op, $value, @rest ) = @$chunk;
1144             $self->_SQLLimit(
1145                 ALIAS    => $memberships,
1146                 FIELD    => 'GroupId',
1147                 VALUE    => $value,
1148                 OPERATOR => $op,
1149                 @rest,
1150             );
1151         }
1152         $self->_CloseParen;
1153     }
1154     else {
1155         $self->_SQLLimit(
1156             ALIAS    => $memberships,
1157             FIELD    => 'GroupId',
1158             VALUE    => $value,
1159             OPERATOR => $op,
1160             @rest,
1161         );
1162     }
1163
1164     # {{{ Tie to groups for tickets we care about
1165     $self->_SQLLimit(
1166         ALIAS           => $groups,
1167         FIELD           => 'Domain',
1168         VALUE           => 'RT::Ticket-Role',
1169         ENTRYAGGREGATOR => 'AND'
1170     );
1171
1172     $self->Join(
1173         ALIAS1 => $groups,
1174         FIELD1 => 'Instance',
1175         ALIAS2 => 'main',
1176         FIELD2 => 'id'
1177     );
1178
1179     # }}}
1180
1181     # If we care about which sort of watcher
1182     my $meta = $FIELD_METADATA{$field};
1183     my $type = ( defined $meta->[1] ? $meta->[1] : undef );
1184
1185     if ($type) {
1186         $self->_SQLLimit(
1187             ALIAS           => $groups,
1188             FIELD           => 'Type',
1189             VALUE           => $type,
1190             ENTRYAGGREGATOR => 'AND'
1191         );
1192     }
1193
1194     $self->Join(
1195         ALIAS1 => $groups,
1196         FIELD1 => 'id',
1197         ALIAS2 => $groupmembers,
1198         FIELD2 => 'GroupId'
1199     );
1200
1201     $self->Join(
1202         ALIAS1 => $groupmembers,
1203         FIELD1 => 'MemberId',
1204         ALIAS2 => $users,
1205         FIELD2 => 'id'
1206     );
1207
1208     $self->Join(
1209         ALIAS1 => $memberships,
1210         FIELD1 => 'MemberId',
1211         ALIAS2 => $users,
1212         FIELD2 => 'id'
1213     );
1214
1215     $self->_CloseParen;
1216
1217 }
1218
1219 =head2 _CustomFieldDecipher
1220
1221 Try and turn a CF descriptor into (cfid, cfname) object pair.
1222
1223 =cut
1224
1225 sub _CustomFieldDecipher {
1226     my ($self, $string) = @_;
1227
1228     my ($queue, $field, $column) = ($string =~ /^(?:(.+?)\.)?{(.+)}(?:\.(.+))?$/);
1229     $field ||= ($string =~ /^{(.*?)}$/)[0] || $string;
1230
1231     my $cf;
1232     if ( $queue ) {
1233         my $q = RT::Queue->new( $self->CurrentUser );
1234         $q->Load( $queue );
1235
1236         if ( $q->id ) {
1237             # $queue = $q->Name; # should we normalize the queue?
1238             $cf = $q->CustomField( $field );
1239         }
1240         else {
1241             $RT::Logger->warning("Queue '$queue' doesn't exist, parsed from '$string'");
1242             $queue = 0;
1243         }
1244     }
1245     elsif ( $field =~ /\D/ ) {
1246         $queue = '';
1247         my $cfs = RT::CustomFields->new( $self->CurrentUser );
1248         $cfs->Limit( FIELD => 'Name', VALUE => $field );
1249         $cfs->LimitToLookupType('RT::Queue-RT::Ticket');
1250
1251         # if there is more then one field the current user can
1252         # see with the same name then we shouldn't return cf object
1253         # as we don't know which one to use
1254         $cf = $cfs->First;
1255         if ( $cf ) {
1256             $cf = undef if $cfs->Next;
1257         }
1258     }
1259     else {
1260         $cf = RT::CustomField->new( $self->CurrentUser );
1261         $cf->Load( $field );
1262     }
1263
1264     return ($queue, $field, $cf, $column);
1265 }
1266
1267 =head2 _CustomFieldJoin
1268
1269 Factor out the Join of custom fields so we can use it for sorting too
1270
1271 =cut
1272
1273 sub _CustomFieldJoin {
1274     my ($self, $cfkey, $cfid, $field) = @_;
1275     # Perform one Join per CustomField
1276     if ( $self->{_sql_object_cfv_alias}{$cfkey} ||
1277          $self->{_sql_cf_alias}{$cfkey} )
1278     {
1279         return ( $self->{_sql_object_cfv_alias}{$cfkey},
1280                  $self->{_sql_cf_alias}{$cfkey} );
1281     }
1282
1283     my ($TicketCFs, $CFs);
1284     if ( $cfid ) {
1285         $TicketCFs = $self->{_sql_object_cfv_alias}{$cfkey} = $self->Join(
1286             TYPE   => 'LEFT',
1287             ALIAS1 => 'main',
1288             FIELD1 => 'id',
1289             TABLE2 => 'ObjectCustomFieldValues',
1290             FIELD2 => 'ObjectId',
1291         );
1292         $self->SUPER::Limit(
1293             LEFTJOIN        => $TicketCFs,
1294             FIELD           => 'CustomField',
1295             VALUE           => $cfid,
1296             ENTRYAGGREGATOR => 'AND'
1297         );
1298     }
1299     else {
1300         my $ocfalias = $self->Join(
1301             TYPE       => 'LEFT',
1302             FIELD1     => 'Queue',
1303             TABLE2     => 'ObjectCustomFields',
1304             FIELD2     => 'ObjectId',
1305         );
1306
1307         $self->SUPER::Limit(
1308             LEFTJOIN        => $ocfalias,
1309             ENTRYAGGREGATOR => 'OR',
1310             FIELD           => 'ObjectId',
1311             VALUE           => '0',
1312         );
1313
1314         $CFs = $self->{_sql_cf_alias}{$cfkey} = $self->Join(
1315             TYPE       => 'LEFT',
1316             ALIAS1     => $ocfalias,
1317             FIELD1     => 'CustomField',
1318             TABLE2     => 'CustomFields',
1319             FIELD2     => 'id',
1320         );
1321         $self->SUPER::Limit(
1322             LEFTJOIN        => $CFs,
1323             ENTRYAGGREGATOR => 'AND',
1324             FIELD           => 'LookupType',
1325             VALUE           => 'RT::Queue-RT::Ticket',
1326         );
1327         $self->SUPER::Limit(
1328             LEFTJOIN        => $CFs,
1329             ENTRYAGGREGATOR => 'AND',
1330             FIELD           => 'Name',
1331             VALUE           => $field,
1332         );
1333
1334         $TicketCFs = $self->{_sql_object_cfv_alias}{$cfkey} = $self->Join(
1335             TYPE   => 'LEFT',
1336             ALIAS1 => $CFs,
1337             FIELD1 => 'id',
1338             TABLE2 => 'ObjectCustomFieldValues',
1339             FIELD2 => 'CustomField',
1340         );
1341         $self->SUPER::Limit(
1342             LEFTJOIN        => $TicketCFs,
1343             FIELD           => 'ObjectId',
1344             VALUE           => 'main.id',
1345             QUOTEVALUE      => 0,
1346             ENTRYAGGREGATOR => 'AND',
1347         );
1348     }
1349     $self->SUPER::Limit(
1350         LEFTJOIN        => $TicketCFs,
1351         FIELD           => 'ObjectType',
1352         VALUE           => 'RT::Ticket',
1353         ENTRYAGGREGATOR => 'AND'
1354     );
1355     $self->SUPER::Limit(
1356         LEFTJOIN        => $TicketCFs,
1357         FIELD           => 'Disabled',
1358         OPERATOR        => '=',
1359         VALUE           => '0',
1360         ENTRYAGGREGATOR => 'AND'
1361     );
1362
1363     return ($TicketCFs, $CFs);
1364 }
1365
1366 =head2 _CustomFieldLimit
1367
1368 Limit based on CustomFields
1369
1370 Meta Data:
1371   none
1372
1373 =cut
1374
1375 sub _CustomFieldLimit {
1376     my ( $self, $_field, $op, $value, %rest ) = @_;
1377
1378     my $field = $rest{'SUBKEY'} || die "No field specified";
1379
1380     # For our sanity, we can only limit on one queue at a time
1381
1382     my ($queue, $cfid, $cf, $column);
1383     ($queue, $field, $cf, $column) = $self->_CustomFieldDecipher( $field );
1384     $cfid = $cf ? $cf->id  : 0 ;
1385
1386 # If we're trying to find custom fields that don't match something, we
1387 # want tickets where the custom field has no value at all.  Note that
1388 # we explicitly don't include the "IS NULL" case, since we would
1389 # otherwise end up with a redundant clause.
1390
1391     my ($negative_op, $null_op, $inv_op, $range_op)
1392         = $self->ClassifySQLOperation( $op );
1393
1394     my $fix_op = sub {
1395         my $op = shift;
1396         return $op unless RT->Config->Get('DatabaseType') eq 'Oracle';
1397         return 'MATCHES' if $op eq '=';
1398         return 'NOT MATCHES' if $op eq '!=';
1399         return $op;
1400     };
1401
1402     my $single_value = !$cf || !$cfid || $cf->SingleValue;
1403
1404     my $cfkey = $cfid ? $cfid : "$queue.$field";
1405
1406     if ( $null_op && !$column ) {
1407         # IS[ NOT] NULL without column is the same as has[ no] any CF value,
1408         # we can reuse our default joins for this operation
1409         # with column specified we have different situation
1410         my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
1411         $self->_OpenParen;
1412         $self->_SQLLimit(
1413             ALIAS    => $TicketCFs,
1414             FIELD    => 'id',
1415             OPERATOR => $op,
1416             VALUE    => $value,
1417             %rest
1418         );
1419         $self->_SQLLimit(
1420             ALIAS      => $CFs,
1421             FIELD      => 'Name',
1422             OPERATOR   => 'IS NOT',
1423             VALUE      => 'NULL',
1424             QUOTEVALUE => 0,
1425             ENTRYAGGREGATOR => 'AND',
1426         ) if $CFs;
1427         $self->_CloseParen;
1428     }
1429     elsif ( !$negative_op || $single_value ) {
1430         $cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++ if !$single_value && !$range_op;
1431         my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
1432
1433         $self->_OpenParen;
1434
1435         $self->_OpenParen;
1436
1437         $self->_OpenParen;
1438         # if column is defined then deal only with it
1439         # otherwise search in Content and in LargeContent
1440         if ( $column ) {
1441             $self->_SQLLimit(
1442                 ALIAS      => $TicketCFs,
1443                 FIELD      => $column,
1444                 OPERATOR   => ($column ne 'LargeContent'? $op : $fix_op->($op)),
1445                 VALUE      => $value,
1446                 %rest
1447             );
1448         }
1449         elsif ( $cf->Type eq 'Date' ) {
1450             $self->_DateFieldLimit( 
1451                 'Content',
1452                 $op,
1453                 $value,
1454                 ALIAS => $TicketCFs,
1455                 %rest
1456             );
1457         }
1458         elsif ( $op eq '=' || $op eq '!=' || $op eq '<>' ) {
1459             unless ( length( Encode::encode_utf8($value) ) > 255 ) {
1460                 $self->_SQLLimit(
1461                     ALIAS      => $TicketCFs,
1462                     FIELD      => 'Content',
1463                     OPERATOR   => $op,
1464                     VALUE      => $value,
1465                     %rest
1466                 );
1467             } else {
1468                 $self->_OpenParen;
1469                 $self->_SQLLimit(
1470                     ALIAS      => $TicketCFs,
1471                     FIELD      => 'Content',
1472                     OPERATOR   => '=',
1473                     VALUE      => '',
1474                     ENTRYAGGREGATOR => 'OR'
1475                 );
1476                 $self->_SQLLimit(
1477                     ALIAS      => $TicketCFs,
1478                     FIELD      => 'Content',
1479                     OPERATOR   => 'IS',
1480                     VALUE      => 'NULL',
1481                     ENTRYAGGREGATOR => 'OR'
1482                 );
1483                 $self->_CloseParen;
1484                 $self->_SQLLimit(
1485                     ALIAS => $TicketCFs,
1486                     FIELD => 'LargeContent',
1487                     OPERATOR => $fix_op->($op),
1488                     VALUE => $value,
1489                     ENTRYAGGREGATOR => 'AND',
1490                 );
1491             }
1492         }
1493         else {
1494             $self->_SQLLimit(
1495                 ALIAS      => $TicketCFs,
1496                 FIELD      => 'Content',
1497                 OPERATOR   => $op,
1498                 VALUE      => $value,
1499                 %rest
1500             );
1501
1502             $self->_OpenParen;
1503             $self->_OpenParen;
1504             $self->_SQLLimit(
1505                 ALIAS      => $TicketCFs,
1506                 FIELD      => 'Content',
1507                 OPERATOR   => '=',
1508                 VALUE      => '',
1509                 ENTRYAGGREGATOR => 'OR'
1510             );
1511             $self->_SQLLimit(
1512                 ALIAS      => $TicketCFs,
1513                 FIELD      => 'Content',
1514                 OPERATOR   => 'IS',
1515                 VALUE      => 'NULL',
1516                 ENTRYAGGREGATOR => 'OR'
1517             );
1518             $self->_CloseParen;
1519             $self->_SQLLimit(
1520                 ALIAS => $TicketCFs,
1521                 FIELD => 'LargeContent',
1522                 OPERATOR => $fix_op->($op),
1523                 VALUE => $value,
1524                 ENTRYAGGREGATOR => 'AND',
1525             );
1526             $self->_CloseParen;
1527         }
1528         $self->_CloseParen;
1529
1530         # XXX: if we join via CustomFields table then
1531         # because of order of left joins we get NULLs in
1532         # CF table and then get nulls for those records
1533         # in OCFVs table what result in wrong results
1534         # as decifer method now tries to load a CF then
1535         # we fall into this situation only when there
1536         # are more than one CF with the name in the DB.
1537         # the same thing applies to order by call.
1538         # TODO: reorder joins T <- OCFVs <- CFs <- OCFs if
1539         # we want treat IS NULL as (not applies or has
1540         # no value)
1541         $self->_SQLLimit(
1542             ALIAS      => $CFs,
1543             FIELD      => 'Name',
1544             OPERATOR   => 'IS NOT',
1545             VALUE      => 'NULL',
1546             QUOTEVALUE => 0,
1547             ENTRYAGGREGATOR => 'AND',
1548         ) if $CFs;
1549         $self->_CloseParen;
1550
1551         if ($negative_op) {
1552             $self->_SQLLimit(
1553                 ALIAS           => $TicketCFs,
1554                 FIELD           => $column || 'Content',
1555                 OPERATOR        => 'IS',
1556                 VALUE           => 'NULL',
1557                 QUOTEVALUE      => 0,
1558                 ENTRYAGGREGATOR => 'OR',
1559             );
1560         }
1561
1562         $self->_CloseParen;
1563     }
1564     else {
1565         $cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++;
1566         my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
1567
1568         # reverse operation
1569         $op =~ s/!|NOT\s+//i;
1570
1571         # if column is defined then deal only with it
1572         # otherwise search in Content and in LargeContent
1573         if ( $column ) {
1574             $self->SUPER::Limit(
1575                 LEFTJOIN   => $TicketCFs,
1576                 ALIAS      => $TicketCFs,
1577                 FIELD      => $column,
1578                 OPERATOR   => ($column ne 'LargeContent'? $op : $fix_op->($op)),
1579                 VALUE      => $value,
1580             );
1581         }
1582         else {
1583             $self->SUPER::Limit(
1584                 LEFTJOIN   => $TicketCFs,
1585                 ALIAS      => $TicketCFs,
1586                 FIELD      => 'Content',
1587                 OPERATOR   => $op,
1588                 VALUE      => $value,
1589             );
1590         }
1591         $self->_SQLLimit(
1592             %rest,
1593             ALIAS      => $TicketCFs,
1594             FIELD      => 'id',
1595             OPERATOR   => 'IS',
1596             VALUE      => 'NULL',
1597             QUOTEVALUE => 0,
1598         );
1599     }
1600 }
1601
1602 sub _HasAttributeLimit {
1603     my ( $self, $field, $op, $value, %rest ) = @_;
1604
1605     my $alias = $self->Join(
1606         TYPE   => 'LEFT',
1607         ALIAS1 => 'main',
1608         FIELD1 => 'id',
1609         TABLE2 => 'Attributes',
1610         FIELD2 => 'ObjectId',
1611     );
1612     $self->SUPER::Limit(
1613         LEFTJOIN        => $alias,
1614         FIELD           => 'ObjectType',
1615         VALUE           => 'RT::Ticket',
1616         ENTRYAGGREGATOR => 'AND'
1617     );
1618     $self->SUPER::Limit(
1619         LEFTJOIN        => $alias,
1620         FIELD           => 'Name',
1621         OPERATOR        => $op,
1622         VALUE           => $value,
1623         ENTRYAGGREGATOR => 'AND'
1624     );
1625     $self->_SQLLimit(
1626         %rest,
1627         ALIAS      => $alias,
1628         FIELD      => 'id',
1629         OPERATOR   => $FIELD_METADATA{$field}->[1]? 'IS NOT': 'IS',
1630         VALUE      => 'NULL',
1631         QUOTEVALUE => 0,
1632     );
1633 }
1634
1635 # End Helper Functions
1636
1637 # End of SQL Stuff -------------------------------------------------
1638
1639 # {{{ Allow sorting on watchers
1640
1641 =head2 OrderByCols ARRAY
1642
1643 A modified version of the OrderBy method which automatically joins where
1644 C<ALIAS> is set to the name of a watcher type.
1645
1646 =cut
1647
1648 sub OrderByCols {
1649     my $self = shift;
1650     my @args = @_;
1651     my $clause;
1652     my @res   = ();
1653     my $order = 0;
1654
1655     foreach my $row (@args) {
1656         if ( $row->{ALIAS} ) {
1657             push @res, $row;
1658             next;
1659         }
1660         if ( $row->{FIELD} !~ /\./ ) {
1661             my $meta = $self->FIELDS->{ $row->{FIELD} };
1662             unless ( $meta ) {
1663                 push @res, $row;
1664                 next;
1665             }
1666
1667             if ( $meta->[0] eq 'ENUM' && ($meta->[1]||'') eq 'Queue' ) {
1668                 my $alias = $self->Join(
1669                     TYPE   => 'LEFT',
1670                     ALIAS1 => 'main',
1671                     FIELD1 => $row->{'FIELD'},
1672                     TABLE2 => 'Queues',
1673                     FIELD2 => 'id',
1674                 );
1675                 push @res, { %$row, ALIAS => $alias, FIELD => "Name" };
1676             } elsif ( ( $meta->[0] eq 'ENUM' && ($meta->[1]||'') eq 'User' )
1677                 || ( $meta->[0] eq 'WATCHERFIELD' && ($meta->[1]||'') eq 'Owner' )
1678             ) {
1679                 my $alias = $self->Join(
1680                     TYPE   => 'LEFT',
1681                     ALIAS1 => 'main',
1682                     FIELD1 => $row->{'FIELD'},
1683                     TABLE2 => 'Users',
1684                     FIELD2 => 'id',
1685                 );
1686                 push @res, { %$row, ALIAS => $alias, FIELD => "Name" };
1687             } else {
1688                 push @res, $row;
1689             }
1690             next;
1691         }
1692
1693         my ( $field, $subkey ) = split /\./, $row->{FIELD}, 2;
1694         my $meta = $self->FIELDS->{$field};
1695         if ( defined $meta->[0] && $meta->[0] eq 'WATCHERFIELD' ) {
1696             # cache alias as we want to use one alias per watcher type for sorting
1697             my $users = $self->{_sql_u_watchers_alias_for_sort}{ $meta->[1] };
1698             unless ( $users ) {
1699                 $self->{_sql_u_watchers_alias_for_sort}{ $meta->[1] }
1700                     = $users = ( $self->_WatcherJoin( $meta->[1] ) )[2];
1701             }
1702             push @res, { %$row, ALIAS => $users, FIELD => $subkey };
1703        } elsif ( defined $meta->[0] && $meta->[0] eq 'CUSTOMFIELD' ) {
1704            my ($queue, $field, $cf_obj, $column) = $self->_CustomFieldDecipher( $subkey );
1705            my $cfkey = $cf_obj ? $cf_obj->id : "$queue.$field";
1706            $cfkey .= ".ordering" if !$cf_obj || ($cf_obj->MaxValues||0) != 1;
1707            my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, ($cf_obj ?$cf_obj->id :0) , $field );
1708            # this is described in _CustomFieldLimit
1709            $self->_SQLLimit(
1710                ALIAS      => $CFs,
1711                FIELD      => 'Name',
1712                OPERATOR   => 'IS NOT',
1713                VALUE      => 'NULL',
1714                QUOTEVALUE => 1,
1715                ENTRYAGGREGATOR => 'AND',
1716            ) if $CFs;
1717            unless ($cf_obj) {
1718                # For those cases where we are doing a join against the
1719                # CF name, and don't have a CFid, use Unique to make sure
1720                # we don't show duplicate tickets.  NOTE: I'm pretty sure
1721                # this will stay mixed in for the life of the
1722                # class/package, and not just for the life of the object.
1723                # Potential performance issue.
1724                require DBIx::SearchBuilder::Unique;
1725                DBIx::SearchBuilder::Unique->import;
1726            }
1727            my $CFvs = $self->Join(
1728                TYPE   => 'LEFT',
1729                ALIAS1 => $TicketCFs,
1730                FIELD1 => 'CustomField',
1731                TABLE2 => 'CustomFieldValues',
1732                FIELD2 => 'CustomField',
1733            );
1734            $self->SUPER::Limit(
1735                LEFTJOIN        => $CFvs,
1736                FIELD           => 'Name',
1737                QUOTEVALUE      => 0,
1738                VALUE           => $TicketCFs . ".Content",
1739                ENTRYAGGREGATOR => 'AND'
1740            );
1741
1742            push @res, { %$row, ALIAS => $CFvs, FIELD => 'SortOrder' };
1743            push @res, { %$row, ALIAS => $TicketCFs, FIELD => 'Content' };
1744        } elsif ( $field eq "Custom" && $subkey eq "Ownership") {
1745            # PAW logic is "reversed"
1746            my $order = "ASC";
1747            if (exists $row->{ORDER} ) {
1748                my $o = $row->{ORDER};
1749                delete $row->{ORDER};
1750                $order = "DESC" if $o =~ /asc/i;
1751            }
1752
1753            # Ticket.Owner    1 0 X
1754            # Unowned Tickets 0 1 X
1755            # Else            0 0 X
1756
1757            foreach my $uid ( $self->CurrentUser->Id, $RT::Nobody->Id ) {
1758                if ( RT->Config->Get('DatabaseType') eq 'Oracle' ) {
1759                    my $f = ($row->{'ALIAS'} || 'main') .'.Owner';
1760                    push @res, { %$row, ALIAS => '', FIELD => "CASE WHEN $f=$uid THEN 1 ELSE 0 END", ORDER => $order } ;
1761                } else {
1762                    push @res, { %$row, FIELD => "Owner=$uid", ORDER => $order } ;
1763                }
1764            }
1765
1766            push @res, { %$row, FIELD => "Priority", ORDER => $order } ;
1767
1768        } elsif ( $field eq 'Customer' ) { #Freeside
1769            if ( $subkey eq 'Number' ) {
1770                my ($linkalias, $custnum_sql) = $self->JoinToCustLinks;
1771                push @res, { %$row,
1772                             ALIAS => '',
1773                             FIELD => $custnum_sql,
1774                         };
1775            }
1776            else {
1777                my $custalias = $self->JoinToCustomer;
1778                my $field;
1779                if ( $subkey eq 'Name' ) {
1780                    $field = "COALESCE( $custalias.company,
1781                    $custalias.last || ', ' || $custalias.first
1782                    )";
1783                }
1784                else {
1785                    # no other cases exist yet, but for obviousness:
1786                    $field = $subkey;
1787                }
1788                push @res, { %$row, ALIAS => '', FIELD => $field };
1789            }
1790
1791        } #Freeside
1792
1793        else {
1794            push @res, $row;
1795        }
1796     }
1797     return $self->SUPER::OrderByCols(@res);
1798 }
1799
1800 #Freeside
1801
1802 sub JoinToCustLinks {
1803     # Set up join to links (id = localbase),
1804     # limit link type to 'MemberOf',
1805     # and target value to any Freeside custnum URI.
1806     # Return the linkalias for further join/limit action,
1807     # and an sql expression to retrieve the custnum.
1808     my $self = shift;
1809     my $linkalias = $self->Join(
1810         TYPE   => 'LEFT',
1811         ALIAS1 => 'main',
1812         FIELD1 => 'id',
1813         TABLE2 => 'Links',
1814         FIELD2 => 'LocalBase',
1815     );
1816
1817     $self->SUPER::Limit(
1818         LEFTJOIN => $linkalias,
1819         FIELD    => 'Type',
1820         OPERATOR => '=',
1821         VALUE    => 'MemberOf',
1822     );
1823     $self->SUPER::Limit(
1824         LEFTJOIN => $linkalias,
1825         FIELD    => 'Target',
1826         OPERATOR => 'STARTSWITH',
1827         VALUE    => 'freeside://freeside/cust_main/',
1828     );
1829     my $custnum_sql = "CAST(SUBSTR($linkalias.Target,31) AS ";
1830     if ( RT->Config->Get('DatabaseType') eq 'mysql' ) {
1831         $custnum_sql .= 'SIGNED INTEGER)';
1832     }
1833     else {
1834         $custnum_sql .= 'INTEGER)';
1835     }
1836     return ($linkalias, $custnum_sql);
1837 }
1838
1839 sub JoinToCustomer {
1840     my $self = shift;
1841     my ($linkalias, $custnum_sql) = $self->JoinToCustLinks;
1842
1843     my $custalias = $self->Join(
1844         TYPE       => 'LEFT',
1845         EXPRESSION => $custnum_sql,
1846         TABLE2     => 'cust_main',
1847         FIELD2     => 'custnum',
1848     );
1849     return $custalias;
1850 }
1851
1852 sub _FreesideFieldLimit {
1853     my ( $self, $field, $op, $value, %rest ) = @_;
1854     my $alias = $self->JoinToCustomer;
1855     my $is_negative = 0;
1856     if ( $op eq '!=' || $op =~ /\bNOT\b/i ) {
1857         # if the op is negative, do the join as though
1858         # the op were positive, then accept only records
1859         # where the right-side join key is null.
1860         $is_negative = 1;
1861         $op = '=' if $op eq '!=';
1862         $op =~ s/\bNOT\b//;
1863     }
1864     my $meta = $FIELD_METADATA{$field};
1865     if ( $meta->[1] ) {
1866         $alias = $self->Join(
1867             TYPE        => 'LEFT',
1868             ALIAS1      => $alias,
1869             FIELD1      => 'custnum',
1870             TABLE2      => $meta->[1],
1871             FIELD2      => 'custnum',
1872         );
1873     }
1874
1875     $self->SUPER::Limit(
1876         LEFTJOIN        => $alias,
1877         FIELD           => lc($field),
1878         OPERATOR        => $op,
1879         VALUE           => $value,
1880         ENTRYAGGREGATOR => 'AND',
1881     );
1882     $self->_SQLLimit(
1883         %rest,
1884         ALIAS           => $alias,
1885         FIELD           => lc($field),
1886         OPERATOR        => $is_negative ? 'IS' : 'IS NOT',
1887         VALUE           => 'NULL',
1888         QUOTEVALUE      => 0,
1889     );
1890 }
1891
1892 #Freeside
1893
1894 # }}}
1895
1896 # {{{ Limit the result set based on content
1897
1898 # {{{ sub Limit
1899
1900 =head2 Limit
1901
1902 Takes a paramhash with the fields FIELD, OPERATOR, VALUE and DESCRIPTION
1903 Generally best called from LimitFoo methods
1904
1905 =cut
1906
1907 sub Limit {
1908     my $self = shift;
1909     my %args = (
1910         FIELD       => undef,
1911         OPERATOR    => '=',
1912         VALUE       => undef,
1913         DESCRIPTION => undef,
1914         @_
1915     );
1916     $args{'DESCRIPTION'} = $self->loc(
1917         "[_1] [_2] [_3]",  $args{'FIELD'},
1918         $args{'OPERATOR'}, $args{'VALUE'}
1919         )
1920         if ( !defined $args{'DESCRIPTION'} );
1921
1922     my $index = $self->_NextIndex;
1923
1924 # make the TicketRestrictions hash the equivalent of whatever we just passed in;
1925
1926     %{ $self->{'TicketRestrictions'}{$index} } = %args;
1927
1928     $self->{'RecalcTicketLimits'} = 1;
1929
1930 # If we're looking at the effective id, we don't want to append the other clause
1931 # which limits us to tickets where id = effective id
1932     if ( $args{'FIELD'} eq 'EffectiveId'
1933         && ( !$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) )
1934     {
1935         $self->{'looking_at_effective_id'} = 1;
1936     }
1937
1938     if ( $args{'FIELD'} eq 'Type'
1939         && ( !$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) )
1940     {
1941         $self->{'looking_at_type'} = 1;
1942     }
1943
1944     return ($index);
1945 }
1946
1947 # }}}
1948
1949 =head2 FreezeLimits
1950
1951 Returns a frozen string suitable for handing back to ThawLimits.
1952
1953 =cut
1954
1955 sub _FreezeThawKeys {
1956     'TicketRestrictions', 'restriction_index', 'looking_at_effective_id',
1957         'looking_at_type';
1958 }
1959
1960 # {{{ sub FreezeLimits
1961
1962 sub FreezeLimits {
1963     my $self = shift;
1964     require Storable;
1965     require MIME::Base64;
1966     MIME::Base64::base64_encode(
1967         Storable::freeze( \@{$self}{ $self->_FreezeThawKeys } ) );
1968 }
1969
1970 # }}}
1971
1972 =head2 ThawLimits
1973
1974 Take a frozen Limits string generated by FreezeLimits and make this tickets
1975 object have that set of limits.
1976
1977 =cut
1978
1979 # {{{ sub ThawLimits
1980
1981 sub ThawLimits {
1982     my $self = shift;
1983     my $in   = shift;
1984
1985     #if we don't have $in, get outta here.
1986     return undef unless ($in);
1987
1988     $self->{'RecalcTicketLimits'} = 1;
1989
1990     require Storable;
1991     require MIME::Base64;
1992
1993     #We don't need to die if the thaw fails.
1994     @{$self}{ $self->_FreezeThawKeys }
1995         = eval { @{ Storable::thaw( MIME::Base64::base64_decode($in) ) }; };
1996
1997     $RT::Logger->error($@) if $@;
1998
1999 }
2000
2001 # }}}
2002
2003 # {{{ Limit by enum or foreign key
2004
2005 # {{{ sub LimitQueue
2006
2007 =head2 LimitQueue
2008
2009 LimitQueue takes a paramhash with the fields OPERATOR and VALUE.
2010 OPERATOR is one of = or !=. (It defaults to =).
2011 VALUE is a queue id or Name.
2012
2013
2014 =cut
2015
2016 sub LimitQueue {
2017     my $self = shift;
2018     my %args = (
2019         VALUE    => undef,
2020         OPERATOR => '=',
2021         @_
2022     );
2023
2024     #TODO  VALUE should also take queue objects
2025     if ( defined $args{'VALUE'} && $args{'VALUE'} !~ /^\d+$/ ) {
2026         my $queue = new RT::Queue( $self->CurrentUser );
2027         $queue->Load( $args{'VALUE'} );
2028         $args{'VALUE'} = $queue->Id;
2029     }
2030
2031     # What if they pass in an Id?  Check for isNum() and convert to
2032     # string.
2033
2034     #TODO check for a valid queue here
2035
2036     $self->Limit(
2037         FIELD       => 'Queue',
2038         VALUE       => $args{'VALUE'},
2039         OPERATOR    => $args{'OPERATOR'},
2040         DESCRIPTION => join(
2041             ' ', $self->loc('Queue'), $args{'OPERATOR'}, $args{'VALUE'},
2042         ),
2043     );
2044
2045 }
2046
2047 # }}}
2048
2049 # {{{ sub LimitStatus
2050
2051 =head2 LimitStatus
2052
2053 Takes a paramhash with the fields OPERATOR and VALUE.
2054 OPERATOR is one of = or !=.
2055 VALUE is a status.
2056
2057 RT adds Status != 'deleted' until object has
2058 allow_deleted_search internal property set.
2059 $tickets->{'allow_deleted_search'} = 1;
2060 $tickets->LimitStatus( VALUE => 'deleted' );
2061
2062 =cut
2063
2064 sub LimitStatus {
2065     my $self = shift;
2066     my %args = (
2067         OPERATOR => '=',
2068         @_
2069     );
2070     $self->Limit(
2071         FIELD       => 'Status',
2072         VALUE       => $args{'VALUE'},
2073         OPERATOR    => $args{'OPERATOR'},
2074         DESCRIPTION => join( ' ',
2075             $self->loc('Status'), $args{'OPERATOR'},
2076             $self->loc( $args{'VALUE'} ) ),
2077     );
2078 }
2079
2080 # }}}
2081
2082 # {{{ sub IgnoreType
2083
2084 =head2 IgnoreType
2085
2086 If called, this search will not automatically limit the set of results found
2087 to tickets of type "Ticket". Tickets of other types, such as "project" and
2088 "approval" will be found.
2089
2090 =cut
2091
2092 sub IgnoreType {
2093     my $self = shift;
2094
2095     # Instead of faking a Limit that later gets ignored, fake up the
2096     # fact that we're already looking at type, so that the check in
2097     # Tickets_Overlay_SQL/FromSQL goes down the right branch
2098
2099     #  $self->LimitType(VALUE => '__any');
2100     $self->{looking_at_type} = 1;
2101 }
2102
2103 # }}}
2104
2105 # {{{ sub LimitType
2106
2107 =head2 LimitType
2108
2109 Takes a paramhash with the fields OPERATOR and VALUE.
2110 OPERATOR is one of = or !=, it defaults to "=".
2111 VALUE is a string to search for in the type of the ticket.
2112
2113
2114
2115 =cut
2116
2117 sub LimitType {
2118     my $self = shift;
2119     my %args = (
2120         OPERATOR => '=',
2121         VALUE    => undef,
2122         @_
2123     );
2124     $self->Limit(
2125         FIELD       => 'Type',
2126         VALUE       => $args{'VALUE'},
2127         OPERATOR    => $args{'OPERATOR'},
2128         DESCRIPTION => join( ' ',
2129             $self->loc('Type'), $args{'OPERATOR'}, $args{'Limit'}, ),
2130     );
2131 }
2132
2133 # }}}
2134
2135 # }}}
2136
2137 # {{{ Limit by string field
2138
2139 # {{{ sub LimitSubject
2140
2141 =head2 LimitSubject
2142
2143 Takes a paramhash with the fields OPERATOR and VALUE.
2144 OPERATOR is one of = or !=.
2145 VALUE is a string to search for in the subject of the ticket.
2146
2147 =cut
2148
2149 sub LimitSubject {
2150     my $self = shift;
2151     my %args = (@_);
2152     $self->Limit(
2153         FIELD       => 'Subject',
2154         VALUE       => $args{'VALUE'},
2155         OPERATOR    => $args{'OPERATOR'},
2156         DESCRIPTION => join( ' ',
2157             $self->loc('Subject'), $args{'OPERATOR'}, $args{'VALUE'}, ),
2158     );
2159 }
2160
2161 # }}}
2162
2163 # }}}
2164
2165 # {{{ Limit based on ticket numerical attributes
2166 # Things that can be > < = !=
2167
2168 # {{{ sub LimitId
2169
2170 =head2 LimitId
2171
2172 Takes a paramhash with the fields OPERATOR and VALUE.
2173 OPERATOR is one of =, >, < or !=.
2174 VALUE is a ticket Id to search for
2175
2176 =cut
2177
2178 sub LimitId {
2179     my $self = shift;
2180     my %args = (
2181         OPERATOR => '=',
2182         @_
2183     );
2184
2185     $self->Limit(
2186         FIELD       => 'id',
2187         VALUE       => $args{'VALUE'},
2188         OPERATOR    => $args{'OPERATOR'},
2189         DESCRIPTION =>
2190             join( ' ', $self->loc('Id'), $args{'OPERATOR'}, $args{'VALUE'}, ),
2191     );
2192 }
2193
2194 # }}}
2195
2196 # {{{ sub LimitPriority
2197
2198 =head2 LimitPriority
2199
2200 Takes a paramhash with the fields OPERATOR and VALUE.
2201 OPERATOR is one of =, >, < or !=.
2202 VALUE is a value to match the ticket\'s priority against
2203
2204 =cut
2205
2206 sub LimitPriority {
2207     my $self = shift;
2208     my %args = (@_);
2209     $self->Limit(
2210         FIELD       => 'Priority',
2211         VALUE       => $args{'VALUE'},
2212         OPERATOR    => $args{'OPERATOR'},
2213         DESCRIPTION => join( ' ',
2214             $self->loc('Priority'),
2215             $args{'OPERATOR'}, $args{'VALUE'}, ),
2216     );
2217 }
2218
2219 # }}}
2220
2221 # {{{ sub LimitInitialPriority
2222
2223 =head2 LimitInitialPriority
2224
2225 Takes a paramhash with the fields OPERATOR and VALUE.
2226 OPERATOR is one of =, >, < or !=.
2227 VALUE is a value to match the ticket\'s initial priority against
2228
2229
2230 =cut
2231
2232 sub LimitInitialPriority {
2233     my $self = shift;
2234     my %args = (@_);
2235     $self->Limit(
2236         FIELD       => 'InitialPriority',
2237         VALUE       => $args{'VALUE'},
2238         OPERATOR    => $args{'OPERATOR'},
2239         DESCRIPTION => join( ' ',
2240             $self->loc('Initial Priority'), $args{'OPERATOR'},
2241             $args{'VALUE'}, ),
2242     );
2243 }
2244
2245 # }}}
2246
2247 # {{{ sub LimitFinalPriority
2248
2249 =head2 LimitFinalPriority
2250
2251 Takes a paramhash with the fields OPERATOR and VALUE.
2252 OPERATOR is one of =, >, < or !=.
2253 VALUE is a value to match the ticket\'s final priority against
2254
2255 =cut
2256
2257 sub LimitFinalPriority {
2258     my $self = shift;
2259     my %args = (@_);
2260     $self->Limit(
2261         FIELD       => 'FinalPriority',
2262         VALUE       => $args{'VALUE'},
2263         OPERATOR    => $args{'OPERATOR'},
2264         DESCRIPTION => join( ' ',
2265             $self->loc('Final Priority'), $args{'OPERATOR'},
2266             $args{'VALUE'}, ),
2267     );
2268 }
2269
2270 # }}}
2271
2272 # {{{ sub LimitTimeWorked
2273
2274 =head2 LimitTimeWorked
2275
2276 Takes a paramhash with the fields OPERATOR and VALUE.
2277 OPERATOR is one of =, >, < or !=.
2278 VALUE is a value to match the ticket's TimeWorked attribute
2279
2280 =cut
2281
2282 sub LimitTimeWorked {
2283     my $self = shift;
2284     my %args = (@_);
2285     $self->Limit(
2286         FIELD       => 'TimeWorked',
2287         VALUE       => $args{'VALUE'},
2288         OPERATOR    => $args{'OPERATOR'},
2289         DESCRIPTION => join( ' ',
2290             $self->loc('Time Worked'),
2291             $args{'OPERATOR'}, $args{'VALUE'}, ),
2292     );
2293 }
2294
2295 # }}}
2296
2297 # {{{ sub LimitTimeLeft
2298
2299 =head2 LimitTimeLeft
2300
2301 Takes a paramhash with the fields OPERATOR and VALUE.
2302 OPERATOR is one of =, >, < or !=.
2303 VALUE is a value to match the ticket's TimeLeft attribute
2304
2305 =cut
2306
2307 sub LimitTimeLeft {
2308     my $self = shift;
2309     my %args = (@_);
2310     $self->Limit(
2311         FIELD       => 'TimeLeft',
2312         VALUE       => $args{'VALUE'},
2313         OPERATOR    => $args{'OPERATOR'},
2314         DESCRIPTION => join( ' ',
2315             $self->loc('Time Left'),
2316             $args{'OPERATOR'}, $args{'VALUE'}, ),
2317     );
2318 }
2319
2320 # }}}
2321
2322 # }}}
2323
2324 # {{{ Limiting based on attachment attributes
2325
2326 # {{{ sub LimitContent
2327
2328 =head2 LimitContent
2329
2330 Takes a paramhash with the fields OPERATOR and VALUE.
2331 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2332 VALUE is a string to search for in the body of the ticket
2333
2334 =cut
2335
2336 sub LimitContent {
2337     my $self = shift;
2338     my %args = (@_);
2339     $self->Limit(
2340         FIELD       => 'Content',
2341         VALUE       => $args{'VALUE'},
2342         OPERATOR    => $args{'OPERATOR'},
2343         DESCRIPTION => join( ' ',
2344             $self->loc('Ticket content'), $args{'OPERATOR'},
2345             $args{'VALUE'}, ),
2346     );
2347 }
2348
2349 # }}}
2350
2351 # {{{ sub LimitFilename
2352
2353 =head2 LimitFilename
2354
2355 Takes a paramhash with the fields OPERATOR and VALUE.
2356 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2357 VALUE is a string to search for in the body of the ticket
2358
2359 =cut
2360
2361 sub LimitFilename {
2362     my $self = shift;
2363     my %args = (@_);
2364     $self->Limit(
2365         FIELD       => 'Filename',
2366         VALUE       => $args{'VALUE'},
2367         OPERATOR    => $args{'OPERATOR'},
2368         DESCRIPTION => join( ' ',
2369             $self->loc('Attachment filename'), $args{'OPERATOR'},
2370             $args{'VALUE'}, ),
2371     );
2372 }
2373
2374 # }}}
2375 # {{{ sub LimitContentType
2376
2377 =head2 LimitContentType
2378
2379 Takes a paramhash with the fields OPERATOR and VALUE.
2380 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2381 VALUE is a content type to search ticket attachments for
2382
2383 =cut
2384
2385 sub LimitContentType {
2386     my $self = shift;
2387     my %args = (@_);
2388     $self->Limit(
2389         FIELD       => 'ContentType',
2390         VALUE       => $args{'VALUE'},
2391         OPERATOR    => $args{'OPERATOR'},
2392         DESCRIPTION => join( ' ',
2393             $self->loc('Ticket content type'), $args{'OPERATOR'},
2394             $args{'VALUE'}, ),
2395     );
2396 }
2397
2398 # }}}
2399
2400 # }}}
2401
2402 # {{{ Limiting based on people
2403
2404 # {{{ sub LimitOwner
2405
2406 =head2 LimitOwner
2407
2408 Takes a paramhash with the fields OPERATOR and VALUE.
2409 OPERATOR is one of = or !=.
2410 VALUE is a user id.
2411
2412 =cut
2413
2414 sub LimitOwner {
2415     my $self = shift;
2416     my %args = (
2417         OPERATOR => '=',
2418         @_
2419     );
2420
2421     my $owner = new RT::User( $self->CurrentUser );
2422     $owner->Load( $args{'VALUE'} );
2423
2424     # FIXME: check for a valid $owner
2425     $self->Limit(
2426         FIELD       => 'Owner',
2427         VALUE       => $args{'VALUE'},
2428         OPERATOR    => $args{'OPERATOR'},
2429         DESCRIPTION => join( ' ',
2430             $self->loc('Owner'), $args{'OPERATOR'}, $owner->Name(), ),
2431     );
2432
2433 }
2434
2435 # }}}
2436
2437 # {{{ Limiting watchers
2438
2439 # {{{ sub LimitWatcher
2440
2441 =head2 LimitWatcher
2442
2443   Takes a paramhash with the fields OPERATOR, TYPE and VALUE.
2444   OPERATOR is one of =, LIKE, NOT LIKE or !=.
2445   VALUE is a value to match the ticket\'s watcher email addresses against
2446   TYPE is the sort of watchers you want to match against. Leave it undef if you want to search all of them
2447
2448
2449 =cut
2450
2451 sub LimitWatcher {
2452     my $self = shift;
2453     my %args = (
2454         OPERATOR => '=',
2455         VALUE    => undef,
2456         TYPE     => undef,
2457         @_
2458     );
2459
2460     #build us up a description
2461     my ( $watcher_type, $desc );
2462     if ( $args{'TYPE'} ) {
2463         $watcher_type = $args{'TYPE'};
2464     }
2465     else {
2466         $watcher_type = "Watcher";
2467     }
2468
2469     $self->Limit(
2470         FIELD       => $watcher_type,
2471         VALUE       => $args{'VALUE'},
2472         OPERATOR    => $args{'OPERATOR'},
2473         TYPE        => $args{'TYPE'},
2474         DESCRIPTION => join( ' ',
2475             $self->loc($watcher_type),
2476             $args{'OPERATOR'}, $args{'VALUE'}, ),
2477     );
2478 }
2479
2480 # }}}
2481
2482 # }}}
2483
2484 # }}}
2485
2486 # {{{ Limiting based on links
2487
2488 # {{{ LimitLinkedTo
2489
2490 =head2 LimitLinkedTo
2491
2492 LimitLinkedTo takes a paramhash with two fields: TYPE and TARGET
2493 TYPE limits the sort of link we want to search on
2494
2495 TYPE = { RefersTo, MemberOf, DependsOn }
2496
2497 TARGET is the id or URI of the TARGET of the link
2498
2499 =cut
2500
2501 sub LimitLinkedTo {
2502     my $self = shift;
2503     my %args = (
2504         TARGET   => undef,
2505         TYPE     => undef,
2506         OPERATOR => '=',
2507         @_
2508     );
2509
2510     $self->Limit(
2511         FIELD       => 'LinkedTo',
2512         BASE        => undef,
2513         TARGET      => $args{'TARGET'},
2514         TYPE        => $args{'TYPE'},
2515         DESCRIPTION => $self->loc(
2516             "Tickets [_1] by [_2]",
2517             $self->loc( $args{'TYPE'} ),
2518             $args{'TARGET'}
2519         ),
2520         OPERATOR    => $args{'OPERATOR'},
2521     );
2522 }
2523
2524 # }}}
2525
2526 # {{{ LimitLinkedFrom
2527
2528 =head2 LimitLinkedFrom
2529
2530 LimitLinkedFrom takes a paramhash with two fields: TYPE and BASE
2531 TYPE limits the sort of link we want to search on
2532
2533
2534 BASE is the id or URI of the BASE of the link
2535
2536 =cut
2537
2538 sub LimitLinkedFrom {
2539     my $self = shift;
2540     my %args = (
2541         BASE     => undef,
2542         TYPE     => undef,
2543         OPERATOR => '=',
2544         @_
2545     );
2546
2547     # translate RT2 From/To naming to RT3 TicketSQL naming
2548     my %fromToMap = qw(DependsOn DependentOn
2549         MemberOf  HasMember
2550         RefersTo  ReferredToBy);
2551
2552     my $type = $args{'TYPE'};
2553     $type = $fromToMap{$type} if exists( $fromToMap{$type} );
2554
2555     $self->Limit(
2556         FIELD       => 'LinkedTo',
2557         TARGET      => undef,
2558         BASE        => $args{'BASE'},
2559         TYPE        => $type,
2560         DESCRIPTION => $self->loc(
2561             "Tickets [_1] [_2]",
2562             $self->loc( $args{'TYPE'} ),
2563             $args{'BASE'},
2564         ),
2565         OPERATOR    => $args{'OPERATOR'},
2566     );
2567 }
2568
2569 # }}}
2570
2571 # {{{ LimitMemberOf
2572 sub LimitMemberOf {
2573     my $self      = shift;
2574     my $ticket_id = shift;
2575     return $self->LimitLinkedTo(
2576         @_,
2577         TARGET => $ticket_id,
2578         TYPE   => 'MemberOf',
2579     );
2580 }
2581
2582 # }}}
2583
2584 # {{{ LimitHasMember
2585 sub LimitHasMember {
2586     my $self      = shift;
2587     my $ticket_id = shift;
2588     return $self->LimitLinkedFrom(
2589         @_,
2590         BASE => "$ticket_id",
2591         TYPE => 'HasMember',
2592     );
2593
2594 }
2595
2596 # }}}
2597
2598 # {{{ LimitDependsOn
2599
2600 sub LimitDependsOn {
2601     my $self      = shift;
2602     my $ticket_id = shift;
2603     return $self->LimitLinkedTo(
2604         @_,
2605         TARGET => $ticket_id,
2606         TYPE   => 'DependsOn',
2607     );
2608
2609 }
2610
2611 # }}}
2612
2613 # {{{ LimitDependedOnBy
2614
2615 sub LimitDependedOnBy {
2616     my $self      = shift;
2617     my $ticket_id = shift;
2618     return $self->LimitLinkedFrom(
2619         @_,
2620         BASE => $ticket_id,
2621         TYPE => 'DependentOn',
2622     );
2623
2624 }
2625
2626 # }}}
2627
2628 # {{{ LimitRefersTo
2629
2630 sub LimitRefersTo {
2631     my $self      = shift;
2632     my $ticket_id = shift;
2633     return $self->LimitLinkedTo(
2634         @_,
2635         TARGET => $ticket_id,
2636         TYPE   => 'RefersTo',
2637     );
2638
2639 }
2640
2641 # }}}
2642
2643 # {{{ LimitReferredToBy
2644
2645 sub LimitReferredToBy {
2646     my $self      = shift;
2647     my $ticket_id = shift;
2648     return $self->LimitLinkedFrom(
2649         @_,
2650         BASE => $ticket_id,
2651         TYPE => 'ReferredToBy',
2652     );
2653 }
2654
2655 # }}}
2656
2657 # }}}
2658
2659 # {{{ limit based on ticket date attribtes
2660
2661 # {{{ sub LimitDate
2662
2663 =head2 LimitDate (FIELD => 'DateField', OPERATOR => $oper, VALUE => $ISODate)
2664
2665 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
2666
2667 OPERATOR is one of > or <
2668 VALUE is a date and time in ISO format in GMT
2669 FIELD is one of Starts, Started, Told, Created, Resolved, LastUpdated
2670
2671 There are also helper functions of the form LimitFIELD that eliminate
2672 the need to pass in a FIELD argument.
2673
2674 =cut
2675
2676 sub LimitDate {
2677     my $self = shift;
2678     my %args = (
2679         FIELD    => undef,
2680         VALUE    => undef,
2681         OPERATOR => undef,
2682
2683         @_
2684     );
2685
2686     #Set the description if we didn't get handed it above
2687     unless ( $args{'DESCRIPTION'} ) {
2688         $args{'DESCRIPTION'} = $args{'FIELD'} . " "
2689             . $args{'OPERATOR'} . " "
2690             . $args{'VALUE'} . " GMT";
2691     }
2692
2693     $self->Limit(%args);
2694
2695 }
2696
2697 # }}}
2698
2699 sub LimitCreated {
2700     my $self = shift;
2701     $self->LimitDate( FIELD => 'Created', @_ );
2702 }
2703
2704 sub LimitDue {
2705     my $self = shift;
2706     $self->LimitDate( FIELD => 'Due', @_ );
2707
2708 }
2709
2710 sub LimitStarts {
2711     my $self = shift;
2712     $self->LimitDate( FIELD => 'Starts', @_ );
2713
2714 }
2715
2716 sub LimitStarted {
2717     my $self = shift;
2718     $self->LimitDate( FIELD => 'Started', @_ );
2719 }
2720
2721 sub LimitResolved {
2722     my $self = shift;
2723     $self->LimitDate( FIELD => 'Resolved', @_ );
2724 }
2725
2726 sub LimitTold {
2727     my $self = shift;
2728     $self->LimitDate( FIELD => 'Told', @_ );
2729 }
2730
2731 sub LimitLastUpdated {
2732     my $self = shift;
2733     $self->LimitDate( FIELD => 'LastUpdated', @_ );
2734 }
2735
2736 #
2737 # {{{ sub LimitTransactionDate
2738
2739 =head2 LimitTransactionDate (OPERATOR => $oper, VALUE => $ISODate)
2740
2741 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
2742
2743 OPERATOR is one of > or <
2744 VALUE is a date and time in ISO format in GMT
2745
2746
2747 =cut
2748
2749 sub LimitTransactionDate {
2750     my $self = shift;
2751     my %args = (
2752         FIELD    => 'TransactionDate',
2753         VALUE    => undef,
2754         OPERATOR => undef,
2755
2756         @_
2757     );
2758
2759     #  <20021217042756.GK28744@pallas.fsck.com>
2760     #    "Kill It" - Jesse.
2761
2762     #Set the description if we didn't get handed it above
2763     unless ( $args{'DESCRIPTION'} ) {
2764         $args{'DESCRIPTION'} = $args{'FIELD'} . " "
2765             . $args{'OPERATOR'} . " "
2766             . $args{'VALUE'} . " GMT";
2767     }
2768
2769     $self->Limit(%args);
2770
2771 }
2772
2773 # }}}
2774
2775 # }}}
2776
2777 # {{{ Limit based on custom fields
2778 # {{{ sub LimitCustomField
2779
2780 =head2 LimitCustomField
2781
2782 Takes a paramhash of key/value pairs with the following keys:
2783
2784 =over 4
2785
2786 =item CUSTOMFIELD - CustomField name or id.  If a name is passed, an additional parameter QUEUE may also be passed to distinguish the custom field.
2787
2788 =item OPERATOR - The usual Limit operators
2789
2790 =item VALUE - The value to compare against
2791
2792 =back
2793
2794 =cut
2795
2796 sub LimitCustomField {
2797     my $self = shift;
2798     my %args = (
2799         VALUE       => undef,
2800         CUSTOMFIELD => undef,
2801         OPERATOR    => '=',
2802         DESCRIPTION => undef,
2803         FIELD       => 'CustomFieldValue',
2804         QUOTEVALUE  => 1,
2805         @_
2806     );
2807
2808     my $CF = RT::CustomField->new( $self->CurrentUser );
2809     if ( $args{CUSTOMFIELD} =~ /^\d+$/ ) {
2810         $CF->Load( $args{CUSTOMFIELD} );
2811     }
2812     else {
2813         $CF->LoadByNameAndQueue(
2814             Name  => $args{CUSTOMFIELD},
2815             Queue => $args{QUEUE}
2816         );
2817         $args{CUSTOMFIELD} = $CF->Id;
2818     }
2819
2820     #If we are looking to compare with a null value.
2821     if ( $args{'OPERATOR'} =~ /^is$/i ) {
2822         $args{'DESCRIPTION'}
2823             ||= $self->loc( "Custom field [_1] has no value.", $CF->Name );
2824     }
2825     elsif ( $args{'OPERATOR'} =~ /^is not$/i ) {
2826         $args{'DESCRIPTION'}
2827             ||= $self->loc( "Custom field [_1] has a value.", $CF->Name );
2828     }
2829
2830     # if we're not looking to compare with a null value
2831     else {
2832         $args{'DESCRIPTION'} ||= $self->loc( "Custom field [_1] [_2] [_3]",
2833             $CF->Name, $args{OPERATOR}, $args{VALUE} );
2834     }
2835
2836     if ( defined $args{'QUEUE'} && $args{'QUEUE'} =~ /\D/ ) {
2837         my $QueueObj = RT::Queue->new( $self->CurrentUser );
2838         $QueueObj->Load( $args{'QUEUE'} );
2839         $args{'QUEUE'} = $QueueObj->Id;
2840     }
2841     delete $args{'QUEUE'} unless defined $args{'QUEUE'} && length $args{'QUEUE'};
2842
2843     my @rest;
2844     @rest = ( ENTRYAGGREGATOR => 'AND' )
2845         if ( $CF->Type eq 'SelectMultiple' );
2846
2847     $self->Limit(
2848         VALUE => $args{VALUE},
2849         FIELD => "CF"
2850             .(defined $args{'QUEUE'}? ".{$args{'QUEUE'}}" : '' )
2851             .".{" . $CF->Name . "}",
2852         OPERATOR    => $args{OPERATOR},
2853         CUSTOMFIELD => 1,
2854         @rest,
2855     );
2856
2857     $self->{'RecalcTicketLimits'} = 1;
2858 }
2859
2860 # }}}
2861 # }}}
2862
2863 # {{{ sub _NextIndex
2864
2865 =head2 _NextIndex
2866
2867 Keep track of the counter for the array of restrictions
2868
2869 =cut
2870
2871 sub _NextIndex {
2872     my $self = shift;
2873     return ( $self->{'restriction_index'}++ );
2874 }
2875
2876 # }}}
2877
2878 # }}}
2879
2880 # {{{ Core bits to make this a DBIx::SearchBuilder object
2881
2882 # {{{ sub _Init
2883 sub _Init {
2884     my $self = shift;
2885     $self->{'table'}                   = "Tickets";
2886     $self->{'RecalcTicketLimits'}      = 1;
2887     $self->{'looking_at_effective_id'} = 0;
2888     $self->{'looking_at_type'}         = 0;
2889     $self->{'restriction_index'}       = 1;
2890     $self->{'primary_key'}             = "id";
2891     delete $self->{'items_array'};
2892     delete $self->{'item_map'};
2893     delete $self->{'columns_to_display'};
2894     $self->SUPER::_Init(@_);
2895
2896     $self->_InitSQL;
2897
2898 }
2899
2900 # }}}
2901
2902 # {{{ sub Count
2903 sub Count {
2904     my $self = shift;
2905     $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2906     return ( $self->SUPER::Count() );
2907 }
2908
2909 # }}}
2910
2911 # {{{ sub CountAll
2912 sub CountAll {
2913     my $self = shift;
2914     $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2915     return ( $self->SUPER::CountAll() );
2916 }
2917
2918 # }}}
2919
2920 # {{{ sub ItemsArrayRef
2921
2922 =head2 ItemsArrayRef
2923
2924 Returns a reference to the set of all items found in this search
2925
2926 =cut
2927
2928 sub ItemsArrayRef {
2929     my $self = shift;
2930
2931     return $self->{'items_array'} if $self->{'items_array'};
2932
2933     my $placeholder = $self->_ItemsCounter;
2934     $self->GotoFirstItem();
2935     while ( my $item = $self->Next ) {
2936         push( @{ $self->{'items_array'} }, $item );
2937     }
2938     $self->GotoItem($placeholder);
2939     $self->{'items_array'}
2940         = $self->ItemsOrderBy( $self->{'items_array'} );
2941
2942     return $self->{'items_array'};
2943 }
2944
2945 sub ItemsArrayRefWindow {
2946     my $self = shift;
2947     my $window = shift;
2948
2949     my @old = ($self->_ItemsCounter, $self->RowsPerPage, $self->FirstRow+1);
2950
2951     $self->RowsPerPage( $window );
2952     $self->FirstRow(1);
2953     $self->GotoFirstItem;
2954
2955     my @res;
2956     while ( my $item = $self->Next ) {
2957         push @res, $item;
2958     }
2959
2960     $self->RowsPerPage( $old[1] );
2961     $self->FirstRow( $old[2] );
2962     $self->GotoItem( $old[0] );
2963
2964     return \@res;
2965 }
2966
2967 # }}}
2968
2969 # {{{ sub Next
2970 sub Next {
2971     my $self = shift;
2972
2973     $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2974
2975     my $Ticket = $self->SUPER::Next;
2976     return $Ticket unless $Ticket;
2977
2978     if ( $Ticket->__Value('Status') eq 'deleted'
2979         && !$self->{'allow_deleted_search'} )
2980     {
2981         return $self->Next;
2982     }
2983     elsif ( RT->Config->Get('UseSQLForACLChecks') ) {
2984         # if we found a ticket with this option enabled then
2985         # all tickets we found are ACLed, cache this fact
2986         my $key = join ";:;", $self->CurrentUser->id, 'ShowTicket', 'RT::Ticket-'. $Ticket->id;
2987         $RT::Principal::_ACL_CACHE->set( $key => 1 );
2988         return $Ticket;
2989     }
2990     elsif ( $Ticket->CurrentUserHasRight('ShowTicket') ) {
2991         # has rights
2992         return $Ticket;
2993     }
2994     else {
2995         # If the user doesn't have the right to show this ticket
2996         return $self->Next;
2997     }
2998 }
2999
3000 sub _DoSearch {
3001     my $self = shift;
3002     $self->CurrentUserCanSee if RT->Config->Get('UseSQLForACLChecks');
3003     return $self->SUPER::_DoSearch( @_ );
3004 }
3005
3006 sub _DoCount {
3007     my $self = shift;
3008     $self->CurrentUserCanSee if RT->Config->Get('UseSQLForACLChecks');
3009     return $self->SUPER::_DoCount( @_ );
3010 }
3011
3012 sub _RolesCanSee {
3013     my $self = shift;
3014
3015     my $cache_key = 'RolesHasRight;:;ShowTicket';
3016  
3017     if ( my $cached = $RT::Principal::_ACL_CACHE->fetch( $cache_key ) ) {
3018         return %$cached;
3019     }
3020
3021     my $ACL = RT::ACL->new( $RT::SystemUser );
3022     $ACL->Limit( FIELD => 'RightName', VALUE => 'ShowTicket' );
3023     $ACL->Limit( FIELD => 'PrincipalType', OPERATOR => '!=', VALUE => 'Group' );
3024     my $principal_alias = $ACL->Join(
3025         ALIAS1 => 'main',
3026         FIELD1 => 'PrincipalId',
3027         TABLE2 => 'Principals',
3028         FIELD2 => 'id',
3029     );
3030     $ACL->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3031
3032     my %res = ();
3033     while ( my $ACE = $ACL->Next ) {
3034         my $role = $ACE->PrincipalType;
3035         my $type = $ACE->ObjectType;
3036         if ( $type eq 'RT::System' ) {
3037             $res{ $role } = 1;
3038         }
3039         elsif ( $type eq 'RT::Queue' ) {
3040             next if $res{ $role } && !ref $res{ $role };
3041             push @{ $res{ $role } ||= [] }, $ACE->ObjectId;
3042         }
3043         else {
3044             $RT::Logger->error('ShowTicket right is granted on unsupported object');
3045         }
3046     }
3047     $RT::Principal::_ACL_CACHE->set( $cache_key => \%res );
3048     return %res;
3049 }
3050
3051 sub _DirectlyCanSeeIn {
3052     my $self = shift;
3053     my $id = $self->CurrentUser->id;
3054
3055     my $cache_key = 'User-'. $id .';:;ShowTicket;:;DirectlyCanSeeIn';
3056     if ( my $cached = $RT::Principal::_ACL_CACHE->fetch( $cache_key ) ) {
3057         return @$cached;
3058     }
3059
3060     my $ACL = RT::ACL->new( $RT::SystemUser );
3061     $ACL->Limit( FIELD => 'RightName', VALUE => 'ShowTicket' );
3062     my $principal_alias = $ACL->Join(
3063         ALIAS1 => 'main',
3064         FIELD1 => 'PrincipalId',
3065         TABLE2 => 'Principals',
3066         FIELD2 => 'id',
3067     );
3068     $ACL->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3069     my $cgm_alias = $ACL->Join(
3070         ALIAS1 => 'main',
3071         FIELD1 => 'PrincipalId',
3072         TABLE2 => 'CachedGroupMembers',
3073         FIELD2 => 'GroupId',
3074     );
3075     $ACL->Limit( ALIAS => $cgm_alias, FIELD => 'MemberId', VALUE => $id );
3076     $ACL->Limit( ALIAS => $cgm_alias, FIELD => 'Disabled', VALUE => 0 );
3077
3078     my @res = ();
3079     while ( my $ACE = $ACL->Next ) {
3080         my $type = $ACE->ObjectType;
3081         if ( $type eq 'RT::System' ) {
3082             # If user is direct member of a group that has the right
3083             # on the system then he can see any ticket
3084             $RT::Principal::_ACL_CACHE->set( $cache_key => [-1] );
3085             return (-1);
3086         }
3087         elsif ( $type eq 'RT::Queue' ) {
3088             push @res, $ACE->ObjectId;
3089         }
3090         else {
3091             $RT::Logger->error('ShowTicket right is granted on unsupported object');
3092         }
3093     }
3094     $RT::Principal::_ACL_CACHE->set( $cache_key => \@res );
3095     return @res;
3096 }
3097
3098 sub CurrentUserCanSee {
3099     my $self = shift;
3100     return if $self->{'_sql_current_user_can_see_applied'};
3101
3102     return $self->{'_sql_current_user_can_see_applied'} = 1
3103         if $self->CurrentUser->UserObj->HasRight(
3104             Right => 'SuperUser', Object => $RT::System
3105         );
3106
3107     my $id = $self->CurrentUser->id;
3108
3109     # directly can see in all queues then we have nothing to do
3110     my @direct_queues = $self->_DirectlyCanSeeIn;
3111     return $self->{'_sql_current_user_can_see_applied'} = 1
3112         if @direct_queues && $direct_queues[0] == -1;
3113
3114     my %roles = $self->_RolesCanSee;
3115     {
3116         my %skip = map { $_ => 1 } @direct_queues;
3117         foreach my $role ( keys %roles ) {
3118             next unless ref $roles{ $role };
3119
3120             my @queues = grep !$skip{$_}, @{ $roles{ $role } };
3121             if ( @queues ) {
3122                 $roles{ $role } = \@queues;
3123             } else {
3124                 delete $roles{ $role };
3125             }
3126         }
3127     }
3128
3129 # there is no global watchers, only queues and tickes, if at
3130 # some point we will add global roles then it's gonna blow
3131 # the idea here is that if the right is set globaly for a role
3132 # and user plays this role for a queue directly not a ticket
3133 # then we have to check in advance
3134     if ( my @tmp = grep $_ ne 'Owner' && !ref $roles{ $_ }, keys %roles ) {
3135
3136         my $groups = RT::Groups->new( $RT::SystemUser );
3137         $groups->Limit( FIELD => 'Domain', VALUE => 'RT::Queue-Role' );
3138         foreach ( @tmp ) {
3139             $groups->Limit( FIELD => 'Type', VALUE => $_ );
3140         }
3141         my $principal_alias = $groups->Join(
3142             ALIAS1 => 'main',
3143             FIELD1 => 'id',
3144             TABLE2 => 'Principals',
3145             FIELD2 => 'id',
3146         );
3147         $groups->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3148         my $cgm_alias = $groups->Join(
3149             ALIAS1 => 'main',
3150             FIELD1 => 'id',
3151             TABLE2 => 'CachedGroupMembers',
3152             FIELD2 => 'GroupId',
3153         );
3154         $groups->Limit( ALIAS => $cgm_alias, FIELD => 'MemberId', VALUE => $id );
3155         $groups->Limit( ALIAS => $cgm_alias, FIELD => 'Disabled', VALUE => 0 );
3156         while ( my $group = $groups->Next ) {
3157             push @direct_queues, $group->Instance;
3158         }
3159     }
3160
3161     unless ( @direct_queues || keys %roles ) {
3162         $self->SUPER::Limit(
3163             SUBCLAUSE => 'ACL',
3164             ALIAS => 'main',
3165             FIELD => 'id',
3166             VALUE => 0,
3167             ENTRYAGGREGATOR => 'AND',
3168         );
3169         return $self->{'_sql_current_user_can_see_applied'} = 1;
3170     }
3171
3172     {
3173         my $join_roles = keys %roles;
3174         $join_roles = 0 if $join_roles == 1 && $roles{'Owner'};
3175         my ($role_group_alias, $cgm_alias);
3176         if ( $join_roles ) {
3177             $role_group_alias = $self->_RoleGroupsJoin( New => 1 );
3178             $cgm_alias = $self->_GroupMembersJoin( GroupsAlias => $role_group_alias );
3179             $self->SUPER::Limit(
3180                 LEFTJOIN   => $cgm_alias,
3181                 FIELD      => 'MemberId',
3182                 OPERATOR   => '=',
3183                 VALUE      => $id,
3184             );
3185         }
3186         my $limit_queues = sub {
3187             my $ea = shift;
3188             my @queues = @_;
3189
3190             return unless @queues;
3191             if ( @queues == 1 ) {
3192                 $self->SUPER::Limit(
3193                     SUBCLAUSE => 'ACL',
3194                     ALIAS => 'main',
3195                     FIELD => 'Queue',
3196                     VALUE => $_[0],
3197                     ENTRYAGGREGATOR => $ea,
3198                 );
3199             } else {
3200                 $self->SUPER::_OpenParen('ACL');
3201                 foreach my $q ( @queues ) {
3202                     $self->SUPER::Limit(
3203                         SUBCLAUSE => 'ACL',
3204                         ALIAS => 'main',
3205                         FIELD => 'Queue',
3206                         VALUE => $q,
3207                         ENTRYAGGREGATOR => $ea,
3208                     );
3209                     $ea = 'OR';
3210                 }
3211                 $self->SUPER::_CloseParen('ACL');
3212             }
3213             return 1;
3214         };
3215
3216         $self->SUPER::_OpenParen('ACL');
3217         my $ea = 'AND';
3218         $ea = 'OR' if $limit_queues->( $ea, @direct_queues );
3219         while ( my ($role, $queues) = each %roles ) {
3220             $self->SUPER::_OpenParen('ACL');
3221             if ( $role eq 'Owner' ) {
3222                 $self->SUPER::Limit(
3223                     SUBCLAUSE => 'ACL',
3224                     FIELD           => 'Owner',
3225                     VALUE           => $id,
3226                     ENTRYAGGREGATOR => $ea,
3227                 );
3228             }
3229             else {
3230                 $self->SUPER::Limit(
3231                     SUBCLAUSE       => 'ACL',
3232                     ALIAS           => $cgm_alias,
3233                     FIELD           => 'MemberId',
3234                     OPERATOR        => 'IS NOT',
3235                     VALUE           => 'NULL',
3236                     QUOTEVALUE      => 0,
3237                     ENTRYAGGREGATOR => $ea,
3238                 );
3239                 $self->SUPER::Limit(
3240                     SUBCLAUSE       => 'ACL',
3241                     ALIAS           => $role_group_alias,
3242                     FIELD           => 'Type',
3243                     VALUE           => $role,
3244                     ENTRYAGGREGATOR => 'AND',
3245                 );
3246             }
3247             $limit_queues->( 'AND', @$queues ) if ref $queues;
3248             $ea = 'OR' if $ea eq 'AND';
3249             $self->SUPER::_CloseParen('ACL');
3250         }
3251         $self->SUPER::_CloseParen('ACL');
3252     }
3253     return $self->{'_sql_current_user_can_see_applied'} = 1;
3254 }
3255
3256 # }}}
3257
3258 # }}}
3259
3260 # {{{ Deal with storing and restoring restrictions
3261
3262 # {{{ sub LoadRestrictions
3263
3264 =head2 LoadRestrictions
3265
3266 LoadRestrictions takes a string which can fully populate the TicketRestrictons hash.
3267 TODO It is not yet implemented
3268
3269 =cut
3270
3271 # }}}
3272
3273 # {{{ sub DescribeRestrictions
3274
3275 =head2 DescribeRestrictions
3276
3277 takes nothing.
3278 Returns a hash keyed by restriction id.
3279 Each element of the hash is currently a one element hash that contains DESCRIPTION which
3280 is a description of the purpose of that TicketRestriction
3281
3282 =cut
3283
3284 sub DescribeRestrictions {
3285     my $self = shift;
3286
3287     my ( $row, %listing );
3288
3289     foreach $row ( keys %{ $self->{'TicketRestrictions'} } ) {
3290         $listing{$row} = $self->{'TicketRestrictions'}{$row}{'DESCRIPTION'};
3291     }
3292     return (%listing);
3293 }
3294
3295 # }}}
3296
3297 # {{{ sub RestrictionValues
3298
3299 =head2 RestrictionValues FIELD
3300
3301 Takes a restriction field and returns a list of values this field is restricted
3302 to.
3303
3304 =cut
3305
3306 sub RestrictionValues {
3307     my $self  = shift;
3308     my $field = shift;
3309     map $self->{'TicketRestrictions'}{$_}{'VALUE'}, grep {
3310                $self->{'TicketRestrictions'}{$_}{'FIELD'}    eq $field
3311             && $self->{'TicketRestrictions'}{$_}{'OPERATOR'} eq "="
3312         }
3313         keys %{ $self->{'TicketRestrictions'} };
3314 }
3315
3316 # }}}
3317
3318 # {{{ sub ClearRestrictions
3319
3320 =head2 ClearRestrictions
3321
3322 Removes all restrictions irretrievably
3323
3324 =cut
3325
3326 sub ClearRestrictions {
3327     my $self = shift;
3328     delete $self->{'TicketRestrictions'};
3329     $self->{'looking_at_effective_id'} = 0;
3330     $self->{'looking_at_type'}         = 0;
3331     $self->{'RecalcTicketLimits'}      = 1;
3332 }
3333
3334 # }}}
3335
3336 # {{{ sub DeleteRestriction
3337
3338 =head2 DeleteRestriction
3339
3340 Takes the row Id of a restriction (From DescribeRestrictions' output, for example.
3341 Removes that restriction from the session's limits.
3342
3343 =cut
3344
3345 sub DeleteRestriction {
3346     my $self = shift;
3347     my $row  = shift;
3348     delete $self->{'TicketRestrictions'}{$row};
3349
3350     $self->{'RecalcTicketLimits'} = 1;
3351
3352     #make the underlying easysearch object forget all its preconceptions
3353 }
3354
3355 # }}}
3356
3357 # {{{ sub _RestrictionsToClauses
3358
3359 # Convert a set of oldstyle SB Restrictions to Clauses for RQL
3360
3361 sub _RestrictionsToClauses {
3362     my $self = shift;
3363
3364     my $row;
3365     my %clause;
3366     foreach $row ( keys %{ $self->{'TicketRestrictions'} } ) {
3367         my $restriction = $self->{'TicketRestrictions'}{$row};
3368
3369         # We need to reimplement the subclause aggregation that SearchBuilder does.
3370         # Default Subclause is ALIAS.FIELD, and default ALIAS is 'main',
3371         # Then SB AND's the different Subclauses together.
3372
3373         # So, we want to group things into Subclauses, convert them to
3374         # SQL, and then join them with the appropriate DefaultEA.
3375         # Then join each subclause group with AND.
3376
3377         my $field = $restriction->{'FIELD'};
3378         my $realfield = $field;    # CustomFields fake up a fieldname, so
3379                                    # we need to figure that out
3380
3381         # One special case
3382         # Rewrite LinkedTo meta field to the real field
3383         if ( $field =~ /LinkedTo/ ) {
3384             $realfield = $field = $restriction->{'TYPE'};
3385         }
3386
3387         # Two special case
3388         # Handle subkey fields with a different real field
3389         if ( $field =~ /^(\w+)\./ ) {
3390             $realfield = $1;
3391         }
3392
3393         die "I don't know about $field yet"
3394             unless ( exists $FIELD_METADATA{$realfield}
3395                 or $restriction->{CUSTOMFIELD} );
3396
3397         my $type = $FIELD_METADATA{$realfield}->[0];
3398         my $op   = $restriction->{'OPERATOR'};
3399
3400         my $value = (
3401             grep    {defined}
3402                 map { $restriction->{$_} } qw(VALUE TICKET BASE TARGET)
3403         )[0];
3404
3405         # this performs the moral equivalent of defined or/dor/C<//>,
3406         # without the short circuiting.You need to use a 'defined or'
3407         # type thing instead of just checking for truth values, because
3408         # VALUE could be 0.(i.e. "false")
3409
3410         # You could also use this, but I find it less aesthetic:
3411         # (although it does short circuit)
3412         #( defined $restriction->{'VALUE'}? $restriction->{VALUE} :
3413         # defined $restriction->{'TICKET'} ?
3414         # $restriction->{TICKET} :
3415         # defined $restriction->{'BASE'} ?
3416         # $restriction->{BASE} :
3417         # defined $restriction->{'TARGET'} ?
3418         # $restriction->{TARGET} )
3419
3420         my $ea = $restriction->{ENTRYAGGREGATOR}
3421             || $DefaultEA{$type}
3422             || "AND";
3423         if ( ref $ea ) {
3424             die "Invalid operator $op for $field ($type)"
3425                 unless exists $ea->{$op};
3426             $ea = $ea->{$op};
3427         }
3428
3429         # Each CustomField should be put into a different Clause so they
3430         # are ANDed together.
3431         if ( $restriction->{CUSTOMFIELD} ) {
3432             $realfield = $field;
3433         }
3434
3435         exists $clause{$realfield} or $clause{$realfield} = [];
3436
3437         # Escape Quotes
3438         $field =~ s!(['"])!\\$1!g;
3439         $value =~ s!(['"])!\\$1!g;
3440         my $data = [ $ea, $type, $field, $op, $value ];
3441
3442         # here is where we store extra data, say if it's a keyword or
3443         # something.  (I.e. "TYPE SPECIFIC STUFF")
3444
3445         push @{ $clause{$realfield} }, $data;
3446     }
3447     return \%clause;
3448 }
3449
3450 # }}}
3451
3452 # {{{ sub _ProcessRestrictions
3453
3454 =head2 _ProcessRestrictions PARAMHASH
3455
3456 # The new _ProcessRestrictions is somewhat dependent on the SQL stuff,
3457 # but isn't quite generic enough to move into Tickets_Overlay_SQL.
3458
3459 =cut
3460
3461 sub _ProcessRestrictions {
3462     my $self = shift;
3463
3464     #Blow away ticket aliases since we'll need to regenerate them for
3465     #a new search
3466     delete $self->{'TicketAliases'};
3467     delete $self->{'items_array'};
3468     delete $self->{'item_map'};
3469     delete $self->{'raw_rows'};
3470     delete $self->{'rows'};
3471     delete $self->{'count_all'};
3472
3473     my $sql = $self->Query;    # Violating the _SQL namespace
3474     if ( !$sql || $self->{'RecalcTicketLimits'} ) {
3475
3476         #  "Restrictions to Clauses Branch\n";
3477         my $clauseRef = eval { $self->_RestrictionsToClauses; };
3478         if ($@) {
3479             $RT::Logger->error( "RestrictionsToClauses: " . $@ );
3480             $self->FromSQL("");
3481         }
3482         else {
3483             $sql = $self->ClausesToSQL($clauseRef);
3484             $self->FromSQL($sql) if $sql;
3485         }
3486     }
3487
3488     $self->{'RecalcTicketLimits'} = 0;
3489
3490 }
3491
3492 =head2 _BuildItemMap
3493
3494 Build up a L</ItemMap> of first/last/next/prev items, so that we can
3495 display search nav quickly.
3496
3497 =cut
3498
3499 sub _BuildItemMap {
3500     my $self = shift;
3501
3502     my $window = RT->Config->Get('TicketsItemMapSize');
3503
3504     $self->{'item_map'} = {};
3505
3506     my $items = $self->ItemsArrayRefWindow( $window );
3507     return unless $items && @$items;
3508
3509     my $prev = 0;
3510     $self->{'item_map'}{'first'} = $items->[0]->EffectiveId;
3511     for ( my $i = 0; $i < @$items; $i++ ) {
3512         my $item = $items->[$i];
3513         my $id = $item->EffectiveId;
3514         $self->{'item_map'}{$id}{'defined'} = 1;
3515         $self->{'item_map'}{$id}{'prev'}    = $prev;
3516         $self->{'item_map'}{$id}{'next'}    = $items->[$i+1]->EffectiveId
3517             if $items->[$i+1];
3518         $prev = $id;
3519     }
3520     $self->{'item_map'}{'last'} = $prev
3521         if !$window || @$items < $window;
3522 }
3523
3524 =head2 ItemMap
3525
3526 Returns an a map of all items found by this search. The map is a hash
3527 of the form:
3528
3529     {
3530         first => <first ticket id found>,
3531         last => <last ticket id found or undef>,
3532
3533         <ticket id> => {
3534             prev => <the ticket id found before>,
3535             next => <the ticket id found after>,
3536         },
3537         <ticket id> => {
3538             prev => ...,
3539             next => ...,
3540         },
3541     }
3542
3543 =cut
3544
3545 sub ItemMap {
3546     my $self = shift;
3547     $self->_BuildItemMap unless $self->{'item_map'};
3548     return $self->{'item_map'};
3549 }
3550
3551
3552 # }}}
3553
3554 # }}}
3555
3556 =head2 PrepForSerialization
3557
3558 You don't want to serialize a big tickets object, as
3559 the {items} hash will be instantly invalid _and_ eat
3560 lots of space
3561
3562 =cut
3563
3564 sub PrepForSerialization {
3565     my $self = shift;
3566     delete $self->{'items'};
3567     delete $self->{'items_array'};
3568     $self->RedoSearch();
3569 }
3570
3571 =head1 FLAGS
3572
3573 RT::Tickets supports several flags which alter search behavior:
3574
3575
3576 allow_deleted_search  (Otherwise never show deleted tickets in search results)
3577 looking_at_type (otherwise limit to type=ticket)
3578
3579 These flags are set by calling 
3580
3581 $tickets->{'flagname'} = 1;
3582
3583 BUG: There should be an API for this
3584
3585
3586
3587 =cut
3588
3589 1;
3590
3591
3592