rt 4.0.23
[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 @_SQLLimit = ();
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       #$self->_SQLLimit(
2311       push @_SQLLimit, {
2312           %rest,
2313           ALIAS           => $a,
2314           FIELD           => $pkey,
2315           OPERATOR        => $is_negative ? 'IS' : 'IS NOT',
2316           VALUE           => 'NULL',
2317           QUOTEVALUE      => 0,
2318           ENTRYAGGREGATOR => $is_negative ? 'AND' : 'OR',
2319           SUBCLAUSE       => 'fs_limit',
2320       };
2321     }
2322
2323     $self->_OpenParen;
2324     foreach my $_SQLLimit (@_SQLLimit) {
2325       $self->_SQLLimit( %$_SQLLimit);
2326     }
2327     $self->_CloseParen;
2328
2329 }
2330
2331 #Freeside
2332
2333 =head2 Limit
2334
2335 Takes a paramhash with the fields FIELD, OPERATOR, VALUE and DESCRIPTION
2336 Generally best called from LimitFoo methods
2337
2338 =cut
2339
2340 sub Limit {
2341     my $self = shift;
2342     my %args = (
2343         FIELD       => undef,
2344         OPERATOR    => '=',
2345         VALUE       => undef,
2346         DESCRIPTION => undef,
2347         @_
2348     );
2349     $args{'DESCRIPTION'} = $self->loc(
2350         "[_1] [_2] [_3]",  $args{'FIELD'},
2351         $args{'OPERATOR'}, $args{'VALUE'}
2352         )
2353         if ( !defined $args{'DESCRIPTION'} );
2354
2355     my $index = $self->_NextIndex;
2356
2357 # make the TicketRestrictions hash the equivalent of whatever we just passed in;
2358
2359     %{ $self->{'TicketRestrictions'}{$index} } = %args;
2360
2361     $self->{'RecalcTicketLimits'} = 1;
2362
2363 # If we're looking at the effective id, we don't want to append the other clause
2364 # which limits us to tickets where id = effective id
2365     if ( $args{'FIELD'} eq 'EffectiveId'
2366         && ( !$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) )
2367     {
2368         $self->{'looking_at_effective_id'} = 1;
2369     }
2370
2371     if ( $args{'FIELD'} eq 'Type'
2372         && ( !$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) )
2373     {
2374         $self->{'looking_at_type'} = 1;
2375     }
2376
2377     return ($index);
2378 }
2379
2380
2381
2382
2383 =head2 LimitQueue
2384
2385 LimitQueue takes a paramhash with the fields OPERATOR and VALUE.
2386 OPERATOR is one of = or !=. (It defaults to =).
2387 VALUE is a queue id or Name.
2388
2389
2390 =cut
2391
2392 sub LimitQueue {
2393     my $self = shift;
2394     my %args = (
2395         VALUE    => undef,
2396         OPERATOR => '=',
2397         @_
2398     );
2399
2400     #TODO  VALUE should also take queue objects
2401     if ( defined $args{'VALUE'} && $args{'VALUE'} !~ /^\d+$/ ) {
2402         my $queue = RT::Queue->new( $self->CurrentUser );
2403         $queue->Load( $args{'VALUE'} );
2404         $args{'VALUE'} = $queue->Id;
2405     }
2406
2407     # What if they pass in an Id?  Check for isNum() and convert to
2408     # string.
2409
2410     #TODO check for a valid queue here
2411
2412     $self->Limit(
2413         FIELD       => 'Queue',
2414         VALUE       => $args{'VALUE'},
2415         OPERATOR    => $args{'OPERATOR'},
2416         DESCRIPTION => join(
2417             ' ', $self->loc('Queue'), $args{'OPERATOR'}, $args{'VALUE'},
2418         ),
2419     );
2420
2421 }
2422
2423
2424
2425 =head2 LimitStatus
2426
2427 Takes a paramhash with the fields OPERATOR and VALUE.
2428 OPERATOR is one of = or !=.
2429 VALUE is a status.
2430
2431 RT adds Status != 'deleted' until object has
2432 allow_deleted_search internal property set.
2433 $tickets->{'allow_deleted_search'} = 1;
2434 $tickets->LimitStatus( VALUE => 'deleted' );
2435
2436 =cut
2437
2438 sub LimitStatus {
2439     my $self = shift;
2440     my %args = (
2441         OPERATOR => '=',
2442         @_
2443     );
2444     $self->Limit(
2445         FIELD       => 'Status',
2446         VALUE       => $args{'VALUE'},
2447         OPERATOR    => $args{'OPERATOR'},
2448         DESCRIPTION => join( ' ',
2449             $self->loc('Status'), $args{'OPERATOR'},
2450             $self->loc( $args{'VALUE'} ) ),
2451     );
2452 }
2453
2454
2455
2456 =head2 IgnoreType
2457
2458 If called, this search will not automatically limit the set of results found
2459 to tickets of type "Ticket". Tickets of other types, such as "project" and
2460 "approval" will be found.
2461
2462 =cut
2463
2464 sub IgnoreType {
2465     my $self = shift;
2466
2467     # Instead of faking a Limit that later gets ignored, fake up the
2468     # fact that we're already looking at type, so that the check in
2469     # Tickets_SQL/FromSQL goes down the right branch
2470
2471     #  $self->LimitType(VALUE => '__any');
2472     $self->{looking_at_type} = 1;
2473 }
2474
2475
2476
2477 =head2 LimitType
2478
2479 Takes a paramhash with the fields OPERATOR and VALUE.
2480 OPERATOR is one of = or !=, it defaults to "=".
2481 VALUE is a string to search for in the type of the ticket.
2482
2483
2484
2485 =cut
2486
2487 sub LimitType {
2488     my $self = shift;
2489     my %args = (
2490         OPERATOR => '=',
2491         VALUE    => undef,
2492         @_
2493     );
2494     $self->Limit(
2495         FIELD       => 'Type',
2496         VALUE       => $args{'VALUE'},
2497         OPERATOR    => $args{'OPERATOR'},
2498         DESCRIPTION => join( ' ',
2499             $self->loc('Type'), $args{'OPERATOR'}, $args{'VALUE'}, ),
2500     );
2501 }
2502
2503
2504
2505
2506
2507 =head2 LimitSubject
2508
2509 Takes a paramhash with the fields OPERATOR and VALUE.
2510 OPERATOR is one of = or !=.
2511 VALUE is a string to search for in the subject of the ticket.
2512
2513 =cut
2514
2515 sub LimitSubject {
2516     my $self = shift;
2517     my %args = (@_);
2518     $self->Limit(
2519         FIELD       => 'Subject',
2520         VALUE       => $args{'VALUE'},
2521         OPERATOR    => $args{'OPERATOR'},
2522         DESCRIPTION => join( ' ',
2523             $self->loc('Subject'), $args{'OPERATOR'}, $args{'VALUE'}, ),
2524     );
2525 }
2526
2527
2528
2529 # Things that can be > < = !=
2530
2531
2532 =head2 LimitId
2533
2534 Takes a paramhash with the fields OPERATOR and VALUE.
2535 OPERATOR is one of =, >, < or !=.
2536 VALUE is a ticket Id to search for
2537
2538 =cut
2539
2540 sub LimitId {
2541     my $self = shift;
2542     my %args = (
2543         OPERATOR => '=',
2544         @_
2545     );
2546
2547     $self->Limit(
2548         FIELD       => 'id',
2549         VALUE       => $args{'VALUE'},
2550         OPERATOR    => $args{'OPERATOR'},
2551         DESCRIPTION =>
2552             join( ' ', $self->loc('Id'), $args{'OPERATOR'}, $args{'VALUE'}, ),
2553     );
2554 }
2555
2556
2557
2558 =head2 LimitPriority
2559
2560 Takes a paramhash with the fields OPERATOR and VALUE.
2561 OPERATOR is one of =, >, < or !=.
2562 VALUE is a value to match the ticket's priority against
2563
2564 =cut
2565
2566 sub LimitPriority {
2567     my $self = shift;
2568     my %args = (@_);
2569     $self->Limit(
2570         FIELD       => 'Priority',
2571         VALUE       => $args{'VALUE'},
2572         OPERATOR    => $args{'OPERATOR'},
2573         DESCRIPTION => join( ' ',
2574             $self->loc('Priority'),
2575             $args{'OPERATOR'}, $args{'VALUE'}, ),
2576     );
2577 }
2578
2579
2580
2581 =head2 LimitInitialPriority
2582
2583 Takes a paramhash with the fields OPERATOR and VALUE.
2584 OPERATOR is one of =, >, < or !=.
2585 VALUE is a value to match the ticket's initial priority against
2586
2587
2588 =cut
2589
2590 sub LimitInitialPriority {
2591     my $self = shift;
2592     my %args = (@_);
2593     $self->Limit(
2594         FIELD       => 'InitialPriority',
2595         VALUE       => $args{'VALUE'},
2596         OPERATOR    => $args{'OPERATOR'},
2597         DESCRIPTION => join( ' ',
2598             $self->loc('Initial Priority'), $args{'OPERATOR'},
2599             $args{'VALUE'}, ),
2600     );
2601 }
2602
2603
2604
2605 =head2 LimitFinalPriority
2606
2607 Takes a paramhash with the fields OPERATOR and VALUE.
2608 OPERATOR is one of =, >, < or !=.
2609 VALUE is a value to match the ticket's final priority against
2610
2611 =cut
2612
2613 sub LimitFinalPriority {
2614     my $self = shift;
2615     my %args = (@_);
2616     $self->Limit(
2617         FIELD       => 'FinalPriority',
2618         VALUE       => $args{'VALUE'},
2619         OPERATOR    => $args{'OPERATOR'},
2620         DESCRIPTION => join( ' ',
2621             $self->loc('Final Priority'), $args{'OPERATOR'},
2622             $args{'VALUE'}, ),
2623     );
2624 }
2625
2626
2627
2628 =head2 LimitTimeWorked
2629
2630 Takes a paramhash with the fields OPERATOR and VALUE.
2631 OPERATOR is one of =, >, < or !=.
2632 VALUE is a value to match the ticket's TimeWorked attribute
2633
2634 =cut
2635
2636 sub LimitTimeWorked {
2637     my $self = shift;
2638     my %args = (@_);
2639     $self->Limit(
2640         FIELD       => 'TimeWorked',
2641         VALUE       => $args{'VALUE'},
2642         OPERATOR    => $args{'OPERATOR'},
2643         DESCRIPTION => join( ' ',
2644             $self->loc('Time Worked'),
2645             $args{'OPERATOR'}, $args{'VALUE'}, ),
2646     );
2647 }
2648
2649
2650
2651 =head2 LimitTimeLeft
2652
2653 Takes a paramhash with the fields OPERATOR and VALUE.
2654 OPERATOR is one of =, >, < or !=.
2655 VALUE is a value to match the ticket's TimeLeft attribute
2656
2657 =cut
2658
2659 sub LimitTimeLeft {
2660     my $self = shift;
2661     my %args = (@_);
2662     $self->Limit(
2663         FIELD       => 'TimeLeft',
2664         VALUE       => $args{'VALUE'},
2665         OPERATOR    => $args{'OPERATOR'},
2666         DESCRIPTION => join( ' ',
2667             $self->loc('Time Left'),
2668             $args{'OPERATOR'}, $args{'VALUE'}, ),
2669     );
2670 }
2671
2672
2673
2674
2675
2676 =head2 LimitContent
2677
2678 Takes a paramhash with the fields OPERATOR and VALUE.
2679 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2680 VALUE is a string to search for in the body of the ticket
2681
2682 =cut
2683
2684 sub LimitContent {
2685     my $self = shift;
2686     my %args = (@_);
2687     $self->Limit(
2688         FIELD       => 'Content',
2689         VALUE       => $args{'VALUE'},
2690         OPERATOR    => $args{'OPERATOR'},
2691         DESCRIPTION => join( ' ',
2692             $self->loc('Ticket content'), $args{'OPERATOR'},
2693             $args{'VALUE'}, ),
2694     );
2695 }
2696
2697
2698
2699 =head2 LimitFilename
2700
2701 Takes a paramhash with the fields OPERATOR and VALUE.
2702 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2703 VALUE is a string to search for in the body of the ticket
2704
2705 =cut
2706
2707 sub LimitFilename {
2708     my $self = shift;
2709     my %args = (@_);
2710     $self->Limit(
2711         FIELD       => 'Filename',
2712         VALUE       => $args{'VALUE'},
2713         OPERATOR    => $args{'OPERATOR'},
2714         DESCRIPTION => join( ' ',
2715             $self->loc('Attachment filename'), $args{'OPERATOR'},
2716             $args{'VALUE'}, ),
2717     );
2718 }
2719
2720
2721 =head2 LimitContentType
2722
2723 Takes a paramhash with the fields OPERATOR and VALUE.
2724 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2725 VALUE is a content type to search ticket attachments for
2726
2727 =cut
2728
2729 sub LimitContentType {
2730     my $self = shift;
2731     my %args = (@_);
2732     $self->Limit(
2733         FIELD       => 'ContentType',
2734         VALUE       => $args{'VALUE'},
2735         OPERATOR    => $args{'OPERATOR'},
2736         DESCRIPTION => join( ' ',
2737             $self->loc('Ticket content type'), $args{'OPERATOR'},
2738             $args{'VALUE'}, ),
2739     );
2740 }
2741
2742
2743
2744
2745
2746 =head2 LimitOwner
2747
2748 Takes a paramhash with the fields OPERATOR and VALUE.
2749 OPERATOR is one of = or !=.
2750 VALUE is a user id.
2751
2752 =cut
2753
2754 sub LimitOwner {
2755     my $self = shift;
2756     my %args = (
2757         OPERATOR => '=',
2758         @_
2759     );
2760
2761     my $owner = RT::User->new( $self->CurrentUser );
2762     $owner->Load( $args{'VALUE'} );
2763
2764     # FIXME: check for a valid $owner
2765     $self->Limit(
2766         FIELD       => 'Owner',
2767         VALUE       => $args{'VALUE'},
2768         OPERATOR    => $args{'OPERATOR'},
2769         DESCRIPTION => join( ' ',
2770             $self->loc('Owner'), $args{'OPERATOR'}, $owner->Name(), ),
2771     );
2772
2773 }
2774
2775
2776
2777
2778 =head2 LimitWatcher
2779
2780   Takes a paramhash with the fields OPERATOR, TYPE and VALUE.
2781   OPERATOR is one of =, LIKE, NOT LIKE or !=.
2782   VALUE is a value to match the ticket's watcher email addresses against
2783   TYPE is the sort of watchers you want to match against. Leave it undef if you want to search all of them
2784
2785
2786 =cut
2787
2788 sub LimitWatcher {
2789     my $self = shift;
2790     my %args = (
2791         OPERATOR => '=',
2792         VALUE    => undef,
2793         TYPE     => undef,
2794         @_
2795     );
2796
2797     #build us up a description
2798     my ( $watcher_type, $desc );
2799     if ( $args{'TYPE'} ) {
2800         $watcher_type = $args{'TYPE'};
2801     }
2802     else {
2803         $watcher_type = "Watcher";
2804     }
2805
2806     $self->Limit(
2807         FIELD       => $watcher_type,
2808         VALUE       => $args{'VALUE'},
2809         OPERATOR    => $args{'OPERATOR'},
2810         TYPE        => $args{'TYPE'},
2811         DESCRIPTION => join( ' ',
2812             $self->loc($watcher_type),
2813             $args{'OPERATOR'}, $args{'VALUE'}, ),
2814     );
2815 }
2816
2817
2818
2819
2820
2821
2822 =head2 LimitLinkedTo
2823
2824 LimitLinkedTo takes a paramhash with two fields: TYPE and TARGET
2825 TYPE limits the sort of link we want to search on
2826
2827 TYPE = { RefersTo, MemberOf, DependsOn }
2828
2829 TARGET is the id or URI of the TARGET of the link
2830
2831 =cut
2832
2833 sub LimitLinkedTo {
2834     my $self = shift;
2835     my %args = (
2836         TARGET   => undef,
2837         TYPE     => undef,
2838         OPERATOR => '=',
2839         @_
2840     );
2841
2842     $self->Limit(
2843         FIELD       => 'LinkedTo',
2844         BASE        => undef,
2845         TARGET      => $args{'TARGET'},
2846         TYPE        => $args{'TYPE'},
2847         DESCRIPTION => $self->loc(
2848             "Tickets [_1] by [_2]",
2849             $self->loc( $args{'TYPE'} ),
2850             $args{'TARGET'}
2851         ),
2852         OPERATOR    => $args{'OPERATOR'},
2853     );
2854 }
2855
2856
2857
2858 =head2 LimitLinkedFrom
2859
2860 LimitLinkedFrom takes a paramhash with two fields: TYPE and BASE
2861 TYPE limits the sort of link we want to search on
2862
2863
2864 BASE is the id or URI of the BASE of the link
2865
2866 =cut
2867
2868 sub LimitLinkedFrom {
2869     my $self = shift;
2870     my %args = (
2871         BASE     => undef,
2872         TYPE     => undef,
2873         OPERATOR => '=',
2874         @_
2875     );
2876
2877     # translate RT2 From/To naming to RT3 TicketSQL naming
2878     my %fromToMap = qw(DependsOn DependentOn
2879         MemberOf  HasMember
2880         RefersTo  ReferredToBy);
2881
2882     my $type = $args{'TYPE'};
2883     $type = $fromToMap{$type} if exists( $fromToMap{$type} );
2884
2885     $self->Limit(
2886         FIELD       => 'LinkedTo',
2887         TARGET      => undef,
2888         BASE        => $args{'BASE'},
2889         TYPE        => $type,
2890         DESCRIPTION => $self->loc(
2891             "Tickets [_1] [_2]",
2892             $self->loc( $args{'TYPE'} ),
2893             $args{'BASE'},
2894         ),
2895         OPERATOR    => $args{'OPERATOR'},
2896     );
2897 }
2898
2899
2900 sub LimitMemberOf {
2901     my $self      = shift;
2902     my $ticket_id = shift;
2903     return $self->LimitLinkedTo(
2904         @_,
2905         TARGET => $ticket_id,
2906         TYPE   => 'MemberOf',
2907     );
2908 }
2909
2910
2911 sub LimitHasMember {
2912     my $self      = shift;
2913     my $ticket_id = shift;
2914     return $self->LimitLinkedFrom(
2915         @_,
2916         BASE => "$ticket_id",
2917         TYPE => 'HasMember',
2918     );
2919
2920 }
2921
2922
2923
2924 sub LimitDependsOn {
2925     my $self      = shift;
2926     my $ticket_id = shift;
2927     return $self->LimitLinkedTo(
2928         @_,
2929         TARGET => $ticket_id,
2930         TYPE   => 'DependsOn',
2931     );
2932
2933 }
2934
2935
2936
2937 sub LimitDependedOnBy {
2938     my $self      = shift;
2939     my $ticket_id = shift;
2940     return $self->LimitLinkedFrom(
2941         @_,
2942         BASE => $ticket_id,
2943         TYPE => 'DependentOn',
2944     );
2945
2946 }
2947
2948
2949
2950 sub LimitRefersTo {
2951     my $self      = shift;
2952     my $ticket_id = shift;
2953     return $self->LimitLinkedTo(
2954         @_,
2955         TARGET => $ticket_id,
2956         TYPE   => 'RefersTo',
2957     );
2958
2959 }
2960
2961
2962
2963 sub LimitReferredToBy {
2964     my $self      = shift;
2965     my $ticket_id = shift;
2966     return $self->LimitLinkedFrom(
2967         @_,
2968         BASE => $ticket_id,
2969         TYPE => 'ReferredToBy',
2970     );
2971 }
2972
2973
2974
2975
2976
2977 =head2 LimitDate (FIELD => 'DateField', OPERATOR => $oper, VALUE => $ISODate)
2978
2979 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
2980
2981 OPERATOR is one of > or <
2982 VALUE is a date and time in ISO format in GMT
2983 FIELD is one of Starts, Started, Told, Created, Resolved, LastUpdated
2984
2985 There are also helper functions of the form LimitFIELD that eliminate
2986 the need to pass in a FIELD argument.
2987
2988 =cut
2989
2990 sub LimitDate {
2991     my $self = shift;
2992     my %args = (
2993         FIELD    => undef,
2994         VALUE    => undef,
2995         OPERATOR => undef,
2996
2997         @_
2998     );
2999
3000     #Set the description if we didn't get handed it above
3001     unless ( $args{'DESCRIPTION'} ) {
3002         $args{'DESCRIPTION'} = $args{'FIELD'} . " "
3003             . $args{'OPERATOR'} . " "
3004             . $args{'VALUE'} . " GMT";
3005     }
3006
3007     $self->Limit(%args);
3008
3009 }
3010
3011
3012 sub LimitCreated {
3013     my $self = shift;
3014     $self->LimitDate( FIELD => 'Created', @_ );
3015 }
3016
3017 sub LimitDue {
3018     my $self = shift;
3019     $self->LimitDate( FIELD => 'Due', @_ );
3020
3021 }
3022
3023 sub LimitStarts {
3024     my $self = shift;
3025     $self->LimitDate( FIELD => 'Starts', @_ );
3026
3027 }
3028
3029 sub LimitStarted {
3030     my $self = shift;
3031     $self->LimitDate( FIELD => 'Started', @_ );
3032 }
3033
3034 sub LimitResolved {
3035     my $self = shift;
3036     $self->LimitDate( FIELD => 'Resolved', @_ );
3037 }
3038
3039 sub LimitTold {
3040     my $self = shift;
3041     $self->LimitDate( FIELD => 'Told', @_ );
3042 }
3043
3044 sub LimitLastUpdated {
3045     my $self = shift;
3046     $self->LimitDate( FIELD => 'LastUpdated', @_ );
3047 }
3048
3049 #
3050
3051 =head2 LimitTransactionDate (OPERATOR => $oper, VALUE => $ISODate)
3052
3053 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
3054
3055 OPERATOR is one of > or <
3056 VALUE is a date and time in ISO format in GMT
3057
3058
3059 =cut
3060
3061 sub LimitTransactionDate {
3062     my $self = shift;
3063     my %args = (
3064         FIELD    => 'TransactionDate',
3065         VALUE    => undef,
3066         OPERATOR => undef,
3067
3068         @_
3069     );
3070
3071     #  <20021217042756.GK28744@pallas.fsck.com>
3072     #    "Kill It" - Jesse.
3073
3074     #Set the description if we didn't get handed it above
3075     unless ( $args{'DESCRIPTION'} ) {
3076         $args{'DESCRIPTION'} = $args{'FIELD'} . " "
3077             . $args{'OPERATOR'} . " "
3078             . $args{'VALUE'} . " GMT";
3079     }
3080
3081     $self->Limit(%args);
3082
3083 }
3084
3085
3086
3087
3088 =head2 LimitCustomField
3089
3090 Takes a paramhash of key/value pairs with the following keys:
3091
3092 =over 4
3093
3094 =item CUSTOMFIELD - CustomField name or id.  If a name is passed, an additional parameter QUEUE may also be passed to distinguish the custom field.
3095
3096 =item OPERATOR - The usual Limit operators
3097
3098 =item VALUE - The value to compare against
3099
3100 =back
3101
3102 =cut
3103
3104 sub LimitCustomField {
3105     my $self = shift;
3106     my %args = (
3107         VALUE       => undef,
3108         CUSTOMFIELD => undef,
3109         OPERATOR    => '=',
3110         DESCRIPTION => undef,
3111         FIELD       => 'CustomFieldValue',
3112         QUOTEVALUE  => 1,
3113         @_
3114     );
3115
3116     my $CF = RT::CustomField->new( $self->CurrentUser );
3117     if ( $args{CUSTOMFIELD} =~ /^\d+$/ ) {
3118         $CF->Load( $args{CUSTOMFIELD} );
3119     }
3120     else {
3121         $CF->LoadByNameAndQueue(
3122             Name  => $args{CUSTOMFIELD},
3123             Queue => $args{QUEUE}
3124         );
3125         $args{CUSTOMFIELD} = $CF->Id;
3126     }
3127
3128     #If we are looking to compare with a null value.
3129     if ( $args{'OPERATOR'} =~ /^is$/i ) {
3130         $args{'DESCRIPTION'}
3131             ||= $self->loc( "Custom field [_1] has no value.", $CF->Name );
3132     }
3133     elsif ( $args{'OPERATOR'} =~ /^is not$/i ) {
3134         $args{'DESCRIPTION'}
3135             ||= $self->loc( "Custom field [_1] has a value.", $CF->Name );
3136     }
3137
3138     # if we're not looking to compare with a null value
3139     else {
3140         $args{'DESCRIPTION'} ||= $self->loc( "Custom field [_1] [_2] [_3]",
3141             $CF->Name, $args{OPERATOR}, $args{VALUE} );
3142     }
3143
3144     if ( defined $args{'QUEUE'} && $args{'QUEUE'} =~ /\D/ ) {
3145         my $QueueObj = RT::Queue->new( $self->CurrentUser );
3146         $QueueObj->Load( $args{'QUEUE'} );
3147         $args{'QUEUE'} = $QueueObj->Id;
3148     }
3149     delete $args{'QUEUE'} unless defined $args{'QUEUE'} && length $args{'QUEUE'};
3150
3151     my @rest;
3152     @rest = ( ENTRYAGGREGATOR => 'AND' )
3153         if ( $CF->Type eq 'SelectMultiple' );
3154
3155     $self->Limit(
3156         VALUE => $args{VALUE},
3157         FIELD => "CF"
3158             .(defined $args{'QUEUE'}? ".$args{'QUEUE'}" : '' )
3159             .".{" . $CF->Name . "}",
3160         OPERATOR    => $args{OPERATOR},
3161         CUSTOMFIELD => 1,
3162         @rest,
3163     );
3164
3165     $self->{'RecalcTicketLimits'} = 1;
3166 }
3167
3168
3169
3170 =head2 _NextIndex
3171
3172 Keep track of the counter for the array of restrictions
3173
3174 =cut
3175
3176 sub _NextIndex {
3177     my $self = shift;
3178     return ( $self->{'restriction_index'}++ );
3179 }
3180
3181
3182
3183
3184 sub _Init {
3185     my $self = shift;
3186     $self->{'table'}                   = "Tickets";
3187     $self->{'RecalcTicketLimits'}      = 1;
3188     $self->{'looking_at_effective_id'} = 0;
3189     $self->{'looking_at_type'}         = 0;
3190     $self->{'restriction_index'}       = 1;
3191     $self->{'primary_key'}             = "id";
3192     delete $self->{'items_array'};
3193     delete $self->{'item_map'};
3194     delete $self->{'columns_to_display'};
3195     $self->SUPER::_Init(@_);
3196
3197     $self->_InitSQL;
3198
3199 }
3200
3201
3202 sub Count {
3203     my $self = shift;
3204     $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
3205     return ( $self->SUPER::Count() );
3206 }
3207
3208
3209 sub CountAll {
3210     my $self = shift;
3211     $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
3212     return ( $self->SUPER::CountAll() );
3213 }
3214
3215
3216
3217 =head2 ItemsArrayRef
3218
3219 Returns a reference to the set of all items found in this search
3220
3221 =cut
3222
3223 sub ItemsArrayRef {
3224     my $self = shift;
3225
3226     return $self->{'items_array'} if $self->{'items_array'};
3227
3228     my $placeholder = $self->_ItemsCounter;
3229     $self->GotoFirstItem();
3230     while ( my $item = $self->Next ) {
3231         push( @{ $self->{'items_array'} }, $item );
3232     }
3233     $self->GotoItem($placeholder);
3234     $self->{'items_array'}
3235         = $self->ItemsOrderBy( $self->{'items_array'} );
3236
3237     return $self->{'items_array'};
3238 }
3239
3240 sub ItemsArrayRefWindow {
3241     my $self = shift;
3242     my $window = shift;
3243
3244     my @old = ($self->_ItemsCounter, $self->RowsPerPage, $self->FirstRow+1);
3245
3246     $self->RowsPerPage( $window );
3247     $self->FirstRow(1);
3248     $self->GotoFirstItem;
3249
3250     my @res;
3251     while ( my $item = $self->Next ) {
3252         push @res, $item;
3253     }
3254
3255     $self->RowsPerPage( $old[1] );
3256     $self->FirstRow( $old[2] );
3257     $self->GotoItem( $old[0] );
3258
3259     return \@res;
3260 }
3261
3262
3263 sub Next {
3264     my $self = shift;
3265
3266     $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
3267
3268     my $Ticket = $self->SUPER::Next;
3269     return $Ticket unless $Ticket;
3270
3271     if ( $Ticket->__Value('Status') eq 'deleted'
3272         && !$self->{'allow_deleted_search'} )
3273     {
3274         return $self->Next;
3275     }
3276     elsif ( RT->Config->Get('UseSQLForACLChecks') ) {
3277         # if we found a ticket with this option enabled then
3278         # all tickets we found are ACLed, cache this fact
3279         my $key = join ";:;", $self->CurrentUser->id, 'ShowTicket', 'RT::Ticket-'. $Ticket->id;
3280         $RT::Principal::_ACL_CACHE->set( $key => 1 );
3281         return $Ticket;
3282     }
3283     elsif ( $Ticket->CurrentUserHasRight('ShowTicket') ) {
3284         # has rights
3285         return $Ticket;
3286     }
3287     else {
3288         # If the user doesn't have the right to show this ticket
3289         return $self->Next;
3290     }
3291 }
3292
3293 sub _DoSearch {
3294     my $self = shift;
3295     $self->CurrentUserCanSee if RT->Config->Get('UseSQLForACLChecks');
3296     return $self->SUPER::_DoSearch( @_ );
3297 }
3298
3299 sub _DoCount {
3300     my $self = shift;
3301     $self->CurrentUserCanSee if RT->Config->Get('UseSQLForACLChecks');
3302     return $self->SUPER::_DoCount( @_ );
3303 }
3304
3305 sub _RolesCanSee {
3306     my $self = shift;
3307
3308     my $cache_key = 'RolesHasRight;:;ShowTicket';
3309  
3310     if ( my $cached = $RT::Principal::_ACL_CACHE->fetch( $cache_key ) ) {
3311         return %$cached;
3312     }
3313
3314     my $ACL = RT::ACL->new( RT->SystemUser );
3315     $ACL->Limit( FIELD => 'RightName', VALUE => 'ShowTicket' );
3316     $ACL->Limit( FIELD => 'PrincipalType', OPERATOR => '!=', VALUE => 'Group' );
3317     my $principal_alias = $ACL->Join(
3318         ALIAS1 => 'main',
3319         FIELD1 => 'PrincipalId',
3320         TABLE2 => 'Principals',
3321         FIELD2 => 'id',
3322     );
3323     $ACL->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3324
3325     my %res = ();
3326     foreach my $ACE ( @{ $ACL->ItemsArrayRef } ) {
3327         my $role = $ACE->__Value('PrincipalType');
3328         my $type = $ACE->__Value('ObjectType');
3329         if ( $type eq 'RT::System' ) {
3330             $res{ $role } = 1;
3331         }
3332         elsif ( $type eq 'RT::Queue' ) {
3333             next if $res{ $role } && !ref $res{ $role };
3334             push @{ $res{ $role } ||= [] }, $ACE->__Value('ObjectId');
3335         }
3336         else {
3337             $RT::Logger->error('ShowTicket right is granted on unsupported object');
3338         }
3339     }
3340     $RT::Principal::_ACL_CACHE->set( $cache_key => \%res );
3341     return %res;
3342 }
3343
3344 sub _DirectlyCanSeeIn {
3345     my $self = shift;
3346     my $id = $self->CurrentUser->id;
3347
3348     my $cache_key = 'User-'. $id .';:;ShowTicket;:;DirectlyCanSeeIn';
3349     if ( my $cached = $RT::Principal::_ACL_CACHE->fetch( $cache_key ) ) {
3350         return @$cached;
3351     }
3352
3353     my $ACL = RT::ACL->new( RT->SystemUser );
3354     $ACL->Limit( FIELD => 'RightName', VALUE => 'ShowTicket' );
3355     my $principal_alias = $ACL->Join(
3356         ALIAS1 => 'main',
3357         FIELD1 => 'PrincipalId',
3358         TABLE2 => 'Principals',
3359         FIELD2 => 'id',
3360     );
3361     $ACL->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3362     my $cgm_alias = $ACL->Join(
3363         ALIAS1 => 'main',
3364         FIELD1 => 'PrincipalId',
3365         TABLE2 => 'CachedGroupMembers',
3366         FIELD2 => 'GroupId',
3367     );
3368     $ACL->Limit( ALIAS => $cgm_alias, FIELD => 'MemberId', VALUE => $id );
3369     $ACL->Limit( ALIAS => $cgm_alias, FIELD => 'Disabled', VALUE => 0 );
3370
3371     my @res = ();
3372     foreach my $ACE ( @{ $ACL->ItemsArrayRef } ) {
3373         my $type = $ACE->__Value('ObjectType');
3374         if ( $type eq 'RT::System' ) {
3375             # If user is direct member of a group that has the right
3376             # on the system then he can see any ticket
3377             $RT::Principal::_ACL_CACHE->set( $cache_key => [-1] );
3378             return (-1);
3379         }
3380         elsif ( $type eq 'RT::Queue' ) {
3381             push @res, $ACE->__Value('ObjectId');
3382         }
3383         else {
3384             $RT::Logger->error('ShowTicket right is granted on unsupported object');
3385         }
3386     }
3387     $RT::Principal::_ACL_CACHE->set( $cache_key => \@res );
3388     return @res;
3389 }
3390
3391 sub CurrentUserCanSee {
3392     my $self = shift;
3393     return if $self->{'_sql_current_user_can_see_applied'};
3394
3395     return $self->{'_sql_current_user_can_see_applied'} = 1
3396         if $self->CurrentUser->UserObj->HasRight(
3397             Right => 'SuperUser', Object => $RT::System
3398         );
3399
3400     my $id = $self->CurrentUser->id;
3401
3402     # directly can see in all queues then we have nothing to do
3403     my @direct_queues = $self->_DirectlyCanSeeIn;
3404     return $self->{'_sql_current_user_can_see_applied'} = 1
3405         if @direct_queues && $direct_queues[0] == -1;
3406
3407     my %roles = $self->_RolesCanSee;
3408     {
3409         my %skip = map { $_ => 1 } @direct_queues;
3410         foreach my $role ( keys %roles ) {
3411             next unless ref $roles{ $role };
3412
3413             my @queues = grep !$skip{$_}, @{ $roles{ $role } };
3414             if ( @queues ) {
3415                 $roles{ $role } = \@queues;
3416             } else {
3417                 delete $roles{ $role };
3418             }
3419         }
3420     }
3421
3422 # there is no global watchers, only queues and tickes, if at
3423 # some point we will add global roles then it's gonna blow
3424 # the idea here is that if the right is set globaly for a role
3425 # and user plays this role for a queue directly not a ticket
3426 # then we have to check in advance
3427     if ( my @tmp = grep $_ ne 'Owner' && !ref $roles{ $_ }, keys %roles ) {
3428
3429         my $groups = RT::Groups->new( RT->SystemUser );
3430         $groups->Limit( FIELD => 'Domain', VALUE => 'RT::Queue-Role' );
3431         foreach ( @tmp ) {
3432             $groups->Limit( FIELD => 'Type', VALUE => $_ );
3433         }
3434         my $principal_alias = $groups->Join(
3435             ALIAS1 => 'main',
3436             FIELD1 => 'id',
3437             TABLE2 => 'Principals',
3438             FIELD2 => 'id',
3439         );
3440         $groups->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3441         my $cgm_alias = $groups->Join(
3442             ALIAS1 => 'main',
3443             FIELD1 => 'id',
3444             TABLE2 => 'CachedGroupMembers',
3445             FIELD2 => 'GroupId',
3446         );
3447         $groups->Limit( ALIAS => $cgm_alias, FIELD => 'MemberId', VALUE => $id );
3448         $groups->Limit( ALIAS => $cgm_alias, FIELD => 'Disabled', VALUE => 0 );
3449         while ( my $group = $groups->Next ) {
3450             push @direct_queues, $group->Instance;
3451         }
3452     }
3453
3454     unless ( @direct_queues || keys %roles ) {
3455         $self->SUPER::Limit(
3456             SUBCLAUSE => 'ACL',
3457             ALIAS => 'main',
3458             FIELD => 'id',
3459             VALUE => 0,
3460             ENTRYAGGREGATOR => 'AND',
3461         );
3462         return $self->{'_sql_current_user_can_see_applied'} = 1;
3463     }
3464
3465     {
3466         my $join_roles = keys %roles;
3467         $join_roles = 0 if $join_roles == 1 && $roles{'Owner'};
3468         my ($role_group_alias, $cgm_alias);
3469         if ( $join_roles ) {
3470             $role_group_alias = $self->_RoleGroupsJoin( New => 1 );
3471             $cgm_alias = $self->_GroupMembersJoin( GroupsAlias => $role_group_alias );
3472             $self->SUPER::Limit(
3473                 LEFTJOIN   => $cgm_alias,
3474                 FIELD      => 'MemberId',
3475                 OPERATOR   => '=',
3476                 VALUE      => $id,
3477             );
3478         }
3479         my $limit_queues = sub {
3480             my $ea = shift;
3481             my @queues = @_;
3482
3483             return unless @queues;
3484             if ( @queues == 1 ) {
3485                 $self->SUPER::Limit(
3486                     SUBCLAUSE => 'ACL',
3487                     ALIAS => 'main',
3488                     FIELD => 'Queue',
3489                     VALUE => $_[0],
3490                     ENTRYAGGREGATOR => $ea,
3491                 );
3492             } else {
3493                 $self->SUPER::_OpenParen('ACL');
3494                 foreach my $q ( @queues ) {
3495                     $self->SUPER::Limit(
3496                         SUBCLAUSE => 'ACL',
3497                         ALIAS => 'main',
3498                         FIELD => 'Queue',
3499                         VALUE => $q,
3500                         ENTRYAGGREGATOR => $ea,
3501                     );
3502                     $ea = 'OR';
3503                 }
3504                 $self->SUPER::_CloseParen('ACL');
3505             }
3506             return 1;
3507         };
3508
3509         $self->SUPER::_OpenParen('ACL');
3510         my $ea = 'AND';
3511         $ea = 'OR' if $limit_queues->( $ea, @direct_queues );
3512         while ( my ($role, $queues) = each %roles ) {
3513             $self->SUPER::_OpenParen('ACL');
3514             if ( $role eq 'Owner' ) {
3515                 $self->SUPER::Limit(
3516                     SUBCLAUSE => 'ACL',
3517                     FIELD           => 'Owner',
3518                     VALUE           => $id,
3519                     ENTRYAGGREGATOR => $ea,
3520                 );
3521             }
3522             else {
3523                 $self->SUPER::Limit(
3524                     SUBCLAUSE       => 'ACL',
3525                     ALIAS           => $cgm_alias,
3526                     FIELD           => 'MemberId',
3527                     OPERATOR        => 'IS NOT',
3528                     VALUE           => 'NULL',
3529                     QUOTEVALUE      => 0,
3530                     ENTRYAGGREGATOR => $ea,
3531                 );
3532                 $self->SUPER::Limit(
3533                     SUBCLAUSE       => 'ACL',
3534                     ALIAS           => $role_group_alias,
3535                     FIELD           => 'Type',
3536                     VALUE           => $role,
3537                     ENTRYAGGREGATOR => 'AND',
3538                 );
3539             }
3540             $limit_queues->( 'AND', @$queues ) if ref $queues;
3541             $ea = 'OR' if $ea eq 'AND';
3542             $self->SUPER::_CloseParen('ACL');
3543         }
3544         $self->SUPER::_CloseParen('ACL');
3545     }
3546     return $self->{'_sql_current_user_can_see_applied'} = 1;
3547 }
3548
3549
3550
3551
3552
3553 =head2 LoadRestrictions
3554
3555 LoadRestrictions takes a string which can fully populate the TicketRestrictons hash.
3556 TODO It is not yet implemented
3557
3558 =cut
3559
3560
3561
3562 =head2 DescribeRestrictions
3563
3564 takes nothing.
3565 Returns a hash keyed by restriction id.
3566 Each element of the hash is currently a one element hash that contains DESCRIPTION which
3567 is a description of the purpose of that TicketRestriction
3568
3569 =cut
3570
3571 sub DescribeRestrictions {
3572     my $self = shift;
3573
3574     my %listing;
3575
3576     foreach my $row ( keys %{ $self->{'TicketRestrictions'} } ) {
3577         $listing{$row} = $self->{'TicketRestrictions'}{$row}{'DESCRIPTION'};
3578     }
3579     return (%listing);
3580 }
3581
3582
3583
3584 =head2 RestrictionValues FIELD
3585
3586 Takes a restriction field and returns a list of values this field is restricted
3587 to.
3588
3589 =cut
3590
3591 sub RestrictionValues {
3592     my $self  = shift;
3593     my $field = shift;
3594     map $self->{'TicketRestrictions'}{$_}{'VALUE'}, grep {
3595                $self->{'TicketRestrictions'}{$_}{'FIELD'}    eq $field
3596             && $self->{'TicketRestrictions'}{$_}{'OPERATOR'} eq "="
3597         }
3598         keys %{ $self->{'TicketRestrictions'} };
3599 }
3600
3601
3602
3603 =head2 ClearRestrictions
3604
3605 Removes all restrictions irretrievably
3606
3607 =cut
3608
3609 sub ClearRestrictions {
3610     my $self = shift;
3611     delete $self->{'TicketRestrictions'};
3612     $self->{'looking_at_effective_id'} = 0;
3613     $self->{'looking_at_type'}         = 0;
3614     $self->{'RecalcTicketLimits'}      = 1;
3615 }
3616
3617
3618
3619 =head2 DeleteRestriction
3620
3621 Takes the row Id of a restriction (From DescribeRestrictions' output, for example.
3622 Removes that restriction from the session's limits.
3623
3624 =cut
3625
3626 sub DeleteRestriction {
3627     my $self = shift;
3628     my $row  = shift;
3629     delete $self->{'TicketRestrictions'}{$row};
3630
3631     $self->{'RecalcTicketLimits'} = 1;
3632
3633     #make the underlying easysearch object forget all its preconceptions
3634 }
3635
3636
3637
3638 # Convert a set of oldstyle SB Restrictions to Clauses for RQL
3639
3640 sub _RestrictionsToClauses {
3641     my $self = shift;
3642
3643     my %clause;
3644     foreach my $row ( keys %{ $self->{'TicketRestrictions'} } ) {
3645         my $restriction = $self->{'TicketRestrictions'}{$row};
3646
3647         # We need to reimplement the subclause aggregation that SearchBuilder does.
3648         # Default Subclause is ALIAS.FIELD, and default ALIAS is 'main',
3649         # Then SB AND's the different Subclauses together.
3650
3651         # So, we want to group things into Subclauses, convert them to
3652         # SQL, and then join them with the appropriate DefaultEA.
3653         # Then join each subclause group with AND.
3654
3655         my $field = $restriction->{'FIELD'};
3656         my $realfield = $field;    # CustomFields fake up a fieldname, so
3657                                    # we need to figure that out
3658
3659         # One special case
3660         # Rewrite LinkedTo meta field to the real field
3661         if ( $field =~ /LinkedTo/ ) {
3662             $realfield = $field = $restriction->{'TYPE'};
3663         }
3664
3665         # Two special case
3666         # Handle subkey fields with a different real field
3667         if ( $field =~ /^(\w+)\./ ) {
3668             $realfield = $1;
3669         }
3670
3671         die "I don't know about $field yet"
3672             unless ( exists $FIELD_METADATA{$realfield}
3673                 or $restriction->{CUSTOMFIELD} );
3674
3675         my $type = $FIELD_METADATA{$realfield}->[0];
3676         my $op   = $restriction->{'OPERATOR'};
3677
3678         my $value = (
3679             grep    {defined}
3680                 map { $restriction->{$_} } qw(VALUE TICKET BASE TARGET)
3681         )[0];
3682
3683         # this performs the moral equivalent of defined or/dor/C<//>,
3684         # without the short circuiting.You need to use a 'defined or'
3685         # type thing instead of just checking for truth values, because
3686         # VALUE could be 0.(i.e. "false")
3687
3688         # You could also use this, but I find it less aesthetic:
3689         # (although it does short circuit)
3690         #( defined $restriction->{'VALUE'}? $restriction->{VALUE} :
3691         # defined $restriction->{'TICKET'} ?
3692         # $restriction->{TICKET} :
3693         # defined $restriction->{'BASE'} ?
3694         # $restriction->{BASE} :
3695         # defined $restriction->{'TARGET'} ?
3696         # $restriction->{TARGET} )
3697
3698         my $ea = $restriction->{ENTRYAGGREGATOR}
3699             || $DefaultEA{$type}
3700             || "AND";
3701         if ( ref $ea ) {
3702             die "Invalid operator $op for $field ($type)"
3703                 unless exists $ea->{$op};
3704             $ea = $ea->{$op};
3705         }
3706
3707         # Each CustomField should be put into a different Clause so they
3708         # are ANDed together.
3709         if ( $restriction->{CUSTOMFIELD} ) {
3710             $realfield = $field;
3711         }
3712
3713         exists $clause{$realfield} or $clause{$realfield} = [];
3714
3715         # Escape Quotes
3716         $field =~ s!(['\\])!\\$1!g;
3717         $value =~ s!(['\\])!\\$1!g;
3718         my $data = [ $ea, $type, $field, $op, $value ];
3719
3720         # here is where we store extra data, say if it's a keyword or
3721         # something.  (I.e. "TYPE SPECIFIC STUFF")
3722
3723         if (lc $ea eq 'none') {
3724             $clause{$realfield} = [ $data ];
3725         } else {
3726             push @{ $clause{$realfield} }, $data;
3727         }
3728     }
3729     return \%clause;
3730 }
3731
3732
3733
3734 =head2 _ProcessRestrictions PARAMHASH
3735
3736 # The new _ProcessRestrictions is somewhat dependent on the SQL stuff,
3737 # but isn't quite generic enough to move into Tickets_SQL.
3738
3739 =cut
3740
3741 sub _ProcessRestrictions {
3742     my $self = shift;
3743
3744     #Blow away ticket aliases since we'll need to regenerate them for
3745     #a new search
3746     delete $self->{'TicketAliases'};
3747     delete $self->{'items_array'};
3748     delete $self->{'item_map'};
3749     delete $self->{'raw_rows'};
3750     delete $self->{'rows'};
3751     delete $self->{'count_all'};
3752
3753     my $sql = $self->Query;    # Violating the _SQL namespace
3754     if ( !$sql || $self->{'RecalcTicketLimits'} ) {
3755
3756         #  "Restrictions to Clauses Branch\n";
3757         my $clauseRef = eval { $self->_RestrictionsToClauses; };
3758         if ($@) {
3759             $RT::Logger->error( "RestrictionsToClauses: " . $@ );
3760             $self->FromSQL("");
3761         }
3762         else {
3763             $sql = $self->ClausesToSQL($clauseRef);
3764             $self->FromSQL($sql) if $sql;
3765         }
3766     }
3767
3768     $self->{'RecalcTicketLimits'} = 0;
3769
3770 }
3771
3772 =head2 _BuildItemMap
3773
3774 Build up a L</ItemMap> of first/last/next/prev items, so that we can
3775 display search nav quickly.
3776
3777 =cut
3778
3779 sub _BuildItemMap {
3780     my $self = shift;
3781
3782     my $window = RT->Config->Get('TicketsItemMapSize');
3783
3784     $self->{'item_map'} = {};
3785
3786     my $items = $self->ItemsArrayRefWindow( $window );
3787     return unless $items && @$items;
3788
3789     my $prev = 0;
3790     $self->{'item_map'}{'first'} = $items->[0]->EffectiveId;
3791     for ( my $i = 0; $i < @$items; $i++ ) {
3792         my $item = $items->[$i];
3793         my $id = $item->EffectiveId;
3794         $self->{'item_map'}{$id}{'defined'} = 1;
3795         $self->{'item_map'}{$id}{'prev'}    = $prev;
3796         $self->{'item_map'}{$id}{'next'}    = $items->[$i+1]->EffectiveId
3797             if $items->[$i+1];
3798         $prev = $id;
3799     }
3800     $self->{'item_map'}{'last'} = $prev
3801         if !$window || @$items < $window;
3802 }
3803
3804 =head2 ItemMap
3805
3806 Returns an a map of all items found by this search. The map is a hash
3807 of the form:
3808
3809     {
3810         first => <first ticket id found>,
3811         last => <last ticket id found or undef>,
3812
3813         <ticket id> => {
3814             prev => <the ticket id found before>,
3815             next => <the ticket id found after>,
3816         },
3817         <ticket id> => {
3818             prev => ...,
3819             next => ...,
3820         },
3821     }
3822
3823 =cut
3824
3825 sub ItemMap {
3826     my $self = shift;
3827     $self->_BuildItemMap unless $self->{'item_map'};
3828     return $self->{'item_map'};
3829 }
3830
3831
3832
3833
3834 =head2 PrepForSerialization
3835
3836 You don't want to serialize a big tickets object, as
3837 the {items} hash will be instantly invalid _and_ eat
3838 lots of space
3839
3840 =cut
3841
3842 sub PrepForSerialization {
3843     my $self = shift;
3844     delete $self->{'items'};
3845     delete $self->{'items_array'};
3846     $self->RedoSearch();
3847 }
3848
3849 =head1 FLAGS
3850
3851 RT::Tickets supports several flags which alter search behavior:
3852
3853
3854 allow_deleted_search  (Otherwise never show deleted tickets in search results)
3855 looking_at_type (otherwise limit to type=ticket)
3856
3857 These flags are set by calling 
3858
3859 $tickets->{'flagname'} = 1;
3860
3861 BUG: There should be an API for this
3862
3863
3864
3865 =cut
3866
3867
3868
3869 =head2 NewItem
3870
3871 Returns an empty new RT::Ticket item
3872
3873 =cut
3874
3875 sub NewItem {
3876     my $self = shift;
3877     return(RT::Ticket->new($self->CurrentUser));
3878 }
3879 RT::Base->_ImportOverlays();
3880
3881 1;