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