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