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