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