RT 4.0.22
[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         my ( $start_ip, $end_ip ) =
1582           RT::ObjectCustomFieldValue->ParseIPRange($value);
1583         if ( $start_ip && $end_ip ) {
1584             if ( $op =~ /^([<>])=?$/ ) {
1585                 my $is_less = $1 eq '<' ? 1 : 0;
1586                 if ( $is_less ) {
1587                     $value = $start_ip;
1588                 }
1589                 else {
1590                     $value = $end_ip;
1591                 }
1592             }
1593             else {
1594                 $value = join '-', $start_ip, $end_ip;
1595             }
1596         }
1597         else {
1598             $RT::Logger->warn("$value is not a valid IPAddressRange");
1599         }
1600     }
1601
1602     if ( $cf && $cf->Type =~ /^Date(?:Time)?$/ ) {
1603         my $date = RT::Date->new( $self->CurrentUser );
1604         $date->Set( Format => 'unknown', Value => $value );
1605         if ( $date->Unix ) {
1606
1607             if (
1608                    $cf->Type eq 'Date'
1609                 || $value =~ /^\s*(?:today|tomorrow|yesterday)\s*$/i
1610                 || (   $value !~ /midnight|\d+:\d+:\d+/i
1611                     && $date->Time( Timezone => 'user' ) eq '00:00:00' )
1612               )
1613             {
1614                 $value = $date->Date( Timezone => 'user' );
1615             }
1616             else {
1617                 $value = $date->DateTime;
1618             }
1619         }
1620         else {
1621             $RT::Logger->warn("$value is not a valid date string");
1622         }
1623     }
1624
1625     my $single_value = !$cf || !$cfid || $cf->SingleValue;
1626
1627     my $cfkey = $cfid ? $cfid : "$type-$object.$field";
1628
1629     if ( $null_op && !$column ) {
1630         # IS[ NOT] NULL without column is the same as has[ no] any CF value,
1631         # we can reuse our default joins for this operation
1632         # with column specified we have different situation
1633         my ($ObjectCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field, $type );
1634         $self->_OpenParen;
1635         $self->_SQLLimit(
1636             ALIAS    => $ObjectCFs,
1637             FIELD    => 'id',
1638             OPERATOR => $op,
1639             VALUE    => $value,
1640             %rest
1641         );
1642         $self->_SQLLimit(
1643             ALIAS      => $CFs,
1644             FIELD      => 'Name',
1645             OPERATOR   => 'IS NOT',
1646             VALUE      => 'NULL',
1647             QUOTEVALUE => 0,
1648             ENTRYAGGREGATOR => 'AND',
1649         ) if $CFs;
1650         $self->_CloseParen;
1651     }
1652     elsif ( $op !~ /^[<>]=?$/ && (  $cf && $cf->Type eq 'IPAddressRange')) {
1653     
1654         my ($start_ip, $end_ip) = split /-/, $value;
1655         
1656         $self->_OpenParen;
1657         if ( $op !~ /NOT|!=|<>/i ) { # positive equation
1658             $self->_CustomFieldLimit(
1659                 $_field, '<=', $end_ip, %rest,
1660                 SUBKEY => $rest{'SUBKEY'}. '.Content',
1661             );
1662             $self->_CustomFieldLimit(
1663                 $_field, '>=', $start_ip, %rest,
1664                 SUBKEY          => $rest{'SUBKEY'}. '.LargeContent',
1665                 ENTRYAGGREGATOR => 'AND',
1666             ); 
1667             # as well limit borders so DB optimizers can use better
1668             # estimations and scan less rows
1669 # have to disable this tweak because of ipv6
1670 #            $self->_CustomFieldLimit(
1671 #                $_field, '>=', '000.000.000.000', %rest,
1672 #                SUBKEY          => $rest{'SUBKEY'}. '.Content',
1673 #                ENTRYAGGREGATOR => 'AND',
1674 #            );
1675 #            $self->_CustomFieldLimit(
1676 #                $_field, '<=', '255.255.255.255', %rest,
1677 #                SUBKEY          => $rest{'SUBKEY'}. '.LargeContent',
1678 #                ENTRYAGGREGATOR => 'AND',
1679 #            );  
1680         }       
1681         else { # negative equation
1682             $self->_CustomFieldLimit($_field, '>', $end_ip, %rest);
1683             $self->_CustomFieldLimit(
1684                 $_field, '<', $start_ip, %rest,
1685                 SUBKEY          => $rest{'SUBKEY'}. '.LargeContent',
1686                 ENTRYAGGREGATOR => 'OR',
1687             );  
1688             # TODO: as well limit borders so DB optimizers can use better
1689             # estimations and scan less rows, but it's harder to do
1690             # as we have OR aggregator
1691         }
1692         $self->_CloseParen;
1693     } 
1694     elsif ( !$negative_op || $single_value ) {
1695         $cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++ if !$single_value && !$range_op;
1696         my ($ObjectCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field, $type );
1697
1698         $self->_OpenParen;
1699
1700         $self->_OpenParen;
1701
1702         $self->_OpenParen;
1703         # if column is defined then deal only with it
1704         # otherwise search in Content and in LargeContent
1705         if ( $column ) {
1706             $self->_SQLLimit( $fix_op->(
1707                 ALIAS      => $ObjectCFs,
1708                 FIELD      => $column,
1709                 OPERATOR   => $op,
1710                 VALUE      => $value,
1711                 CASESENSITIVE => 0,
1712                 %rest
1713             ) );
1714             $self->_CloseParen;
1715             $self->_CloseParen;
1716             $self->_CloseParen;
1717         }
1718         else {
1719             # need special treatment for Date
1720             if ( $cf and $cf->Type eq 'DateTime' and $op eq '=' && $value !~ /:/ ) {
1721                 # no time specified, that means we want everything on a
1722                 # particular day.  in the database, we need to check for >
1723                 # and < the edges of that day.
1724                     my $date = RT::Date->new( $self->CurrentUser );
1725                     $date->Set( Format => 'unknown', Value => $value );
1726                     my $daystart = $date->ISO;
1727                     $date->AddDay;
1728                     my $dayend = $date->ISO;
1729
1730                     $self->_OpenParen;
1731
1732                     $self->_SQLLimit(
1733                         ALIAS    => $ObjectCFs,
1734                         FIELD    => 'Content',
1735                         OPERATOR => ">=",
1736                         VALUE    => $daystart,
1737                         %rest,
1738                     );
1739
1740                     $self->_SQLLimit(
1741                         ALIAS    => $ObjectCFs,
1742                         FIELD    => 'Content',
1743                         OPERATOR => "<",
1744                         VALUE    => $dayend,
1745                         %rest,
1746                         ENTRYAGGREGATOR => 'AND',
1747                     );
1748
1749                     $self->_CloseParen;
1750             }
1751             elsif ( $op eq '=' || $op eq '!=' || $op eq '<>' ) {
1752                 if ( length( Encode::encode( "UTF-8", $value) ) < 256 ) {
1753                     $self->_SQLLimit(
1754                         ALIAS    => $ObjectCFs,
1755                         FIELD    => 'Content',
1756                         OPERATOR => $op,
1757                         VALUE    => $value,
1758                         CASESENSITIVE => 0,
1759                         %rest
1760                     );
1761                 }
1762                 else {
1763                     $self->_OpenParen;
1764                     $self->_SQLLimit(
1765                         ALIAS           => $ObjectCFs,
1766                         FIELD           => 'Content',
1767                         OPERATOR        => '=',
1768                         VALUE           => '',
1769                         ENTRYAGGREGATOR => 'OR'
1770                     );
1771                     $self->_SQLLimit(
1772                         ALIAS           => $ObjectCFs,
1773                         FIELD           => 'Content',
1774                         OPERATOR        => 'IS',
1775                         VALUE           => 'NULL',
1776                         ENTRYAGGREGATOR => 'OR'
1777                     );
1778                     $self->_CloseParen;
1779                     $self->_SQLLimit( $fix_op->(
1780                         ALIAS           => $ObjectCFs,
1781                         FIELD           => 'LargeContent',
1782                         OPERATOR        => $op,
1783                         VALUE           => $value,
1784                         ENTRYAGGREGATOR => 'AND',
1785                         CASESENSITIVE => 0,
1786                     ) );
1787                 }
1788             }
1789             else {
1790                 $self->_SQLLimit(
1791                     ALIAS    => $ObjectCFs,
1792                     FIELD    => 'Content',
1793                     OPERATOR => $op,
1794                     VALUE    => $value,
1795                     CASESENSITIVE => 0,
1796                     %rest
1797                 );
1798
1799                 $self->_OpenParen;
1800                 $self->_OpenParen;
1801                 $self->_SQLLimit(
1802                     ALIAS           => $ObjectCFs,
1803                     FIELD           => 'Content',
1804                     OPERATOR        => '=',
1805                     VALUE           => '',
1806                     ENTRYAGGREGATOR => 'OR'
1807                 );
1808                 $self->_SQLLimit(
1809                     ALIAS           => $ObjectCFs,
1810                     FIELD           => 'Content',
1811                     OPERATOR        => 'IS',
1812                     VALUE           => 'NULL',
1813                     ENTRYAGGREGATOR => 'OR'
1814                 );
1815                 $self->_CloseParen;
1816                 $self->_SQLLimit( $fix_op->(
1817                     ALIAS           => $ObjectCFs,
1818                     FIELD           => 'LargeContent',
1819                     OPERATOR        => $op,
1820                     VALUE           => $value,
1821                     ENTRYAGGREGATOR => 'AND',
1822                     CASESENSITIVE => 0,
1823                 ) );
1824                 $self->_CloseParen;
1825             }
1826             $self->_CloseParen;
1827
1828             # XXX: if we join via CustomFields table then
1829             # because of order of left joins we get NULLs in
1830             # CF table and then get nulls for those records
1831             # in OCFVs table what result in wrong results
1832             # as decifer method now tries to load a CF then
1833             # we fall into this situation only when there
1834             # are more than one CF with the name in the DB.
1835             # the same thing applies to order by call.
1836             # TODO: reorder joins T <- OCFVs <- CFs <- OCFs if
1837             # we want treat IS NULL as (not applies or has
1838             # no value)
1839             $self->_SQLLimit(
1840                 ALIAS           => $CFs,
1841                 FIELD           => 'Name',
1842                 OPERATOR        => 'IS NOT',
1843                 VALUE           => 'NULL',
1844                 QUOTEVALUE      => 0,
1845                 ENTRYAGGREGATOR => 'AND',
1846             ) if $CFs;
1847             $self->_CloseParen;
1848
1849             if ($negative_op) {
1850                 $self->_SQLLimit(
1851                     ALIAS           => $ObjectCFs,
1852                     FIELD           => $column || 'Content',
1853                     OPERATOR        => 'IS',
1854                     VALUE           => 'NULL',
1855                     QUOTEVALUE      => 0,
1856                     ENTRYAGGREGATOR => 'OR',
1857                 );
1858             }
1859
1860             $self->_CloseParen;
1861         }
1862     }
1863     else {
1864         $cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++;
1865         my ($ObjectCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field, $type );
1866
1867         # reverse operation
1868         $op =~ s/!|NOT\s+//i;
1869
1870         # if column is defined then deal only with it
1871         # otherwise search in Content and in LargeContent
1872         if ( $column ) {
1873             $self->SUPER::Limit( $fix_op->(
1874                 LEFTJOIN   => $ObjectCFs,
1875                 ALIAS      => $ObjectCFs,
1876                 FIELD      => $column,
1877                 OPERATOR   => $op,
1878                 VALUE      => $value,
1879                 CASESENSITIVE => 0,
1880             ) );
1881         }
1882         else {
1883             $self->SUPER::Limit(
1884                 LEFTJOIN   => $ObjectCFs,
1885                 ALIAS      => $ObjectCFs,
1886                 FIELD      => 'Content',
1887                 OPERATOR   => $op,
1888                 VALUE      => $value,
1889                 CASESENSITIVE => 0,
1890             );
1891         }
1892         $self->_SQLLimit(
1893             %rest,
1894             ALIAS      => $ObjectCFs,
1895             FIELD      => 'id',
1896             OPERATOR   => 'IS',
1897             VALUE      => 'NULL',
1898             QUOTEVALUE => 0,
1899         );
1900     }
1901 }
1902
1903 sub _HasAttributeLimit {
1904     my ( $self, $field, $op, $value, %rest ) = @_;
1905
1906     my $alias = $self->Join(
1907         TYPE   => 'LEFT',
1908         ALIAS1 => 'main',
1909         FIELD1 => 'id',
1910         TABLE2 => 'Attributes',
1911         FIELD2 => 'ObjectId',
1912     );
1913     $self->SUPER::Limit(
1914         LEFTJOIN        => $alias,
1915         FIELD           => 'ObjectType',
1916         VALUE           => 'RT::Ticket',
1917         ENTRYAGGREGATOR => 'AND'
1918     );
1919     $self->SUPER::Limit(
1920         LEFTJOIN        => $alias,
1921         FIELD           => 'Name',
1922         OPERATOR        => $op,
1923         VALUE           => $value,
1924         ENTRYAGGREGATOR => 'AND'
1925     );
1926     $self->_SQLLimit(
1927         %rest,
1928         ALIAS      => $alias,
1929         FIELD      => 'id',
1930         OPERATOR   => $FIELD_METADATA{$field}->[1]? 'IS NOT': 'IS',
1931         VALUE      => 'NULL',
1932         QUOTEVALUE => 0,
1933     );
1934 }
1935
1936 # End Helper Functions
1937
1938 # End of SQL Stuff -------------------------------------------------
1939
1940
1941 =head2 OrderByCols ARRAY
1942
1943 A modified version of the OrderBy method which automatically joins where
1944 C<ALIAS> is set to the name of a watcher type.
1945
1946 =cut
1947
1948 sub OrderByCols {
1949     my $self = shift;
1950     my @args = @_;
1951     my $clause;
1952     my @res   = ();
1953     my $order = 0;
1954
1955     foreach my $row (@args) {
1956         if ( $row->{ALIAS} ) {
1957             push @res, $row;
1958             next;
1959         }
1960         if ( $row->{FIELD} !~ /\./ ) {
1961             my $meta = $self->FIELDS->{ $row->{FIELD} };
1962             unless ( $meta ) {
1963                 push @res, $row;
1964                 next;
1965             }
1966
1967             if ( $meta->[0] eq 'ENUM' && ($meta->[1]||'') eq 'Queue' ) {
1968                 my $alias = $self->Join(
1969                     TYPE   => 'LEFT',
1970                     ALIAS1 => 'main',
1971                     FIELD1 => $row->{'FIELD'},
1972                     TABLE2 => 'Queues',
1973                     FIELD2 => 'id',
1974                 );
1975                 push @res, { %$row, ALIAS => $alias, FIELD => "Name" };
1976             } elsif ( ( $meta->[0] eq 'ENUM' && ($meta->[1]||'') eq 'User' )
1977                 || ( $meta->[0] eq 'WATCHERFIELD' && ($meta->[1]||'') eq 'Owner' )
1978             ) {
1979                 my $alias = $self->Join(
1980                     TYPE   => 'LEFT',
1981                     ALIAS1 => 'main',
1982                     FIELD1 => $row->{'FIELD'},
1983                     TABLE2 => 'Users',
1984                     FIELD2 => 'id',
1985                 );
1986                 push @res, { %$row, ALIAS => $alias, FIELD => "Name" };
1987             } else {
1988                 push @res, $row;
1989             }
1990             next;
1991         }
1992
1993         my ( $field, $subkey ) = split /\./, $row->{FIELD}, 2;
1994         my $meta = $self->FIELDS->{$field};
1995         if ( defined $meta->[0] && $meta->[0] eq 'WATCHERFIELD' ) {
1996             # cache alias as we want to use one alias per watcher type for sorting
1997             my $users = $self->{_sql_u_watchers_alias_for_sort}{ $meta->[1] };
1998             unless ( $users ) {
1999                 $self->{_sql_u_watchers_alias_for_sort}{ $meta->[1] }
2000                     = $users = ( $self->_WatcherJoin( $meta->[1] ) )[2];
2001             }
2002             push @res, { %$row, ALIAS => $users, FIELD => $subkey };
2003        } elsif ( defined $meta->[0] && $meta->[0] eq 'CUSTOMFIELD' ) {
2004            my ($object, $field, $cf_obj, $column) = $self->_CustomFieldDecipher( $subkey );
2005            my $cfkey = $cf_obj ? $cf_obj->id : "$object.$field";
2006            $cfkey .= ".ordering" if !$cf_obj || ($cf_obj->MaxValues||0) != 1;
2007            my ($ObjectCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, ($cf_obj ?$cf_obj->id :0) , $field );
2008            # this is described in _CustomFieldLimit
2009            $self->_SQLLimit(
2010                ALIAS      => $CFs,
2011                FIELD      => 'Name',
2012                OPERATOR   => 'IS NOT',
2013                VALUE      => 'NULL',
2014                QUOTEVALUE => 1,
2015                ENTRYAGGREGATOR => 'AND',
2016            ) if $CFs;
2017            unless ($cf_obj) {
2018                # For those cases where we are doing a join against the
2019                # CF name, and don't have a CFid, use Unique to make sure
2020                # we don't show duplicate tickets.  NOTE: I'm pretty sure
2021                # this will stay mixed in for the life of the
2022                # class/package, and not just for the life of the object.
2023                # Potential performance issue.
2024                require DBIx::SearchBuilder::Unique;
2025                DBIx::SearchBuilder::Unique->import;
2026            }
2027            my $CFvs = $self->Join(
2028                TYPE   => 'LEFT',
2029                ALIAS1 => $ObjectCFs,
2030                FIELD1 => 'CustomField',
2031                TABLE2 => 'CustomFieldValues',
2032                FIELD2 => 'CustomField',
2033            );
2034            $self->SUPER::Limit(
2035                LEFTJOIN        => $CFvs,
2036                FIELD           => 'Name',
2037                QUOTEVALUE      => 0,
2038                VALUE           => $ObjectCFs . ".Content",
2039                ENTRYAGGREGATOR => 'AND'
2040            );
2041
2042            push @res, { %$row, ALIAS => $CFvs, FIELD => 'SortOrder' };
2043            push @res, { %$row, ALIAS => $ObjectCFs, FIELD => 'Content' };
2044        } elsif ( $field eq "Custom" && $subkey eq "Ownership") {
2045            # PAW logic is "reversed"
2046            my $order = "ASC";
2047            if (exists $row->{ORDER} ) {
2048                my $o = $row->{ORDER};
2049                delete $row->{ORDER};
2050                $order = "DESC" if $o =~ /asc/i;
2051            }
2052
2053            # Ticket.Owner    1 0 X
2054            # Unowned Tickets 0 1 X
2055            # Else            0 0 X
2056
2057            foreach my $uid ( $self->CurrentUser->Id, RT->Nobody->Id ) {
2058                if ( RT->Config->Get('DatabaseType') eq 'Oracle' ) {
2059                    my $f = ($row->{'ALIAS'} || 'main') .'.Owner';
2060                    push @res, {
2061                        %$row,
2062                        FIELD => undef,
2063                        ALIAS => '',
2064                        FUNCTION => "CASE WHEN $f=$uid THEN 1 ELSE 0 END",
2065                        ORDER => $order
2066                    };
2067                } else {
2068                    push @res, {
2069                        %$row,
2070                        FIELD => undef,
2071                        FUNCTION => "Owner=$uid",
2072                        ORDER => $order
2073                    };
2074                }
2075            }
2076
2077            push @res, { %$row, FIELD => "Priority", ORDER => $order } ;
2078
2079        } elsif ( $field eq 'Customer' ) { #Freeside
2080            # OrderBy(FIELD => expression) doesn't work, it has to be 
2081            # an actual field, so we have to do the join even if sorting
2082            # by custnum
2083            my $custalias = $self->JoinToCustomer;
2084            my $cust_field = lc($subkey);
2085            if ( !$cust_field or $cust_field eq 'number' ) {
2086                $cust_field = 'custnum';
2087            }
2088            elsif ( $cust_field eq 'name' ) {
2089                $cust_field = "COALESCE( $custalias.company,
2090                $custalias.last || ', ' || $custalias.first
2091                )";
2092            }
2093            else { # order by cust_main fields directly: 'Customer.agentnum'
2094                $cust_field = $subkey;
2095            }
2096            push @res, { %$row, ALIAS => $custalias, FIELD => $cust_field };
2097
2098       } elsif ( $field eq 'Service' ) {
2099           
2100           my $svcalias = $self->JoinToService;
2101           my $svc_field = lc($subkey);
2102           if ( !$svc_field or $svc_field eq 'number' ) {
2103               $svc_field = 'svcnum';
2104           }
2105           push @res, { %$row, ALIAS => $svcalias, FIELD => $svc_field };
2106
2107        } #Freeside
2108
2109        else {
2110            push @res, $row;
2111        }
2112     }
2113     return $self->SUPER::OrderByCols(@res);
2114 }
2115
2116 #Freeside
2117
2118 sub JoinToCustLinks {
2119     # Set up join to links (id = localbase),
2120     # limit link type to 'MemberOf',
2121     # and target value to any Freeside custnum URI.
2122     # Return the linkalias for further join/limit action,
2123     # and an sql expression to retrieve the custnum.
2124     my $self = shift;
2125     # only join once for each RT::Tickets object
2126     my $linkalias = $self->{cust_main_linkalias};
2127     if (!$linkalias) {
2128         $linkalias = $self->Join(
2129             TYPE   => 'LEFT',
2130             ALIAS1 => 'main',
2131             FIELD1 => 'id',
2132             TABLE2 => 'Links',
2133             FIELD2 => 'LocalBase',
2134         );
2135        $self->SUPER::Limit(
2136          LEFTJOIN => $linkalias,
2137          FIELD    => 'Base',
2138          OPERATOR => 'LIKE',
2139          VALUE    => 'fsck.com-rt://%/ticket/%',
2140        );
2141         $self->SUPER::Limit(
2142             LEFTJOIN => $linkalias,
2143             FIELD    => 'Type',
2144             OPERATOR => '=',
2145             VALUE    => 'MemberOf',
2146         );
2147         $self->SUPER::Limit(
2148             LEFTJOIN => $linkalias,
2149             FIELD    => 'Target',
2150             OPERATOR => 'STARTSWITH',
2151             VALUE    => 'freeside://freeside/cust_main/',
2152         );
2153         $self->{cust_main_linkalias} = $linkalias;
2154     }
2155     my $custnum_sql = "CAST(SUBSTR($linkalias.Target,31) AS ";
2156     if ( RT->Config->Get('DatabaseType') eq 'mysql' ) {
2157         $custnum_sql .= 'SIGNED INTEGER)';
2158     }
2159     else {
2160         $custnum_sql .= 'INTEGER)';
2161     }
2162     return ($linkalias, $custnum_sql);
2163 }
2164
2165 sub JoinToCustomer {
2166     my $self = shift;
2167     my ($linkalias, $custnum_sql) = $self->JoinToCustLinks;
2168     # don't reuse this join, though--negative queries need 
2169     # independent joins
2170     my $custalias = $self->Join(
2171         TYPE       => 'LEFT',
2172         EXPRESSION => $custnum_sql,
2173         TABLE2     => 'cust_main',
2174         FIELD2     => 'custnum',
2175     );
2176     return $custalias;
2177 }
2178
2179 sub JoinToSvcLinks {
2180     my $self = shift;
2181     my $linkalias = $self->{cust_svc_linkalias};
2182     if (!$linkalias) {
2183         $linkalias = $self->Join(
2184             TYPE   => 'LEFT',
2185             ALIAS1 => 'main',
2186             FIELD1 => 'id',
2187             TABLE2 => 'Links',
2188             FIELD2 => 'LocalBase',
2189         );
2190        $self->SUPER::Limit(
2191          LEFTJOIN => $linkalias,
2192          FIELD    => 'Base',
2193          OPERATOR => 'LIKE',
2194          VALUE    => 'fsck.com-rt://%/ticket/%',
2195        );
2196
2197         $self->SUPER::Limit(
2198             LEFTJOIN => $linkalias,
2199             FIELD    => 'Type',
2200             OPERATOR => '=',
2201             VALUE    => 'MemberOf',
2202         );
2203         $self->SUPER::Limit(
2204             LEFTJOIN => $linkalias,
2205             FIELD    => 'Target',
2206             OPERATOR => 'STARTSWITH',
2207             VALUE    => 'freeside://freeside/cust_svc/',
2208         );
2209         $self->{cust_svc_linkalias} = $linkalias;
2210     }
2211     my $svcnum_sql = "CAST(SUBSTR($linkalias.Target,30) AS ";
2212     if ( RT->Config->Get('DatabaseType') eq 'mysql' ) {
2213         $svcnum_sql .= 'SIGNED INTEGER)';
2214     }
2215     else {
2216         $svcnum_sql .= 'INTEGER)';
2217     }
2218     return ($linkalias, $svcnum_sql);
2219 }
2220
2221 sub JoinToService {
2222     my $self = shift;
2223     my ($linkalias, $svcnum_sql) = $self->JoinToSvcLinks;
2224     $self->Join(
2225         TYPE       => 'LEFT',
2226         EXPRESSION => $svcnum_sql,
2227         TABLE2     => 'cust_svc',
2228         FIELD2     => 'svcnum',
2229     );
2230 }
2231
2232 # This creates an alternate left join path to cust_main via cust_svc.
2233 # _FreesideFieldLimit needs to add this as a separate, independent join
2234 # and include all tickets that have a matching cust_main record via 
2235 # either path.
2236 sub JoinToCustomerViaService {
2237     my $self = shift;
2238     my $svcalias = $self->JoinToService;
2239     my $cust_pkg = $self->Join(
2240         TYPE      => 'LEFT',
2241         ALIAS1    => $svcalias,
2242         FIELD1    => 'pkgnum',
2243         TABLE2    => 'cust_pkg',
2244         FIELD2    => 'pkgnum',
2245     );
2246     my $cust_main = $self->Join(
2247         TYPE      => 'LEFT',
2248         ALIAS1    => $cust_pkg,
2249         FIELD1    => 'custnum',
2250         TABLE2    => 'cust_main',
2251         FIELD2    => 'custnum',
2252     );
2253     $cust_main;
2254 }
2255
2256 sub _FreesideFieldLimit {
2257     my ( $self, $field, $op, $value, %rest ) = @_;
2258     my $is_negative = 0;
2259     if ( $op eq '!=' || $op =~ /\bNOT\b/i ) {
2260         # if the op is negative, do the join as though
2261         # the op were positive, then accept only records
2262         # where the right-side join key is null.
2263         $is_negative = 1;
2264         $op = '=' if $op eq '!=';
2265         $op =~ s/\bNOT\b//;
2266     }
2267
2268     my (@alias, $table2, $subfield, $pkey);
2269     if ( $field eq 'Customer' ) {
2270       push @alias, $self->JoinToCustomer;
2271       push @alias, $self->JoinToCustomerViaService;
2272       $pkey = 'custnum';
2273     }
2274     elsif ( $field eq 'Service' ) {
2275       push @alias, $self->JoinToService;
2276       $pkey = 'svcnum';
2277     }
2278     else {
2279       die "malformed Freeside query: $field";
2280     }
2281
2282     $subfield = $rest{SUBKEY} || $pkey;
2283     # compound subkey: separate into table name and field in that table
2284     # (must be linked by custnum)
2285     $subfield = lc($subfield);
2286     ($table2, $subfield) = ($1, $2) if $subfield =~ /^(\w+)?\.(\w+)$/;
2287     $subfield = $pkey if $subfield eq 'number';
2288
2289     # if it's compound, create a join from cust_main or cust_svc to that 
2290     # table, using custnum or svcnum, and Limit on that table instead.
2291     my @_SQLLimit = ();
2292     foreach my $a (@alias) {
2293       if ( $table2 ) {
2294           $a = $self->Join(
2295               TYPE        => 'LEFT',
2296               ALIAS1      => $a,
2297               FIELD1      => $pkey,
2298               TABLE2      => $table2,
2299               FIELD2      => $pkey,
2300           );
2301       }
2302
2303       # do the actual Limit
2304       $self->SUPER::Limit(
2305           LEFTJOIN        => $a,
2306           FIELD           => $subfield,
2307           OPERATOR        => $op,
2308           VALUE           => $value,
2309           ENTRYAGGREGATOR => 'AND',
2310           # no SUBCLAUSE needed, limits on different aliases across left joins
2311           # are inherently independent
2312       );
2313
2314       # then, since it's a left join, exclude tickets for which there is now 
2315       # no matching record in the table we just limited on.  (Or where there 
2316       # is a matching record, if $is_negative.)
2317       # For a cust_main query (where there are two different aliases), this 
2318       # will produce a subclause: "cust_main_1.custnum IS NOT NULL OR 
2319       # cust_main_2.custnum IS NOT NULL" (or "IS NULL AND..." for a negative
2320       # query).
2321       #$self->_SQLLimit(
2322       push @_SQLLimit, {
2323           %rest,
2324           ALIAS           => $a,
2325           FIELD           => $pkey,
2326           OPERATOR        => $is_negative ? 'IS' : 'IS NOT',
2327           VALUE           => 'NULL',
2328           QUOTEVALUE      => 0,
2329           ENTRYAGGREGATOR => $is_negative ? 'AND' : 'OR',
2330           SUBCLAUSE       => 'fs_limit',
2331       };
2332     }
2333
2334     $self->_OpenParen;
2335     foreach my $_SQLLimit (@_SQLLimit) {
2336       $self->_SQLLimit( %$_SQLLimit);
2337     }
2338     $self->_CloseParen;
2339
2340 }
2341
2342 #Freeside
2343
2344 =head2 Limit
2345
2346 Takes a paramhash with the fields FIELD, OPERATOR, VALUE and DESCRIPTION
2347 Generally best called from LimitFoo methods
2348
2349 =cut
2350
2351 sub Limit {
2352     my $self = shift;
2353     my %args = (
2354         FIELD       => undef,
2355         OPERATOR    => '=',
2356         VALUE       => undef,
2357         DESCRIPTION => undef,
2358         @_
2359     );
2360     $args{'DESCRIPTION'} = $self->loc(
2361         "[_1] [_2] [_3]",  $args{'FIELD'},
2362         $args{'OPERATOR'}, $args{'VALUE'}
2363         )
2364         if ( !defined $args{'DESCRIPTION'} );
2365
2366     my $index = $self->_NextIndex;
2367
2368 # make the TicketRestrictions hash the equivalent of whatever we just passed in;
2369
2370     %{ $self->{'TicketRestrictions'}{$index} } = %args;
2371
2372     $self->{'RecalcTicketLimits'} = 1;
2373
2374 # If we're looking at the effective id, we don't want to append the other clause
2375 # which limits us to tickets where id = effective id
2376     if ( $args{'FIELD'} eq 'EffectiveId'
2377         && ( !$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) )
2378     {
2379         $self->{'looking_at_effective_id'} = 1;
2380     }
2381
2382     if ( $args{'FIELD'} eq 'Type'
2383         && ( !$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) )
2384     {
2385         $self->{'looking_at_type'} = 1;
2386     }
2387
2388     return ($index);
2389 }
2390
2391
2392
2393
2394 =head2 LimitQueue
2395
2396 LimitQueue takes a paramhash with the fields OPERATOR and VALUE.
2397 OPERATOR is one of = or !=. (It defaults to =).
2398 VALUE is a queue id or Name.
2399
2400
2401 =cut
2402
2403 sub LimitQueue {
2404     my $self = shift;
2405     my %args = (
2406         VALUE    => undef,
2407         OPERATOR => '=',
2408         @_
2409     );
2410
2411     #TODO  VALUE should also take queue objects
2412     if ( defined $args{'VALUE'} && $args{'VALUE'} !~ /^\d+$/ ) {
2413         my $queue = RT::Queue->new( $self->CurrentUser );
2414         $queue->Load( $args{'VALUE'} );
2415         $args{'VALUE'} = $queue->Id;
2416     }
2417
2418     # What if they pass in an Id?  Check for isNum() and convert to
2419     # string.
2420
2421     #TODO check for a valid queue here
2422
2423     $self->Limit(
2424         FIELD       => 'Queue',
2425         VALUE       => $args{'VALUE'},
2426         OPERATOR    => $args{'OPERATOR'},
2427         DESCRIPTION => join(
2428             ' ', $self->loc('Queue'), $args{'OPERATOR'}, $args{'VALUE'},
2429         ),
2430     );
2431
2432 }
2433
2434
2435
2436 =head2 LimitStatus
2437
2438 Takes a paramhash with the fields OPERATOR and VALUE.
2439 OPERATOR is one of = or !=.
2440 VALUE is a status.
2441
2442 RT adds Status != 'deleted' until object has
2443 allow_deleted_search internal property set.
2444 $tickets->{'allow_deleted_search'} = 1;
2445 $tickets->LimitStatus( VALUE => 'deleted' );
2446
2447 =cut
2448
2449 sub LimitStatus {
2450     my $self = shift;
2451     my %args = (
2452         OPERATOR => '=',
2453         @_
2454     );
2455     $self->Limit(
2456         FIELD       => 'Status',
2457         VALUE       => $args{'VALUE'},
2458         OPERATOR    => $args{'OPERATOR'},
2459         DESCRIPTION => join( ' ',
2460             $self->loc('Status'), $args{'OPERATOR'},
2461             $self->loc( $args{'VALUE'} ) ),
2462     );
2463 }
2464
2465
2466
2467 =head2 IgnoreType
2468
2469 If called, this search will not automatically limit the set of results found
2470 to tickets of type "Ticket". Tickets of other types, such as "project" and
2471 "approval" will be found.
2472
2473 =cut
2474
2475 sub IgnoreType {
2476     my $self = shift;
2477
2478     # Instead of faking a Limit that later gets ignored, fake up the
2479     # fact that we're already looking at type, so that the check in
2480     # Tickets_SQL/FromSQL goes down the right branch
2481
2482     #  $self->LimitType(VALUE => '__any');
2483     $self->{looking_at_type} = 1;
2484 }
2485
2486
2487
2488 =head2 LimitType
2489
2490 Takes a paramhash with the fields OPERATOR and VALUE.
2491 OPERATOR is one of = or !=, it defaults to "=".
2492 VALUE is a string to search for in the type of the ticket.
2493
2494
2495
2496 =cut
2497
2498 sub LimitType {
2499     my $self = shift;
2500     my %args = (
2501         OPERATOR => '=',
2502         VALUE    => undef,
2503         @_
2504     );
2505     $self->Limit(
2506         FIELD       => 'Type',
2507         VALUE       => $args{'VALUE'},
2508         OPERATOR    => $args{'OPERATOR'},
2509         DESCRIPTION => join( ' ',
2510             $self->loc('Type'), $args{'OPERATOR'}, $args{'VALUE'}, ),
2511     );
2512 }
2513
2514
2515
2516
2517
2518 =head2 LimitSubject
2519
2520 Takes a paramhash with the fields OPERATOR and VALUE.
2521 OPERATOR is one of = or !=.
2522 VALUE is a string to search for in the subject of the ticket.
2523
2524 =cut
2525
2526 sub LimitSubject {
2527     my $self = shift;
2528     my %args = (@_);
2529     $self->Limit(
2530         FIELD       => 'Subject',
2531         VALUE       => $args{'VALUE'},
2532         OPERATOR    => $args{'OPERATOR'},
2533         DESCRIPTION => join( ' ',
2534             $self->loc('Subject'), $args{'OPERATOR'}, $args{'VALUE'}, ),
2535     );
2536 }
2537
2538
2539
2540 # Things that can be > < = !=
2541
2542
2543 =head2 LimitId
2544
2545 Takes a paramhash with the fields OPERATOR and VALUE.
2546 OPERATOR is one of =, >, < or !=.
2547 VALUE is a ticket Id to search for
2548
2549 =cut
2550
2551 sub LimitId {
2552     my $self = shift;
2553     my %args = (
2554         OPERATOR => '=',
2555         @_
2556     );
2557
2558     $self->Limit(
2559         FIELD       => 'id',
2560         VALUE       => $args{'VALUE'},
2561         OPERATOR    => $args{'OPERATOR'},
2562         DESCRIPTION =>
2563             join( ' ', $self->loc('Id'), $args{'OPERATOR'}, $args{'VALUE'}, ),
2564     );
2565 }
2566
2567
2568
2569 =head2 LimitPriority
2570
2571 Takes a paramhash with the fields OPERATOR and VALUE.
2572 OPERATOR is one of =, >, < or !=.
2573 VALUE is a value to match the ticket's priority against
2574
2575 =cut
2576
2577 sub LimitPriority {
2578     my $self = shift;
2579     my %args = (@_);
2580     $self->Limit(
2581         FIELD       => 'Priority',
2582         VALUE       => $args{'VALUE'},
2583         OPERATOR    => $args{'OPERATOR'},
2584         DESCRIPTION => join( ' ',
2585             $self->loc('Priority'),
2586             $args{'OPERATOR'}, $args{'VALUE'}, ),
2587     );
2588 }
2589
2590
2591
2592 =head2 LimitInitialPriority
2593
2594 Takes a paramhash with the fields OPERATOR and VALUE.
2595 OPERATOR is one of =, >, < or !=.
2596 VALUE is a value to match the ticket's initial priority against
2597
2598
2599 =cut
2600
2601 sub LimitInitialPriority {
2602     my $self = shift;
2603     my %args = (@_);
2604     $self->Limit(
2605         FIELD       => 'InitialPriority',
2606         VALUE       => $args{'VALUE'},
2607         OPERATOR    => $args{'OPERATOR'},
2608         DESCRIPTION => join( ' ',
2609             $self->loc('Initial Priority'), $args{'OPERATOR'},
2610             $args{'VALUE'}, ),
2611     );
2612 }
2613
2614
2615
2616 =head2 LimitFinalPriority
2617
2618 Takes a paramhash with the fields OPERATOR and VALUE.
2619 OPERATOR is one of =, >, < or !=.
2620 VALUE is a value to match the ticket's final priority against
2621
2622 =cut
2623
2624 sub LimitFinalPriority {
2625     my $self = shift;
2626     my %args = (@_);
2627     $self->Limit(
2628         FIELD       => 'FinalPriority',
2629         VALUE       => $args{'VALUE'},
2630         OPERATOR    => $args{'OPERATOR'},
2631         DESCRIPTION => join( ' ',
2632             $self->loc('Final Priority'), $args{'OPERATOR'},
2633             $args{'VALUE'}, ),
2634     );
2635 }
2636
2637
2638
2639 =head2 LimitTimeWorked
2640
2641 Takes a paramhash with the fields OPERATOR and VALUE.
2642 OPERATOR is one of =, >, < or !=.
2643 VALUE is a value to match the ticket's TimeWorked attribute
2644
2645 =cut
2646
2647 sub LimitTimeWorked {
2648     my $self = shift;
2649     my %args = (@_);
2650     $self->Limit(
2651         FIELD       => 'TimeWorked',
2652         VALUE       => $args{'VALUE'},
2653         OPERATOR    => $args{'OPERATOR'},
2654         DESCRIPTION => join( ' ',
2655             $self->loc('Time Worked'),
2656             $args{'OPERATOR'}, $args{'VALUE'}, ),
2657     );
2658 }
2659
2660
2661
2662 =head2 LimitTimeLeft
2663
2664 Takes a paramhash with the fields OPERATOR and VALUE.
2665 OPERATOR is one of =, >, < or !=.
2666 VALUE is a value to match the ticket's TimeLeft attribute
2667
2668 =cut
2669
2670 sub LimitTimeLeft {
2671     my $self = shift;
2672     my %args = (@_);
2673     $self->Limit(
2674         FIELD       => 'TimeLeft',
2675         VALUE       => $args{'VALUE'},
2676         OPERATOR    => $args{'OPERATOR'},
2677         DESCRIPTION => join( ' ',
2678             $self->loc('Time Left'),
2679             $args{'OPERATOR'}, $args{'VALUE'}, ),
2680     );
2681 }
2682
2683
2684
2685
2686
2687 =head2 LimitContent
2688
2689 Takes a paramhash with the fields OPERATOR and VALUE.
2690 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2691 VALUE is a string to search for in the body of the ticket
2692
2693 =cut
2694
2695 sub LimitContent {
2696     my $self = shift;
2697     my %args = (@_);
2698     $self->Limit(
2699         FIELD       => 'Content',
2700         VALUE       => $args{'VALUE'},
2701         OPERATOR    => $args{'OPERATOR'},
2702         DESCRIPTION => join( ' ',
2703             $self->loc('Ticket content'), $args{'OPERATOR'},
2704             $args{'VALUE'}, ),
2705     );
2706 }
2707
2708
2709
2710 =head2 LimitFilename
2711
2712 Takes a paramhash with the fields OPERATOR and VALUE.
2713 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2714 VALUE is a string to search for in the body of the ticket
2715
2716 =cut
2717
2718 sub LimitFilename {
2719     my $self = shift;
2720     my %args = (@_);
2721     $self->Limit(
2722         FIELD       => 'Filename',
2723         VALUE       => $args{'VALUE'},
2724         OPERATOR    => $args{'OPERATOR'},
2725         DESCRIPTION => join( ' ',
2726             $self->loc('Attachment filename'), $args{'OPERATOR'},
2727             $args{'VALUE'}, ),
2728     );
2729 }
2730
2731
2732 =head2 LimitContentType
2733
2734 Takes a paramhash with the fields OPERATOR and VALUE.
2735 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2736 VALUE is a content type to search ticket attachments for
2737
2738 =cut
2739
2740 sub LimitContentType {
2741     my $self = shift;
2742     my %args = (@_);
2743     $self->Limit(
2744         FIELD       => 'ContentType',
2745         VALUE       => $args{'VALUE'},
2746         OPERATOR    => $args{'OPERATOR'},
2747         DESCRIPTION => join( ' ',
2748             $self->loc('Ticket content type'), $args{'OPERATOR'},
2749             $args{'VALUE'}, ),
2750     );
2751 }
2752
2753
2754
2755
2756
2757 =head2 LimitOwner
2758
2759 Takes a paramhash with the fields OPERATOR and VALUE.
2760 OPERATOR is one of = or !=.
2761 VALUE is a user id.
2762
2763 =cut
2764
2765 sub LimitOwner {
2766     my $self = shift;
2767     my %args = (
2768         OPERATOR => '=',
2769         @_
2770     );
2771
2772     my $owner = RT::User->new( $self->CurrentUser );
2773     $owner->Load( $args{'VALUE'} );
2774
2775     # FIXME: check for a valid $owner
2776     $self->Limit(
2777         FIELD       => 'Owner',
2778         VALUE       => $args{'VALUE'},
2779         OPERATOR    => $args{'OPERATOR'},
2780         DESCRIPTION => join( ' ',
2781             $self->loc('Owner'), $args{'OPERATOR'}, $owner->Name(), ),
2782     );
2783
2784 }
2785
2786
2787
2788
2789 =head2 LimitWatcher
2790
2791   Takes a paramhash with the fields OPERATOR, TYPE and VALUE.
2792   OPERATOR is one of =, LIKE, NOT LIKE or !=.
2793   VALUE is a value to match the ticket's watcher email addresses against
2794   TYPE is the sort of watchers you want to match against. Leave it undef if you want to search all of them
2795
2796
2797 =cut
2798
2799 sub LimitWatcher {
2800     my $self = shift;
2801     my %args = (
2802         OPERATOR => '=',
2803         VALUE    => undef,
2804         TYPE     => undef,
2805         @_
2806     );
2807
2808     #build us up a description
2809     my ( $watcher_type, $desc );
2810     if ( $args{'TYPE'} ) {
2811         $watcher_type = $args{'TYPE'};
2812     }
2813     else {
2814         $watcher_type = "Watcher";
2815     }
2816
2817     $self->Limit(
2818         FIELD       => $watcher_type,
2819         VALUE       => $args{'VALUE'},
2820         OPERATOR    => $args{'OPERATOR'},
2821         TYPE        => $args{'TYPE'},
2822         DESCRIPTION => join( ' ',
2823             $self->loc($watcher_type),
2824             $args{'OPERATOR'}, $args{'VALUE'}, ),
2825     );
2826 }
2827
2828
2829
2830
2831
2832
2833 =head2 LimitLinkedTo
2834
2835 LimitLinkedTo takes a paramhash with two fields: TYPE and TARGET
2836 TYPE limits the sort of link we want to search on
2837
2838 TYPE = { RefersTo, MemberOf, DependsOn }
2839
2840 TARGET is the id or URI of the TARGET of the link
2841
2842 =cut
2843
2844 sub LimitLinkedTo {
2845     my $self = shift;
2846     my %args = (
2847         TARGET   => undef,
2848         TYPE     => undef,
2849         OPERATOR => '=',
2850         @_
2851     );
2852
2853     $self->Limit(
2854         FIELD       => 'LinkedTo',
2855         BASE        => undef,
2856         TARGET      => $args{'TARGET'},
2857         TYPE        => $args{'TYPE'},
2858         DESCRIPTION => $self->loc(
2859             "Tickets [_1] by [_2]",
2860             $self->loc( $args{'TYPE'} ),
2861             $args{'TARGET'}
2862         ),
2863         OPERATOR    => $args{'OPERATOR'},
2864     );
2865 }
2866
2867
2868
2869 =head2 LimitLinkedFrom
2870
2871 LimitLinkedFrom takes a paramhash with two fields: TYPE and BASE
2872 TYPE limits the sort of link we want to search on
2873
2874
2875 BASE is the id or URI of the BASE of the link
2876
2877 =cut
2878
2879 sub LimitLinkedFrom {
2880     my $self = shift;
2881     my %args = (
2882         BASE     => undef,
2883         TYPE     => undef,
2884         OPERATOR => '=',
2885         @_
2886     );
2887
2888     # translate RT2 From/To naming to RT3 TicketSQL naming
2889     my %fromToMap = qw(DependsOn DependentOn
2890         MemberOf  HasMember
2891         RefersTo  ReferredToBy);
2892
2893     my $type = $args{'TYPE'};
2894     $type = $fromToMap{$type} if exists( $fromToMap{$type} );
2895
2896     $self->Limit(
2897         FIELD       => 'LinkedTo',
2898         TARGET      => undef,
2899         BASE        => $args{'BASE'},
2900         TYPE        => $type,
2901         DESCRIPTION => $self->loc(
2902             "Tickets [_1] [_2]",
2903             $self->loc( $args{'TYPE'} ),
2904             $args{'BASE'},
2905         ),
2906         OPERATOR    => $args{'OPERATOR'},
2907     );
2908 }
2909
2910
2911 sub LimitMemberOf {
2912     my $self      = shift;
2913     my $ticket_id = shift;
2914     return $self->LimitLinkedTo(
2915         @_,
2916         TARGET => $ticket_id,
2917         TYPE   => 'MemberOf',
2918     );
2919 }
2920
2921
2922 sub LimitHasMember {
2923     my $self      = shift;
2924     my $ticket_id = shift;
2925     return $self->LimitLinkedFrom(
2926         @_,
2927         BASE => "$ticket_id",
2928         TYPE => 'HasMember',
2929     );
2930
2931 }
2932
2933
2934
2935 sub LimitDependsOn {
2936     my $self      = shift;
2937     my $ticket_id = shift;
2938     return $self->LimitLinkedTo(
2939         @_,
2940         TARGET => $ticket_id,
2941         TYPE   => 'DependsOn',
2942     );
2943
2944 }
2945
2946
2947
2948 sub LimitDependedOnBy {
2949     my $self      = shift;
2950     my $ticket_id = shift;
2951     return $self->LimitLinkedFrom(
2952         @_,
2953         BASE => $ticket_id,
2954         TYPE => 'DependentOn',
2955     );
2956
2957 }
2958
2959
2960
2961 sub LimitRefersTo {
2962     my $self      = shift;
2963     my $ticket_id = shift;
2964     return $self->LimitLinkedTo(
2965         @_,
2966         TARGET => $ticket_id,
2967         TYPE   => 'RefersTo',
2968     );
2969
2970 }
2971
2972
2973
2974 sub LimitReferredToBy {
2975     my $self      = shift;
2976     my $ticket_id = shift;
2977     return $self->LimitLinkedFrom(
2978         @_,
2979         BASE => $ticket_id,
2980         TYPE => 'ReferredToBy',
2981     );
2982 }
2983
2984
2985
2986
2987
2988 =head2 LimitDate (FIELD => 'DateField', OPERATOR => $oper, VALUE => $ISODate)
2989
2990 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
2991
2992 OPERATOR is one of > or <
2993 VALUE is a date and time in ISO format in GMT
2994 FIELD is one of Starts, Started, Told, Created, Resolved, LastUpdated
2995
2996 There are also helper functions of the form LimitFIELD that eliminate
2997 the need to pass in a FIELD argument.
2998
2999 =cut
3000
3001 sub LimitDate {
3002     my $self = shift;
3003     my %args = (
3004         FIELD    => undef,
3005         VALUE    => undef,
3006         OPERATOR => undef,
3007
3008         @_
3009     );
3010
3011     #Set the description if we didn't get handed it above
3012     unless ( $args{'DESCRIPTION'} ) {
3013         $args{'DESCRIPTION'} = $args{'FIELD'} . " "
3014             . $args{'OPERATOR'} . " "
3015             . $args{'VALUE'} . " GMT";
3016     }
3017
3018     $self->Limit(%args);
3019
3020 }
3021
3022
3023 sub LimitCreated {
3024     my $self = shift;
3025     $self->LimitDate( FIELD => 'Created', @_ );
3026 }
3027
3028 sub LimitDue {
3029     my $self = shift;
3030     $self->LimitDate( FIELD => 'Due', @_ );
3031
3032 }
3033
3034 sub LimitStarts {
3035     my $self = shift;
3036     $self->LimitDate( FIELD => 'Starts', @_ );
3037
3038 }
3039
3040 sub LimitStarted {
3041     my $self = shift;
3042     $self->LimitDate( FIELD => 'Started', @_ );
3043 }
3044
3045 sub LimitResolved {
3046     my $self = shift;
3047     $self->LimitDate( FIELD => 'Resolved', @_ );
3048 }
3049
3050 sub LimitTold {
3051     my $self = shift;
3052     $self->LimitDate( FIELD => 'Told', @_ );
3053 }
3054
3055 sub LimitLastUpdated {
3056     my $self = shift;
3057     $self->LimitDate( FIELD => 'LastUpdated', @_ );
3058 }
3059
3060 #
3061
3062 =head2 LimitTransactionDate (OPERATOR => $oper, VALUE => $ISODate)
3063
3064 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
3065
3066 OPERATOR is one of > or <
3067 VALUE is a date and time in ISO format in GMT
3068
3069
3070 =cut
3071
3072 sub LimitTransactionDate {
3073     my $self = shift;
3074     my %args = (
3075         FIELD    => 'TransactionDate',
3076         VALUE    => undef,
3077         OPERATOR => undef,
3078
3079         @_
3080     );
3081
3082     #  <20021217042756.GK28744@pallas.fsck.com>
3083     #    "Kill It" - Jesse.
3084
3085     #Set the description if we didn't get handed it above
3086     unless ( $args{'DESCRIPTION'} ) {
3087         $args{'DESCRIPTION'} = $args{'FIELD'} . " "
3088             . $args{'OPERATOR'} . " "
3089             . $args{'VALUE'} . " GMT";
3090     }
3091
3092     $self->Limit(%args);
3093
3094 }
3095
3096
3097
3098
3099 =head2 LimitCustomField
3100
3101 Takes a paramhash of key/value pairs with the following keys:
3102
3103 =over 4
3104
3105 =item CUSTOMFIELD - CustomField name or id.  If a name is passed, an additional parameter QUEUE may also be passed to distinguish the custom field.
3106
3107 =item OPERATOR - The usual Limit operators
3108
3109 =item VALUE - The value to compare against
3110
3111 =back
3112
3113 =cut
3114
3115 sub LimitCustomField {
3116     my $self = shift;
3117     my %args = (
3118         VALUE       => undef,
3119         CUSTOMFIELD => undef,
3120         OPERATOR    => '=',
3121         DESCRIPTION => undef,
3122         FIELD       => 'CustomFieldValue',
3123         QUOTEVALUE  => 1,
3124         @_
3125     );
3126
3127     my $CF = RT::CustomField->new( $self->CurrentUser );
3128     if ( $args{CUSTOMFIELD} =~ /^\d+$/ ) {
3129         $CF->Load( $args{CUSTOMFIELD} );
3130     }
3131     else {
3132         $CF->LoadByNameAndQueue(
3133             Name  => $args{CUSTOMFIELD},
3134             Queue => $args{QUEUE}
3135         );
3136         $args{CUSTOMFIELD} = $CF->Id;
3137     }
3138
3139     #If we are looking to compare with a null value.
3140     if ( $args{'OPERATOR'} =~ /^is$/i ) {
3141         $args{'DESCRIPTION'}
3142             ||= $self->loc( "Custom field [_1] has no value.", $CF->Name );
3143     }
3144     elsif ( $args{'OPERATOR'} =~ /^is not$/i ) {
3145         $args{'DESCRIPTION'}
3146             ||= $self->loc( "Custom field [_1] has a value.", $CF->Name );
3147     }
3148
3149     # if we're not looking to compare with a null value
3150     else {
3151         $args{'DESCRIPTION'} ||= $self->loc( "Custom field [_1] [_2] [_3]",
3152             $CF->Name, $args{OPERATOR}, $args{VALUE} );
3153     }
3154
3155     if ( defined $args{'QUEUE'} && $args{'QUEUE'} =~ /\D/ ) {
3156         my $QueueObj = RT::Queue->new( $self->CurrentUser );
3157         $QueueObj->Load( $args{'QUEUE'} );
3158         $args{'QUEUE'} = $QueueObj->Id;
3159     }
3160     delete $args{'QUEUE'} unless defined $args{'QUEUE'} && length $args{'QUEUE'};
3161
3162     my @rest;
3163     @rest = ( ENTRYAGGREGATOR => 'AND' )
3164         if ( $CF->Type eq 'SelectMultiple' );
3165
3166     $self->Limit(
3167         VALUE => $args{VALUE},
3168         FIELD => "CF"
3169             .(defined $args{'QUEUE'}? ".$args{'QUEUE'}" : '' )
3170             .".{" . $CF->Name . "}",
3171         OPERATOR    => $args{OPERATOR},
3172         CUSTOMFIELD => 1,
3173         @rest,
3174     );
3175
3176     $self->{'RecalcTicketLimits'} = 1;
3177 }
3178
3179
3180
3181 =head2 _NextIndex
3182
3183 Keep track of the counter for the array of restrictions
3184
3185 =cut
3186
3187 sub _NextIndex {
3188     my $self = shift;
3189     return ( $self->{'restriction_index'}++ );
3190 }
3191
3192
3193
3194
3195 sub _Init {
3196     my $self = shift;
3197     $self->{'table'}                   = "Tickets";
3198     $self->{'RecalcTicketLimits'}      = 1;
3199     $self->{'looking_at_effective_id'} = 0;
3200     $self->{'looking_at_type'}         = 0;
3201     $self->{'restriction_index'}       = 1;
3202     $self->{'primary_key'}             = "id";
3203     delete $self->{'items_array'};
3204     delete $self->{'item_map'};
3205     delete $self->{'columns_to_display'};
3206     $self->SUPER::_Init(@_);
3207
3208     $self->_InitSQL;
3209
3210 }
3211
3212
3213 sub Count {
3214     my $self = shift;
3215     $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
3216     return ( $self->SUPER::Count() );
3217 }
3218
3219
3220 sub CountAll {
3221     my $self = shift;
3222     $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
3223     return ( $self->SUPER::CountAll() );
3224 }
3225
3226
3227
3228 =head2 ItemsArrayRef
3229
3230 Returns a reference to the set of all items found in this search
3231
3232 =cut
3233
3234 sub ItemsArrayRef {
3235     my $self = shift;
3236
3237     return $self->{'items_array'} if $self->{'items_array'};
3238
3239     my $placeholder = $self->_ItemsCounter;
3240     $self->GotoFirstItem();
3241     while ( my $item = $self->Next ) {
3242         push( @{ $self->{'items_array'} }, $item );
3243     }
3244     $self->GotoItem($placeholder);
3245     $self->{'items_array'}
3246         = $self->ItemsOrderBy( $self->{'items_array'} );
3247
3248     return $self->{'items_array'};
3249 }
3250
3251 sub ItemsArrayRefWindow {
3252     my $self = shift;
3253     my $window = shift;
3254
3255     my @old = ($self->_ItemsCounter, $self->RowsPerPage, $self->FirstRow+1);
3256
3257     $self->RowsPerPage( $window );
3258     $self->FirstRow(1);
3259     $self->GotoFirstItem;
3260
3261     my @res;
3262     while ( my $item = $self->Next ) {
3263         push @res, $item;
3264     }
3265
3266     $self->RowsPerPage( $old[1] );
3267     $self->FirstRow( $old[2] );
3268     $self->GotoItem( $old[0] );
3269
3270     return \@res;
3271 }
3272
3273
3274 sub Next {
3275     my $self = shift;
3276
3277     $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
3278
3279     my $Ticket = $self->SUPER::Next;
3280     return $Ticket unless $Ticket;
3281
3282     if ( $Ticket->__Value('Status') eq 'deleted'
3283         && !$self->{'allow_deleted_search'} )
3284     {
3285         return $self->Next;
3286     }
3287     elsif ( RT->Config->Get('UseSQLForACLChecks') ) {
3288         # if we found a ticket with this option enabled then
3289         # all tickets we found are ACLed, cache this fact
3290         my $key = join ";:;", $self->CurrentUser->id, 'ShowTicket', 'RT::Ticket-'. $Ticket->id;
3291         $RT::Principal::_ACL_CACHE->set( $key => 1 );
3292         return $Ticket;
3293     }
3294     elsif ( $Ticket->CurrentUserHasRight('ShowTicket') ) {
3295         # has rights
3296         return $Ticket;
3297     }
3298     else {
3299         # If the user doesn't have the right to show this ticket
3300         return $self->Next;
3301     }
3302 }
3303
3304 sub _DoSearch {
3305     my $self = shift;
3306     $self->CurrentUserCanSee if RT->Config->Get('UseSQLForACLChecks');
3307     return $self->SUPER::_DoSearch( @_ );
3308 }
3309
3310 sub _DoCount {
3311     my $self = shift;
3312     $self->CurrentUserCanSee if RT->Config->Get('UseSQLForACLChecks');
3313     return $self->SUPER::_DoCount( @_ );
3314 }
3315
3316 sub _RolesCanSee {
3317     my $self = shift;
3318
3319     my $cache_key = 'RolesHasRight;:;ShowTicket';
3320  
3321     if ( my $cached = $RT::Principal::_ACL_CACHE->fetch( $cache_key ) ) {
3322         return %$cached;
3323     }
3324
3325     my $ACL = RT::ACL->new( RT->SystemUser );
3326     $ACL->Limit( FIELD => 'RightName', VALUE => 'ShowTicket' );
3327     $ACL->Limit( FIELD => 'PrincipalType', OPERATOR => '!=', VALUE => 'Group' );
3328     my $principal_alias = $ACL->Join(
3329         ALIAS1 => 'main',
3330         FIELD1 => 'PrincipalId',
3331         TABLE2 => 'Principals',
3332         FIELD2 => 'id',
3333     );
3334     $ACL->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3335
3336     my %res = ();
3337     foreach my $ACE ( @{ $ACL->ItemsArrayRef } ) {
3338         my $role = $ACE->__Value('PrincipalType');
3339         my $type = $ACE->__Value('ObjectType');
3340         if ( $type eq 'RT::System' ) {
3341             $res{ $role } = 1;
3342         }
3343         elsif ( $type eq 'RT::Queue' ) {
3344             next if $res{ $role } && !ref $res{ $role };
3345             push @{ $res{ $role } ||= [] }, $ACE->__Value('ObjectId');
3346         }
3347         else {
3348             $RT::Logger->error('ShowTicket right is granted on unsupported object');
3349         }
3350     }
3351     $RT::Principal::_ACL_CACHE->set( $cache_key => \%res );
3352     return %res;
3353 }
3354
3355 sub _DirectlyCanSeeIn {
3356     my $self = shift;
3357     my $id = $self->CurrentUser->id;
3358
3359     my $cache_key = 'User-'. $id .';:;ShowTicket;:;DirectlyCanSeeIn';
3360     if ( my $cached = $RT::Principal::_ACL_CACHE->fetch( $cache_key ) ) {
3361         return @$cached;
3362     }
3363
3364     my $ACL = RT::ACL->new( RT->SystemUser );
3365     $ACL->Limit( FIELD => 'RightName', VALUE => 'ShowTicket' );
3366     my $principal_alias = $ACL->Join(
3367         ALIAS1 => 'main',
3368         FIELD1 => 'PrincipalId',
3369         TABLE2 => 'Principals',
3370         FIELD2 => 'id',
3371     );
3372     $ACL->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3373     my $cgm_alias = $ACL->Join(
3374         ALIAS1 => 'main',
3375         FIELD1 => 'PrincipalId',
3376         TABLE2 => 'CachedGroupMembers',
3377         FIELD2 => 'GroupId',
3378     );
3379     $ACL->Limit( ALIAS => $cgm_alias, FIELD => 'MemberId', VALUE => $id );
3380     $ACL->Limit( ALIAS => $cgm_alias, FIELD => 'Disabled', VALUE => 0 );
3381
3382     my @res = ();
3383     foreach my $ACE ( @{ $ACL->ItemsArrayRef } ) {
3384         my $type = $ACE->__Value('ObjectType');
3385         if ( $type eq 'RT::System' ) {
3386             # If user is direct member of a group that has the right
3387             # on the system then he can see any ticket
3388             $RT::Principal::_ACL_CACHE->set( $cache_key => [-1] );
3389             return (-1);
3390         }
3391         elsif ( $type eq 'RT::Queue' ) {
3392             push @res, $ACE->__Value('ObjectId');
3393         }
3394         else {
3395             $RT::Logger->error('ShowTicket right is granted on unsupported object');
3396         }
3397     }
3398     $RT::Principal::_ACL_CACHE->set( $cache_key => \@res );
3399     return @res;
3400 }
3401
3402 sub CurrentUserCanSee {
3403     my $self = shift;
3404     return if $self->{'_sql_current_user_can_see_applied'};
3405
3406     return $self->{'_sql_current_user_can_see_applied'} = 1
3407         if $self->CurrentUser->UserObj->HasRight(
3408             Right => 'SuperUser', Object => $RT::System
3409         );
3410
3411     my $id = $self->CurrentUser->id;
3412
3413     # directly can see in all queues then we have nothing to do
3414     my @direct_queues = $self->_DirectlyCanSeeIn;
3415     return $self->{'_sql_current_user_can_see_applied'} = 1
3416         if @direct_queues && $direct_queues[0] == -1;
3417
3418     my %roles = $self->_RolesCanSee;
3419     {
3420         my %skip = map { $_ => 1 } @direct_queues;
3421         foreach my $role ( keys %roles ) {
3422             next unless ref $roles{ $role };
3423
3424             my @queues = grep !$skip{$_}, @{ $roles{ $role } };
3425             if ( @queues ) {
3426                 $roles{ $role } = \@queues;
3427             } else {
3428                 delete $roles{ $role };
3429             }
3430         }
3431     }
3432
3433 # there is no global watchers, only queues and tickes, if at
3434 # some point we will add global roles then it's gonna blow
3435 # the idea here is that if the right is set globaly for a role
3436 # and user plays this role for a queue directly not a ticket
3437 # then we have to check in advance
3438     if ( my @tmp = grep $_ ne 'Owner' && !ref $roles{ $_ }, keys %roles ) {
3439
3440         my $groups = RT::Groups->new( RT->SystemUser );
3441         $groups->Limit( FIELD => 'Domain', VALUE => 'RT::Queue-Role' );
3442         foreach ( @tmp ) {
3443             $groups->Limit( FIELD => 'Type', VALUE => $_ );
3444         }
3445         my $principal_alias = $groups->Join(
3446             ALIAS1 => 'main',
3447             FIELD1 => 'id',
3448             TABLE2 => 'Principals',
3449             FIELD2 => 'id',
3450         );
3451         $groups->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3452         my $cgm_alias = $groups->Join(
3453             ALIAS1 => 'main',
3454             FIELD1 => 'id',
3455             TABLE2 => 'CachedGroupMembers',
3456             FIELD2 => 'GroupId',
3457         );
3458         $groups->Limit( ALIAS => $cgm_alias, FIELD => 'MemberId', VALUE => $id );
3459         $groups->Limit( ALIAS => $cgm_alias, FIELD => 'Disabled', VALUE => 0 );
3460         while ( my $group = $groups->Next ) {
3461             push @direct_queues, $group->Instance;
3462         }
3463     }
3464
3465     unless ( @direct_queues || keys %roles ) {
3466         $self->SUPER::Limit(
3467             SUBCLAUSE => 'ACL',
3468             ALIAS => 'main',
3469             FIELD => 'id',
3470             VALUE => 0,
3471             ENTRYAGGREGATOR => 'AND',
3472         );
3473         return $self->{'_sql_current_user_can_see_applied'} = 1;
3474     }
3475
3476     {
3477         my $join_roles = keys %roles;
3478         $join_roles = 0 if $join_roles == 1 && $roles{'Owner'};
3479         my ($role_group_alias, $cgm_alias);
3480         if ( $join_roles ) {
3481             $role_group_alias = $self->_RoleGroupsJoin( New => 1 );
3482             $cgm_alias = $self->_GroupMembersJoin( GroupsAlias => $role_group_alias );
3483             $self->SUPER::Limit(
3484                 LEFTJOIN   => $cgm_alias,
3485                 FIELD      => 'MemberId',
3486                 OPERATOR   => '=',
3487                 VALUE      => $id,
3488             );
3489         }
3490         my $limit_queues = sub {
3491             my $ea = shift;
3492             my @queues = @_;
3493
3494             return unless @queues;
3495             if ( @queues == 1 ) {
3496                 $self->SUPER::Limit(
3497                     SUBCLAUSE => 'ACL',
3498                     ALIAS => 'main',
3499                     FIELD => 'Queue',
3500                     VALUE => $_[0],
3501                     ENTRYAGGREGATOR => $ea,
3502                 );
3503             } else {
3504                 $self->SUPER::_OpenParen('ACL');
3505                 foreach my $q ( @queues ) {
3506                     $self->SUPER::Limit(
3507                         SUBCLAUSE => 'ACL',
3508                         ALIAS => 'main',
3509                         FIELD => 'Queue',
3510                         VALUE => $q,
3511                         ENTRYAGGREGATOR => $ea,
3512                     );
3513                     $ea = 'OR';
3514                 }
3515                 $self->SUPER::_CloseParen('ACL');
3516             }
3517             return 1;
3518         };
3519
3520         $self->SUPER::_OpenParen('ACL');
3521         my $ea = 'AND';
3522         $ea = 'OR' if $limit_queues->( $ea, @direct_queues );
3523         while ( my ($role, $queues) = each %roles ) {
3524             $self->SUPER::_OpenParen('ACL');
3525             if ( $role eq 'Owner' ) {
3526                 $self->SUPER::Limit(
3527                     SUBCLAUSE => 'ACL',
3528                     FIELD           => 'Owner',
3529                     VALUE           => $id,
3530                     ENTRYAGGREGATOR => $ea,
3531                 );
3532             }
3533             else {
3534                 $self->SUPER::Limit(
3535                     SUBCLAUSE       => 'ACL',
3536                     ALIAS           => $cgm_alias,
3537                     FIELD           => 'MemberId',
3538                     OPERATOR        => 'IS NOT',
3539                     VALUE           => 'NULL',
3540                     QUOTEVALUE      => 0,
3541                     ENTRYAGGREGATOR => $ea,
3542                 );
3543                 $self->SUPER::Limit(
3544                     SUBCLAUSE       => 'ACL',
3545                     ALIAS           => $role_group_alias,
3546                     FIELD           => 'Type',
3547                     VALUE           => $role,
3548                     ENTRYAGGREGATOR => 'AND',
3549                 );
3550             }
3551             $limit_queues->( 'AND', @$queues ) if ref $queues;
3552             $ea = 'OR' if $ea eq 'AND';
3553             $self->SUPER::_CloseParen('ACL');
3554         }
3555         $self->SUPER::_CloseParen('ACL');
3556     }
3557     return $self->{'_sql_current_user_can_see_applied'} = 1;
3558 }
3559
3560
3561
3562
3563
3564 =head2 LoadRestrictions
3565
3566 LoadRestrictions takes a string which can fully populate the TicketRestrictons hash.
3567 TODO It is not yet implemented
3568
3569 =cut
3570
3571
3572
3573 =head2 DescribeRestrictions
3574
3575 takes nothing.
3576 Returns a hash keyed by restriction id.
3577 Each element of the hash is currently a one element hash that contains DESCRIPTION which
3578 is a description of the purpose of that TicketRestriction
3579
3580 =cut
3581
3582 sub DescribeRestrictions {
3583     my $self = shift;
3584
3585     my %listing;
3586
3587     foreach my $row ( keys %{ $self->{'TicketRestrictions'} } ) {
3588         $listing{$row} = $self->{'TicketRestrictions'}{$row}{'DESCRIPTION'};
3589     }
3590     return (%listing);
3591 }
3592
3593
3594
3595 =head2 RestrictionValues FIELD
3596
3597 Takes a restriction field and returns a list of values this field is restricted
3598 to.
3599
3600 =cut
3601
3602 sub RestrictionValues {
3603     my $self  = shift;
3604     my $field = shift;
3605     map $self->{'TicketRestrictions'}{$_}{'VALUE'}, grep {
3606                $self->{'TicketRestrictions'}{$_}{'FIELD'}    eq $field
3607             && $self->{'TicketRestrictions'}{$_}{'OPERATOR'} eq "="
3608         }
3609         keys %{ $self->{'TicketRestrictions'} };
3610 }
3611
3612
3613
3614 =head2 ClearRestrictions
3615
3616 Removes all restrictions irretrievably
3617
3618 =cut
3619
3620 sub ClearRestrictions {
3621     my $self = shift;
3622     delete $self->{'TicketRestrictions'};
3623     $self->{'looking_at_effective_id'} = 0;
3624     $self->{'looking_at_type'}         = 0;
3625     $self->{'RecalcTicketLimits'}      = 1;
3626 }
3627
3628
3629
3630 =head2 DeleteRestriction
3631
3632 Takes the row Id of a restriction (From DescribeRestrictions' output, for example.
3633 Removes that restriction from the session's limits.
3634
3635 =cut
3636
3637 sub DeleteRestriction {
3638     my $self = shift;
3639     my $row  = shift;
3640     delete $self->{'TicketRestrictions'}{$row};
3641
3642     $self->{'RecalcTicketLimits'} = 1;
3643
3644     #make the underlying easysearch object forget all its preconceptions
3645 }
3646
3647
3648
3649 # Convert a set of oldstyle SB Restrictions to Clauses for RQL
3650
3651 sub _RestrictionsToClauses {
3652     my $self = shift;
3653
3654     my %clause;
3655     foreach my $row ( keys %{ $self->{'TicketRestrictions'} } ) {
3656         my $restriction = $self->{'TicketRestrictions'}{$row};
3657
3658         # We need to reimplement the subclause aggregation that SearchBuilder does.
3659         # Default Subclause is ALIAS.FIELD, and default ALIAS is 'main',
3660         # Then SB AND's the different Subclauses together.
3661
3662         # So, we want to group things into Subclauses, convert them to
3663         # SQL, and then join them with the appropriate DefaultEA.
3664         # Then join each subclause group with AND.
3665
3666         my $field = $restriction->{'FIELD'};
3667         my $realfield = $field;    # CustomFields fake up a fieldname, so
3668                                    # we need to figure that out
3669
3670         # One special case
3671         # Rewrite LinkedTo meta field to the real field
3672         if ( $field =~ /LinkedTo/ ) {
3673             $realfield = $field = $restriction->{'TYPE'};
3674         }
3675
3676         # Two special case
3677         # Handle subkey fields with a different real field
3678         if ( $field =~ /^(\w+)\./ ) {
3679             $realfield = $1;
3680         }
3681
3682         die "I don't know about $field yet"
3683             unless ( exists $FIELD_METADATA{$realfield}
3684                 or $restriction->{CUSTOMFIELD} );
3685
3686         my $type = $FIELD_METADATA{$realfield}->[0];
3687         my $op   = $restriction->{'OPERATOR'};
3688
3689         my $value = (
3690             grep    {defined}
3691                 map { $restriction->{$_} } qw(VALUE TICKET BASE TARGET)
3692         )[0];
3693
3694         # this performs the moral equivalent of defined or/dor/C<//>,
3695         # without the short circuiting.You need to use a 'defined or'
3696         # type thing instead of just checking for truth values, because
3697         # VALUE could be 0.(i.e. "false")
3698
3699         # You could also use this, but I find it less aesthetic:
3700         # (although it does short circuit)
3701         #( defined $restriction->{'VALUE'}? $restriction->{VALUE} :
3702         # defined $restriction->{'TICKET'} ?
3703         # $restriction->{TICKET} :
3704         # defined $restriction->{'BASE'} ?
3705         # $restriction->{BASE} :
3706         # defined $restriction->{'TARGET'} ?
3707         # $restriction->{TARGET} )
3708
3709         my $ea = $restriction->{ENTRYAGGREGATOR}
3710             || $DefaultEA{$type}
3711             || "AND";
3712         if ( ref $ea ) {
3713             die "Invalid operator $op for $field ($type)"
3714                 unless exists $ea->{$op};
3715             $ea = $ea->{$op};
3716         }
3717
3718         # Each CustomField should be put into a different Clause so they
3719         # are ANDed together.
3720         if ( $restriction->{CUSTOMFIELD} ) {
3721             $realfield = $field;
3722         }
3723
3724         exists $clause{$realfield} or $clause{$realfield} = [];
3725
3726         # Escape Quotes
3727         $field =~ s!(['\\])!\\$1!g;
3728         $value =~ s!(['\\])!\\$1!g;
3729         my $data = [ $ea, $type, $field, $op, $value ];
3730
3731         # here is where we store extra data, say if it's a keyword or
3732         # something.  (I.e. "TYPE SPECIFIC STUFF")
3733
3734         if (lc $ea eq 'none') {
3735             $clause{$realfield} = [ $data ];
3736         } else {
3737             push @{ $clause{$realfield} }, $data;
3738         }
3739     }
3740     return \%clause;
3741 }
3742
3743
3744
3745 =head2 _ProcessRestrictions PARAMHASH
3746
3747 # The new _ProcessRestrictions is somewhat dependent on the SQL stuff,
3748 # but isn't quite generic enough to move into Tickets_SQL.
3749
3750 =cut
3751
3752 sub _ProcessRestrictions {
3753     my $self = shift;
3754
3755     #Blow away ticket aliases since we'll need to regenerate them for
3756     #a new search
3757     delete $self->{'TicketAliases'};
3758     delete $self->{'items_array'};
3759     delete $self->{'item_map'};
3760     delete $self->{'raw_rows'};
3761     delete $self->{'rows'};
3762     delete $self->{'count_all'};
3763
3764     my $sql = $self->Query;    # Violating the _SQL namespace
3765     if ( !$sql || $self->{'RecalcTicketLimits'} ) {
3766
3767         #  "Restrictions to Clauses Branch\n";
3768         my $clauseRef = eval { $self->_RestrictionsToClauses; };
3769         if ($@) {
3770             $RT::Logger->error( "RestrictionsToClauses: " . $@ );
3771             $self->FromSQL("");
3772         }
3773         else {
3774             $sql = $self->ClausesToSQL($clauseRef);
3775             $self->FromSQL($sql) if $sql;
3776         }
3777     }
3778
3779     $self->{'RecalcTicketLimits'} = 0;
3780
3781 }
3782
3783 =head2 _BuildItemMap
3784
3785 Build up a L</ItemMap> of first/last/next/prev items, so that we can
3786 display search nav quickly.
3787
3788 =cut
3789
3790 sub _BuildItemMap {
3791     my $self = shift;
3792
3793     my $window = RT->Config->Get('TicketsItemMapSize');
3794
3795     $self->{'item_map'} = {};
3796
3797     my $items = $self->ItemsArrayRefWindow( $window );
3798     return unless $items && @$items;
3799
3800     my $prev = 0;
3801     $self->{'item_map'}{'first'} = $items->[0]->EffectiveId;
3802     for ( my $i = 0; $i < @$items; $i++ ) {
3803         my $item = $items->[$i];
3804         my $id = $item->EffectiveId;
3805         $self->{'item_map'}{$id}{'defined'} = 1;
3806         $self->{'item_map'}{$id}{'prev'}    = $prev;
3807         $self->{'item_map'}{$id}{'next'}    = $items->[$i+1]->EffectiveId
3808             if $items->[$i+1];
3809         $prev = $id;
3810     }
3811     $self->{'item_map'}{'last'} = $prev
3812         if !$window || @$items < $window;
3813 }
3814
3815 =head2 ItemMap
3816
3817 Returns an a map of all items found by this search. The map is a hash
3818 of the form:
3819
3820     {
3821         first => <first ticket id found>,
3822         last => <last ticket id found or undef>,
3823
3824         <ticket id> => {
3825             prev => <the ticket id found before>,
3826             next => <the ticket id found after>,
3827         },
3828         <ticket id> => {
3829             prev => ...,
3830             next => ...,
3831         },
3832     }
3833
3834 =cut
3835
3836 sub ItemMap {
3837     my $self = shift;
3838     $self->_BuildItemMap unless $self->{'item_map'};
3839     return $self->{'item_map'};
3840 }
3841
3842
3843
3844
3845 =head2 PrepForSerialization
3846
3847 You don't want to serialize a big tickets object, as
3848 the {items} hash will be instantly invalid _and_ eat
3849 lots of space
3850
3851 =cut
3852
3853 sub PrepForSerialization {
3854     my $self = shift;
3855     delete $self->{'items'};
3856     delete $self->{'items_array'};
3857     $self->RedoSearch();
3858 }
3859
3860 =head1 FLAGS
3861
3862 RT::Tickets supports several flags which alter search behavior:
3863
3864
3865 allow_deleted_search  (Otherwise never show deleted tickets in search results)
3866 looking_at_type (otherwise limit to type=ticket)
3867
3868 These flags are set by calling 
3869
3870 $tickets->{'flagname'} = 1;
3871
3872 BUG: There should be an API for this
3873
3874
3875
3876 =cut
3877
3878
3879
3880 =head2 NewItem
3881
3882 Returns an empty new RT::Ticket item
3883
3884 =cut
3885
3886 sub NewItem {
3887     my $self = shift;
3888     return(RT::Ticket->new($self->CurrentUser));
3889 }
3890 RT::Base->_ImportOverlays();
3891
3892 1;