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