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