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