Putting 4.2.0 on top of 4.0.17
[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
84fb5b46
MKG
49=head1 NAME
50
51 RT::Tickets - A collection of Ticket objects
52
53
54=head1 SYNOPSIS
55
56 use RT::Tickets;
57 my $tickets = RT::Tickets->new($CurrentUser);
58
59=head1 DESCRIPTION
60
61 A collection of RT::Tickets.
62
63=head1 METHODS
64
65
66=cut
67
68package RT::Tickets;
69
70use strict;
71use warnings;
72
af59614d 73use base 'RT::SearchBuilder';
84fb5b46 74
af59614d
MKG
75use Role::Basic 'with';
76with 'RT::SearchBuilder::Role::Roles';
84fb5b46 77
af59614d
MKG
78use Scalar::Util qw/blessed/;
79
80use RT::Ticket;
81use RT::SQL;
84fb5b46
MKG
82
83sub Table { 'Tickets'}
84
85use RT::CustomFields;
af59614d
MKG
86
87__PACKAGE__->RegisterCustomFieldJoin(@$_) for
88 [ "RT::Transaction" => sub { $_[0]->JoinTransactions } ],
89 [ "RT::Queue" => sub {
90 # XXX: Could avoid join and use main.Queue with some refactoring?
91 return $_[0]->{_sql_aliases}{queues} ||= $_[0]->Join(
92 ALIAS1 => 'main',
93 FIELD1 => 'Queue',
94 TABLE2 => 'Queues',
95 FIELD2 => 'id',
96 );
97 }
98 ];
84fb5b46
MKG
99
100# Configuration Tables:
101
102# FIELD_METADATA is a mapping of searchable Field name, to Type, and other
103# metadata.
104
105our %FIELD_METADATA = (
106 Status => [ 'ENUM', ], #loc_left_pair
107 Queue => [ 'ENUM' => 'Queue', ], #loc_left_pair
108 Type => [ 'ENUM', ], #loc_left_pair
109 Creator => [ 'ENUM' => 'User', ], #loc_left_pair
110 LastUpdatedBy => [ 'ENUM' => 'User', ], #loc_left_pair
111 Owner => [ 'WATCHERFIELD' => 'Owner', ], #loc_left_pair
112 EffectiveId => [ 'INT', ], #loc_left_pair
113 id => [ 'ID', ], #loc_left_pair
114 InitialPriority => [ 'INT', ], #loc_left_pair
115 FinalPriority => [ 'INT', ], #loc_left_pair
116 Priority => [ 'INT', ], #loc_left_pair
117 TimeLeft => [ 'INT', ], #loc_left_pair
118 TimeWorked => [ 'INT', ], #loc_left_pair
119 TimeEstimated => [ 'INT', ], #loc_left_pair
120
121 Linked => [ 'LINK' ], #loc_left_pair
122 LinkedTo => [ 'LINK' => 'To' ], #loc_left_pair
123 LinkedFrom => [ 'LINK' => 'From' ], #loc_left_pair
124 MemberOf => [ 'LINK' => To => 'MemberOf', ], #loc_left_pair
125 DependsOn => [ 'LINK' => To => 'DependsOn', ], #loc_left_pair
126 RefersTo => [ 'LINK' => To => 'RefersTo', ], #loc_left_pair
127 HasMember => [ 'LINK' => From => 'MemberOf', ], #loc_left_pair
128 DependentOn => [ 'LINK' => From => 'DependsOn', ], #loc_left_pair
129 DependedOnBy => [ 'LINK' => From => 'DependsOn', ], #loc_left_pair
130 ReferredToBy => [ 'LINK' => From => 'RefersTo', ], #loc_left_pair
131 Told => [ 'DATE' => 'Told', ], #loc_left_pair
132 Starts => [ 'DATE' => 'Starts', ], #loc_left_pair
133 Started => [ 'DATE' => 'Started', ], #loc_left_pair
134 Due => [ 'DATE' => 'Due', ], #loc_left_pair
135 Resolved => [ 'DATE' => 'Resolved', ], #loc_left_pair
136 LastUpdated => [ 'DATE' => 'LastUpdated', ], #loc_left_pair
137 Created => [ 'DATE' => 'Created', ], #loc_left_pair
138 Subject => [ 'STRING', ], #loc_left_pair
139 Content => [ 'TRANSCONTENT', ], #loc_left_pair
140 ContentType => [ 'TRANSFIELD', ], #loc_left_pair
141 Filename => [ 'TRANSFIELD', ], #loc_left_pair
142 TransactionDate => [ 'TRANSDATE', ], #loc_left_pair
143 Requestor => [ 'WATCHERFIELD' => 'Requestor', ], #loc_left_pair
144 Requestors => [ 'WATCHERFIELD' => 'Requestor', ], #loc_left_pair
145 Cc => [ 'WATCHERFIELD' => 'Cc', ], #loc_left_pair
146 AdminCc => [ 'WATCHERFIELD' => 'AdminCc', ], #loc_left_pair
147 Watcher => [ 'WATCHERFIELD', ], #loc_left_pair
148 QueueCc => [ 'WATCHERFIELD' => 'Cc' => 'Queue', ], #loc_left_pair
149 QueueAdminCc => [ 'WATCHERFIELD' => 'AdminCc' => 'Queue', ], #loc_left_pair
150 QueueWatcher => [ 'WATCHERFIELD' => undef => 'Queue', ], #loc_left_pair
01e3b242
MKG
151 CustomFieldValue => [ 'CUSTOMFIELD' => 'Ticket' ], #loc_left_pair
152 CustomField => [ 'CUSTOMFIELD' => 'Ticket' ], #loc_left_pair
153 CF => [ 'CUSTOMFIELD' => 'Ticket' ], #loc_left_pair
af59614d
MKG
154 TxnCF => [ 'CUSTOMFIELD' => 'Transaction' ], #loc_left_pair
155 TransactionCF => [ 'CUSTOMFIELD' => 'Transaction' ], #loc_left_pair
156 QueueCF => [ 'CUSTOMFIELD' => 'Queue' ], #loc_left_pair
84fb5b46 157 Updated => [ 'TRANSDATE', ], #loc_left_pair
af59614d 158 OwnerGroup => [ 'MEMBERSHIPFIELD' => 'Owner', ], #loc_left_pair
84fb5b46
MKG
159 RequestorGroup => [ 'MEMBERSHIPFIELD' => 'Requestor', ], #loc_left_pair
160 CCGroup => [ 'MEMBERSHIPFIELD' => 'Cc', ], #loc_left_pair
161 AdminCCGroup => [ 'MEMBERSHIPFIELD' => 'AdminCc', ], #loc_left_pair
162 WatcherGroup => [ 'MEMBERSHIPFIELD', ], #loc_left_pair
163 HasAttribute => [ 'HASATTRIBUTE', 1 ],
164 HasNoAttribute => [ 'HASATTRIBUTE', 0 ],
165);
166
403d7b0b
MKG
167# Lower Case version of FIELDS, for case insensitivity
168our %LOWER_CASE_FIELDS = map { ( lc($_) => $_ ) } (keys %FIELD_METADATA);
169
84fb5b46
MKG
170our %SEARCHABLE_SUBFIELDS = (
171 User => [qw(
172 EmailAddress Name RealName Nickname Organization Address1 Address2
173 WorkPhone HomePhone MobilePhone PagerPhone id
174 )],
175);
176
177# Mapping of Field Type to Function
178our %dispatch = (
179 ENUM => \&_EnumLimit,
180 INT => \&_IntLimit,
181 ID => \&_IdLimit,
182 LINK => \&_LinkLimit,
183 DATE => \&_DateLimit,
184 STRING => \&_StringLimit,
185 TRANSFIELD => \&_TransLimit,
186 TRANSCONTENT => \&_TransContentLimit,
187 TRANSDATE => \&_TransDateLimit,
188 WATCHERFIELD => \&_WatcherLimit,
189 MEMBERSHIPFIELD => \&_WatcherMembershipLimit,
190 CUSTOMFIELD => \&_CustomFieldLimit,
191 HASATTRIBUTE => \&_HasAttributeLimit,
192);
84fb5b46
MKG
193
194# Default EntryAggregator per type
195# if you specify OP, you must specify all valid OPs
196my %DefaultEA = (
197 INT => 'AND',
198 ENUM => {
199 '=' => 'OR',
200 '!=' => 'AND'
201 },
202 DATE => {
203 '=' => 'OR',
204 '>=' => 'AND',
205 '<=' => 'AND',
206 '>' => 'AND',
207 '<' => 'AND'
208 },
209 STRING => {
210 '=' => 'OR',
211 '!=' => 'AND',
212 'LIKE' => 'AND',
213 'NOT LIKE' => 'AND'
214 },
215 TRANSFIELD => 'AND',
216 TRANSDATE => 'AND',
217 LINK => 'OR',
218 LINKFIELD => 'AND',
219 TARGET => 'AND',
220 BASE => 'AND',
221 WATCHERFIELD => {
222 '=' => 'OR',
223 '!=' => 'AND',
224 'LIKE' => 'OR',
225 'NOT LIKE' => 'AND'
226 },
227
228 HASATTRIBUTE => {
229 '=' => 'AND',
230 '!=' => 'AND',
231 },
232
233 CUSTOMFIELD => 'OR',
234);
235
84fb5b46 236sub FIELDS { return \%FIELD_METADATA }
84fb5b46
MKG
237
238our @SORTFIELDS = qw(id Status
239 Queue Subject
240 Owner Created Due Starts Started
241 Told
242 Resolved LastUpdated Priority TimeWorked TimeLeft);
243
244=head2 SortFields
245
246Returns the list of fields that lists of tickets can easily be sorted by
247
248=cut
249
250sub SortFields {
251 my $self = shift;
252 return (@SORTFIELDS);
253}
254
255
256# BEGIN SQL STUFF *********************************
257
258
259sub CleanSlate {
260 my $self = shift;
261 $self->SUPER::CleanSlate( @_ );
262 delete $self->{$_} foreach qw(
263 _sql_cf_alias
264 _sql_group_members_aliases
265 _sql_object_cfv_alias
266 _sql_role_group_aliases
267 _sql_trattachalias
268 _sql_u_watchers_alias_for_sort
269 _sql_u_watchers_aliases
270 _sql_current_user_can_see_applied
271 );
272}
273
274=head1 Limit Helper Routines
275
276These routines are the targets of a dispatch table depending on the
277type of field. They all share the same signature:
278
279 my ($self,$field,$op,$value,@rest) = @_;
280
281The values in @rest should be suitable for passing directly to
282DBIx::SearchBuilder::Limit.
283
284Essentially they are an expanded/broken out (and much simplified)
285version of what ProcessRestrictions used to do. They're also much
286more clearly delineated by the TYPE of field being processed.
287
288=head2 _IdLimit
289
290Handle ID field.
291
292=cut
293
294sub _IdLimit {
295 my ( $sb, $field, $op, $value, @rest ) = @_;
296
297 if ( $value eq '__Bookmarked__' ) {
298 return $sb->_BookmarkLimit( $field, $op, $value, @rest );
299 } else {
300 return $sb->_IntLimit( $field, $op, $value, @rest );
301 }
302}
303
304sub _BookmarkLimit {
305 my ( $sb, $field, $op, $value, @rest ) = @_;
306
307 die "Invalid operator $op for __Bookmarked__ search on $field"
308 unless $op =~ /^(=|!=)$/;
309
af59614d 310 my @bookmarks = $sb->CurrentUser->UserObj->Bookmarks;
84fb5b46 311
af59614d 312 return $sb->Limit(
84fb5b46
MKG
313 FIELD => $field,
314 OPERATOR => $op,
315 VALUE => 0,
316 @rest,
317 ) unless @bookmarks;
318
319 # as bookmarked tickets can be merged we have to use a join
320 # but it should be pretty lightweight
321 my $tickets_alias = $sb->Join(
322 TYPE => 'LEFT',
323 ALIAS1 => 'main',
324 FIELD1 => 'id',
325 TABLE2 => 'Tickets',
326 FIELD2 => 'EffectiveId',
327 );
328 $sb->_OpenParen;
329 my $first = 1;
330 my $ea = $op eq '='? 'OR': 'AND';
331 foreach my $id ( sort @bookmarks ) {
af59614d 332 $sb->Limit(
84fb5b46
MKG
333 ALIAS => $tickets_alias,
334 FIELD => 'id',
335 OPERATOR => $op,
336 VALUE => $id,
337 $first? (@rest): ( ENTRYAGGREGATOR => $ea )
338 );
339 $first = 0 if $first;
340 }
341 $sb->_CloseParen;
342}
343
344=head2 _EnumLimit
345
346Handle Fields which are limited to certain values, and potentially
347need to be looked up from another class.
348
349This subroutine actually handles two different kinds of fields. For
350some the user is responsible for limiting the values. (i.e. Status,
351Type).
352
353For others, the value specified by the user will be looked by via
354specified class.
355
356Meta Data:
357 name of class to lookup in (Optional)
358
359=cut
360
361sub _EnumLimit {
362 my ( $sb, $field, $op, $value, @rest ) = @_;
363
364 # SQL::Statement changes != to <>. (Can we remove this now?)
365 $op = "!=" if $op eq "<>";
366
367 die "Invalid Operation: $op for $field"
368 unless $op eq "="
369 or $op eq "!=";
370
371 my $meta = $FIELD_METADATA{$field};
372 if ( defined $meta->[1] && defined $value && $value !~ /^\d+$/ ) {
373 my $class = "RT::" . $meta->[1];
374 my $o = $class->new( $sb->CurrentUser );
375 $o->Load($value);
403d7b0b 376 $value = $o->Id || 0;
5b0d0914
MKG
377 } elsif ( $field eq "Type" ) {
378 $value = lc $value if $value =~ /^(ticket|approval|reminder)$/i;
379 } elsif ($field eq "Status") {
380 $value = lc $value;
84fb5b46 381 }
af59614d 382 $sb->Limit(
84fb5b46
MKG
383 FIELD => $field,
384 VALUE => $value,
385 OPERATOR => $op,
386 @rest,
387 );
388}
389
390=head2 _IntLimit
391
392Handle fields where the values are limited to integers. (For example,
393Priority, TimeWorked.)
394
395Meta Data:
396 None
397
398=cut
399
400sub _IntLimit {
401 my ( $sb, $field, $op, $value, @rest ) = @_;
402
af59614d
MKG
403 my $is_a_like = $op =~ /MATCHES|ENDSWITH|STARTSWITH|LIKE/i;
404
405 # We want to support <id LIKE '1%'> for ticket autocomplete,
406 # but we need to explicitly typecast on Postgres
407 if ( $is_a_like && RT->Config->Get('DatabaseType') eq 'Pg' ) {
408 return $sb->Limit(
409 FUNCTION => "CAST(main.$field AS TEXT)",
410 OPERATOR => $op,
411 VALUE => $value,
412 @rest,
413 );
414 }
84fb5b46 415
af59614d 416 $sb->Limit(
84fb5b46
MKG
417 FIELD => $field,
418 VALUE => $value,
419 OPERATOR => $op,
420 @rest,
421 );
422}
423
424=head2 _LinkLimit
425
426Handle fields which deal with links between tickets. (MemberOf, DependsOn)
427
428Meta Data:
429 1: Direction (From, To)
430 2: Link Type (MemberOf, DependsOn, RefersTo)
431
432=cut
433
434sub _LinkLimit {
435 my ( $sb, $field, $op, $value, @rest ) = @_;
436
437 my $meta = $FIELD_METADATA{$field};
438 die "Invalid Operator $op for $field" unless $op =~ /^(=|!=|IS|IS NOT)$/io;
439
440 my $is_negative = 0;
441 if ( $op eq '!=' || $op =~ /\bNOT\b/i ) {
442 $is_negative = 1;
443 }
444 my $is_null = 0;
445 $is_null = 1 if !$value || $value =~ /^null$/io;
446
dab09ea8
MKG
447 unless ($is_null) {
448 $value = RT::URI->new( $sb->CurrentUser )->CanonicalizeURI( $value );
449 }
450
84fb5b46
MKG
451 my $direction = $meta->[1] || '';
452 my ($matchfield, $linkfield) = ('', '');
453 if ( $direction eq 'To' ) {
454 ($matchfield, $linkfield) = ("Target", "Base");
455 }
456 elsif ( $direction eq 'From' ) {
457 ($matchfield, $linkfield) = ("Base", "Target");
458 }
459 elsif ( $direction ) {
460 die "Invalid link direction '$direction' for $field\n";
461 } else {
462 $sb->_OpenParen;
463 $sb->_LinkLimit( 'LinkedTo', $op, $value, @rest );
464 $sb->_LinkLimit(
465 'LinkedFrom', $op, $value, @rest,
466 ENTRYAGGREGATOR => (($is_negative && $is_null) || (!$is_null && !$is_negative))? 'OR': 'AND',
467 );
468 $sb->_CloseParen;
469 return;
470 }
471
472 my $is_local = 1;
473 if ( $is_null ) {
403d7b0b 474 $op = ($op =~ /^(=|IS)$/i)? 'IS': 'IS NOT';
84fb5b46
MKG
475 }
476 elsif ( $value =~ /\D/ ) {
477 $is_local = 0;
478 }
479 $matchfield = "Local$matchfield" if $is_local;
480
481#For doing a left join to find "unlinked tickets" we want to generate a query that looks like this
482# SELECT main.* FROM Tickets main
483# LEFT JOIN Links Links_1 ON ( (Links_1.Type = 'MemberOf')
484# AND(main.id = Links_1.LocalTarget))
485# WHERE Links_1.LocalBase IS NULL;
486
487 if ( $is_null ) {
488 my $linkalias = $sb->Join(
489 TYPE => 'LEFT',
490 ALIAS1 => 'main',
491 FIELD1 => 'id',
492 TABLE2 => 'Links',
493 FIELD2 => 'Local' . $linkfield
494 );
af59614d 495 $sb->Limit(
84fb5b46
MKG
496 LEFTJOIN => $linkalias,
497 FIELD => 'Type',
498 OPERATOR => '=',
499 VALUE => $meta->[2],
500 ) if $meta->[2];
af59614d 501 $sb->Limit(
84fb5b46
MKG
502 @rest,
503 ALIAS => $linkalias,
504 FIELD => $matchfield,
505 OPERATOR => $op,
506 VALUE => 'NULL',
507 QUOTEVALUE => 0,
508 );
509 }
510 else {
511 my $linkalias = $sb->Join(
512 TYPE => 'LEFT',
513 ALIAS1 => 'main',
514 FIELD1 => 'id',
515 TABLE2 => 'Links',
516 FIELD2 => 'Local' . $linkfield
517 );
af59614d 518 $sb->Limit(
84fb5b46
MKG
519 LEFTJOIN => $linkalias,
520 FIELD => 'Type',
521 OPERATOR => '=',
522 VALUE => $meta->[2],
523 ) if $meta->[2];
af59614d 524 $sb->Limit(
84fb5b46
MKG
525 LEFTJOIN => $linkalias,
526 FIELD => $matchfield,
527 OPERATOR => '=',
528 VALUE => $value,
529 );
af59614d 530 $sb->Limit(
84fb5b46
MKG
531 @rest,
532 ALIAS => $linkalias,
533 FIELD => $matchfield,
534 OPERATOR => $is_negative? 'IS': 'IS NOT',
535 VALUE => 'NULL',
536 QUOTEVALUE => 0,
537 );
538 }
539}
540
541=head2 _DateLimit
542
543Handle date fields. (Created, LastTold..)
544
545Meta Data:
546 1: type of link. (Probably not necessary.)
547
548=cut
549
550sub _DateLimit {
af59614d 551 my ( $sb, $field, $op, $value, %rest ) = @_;
84fb5b46
MKG
552
553 die "Invalid Date Op: $op"
554 unless $op =~ /^(=|>|<|>=|<=)$/;
555
556 my $meta = $FIELD_METADATA{$field};
557 die "Incorrect Meta Data for $field"
558 unless ( defined $meta->[1] );
559
af59614d
MKG
560 if ( my $subkey = $rest{SUBKEY} ) {
561 if ( $subkey eq 'DayOfWeek' && $op !~ /IS/i && $value =~ /[^0-9]/ ) {
562 for ( my $i = 0; $i < @RT::Date::DAYS_OF_WEEK; $i++ ) {
563 # Use a case-insensitive regex for better matching across
564 # locales since we don't have fc() and lc() is worse. Really
565 # we should be doing Unicode normalization too, but we don't do
566 # that elsewhere in RT.
567 #
568 # XXX I18N: Replace the regex with fc() once we're guaranteed 5.16.
569 next unless lc $RT::Date::DAYS_OF_WEEK[ $i ] eq lc $value
570 or $sb->CurrentUser->loc($RT::Date::DAYS_OF_WEEK[ $i ]) =~ /^\Q$value\E$/i;
571
572 $value = $i; last;
573 }
574 return $sb->Limit( FIELD => 'id', VALUE => 0, %rest )
575 if $value =~ /[^0-9]/;
576 }
577 elsif ( $subkey eq 'Month' && $op !~ /IS/i && $value =~ /[^0-9]/ ) {
578 for ( my $i = 0; $i < @RT::Date::MONTHS; $i++ ) {
579 # Use a case-insensitive regex for better matching across
580 # locales since we don't have fc() and lc() is worse. Really
581 # we should be doing Unicode normalization too, but we don't do
582 # that elsewhere in RT.
583 #
584 # XXX I18N: Replace the regex with fc() once we're guaranteed 5.16.
585 next unless lc $RT::Date::MONTHS[ $i ] eq lc $value
586 or $sb->CurrentUser->loc($RT::Date::MONTHS[ $i ]) =~ /^\Q$value\E$/i;
587
588 $value = $i + 1; last;
589 }
590 return $sb->Limit( FIELD => 'id', VALUE => 0, %rest )
591 if $value =~ /[^0-9]/;
592 }
593
594 my $tz;
595 if ( RT->Config->Get('ChartsTimezonesInDB') ) {
596 my $to = $sb->CurrentUser->UserObj->Timezone
597 || RT->Config->Get('Timezone');
598 $tz = { From => 'UTC', To => $to }
599 if $to && lc $to ne 'utc';
600 }
601
602 # $subkey is validated by DateTimeFunction
603 my $function = $RT::Handle->DateTimeFunction(
604 Type => $subkey,
605 Field => $sb->NotSetDateToNullFunction,
606 Timezone => $tz,
607 );
608
609 return $sb->Limit(
610 FUNCTION => $function,
611 FIELD => $meta->[1],
612 OPERATOR => $op,
613 VALUE => $value,
614 %rest,
615 );
616 }
617
84fb5b46
MKG
618 my $date = RT::Date->new( $sb->CurrentUser );
619 $date->Set( Format => 'unknown', Value => $value );
620
621 if ( $op eq "=" ) {
622
623 # if we're specifying =, that means we want everything on a
624 # particular single day. in the database, we need to check for >
625 # and < the edges of that day.
626
627 $date->SetToMidnight( Timezone => 'server' );
628 my $daystart = $date->ISO;
629 $date->AddDay;
630 my $dayend = $date->ISO;
631
632 $sb->_OpenParen;
633
af59614d 634 $sb->Limit(
84fb5b46
MKG
635 FIELD => $meta->[1],
636 OPERATOR => ">=",
637 VALUE => $daystart,
af59614d 638 %rest,
84fb5b46
MKG
639 );
640
af59614d 641 $sb->Limit(
84fb5b46
MKG
642 FIELD => $meta->[1],
643 OPERATOR => "<",
644 VALUE => $dayend,
af59614d 645 %rest,
84fb5b46
MKG
646 ENTRYAGGREGATOR => 'AND',
647 );
648
649 $sb->_CloseParen;
650
651 }
652 else {
af59614d
MKG
653 $sb->Limit(
654 FUNCTION => $sb->NotSetDateToNullFunction,
84fb5b46
MKG
655 FIELD => $meta->[1],
656 OPERATOR => $op,
657 VALUE => $date->ISO,
af59614d 658 %rest,
84fb5b46
MKG
659 );
660 }
661}
662
663=head2 _StringLimit
664
665Handle simple fields which are just strings. (Subject,Type)
666
667Meta Data:
668 None
669
670=cut
671
672sub _StringLimit {
673 my ( $sb, $field, $op, $value, @rest ) = @_;
674
675 # FIXME:
676 # Valid Operators:
677 # =, !=, LIKE, NOT LIKE
678 if ( RT->Config->Get('DatabaseType') eq 'Oracle'
679 && (!defined $value || !length $value)
680 && lc($op) ne 'is' && lc($op) ne 'is not'
681 ) {
682 if ($op eq '!=' || $op =~ /^NOT\s/i) {
683 $op = 'IS NOT';
684 } else {
685 $op = 'IS';
686 }
687 $value = 'NULL';
688 }
689
af59614d 690 $sb->Limit(
84fb5b46
MKG
691 FIELD => $field,
692 OPERATOR => $op,
693 VALUE => $value,
694 CASESENSITIVE => 0,
695 @rest,
696 );
697}
698
699=head2 _TransDateLimit
700
701Handle fields limiting based on Transaction Date.
702
703The inpupt value must be in a format parseable by Time::ParseDate
704
705Meta Data:
706 None
707
708=cut
709
710# This routine should really be factored into translimit.
711sub _TransDateLimit {
712 my ( $sb, $field, $op, $value, @rest ) = @_;
713
714 # See the comments for TransLimit, they apply here too
715
716 my $txn_alias = $sb->JoinTransactions;
717
718 my $date = RT::Date->new( $sb->CurrentUser );
719 $date->Set( Format => 'unknown', Value => $value );
720
721 $sb->_OpenParen;
722 if ( $op eq "=" ) {
723
724 # if we're specifying =, that means we want everything on a
725 # particular single day. in the database, we need to check for >
726 # and < the edges of that day.
727
728 $date->SetToMidnight( Timezone => 'server' );
729 my $daystart = $date->ISO;
730 $date->AddDay;
731 my $dayend = $date->ISO;
732
af59614d 733 $sb->Limit(
84fb5b46
MKG
734 ALIAS => $txn_alias,
735 FIELD => 'Created',
736 OPERATOR => ">=",
737 VALUE => $daystart,
738 @rest
739 );
af59614d 740 $sb->Limit(
84fb5b46
MKG
741 ALIAS => $txn_alias,
742 FIELD => 'Created',
743 OPERATOR => "<=",
744 VALUE => $dayend,
745 @rest,
746 ENTRYAGGREGATOR => 'AND',
747 );
748
749 }
750
751 # not searching for a single day
752 else {
753
754 #Search for the right field
af59614d 755 $sb->Limit(
84fb5b46
MKG
756 ALIAS => $txn_alias,
757 FIELD => 'Created',
758 OPERATOR => $op,
759 VALUE => $date->ISO,
760 @rest
761 );
762 }
763
764 $sb->_CloseParen;
765}
766
767=head2 _TransLimit
768
769Limit based on the ContentType or the Filename of a transaction.
770
771=cut
772
773sub _TransLimit {
774 my ( $self, $field, $op, $value, %rest ) = @_;
775
776 my $txn_alias = $self->JoinTransactions;
777 unless ( defined $self->{_sql_trattachalias} ) {
af59614d 778 $self->{_sql_trattachalias} = $self->Join(
84fb5b46
MKG
779 TYPE => 'LEFT', # not all txns have an attachment
780 ALIAS1 => $txn_alias,
781 FIELD1 => 'id',
782 TABLE2 => 'Attachments',
783 FIELD2 => 'TransactionId',
784 );
785 }
786
af59614d 787 $self->Limit(
84fb5b46
MKG
788 %rest,
789 ALIAS => $self->{_sql_trattachalias},
790 FIELD => $field,
791 OPERATOR => $op,
792 VALUE => $value,
793 CASESENSITIVE => 0,
794 );
795}
796
797=head2 _TransContentLimit
798
799Limit based on the Content of a transaction.
800
801=cut
802
803sub _TransContentLimit {
804
805 # Content search
806
807 # If only this was this simple. We've got to do something
808 # complicated here:
809
810 #Basically, we want to make sure that the limits apply to
811 #the same attachment, rather than just another attachment
812 #for the same ticket, no matter how many clauses we lump
af59614d 813 #on.
84fb5b46
MKG
814
815 # In the SQL, we might have
816 # (( Content = foo ) or ( Content = bar AND Content = baz ))
817 # The AND group should share the same Alias.
818
819 # Actually, maybe it doesn't matter. We use the same alias and it
820 # works itself out? (er.. different.)
821
822 # Steal more from _ProcessRestrictions
823
824 # FIXME: Maybe look at the previous FooLimit call, and if it was a
825 # TransLimit and EntryAggregator == AND, reuse the Aliases?
826
827 # Or better - store the aliases on a per subclause basis - since
828 # those are going to be the things we want to relate to each other,
829 # anyway.
830
831 # maybe we should not allow certain kinds of aggregation of these
832 # clauses and do a psuedo regex instead? - the problem is getting
833 # them all into the same subclause when you have (A op B op C) - the
834 # way they get parsed in the tree they're in different subclauses.
835
836 my ( $self, $field, $op, $value, %rest ) = @_;
837 $field = 'Content' if $field =~ /\W/;
838
839 my $config = RT->Config->Get('FullTextSearch') || {};
840 unless ( $config->{'Enable'} ) {
af59614d 841 $self->Limit( %rest, FIELD => 'id', VALUE => 0 );
84fb5b46
MKG
842 return;
843 }
844
845 my $txn_alias = $self->JoinTransactions;
846 unless ( defined $self->{_sql_trattachalias} ) {
af59614d 847 $self->{_sql_trattachalias} = $self->Join(
84fb5b46
MKG
848 TYPE => 'LEFT', # not all txns have an attachment
849 ALIAS1 => $txn_alias,
850 FIELD1 => 'id',
851 TABLE2 => 'Attachments',
852 FIELD2 => 'TransactionId',
853 );
854 }
855
856 $self->_OpenParen;
857 if ( $config->{'Indexed'} ) {
858 my $db_type = RT->Config->Get('DatabaseType');
859
860 my $alias;
861 if ( $config->{'Table'} and $config->{'Table'} ne "Attachments") {
af59614d 862 $alias = $self->{'_sql_aliases'}{'full_text'} ||= $self->Join(
84fb5b46
MKG
863 TYPE => 'LEFT',
864 ALIAS1 => $self->{'_sql_trattachalias'},
865 FIELD1 => 'id',
866 TABLE2 => $config->{'Table'},
867 FIELD2 => 'id',
868 );
869 } else {
870 $alias = $self->{'_sql_trattachalias'};
871 }
872
873 #XXX: handle negative searches
874 my $index = $config->{'Column'};
875 if ( $db_type eq 'Oracle' ) {
876 my $dbh = $RT::Handle->dbh;
877 my $alias = $self->{_sql_trattachalias};
af59614d 878 $self->Limit(
84fb5b46
MKG
879 %rest,
880 FUNCTION => "CONTAINS( $alias.$field, ".$dbh->quote($value) .")",
881 OPERATOR => '>',
882 VALUE => 0,
883 QUOTEVALUE => 0,
884 CASESENSITIVE => 1,
885 );
886 # this is required to trick DBIx::SB's LEFT JOINS optimizer
887 # into deciding that join is redundant as it is
af59614d 888 $self->Limit(
84fb5b46
MKG
889 ENTRYAGGREGATOR => 'AND',
890 ALIAS => $self->{_sql_trattachalias},
891 FIELD => 'Content',
892 OPERATOR => 'IS NOT',
893 VALUE => 'NULL',
894 );
895 }
896 elsif ( $db_type eq 'Pg' ) {
897 my $dbh = $RT::Handle->dbh;
af59614d 898 $self->Limit(
84fb5b46
MKG
899 %rest,
900 ALIAS => $alias,
901 FIELD => $index,
902 OPERATOR => '@@',
903 VALUE => 'plainto_tsquery('. $dbh->quote($value) .')',
904 QUOTEVALUE => 0,
905 );
906 }
907 elsif ( $db_type eq 'mysql' ) {
908 # XXX: We could theoretically skip the join to Attachments,
909 # and have Sphinx simply index and group by the TicketId,
910 # and join Ticket.id to that attribute, which would be much
911 # more efficient -- however, this is only a possibility if
912 # there are no other transaction limits.
913
914 # This is a special character. Note that \ does not escape
915 # itself (in Sphinx 2.1.0, at least), so 'foo\;bar' becoming
916 # 'foo\\;bar' is not a vulnerability, and is still parsed as
917 # "foo, \, ;, then bar". Happily, the default mode is
918 # "all", meaning that boolean operators are not special.
919 $value =~ s/;/\\;/g;
920
921 my $max = $config->{'MaxMatches'};
af59614d 922 $self->Limit(
84fb5b46
MKG
923 %rest,
924 ALIAS => $alias,
925 FIELD => 'query',
926 OPERATOR => '=',
927 VALUE => "$value;limit=$max;maxmatches=$max",
928 );
929 }
930 } else {
af59614d 931 $self->Limit(
84fb5b46
MKG
932 %rest,
933 ALIAS => $self->{_sql_trattachalias},
934 FIELD => $field,
935 OPERATOR => $op,
936 VALUE => $value,
937 CASESENSITIVE => 0,
938 );
939 }
940 if ( RT->Config->Get('DontSearchFileAttachments') ) {
af59614d 941 $self->Limit(
84fb5b46
MKG
942 ENTRYAGGREGATOR => 'AND',
943 ALIAS => $self->{_sql_trattachalias},
944 FIELD => 'Filename',
945 OPERATOR => 'IS',
946 VALUE => 'NULL',
947 );
948 }
949 $self->_CloseParen;
950}
951
952=head2 _WatcherLimit
953
954Handle watcher limits. (Requestor, CC, etc..)
955
956Meta Data:
957 1: Field to query on
958
959
960
961=cut
962
963sub _WatcherLimit {
964 my $self = shift;
965 my $field = shift;
966 my $op = shift;
967 my $value = shift;
968 my %rest = (@_);
969
970 my $meta = $FIELD_METADATA{ $field };
971 my $type = $meta->[1] || '';
972 my $class = $meta->[2] || 'Ticket';
973
974 # Bail if the subfield is not allowed
975 if ( $rest{SUBKEY}
976 and not grep { $_ eq $rest{SUBKEY} } @{$SEARCHABLE_SUBFIELDS{'User'}})
977 {
978 die "Invalid watcher subfield: '$rest{SUBKEY}'";
979 }
980
af59614d
MKG
981 $self->RoleLimit(
982 TYPE => $type,
983 CLASS => "RT::$class",
984 FIELD => $rest{SUBKEY},
985 OPERATOR => $op,
986 VALUE => $value,
987 SUBCLAUSE => "ticketsql",
988 %rest,
84fb5b46 989 );
84fb5b46
MKG
990}
991
992=head2 _WatcherMembershipLimit
993
994Handle watcher membership limits, i.e. whether the watcher belongs to a
995specific group or not.
996
997Meta Data:
af59614d 998 1: Role to query on
84fb5b46
MKG
999
1000=cut
1001
1002sub _WatcherMembershipLimit {
af59614d 1003 my ( $self, $field, $op, $value, %rest ) = @_;
84fb5b46 1004
af59614d
MKG
1005 # we don't support anything but '='
1006 die "Invalid $field Op: $op"
1007 unless $op =~ /^=$/;
84fb5b46 1008
af59614d
MKG
1009 unless ( $value =~ /^\d+$/ ) {
1010 my $group = RT::Group->new( $self->CurrentUser );
1011 $group->LoadUserDefinedGroup( $value );
1012 $value = $group->id || 0;
84fb5b46 1013 }
84fb5b46 1014
84fb5b46 1015 my $meta = $FIELD_METADATA{$field};
af59614d 1016 my $type = $meta->[1] || '';
84fb5b46 1017
af59614d
MKG
1018 my ($members_alias, $members_column);
1019 if ( $type eq 'Owner' ) {
1020 ($members_alias, $members_column) = ('main', 'Owner');
1021 } else {
1022 (undef, undef, $members_alias) = $self->_WatcherJoin( New => 1, Name => $type );
1023 $members_column = 'id';
1024 }
84fb5b46 1025
af59614d
MKG
1026 my $cgm_alias = $self->Join(
1027 ALIAS1 => $members_alias,
1028 FIELD1 => $members_column,
1029 TABLE2 => 'CachedGroupMembers',
1030 FIELD2 => 'MemberId',
84fb5b46 1031 );
84fb5b46 1032 $self->Limit(
af59614d
MKG
1033 LEFTJOIN => $cgm_alias,
1034 ALIAS => $cgm_alias,
84fb5b46
MKG
1035 FIELD => 'Disabled',
1036 VALUE => 0,
1037 );
1038
84fb5b46 1039 $self->Limit(
af59614d
MKG
1040 ALIAS => $cgm_alias,
1041 FIELD => 'GroupId',
1042 VALUE => $value,
1043 OPERATOR => $op,
1044 %rest,
84fb5b46 1045 );
84fb5b46
MKG
1046}
1047
1048=head2 _CustomFieldDecipher
1049
1050Try and turn a CF descriptor into (cfid, cfname) object pair.
1051
01e3b242
MKG
1052Takes an optional second parameter of the CF LookupType, defaults to Ticket CFs.
1053
84fb5b46
MKG
1054=cut
1055
1056sub _CustomFieldDecipher {
01e3b242
MKG
1057 my ($self, $string, $lookuptype) = @_;
1058 $lookuptype ||= $self->_SingularClass->CustomFieldLookupType;
84fb5b46 1059
01e3b242 1060 my ($object, $field, $column) = ($string =~ /^(?:(.+?)\.)?\{(.+)\}(?:\.(Content|LargeContent))?$/);
af59614d 1061 $field ||= ($string =~ /^\{(.*?)\}$/)[0] || $string;
84fb5b46 1062
01e3b242
MKG
1063 my ($cf, $applied_to);
1064
1065 if ( $object ) {
1066 my $record_class = RT::CustomField->RecordClassFromLookupType($lookuptype);
1067 $applied_to = $record_class->new( $self->CurrentUser );
1068 $applied_to->Load( $object );
84fb5b46 1069
01e3b242
MKG
1070 if ( $applied_to->id ) {
1071 RT->Logger->debug("Limiting to CFs identified by '$field' applied to $record_class #@{[$applied_to->id]} (loaded via '$object')");
84fb5b46
MKG
1072 }
1073 else {
01e3b242
MKG
1074 RT->Logger->warning("$record_class '$object' doesn't exist, parsed from '$string'");
1075 $object = 0;
1076 undef $applied_to;
84fb5b46
MKG
1077 }
1078 }
01e3b242
MKG
1079
1080 if ( $field =~ /\D/ ) {
1081 $object ||= '';
84fb5b46 1082 my $cfs = RT::CustomFields->new( $self->CurrentUser );
af59614d 1083 $cfs->Limit( FIELD => 'Name', VALUE => $field, CASESENSITIVE => 0 );
01e3b242
MKG
1084 $cfs->LimitToLookupType($lookuptype);
1085
1086 if ($applied_to) {
1087 $cfs->SetContextObject($applied_to);
1088 $cfs->LimitToObjectId($applied_to->id);
1089 }
84fb5b46
MKG
1090
1091 # if there is more then one field the current user can
1092 # see with the same name then we shouldn't return cf object
1093 # as we don't know which one to use
1094 $cf = $cfs->First;
1095 if ( $cf ) {
1096 $cf = undef if $cfs->Next;
1097 }
1098 }
1099 else {
1100 $cf = RT::CustomField->new( $self->CurrentUser );
1101 $cf->Load( $field );
01e3b242
MKG
1102 $cf->SetContextObject($applied_to)
1103 if $cf->id and $applied_to;
84fb5b46
MKG
1104 }
1105
01e3b242 1106 return ($object, $field, $cf, $column);
84fb5b46
MKG
1107}
1108
84fb5b46
MKG
1109=head2 _CustomFieldLimit
1110
1111Limit based on CustomFields
1112
1113Meta Data:
1114 none
1115
1116=cut
1117
84fb5b46
MKG
1118sub _CustomFieldLimit {
1119 my ( $self, $_field, $op, $value, %rest ) = @_;
1120
01e3b242
MKG
1121 my $meta = $FIELD_METADATA{ $_field };
1122 my $class = $meta->[1] || 'Ticket';
1123 my $type = "RT::$class"->CustomFieldLookupType;
1124
84fb5b46
MKG
1125 my $field = $rest{'SUBKEY'} || die "No field specified";
1126
af59614d 1127 # For our sanity, we can only limit on one object at a time
84fb5b46 1128
01e3b242
MKG
1129 my ($object, $cfid, $cf, $column);
1130 ($object, $field, $cf, $column) = $self->_CustomFieldDecipher( $field, $type );
5b0d0914 1131
84fb5b46 1132
af59614d
MKG
1133 $self->_LimitCustomField(
1134 %rest,
1135 LOOKUPTYPE => $type,
1136 CUSTOMFIELD => $cf || $field,
1137 KEY => $cf ? $cf->id : "$type-$object.$field",
1138 OPERATOR => $op,
1139 VALUE => $value,
1140 COLUMN => $column,
1141 SUBCLAUSE => "ticketsql",
1142 );
1143}
84fb5b46 1144
af59614d
MKG
1145sub _CustomFieldJoinByName {
1146 my $self = shift;
1147 my ($ObjectAlias, $cf, $type) = @_;
84fb5b46 1148
af59614d
MKG
1149 my ($ocfvalias, $CFs, $ocfalias) = $self->SUPER::_CustomFieldJoinByName(@_);
1150 $self->Limit(
1151 LEFTJOIN => $ocfalias,
1152 ENTRYAGGREGATOR => 'OR',
1153 FIELD => 'ObjectId',
1154 VALUE => 'main.Queue',
1155 QUOTEVALUE => 0,
1156 );
1157 return ($ocfvalias, $CFs, $ocfalias);
84fb5b46
MKG
1158}
1159
1160sub _HasAttributeLimit {
1161 my ( $self, $field, $op, $value, %rest ) = @_;
1162
1163 my $alias = $self->Join(
1164 TYPE => 'LEFT',
1165 ALIAS1 => 'main',
1166 FIELD1 => 'id',
1167 TABLE2 => 'Attributes',
1168 FIELD2 => 'ObjectId',
1169 );
af59614d 1170 $self->Limit(
84fb5b46
MKG
1171 LEFTJOIN => $alias,
1172 FIELD => 'ObjectType',
1173 VALUE => 'RT::Ticket',
1174 ENTRYAGGREGATOR => 'AND'
1175 );
af59614d 1176 $self->Limit(
84fb5b46
MKG
1177 LEFTJOIN => $alias,
1178 FIELD => 'Name',
1179 OPERATOR => $op,
1180 VALUE => $value,
1181 ENTRYAGGREGATOR => 'AND'
1182 );
af59614d 1183 $self->Limit(
84fb5b46
MKG
1184 %rest,
1185 ALIAS => $alias,
1186 FIELD => 'id',
1187 OPERATOR => $FIELD_METADATA{$field}->[1]? 'IS NOT': 'IS',
1188 VALUE => 'NULL',
1189 QUOTEVALUE => 0,
1190 );
1191}
1192
1193
1194# End Helper Functions
1195
1196# End of SQL Stuff -------------------------------------------------
1197
1198
1199=head2 OrderByCols ARRAY
1200
1201A modified version of the OrderBy method which automatically joins where
1202C<ALIAS> is set to the name of a watcher type.
1203
1204=cut
1205
1206sub OrderByCols {
1207 my $self = shift;
1208 my @args = @_;
1209 my $clause;
1210 my @res = ();
1211 my $order = 0;
1212
1213 foreach my $row (@args) {
1214 if ( $row->{ALIAS} ) {
1215 push @res, $row;
1216 next;
1217 }
1218 if ( $row->{FIELD} !~ /\./ ) {
af59614d 1219 my $meta = $FIELD_METADATA{ $row->{FIELD} };
84fb5b46
MKG
1220 unless ( $meta ) {
1221 push @res, $row;
1222 next;
1223 }
1224
1225 if ( $meta->[0] eq 'ENUM' && ($meta->[1]||'') eq 'Queue' ) {
1226 my $alias = $self->Join(
1227 TYPE => 'LEFT',
1228 ALIAS1 => 'main',
1229 FIELD1 => $row->{'FIELD'},
1230 TABLE2 => 'Queues',
1231 FIELD2 => 'id',
1232 );
af59614d 1233 push @res, { %$row, ALIAS => $alias, FIELD => "Name", CASESENSITIVE => 0 };
84fb5b46
MKG
1234 } elsif ( ( $meta->[0] eq 'ENUM' && ($meta->[1]||'') eq 'User' )
1235 || ( $meta->[0] eq 'WATCHERFIELD' && ($meta->[1]||'') eq 'Owner' )
1236 ) {
1237 my $alias = $self->Join(
1238 TYPE => 'LEFT',
1239 ALIAS1 => 'main',
1240 FIELD1 => $row->{'FIELD'},
1241 TABLE2 => 'Users',
1242 FIELD2 => 'id',
1243 );
af59614d 1244 push @res, { %$row, ALIAS => $alias, FIELD => "Name", CASESENSITIVE => 0 };
84fb5b46
MKG
1245 } else {
1246 push @res, $row;
1247 }
1248 next;
1249 }
1250
1251 my ( $field, $subkey ) = split /\./, $row->{FIELD}, 2;
af59614d 1252 my $meta = $FIELD_METADATA{$field};
84fb5b46
MKG
1253 if ( defined $meta->[0] && $meta->[0] eq 'WATCHERFIELD' ) {
1254 # cache alias as we want to use one alias per watcher type for sorting
af59614d
MKG
1255 my $cache_key = join "-", map { $_ || "" } @$meta[1,2];
1256 my $users = $self->{_sql_u_watchers_alias_for_sort}{ $cache_key };
84fb5b46 1257 unless ( $users ) {
af59614d
MKG
1258 $self->{_sql_u_watchers_alias_for_sort}{ $cache_key }
1259 = $users = ( $self->_WatcherJoin( Name => $meta->[1], Class => "RT::" . ($meta->[2] || 'Ticket') ) )[2];
84fb5b46
MKG
1260 }
1261 push @res, { %$row, ALIAS => $users, FIELD => $subkey };
1262 } elsif ( defined $meta->[0] && $meta->[0] eq 'CUSTOMFIELD' ) {
af59614d
MKG
1263 my ($object, $field, $cf, $column) = $self->_CustomFieldDecipher( $subkey );
1264 my $cfkey = $cf ? $cf->id : "$object.$field";
1265 push @res, $self->_OrderByCF( $row, $cfkey, ($cf || $field) );
84fb5b46
MKG
1266 } elsif ( $field eq "Custom" && $subkey eq "Ownership") {
1267 # PAW logic is "reversed"
1268 my $order = "ASC";
1269 if (exists $row->{ORDER} ) {
1270 my $o = $row->{ORDER};
1271 delete $row->{ORDER};
1272 $order = "DESC" if $o =~ /asc/i;
1273 }
1274
1275 # Ticket.Owner 1 0 X
1276 # Unowned Tickets 0 1 X
1277 # Else 0 0 X
1278
1279 foreach my $uid ( $self->CurrentUser->Id, RT->Nobody->Id ) {
1280 if ( RT->Config->Get('DatabaseType') eq 'Oracle' ) {
1281 my $f = ($row->{'ALIAS'} || 'main') .'.Owner';
1282 push @res, {
1283 %$row,
1284 FIELD => undef,
1285 ALIAS => '',
1286 FUNCTION => "CASE WHEN $f=$uid THEN 1 ELSE 0 END",
1287 ORDER => $order
1288 };
1289 } else {
1290 push @res, {
1291 %$row,
1292 FIELD => undef,
1293 FUNCTION => "Owner=$uid",
1294 ORDER => $order
1295 };
1296 }
1297 }
1298
1299 push @res, { %$row, FIELD => "Priority", ORDER => $order } ;
1300 }
1301 else {
1302 push @res, $row;
1303 }
1304 }
1305 return $self->SUPER::OrderByCols(@res);
1306}
1307
af59614d
MKG
1308sub _SQLLimit {
1309 my $self = shift;
1310 RT->Deprecated( Remove => "4.4", Instead => "Limit" );
1311 $self->Limit(@_);
1312}
1313sub _SQLJoin {
1314 my $self = shift;
1315 RT->Deprecated( Remove => "4.4", Instead => "Join" );
1316 $self->Join(@_);
1317}
1318
1319sub _OpenParen {
1320 $_[0]->SUPER::_OpenParen( $_[1] || 'ticketsql' );
1321}
1322sub _CloseParen {
1323 $_[0]->SUPER::_CloseParen( $_[1] || 'ticketsql' );
1324}
1325
1326sub Limit {
1327 my $self = shift;
1328 my %args = @_;
1329 $self->{'must_redo_search'} = 1;
1330 delete $self->{'raw_rows'};
1331 delete $self->{'count_all'};
1332
1333 if ($self->{'using_restrictions'}) {
1334 RT->Deprecated( Message => "Mixing old-style LimitFoo methods with Limit is deprecated" );
1335 $self->LimitField(@_);
1336 }
1337
1338 $args{SUBCLAUSE} ||= "ticketsql"
1339 if $self->{parsing_ticketsql} and not $args{LEFTJOIN};
1340
1341 $self->{_sql_looking_at}{ lc $args{FIELD} } = 1
1342 if $args{FIELD} and (not $args{ALIAS} or $args{ALIAS} eq "main");
84fb5b46 1343
af59614d
MKG
1344 $self->SUPER::Limit(%args);
1345}
84fb5b46
MKG
1346
1347
af59614d 1348=head2 LimitField
84fb5b46
MKG
1349
1350Takes a paramhash with the fields FIELD, OPERATOR, VALUE and DESCRIPTION
1351Generally best called from LimitFoo methods
1352
1353=cut
1354
af59614d 1355sub LimitField {
84fb5b46
MKG
1356 my $self = shift;
1357 my %args = (
1358 FIELD => undef,
1359 OPERATOR => '=',
1360 VALUE => undef,
1361 DESCRIPTION => undef,
1362 @_
1363 );
1364 $args{'DESCRIPTION'} = $self->loc(
1365 "[_1] [_2] [_3]", $args{'FIELD'},
1366 $args{'OPERATOR'}, $args{'VALUE'}
1367 )
1368 if ( !defined $args{'DESCRIPTION'} );
1369
af59614d
MKG
1370
1371 if ($self->_isLimited > 1) {
1372 RT->Deprecated( Message => "Mixing old-style LimitFoo methods with Limit is deprecated" );
1373 }
1374 $self->{using_restrictions} = 1;
1375
84fb5b46
MKG
1376 my $index = $self->_NextIndex;
1377
1378# make the TicketRestrictions hash the equivalent of whatever we just passed in;
1379
1380 %{ $self->{'TicketRestrictions'}{$index} } = %args;
1381
1382 $self->{'RecalcTicketLimits'} = 1;
1383
84fb5b46
MKG
1384 return ($index);
1385}
1386
1387
1388
1389
1390=head2 LimitQueue
1391
1392LimitQueue takes a paramhash with the fields OPERATOR and VALUE.
1393OPERATOR is one of = or !=. (It defaults to =).
1394VALUE is a queue id or Name.
1395
1396
1397=cut
1398
1399sub LimitQueue {
1400 my $self = shift;
1401 my %args = (
1402 VALUE => undef,
1403 OPERATOR => '=',
1404 @_
1405 );
1406
1407 #TODO VALUE should also take queue objects
1408 if ( defined $args{'VALUE'} && $args{'VALUE'} !~ /^\d+$/ ) {
1409 my $queue = RT::Queue->new( $self->CurrentUser );
1410 $queue->Load( $args{'VALUE'} );
1411 $args{'VALUE'} = $queue->Id;
1412 }
1413
1414 # What if they pass in an Id? Check for isNum() and convert to
1415 # string.
1416
1417 #TODO check for a valid queue here
1418
af59614d 1419 $self->LimitField(
84fb5b46
MKG
1420 FIELD => 'Queue',
1421 VALUE => $args{'VALUE'},
1422 OPERATOR => $args{'OPERATOR'},
1423 DESCRIPTION => join(
1424 ' ', $self->loc('Queue'), $args{'OPERATOR'}, $args{'VALUE'},
1425 ),
1426 );
1427
1428}
1429
1430
1431
1432=head2 LimitStatus
1433
1434Takes a paramhash with the fields OPERATOR and VALUE.
1435OPERATOR is one of = or !=.
1436VALUE is a status.
1437
1438RT adds Status != 'deleted' until object has
1439allow_deleted_search internal property set.
1440$tickets->{'allow_deleted_search'} = 1;
1441$tickets->LimitStatus( VALUE => 'deleted' );
1442
1443=cut
1444
1445sub LimitStatus {
1446 my $self = shift;
1447 my %args = (
1448 OPERATOR => '=',
1449 @_
1450 );
af59614d 1451 $self->LimitField(
84fb5b46
MKG
1452 FIELD => 'Status',
1453 VALUE => $args{'VALUE'},
1454 OPERATOR => $args{'OPERATOR'},
1455 DESCRIPTION => join( ' ',
1456 $self->loc('Status'), $args{'OPERATOR'},
1457 $self->loc( $args{'VALUE'} ) ),
1458 );
1459}
1460
af59614d
MKG
1461=head2 LimitToActiveStatus
1462
1463Limits the status to L<RT::Queue/ActiveStatusArray>
1464
1465TODO: make this respect lifecycles for the queues associated with the search
84fb5b46 1466
af59614d
MKG
1467=cut
1468
1469sub LimitToActiveStatus {
1470 my $self = shift;
1471
1472 my @active = RT::Queue->ActiveStatusArray();
1473 for my $active (@active) {
1474 $self->LimitStatus(
1475 VALUE => $active,
1476 );
1477 }
1478}
1479
1480=head2 LimitToInactiveStatus
1481
1482Limits the status to L<RT::Queue/InactiveStatusArray>
1483
1484TODO: make this respect lifecycles for the queues associated with the search
1485
1486=cut
1487
1488sub LimitToInactiveStatus {
1489 my $self = shift;
1490
1491 my @active = RT::Queue->InactiveStatusArray();
1492 for my $active (@active) {
1493 $self->LimitStatus(
1494 VALUE => $active,
1495 );
1496 }
1497}
84fb5b46
MKG
1498
1499=head2 IgnoreType
1500
1501If called, this search will not automatically limit the set of results found
1502to tickets of type "Ticket". Tickets of other types, such as "project" and
1503"approval" will be found.
1504
1505=cut
1506
1507sub IgnoreType {
1508 my $self = shift;
1509
1510 # Instead of faking a Limit that later gets ignored, fake up the
1511 # fact that we're already looking at type, so that the check in
af59614d 1512 # FromSQL goes down the right branch
84fb5b46
MKG
1513
1514 # $self->LimitType(VALUE => '__any');
af59614d 1515 $self->{_sql_looking_at}{type} = 1;
84fb5b46
MKG
1516}
1517
1518
1519
1520=head2 LimitType
1521
1522Takes a paramhash with the fields OPERATOR and VALUE.
1523OPERATOR is one of = or !=, it defaults to "=".
1524VALUE is a string to search for in the type of the ticket.
1525
1526
1527
1528=cut
1529
1530sub LimitType {
1531 my $self = shift;
1532 my %args = (
1533 OPERATOR => '=',
1534 VALUE => undef,
1535 @_
1536 );
af59614d 1537 $self->LimitField(
84fb5b46
MKG
1538 FIELD => 'Type',
1539 VALUE => $args{'VALUE'},
1540 OPERATOR => $args{'OPERATOR'},
1541 DESCRIPTION => join( ' ',
c36a7e1d 1542 $self->loc('Type'), $args{'OPERATOR'}, $args{'VALUE'}, ),
84fb5b46
MKG
1543 );
1544}
1545
1546
1547
1548
1549
1550=head2 LimitSubject
1551
1552Takes a paramhash with the fields OPERATOR and VALUE.
1553OPERATOR is one of = or !=.
1554VALUE is a string to search for in the subject of the ticket.
1555
1556=cut
1557
1558sub LimitSubject {
1559 my $self = shift;
1560 my %args = (@_);
af59614d 1561 $self->LimitField(
84fb5b46
MKG
1562 FIELD => 'Subject',
1563 VALUE => $args{'VALUE'},
1564 OPERATOR => $args{'OPERATOR'},
1565 DESCRIPTION => join( ' ',
1566 $self->loc('Subject'), $args{'OPERATOR'}, $args{'VALUE'}, ),
1567 );
1568}
1569
1570
1571
1572# Things that can be > < = !=
1573
1574
1575=head2 LimitId
1576
1577Takes a paramhash with the fields OPERATOR and VALUE.
1578OPERATOR is one of =, >, < or !=.
1579VALUE is a ticket Id to search for
1580
1581=cut
1582
1583sub LimitId {
1584 my $self = shift;
1585 my %args = (
1586 OPERATOR => '=',
1587 @_
1588 );
1589
af59614d 1590 $self->LimitField(
84fb5b46
MKG
1591 FIELD => 'id',
1592 VALUE => $args{'VALUE'},
1593 OPERATOR => $args{'OPERATOR'},
1594 DESCRIPTION =>
1595 join( ' ', $self->loc('Id'), $args{'OPERATOR'}, $args{'VALUE'}, ),
1596 );
1597}
1598
1599
1600
1601=head2 LimitPriority
1602
1603Takes a paramhash with the fields OPERATOR and VALUE.
1604OPERATOR is one of =, >, < or !=.
403d7b0b 1605VALUE is a value to match the ticket's priority against
84fb5b46
MKG
1606
1607=cut
1608
1609sub LimitPriority {
1610 my $self = shift;
1611 my %args = (@_);
af59614d 1612 $self->LimitField(
84fb5b46
MKG
1613 FIELD => 'Priority',
1614 VALUE => $args{'VALUE'},
1615 OPERATOR => $args{'OPERATOR'},
1616 DESCRIPTION => join( ' ',
1617 $self->loc('Priority'),
1618 $args{'OPERATOR'}, $args{'VALUE'}, ),
1619 );
1620}
1621
1622
1623
1624=head2 LimitInitialPriority
1625
1626Takes a paramhash with the fields OPERATOR and VALUE.
1627OPERATOR is one of =, >, < or !=.
403d7b0b 1628VALUE is a value to match the ticket's initial priority against
84fb5b46
MKG
1629
1630
1631=cut
1632
1633sub LimitInitialPriority {
1634 my $self = shift;
1635 my %args = (@_);
af59614d 1636 $self->LimitField(
84fb5b46
MKG
1637 FIELD => 'InitialPriority',
1638 VALUE => $args{'VALUE'},
1639 OPERATOR => $args{'OPERATOR'},
1640 DESCRIPTION => join( ' ',
1641 $self->loc('Initial Priority'), $args{'OPERATOR'},
1642 $args{'VALUE'}, ),
1643 );
1644}
1645
1646
1647
1648=head2 LimitFinalPriority
1649
1650Takes a paramhash with the fields OPERATOR and VALUE.
1651OPERATOR is one of =, >, < or !=.
403d7b0b 1652VALUE is a value to match the ticket's final priority against
84fb5b46
MKG
1653
1654=cut
1655
1656sub LimitFinalPriority {
1657 my $self = shift;
1658 my %args = (@_);
af59614d 1659 $self->LimitField(
84fb5b46
MKG
1660 FIELD => 'FinalPriority',
1661 VALUE => $args{'VALUE'},
1662 OPERATOR => $args{'OPERATOR'},
1663 DESCRIPTION => join( ' ',
1664 $self->loc('Final Priority'), $args{'OPERATOR'},
1665 $args{'VALUE'}, ),
1666 );
1667}
1668
1669
1670
1671=head2 LimitTimeWorked
1672
1673Takes a paramhash with the fields OPERATOR and VALUE.
1674OPERATOR is one of =, >, < or !=.
1675VALUE is a value to match the ticket's TimeWorked attribute
1676
1677=cut
1678
1679sub LimitTimeWorked {
1680 my $self = shift;
1681 my %args = (@_);
af59614d 1682 $self->LimitField(
84fb5b46
MKG
1683 FIELD => 'TimeWorked',
1684 VALUE => $args{'VALUE'},
1685 OPERATOR => $args{'OPERATOR'},
1686 DESCRIPTION => join( ' ',
1687 $self->loc('Time Worked'),
1688 $args{'OPERATOR'}, $args{'VALUE'}, ),
1689 );
1690}
1691
1692
1693
1694=head2 LimitTimeLeft
1695
1696Takes a paramhash with the fields OPERATOR and VALUE.
1697OPERATOR is one of =, >, < or !=.
1698VALUE is a value to match the ticket's TimeLeft attribute
1699
1700=cut
1701
1702sub LimitTimeLeft {
1703 my $self = shift;
1704 my %args = (@_);
af59614d 1705 $self->LimitField(
84fb5b46
MKG
1706 FIELD => 'TimeLeft',
1707 VALUE => $args{'VALUE'},
1708 OPERATOR => $args{'OPERATOR'},
1709 DESCRIPTION => join( ' ',
1710 $self->loc('Time Left'),
1711 $args{'OPERATOR'}, $args{'VALUE'}, ),
1712 );
1713}
1714
1715
1716
1717
1718
1719=head2 LimitContent
1720
1721Takes a paramhash with the fields OPERATOR and VALUE.
1722OPERATOR is one of =, LIKE, NOT LIKE or !=.
1723VALUE is a string to search for in the body of the ticket
1724
1725=cut
1726
1727sub LimitContent {
1728 my $self = shift;
1729 my %args = (@_);
af59614d 1730 $self->LimitField(
84fb5b46
MKG
1731 FIELD => 'Content',
1732 VALUE => $args{'VALUE'},
1733 OPERATOR => $args{'OPERATOR'},
1734 DESCRIPTION => join( ' ',
1735 $self->loc('Ticket content'), $args{'OPERATOR'},
1736 $args{'VALUE'}, ),
1737 );
1738}
1739
1740
1741
1742=head2 LimitFilename
1743
1744Takes a paramhash with the fields OPERATOR and VALUE.
1745OPERATOR is one of =, LIKE, NOT LIKE or !=.
1746VALUE is a string to search for in the body of the ticket
1747
1748=cut
1749
1750sub LimitFilename {
1751 my $self = shift;
1752 my %args = (@_);
af59614d 1753 $self->LimitField(
84fb5b46
MKG
1754 FIELD => 'Filename',
1755 VALUE => $args{'VALUE'},
1756 OPERATOR => $args{'OPERATOR'},
1757 DESCRIPTION => join( ' ',
1758 $self->loc('Attachment filename'), $args{'OPERATOR'},
1759 $args{'VALUE'}, ),
1760 );
1761}
1762
1763
1764=head2 LimitContentType
1765
1766Takes a paramhash with the fields OPERATOR and VALUE.
1767OPERATOR is one of =, LIKE, NOT LIKE or !=.
1768VALUE is a content type to search ticket attachments for
1769
1770=cut
1771
1772sub LimitContentType {
1773 my $self = shift;
1774 my %args = (@_);
af59614d 1775 $self->LimitField(
84fb5b46
MKG
1776 FIELD => 'ContentType',
1777 VALUE => $args{'VALUE'},
1778 OPERATOR => $args{'OPERATOR'},
1779 DESCRIPTION => join( ' ',
1780 $self->loc('Ticket content type'), $args{'OPERATOR'},
1781 $args{'VALUE'}, ),
1782 );
1783}
1784
1785
1786
1787
1788
1789=head2 LimitOwner
1790
1791Takes a paramhash with the fields OPERATOR and VALUE.
1792OPERATOR is one of = or !=.
1793VALUE is a user id.
1794
1795=cut
1796
1797sub LimitOwner {
1798 my $self = shift;
1799 my %args = (
1800 OPERATOR => '=',
1801 @_
1802 );
1803
1804 my $owner = RT::User->new( $self->CurrentUser );
1805 $owner->Load( $args{'VALUE'} );
1806
1807 # FIXME: check for a valid $owner
af59614d 1808 $self->LimitField(
84fb5b46
MKG
1809 FIELD => 'Owner',
1810 VALUE => $args{'VALUE'},
1811 OPERATOR => $args{'OPERATOR'},
1812 DESCRIPTION => join( ' ',
1813 $self->loc('Owner'), $args{'OPERATOR'}, $owner->Name(), ),
1814 );
1815
1816}
1817
1818
1819
1820
1821=head2 LimitWatcher
1822
1823 Takes a paramhash with the fields OPERATOR, TYPE and VALUE.
1824 OPERATOR is one of =, LIKE, NOT LIKE or !=.
403d7b0b 1825 VALUE is a value to match the ticket's watcher email addresses against
84fb5b46
MKG
1826 TYPE is the sort of watchers you want to match against. Leave it undef if you want to search all of them
1827
1828
1829=cut
1830
1831sub LimitWatcher {
1832 my $self = shift;
1833 my %args = (
1834 OPERATOR => '=',
1835 VALUE => undef,
1836 TYPE => undef,
1837 @_
1838 );
1839
1840 #build us up a description
1841 my ( $watcher_type, $desc );
1842 if ( $args{'TYPE'} ) {
1843 $watcher_type = $args{'TYPE'};
1844 }
1845 else {
1846 $watcher_type = "Watcher";
1847 }
1848
af59614d 1849 $self->LimitField(
84fb5b46
MKG
1850 FIELD => $watcher_type,
1851 VALUE => $args{'VALUE'},
1852 OPERATOR => $args{'OPERATOR'},
1853 TYPE => $args{'TYPE'},
1854 DESCRIPTION => join( ' ',
1855 $self->loc($watcher_type),
1856 $args{'OPERATOR'}, $args{'VALUE'}, ),
1857 );
1858}
1859
1860
1861
1862
1863
1864
1865=head2 LimitLinkedTo
1866
1867LimitLinkedTo takes a paramhash with two fields: TYPE and TARGET
1868TYPE limits the sort of link we want to search on
1869
1870TYPE = { RefersTo, MemberOf, DependsOn }
1871
1872TARGET is the id or URI of the TARGET of the link
1873
1874=cut
1875
1876sub LimitLinkedTo {
1877 my $self = shift;
1878 my %args = (
1879 TARGET => undef,
1880 TYPE => undef,
1881 OPERATOR => '=',
1882 @_
1883 );
1884
af59614d 1885 $self->LimitField(
84fb5b46
MKG
1886 FIELD => 'LinkedTo',
1887 BASE => undef,
1888 TARGET => $args{'TARGET'},
1889 TYPE => $args{'TYPE'},
1890 DESCRIPTION => $self->loc(
1891 "Tickets [_1] by [_2]",
1892 $self->loc( $args{'TYPE'} ),
1893 $args{'TARGET'}
1894 ),
1895 OPERATOR => $args{'OPERATOR'},
1896 );
1897}
1898
1899
1900
1901=head2 LimitLinkedFrom
1902
1903LimitLinkedFrom takes a paramhash with two fields: TYPE and BASE
1904TYPE limits the sort of link we want to search on
1905
1906
1907BASE is the id or URI of the BASE of the link
1908
1909=cut
1910
1911sub LimitLinkedFrom {
1912 my $self = shift;
1913 my %args = (
1914 BASE => undef,
1915 TYPE => undef,
1916 OPERATOR => '=',
1917 @_
1918 );
1919
1920 # translate RT2 From/To naming to RT3 TicketSQL naming
1921 my %fromToMap = qw(DependsOn DependentOn
1922 MemberOf HasMember
1923 RefersTo ReferredToBy);
1924
1925 my $type = $args{'TYPE'};
1926 $type = $fromToMap{$type} if exists( $fromToMap{$type} );
1927
af59614d 1928 $self->LimitField(
84fb5b46
MKG
1929 FIELD => 'LinkedTo',
1930 TARGET => undef,
1931 BASE => $args{'BASE'},
1932 TYPE => $type,
1933 DESCRIPTION => $self->loc(
1934 "Tickets [_1] [_2]",
1935 $self->loc( $args{'TYPE'} ),
1936 $args{'BASE'},
1937 ),
1938 OPERATOR => $args{'OPERATOR'},
1939 );
1940}
1941
1942
1943sub LimitMemberOf {
1944 my $self = shift;
1945 my $ticket_id = shift;
1946 return $self->LimitLinkedTo(
1947 @_,
1948 TARGET => $ticket_id,
1949 TYPE => 'MemberOf',
1950 );
1951}
1952
1953
1954sub LimitHasMember {
1955 my $self = shift;
1956 my $ticket_id = shift;
1957 return $self->LimitLinkedFrom(
1958 @_,
1959 BASE => "$ticket_id",
1960 TYPE => 'HasMember',
1961 );
1962
1963}
1964
1965
1966
1967sub LimitDependsOn {
1968 my $self = shift;
1969 my $ticket_id = shift;
1970 return $self->LimitLinkedTo(
1971 @_,
1972 TARGET => $ticket_id,
1973 TYPE => 'DependsOn',
1974 );
1975
1976}
1977
1978
1979
1980sub LimitDependedOnBy {
1981 my $self = shift;
1982 my $ticket_id = shift;
1983 return $self->LimitLinkedFrom(
1984 @_,
1985 BASE => $ticket_id,
1986 TYPE => 'DependentOn',
1987 );
1988
1989}
1990
1991
1992
1993sub LimitRefersTo {
1994 my $self = shift;
1995 my $ticket_id = shift;
1996 return $self->LimitLinkedTo(
1997 @_,
1998 TARGET => $ticket_id,
1999 TYPE => 'RefersTo',
2000 );
2001
2002}
2003
2004
2005
2006sub LimitReferredToBy {
2007 my $self = shift;
2008 my $ticket_id = shift;
2009 return $self->LimitLinkedFrom(
2010 @_,
2011 BASE => $ticket_id,
2012 TYPE => 'ReferredToBy',
2013 );
2014}
2015
2016
2017
2018
2019
2020=head2 LimitDate (FIELD => 'DateField', OPERATOR => $oper, VALUE => $ISODate)
2021
2022Takes a paramhash with the fields FIELD OPERATOR and VALUE.
2023
2024OPERATOR is one of > or <
2025VALUE is a date and time in ISO format in GMT
2026FIELD is one of Starts, Started, Told, Created, Resolved, LastUpdated
2027
2028There are also helper functions of the form LimitFIELD that eliminate
2029the need to pass in a FIELD argument.
2030
2031=cut
2032
2033sub LimitDate {
2034 my $self = shift;
2035 my %args = (
2036 FIELD => undef,
2037 VALUE => undef,
2038 OPERATOR => undef,
2039
2040 @_
2041 );
2042
2043 #Set the description if we didn't get handed it above
2044 unless ( $args{'DESCRIPTION'} ) {
2045 $args{'DESCRIPTION'} = $args{'FIELD'} . " "
2046 . $args{'OPERATOR'} . " "
2047 . $args{'VALUE'} . " GMT";
2048 }
2049
af59614d 2050 $self->LimitField(%args);
84fb5b46
MKG
2051
2052}
2053
2054
2055sub LimitCreated {
2056 my $self = shift;
2057 $self->LimitDate( FIELD => 'Created', @_ );
2058}
2059
2060sub LimitDue {
2061 my $self = shift;
2062 $self->LimitDate( FIELD => 'Due', @_ );
2063
2064}
2065
2066sub LimitStarts {
2067 my $self = shift;
2068 $self->LimitDate( FIELD => 'Starts', @_ );
2069
2070}
2071
2072sub LimitStarted {
2073 my $self = shift;
2074 $self->LimitDate( FIELD => 'Started', @_ );
2075}
2076
2077sub LimitResolved {
2078 my $self = shift;
2079 $self->LimitDate( FIELD => 'Resolved', @_ );
2080}
2081
2082sub LimitTold {
2083 my $self = shift;
2084 $self->LimitDate( FIELD => 'Told', @_ );
2085}
2086
2087sub LimitLastUpdated {
2088 my $self = shift;
2089 $self->LimitDate( FIELD => 'LastUpdated', @_ );
2090}
2091
2092#
2093
2094=head2 LimitTransactionDate (OPERATOR => $oper, VALUE => $ISODate)
2095
2096Takes a paramhash with the fields FIELD OPERATOR and VALUE.
2097
2098OPERATOR is one of > or <
2099VALUE is a date and time in ISO format in GMT
2100
2101
2102=cut
2103
2104sub LimitTransactionDate {
2105 my $self = shift;
2106 my %args = (
2107 FIELD => 'TransactionDate',
2108 VALUE => undef,
2109 OPERATOR => undef,
2110
2111 @_
2112 );
2113
2114 # <20021217042756.GK28744@pallas.fsck.com>
2115 # "Kill It" - Jesse.
2116
2117 #Set the description if we didn't get handed it above
2118 unless ( $args{'DESCRIPTION'} ) {
2119 $args{'DESCRIPTION'} = $args{'FIELD'} . " "
2120 . $args{'OPERATOR'} . " "
2121 . $args{'VALUE'} . " GMT";
2122 }
2123
af59614d 2124 $self->LimitField(%args);
84fb5b46
MKG
2125
2126}
2127
2128
2129
2130
2131=head2 LimitCustomField
2132
2133Takes a paramhash of key/value pairs with the following keys:
2134
2135=over 4
2136
2137=item CUSTOMFIELD - CustomField name or id. If a name is passed, an additional parameter QUEUE may also be passed to distinguish the custom field.
2138
2139=item OPERATOR - The usual Limit operators
2140
2141=item VALUE - The value to compare against
2142
2143=back
2144
2145=cut
2146
2147sub LimitCustomField {
2148 my $self = shift;
2149 my %args = (
2150 VALUE => undef,
2151 CUSTOMFIELD => undef,
2152 OPERATOR => '=',
2153 DESCRIPTION => undef,
2154 FIELD => 'CustomFieldValue',
2155 QUOTEVALUE => 1,
2156 @_
2157 );
2158
2159 my $CF = RT::CustomField->new( $self->CurrentUser );
2160 if ( $args{CUSTOMFIELD} =~ /^\d+$/ ) {
2161 $CF->Load( $args{CUSTOMFIELD} );
2162 }
2163 else {
2164 $CF->LoadByNameAndQueue(
2165 Name => $args{CUSTOMFIELD},
2166 Queue => $args{QUEUE}
2167 );
2168 $args{CUSTOMFIELD} = $CF->Id;
2169 }
2170
2171 #If we are looking to compare with a null value.
2172 if ( $args{'OPERATOR'} =~ /^is$/i ) {
2173 $args{'DESCRIPTION'}
2174 ||= $self->loc( "Custom field [_1] has no value.", $CF->Name );
2175 }
2176 elsif ( $args{'OPERATOR'} =~ /^is not$/i ) {
2177 $args{'DESCRIPTION'}
2178 ||= $self->loc( "Custom field [_1] has a value.", $CF->Name );
2179 }
2180
2181 # if we're not looking to compare with a null value
2182 else {
2183 $args{'DESCRIPTION'} ||= $self->loc( "Custom field [_1] [_2] [_3]",
2184 $CF->Name, $args{OPERATOR}, $args{VALUE} );
2185 }
2186
2187 if ( defined $args{'QUEUE'} && $args{'QUEUE'} =~ /\D/ ) {
2188 my $QueueObj = RT::Queue->new( $self->CurrentUser );
2189 $QueueObj->Load( $args{'QUEUE'} );
2190 $args{'QUEUE'} = $QueueObj->Id;
2191 }
2192 delete $args{'QUEUE'} unless defined $args{'QUEUE'} && length $args{'QUEUE'};
2193
2194 my @rest;
2195 @rest = ( ENTRYAGGREGATOR => 'AND' )
2196 if ( $CF->Type eq 'SelectMultiple' );
2197
af59614d 2198 $self->LimitField(
84fb5b46
MKG
2199 VALUE => $args{VALUE},
2200 FIELD => "CF"
af59614d 2201 .(defined $args{'QUEUE'}? ".$args{'QUEUE'}" : '' )
84fb5b46
MKG
2202 .".{" . $CF->Name . "}",
2203 OPERATOR => $args{OPERATOR},
2204 CUSTOMFIELD => 1,
2205 @rest,
2206 );
2207
2208 $self->{'RecalcTicketLimits'} = 1;
2209}
2210
2211
2212
2213=head2 _NextIndex
2214
2215Keep track of the counter for the array of restrictions
2216
2217=cut
2218
2219sub _NextIndex {
2220 my $self = shift;
2221 return ( $self->{'restriction_index'}++ );
2222}
2223
2224
2225
2226
2227sub _Init {
2228 my $self = shift;
2229 $self->{'table'} = "Tickets";
2230 $self->{'RecalcTicketLimits'} = 1;
84fb5b46
MKG
2231 $self->{'restriction_index'} = 1;
2232 $self->{'primary_key'} = "id";
2233 delete $self->{'items_array'};
2234 delete $self->{'item_map'};
2235 delete $self->{'columns_to_display'};
2236 $self->SUPER::_Init(@_);
2237
af59614d
MKG
2238 $self->_InitSQL();
2239}
84fb5b46 2240
af59614d
MKG
2241sub _InitSQL {
2242 my $self = shift;
2243 # Private Member Variables (which should get cleaned)
2244 $self->{'_sql_transalias'} = undef;
2245 $self->{'_sql_trattachalias'} = undef;
2246 $self->{'_sql_cf_alias'} = undef;
2247 $self->{'_sql_object_cfv_alias'} = undef;
2248 $self->{'_sql_watcher_join_users_alias'} = undef;
2249 $self->{'_sql_query'} = '';
2250 $self->{'_sql_looking_at'} = {};
84fb5b46
MKG
2251}
2252
2253
2254sub Count {
2255 my $self = shift;
2256 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2257 return ( $self->SUPER::Count() );
2258}
2259
2260
2261sub CountAll {
2262 my $self = shift;
2263 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2264 return ( $self->SUPER::CountAll() );
2265}
2266
2267
2268
2269=head2 ItemsArrayRef
2270
2271Returns a reference to the set of all items found in this search
2272
2273=cut
2274
2275sub ItemsArrayRef {
2276 my $self = shift;
2277
2278 return $self->{'items_array'} if $self->{'items_array'};
2279
2280 my $placeholder = $self->_ItemsCounter;
2281 $self->GotoFirstItem();
2282 while ( my $item = $self->Next ) {
2283 push( @{ $self->{'items_array'} }, $item );
2284 }
2285 $self->GotoItem($placeholder);
2286 $self->{'items_array'}
2287 = $self->ItemsOrderBy( $self->{'items_array'} );
2288
2289 return $self->{'items_array'};
2290}
2291
2292sub ItemsArrayRefWindow {
2293 my $self = shift;
2294 my $window = shift;
2295
2296 my @old = ($self->_ItemsCounter, $self->RowsPerPage, $self->FirstRow+1);
2297
2298 $self->RowsPerPage( $window );
2299 $self->FirstRow(1);
2300 $self->GotoFirstItem;
2301
2302 my @res;
2303 while ( my $item = $self->Next ) {
2304 push @res, $item;
2305 }
2306
2307 $self->RowsPerPage( $old[1] );
2308 $self->FirstRow( $old[2] );
2309 $self->GotoItem( $old[0] );
2310
2311 return \@res;
2312}
2313
2314
2315sub Next {
2316 my $self = shift;
2317
2318 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2319
2320 my $Ticket = $self->SUPER::Next;
2321 return $Ticket unless $Ticket;
2322
2323 if ( $Ticket->__Value('Status') eq 'deleted'
2324 && !$self->{'allow_deleted_search'} )
2325 {
2326 return $self->Next;
2327 }
2328 elsif ( RT->Config->Get('UseSQLForACLChecks') ) {
2329 # if we found a ticket with this option enabled then
2330 # all tickets we found are ACLed, cache this fact
2331 my $key = join ";:;", $self->CurrentUser->id, 'ShowTicket', 'RT::Ticket-'. $Ticket->id;
af59614d 2332 $RT::Principal::_ACL_CACHE->{ $key } = 1;
84fb5b46
MKG
2333 return $Ticket;
2334 }
2335 elsif ( $Ticket->CurrentUserHasRight('ShowTicket') ) {
2336 # has rights
2337 return $Ticket;
2338 }
2339 else {
2340 # If the user doesn't have the right to show this ticket
2341 return $self->Next;
2342 }
2343}
2344
2345sub _DoSearch {
2346 my $self = shift;
2347 $self->CurrentUserCanSee if RT->Config->Get('UseSQLForACLChecks');
2348 return $self->SUPER::_DoSearch( @_ );
2349}
2350
2351sub _DoCount {
2352 my $self = shift;
2353 $self->CurrentUserCanSee if RT->Config->Get('UseSQLForACLChecks');
2354 return $self->SUPER::_DoCount( @_ );
2355}
2356
2357sub _RolesCanSee {
2358 my $self = shift;
2359
2360 my $cache_key = 'RolesHasRight;:;ShowTicket';
2361
af59614d 2362 if ( my $cached = $RT::Principal::_ACL_CACHE->{ $cache_key } ) {
84fb5b46
MKG
2363 return %$cached;
2364 }
2365
2366 my $ACL = RT::ACL->new( RT->SystemUser );
2367 $ACL->Limit( FIELD => 'RightName', VALUE => 'ShowTicket' );
2368 $ACL->Limit( FIELD => 'PrincipalType', OPERATOR => '!=', VALUE => 'Group' );
2369 my $principal_alias = $ACL->Join(
2370 ALIAS1 => 'main',
2371 FIELD1 => 'PrincipalId',
2372 TABLE2 => 'Principals',
2373 FIELD2 => 'id',
2374 );
2375 $ACL->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
2376
2377 my %res = ();
2378 foreach my $ACE ( @{ $ACL->ItemsArrayRef } ) {
2379 my $role = $ACE->__Value('PrincipalType');
2380 my $type = $ACE->__Value('ObjectType');
2381 if ( $type eq 'RT::System' ) {
2382 $res{ $role } = 1;
2383 }
2384 elsif ( $type eq 'RT::Queue' ) {
2385 next if $res{ $role } && !ref $res{ $role };
2386 push @{ $res{ $role } ||= [] }, $ACE->__Value('ObjectId');
2387 }
2388 else {
2389 $RT::Logger->error('ShowTicket right is granted on unsupported object');
2390 }
2391 }
af59614d 2392 $RT::Principal::_ACL_CACHE->{ $cache_key } = \%res;
84fb5b46
MKG
2393 return %res;
2394}
2395
2396sub _DirectlyCanSeeIn {
2397 my $self = shift;
2398 my $id = $self->CurrentUser->id;
2399
2400 my $cache_key = 'User-'. $id .';:;ShowTicket;:;DirectlyCanSeeIn';
af59614d 2401 if ( my $cached = $RT::Principal::_ACL_CACHE->{ $cache_key } ) {
84fb5b46
MKG
2402 return @$cached;
2403 }
2404
2405 my $ACL = RT::ACL->new( RT->SystemUser );
2406 $ACL->Limit( FIELD => 'RightName', VALUE => 'ShowTicket' );
2407 my $principal_alias = $ACL->Join(
2408 ALIAS1 => 'main',
2409 FIELD1 => 'PrincipalId',
2410 TABLE2 => 'Principals',
2411 FIELD2 => 'id',
2412 );
2413 $ACL->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
2414 my $cgm_alias = $ACL->Join(
2415 ALIAS1 => 'main',
2416 FIELD1 => 'PrincipalId',
2417 TABLE2 => 'CachedGroupMembers',
2418 FIELD2 => 'GroupId',
2419 );
2420 $ACL->Limit( ALIAS => $cgm_alias, FIELD => 'MemberId', VALUE => $id );
2421 $ACL->Limit( ALIAS => $cgm_alias, FIELD => 'Disabled', VALUE => 0 );
2422
2423 my @res = ();
2424 foreach my $ACE ( @{ $ACL->ItemsArrayRef } ) {
2425 my $type = $ACE->__Value('ObjectType');
2426 if ( $type eq 'RT::System' ) {
2427 # If user is direct member of a group that has the right
2428 # on the system then he can see any ticket
af59614d 2429 $RT::Principal::_ACL_CACHE->{ $cache_key } = [-1];
84fb5b46
MKG
2430 return (-1);
2431 }
2432 elsif ( $type eq 'RT::Queue' ) {
2433 push @res, $ACE->__Value('ObjectId');
2434 }
2435 else {
2436 $RT::Logger->error('ShowTicket right is granted on unsupported object');
2437 }
2438 }
af59614d 2439 $RT::Principal::_ACL_CACHE->{ $cache_key } = \@res;
84fb5b46
MKG
2440 return @res;
2441}
2442
2443sub CurrentUserCanSee {
2444 my $self = shift;
2445 return if $self->{'_sql_current_user_can_see_applied'};
2446
2447 return $self->{'_sql_current_user_can_see_applied'} = 1
2448 if $self->CurrentUser->UserObj->HasRight(
2449 Right => 'SuperUser', Object => $RT::System
2450 );
2451
af59614d
MKG
2452 local $self->{using_restrictions};
2453
84fb5b46
MKG
2454 my $id = $self->CurrentUser->id;
2455
2456 # directly can see in all queues then we have nothing to do
2457 my @direct_queues = $self->_DirectlyCanSeeIn;
2458 return $self->{'_sql_current_user_can_see_applied'} = 1
2459 if @direct_queues && $direct_queues[0] == -1;
2460
2461 my %roles = $self->_RolesCanSee;
2462 {
2463 my %skip = map { $_ => 1 } @direct_queues;
2464 foreach my $role ( keys %roles ) {
2465 next unless ref $roles{ $role };
2466
2467 my @queues = grep !$skip{$_}, @{ $roles{ $role } };
2468 if ( @queues ) {
2469 $roles{ $role } = \@queues;
2470 } else {
2471 delete $roles{ $role };
2472 }
2473 }
2474 }
2475
2476# there is no global watchers, only queues and tickes, if at
2477# some point we will add global roles then it's gonna blow
2478# the idea here is that if the right is set globaly for a role
2479# and user plays this role for a queue directly not a ticket
2480# then we have to check in advance
2481 if ( my @tmp = grep $_ ne 'Owner' && !ref $roles{ $_ }, keys %roles ) {
2482
2483 my $groups = RT::Groups->new( RT->SystemUser );
af59614d 2484 $groups->Limit( FIELD => 'Domain', VALUE => 'RT::Queue-Role', CASESENSITIVE => 0 );
84fb5b46 2485 foreach ( @tmp ) {
af59614d 2486 $groups->Limit( FIELD => 'Name', VALUE => $_, CASESENSITIVE => 0 );
84fb5b46
MKG
2487 }
2488 my $principal_alias = $groups->Join(
2489 ALIAS1 => 'main',
2490 FIELD1 => 'id',
2491 TABLE2 => 'Principals',
2492 FIELD2 => 'id',
2493 );
2494 $groups->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
2495 my $cgm_alias = $groups->Join(
2496 ALIAS1 => 'main',
2497 FIELD1 => 'id',
2498 TABLE2 => 'CachedGroupMembers',
2499 FIELD2 => 'GroupId',
2500 );
2501 $groups->Limit( ALIAS => $cgm_alias, FIELD => 'MemberId', VALUE => $id );
2502 $groups->Limit( ALIAS => $cgm_alias, FIELD => 'Disabled', VALUE => 0 );
2503 while ( my $group = $groups->Next ) {
2504 push @direct_queues, $group->Instance;
2505 }
2506 }
2507
2508 unless ( @direct_queues || keys %roles ) {
af59614d 2509 $self->Limit(
84fb5b46
MKG
2510 SUBCLAUSE => 'ACL',
2511 ALIAS => 'main',
2512 FIELD => 'id',
2513 VALUE => 0,
2514 ENTRYAGGREGATOR => 'AND',
2515 );
2516 return $self->{'_sql_current_user_can_see_applied'} = 1;
2517 }
2518
2519 {
2520 my $join_roles = keys %roles;
2521 $join_roles = 0 if $join_roles == 1 && $roles{'Owner'};
2522 my ($role_group_alias, $cgm_alias);
2523 if ( $join_roles ) {
2524 $role_group_alias = $self->_RoleGroupsJoin( New => 1 );
2525 $cgm_alias = $self->_GroupMembersJoin( GroupsAlias => $role_group_alias );
af59614d 2526 $self->Limit(
84fb5b46
MKG
2527 LEFTJOIN => $cgm_alias,
2528 FIELD => 'MemberId',
2529 OPERATOR => '=',
2530 VALUE => $id,
2531 );
2532 }
2533 my $limit_queues = sub {
2534 my $ea = shift;
2535 my @queues = @_;
2536
2537 return unless @queues;
2538 if ( @queues == 1 ) {
af59614d 2539 $self->Limit(
84fb5b46
MKG
2540 SUBCLAUSE => 'ACL',
2541 ALIAS => 'main',
2542 FIELD => 'Queue',
2543 VALUE => $_[0],
2544 ENTRYAGGREGATOR => $ea,
2545 );
2546 } else {
2547 $self->SUPER::_OpenParen('ACL');
2548 foreach my $q ( @queues ) {
af59614d 2549 $self->Limit(
84fb5b46
MKG
2550 SUBCLAUSE => 'ACL',
2551 ALIAS => 'main',
2552 FIELD => 'Queue',
2553 VALUE => $q,
2554 ENTRYAGGREGATOR => $ea,
2555 );
2556 $ea = 'OR';
2557 }
2558 $self->SUPER::_CloseParen('ACL');
2559 }
2560 return 1;
2561 };
2562
2563 $self->SUPER::_OpenParen('ACL');
2564 my $ea = 'AND';
2565 $ea = 'OR' if $limit_queues->( $ea, @direct_queues );
2566 while ( my ($role, $queues) = each %roles ) {
2567 $self->SUPER::_OpenParen('ACL');
2568 if ( $role eq 'Owner' ) {
af59614d 2569 $self->Limit(
84fb5b46
MKG
2570 SUBCLAUSE => 'ACL',
2571 FIELD => 'Owner',
2572 VALUE => $id,
2573 ENTRYAGGREGATOR => $ea,
2574 );
2575 }
2576 else {
af59614d 2577 $self->Limit(
84fb5b46
MKG
2578 SUBCLAUSE => 'ACL',
2579 ALIAS => $cgm_alias,
2580 FIELD => 'MemberId',
2581 OPERATOR => 'IS NOT',
2582 VALUE => 'NULL',
2583 QUOTEVALUE => 0,
2584 ENTRYAGGREGATOR => $ea,
2585 );
af59614d 2586 $self->Limit(
84fb5b46
MKG
2587 SUBCLAUSE => 'ACL',
2588 ALIAS => $role_group_alias,
af59614d 2589 FIELD => 'Name',
84fb5b46
MKG
2590 VALUE => $role,
2591 ENTRYAGGREGATOR => 'AND',
af59614d 2592 CASESENSITIVE => 0,
84fb5b46
MKG
2593 );
2594 }
2595 $limit_queues->( 'AND', @$queues ) if ref $queues;
2596 $ea = 'OR' if $ea eq 'AND';
2597 $self->SUPER::_CloseParen('ACL');
2598 }
2599 $self->SUPER::_CloseParen('ACL');
2600 }
2601 return $self->{'_sql_current_user_can_see_applied'} = 1;
2602}
2603
2604
2605
84fb5b46
MKG
2606=head2 ClearRestrictions
2607
2608Removes all restrictions irretrievably
2609
2610=cut
2611
2612sub ClearRestrictions {
2613 my $self = shift;
2614 delete $self->{'TicketRestrictions'};
af59614d 2615 $self->{_sql_looking_at} = {};
84fb5b46
MKG
2616 $self->{'RecalcTicketLimits'} = 1;
2617}
2618
84fb5b46
MKG
2619# Convert a set of oldstyle SB Restrictions to Clauses for RQL
2620
2621sub _RestrictionsToClauses {
2622 my $self = shift;
2623
2624 my %clause;
2625 foreach my $row ( keys %{ $self->{'TicketRestrictions'} } ) {
2626 my $restriction = $self->{'TicketRestrictions'}{$row};
2627
2628 # We need to reimplement the subclause aggregation that SearchBuilder does.
2629 # Default Subclause is ALIAS.FIELD, and default ALIAS is 'main',
2630 # Then SB AND's the different Subclauses together.
2631
2632 # So, we want to group things into Subclauses, convert them to
2633 # SQL, and then join them with the appropriate DefaultEA.
2634 # Then join each subclause group with AND.
2635
2636 my $field = $restriction->{'FIELD'};
2637 my $realfield = $field; # CustomFields fake up a fieldname, so
2638 # we need to figure that out
2639
2640 # One special case
2641 # Rewrite LinkedTo meta field to the real field
2642 if ( $field =~ /LinkedTo/ ) {
2643 $realfield = $field = $restriction->{'TYPE'};
2644 }
2645
2646 # Two special case
2647 # Handle subkey fields with a different real field
2648 if ( $field =~ /^(\w+)\./ ) {
2649 $realfield = $1;
2650 }
2651
2652 die "I don't know about $field yet"
2653 unless ( exists $FIELD_METADATA{$realfield}
2654 or $restriction->{CUSTOMFIELD} );
2655
2656 my $type = $FIELD_METADATA{$realfield}->[0];
2657 my $op = $restriction->{'OPERATOR'};
2658
2659 my $value = (
2660 grep {defined}
2661 map { $restriction->{$_} } qw(VALUE TICKET BASE TARGET)
2662 )[0];
2663
2664 # this performs the moral equivalent of defined or/dor/C<//>,
2665 # without the short circuiting.You need to use a 'defined or'
2666 # type thing instead of just checking for truth values, because
2667 # VALUE could be 0.(i.e. "false")
2668
2669 # You could also use this, but I find it less aesthetic:
2670 # (although it does short circuit)
2671 #( defined $restriction->{'VALUE'}? $restriction->{VALUE} :
2672 # defined $restriction->{'TICKET'} ?
2673 # $restriction->{TICKET} :
2674 # defined $restriction->{'BASE'} ?
2675 # $restriction->{BASE} :
2676 # defined $restriction->{'TARGET'} ?
2677 # $restriction->{TARGET} )
2678
2679 my $ea = $restriction->{ENTRYAGGREGATOR}
2680 || $DefaultEA{$type}
2681 || "AND";
2682 if ( ref $ea ) {
2683 die "Invalid operator $op for $field ($type)"
2684 unless exists $ea->{$op};
2685 $ea = $ea->{$op};
2686 }
2687
2688 # Each CustomField should be put into a different Clause so they
2689 # are ANDed together.
2690 if ( $restriction->{CUSTOMFIELD} ) {
2691 $realfield = $field;
2692 }
2693
2694 exists $clause{$realfield} or $clause{$realfield} = [];
2695
2696 # Escape Quotes
2697 $field =~ s!(['\\])!\\$1!g;
2698 $value =~ s!(['\\])!\\$1!g;
2699 my $data = [ $ea, $type, $field, $op, $value ];
2700
2701 # here is where we store extra data, say if it's a keyword or
2702 # something. (I.e. "TYPE SPECIFIC STUFF")
2703
b5747ff2
MKG
2704 if (lc $ea eq 'none') {
2705 $clause{$realfield} = [ $data ];
2706 } else {
2707 push @{ $clause{$realfield} }, $data;
2708 }
84fb5b46
MKG
2709 }
2710 return \%clause;
2711}
2712
af59614d 2713=head2 ClausesToSQL
84fb5b46 2714
af59614d 2715=cut
84fb5b46 2716
af59614d
MKG
2717sub ClausesToSQL {
2718 my $self = shift;
2719 my $clauses = shift;
2720 my @sql;
84fb5b46 2721
af59614d
MKG
2722 for my $f (keys %{$clauses}) {
2723 my $sql;
2724 my $first = 1;
84fb5b46 2725
af59614d
MKG
2726 # Build SQL from the data hash
2727 for my $data ( @{ $clauses->{$f} } ) {
2728 $sql .= $data->[0] unless $first; $first=0; # ENTRYAGGREGATOR
2729 $sql .= " '". $data->[2] . "' "; # FIELD
2730 $sql .= $data->[3] . " "; # OPERATOR
2731 $sql .= "'". $data->[4] . "' "; # VALUE
2732 }
2733
2734 push @sql, " ( " . $sql . " ) ";
2735 }
2736
2737 return join("AND",@sql);
2738}
84fb5b46
MKG
2739
2740sub _ProcessRestrictions {
2741 my $self = shift;
2742
84fb5b46
MKG
2743 delete $self->{'items_array'};
2744 delete $self->{'item_map'};
2745 delete $self->{'raw_rows'};
84fb5b46
MKG
2746 delete $self->{'count_all'};
2747
af59614d 2748 my $sql = $self->Query;
84fb5b46
MKG
2749 if ( !$sql || $self->{'RecalcTicketLimits'} ) {
2750
af59614d 2751 local $self->{using_restrictions};
84fb5b46
MKG
2752 # "Restrictions to Clauses Branch\n";
2753 my $clauseRef = eval { $self->_RestrictionsToClauses; };
2754 if ($@) {
2755 $RT::Logger->error( "RestrictionsToClauses: " . $@ );
2756 $self->FromSQL("");
2757 }
2758 else {
2759 $sql = $self->ClausesToSQL($clauseRef);
2760 $self->FromSQL($sql) if $sql;
2761 }
2762 }
2763
2764 $self->{'RecalcTicketLimits'} = 0;
2765
2766}
2767
2768=head2 _BuildItemMap
2769
2770Build up a L</ItemMap> of first/last/next/prev items, so that we can
2771display search nav quickly.
2772
2773=cut
2774
2775sub _BuildItemMap {
2776 my $self = shift;
2777
2778 my $window = RT->Config->Get('TicketsItemMapSize');
2779
2780 $self->{'item_map'} = {};
2781
2782 my $items = $self->ItemsArrayRefWindow( $window );
2783 return unless $items && @$items;
2784
2785 my $prev = 0;
2786 $self->{'item_map'}{'first'} = $items->[0]->EffectiveId;
2787 for ( my $i = 0; $i < @$items; $i++ ) {
2788 my $item = $items->[$i];
2789 my $id = $item->EffectiveId;
2790 $self->{'item_map'}{$id}{'defined'} = 1;
2791 $self->{'item_map'}{$id}{'prev'} = $prev;
2792 $self->{'item_map'}{$id}{'next'} = $items->[$i+1]->EffectiveId
2793 if $items->[$i+1];
2794 $prev = $id;
2795 }
2796 $self->{'item_map'}{'last'} = $prev
2797 if !$window || @$items < $window;
2798}
2799
2800=head2 ItemMap
2801
2802Returns an a map of all items found by this search. The map is a hash
2803of the form:
2804
2805 {
2806 first => <first ticket id found>,
2807 last => <last ticket id found or undef>,
2808
2809 <ticket id> => {
2810 prev => <the ticket id found before>,
2811 next => <the ticket id found after>,
2812 },
2813 <ticket id> => {
2814 prev => ...,
2815 next => ...,
2816 },
2817 }
2818
2819=cut
2820
2821sub ItemMap {
2822 my $self = shift;
2823 $self->_BuildItemMap unless $self->{'item_map'};
2824 return $self->{'item_map'};
2825}
2826
2827
2828
2829
2830=head2 PrepForSerialization
2831
2832You don't want to serialize a big tickets object, as
2833the {items} hash will be instantly invalid _and_ eat
2834lots of space
2835
2836=cut
2837
2838sub PrepForSerialization {
2839 my $self = shift;
2840 delete $self->{'items'};
2841 delete $self->{'items_array'};
2842 $self->RedoSearch();
2843}
2844
2845=head1 FLAGS
2846
2847RT::Tickets supports several flags which alter search behavior:
2848
2849
2850allow_deleted_search (Otherwise never show deleted tickets in search results)
84fb5b46
MKG
2851
2852These flags are set by calling
2853
2854$tickets->{'flagname'} = 1;
2855
2856BUG: There should be an API for this
2857
2858
2859
2860=cut
2861
af59614d
MKG
2862=head2 FromSQL
2863
2864Convert a RT-SQL string into a set of SearchBuilder restrictions.
2865
2866Returns (1, 'Status message') on success and (0, 'Error Message') on
2867failure.
2868
2869=cut
2870
2871sub _parser {
2872 my ($self,$string) = @_;
2873 my $ea = '';
2874
2875 # Bundling of joins is implemented by dynamically tracking a parallel query
2876 # tree in %sub_tree as the TicketSQL is parsed.
2877 #
2878 # Only positive, OR'd watcher conditions are bundled currently. Each key
2879 # in %sub_tree is a watcher type (Requestor, Cc, AdminCc) or the generic
2880 # "Watcher" for any watcher type. Owner is not bundled because it is
2881 # denormalized into a Tickets column and doesn't need a join. AND'd
2882 # conditions are not bundled since a record may have multiple watchers
2883 # which independently match the conditions, thus necessitating two joins.
2884 #
2885 # The values of %sub_tree are arrayrefs made up of:
2886 #
2887 # * Open parentheses "(" pushed on by the OpenParen callback
2888 # * Arrayrefs of bundled join aliases pushed on by the Condition callback
2889 # * Entry aggregators (AND/OR) pushed on by the EntryAggregator callback
2890 #
2891 # The CloseParen callback takes care of backing off the query trees until
2892 # outside of the just-closed parenthetical, thus restoring the tree state
2893 # an equivalent of before the parenthetical was entered.
2894 #
2895 # The Condition callback handles starting a new subtree or extending an
2896 # existing one, determining if bundling the current condition with any
2897 # subtree is possible, and pruning any dangling entry aggregators from
2898 # trees.
2899 #
2900
2901 my %sub_tree;
2902 my $depth = 0;
2903
2904 my %callback;
2905 $callback{'OpenParen'} = sub {
2906 $self->_OpenParen;
2907 $depth++;
2908 push @$_, '(' foreach values %sub_tree;
2909 };
2910 $callback{'CloseParen'} = sub {
2911 $self->_CloseParen;
2912 $depth--;
2913 foreach my $list ( values %sub_tree ) {
2914 if ( $list->[-1] eq '(' ) {
2915 pop @$list;
2916 pop @$list if $list->[-1] =~ /^(?:AND|OR)$/i;
2917 }
2918 else {
2919 pop @$list while $list->[-2] ne '(';
2920 $list->[-1] = pop @$list;
2921 }
2922 }
2923 };
2924 $callback{'EntryAggregator'} = sub {
2925 $ea = $_[0] || '';
2926 push @$_, $ea foreach grep @$_ && $_->[-1] ne '(', values %sub_tree;
2927 };
2928 $callback{'Condition'} = sub {
2929 my ($key, $op, $value) = @_;
2930
2931 my $negative_op = ($op eq '!=' || $op =~ /\bNOT\b/i);
2932 my $null_op = ( 'is not' eq lc($op) || 'is' eq lc($op) );
2933 # key has dot then it's compound variant and we have subkey
2934 my $subkey = '';
2935 ($key, $subkey) = ($1, $2) if $key =~ /^([^\.]+)\.(.+)$/;
2936
2937 # normalize key and get class (type)
2938 my $class;
2939 if (exists $LOWER_CASE_FIELDS{lc $key}) {
2940 $key = $LOWER_CASE_FIELDS{lc $key};
2941 $class = $FIELD_METADATA{$key}->[0];
2942 }
2943 die "Unknown field '$key' in '$string'" unless $class;
2944
2945 # replace __CurrentUser__ with id
2946 $value = $self->CurrentUser->id if $value eq '__CurrentUser__';
2947
2948
2949 unless( $dispatch{ $class } ) {
2950 die "No dispatch method for class '$class'"
2951 }
2952 my $sub = $dispatch{ $class };
2953
2954 my @res; my $bundle_with;
2955 if ( $class eq 'WATCHERFIELD' && $key ne 'Owner' && !$negative_op && (!$null_op || $subkey) ) {
2956 if ( !$sub_tree{$key} ) {
2957 $sub_tree{$key} = [ ('(')x$depth, \@res ];
2958 } else {
2959 $bundle_with = $self->_check_bundling_possibility( $string, @{ $sub_tree{$key} } );
2960 if ( $sub_tree{$key}[-1] eq '(' ) {
2961 push @{ $sub_tree{$key} }, \@res;
2962 }
2963 }
2964 }
2965
2966 # Remove our aggregator from subtrees where our condition didn't get added
2967 pop @$_ foreach grep @$_ && $_->[-1] =~ /^(?:AND|OR)$/i, values %sub_tree;
2968
2969 # A reference to @res may be pushed onto $sub_tree{$key} from
2970 # above, and we fill it here.
2971 @res = $sub->( $self, $key, $op, $value,
2972 SUBCLAUSE => '', # don't need anymore
2973 ENTRYAGGREGATOR => $ea,
2974 SUBKEY => $subkey,
2975 BUNDLE => $bundle_with,
2976 );
2977 $ea = '';
2978 };
2979 RT::SQL::Parse($string, \%callback);
2980}
2981
2982sub FromSQL {
2983 my ($self,$query) = @_;
2984
2985 {
2986 # preserve first_row and show_rows across the CleanSlate
2987 local ($self->{'first_row'}, $self->{'show_rows'}, $self->{_sql_looking_at});
2988 $self->CleanSlate;
2989 $self->_InitSQL();
2990 }
2991
2992 return (1, $self->loc("No Query")) unless $query;
84fb5b46 2993
af59614d
MKG
2994 $self->{_sql_query} = $query;
2995 eval {
2996 local $self->{parsing_ticketsql} = 1;
2997 $self->_parser( $query );
2998 };
2999 if ( $@ ) {
3000 my $error = "$@";
3001 $RT::Logger->error("Couldn't parse query: $error");
3002 return (0, $error);
3003 }
84fb5b46 3004
af59614d
MKG
3005 # We only want to look at EffectiveId's (mostly) for these searches.
3006 unless ( $self->{_sql_looking_at}{effectiveid} ) {
3007 # instead of EffectiveId = id we do IsMerged IS NULL
3008 $self->Limit(
3009 FIELD => 'IsMerged',
3010 OPERATOR => 'IS',
3011 VALUE => 'NULL',
3012 ENTRYAGGREGATOR => 'AND',
3013 QUOTEVALUE => 0,
3014 );
3015 }
3016 unless ( $self->{_sql_looking_at}{type} ) {
3017 $self->Limit( FIELD => 'Type', VALUE => 'ticket' );
3018 }
84fb5b46 3019
af59614d
MKG
3020 # We don't want deleted tickets unless 'allow_deleted_search' is set
3021 unless( $self->{'allow_deleted_search'} ) {
3022 $self->Limit(
3023 FIELD => 'Status',
3024 OPERATOR => '!=',
3025 VALUE => 'deleted',
3026 );
3027 }
3028
3029 # set SB's dirty flag
3030 $self->{'must_redo_search'} = 1;
3031 $self->{'RecalcTicketLimits'} = 0;
3032
3033 return (1, $self->loc("Valid Query"));
3034}
3035
3036=head2 Query
3037
3038Returns the last string passed to L</FromSQL>.
84fb5b46
MKG
3039
3040=cut
3041
af59614d
MKG
3042sub Query {
3043 my $self = shift;
3044 return $self->{_sql_query};
3045}
3046
3047sub _check_bundling_possibility {
84fb5b46 3048 my $self = shift;
af59614d
MKG
3049 my $string = shift;
3050 my @list = reverse @_;
3051 while (my $e = shift @list) {
3052 next if $e eq '(';
3053 if ( lc($e) eq 'and' ) {
3054 return undef;
3055 }
3056 elsif ( lc($e) eq 'or' ) {
3057 return shift @list;
3058 }
3059 else {
3060 # should not happen
3061 $RT::Logger->error(
3062 "Joins optimization failed when parsing '$string'. It's bug in RT, contact Best Practical"
3063 );
3064 die "Internal error. Contact your system administrator.";
3065 }
3066 }
3067 return undef;
84fb5b46 3068}
af59614d 3069
84fb5b46
MKG
3070RT::Base->_ImportOverlays();
3071
30721;