first pass RT4 merge, RT#13852
[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     my $table2;
1998     # compound subkey: separate into table name and field in that table
1999     # (must be linked by custnum)
2000     $subfield = lc($subfield);
2001     ($table2, $subfield) = ($1, $2) if $subfield =~ /^(\w+)?\.(\w+)$/;
2002     $subfield = $pkey if $subfield eq 'number';
2003
2004     # if it's compound, create a join from cust_main or cust_svc to that 
2005     # table, using custnum or svcnum, and Limit on that table instead.
2006     foreach my $a (@alias) {
2007       if ( $table2 ) {
2008           $a = $self->Join(
2009               TYPE        => 'LEFT',
2010               ALIAS1      => $a,
2011               FIELD1      => $pkey,
2012               TABLE2      => $table2,
2013               FIELD2      => $pkey,
2014           );
2015       }
2016
2017       # do the actual Limit
2018       $self->SUPER::Limit(
2019           LEFTJOIN        => $a,
2020           FIELD           => $subfield,
2021           OPERATOR        => $op,
2022           VALUE           => $value,
2023           ENTRYAGGREGATOR => 'AND',
2024           # no SUBCLAUSE needed, limits on different aliases across left joins
2025           # are inherently independent
2026       );
2027
2028       # then, since it's a left join, exclude tickets for which there is now 
2029       # no matching record in the table we just limited on.  (Or where there 
2030       # is a matching record, if $is_negative.)
2031       # For a cust_main query (where there are two different aliases), this 
2032       # will produce a subclause: "cust_main_1.custnum IS NOT NULL OR 
2033       # cust_main_2.custnum IS NOT NULL" (or "IS NULL AND..." for a negative
2034       # query).
2035       $self->_SQLLimit(
2036           %rest,
2037           ALIAS           => $a,
2038           FIELD           => $pkey,
2039           OPERATOR        => $is_negative ? 'IS' : 'IS NOT',
2040           VALUE           => 'NULL',
2041           QUOTEVALUE      => 0,
2042           ENTRYAGGREGATOR => $is_negative ? 'AND' : 'OR',
2043           SUBCLAUSE       => 'fs_limit',
2044       );
2045     }
2046 }
2047
2048 #Freeside
2049
2050 # }}}
2051
2052 # {{{ Limit the result set based on content
2053
2054 # {{{ sub Limit
2055
2056 =head2 Limit
2057
2058 Takes a paramhash with the fields FIELD, OPERATOR, VALUE and DESCRIPTION
2059 Generally best called from LimitFoo methods
2060
2061 =cut
2062
2063 sub Limit {
2064     my $self = shift;
2065     my %args = (
2066         FIELD       => undef,
2067         OPERATOR    => '=',
2068         VALUE       => undef,
2069         DESCRIPTION => undef,
2070         @_
2071     );
2072     $args{'DESCRIPTION'} = $self->loc(
2073         "[_1] [_2] [_3]",  $args{'FIELD'},
2074         $args{'OPERATOR'}, $args{'VALUE'}
2075         )
2076         if ( !defined $args{'DESCRIPTION'} );
2077
2078     my $index = $self->_NextIndex;
2079
2080 # make the TicketRestrictions hash the equivalent of whatever we just passed in;
2081
2082     %{ $self->{'TicketRestrictions'}{$index} } = %args;
2083
2084     $self->{'RecalcTicketLimits'} = 1;
2085
2086 # If we're looking at the effective id, we don't want to append the other clause
2087 # which limits us to tickets where id = effective id
2088     if ( $args{'FIELD'} eq 'EffectiveId'
2089         && ( !$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) )
2090     {
2091         $self->{'looking_at_effective_id'} = 1;
2092     }
2093
2094     if ( $args{'FIELD'} eq 'Type'
2095         && ( !$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) )
2096     {
2097         $self->{'looking_at_type'} = 1;
2098     }
2099
2100     return ($index);
2101 }
2102
2103 # }}}
2104
2105 =head2 FreezeLimits
2106
2107 Returns a frozen string suitable for handing back to ThawLimits.
2108
2109 =cut
2110
2111 sub _FreezeThawKeys {
2112     'TicketRestrictions', 'restriction_index', 'looking_at_effective_id',
2113         'looking_at_type';
2114 }
2115
2116 # {{{ sub FreezeLimits
2117
2118 sub FreezeLimits {
2119     my $self = shift;
2120     require Storable;
2121     require MIME::Base64;
2122     MIME::Base64::base64_encode(
2123         Storable::freeze( \@{$self}{ $self->_FreezeThawKeys } ) );
2124 }
2125
2126 # }}}
2127
2128 =head2 ThawLimits
2129
2130 Take a frozen Limits string generated by FreezeLimits and make this tickets
2131 object have that set of limits.
2132
2133 =cut
2134
2135 # {{{ sub ThawLimits
2136
2137 sub ThawLimits {
2138     my $self = shift;
2139     my $in   = shift;
2140
2141     #if we don't have $in, get outta here.
2142     return undef unless ($in);
2143
2144     $self->{'RecalcTicketLimits'} = 1;
2145
2146     require Storable;
2147     require MIME::Base64;
2148
2149     #We don't need to die if the thaw fails.
2150     @{$self}{ $self->_FreezeThawKeys }
2151         = eval { @{ Storable::thaw( MIME::Base64::base64_decode($in) ) }; };
2152
2153     $RT::Logger->error($@) if $@;
2154
2155 }
2156
2157 # }}}
2158
2159 # {{{ Limit by enum or foreign key
2160
2161 # {{{ sub LimitQueue
2162
2163 =head2 LimitQueue
2164
2165 LimitQueue takes a paramhash with the fields OPERATOR and VALUE.
2166 OPERATOR is one of = or !=. (It defaults to =).
2167 VALUE is a queue id or Name.
2168
2169
2170 =cut
2171
2172 sub LimitQueue {
2173     my $self = shift;
2174     my %args = (
2175         VALUE    => undef,
2176         OPERATOR => '=',
2177         @_
2178     );
2179
2180     #TODO  VALUE should also take queue objects
2181     if ( defined $args{'VALUE'} && $args{'VALUE'} !~ /^\d+$/ ) {
2182         my $queue = new RT::Queue( $self->CurrentUser );
2183         $queue->Load( $args{'VALUE'} );
2184         $args{'VALUE'} = $queue->Id;
2185     }
2186
2187     # What if they pass in an Id?  Check for isNum() and convert to
2188     # string.
2189
2190     #TODO check for a valid queue here
2191
2192     $self->Limit(
2193         FIELD       => 'Queue',
2194         VALUE       => $args{'VALUE'},
2195         OPERATOR    => $args{'OPERATOR'},
2196         DESCRIPTION => join(
2197             ' ', $self->loc('Queue'), $args{'OPERATOR'}, $args{'VALUE'},
2198         ),
2199     );
2200
2201 }
2202
2203 # }}}
2204
2205 # {{{ sub LimitStatus
2206
2207 =head2 LimitStatus
2208
2209 Takes a paramhash with the fields OPERATOR and VALUE.
2210 OPERATOR is one of = or !=.
2211 VALUE is a status.
2212
2213 RT adds Status != 'deleted' until object has
2214 allow_deleted_search internal property set.
2215 $tickets->{'allow_deleted_search'} = 1;
2216 $tickets->LimitStatus( VALUE => 'deleted' );
2217
2218 =cut
2219
2220 sub LimitStatus {
2221     my $self = shift;
2222     my %args = (
2223         OPERATOR => '=',
2224         @_
2225     );
2226     $self->Limit(
2227         FIELD       => 'Status',
2228         VALUE       => $args{'VALUE'},
2229         OPERATOR    => $args{'OPERATOR'},
2230         DESCRIPTION => join( ' ',
2231             $self->loc('Status'), $args{'OPERATOR'},
2232             $self->loc( $args{'VALUE'} ) ),
2233     );
2234 }
2235
2236 # }}}
2237
2238 # {{{ sub IgnoreType
2239
2240 =head2 IgnoreType
2241
2242 If called, this search will not automatically limit the set of results found
2243 to tickets of type "Ticket". Tickets of other types, such as "project" and
2244 "approval" will be found.
2245
2246 =cut
2247
2248 sub IgnoreType {
2249     my $self = shift;
2250
2251     # Instead of faking a Limit that later gets ignored, fake up the
2252     # fact that we're already looking at type, so that the check in
2253     # Tickets_Overlay_SQL/FromSQL goes down the right branch
2254
2255     #  $self->LimitType(VALUE => '__any');
2256     $self->{looking_at_type} = 1;
2257 }
2258
2259 # }}}
2260
2261 # {{{ sub LimitType
2262
2263 =head2 LimitType
2264
2265 Takes a paramhash with the fields OPERATOR and VALUE.
2266 OPERATOR is one of = or !=, it defaults to "=".
2267 VALUE is a string to search for in the type of the ticket.
2268
2269
2270
2271 =cut
2272
2273 sub LimitType {
2274     my $self = shift;
2275     my %args = (
2276         OPERATOR => '=',
2277         VALUE    => undef,
2278         @_
2279     );
2280     $self->Limit(
2281         FIELD       => 'Type',
2282         VALUE       => $args{'VALUE'},
2283         OPERATOR    => $args{'OPERATOR'},
2284         DESCRIPTION => join( ' ',
2285             $self->loc('Type'), $args{'OPERATOR'}, $args{'Limit'}, ),
2286     );
2287 }
2288
2289 # }}}
2290
2291 # }}}
2292
2293 # {{{ Limit by string field
2294
2295 # {{{ sub LimitSubject
2296
2297 =head2 LimitSubject
2298
2299 Takes a paramhash with the fields OPERATOR and VALUE.
2300 OPERATOR is one of = or !=.
2301 VALUE is a string to search for in the subject of the ticket.
2302
2303 =cut
2304
2305 sub LimitSubject {
2306     my $self = shift;
2307     my %args = (@_);
2308     $self->Limit(
2309         FIELD       => 'Subject',
2310         VALUE       => $args{'VALUE'},
2311         OPERATOR    => $args{'OPERATOR'},
2312         DESCRIPTION => join( ' ',
2313             $self->loc('Subject'), $args{'OPERATOR'}, $args{'VALUE'}, ),
2314     );
2315 }
2316
2317 # }}}
2318
2319 # }}}
2320
2321 # {{{ Limit based on ticket numerical attributes
2322 # Things that can be > < = !=
2323
2324 # {{{ sub LimitId
2325
2326 =head2 LimitId
2327
2328 Takes a paramhash with the fields OPERATOR and VALUE.
2329 OPERATOR is one of =, >, < or !=.
2330 VALUE is a ticket Id to search for
2331
2332 =cut
2333
2334 sub LimitId {
2335     my $self = shift;
2336     my %args = (
2337         OPERATOR => '=',
2338         @_
2339     );
2340
2341     $self->Limit(
2342         FIELD       => 'id',
2343         VALUE       => $args{'VALUE'},
2344         OPERATOR    => $args{'OPERATOR'},
2345         DESCRIPTION =>
2346             join( ' ', $self->loc('Id'), $args{'OPERATOR'}, $args{'VALUE'}, ),
2347     );
2348 }
2349
2350 # }}}
2351
2352 # {{{ sub LimitPriority
2353
2354 =head2 LimitPriority
2355
2356 Takes a paramhash with the fields OPERATOR and VALUE.
2357 OPERATOR is one of =, >, < or !=.
2358 VALUE is a value to match the ticket\'s priority against
2359
2360 =cut
2361
2362 sub LimitPriority {
2363     my $self = shift;
2364     my %args = (@_);
2365     $self->Limit(
2366         FIELD       => 'Priority',
2367         VALUE       => $args{'VALUE'},
2368         OPERATOR    => $args{'OPERATOR'},
2369         DESCRIPTION => join( ' ',
2370             $self->loc('Priority'),
2371             $args{'OPERATOR'}, $args{'VALUE'}, ),
2372     );
2373 }
2374
2375 # }}}
2376
2377 # {{{ sub LimitInitialPriority
2378
2379 =head2 LimitInitialPriority
2380
2381 Takes a paramhash with the fields OPERATOR and VALUE.
2382 OPERATOR is one of =, >, < or !=.
2383 VALUE is a value to match the ticket\'s initial priority against
2384
2385
2386 =cut
2387
2388 sub LimitInitialPriority {
2389     my $self = shift;
2390     my %args = (@_);
2391     $self->Limit(
2392         FIELD       => 'InitialPriority',
2393         VALUE       => $args{'VALUE'},
2394         OPERATOR    => $args{'OPERATOR'},
2395         DESCRIPTION => join( ' ',
2396             $self->loc('Initial Priority'), $args{'OPERATOR'},
2397             $args{'VALUE'}, ),
2398     );
2399 }
2400
2401 # }}}
2402
2403 # {{{ sub LimitFinalPriority
2404
2405 =head2 LimitFinalPriority
2406
2407 Takes a paramhash with the fields OPERATOR and VALUE.
2408 OPERATOR is one of =, >, < or !=.
2409 VALUE is a value to match the ticket\'s final priority against
2410
2411 =cut
2412
2413 sub LimitFinalPriority {
2414     my $self = shift;
2415     my %args = (@_);
2416     $self->Limit(
2417         FIELD       => 'FinalPriority',
2418         VALUE       => $args{'VALUE'},
2419         OPERATOR    => $args{'OPERATOR'},
2420         DESCRIPTION => join( ' ',
2421             $self->loc('Final Priority'), $args{'OPERATOR'},
2422             $args{'VALUE'}, ),
2423     );
2424 }
2425
2426 # }}}
2427
2428 # {{{ sub LimitTimeWorked
2429
2430 =head2 LimitTimeWorked
2431
2432 Takes a paramhash with the fields OPERATOR and VALUE.
2433 OPERATOR is one of =, >, < or !=.
2434 VALUE is a value to match the ticket's TimeWorked attribute
2435
2436 =cut
2437
2438 sub LimitTimeWorked {
2439     my $self = shift;
2440     my %args = (@_);
2441     $self->Limit(
2442         FIELD       => 'TimeWorked',
2443         VALUE       => $args{'VALUE'},
2444         OPERATOR    => $args{'OPERATOR'},
2445         DESCRIPTION => join( ' ',
2446             $self->loc('Time Worked'),
2447             $args{'OPERATOR'}, $args{'VALUE'}, ),
2448     );
2449 }
2450
2451 # }}}
2452
2453 # {{{ sub LimitTimeLeft
2454
2455 =head2 LimitTimeLeft
2456
2457 Takes a paramhash with the fields OPERATOR and VALUE.
2458 OPERATOR is one of =, >, < or !=.
2459 VALUE is a value to match the ticket's TimeLeft attribute
2460
2461 =cut
2462
2463 sub LimitTimeLeft {
2464     my $self = shift;
2465     my %args = (@_);
2466     $self->Limit(
2467         FIELD       => 'TimeLeft',
2468         VALUE       => $args{'VALUE'},
2469         OPERATOR    => $args{'OPERATOR'},
2470         DESCRIPTION => join( ' ',
2471             $self->loc('Time Left'),
2472             $args{'OPERATOR'}, $args{'VALUE'}, ),
2473     );
2474 }
2475
2476 # }}}
2477
2478 # }}}
2479
2480 # {{{ Limiting based on attachment attributes
2481
2482 # {{{ sub LimitContent
2483
2484 =head2 LimitContent
2485
2486 Takes a paramhash with the fields OPERATOR and VALUE.
2487 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2488 VALUE is a string to search for in the body of the ticket
2489
2490 =cut
2491
2492 sub LimitContent {
2493     my $self = shift;
2494     my %args = (@_);
2495     $self->Limit(
2496         FIELD       => 'Content',
2497         VALUE       => $args{'VALUE'},
2498         OPERATOR    => $args{'OPERATOR'},
2499         DESCRIPTION => join( ' ',
2500             $self->loc('Ticket content'), $args{'OPERATOR'},
2501             $args{'VALUE'}, ),
2502     );
2503 }
2504
2505 # }}}
2506
2507 # {{{ sub LimitFilename
2508
2509 =head2 LimitFilename
2510
2511 Takes a paramhash with the fields OPERATOR and VALUE.
2512 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2513 VALUE is a string to search for in the body of the ticket
2514
2515 =cut
2516
2517 sub LimitFilename {
2518     my $self = shift;
2519     my %args = (@_);
2520     $self->Limit(
2521         FIELD       => 'Filename',
2522         VALUE       => $args{'VALUE'},
2523         OPERATOR    => $args{'OPERATOR'},
2524         DESCRIPTION => join( ' ',
2525             $self->loc('Attachment filename'), $args{'OPERATOR'},
2526             $args{'VALUE'}, ),
2527     );
2528 }
2529
2530 # }}}
2531 # {{{ sub LimitContentType
2532
2533 =head2 LimitContentType
2534
2535 Takes a paramhash with the fields OPERATOR and VALUE.
2536 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2537 VALUE is a content type to search ticket attachments for
2538
2539 =cut
2540
2541 sub LimitContentType {
2542     my $self = shift;
2543     my %args = (@_);
2544     $self->Limit(
2545         FIELD       => 'ContentType',
2546         VALUE       => $args{'VALUE'},
2547         OPERATOR    => $args{'OPERATOR'},
2548         DESCRIPTION => join( ' ',
2549             $self->loc('Ticket content type'), $args{'OPERATOR'},
2550             $args{'VALUE'}, ),
2551     );
2552 }
2553
2554 # }}}
2555
2556 # }}}
2557
2558 # {{{ Limiting based on people
2559
2560 # {{{ sub LimitOwner
2561
2562 =head2 LimitOwner
2563
2564 Takes a paramhash with the fields OPERATOR and VALUE.
2565 OPERATOR is one of = or !=.
2566 VALUE is a user id.
2567
2568 =cut
2569
2570 sub LimitOwner {
2571     my $self = shift;
2572     my %args = (
2573         OPERATOR => '=',
2574         @_
2575     );
2576
2577     my $owner = new RT::User( $self->CurrentUser );
2578     $owner->Load( $args{'VALUE'} );
2579
2580     # FIXME: check for a valid $owner
2581     $self->Limit(
2582         FIELD       => 'Owner',
2583         VALUE       => $args{'VALUE'},
2584         OPERATOR    => $args{'OPERATOR'},
2585         DESCRIPTION => join( ' ',
2586             $self->loc('Owner'), $args{'OPERATOR'}, $owner->Name(), ),
2587     );
2588
2589 }
2590
2591 # }}}
2592
2593 # {{{ Limiting watchers
2594
2595 # {{{ sub LimitWatcher
2596
2597 =head2 LimitWatcher
2598
2599   Takes a paramhash with the fields OPERATOR, TYPE and VALUE.
2600   OPERATOR is one of =, LIKE, NOT LIKE or !=.
2601   VALUE is a value to match the ticket\'s watcher email addresses against
2602   TYPE is the sort of watchers you want to match against. Leave it undef if you want to search all of them
2603
2604
2605 =cut
2606
2607 sub LimitWatcher {
2608     my $self = shift;
2609     my %args = (
2610         OPERATOR => '=',
2611         VALUE    => undef,
2612         TYPE     => undef,
2613         @_
2614     );
2615
2616     #build us up a description
2617     my ( $watcher_type, $desc );
2618     if ( $args{'TYPE'} ) {
2619         $watcher_type = $args{'TYPE'};
2620     }
2621     else {
2622         $watcher_type = "Watcher";
2623     }
2624
2625     $self->Limit(
2626         FIELD       => $watcher_type,
2627         VALUE       => $args{'VALUE'},
2628         OPERATOR    => $args{'OPERATOR'},
2629         TYPE        => $args{'TYPE'},
2630         DESCRIPTION => join( ' ',
2631             $self->loc($watcher_type),
2632             $args{'OPERATOR'}, $args{'VALUE'}, ),
2633     );
2634 }
2635
2636 # }}}
2637
2638 # }}}
2639
2640 # }}}
2641
2642 # {{{ Limiting based on links
2643
2644 # {{{ LimitLinkedTo
2645
2646 =head2 LimitLinkedTo
2647
2648 LimitLinkedTo takes a paramhash with two fields: TYPE and TARGET
2649 TYPE limits the sort of link we want to search on
2650
2651 TYPE = { RefersTo, MemberOf, DependsOn }
2652
2653 TARGET is the id or URI of the TARGET of the link
2654
2655 =cut
2656
2657 sub LimitLinkedTo {
2658     my $self = shift;
2659     my %args = (
2660         TARGET   => undef,
2661         TYPE     => undef,
2662         OPERATOR => '=',
2663         @_
2664     );
2665
2666     $self->Limit(
2667         FIELD       => 'LinkedTo',
2668         BASE        => undef,
2669         TARGET      => $args{'TARGET'},
2670         TYPE        => $args{'TYPE'},
2671         DESCRIPTION => $self->loc(
2672             "Tickets [_1] by [_2]",
2673             $self->loc( $args{'TYPE'} ),
2674             $args{'TARGET'}
2675         ),
2676         OPERATOR    => $args{'OPERATOR'},
2677     );
2678 }
2679
2680 # }}}
2681
2682 # {{{ LimitLinkedFrom
2683
2684 =head2 LimitLinkedFrom
2685
2686 LimitLinkedFrom takes a paramhash with two fields: TYPE and BASE
2687 TYPE limits the sort of link we want to search on
2688
2689
2690 BASE is the id or URI of the BASE of the link
2691
2692 =cut
2693
2694 sub LimitLinkedFrom {
2695     my $self = shift;
2696     my %args = (
2697         BASE     => undef,
2698         TYPE     => undef,
2699         OPERATOR => '=',
2700         @_
2701     );
2702
2703     # translate RT2 From/To naming to RT3 TicketSQL naming
2704     my %fromToMap = qw(DependsOn DependentOn
2705         MemberOf  HasMember
2706         RefersTo  ReferredToBy);
2707
2708     my $type = $args{'TYPE'};
2709     $type = $fromToMap{$type} if exists( $fromToMap{$type} );
2710
2711     $self->Limit(
2712         FIELD       => 'LinkedTo',
2713         TARGET      => undef,
2714         BASE        => $args{'BASE'},
2715         TYPE        => $type,
2716         DESCRIPTION => $self->loc(
2717             "Tickets [_1] [_2]",
2718             $self->loc( $args{'TYPE'} ),
2719             $args{'BASE'},
2720         ),
2721         OPERATOR    => $args{'OPERATOR'},
2722     );
2723 }
2724
2725 # }}}
2726
2727 # {{{ LimitMemberOf
2728 sub LimitMemberOf {
2729     my $self      = shift;
2730     my $ticket_id = shift;
2731     return $self->LimitLinkedTo(
2732         @_,
2733         TARGET => $ticket_id,
2734         TYPE   => 'MemberOf',
2735     );
2736 }
2737
2738 # }}}
2739
2740 # {{{ LimitHasMember
2741 sub LimitHasMember {
2742     my $self      = shift;
2743     my $ticket_id = shift;
2744     return $self->LimitLinkedFrom(
2745         @_,
2746         BASE => "$ticket_id",
2747         TYPE => 'HasMember',
2748     );
2749
2750 }
2751
2752 # }}}
2753
2754 # {{{ LimitDependsOn
2755
2756 sub LimitDependsOn {
2757     my $self      = shift;
2758     my $ticket_id = shift;
2759     return $self->LimitLinkedTo(
2760         @_,
2761         TARGET => $ticket_id,
2762         TYPE   => 'DependsOn',
2763     );
2764
2765 }
2766
2767 # }}}
2768
2769 # {{{ LimitDependedOnBy
2770
2771 sub LimitDependedOnBy {
2772     my $self      = shift;
2773     my $ticket_id = shift;
2774     return $self->LimitLinkedFrom(
2775         @_,
2776         BASE => $ticket_id,
2777         TYPE => 'DependentOn',
2778     );
2779
2780 }
2781
2782 # }}}
2783
2784 # {{{ LimitRefersTo
2785
2786 sub LimitRefersTo {
2787     my $self      = shift;
2788     my $ticket_id = shift;
2789     return $self->LimitLinkedTo(
2790         @_,
2791         TARGET => $ticket_id,
2792         TYPE   => 'RefersTo',
2793     );
2794
2795 }
2796
2797 # }}}
2798
2799 # {{{ LimitReferredToBy
2800
2801 sub LimitReferredToBy {
2802     my $self      = shift;
2803     my $ticket_id = shift;
2804     return $self->LimitLinkedFrom(
2805         @_,
2806         BASE => $ticket_id,
2807         TYPE => 'ReferredToBy',
2808     );
2809 }
2810
2811 # }}}
2812
2813 # }}}
2814
2815 # {{{ limit based on ticket date attribtes
2816
2817 # {{{ sub LimitDate
2818
2819 =head2 LimitDate (FIELD => 'DateField', OPERATOR => $oper, VALUE => $ISODate)
2820
2821 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
2822
2823 OPERATOR is one of > or <
2824 VALUE is a date and time in ISO format in GMT
2825 FIELD is one of Starts, Started, Told, Created, Resolved, LastUpdated
2826
2827 There are also helper functions of the form LimitFIELD that eliminate
2828 the need to pass in a FIELD argument.
2829
2830 =cut
2831
2832 sub LimitDate {
2833     my $self = shift;
2834     my %args = (
2835         FIELD    => undef,
2836         VALUE    => undef,
2837         OPERATOR => undef,
2838
2839         @_
2840     );
2841
2842     #Set the description if we didn't get handed it above
2843     unless ( $args{'DESCRIPTION'} ) {
2844         $args{'DESCRIPTION'} = $args{'FIELD'} . " "
2845             . $args{'OPERATOR'} . " "
2846             . $args{'VALUE'} . " GMT";
2847     }
2848
2849     $self->Limit(%args);
2850
2851 }
2852
2853 # }}}
2854
2855 sub LimitCreated {
2856     my $self = shift;
2857     $self->LimitDate( FIELD => 'Created', @_ );
2858 }
2859
2860 sub LimitDue {
2861     my $self = shift;
2862     $self->LimitDate( FIELD => 'Due', @_ );
2863
2864 }
2865
2866 sub LimitStarts {
2867     my $self = shift;
2868     $self->LimitDate( FIELD => 'Starts', @_ );
2869
2870 }
2871
2872 sub LimitStarted {
2873     my $self = shift;
2874     $self->LimitDate( FIELD => 'Started', @_ );
2875 }
2876
2877 sub LimitResolved {
2878     my $self = shift;
2879     $self->LimitDate( FIELD => 'Resolved', @_ );
2880 }
2881
2882 sub LimitTold {
2883     my $self = shift;
2884     $self->LimitDate( FIELD => 'Told', @_ );
2885 }
2886
2887 sub LimitLastUpdated {
2888     my $self = shift;
2889     $self->LimitDate( FIELD => 'LastUpdated', @_ );
2890 }
2891
2892 #
2893 # {{{ sub LimitTransactionDate
2894
2895 =head2 LimitTransactionDate (OPERATOR => $oper, VALUE => $ISODate)
2896
2897 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
2898
2899 OPERATOR is one of > or <
2900 VALUE is a date and time in ISO format in GMT
2901
2902
2903 =cut
2904
2905 sub LimitTransactionDate {
2906     my $self = shift;
2907     my %args = (
2908         FIELD    => 'TransactionDate',
2909         VALUE    => undef,
2910         OPERATOR => undef,
2911
2912         @_
2913     );
2914
2915     #  <20021217042756.GK28744@pallas.fsck.com>
2916     #    "Kill It" - Jesse.
2917
2918     #Set the description if we didn't get handed it above
2919     unless ( $args{'DESCRIPTION'} ) {
2920         $args{'DESCRIPTION'} = $args{'FIELD'} . " "
2921             . $args{'OPERATOR'} . " "
2922             . $args{'VALUE'} . " GMT";
2923     }
2924
2925     $self->Limit(%args);
2926
2927 }
2928
2929 # }}}
2930
2931 # }}}
2932
2933 # {{{ Limit based on custom fields
2934 # {{{ sub LimitCustomField
2935
2936 =head2 LimitCustomField
2937
2938 Takes a paramhash of key/value pairs with the following keys:
2939
2940 =over 4
2941
2942 =item CUSTOMFIELD - CustomField name or id.  If a name is passed, an additional parameter QUEUE may also be passed to distinguish the custom field.
2943
2944 =item OPERATOR - The usual Limit operators
2945
2946 =item VALUE - The value to compare against
2947
2948 =back
2949
2950 =cut
2951
2952 sub LimitCustomField {
2953     my $self = shift;
2954     my %args = (
2955         VALUE       => undef,
2956         CUSTOMFIELD => undef,
2957         OPERATOR    => '=',
2958         DESCRIPTION => undef,
2959         FIELD       => 'CustomFieldValue',
2960         QUOTEVALUE  => 1,
2961         @_
2962     );
2963
2964     my $CF = RT::CustomField->new( $self->CurrentUser );
2965     if ( $args{CUSTOMFIELD} =~ /^\d+$/ ) {
2966         $CF->Load( $args{CUSTOMFIELD} );
2967     }
2968     else {
2969         $CF->LoadByNameAndQueue(
2970             Name  => $args{CUSTOMFIELD},
2971             Queue => $args{QUEUE}
2972         );
2973         $args{CUSTOMFIELD} = $CF->Id;
2974     }
2975
2976     #If we are looking to compare with a null value.
2977     if ( $args{'OPERATOR'} =~ /^is$/i ) {
2978         $args{'DESCRIPTION'}
2979             ||= $self->loc( "Custom field [_1] has no value.", $CF->Name );
2980     }
2981     elsif ( $args{'OPERATOR'} =~ /^is not$/i ) {
2982         $args{'DESCRIPTION'}
2983             ||= $self->loc( "Custom field [_1] has a value.", $CF->Name );
2984     }
2985
2986     # if we're not looking to compare with a null value
2987     else {
2988         $args{'DESCRIPTION'} ||= $self->loc( "Custom field [_1] [_2] [_3]",
2989             $CF->Name, $args{OPERATOR}, $args{VALUE} );
2990     }
2991
2992     if ( defined $args{'QUEUE'} && $args{'QUEUE'} =~ /\D/ ) {
2993         my $QueueObj = RT::Queue->new( $self->CurrentUser );
2994         $QueueObj->Load( $args{'QUEUE'} );
2995         $args{'QUEUE'} = $QueueObj->Id;
2996     }
2997     delete $args{'QUEUE'} unless defined $args{'QUEUE'} && length $args{'QUEUE'};
2998
2999     my @rest;
3000     @rest = ( ENTRYAGGREGATOR => 'AND' )
3001         if ( $CF->Type eq 'SelectMultiple' );
3002
3003     $self->Limit(
3004         VALUE => $args{VALUE},
3005         FIELD => "CF"
3006             .(defined $args{'QUEUE'}? ".{$args{'QUEUE'}}" : '' )
3007             .".{" . $CF->Name . "}",
3008         OPERATOR    => $args{OPERATOR},
3009         CUSTOMFIELD => 1,
3010         @rest,
3011     );
3012
3013     $self->{'RecalcTicketLimits'} = 1;
3014 }
3015
3016 # }}}
3017 # }}}
3018
3019 # {{{ sub _NextIndex
3020
3021 =head2 _NextIndex
3022
3023 Keep track of the counter for the array of restrictions
3024
3025 =cut
3026
3027 sub _NextIndex {
3028     my $self = shift;
3029     return ( $self->{'restriction_index'}++ );
3030 }
3031
3032 # }}}
3033
3034 # }}}
3035
3036 # {{{ Core bits to make this a DBIx::SearchBuilder object
3037
3038 # {{{ sub _Init
3039 sub _Init {
3040     my $self = shift;
3041     $self->{'table'}                   = "Tickets";
3042     $self->{'RecalcTicketLimits'}      = 1;
3043     $self->{'looking_at_effective_id'} = 0;
3044     $self->{'looking_at_type'}         = 0;
3045     $self->{'restriction_index'}       = 1;
3046     $self->{'primary_key'}             = "id";
3047     delete $self->{'items_array'};
3048     delete $self->{'item_map'};
3049     delete $self->{'columns_to_display'};
3050     $self->SUPER::_Init(@_);
3051
3052     $self->_InitSQL;
3053
3054 }
3055
3056 # }}}
3057
3058 # {{{ sub Count
3059 sub Count {
3060     my $self = shift;
3061     $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
3062     return ( $self->SUPER::Count() );
3063 }
3064
3065 # }}}
3066
3067 # {{{ sub CountAll
3068 sub CountAll {
3069     my $self = shift;
3070     $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
3071     return ( $self->SUPER::CountAll() );
3072 }
3073
3074 # }}}
3075
3076 # {{{ sub ItemsArrayRef
3077
3078 =head2 ItemsArrayRef
3079
3080 Returns a reference to the set of all items found in this search
3081
3082 =cut
3083
3084 sub ItemsArrayRef {
3085     my $self = shift;
3086
3087     return $self->{'items_array'} if $self->{'items_array'};
3088
3089     my $placeholder = $self->_ItemsCounter;
3090     $self->GotoFirstItem();
3091     while ( my $item = $self->Next ) {
3092         push( @{ $self->{'items_array'} }, $item );
3093     }
3094     $self->GotoItem($placeholder);
3095     $self->{'items_array'}
3096         = $self->ItemsOrderBy( $self->{'items_array'} );
3097
3098     return $self->{'items_array'};
3099 }
3100
3101 sub ItemsArrayRefWindow {
3102     my $self = shift;
3103     my $window = shift;
3104
3105     my @old = ($self->_ItemsCounter, $self->RowsPerPage, $self->FirstRow+1);
3106
3107     $self->RowsPerPage( $window );
3108     $self->FirstRow(1);
3109     $self->GotoFirstItem;
3110
3111     my @res;
3112     while ( my $item = $self->Next ) {
3113         push @res, $item;
3114     }
3115
3116     $self->RowsPerPage( $old[1] );
3117     $self->FirstRow( $old[2] );
3118     $self->GotoItem( $old[0] );
3119
3120     return \@res;
3121 }
3122
3123 # }}}
3124
3125 # {{{ sub Next
3126 sub Next {
3127     my $self = shift;
3128
3129     $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
3130
3131     my $Ticket = $self->SUPER::Next;
3132     return $Ticket unless $Ticket;
3133
3134     if ( $Ticket->__Value('Status') eq 'deleted'
3135         && !$self->{'allow_deleted_search'} )
3136     {
3137         return $self->Next;
3138     }
3139     elsif ( RT->Config->Get('UseSQLForACLChecks') ) {
3140         # if we found a ticket with this option enabled then
3141         # all tickets we found are ACLed, cache this fact
3142         my $key = join ";:;", $self->CurrentUser->id, 'ShowTicket', 'RT::Ticket-'. $Ticket->id;
3143         $RT::Principal::_ACL_CACHE->set( $key => 1 );
3144         return $Ticket;
3145     }
3146     elsif ( $Ticket->CurrentUserHasRight('ShowTicket') ) {
3147         # has rights
3148         return $Ticket;
3149     }
3150     else {
3151         # If the user doesn't have the right to show this ticket
3152         return $self->Next;
3153     }
3154 }
3155
3156 sub _DoSearch {
3157     my $self = shift;
3158     $self->CurrentUserCanSee if RT->Config->Get('UseSQLForACLChecks');
3159     return $self->SUPER::_DoSearch( @_ );
3160 }
3161
3162 sub _DoCount {
3163     my $self = shift;
3164     $self->CurrentUserCanSee if RT->Config->Get('UseSQLForACLChecks');
3165     return $self->SUPER::_DoCount( @_ );
3166 }
3167
3168 sub _RolesCanSee {
3169     my $self = shift;
3170
3171     my $cache_key = 'RolesHasRight;:;ShowTicket';
3172  
3173     if ( my $cached = $RT::Principal::_ACL_CACHE->fetch( $cache_key ) ) {
3174         return %$cached;
3175     }
3176
3177     my $ACL = RT::ACL->new( $RT::SystemUser );
3178     $ACL->Limit( FIELD => 'RightName', VALUE => 'ShowTicket' );
3179     $ACL->Limit( FIELD => 'PrincipalType', OPERATOR => '!=', VALUE => 'Group' );
3180     my $principal_alias = $ACL->Join(
3181         ALIAS1 => 'main',
3182         FIELD1 => 'PrincipalId',
3183         TABLE2 => 'Principals',
3184         FIELD2 => 'id',
3185     );
3186     $ACL->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3187
3188     my %res = ();
3189     while ( my $ACE = $ACL->Next ) {
3190         my $role = $ACE->PrincipalType;
3191         my $type = $ACE->ObjectType;
3192         if ( $type eq 'RT::System' ) {
3193             $res{ $role } = 1;
3194         }
3195         elsif ( $type eq 'RT::Queue' ) {
3196             next if $res{ $role } && !ref $res{ $role };
3197             push @{ $res{ $role } ||= [] }, $ACE->ObjectId;
3198         }
3199         else {
3200             $RT::Logger->error('ShowTicket right is granted on unsupported object');
3201         }
3202     }
3203     $RT::Principal::_ACL_CACHE->set( $cache_key => \%res );
3204     return %res;
3205 }
3206
3207 sub _DirectlyCanSeeIn {
3208     my $self = shift;
3209     my $id = $self->CurrentUser->id;
3210
3211     my $cache_key = 'User-'. $id .';:;ShowTicket;:;DirectlyCanSeeIn';
3212     if ( my $cached = $RT::Principal::_ACL_CACHE->fetch( $cache_key ) ) {
3213         return @$cached;
3214     }
3215
3216     my $ACL = RT::ACL->new( $RT::SystemUser );
3217     $ACL->Limit( FIELD => 'RightName', VALUE => 'ShowTicket' );
3218     my $principal_alias = $ACL->Join(
3219         ALIAS1 => 'main',
3220         FIELD1 => 'PrincipalId',
3221         TABLE2 => 'Principals',
3222         FIELD2 => 'id',
3223     );
3224     $ACL->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3225     my $cgm_alias = $ACL->Join(
3226         ALIAS1 => 'main',
3227         FIELD1 => 'PrincipalId',
3228         TABLE2 => 'CachedGroupMembers',
3229         FIELD2 => 'GroupId',
3230     );
3231     $ACL->Limit( ALIAS => $cgm_alias, FIELD => 'MemberId', VALUE => $id );
3232     $ACL->Limit( ALIAS => $cgm_alias, FIELD => 'Disabled', VALUE => 0 );
3233
3234     my @res = ();
3235     while ( my $ACE = $ACL->Next ) {
3236         my $type = $ACE->ObjectType;
3237         if ( $type eq 'RT::System' ) {
3238             # If user is direct member of a group that has the right
3239             # on the system then he can see any ticket
3240             $RT::Principal::_ACL_CACHE->set( $cache_key => [-1] );
3241             return (-1);
3242         }
3243         elsif ( $type eq 'RT::Queue' ) {
3244             push @res, $ACE->ObjectId;
3245         }
3246         else {
3247             $RT::Logger->error('ShowTicket right is granted on unsupported object');
3248         }
3249     }
3250     $RT::Principal::_ACL_CACHE->set( $cache_key => \@res );
3251     return @res;
3252 }
3253
3254 sub CurrentUserCanSee {
3255     my $self = shift;
3256     return if $self->{'_sql_current_user_can_see_applied'};
3257
3258     return $self->{'_sql_current_user_can_see_applied'} = 1
3259         if $self->CurrentUser->UserObj->HasRight(
3260             Right => 'SuperUser', Object => $RT::System
3261         );
3262
3263     my $id = $self->CurrentUser->id;
3264
3265     # directly can see in all queues then we have nothing to do
3266     my @direct_queues = $self->_DirectlyCanSeeIn;
3267     return $self->{'_sql_current_user_can_see_applied'} = 1
3268         if @direct_queues && $direct_queues[0] == -1;
3269
3270     my %roles = $self->_RolesCanSee;
3271     {
3272         my %skip = map { $_ => 1 } @direct_queues;
3273         foreach my $role ( keys %roles ) {
3274             next unless ref $roles{ $role };
3275
3276             my @queues = grep !$skip{$_}, @{ $roles{ $role } };
3277             if ( @queues ) {
3278                 $roles{ $role } = \@queues;
3279             } else {
3280                 delete $roles{ $role };
3281             }
3282         }
3283     }
3284
3285 # there is no global watchers, only queues and tickes, if at
3286 # some point we will add global roles then it's gonna blow
3287 # the idea here is that if the right is set globaly for a role
3288 # and user plays this role for a queue directly not a ticket
3289 # then we have to check in advance
3290     if ( my @tmp = grep $_ ne 'Owner' && !ref $roles{ $_ }, keys %roles ) {
3291
3292         my $groups = RT::Groups->new( $RT::SystemUser );
3293         $groups->Limit( FIELD => 'Domain', VALUE => 'RT::Queue-Role' );
3294         foreach ( @tmp ) {
3295             $groups->Limit( FIELD => 'Type', VALUE => $_ );
3296         }
3297         my $principal_alias = $groups->Join(
3298             ALIAS1 => 'main',
3299             FIELD1 => 'id',
3300             TABLE2 => 'Principals',
3301             FIELD2 => 'id',
3302         );
3303         $groups->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3304         my $cgm_alias = $groups->Join(
3305             ALIAS1 => 'main',
3306             FIELD1 => 'id',
3307             TABLE2 => 'CachedGroupMembers',
3308             FIELD2 => 'GroupId',
3309         );
3310         $groups->Limit( ALIAS => $cgm_alias, FIELD => 'MemberId', VALUE => $id );
3311         $groups->Limit( ALIAS => $cgm_alias, FIELD => 'Disabled', VALUE => 0 );
3312         while ( my $group = $groups->Next ) {
3313             push @direct_queues, $group->Instance;
3314         }
3315     }
3316
3317     unless ( @direct_queues || keys %roles ) {
3318         $self->SUPER::Limit(
3319             SUBCLAUSE => 'ACL',
3320             ALIAS => 'main',
3321             FIELD => 'id',
3322             VALUE => 0,
3323             ENTRYAGGREGATOR => 'AND',
3324         );
3325         return $self->{'_sql_current_user_can_see_applied'} = 1;
3326     }
3327
3328     {
3329         my $join_roles = keys %roles;
3330         $join_roles = 0 if $join_roles == 1 && $roles{'Owner'};
3331         my ($role_group_alias, $cgm_alias);
3332         if ( $join_roles ) {
3333             $role_group_alias = $self->_RoleGroupsJoin( New => 1 );
3334             $cgm_alias = $self->_GroupMembersJoin( GroupsAlias => $role_group_alias );
3335             $self->SUPER::Limit(
3336                 LEFTJOIN   => $cgm_alias,
3337                 FIELD      => 'MemberId',
3338                 OPERATOR   => '=',
3339                 VALUE      => $id,
3340             );
3341         }
3342         my $limit_queues = sub {
3343             my $ea = shift;
3344             my @queues = @_;
3345
3346             return unless @queues;
3347             if ( @queues == 1 ) {
3348                 $self->SUPER::Limit(
3349                     SUBCLAUSE => 'ACL',
3350                     ALIAS => 'main',
3351                     FIELD => 'Queue',
3352                     VALUE => $_[0],
3353                     ENTRYAGGREGATOR => $ea,
3354                 );
3355             } else {
3356                 $self->SUPER::_OpenParen('ACL');
3357                 foreach my $q ( @queues ) {
3358                     $self->SUPER::Limit(
3359                         SUBCLAUSE => 'ACL',
3360                         ALIAS => 'main',
3361                         FIELD => 'Queue',
3362                         VALUE => $q,
3363                         ENTRYAGGREGATOR => $ea,
3364                     );
3365                     $ea = 'OR';
3366                 }
3367                 $self->SUPER::_CloseParen('ACL');
3368             }
3369             return 1;
3370         };
3371
3372         $self->SUPER::_OpenParen('ACL');
3373         my $ea = 'AND';
3374         $ea = 'OR' if $limit_queues->( $ea, @direct_queues );
3375         while ( my ($role, $queues) = each %roles ) {
3376             $self->SUPER::_OpenParen('ACL');
3377             if ( $role eq 'Owner' ) {
3378                 $self->SUPER::Limit(
3379                     SUBCLAUSE => 'ACL',
3380                     FIELD           => 'Owner',
3381                     VALUE           => $id,
3382                     ENTRYAGGREGATOR => $ea,
3383                 );
3384             }
3385             else {
3386                 $self->SUPER::Limit(
3387                     SUBCLAUSE       => 'ACL',
3388                     ALIAS           => $cgm_alias,
3389                     FIELD           => 'MemberId',
3390                     OPERATOR        => 'IS NOT',
3391                     VALUE           => 'NULL',
3392                     QUOTEVALUE      => 0,
3393                     ENTRYAGGREGATOR => $ea,
3394                 );
3395                 $self->SUPER::Limit(
3396                     SUBCLAUSE       => 'ACL',
3397                     ALIAS           => $role_group_alias,
3398                     FIELD           => 'Type',
3399                     VALUE           => $role,
3400                     ENTRYAGGREGATOR => 'AND',
3401                 );
3402             }
3403             $limit_queues->( 'AND', @$queues ) if ref $queues;
3404             $ea = 'OR' if $ea eq 'AND';
3405             $self->SUPER::_CloseParen('ACL');
3406         }
3407         $self->SUPER::_CloseParen('ACL');
3408     }
3409     return $self->{'_sql_current_user_can_see_applied'} = 1;
3410 }
3411
3412 # }}}
3413
3414 # }}}
3415
3416 # {{{ Deal with storing and restoring restrictions
3417
3418 # {{{ sub LoadRestrictions
3419
3420 =head2 LoadRestrictions
3421
3422 LoadRestrictions takes a string which can fully populate the TicketRestrictons hash.
3423 TODO It is not yet implemented
3424
3425 =cut
3426
3427 # }}}
3428
3429 # {{{ sub DescribeRestrictions
3430
3431 =head2 DescribeRestrictions
3432
3433 takes nothing.
3434 Returns a hash keyed by restriction id.
3435 Each element of the hash is currently a one element hash that contains DESCRIPTION which
3436 is a description of the purpose of that TicketRestriction
3437
3438 =cut
3439
3440 sub DescribeRestrictions {
3441     my $self = shift;
3442
3443     my %listing;
3444
3445     foreach my $row ( keys %{ $self->{'TicketRestrictions'} } ) {
3446         $listing{$row} = $self->{'TicketRestrictions'}{$row}{'DESCRIPTION'};
3447     }
3448     return (%listing);
3449 }
3450
3451 # }}}
3452
3453 # {{{ sub RestrictionValues
3454
3455 =head2 RestrictionValues FIELD
3456
3457 Takes a restriction field and returns a list of values this field is restricted
3458 to.
3459
3460 =cut
3461
3462 sub RestrictionValues {
3463     my $self  = shift;
3464     my $field = shift;
3465     map $self->{'TicketRestrictions'}{$_}{'VALUE'}, grep {
3466                $self->{'TicketRestrictions'}{$_}{'FIELD'}    eq $field
3467             && $self->{'TicketRestrictions'}{$_}{'OPERATOR'} eq "="
3468         }
3469         keys %{ $self->{'TicketRestrictions'} };
3470 }
3471
3472 # }}}
3473
3474 # {{{ sub ClearRestrictions
3475
3476 =head2 ClearRestrictions
3477
3478 Removes all restrictions irretrievably
3479
3480 =cut
3481
3482 sub ClearRestrictions {
3483     my $self = shift;
3484     delete $self->{'TicketRestrictions'};
3485     $self->{'looking_at_effective_id'} = 0;
3486     $self->{'looking_at_type'}         = 0;
3487     $self->{'RecalcTicketLimits'}      = 1;
3488 }
3489
3490 # }}}
3491
3492 # {{{ sub DeleteRestriction
3493
3494 =head2 DeleteRestriction
3495
3496 Takes the row Id of a restriction (From DescribeRestrictions' output, for example.
3497 Removes that restriction from the session's limits.
3498
3499 =cut
3500
3501 sub DeleteRestriction {
3502     my $self = shift;
3503     my $row  = shift;
3504     delete $self->{'TicketRestrictions'}{$row};
3505
3506     $self->{'RecalcTicketLimits'} = 1;
3507
3508     #make the underlying easysearch object forget all its preconceptions
3509 }
3510
3511 # }}}
3512
3513 # {{{ sub _RestrictionsToClauses
3514
3515 # Convert a set of oldstyle SB Restrictions to Clauses for RQL
3516
3517 sub _RestrictionsToClauses {
3518     my $self = shift;
3519
3520     my %clause;
3521     foreach my $row ( keys %{ $self->{'TicketRestrictions'} } ) {
3522         my $restriction = $self->{'TicketRestrictions'}{$row};
3523
3524         # We need to reimplement the subclause aggregation that SearchBuilder does.
3525         # Default Subclause is ALIAS.FIELD, and default ALIAS is 'main',
3526         # Then SB AND's the different Subclauses together.
3527
3528         # So, we want to group things into Subclauses, convert them to
3529         # SQL, and then join them with the appropriate DefaultEA.
3530         # Then join each subclause group with AND.
3531
3532         my $field = $restriction->{'FIELD'};
3533         my $realfield = $field;    # CustomFields fake up a fieldname, so
3534                                    # we need to figure that out
3535
3536         # One special case
3537         # Rewrite LinkedTo meta field to the real field
3538         if ( $field =~ /LinkedTo/ ) {
3539             $realfield = $field = $restriction->{'TYPE'};
3540         }
3541
3542         # Two special case
3543         # Handle subkey fields with a different real field
3544         if ( $field =~ /^(\w+)\./ ) {
3545             $realfield = $1;
3546         }
3547
3548         die "I don't know about $field yet"
3549             unless ( exists $FIELD_METADATA{$realfield}
3550                 or $restriction->{CUSTOMFIELD} );
3551
3552         my $type = $FIELD_METADATA{$realfield}->[0];
3553         my $op   = $restriction->{'OPERATOR'};
3554
3555         my $value = (
3556             grep    {defined}
3557                 map { $restriction->{$_} } qw(VALUE TICKET BASE TARGET)
3558         )[0];
3559
3560         # this performs the moral equivalent of defined or/dor/C<//>,
3561         # without the short circuiting.You need to use a 'defined or'
3562         # type thing instead of just checking for truth values, because
3563         # VALUE could be 0.(i.e. "false")
3564
3565         # You could also use this, but I find it less aesthetic:
3566         # (although it does short circuit)
3567         #( defined $restriction->{'VALUE'}? $restriction->{VALUE} :
3568         # defined $restriction->{'TICKET'} ?
3569         # $restriction->{TICKET} :
3570         # defined $restriction->{'BASE'} ?
3571         # $restriction->{BASE} :
3572         # defined $restriction->{'TARGET'} ?
3573         # $restriction->{TARGET} )
3574
3575         my $ea = $restriction->{ENTRYAGGREGATOR}
3576             || $DefaultEA{$type}
3577             || "AND";
3578         if ( ref $ea ) {
3579             die "Invalid operator $op for $field ($type)"
3580                 unless exists $ea->{$op};
3581             $ea = $ea->{$op};
3582         }
3583
3584         # Each CustomField should be put into a different Clause so they
3585         # are ANDed together.
3586         if ( $restriction->{CUSTOMFIELD} ) {
3587             $realfield = $field;
3588         }
3589
3590         exists $clause{$realfield} or $clause{$realfield} = [];
3591
3592         # Escape Quotes
3593         $field =~ s!(['"])!\\$1!g;
3594         $value =~ s!(['"])!\\$1!g;
3595         my $data = [ $ea, $type, $field, $op, $value ];
3596
3597         # here is where we store extra data, say if it's a keyword or
3598         # something.  (I.e. "TYPE SPECIFIC STUFF")
3599
3600         push @{ $clause{$realfield} }, $data;
3601     }
3602     return \%clause;
3603 }
3604
3605 # }}}
3606
3607 # {{{ sub _ProcessRestrictions
3608
3609 =head2 _ProcessRestrictions PARAMHASH
3610
3611 # The new _ProcessRestrictions is somewhat dependent on the SQL stuff,
3612 # but isn't quite generic enough to move into Tickets_Overlay_SQL.
3613
3614 =cut
3615
3616 sub _ProcessRestrictions {
3617     my $self = shift;
3618
3619     #Blow away ticket aliases since we'll need to regenerate them for
3620     #a new search
3621     delete $self->{'TicketAliases'};
3622     delete $self->{'items_array'};
3623     delete $self->{'item_map'};
3624     delete $self->{'raw_rows'};
3625     delete $self->{'rows'};
3626     delete $self->{'count_all'};
3627
3628     my $sql = $self->Query;    # Violating the _SQL namespace
3629     if ( !$sql || $self->{'RecalcTicketLimits'} ) {
3630
3631         #  "Restrictions to Clauses Branch\n";
3632         my $clauseRef = eval { $self->_RestrictionsToClauses; };
3633         if ($@) {
3634             $RT::Logger->error( "RestrictionsToClauses: " . $@ );
3635             $self->FromSQL("");
3636         }
3637         else {
3638             $sql = $self->ClausesToSQL($clauseRef);
3639             $self->FromSQL($sql) if $sql;
3640         }
3641     }
3642
3643     $self->{'RecalcTicketLimits'} = 0;
3644
3645 }
3646
3647 =head2 _BuildItemMap
3648
3649 Build up a L</ItemMap> of first/last/next/prev items, so that we can
3650 display search nav quickly.
3651
3652 =cut
3653
3654 sub _BuildItemMap {
3655     my $self = shift;
3656
3657     my $window = RT->Config->Get('TicketsItemMapSize');
3658
3659     $self->{'item_map'} = {};
3660
3661     my $items = $self->ItemsArrayRefWindow( $window );
3662     return unless $items && @$items;
3663
3664     my $prev = 0;
3665     $self->{'item_map'}{'first'} = $items->[0]->EffectiveId;
3666     for ( my $i = 0; $i < @$items; $i++ ) {
3667         my $item = $items->[$i];
3668         my $id = $item->EffectiveId;
3669         $self->{'item_map'}{$id}{'defined'} = 1;
3670         $self->{'item_map'}{$id}{'prev'}    = $prev;
3671         $self->{'item_map'}{$id}{'next'}    = $items->[$i+1]->EffectiveId
3672             if $items->[$i+1];
3673         $prev = $id;
3674     }
3675     $self->{'item_map'}{'last'} = $prev
3676         if !$window || @$items < $window;
3677 }
3678
3679 =head2 ItemMap
3680
3681 Returns an a map of all items found by this search. The map is a hash
3682 of the form:
3683
3684     {
3685         first => <first ticket id found>,
3686         last => <last ticket id found or undef>,
3687
3688         <ticket id> => {
3689             prev => <the ticket id found before>,
3690             next => <the ticket id found after>,
3691         },
3692         <ticket id> => {
3693             prev => ...,
3694             next => ...,
3695         },
3696     }
3697
3698 =cut
3699
3700 sub ItemMap {
3701     my $self = shift;
3702     $self->_BuildItemMap unless $self->{'item_map'};
3703     return $self->{'item_map'};
3704 }
3705
3706
3707 # }}}
3708
3709 # }}}
3710
3711 =head2 PrepForSerialization
3712
3713 You don't want to serialize a big tickets object, as
3714 the {items} hash will be instantly invalid _and_ eat
3715 lots of space
3716
3717 =cut
3718
3719 sub PrepForSerialization {
3720     my $self = shift;
3721     delete $self->{'items'};
3722     delete $self->{'items_array'};
3723     $self->RedoSearch();
3724 }
3725
3726 =head1 FLAGS
3727
3728 RT::Tickets supports several flags which alter search behavior:
3729
3730
3731 allow_deleted_search  (Otherwise never show deleted tickets in search results)
3732 looking_at_type (otherwise limit to type=ticket)
3733
3734 These flags are set by calling 
3735
3736 $tickets->{'flagname'} = 1;
3737
3738 BUG: There should be an API for this
3739
3740
3741
3742 =cut
3743
3744 1;
3745
3746
3747