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