Merge branch 'master' of git.freeside.biz:/home/git/freeside
[freeside.git] / rt / lib / RT / Tickets.pm
1 # BEGIN BPS TAGGED BLOCK {{{
2 #
3 # COPYRIGHT:
4 #
5 # This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC
6 #                                          <sales@bestpractical.com>
7 #
8 # (Except where explicitly superseded by other copyright notices)
9 #
10 #
11 # LICENSE:
12 #
13 # This work is made available to you under the terms of Version 2 of
14 # the GNU General Public License. A copy of that license should have
15 # been provided with this software, but in any event can be snarfed
16 # from www.gnu.org.
17 #
18 # This work is distributed in the hope that it will be useful, but
19 # WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
21 # General Public License for more details.
22 #
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
26 # 02110-1301 or visit their web page on the internet at
27 # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
28 #
29 #
30 # CONTRIBUTION SUBMISSION POLICY:
31 #
32 # (The following paragraph is not intended to limit the rights granted
33 # to you to modify and distribute this software under the terms of
34 # the GNU General Public License and is only of importance to you if
35 # you choose to contribute your changes and enhancements to the
36 # community by submitting them to Best Practical Solutions, LLC.)
37 #
38 # By intentionally submitting any modifications, corrections or
39 # derivatives to this work, or any other work intended for use with
40 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
41 # you are the copyright holder for those contributions and you grant
42 # Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
43 # royalty-free, perpetual, license to use, copy, create derivative
44 # works based on those contributions, and sublicense and distribute
45 # those contributions and any derivatives thereof.
46 #
47 # END BPS TAGGED BLOCK }}}
48
49 # Major Changes:
50
51 # - Decimated ProcessRestrictions and broke it into multiple
52 # functions joined by a LUT
53 # - Semi-Generic SQL stuff moved to another file
54
55 # Known Issues: FIXME!
56
57 # - ClearRestrictions and Reinitialization is messy and unclear.  The
58 # only good way to do it is to create a new RT::Tickets object.
59
60 =head1 NAME
61
62   RT::Tickets - A collection of Ticket objects
63
64
65 =head1 SYNOPSIS
66
67   use RT::Tickets;
68   my $tickets = RT::Tickets->new($CurrentUser);
69
70 =head1 DESCRIPTION
71
72    A collection of RT::Tickets.
73
74 =head1 METHODS
75
76
77 =cut
78
79 package RT::Tickets;
80
81 use strict;
82 use warnings;
83
84
85 use RT::Ticket;
86
87 use base 'RT::SearchBuilder';
88
89 sub Table { 'Tickets'}
90
91 use RT::CustomFields;
92 use DBIx::SearchBuilder::Unique;
93
94 # Configuration Tables:
95
96 # FIELD_METADATA is a mapping of searchable Field name, to Type, and other
97 # metadata.
98
99 our %FIELD_METADATA = (
100     Status          => [ 'ENUM', ], #loc_left_pair
101     Queue           => [ 'ENUM' => 'Queue', ], #loc_left_pair
102     Type            => [ 'ENUM', ], #loc_left_pair
103     Creator         => [ 'ENUM' => 'User', ], #loc_left_pair
104     LastUpdatedBy   => [ 'ENUM' => 'User', ], #loc_left_pair
105     Owner           => [ 'WATCHERFIELD' => 'Owner', ], #loc_left_pair
106     EffectiveId     => [ 'INT', ], #loc_left_pair
107     id              => [ 'ID', ], #loc_left_pair
108     InitialPriority => [ 'INT', ], #loc_left_pair
109     FinalPriority   => [ 'INT', ], #loc_left_pair
110     Priority        => [ 'INT', ], #loc_left_pair
111     TimeLeft        => [ 'INT', ], #loc_left_pair
112     TimeWorked      => [ 'INT', ], #loc_left_pair
113     TimeEstimated   => [ 'INT', ], #loc_left_pair
114
115     Linked          => [ 'LINK' ], #loc_left_pair
116     LinkedTo        => [ 'LINK' => 'To' ], #loc_left_pair
117     LinkedFrom      => [ 'LINK' => 'From' ], #loc_left_pair
118     MemberOf        => [ 'LINK' => To => 'MemberOf', ], #loc_left_pair
119     DependsOn       => [ 'LINK' => To => 'DependsOn', ], #loc_left_pair
120     RefersTo        => [ 'LINK' => To => 'RefersTo', ], #loc_left_pair
121     HasMember       => [ 'LINK' => From => 'MemberOf', ], #loc_left_pair
122     DependentOn     => [ 'LINK' => From => 'DependsOn', ], #loc_left_pair
123     DependedOnBy    => [ 'LINK' => From => 'DependsOn', ], #loc_left_pair
124     ReferredToBy    => [ 'LINK' => From => 'RefersTo', ], #loc_left_pair
125     Told             => [ 'DATE'            => 'Told', ], #loc_left_pair
126     Starts           => [ 'DATE'            => 'Starts', ], #loc_left_pair
127     Started          => [ 'DATE'            => 'Started', ], #loc_left_pair
128     Due              => [ 'DATE'            => 'Due', ], #loc_left_pair
129     Resolved         => [ 'DATE'            => 'Resolved', ], #loc_left_pair
130     LastUpdated      => [ 'DATE'            => 'LastUpdated', ], #loc_left_pair
131     Created          => [ 'DATE'            => 'Created', ], #loc_left_pair
132     Subject          => [ 'STRING', ], #loc_left_pair
133     Content          => [ 'TRANSCONTENT', ], #loc_left_pair
134     ContentType      => [ 'TRANSFIELD', ], #loc_left_pair
135     Filename         => [ 'TRANSFIELD', ], #loc_left_pair
136     TransactionDate  => [ 'TRANSDATE', ], #loc_left_pair
137     Requestor        => [ 'WATCHERFIELD'    => 'Requestor', ], #loc_left_pair
138     Requestors       => [ 'WATCHERFIELD'    => 'Requestor', ], #loc_left_pair
139     Cc               => [ 'WATCHERFIELD'    => 'Cc', ], #loc_left_pair
140     AdminCc          => [ 'WATCHERFIELD'    => 'AdminCc', ], #loc_left_pair
141     Watcher          => [ 'WATCHERFIELD', ], #loc_left_pair
142     QueueCc          => [ 'WATCHERFIELD'    => 'Cc'      => 'Queue', ], #loc_left_pair
143     QueueAdminCc     => [ 'WATCHERFIELD'    => 'AdminCc' => 'Queue', ], #loc_left_pair
144     QueueWatcher     => [ 'WATCHERFIELD'    => undef     => 'Queue', ], #loc_left_pair
145     CustomFieldValue => [ 'CUSTOMFIELD', ], #loc_left_pair
146     CustomField      => [ 'CUSTOMFIELD', ], #loc_left_pair
147     CF               => [ 'CUSTOMFIELD', ], #loc_left_pair
148     Updated          => [ 'TRANSDATE', ], #loc_left_pair
149     RequestorGroup   => [ 'MEMBERSHIPFIELD' => 'Requestor', ], #loc_left_pair
150     CCGroup          => [ 'MEMBERSHIPFIELD' => 'Cc', ], #loc_left_pair
151     AdminCCGroup     => [ 'MEMBERSHIPFIELD' => 'AdminCc', ], #loc_left_pair
152     WatcherGroup     => [ 'MEMBERSHIPFIELD', ], #loc_left_pair
153     HasAttribute     => [ 'HASATTRIBUTE', 1 ],
154     HasNoAttribute     => [ 'HASATTRIBUTE', 0 ],
155     #freeside
156     Customer         => [ 'FREESIDEFIELD' => 'Customer' ],
157     Service          => [ 'FREESIDEFIELD' => 'Service' ],
158     WillResolve      => [ 'DATE'            => 'WillResolve', ], #loc_left_pair
159 );
160
161 our %SEARCHABLE_SUBFIELDS = (
162     User => [qw(
163         EmailAddress Name RealName Nickname Organization Address1 Address2
164         WorkPhone HomePhone MobilePhone PagerPhone id
165     )],
166 );
167
168 # Mapping of Field Type to Function
169 our %dispatch = (
170     ENUM            => \&_EnumLimit,
171     INT             => \&_IntLimit,
172     ID              => \&_IdLimit,
173     LINK            => \&_LinkLimit,
174     DATE            => \&_DateLimit,
175     STRING          => \&_StringLimit,
176     TRANSFIELD      => \&_TransLimit,
177     TRANSCONTENT    => \&_TransContentLimit,
178     TRANSDATE       => \&_TransDateLimit,
179     WATCHERFIELD    => \&_WatcherLimit,
180     MEMBERSHIPFIELD => \&_WatcherMembershipLimit,
181     CUSTOMFIELD     => \&_CustomFieldLimit,
182     HASATTRIBUTE    => \&_HasAttributeLimit,
183     FREESIDEFIELD   => \&_FreesideFieldLimit,
184 );
185 our %can_bundle = ();# WATCHERFIELD => "yes", );
186
187 # Default EntryAggregator per type
188 # if you specify OP, you must specify all valid OPs
189 my %DefaultEA = (
190     INT  => 'AND',
191     ENUM => {
192         '='  => 'OR',
193         '!=' => 'AND'
194     },
195     DATE => {
196         '='  => 'OR',
197         '>=' => 'AND',
198         '<=' => 'AND',
199         '>'  => 'AND',
200         '<'  => 'AND'
201     },
202     STRING => {
203         '='        => 'OR',
204         '!='       => 'AND',
205         'LIKE'     => 'AND',
206         'NOT LIKE' => 'AND'
207     },
208     TRANSFIELD   => 'AND',
209     TRANSDATE    => 'AND',
210     LINK         => 'OR',
211     LINKFIELD    => 'AND',
212     TARGET       => 'AND',
213     BASE         => 'AND',
214     WATCHERFIELD => {
215         '='        => 'OR',
216         '!='       => 'AND',
217         'LIKE'     => 'OR',
218         'NOT LIKE' => 'AND'
219     },
220
221     HASATTRIBUTE => {
222         '='        => 'AND',
223         '!='       => 'AND',
224     },
225
226     CUSTOMFIELD => 'OR',
227 );
228
229 # Helper functions for passing the above lexically scoped tables above
230 # into Tickets_SQL.
231 sub FIELDS     { return \%FIELD_METADATA }
232 sub dispatch   { return \%dispatch }
233 sub can_bundle { return \%can_bundle }
234
235 # Bring in the clowns.
236 require RT::Tickets_SQL;
237
238
239 our @SORTFIELDS = qw(id Status
240     Queue Subject
241     Owner Created Due Starts Started
242     Told
243     Resolved LastUpdated Priority TimeWorked TimeLeft);
244
245 =head2 SortFields
246
247 Returns the list of fields that lists of tickets can easily be sorted by
248
249 =cut
250
251 sub SortFields {
252     my $self = shift;
253     return (@SORTFIELDS);
254 }
255
256
257 # BEGIN SQL STUFF *********************************
258
259
260 sub CleanSlate {
261     my $self = shift;
262     $self->SUPER::CleanSlate( @_ );
263     delete $self->{$_} foreach qw(
264         _sql_cf_alias
265         _sql_group_members_aliases
266         _sql_object_cfv_alias
267         _sql_role_group_aliases
268         _sql_trattachalias
269         _sql_u_watchers_alias_for_sort
270         _sql_u_watchers_aliases
271         _sql_current_user_can_see_applied
272     );
273 }
274
275 =head1 Limit Helper Routines
276
277 These routines are the targets of a dispatch table depending on the
278 type of field.  They all share the same signature:
279
280   my ($self,$field,$op,$value,@rest) = @_;
281
282 The values in @rest should be suitable for passing directly to
283 DBIx::SearchBuilder::Limit.
284
285 Essentially they are an expanded/broken out (and much simplified)
286 version of what ProcessRestrictions used to do.  They're also much
287 more clearly delineated by the TYPE of field being processed.
288
289 =head2 _IdLimit
290
291 Handle ID field.
292
293 =cut
294
295 sub _IdLimit {
296     my ( $sb, $field, $op, $value, @rest ) = @_;
297
298     if ( $value eq '__Bookmarked__' ) {
299         return $sb->_BookmarkLimit( $field, $op, $value, @rest );
300     } else {
301         return $sb->_IntLimit( $field, $op, $value, @rest );
302     }
303 }
304
305 sub _BookmarkLimit {
306     my ( $sb, $field, $op, $value, @rest ) = @_;
307
308     die "Invalid operator $op for __Bookmarked__ search on $field"
309         unless $op =~ /^(=|!=)$/;
310
311     my @bookmarks = do {
312         my $tmp = $sb->CurrentUser->UserObj->FirstAttribute('Bookmarks');
313         $tmp = $tmp->Content if $tmp;
314         $tmp ||= {};
315         grep $_, keys %$tmp;
316     };
317
318     return $sb->_SQLLimit(
319         FIELD    => $field,
320         OPERATOR => $op,
321         VALUE    => 0,
322         @rest,
323     ) unless @bookmarks;
324
325     # as bookmarked tickets can be merged we have to use a join
326     # but it should be pretty lightweight
327     my $tickets_alias = $sb->Join(
328         TYPE   => 'LEFT',
329         ALIAS1 => 'main',
330         FIELD1 => 'id',
331         TABLE2 => 'Tickets',
332         FIELD2 => 'EffectiveId',
333     );
334     $sb->_OpenParen;
335     my $first = 1;
336     my $ea = $op eq '='? 'OR': 'AND';
337     foreach my $id ( sort @bookmarks ) {
338         $sb->_SQLLimit(
339             ALIAS    => $tickets_alias,
340             FIELD    => 'id',
341             OPERATOR => $op,
342             VALUE    => $id,
343             $first? (@rest): ( ENTRYAGGREGATOR => $ea )
344         );
345         $first = 0 if $first;
346     }
347     $sb->_CloseParen;
348 }
349
350 =head2 _EnumLimit
351
352 Handle Fields which are limited to certain values, and potentially
353 need to be looked up from another class.
354
355 This subroutine actually handles two different kinds of fields.  For
356 some the user is responsible for limiting the values.  (i.e. Status,
357 Type).
358
359 For others, the value specified by the user will be looked by via
360 specified class.
361
362 Meta Data:
363   name of class to lookup in (Optional)
364
365 =cut
366
367 sub _EnumLimit {
368     my ( $sb, $field, $op, $value, @rest ) = @_;
369
370     # SQL::Statement changes != to <>.  (Can we remove this now?)
371     $op = "!=" if $op eq "<>";
372
373     die "Invalid Operation: $op for $field"
374         unless $op eq "="
375         or $op     eq "!=";
376
377     my $meta = $FIELD_METADATA{$field};
378     if ( defined $meta->[1] && defined $value && $value !~ /^\d+$/ ) {
379         my $class = "RT::" . $meta->[1];
380         my $o     = $class->new( $sb->CurrentUser );
381         $o->Load($value);
382         $value = $o->Id;
383     }
384     $sb->_SQLLimit(
385         FIELD    => $field,
386         VALUE    => $value,
387         OPERATOR => $op,
388         @rest,
389     );
390 }
391
392 =head2 _IntLimit
393
394 Handle fields where the values are limited to integers.  (For example,
395 Priority, TimeWorked.)
396
397 Meta Data:
398   None
399
400 =cut
401
402 sub _IntLimit {
403     my ( $sb, $field, $op, $value, @rest ) = @_;
404
405     die "Invalid Operator $op for $field"
406         unless $op =~ /^(=|!=|>|<|>=|<=)$/;
407
408     $sb->_SQLLimit(
409         FIELD    => $field,
410         VALUE    => $value,
411         OPERATOR => $op,
412         @rest,
413     );
414 }
415
416 =head2 _LinkLimit
417
418 Handle fields which deal with links between tickets.  (MemberOf, DependsOn)
419
420 Meta Data:
421   1: Direction (From, To)
422   2: Link Type (MemberOf, DependsOn, RefersTo)
423
424 =cut
425
426 sub _LinkLimit {
427     my ( $sb, $field, $op, $value, @rest ) = @_;
428
429     my $meta = $FIELD_METADATA{$field};
430     die "Invalid Operator $op for $field" unless $op =~ /^(=|!=|IS|IS NOT)$/io;
431
432     my $is_negative = 0;
433     if ( $op eq '!=' || $op =~ /\bNOT\b/i ) {
434         $is_negative = 1;
435     }
436     my $is_null = 0;
437     $is_null = 1 if !$value || $value =~ /^null$/io;
438
439     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        $self->SUPER::Limit(
2088          LEFTJOIN => $linkalias,
2089          FIELD    => 'Base',
2090          OPERATOR => 'LIKE',
2091          VALUE    => 'fsck.com-rt://%/ticket/%',
2092        );
2093         $self->SUPER::Limit(
2094             LEFTJOIN => $linkalias,
2095             FIELD    => 'Type',
2096             OPERATOR => '=',
2097             VALUE    => 'MemberOf',
2098         );
2099         $self->SUPER::Limit(
2100             LEFTJOIN => $linkalias,
2101             FIELD    => 'Target',
2102             OPERATOR => 'STARTSWITH',
2103             VALUE    => 'freeside://freeside/cust_main/',
2104         );
2105         $self->{cust_main_linkalias} = $linkalias;
2106     }
2107     my $custnum_sql = "CAST(SUBSTR($linkalias.Target,31) AS ";
2108     if ( RT->Config->Get('DatabaseType') eq 'mysql' ) {
2109         $custnum_sql .= 'SIGNED INTEGER)';
2110     }
2111     else {
2112         $custnum_sql .= 'INTEGER)';
2113     }
2114     return ($linkalias, $custnum_sql);
2115 }
2116
2117 sub JoinToCustomer {
2118     my $self = shift;
2119     my ($linkalias, $custnum_sql) = $self->JoinToCustLinks;
2120     # don't reuse this join, though--negative queries need 
2121     # independent joins
2122     my $custalias = $self->Join(
2123         TYPE       => 'LEFT',
2124         EXPRESSION => $custnum_sql,
2125         TABLE2     => 'cust_main',
2126         FIELD2     => 'custnum',
2127     );
2128     return $custalias;
2129 }
2130
2131 sub JoinToSvcLinks {
2132     my $self = shift;
2133     my $linkalias = $self->{cust_svc_linkalias};
2134     if (!$linkalias) {
2135         $linkalias = $self->Join(
2136             TYPE   => 'LEFT',
2137             ALIAS1 => 'main',
2138             FIELD1 => 'id',
2139             TABLE2 => 'Links',
2140             FIELD2 => 'LocalBase',
2141         );
2142        $self->SUPER::Limit(
2143          LEFTJOIN => $linkalias,
2144          FIELD    => 'Base',
2145          OPERATOR => 'LIKE',
2146          VALUE    => 'fsck.com-rt://%/ticket/%',
2147        );
2148
2149         $self->SUPER::Limit(
2150             LEFTJOIN => $linkalias,
2151             FIELD    => 'Type',
2152             OPERATOR => '=',
2153             VALUE    => 'MemberOf',
2154         );
2155         $self->SUPER::Limit(
2156             LEFTJOIN => $linkalias,
2157             FIELD    => 'Target',
2158             OPERATOR => 'STARTSWITH',
2159             VALUE    => 'freeside://freeside/cust_svc/',
2160         );
2161         $self->{cust_svc_linkalias} = $linkalias;
2162     }
2163     my $svcnum_sql = "CAST(SUBSTR($linkalias.Target,30) AS ";
2164     if ( RT->Config->Get('DatabaseType') eq 'mysql' ) {
2165         $svcnum_sql .= 'SIGNED INTEGER)';
2166     }
2167     else {
2168         $svcnum_sql .= 'INTEGER)';
2169     }
2170     return ($linkalias, $svcnum_sql);
2171 }
2172
2173 sub JoinToService {
2174     my $self = shift;
2175     my ($linkalias, $svcnum_sql) = $self->JoinToSvcLinks;
2176     $self->Join(
2177         TYPE       => 'LEFT',
2178         EXPRESSION => $svcnum_sql,
2179         TABLE2     => 'cust_svc',
2180         FIELD2     => 'svcnum',
2181     );
2182 }
2183
2184 # This creates an alternate left join path to cust_main via cust_svc.
2185 # _FreesideFieldLimit needs to add this as a separate, independent join
2186 # and include all tickets that have a matching cust_main record via 
2187 # either path.
2188 sub JoinToCustomerViaService {
2189     my $self = shift;
2190     my $svcalias = $self->JoinToService;
2191     my $cust_pkg = $self->Join(
2192         TYPE      => 'LEFT',
2193         ALIAS1    => $svcalias,
2194         FIELD1    => 'pkgnum',
2195         TABLE2    => 'cust_pkg',
2196         FIELD2    => 'pkgnum',
2197     );
2198     my $cust_main = $self->Join(
2199         TYPE      => 'LEFT',
2200         ALIAS1    => $cust_pkg,
2201         FIELD1    => 'custnum',
2202         TABLE2    => 'cust_main',
2203         FIELD2    => 'custnum',
2204     );
2205     $cust_main;
2206 }
2207
2208 sub _FreesideFieldLimit {
2209     my ( $self, $field, $op, $value, %rest ) = @_;
2210     my $is_negative = 0;
2211     if ( $op eq '!=' || $op =~ /\bNOT\b/i ) {
2212         # if the op is negative, do the join as though
2213         # the op were positive, then accept only records
2214         # where the right-side join key is null.
2215         $is_negative = 1;
2216         $op = '=' if $op eq '!=';
2217         $op =~ s/\bNOT\b//;
2218     }
2219
2220     my (@alias, $table2, $subfield, $pkey);
2221     if ( $field eq 'Customer' ) {
2222       push @alias, $self->JoinToCustomer;
2223       push @alias, $self->JoinToCustomerViaService;
2224       $pkey = 'custnum';
2225     }
2226     elsif ( $field eq 'Service' ) {
2227       push @alias, $self->JoinToService;
2228       $pkey = 'svcnum';
2229     }
2230     else {
2231       die "malformed Freeside query: $field";
2232     }
2233
2234     $subfield = $rest{SUBKEY} || $pkey;
2235     # compound subkey: separate into table name and field in that table
2236     # (must be linked by custnum)
2237     $subfield = lc($subfield);
2238     ($table2, $subfield) = ($1, $2) if $subfield =~ /^(\w+)?\.(\w+)$/;
2239     $subfield = $pkey if $subfield eq 'number';
2240
2241     # if it's compound, create a join from cust_main or cust_svc to that 
2242     # table, using custnum or svcnum, and Limit on that table instead.
2243     my @_SQLLimit = ();
2244     foreach my $a (@alias) {
2245       if ( $table2 ) {
2246           $a = $self->Join(
2247               TYPE        => 'LEFT',
2248               ALIAS1      => $a,
2249               FIELD1      => $pkey,
2250               TABLE2      => $table2,
2251               FIELD2      => $pkey,
2252           );
2253       }
2254
2255       # do the actual Limit
2256       $self->SUPER::Limit(
2257           LEFTJOIN        => $a,
2258           FIELD           => $subfield,
2259           OPERATOR        => $op,
2260           VALUE           => $value,
2261           ENTRYAGGREGATOR => 'AND',
2262           # no SUBCLAUSE needed, limits on different aliases across left joins
2263           # are inherently independent
2264       );
2265
2266       # then, since it's a left join, exclude tickets for which there is now 
2267       # no matching record in the table we just limited on.  (Or where there 
2268       # is a matching record, if $is_negative.)
2269       # For a cust_main query (where there are two different aliases), this 
2270       # will produce a subclause: "cust_main_1.custnum IS NOT NULL OR 
2271       # cust_main_2.custnum IS NOT NULL" (or "IS NULL AND..." for a negative
2272       # query).
2273       #$self->_SQLLimit(
2274       push @_SQLLimit, {
2275           %rest,
2276           ALIAS           => $a,
2277           FIELD           => $pkey,
2278           OPERATOR        => $is_negative ? 'IS' : 'IS NOT',
2279           VALUE           => 'NULL',
2280           QUOTEVALUE      => 0,
2281           ENTRYAGGREGATOR => $is_negative ? 'AND' : 'OR',
2282           SUBCLAUSE       => 'fs_limit',
2283       };
2284     }
2285
2286     $self->_OpenParen;
2287     foreach my $_SQLLimit (@_SQLLimit) {
2288       $self->_SQLLimit( %$_SQLLimit);
2289     }
2290     $self->_CloseParen;
2291
2292 }
2293
2294 #Freeside
2295
2296 =head2 Limit
2297
2298 Takes a paramhash with the fields FIELD, OPERATOR, VALUE and DESCRIPTION
2299 Generally best called from LimitFoo methods
2300
2301 =cut
2302
2303 sub Limit {
2304     my $self = shift;
2305     my %args = (
2306         FIELD       => undef,
2307         OPERATOR    => '=',
2308         VALUE       => undef,
2309         DESCRIPTION => undef,
2310         @_
2311     );
2312     $args{'DESCRIPTION'} = $self->loc(
2313         "[_1] [_2] [_3]",  $args{'FIELD'},
2314         $args{'OPERATOR'}, $args{'VALUE'}
2315         )
2316         if ( !defined $args{'DESCRIPTION'} );
2317
2318     my $index = $self->_NextIndex;
2319
2320 # make the TicketRestrictions hash the equivalent of whatever we just passed in;
2321
2322     %{ $self->{'TicketRestrictions'}{$index} } = %args;
2323
2324     $self->{'RecalcTicketLimits'} = 1;
2325
2326 # If we're looking at the effective id, we don't want to append the other clause
2327 # which limits us to tickets where id = effective id
2328     if ( $args{'FIELD'} eq 'EffectiveId'
2329         && ( !$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) )
2330     {
2331         $self->{'looking_at_effective_id'} = 1;
2332     }
2333
2334     if ( $args{'FIELD'} eq 'Type'
2335         && ( !$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) )
2336     {
2337         $self->{'looking_at_type'} = 1;
2338     }
2339
2340     return ($index);
2341 }
2342
2343
2344
2345
2346 =head2 LimitQueue
2347
2348 LimitQueue takes a paramhash with the fields OPERATOR and VALUE.
2349 OPERATOR is one of = or !=. (It defaults to =).
2350 VALUE is a queue id or Name.
2351
2352
2353 =cut
2354
2355 sub LimitQueue {
2356     my $self = shift;
2357     my %args = (
2358         VALUE    => undef,
2359         OPERATOR => '=',
2360         @_
2361     );
2362
2363     #TODO  VALUE should also take queue objects
2364     if ( defined $args{'VALUE'} && $args{'VALUE'} !~ /^\d+$/ ) {
2365         my $queue = RT::Queue->new( $self->CurrentUser );
2366         $queue->Load( $args{'VALUE'} );
2367         $args{'VALUE'} = $queue->Id;
2368     }
2369
2370     # What if they pass in an Id?  Check for isNum() and convert to
2371     # string.
2372
2373     #TODO check for a valid queue here
2374
2375     $self->Limit(
2376         FIELD       => 'Queue',
2377         VALUE       => $args{'VALUE'},
2378         OPERATOR    => $args{'OPERATOR'},
2379         DESCRIPTION => join(
2380             ' ', $self->loc('Queue'), $args{'OPERATOR'}, $args{'VALUE'},
2381         ),
2382     );
2383
2384 }
2385
2386
2387
2388 =head2 LimitStatus
2389
2390 Takes a paramhash with the fields OPERATOR and VALUE.
2391 OPERATOR is one of = or !=.
2392 VALUE is a status.
2393
2394 RT adds Status != 'deleted' until object has
2395 allow_deleted_search internal property set.
2396 $tickets->{'allow_deleted_search'} = 1;
2397 $tickets->LimitStatus( VALUE => 'deleted' );
2398
2399 =cut
2400
2401 sub LimitStatus {
2402     my $self = shift;
2403     my %args = (
2404         OPERATOR => '=',
2405         @_
2406     );
2407     $self->Limit(
2408         FIELD       => 'Status',
2409         VALUE       => $args{'VALUE'},
2410         OPERATOR    => $args{'OPERATOR'},
2411         DESCRIPTION => join( ' ',
2412             $self->loc('Status'), $args{'OPERATOR'},
2413             $self->loc( $args{'VALUE'} ) ),
2414     );
2415 }
2416
2417
2418
2419 =head2 IgnoreType
2420
2421 If called, this search will not automatically limit the set of results found
2422 to tickets of type "Ticket". Tickets of other types, such as "project" and
2423 "approval" will be found.
2424
2425 =cut
2426
2427 sub IgnoreType {
2428     my $self = shift;
2429
2430     # Instead of faking a Limit that later gets ignored, fake up the
2431     # fact that we're already looking at type, so that the check in
2432     # Tickets_SQL/FromSQL goes down the right branch
2433
2434     #  $self->LimitType(VALUE => '__any');
2435     $self->{looking_at_type} = 1;
2436 }
2437
2438
2439
2440 =head2 LimitType
2441
2442 Takes a paramhash with the fields OPERATOR and VALUE.
2443 OPERATOR is one of = or !=, it defaults to "=".
2444 VALUE is a string to search for in the type of the ticket.
2445
2446
2447
2448 =cut
2449
2450 sub LimitType {
2451     my $self = shift;
2452     my %args = (
2453         OPERATOR => '=',
2454         VALUE    => undef,
2455         @_
2456     );
2457     $self->Limit(
2458         FIELD       => 'Type',
2459         VALUE       => $args{'VALUE'},
2460         OPERATOR    => $args{'OPERATOR'},
2461         DESCRIPTION => join( ' ',
2462             $self->loc('Type'), $args{'OPERATOR'}, $args{'Limit'}, ),
2463     );
2464 }
2465
2466
2467
2468
2469
2470 =head2 LimitSubject
2471
2472 Takes a paramhash with the fields OPERATOR and VALUE.
2473 OPERATOR is one of = or !=.
2474 VALUE is a string to search for in the subject of the ticket.
2475
2476 =cut
2477
2478 sub LimitSubject {
2479     my $self = shift;
2480     my %args = (@_);
2481     $self->Limit(
2482         FIELD       => 'Subject',
2483         VALUE       => $args{'VALUE'},
2484         OPERATOR    => $args{'OPERATOR'},
2485         DESCRIPTION => join( ' ',
2486             $self->loc('Subject'), $args{'OPERATOR'}, $args{'VALUE'}, ),
2487     );
2488 }
2489
2490
2491
2492 # Things that can be > < = !=
2493
2494
2495 =head2 LimitId
2496
2497 Takes a paramhash with the fields OPERATOR and VALUE.
2498 OPERATOR is one of =, >, < or !=.
2499 VALUE is a ticket Id to search for
2500
2501 =cut
2502
2503 sub LimitId {
2504     my $self = shift;
2505     my %args = (
2506         OPERATOR => '=',
2507         @_
2508     );
2509
2510     $self->Limit(
2511         FIELD       => 'id',
2512         VALUE       => $args{'VALUE'},
2513         OPERATOR    => $args{'OPERATOR'},
2514         DESCRIPTION =>
2515             join( ' ', $self->loc('Id'), $args{'OPERATOR'}, $args{'VALUE'}, ),
2516     );
2517 }
2518
2519
2520
2521 =head2 LimitPriority
2522
2523 Takes a paramhash with the fields OPERATOR and VALUE.
2524 OPERATOR is one of =, >, < or !=.
2525 VALUE is a value to match the ticket\'s priority against
2526
2527 =cut
2528
2529 sub LimitPriority {
2530     my $self = shift;
2531     my %args = (@_);
2532     $self->Limit(
2533         FIELD       => 'Priority',
2534         VALUE       => $args{'VALUE'},
2535         OPERATOR    => $args{'OPERATOR'},
2536         DESCRIPTION => join( ' ',
2537             $self->loc('Priority'),
2538             $args{'OPERATOR'}, $args{'VALUE'}, ),
2539     );
2540 }
2541
2542
2543
2544 =head2 LimitInitialPriority
2545
2546 Takes a paramhash with the fields OPERATOR and VALUE.
2547 OPERATOR is one of =, >, < or !=.
2548 VALUE is a value to match the ticket\'s initial priority against
2549
2550
2551 =cut
2552
2553 sub LimitInitialPriority {
2554     my $self = shift;
2555     my %args = (@_);
2556     $self->Limit(
2557         FIELD       => 'InitialPriority',
2558         VALUE       => $args{'VALUE'},
2559         OPERATOR    => $args{'OPERATOR'},
2560         DESCRIPTION => join( ' ',
2561             $self->loc('Initial Priority'), $args{'OPERATOR'},
2562             $args{'VALUE'}, ),
2563     );
2564 }
2565
2566
2567
2568 =head2 LimitFinalPriority
2569
2570 Takes a paramhash with the fields OPERATOR and VALUE.
2571 OPERATOR is one of =, >, < or !=.
2572 VALUE is a value to match the ticket\'s final priority against
2573
2574 =cut
2575
2576 sub LimitFinalPriority {
2577     my $self = shift;
2578     my %args = (@_);
2579     $self->Limit(
2580         FIELD       => 'FinalPriority',
2581         VALUE       => $args{'VALUE'},
2582         OPERATOR    => $args{'OPERATOR'},
2583         DESCRIPTION => join( ' ',
2584             $self->loc('Final Priority'), $args{'OPERATOR'},
2585             $args{'VALUE'}, ),
2586     );
2587 }
2588
2589
2590
2591 =head2 LimitTimeWorked
2592
2593 Takes a paramhash with the fields OPERATOR and VALUE.
2594 OPERATOR is one of =, >, < or !=.
2595 VALUE is a value to match the ticket's TimeWorked attribute
2596
2597 =cut
2598
2599 sub LimitTimeWorked {
2600     my $self = shift;
2601     my %args = (@_);
2602     $self->Limit(
2603         FIELD       => 'TimeWorked',
2604         VALUE       => $args{'VALUE'},
2605         OPERATOR    => $args{'OPERATOR'},
2606         DESCRIPTION => join( ' ',
2607             $self->loc('Time Worked'),
2608             $args{'OPERATOR'}, $args{'VALUE'}, ),
2609     );
2610 }
2611
2612
2613
2614 =head2 LimitTimeLeft
2615
2616 Takes a paramhash with the fields OPERATOR and VALUE.
2617 OPERATOR is one of =, >, < or !=.
2618 VALUE is a value to match the ticket's TimeLeft attribute
2619
2620 =cut
2621
2622 sub LimitTimeLeft {
2623     my $self = shift;
2624     my %args = (@_);
2625     $self->Limit(
2626         FIELD       => 'TimeLeft',
2627         VALUE       => $args{'VALUE'},
2628         OPERATOR    => $args{'OPERATOR'},
2629         DESCRIPTION => join( ' ',
2630             $self->loc('Time Left'),
2631             $args{'OPERATOR'}, $args{'VALUE'}, ),
2632     );
2633 }
2634
2635
2636
2637
2638
2639 =head2 LimitContent
2640
2641 Takes a paramhash with the fields OPERATOR and VALUE.
2642 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2643 VALUE is a string to search for in the body of the ticket
2644
2645 =cut
2646
2647 sub LimitContent {
2648     my $self = shift;
2649     my %args = (@_);
2650     $self->Limit(
2651         FIELD       => 'Content',
2652         VALUE       => $args{'VALUE'},
2653         OPERATOR    => $args{'OPERATOR'},
2654         DESCRIPTION => join( ' ',
2655             $self->loc('Ticket content'), $args{'OPERATOR'},
2656             $args{'VALUE'}, ),
2657     );
2658 }
2659
2660
2661
2662 =head2 LimitFilename
2663
2664 Takes a paramhash with the fields OPERATOR and VALUE.
2665 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2666 VALUE is a string to search for in the body of the ticket
2667
2668 =cut
2669
2670 sub LimitFilename {
2671     my $self = shift;
2672     my %args = (@_);
2673     $self->Limit(
2674         FIELD       => 'Filename',
2675         VALUE       => $args{'VALUE'},
2676         OPERATOR    => $args{'OPERATOR'},
2677         DESCRIPTION => join( ' ',
2678             $self->loc('Attachment filename'), $args{'OPERATOR'},
2679             $args{'VALUE'}, ),
2680     );
2681 }
2682
2683
2684 =head2 LimitContentType
2685
2686 Takes a paramhash with the fields OPERATOR and VALUE.
2687 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2688 VALUE is a content type to search ticket attachments for
2689
2690 =cut
2691
2692 sub LimitContentType {
2693     my $self = shift;
2694     my %args = (@_);
2695     $self->Limit(
2696         FIELD       => 'ContentType',
2697         VALUE       => $args{'VALUE'},
2698         OPERATOR    => $args{'OPERATOR'},
2699         DESCRIPTION => join( ' ',
2700             $self->loc('Ticket content type'), $args{'OPERATOR'},
2701             $args{'VALUE'}, ),
2702     );
2703 }
2704
2705
2706
2707
2708
2709 =head2 LimitOwner
2710
2711 Takes a paramhash with the fields OPERATOR and VALUE.
2712 OPERATOR is one of = or !=.
2713 VALUE is a user id.
2714
2715 =cut
2716
2717 sub LimitOwner {
2718     my $self = shift;
2719     my %args = (
2720         OPERATOR => '=',
2721         @_
2722     );
2723
2724     my $owner = RT::User->new( $self->CurrentUser );
2725     $owner->Load( $args{'VALUE'} );
2726
2727     # FIXME: check for a valid $owner
2728     $self->Limit(
2729         FIELD       => 'Owner',
2730         VALUE       => $args{'VALUE'},
2731         OPERATOR    => $args{'OPERATOR'},
2732         DESCRIPTION => join( ' ',
2733             $self->loc('Owner'), $args{'OPERATOR'}, $owner->Name(), ),
2734     );
2735
2736 }
2737
2738
2739
2740
2741 =head2 LimitWatcher
2742
2743   Takes a paramhash with the fields OPERATOR, TYPE and VALUE.
2744   OPERATOR is one of =, LIKE, NOT LIKE or !=.
2745   VALUE is a value to match the ticket\'s watcher email addresses against
2746   TYPE is the sort of watchers you want to match against. Leave it undef if you want to search all of them
2747
2748
2749 =cut
2750
2751 sub LimitWatcher {
2752     my $self = shift;
2753     my %args = (
2754         OPERATOR => '=',
2755         VALUE    => undef,
2756         TYPE     => undef,
2757         @_
2758     );
2759
2760     #build us up a description
2761     my ( $watcher_type, $desc );
2762     if ( $args{'TYPE'} ) {
2763         $watcher_type = $args{'TYPE'};
2764     }
2765     else {
2766         $watcher_type = "Watcher";
2767     }
2768
2769     $self->Limit(
2770         FIELD       => $watcher_type,
2771         VALUE       => $args{'VALUE'},
2772         OPERATOR    => $args{'OPERATOR'},
2773         TYPE        => $args{'TYPE'},
2774         DESCRIPTION => join( ' ',
2775             $self->loc($watcher_type),
2776             $args{'OPERATOR'}, $args{'VALUE'}, ),
2777     );
2778 }
2779
2780
2781
2782
2783
2784
2785 =head2 LimitLinkedTo
2786
2787 LimitLinkedTo takes a paramhash with two fields: TYPE and TARGET
2788 TYPE limits the sort of link we want to search on
2789
2790 TYPE = { RefersTo, MemberOf, DependsOn }
2791
2792 TARGET is the id or URI of the TARGET of the link
2793
2794 =cut
2795
2796 sub LimitLinkedTo {
2797     my $self = shift;
2798     my %args = (
2799         TARGET   => undef,
2800         TYPE     => undef,
2801         OPERATOR => '=',
2802         @_
2803     );
2804
2805     $self->Limit(
2806         FIELD       => 'LinkedTo',
2807         BASE        => undef,
2808         TARGET      => $args{'TARGET'},
2809         TYPE        => $args{'TYPE'},
2810         DESCRIPTION => $self->loc(
2811             "Tickets [_1] by [_2]",
2812             $self->loc( $args{'TYPE'} ),
2813             $args{'TARGET'}
2814         ),
2815         OPERATOR    => $args{'OPERATOR'},
2816     );
2817 }
2818
2819
2820
2821 =head2 LimitLinkedFrom
2822
2823 LimitLinkedFrom takes a paramhash with two fields: TYPE and BASE
2824 TYPE limits the sort of link we want to search on
2825
2826
2827 BASE is the id or URI of the BASE of the link
2828
2829 =cut
2830
2831 sub LimitLinkedFrom {
2832     my $self = shift;
2833     my %args = (
2834         BASE     => undef,
2835         TYPE     => undef,
2836         OPERATOR => '=',
2837         @_
2838     );
2839
2840     # translate RT2 From/To naming to RT3 TicketSQL naming
2841     my %fromToMap = qw(DependsOn DependentOn
2842         MemberOf  HasMember
2843         RefersTo  ReferredToBy);
2844
2845     my $type = $args{'TYPE'};
2846     $type = $fromToMap{$type} if exists( $fromToMap{$type} );
2847
2848     $self->Limit(
2849         FIELD       => 'LinkedTo',
2850         TARGET      => undef,
2851         BASE        => $args{'BASE'},
2852         TYPE        => $type,
2853         DESCRIPTION => $self->loc(
2854             "Tickets [_1] [_2]",
2855             $self->loc( $args{'TYPE'} ),
2856             $args{'BASE'},
2857         ),
2858         OPERATOR    => $args{'OPERATOR'},
2859     );
2860 }
2861
2862
2863 sub LimitMemberOf {
2864     my $self      = shift;
2865     my $ticket_id = shift;
2866     return $self->LimitLinkedTo(
2867         @_,
2868         TARGET => $ticket_id,
2869         TYPE   => 'MemberOf',
2870     );
2871 }
2872
2873
2874 sub LimitHasMember {
2875     my $self      = shift;
2876     my $ticket_id = shift;
2877     return $self->LimitLinkedFrom(
2878         @_,
2879         BASE => "$ticket_id",
2880         TYPE => 'HasMember',
2881     );
2882
2883 }
2884
2885
2886
2887 sub LimitDependsOn {
2888     my $self      = shift;
2889     my $ticket_id = shift;
2890     return $self->LimitLinkedTo(
2891         @_,
2892         TARGET => $ticket_id,
2893         TYPE   => 'DependsOn',
2894     );
2895
2896 }
2897
2898
2899
2900 sub LimitDependedOnBy {
2901     my $self      = shift;
2902     my $ticket_id = shift;
2903     return $self->LimitLinkedFrom(
2904         @_,
2905         BASE => $ticket_id,
2906         TYPE => 'DependentOn',
2907     );
2908
2909 }
2910
2911
2912
2913 sub LimitRefersTo {
2914     my $self      = shift;
2915     my $ticket_id = shift;
2916     return $self->LimitLinkedTo(
2917         @_,
2918         TARGET => $ticket_id,
2919         TYPE   => 'RefersTo',
2920     );
2921
2922 }
2923
2924
2925
2926 sub LimitReferredToBy {
2927     my $self      = shift;
2928     my $ticket_id = shift;
2929     return $self->LimitLinkedFrom(
2930         @_,
2931         BASE => $ticket_id,
2932         TYPE => 'ReferredToBy',
2933     );
2934 }
2935
2936
2937
2938
2939
2940 =head2 LimitDate (FIELD => 'DateField', OPERATOR => $oper, VALUE => $ISODate)
2941
2942 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
2943
2944 OPERATOR is one of > or <
2945 VALUE is a date and time in ISO format in GMT
2946 FIELD is one of Starts, Started, Told, Created, Resolved, LastUpdated
2947
2948 There are also helper functions of the form LimitFIELD that eliminate
2949 the need to pass in a FIELD argument.
2950
2951 =cut
2952
2953 sub LimitDate {
2954     my $self = shift;
2955     my %args = (
2956         FIELD    => undef,
2957         VALUE    => undef,
2958         OPERATOR => undef,
2959
2960         @_
2961     );
2962
2963     #Set the description if we didn't get handed it above
2964     unless ( $args{'DESCRIPTION'} ) {
2965         $args{'DESCRIPTION'} = $args{'FIELD'} . " "
2966             . $args{'OPERATOR'} . " "
2967             . $args{'VALUE'} . " GMT";
2968     }
2969
2970     $self->Limit(%args);
2971
2972 }
2973
2974
2975 sub LimitCreated {
2976     my $self = shift;
2977     $self->LimitDate( FIELD => 'Created', @_ );
2978 }
2979
2980 sub LimitDue {
2981     my $self = shift;
2982     $self->LimitDate( FIELD => 'Due', @_ );
2983
2984 }
2985
2986 sub LimitStarts {
2987     my $self = shift;
2988     $self->LimitDate( FIELD => 'Starts', @_ );
2989
2990 }
2991
2992 sub LimitStarted {
2993     my $self = shift;
2994     $self->LimitDate( FIELD => 'Started', @_ );
2995 }
2996
2997 sub LimitResolved {
2998     my $self = shift;
2999     $self->LimitDate( FIELD => 'Resolved', @_ );
3000 }
3001
3002 sub LimitTold {
3003     my $self = shift;
3004     $self->LimitDate( FIELD => 'Told', @_ );
3005 }
3006
3007 sub LimitLastUpdated {
3008     my $self = shift;
3009     $self->LimitDate( FIELD => 'LastUpdated', @_ );
3010 }
3011
3012 #
3013
3014 =head2 LimitTransactionDate (OPERATOR => $oper, VALUE => $ISODate)
3015
3016 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
3017
3018 OPERATOR is one of > or <
3019 VALUE is a date and time in ISO format in GMT
3020
3021
3022 =cut
3023
3024 sub LimitTransactionDate {
3025     my $self = shift;
3026     my %args = (
3027         FIELD    => 'TransactionDate',
3028         VALUE    => undef,
3029         OPERATOR => undef,
3030
3031         @_
3032     );
3033
3034     #  <20021217042756.GK28744@pallas.fsck.com>
3035     #    "Kill It" - Jesse.
3036
3037     #Set the description if we didn't get handed it above
3038     unless ( $args{'DESCRIPTION'} ) {
3039         $args{'DESCRIPTION'} = $args{'FIELD'} . " "
3040             . $args{'OPERATOR'} . " "
3041             . $args{'VALUE'} . " GMT";
3042     }
3043
3044     $self->Limit(%args);
3045
3046 }
3047
3048
3049
3050
3051 =head2 LimitCustomField
3052
3053 Takes a paramhash of key/value pairs with the following keys:
3054
3055 =over 4
3056
3057 =item CUSTOMFIELD - CustomField name or id.  If a name is passed, an additional parameter QUEUE may also be passed to distinguish the custom field.
3058
3059 =item OPERATOR - The usual Limit operators
3060
3061 =item VALUE - The value to compare against
3062
3063 =back
3064
3065 =cut
3066
3067 sub LimitCustomField {
3068     my $self = shift;
3069     my %args = (
3070         VALUE       => undef,
3071         CUSTOMFIELD => undef,
3072         OPERATOR    => '=',
3073         DESCRIPTION => undef,
3074         FIELD       => 'CustomFieldValue',
3075         QUOTEVALUE  => 1,
3076         @_
3077     );
3078
3079     my $CF = RT::CustomField->new( $self->CurrentUser );
3080     if ( $args{CUSTOMFIELD} =~ /^\d+$/ ) {
3081         $CF->Load( $args{CUSTOMFIELD} );
3082     }
3083     else {
3084         $CF->LoadByNameAndQueue(
3085             Name  => $args{CUSTOMFIELD},
3086             Queue => $args{QUEUE}
3087         );
3088         $args{CUSTOMFIELD} = $CF->Id;
3089     }
3090
3091     #If we are looking to compare with a null value.
3092     if ( $args{'OPERATOR'} =~ /^is$/i ) {
3093         $args{'DESCRIPTION'}
3094             ||= $self->loc( "Custom field [_1] has no value.", $CF->Name );
3095     }
3096     elsif ( $args{'OPERATOR'} =~ /^is not$/i ) {
3097         $args{'DESCRIPTION'}
3098             ||= $self->loc( "Custom field [_1] has a value.", $CF->Name );
3099     }
3100
3101     # if we're not looking to compare with a null value
3102     else {
3103         $args{'DESCRIPTION'} ||= $self->loc( "Custom field [_1] [_2] [_3]",
3104             $CF->Name, $args{OPERATOR}, $args{VALUE} );
3105     }
3106
3107     if ( defined $args{'QUEUE'} && $args{'QUEUE'} =~ /\D/ ) {
3108         my $QueueObj = RT::Queue->new( $self->CurrentUser );
3109         $QueueObj->Load( $args{'QUEUE'} );
3110         $args{'QUEUE'} = $QueueObj->Id;
3111     }
3112     delete $args{'QUEUE'} unless defined $args{'QUEUE'} && length $args{'QUEUE'};
3113
3114     my @rest;
3115     @rest = ( ENTRYAGGREGATOR => 'AND' )
3116         if ( $CF->Type eq 'SelectMultiple' );
3117
3118     $self->Limit(
3119         VALUE => $args{VALUE},
3120         FIELD => "CF"
3121             .(defined $args{'QUEUE'}? ".{$args{'QUEUE'}}" : '' )
3122             .".{" . $CF->Name . "}",
3123         OPERATOR    => $args{OPERATOR},
3124         CUSTOMFIELD => 1,
3125         @rest,
3126     );
3127
3128     $self->{'RecalcTicketLimits'} = 1;
3129 }
3130
3131
3132
3133 =head2 _NextIndex
3134
3135 Keep track of the counter for the array of restrictions
3136
3137 =cut
3138
3139 sub _NextIndex {
3140     my $self = shift;
3141     return ( $self->{'restriction_index'}++ );
3142 }
3143
3144
3145
3146
3147 sub _Init {
3148     my $self = shift;
3149     $self->{'table'}                   = "Tickets";
3150     $self->{'RecalcTicketLimits'}      = 1;
3151     $self->{'looking_at_effective_id'} = 0;
3152     $self->{'looking_at_type'}         = 0;
3153     $self->{'restriction_index'}       = 1;
3154     $self->{'primary_key'}             = "id";
3155     delete $self->{'items_array'};
3156     delete $self->{'item_map'};
3157     delete $self->{'columns_to_display'};
3158     $self->SUPER::_Init(@_);
3159
3160     $self->_InitSQL;
3161
3162 }
3163
3164
3165 sub Count {
3166     my $self = shift;
3167     $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
3168     return ( $self->SUPER::Count() );
3169 }
3170
3171
3172 sub CountAll {
3173     my $self = shift;
3174     $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
3175     return ( $self->SUPER::CountAll() );
3176 }
3177
3178
3179
3180 =head2 ItemsArrayRef
3181
3182 Returns a reference to the set of all items found in this search
3183
3184 =cut
3185
3186 sub ItemsArrayRef {
3187     my $self = shift;
3188
3189     return $self->{'items_array'} if $self->{'items_array'};
3190
3191     my $placeholder = $self->_ItemsCounter;
3192     $self->GotoFirstItem();
3193     while ( my $item = $self->Next ) {
3194         push( @{ $self->{'items_array'} }, $item );
3195     }
3196     $self->GotoItem($placeholder);
3197     $self->{'items_array'}
3198         = $self->ItemsOrderBy( $self->{'items_array'} );
3199
3200     return $self->{'items_array'};
3201 }
3202
3203 sub ItemsArrayRefWindow {
3204     my $self = shift;
3205     my $window = shift;
3206
3207     my @old = ($self->_ItemsCounter, $self->RowsPerPage, $self->FirstRow+1);
3208
3209     $self->RowsPerPage( $window );
3210     $self->FirstRow(1);
3211     $self->GotoFirstItem;
3212
3213     my @res;
3214     while ( my $item = $self->Next ) {
3215         push @res, $item;
3216     }
3217
3218     $self->RowsPerPage( $old[1] );
3219     $self->FirstRow( $old[2] );
3220     $self->GotoItem( $old[0] );
3221
3222     return \@res;
3223 }
3224
3225
3226 sub Next {
3227     my $self = shift;
3228
3229     $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
3230
3231     my $Ticket = $self->SUPER::Next;
3232     return $Ticket unless $Ticket;
3233
3234     if ( $Ticket->__Value('Status') eq 'deleted'
3235         && !$self->{'allow_deleted_search'} )
3236     {
3237         return $self->Next;
3238     }
3239     elsif ( RT->Config->Get('UseSQLForACLChecks') ) {
3240         # if we found a ticket with this option enabled then
3241         # all tickets we found are ACLed, cache this fact
3242         my $key = join ";:;", $self->CurrentUser->id, 'ShowTicket', 'RT::Ticket-'. $Ticket->id;
3243         $RT::Principal::_ACL_CACHE->set( $key => 1 );
3244         return $Ticket;
3245     }
3246     elsif ( $Ticket->CurrentUserHasRight('ShowTicket') ) {
3247         # has rights
3248         return $Ticket;
3249     }
3250     else {
3251         # If the user doesn't have the right to show this ticket
3252         return $self->Next;
3253     }
3254 }
3255
3256 sub _DoSearch {
3257     my $self = shift;
3258     $self->CurrentUserCanSee if RT->Config->Get('UseSQLForACLChecks');
3259     return $self->SUPER::_DoSearch( @_ );
3260 }
3261
3262 sub _DoCount {
3263     my $self = shift;
3264     $self->CurrentUserCanSee if RT->Config->Get('UseSQLForACLChecks');
3265     return $self->SUPER::_DoCount( @_ );
3266 }
3267
3268 sub _RolesCanSee {
3269     my $self = shift;
3270
3271     my $cache_key = 'RolesHasRight;:;ShowTicket';
3272  
3273     if ( my $cached = $RT::Principal::_ACL_CACHE->fetch( $cache_key ) ) {
3274         return %$cached;
3275     }
3276
3277     my $ACL = RT::ACL->new( RT->SystemUser );
3278     $ACL->Limit( FIELD => 'RightName', VALUE => 'ShowTicket' );
3279     $ACL->Limit( FIELD => 'PrincipalType', OPERATOR => '!=', VALUE => 'Group' );
3280     my $principal_alias = $ACL->Join(
3281         ALIAS1 => 'main',
3282         FIELD1 => 'PrincipalId',
3283         TABLE2 => 'Principals',
3284         FIELD2 => 'id',
3285     );
3286     $ACL->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3287
3288     my %res = ();
3289     foreach my $ACE ( @{ $ACL->ItemsArrayRef } ) {
3290         my $role = $ACE->__Value('PrincipalType');
3291         my $type = $ACE->__Value('ObjectType');
3292         if ( $type eq 'RT::System' ) {
3293             $res{ $role } = 1;
3294         }
3295         elsif ( $type eq 'RT::Queue' ) {
3296             next if $res{ $role } && !ref $res{ $role };
3297             push @{ $res{ $role } ||= [] }, $ACE->__Value('ObjectId');
3298         }
3299         else {
3300             $RT::Logger->error('ShowTicket right is granted on unsupported object');
3301         }
3302     }
3303     $RT::Principal::_ACL_CACHE->set( $cache_key => \%res );
3304     return %res;
3305 }
3306
3307 sub _DirectlyCanSeeIn {
3308     my $self = shift;
3309     my $id = $self->CurrentUser->id;
3310
3311     my $cache_key = 'User-'. $id .';:;ShowTicket;:;DirectlyCanSeeIn';
3312     if ( my $cached = $RT::Principal::_ACL_CACHE->fetch( $cache_key ) ) {
3313         return @$cached;
3314     }
3315
3316     my $ACL = RT::ACL->new( RT->SystemUser );
3317     $ACL->Limit( FIELD => 'RightName', VALUE => 'ShowTicket' );
3318     my $principal_alias = $ACL->Join(
3319         ALIAS1 => 'main',
3320         FIELD1 => 'PrincipalId',
3321         TABLE2 => 'Principals',
3322         FIELD2 => 'id',
3323     );
3324     $ACL->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3325     my $cgm_alias = $ACL->Join(
3326         ALIAS1 => 'main',
3327         FIELD1 => 'PrincipalId',
3328         TABLE2 => 'CachedGroupMembers',
3329         FIELD2 => 'GroupId',
3330     );
3331     $ACL->Limit( ALIAS => $cgm_alias, FIELD => 'MemberId', VALUE => $id );
3332     $ACL->Limit( ALIAS => $cgm_alias, FIELD => 'Disabled', VALUE => 0 );
3333
3334     my @res = ();
3335     foreach my $ACE ( @{ $ACL->ItemsArrayRef } ) {
3336         my $type = $ACE->__Value('ObjectType');
3337         if ( $type eq 'RT::System' ) {
3338             # If user is direct member of a group that has the right
3339             # on the system then he can see any ticket
3340             $RT::Principal::_ACL_CACHE->set( $cache_key => [-1] );
3341             return (-1);
3342         }
3343         elsif ( $type eq 'RT::Queue' ) {
3344             push @res, $ACE->__Value('ObjectId');
3345         }
3346         else {
3347             $RT::Logger->error('ShowTicket right is granted on unsupported object');
3348         }
3349     }
3350     $RT::Principal::_ACL_CACHE->set( $cache_key => \@res );
3351     return @res;
3352 }
3353
3354 sub CurrentUserCanSee {
3355     my $self = shift;
3356     return if $self->{'_sql_current_user_can_see_applied'};
3357
3358     return $self->{'_sql_current_user_can_see_applied'} = 1
3359         if $self->CurrentUser->UserObj->HasRight(
3360             Right => 'SuperUser', Object => $RT::System
3361         );
3362
3363     my $id = $self->CurrentUser->id;
3364
3365     # directly can see in all queues then we have nothing to do
3366     my @direct_queues = $self->_DirectlyCanSeeIn;
3367     return $self->{'_sql_current_user_can_see_applied'} = 1
3368         if @direct_queues && $direct_queues[0] == -1;
3369
3370     my %roles = $self->_RolesCanSee;
3371     {
3372         my %skip = map { $_ => 1 } @direct_queues;
3373         foreach my $role ( keys %roles ) {
3374             next unless ref $roles{ $role };
3375
3376             my @queues = grep !$skip{$_}, @{ $roles{ $role } };
3377             if ( @queues ) {
3378                 $roles{ $role } = \@queues;
3379             } else {
3380                 delete $roles{ $role };
3381             }
3382         }
3383     }
3384
3385 # there is no global watchers, only queues and tickes, if at
3386 # some point we will add global roles then it's gonna blow
3387 # the idea here is that if the right is set globaly for a role
3388 # and user plays this role for a queue directly not a ticket
3389 # then we have to check in advance
3390     if ( my @tmp = grep $_ ne 'Owner' && !ref $roles{ $_ }, keys %roles ) {
3391
3392         my $groups = RT::Groups->new( RT->SystemUser );
3393         $groups->Limit( FIELD => 'Domain', VALUE => 'RT::Queue-Role' );
3394         foreach ( @tmp ) {
3395             $groups->Limit( FIELD => 'Type', VALUE => $_ );
3396         }
3397         my $principal_alias = $groups->Join(
3398             ALIAS1 => 'main',
3399             FIELD1 => 'id',
3400             TABLE2 => 'Principals',
3401             FIELD2 => 'id',
3402         );
3403         $groups->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3404         my $cgm_alias = $groups->Join(
3405             ALIAS1 => 'main',
3406             FIELD1 => 'id',
3407             TABLE2 => 'CachedGroupMembers',
3408             FIELD2 => 'GroupId',
3409         );
3410         $groups->Limit( ALIAS => $cgm_alias, FIELD => 'MemberId', VALUE => $id );
3411         $groups->Limit( ALIAS => $cgm_alias, FIELD => 'Disabled', VALUE => 0 );
3412         while ( my $group = $groups->Next ) {
3413             push @direct_queues, $group->Instance;
3414         }
3415     }
3416
3417     unless ( @direct_queues || keys %roles ) {
3418         $self->SUPER::Limit(
3419             SUBCLAUSE => 'ACL',
3420             ALIAS => 'main',
3421             FIELD => 'id',
3422             VALUE => 0,
3423             ENTRYAGGREGATOR => 'AND',
3424         );
3425         return $self->{'_sql_current_user_can_see_applied'} = 1;
3426     }
3427
3428     {
3429         my $join_roles = keys %roles;
3430         $join_roles = 0 if $join_roles == 1 && $roles{'Owner'};
3431         my ($role_group_alias, $cgm_alias);
3432         if ( $join_roles ) {
3433             $role_group_alias = $self->_RoleGroupsJoin( New => 1 );
3434             $cgm_alias = $self->_GroupMembersJoin( GroupsAlias => $role_group_alias );
3435             $self->SUPER::Limit(
3436                 LEFTJOIN   => $cgm_alias,
3437                 FIELD      => 'MemberId',
3438                 OPERATOR   => '=',
3439                 VALUE      => $id,
3440             );
3441         }
3442         my $limit_queues = sub {
3443             my $ea = shift;
3444             my @queues = @_;
3445
3446             return unless @queues;
3447             if ( @queues == 1 ) {
3448                 $self->SUPER::Limit(
3449                     SUBCLAUSE => 'ACL',
3450                     ALIAS => 'main',
3451                     FIELD => 'Queue',
3452                     VALUE => $_[0],
3453                     ENTRYAGGREGATOR => $ea,
3454                 );
3455             } else {
3456                 $self->SUPER::_OpenParen('ACL');
3457                 foreach my $q ( @queues ) {
3458                     $self->SUPER::Limit(
3459                         SUBCLAUSE => 'ACL',
3460                         ALIAS => 'main',
3461                         FIELD => 'Queue',
3462                         VALUE => $q,
3463                         ENTRYAGGREGATOR => $ea,
3464                     );
3465                     $ea = 'OR';
3466                 }
3467                 $self->SUPER::_CloseParen('ACL');
3468             }
3469             return 1;
3470         };
3471
3472         $self->SUPER::_OpenParen('ACL');
3473         my $ea = 'AND';
3474         $ea = 'OR' if $limit_queues->( $ea, @direct_queues );
3475         while ( my ($role, $queues) = each %roles ) {
3476             $self->SUPER::_OpenParen('ACL');
3477             if ( $role eq 'Owner' ) {
3478                 $self->SUPER::Limit(
3479                     SUBCLAUSE => 'ACL',
3480                     FIELD           => 'Owner',
3481                     VALUE           => $id,
3482                     ENTRYAGGREGATOR => $ea,
3483                 );
3484             }
3485             else {
3486                 $self->SUPER::Limit(
3487                     SUBCLAUSE       => 'ACL',
3488                     ALIAS           => $cgm_alias,
3489                     FIELD           => 'MemberId',
3490                     OPERATOR        => 'IS NOT',
3491                     VALUE           => 'NULL',
3492                     QUOTEVALUE      => 0,
3493                     ENTRYAGGREGATOR => $ea,
3494                 );
3495                 $self->SUPER::Limit(
3496                     SUBCLAUSE       => 'ACL',
3497                     ALIAS           => $role_group_alias,
3498                     FIELD           => 'Type',
3499                     VALUE           => $role,
3500                     ENTRYAGGREGATOR => 'AND',
3501                 );
3502             }
3503             $limit_queues->( 'AND', @$queues ) if ref $queues;
3504             $ea = 'OR' if $ea eq 'AND';
3505             $self->SUPER::_CloseParen('ACL');
3506         }
3507         $self->SUPER::_CloseParen('ACL');
3508     }
3509     return $self->{'_sql_current_user_can_see_applied'} = 1;
3510 }
3511
3512
3513
3514
3515
3516 =head2 LoadRestrictions
3517
3518 LoadRestrictions takes a string which can fully populate the TicketRestrictons hash.
3519 TODO It is not yet implemented
3520
3521 =cut
3522
3523
3524
3525 =head2 DescribeRestrictions
3526
3527 takes nothing.
3528 Returns a hash keyed by restriction id.
3529 Each element of the hash is currently a one element hash that contains DESCRIPTION which
3530 is a description of the purpose of that TicketRestriction
3531
3532 =cut
3533
3534 sub DescribeRestrictions {
3535     my $self = shift;
3536
3537     my %listing;
3538
3539     foreach my $row ( keys %{ $self->{'TicketRestrictions'} } ) {
3540         $listing{$row} = $self->{'TicketRestrictions'}{$row}{'DESCRIPTION'};
3541     }
3542     return (%listing);
3543 }
3544
3545
3546
3547 =head2 RestrictionValues FIELD
3548
3549 Takes a restriction field and returns a list of values this field is restricted
3550 to.
3551
3552 =cut
3553
3554 sub RestrictionValues {
3555     my $self  = shift;
3556     my $field = shift;
3557     map $self->{'TicketRestrictions'}{$_}{'VALUE'}, grep {
3558                $self->{'TicketRestrictions'}{$_}{'FIELD'}    eq $field
3559             && $self->{'TicketRestrictions'}{$_}{'OPERATOR'} eq "="
3560         }
3561         keys %{ $self->{'TicketRestrictions'} };
3562 }
3563
3564
3565
3566 =head2 ClearRestrictions
3567
3568 Removes all restrictions irretrievably
3569
3570 =cut
3571
3572 sub ClearRestrictions {
3573     my $self = shift;
3574     delete $self->{'TicketRestrictions'};
3575     $self->{'looking_at_effective_id'} = 0;
3576     $self->{'looking_at_type'}         = 0;
3577     $self->{'RecalcTicketLimits'}      = 1;
3578 }
3579
3580
3581
3582 =head2 DeleteRestriction
3583
3584 Takes the row Id of a restriction (From DescribeRestrictions' output, for example.
3585 Removes that restriction from the session's limits.
3586
3587 =cut
3588
3589 sub DeleteRestriction {
3590     my $self = shift;
3591     my $row  = shift;
3592     delete $self->{'TicketRestrictions'}{$row};
3593
3594     $self->{'RecalcTicketLimits'} = 1;
3595
3596     #make the underlying easysearch object forget all its preconceptions
3597 }
3598
3599
3600
3601 # Convert a set of oldstyle SB Restrictions to Clauses for RQL
3602
3603 sub _RestrictionsToClauses {
3604     my $self = shift;
3605
3606     my %clause;
3607     foreach my $row ( keys %{ $self->{'TicketRestrictions'} } ) {
3608         my $restriction = $self->{'TicketRestrictions'}{$row};
3609
3610         # We need to reimplement the subclause aggregation that SearchBuilder does.
3611         # Default Subclause is ALIAS.FIELD, and default ALIAS is 'main',
3612         # Then SB AND's the different Subclauses together.
3613
3614         # So, we want to group things into Subclauses, convert them to
3615         # SQL, and then join them with the appropriate DefaultEA.
3616         # Then join each subclause group with AND.
3617
3618         my $field = $restriction->{'FIELD'};
3619         my $realfield = $field;    # CustomFields fake up a fieldname, so
3620                                    # we need to figure that out
3621
3622         # One special case
3623         # Rewrite LinkedTo meta field to the real field
3624         if ( $field =~ /LinkedTo/ ) {
3625             $realfield = $field = $restriction->{'TYPE'};
3626         }
3627
3628         # Two special case
3629         # Handle subkey fields with a different real field
3630         if ( $field =~ /^(\w+)\./ ) {
3631             $realfield = $1;
3632         }
3633
3634         die "I don't know about $field yet"
3635             unless ( exists $FIELD_METADATA{$realfield}
3636                 or $restriction->{CUSTOMFIELD} );
3637
3638         my $type = $FIELD_METADATA{$realfield}->[0];
3639         my $op   = $restriction->{'OPERATOR'};
3640
3641         my $value = (
3642             grep    {defined}
3643                 map { $restriction->{$_} } qw(VALUE TICKET BASE TARGET)
3644         )[0];
3645
3646         # this performs the moral equivalent of defined or/dor/C<//>,
3647         # without the short circuiting.You need to use a 'defined or'
3648         # type thing instead of just checking for truth values, because
3649         # VALUE could be 0.(i.e. "false")
3650
3651         # You could also use this, but I find it less aesthetic:
3652         # (although it does short circuit)
3653         #( defined $restriction->{'VALUE'}? $restriction->{VALUE} :
3654         # defined $restriction->{'TICKET'} ?
3655         # $restriction->{TICKET} :
3656         # defined $restriction->{'BASE'} ?
3657         # $restriction->{BASE} :
3658         # defined $restriction->{'TARGET'} ?
3659         # $restriction->{TARGET} )
3660
3661         my $ea = $restriction->{ENTRYAGGREGATOR}
3662             || $DefaultEA{$type}
3663             || "AND";
3664         if ( ref $ea ) {
3665             die "Invalid operator $op for $field ($type)"
3666                 unless exists $ea->{$op};
3667             $ea = $ea->{$op};
3668         }
3669
3670         # Each CustomField should be put into a different Clause so they
3671         # are ANDed together.
3672         if ( $restriction->{CUSTOMFIELD} ) {
3673             $realfield = $field;
3674         }
3675
3676         exists $clause{$realfield} or $clause{$realfield} = [];
3677
3678         # Escape Quotes
3679         $field =~ s!(['\\])!\\$1!g;
3680         $value =~ s!(['\\])!\\$1!g;
3681         my $data = [ $ea, $type, $field, $op, $value ];
3682
3683         # here is where we store extra data, say if it's a keyword or
3684         # something.  (I.e. "TYPE SPECIFIC STUFF")
3685
3686         if (lc $ea eq 'none') {
3687             $clause{$realfield} = [ $data ];
3688         } else {
3689             push @{ $clause{$realfield} }, $data;
3690         }
3691     }
3692     return \%clause;
3693 }
3694
3695
3696
3697 =head2 _ProcessRestrictions PARAMHASH
3698
3699 # The new _ProcessRestrictions is somewhat dependent on the SQL stuff,
3700 # but isn't quite generic enough to move into Tickets_SQL.
3701
3702 =cut
3703
3704 sub _ProcessRestrictions {
3705     my $self = shift;
3706
3707     #Blow away ticket aliases since we'll need to regenerate them for
3708     #a new search
3709     delete $self->{'TicketAliases'};
3710     delete $self->{'items_array'};
3711     delete $self->{'item_map'};
3712     delete $self->{'raw_rows'};
3713     delete $self->{'rows'};
3714     delete $self->{'count_all'};
3715
3716     my $sql = $self->Query;    # Violating the _SQL namespace
3717     if ( !$sql || $self->{'RecalcTicketLimits'} ) {
3718
3719         #  "Restrictions to Clauses Branch\n";
3720         my $clauseRef = eval { $self->_RestrictionsToClauses; };
3721         if ($@) {
3722             $RT::Logger->error( "RestrictionsToClauses: " . $@ );
3723             $self->FromSQL("");
3724         }
3725         else {
3726             $sql = $self->ClausesToSQL($clauseRef);
3727             $self->FromSQL($sql) if $sql;
3728         }
3729     }
3730
3731     $self->{'RecalcTicketLimits'} = 0;
3732
3733 }
3734
3735 =head2 _BuildItemMap
3736
3737 Build up a L</ItemMap> of first/last/next/prev items, so that we can
3738 display search nav quickly.
3739
3740 =cut
3741
3742 sub _BuildItemMap {
3743     my $self = shift;
3744
3745     my $window = RT->Config->Get('TicketsItemMapSize');
3746
3747     $self->{'item_map'} = {};
3748
3749     my $items = $self->ItemsArrayRefWindow( $window );
3750     return unless $items && @$items;
3751
3752     my $prev = 0;
3753     $self->{'item_map'}{'first'} = $items->[0]->EffectiveId;
3754     for ( my $i = 0; $i < @$items; $i++ ) {
3755         my $item = $items->[$i];
3756         my $id = $item->EffectiveId;
3757         $self->{'item_map'}{$id}{'defined'} = 1;
3758         $self->{'item_map'}{$id}{'prev'}    = $prev;
3759         $self->{'item_map'}{$id}{'next'}    = $items->[$i+1]->EffectiveId
3760             if $items->[$i+1];
3761         $prev = $id;
3762     }
3763     $self->{'item_map'}{'last'} = $prev
3764         if !$window || @$items < $window;
3765 }
3766
3767 =head2 ItemMap
3768
3769 Returns an a map of all items found by this search. The map is a hash
3770 of the form:
3771
3772     {
3773         first => <first ticket id found>,
3774         last => <last ticket id found or undef>,
3775
3776         <ticket id> => {
3777             prev => <the ticket id found before>,
3778             next => <the ticket id found after>,
3779         },
3780         <ticket id> => {
3781             prev => ...,
3782             next => ...,
3783         },
3784     }
3785
3786 =cut
3787
3788 sub ItemMap {
3789     my $self = shift;
3790     $self->_BuildItemMap unless $self->{'item_map'};
3791     return $self->{'item_map'};
3792 }
3793
3794
3795
3796
3797 =head2 PrepForSerialization
3798
3799 You don't want to serialize a big tickets object, as
3800 the {items} hash will be instantly invalid _and_ eat
3801 lots of space
3802
3803 =cut
3804
3805 sub PrepForSerialization {
3806     my $self = shift;
3807     delete $self->{'items'};
3808     delete $self->{'items_array'};
3809     $self->RedoSearch();
3810 }
3811
3812 =head1 FLAGS
3813
3814 RT::Tickets supports several flags which alter search behavior:
3815
3816
3817 allow_deleted_search  (Otherwise never show deleted tickets in search results)
3818 looking_at_type (otherwise limit to type=ticket)
3819
3820 These flags are set by calling 
3821
3822 $tickets->{'flagname'} = 1;
3823
3824 BUG: There should be an API for this
3825
3826
3827
3828 =cut
3829
3830
3831
3832 =head2 NewItem
3833
3834 Returns an empty new RT::Ticket item
3835
3836 =cut
3837
3838 sub NewItem {
3839     my $self = shift;
3840     return(RT::Ticket->new($self->CurrentUser));
3841 }
3842 RT::Base->_ImportOverlays();
3843
3844 1;