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