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