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