1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2011 Best Practical Solutions, LLC
6 # <sales@bestpractical.com>
8 # (Except where explicitly superseded by other copyright notices)
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
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.
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.
30 # CONTRIBUTION SUBMISSION POLICY:
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.)
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.
47 # END BPS TAGGED BLOCK }}}
51 # - Decimated ProcessRestrictions and broke it into multiple
52 # functions joined by a LUT
53 # - Semi-Generic SQL stuff moved to another file
55 # Known Issues: FIXME!
57 # - ClearRestrictions and Reinitialization is messy and unclear. The
58 # only good way to do it is to create a new RT::Tickets object.
62 RT::Tickets - A collection of Ticket objects
68 my $tickets = new RT::Tickets($CurrentUser);
72 A collection of RT::Tickets.
82 no warnings qw(redefine);
85 use DBIx::SearchBuilder::Unique;
87 # Configuration Tables:
89 # FIELD_METADATA is a mapping of searchable Field name, to Type, and other
92 our %FIELD_METADATA = (
93 Status => [ 'ENUM', ], #loc_left_pair
94 Queue => [ 'ENUM' => 'Queue', ], #loc_left_pair
95 Type => [ 'ENUM', ], #loc_left_pair
96 Creator => [ 'ENUM' => 'User', ], #loc_left_pair
97 LastUpdatedBy => [ 'ENUM' => 'User', ], #loc_left_pair
98 Owner => [ 'WATCHERFIELD' => 'Owner', ], #loc_left_pair
99 EffectiveId => [ 'INT', ], #loc_left_pair
100 id => [ 'ID', ], #loc_left_pair
101 InitialPriority => [ 'INT', ], #loc_left_pair
102 FinalPriority => [ 'INT', ], #loc_left_pair
103 Priority => [ 'INT', ], #loc_left_pair
104 TimeLeft => [ 'INT', ], #loc_left_pair
105 TimeWorked => [ 'INT', ], #loc_left_pair
106 TimeEstimated => [ 'INT', ], #loc_left_pair
108 Linked => [ 'LINK' ], #loc_left_pair
109 LinkedTo => [ 'LINK' => 'To' ], #loc_left_pair
110 LinkedFrom => [ 'LINK' => 'From' ], #loc_left_pair
111 MemberOf => [ 'LINK' => To => 'MemberOf', ], #loc_left_pair
112 DependsOn => [ 'LINK' => To => 'DependsOn', ], #loc_left_pair
113 RefersTo => [ 'LINK' => To => 'RefersTo', ], #loc_left_pair
114 HasMember => [ 'LINK' => From => 'MemberOf', ], #loc_left_pair
115 DependentOn => [ 'LINK' => From => 'DependsOn', ], #loc_left_pair
116 DependedOnBy => [ 'LINK' => From => 'DependsOn', ], #loc_left_pair
117 ReferredToBy => [ 'LINK' => From => 'RefersTo', ], #loc_left_pair
118 Told => [ 'DATE' => 'Told', ], #loc_left_pair
119 Starts => [ 'DATE' => 'Starts', ], #loc_left_pair
120 Started => [ 'DATE' => 'Started', ], #loc_left_pair
121 Due => [ 'DATE' => 'Due', ], #loc_left_pair
122 Resolved => [ 'DATE' => 'Resolved', ], #loc_left_pair
123 LastUpdated => [ 'DATE' => 'LastUpdated', ], #loc_left_pair
124 Created => [ 'DATE' => 'Created', ], #loc_left_pair
125 Subject => [ 'STRING', ], #loc_left_pair
126 Content => [ 'TRANSFIELD', ], #loc_left_pair
127 ContentType => [ 'TRANSFIELD', ], #loc_left_pair
128 Filename => [ 'TRANSFIELD', ], #loc_left_pair
129 TransactionDate => [ 'TRANSDATE', ], #loc_left_pair
130 Requestor => [ 'WATCHERFIELD' => 'Requestor', ], #loc_left_pair
131 Requestors => [ 'WATCHERFIELD' => 'Requestor', ], #loc_left_pair
132 Cc => [ 'WATCHERFIELD' => 'Cc', ], #loc_left_pair
133 AdminCc => [ 'WATCHERFIELD' => 'AdminCc', ], #loc_left_pair
134 Watcher => [ 'WATCHERFIELD', ], #loc_left_pair
135 QueueCc => [ 'WATCHERFIELD' => 'Cc' => 'Queue', ], #loc_left_pair
136 QueueAdminCc => [ 'WATCHERFIELD' => 'AdminCc' => 'Queue', ], #loc_left_pair
137 QueueWatcher => [ 'WATCHERFIELD' => undef => 'Queue', ], #loc_left_pair
138 CustomFieldValue => [ 'CUSTOMFIELD', ], #loc_left_pair
139 CustomField => [ 'CUSTOMFIELD', ], #loc_left_pair
140 CF => [ 'CUSTOMFIELD', ], #loc_left_pair
141 Updated => [ 'TRANSDATE', ], #loc_left_pair
142 RequestorGroup => [ 'MEMBERSHIPFIELD' => 'Requestor', ], #loc_left_pair
143 CCGroup => [ 'MEMBERSHIPFIELD' => 'Cc', ], #loc_left_pair
144 AdminCCGroup => [ 'MEMBERSHIPFIELD' => 'AdminCc', ], #loc_left_pair
145 WatcherGroup => [ 'MEMBERSHIPFIELD', ], #loc_left_pair
146 HasAttribute => [ 'HASATTRIBUTE', 1 ],
147 HasNoAttribute => [ 'HASATTRIBUTE', 0 ],
149 Customer => [ 'FREESIDEFIELD', ],
150 # Agentnum => [ 'FREESIDEFIELD', ],
151 # Classnum => [ 'FREESIDEFIELD', ],
152 # Refnum => [ 'FREESIDEFIELD', ],
153 # Tagnum => [ 'FREESIDEFIELD', 'cust_tag' ],
154 WillResolve => [ 'DATE' => 'WillResolve', ], #loc_left_pair
157 our %SEARCHABLE_SUBFIELDS = (
159 EmailAddress Name RealName Nickname Organization Address1 Address2
160 WorkPhone HomePhone MobilePhone PagerPhone id
164 # Mapping of Field Type to Function
166 ENUM => \&_EnumLimit,
169 LINK => \&_LinkLimit,
170 DATE => \&_DateLimit,
171 STRING => \&_StringLimit,
172 TRANSFIELD => \&_TransLimit,
173 TRANSDATE => \&_TransDateLimit,
174 WATCHERFIELD => \&_WatcherLimit,
175 MEMBERSHIPFIELD => \&_WatcherMembershipLimit,
176 CUSTOMFIELD => \&_CustomFieldLimit,
177 HASATTRIBUTE => \&_HasAttributeLimit,
178 FREESIDEFIELD => \&_FreesideFieldLimit,
180 our %can_bundle = ();# WATCHERFIELD => "yes", );
182 # Default EntryAggregator per type
183 # if you specify OP, you must specify all valid OPs
224 # Helper functions for passing the above lexically scoped tables above
225 # into Tickets_Overlay_SQL.
226 sub FIELDS { return \%FIELD_METADATA }
227 sub dispatch { return \%dispatch }
228 sub can_bundle { return \%can_bundle }
230 # Bring in the clowns.
231 require RT::Tickets_Overlay_SQL;
235 our @SORTFIELDS = qw(id Status
237 Owner Created Due Starts Started
239 Resolved LastUpdated Priority TimeWorked TimeLeft);
243 Returns the list of fields that lists of tickets can easily be sorted by
249 return (@SORTFIELDS);
254 # BEGIN SQL STUFF *********************************
259 $self->SUPER::CleanSlate( @_ );
260 delete $self->{$_} foreach qw(
262 _sql_group_members_aliases
263 _sql_object_cfv_alias
264 _sql_role_group_aliases
267 _sql_u_watchers_alias_for_sort
268 _sql_u_watchers_aliases
269 _sql_current_user_can_see_applied
273 =head1 Limit Helper Routines
275 These routines are the targets of a dispatch table depending on the
276 type of field. They all share the same signature:
278 my ($self,$field,$op,$value,@rest) = @_;
280 The values in @rest should be suitable for passing directly to
281 DBIx::SearchBuilder::Limit.
283 Essentially they are an expanded/broken out (and much simplified)
284 version of what ProcessRestrictions used to do. They're also much
285 more clearly delineated by the TYPE of field being processed.
294 my ( $sb, $field, $op, $value, @rest ) = @_;
296 return $sb->_IntLimit( $field, $op, $value, @rest ) unless $value eq '__Bookmarked__';
298 die "Invalid operator $op for __Bookmarked__ search on $field"
299 unless $op =~ /^(=|!=)$/;
302 my $tmp = $sb->CurrentUser->UserObj->FirstAttribute('Bookmarks');
303 $tmp = $tmp->Content if $tmp;
308 return $sb->_SQLLimit(
315 # as bookmarked tickets can be merged we have to use a join
316 # but it should be pretty lightweight
317 my $tickets_alias = $sb->Join(
322 FIELD2 => 'EffectiveId',
326 my $ea = $op eq '='? 'OR': 'AND';
327 foreach my $id ( sort @bookmarks ) {
329 ALIAS => $tickets_alias,
333 $first? (@rest): ( ENTRYAGGREGATOR => $ea )
341 Handle Fields which are limited to certain values, and potentially
342 need to be looked up from another class.
344 This subroutine actually handles two different kinds of fields. For
345 some the user is responsible for limiting the values. (i.e. Status,
348 For others, the value specified by the user will be looked by via
352 name of class to lookup in (Optional)
357 my ( $sb, $field, $op, $value, @rest ) = @_;
359 # SQL::Statement changes != to <>. (Can we remove this now?)
360 $op = "!=" if $op eq "<>";
362 die "Invalid Operation: $op for $field"
366 my $meta = $FIELD_METADATA{$field};
367 if ( defined $meta->[1] && defined $value && $value !~ /^\d+$/ ) {
368 my $class = "RT::" . $meta->[1];
369 my $o = $class->new( $sb->CurrentUser );
383 Handle fields where the values are limited to integers. (For example,
384 Priority, TimeWorked.)
392 my ( $sb, $field, $op, $value, @rest ) = @_;
394 die "Invalid Operator $op for $field"
395 unless $op =~ /^(=|!=|>|<|>=|<=)$/;
407 Handle fields which deal with links between tickets. (MemberOf, DependsOn)
410 1: Direction (From, To)
411 2: Link Type (MemberOf, DependsOn, RefersTo)
416 my ( $sb, $field, $op, $value, @rest ) = @_;
418 my $meta = $FIELD_METADATA{$field};
419 die "Invalid Operator $op for $field" unless $op =~ /^(=|!=|IS|IS NOT)$/io;
422 if ( $op eq '!=' || $op =~ /\bNOT\b/i ) {
426 $is_null = 1 if !$value || $value =~ /^null$/io;
428 my $direction = $meta->[1] || '';
429 my ($matchfield, $linkfield) = ('', '');
430 if ( $direction eq 'To' ) {
431 ($matchfield, $linkfield) = ("Target", "Base");
433 elsif ( $direction eq 'From' ) {
434 ($matchfield, $linkfield) = ("Base", "Target");
436 elsif ( $direction ) {
437 die "Invalid link direction '$direction' for $field\n";
440 $sb->_LinkLimit( 'LinkedTo', $op, $value, @rest );
442 'LinkedFrom', $op, $value, @rest,
443 ENTRYAGGREGATOR => (($is_negative && $is_null) || (!$is_null && !$is_negative))? 'OR': 'AND',
451 $op = ($op =~ /^(=|IS)$/)? 'IS': 'IS NOT';
453 elsif ( $value =~ /\D/ ) {
456 $matchfield = "Local$matchfield" if $is_local;
458 #For doing a left join to find "unlinked tickets" we want to generate a query that looks like this
459 # SELECT main.* FROM Tickets main
460 # LEFT JOIN Links Links_1 ON ( (Links_1.Type = 'MemberOf')
461 # AND(main.id = Links_1.LocalTarget))
462 # WHERE Links_1.LocalBase IS NULL;
465 my $linkalias = $sb->Join(
470 FIELD2 => 'Local' . $linkfield
473 LEFTJOIN => $linkalias,
481 FIELD => $matchfield,
488 my $linkalias = $sb->Join(
493 FIELD2 => 'Local' . $linkfield
496 LEFTJOIN => $linkalias,
502 LEFTJOIN => $linkalias,
503 FIELD => $matchfield,
510 FIELD => $matchfield,
511 OPERATOR => $is_negative? 'IS': 'IS NOT',
520 Handle date fields. (Created, LastTold..)
523 1: type of link. (Probably not necessary.)
528 my ( $sb, $field, $op, $value, @rest ) = @_;
530 die "Invalid Date Op: $op"
531 unless $op =~ /^(=|>|<|>=|<=)$/;
533 my $meta = $FIELD_METADATA{$field};
534 die "Incorrect Meta Data for $field"
535 unless ( defined $meta->[1] );
537 $sb->_DateFieldLimit( $meta->[1], $op, $value, @rest );
540 # Factor this out for use by custom fields
542 sub _DateFieldLimit {
543 my ( $sb, $field, $op, $value, @rest ) = @_;
545 my $date = RT::Date->new( $sb->CurrentUser );
546 $date->Set( Format => 'unknown', Value => $value );
550 # if we're specifying =, that means we want everything on a
551 # particular single day. in the database, we need to check for >
552 # and < the edges of that day.
554 # Except if the value is 'this month' or 'last month', check
555 # > and < the edges of the month.
557 my ($daystart, $dayend);
558 if ( lc($value) eq 'this month' ) {
560 $date->SetToStart('month', Timezone => 'server');
561 $daystart = $date->ISO;
562 $date->AddMonth(Timezone => 'server');
563 $dayend = $date->ISO;
565 elsif ( lc($value) eq 'last month' ) {
567 $date->SetToStart('month', Timezone => 'server');
568 $dayend = $date->ISO;
570 $date->SetToStart('month', Timezone => 'server');
571 $daystart = $date->ISO;
574 $date->SetToMidnight( Timezone => 'server' );
575 $daystart = $date->ISO;
577 $dayend = $date->ISO;
594 ENTRYAGGREGATOR => 'AND',
612 Handle simple fields which are just strings. (Subject,Type)
620 my ( $sb, $field, $op, $value, @rest ) = @_;
624 # =, !=, LIKE, NOT LIKE
625 if ( (!defined $value || !length $value)
626 && lc($op) ne 'is' && lc($op) ne 'is not'
627 && RT->Config->Get('DatabaseType') eq 'Oracle'
629 my $negative = 1 if $op eq '!=' || $op =~ /^NOT\s/;
630 $op = $negative? 'IS NOT': 'IS';
643 =head2 _TransDateLimit
645 Handle fields limiting based on Transaction Date.
647 The inpupt value must be in a format parseable by Time::ParseDate
654 # This routine should really be factored into translimit.
655 sub _TransDateLimit {
656 my ( $sb, $field, $op, $value, @rest ) = @_;
658 # See the comments for TransLimit, they apply here too
660 unless ( $sb->{_sql_transalias} ) {
661 $sb->{_sql_transalias} = $sb->Join(
664 TABLE2 => 'Transactions',
665 FIELD2 => 'ObjectId',
668 ALIAS => $sb->{_sql_transalias},
669 FIELD => 'ObjectType',
670 VALUE => 'RT::Ticket',
671 ENTRYAGGREGATOR => 'AND',
675 my $date = RT::Date->new( $sb->CurrentUser );
676 $date->Set( Format => 'unknown', Value => $value );
681 # if we're specifying =, that means we want everything on a
682 # particular single day. in the database, we need to check for >
683 # and < the edges of that day.
685 $date->SetToMidnight( Timezone => 'server' );
686 my $daystart = $date->ISO;
688 my $dayend = $date->ISO;
691 ALIAS => $sb->{_sql_transalias},
699 ALIAS => $sb->{_sql_transalias},
705 ENTRYAGGREGATOR => 'AND',
710 # not searching for a single day
713 #Search for the right field
715 ALIAS => $sb->{_sql_transalias},
729 Limit based on the Content of a transaction or the ContentType.
738 # Content, ContentType, Filename
740 # If only this was this simple. We've got to do something
743 #Basically, we want to make sure that the limits apply to
744 #the same attachment, rather than just another attachment
745 #for the same ticket, no matter how many clauses we lump
746 #on. We put them in TicketAliases so that they get nuked
747 #when we redo the join.
749 # In the SQL, we might have
750 # (( Content = foo ) or ( Content = bar AND Content = baz ))
751 # The AND group should share the same Alias.
753 # Actually, maybe it doesn't matter. We use the same alias and it
754 # works itself out? (er.. different.)
756 # Steal more from _ProcessRestrictions
758 # FIXME: Maybe look at the previous FooLimit call, and if it was a
759 # TransLimit and EntryAggregator == AND, reuse the Aliases?
761 # Or better - store the aliases on a per subclause basis - since
762 # those are going to be the things we want to relate to each other,
765 # maybe we should not allow certain kinds of aggregation of these
766 # clauses and do a psuedo regex instead? - the problem is getting
767 # them all into the same subclause when you have (A op B op C) - the
768 # way they get parsed in the tree they're in different subclauses.
770 my ( $self, $field, $op, $value, %rest ) = @_;
772 unless ( $self->{_sql_transalias} ) {
773 $self->{_sql_transalias} = $self->Join(
776 TABLE2 => 'Transactions',
777 FIELD2 => 'ObjectId',
780 ALIAS => $self->{_sql_transalias},
781 FIELD => 'ObjectType',
782 VALUE => 'RT::Ticket',
783 ENTRYAGGREGATOR => 'AND',
786 unless ( defined $self->{_sql_trattachalias} ) {
787 $self->{_sql_trattachalias} = $self->_SQLJoin(
788 TYPE => 'LEFT', # not all txns have an attachment
789 ALIAS1 => $self->{_sql_transalias},
791 TABLE2 => 'Attachments',
792 FIELD2 => 'TransactionId',
796 #Search for the right field
797 if ( $field eq 'Content' and RT->Config->Get('DontSearchFileAttachments') ) {
801 ALIAS => $self->{_sql_trattachalias},
808 ENTRYAGGREGATOR => 'AND',
809 ALIAS => $self->{_sql_trattachalias},
818 ALIAS => $self->{_sql_trattachalias},
831 Handle watcher limits. (Requestor, CC, etc..)
847 my $meta = $FIELD_METADATA{ $field };
848 my $type = $meta->[1] || '';
849 my $class = $meta->[2] || 'Ticket';
851 # Bail if the subfield is not allowed
853 and not grep { $_ eq $rest{SUBKEY} } @{$SEARCHABLE_SUBFIELDS{'User'}})
855 die "Invalid watcher subfield: '$rest{SUBKEY}'";
858 # Owner was ENUM field, so "Owner = 'xxx'" allowed user to
859 # search by id and Name at the same time, this is workaround
860 # to preserve backward compatibility
861 if ( $field eq 'Owner' ) {
862 if ( $op =~ /^!?=$/ && (!$rest{'SUBKEY'} || $rest{'SUBKEY'} eq 'Name' || $rest{'SUBKEY'} eq 'EmailAddress') ) {
863 my $o = RT::User->new( $self->CurrentUser );
864 my $method = ($rest{'SUBKEY'}||'') eq 'EmailAddress' ? 'LoadByEmail': 'Load';
865 $o->$method( $value );
874 if ( ($rest{'SUBKEY'}||'') eq 'id' ) {
884 $rest{SUBKEY} ||= 'EmailAddress';
886 my $groups = $self->_RoleGroupsJoin( Type => $type, Class => $class );
889 if ( $op =~ /^IS(?: NOT)?$/ ) {
890 my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
891 # to avoid joining the table Users into the query, we just join GM
892 # and make sure we don't match records where group is member of itself
894 LEFTJOIN => $group_members,
897 VALUE => "$group_members.MemberId",
901 ALIAS => $group_members,
908 elsif ( $op =~ /^!=$|^NOT\s+/i ) {
910 $op =~ s/!|NOT\s+//i;
912 # XXX: we have no way to build correct "Watcher.X != 'Y'" when condition
913 # "X = 'Y'" matches more then one user so we try to fetch two records and
914 # do the right thing when there is only one exist and semi-working solution
916 my $users_obj = RT::Users->new( $self->CurrentUser );
918 FIELD => $rest{SUBKEY},
923 $users_obj->RowsPerPage(2);
924 my @users = @{ $users_obj->ItemsArrayRef };
926 my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
929 $uid = $users[0]->id if @users;
931 LEFTJOIN => $group_members,
932 ALIAS => $group_members,
938 ALIAS => $group_members,
945 LEFTJOIN => $group_members,
948 VALUE => "$group_members.MemberId",
951 my $users = $self->Join(
953 ALIAS1 => $group_members,
954 FIELD1 => 'MemberId',
961 FIELD => $rest{SUBKEY},
975 my $group_members = $self->_GroupMembersJoin(
976 GroupsAlias => $groups,
980 my $users = $self->{'_sql_u_watchers_aliases'}{$group_members};
982 $users = $self->{'_sql_u_watchers_aliases'}{$group_members} =
983 $self->NewAlias('Users');
985 LEFTJOIN => $group_members,
986 ALIAS => $group_members,
988 VALUE => "$users.id",
993 # we join users table without adding some join condition between tables,
994 # the only conditions we have are conditions on the table iteslf,
995 # for example Users.EmailAddress = 'x'. We should add this condition to
996 # the top level of the query and bundle it with another similar conditions,
997 # for example "Users.EmailAddress = 'x' OR Users.EmailAddress = 'Y'".
998 # To achive this goal we use own SUBCLAUSE for conditions on the users table.
1001 SUBCLAUSE => '_sql_u_watchers_'. $users,
1003 FIELD => $rest{'SUBKEY'},
1008 # A condition which ties Users and Groups (role groups) is a left join condition
1009 # of CachedGroupMembers table. To get correct results of the query we check
1010 # if there are matches in CGM table or not using 'cgm.id IS NOT NULL'.
1013 ALIAS => $group_members,
1015 OPERATOR => 'IS NOT',
1022 sub _RoleGroupsJoin {
1024 my %args = (New => 0, Class => 'Ticket', Type => '', @_);
1025 return $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $args{'Type'} }
1026 if $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $args{'Type'} }
1029 # we always have watcher groups for ticket, so we use INNER join
1030 my $groups = $self->Join(
1032 FIELD1 => $args{'Class'} eq 'Queue'? 'Queue': 'id',
1034 FIELD2 => 'Instance',
1035 ENTRYAGGREGATOR => 'AND',
1037 $self->SUPER::Limit(
1038 LEFTJOIN => $groups,
1041 VALUE => 'RT::'. $args{'Class'} .'-Role',
1043 $self->SUPER::Limit(
1044 LEFTJOIN => $groups,
1047 VALUE => $args{'Type'},
1050 $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $args{'Type'} } = $groups
1051 unless $args{'New'};
1056 sub _GroupMembersJoin {
1058 my %args = (New => 1, GroupsAlias => undef, @_);
1060 return $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} }
1061 if $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} }
1064 my $alias = $self->Join(
1066 ALIAS1 => $args{'GroupsAlias'},
1068 TABLE2 => 'CachedGroupMembers',
1069 FIELD2 => 'GroupId',
1070 ENTRYAGGREGATOR => 'AND',
1072 $self->SUPER::Limit(
1075 FIELD => 'Disabled',
1079 $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} } = $alias
1080 unless $args{'New'};
1087 Helper function which provides joins to a watchers table both for limits
1094 my $type = shift || '';
1097 my $groups = $self->_RoleGroupsJoin( Type => $type );
1098 my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
1099 # XXX: work around, we must hide groups that
1100 # are members of the role group we search in,
1101 # otherwise them result in wrong NULLs in Users
1102 # table and break ordering. Now, we know that
1103 # RT doesn't allow to add groups as members of the
1104 # ticket roles, so we just hide entries in CGM table
1105 # with MemberId == GroupId from results
1106 $self->SUPER::Limit(
1107 LEFTJOIN => $group_members,
1110 VALUE => "$group_members.MemberId",
1113 my $users = $self->Join(
1115 ALIAS1 => $group_members,
1116 FIELD1 => 'MemberId',
1120 return ($groups, $group_members, $users);
1123 =head2 _WatcherMembershipLimit
1125 Handle watcher membership limits, i.e. whether the watcher belongs to a
1126 specific group or not.
1129 1: Field to query on
1131 SELECT DISTINCT main.*
1135 CachedGroupMembers CachedGroupMembers_2,
1138 (main.EffectiveId = main.id)
1140 (main.Status != 'deleted')
1142 (main.Type = 'ticket')
1145 (Users_3.EmailAddress = '22')
1147 (Groups_1.Domain = 'RT::Ticket-Role')
1149 (Groups_1.Type = 'RequestorGroup')
1152 Groups_1.Instance = main.id
1154 Groups_1.id = CachedGroupMembers_2.GroupId
1156 CachedGroupMembers_2.MemberId = Users_3.id
1157 ORDER BY main.id ASC
1162 sub _WatcherMembershipLimit {
1163 my ( $self, $field, $op, $value, @rest ) = @_;
1168 my $groups = $self->NewAlias('Groups');
1169 my $groupmembers = $self->NewAlias('CachedGroupMembers');
1170 my $users = $self->NewAlias('Users');
1171 my $memberships = $self->NewAlias('CachedGroupMembers');
1173 if ( ref $field ) { # gross hack
1174 my @bundle = @$field;
1176 for my $chunk (@bundle) {
1177 ( $field, $op, $value, @rest ) = @$chunk;
1179 ALIAS => $memberships,
1190 ALIAS => $memberships,
1198 # {{{ Tie to groups for tickets we care about
1202 VALUE => 'RT::Ticket-Role',
1203 ENTRYAGGREGATOR => 'AND'
1208 FIELD1 => 'Instance',
1215 # If we care about which sort of watcher
1216 my $meta = $FIELD_METADATA{$field};
1217 my $type = ( defined $meta->[1] ? $meta->[1] : undef );
1224 ENTRYAGGREGATOR => 'AND'
1231 ALIAS2 => $groupmembers,
1236 ALIAS1 => $groupmembers,
1237 FIELD1 => 'MemberId',
1243 ALIAS => $groupmembers,
1244 FIELD => 'Disabled',
1249 ALIAS1 => $memberships,
1250 FIELD1 => 'MemberId',
1256 ALIAS => $memberships,
1257 FIELD => 'Disabled',
1266 =head2 _CustomFieldDecipher
1268 Try and turn a CF descriptor into (cfid, cfname) object pair.
1272 sub _CustomFieldDecipher {
1273 my ($self, $string) = @_;
1275 my ($queue, $field, $column) = ($string =~ /^(?:(.+?)\.)?{(.+)}(?:\.(Content|LargeContent))?$/);
1276 $field ||= ($string =~ /^{(.*?)}$/)[0] || $string;
1280 my $q = RT::Queue->new( $self->CurrentUser );
1284 # $queue = $q->Name; # should we normalize the queue?
1285 $cf = $q->CustomField( $field );
1288 $RT::Logger->warning("Queue '$queue' doesn't exist, parsed from '$string'");
1292 elsif ( $field =~ /\D/ ) {
1294 my $cfs = RT::CustomFields->new( $self->CurrentUser );
1295 $cfs->Limit( FIELD => 'Name', VALUE => $field );
1296 $cfs->LimitToLookupType('RT::Queue-RT::Ticket');
1298 # if there is more then one field the current user can
1299 # see with the same name then we shouldn't return cf object
1300 # as we don't know which one to use
1303 $cf = undef if $cfs->Next;
1307 $cf = RT::CustomField->new( $self->CurrentUser );
1308 $cf->Load( $field );
1311 return ($queue, $field, $cf, $column);
1314 =head2 _CustomFieldJoin
1316 Factor out the Join of custom fields so we can use it for sorting too
1320 sub _CustomFieldJoin {
1321 my ($self, $cfkey, $cfid, $field) = @_;
1322 # Perform one Join per CustomField
1323 if ( $self->{_sql_object_cfv_alias}{$cfkey} ||
1324 $self->{_sql_cf_alias}{$cfkey} )
1326 return ( $self->{_sql_object_cfv_alias}{$cfkey},
1327 $self->{_sql_cf_alias}{$cfkey} );
1330 my ($TicketCFs, $CFs);
1332 $TicketCFs = $self->{_sql_object_cfv_alias}{$cfkey} = $self->Join(
1336 TABLE2 => 'ObjectCustomFieldValues',
1337 FIELD2 => 'ObjectId',
1339 $self->SUPER::Limit(
1340 LEFTJOIN => $TicketCFs,
1341 FIELD => 'CustomField',
1343 ENTRYAGGREGATOR => 'AND'
1347 my $ocfalias = $self->Join(
1350 TABLE2 => 'ObjectCustomFields',
1351 FIELD2 => 'ObjectId',
1354 $self->SUPER::Limit(
1355 LEFTJOIN => $ocfalias,
1356 ENTRYAGGREGATOR => 'OR',
1357 FIELD => 'ObjectId',
1361 $CFs = $self->{_sql_cf_alias}{$cfkey} = $self->Join(
1363 ALIAS1 => $ocfalias,
1364 FIELD1 => 'CustomField',
1365 TABLE2 => 'CustomFields',
1368 $self->SUPER::Limit(
1370 ENTRYAGGREGATOR => 'AND',
1371 FIELD => 'LookupType',
1372 VALUE => 'RT::Queue-RT::Ticket',
1374 $self->SUPER::Limit(
1376 ENTRYAGGREGATOR => 'AND',
1381 $TicketCFs = $self->{_sql_object_cfv_alias}{$cfkey} = $self->Join(
1385 TABLE2 => 'ObjectCustomFieldValues',
1386 FIELD2 => 'CustomField',
1388 $self->SUPER::Limit(
1389 LEFTJOIN => $TicketCFs,
1390 FIELD => 'ObjectId',
1393 ENTRYAGGREGATOR => 'AND',
1396 $self->SUPER::Limit(
1397 LEFTJOIN => $TicketCFs,
1398 FIELD => 'ObjectType',
1399 VALUE => 'RT::Ticket',
1400 ENTRYAGGREGATOR => 'AND'
1402 $self->SUPER::Limit(
1403 LEFTJOIN => $TicketCFs,
1404 FIELD => 'Disabled',
1407 ENTRYAGGREGATOR => 'AND'
1410 return ($TicketCFs, $CFs);
1413 =head2 _CustomFieldLimit
1415 Limit based on CustomFields
1422 sub _CustomFieldLimit {
1423 my ( $self, $_field, $op, $value, %rest ) = @_;
1425 my $field = $rest{'SUBKEY'} || die "No field specified";
1427 # For our sanity, we can only limit on one queue at a time
1429 my ($queue, $cfid, $cf, $column);
1430 ($queue, $field, $cf, $column) = $self->_CustomFieldDecipher( $field );
1431 $cfid = $cf ? $cf->id : 0 ;
1433 # If we're trying to find custom fields that don't match something, we
1434 # want tickets where the custom field has no value at all. Note that
1435 # we explicitly don't include the "IS NULL" case, since we would
1436 # otherwise end up with a redundant clause.
1438 my ($negative_op, $null_op, $inv_op, $range_op)
1439 = $self->ClassifySQLOperation( $op );
1443 return $op unless RT->Config->Get('DatabaseType') eq 'Oracle';
1444 return 'MATCHES' if $op eq '=';
1445 return 'NOT MATCHES' if $op eq '!=';
1449 my $single_value = !$cf || !$cfid || $cf->SingleValue;
1451 my $cfkey = $cfid ? $cfid : "$queue.$field";
1453 if ( $null_op && !$column ) {
1454 # IS[ NOT] NULL without column is the same as has[ no] any CF value,
1455 # we can reuse our default joins for this operation
1456 # with column specified we have different situation
1457 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
1460 ALIAS => $TicketCFs,
1469 OPERATOR => 'IS NOT',
1472 ENTRYAGGREGATOR => 'AND',
1476 elsif ( !$negative_op || $single_value ) {
1477 $cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++ if !$single_value && !$range_op;
1478 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
1485 # if column is defined then deal only with it
1486 # otherwise search in Content and in LargeContent
1489 ALIAS => $TicketCFs,
1491 OPERATOR => ($column ne 'LargeContent'? $op : $fix_op->($op)),
1496 elsif ( $cfid and $cf->Type eq 'Date' ) {
1497 $self->_DateFieldLimit(
1501 ALIAS => $TicketCFs,
1505 elsif ( $op eq '=' || $op eq '!=' || $op eq '<>' ) {
1506 unless ( length( Encode::encode_utf8($value) ) > 255 ) {
1508 ALIAS => $TicketCFs,
1517 ALIAS => $TicketCFs,
1521 ENTRYAGGREGATOR => 'OR'
1524 ALIAS => $TicketCFs,
1528 ENTRYAGGREGATOR => 'OR'
1532 ALIAS => $TicketCFs,
1533 FIELD => 'LargeContent',
1534 OPERATOR => $fix_op->($op),
1536 ENTRYAGGREGATOR => 'AND',
1542 ALIAS => $TicketCFs,
1552 ALIAS => $TicketCFs,
1556 ENTRYAGGREGATOR => 'OR'
1559 ALIAS => $TicketCFs,
1563 ENTRYAGGREGATOR => 'OR'
1567 ALIAS => $TicketCFs,
1568 FIELD => 'LargeContent',
1569 OPERATOR => $fix_op->($op),
1571 ENTRYAGGREGATOR => 'AND',
1577 # XXX: if we join via CustomFields table then
1578 # because of order of left joins we get NULLs in
1579 # CF table and then get nulls for those records
1580 # in OCFVs table what result in wrong results
1581 # as decifer method now tries to load a CF then
1582 # we fall into this situation only when there
1583 # are more than one CF with the name in the DB.
1584 # the same thing applies to order by call.
1585 # TODO: reorder joins T <- OCFVs <- CFs <- OCFs if
1586 # we want treat IS NULL as (not applies or has
1591 OPERATOR => 'IS NOT',
1594 ENTRYAGGREGATOR => 'AND',
1600 ALIAS => $TicketCFs,
1601 FIELD => $column || 'Content',
1605 ENTRYAGGREGATOR => 'OR',
1612 $cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++;
1613 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
1616 $op =~ s/!|NOT\s+//i;
1618 # if column is defined then deal only with it
1619 # otherwise search in Content and in LargeContent
1621 $self->SUPER::Limit(
1622 LEFTJOIN => $TicketCFs,
1623 ALIAS => $TicketCFs,
1625 OPERATOR => ($column ne 'LargeContent'? $op : $fix_op->($op)),
1630 $self->SUPER::Limit(
1631 LEFTJOIN => $TicketCFs,
1632 ALIAS => $TicketCFs,
1640 ALIAS => $TicketCFs,
1649 sub _HasAttributeLimit {
1650 my ( $self, $field, $op, $value, %rest ) = @_;
1652 my $alias = $self->Join(
1656 TABLE2 => 'Attributes',
1657 FIELD2 => 'ObjectId',
1659 $self->SUPER::Limit(
1661 FIELD => 'ObjectType',
1662 VALUE => 'RT::Ticket',
1663 ENTRYAGGREGATOR => 'AND'
1665 $self->SUPER::Limit(
1670 ENTRYAGGREGATOR => 'AND'
1676 OPERATOR => $FIELD_METADATA{$field}->[1]? 'IS NOT': 'IS',
1682 # End Helper Functions
1684 # End of SQL Stuff -------------------------------------------------
1686 # {{{ Allow sorting on watchers
1688 =head2 OrderByCols ARRAY
1690 A modified version of the OrderBy method which automatically joins where
1691 C<ALIAS> is set to the name of a watcher type.
1702 foreach my $row (@args) {
1703 if ( $row->{ALIAS} ) {
1707 if ( $row->{FIELD} !~ /\./ ) {
1708 my $meta = $self->FIELDS->{ $row->{FIELD} };
1714 if ( $meta->[0] eq 'ENUM' && ($meta->[1]||'') eq 'Queue' ) {
1715 my $alias = $self->Join(
1718 FIELD1 => $row->{'FIELD'},
1722 push @res, { %$row, ALIAS => $alias, FIELD => "Name" };
1723 } elsif ( ( $meta->[0] eq 'ENUM' && ($meta->[1]||'') eq 'User' )
1724 || ( $meta->[0] eq 'WATCHERFIELD' && ($meta->[1]||'') eq 'Owner' )
1726 my $alias = $self->Join(
1729 FIELD1 => $row->{'FIELD'},
1733 push @res, { %$row, ALIAS => $alias, FIELD => "Name" };
1740 my ( $field, $subkey ) = split /\./, $row->{FIELD}, 2;
1741 my $meta = $self->FIELDS->{$field};
1742 if ( defined $meta->[0] && $meta->[0] eq 'WATCHERFIELD' ) {
1743 # cache alias as we want to use one alias per watcher type for sorting
1744 my $users = $self->{_sql_u_watchers_alias_for_sort}{ $meta->[1] };
1746 $self->{_sql_u_watchers_alias_for_sort}{ $meta->[1] }
1747 = $users = ( $self->_WatcherJoin( $meta->[1] ) )[2];
1749 push @res, { %$row, ALIAS => $users, FIELD => $subkey };
1750 } elsif ( defined $meta->[0] && $meta->[0] eq 'CUSTOMFIELD' ) {
1751 my ($queue, $field, $cf_obj, $column) = $self->_CustomFieldDecipher( $subkey );
1752 my $cfkey = $cf_obj ? $cf_obj->id : "$queue.$field";
1753 $cfkey .= ".ordering" if !$cf_obj || ($cf_obj->MaxValues||0) != 1;
1754 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, ($cf_obj ?$cf_obj->id :0) , $field );
1755 # this is described in _CustomFieldLimit
1759 OPERATOR => 'IS NOT',
1762 ENTRYAGGREGATOR => 'AND',
1765 # For those cases where we are doing a join against the
1766 # CF name, and don't have a CFid, use Unique to make sure
1767 # we don't show duplicate tickets. NOTE: I'm pretty sure
1768 # this will stay mixed in for the life of the
1769 # class/package, and not just for the life of the object.
1770 # Potential performance issue.
1771 require DBIx::SearchBuilder::Unique;
1772 DBIx::SearchBuilder::Unique->import;
1774 my $CFvs = $self->Join(
1776 ALIAS1 => $TicketCFs,
1777 FIELD1 => 'CustomField',
1778 TABLE2 => 'CustomFieldValues',
1779 FIELD2 => 'CustomField',
1781 $self->SUPER::Limit(
1785 VALUE => $TicketCFs . ".Content",
1786 ENTRYAGGREGATOR => 'AND'
1789 push @res, { %$row, ALIAS => $CFvs, FIELD => 'SortOrder' };
1790 push @res, { %$row, ALIAS => $TicketCFs, FIELD => 'Content' };
1791 } elsif ( $field eq "Custom" && $subkey eq "Ownership") {
1792 # PAW logic is "reversed"
1794 if (exists $row->{ORDER} ) {
1795 my $o = $row->{ORDER};
1796 delete $row->{ORDER};
1797 $order = "DESC" if $o =~ /asc/i;
1800 # Ticket.Owner 1 0 X
1801 # Unowned Tickets 0 1 X
1804 foreach my $uid ( $self->CurrentUser->Id, $RT::Nobody->Id ) {
1805 if ( RT->Config->Get('DatabaseType') eq 'Oracle' ) {
1806 my $f = ($row->{'ALIAS'} || 'main') .'.Owner';
1811 FUNCTION => "CASE WHEN $f=$uid THEN 1 ELSE 0 END",
1818 FUNCTION => "Owner=$uid",
1824 push @res, { %$row, FIELD => "Priority", ORDER => $order } ;
1826 } elsif ( $field eq 'Customer' ) { #Freeside
1827 # OrderBy(FIELD => expression) doesn't work, it has to be
1828 # an actual field, so we have to do the join even if sorting
1830 my $custalias = $self->JoinToCustomer;
1831 my $cust_field = lc($subkey);
1832 if ( !$cust_field or $cust_field eq 'number' ) {
1833 $cust_field = 'custnum';
1835 elsif ( $cust_field eq 'name' ) {
1836 $cust_field = "COALESCE( $custalias.company,
1837 $custalias.last || ', ' || $custalias.first
1840 else { # order by cust_main fields directly: 'Customer.agentnum'
1841 $cust_field = $subkey;
1843 push @res, { %$row, ALIAS => $custalias, FIELD => $cust_field };
1851 return $self->SUPER::OrderByCols(@res);
1856 sub JoinToCustLinks {
1857 # Set up join to links (id = localbase),
1858 # limit link type to 'MemberOf',
1859 # and target value to any Freeside custnum URI.
1860 # Return the linkalias for further join/limit action,
1861 # and an sql expression to retrieve the custnum.
1863 # only join once for each RT::Tickets object
1864 my $linkalias = $self->{cust_linkalias};
1866 $linkalias = $self->Join(
1871 FIELD2 => 'LocalBase',
1874 $self->SUPER::Limit(
1875 LEFTJOIN => $linkalias,
1878 VALUE => 'MemberOf',
1880 $self->SUPER::Limit(
1881 LEFTJOIN => $linkalias,
1883 OPERATOR => 'STARTSWITH',
1884 VALUE => 'freeside://freeside/cust_main/',
1886 $self->{cust_linkalias} = $linkalias;
1888 my $custnum_sql = "CAST(SUBSTR($linkalias.Target,31) AS ";
1889 if ( RT->Config->Get('DatabaseType') eq 'mysql' ) {
1890 $custnum_sql .= 'SIGNED INTEGER)';
1893 $custnum_sql .= 'INTEGER)';
1895 return ($linkalias, $custnum_sql);
1898 sub JoinToCustomer {
1900 my ($linkalias, $custnum_sql) = $self->JoinToCustLinks;
1901 # don't reuse this join, though--negative queries need
1903 my $custalias = $self->Join(
1905 EXPRESSION => $custnum_sql,
1906 TABLE2 => 'cust_main',
1907 FIELD2 => 'custnum',
1912 sub _FreesideFieldLimit {
1913 my ( $self, $field, $op, $value, %rest ) = @_;
1914 my $alias = $self->JoinToCustomer;
1915 my $is_negative = 0;
1916 if ( $op eq '!=' || $op =~ /\bNOT\b/i ) {
1917 # if the op is negative, do the join as though
1918 # the op were positive, then accept only records
1919 # where the right-side join key is null.
1921 $op = '=' if $op eq '!=';
1925 my $cust_field = $rest{SUBKEY} || 'custnum';
1927 # compound subkey: separate into table name and field in that table
1928 # (must be linked by custnum)
1929 ($table2, $cust_field) = ($1, $2) if $cust_field =~ /^(\w+)?\.(\w+)$/;
1931 $cust_field = lc($cust_field);
1932 $cust_field = 'custnum' if !$cust_field or $cust_field eq 'number';
1935 $alias = $self->Join(
1938 FIELD1 => 'custnum',
1940 FIELD2 => 'custnum',
1944 $self->SUPER::Limit(
1946 FIELD => $cust_field,
1949 ENTRYAGGREGATOR => 'AND',
1955 OPERATOR => $is_negative ? 'IS' : 'IS NOT',
1965 # {{{ Limit the result set based on content
1971 Takes a paramhash with the fields FIELD, OPERATOR, VALUE and DESCRIPTION
1972 Generally best called from LimitFoo methods
1982 DESCRIPTION => undef,
1985 $args{'DESCRIPTION'} = $self->loc(
1986 "[_1] [_2] [_3]", $args{'FIELD'},
1987 $args{'OPERATOR'}, $args{'VALUE'}
1989 if ( !defined $args{'DESCRIPTION'} );
1991 my $index = $self->_NextIndex;
1993 # make the TicketRestrictions hash the equivalent of whatever we just passed in;
1995 %{ $self->{'TicketRestrictions'}{$index} } = %args;
1997 $self->{'RecalcTicketLimits'} = 1;
1999 # If we're looking at the effective id, we don't want to append the other clause
2000 # which limits us to tickets where id = effective id
2001 if ( $args{'FIELD'} eq 'EffectiveId'
2002 && ( !$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) )
2004 $self->{'looking_at_effective_id'} = 1;
2007 if ( $args{'FIELD'} eq 'Type'
2008 && ( !$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) )
2010 $self->{'looking_at_type'} = 1;
2020 Returns a frozen string suitable for handing back to ThawLimits.
2024 sub _FreezeThawKeys {
2025 'TicketRestrictions', 'restriction_index', 'looking_at_effective_id',
2029 # {{{ sub FreezeLimits
2034 require MIME::Base64;
2035 MIME::Base64::base64_encode(
2036 Storable::freeze( \@{$self}{ $self->_FreezeThawKeys } ) );
2043 Take a frozen Limits string generated by FreezeLimits and make this tickets
2044 object have that set of limits.
2048 # {{{ sub ThawLimits
2054 #if we don't have $in, get outta here.
2055 return undef unless ($in);
2057 $self->{'RecalcTicketLimits'} = 1;
2060 require MIME::Base64;
2062 #We don't need to die if the thaw fails.
2063 @{$self}{ $self->_FreezeThawKeys }
2064 = eval { @{ Storable::thaw( MIME::Base64::base64_decode($in) ) }; };
2066 $RT::Logger->error($@) if $@;
2072 # {{{ Limit by enum or foreign key
2074 # {{{ sub LimitQueue
2078 LimitQueue takes a paramhash with the fields OPERATOR and VALUE.
2079 OPERATOR is one of = or !=. (It defaults to =).
2080 VALUE is a queue id or Name.
2093 #TODO VALUE should also take queue objects
2094 if ( defined $args{'VALUE'} && $args{'VALUE'} !~ /^\d+$/ ) {
2095 my $queue = new RT::Queue( $self->CurrentUser );
2096 $queue->Load( $args{'VALUE'} );
2097 $args{'VALUE'} = $queue->Id;
2100 # What if they pass in an Id? Check for isNum() and convert to
2103 #TODO check for a valid queue here
2107 VALUE => $args{'VALUE'},
2108 OPERATOR => $args{'OPERATOR'},
2109 DESCRIPTION => join(
2110 ' ', $self->loc('Queue'), $args{'OPERATOR'}, $args{'VALUE'},
2118 # {{{ sub LimitStatus
2122 Takes a paramhash with the fields OPERATOR and VALUE.
2123 OPERATOR is one of = or !=.
2126 RT adds Status != 'deleted' until object has
2127 allow_deleted_search internal property set.
2128 $tickets->{'allow_deleted_search'} = 1;
2129 $tickets->LimitStatus( VALUE => 'deleted' );
2141 VALUE => $args{'VALUE'},
2142 OPERATOR => $args{'OPERATOR'},
2143 DESCRIPTION => join( ' ',
2144 $self->loc('Status'), $args{'OPERATOR'},
2145 $self->loc( $args{'VALUE'} ) ),
2151 # {{{ sub IgnoreType
2155 If called, this search will not automatically limit the set of results found
2156 to tickets of type "Ticket". Tickets of other types, such as "project" and
2157 "approval" will be found.
2164 # Instead of faking a Limit that later gets ignored, fake up the
2165 # fact that we're already looking at type, so that the check in
2166 # Tickets_Overlay_SQL/FromSQL goes down the right branch
2168 # $self->LimitType(VALUE => '__any');
2169 $self->{looking_at_type} = 1;
2178 Takes a paramhash with the fields OPERATOR and VALUE.
2179 OPERATOR is one of = or !=, it defaults to "=".
2180 VALUE is a string to search for in the type of the ticket.
2195 VALUE => $args{'VALUE'},
2196 OPERATOR => $args{'OPERATOR'},
2197 DESCRIPTION => join( ' ',
2198 $self->loc('Type'), $args{'OPERATOR'}, $args{'Limit'}, ),
2206 # {{{ Limit by string field
2208 # {{{ sub LimitSubject
2212 Takes a paramhash with the fields OPERATOR and VALUE.
2213 OPERATOR is one of = or !=.
2214 VALUE is a string to search for in the subject of the ticket.
2223 VALUE => $args{'VALUE'},
2224 OPERATOR => $args{'OPERATOR'},
2225 DESCRIPTION => join( ' ',
2226 $self->loc('Subject'), $args{'OPERATOR'}, $args{'VALUE'}, ),
2234 # {{{ Limit based on ticket numerical attributes
2235 # Things that can be > < = !=
2241 Takes a paramhash with the fields OPERATOR and VALUE.
2242 OPERATOR is one of =, >, < or !=.
2243 VALUE is a ticket Id to search for
2256 VALUE => $args{'VALUE'},
2257 OPERATOR => $args{'OPERATOR'},
2259 join( ' ', $self->loc('Id'), $args{'OPERATOR'}, $args{'VALUE'}, ),
2265 # {{{ sub LimitPriority
2267 =head2 LimitPriority
2269 Takes a paramhash with the fields OPERATOR and VALUE.
2270 OPERATOR is one of =, >, < or !=.
2271 VALUE is a value to match the ticket\'s priority against
2279 FIELD => 'Priority',
2280 VALUE => $args{'VALUE'},
2281 OPERATOR => $args{'OPERATOR'},
2282 DESCRIPTION => join( ' ',
2283 $self->loc('Priority'),
2284 $args{'OPERATOR'}, $args{'VALUE'}, ),
2290 # {{{ sub LimitInitialPriority
2292 =head2 LimitInitialPriority
2294 Takes a paramhash with the fields OPERATOR and VALUE.
2295 OPERATOR is one of =, >, < or !=.
2296 VALUE is a value to match the ticket\'s initial priority against
2301 sub LimitInitialPriority {
2305 FIELD => 'InitialPriority',
2306 VALUE => $args{'VALUE'},
2307 OPERATOR => $args{'OPERATOR'},
2308 DESCRIPTION => join( ' ',
2309 $self->loc('Initial Priority'), $args{'OPERATOR'},
2316 # {{{ sub LimitFinalPriority
2318 =head2 LimitFinalPriority
2320 Takes a paramhash with the fields OPERATOR and VALUE.
2321 OPERATOR is one of =, >, < or !=.
2322 VALUE is a value to match the ticket\'s final priority against
2326 sub LimitFinalPriority {
2330 FIELD => 'FinalPriority',
2331 VALUE => $args{'VALUE'},
2332 OPERATOR => $args{'OPERATOR'},
2333 DESCRIPTION => join( ' ',
2334 $self->loc('Final Priority'), $args{'OPERATOR'},
2341 # {{{ sub LimitTimeWorked
2343 =head2 LimitTimeWorked
2345 Takes a paramhash with the fields OPERATOR and VALUE.
2346 OPERATOR is one of =, >, < or !=.
2347 VALUE is a value to match the ticket's TimeWorked attribute
2351 sub LimitTimeWorked {
2355 FIELD => 'TimeWorked',
2356 VALUE => $args{'VALUE'},
2357 OPERATOR => $args{'OPERATOR'},
2358 DESCRIPTION => join( ' ',
2359 $self->loc('Time Worked'),
2360 $args{'OPERATOR'}, $args{'VALUE'}, ),
2366 # {{{ sub LimitTimeLeft
2368 =head2 LimitTimeLeft
2370 Takes a paramhash with the fields OPERATOR and VALUE.
2371 OPERATOR is one of =, >, < or !=.
2372 VALUE is a value to match the ticket's TimeLeft attribute
2380 FIELD => 'TimeLeft',
2381 VALUE => $args{'VALUE'},
2382 OPERATOR => $args{'OPERATOR'},
2383 DESCRIPTION => join( ' ',
2384 $self->loc('Time Left'),
2385 $args{'OPERATOR'}, $args{'VALUE'}, ),
2393 # {{{ Limiting based on attachment attributes
2395 # {{{ sub LimitContent
2399 Takes a paramhash with the fields OPERATOR and VALUE.
2400 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2401 VALUE is a string to search for in the body of the ticket
2410 VALUE => $args{'VALUE'},
2411 OPERATOR => $args{'OPERATOR'},
2412 DESCRIPTION => join( ' ',
2413 $self->loc('Ticket content'), $args{'OPERATOR'},
2420 # {{{ sub LimitFilename
2422 =head2 LimitFilename
2424 Takes a paramhash with the fields OPERATOR and VALUE.
2425 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2426 VALUE is a string to search for in the body of the ticket
2434 FIELD => 'Filename',
2435 VALUE => $args{'VALUE'},
2436 OPERATOR => $args{'OPERATOR'},
2437 DESCRIPTION => join( ' ',
2438 $self->loc('Attachment filename'), $args{'OPERATOR'},
2444 # {{{ sub LimitContentType
2446 =head2 LimitContentType
2448 Takes a paramhash with the fields OPERATOR and VALUE.
2449 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2450 VALUE is a content type to search ticket attachments for
2454 sub LimitContentType {
2458 FIELD => 'ContentType',
2459 VALUE => $args{'VALUE'},
2460 OPERATOR => $args{'OPERATOR'},
2461 DESCRIPTION => join( ' ',
2462 $self->loc('Ticket content type'), $args{'OPERATOR'},
2471 # {{{ Limiting based on people
2473 # {{{ sub LimitOwner
2477 Takes a paramhash with the fields OPERATOR and VALUE.
2478 OPERATOR is one of = or !=.
2490 my $owner = new RT::User( $self->CurrentUser );
2491 $owner->Load( $args{'VALUE'} );
2493 # FIXME: check for a valid $owner
2496 VALUE => $args{'VALUE'},
2497 OPERATOR => $args{'OPERATOR'},
2498 DESCRIPTION => join( ' ',
2499 $self->loc('Owner'), $args{'OPERATOR'}, $owner->Name(), ),
2506 # {{{ Limiting watchers
2508 # {{{ sub LimitWatcher
2512 Takes a paramhash with the fields OPERATOR, TYPE and VALUE.
2513 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2514 VALUE is a value to match the ticket\'s watcher email addresses against
2515 TYPE is the sort of watchers you want to match against. Leave it undef if you want to search all of them
2529 #build us up a description
2530 my ( $watcher_type, $desc );
2531 if ( $args{'TYPE'} ) {
2532 $watcher_type = $args{'TYPE'};
2535 $watcher_type = "Watcher";
2539 FIELD => $watcher_type,
2540 VALUE => $args{'VALUE'},
2541 OPERATOR => $args{'OPERATOR'},
2542 TYPE => $args{'TYPE'},
2543 DESCRIPTION => join( ' ',
2544 $self->loc($watcher_type),
2545 $args{'OPERATOR'}, $args{'VALUE'}, ),
2555 # {{{ Limiting based on links
2559 =head2 LimitLinkedTo
2561 LimitLinkedTo takes a paramhash with two fields: TYPE and TARGET
2562 TYPE limits the sort of link we want to search on
2564 TYPE = { RefersTo, MemberOf, DependsOn }
2566 TARGET is the id or URI of the TARGET of the link
2580 FIELD => 'LinkedTo',
2582 TARGET => $args{'TARGET'},
2583 TYPE => $args{'TYPE'},
2584 DESCRIPTION => $self->loc(
2585 "Tickets [_1] by [_2]",
2586 $self->loc( $args{'TYPE'} ),
2589 OPERATOR => $args{'OPERATOR'},
2595 # {{{ LimitLinkedFrom
2597 =head2 LimitLinkedFrom
2599 LimitLinkedFrom takes a paramhash with two fields: TYPE and BASE
2600 TYPE limits the sort of link we want to search on
2603 BASE is the id or URI of the BASE of the link
2607 sub LimitLinkedFrom {
2616 # translate RT2 From/To naming to RT3 TicketSQL naming
2617 my %fromToMap = qw(DependsOn DependentOn
2619 RefersTo ReferredToBy);
2621 my $type = $args{'TYPE'};
2622 $type = $fromToMap{$type} if exists( $fromToMap{$type} );
2625 FIELD => 'LinkedTo',
2627 BASE => $args{'BASE'},
2629 DESCRIPTION => $self->loc(
2630 "Tickets [_1] [_2]",
2631 $self->loc( $args{'TYPE'} ),
2634 OPERATOR => $args{'OPERATOR'},
2643 my $ticket_id = shift;
2644 return $self->LimitLinkedTo(
2646 TARGET => $ticket_id,
2653 # {{{ LimitHasMember
2654 sub LimitHasMember {
2656 my $ticket_id = shift;
2657 return $self->LimitLinkedFrom(
2659 BASE => "$ticket_id",
2660 TYPE => 'HasMember',
2667 # {{{ LimitDependsOn
2669 sub LimitDependsOn {
2671 my $ticket_id = shift;
2672 return $self->LimitLinkedTo(
2674 TARGET => $ticket_id,
2675 TYPE => 'DependsOn',
2682 # {{{ LimitDependedOnBy
2684 sub LimitDependedOnBy {
2686 my $ticket_id = shift;
2687 return $self->LimitLinkedFrom(
2690 TYPE => 'DependentOn',
2701 my $ticket_id = shift;
2702 return $self->LimitLinkedTo(
2704 TARGET => $ticket_id,
2712 # {{{ LimitReferredToBy
2714 sub LimitReferredToBy {
2716 my $ticket_id = shift;
2717 return $self->LimitLinkedFrom(
2720 TYPE => 'ReferredToBy',
2728 # {{{ limit based on ticket date attribtes
2732 =head2 LimitDate (FIELD => 'DateField', OPERATOR => $oper, VALUE => $ISODate)
2734 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
2736 OPERATOR is one of > or <
2737 VALUE is a date and time in ISO format in GMT
2738 FIELD is one of Starts, Started, Told, Created, Resolved, LastUpdated
2740 There are also helper functions of the form LimitFIELD that eliminate
2741 the need to pass in a FIELD argument.
2755 #Set the description if we didn't get handed it above
2756 unless ( $args{'DESCRIPTION'} ) {
2757 $args{'DESCRIPTION'} = $args{'FIELD'} . " "
2758 . $args{'OPERATOR'} . " "
2759 . $args{'VALUE'} . " GMT";
2762 $self->Limit(%args);
2770 $self->LimitDate( FIELD => 'Created', @_ );
2775 $self->LimitDate( FIELD => 'Due', @_ );
2781 $self->LimitDate( FIELD => 'Starts', @_ );
2787 $self->LimitDate( FIELD => 'Started', @_ );
2792 $self->LimitDate( FIELD => 'Resolved', @_ );
2797 $self->LimitDate( FIELD => 'Told', @_ );
2800 sub LimitLastUpdated {
2802 $self->LimitDate( FIELD => 'LastUpdated', @_ );
2806 # {{{ sub LimitTransactionDate
2808 =head2 LimitTransactionDate (OPERATOR => $oper, VALUE => $ISODate)
2810 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
2812 OPERATOR is one of > or <
2813 VALUE is a date and time in ISO format in GMT
2818 sub LimitTransactionDate {
2821 FIELD => 'TransactionDate',
2828 # <20021217042756.GK28744@pallas.fsck.com>
2829 # "Kill It" - Jesse.
2831 #Set the description if we didn't get handed it above
2832 unless ( $args{'DESCRIPTION'} ) {
2833 $args{'DESCRIPTION'} = $args{'FIELD'} . " "
2834 . $args{'OPERATOR'} . " "
2835 . $args{'VALUE'} . " GMT";
2838 $self->Limit(%args);
2846 # {{{ Limit based on custom fields
2847 # {{{ sub LimitCustomField
2849 =head2 LimitCustomField
2851 Takes a paramhash of key/value pairs with the following keys:
2855 =item CUSTOMFIELD - CustomField name or id. If a name is passed, an additional parameter QUEUE may also be passed to distinguish the custom field.
2857 =item OPERATOR - The usual Limit operators
2859 =item VALUE - The value to compare against
2865 sub LimitCustomField {
2869 CUSTOMFIELD => undef,
2871 DESCRIPTION => undef,
2872 FIELD => 'CustomFieldValue',
2877 my $CF = RT::CustomField->new( $self->CurrentUser );
2878 if ( $args{CUSTOMFIELD} =~ /^\d+$/ ) {
2879 $CF->Load( $args{CUSTOMFIELD} );
2882 $CF->LoadByNameAndQueue(
2883 Name => $args{CUSTOMFIELD},
2884 Queue => $args{QUEUE}
2886 $args{CUSTOMFIELD} = $CF->Id;
2889 #If we are looking to compare with a null value.
2890 if ( $args{'OPERATOR'} =~ /^is$/i ) {
2891 $args{'DESCRIPTION'}
2892 ||= $self->loc( "Custom field [_1] has no value.", $CF->Name );
2894 elsif ( $args{'OPERATOR'} =~ /^is not$/i ) {
2895 $args{'DESCRIPTION'}
2896 ||= $self->loc( "Custom field [_1] has a value.", $CF->Name );
2899 # if we're not looking to compare with a null value
2901 $args{'DESCRIPTION'} ||= $self->loc( "Custom field [_1] [_2] [_3]",
2902 $CF->Name, $args{OPERATOR}, $args{VALUE} );
2905 if ( defined $args{'QUEUE'} && $args{'QUEUE'} =~ /\D/ ) {
2906 my $QueueObj = RT::Queue->new( $self->CurrentUser );
2907 $QueueObj->Load( $args{'QUEUE'} );
2908 $args{'QUEUE'} = $QueueObj->Id;
2910 delete $args{'QUEUE'} unless defined $args{'QUEUE'} && length $args{'QUEUE'};
2913 @rest = ( ENTRYAGGREGATOR => 'AND' )
2914 if ( $CF->Type eq 'SelectMultiple' );
2917 VALUE => $args{VALUE},
2919 .(defined $args{'QUEUE'}? ".{$args{'QUEUE'}}" : '' )
2920 .".{" . $CF->Name . "}",
2921 OPERATOR => $args{OPERATOR},
2926 $self->{'RecalcTicketLimits'} = 1;
2932 # {{{ sub _NextIndex
2936 Keep track of the counter for the array of restrictions
2942 return ( $self->{'restriction_index'}++ );
2949 # {{{ Core bits to make this a DBIx::SearchBuilder object
2954 $self->{'table'} = "Tickets";
2955 $self->{'RecalcTicketLimits'} = 1;
2956 $self->{'looking_at_effective_id'} = 0;
2957 $self->{'looking_at_type'} = 0;
2958 $self->{'restriction_index'} = 1;
2959 $self->{'primary_key'} = "id";
2960 delete $self->{'items_array'};
2961 delete $self->{'item_map'};
2962 delete $self->{'columns_to_display'};
2963 $self->SUPER::_Init(@_);
2974 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2975 return ( $self->SUPER::Count() );
2983 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2984 return ( $self->SUPER::CountAll() );
2989 # {{{ sub ItemsArrayRef
2991 =head2 ItemsArrayRef
2993 Returns a reference to the set of all items found in this search
3000 return $self->{'items_array'} if $self->{'items_array'};
3002 my $placeholder = $self->_ItemsCounter;
3003 $self->GotoFirstItem();
3004 while ( my $item = $self->Next ) {
3005 push( @{ $self->{'items_array'} }, $item );
3007 $self->GotoItem($placeholder);
3008 $self->{'items_array'}
3009 = $self->ItemsOrderBy( $self->{'items_array'} );
3011 return $self->{'items_array'};
3014 sub ItemsArrayRefWindow {
3018 my @old = ($self->_ItemsCounter, $self->RowsPerPage, $self->FirstRow+1);
3020 $self->RowsPerPage( $window );
3022 $self->GotoFirstItem;
3025 while ( my $item = $self->Next ) {
3029 $self->RowsPerPage( $old[1] );
3030 $self->FirstRow( $old[2] );
3031 $self->GotoItem( $old[0] );
3042 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
3044 my $Ticket = $self->SUPER::Next;
3045 return $Ticket unless $Ticket;
3047 if ( $Ticket->__Value('Status') eq 'deleted'
3048 && !$self->{'allow_deleted_search'} )
3052 elsif ( RT->Config->Get('UseSQLForACLChecks') ) {
3053 # if we found a ticket with this option enabled then
3054 # all tickets we found are ACLed, cache this fact
3055 my $key = join ";:;", $self->CurrentUser->id, 'ShowTicket', 'RT::Ticket-'. $Ticket->id;
3056 $RT::Principal::_ACL_CACHE->set( $key => 1 );
3059 elsif ( $Ticket->CurrentUserHasRight('ShowTicket') ) {
3064 # If the user doesn't have the right to show this ticket
3071 $self->CurrentUserCanSee if RT->Config->Get('UseSQLForACLChecks');
3072 return $self->SUPER::_DoSearch( @_ );
3077 $self->CurrentUserCanSee if RT->Config->Get('UseSQLForACLChecks');
3078 return $self->SUPER::_DoCount( @_ );
3084 my $cache_key = 'RolesHasRight;:;ShowTicket';
3086 if ( my $cached = $RT::Principal::_ACL_CACHE->fetch( $cache_key ) ) {
3090 my $ACL = RT::ACL->new( $RT::SystemUser );
3091 $ACL->Limit( FIELD => 'RightName', VALUE => 'ShowTicket' );
3092 $ACL->Limit( FIELD => 'PrincipalType', OPERATOR => '!=', VALUE => 'Group' );
3093 my $principal_alias = $ACL->Join(
3095 FIELD1 => 'PrincipalId',
3096 TABLE2 => 'Principals',
3099 $ACL->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3102 while ( my $ACE = $ACL->Next ) {
3103 my $role = $ACE->PrincipalType;
3104 my $type = $ACE->ObjectType;
3105 if ( $type eq 'RT::System' ) {
3108 elsif ( $type eq 'RT::Queue' ) {
3109 next if $res{ $role } && !ref $res{ $role };
3110 push @{ $res{ $role } ||= [] }, $ACE->ObjectId;
3113 $RT::Logger->error('ShowTicket right is granted on unsupported object');
3116 $RT::Principal::_ACL_CACHE->set( $cache_key => \%res );
3120 sub _DirectlyCanSeeIn {
3122 my $id = $self->CurrentUser->id;
3124 my $cache_key = 'User-'. $id .';:;ShowTicket;:;DirectlyCanSeeIn';
3125 if ( my $cached = $RT::Principal::_ACL_CACHE->fetch( $cache_key ) ) {
3129 my $ACL = RT::ACL->new( $RT::SystemUser );
3130 $ACL->Limit( FIELD => 'RightName', VALUE => 'ShowTicket' );
3131 my $principal_alias = $ACL->Join(
3133 FIELD1 => 'PrincipalId',
3134 TABLE2 => 'Principals',
3137 $ACL->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3138 my $cgm_alias = $ACL->Join(
3140 FIELD1 => 'PrincipalId',
3141 TABLE2 => 'CachedGroupMembers',
3142 FIELD2 => 'GroupId',
3144 $ACL->Limit( ALIAS => $cgm_alias, FIELD => 'MemberId', VALUE => $id );
3145 $ACL->Limit( ALIAS => $cgm_alias, FIELD => 'Disabled', VALUE => 0 );
3148 while ( my $ACE = $ACL->Next ) {
3149 my $type = $ACE->ObjectType;
3150 if ( $type eq 'RT::System' ) {
3151 # If user is direct member of a group that has the right
3152 # on the system then he can see any ticket
3153 $RT::Principal::_ACL_CACHE->set( $cache_key => [-1] );
3156 elsif ( $type eq 'RT::Queue' ) {
3157 push @res, $ACE->ObjectId;
3160 $RT::Logger->error('ShowTicket right is granted on unsupported object');
3163 $RT::Principal::_ACL_CACHE->set( $cache_key => \@res );
3167 sub CurrentUserCanSee {
3169 return if $self->{'_sql_current_user_can_see_applied'};
3171 return $self->{'_sql_current_user_can_see_applied'} = 1
3172 if $self->CurrentUser->UserObj->HasRight(
3173 Right => 'SuperUser', Object => $RT::System
3176 my $id = $self->CurrentUser->id;
3178 # directly can see in all queues then we have nothing to do
3179 my @direct_queues = $self->_DirectlyCanSeeIn;
3180 return $self->{'_sql_current_user_can_see_applied'} = 1
3181 if @direct_queues && $direct_queues[0] == -1;
3183 my %roles = $self->_RolesCanSee;
3185 my %skip = map { $_ => 1 } @direct_queues;
3186 foreach my $role ( keys %roles ) {
3187 next unless ref $roles{ $role };
3189 my @queues = grep !$skip{$_}, @{ $roles{ $role } };
3191 $roles{ $role } = \@queues;
3193 delete $roles{ $role };
3198 # there is no global watchers, only queues and tickes, if at
3199 # some point we will add global roles then it's gonna blow
3200 # the idea here is that if the right is set globaly for a role
3201 # and user plays this role for a queue directly not a ticket
3202 # then we have to check in advance
3203 if ( my @tmp = grep $_ ne 'Owner' && !ref $roles{ $_ }, keys %roles ) {
3205 my $groups = RT::Groups->new( $RT::SystemUser );
3206 $groups->Limit( FIELD => 'Domain', VALUE => 'RT::Queue-Role' );
3208 $groups->Limit( FIELD => 'Type', VALUE => $_ );
3210 my $principal_alias = $groups->Join(
3213 TABLE2 => 'Principals',
3216 $groups->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3217 my $cgm_alias = $groups->Join(
3220 TABLE2 => 'CachedGroupMembers',
3221 FIELD2 => 'GroupId',
3223 $groups->Limit( ALIAS => $cgm_alias, FIELD => 'MemberId', VALUE => $id );
3224 $groups->Limit( ALIAS => $cgm_alias, FIELD => 'Disabled', VALUE => 0 );
3225 while ( my $group = $groups->Next ) {
3226 push @direct_queues, $group->Instance;
3230 unless ( @direct_queues || keys %roles ) {
3231 $self->SUPER::Limit(
3236 ENTRYAGGREGATOR => 'AND',
3238 return $self->{'_sql_current_user_can_see_applied'} = 1;
3242 my $join_roles = keys %roles;
3243 $join_roles = 0 if $join_roles == 1 && $roles{'Owner'};
3244 my ($role_group_alias, $cgm_alias);
3245 if ( $join_roles ) {
3246 $role_group_alias = $self->_RoleGroupsJoin( New => 1 );
3247 $cgm_alias = $self->_GroupMembersJoin( GroupsAlias => $role_group_alias );
3248 $self->SUPER::Limit(
3249 LEFTJOIN => $cgm_alias,
3250 FIELD => 'MemberId',
3255 my $limit_queues = sub {
3259 return unless @queues;
3260 if ( @queues == 1 ) {
3261 $self->SUPER::Limit(
3266 ENTRYAGGREGATOR => $ea,
3269 $self->SUPER::_OpenParen('ACL');
3270 foreach my $q ( @queues ) {
3271 $self->SUPER::Limit(
3276 ENTRYAGGREGATOR => $ea,
3280 $self->SUPER::_CloseParen('ACL');
3285 $self->SUPER::_OpenParen('ACL');
3287 $ea = 'OR' if $limit_queues->( $ea, @direct_queues );
3288 while ( my ($role, $queues) = each %roles ) {
3289 $self->SUPER::_OpenParen('ACL');
3290 if ( $role eq 'Owner' ) {
3291 $self->SUPER::Limit(
3295 ENTRYAGGREGATOR => $ea,
3299 $self->SUPER::Limit(
3301 ALIAS => $cgm_alias,
3302 FIELD => 'MemberId',
3303 OPERATOR => 'IS NOT',
3306 ENTRYAGGREGATOR => $ea,
3308 $self->SUPER::Limit(
3310 ALIAS => $role_group_alias,
3313 ENTRYAGGREGATOR => 'AND',
3316 $limit_queues->( 'AND', @$queues ) if ref $queues;
3317 $ea = 'OR' if $ea eq 'AND';
3318 $self->SUPER::_CloseParen('ACL');
3320 $self->SUPER::_CloseParen('ACL');
3322 return $self->{'_sql_current_user_can_see_applied'} = 1;
3329 # {{{ Deal with storing and restoring restrictions
3331 # {{{ sub LoadRestrictions
3333 =head2 LoadRestrictions
3335 LoadRestrictions takes a string which can fully populate the TicketRestrictons hash.
3336 TODO It is not yet implemented
3342 # {{{ sub DescribeRestrictions
3344 =head2 DescribeRestrictions
3347 Returns a hash keyed by restriction id.
3348 Each element of the hash is currently a one element hash that contains DESCRIPTION which
3349 is a description of the purpose of that TicketRestriction
3353 sub DescribeRestrictions {
3358 foreach my $row ( keys %{ $self->{'TicketRestrictions'} } ) {
3359 $listing{$row} = $self->{'TicketRestrictions'}{$row}{'DESCRIPTION'};
3366 # {{{ sub RestrictionValues
3368 =head2 RestrictionValues FIELD
3370 Takes a restriction field and returns a list of values this field is restricted
3375 sub RestrictionValues {
3378 map $self->{'TicketRestrictions'}{$_}{'VALUE'}, grep {
3379 $self->{'TicketRestrictions'}{$_}{'FIELD'} eq $field
3380 && $self->{'TicketRestrictions'}{$_}{'OPERATOR'} eq "="
3382 keys %{ $self->{'TicketRestrictions'} };
3387 # {{{ sub ClearRestrictions
3389 =head2 ClearRestrictions
3391 Removes all restrictions irretrievably
3395 sub ClearRestrictions {
3397 delete $self->{'TicketRestrictions'};
3398 $self->{'looking_at_effective_id'} = 0;
3399 $self->{'looking_at_type'} = 0;
3400 $self->{'RecalcTicketLimits'} = 1;
3405 # {{{ sub DeleteRestriction
3407 =head2 DeleteRestriction
3409 Takes the row Id of a restriction (From DescribeRestrictions' output, for example.
3410 Removes that restriction from the session's limits.
3414 sub DeleteRestriction {
3417 delete $self->{'TicketRestrictions'}{$row};
3419 $self->{'RecalcTicketLimits'} = 1;
3421 #make the underlying easysearch object forget all its preconceptions
3426 # {{{ sub _RestrictionsToClauses
3428 # Convert a set of oldstyle SB Restrictions to Clauses for RQL
3430 sub _RestrictionsToClauses {
3434 foreach my $row ( keys %{ $self->{'TicketRestrictions'} } ) {
3435 my $restriction = $self->{'TicketRestrictions'}{$row};
3437 # We need to reimplement the subclause aggregation that SearchBuilder does.
3438 # Default Subclause is ALIAS.FIELD, and default ALIAS is 'main',
3439 # Then SB AND's the different Subclauses together.
3441 # So, we want to group things into Subclauses, convert them to
3442 # SQL, and then join them with the appropriate DefaultEA.
3443 # Then join each subclause group with AND.
3445 my $field = $restriction->{'FIELD'};
3446 my $realfield = $field; # CustomFields fake up a fieldname, so
3447 # we need to figure that out
3450 # Rewrite LinkedTo meta field to the real field
3451 if ( $field =~ /LinkedTo/ ) {
3452 $realfield = $field = $restriction->{'TYPE'};
3456 # Handle subkey fields with a different real field
3457 if ( $field =~ /^(\w+)\./ ) {
3461 die "I don't know about $field yet"
3462 unless ( exists $FIELD_METADATA{$realfield}
3463 or $restriction->{CUSTOMFIELD} );
3465 my $type = $FIELD_METADATA{$realfield}->[0];
3466 my $op = $restriction->{'OPERATOR'};
3470 map { $restriction->{$_} } qw(VALUE TICKET BASE TARGET)
3473 # this performs the moral equivalent of defined or/dor/C<//>,
3474 # without the short circuiting.You need to use a 'defined or'
3475 # type thing instead of just checking for truth values, because
3476 # VALUE could be 0.(i.e. "false")
3478 # You could also use this, but I find it less aesthetic:
3479 # (although it does short circuit)
3480 #( defined $restriction->{'VALUE'}? $restriction->{VALUE} :
3481 # defined $restriction->{'TICKET'} ?
3482 # $restriction->{TICKET} :
3483 # defined $restriction->{'BASE'} ?
3484 # $restriction->{BASE} :
3485 # defined $restriction->{'TARGET'} ?
3486 # $restriction->{TARGET} )
3488 my $ea = $restriction->{ENTRYAGGREGATOR}
3489 || $DefaultEA{$type}
3492 die "Invalid operator $op for $field ($type)"
3493 unless exists $ea->{$op};
3497 # Each CustomField should be put into a different Clause so they
3498 # are ANDed together.
3499 if ( $restriction->{CUSTOMFIELD} ) {
3500 $realfield = $field;
3503 exists $clause{$realfield} or $clause{$realfield} = [];
3506 $field =~ s!(['"])!\\$1!g;
3507 $value =~ s!(['"])!\\$1!g;
3508 my $data = [ $ea, $type, $field, $op, $value ];
3510 # here is where we store extra data, say if it's a keyword or
3511 # something. (I.e. "TYPE SPECIFIC STUFF")
3513 push @{ $clause{$realfield} }, $data;
3520 # {{{ sub _ProcessRestrictions
3522 =head2 _ProcessRestrictions PARAMHASH
3524 # The new _ProcessRestrictions is somewhat dependent on the SQL stuff,
3525 # but isn't quite generic enough to move into Tickets_Overlay_SQL.
3529 sub _ProcessRestrictions {
3532 #Blow away ticket aliases since we'll need to regenerate them for
3534 delete $self->{'TicketAliases'};
3535 delete $self->{'items_array'};
3536 delete $self->{'item_map'};
3537 delete $self->{'raw_rows'};
3538 delete $self->{'rows'};
3539 delete $self->{'count_all'};
3541 my $sql = $self->Query; # Violating the _SQL namespace
3542 if ( !$sql || $self->{'RecalcTicketLimits'} ) {
3544 # "Restrictions to Clauses Branch\n";
3545 my $clauseRef = eval { $self->_RestrictionsToClauses; };
3547 $RT::Logger->error( "RestrictionsToClauses: " . $@ );
3551 $sql = $self->ClausesToSQL($clauseRef);
3552 $self->FromSQL($sql) if $sql;
3556 $self->{'RecalcTicketLimits'} = 0;
3560 =head2 _BuildItemMap
3562 Build up a L</ItemMap> of first/last/next/prev items, so that we can
3563 display search nav quickly.
3570 my $window = RT->Config->Get('TicketsItemMapSize');
3572 $self->{'item_map'} = {};
3574 my $items = $self->ItemsArrayRefWindow( $window );
3575 return unless $items && @$items;
3578 $self->{'item_map'}{'first'} = $items->[0]->EffectiveId;
3579 for ( my $i = 0; $i < @$items; $i++ ) {
3580 my $item = $items->[$i];
3581 my $id = $item->EffectiveId;
3582 $self->{'item_map'}{$id}{'defined'} = 1;
3583 $self->{'item_map'}{$id}{'prev'} = $prev;
3584 $self->{'item_map'}{$id}{'next'} = $items->[$i+1]->EffectiveId
3588 $self->{'item_map'}{'last'} = $prev
3589 if !$window || @$items < $window;
3594 Returns an a map of all items found by this search. The map is a hash
3598 first => <first ticket id found>,
3599 last => <last ticket id found or undef>,
3602 prev => <the ticket id found before>,
3603 next => <the ticket id found after>,
3615 $self->_BuildItemMap unless $self->{'item_map'};
3616 return $self->{'item_map'};
3624 =head2 PrepForSerialization
3626 You don't want to serialize a big tickets object, as
3627 the {items} hash will be instantly invalid _and_ eat
3632 sub PrepForSerialization {
3634 delete $self->{'items'};
3635 delete $self->{'items_array'};
3636 $self->RedoSearch();
3641 RT::Tickets supports several flags which alter search behavior:
3644 allow_deleted_search (Otherwise never show deleted tickets in search results)
3645 looking_at_type (otherwise limit to type=ticket)
3647 These flags are set by calling
3649 $tickets->{'flagname'} = 1;
3651 BUG: There should be an API for this