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