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