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