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