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