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