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