improve customer field access in RT queries, #16490
[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
1073     $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} } = $alias
1074         unless $args{'New'};
1075
1076     return $alias;
1077 }
1078
1079 =head2 _WatcherJoin
1080
1081 Helper function which provides joins to a watchers table both for limits
1082 and for ordering.
1083
1084 =cut
1085
1086 sub _WatcherJoin {
1087     my $self = shift;
1088     my $type = shift || '';
1089
1090
1091     my $groups = $self->_RoleGroupsJoin( Type => $type );
1092     my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
1093     # XXX: work around, we must hide groups that
1094     # are members of the role group we search in,
1095     # otherwise them result in wrong NULLs in Users
1096     # table and break ordering. Now, we know that
1097     # RT doesn't allow to add groups as members of the
1098     # ticket roles, so we just hide entries in CGM table
1099     # with MemberId == GroupId from results
1100     $self->SUPER::Limit(
1101         LEFTJOIN   => $group_members,
1102         FIELD      => 'GroupId',
1103         OPERATOR   => '!=',
1104         VALUE      => "$group_members.MemberId",
1105         QUOTEVALUE => 0,
1106     );
1107     my $users = $self->Join(
1108         TYPE            => 'LEFT',
1109         ALIAS1          => $group_members,
1110         FIELD1          => 'MemberId',
1111         TABLE2          => 'Users',
1112         FIELD2          => 'id',
1113     );
1114     return ($groups, $group_members, $users);
1115 }
1116
1117 =head2 _WatcherMembershipLimit
1118
1119 Handle watcher membership limits, i.e. whether the watcher belongs to a
1120 specific group or not.
1121
1122 Meta Data:
1123   1: Field to query on
1124
1125 SELECT DISTINCT main.*
1126 FROM
1127     Tickets main,
1128     Groups Groups_1,
1129     CachedGroupMembers CachedGroupMembers_2,
1130     Users Users_3
1131 WHERE (
1132     (main.EffectiveId = main.id)
1133 ) AND (
1134     (main.Status != 'deleted')
1135 ) AND (
1136     (main.Type = 'ticket')
1137 ) AND (
1138     (
1139         (Users_3.EmailAddress = '22')
1140             AND
1141         (Groups_1.Domain = 'RT::Ticket-Role')
1142             AND
1143         (Groups_1.Type = 'RequestorGroup')
1144     )
1145 ) AND
1146     Groups_1.Instance = main.id
1147 AND
1148     Groups_1.id = CachedGroupMembers_2.GroupId
1149 AND
1150     CachedGroupMembers_2.MemberId = Users_3.id
1151 ORDER BY main.id ASC
1152 LIMIT 25
1153
1154 =cut
1155
1156 sub _WatcherMembershipLimit {
1157     my ( $self, $field, $op, $value, @rest ) = @_;
1158     my %rest = @rest;
1159
1160     $self->_OpenParen;
1161
1162     my $groups       = $self->NewAlias('Groups');
1163     my $groupmembers = $self->NewAlias('CachedGroupMembers');
1164     my $users        = $self->NewAlias('Users');
1165     my $memberships  = $self->NewAlias('CachedGroupMembers');
1166
1167     if ( ref $field ) {    # gross hack
1168         my @bundle = @$field;
1169         $self->_OpenParen;
1170         for my $chunk (@bundle) {
1171             ( $field, $op, $value, @rest ) = @$chunk;
1172             $self->_SQLLimit(
1173                 ALIAS    => $memberships,
1174                 FIELD    => 'GroupId',
1175                 VALUE    => $value,
1176                 OPERATOR => $op,
1177                 @rest,
1178             );
1179         }
1180         $self->_CloseParen;
1181     }
1182     else {
1183         $self->_SQLLimit(
1184             ALIAS    => $memberships,
1185             FIELD    => 'GroupId',
1186             VALUE    => $value,
1187             OPERATOR => $op,
1188             @rest,
1189         );
1190     }
1191
1192     # {{{ Tie to groups for tickets we care about
1193     $self->_SQLLimit(
1194         ALIAS           => $groups,
1195         FIELD           => 'Domain',
1196         VALUE           => 'RT::Ticket-Role',
1197         ENTRYAGGREGATOR => 'AND'
1198     );
1199
1200     $self->Join(
1201         ALIAS1 => $groups,
1202         FIELD1 => 'Instance',
1203         ALIAS2 => 'main',
1204         FIELD2 => 'id'
1205     );
1206
1207     # }}}
1208
1209     # If we care about which sort of watcher
1210     my $meta = $FIELD_METADATA{$field};
1211     my $type = ( defined $meta->[1] ? $meta->[1] : undef );
1212
1213     if ($type) {
1214         $self->_SQLLimit(
1215             ALIAS           => $groups,
1216             FIELD           => 'Type',
1217             VALUE           => $type,
1218             ENTRYAGGREGATOR => 'AND'
1219         );
1220     }
1221
1222     $self->Join(
1223         ALIAS1 => $groups,
1224         FIELD1 => 'id',
1225         ALIAS2 => $groupmembers,
1226         FIELD2 => 'GroupId'
1227     );
1228
1229     $self->Join(
1230         ALIAS1 => $groupmembers,
1231         FIELD1 => 'MemberId',
1232         ALIAS2 => $users,
1233         FIELD2 => 'id'
1234     );
1235
1236     $self->Join(
1237         ALIAS1 => $memberships,
1238         FIELD1 => 'MemberId',
1239         ALIAS2 => $users,
1240         FIELD2 => 'id'
1241     );
1242
1243     $self->_CloseParen;
1244
1245 }
1246
1247 =head2 _CustomFieldDecipher
1248
1249 Try and turn a CF descriptor into (cfid, cfname) object pair.
1250
1251 =cut
1252
1253 sub _CustomFieldDecipher {
1254     my ($self, $string) = @_;
1255
1256     my ($queue, $field, $column) = ($string =~ /^(?:(.+?)\.)?{(.+)}(?:\.(Content|LargeContent))?$/);
1257     $field ||= ($string =~ /^{(.*?)}$/)[0] || $string;
1258
1259     my $cf;
1260     if ( $queue ) {
1261         my $q = RT::Queue->new( $self->CurrentUser );
1262         $q->Load( $queue );
1263
1264         if ( $q->id ) {
1265             # $queue = $q->Name; # should we normalize the queue?
1266             $cf = $q->CustomField( $field );
1267         }
1268         else {
1269             $RT::Logger->warning("Queue '$queue' doesn't exist, parsed from '$string'");
1270             $queue = 0;
1271         }
1272     }
1273     elsif ( $field =~ /\D/ ) {
1274         $queue = '';
1275         my $cfs = RT::CustomFields->new( $self->CurrentUser );
1276         $cfs->Limit( FIELD => 'Name', VALUE => $field );
1277         $cfs->LimitToLookupType('RT::Queue-RT::Ticket');
1278
1279         # if there is more then one field the current user can
1280         # see with the same name then we shouldn't return cf object
1281         # as we don't know which one to use
1282         $cf = $cfs->First;
1283         if ( $cf ) {
1284             $cf = undef if $cfs->Next;
1285         }
1286     }
1287     else {
1288         $cf = RT::CustomField->new( $self->CurrentUser );
1289         $cf->Load( $field );
1290     }
1291
1292     return ($queue, $field, $cf, $column);
1293 }
1294
1295 =head2 _CustomFieldJoin
1296
1297 Factor out the Join of custom fields so we can use it for sorting too
1298
1299 =cut
1300
1301 sub _CustomFieldJoin {
1302     my ($self, $cfkey, $cfid, $field) = @_;
1303     # Perform one Join per CustomField
1304     if ( $self->{_sql_object_cfv_alias}{$cfkey} ||
1305          $self->{_sql_cf_alias}{$cfkey} )
1306     {
1307         return ( $self->{_sql_object_cfv_alias}{$cfkey},
1308                  $self->{_sql_cf_alias}{$cfkey} );
1309     }
1310
1311     my ($TicketCFs, $CFs);
1312     if ( $cfid ) {
1313         $TicketCFs = $self->{_sql_object_cfv_alias}{$cfkey} = $self->Join(
1314             TYPE   => 'LEFT',
1315             ALIAS1 => 'main',
1316             FIELD1 => 'id',
1317             TABLE2 => 'ObjectCustomFieldValues',
1318             FIELD2 => 'ObjectId',
1319         );
1320         $self->SUPER::Limit(
1321             LEFTJOIN        => $TicketCFs,
1322             FIELD           => 'CustomField',
1323             VALUE           => $cfid,
1324             ENTRYAGGREGATOR => 'AND'
1325         );
1326     }
1327     else {
1328         my $ocfalias = $self->Join(
1329             TYPE       => 'LEFT',
1330             FIELD1     => 'Queue',
1331             TABLE2     => 'ObjectCustomFields',
1332             FIELD2     => 'ObjectId',
1333         );
1334
1335         $self->SUPER::Limit(
1336             LEFTJOIN        => $ocfalias,
1337             ENTRYAGGREGATOR => 'OR',
1338             FIELD           => 'ObjectId',
1339             VALUE           => '0',
1340         );
1341
1342         $CFs = $self->{_sql_cf_alias}{$cfkey} = $self->Join(
1343             TYPE       => 'LEFT',
1344             ALIAS1     => $ocfalias,
1345             FIELD1     => 'CustomField',
1346             TABLE2     => 'CustomFields',
1347             FIELD2     => 'id',
1348         );
1349         $self->SUPER::Limit(
1350             LEFTJOIN        => $CFs,
1351             ENTRYAGGREGATOR => 'AND',
1352             FIELD           => 'LookupType',
1353             VALUE           => 'RT::Queue-RT::Ticket',
1354         );
1355         $self->SUPER::Limit(
1356             LEFTJOIN        => $CFs,
1357             ENTRYAGGREGATOR => 'AND',
1358             FIELD           => 'Name',
1359             VALUE           => $field,
1360         );
1361
1362         $TicketCFs = $self->{_sql_object_cfv_alias}{$cfkey} = $self->Join(
1363             TYPE   => 'LEFT',
1364             ALIAS1 => $CFs,
1365             FIELD1 => 'id',
1366             TABLE2 => 'ObjectCustomFieldValues',
1367             FIELD2 => 'CustomField',
1368         );
1369         $self->SUPER::Limit(
1370             LEFTJOIN        => $TicketCFs,
1371             FIELD           => 'ObjectId',
1372             VALUE           => 'main.id',
1373             QUOTEVALUE      => 0,
1374             ENTRYAGGREGATOR => 'AND',
1375         );
1376     }
1377     $self->SUPER::Limit(
1378         LEFTJOIN        => $TicketCFs,
1379         FIELD           => 'ObjectType',
1380         VALUE           => 'RT::Ticket',
1381         ENTRYAGGREGATOR => 'AND'
1382     );
1383     $self->SUPER::Limit(
1384         LEFTJOIN        => $TicketCFs,
1385         FIELD           => 'Disabled',
1386         OPERATOR        => '=',
1387         VALUE           => '0',
1388         ENTRYAGGREGATOR => 'AND'
1389     );
1390
1391     return ($TicketCFs, $CFs);
1392 }
1393
1394 =head2 _CustomFieldLimit
1395
1396 Limit based on CustomFields
1397
1398 Meta Data:
1399   none
1400
1401 =cut
1402
1403 sub _CustomFieldLimit {
1404     my ( $self, $_field, $op, $value, %rest ) = @_;
1405
1406     my $field = $rest{'SUBKEY'} || die "No field specified";
1407
1408     # For our sanity, we can only limit on one queue at a time
1409
1410     my ($queue, $cfid, $cf, $column);
1411     ($queue, $field, $cf, $column) = $self->_CustomFieldDecipher( $field );
1412     $cfid = $cf ? $cf->id  : 0 ;
1413
1414 # If we're trying to find custom fields that don't match something, we
1415 # want tickets where the custom field has no value at all.  Note that
1416 # we explicitly don't include the "IS NULL" case, since we would
1417 # otherwise end up with a redundant clause.
1418
1419     my ($negative_op, $null_op, $inv_op, $range_op)
1420         = $self->ClassifySQLOperation( $op );
1421
1422     my $fix_op = sub {
1423         my $op = shift;
1424         return $op unless RT->Config->Get('DatabaseType') eq 'Oracle';
1425         return 'MATCHES' if $op eq '=';
1426         return 'NOT MATCHES' if $op eq '!=';
1427         return $op;
1428     };
1429
1430     my $single_value = !$cf || !$cfid || $cf->SingleValue;
1431
1432     my $cfkey = $cfid ? $cfid : "$queue.$field";
1433
1434     if ( $null_op && !$column ) {
1435         # IS[ NOT] NULL without column is the same as has[ no] any CF value,
1436         # we can reuse our default joins for this operation
1437         # with column specified we have different situation
1438         my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
1439         $self->_OpenParen;
1440         $self->_SQLLimit(
1441             ALIAS    => $TicketCFs,
1442             FIELD    => 'id',
1443             OPERATOR => $op,
1444             VALUE    => $value,
1445             %rest
1446         );
1447         $self->_SQLLimit(
1448             ALIAS      => $CFs,
1449             FIELD      => 'Name',
1450             OPERATOR   => 'IS NOT',
1451             VALUE      => 'NULL',
1452             QUOTEVALUE => 0,
1453             ENTRYAGGREGATOR => 'AND',
1454         ) if $CFs;
1455         $self->_CloseParen;
1456     }
1457     elsif ( !$negative_op || $single_value ) {
1458         $cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++ if !$single_value && !$range_op;
1459         my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
1460
1461         $self->_OpenParen;
1462
1463         $self->_OpenParen;
1464
1465         $self->_OpenParen;
1466         # if column is defined then deal only with it
1467         # otherwise search in Content and in LargeContent
1468         if ( $column ) {
1469             $self->_SQLLimit(
1470                 ALIAS      => $TicketCFs,
1471                 FIELD      => $column,
1472                 OPERATOR   => ($column ne 'LargeContent'? $op : $fix_op->($op)),
1473                 VALUE      => $value,
1474                 %rest
1475             );
1476         }
1477         elsif ( $cfid and $cf->Type eq 'Date' ) {
1478             $self->_DateFieldLimit( 
1479                 'Content',
1480                 $op,
1481                 $value,
1482                 ALIAS => $TicketCFs,
1483                 %rest
1484             );
1485         }
1486         elsif ( $op eq '=' || $op eq '!=' || $op eq '<>' ) {
1487             unless ( length( Encode::encode_utf8($value) ) > 255 ) {
1488                 $self->_SQLLimit(
1489                     ALIAS      => $TicketCFs,
1490                     FIELD      => 'Content',
1491                     OPERATOR   => $op,
1492                     VALUE      => $value,
1493                     %rest
1494                 );
1495             } else {
1496                 $self->_OpenParen;
1497                 $self->_SQLLimit(
1498                     ALIAS      => $TicketCFs,
1499                     FIELD      => 'Content',
1500                     OPERATOR   => '=',
1501                     VALUE      => '',
1502                     ENTRYAGGREGATOR => 'OR'
1503                 );
1504                 $self->_SQLLimit(
1505                     ALIAS      => $TicketCFs,
1506                     FIELD      => 'Content',
1507                     OPERATOR   => 'IS',
1508                     VALUE      => 'NULL',
1509                     ENTRYAGGREGATOR => 'OR'
1510                 );
1511                 $self->_CloseParen;
1512                 $self->_SQLLimit(
1513                     ALIAS => $TicketCFs,
1514                     FIELD => 'LargeContent',
1515                     OPERATOR => $fix_op->($op),
1516                     VALUE => $value,
1517                     ENTRYAGGREGATOR => 'AND',
1518                 );
1519             }
1520         }
1521         else {
1522             $self->_SQLLimit(
1523                 ALIAS      => $TicketCFs,
1524                 FIELD      => 'Content',
1525                 OPERATOR   => $op,
1526                 VALUE      => $value,
1527                 %rest
1528             );
1529
1530             $self->_OpenParen;
1531             $self->_OpenParen;
1532             $self->_SQLLimit(
1533                 ALIAS      => $TicketCFs,
1534                 FIELD      => 'Content',
1535                 OPERATOR   => '=',
1536                 VALUE      => '',
1537                 ENTRYAGGREGATOR => 'OR'
1538             );
1539             $self->_SQLLimit(
1540                 ALIAS      => $TicketCFs,
1541                 FIELD      => 'Content',
1542                 OPERATOR   => 'IS',
1543                 VALUE      => 'NULL',
1544                 ENTRYAGGREGATOR => 'OR'
1545             );
1546             $self->_CloseParen;
1547             $self->_SQLLimit(
1548                 ALIAS => $TicketCFs,
1549                 FIELD => 'LargeContent',
1550                 OPERATOR => $fix_op->($op),
1551                 VALUE => $value,
1552                 ENTRYAGGREGATOR => 'AND',
1553             );
1554             $self->_CloseParen;
1555         }
1556         $self->_CloseParen;
1557
1558         # XXX: if we join via CustomFields table then
1559         # because of order of left joins we get NULLs in
1560         # CF table and then get nulls for those records
1561         # in OCFVs table what result in wrong results
1562         # as decifer method now tries to load a CF then
1563         # we fall into this situation only when there
1564         # are more than one CF with the name in the DB.
1565         # the same thing applies to order by call.
1566         # TODO: reorder joins T <- OCFVs <- CFs <- OCFs if
1567         # we want treat IS NULL as (not applies or has
1568         # no value)
1569         $self->_SQLLimit(
1570             ALIAS      => $CFs,
1571             FIELD      => 'Name',
1572             OPERATOR   => 'IS NOT',
1573             VALUE      => 'NULL',
1574             QUOTEVALUE => 0,
1575             ENTRYAGGREGATOR => 'AND',
1576         ) if $CFs;
1577         $self->_CloseParen;
1578
1579         if ($negative_op) {
1580             $self->_SQLLimit(
1581                 ALIAS           => $TicketCFs,
1582                 FIELD           => $column || 'Content',
1583                 OPERATOR        => 'IS',
1584                 VALUE           => 'NULL',
1585                 QUOTEVALUE      => 0,
1586                 ENTRYAGGREGATOR => 'OR',
1587             );
1588         }
1589
1590         $self->_CloseParen;
1591     }
1592     else {
1593         $cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++;
1594         my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
1595
1596         # reverse operation
1597         $op =~ s/!|NOT\s+//i;
1598
1599         # if column is defined then deal only with it
1600         # otherwise search in Content and in LargeContent
1601         if ( $column ) {
1602             $self->SUPER::Limit(
1603                 LEFTJOIN   => $TicketCFs,
1604                 ALIAS      => $TicketCFs,
1605                 FIELD      => $column,
1606                 OPERATOR   => ($column ne 'LargeContent'? $op : $fix_op->($op)),
1607                 VALUE      => $value,
1608             );
1609         }
1610         else {
1611             $self->SUPER::Limit(
1612                 LEFTJOIN   => $TicketCFs,
1613                 ALIAS      => $TicketCFs,
1614                 FIELD      => 'Content',
1615                 OPERATOR   => $op,
1616                 VALUE      => $value,
1617             );
1618         }
1619         $self->_SQLLimit(
1620             %rest,
1621             ALIAS      => $TicketCFs,
1622             FIELD      => 'id',
1623             OPERATOR   => 'IS',
1624             VALUE      => 'NULL',
1625             QUOTEVALUE => 0,
1626         );
1627     }
1628 }
1629
1630 sub _HasAttributeLimit {
1631     my ( $self, $field, $op, $value, %rest ) = @_;
1632
1633     my $alias = $self->Join(
1634         TYPE   => 'LEFT',
1635         ALIAS1 => 'main',
1636         FIELD1 => 'id',
1637         TABLE2 => 'Attributes',
1638         FIELD2 => 'ObjectId',
1639     );
1640     $self->SUPER::Limit(
1641         LEFTJOIN        => $alias,
1642         FIELD           => 'ObjectType',
1643         VALUE           => 'RT::Ticket',
1644         ENTRYAGGREGATOR => 'AND'
1645     );
1646     $self->SUPER::Limit(
1647         LEFTJOIN        => $alias,
1648         FIELD           => 'Name',
1649         OPERATOR        => $op,
1650         VALUE           => $value,
1651         ENTRYAGGREGATOR => 'AND'
1652     );
1653     $self->_SQLLimit(
1654         %rest,
1655         ALIAS      => $alias,
1656         FIELD      => 'id',
1657         OPERATOR   => $FIELD_METADATA{$field}->[1]? 'IS NOT': 'IS',
1658         VALUE      => 'NULL',
1659         QUOTEVALUE => 0,
1660     );
1661 }
1662
1663 # End Helper Functions
1664
1665 # End of SQL Stuff -------------------------------------------------
1666
1667 # {{{ Allow sorting on watchers
1668
1669 =head2 OrderByCols ARRAY
1670
1671 A modified version of the OrderBy method which automatically joins where
1672 C<ALIAS> is set to the name of a watcher type.
1673
1674 =cut
1675
1676 sub OrderByCols {
1677     my $self = shift;
1678     my @args = @_;
1679     my $clause;
1680     my @res   = ();
1681     my $order = 0;
1682
1683     foreach my $row (@args) {
1684         if ( $row->{ALIAS} ) {
1685             push @res, $row;
1686             next;
1687         }
1688         if ( $row->{FIELD} !~ /\./ ) {
1689             my $meta = $self->FIELDS->{ $row->{FIELD} };
1690             unless ( $meta ) {
1691                 push @res, $row;
1692                 next;
1693             }
1694
1695             if ( $meta->[0] eq 'ENUM' && ($meta->[1]||'') eq 'Queue' ) {
1696                 my $alias = $self->Join(
1697                     TYPE   => 'LEFT',
1698                     ALIAS1 => 'main',
1699                     FIELD1 => $row->{'FIELD'},
1700                     TABLE2 => 'Queues',
1701                     FIELD2 => 'id',
1702                 );
1703                 push @res, { %$row, ALIAS => $alias, FIELD => "Name" };
1704             } elsif ( ( $meta->[0] eq 'ENUM' && ($meta->[1]||'') eq 'User' )
1705                 || ( $meta->[0] eq 'WATCHERFIELD' && ($meta->[1]||'') eq 'Owner' )
1706             ) {
1707                 my $alias = $self->Join(
1708                     TYPE   => 'LEFT',
1709                     ALIAS1 => 'main',
1710                     FIELD1 => $row->{'FIELD'},
1711                     TABLE2 => 'Users',
1712                     FIELD2 => 'id',
1713                 );
1714                 push @res, { %$row, ALIAS => $alias, FIELD => "Name" };
1715             } else {
1716                 push @res, $row;
1717             }
1718             next;
1719         }
1720
1721         my ( $field, $subkey ) = split /\./, $row->{FIELD}, 2;
1722         my $meta = $self->FIELDS->{$field};
1723         if ( defined $meta->[0] && $meta->[0] eq 'WATCHERFIELD' ) {
1724             # cache alias as we want to use one alias per watcher type for sorting
1725             my $users = $self->{_sql_u_watchers_alias_for_sort}{ $meta->[1] };
1726             unless ( $users ) {
1727                 $self->{_sql_u_watchers_alias_for_sort}{ $meta->[1] }
1728                     = $users = ( $self->_WatcherJoin( $meta->[1] ) )[2];
1729             }
1730             push @res, { %$row, ALIAS => $users, FIELD => $subkey };
1731        } elsif ( defined $meta->[0] && $meta->[0] eq 'CUSTOMFIELD' ) {
1732            my ($queue, $field, $cf_obj, $column) = $self->_CustomFieldDecipher( $subkey );
1733            my $cfkey = $cf_obj ? $cf_obj->id : "$queue.$field";
1734            $cfkey .= ".ordering" if !$cf_obj || ($cf_obj->MaxValues||0) != 1;
1735            my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, ($cf_obj ?$cf_obj->id :0) , $field );
1736            # this is described in _CustomFieldLimit
1737            $self->_SQLLimit(
1738                ALIAS      => $CFs,
1739                FIELD      => 'Name',
1740                OPERATOR   => 'IS NOT',
1741                VALUE      => 'NULL',
1742                QUOTEVALUE => 1,
1743                ENTRYAGGREGATOR => 'AND',
1744            ) if $CFs;
1745            unless ($cf_obj) {
1746                # For those cases where we are doing a join against the
1747                # CF name, and don't have a CFid, use Unique to make sure
1748                # we don't show duplicate tickets.  NOTE: I'm pretty sure
1749                # this will stay mixed in for the life of the
1750                # class/package, and not just for the life of the object.
1751                # Potential performance issue.
1752                require DBIx::SearchBuilder::Unique;
1753                DBIx::SearchBuilder::Unique->import;
1754            }
1755            my $CFvs = $self->Join(
1756                TYPE   => 'LEFT',
1757                ALIAS1 => $TicketCFs,
1758                FIELD1 => 'CustomField',
1759                TABLE2 => 'CustomFieldValues',
1760                FIELD2 => 'CustomField',
1761            );
1762            $self->SUPER::Limit(
1763                LEFTJOIN        => $CFvs,
1764                FIELD           => 'Name',
1765                QUOTEVALUE      => 0,
1766                VALUE           => $TicketCFs . ".Content",
1767                ENTRYAGGREGATOR => 'AND'
1768            );
1769
1770            push @res, { %$row, ALIAS => $CFvs, FIELD => 'SortOrder' };
1771            push @res, { %$row, ALIAS => $TicketCFs, FIELD => 'Content' };
1772        } elsif ( $field eq "Custom" && $subkey eq "Ownership") {
1773            # PAW logic is "reversed"
1774            my $order = "ASC";
1775            if (exists $row->{ORDER} ) {
1776                my $o = $row->{ORDER};
1777                delete $row->{ORDER};
1778                $order = "DESC" if $o =~ /asc/i;
1779            }
1780
1781            # Ticket.Owner    1 0 X
1782            # Unowned Tickets 0 1 X
1783            # Else            0 0 X
1784
1785            foreach my $uid ( $self->CurrentUser->Id, $RT::Nobody->Id ) {
1786                if ( RT->Config->Get('DatabaseType') eq 'Oracle' ) {
1787                    my $f = ($row->{'ALIAS'} || 'main') .'.Owner';
1788                    push @res, {
1789                        %$row,
1790                        FIELD => undef,
1791                        ALIAS => '',
1792                        FUNCTION => "CASE WHEN $f=$uid THEN 1 ELSE 0 END",
1793                        ORDER => $order
1794                    };
1795                } else {
1796                    push @res, {
1797                        %$row,
1798                        FIELD => undef,
1799                        FUNCTION => "Owner=$uid",
1800                        ORDER => $order
1801                    };
1802                }
1803            }
1804
1805            push @res, { %$row, FIELD => "Priority", ORDER => $order } ;
1806
1807        } elsif ( $field eq 'Customer' ) { #Freeside
1808            # OrderBy(FIELD => expression) doesn't work, it has to be 
1809            # an actual field, so we have to do the join even if sorting
1810            # by custnum
1811            my $custalias = $self->JoinToCustomer;
1812            my $cust_field = lc($subkey);
1813            if ( !$cust_field or $cust_field eq 'number' ) {
1814                $cust_field = 'custnum';
1815            }
1816            elsif ( $cust_field eq 'name' ) {
1817                $cust_field = "COALESCE( $custalias.company,
1818                $custalias.last || ', ' || $custalias.first
1819                )";
1820            }
1821            else { # order by cust_main fields directly: 'Customer.agentnum'
1822                $cust_field = $subkey;
1823            }
1824            push @res, { %$row, ALIAS => $custalias, FIELD => $cust_field };
1825
1826        } #Freeside
1827
1828        else {
1829            push @res, $row;
1830        }
1831     }
1832     return $self->SUPER::OrderByCols(@res);
1833 }
1834
1835 #Freeside
1836
1837 sub JoinToCustLinks {
1838     # Set up join to links (id = localbase),
1839     # limit link type to 'MemberOf',
1840     # and target value to any Freeside custnum URI.
1841     # Return the linkalias for further join/limit action,
1842     # and an sql expression to retrieve the custnum.
1843     my $self = shift;
1844     # only join once for each RT::Tickets object
1845     my $linkalias = $self->{cust_linkalias};
1846     if (!$linkalias) {
1847         $linkalias = $self->Join(
1848             TYPE   => 'LEFT',
1849             ALIAS1 => 'main',
1850             FIELD1 => 'id',
1851             TABLE2 => 'Links',
1852             FIELD2 => 'LocalBase',
1853         );
1854
1855         $self->SUPER::Limit(
1856             LEFTJOIN => $linkalias,
1857             FIELD    => 'Type',
1858             OPERATOR => '=',
1859             VALUE    => 'MemberOf',
1860         );
1861         $self->SUPER::Limit(
1862             LEFTJOIN => $linkalias,
1863             FIELD    => 'Target',
1864             OPERATOR => 'STARTSWITH',
1865             VALUE    => 'freeside://freeside/cust_main/',
1866         );
1867         $self->{cust_linkalias} = $linkalias;
1868     }
1869     my $custnum_sql = "CAST(SUBSTR($linkalias.Target,31) AS ";
1870     if ( RT->Config->Get('DatabaseType') eq 'mysql' ) {
1871         $custnum_sql .= 'SIGNED INTEGER)';
1872     }
1873     else {
1874         $custnum_sql .= 'INTEGER)';
1875     }
1876     return ($linkalias, $custnum_sql);
1877 }
1878
1879 sub JoinToCustomer {
1880     my $self = shift;
1881     my ($linkalias, $custnum_sql) = $self->JoinToCustLinks;
1882     # don't reuse this join, though--negative queries need 
1883     # independent joins
1884     my $custalias = $self->Join(
1885         TYPE       => 'LEFT',
1886         EXPRESSION => $custnum_sql,
1887         TABLE2     => 'cust_main',
1888         FIELD2     => 'custnum',
1889     );
1890     return $custalias;
1891 }
1892
1893 sub _FreesideFieldLimit {
1894     my ( $self, $field, $op, $value, %rest ) = @_;
1895     my $alias = $self->JoinToCustomer;
1896     my $is_negative = 0;
1897     if ( $op eq '!=' || $op =~ /\bNOT\b/i ) {
1898         # if the op is negative, do the join as though
1899         # the op were positive, then accept only records
1900         # where the right-side join key is null.
1901         $is_negative = 1;
1902         $op = '=' if $op eq '!=';
1903         $op =~ s/\bNOT\b//;
1904     }
1905
1906     my $cust_field = $rest{SUBKEY} || 'custnum';
1907     my $table2;
1908     # compound subkey: separate into table name and field in that table
1909     # (must be linked by custnum)
1910     ($table2, $cust_field) = ($1, $2) if $cust_field =~ /^(\w+)?\.(\w+)$/;
1911
1912     $cust_field = lc($cust_field);
1913     $cust_field = 'custnum' if !$cust_field or $cust_field eq 'number';
1914
1915     if ( $table2 ) {
1916         $alias = $self->Join(
1917             TYPE        => 'LEFT',
1918             ALIAS1      => $alias,
1919             FIELD1      => 'custnum',
1920             TABLE2      => $table2,
1921             FIELD2      => 'custnum',
1922         );
1923     }
1924
1925     $self->SUPER::Limit(
1926         LEFTJOIN        => $alias,
1927         FIELD           => $cust_field,
1928         OPERATOR        => $op,
1929         VALUE           => $value,
1930         ENTRYAGGREGATOR => 'AND',
1931     );
1932     $self->_SQLLimit(
1933         %rest,
1934         ALIAS           => $alias,
1935         FIELD           => 'custnum',
1936         OPERATOR        => $is_negative ? 'IS' : 'IS NOT',
1937         VALUE           => 'NULL',
1938         QUOTEVALUE      => 0,
1939     );
1940 }
1941
1942 #Freeside
1943
1944 # }}}
1945
1946 # {{{ Limit the result set based on content
1947
1948 # {{{ sub Limit
1949
1950 =head2 Limit
1951
1952 Takes a paramhash with the fields FIELD, OPERATOR, VALUE and DESCRIPTION
1953 Generally best called from LimitFoo methods
1954
1955 =cut
1956
1957 sub Limit {
1958     my $self = shift;
1959     my %args = (
1960         FIELD       => undef,
1961         OPERATOR    => '=',
1962         VALUE       => undef,
1963         DESCRIPTION => undef,
1964         @_
1965     );
1966     $args{'DESCRIPTION'} = $self->loc(
1967         "[_1] [_2] [_3]",  $args{'FIELD'},
1968         $args{'OPERATOR'}, $args{'VALUE'}
1969         )
1970         if ( !defined $args{'DESCRIPTION'} );
1971
1972     my $index = $self->_NextIndex;
1973
1974 # make the TicketRestrictions hash the equivalent of whatever we just passed in;
1975
1976     %{ $self->{'TicketRestrictions'}{$index} } = %args;
1977
1978     $self->{'RecalcTicketLimits'} = 1;
1979
1980 # If we're looking at the effective id, we don't want to append the other clause
1981 # which limits us to tickets where id = effective id
1982     if ( $args{'FIELD'} eq 'EffectiveId'
1983         && ( !$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) )
1984     {
1985         $self->{'looking_at_effective_id'} = 1;
1986     }
1987
1988     if ( $args{'FIELD'} eq 'Type'
1989         && ( !$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) )
1990     {
1991         $self->{'looking_at_type'} = 1;
1992     }
1993
1994     return ($index);
1995 }
1996
1997 # }}}
1998
1999 =head2 FreezeLimits
2000
2001 Returns a frozen string suitable for handing back to ThawLimits.
2002
2003 =cut
2004
2005 sub _FreezeThawKeys {
2006     'TicketRestrictions', 'restriction_index', 'looking_at_effective_id',
2007         'looking_at_type';
2008 }
2009
2010 # {{{ sub FreezeLimits
2011
2012 sub FreezeLimits {
2013     my $self = shift;
2014     require Storable;
2015     require MIME::Base64;
2016     MIME::Base64::base64_encode(
2017         Storable::freeze( \@{$self}{ $self->_FreezeThawKeys } ) );
2018 }
2019
2020 # }}}
2021
2022 =head2 ThawLimits
2023
2024 Take a frozen Limits string generated by FreezeLimits and make this tickets
2025 object have that set of limits.
2026
2027 =cut
2028
2029 # {{{ sub ThawLimits
2030
2031 sub ThawLimits {
2032     my $self = shift;
2033     my $in   = shift;
2034
2035     #if we don't have $in, get outta here.
2036     return undef unless ($in);
2037
2038     $self->{'RecalcTicketLimits'} = 1;
2039
2040     require Storable;
2041     require MIME::Base64;
2042
2043     #We don't need to die if the thaw fails.
2044     @{$self}{ $self->_FreezeThawKeys }
2045         = eval { @{ Storable::thaw( MIME::Base64::base64_decode($in) ) }; };
2046
2047     $RT::Logger->error($@) if $@;
2048
2049 }
2050
2051 # }}}
2052
2053 # {{{ Limit by enum or foreign key
2054
2055 # {{{ sub LimitQueue
2056
2057 =head2 LimitQueue
2058
2059 LimitQueue takes a paramhash with the fields OPERATOR and VALUE.
2060 OPERATOR is one of = or !=. (It defaults to =).
2061 VALUE is a queue id or Name.
2062
2063
2064 =cut
2065
2066 sub LimitQueue {
2067     my $self = shift;
2068     my %args = (
2069         VALUE    => undef,
2070         OPERATOR => '=',
2071         @_
2072     );
2073
2074     #TODO  VALUE should also take queue objects
2075     if ( defined $args{'VALUE'} && $args{'VALUE'} !~ /^\d+$/ ) {
2076         my $queue = new RT::Queue( $self->CurrentUser );
2077         $queue->Load( $args{'VALUE'} );
2078         $args{'VALUE'} = $queue->Id;
2079     }
2080
2081     # What if they pass in an Id?  Check for isNum() and convert to
2082     # string.
2083
2084     #TODO check for a valid queue here
2085
2086     $self->Limit(
2087         FIELD       => 'Queue',
2088         VALUE       => $args{'VALUE'},
2089         OPERATOR    => $args{'OPERATOR'},
2090         DESCRIPTION => join(
2091             ' ', $self->loc('Queue'), $args{'OPERATOR'}, $args{'VALUE'},
2092         ),
2093     );
2094
2095 }
2096
2097 # }}}
2098
2099 # {{{ sub LimitStatus
2100
2101 =head2 LimitStatus
2102
2103 Takes a paramhash with the fields OPERATOR and VALUE.
2104 OPERATOR is one of = or !=.
2105 VALUE is a status.
2106
2107 RT adds Status != 'deleted' until object has
2108 allow_deleted_search internal property set.
2109 $tickets->{'allow_deleted_search'} = 1;
2110 $tickets->LimitStatus( VALUE => 'deleted' );
2111
2112 =cut
2113
2114 sub LimitStatus {
2115     my $self = shift;
2116     my %args = (
2117         OPERATOR => '=',
2118         @_
2119     );
2120     $self->Limit(
2121         FIELD       => 'Status',
2122         VALUE       => $args{'VALUE'},
2123         OPERATOR    => $args{'OPERATOR'},
2124         DESCRIPTION => join( ' ',
2125             $self->loc('Status'), $args{'OPERATOR'},
2126             $self->loc( $args{'VALUE'} ) ),
2127     );
2128 }
2129
2130 # }}}
2131
2132 # {{{ sub IgnoreType
2133
2134 =head2 IgnoreType
2135
2136 If called, this search will not automatically limit the set of results found
2137 to tickets of type "Ticket". Tickets of other types, such as "project" and
2138 "approval" will be found.
2139
2140 =cut
2141
2142 sub IgnoreType {
2143     my $self = shift;
2144
2145     # Instead of faking a Limit that later gets ignored, fake up the
2146     # fact that we're already looking at type, so that the check in
2147     # Tickets_Overlay_SQL/FromSQL goes down the right branch
2148
2149     #  $self->LimitType(VALUE => '__any');
2150     $self->{looking_at_type} = 1;
2151 }
2152
2153 # }}}
2154
2155 # {{{ sub LimitType
2156
2157 =head2 LimitType
2158
2159 Takes a paramhash with the fields OPERATOR and VALUE.
2160 OPERATOR is one of = or !=, it defaults to "=".
2161 VALUE is a string to search for in the type of the ticket.
2162
2163
2164
2165 =cut
2166
2167 sub LimitType {
2168     my $self = shift;
2169     my %args = (
2170         OPERATOR => '=',
2171         VALUE    => undef,
2172         @_
2173     );
2174     $self->Limit(
2175         FIELD       => 'Type',
2176         VALUE       => $args{'VALUE'},
2177         OPERATOR    => $args{'OPERATOR'},
2178         DESCRIPTION => join( ' ',
2179             $self->loc('Type'), $args{'OPERATOR'}, $args{'Limit'}, ),
2180     );
2181 }
2182
2183 # }}}
2184
2185 # }}}
2186
2187 # {{{ Limit by string field
2188
2189 # {{{ sub LimitSubject
2190
2191 =head2 LimitSubject
2192
2193 Takes a paramhash with the fields OPERATOR and VALUE.
2194 OPERATOR is one of = or !=.
2195 VALUE is a string to search for in the subject of the ticket.
2196
2197 =cut
2198
2199 sub LimitSubject {
2200     my $self = shift;
2201     my %args = (@_);
2202     $self->Limit(
2203         FIELD       => 'Subject',
2204         VALUE       => $args{'VALUE'},
2205         OPERATOR    => $args{'OPERATOR'},
2206         DESCRIPTION => join( ' ',
2207             $self->loc('Subject'), $args{'OPERATOR'}, $args{'VALUE'}, ),
2208     );
2209 }
2210
2211 # }}}
2212
2213 # }}}
2214
2215 # {{{ Limit based on ticket numerical attributes
2216 # Things that can be > < = !=
2217
2218 # {{{ sub LimitId
2219
2220 =head2 LimitId
2221
2222 Takes a paramhash with the fields OPERATOR and VALUE.
2223 OPERATOR is one of =, >, < or !=.
2224 VALUE is a ticket Id to search for
2225
2226 =cut
2227
2228 sub LimitId {
2229     my $self = shift;
2230     my %args = (
2231         OPERATOR => '=',
2232         @_
2233     );
2234
2235     $self->Limit(
2236         FIELD       => 'id',
2237         VALUE       => $args{'VALUE'},
2238         OPERATOR    => $args{'OPERATOR'},
2239         DESCRIPTION =>
2240             join( ' ', $self->loc('Id'), $args{'OPERATOR'}, $args{'VALUE'}, ),
2241     );
2242 }
2243
2244 # }}}
2245
2246 # {{{ sub LimitPriority
2247
2248 =head2 LimitPriority
2249
2250 Takes a paramhash with the fields OPERATOR and VALUE.
2251 OPERATOR is one of =, >, < or !=.
2252 VALUE is a value to match the ticket\'s priority against
2253
2254 =cut
2255
2256 sub LimitPriority {
2257     my $self = shift;
2258     my %args = (@_);
2259     $self->Limit(
2260         FIELD       => 'Priority',
2261         VALUE       => $args{'VALUE'},
2262         OPERATOR    => $args{'OPERATOR'},
2263         DESCRIPTION => join( ' ',
2264             $self->loc('Priority'),
2265             $args{'OPERATOR'}, $args{'VALUE'}, ),
2266     );
2267 }
2268
2269 # }}}
2270
2271 # {{{ sub LimitInitialPriority
2272
2273 =head2 LimitInitialPriority
2274
2275 Takes a paramhash with the fields OPERATOR and VALUE.
2276 OPERATOR is one of =, >, < or !=.
2277 VALUE is a value to match the ticket\'s initial priority against
2278
2279
2280 =cut
2281
2282 sub LimitInitialPriority {
2283     my $self = shift;
2284     my %args = (@_);
2285     $self->Limit(
2286         FIELD       => 'InitialPriority',
2287         VALUE       => $args{'VALUE'},
2288         OPERATOR    => $args{'OPERATOR'},
2289         DESCRIPTION => join( ' ',
2290             $self->loc('Initial Priority'), $args{'OPERATOR'},
2291             $args{'VALUE'}, ),
2292     );
2293 }
2294
2295 # }}}
2296
2297 # {{{ sub LimitFinalPriority
2298
2299 =head2 LimitFinalPriority
2300
2301 Takes a paramhash with the fields OPERATOR and VALUE.
2302 OPERATOR is one of =, >, < or !=.
2303 VALUE is a value to match the ticket\'s final priority against
2304
2305 =cut
2306
2307 sub LimitFinalPriority {
2308     my $self = shift;
2309     my %args = (@_);
2310     $self->Limit(
2311         FIELD       => 'FinalPriority',
2312         VALUE       => $args{'VALUE'},
2313         OPERATOR    => $args{'OPERATOR'},
2314         DESCRIPTION => join( ' ',
2315             $self->loc('Final Priority'), $args{'OPERATOR'},
2316             $args{'VALUE'}, ),
2317     );
2318 }
2319
2320 # }}}
2321
2322 # {{{ sub LimitTimeWorked
2323
2324 =head2 LimitTimeWorked
2325
2326 Takes a paramhash with the fields OPERATOR and VALUE.
2327 OPERATOR is one of =, >, < or !=.
2328 VALUE is a value to match the ticket's TimeWorked attribute
2329
2330 =cut
2331
2332 sub LimitTimeWorked {
2333     my $self = shift;
2334     my %args = (@_);
2335     $self->Limit(
2336         FIELD       => 'TimeWorked',
2337         VALUE       => $args{'VALUE'},
2338         OPERATOR    => $args{'OPERATOR'},
2339         DESCRIPTION => join( ' ',
2340             $self->loc('Time Worked'),
2341             $args{'OPERATOR'}, $args{'VALUE'}, ),
2342     );
2343 }
2344
2345 # }}}
2346
2347 # {{{ sub LimitTimeLeft
2348
2349 =head2 LimitTimeLeft
2350
2351 Takes a paramhash with the fields OPERATOR and VALUE.
2352 OPERATOR is one of =, >, < or !=.
2353 VALUE is a value to match the ticket's TimeLeft attribute
2354
2355 =cut
2356
2357 sub LimitTimeLeft {
2358     my $self = shift;
2359     my %args = (@_);
2360     $self->Limit(
2361         FIELD       => 'TimeLeft',
2362         VALUE       => $args{'VALUE'},
2363         OPERATOR    => $args{'OPERATOR'},
2364         DESCRIPTION => join( ' ',
2365             $self->loc('Time Left'),
2366             $args{'OPERATOR'}, $args{'VALUE'}, ),
2367     );
2368 }
2369
2370 # }}}
2371
2372 # }}}
2373
2374 # {{{ Limiting based on attachment attributes
2375
2376 # {{{ sub LimitContent
2377
2378 =head2 LimitContent
2379
2380 Takes a paramhash with the fields OPERATOR and VALUE.
2381 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2382 VALUE is a string to search for in the body of the ticket
2383
2384 =cut
2385
2386 sub LimitContent {
2387     my $self = shift;
2388     my %args = (@_);
2389     $self->Limit(
2390         FIELD       => 'Content',
2391         VALUE       => $args{'VALUE'},
2392         OPERATOR    => $args{'OPERATOR'},
2393         DESCRIPTION => join( ' ',
2394             $self->loc('Ticket content'), $args{'OPERATOR'},
2395             $args{'VALUE'}, ),
2396     );
2397 }
2398
2399 # }}}
2400
2401 # {{{ sub LimitFilename
2402
2403 =head2 LimitFilename
2404
2405 Takes a paramhash with the fields OPERATOR and VALUE.
2406 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2407 VALUE is a string to search for in the body of the ticket
2408
2409 =cut
2410
2411 sub LimitFilename {
2412     my $self = shift;
2413     my %args = (@_);
2414     $self->Limit(
2415         FIELD       => 'Filename',
2416         VALUE       => $args{'VALUE'},
2417         OPERATOR    => $args{'OPERATOR'},
2418         DESCRIPTION => join( ' ',
2419             $self->loc('Attachment filename'), $args{'OPERATOR'},
2420             $args{'VALUE'}, ),
2421     );
2422 }
2423
2424 # }}}
2425 # {{{ sub LimitContentType
2426
2427 =head2 LimitContentType
2428
2429 Takes a paramhash with the fields OPERATOR and VALUE.
2430 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2431 VALUE is a content type to search ticket attachments for
2432
2433 =cut
2434
2435 sub LimitContentType {
2436     my $self = shift;
2437     my %args = (@_);
2438     $self->Limit(
2439         FIELD       => 'ContentType',
2440         VALUE       => $args{'VALUE'},
2441         OPERATOR    => $args{'OPERATOR'},
2442         DESCRIPTION => join( ' ',
2443             $self->loc('Ticket content type'), $args{'OPERATOR'},
2444             $args{'VALUE'}, ),
2445     );
2446 }
2447
2448 # }}}
2449
2450 # }}}
2451
2452 # {{{ Limiting based on people
2453
2454 # {{{ sub LimitOwner
2455
2456 =head2 LimitOwner
2457
2458 Takes a paramhash with the fields OPERATOR and VALUE.
2459 OPERATOR is one of = or !=.
2460 VALUE is a user id.
2461
2462 =cut
2463
2464 sub LimitOwner {
2465     my $self = shift;
2466     my %args = (
2467         OPERATOR => '=',
2468         @_
2469     );
2470
2471     my $owner = new RT::User( $self->CurrentUser );
2472     $owner->Load( $args{'VALUE'} );
2473
2474     # FIXME: check for a valid $owner
2475     $self->Limit(
2476         FIELD       => 'Owner',
2477         VALUE       => $args{'VALUE'},
2478         OPERATOR    => $args{'OPERATOR'},
2479         DESCRIPTION => join( ' ',
2480             $self->loc('Owner'), $args{'OPERATOR'}, $owner->Name(), ),
2481     );
2482
2483 }
2484
2485 # }}}
2486
2487 # {{{ Limiting watchers
2488
2489 # {{{ sub LimitWatcher
2490
2491 =head2 LimitWatcher
2492
2493   Takes a paramhash with the fields OPERATOR, TYPE and VALUE.
2494   OPERATOR is one of =, LIKE, NOT LIKE or !=.
2495   VALUE is a value to match the ticket\'s watcher email addresses against
2496   TYPE is the sort of watchers you want to match against. Leave it undef if you want to search all of them
2497
2498
2499 =cut
2500
2501 sub LimitWatcher {
2502     my $self = shift;
2503     my %args = (
2504         OPERATOR => '=',
2505         VALUE    => undef,
2506         TYPE     => undef,
2507         @_
2508     );
2509
2510     #build us up a description
2511     my ( $watcher_type, $desc );
2512     if ( $args{'TYPE'} ) {
2513         $watcher_type = $args{'TYPE'};
2514     }
2515     else {
2516         $watcher_type = "Watcher";
2517     }
2518
2519     $self->Limit(
2520         FIELD       => $watcher_type,
2521         VALUE       => $args{'VALUE'},
2522         OPERATOR    => $args{'OPERATOR'},
2523         TYPE        => $args{'TYPE'},
2524         DESCRIPTION => join( ' ',
2525             $self->loc($watcher_type),
2526             $args{'OPERATOR'}, $args{'VALUE'}, ),
2527     );
2528 }
2529
2530 # }}}
2531
2532 # }}}
2533
2534 # }}}
2535
2536 # {{{ Limiting based on links
2537
2538 # {{{ LimitLinkedTo
2539
2540 =head2 LimitLinkedTo
2541
2542 LimitLinkedTo takes a paramhash with two fields: TYPE and TARGET
2543 TYPE limits the sort of link we want to search on
2544
2545 TYPE = { RefersTo, MemberOf, DependsOn }
2546
2547 TARGET is the id or URI of the TARGET of the link
2548
2549 =cut
2550
2551 sub LimitLinkedTo {
2552     my $self = shift;
2553     my %args = (
2554         TARGET   => undef,
2555         TYPE     => undef,
2556         OPERATOR => '=',
2557         @_
2558     );
2559
2560     $self->Limit(
2561         FIELD       => 'LinkedTo',
2562         BASE        => undef,
2563         TARGET      => $args{'TARGET'},
2564         TYPE        => $args{'TYPE'},
2565         DESCRIPTION => $self->loc(
2566             "Tickets [_1] by [_2]",
2567             $self->loc( $args{'TYPE'} ),
2568             $args{'TARGET'}
2569         ),
2570         OPERATOR    => $args{'OPERATOR'},
2571     );
2572 }
2573
2574 # }}}
2575
2576 # {{{ LimitLinkedFrom
2577
2578 =head2 LimitLinkedFrom
2579
2580 LimitLinkedFrom takes a paramhash with two fields: TYPE and BASE
2581 TYPE limits the sort of link we want to search on
2582
2583
2584 BASE is the id or URI of the BASE of the link
2585
2586 =cut
2587
2588 sub LimitLinkedFrom {
2589     my $self = shift;
2590     my %args = (
2591         BASE     => undef,
2592         TYPE     => undef,
2593         OPERATOR => '=',
2594         @_
2595     );
2596
2597     # translate RT2 From/To naming to RT3 TicketSQL naming
2598     my %fromToMap = qw(DependsOn DependentOn
2599         MemberOf  HasMember
2600         RefersTo  ReferredToBy);
2601
2602     my $type = $args{'TYPE'};
2603     $type = $fromToMap{$type} if exists( $fromToMap{$type} );
2604
2605     $self->Limit(
2606         FIELD       => 'LinkedTo',
2607         TARGET      => undef,
2608         BASE        => $args{'BASE'},
2609         TYPE        => $type,
2610         DESCRIPTION => $self->loc(
2611             "Tickets [_1] [_2]",
2612             $self->loc( $args{'TYPE'} ),
2613             $args{'BASE'},
2614         ),
2615         OPERATOR    => $args{'OPERATOR'},
2616     );
2617 }
2618
2619 # }}}
2620
2621 # {{{ LimitMemberOf
2622 sub LimitMemberOf {
2623     my $self      = shift;
2624     my $ticket_id = shift;
2625     return $self->LimitLinkedTo(
2626         @_,
2627         TARGET => $ticket_id,
2628         TYPE   => 'MemberOf',
2629     );
2630 }
2631
2632 # }}}
2633
2634 # {{{ LimitHasMember
2635 sub LimitHasMember {
2636     my $self      = shift;
2637     my $ticket_id = shift;
2638     return $self->LimitLinkedFrom(
2639         @_,
2640         BASE => "$ticket_id",
2641         TYPE => 'HasMember',
2642     );
2643
2644 }
2645
2646 # }}}
2647
2648 # {{{ LimitDependsOn
2649
2650 sub LimitDependsOn {
2651     my $self      = shift;
2652     my $ticket_id = shift;
2653     return $self->LimitLinkedTo(
2654         @_,
2655         TARGET => $ticket_id,
2656         TYPE   => 'DependsOn',
2657     );
2658
2659 }
2660
2661 # }}}
2662
2663 # {{{ LimitDependedOnBy
2664
2665 sub LimitDependedOnBy {
2666     my $self      = shift;
2667     my $ticket_id = shift;
2668     return $self->LimitLinkedFrom(
2669         @_,
2670         BASE => $ticket_id,
2671         TYPE => 'DependentOn',
2672     );
2673
2674 }
2675
2676 # }}}
2677
2678 # {{{ LimitRefersTo
2679
2680 sub LimitRefersTo {
2681     my $self      = shift;
2682     my $ticket_id = shift;
2683     return $self->LimitLinkedTo(
2684         @_,
2685         TARGET => $ticket_id,
2686         TYPE   => 'RefersTo',
2687     );
2688
2689 }
2690
2691 # }}}
2692
2693 # {{{ LimitReferredToBy
2694
2695 sub LimitReferredToBy {
2696     my $self      = shift;
2697     my $ticket_id = shift;
2698     return $self->LimitLinkedFrom(
2699         @_,
2700         BASE => $ticket_id,
2701         TYPE => 'ReferredToBy',
2702     );
2703 }
2704
2705 # }}}
2706
2707 # }}}
2708
2709 # {{{ limit based on ticket date attribtes
2710
2711 # {{{ sub LimitDate
2712
2713 =head2 LimitDate (FIELD => 'DateField', OPERATOR => $oper, VALUE => $ISODate)
2714
2715 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
2716
2717 OPERATOR is one of > or <
2718 VALUE is a date and time in ISO format in GMT
2719 FIELD is one of Starts, Started, Told, Created, Resolved, LastUpdated
2720
2721 There are also helper functions of the form LimitFIELD that eliminate
2722 the need to pass in a FIELD argument.
2723
2724 =cut
2725
2726 sub LimitDate {
2727     my $self = shift;
2728     my %args = (
2729         FIELD    => undef,
2730         VALUE    => undef,
2731         OPERATOR => undef,
2732
2733         @_
2734     );
2735
2736     #Set the description if we didn't get handed it above
2737     unless ( $args{'DESCRIPTION'} ) {
2738         $args{'DESCRIPTION'} = $args{'FIELD'} . " "
2739             . $args{'OPERATOR'} . " "
2740             . $args{'VALUE'} . " GMT";
2741     }
2742
2743     $self->Limit(%args);
2744
2745 }
2746
2747 # }}}
2748
2749 sub LimitCreated {
2750     my $self = shift;
2751     $self->LimitDate( FIELD => 'Created', @_ );
2752 }
2753
2754 sub LimitDue {
2755     my $self = shift;
2756     $self->LimitDate( FIELD => 'Due', @_ );
2757
2758 }
2759
2760 sub LimitStarts {
2761     my $self = shift;
2762     $self->LimitDate( FIELD => 'Starts', @_ );
2763
2764 }
2765
2766 sub LimitStarted {
2767     my $self = shift;
2768     $self->LimitDate( FIELD => 'Started', @_ );
2769 }
2770
2771 sub LimitResolved {
2772     my $self = shift;
2773     $self->LimitDate( FIELD => 'Resolved', @_ );
2774 }
2775
2776 sub LimitTold {
2777     my $self = shift;
2778     $self->LimitDate( FIELD => 'Told', @_ );
2779 }
2780
2781 sub LimitLastUpdated {
2782     my $self = shift;
2783     $self->LimitDate( FIELD => 'LastUpdated', @_ );
2784 }
2785
2786 #
2787 # {{{ sub LimitTransactionDate
2788
2789 =head2 LimitTransactionDate (OPERATOR => $oper, VALUE => $ISODate)
2790
2791 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
2792
2793 OPERATOR is one of > or <
2794 VALUE is a date and time in ISO format in GMT
2795
2796
2797 =cut
2798
2799 sub LimitTransactionDate {
2800     my $self = shift;
2801     my %args = (
2802         FIELD    => 'TransactionDate',
2803         VALUE    => undef,
2804         OPERATOR => undef,
2805
2806         @_
2807     );
2808
2809     #  <20021217042756.GK28744@pallas.fsck.com>
2810     #    "Kill It" - Jesse.
2811
2812     #Set the description if we didn't get handed it above
2813     unless ( $args{'DESCRIPTION'} ) {
2814         $args{'DESCRIPTION'} = $args{'FIELD'} . " "
2815             . $args{'OPERATOR'} . " "
2816             . $args{'VALUE'} . " GMT";
2817     }
2818
2819     $self->Limit(%args);
2820
2821 }
2822
2823 # }}}
2824
2825 # }}}
2826
2827 # {{{ Limit based on custom fields
2828 # {{{ sub LimitCustomField
2829
2830 =head2 LimitCustomField
2831
2832 Takes a paramhash of key/value pairs with the following keys:
2833
2834 =over 4
2835
2836 =item CUSTOMFIELD - CustomField name or id.  If a name is passed, an additional parameter QUEUE may also be passed to distinguish the custom field.
2837
2838 =item OPERATOR - The usual Limit operators
2839
2840 =item VALUE - The value to compare against
2841
2842 =back
2843
2844 =cut
2845
2846 sub LimitCustomField {
2847     my $self = shift;
2848     my %args = (
2849         VALUE       => undef,
2850         CUSTOMFIELD => undef,
2851         OPERATOR    => '=',
2852         DESCRIPTION => undef,
2853         FIELD       => 'CustomFieldValue',
2854         QUOTEVALUE  => 1,
2855         @_
2856     );
2857
2858     my $CF = RT::CustomField->new( $self->CurrentUser );
2859     if ( $args{CUSTOMFIELD} =~ /^\d+$/ ) {
2860         $CF->Load( $args{CUSTOMFIELD} );
2861     }
2862     else {
2863         $CF->LoadByNameAndQueue(
2864             Name  => $args{CUSTOMFIELD},
2865             Queue => $args{QUEUE}
2866         );
2867         $args{CUSTOMFIELD} = $CF->Id;
2868     }
2869
2870     #If we are looking to compare with a null value.
2871     if ( $args{'OPERATOR'} =~ /^is$/i ) {
2872         $args{'DESCRIPTION'}
2873             ||= $self->loc( "Custom field [_1] has no value.", $CF->Name );
2874     }
2875     elsif ( $args{'OPERATOR'} =~ /^is not$/i ) {
2876         $args{'DESCRIPTION'}
2877             ||= $self->loc( "Custom field [_1] has a value.", $CF->Name );
2878     }
2879
2880     # if we're not looking to compare with a null value
2881     else {
2882         $args{'DESCRIPTION'} ||= $self->loc( "Custom field [_1] [_2] [_3]",
2883             $CF->Name, $args{OPERATOR}, $args{VALUE} );
2884     }
2885
2886     if ( defined $args{'QUEUE'} && $args{'QUEUE'} =~ /\D/ ) {
2887         my $QueueObj = RT::Queue->new( $self->CurrentUser );
2888         $QueueObj->Load( $args{'QUEUE'} );
2889         $args{'QUEUE'} = $QueueObj->Id;
2890     }
2891     delete $args{'QUEUE'} unless defined $args{'QUEUE'} && length $args{'QUEUE'};
2892
2893     my @rest;
2894     @rest = ( ENTRYAGGREGATOR => 'AND' )
2895         if ( $CF->Type eq 'SelectMultiple' );
2896
2897     $self->Limit(
2898         VALUE => $args{VALUE},
2899         FIELD => "CF"
2900             .(defined $args{'QUEUE'}? ".{$args{'QUEUE'}}" : '' )
2901             .".{" . $CF->Name . "}",
2902         OPERATOR    => $args{OPERATOR},
2903         CUSTOMFIELD => 1,
2904         @rest,
2905     );
2906
2907     $self->{'RecalcTicketLimits'} = 1;
2908 }
2909
2910 # }}}
2911 # }}}
2912
2913 # {{{ sub _NextIndex
2914
2915 =head2 _NextIndex
2916
2917 Keep track of the counter for the array of restrictions
2918
2919 =cut
2920
2921 sub _NextIndex {
2922     my $self = shift;
2923     return ( $self->{'restriction_index'}++ );
2924 }
2925
2926 # }}}
2927
2928 # }}}
2929
2930 # {{{ Core bits to make this a DBIx::SearchBuilder object
2931
2932 # {{{ sub _Init
2933 sub _Init {
2934     my $self = shift;
2935     $self->{'table'}                   = "Tickets";
2936     $self->{'RecalcTicketLimits'}      = 1;
2937     $self->{'looking_at_effective_id'} = 0;
2938     $self->{'looking_at_type'}         = 0;
2939     $self->{'restriction_index'}       = 1;
2940     $self->{'primary_key'}             = "id";
2941     delete $self->{'items_array'};
2942     delete $self->{'item_map'};
2943     delete $self->{'columns_to_display'};
2944     $self->SUPER::_Init(@_);
2945
2946     $self->_InitSQL;
2947
2948 }
2949
2950 # }}}
2951
2952 # {{{ sub Count
2953 sub Count {
2954     my $self = shift;
2955     $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2956     return ( $self->SUPER::Count() );
2957 }
2958
2959 # }}}
2960
2961 # {{{ sub CountAll
2962 sub CountAll {
2963     my $self = shift;
2964     $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2965     return ( $self->SUPER::CountAll() );
2966 }
2967
2968 # }}}
2969
2970 # {{{ sub ItemsArrayRef
2971
2972 =head2 ItemsArrayRef
2973
2974 Returns a reference to the set of all items found in this search
2975
2976 =cut
2977
2978 sub ItemsArrayRef {
2979     my $self = shift;
2980
2981     return $self->{'items_array'} if $self->{'items_array'};
2982
2983     my $placeholder = $self->_ItemsCounter;
2984     $self->GotoFirstItem();
2985     while ( my $item = $self->Next ) {
2986         push( @{ $self->{'items_array'} }, $item );
2987     }
2988     $self->GotoItem($placeholder);
2989     $self->{'items_array'}
2990         = $self->ItemsOrderBy( $self->{'items_array'} );
2991
2992     return $self->{'items_array'};
2993 }
2994
2995 sub ItemsArrayRefWindow {
2996     my $self = shift;
2997     my $window = shift;
2998
2999     my @old = ($self->_ItemsCounter, $self->RowsPerPage, $self->FirstRow+1);
3000
3001     $self->RowsPerPage( $window );
3002     $self->FirstRow(1);
3003     $self->GotoFirstItem;
3004
3005     my @res;
3006     while ( my $item = $self->Next ) {
3007         push @res, $item;
3008     }
3009
3010     $self->RowsPerPage( $old[1] );
3011     $self->FirstRow( $old[2] );
3012     $self->GotoItem( $old[0] );
3013
3014     return \@res;
3015 }
3016
3017 # }}}
3018
3019 # {{{ sub Next
3020 sub Next {
3021     my $self = shift;
3022
3023     $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
3024
3025     my $Ticket = $self->SUPER::Next;
3026     return $Ticket unless $Ticket;
3027
3028     if ( $Ticket->__Value('Status') eq 'deleted'
3029         && !$self->{'allow_deleted_search'} )
3030     {
3031         return $self->Next;
3032     }
3033     elsif ( RT->Config->Get('UseSQLForACLChecks') ) {
3034         # if we found a ticket with this option enabled then
3035         # all tickets we found are ACLed, cache this fact
3036         my $key = join ";:;", $self->CurrentUser->id, 'ShowTicket', 'RT::Ticket-'. $Ticket->id;
3037         $RT::Principal::_ACL_CACHE->set( $key => 1 );
3038         return $Ticket;
3039     }
3040     elsif ( $Ticket->CurrentUserHasRight('ShowTicket') ) {
3041         # has rights
3042         return $Ticket;
3043     }
3044     else {
3045         # If the user doesn't have the right to show this ticket
3046         return $self->Next;
3047     }
3048 }
3049
3050 sub _DoSearch {
3051     my $self = shift;
3052     $self->CurrentUserCanSee if RT->Config->Get('UseSQLForACLChecks');
3053     return $self->SUPER::_DoSearch( @_ );
3054 }
3055
3056 sub _DoCount {
3057     my $self = shift;
3058     $self->CurrentUserCanSee if RT->Config->Get('UseSQLForACLChecks');
3059     return $self->SUPER::_DoCount( @_ );
3060 }
3061
3062 sub _RolesCanSee {
3063     my $self = shift;
3064
3065     my $cache_key = 'RolesHasRight;:;ShowTicket';
3066  
3067     if ( my $cached = $RT::Principal::_ACL_CACHE->fetch( $cache_key ) ) {
3068         return %$cached;
3069     }
3070
3071     my $ACL = RT::ACL->new( $RT::SystemUser );
3072     $ACL->Limit( FIELD => 'RightName', VALUE => 'ShowTicket' );
3073     $ACL->Limit( FIELD => 'PrincipalType', OPERATOR => '!=', VALUE => 'Group' );
3074     my $principal_alias = $ACL->Join(
3075         ALIAS1 => 'main',
3076         FIELD1 => 'PrincipalId',
3077         TABLE2 => 'Principals',
3078         FIELD2 => 'id',
3079     );
3080     $ACL->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3081
3082     my %res = ();
3083     while ( my $ACE = $ACL->Next ) {
3084         my $role = $ACE->PrincipalType;
3085         my $type = $ACE->ObjectType;
3086         if ( $type eq 'RT::System' ) {
3087             $res{ $role } = 1;
3088         }
3089         elsif ( $type eq 'RT::Queue' ) {
3090             next if $res{ $role } && !ref $res{ $role };
3091             push @{ $res{ $role } ||= [] }, $ACE->ObjectId;
3092         }
3093         else {
3094             $RT::Logger->error('ShowTicket right is granted on unsupported object');
3095         }
3096     }
3097     $RT::Principal::_ACL_CACHE->set( $cache_key => \%res );
3098     return %res;
3099 }
3100
3101 sub _DirectlyCanSeeIn {
3102     my $self = shift;
3103     my $id = $self->CurrentUser->id;
3104
3105     my $cache_key = 'User-'. $id .';:;ShowTicket;:;DirectlyCanSeeIn';
3106     if ( my $cached = $RT::Principal::_ACL_CACHE->fetch( $cache_key ) ) {
3107         return @$cached;
3108     }
3109
3110     my $ACL = RT::ACL->new( $RT::SystemUser );
3111     $ACL->Limit( FIELD => 'RightName', VALUE => 'ShowTicket' );
3112     my $principal_alias = $ACL->Join(
3113         ALIAS1 => 'main',
3114         FIELD1 => 'PrincipalId',
3115         TABLE2 => 'Principals',
3116         FIELD2 => 'id',
3117     );
3118     $ACL->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3119     my $cgm_alias = $ACL->Join(
3120         ALIAS1 => 'main',
3121         FIELD1 => 'PrincipalId',
3122         TABLE2 => 'CachedGroupMembers',
3123         FIELD2 => 'GroupId',
3124     );
3125     $ACL->Limit( ALIAS => $cgm_alias, FIELD => 'MemberId', VALUE => $id );
3126     $ACL->Limit( ALIAS => $cgm_alias, FIELD => 'Disabled', VALUE => 0 );
3127
3128     my @res = ();
3129     while ( my $ACE = $ACL->Next ) {
3130         my $type = $ACE->ObjectType;
3131         if ( $type eq 'RT::System' ) {
3132             # If user is direct member of a group that has the right
3133             # on the system then he can see any ticket
3134             $RT::Principal::_ACL_CACHE->set( $cache_key => [-1] );
3135             return (-1);
3136         }
3137         elsif ( $type eq 'RT::Queue' ) {
3138             push @res, $ACE->ObjectId;
3139         }
3140         else {
3141             $RT::Logger->error('ShowTicket right is granted on unsupported object');
3142         }
3143     }
3144     $RT::Principal::_ACL_CACHE->set( $cache_key => \@res );
3145     return @res;
3146 }
3147
3148 sub CurrentUserCanSee {
3149     my $self = shift;
3150     return if $self->{'_sql_current_user_can_see_applied'};
3151
3152     return $self->{'_sql_current_user_can_see_applied'} = 1
3153         if $self->CurrentUser->UserObj->HasRight(
3154             Right => 'SuperUser', Object => $RT::System
3155         );
3156
3157     my $id = $self->CurrentUser->id;
3158
3159     # directly can see in all queues then we have nothing to do
3160     my @direct_queues = $self->_DirectlyCanSeeIn;
3161     return $self->{'_sql_current_user_can_see_applied'} = 1
3162         if @direct_queues && $direct_queues[0] == -1;
3163
3164     my %roles = $self->_RolesCanSee;
3165     {
3166         my %skip = map { $_ => 1 } @direct_queues;
3167         foreach my $role ( keys %roles ) {
3168             next unless ref $roles{ $role };
3169
3170             my @queues = grep !$skip{$_}, @{ $roles{ $role } };
3171             if ( @queues ) {
3172                 $roles{ $role } = \@queues;
3173             } else {
3174                 delete $roles{ $role };
3175             }
3176         }
3177     }
3178
3179 # there is no global watchers, only queues and tickes, if at
3180 # some point we will add global roles then it's gonna blow
3181 # the idea here is that if the right is set globaly for a role
3182 # and user plays this role for a queue directly not a ticket
3183 # then we have to check in advance
3184     if ( my @tmp = grep $_ ne 'Owner' && !ref $roles{ $_ }, keys %roles ) {
3185
3186         my $groups = RT::Groups->new( $RT::SystemUser );
3187         $groups->Limit( FIELD => 'Domain', VALUE => 'RT::Queue-Role' );
3188         foreach ( @tmp ) {
3189             $groups->Limit( FIELD => 'Type', VALUE => $_ );
3190         }
3191         my $principal_alias = $groups->Join(
3192             ALIAS1 => 'main',
3193             FIELD1 => 'id',
3194             TABLE2 => 'Principals',
3195             FIELD2 => 'id',
3196         );
3197         $groups->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3198         my $cgm_alias = $groups->Join(
3199             ALIAS1 => 'main',
3200             FIELD1 => 'id',
3201             TABLE2 => 'CachedGroupMembers',
3202             FIELD2 => 'GroupId',
3203         );
3204         $groups->Limit( ALIAS => $cgm_alias, FIELD => 'MemberId', VALUE => $id );
3205         $groups->Limit( ALIAS => $cgm_alias, FIELD => 'Disabled', VALUE => 0 );
3206         while ( my $group = $groups->Next ) {
3207             push @direct_queues, $group->Instance;
3208         }
3209     }
3210
3211     unless ( @direct_queues || keys %roles ) {
3212         $self->SUPER::Limit(
3213             SUBCLAUSE => 'ACL',
3214             ALIAS => 'main',
3215             FIELD => 'id',
3216             VALUE => 0,
3217             ENTRYAGGREGATOR => 'AND',
3218         );
3219         return $self->{'_sql_current_user_can_see_applied'} = 1;
3220     }
3221
3222     {
3223         my $join_roles = keys %roles;
3224         $join_roles = 0 if $join_roles == 1 && $roles{'Owner'};
3225         my ($role_group_alias, $cgm_alias);
3226         if ( $join_roles ) {
3227             $role_group_alias = $self->_RoleGroupsJoin( New => 1 );
3228             $cgm_alias = $self->_GroupMembersJoin( GroupsAlias => $role_group_alias );
3229             $self->SUPER::Limit(
3230                 LEFTJOIN   => $cgm_alias,
3231                 FIELD      => 'MemberId',
3232                 OPERATOR   => '=',
3233                 VALUE      => $id,
3234             );
3235         }
3236         my $limit_queues = sub {
3237             my $ea = shift;
3238             my @queues = @_;
3239
3240             return unless @queues;
3241             if ( @queues == 1 ) {
3242                 $self->SUPER::Limit(
3243                     SUBCLAUSE => 'ACL',
3244                     ALIAS => 'main',
3245                     FIELD => 'Queue',
3246                     VALUE => $_[0],
3247                     ENTRYAGGREGATOR => $ea,
3248                 );
3249             } else {
3250                 $self->SUPER::_OpenParen('ACL');
3251                 foreach my $q ( @queues ) {
3252                     $self->SUPER::Limit(
3253                         SUBCLAUSE => 'ACL',
3254                         ALIAS => 'main',
3255                         FIELD => 'Queue',
3256                         VALUE => $q,
3257                         ENTRYAGGREGATOR => $ea,
3258                     );
3259                     $ea = 'OR';
3260                 }
3261                 $self->SUPER::_CloseParen('ACL');
3262             }
3263             return 1;
3264         };
3265
3266         $self->SUPER::_OpenParen('ACL');
3267         my $ea = 'AND';
3268         $ea = 'OR' if $limit_queues->( $ea, @direct_queues );
3269         while ( my ($role, $queues) = each %roles ) {
3270             $self->SUPER::_OpenParen('ACL');
3271             if ( $role eq 'Owner' ) {
3272                 $self->SUPER::Limit(
3273                     SUBCLAUSE => 'ACL',
3274                     FIELD           => 'Owner',
3275                     VALUE           => $id,
3276                     ENTRYAGGREGATOR => $ea,
3277                 );
3278             }
3279             else {
3280                 $self->SUPER::Limit(
3281                     SUBCLAUSE       => 'ACL',
3282                     ALIAS           => $cgm_alias,
3283                     FIELD           => 'MemberId',
3284                     OPERATOR        => 'IS NOT',
3285                     VALUE           => 'NULL',
3286                     QUOTEVALUE      => 0,
3287                     ENTRYAGGREGATOR => $ea,
3288                 );
3289                 $self->SUPER::Limit(
3290                     SUBCLAUSE       => 'ACL',
3291                     ALIAS           => $role_group_alias,
3292                     FIELD           => 'Type',
3293                     VALUE           => $role,
3294                     ENTRYAGGREGATOR => 'AND',
3295                 );
3296             }
3297             $limit_queues->( 'AND', @$queues ) if ref $queues;
3298             $ea = 'OR' if $ea eq 'AND';
3299             $self->SUPER::_CloseParen('ACL');
3300         }
3301         $self->SUPER::_CloseParen('ACL');
3302     }
3303     return $self->{'_sql_current_user_can_see_applied'} = 1;
3304 }
3305
3306 # }}}
3307
3308 # }}}
3309
3310 # {{{ Deal with storing and restoring restrictions
3311
3312 # {{{ sub LoadRestrictions
3313
3314 =head2 LoadRestrictions
3315
3316 LoadRestrictions takes a string which can fully populate the TicketRestrictons hash.
3317 TODO It is not yet implemented
3318
3319 =cut
3320
3321 # }}}
3322
3323 # {{{ sub DescribeRestrictions
3324
3325 =head2 DescribeRestrictions
3326
3327 takes nothing.
3328 Returns a hash keyed by restriction id.
3329 Each element of the hash is currently a one element hash that contains DESCRIPTION which
3330 is a description of the purpose of that TicketRestriction
3331
3332 =cut
3333
3334 sub DescribeRestrictions {
3335     my $self = shift;
3336
3337     my %listing;
3338
3339     foreach my $row ( keys %{ $self->{'TicketRestrictions'} } ) {
3340         $listing{$row} = $self->{'TicketRestrictions'}{$row}{'DESCRIPTION'};
3341     }
3342     return (%listing);
3343 }
3344
3345 # }}}
3346
3347 # {{{ sub RestrictionValues
3348
3349 =head2 RestrictionValues FIELD
3350
3351 Takes a restriction field and returns a list of values this field is restricted
3352 to.
3353
3354 =cut
3355
3356 sub RestrictionValues {
3357     my $self  = shift;
3358     my $field = shift;
3359     map $self->{'TicketRestrictions'}{$_}{'VALUE'}, grep {
3360                $self->{'TicketRestrictions'}{$_}{'FIELD'}    eq $field
3361             && $self->{'TicketRestrictions'}{$_}{'OPERATOR'} eq "="
3362         }
3363         keys %{ $self->{'TicketRestrictions'} };
3364 }
3365
3366 # }}}
3367
3368 # {{{ sub ClearRestrictions
3369
3370 =head2 ClearRestrictions
3371
3372 Removes all restrictions irretrievably
3373
3374 =cut
3375
3376 sub ClearRestrictions {
3377     my $self = shift;
3378     delete $self->{'TicketRestrictions'};
3379     $self->{'looking_at_effective_id'} = 0;
3380     $self->{'looking_at_type'}         = 0;
3381     $self->{'RecalcTicketLimits'}      = 1;
3382 }
3383
3384 # }}}
3385
3386 # {{{ sub DeleteRestriction
3387
3388 =head2 DeleteRestriction
3389
3390 Takes the row Id of a restriction (From DescribeRestrictions' output, for example.
3391 Removes that restriction from the session's limits.
3392
3393 =cut
3394
3395 sub DeleteRestriction {
3396     my $self = shift;
3397     my $row  = shift;
3398     delete $self->{'TicketRestrictions'}{$row};
3399
3400     $self->{'RecalcTicketLimits'} = 1;
3401
3402     #make the underlying easysearch object forget all its preconceptions
3403 }
3404
3405 # }}}
3406
3407 # {{{ sub _RestrictionsToClauses
3408
3409 # Convert a set of oldstyle SB Restrictions to Clauses for RQL
3410
3411 sub _RestrictionsToClauses {
3412     my $self = shift;
3413
3414     my %clause;
3415     foreach my $row ( keys %{ $self->{'TicketRestrictions'} } ) {
3416         my $restriction = $self->{'TicketRestrictions'}{$row};
3417
3418         # We need to reimplement the subclause aggregation that SearchBuilder does.
3419         # Default Subclause is ALIAS.FIELD, and default ALIAS is 'main',
3420         # Then SB AND's the different Subclauses together.
3421
3422         # So, we want to group things into Subclauses, convert them to
3423         # SQL, and then join them with the appropriate DefaultEA.
3424         # Then join each subclause group with AND.
3425
3426         my $field = $restriction->{'FIELD'};
3427         my $realfield = $field;    # CustomFields fake up a fieldname, so
3428                                    # we need to figure that out
3429
3430         # One special case
3431         # Rewrite LinkedTo meta field to the real field
3432         if ( $field =~ /LinkedTo/ ) {
3433             $realfield = $field = $restriction->{'TYPE'};
3434         }
3435
3436         # Two special case
3437         # Handle subkey fields with a different real field
3438         if ( $field =~ /^(\w+)\./ ) {
3439             $realfield = $1;
3440         }
3441
3442         die "I don't know about $field yet"
3443             unless ( exists $FIELD_METADATA{$realfield}
3444                 or $restriction->{CUSTOMFIELD} );
3445
3446         my $type = $FIELD_METADATA{$realfield}->[0];
3447         my $op   = $restriction->{'OPERATOR'};
3448
3449         my $value = (
3450             grep    {defined}
3451                 map { $restriction->{$_} } qw(VALUE TICKET BASE TARGET)
3452         )[0];
3453
3454         # this performs the moral equivalent of defined or/dor/C<//>,
3455         # without the short circuiting.You need to use a 'defined or'
3456         # type thing instead of just checking for truth values, because
3457         # VALUE could be 0.(i.e. "false")
3458
3459         # You could also use this, but I find it less aesthetic:
3460         # (although it does short circuit)
3461         #( defined $restriction->{'VALUE'}? $restriction->{VALUE} :
3462         # defined $restriction->{'TICKET'} ?
3463         # $restriction->{TICKET} :
3464         # defined $restriction->{'BASE'} ?
3465         # $restriction->{BASE} :
3466         # defined $restriction->{'TARGET'} ?
3467         # $restriction->{TARGET} )
3468
3469         my $ea = $restriction->{ENTRYAGGREGATOR}
3470             || $DefaultEA{$type}
3471             || "AND";
3472         if ( ref $ea ) {
3473             die "Invalid operator $op for $field ($type)"
3474                 unless exists $ea->{$op};
3475             $ea = $ea->{$op};
3476         }
3477
3478         # Each CustomField should be put into a different Clause so they
3479         # are ANDed together.
3480         if ( $restriction->{CUSTOMFIELD} ) {
3481             $realfield = $field;
3482         }
3483
3484         exists $clause{$realfield} or $clause{$realfield} = [];
3485
3486         # Escape Quotes
3487         $field =~ s!(['"])!\\$1!g;
3488         $value =~ s!(['"])!\\$1!g;
3489         my $data = [ $ea, $type, $field, $op, $value ];
3490
3491         # here is where we store extra data, say if it's a keyword or
3492         # something.  (I.e. "TYPE SPECIFIC STUFF")
3493
3494         push @{ $clause{$realfield} }, $data;
3495     }
3496     return \%clause;
3497 }
3498
3499 # }}}
3500
3501 # {{{ sub _ProcessRestrictions
3502
3503 =head2 _ProcessRestrictions PARAMHASH
3504
3505 # The new _ProcessRestrictions is somewhat dependent on the SQL stuff,
3506 # but isn't quite generic enough to move into Tickets_Overlay_SQL.
3507
3508 =cut
3509
3510 sub _ProcessRestrictions {
3511     my $self = shift;
3512
3513     #Blow away ticket aliases since we'll need to regenerate them for
3514     #a new search
3515     delete $self->{'TicketAliases'};
3516     delete $self->{'items_array'};
3517     delete $self->{'item_map'};
3518     delete $self->{'raw_rows'};
3519     delete $self->{'rows'};
3520     delete $self->{'count_all'};
3521
3522     my $sql = $self->Query;    # Violating the _SQL namespace
3523     if ( !$sql || $self->{'RecalcTicketLimits'} ) {
3524
3525         #  "Restrictions to Clauses Branch\n";
3526         my $clauseRef = eval { $self->_RestrictionsToClauses; };
3527         if ($@) {
3528             $RT::Logger->error( "RestrictionsToClauses: " . $@ );
3529             $self->FromSQL("");
3530         }
3531         else {
3532             $sql = $self->ClausesToSQL($clauseRef);
3533             $self->FromSQL($sql) if $sql;
3534         }
3535     }
3536
3537     $self->{'RecalcTicketLimits'} = 0;
3538
3539 }
3540
3541 =head2 _BuildItemMap
3542
3543 Build up a L</ItemMap> of first/last/next/prev items, so that we can
3544 display search nav quickly.
3545
3546 =cut
3547
3548 sub _BuildItemMap {
3549     my $self = shift;
3550
3551     my $window = RT->Config->Get('TicketsItemMapSize');
3552
3553     $self->{'item_map'} = {};
3554
3555     my $items = $self->ItemsArrayRefWindow( $window );
3556     return unless $items && @$items;
3557
3558     my $prev = 0;
3559     $self->{'item_map'}{'first'} = $items->[0]->EffectiveId;
3560     for ( my $i = 0; $i < @$items; $i++ ) {
3561         my $item = $items->[$i];
3562         my $id = $item->EffectiveId;
3563         $self->{'item_map'}{$id}{'defined'} = 1;
3564         $self->{'item_map'}{$id}{'prev'}    = $prev;
3565         $self->{'item_map'}{$id}{'next'}    = $items->[$i+1]->EffectiveId
3566             if $items->[$i+1];
3567         $prev = $id;
3568     }
3569     $self->{'item_map'}{'last'} = $prev
3570         if !$window || @$items < $window;
3571 }
3572
3573 =head2 ItemMap
3574
3575 Returns an a map of all items found by this search. The map is a hash
3576 of the form:
3577
3578     {
3579         first => <first ticket id found>,
3580         last => <last ticket id found or undef>,
3581
3582         <ticket id> => {
3583             prev => <the ticket id found before>,
3584             next => <the ticket id found after>,
3585         },
3586         <ticket id> => {
3587             prev => ...,
3588             next => ...,
3589         },
3590     }
3591
3592 =cut
3593
3594 sub ItemMap {
3595     my $self = shift;
3596     $self->_BuildItemMap unless $self->{'item_map'};
3597     return $self->{'item_map'};
3598 }
3599
3600
3601 # }}}
3602
3603 # }}}
3604
3605 =head2 PrepForSerialization
3606
3607 You don't want to serialize a big tickets object, as
3608 the {items} hash will be instantly invalid _and_ eat
3609 lots of space
3610
3611 =cut
3612
3613 sub PrepForSerialization {
3614     my $self = shift;
3615     delete $self->{'items'};
3616     delete $self->{'items_array'};
3617     $self->RedoSearch();
3618 }
3619
3620 =head1 FLAGS
3621
3622 RT::Tickets supports several flags which alter search behavior:
3623
3624
3625 allow_deleted_search  (Otherwise never show deleted tickets in search results)
3626 looking_at_type (otherwise limit to type=ticket)
3627
3628 These flags are set by calling 
3629
3630 $tickets->{'flagname'} = 1;
3631
3632 BUG: There should be an API for this
3633
3634
3635
3636 =cut
3637
3638 1;
3639
3640
3641