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