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