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