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