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