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