RT custom fields patch, RT#8449
[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        else {
1835            push @res, $row;
1836        }
1837     }
1838     return $self->SUPER::OrderByCols(@res);
1839 }
1840
1841 # }}}
1842
1843 # {{{ Limit the result set based on content
1844
1845 # {{{ sub Limit
1846
1847 =head2 Limit
1848
1849 Takes a paramhash with the fields FIELD, OPERATOR, VALUE and DESCRIPTION
1850 Generally best called from LimitFoo methods
1851
1852 =cut
1853
1854 sub Limit {
1855     my $self = shift;
1856     my %args = (
1857         FIELD       => undef,
1858         OPERATOR    => '=',
1859         VALUE       => undef,
1860         DESCRIPTION => undef,
1861         @_
1862     );
1863     $args{'DESCRIPTION'} = $self->loc(
1864         "[_1] [_2] [_3]",  $args{'FIELD'},
1865         $args{'OPERATOR'}, $args{'VALUE'}
1866         )
1867         if ( !defined $args{'DESCRIPTION'} );
1868
1869     my $index = $self->_NextIndex;
1870
1871 # make the TicketRestrictions hash the equivalent of whatever we just passed in;
1872
1873     %{ $self->{'TicketRestrictions'}{$index} } = %args;
1874
1875     $self->{'RecalcTicketLimits'} = 1;
1876
1877 # If we're looking at the effective id, we don't want to append the other clause
1878 # which limits us to tickets where id = effective id
1879     if ( $args{'FIELD'} eq 'EffectiveId'
1880         && ( !$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) )
1881     {
1882         $self->{'looking_at_effective_id'} = 1;
1883     }
1884
1885     if ( $args{'FIELD'} eq 'Type'
1886         && ( !$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) )
1887     {
1888         $self->{'looking_at_type'} = 1;
1889     }
1890
1891     return ($index);
1892 }
1893
1894 # }}}
1895
1896 =head2 FreezeLimits
1897
1898 Returns a frozen string suitable for handing back to ThawLimits.
1899
1900 =cut
1901
1902 sub _FreezeThawKeys {
1903     'TicketRestrictions', 'restriction_index', 'looking_at_effective_id',
1904         'looking_at_type';
1905 }
1906
1907 # {{{ sub FreezeLimits
1908
1909 sub FreezeLimits {
1910     my $self = shift;
1911     require Storable;
1912     require MIME::Base64;
1913     MIME::Base64::base64_encode(
1914         Storable::freeze( \@{$self}{ $self->_FreezeThawKeys } ) );
1915 }
1916
1917 # }}}
1918
1919 =head2 ThawLimits
1920
1921 Take a frozen Limits string generated by FreezeLimits and make this tickets
1922 object have that set of limits.
1923
1924 =cut
1925
1926 # {{{ sub ThawLimits
1927
1928 sub ThawLimits {
1929     my $self = shift;
1930     my $in   = shift;
1931
1932     #if we don't have $in, get outta here.
1933     return undef unless ($in);
1934
1935     $self->{'RecalcTicketLimits'} = 1;
1936
1937     require Storable;
1938     require MIME::Base64;
1939
1940     #We don't need to die if the thaw fails.
1941     @{$self}{ $self->_FreezeThawKeys }
1942         = eval { @{ Storable::thaw( MIME::Base64::base64_decode($in) ) }; };
1943
1944     $RT::Logger->error($@) if $@;
1945
1946 }
1947
1948 # }}}
1949
1950 # {{{ Limit by enum or foreign key
1951
1952 # {{{ sub LimitQueue
1953
1954 =head2 LimitQueue
1955
1956 LimitQueue takes a paramhash with the fields OPERATOR and VALUE.
1957 OPERATOR is one of = or !=. (It defaults to =).
1958 VALUE is a queue id or Name.
1959
1960
1961 =cut
1962
1963 sub LimitQueue {
1964     my $self = shift;
1965     my %args = (
1966         VALUE    => undef,
1967         OPERATOR => '=',
1968         @_
1969     );
1970
1971     #TODO  VALUE should also take queue objects
1972     if ( defined $args{'VALUE'} && $args{'VALUE'} !~ /^\d+$/ ) {
1973         my $queue = new RT::Queue( $self->CurrentUser );
1974         $queue->Load( $args{'VALUE'} );
1975         $args{'VALUE'} = $queue->Id;
1976     }
1977
1978     # What if they pass in an Id?  Check for isNum() and convert to
1979     # string.
1980
1981     #TODO check for a valid queue here
1982
1983     $self->Limit(
1984         FIELD       => 'Queue',
1985         VALUE       => $args{'VALUE'},
1986         OPERATOR    => $args{'OPERATOR'},
1987         DESCRIPTION => join(
1988             ' ', $self->loc('Queue'), $args{'OPERATOR'}, $args{'VALUE'},
1989         ),
1990     );
1991
1992 }
1993
1994 # }}}
1995
1996 # {{{ sub LimitStatus
1997
1998 =head2 LimitStatus
1999
2000 Takes a paramhash with the fields OPERATOR and VALUE.
2001 OPERATOR is one of = or !=.
2002 VALUE is a status.
2003
2004 RT adds Status != 'deleted' until object has
2005 allow_deleted_search internal property set.
2006 $tickets->{'allow_deleted_search'} = 1;
2007 $tickets->LimitStatus( VALUE => 'deleted' );
2008
2009 =cut
2010
2011 sub LimitStatus {
2012     my $self = shift;
2013     my %args = (
2014         OPERATOR => '=',
2015         @_
2016     );
2017     $self->Limit(
2018         FIELD       => 'Status',
2019         VALUE       => $args{'VALUE'},
2020         OPERATOR    => $args{'OPERATOR'},
2021         DESCRIPTION => join( ' ',
2022             $self->loc('Status'), $args{'OPERATOR'},
2023             $self->loc( $args{'VALUE'} ) ),
2024     );
2025 }
2026
2027 # }}}
2028
2029 # {{{ sub IgnoreType
2030
2031 =head2 IgnoreType
2032
2033 If called, this search will not automatically limit the set of results found
2034 to tickets of type "Ticket". Tickets of other types, such as "project" and
2035 "approval" will be found.
2036
2037 =cut
2038
2039 sub IgnoreType {
2040     my $self = shift;
2041
2042     # Instead of faking a Limit that later gets ignored, fake up the
2043     # fact that we're already looking at type, so that the check in
2044     # Tickets_Overlay_SQL/FromSQL goes down the right branch
2045
2046     #  $self->LimitType(VALUE => '__any');
2047     $self->{looking_at_type} = 1;
2048 }
2049
2050 # }}}
2051
2052 # {{{ sub LimitType
2053
2054 =head2 LimitType
2055
2056 Takes a paramhash with the fields OPERATOR and VALUE.
2057 OPERATOR is one of = or !=, it defaults to "=".
2058 VALUE is a string to search for in the type of the ticket.
2059
2060
2061
2062 =cut
2063
2064 sub LimitType {
2065     my $self = shift;
2066     my %args = (
2067         OPERATOR => '=',
2068         VALUE    => undef,
2069         @_
2070     );
2071     $self->Limit(
2072         FIELD       => 'Type',
2073         VALUE       => $args{'VALUE'},
2074         OPERATOR    => $args{'OPERATOR'},
2075         DESCRIPTION => join( ' ',
2076             $self->loc('Type'), $args{'OPERATOR'}, $args{'Limit'}, ),
2077     );
2078 }
2079
2080 # }}}
2081
2082 # }}}
2083
2084 # {{{ Limit by string field
2085
2086 # {{{ sub LimitSubject
2087
2088 =head2 LimitSubject
2089
2090 Takes a paramhash with the fields OPERATOR and VALUE.
2091 OPERATOR is one of = or !=.
2092 VALUE is a string to search for in the subject of the ticket.
2093
2094 =cut
2095
2096 sub LimitSubject {
2097     my $self = shift;
2098     my %args = (@_);
2099     $self->Limit(
2100         FIELD       => 'Subject',
2101         VALUE       => $args{'VALUE'},
2102         OPERATOR    => $args{'OPERATOR'},
2103         DESCRIPTION => join( ' ',
2104             $self->loc('Subject'), $args{'OPERATOR'}, $args{'VALUE'}, ),
2105     );
2106 }
2107
2108 # }}}
2109
2110 # }}}
2111
2112 # {{{ Limit based on ticket numerical attributes
2113 # Things that can be > < = !=
2114
2115 # {{{ sub LimitId
2116
2117 =head2 LimitId
2118
2119 Takes a paramhash with the fields OPERATOR and VALUE.
2120 OPERATOR is one of =, >, < or !=.
2121 VALUE is a ticket Id to search for
2122
2123 =cut
2124
2125 sub LimitId {
2126     my $self = shift;
2127     my %args = (
2128         OPERATOR => '=',
2129         @_
2130     );
2131
2132     $self->Limit(
2133         FIELD       => 'id',
2134         VALUE       => $args{'VALUE'},
2135         OPERATOR    => $args{'OPERATOR'},
2136         DESCRIPTION =>
2137             join( ' ', $self->loc('Id'), $args{'OPERATOR'}, $args{'VALUE'}, ),
2138     );
2139 }
2140
2141 # }}}
2142
2143 # {{{ sub LimitPriority
2144
2145 =head2 LimitPriority
2146
2147 Takes a paramhash with the fields OPERATOR and VALUE.
2148 OPERATOR is one of =, >, < or !=.
2149 VALUE is a value to match the ticket\'s priority against
2150
2151 =cut
2152
2153 sub LimitPriority {
2154     my $self = shift;
2155     my %args = (@_);
2156     $self->Limit(
2157         FIELD       => 'Priority',
2158         VALUE       => $args{'VALUE'},
2159         OPERATOR    => $args{'OPERATOR'},
2160         DESCRIPTION => join( ' ',
2161             $self->loc('Priority'),
2162             $args{'OPERATOR'}, $args{'VALUE'}, ),
2163     );
2164 }
2165
2166 # }}}
2167
2168 # {{{ sub LimitInitialPriority
2169
2170 =head2 LimitInitialPriority
2171
2172 Takes a paramhash with the fields OPERATOR and VALUE.
2173 OPERATOR is one of =, >, < or !=.
2174 VALUE is a value to match the ticket\'s initial priority against
2175
2176
2177 =cut
2178
2179 sub LimitInitialPriority {
2180     my $self = shift;
2181     my %args = (@_);
2182     $self->Limit(
2183         FIELD       => 'InitialPriority',
2184         VALUE       => $args{'VALUE'},
2185         OPERATOR    => $args{'OPERATOR'},
2186         DESCRIPTION => join( ' ',
2187             $self->loc('Initial Priority'), $args{'OPERATOR'},
2188             $args{'VALUE'}, ),
2189     );
2190 }
2191
2192 # }}}
2193
2194 # {{{ sub LimitFinalPriority
2195
2196 =head2 LimitFinalPriority
2197
2198 Takes a paramhash with the fields OPERATOR and VALUE.
2199 OPERATOR is one of =, >, < or !=.
2200 VALUE is a value to match the ticket\'s final priority against
2201
2202 =cut
2203
2204 sub LimitFinalPriority {
2205     my $self = shift;
2206     my %args = (@_);
2207     $self->Limit(
2208         FIELD       => 'FinalPriority',
2209         VALUE       => $args{'VALUE'},
2210         OPERATOR    => $args{'OPERATOR'},
2211         DESCRIPTION => join( ' ',
2212             $self->loc('Final Priority'), $args{'OPERATOR'},
2213             $args{'VALUE'}, ),
2214     );
2215 }
2216
2217 # }}}
2218
2219 # {{{ sub LimitTimeWorked
2220
2221 =head2 LimitTimeWorked
2222
2223 Takes a paramhash with the fields OPERATOR and VALUE.
2224 OPERATOR is one of =, >, < or !=.
2225 VALUE is a value to match the ticket's TimeWorked attribute
2226
2227 =cut
2228
2229 sub LimitTimeWorked {
2230     my $self = shift;
2231     my %args = (@_);
2232     $self->Limit(
2233         FIELD       => 'TimeWorked',
2234         VALUE       => $args{'VALUE'},
2235         OPERATOR    => $args{'OPERATOR'},
2236         DESCRIPTION => join( ' ',
2237             $self->loc('Time Worked'),
2238             $args{'OPERATOR'}, $args{'VALUE'}, ),
2239     );
2240 }
2241
2242 # }}}
2243
2244 # {{{ sub LimitTimeLeft
2245
2246 =head2 LimitTimeLeft
2247
2248 Takes a paramhash with the fields OPERATOR and VALUE.
2249 OPERATOR is one of =, >, < or !=.
2250 VALUE is a value to match the ticket's TimeLeft attribute
2251
2252 =cut
2253
2254 sub LimitTimeLeft {
2255     my $self = shift;
2256     my %args = (@_);
2257     $self->Limit(
2258         FIELD       => 'TimeLeft',
2259         VALUE       => $args{'VALUE'},
2260         OPERATOR    => $args{'OPERATOR'},
2261         DESCRIPTION => join( ' ',
2262             $self->loc('Time Left'),
2263             $args{'OPERATOR'}, $args{'VALUE'}, ),
2264     );
2265 }
2266
2267 # }}}
2268
2269 # }}}
2270
2271 # {{{ Limiting based on attachment attributes
2272
2273 # {{{ sub LimitContent
2274
2275 =head2 LimitContent
2276
2277 Takes a paramhash with the fields OPERATOR and VALUE.
2278 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2279 VALUE is a string to search for in the body of the ticket
2280
2281 =cut
2282
2283 sub LimitContent {
2284     my $self = shift;
2285     my %args = (@_);
2286     $self->Limit(
2287         FIELD       => 'Content',
2288         VALUE       => $args{'VALUE'},
2289         OPERATOR    => $args{'OPERATOR'},
2290         DESCRIPTION => join( ' ',
2291             $self->loc('Ticket content'), $args{'OPERATOR'},
2292             $args{'VALUE'}, ),
2293     );
2294 }
2295
2296 # }}}
2297
2298 # {{{ sub LimitFilename
2299
2300 =head2 LimitFilename
2301
2302 Takes a paramhash with the fields OPERATOR and VALUE.
2303 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2304 VALUE is a string to search for in the body of the ticket
2305
2306 =cut
2307
2308 sub LimitFilename {
2309     my $self = shift;
2310     my %args = (@_);
2311     $self->Limit(
2312         FIELD       => 'Filename',
2313         VALUE       => $args{'VALUE'},
2314         OPERATOR    => $args{'OPERATOR'},
2315         DESCRIPTION => join( ' ',
2316             $self->loc('Attachment filename'), $args{'OPERATOR'},
2317             $args{'VALUE'}, ),
2318     );
2319 }
2320
2321 # }}}
2322 # {{{ sub LimitContentType
2323
2324 =head2 LimitContentType
2325
2326 Takes a paramhash with the fields OPERATOR and VALUE.
2327 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2328 VALUE is a content type to search ticket attachments for
2329
2330 =cut
2331
2332 sub LimitContentType {
2333     my $self = shift;
2334     my %args = (@_);
2335     $self->Limit(
2336         FIELD       => 'ContentType',
2337         VALUE       => $args{'VALUE'},
2338         OPERATOR    => $args{'OPERATOR'},
2339         DESCRIPTION => join( ' ',
2340             $self->loc('Ticket content type'), $args{'OPERATOR'},
2341             $args{'VALUE'}, ),
2342     );
2343 }
2344
2345 # }}}
2346
2347 # }}}
2348
2349 # {{{ Limiting based on people
2350
2351 # {{{ sub LimitOwner
2352
2353 =head2 LimitOwner
2354
2355 Takes a paramhash with the fields OPERATOR and VALUE.
2356 OPERATOR is one of = or !=.
2357 VALUE is a user id.
2358
2359 =cut
2360
2361 sub LimitOwner {
2362     my $self = shift;
2363     my %args = (
2364         OPERATOR => '=',
2365         @_
2366     );
2367
2368     my $owner = new RT::User( $self->CurrentUser );
2369     $owner->Load( $args{'VALUE'} );
2370
2371     # FIXME: check for a valid $owner
2372     $self->Limit(
2373         FIELD       => 'Owner',
2374         VALUE       => $args{'VALUE'},
2375         OPERATOR    => $args{'OPERATOR'},
2376         DESCRIPTION => join( ' ',
2377             $self->loc('Owner'), $args{'OPERATOR'}, $owner->Name(), ),
2378     );
2379
2380 }
2381
2382 # }}}
2383
2384 # {{{ Limiting watchers
2385
2386 # {{{ sub LimitWatcher
2387
2388 =head2 LimitWatcher
2389
2390   Takes a paramhash with the fields OPERATOR, TYPE and VALUE.
2391   OPERATOR is one of =, LIKE, NOT LIKE or !=.
2392   VALUE is a value to match the ticket\'s watcher email addresses against
2393   TYPE is the sort of watchers you want to match against. Leave it undef if you want to search all of them
2394
2395
2396 =cut
2397
2398 sub LimitWatcher {
2399     my $self = shift;
2400     my %args = (
2401         OPERATOR => '=',
2402         VALUE    => undef,
2403         TYPE     => undef,
2404         @_
2405     );
2406
2407     #build us up a description
2408     my ( $watcher_type, $desc );
2409     if ( $args{'TYPE'} ) {
2410         $watcher_type = $args{'TYPE'};
2411     }
2412     else {
2413         $watcher_type = "Watcher";
2414     }
2415
2416     $self->Limit(
2417         FIELD       => $watcher_type,
2418         VALUE       => $args{'VALUE'},
2419         OPERATOR    => $args{'OPERATOR'},
2420         TYPE        => $args{'TYPE'},
2421         DESCRIPTION => join( ' ',
2422             $self->loc($watcher_type),
2423             $args{'OPERATOR'}, $args{'VALUE'}, ),
2424     );
2425 }
2426
2427 # }}}
2428
2429 # }}}
2430
2431 # }}}
2432
2433 # {{{ Limiting based on links
2434
2435 # {{{ LimitLinkedTo
2436
2437 =head2 LimitLinkedTo
2438
2439 LimitLinkedTo takes a paramhash with two fields: TYPE and TARGET
2440 TYPE limits the sort of link we want to search on
2441
2442 TYPE = { RefersTo, MemberOf, DependsOn }
2443
2444 TARGET is the id or URI of the TARGET of the link
2445
2446 =cut
2447
2448 sub LimitLinkedTo {
2449     my $self = shift;
2450     my %args = (
2451         TARGET   => undef,
2452         TYPE     => undef,
2453         OPERATOR => '=',
2454         @_
2455     );
2456
2457     $self->Limit(
2458         FIELD       => 'LinkedTo',
2459         BASE        => undef,
2460         TARGET      => $args{'TARGET'},
2461         TYPE        => $args{'TYPE'},
2462         DESCRIPTION => $self->loc(
2463             "Tickets [_1] by [_2]",
2464             $self->loc( $args{'TYPE'} ),
2465             $args{'TARGET'}
2466         ),
2467         OPERATOR    => $args{'OPERATOR'},
2468     );
2469 }
2470
2471 # }}}
2472
2473 # {{{ LimitLinkedFrom
2474
2475 =head2 LimitLinkedFrom
2476
2477 LimitLinkedFrom takes a paramhash with two fields: TYPE and BASE
2478 TYPE limits the sort of link we want to search on
2479
2480
2481 BASE is the id or URI of the BASE of the link
2482
2483 =cut
2484
2485 sub LimitLinkedFrom {
2486     my $self = shift;
2487     my %args = (
2488         BASE     => undef,
2489         TYPE     => undef,
2490         OPERATOR => '=',
2491         @_
2492     );
2493
2494     # translate RT2 From/To naming to RT3 TicketSQL naming
2495     my %fromToMap = qw(DependsOn DependentOn
2496         MemberOf  HasMember
2497         RefersTo  ReferredToBy);
2498
2499     my $type = $args{'TYPE'};
2500     $type = $fromToMap{$type} if exists( $fromToMap{$type} );
2501
2502     $self->Limit(
2503         FIELD       => 'LinkedTo',
2504         TARGET      => undef,
2505         BASE        => $args{'BASE'},
2506         TYPE        => $type,
2507         DESCRIPTION => $self->loc(
2508             "Tickets [_1] [_2]",
2509             $self->loc( $args{'TYPE'} ),
2510             $args{'BASE'},
2511         ),
2512         OPERATOR    => $args{'OPERATOR'},
2513     );
2514 }
2515
2516 # }}}
2517
2518 # {{{ LimitMemberOf
2519 sub LimitMemberOf {
2520     my $self      = shift;
2521     my $ticket_id = shift;
2522     return $self->LimitLinkedTo(
2523         @_,
2524         TARGET => $ticket_id,
2525         TYPE   => 'MemberOf',
2526     );
2527 }
2528
2529 # }}}
2530
2531 # {{{ LimitHasMember
2532 sub LimitHasMember {
2533     my $self      = shift;
2534     my $ticket_id = shift;
2535     return $self->LimitLinkedFrom(
2536         @_,
2537         BASE => "$ticket_id",
2538         TYPE => 'HasMember',
2539     );
2540
2541 }
2542
2543 # }}}
2544
2545 # {{{ LimitDependsOn
2546
2547 sub LimitDependsOn {
2548     my $self      = shift;
2549     my $ticket_id = shift;
2550     return $self->LimitLinkedTo(
2551         @_,
2552         TARGET => $ticket_id,
2553         TYPE   => 'DependsOn',
2554     );
2555
2556 }
2557
2558 # }}}
2559
2560 # {{{ LimitDependedOnBy
2561
2562 sub LimitDependedOnBy {
2563     my $self      = shift;
2564     my $ticket_id = shift;
2565     return $self->LimitLinkedFrom(
2566         @_,
2567         BASE => $ticket_id,
2568         TYPE => 'DependentOn',
2569     );
2570
2571 }
2572
2573 # }}}
2574
2575 # {{{ LimitRefersTo
2576
2577 sub LimitRefersTo {
2578     my $self      = shift;
2579     my $ticket_id = shift;
2580     return $self->LimitLinkedTo(
2581         @_,
2582         TARGET => $ticket_id,
2583         TYPE   => 'RefersTo',
2584     );
2585
2586 }
2587
2588 # }}}
2589
2590 # {{{ LimitReferredToBy
2591
2592 sub LimitReferredToBy {
2593     my $self      = shift;
2594     my $ticket_id = shift;
2595     return $self->LimitLinkedFrom(
2596         @_,
2597         BASE => $ticket_id,
2598         TYPE => 'ReferredToBy',
2599     );
2600 }
2601
2602 # }}}
2603
2604 # }}}
2605
2606 # {{{ limit based on ticket date attribtes
2607
2608 # {{{ sub LimitDate
2609
2610 =head2 LimitDate (FIELD => 'DateField', OPERATOR => $oper, VALUE => $ISODate)
2611
2612 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
2613
2614 OPERATOR is one of > or <
2615 VALUE is a date and time in ISO format in GMT
2616 FIELD is one of Starts, Started, Told, Created, Resolved, LastUpdated
2617
2618 There are also helper functions of the form LimitFIELD that eliminate
2619 the need to pass in a FIELD argument.
2620
2621 =cut
2622
2623 sub LimitDate {
2624     my $self = shift;
2625     my %args = (
2626         FIELD    => undef,
2627         VALUE    => undef,
2628         OPERATOR => undef,
2629
2630         @_
2631     );
2632
2633     #Set the description if we didn't get handed it above
2634     unless ( $args{'DESCRIPTION'} ) {
2635         $args{'DESCRIPTION'} = $args{'FIELD'} . " "
2636             . $args{'OPERATOR'} . " "
2637             . $args{'VALUE'} . " GMT";
2638     }
2639
2640     $self->Limit(%args);
2641
2642 }
2643
2644 # }}}
2645
2646 sub LimitCreated {
2647     my $self = shift;
2648     $self->LimitDate( FIELD => 'Created', @_ );
2649 }
2650
2651 sub LimitDue {
2652     my $self = shift;
2653     $self->LimitDate( FIELD => 'Due', @_ );
2654
2655 }
2656
2657 sub LimitStarts {
2658     my $self = shift;
2659     $self->LimitDate( FIELD => 'Starts', @_ );
2660
2661 }
2662
2663 sub LimitStarted {
2664     my $self = shift;
2665     $self->LimitDate( FIELD => 'Started', @_ );
2666 }
2667
2668 sub LimitResolved {
2669     my $self = shift;
2670     $self->LimitDate( FIELD => 'Resolved', @_ );
2671 }
2672
2673 sub LimitTold {
2674     my $self = shift;
2675     $self->LimitDate( FIELD => 'Told', @_ );
2676 }
2677
2678 sub LimitLastUpdated {
2679     my $self = shift;
2680     $self->LimitDate( FIELD => 'LastUpdated', @_ );
2681 }
2682
2683 #
2684 # {{{ sub LimitTransactionDate
2685
2686 =head2 LimitTransactionDate (OPERATOR => $oper, VALUE => $ISODate)
2687
2688 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
2689
2690 OPERATOR is one of > or <
2691 VALUE is a date and time in ISO format in GMT
2692
2693
2694 =cut
2695
2696 sub LimitTransactionDate {
2697     my $self = shift;
2698     my %args = (
2699         FIELD    => 'TransactionDate',
2700         VALUE    => undef,
2701         OPERATOR => undef,
2702
2703         @_
2704     );
2705
2706     #  <20021217042756.GK28744@pallas.fsck.com>
2707     #    "Kill It" - Jesse.
2708
2709     #Set the description if we didn't get handed it above
2710     unless ( $args{'DESCRIPTION'} ) {
2711         $args{'DESCRIPTION'} = $args{'FIELD'} . " "
2712             . $args{'OPERATOR'} . " "
2713             . $args{'VALUE'} . " GMT";
2714     }
2715
2716     $self->Limit(%args);
2717
2718 }
2719
2720 # }}}
2721
2722 # }}}
2723
2724 # {{{ Limit based on custom fields
2725 # {{{ sub LimitCustomField
2726
2727 =head2 LimitCustomField
2728
2729 Takes a paramhash of key/value pairs with the following keys:
2730
2731 =over 4
2732
2733 =item CUSTOMFIELD - CustomField name or id.  If a name is passed, an additional parameter QUEUE may also be passed to distinguish the custom field.
2734
2735 =item OPERATOR - The usual Limit operators
2736
2737 =item VALUE - The value to compare against
2738
2739 =back
2740
2741 =cut
2742
2743 sub LimitCustomField {
2744     my $self = shift;
2745     my %args = (
2746         VALUE       => undef,
2747         CUSTOMFIELD => undef,
2748         OPERATOR    => '=',
2749         DESCRIPTION => undef,
2750         FIELD       => 'CustomFieldValue',
2751         QUOTEVALUE  => 1,
2752         @_
2753     );
2754
2755     my $CF = RT::CustomField->new( $self->CurrentUser );
2756     if ( $args{CUSTOMFIELD} =~ /^\d+$/ ) {
2757         $CF->Load( $args{CUSTOMFIELD} );
2758     }
2759     else {
2760         $CF->LoadByNameAndQueue(
2761             Name  => $args{CUSTOMFIELD},
2762             Queue => $args{QUEUE}
2763         );
2764         $args{CUSTOMFIELD} = $CF->Id;
2765     }
2766
2767     # Handle special customfields types
2768     if ($CF->Type eq 'Date') {
2769         $args{FIELD} = 'DateCustomFieldValue';
2770     }
2771
2772     #If we are looking to compare with a null value.
2773     if ( $args{'OPERATOR'} =~ /^is$/i ) {
2774         $args{'DESCRIPTION'}
2775             ||= $self->loc( "Custom field [_1] has no value.", $CF->Name );
2776     }
2777     elsif ( $args{'OPERATOR'} =~ /^is not$/i ) {
2778         $args{'DESCRIPTION'}
2779             ||= $self->loc( "Custom field [_1] has a value.", $CF->Name );
2780     }
2781
2782     # if we're not looking to compare with a null value
2783     else {
2784         $args{'DESCRIPTION'} ||= $self->loc( "Custom field [_1] [_2] [_3]",
2785             $CF->Name, $args{OPERATOR}, $args{VALUE} );
2786     }
2787
2788     if ( defined $args{'QUEUE'} && $args{'QUEUE'} =~ /\D/ ) {
2789         my $QueueObj = RT::Queue->new( $self->CurrentUser );
2790         $QueueObj->Load( $args{'QUEUE'} );
2791         $args{'QUEUE'} = $QueueObj->Id;
2792     }
2793     delete $args{'QUEUE'} unless defined $args{'QUEUE'} && length $args{'QUEUE'};
2794
2795     my @rest;
2796     @rest = ( ENTRYAGGREGATOR => 'AND' )
2797         if ( $CF->Type eq 'SelectMultiple' );
2798
2799     $self->Limit(
2800         VALUE => $args{VALUE},
2801         FIELD => "CF"
2802             .(defined $args{'QUEUE'}? ".{$args{'QUEUE'}}" : '' )
2803             .".{" . $CF->Name . "}",
2804         OPERATOR    => $args{OPERATOR},
2805         CUSTOMFIELD => 1,
2806         @rest,
2807     );
2808
2809     $self->{'RecalcTicketLimits'} = 1;
2810 }
2811
2812 # }}}
2813 # }}}
2814
2815 # {{{ sub _NextIndex
2816
2817 =head2 _NextIndex
2818
2819 Keep track of the counter for the array of restrictions
2820
2821 =cut
2822
2823 sub _NextIndex {
2824     my $self = shift;
2825     return ( $self->{'restriction_index'}++ );
2826 }
2827
2828 # }}}
2829
2830 # }}}
2831
2832 # {{{ Core bits to make this a DBIx::SearchBuilder object
2833
2834 # {{{ sub _Init
2835 sub _Init {
2836     my $self = shift;
2837     $self->{'table'}                   = "Tickets";
2838     $self->{'RecalcTicketLimits'}      = 1;
2839     $self->{'looking_at_effective_id'} = 0;
2840     $self->{'looking_at_type'}         = 0;
2841     $self->{'restriction_index'}       = 1;
2842     $self->{'primary_key'}             = "id";
2843     delete $self->{'items_array'};
2844     delete $self->{'item_map'};
2845     delete $self->{'columns_to_display'};
2846     $self->SUPER::_Init(@_);
2847
2848     $self->_InitSQL;
2849
2850 }
2851
2852 # }}}
2853
2854 # {{{ sub Count
2855 sub Count {
2856     my $self = shift;
2857     $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2858     return ( $self->SUPER::Count() );
2859 }
2860
2861 # }}}
2862
2863 # {{{ sub CountAll
2864 sub CountAll {
2865     my $self = shift;
2866     $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2867     return ( $self->SUPER::CountAll() );
2868 }
2869
2870 # }}}
2871
2872 # {{{ sub ItemsArrayRef
2873
2874 =head2 ItemsArrayRef
2875
2876 Returns a reference to the set of all items found in this search
2877
2878 =cut
2879
2880 sub ItemsArrayRef {
2881     my $self = shift;
2882
2883     return $self->{'items_array'} if $self->{'items_array'};
2884
2885     my $placeholder = $self->_ItemsCounter;
2886     $self->GotoFirstItem();
2887     while ( my $item = $self->Next ) {
2888         push( @{ $self->{'items_array'} }, $item );
2889     }
2890     $self->GotoItem($placeholder);
2891     $self->{'items_array'}
2892         = $self->ItemsOrderBy( $self->{'items_array'} );
2893
2894     return $self->{'items_array'};
2895 }
2896
2897 sub ItemsArrayRefWindow {
2898     my $self = shift;
2899     my $window = shift;
2900
2901     my @old = ($self->_ItemsCounter, $self->RowsPerPage, $self->FirstRow+1);
2902
2903     $self->RowsPerPage( $window );
2904     $self->FirstRow(1);
2905     $self->GotoFirstItem;
2906
2907     my @res;
2908     while ( my $item = $self->Next ) {
2909         push @res, $item;
2910     }
2911
2912     $self->RowsPerPage( $old[1] );
2913     $self->FirstRow( $old[2] );
2914     $self->GotoItem( $old[0] );
2915
2916     return \@res;
2917 }
2918
2919 # }}}
2920
2921 # {{{ sub Next
2922 sub Next {
2923     my $self = shift;
2924
2925     $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2926
2927     my $Ticket = $self->SUPER::Next;
2928     return $Ticket unless $Ticket;
2929
2930     if ( $Ticket->__Value('Status') eq 'deleted'
2931         && !$self->{'allow_deleted_search'} )
2932     {
2933         return $self->Next;
2934     }
2935     elsif ( RT->Config->Get('UseSQLForACLChecks') ) {
2936         # if we found a ticket with this option enabled then
2937         # all tickets we found are ACLed, cache this fact
2938         my $key = join ";:;", $self->CurrentUser->id, 'ShowTicket', 'RT::Ticket-'. $Ticket->id;
2939         $RT::Principal::_ACL_CACHE->set( $key => 1 );
2940         return $Ticket;
2941     }
2942     elsif ( $Ticket->CurrentUserHasRight('ShowTicket') ) {
2943         # has rights
2944         return $Ticket;
2945     }
2946     else {
2947         # If the user doesn't have the right to show this ticket
2948         return $self->Next;
2949     }
2950 }
2951
2952 sub _DoSearch {
2953     my $self = shift;
2954     $self->CurrentUserCanSee if RT->Config->Get('UseSQLForACLChecks');
2955     return $self->SUPER::_DoSearch( @_ );
2956 }
2957
2958 sub _DoCount {
2959     my $self = shift;
2960     $self->CurrentUserCanSee if RT->Config->Get('UseSQLForACLChecks');
2961     return $self->SUPER::_DoCount( @_ );
2962 }
2963
2964 sub _RolesCanSee {
2965     my $self = shift;
2966
2967     my $cache_key = 'RolesHasRight;:;ShowTicket';
2968  
2969     if ( my $cached = $RT::Principal::_ACL_CACHE->fetch( $cache_key ) ) {
2970         return %$cached;
2971     }
2972
2973     my $ACL = RT::ACL->new( $RT::SystemUser );
2974     $ACL->Limit( FIELD => 'RightName', VALUE => 'ShowTicket' );
2975     $ACL->Limit( FIELD => 'PrincipalType', OPERATOR => '!=', VALUE => 'Group' );
2976     my $principal_alias = $ACL->Join(
2977         ALIAS1 => 'main',
2978         FIELD1 => 'PrincipalId',
2979         TABLE2 => 'Principals',
2980         FIELD2 => 'id',
2981     );
2982     $ACL->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
2983
2984     my %res = ();
2985     while ( my $ACE = $ACL->Next ) {
2986         my $role = $ACE->PrincipalType;
2987         my $type = $ACE->ObjectType;
2988         if ( $type eq 'RT::System' ) {
2989             $res{ $role } = 1;
2990         }
2991         elsif ( $type eq 'RT::Queue' ) {
2992             next if $res{ $role } && !ref $res{ $role };
2993             push @{ $res{ $role } ||= [] }, $ACE->ObjectId;
2994         }
2995         else {
2996             $RT::Logger->error('ShowTicket right is granted on unsupported object');
2997         }
2998     }
2999     $RT::Principal::_ACL_CACHE->set( $cache_key => \%res );
3000     return %res;
3001 }
3002
3003 sub _DirectlyCanSeeIn {
3004     my $self = shift;
3005     my $id = $self->CurrentUser->id;
3006
3007     my $cache_key = 'User-'. $id .';:;ShowTicket;:;DirectlyCanSeeIn';
3008     if ( my $cached = $RT::Principal::_ACL_CACHE->fetch( $cache_key ) ) {
3009         return @$cached;
3010     }
3011
3012     my $ACL = RT::ACL->new( $RT::SystemUser );
3013     $ACL->Limit( FIELD => 'RightName', VALUE => 'ShowTicket' );
3014     my $principal_alias = $ACL->Join(
3015         ALIAS1 => 'main',
3016         FIELD1 => 'PrincipalId',
3017         TABLE2 => 'Principals',
3018         FIELD2 => 'id',
3019     );
3020     $ACL->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3021     my $cgm_alias = $ACL->Join(
3022         ALIAS1 => 'main',
3023         FIELD1 => 'PrincipalId',
3024         TABLE2 => 'CachedGroupMembers',
3025         FIELD2 => 'GroupId',
3026     );
3027     $ACL->Limit( ALIAS => $cgm_alias, FIELD => 'MemberId', VALUE => $id );
3028     $ACL->Limit( ALIAS => $cgm_alias, FIELD => 'Disabled', VALUE => 0 );
3029
3030     my @res = ();
3031     while ( my $ACE = $ACL->Next ) {
3032         my $type = $ACE->ObjectType;
3033         if ( $type eq 'RT::System' ) {
3034             # If user is direct member of a group that has the right
3035             # on the system then he can see any ticket
3036             $RT::Principal::_ACL_CACHE->set( $cache_key => [-1] );
3037             return (-1);
3038         }
3039         elsif ( $type eq 'RT::Queue' ) {
3040             push @res, $ACE->ObjectId;
3041         }
3042         else {
3043             $RT::Logger->error('ShowTicket right is granted on unsupported object');
3044         }
3045     }
3046     $RT::Principal::_ACL_CACHE->set( $cache_key => \@res );
3047     return @res;
3048 }
3049
3050 sub CurrentUserCanSee {
3051     my $self = shift;
3052     return if $self->{'_sql_current_user_can_see_applied'};
3053
3054     return $self->{'_sql_current_user_can_see_applied'} = 1
3055         if $self->CurrentUser->UserObj->HasRight(
3056             Right => 'SuperUser', Object => $RT::System
3057         );
3058
3059     my $id = $self->CurrentUser->id;
3060
3061     # directly can see in all queues then we have nothing to do
3062     my @direct_queues = $self->_DirectlyCanSeeIn;
3063     return $self->{'_sql_current_user_can_see_applied'} = 1
3064         if @direct_queues && $direct_queues[0] == -1;
3065
3066     my %roles = $self->_RolesCanSee;
3067     {
3068         my %skip = map { $_ => 1 } @direct_queues;
3069         foreach my $role ( keys %roles ) {
3070             next unless ref $roles{ $role };
3071
3072             my @queues = grep !$skip{$_}, @{ $roles{ $role } };
3073             if ( @queues ) {
3074                 $roles{ $role } = \@queues;
3075             } else {
3076                 delete $roles{ $role };
3077             }
3078         }
3079     }
3080
3081 # there is no global watchers, only queues and tickes, if at
3082 # some point we will add global roles then it's gonna blow
3083 # the idea here is that if the right is set globaly for a role
3084 # and user plays this role for a queue directly not a ticket
3085 # then we have to check in advance
3086     if ( my @tmp = grep $_ ne 'Owner' && !ref $roles{ $_ }, keys %roles ) {
3087
3088         my $groups = RT::Groups->new( $RT::SystemUser );
3089         $groups->Limit( FIELD => 'Domain', VALUE => 'RT::Queue-Role' );
3090         foreach ( @tmp ) {
3091             $groups->Limit( FIELD => 'Type', VALUE => $_ );
3092         }
3093         my $principal_alias = $groups->Join(
3094             ALIAS1 => 'main',
3095             FIELD1 => 'id',
3096             TABLE2 => 'Principals',
3097             FIELD2 => 'id',
3098         );
3099         $groups->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3100         my $cgm_alias = $groups->Join(
3101             ALIAS1 => 'main',
3102             FIELD1 => 'id',
3103             TABLE2 => 'CachedGroupMembers',
3104             FIELD2 => 'GroupId',
3105         );
3106         $groups->Limit( ALIAS => $cgm_alias, FIELD => 'MemberId', VALUE => $id );
3107         $groups->Limit( ALIAS => $cgm_alias, FIELD => 'Disabled', VALUE => 0 );
3108         while ( my $group = $groups->Next ) {
3109             push @direct_queues, $group->Instance;
3110         }
3111     }
3112
3113     unless ( @direct_queues || keys %roles ) {
3114         $self->SUPER::Limit(
3115             SUBCLAUSE => 'ACL',
3116             ALIAS => 'main',
3117             FIELD => 'id',
3118             VALUE => 0,
3119             ENTRYAGGREGATOR => 'AND',
3120         );
3121         return $self->{'_sql_current_user_can_see_applied'} = 1;
3122     }
3123
3124     {
3125         my $join_roles = keys %roles;
3126         $join_roles = 0 if $join_roles == 1 && $roles{'Owner'};
3127         my ($role_group_alias, $cgm_alias);
3128         if ( $join_roles ) {
3129             $role_group_alias = $self->_RoleGroupsJoin( New => 1 );
3130             $cgm_alias = $self->_GroupMembersJoin( GroupsAlias => $role_group_alias );
3131             $self->SUPER::Limit(
3132                 LEFTJOIN   => $cgm_alias,
3133                 FIELD      => 'MemberId',
3134                 OPERATOR   => '=',
3135                 VALUE      => $id,
3136             );
3137         }
3138         my $limit_queues = sub {
3139             my $ea = shift;
3140             my @queues = @_;
3141
3142             return unless @queues;
3143             if ( @queues == 1 ) {
3144                 $self->SUPER::Limit(
3145                     SUBCLAUSE => 'ACL',
3146                     ALIAS => 'main',
3147                     FIELD => 'Queue',
3148                     VALUE => $_[0],
3149                     ENTRYAGGREGATOR => $ea,
3150                 );
3151             } else {
3152                 $self->SUPER::_OpenParen('ACL');
3153                 foreach my $q ( @queues ) {
3154                     $self->SUPER::Limit(
3155                         SUBCLAUSE => 'ACL',
3156                         ALIAS => 'main',
3157                         FIELD => 'Queue',
3158                         VALUE => $q,
3159                         ENTRYAGGREGATOR => $ea,
3160                     );
3161                     $ea = 'OR';
3162                 }
3163                 $self->SUPER::_CloseParen('ACL');
3164             }
3165             return 1;
3166         };
3167
3168         $self->SUPER::_OpenParen('ACL');
3169         my $ea = 'AND';
3170         $ea = 'OR' if $limit_queues->( $ea, @direct_queues );
3171         while ( my ($role, $queues) = each %roles ) {
3172             $self->SUPER::_OpenParen('ACL');
3173             if ( $role eq 'Owner' ) {
3174                 $self->SUPER::Limit(
3175                     SUBCLAUSE => 'ACL',
3176                     FIELD           => 'Owner',
3177                     VALUE           => $id,
3178                     ENTRYAGGREGATOR => $ea,
3179                 );
3180             }
3181             else {
3182                 $self->SUPER::Limit(
3183                     SUBCLAUSE       => 'ACL',
3184                     ALIAS           => $cgm_alias,
3185                     FIELD           => 'MemberId',
3186                     OPERATOR        => 'IS NOT',
3187                     VALUE           => 'NULL',
3188                     QUOTEVALUE      => 0,
3189                     ENTRYAGGREGATOR => $ea,
3190                 );
3191                 $self->SUPER::Limit(
3192                     SUBCLAUSE       => 'ACL',
3193                     ALIAS           => $role_group_alias,
3194                     FIELD           => 'Type',
3195                     VALUE           => $role,
3196                     ENTRYAGGREGATOR => 'AND',
3197                 );
3198             }
3199             $limit_queues->( 'AND', @$queues ) if ref $queues;
3200             $ea = 'OR' if $ea eq 'AND';
3201             $self->SUPER::_CloseParen('ACL');
3202         }
3203         $self->SUPER::_CloseParen('ACL');
3204     }
3205     return $self->{'_sql_current_user_can_see_applied'} = 1;
3206 }
3207
3208 # }}}
3209
3210 # }}}
3211
3212 # {{{ Deal with storing and restoring restrictions
3213
3214 # {{{ sub LoadRestrictions
3215
3216 =head2 LoadRestrictions
3217
3218 LoadRestrictions takes a string which can fully populate the TicketRestrictons hash.
3219 TODO It is not yet implemented
3220
3221 =cut
3222
3223 # }}}
3224
3225 # {{{ sub DescribeRestrictions
3226
3227 =head2 DescribeRestrictions
3228
3229 takes nothing.
3230 Returns a hash keyed by restriction id.
3231 Each element of the hash is currently a one element hash that contains DESCRIPTION which
3232 is a description of the purpose of that TicketRestriction
3233
3234 =cut
3235
3236 sub DescribeRestrictions {
3237     my $self = shift;
3238
3239     my ( $row, %listing );
3240
3241     foreach $row ( keys %{ $self->{'TicketRestrictions'} } ) {
3242         $listing{$row} = $self->{'TicketRestrictions'}{$row}{'DESCRIPTION'};
3243     }
3244     return (%listing);
3245 }
3246
3247 # }}}
3248
3249 # {{{ sub RestrictionValues
3250
3251 =head2 RestrictionValues FIELD
3252
3253 Takes a restriction field and returns a list of values this field is restricted
3254 to.
3255
3256 =cut
3257
3258 sub RestrictionValues {
3259     my $self  = shift;
3260     my $field = shift;
3261     map $self->{'TicketRestrictions'}{$_}{'VALUE'}, grep {
3262                $self->{'TicketRestrictions'}{$_}{'FIELD'}    eq $field
3263             && $self->{'TicketRestrictions'}{$_}{'OPERATOR'} eq "="
3264         }
3265         keys %{ $self->{'TicketRestrictions'} };
3266 }
3267
3268 # }}}
3269
3270 # {{{ sub ClearRestrictions
3271
3272 =head2 ClearRestrictions
3273
3274 Removes all restrictions irretrievably
3275
3276 =cut
3277
3278 sub ClearRestrictions {
3279     my $self = shift;
3280     delete $self->{'TicketRestrictions'};
3281     $self->{'looking_at_effective_id'} = 0;
3282     $self->{'looking_at_type'}         = 0;
3283     $self->{'RecalcTicketLimits'}      = 1;
3284 }
3285
3286 # }}}
3287
3288 # {{{ sub DeleteRestriction
3289
3290 =head2 DeleteRestriction
3291
3292 Takes the row Id of a restriction (From DescribeRestrictions' output, for example.
3293 Removes that restriction from the session's limits.
3294
3295 =cut
3296
3297 sub DeleteRestriction {
3298     my $self = shift;
3299     my $row  = shift;
3300     delete $self->{'TicketRestrictions'}{$row};
3301
3302     $self->{'RecalcTicketLimits'} = 1;
3303
3304     #make the underlying easysearch object forget all its preconceptions
3305 }
3306
3307 # }}}
3308
3309 # {{{ sub _RestrictionsToClauses
3310
3311 # Convert a set of oldstyle SB Restrictions to Clauses for RQL
3312
3313 sub _RestrictionsToClauses {
3314     my $self = shift;
3315
3316     my $row;
3317     my %clause;
3318     foreach $row ( keys %{ $self->{'TicketRestrictions'} } ) {
3319         my $restriction = $self->{'TicketRestrictions'}{$row};
3320
3321         # We need to reimplement the subclause aggregation that SearchBuilder does.
3322         # Default Subclause is ALIAS.FIELD, and default ALIAS is 'main',
3323         # Then SB AND's the different Subclauses together.
3324
3325         # So, we want to group things into Subclauses, convert them to
3326         # SQL, and then join them with the appropriate DefaultEA.
3327         # Then join each subclause group with AND.
3328
3329         my $field = $restriction->{'FIELD'};
3330         my $realfield = $field;    # CustomFields fake up a fieldname, so
3331                                    # we need to figure that out
3332
3333         # One special case
3334         # Rewrite LinkedTo meta field to the real field
3335         if ( $field =~ /LinkedTo/ ) {
3336             $realfield = $field = $restriction->{'TYPE'};
3337         }
3338
3339         # Two special case
3340         # Handle subkey fields with a different real field
3341         if ( $field =~ /^(\w+)\./ ) {
3342             $realfield = $1;
3343         }
3344
3345         die "I don't know about $field yet"
3346             unless ( exists $FIELD_METADATA{$realfield}
3347                 or $restriction->{CUSTOMFIELD} );
3348
3349         my $type = $FIELD_METADATA{$realfield}->[0];
3350         my $op   = $restriction->{'OPERATOR'};
3351
3352         my $value = (
3353             grep    {defined}
3354                 map { $restriction->{$_} } qw(VALUE TICKET BASE TARGET)
3355         )[0];
3356
3357         # this performs the moral equivalent of defined or/dor/C<//>,
3358         # without the short circuiting.You need to use a 'defined or'
3359         # type thing instead of just checking for truth values, because
3360         # VALUE could be 0.(i.e. "false")
3361
3362         # You could also use this, but I find it less aesthetic:
3363         # (although it does short circuit)
3364         #( defined $restriction->{'VALUE'}? $restriction->{VALUE} :
3365         # defined $restriction->{'TICKET'} ?
3366         # $restriction->{TICKET} :
3367         # defined $restriction->{'BASE'} ?
3368         # $restriction->{BASE} :
3369         # defined $restriction->{'TARGET'} ?
3370         # $restriction->{TARGET} )
3371
3372         my $ea = $restriction->{ENTRYAGGREGATOR}
3373             || $DefaultEA{$type}
3374             || "AND";
3375         if ( ref $ea ) {
3376             die "Invalid operator $op for $field ($type)"
3377                 unless exists $ea->{$op};
3378             $ea = $ea->{$op};
3379         }
3380
3381         # Each CustomField should be put into a different Clause so they
3382         # are ANDed together.
3383         if ( $restriction->{CUSTOMFIELD} ) {
3384             $realfield = $field;
3385         }
3386
3387         exists $clause{$realfield} or $clause{$realfield} = [];
3388
3389         # Escape Quotes
3390         $field =~ s!(['"])!\\$1!g;
3391         $value =~ s!(['"])!\\$1!g;
3392         my $data = [ $ea, $type, $field, $op, $value ];
3393
3394         # here is where we store extra data, say if it's a keyword or
3395         # something.  (I.e. "TYPE SPECIFIC STUFF")
3396
3397         push @{ $clause{$realfield} }, $data;
3398     }
3399     return \%clause;
3400 }
3401
3402 # }}}
3403
3404 # {{{ sub _ProcessRestrictions
3405
3406 =head2 _ProcessRestrictions PARAMHASH
3407
3408 # The new _ProcessRestrictions is somewhat dependent on the SQL stuff,
3409 # but isn't quite generic enough to move into Tickets_Overlay_SQL.
3410
3411 =cut
3412
3413 sub _ProcessRestrictions {
3414     my $self = shift;
3415
3416     #Blow away ticket aliases since we'll need to regenerate them for
3417     #a new search
3418     delete $self->{'TicketAliases'};
3419     delete $self->{'items_array'};
3420     delete $self->{'item_map'};
3421     delete $self->{'raw_rows'};
3422     delete $self->{'rows'};
3423     delete $self->{'count_all'};
3424
3425     my $sql = $self->Query;    # Violating the _SQL namespace
3426     if ( !$sql || $self->{'RecalcTicketLimits'} ) {
3427
3428         #  "Restrictions to Clauses Branch\n";
3429         my $clauseRef = eval { $self->_RestrictionsToClauses; };
3430         if ($@) {
3431             $RT::Logger->error( "RestrictionsToClauses: " . $@ );
3432             $self->FromSQL("");
3433         }
3434         else {
3435             $sql = $self->ClausesToSQL($clauseRef);
3436             $self->FromSQL($sql) if $sql;
3437         }
3438     }
3439
3440     $self->{'RecalcTicketLimits'} = 0;
3441
3442 }
3443
3444 =head2 _BuildItemMap
3445
3446 Build up a L</ItemMap> of first/last/next/prev items, so that we can
3447 display search nav quickly.
3448
3449 =cut
3450
3451 sub _BuildItemMap {
3452     my $self = shift;
3453
3454     my $window = RT->Config->Get('TicketsItemMapSize');
3455
3456     $self->{'item_map'} = {};
3457
3458     my $items = $self->ItemsArrayRefWindow( $window );
3459     return unless $items && @$items;
3460
3461     my $prev = 0;
3462     $self->{'item_map'}{'first'} = $items->[0]->EffectiveId;
3463     for ( my $i = 0; $i < @$items; $i++ ) {
3464         my $item = $items->[$i];
3465         my $id = $item->EffectiveId;
3466         $self->{'item_map'}{$id}{'defined'} = 1;
3467         $self->{'item_map'}{$id}{'prev'}    = $prev;
3468         $self->{'item_map'}{$id}{'next'}    = $items->[$i+1]->EffectiveId
3469             if $items->[$i+1];
3470         $prev = $id;
3471     }
3472     $self->{'item_map'}{'last'} = $prev
3473         if !$window || @$items < $window;
3474 }
3475
3476 =head2 ItemMap
3477
3478 Returns an a map of all items found by this search. The map is a hash
3479 of the form:
3480
3481     {
3482         first => <first ticket id found>,
3483         last => <last ticket id found or undef>,
3484
3485         <ticket id> => {
3486             prev => <the ticket id found before>,
3487             next => <the ticket id found after>,
3488         },
3489         <ticket id> => {
3490             prev => ...,
3491             next => ...,
3492         },
3493     }
3494
3495 =cut
3496
3497 sub ItemMap {
3498     my $self = shift;
3499     $self->_BuildItemMap unless $self->{'item_map'};
3500     return $self->{'item_map'};
3501 }
3502
3503
3504 # }}}
3505
3506 # }}}
3507
3508 =head2 PrepForSerialization
3509
3510 You don't want to serialize a big tickets object, as
3511 the {items} hash will be instantly invalid _and_ eat
3512 lots of space
3513
3514 =cut
3515
3516 sub PrepForSerialization {
3517     my $self = shift;
3518     delete $self->{'items'};
3519     delete $self->{'items_array'};
3520     $self->RedoSearch();
3521 }
3522
3523 =head1 FLAGS
3524
3525 RT::Tickets supports several flags which alter search behavior:
3526
3527
3528 allow_deleted_search  (Otherwise never show deleted tickets in search results)
3529 looking_at_type (otherwise limit to type=ticket)
3530
3531 These flags are set by calling 
3532
3533 $tickets->{'flagname'} = 1;
3534
3535 BUG: There should be an API for this
3536
3537
3538
3539 =cut
3540
3541 1;
3542
3543
3544