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