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