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