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