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