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