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