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