1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2012 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 = RT::Tickets->new($CurrentUser);
72 A collection of RT::Tickets.
87 use base 'RT::SearchBuilder';
89 sub Table { 'Tickets'}
92 use DBIx::SearchBuilder::Unique;
94 # Configuration Tables:
96 # FIELD_METADATA is a mapping of searchable Field name, to Type, and other
99 our %FIELD_METADATA = (
100 Status => [ 'ENUM', ], #loc_left_pair
101 Queue => [ 'ENUM' => 'Queue', ], #loc_left_pair
102 Type => [ 'ENUM', ], #loc_left_pair
103 Creator => [ 'ENUM' => 'User', ], #loc_left_pair
104 LastUpdatedBy => [ 'ENUM' => 'User', ], #loc_left_pair
105 Owner => [ 'WATCHERFIELD' => 'Owner', ], #loc_left_pair
106 EffectiveId => [ 'INT', ], #loc_left_pair
107 id => [ 'ID', ], #loc_left_pair
108 InitialPriority => [ 'INT', ], #loc_left_pair
109 FinalPriority => [ 'INT', ], #loc_left_pair
110 Priority => [ 'INT', ], #loc_left_pair
111 TimeLeft => [ 'INT', ], #loc_left_pair
112 TimeWorked => [ 'INT', ], #loc_left_pair
113 TimeEstimated => [ 'INT', ], #loc_left_pair
115 Linked => [ 'LINK' ], #loc_left_pair
116 LinkedTo => [ 'LINK' => 'To' ], #loc_left_pair
117 LinkedFrom => [ 'LINK' => 'From' ], #loc_left_pair
118 MemberOf => [ 'LINK' => To => 'MemberOf', ], #loc_left_pair
119 DependsOn => [ 'LINK' => To => 'DependsOn', ], #loc_left_pair
120 RefersTo => [ 'LINK' => To => 'RefersTo', ], #loc_left_pair
121 HasMember => [ 'LINK' => From => 'MemberOf', ], #loc_left_pair
122 DependentOn => [ 'LINK' => From => 'DependsOn', ], #loc_left_pair
123 DependedOnBy => [ 'LINK' => From => 'DependsOn', ], #loc_left_pair
124 ReferredToBy => [ 'LINK' => From => 'RefersTo', ], #loc_left_pair
125 Told => [ 'DATE' => 'Told', ], #loc_left_pair
126 Starts => [ 'DATE' => 'Starts', ], #loc_left_pair
127 Started => [ 'DATE' => 'Started', ], #loc_left_pair
128 Due => [ 'DATE' => 'Due', ], #loc_left_pair
129 Resolved => [ 'DATE' => 'Resolved', ], #loc_left_pair
130 LastUpdated => [ 'DATE' => 'LastUpdated', ], #loc_left_pair
131 Created => [ 'DATE' => 'Created', ], #loc_left_pair
132 Subject => [ 'STRING', ], #loc_left_pair
133 Content => [ 'TRANSCONTENT', ], #loc_left_pair
134 ContentType => [ 'TRANSFIELD', ], #loc_left_pair
135 Filename => [ 'TRANSFIELD', ], #loc_left_pair
136 TransactionDate => [ 'TRANSDATE', ], #loc_left_pair
137 Requestor => [ 'WATCHERFIELD' => 'Requestor', ], #loc_left_pair
138 Requestors => [ 'WATCHERFIELD' => 'Requestor', ], #loc_left_pair
139 Cc => [ 'WATCHERFIELD' => 'Cc', ], #loc_left_pair
140 AdminCc => [ 'WATCHERFIELD' => 'AdminCc', ], #loc_left_pair
141 Watcher => [ 'WATCHERFIELD', ], #loc_left_pair
142 QueueCc => [ 'WATCHERFIELD' => 'Cc' => 'Queue', ], #loc_left_pair
143 QueueAdminCc => [ 'WATCHERFIELD' => 'AdminCc' => 'Queue', ], #loc_left_pair
144 QueueWatcher => [ 'WATCHERFIELD' => undef => 'Queue', ], #loc_left_pair
145 CustomFieldValue => [ 'CUSTOMFIELD', ], #loc_left_pair
146 CustomField => [ 'CUSTOMFIELD', ], #loc_left_pair
147 CF => [ 'CUSTOMFIELD', ], #loc_left_pair
148 Updated => [ 'TRANSDATE', ], #loc_left_pair
149 RequestorGroup => [ 'MEMBERSHIPFIELD' => 'Requestor', ], #loc_left_pair
150 CCGroup => [ 'MEMBERSHIPFIELD' => 'Cc', ], #loc_left_pair
151 AdminCCGroup => [ 'MEMBERSHIPFIELD' => 'AdminCc', ], #loc_left_pair
152 WatcherGroup => [ 'MEMBERSHIPFIELD', ], #loc_left_pair
153 HasAttribute => [ 'HASATTRIBUTE', 1 ],
154 HasNoAttribute => [ 'HASATTRIBUTE', 0 ],
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 TRANSCONTENT => \&_TransContentLimit,
174 TRANSDATE => \&_TransDateLimit,
175 WATCHERFIELD => \&_WatcherLimit,
176 MEMBERSHIPFIELD => \&_WatcherMembershipLimit,
177 CUSTOMFIELD => \&_CustomFieldLimit,
178 HASATTRIBUTE => \&_HasAttributeLimit,
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
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_SQL;
234 our @SORTFIELDS = qw(id Status
236 Owner Created Due Starts Started
238 Resolved LastUpdated Priority TimeWorked TimeLeft);
242 Returns the list of fields that lists of tickets can easily be sorted by
248 return (@SORTFIELDS);
252 # BEGIN SQL STUFF *********************************
257 $self->SUPER::CleanSlate( @_ );
258 delete $self->{$_} foreach qw(
260 _sql_group_members_aliases
261 _sql_object_cfv_alias
262 _sql_role_group_aliases
264 _sql_u_watchers_alias_for_sort
265 _sql_u_watchers_aliases
266 _sql_current_user_can_see_applied
270 =head1 Limit Helper Routines
272 These routines are the targets of a dispatch table depending on the
273 type of field. They all share the same signature:
275 my ($self,$field,$op,$value,@rest) = @_;
277 The values in @rest should be suitable for passing directly to
278 DBIx::SearchBuilder::Limit.
280 Essentially they are an expanded/broken out (and much simplified)
281 version of what ProcessRestrictions used to do. They're also much
282 more clearly delineated by the TYPE of field being processed.
291 my ( $sb, $field, $op, $value, @rest ) = @_;
293 if ( $value eq '__Bookmarked__' ) {
294 return $sb->_BookmarkLimit( $field, $op, $value, @rest );
296 return $sb->_IntLimit( $field, $op, $value, @rest );
301 my ( $sb, $field, $op, $value, @rest ) = @_;
303 die "Invalid operator $op for __Bookmarked__ search on $field"
304 unless $op =~ /^(=|!=)$/;
307 my $tmp = $sb->CurrentUser->UserObj->FirstAttribute('Bookmarks');
308 $tmp = $tmp->Content if $tmp;
313 return $sb->_SQLLimit(
320 # as bookmarked tickets can be merged we have to use a join
321 # but it should be pretty lightweight
322 my $tickets_alias = $sb->Join(
327 FIELD2 => 'EffectiveId',
331 my $ea = $op eq '='? 'OR': 'AND';
332 foreach my $id ( sort @bookmarks ) {
334 ALIAS => $tickets_alias,
338 $first? (@rest): ( ENTRYAGGREGATOR => $ea )
340 $first = 0 if $first;
347 Handle Fields which are limited to certain values, and potentially
348 need to be looked up from another class.
350 This subroutine actually handles two different kinds of fields. For
351 some the user is responsible for limiting the values. (i.e. Status,
354 For others, the value specified by the user will be looked by via
358 name of class to lookup in (Optional)
363 my ( $sb, $field, $op, $value, @rest ) = @_;
365 # SQL::Statement changes != to <>. (Can we remove this now?)
366 $op = "!=" if $op eq "<>";
368 die "Invalid Operation: $op for $field"
372 my $meta = $FIELD_METADATA{$field};
373 if ( defined $meta->[1] && defined $value && $value !~ /^\d+$/ ) {
374 my $class = "RT::" . $meta->[1];
375 my $o = $class->new( $sb->CurrentUser );
389 Handle fields where the values are limited to integers. (For example,
390 Priority, TimeWorked.)
398 my ( $sb, $field, $op, $value, @rest ) = @_;
400 die "Invalid Operator $op for $field"
401 unless $op =~ /^(=|!=|>|<|>=|<=)$/;
413 Handle fields which deal with links between tickets. (MemberOf, DependsOn)
416 1: Direction (From, To)
417 2: Link Type (MemberOf, DependsOn, RefersTo)
422 my ( $sb, $field, $op, $value, @rest ) = @_;
424 my $meta = $FIELD_METADATA{$field};
425 die "Invalid Operator $op for $field" unless $op =~ /^(=|!=|IS|IS NOT)$/io;
428 if ( $op eq '!=' || $op =~ /\bNOT\b/i ) {
432 $is_null = 1 if !$value || $value =~ /^null$/io;
435 $value = RT::URI->new( $sb->CurrentUser )->CanonicalizeURI( $value );
438 my $direction = $meta->[1] || '';
439 my ($matchfield, $linkfield) = ('', '');
440 if ( $direction eq 'To' ) {
441 ($matchfield, $linkfield) = ("Target", "Base");
443 elsif ( $direction eq 'From' ) {
444 ($matchfield, $linkfield) = ("Base", "Target");
446 elsif ( $direction ) {
447 die "Invalid link direction '$direction' for $field\n";
450 $sb->_LinkLimit( 'LinkedTo', $op, $value, @rest );
452 'LinkedFrom', $op, $value, @rest,
453 ENTRYAGGREGATOR => (($is_negative && $is_null) || (!$is_null && !$is_negative))? 'OR': 'AND',
461 $op = ($op =~ /^(=|IS)$/)? 'IS': 'IS NOT';
463 elsif ( $value =~ /\D/ ) {
466 $matchfield = "Local$matchfield" if $is_local;
468 #For doing a left join to find "unlinked tickets" we want to generate a query that looks like this
469 # SELECT main.* FROM Tickets main
470 # LEFT JOIN Links Links_1 ON ( (Links_1.Type = 'MemberOf')
471 # AND(main.id = Links_1.LocalTarget))
472 # WHERE Links_1.LocalBase IS NULL;
475 my $linkalias = $sb->Join(
480 FIELD2 => 'Local' . $linkfield
483 LEFTJOIN => $linkalias,
491 FIELD => $matchfield,
498 my $linkalias = $sb->Join(
503 FIELD2 => 'Local' . $linkfield
506 LEFTJOIN => $linkalias,
512 LEFTJOIN => $linkalias,
513 FIELD => $matchfield,
520 FIELD => $matchfield,
521 OPERATOR => $is_negative? 'IS': 'IS NOT',
530 Handle date fields. (Created, LastTold..)
533 1: type of link. (Probably not necessary.)
538 my ( $sb, $field, $op, $value, @rest ) = @_;
540 die "Invalid Date Op: $op"
541 unless $op =~ /^(=|>|<|>=|<=)$/;
543 my $meta = $FIELD_METADATA{$field};
544 die "Incorrect Meta Data for $field"
545 unless ( defined $meta->[1] );
547 my $date = RT::Date->new( $sb->CurrentUser );
548 $date->Set( Format => 'unknown', Value => $value );
552 # if we're specifying =, that means we want everything on a
553 # particular single day. in the database, we need to check for >
554 # and < the edges of that day.
556 $date->SetToMidnight( Timezone => 'server' );
557 my $daystart = $date->ISO;
559 my $dayend = $date->ISO;
575 ENTRYAGGREGATOR => 'AND',
593 Handle simple fields which are just strings. (Subject,Type)
601 my ( $sb, $field, $op, $value, @rest ) = @_;
605 # =, !=, LIKE, NOT LIKE
606 if ( RT->Config->Get('DatabaseType') eq 'Oracle'
607 && (!defined $value || !length $value)
608 && lc($op) ne 'is' && lc($op) ne 'is not'
610 if ($op eq '!=' || $op =~ /^NOT\s/i) {
627 =head2 _TransDateLimit
629 Handle fields limiting based on Transaction Date.
631 The inpupt value must be in a format parseable by Time::ParseDate
638 # This routine should really be factored into translimit.
639 sub _TransDateLimit {
640 my ( $sb, $field, $op, $value, @rest ) = @_;
642 # See the comments for TransLimit, they apply here too
644 my $txn_alias = $sb->JoinTransactions;
646 my $date = RT::Date->new( $sb->CurrentUser );
647 $date->Set( Format => 'unknown', Value => $value );
652 # if we're specifying =, that means we want everything on a
653 # particular single day. in the database, we need to check for >
654 # and < the edges of that day.
656 $date->SetToMidnight( Timezone => 'server' );
657 my $daystart = $date->ISO;
659 my $dayend = $date->ISO;
674 ENTRYAGGREGATOR => 'AND',
679 # not searching for a single day
682 #Search for the right field
697 Limit based on the ContentType or the Filename of a transaction.
702 my ( $self, $field, $op, $value, %rest ) = @_;
704 my $txn_alias = $self->JoinTransactions;
705 unless ( defined $self->{_sql_trattachalias} ) {
706 $self->{_sql_trattachalias} = $self->_SQLJoin(
707 TYPE => 'LEFT', # not all txns have an attachment
708 ALIAS1 => $txn_alias,
710 TABLE2 => 'Attachments',
711 FIELD2 => 'TransactionId',
717 ALIAS => $self->{_sql_trattachalias},
725 =head2 _TransContentLimit
727 Limit based on the Content of a transaction.
731 sub _TransContentLimit {
735 # If only this was this simple. We've got to do something
738 #Basically, we want to make sure that the limits apply to
739 #the same attachment, rather than just another attachment
740 #for the same ticket, no matter how many clauses we lump
741 #on. We put them in TicketAliases so that they get nuked
742 #when we redo the join.
744 # In the SQL, we might have
745 # (( Content = foo ) or ( Content = bar AND Content = baz ))
746 # The AND group should share the same Alias.
748 # Actually, maybe it doesn't matter. We use the same alias and it
749 # works itself out? (er.. different.)
751 # Steal more from _ProcessRestrictions
753 # FIXME: Maybe look at the previous FooLimit call, and if it was a
754 # TransLimit and EntryAggregator == AND, reuse the Aliases?
756 # Or better - store the aliases on a per subclause basis - since
757 # those are going to be the things we want to relate to each other,
760 # maybe we should not allow certain kinds of aggregation of these
761 # clauses and do a psuedo regex instead? - the problem is getting
762 # them all into the same subclause when you have (A op B op C) - the
763 # way they get parsed in the tree they're in different subclauses.
765 my ( $self, $field, $op, $value, %rest ) = @_;
766 $field = 'Content' if $field =~ /\W/;
768 my $config = RT->Config->Get('FullTextSearch') || {};
769 unless ( $config->{'Enable'} ) {
770 $self->_SQLLimit( %rest, FIELD => 'id', VALUE => 0 );
774 my $txn_alias = $self->JoinTransactions;
775 unless ( defined $self->{_sql_trattachalias} ) {
776 $self->{_sql_trattachalias} = $self->_SQLJoin(
777 TYPE => 'LEFT', # not all txns have an attachment
778 ALIAS1 => $txn_alias,
780 TABLE2 => 'Attachments',
781 FIELD2 => 'TransactionId',
786 if ( $config->{'Indexed'} ) {
787 my $db_type = RT->Config->Get('DatabaseType');
790 if ( $config->{'Table'} and $config->{'Table'} ne "Attachments") {
791 $alias = $self->{'_sql_aliases'}{'full_text'} ||= $self->_SQLJoin(
793 ALIAS1 => $self->{'_sql_trattachalias'},
795 TABLE2 => $config->{'Table'},
799 $alias = $self->{'_sql_trattachalias'};
802 #XXX: handle negative searches
803 my $index = $config->{'Column'};
804 if ( $db_type eq 'Oracle' ) {
805 my $dbh = $RT::Handle->dbh;
806 my $alias = $self->{_sql_trattachalias};
809 FUNCTION => "CONTAINS( $alias.$field, ".$dbh->quote($value) .")",
815 # this is required to trick DBIx::SB's LEFT JOINS optimizer
816 # into deciding that join is redundant as it is
818 ENTRYAGGREGATOR => 'AND',
819 ALIAS => $self->{_sql_trattachalias},
821 OPERATOR => 'IS NOT',
825 elsif ( $db_type eq 'Pg' ) {
826 my $dbh = $RT::Handle->dbh;
832 VALUE => 'plainto_tsquery('. $dbh->quote($value) .')',
836 elsif ( $db_type eq 'mysql' ) {
837 # XXX: We could theoretically skip the join to Attachments,
838 # and have Sphinx simply index and group by the TicketId,
839 # and join Ticket.id to that attribute, which would be much
840 # more efficient -- however, this is only a possibility if
841 # there are no other transaction limits.
843 # This is a special character. Note that \ does not escape
844 # itself (in Sphinx 2.1.0, at least), so 'foo\;bar' becoming
845 # 'foo\\;bar' is not a vulnerability, and is still parsed as
846 # "foo, \, ;, then bar". Happily, the default mode is
847 # "all", meaning that boolean operators are not special.
850 my $max = $config->{'MaxMatches'};
856 VALUE => "$value;limit=$max;maxmatches=$max",
862 ALIAS => $self->{_sql_trattachalias},
869 if ( RT->Config->Get('DontSearchFileAttachments') ) {
871 ENTRYAGGREGATOR => 'AND',
872 ALIAS => $self->{_sql_trattachalias},
883 Handle watcher limits. (Requestor, CC, etc..)
899 my $meta = $FIELD_METADATA{ $field };
900 my $type = $meta->[1] || '';
901 my $class = $meta->[2] || 'Ticket';
903 # Bail if the subfield is not allowed
905 and not grep { $_ eq $rest{SUBKEY} } @{$SEARCHABLE_SUBFIELDS{'User'}})
907 die "Invalid watcher subfield: '$rest{SUBKEY}'";
910 # Owner was ENUM field, so "Owner = 'xxx'" allowed user to
911 # search by id and Name at the same time, this is workaround
912 # to preserve backward compatibility
913 if ( $field eq 'Owner' ) {
914 if ( $op =~ /^!?=$/ && (!$rest{'SUBKEY'} || $rest{'SUBKEY'} eq 'Name' || $rest{'SUBKEY'} eq 'EmailAddress') ) {
915 my $o = RT::User->new( $self->CurrentUser );
916 my $method = ($rest{'SUBKEY'}||'') eq 'EmailAddress' ? 'LoadByEmail': 'Load';
917 $o->$method( $value );
926 if ( ($rest{'SUBKEY'}||'') eq 'id' ) {
936 $rest{SUBKEY} ||= 'EmailAddress';
938 my $groups = $self->_RoleGroupsJoin( Type => $type, Class => $class, New => !$type );
941 if ( $op =~ /^IS(?: NOT)?$/ ) {
942 # is [not] empty case
944 my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
945 # to avoid joining the table Users into the query, we just join GM
946 # and make sure we don't match records where group is member of itself
948 LEFTJOIN => $group_members,
951 VALUE => "$group_members.MemberId",
955 ALIAS => $group_members,
962 elsif ( $op =~ /^!=$|^NOT\s+/i ) {
963 # negative condition case
966 $op =~ s/!|NOT\s+//i;
968 # XXX: we have no way to build correct "Watcher.X != 'Y'" when condition
969 # "X = 'Y'" matches more then one user so we try to fetch two records and
970 # do the right thing when there is only one exist and semi-working solution
972 my $users_obj = RT::Users->new( $self->CurrentUser );
974 FIELD => $rest{SUBKEY},
979 $users_obj->RowsPerPage(2);
980 my @users = @{ $users_obj->ItemsArrayRef };
982 my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
985 $uid = $users[0]->id if @users;
987 LEFTJOIN => $group_members,
988 ALIAS => $group_members,
994 ALIAS => $group_members,
1000 $self->SUPER::Limit(
1001 LEFTJOIN => $group_members,
1004 VALUE => "$group_members.MemberId",
1007 my $users = $self->Join(
1009 ALIAS1 => $group_members,
1010 FIELD1 => 'MemberId',
1014 $self->SUPER::Limit(
1017 FIELD => $rest{SUBKEY},
1031 # positive condition case
1033 my $group_members = $self->_GroupMembersJoin(
1034 GroupsAlias => $groups, New => 1, Left => 0
1036 my $users = $self->Join(
1038 ALIAS1 => $group_members,
1039 FIELD1 => 'MemberId',
1046 FIELD => $rest{'SUBKEY'},
1055 sub _RoleGroupsJoin {
1057 my %args = (New => 0, Class => 'Ticket', Type => '', @_);
1058 return $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $args{'Type'} }
1059 if $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $args{'Type'} }
1062 # we always have watcher groups for ticket, so we use INNER join
1063 my $groups = $self->Join(
1065 FIELD1 => $args{'Class'} eq 'Queue'? 'Queue': 'id',
1067 FIELD2 => 'Instance',
1068 ENTRYAGGREGATOR => 'AND',
1070 $self->SUPER::Limit(
1071 LEFTJOIN => $groups,
1074 VALUE => 'RT::'. $args{'Class'} .'-Role',
1076 $self->SUPER::Limit(
1077 LEFTJOIN => $groups,
1080 VALUE => $args{'Type'},
1083 $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $args{'Type'} } = $groups
1084 unless $args{'New'};
1089 sub _GroupMembersJoin {
1091 my %args = (New => 1, GroupsAlias => undef, Left => 1, @_);
1093 return $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} }
1094 if $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} }
1097 my $alias = $self->Join(
1098 $args{'Left'} ? (TYPE => 'LEFT') : (),
1099 ALIAS1 => $args{'GroupsAlias'},
1101 TABLE2 => 'CachedGroupMembers',
1102 FIELD2 => 'GroupId',
1103 ENTRYAGGREGATOR => 'AND',
1105 $self->SUPER::Limit(
1106 $args{'Left'} ? (LEFTJOIN => $alias) : (),
1108 FIELD => 'Disabled',
1112 $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} } = $alias
1113 unless $args{'New'};
1120 Helper function which provides joins to a watchers table both for limits
1127 my $type = shift || '';
1130 my $groups = $self->_RoleGroupsJoin( Type => $type );
1131 my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
1132 # XXX: work around, we must hide groups that
1133 # are members of the role group we search in,
1134 # otherwise them result in wrong NULLs in Users
1135 # table and break ordering. Now, we know that
1136 # RT doesn't allow to add groups as members of the
1137 # ticket roles, so we just hide entries in CGM table
1138 # with MemberId == GroupId from results
1139 $self->SUPER::Limit(
1140 LEFTJOIN => $group_members,
1143 VALUE => "$group_members.MemberId",
1146 my $users = $self->Join(
1148 ALIAS1 => $group_members,
1149 FIELD1 => 'MemberId',
1153 return ($groups, $group_members, $users);
1156 =head2 _WatcherMembershipLimit
1158 Handle watcher membership limits, i.e. whether the watcher belongs to a
1159 specific group or not.
1162 1: Field to query on
1164 SELECT DISTINCT main.*
1168 CachedGroupMembers CachedGroupMembers_2,
1171 (main.EffectiveId = main.id)
1173 (main.Status != 'deleted')
1175 (main.Type = 'ticket')
1178 (Users_3.EmailAddress = '22')
1180 (Groups_1.Domain = 'RT::Ticket-Role')
1182 (Groups_1.Type = 'RequestorGroup')
1185 Groups_1.Instance = main.id
1187 Groups_1.id = CachedGroupMembers_2.GroupId
1189 CachedGroupMembers_2.MemberId = Users_3.id
1190 ORDER BY main.id ASC
1195 sub _WatcherMembershipLimit {
1196 my ( $self, $field, $op, $value, @rest ) = @_;
1201 my $groups = $self->NewAlias('Groups');
1202 my $groupmembers = $self->NewAlias('CachedGroupMembers');
1203 my $users = $self->NewAlias('Users');
1204 my $memberships = $self->NewAlias('CachedGroupMembers');
1206 if ( ref $field ) { # gross hack
1207 my @bundle = @$field;
1209 for my $chunk (@bundle) {
1210 ( $field, $op, $value, @rest ) = @$chunk;
1212 ALIAS => $memberships,
1223 ALIAS => $memberships,
1231 # Tie to groups for tickets we care about
1235 VALUE => 'RT::Ticket-Role',
1236 ENTRYAGGREGATOR => 'AND'
1241 FIELD1 => 'Instance',
1248 # If we care about which sort of watcher
1249 my $meta = $FIELD_METADATA{$field};
1250 my $type = ( defined $meta->[1] ? $meta->[1] : undef );
1257 ENTRYAGGREGATOR => 'AND'
1264 ALIAS2 => $groupmembers,
1269 ALIAS1 => $groupmembers,
1270 FIELD1 => 'MemberId',
1276 ALIAS => $groupmembers,
1277 FIELD => 'Disabled',
1282 ALIAS1 => $memberships,
1283 FIELD1 => 'MemberId',
1289 ALIAS => $memberships,
1290 FIELD => 'Disabled',
1299 =head2 _CustomFieldDecipher
1301 Try and turn a CF descriptor into (cfid, cfname) object pair.
1305 sub _CustomFieldDecipher {
1306 my ($self, $string) = @_;
1308 my ($queue, $field, $column) = ($string =~ /^(?:(.+?)\.)?{(.+)}(?:\.(Content|LargeContent))?$/);
1309 $field ||= ($string =~ /^{(.*?)}$/)[0] || $string;
1313 my $q = RT::Queue->new( $self->CurrentUser );
1317 # $queue = $q->Name; # should we normalize the queue?
1318 $cf = $q->CustomField( $field );
1321 $RT::Logger->warning("Queue '$queue' doesn't exist, parsed from '$string'");
1325 elsif ( $field =~ /\D/ ) {
1327 my $cfs = RT::CustomFields->new( $self->CurrentUser );
1328 $cfs->Limit( FIELD => 'Name', VALUE => $field );
1329 $cfs->LimitToLookupType('RT::Queue-RT::Ticket');
1331 # if there is more then one field the current user can
1332 # see with the same name then we shouldn't return cf object
1333 # as we don't know which one to use
1336 $cf = undef if $cfs->Next;
1340 $cf = RT::CustomField->new( $self->CurrentUser );
1341 $cf->Load( $field );
1344 return ($queue, $field, $cf, $column);
1347 =head2 _CustomFieldJoin
1349 Factor out the Join of custom fields so we can use it for sorting too
1353 sub _CustomFieldJoin {
1354 my ($self, $cfkey, $cfid, $field) = @_;
1355 # Perform one Join per CustomField
1356 if ( $self->{_sql_object_cfv_alias}{$cfkey} ||
1357 $self->{_sql_cf_alias}{$cfkey} )
1359 return ( $self->{_sql_object_cfv_alias}{$cfkey},
1360 $self->{_sql_cf_alias}{$cfkey} );
1363 my ($TicketCFs, $CFs);
1365 $TicketCFs = $self->{_sql_object_cfv_alias}{$cfkey} = $self->Join(
1369 TABLE2 => 'ObjectCustomFieldValues',
1370 FIELD2 => 'ObjectId',
1372 $self->SUPER::Limit(
1373 LEFTJOIN => $TicketCFs,
1374 FIELD => 'CustomField',
1376 ENTRYAGGREGATOR => 'AND'
1380 my $ocfalias = $self->Join(
1383 TABLE2 => 'ObjectCustomFields',
1384 FIELD2 => 'ObjectId',
1387 $self->SUPER::Limit(
1388 LEFTJOIN => $ocfalias,
1389 ENTRYAGGREGATOR => 'OR',
1390 FIELD => 'ObjectId',
1394 $CFs = $self->{_sql_cf_alias}{$cfkey} = $self->Join(
1396 ALIAS1 => $ocfalias,
1397 FIELD1 => 'CustomField',
1398 TABLE2 => 'CustomFields',
1401 $self->SUPER::Limit(
1403 ENTRYAGGREGATOR => 'AND',
1404 FIELD => 'LookupType',
1405 VALUE => 'RT::Queue-RT::Ticket',
1407 $self->SUPER::Limit(
1409 ENTRYAGGREGATOR => 'AND',
1414 $TicketCFs = $self->{_sql_object_cfv_alias}{$cfkey} = $self->Join(
1418 TABLE2 => 'ObjectCustomFieldValues',
1419 FIELD2 => 'CustomField',
1421 $self->SUPER::Limit(
1422 LEFTJOIN => $TicketCFs,
1423 FIELD => 'ObjectId',
1426 ENTRYAGGREGATOR => 'AND',
1429 $self->SUPER::Limit(
1430 LEFTJOIN => $TicketCFs,
1431 FIELD => 'ObjectType',
1432 VALUE => 'RT::Ticket',
1433 ENTRYAGGREGATOR => 'AND'
1435 $self->SUPER::Limit(
1436 LEFTJOIN => $TicketCFs,
1437 FIELD => 'Disabled',
1440 ENTRYAGGREGATOR => 'AND'
1443 return ($TicketCFs, $CFs);
1446 =head2 _CustomFieldLimit
1448 Limit based on CustomFields
1455 use Regexp::Common qw(RE_net_IPv4);
1456 use Regexp::Common::net::CIDR;
1459 sub _CustomFieldLimit {
1460 my ( $self, $_field, $op, $value, %rest ) = @_;
1462 my $field = $rest{'SUBKEY'} || die "No field specified";
1464 # For our sanity, we can only limit on one queue at a time
1466 my ($queue, $cfid, $cf, $column);
1467 ($queue, $field, $cf, $column) = $self->_CustomFieldDecipher( $field );
1468 $cfid = $cf ? $cf->id : 0 ;
1470 # If we're trying to find custom fields that don't match something, we
1471 # want tickets where the custom field has no value at all. Note that
1472 # we explicitly don't include the "IS NULL" case, since we would
1473 # otherwise end up with a redundant clause.
1475 my ($negative_op, $null_op, $inv_op, $range_op)
1476 = $self->ClassifySQLOperation( $op );
1479 return @_ unless RT->Config->Get('DatabaseType') eq 'Oracle';
1482 return %args unless $args{'FIELD'} eq 'LargeContent';
1484 my $op = $args{'OPERATOR'};
1486 $args{'OPERATOR'} = 'MATCHES';
1488 elsif ( $op eq '!=' ) {
1489 $args{'OPERATOR'} = 'NOT MATCHES';
1491 elsif ( $op =~ /^[<>]=?$/ ) {
1492 $args{'FUNCTION'} = "TO_CHAR( $args{'ALIAS'}.LargeContent )";
1497 if ( $cf && $cf->Type eq 'IPAddress' ) {
1498 my $parsed = RT::ObjectCustomFieldValue->ParseIP($value);
1503 $RT::Logger->warn("$value is not a valid IPAddress");
1507 if ( $cf && $cf->Type eq 'IPAddressRange' ) {
1509 if ( $value =~ /^\s*$RE{net}{CIDR}{IPv4}{-keep}\s*$/o ) {
1511 # convert incomplete 192.168/24 to 192.168.0.0/24 format
1513 join( '.', map $_ || 0, ( split /\./, $1 )[ 0 .. 3 ] ) . "/$2"
1517 my ( $start_ip, $end_ip ) =
1518 RT::ObjectCustomFieldValue->ParseIPRange($value);
1519 if ( $start_ip && $end_ip ) {
1520 if ( $op =~ /^([<>])=?$/ ) {
1521 my $is_less = $1 eq '<' ? 1 : 0;
1530 $value = join '-', $start_ip, $end_ip;
1534 $RT::Logger->warn("$value is not a valid IPAddressRange");
1538 my $single_value = !$cf || !$cfid || $cf->SingleValue;
1540 my $cfkey = $cfid ? $cfid : "$queue.$field";
1542 if ( $null_op && !$column ) {
1543 # IS[ NOT] NULL without column is the same as has[ no] any CF value,
1544 # we can reuse our default joins for this operation
1545 # with column specified we have different situation
1546 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
1549 ALIAS => $TicketCFs,
1558 OPERATOR => 'IS NOT',
1561 ENTRYAGGREGATOR => 'AND',
1565 elsif ( $op !~ /^[<>]=?$/ && ( $cf && $cf->Type eq 'IPAddressRange')) {
1567 my ($start_ip, $end_ip) = split /-/, $value;
1570 if ( $op !~ /NOT|!=|<>/i ) { # positive equation
1571 $self->_CustomFieldLimit(
1572 'CF', '<=', $end_ip, %rest,
1573 SUBKEY => $rest{'SUBKEY'}. '.Content',
1575 $self->_CustomFieldLimit(
1576 'CF', '>=', $start_ip, %rest,
1577 SUBKEY => $rest{'SUBKEY'}. '.LargeContent',
1578 ENTRYAGGREGATOR => 'AND',
1580 # as well limit borders so DB optimizers can use better
1581 # estimations and scan less rows
1582 # have to disable this tweak because of ipv6
1583 # $self->_CustomFieldLimit(
1584 # $field, '>=', '000.000.000.000', %rest,
1585 # SUBKEY => $rest{'SUBKEY'}. '.Content',
1586 # ENTRYAGGREGATOR => 'AND',
1588 # $self->_CustomFieldLimit(
1589 # $field, '<=', '255.255.255.255', %rest,
1590 # SUBKEY => $rest{'SUBKEY'}. '.LargeContent',
1591 # ENTRYAGGREGATOR => 'AND',
1594 else { # negative equation
1595 $self->_CustomFieldLimit($field, '>', $end_ip, %rest);
1596 $self->_CustomFieldLimit(
1597 $field, '<', $start_ip, %rest,
1598 SUBKEY => $rest{'SUBKEY'}. '.LargeContent',
1599 ENTRYAGGREGATOR => 'OR',
1601 # TODO: as well limit borders so DB optimizers can use better
1602 # estimations and scan less rows, but it's harder to do
1603 # as we have OR aggregator
1607 elsif ( !$negative_op || $single_value ) {
1608 $cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++ if !$single_value && !$range_op;
1609 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
1616 # if column is defined then deal only with it
1617 # otherwise search in Content and in LargeContent
1619 $self->_SQLLimit( $fix_op->(
1620 ALIAS => $TicketCFs,
1632 # need special treatment for Date
1633 if ( $cf and $cf->Type eq 'DateTime' and $op eq '=' ) {
1635 if ( $value =~ /:/ ) {
1636 # there is time speccified.
1637 my $date = RT::Date->new( $self->CurrentUser );
1638 $date->Set( Format => 'unknown', Value => $value );
1640 ALIAS => $TicketCFs,
1643 VALUE => $date->ISO,
1648 # no time specified, that means we want everything on a
1649 # particular day. in the database, we need to check for >
1650 # and < the edges of that day.
1651 my $date = RT::Date->new( $self->CurrentUser );
1652 $date->Set( Format => 'unknown', Value => $value );
1653 $date->SetToMidnight( Timezone => 'server' );
1654 my $daystart = $date->ISO;
1656 my $dayend = $date->ISO;
1661 ALIAS => $TicketCFs,
1669 ALIAS => $TicketCFs,
1674 ENTRYAGGREGATOR => 'AND',
1680 elsif ( $op eq '=' || $op eq '!=' || $op eq '<>' ) {
1681 if ( length( Encode::encode_utf8($value) ) < 256 ) {
1683 ALIAS => $TicketCFs,
1694 ALIAS => $TicketCFs,
1698 ENTRYAGGREGATOR => 'OR'
1701 ALIAS => $TicketCFs,
1705 ENTRYAGGREGATOR => 'OR'
1708 $self->_SQLLimit( $fix_op->(
1709 ALIAS => $TicketCFs,
1710 FIELD => 'LargeContent',
1713 ENTRYAGGREGATOR => 'AND',
1720 ALIAS => $TicketCFs,
1731 ALIAS => $TicketCFs,
1735 ENTRYAGGREGATOR => 'OR'
1738 ALIAS => $TicketCFs,
1742 ENTRYAGGREGATOR => 'OR'
1745 $self->_SQLLimit( $fix_op->(
1746 ALIAS => $TicketCFs,
1747 FIELD => 'LargeContent',
1750 ENTRYAGGREGATOR => 'AND',
1757 # XXX: if we join via CustomFields table then
1758 # because of order of left joins we get NULLs in
1759 # CF table and then get nulls for those records
1760 # in OCFVs table what result in wrong results
1761 # as decifer method now tries to load a CF then
1762 # we fall into this situation only when there
1763 # are more than one CF with the name in the DB.
1764 # the same thing applies to order by call.
1765 # TODO: reorder joins T <- OCFVs <- CFs <- OCFs if
1766 # we want treat IS NULL as (not applies or has
1771 OPERATOR => 'IS NOT',
1774 ENTRYAGGREGATOR => 'AND',
1780 ALIAS => $TicketCFs,
1781 FIELD => $column || 'Content',
1785 ENTRYAGGREGATOR => 'OR',
1793 $cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++;
1794 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
1797 $op =~ s/!|NOT\s+//i;
1799 # if column is defined then deal only with it
1800 # otherwise search in Content and in LargeContent
1802 $self->SUPER::Limit( $fix_op->(
1803 LEFTJOIN => $TicketCFs,
1804 ALIAS => $TicketCFs,
1812 $self->SUPER::Limit(
1813 LEFTJOIN => $TicketCFs,
1814 ALIAS => $TicketCFs,
1823 ALIAS => $TicketCFs,
1832 sub _HasAttributeLimit {
1833 my ( $self, $field, $op, $value, %rest ) = @_;
1835 my $alias = $self->Join(
1839 TABLE2 => 'Attributes',
1840 FIELD2 => 'ObjectId',
1842 $self->SUPER::Limit(
1844 FIELD => 'ObjectType',
1845 VALUE => 'RT::Ticket',
1846 ENTRYAGGREGATOR => 'AND'
1848 $self->SUPER::Limit(
1853 ENTRYAGGREGATOR => 'AND'
1859 OPERATOR => $FIELD_METADATA{$field}->[1]? 'IS NOT': 'IS',
1866 # End Helper Functions
1868 # End of SQL Stuff -------------------------------------------------
1871 =head2 OrderByCols ARRAY
1873 A modified version of the OrderBy method which automatically joins where
1874 C<ALIAS> is set to the name of a watcher type.
1885 foreach my $row (@args) {
1886 if ( $row->{ALIAS} ) {
1890 if ( $row->{FIELD} !~ /\./ ) {
1891 my $meta = $self->FIELDS->{ $row->{FIELD} };
1897 if ( $meta->[0] eq 'ENUM' && ($meta->[1]||'') eq 'Queue' ) {
1898 my $alias = $self->Join(
1901 FIELD1 => $row->{'FIELD'},
1905 push @res, { %$row, ALIAS => $alias, FIELD => "Name" };
1906 } elsif ( ( $meta->[0] eq 'ENUM' && ($meta->[1]||'') eq 'User' )
1907 || ( $meta->[0] eq 'WATCHERFIELD' && ($meta->[1]||'') eq 'Owner' )
1909 my $alias = $self->Join(
1912 FIELD1 => $row->{'FIELD'},
1916 push @res, { %$row, ALIAS => $alias, FIELD => "Name" };
1923 my ( $field, $subkey ) = split /\./, $row->{FIELD}, 2;
1924 my $meta = $self->FIELDS->{$field};
1925 if ( defined $meta->[0] && $meta->[0] eq 'WATCHERFIELD' ) {
1926 # cache alias as we want to use one alias per watcher type for sorting
1927 my $users = $self->{_sql_u_watchers_alias_for_sort}{ $meta->[1] };
1929 $self->{_sql_u_watchers_alias_for_sort}{ $meta->[1] }
1930 = $users = ( $self->_WatcherJoin( $meta->[1] ) )[2];
1932 push @res, { %$row, ALIAS => $users, FIELD => $subkey };
1933 } elsif ( defined $meta->[0] && $meta->[0] eq 'CUSTOMFIELD' ) {
1934 my ($queue, $field, $cf_obj, $column) = $self->_CustomFieldDecipher( $subkey );
1935 my $cfkey = $cf_obj ? $cf_obj->id : "$queue.$field";
1936 $cfkey .= ".ordering" if !$cf_obj || ($cf_obj->MaxValues||0) != 1;
1937 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, ($cf_obj ?$cf_obj->id :0) , $field );
1938 # this is described in _CustomFieldLimit
1942 OPERATOR => 'IS NOT',
1945 ENTRYAGGREGATOR => 'AND',
1948 # For those cases where we are doing a join against the
1949 # CF name, and don't have a CFid, use Unique to make sure
1950 # we don't show duplicate tickets. NOTE: I'm pretty sure
1951 # this will stay mixed in for the life of the
1952 # class/package, and not just for the life of the object.
1953 # Potential performance issue.
1954 require DBIx::SearchBuilder::Unique;
1955 DBIx::SearchBuilder::Unique->import;
1957 my $CFvs = $self->Join(
1959 ALIAS1 => $TicketCFs,
1960 FIELD1 => 'CustomField',
1961 TABLE2 => 'CustomFieldValues',
1962 FIELD2 => 'CustomField',
1964 $self->SUPER::Limit(
1968 VALUE => $TicketCFs . ".Content",
1969 ENTRYAGGREGATOR => 'AND'
1972 push @res, { %$row, ALIAS => $CFvs, FIELD => 'SortOrder' };
1973 push @res, { %$row, ALIAS => $TicketCFs, FIELD => 'Content' };
1974 } elsif ( $field eq "Custom" && $subkey eq "Ownership") {
1975 # PAW logic is "reversed"
1977 if (exists $row->{ORDER} ) {
1978 my $o = $row->{ORDER};
1979 delete $row->{ORDER};
1980 $order = "DESC" if $o =~ /asc/i;
1983 # Ticket.Owner 1 0 X
1984 # Unowned Tickets 0 1 X
1987 foreach my $uid ( $self->CurrentUser->Id, RT->Nobody->Id ) {
1988 if ( RT->Config->Get('DatabaseType') eq 'Oracle' ) {
1989 my $f = ($row->{'ALIAS'} || 'main') .'.Owner';
1994 FUNCTION => "CASE WHEN $f=$uid THEN 1 ELSE 0 END",
2001 FUNCTION => "Owner=$uid",
2007 push @res, { %$row, FIELD => "Priority", ORDER => $order } ;
2013 return $self->SUPER::OrderByCols(@res);
2021 Takes a paramhash with the fields FIELD, OPERATOR, VALUE and DESCRIPTION
2022 Generally best called from LimitFoo methods
2032 DESCRIPTION => undef,
2035 $args{'DESCRIPTION'} = $self->loc(
2036 "[_1] [_2] [_3]", $args{'FIELD'},
2037 $args{'OPERATOR'}, $args{'VALUE'}
2039 if ( !defined $args{'DESCRIPTION'} );
2041 my $index = $self->_NextIndex;
2043 # make the TicketRestrictions hash the equivalent of whatever we just passed in;
2045 %{ $self->{'TicketRestrictions'}{$index} } = %args;
2047 $self->{'RecalcTicketLimits'} = 1;
2049 # If we're looking at the effective id, we don't want to append the other clause
2050 # which limits us to tickets where id = effective id
2051 if ( $args{'FIELD'} eq 'EffectiveId'
2052 && ( !$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) )
2054 $self->{'looking_at_effective_id'} = 1;
2057 if ( $args{'FIELD'} eq 'Type'
2058 && ( !$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) )
2060 $self->{'looking_at_type'} = 1;
2071 LimitQueue takes a paramhash with the fields OPERATOR and VALUE.
2072 OPERATOR is one of = or !=. (It defaults to =).
2073 VALUE is a queue id or Name.
2086 #TODO VALUE should also take queue objects
2087 if ( defined $args{'VALUE'} && $args{'VALUE'} !~ /^\d+$/ ) {
2088 my $queue = RT::Queue->new( $self->CurrentUser );
2089 $queue->Load( $args{'VALUE'} );
2090 $args{'VALUE'} = $queue->Id;
2093 # What if they pass in an Id? Check for isNum() and convert to
2096 #TODO check for a valid queue here
2100 VALUE => $args{'VALUE'},
2101 OPERATOR => $args{'OPERATOR'},
2102 DESCRIPTION => join(
2103 ' ', $self->loc('Queue'), $args{'OPERATOR'}, $args{'VALUE'},
2113 Takes a paramhash with the fields OPERATOR and VALUE.
2114 OPERATOR is one of = or !=.
2117 RT adds Status != 'deleted' until object has
2118 allow_deleted_search internal property set.
2119 $tickets->{'allow_deleted_search'} = 1;
2120 $tickets->LimitStatus( VALUE => 'deleted' );
2132 VALUE => $args{'VALUE'},
2133 OPERATOR => $args{'OPERATOR'},
2134 DESCRIPTION => join( ' ',
2135 $self->loc('Status'), $args{'OPERATOR'},
2136 $self->loc( $args{'VALUE'} ) ),
2144 If called, this search will not automatically limit the set of results found
2145 to tickets of type "Ticket". Tickets of other types, such as "project" and
2146 "approval" will be found.
2153 # Instead of faking a Limit that later gets ignored, fake up the
2154 # fact that we're already looking at type, so that the check in
2155 # Tickets_SQL/FromSQL goes down the right branch
2157 # $self->LimitType(VALUE => '__any');
2158 $self->{looking_at_type} = 1;
2165 Takes a paramhash with the fields OPERATOR and VALUE.
2166 OPERATOR is one of = or !=, it defaults to "=".
2167 VALUE is a string to search for in the type of the ticket.
2182 VALUE => $args{'VALUE'},
2183 OPERATOR => $args{'OPERATOR'},
2184 DESCRIPTION => join( ' ',
2185 $self->loc('Type'), $args{'OPERATOR'}, $args{'Limit'}, ),
2195 Takes a paramhash with the fields OPERATOR and VALUE.
2196 OPERATOR is one of = or !=.
2197 VALUE is a string to search for in the subject of the ticket.
2206 VALUE => $args{'VALUE'},
2207 OPERATOR => $args{'OPERATOR'},
2208 DESCRIPTION => join( ' ',
2209 $self->loc('Subject'), $args{'OPERATOR'}, $args{'VALUE'}, ),
2215 # Things that can be > < = !=
2220 Takes a paramhash with the fields OPERATOR and VALUE.
2221 OPERATOR is one of =, >, < or !=.
2222 VALUE is a ticket Id to search for
2235 VALUE => $args{'VALUE'},
2236 OPERATOR => $args{'OPERATOR'},
2238 join( ' ', $self->loc('Id'), $args{'OPERATOR'}, $args{'VALUE'}, ),
2244 =head2 LimitPriority
2246 Takes a paramhash with the fields OPERATOR and VALUE.
2247 OPERATOR is one of =, >, < or !=.
2248 VALUE is a value to match the ticket\'s priority against
2256 FIELD => 'Priority',
2257 VALUE => $args{'VALUE'},
2258 OPERATOR => $args{'OPERATOR'},
2259 DESCRIPTION => join( ' ',
2260 $self->loc('Priority'),
2261 $args{'OPERATOR'}, $args{'VALUE'}, ),
2267 =head2 LimitInitialPriority
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 initial priority against
2276 sub LimitInitialPriority {
2280 FIELD => 'InitialPriority',
2281 VALUE => $args{'VALUE'},
2282 OPERATOR => $args{'OPERATOR'},
2283 DESCRIPTION => join( ' ',
2284 $self->loc('Initial Priority'), $args{'OPERATOR'},
2291 =head2 LimitFinalPriority
2293 Takes a paramhash with the fields OPERATOR and VALUE.
2294 OPERATOR is one of =, >, < or !=.
2295 VALUE is a value to match the ticket\'s final priority against
2299 sub LimitFinalPriority {
2303 FIELD => 'FinalPriority',
2304 VALUE => $args{'VALUE'},
2305 OPERATOR => $args{'OPERATOR'},
2306 DESCRIPTION => join( ' ',
2307 $self->loc('Final Priority'), $args{'OPERATOR'},
2314 =head2 LimitTimeWorked
2316 Takes a paramhash with the fields OPERATOR and VALUE.
2317 OPERATOR is one of =, >, < or !=.
2318 VALUE is a value to match the ticket's TimeWorked attribute
2322 sub LimitTimeWorked {
2326 FIELD => 'TimeWorked',
2327 VALUE => $args{'VALUE'},
2328 OPERATOR => $args{'OPERATOR'},
2329 DESCRIPTION => join( ' ',
2330 $self->loc('Time Worked'),
2331 $args{'OPERATOR'}, $args{'VALUE'}, ),
2337 =head2 LimitTimeLeft
2339 Takes a paramhash with the fields OPERATOR and VALUE.
2340 OPERATOR is one of =, >, < or !=.
2341 VALUE is a value to match the ticket's TimeLeft attribute
2349 FIELD => 'TimeLeft',
2350 VALUE => $args{'VALUE'},
2351 OPERATOR => $args{'OPERATOR'},
2352 DESCRIPTION => join( ' ',
2353 $self->loc('Time Left'),
2354 $args{'OPERATOR'}, $args{'VALUE'}, ),
2364 Takes a paramhash with the fields OPERATOR and VALUE.
2365 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2366 VALUE is a string to search for in the body of the ticket
2375 VALUE => $args{'VALUE'},
2376 OPERATOR => $args{'OPERATOR'},
2377 DESCRIPTION => join( ' ',
2378 $self->loc('Ticket content'), $args{'OPERATOR'},
2385 =head2 LimitFilename
2387 Takes a paramhash with the fields OPERATOR and VALUE.
2388 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2389 VALUE is a string to search for in the body of the ticket
2397 FIELD => 'Filename',
2398 VALUE => $args{'VALUE'},
2399 OPERATOR => $args{'OPERATOR'},
2400 DESCRIPTION => join( ' ',
2401 $self->loc('Attachment filename'), $args{'OPERATOR'},
2407 =head2 LimitContentType
2409 Takes a paramhash with the fields OPERATOR and VALUE.
2410 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2411 VALUE is a content type to search ticket attachments for
2415 sub LimitContentType {
2419 FIELD => 'ContentType',
2420 VALUE => $args{'VALUE'},
2421 OPERATOR => $args{'OPERATOR'},
2422 DESCRIPTION => join( ' ',
2423 $self->loc('Ticket content type'), $args{'OPERATOR'},
2434 Takes a paramhash with the fields OPERATOR and VALUE.
2435 OPERATOR is one of = or !=.
2447 my $owner = RT::User->new( $self->CurrentUser );
2448 $owner->Load( $args{'VALUE'} );
2450 # FIXME: check for a valid $owner
2453 VALUE => $args{'VALUE'},
2454 OPERATOR => $args{'OPERATOR'},
2455 DESCRIPTION => join( ' ',
2456 $self->loc('Owner'), $args{'OPERATOR'}, $owner->Name(), ),
2466 Takes a paramhash with the fields OPERATOR, TYPE and VALUE.
2467 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2468 VALUE is a value to match the ticket\'s watcher email addresses against
2469 TYPE is the sort of watchers you want to match against. Leave it undef if you want to search all of them
2483 #build us up a description
2484 my ( $watcher_type, $desc );
2485 if ( $args{'TYPE'} ) {
2486 $watcher_type = $args{'TYPE'};
2489 $watcher_type = "Watcher";
2493 FIELD => $watcher_type,
2494 VALUE => $args{'VALUE'},
2495 OPERATOR => $args{'OPERATOR'},
2496 TYPE => $args{'TYPE'},
2497 DESCRIPTION => join( ' ',
2498 $self->loc($watcher_type),
2499 $args{'OPERATOR'}, $args{'VALUE'}, ),
2508 =head2 LimitLinkedTo
2510 LimitLinkedTo takes a paramhash with two fields: TYPE and TARGET
2511 TYPE limits the sort of link we want to search on
2513 TYPE = { RefersTo, MemberOf, DependsOn }
2515 TARGET is the id or URI of the TARGET of the link
2529 FIELD => 'LinkedTo',
2531 TARGET => $args{'TARGET'},
2532 TYPE => $args{'TYPE'},
2533 DESCRIPTION => $self->loc(
2534 "Tickets [_1] by [_2]",
2535 $self->loc( $args{'TYPE'} ),
2538 OPERATOR => $args{'OPERATOR'},
2544 =head2 LimitLinkedFrom
2546 LimitLinkedFrom takes a paramhash with two fields: TYPE and BASE
2547 TYPE limits the sort of link we want to search on
2550 BASE is the id or URI of the BASE of the link
2554 sub LimitLinkedFrom {
2563 # translate RT2 From/To naming to RT3 TicketSQL naming
2564 my %fromToMap = qw(DependsOn DependentOn
2566 RefersTo ReferredToBy);
2568 my $type = $args{'TYPE'};
2569 $type = $fromToMap{$type} if exists( $fromToMap{$type} );
2572 FIELD => 'LinkedTo',
2574 BASE => $args{'BASE'},
2576 DESCRIPTION => $self->loc(
2577 "Tickets [_1] [_2]",
2578 $self->loc( $args{'TYPE'} ),
2581 OPERATOR => $args{'OPERATOR'},
2588 my $ticket_id = shift;
2589 return $self->LimitLinkedTo(
2591 TARGET => $ticket_id,
2597 sub LimitHasMember {
2599 my $ticket_id = shift;
2600 return $self->LimitLinkedFrom(
2602 BASE => "$ticket_id",
2603 TYPE => 'HasMember',
2610 sub LimitDependsOn {
2612 my $ticket_id = shift;
2613 return $self->LimitLinkedTo(
2615 TARGET => $ticket_id,
2616 TYPE => 'DependsOn',
2623 sub LimitDependedOnBy {
2625 my $ticket_id = shift;
2626 return $self->LimitLinkedFrom(
2629 TYPE => 'DependentOn',
2638 my $ticket_id = shift;
2639 return $self->LimitLinkedTo(
2641 TARGET => $ticket_id,
2649 sub LimitReferredToBy {
2651 my $ticket_id = shift;
2652 return $self->LimitLinkedFrom(
2655 TYPE => 'ReferredToBy',
2663 =head2 LimitDate (FIELD => 'DateField', OPERATOR => $oper, VALUE => $ISODate)
2665 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
2667 OPERATOR is one of > or <
2668 VALUE is a date and time in ISO format in GMT
2669 FIELD is one of Starts, Started, Told, Created, Resolved, LastUpdated
2671 There are also helper functions of the form LimitFIELD that eliminate
2672 the need to pass in a FIELD argument.
2686 #Set the description if we didn't get handed it above
2687 unless ( $args{'DESCRIPTION'} ) {
2688 $args{'DESCRIPTION'} = $args{'FIELD'} . " "
2689 . $args{'OPERATOR'} . " "
2690 . $args{'VALUE'} . " GMT";
2693 $self->Limit(%args);
2700 $self->LimitDate( FIELD => 'Created', @_ );
2705 $self->LimitDate( FIELD => 'Due', @_ );
2711 $self->LimitDate( FIELD => 'Starts', @_ );
2717 $self->LimitDate( FIELD => 'Started', @_ );
2722 $self->LimitDate( FIELD => 'Resolved', @_ );
2727 $self->LimitDate( FIELD => 'Told', @_ );
2730 sub LimitLastUpdated {
2732 $self->LimitDate( FIELD => 'LastUpdated', @_ );
2737 =head2 LimitTransactionDate (OPERATOR => $oper, VALUE => $ISODate)
2739 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
2741 OPERATOR is one of > or <
2742 VALUE is a date and time in ISO format in GMT
2747 sub LimitTransactionDate {
2750 FIELD => 'TransactionDate',
2757 # <20021217042756.GK28744@pallas.fsck.com>
2758 # "Kill It" - Jesse.
2760 #Set the description if we didn't get handed it above
2761 unless ( $args{'DESCRIPTION'} ) {
2762 $args{'DESCRIPTION'} = $args{'FIELD'} . " "
2763 . $args{'OPERATOR'} . " "
2764 . $args{'VALUE'} . " GMT";
2767 $self->Limit(%args);
2774 =head2 LimitCustomField
2776 Takes a paramhash of key/value pairs with the following keys:
2780 =item CUSTOMFIELD - CustomField name or id. If a name is passed, an additional parameter QUEUE may also be passed to distinguish the custom field.
2782 =item OPERATOR - The usual Limit operators
2784 =item VALUE - The value to compare against
2790 sub LimitCustomField {
2794 CUSTOMFIELD => undef,
2796 DESCRIPTION => undef,
2797 FIELD => 'CustomFieldValue',
2802 my $CF = RT::CustomField->new( $self->CurrentUser );
2803 if ( $args{CUSTOMFIELD} =~ /^\d+$/ ) {
2804 $CF->Load( $args{CUSTOMFIELD} );
2807 $CF->LoadByNameAndQueue(
2808 Name => $args{CUSTOMFIELD},
2809 Queue => $args{QUEUE}
2811 $args{CUSTOMFIELD} = $CF->Id;
2814 #If we are looking to compare with a null value.
2815 if ( $args{'OPERATOR'} =~ /^is$/i ) {
2816 $args{'DESCRIPTION'}
2817 ||= $self->loc( "Custom field [_1] has no value.", $CF->Name );
2819 elsif ( $args{'OPERATOR'} =~ /^is not$/i ) {
2820 $args{'DESCRIPTION'}
2821 ||= $self->loc( "Custom field [_1] has a value.", $CF->Name );
2824 # if we're not looking to compare with a null value
2826 $args{'DESCRIPTION'} ||= $self->loc( "Custom field [_1] [_2] [_3]",
2827 $CF->Name, $args{OPERATOR}, $args{VALUE} );
2830 if ( defined $args{'QUEUE'} && $args{'QUEUE'} =~ /\D/ ) {
2831 my $QueueObj = RT::Queue->new( $self->CurrentUser );
2832 $QueueObj->Load( $args{'QUEUE'} );
2833 $args{'QUEUE'} = $QueueObj->Id;
2835 delete $args{'QUEUE'} unless defined $args{'QUEUE'} && length $args{'QUEUE'};
2838 @rest = ( ENTRYAGGREGATOR => 'AND' )
2839 if ( $CF->Type eq 'SelectMultiple' );
2842 VALUE => $args{VALUE},
2844 .(defined $args{'QUEUE'}? ".{$args{'QUEUE'}}" : '' )
2845 .".{" . $CF->Name . "}",
2846 OPERATOR => $args{OPERATOR},
2851 $self->{'RecalcTicketLimits'} = 1;
2858 Keep track of the counter for the array of restrictions
2864 return ( $self->{'restriction_index'}++ );
2872 $self->{'table'} = "Tickets";
2873 $self->{'RecalcTicketLimits'} = 1;
2874 $self->{'looking_at_effective_id'} = 0;
2875 $self->{'looking_at_type'} = 0;
2876 $self->{'restriction_index'} = 1;
2877 $self->{'primary_key'} = "id";
2878 delete $self->{'items_array'};
2879 delete $self->{'item_map'};
2880 delete $self->{'columns_to_display'};
2881 $self->SUPER::_Init(@_);
2890 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2891 return ( $self->SUPER::Count() );
2897 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2898 return ( $self->SUPER::CountAll() );
2903 =head2 ItemsArrayRef
2905 Returns a reference to the set of all items found in this search
2912 return $self->{'items_array'} if $self->{'items_array'};
2914 my $placeholder = $self->_ItemsCounter;
2915 $self->GotoFirstItem();
2916 while ( my $item = $self->Next ) {
2917 push( @{ $self->{'items_array'} }, $item );
2919 $self->GotoItem($placeholder);
2920 $self->{'items_array'}
2921 = $self->ItemsOrderBy( $self->{'items_array'} );
2923 return $self->{'items_array'};
2926 sub ItemsArrayRefWindow {
2930 my @old = ($self->_ItemsCounter, $self->RowsPerPage, $self->FirstRow+1);
2932 $self->RowsPerPage( $window );
2934 $self->GotoFirstItem;
2937 while ( my $item = $self->Next ) {
2941 $self->RowsPerPage( $old[1] );
2942 $self->FirstRow( $old[2] );
2943 $self->GotoItem( $old[0] );
2952 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2954 my $Ticket = $self->SUPER::Next;
2955 return $Ticket unless $Ticket;
2957 if ( $Ticket->__Value('Status') eq 'deleted'
2958 && !$self->{'allow_deleted_search'} )
2962 elsif ( RT->Config->Get('UseSQLForACLChecks') ) {
2963 # if we found a ticket with this option enabled then
2964 # all tickets we found are ACLed, cache this fact
2965 my $key = join ";:;", $self->CurrentUser->id, 'ShowTicket', 'RT::Ticket-'. $Ticket->id;
2966 $RT::Principal::_ACL_CACHE->set( $key => 1 );
2969 elsif ( $Ticket->CurrentUserHasRight('ShowTicket') ) {
2974 # If the user doesn't have the right to show this ticket
2981 $self->CurrentUserCanSee if RT->Config->Get('UseSQLForACLChecks');
2982 return $self->SUPER::_DoSearch( @_ );
2987 $self->CurrentUserCanSee if RT->Config->Get('UseSQLForACLChecks');
2988 return $self->SUPER::_DoCount( @_ );
2994 my $cache_key = 'RolesHasRight;:;ShowTicket';
2996 if ( my $cached = $RT::Principal::_ACL_CACHE->fetch( $cache_key ) ) {
3000 my $ACL = RT::ACL->new( RT->SystemUser );
3001 $ACL->Limit( FIELD => 'RightName', VALUE => 'ShowTicket' );
3002 $ACL->Limit( FIELD => 'PrincipalType', OPERATOR => '!=', VALUE => 'Group' );
3003 my $principal_alias = $ACL->Join(
3005 FIELD1 => 'PrincipalId',
3006 TABLE2 => 'Principals',
3009 $ACL->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3012 foreach my $ACE ( @{ $ACL->ItemsArrayRef } ) {
3013 my $role = $ACE->__Value('PrincipalType');
3014 my $type = $ACE->__Value('ObjectType');
3015 if ( $type eq 'RT::System' ) {
3018 elsif ( $type eq 'RT::Queue' ) {
3019 next if $res{ $role } && !ref $res{ $role };
3020 push @{ $res{ $role } ||= [] }, $ACE->__Value('ObjectId');
3023 $RT::Logger->error('ShowTicket right is granted on unsupported object');
3026 $RT::Principal::_ACL_CACHE->set( $cache_key => \%res );
3030 sub _DirectlyCanSeeIn {
3032 my $id = $self->CurrentUser->id;
3034 my $cache_key = 'User-'. $id .';:;ShowTicket;:;DirectlyCanSeeIn';
3035 if ( my $cached = $RT::Principal::_ACL_CACHE->fetch( $cache_key ) ) {
3039 my $ACL = RT::ACL->new( RT->SystemUser );
3040 $ACL->Limit( FIELD => 'RightName', VALUE => 'ShowTicket' );
3041 my $principal_alias = $ACL->Join(
3043 FIELD1 => 'PrincipalId',
3044 TABLE2 => 'Principals',
3047 $ACL->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3048 my $cgm_alias = $ACL->Join(
3050 FIELD1 => 'PrincipalId',
3051 TABLE2 => 'CachedGroupMembers',
3052 FIELD2 => 'GroupId',
3054 $ACL->Limit( ALIAS => $cgm_alias, FIELD => 'MemberId', VALUE => $id );
3055 $ACL->Limit( ALIAS => $cgm_alias, FIELD => 'Disabled', VALUE => 0 );
3058 foreach my $ACE ( @{ $ACL->ItemsArrayRef } ) {
3059 my $type = $ACE->__Value('ObjectType');
3060 if ( $type eq 'RT::System' ) {
3061 # If user is direct member of a group that has the right
3062 # on the system then he can see any ticket
3063 $RT::Principal::_ACL_CACHE->set( $cache_key => [-1] );
3066 elsif ( $type eq 'RT::Queue' ) {
3067 push @res, $ACE->__Value('ObjectId');
3070 $RT::Logger->error('ShowTicket right is granted on unsupported object');
3073 $RT::Principal::_ACL_CACHE->set( $cache_key => \@res );
3077 sub CurrentUserCanSee {
3079 return if $self->{'_sql_current_user_can_see_applied'};
3081 return $self->{'_sql_current_user_can_see_applied'} = 1
3082 if $self->CurrentUser->UserObj->HasRight(
3083 Right => 'SuperUser', Object => $RT::System
3086 my $id = $self->CurrentUser->id;
3088 # directly can see in all queues then we have nothing to do
3089 my @direct_queues = $self->_DirectlyCanSeeIn;
3090 return $self->{'_sql_current_user_can_see_applied'} = 1
3091 if @direct_queues && $direct_queues[0] == -1;
3093 my %roles = $self->_RolesCanSee;
3095 my %skip = map { $_ => 1 } @direct_queues;
3096 foreach my $role ( keys %roles ) {
3097 next unless ref $roles{ $role };
3099 my @queues = grep !$skip{$_}, @{ $roles{ $role } };
3101 $roles{ $role } = \@queues;
3103 delete $roles{ $role };
3108 # there is no global watchers, only queues and tickes, if at
3109 # some point we will add global roles then it's gonna blow
3110 # the idea here is that if the right is set globaly for a role
3111 # and user plays this role for a queue directly not a ticket
3112 # then we have to check in advance
3113 if ( my @tmp = grep $_ ne 'Owner' && !ref $roles{ $_ }, keys %roles ) {
3115 my $groups = RT::Groups->new( RT->SystemUser );
3116 $groups->Limit( FIELD => 'Domain', VALUE => 'RT::Queue-Role' );
3118 $groups->Limit( FIELD => 'Type', VALUE => $_ );
3120 my $principal_alias = $groups->Join(
3123 TABLE2 => 'Principals',
3126 $groups->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3127 my $cgm_alias = $groups->Join(
3130 TABLE2 => 'CachedGroupMembers',
3131 FIELD2 => 'GroupId',
3133 $groups->Limit( ALIAS => $cgm_alias, FIELD => 'MemberId', VALUE => $id );
3134 $groups->Limit( ALIAS => $cgm_alias, FIELD => 'Disabled', VALUE => 0 );
3135 while ( my $group = $groups->Next ) {
3136 push @direct_queues, $group->Instance;
3140 unless ( @direct_queues || keys %roles ) {
3141 $self->SUPER::Limit(
3146 ENTRYAGGREGATOR => 'AND',
3148 return $self->{'_sql_current_user_can_see_applied'} = 1;
3152 my $join_roles = keys %roles;
3153 $join_roles = 0 if $join_roles == 1 && $roles{'Owner'};
3154 my ($role_group_alias, $cgm_alias);
3155 if ( $join_roles ) {
3156 $role_group_alias = $self->_RoleGroupsJoin( New => 1 );
3157 $cgm_alias = $self->_GroupMembersJoin( GroupsAlias => $role_group_alias );
3158 $self->SUPER::Limit(
3159 LEFTJOIN => $cgm_alias,
3160 FIELD => 'MemberId',
3165 my $limit_queues = sub {
3169 return unless @queues;
3170 if ( @queues == 1 ) {
3171 $self->SUPER::Limit(
3176 ENTRYAGGREGATOR => $ea,
3179 $self->SUPER::_OpenParen('ACL');
3180 foreach my $q ( @queues ) {
3181 $self->SUPER::Limit(
3186 ENTRYAGGREGATOR => $ea,
3190 $self->SUPER::_CloseParen('ACL');
3195 $self->SUPER::_OpenParen('ACL');
3197 $ea = 'OR' if $limit_queues->( $ea, @direct_queues );
3198 while ( my ($role, $queues) = each %roles ) {
3199 $self->SUPER::_OpenParen('ACL');
3200 if ( $role eq 'Owner' ) {
3201 $self->SUPER::Limit(
3205 ENTRYAGGREGATOR => $ea,
3209 $self->SUPER::Limit(
3211 ALIAS => $cgm_alias,
3212 FIELD => 'MemberId',
3213 OPERATOR => 'IS NOT',
3216 ENTRYAGGREGATOR => $ea,
3218 $self->SUPER::Limit(
3220 ALIAS => $role_group_alias,
3223 ENTRYAGGREGATOR => 'AND',
3226 $limit_queues->( 'AND', @$queues ) if ref $queues;
3227 $ea = 'OR' if $ea eq 'AND';
3228 $self->SUPER::_CloseParen('ACL');
3230 $self->SUPER::_CloseParen('ACL');
3232 return $self->{'_sql_current_user_can_see_applied'} = 1;
3239 =head2 LoadRestrictions
3241 LoadRestrictions takes a string which can fully populate the TicketRestrictons hash.
3242 TODO It is not yet implemented
3248 =head2 DescribeRestrictions
3251 Returns a hash keyed by restriction id.
3252 Each element of the hash is currently a one element hash that contains DESCRIPTION which
3253 is a description of the purpose of that TicketRestriction
3257 sub DescribeRestrictions {
3262 foreach my $row ( keys %{ $self->{'TicketRestrictions'} } ) {
3263 $listing{$row} = $self->{'TicketRestrictions'}{$row}{'DESCRIPTION'};
3270 =head2 RestrictionValues FIELD
3272 Takes a restriction field and returns a list of values this field is restricted
3277 sub RestrictionValues {
3280 map $self->{'TicketRestrictions'}{$_}{'VALUE'}, grep {
3281 $self->{'TicketRestrictions'}{$_}{'FIELD'} eq $field
3282 && $self->{'TicketRestrictions'}{$_}{'OPERATOR'} eq "="
3284 keys %{ $self->{'TicketRestrictions'} };
3289 =head2 ClearRestrictions
3291 Removes all restrictions irretrievably
3295 sub ClearRestrictions {
3297 delete $self->{'TicketRestrictions'};
3298 $self->{'looking_at_effective_id'} = 0;
3299 $self->{'looking_at_type'} = 0;
3300 $self->{'RecalcTicketLimits'} = 1;
3305 =head2 DeleteRestriction
3307 Takes the row Id of a restriction (From DescribeRestrictions' output, for example.
3308 Removes that restriction from the session's limits.
3312 sub DeleteRestriction {
3315 delete $self->{'TicketRestrictions'}{$row};
3317 $self->{'RecalcTicketLimits'} = 1;
3319 #make the underlying easysearch object forget all its preconceptions
3324 # Convert a set of oldstyle SB Restrictions to Clauses for RQL
3326 sub _RestrictionsToClauses {
3330 foreach my $row ( keys %{ $self->{'TicketRestrictions'} } ) {
3331 my $restriction = $self->{'TicketRestrictions'}{$row};
3333 # We need to reimplement the subclause aggregation that SearchBuilder does.
3334 # Default Subclause is ALIAS.FIELD, and default ALIAS is 'main',
3335 # Then SB AND's the different Subclauses together.
3337 # So, we want to group things into Subclauses, convert them to
3338 # SQL, and then join them with the appropriate DefaultEA.
3339 # Then join each subclause group with AND.
3341 my $field = $restriction->{'FIELD'};
3342 my $realfield = $field; # CustomFields fake up a fieldname, so
3343 # we need to figure that out
3346 # Rewrite LinkedTo meta field to the real field
3347 if ( $field =~ /LinkedTo/ ) {
3348 $realfield = $field = $restriction->{'TYPE'};
3352 # Handle subkey fields with a different real field
3353 if ( $field =~ /^(\w+)\./ ) {
3357 die "I don't know about $field yet"
3358 unless ( exists $FIELD_METADATA{$realfield}
3359 or $restriction->{CUSTOMFIELD} );
3361 my $type = $FIELD_METADATA{$realfield}->[0];
3362 my $op = $restriction->{'OPERATOR'};
3366 map { $restriction->{$_} } qw(VALUE TICKET BASE TARGET)
3369 # this performs the moral equivalent of defined or/dor/C<//>,
3370 # without the short circuiting.You need to use a 'defined or'
3371 # type thing instead of just checking for truth values, because
3372 # VALUE could be 0.(i.e. "false")
3374 # You could also use this, but I find it less aesthetic:
3375 # (although it does short circuit)
3376 #( defined $restriction->{'VALUE'}? $restriction->{VALUE} :
3377 # defined $restriction->{'TICKET'} ?
3378 # $restriction->{TICKET} :
3379 # defined $restriction->{'BASE'} ?
3380 # $restriction->{BASE} :
3381 # defined $restriction->{'TARGET'} ?
3382 # $restriction->{TARGET} )
3384 my $ea = $restriction->{ENTRYAGGREGATOR}
3385 || $DefaultEA{$type}
3388 die "Invalid operator $op for $field ($type)"
3389 unless exists $ea->{$op};
3393 # Each CustomField should be put into a different Clause so they
3394 # are ANDed together.
3395 if ( $restriction->{CUSTOMFIELD} ) {
3396 $realfield = $field;
3399 exists $clause{$realfield} or $clause{$realfield} = [];
3402 $field =~ s!(['\\])!\\$1!g;
3403 $value =~ s!(['\\])!\\$1!g;
3404 my $data = [ $ea, $type, $field, $op, $value ];
3406 # here is where we store extra data, say if it's a keyword or
3407 # something. (I.e. "TYPE SPECIFIC STUFF")
3409 if (lc $ea eq 'none') {
3410 $clause{$realfield} = [ $data ];
3412 push @{ $clause{$realfield} }, $data;
3420 =head2 _ProcessRestrictions PARAMHASH
3422 # The new _ProcessRestrictions is somewhat dependent on the SQL stuff,
3423 # but isn't quite generic enough to move into Tickets_SQL.
3427 sub _ProcessRestrictions {
3430 #Blow away ticket aliases since we'll need to regenerate them for
3432 delete $self->{'TicketAliases'};
3433 delete $self->{'items_array'};
3434 delete $self->{'item_map'};
3435 delete $self->{'raw_rows'};
3436 delete $self->{'rows'};
3437 delete $self->{'count_all'};
3439 my $sql = $self->Query; # Violating the _SQL namespace
3440 if ( !$sql || $self->{'RecalcTicketLimits'} ) {
3442 # "Restrictions to Clauses Branch\n";
3443 my $clauseRef = eval { $self->_RestrictionsToClauses; };
3445 $RT::Logger->error( "RestrictionsToClauses: " . $@ );
3449 $sql = $self->ClausesToSQL($clauseRef);
3450 $self->FromSQL($sql) if $sql;
3454 $self->{'RecalcTicketLimits'} = 0;
3458 =head2 _BuildItemMap
3460 Build up a L</ItemMap> of first/last/next/prev items, so that we can
3461 display search nav quickly.
3468 my $window = RT->Config->Get('TicketsItemMapSize');
3470 $self->{'item_map'} = {};
3472 my $items = $self->ItemsArrayRefWindow( $window );
3473 return unless $items && @$items;
3476 $self->{'item_map'}{'first'} = $items->[0]->EffectiveId;
3477 for ( my $i = 0; $i < @$items; $i++ ) {
3478 my $item = $items->[$i];
3479 my $id = $item->EffectiveId;
3480 $self->{'item_map'}{$id}{'defined'} = 1;
3481 $self->{'item_map'}{$id}{'prev'} = $prev;
3482 $self->{'item_map'}{$id}{'next'} = $items->[$i+1]->EffectiveId
3486 $self->{'item_map'}{'last'} = $prev
3487 if !$window || @$items < $window;
3492 Returns an a map of all items found by this search. The map is a hash
3496 first => <first ticket id found>,
3497 last => <last ticket id found or undef>,
3500 prev => <the ticket id found before>,
3501 next => <the ticket id found after>,
3513 $self->_BuildItemMap unless $self->{'item_map'};
3514 return $self->{'item_map'};
3520 =head2 PrepForSerialization
3522 You don't want to serialize a big tickets object, as
3523 the {items} hash will be instantly invalid _and_ eat
3528 sub PrepForSerialization {
3530 delete $self->{'items'};
3531 delete $self->{'items_array'};
3532 $self->RedoSearch();
3537 RT::Tickets supports several flags which alter search behavior:
3540 allow_deleted_search (Otherwise never show deleted tickets in search results)
3541 looking_at_type (otherwise limit to type=ticket)
3543 These flags are set by calling
3545 $tickets->{'flagname'} = 1;
3547 BUG: There should be an API for this
3557 Returns an empty new RT::Ticket item
3563 return(RT::Ticket->new($self->CurrentUser));
3565 RT::Base->_ImportOverlays();