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