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