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;
434 my $direction = $meta->[1] || '';
435 my ($matchfield, $linkfield) = ('', '');
436 if ( $direction eq 'To' ) {
437 ($matchfield, $linkfield) = ("Target", "Base");
439 elsif ( $direction eq 'From' ) {
440 ($matchfield, $linkfield) = ("Base", "Target");
442 elsif ( $direction ) {
443 die "Invalid link direction '$direction' for $field\n";
446 $sb->_LinkLimit( 'LinkedTo', $op, $value, @rest );
448 'LinkedFrom', $op, $value, @rest,
449 ENTRYAGGREGATOR => (($is_negative && $is_null) || (!$is_null && !$is_negative))? 'OR': 'AND',
457 $op = ($op =~ /^(=|IS)$/)? 'IS': 'IS NOT';
459 elsif ( $value =~ /\D/ ) {
462 $matchfield = "Local$matchfield" if $is_local;
464 #For doing a left join to find "unlinked tickets" we want to generate a query that looks like this
465 # SELECT main.* FROM Tickets main
466 # LEFT JOIN Links Links_1 ON ( (Links_1.Type = 'MemberOf')
467 # AND(main.id = Links_1.LocalTarget))
468 # WHERE Links_1.LocalBase IS NULL;
471 my $linkalias = $sb->Join(
476 FIELD2 => 'Local' . $linkfield
479 LEFTJOIN => $linkalias,
487 FIELD => $matchfield,
494 my $linkalias = $sb->Join(
499 FIELD2 => 'Local' . $linkfield
502 LEFTJOIN => $linkalias,
508 LEFTJOIN => $linkalias,
509 FIELD => $matchfield,
516 FIELD => $matchfield,
517 OPERATOR => $is_negative? 'IS': 'IS NOT',
526 Handle date fields. (Created, LastTold..)
529 1: type of link. (Probably not necessary.)
534 my ( $sb, $field, $op, $value, @rest ) = @_;
536 die "Invalid Date Op: $op"
537 unless $op =~ /^(=|>|<|>=|<=)$/;
539 my $meta = $FIELD_METADATA{$field};
540 die "Incorrect Meta Data for $field"
541 unless ( defined $meta->[1] );
543 my $date = RT::Date->new( $sb->CurrentUser );
544 $date->Set( Format => 'unknown', Value => $value );
548 # if we're specifying =, that means we want everything on a
549 # particular single day. in the database, we need to check for >
550 # and < the edges of that day.
552 $date->SetToMidnight( Timezone => 'server' );
553 my $daystart = $date->ISO;
555 my $dayend = $date->ISO;
571 ENTRYAGGREGATOR => 'AND',
589 Handle simple fields which are just strings. (Subject,Type)
597 my ( $sb, $field, $op, $value, @rest ) = @_;
601 # =, !=, LIKE, NOT LIKE
602 if ( RT->Config->Get('DatabaseType') eq 'Oracle'
603 && (!defined $value || !length $value)
604 && lc($op) ne 'is' && lc($op) ne 'is not'
606 if ($op eq '!=' || $op =~ /^NOT\s/i) {
623 =head2 _TransDateLimit
625 Handle fields limiting based on Transaction Date.
627 The inpupt value must be in a format parseable by Time::ParseDate
634 # This routine should really be factored into translimit.
635 sub _TransDateLimit {
636 my ( $sb, $field, $op, $value, @rest ) = @_;
638 # See the comments for TransLimit, they apply here too
640 my $txn_alias = $sb->JoinTransactions;
642 my $date = RT::Date->new( $sb->CurrentUser );
643 $date->Set( Format => 'unknown', Value => $value );
648 # if we're specifying =, that means we want everything on a
649 # particular single day. in the database, we need to check for >
650 # and < the edges of that day.
652 $date->SetToMidnight( Timezone => 'server' );
653 my $daystart = $date->ISO;
655 my $dayend = $date->ISO;
670 ENTRYAGGREGATOR => 'AND',
675 # not searching for a single day
678 #Search for the right field
693 Limit based on the ContentType or the Filename of a transaction.
698 my ( $self, $field, $op, $value, %rest ) = @_;
700 my $txn_alias = $self->JoinTransactions;
701 unless ( defined $self->{_sql_trattachalias} ) {
702 $self->{_sql_trattachalias} = $self->_SQLJoin(
703 TYPE => 'LEFT', # not all txns have an attachment
704 ALIAS1 => $txn_alias,
706 TABLE2 => 'Attachments',
707 FIELD2 => 'TransactionId',
713 ALIAS => $self->{_sql_trattachalias},
721 =head2 _TransContentLimit
723 Limit based on the Content of a transaction.
727 sub _TransContentLimit {
731 # If only this was this simple. We've got to do something
734 #Basically, we want to make sure that the limits apply to
735 #the same attachment, rather than just another attachment
736 #for the same ticket, no matter how many clauses we lump
737 #on. We put them in TicketAliases so that they get nuked
738 #when we redo the join.
740 # In the SQL, we might have
741 # (( Content = foo ) or ( Content = bar AND Content = baz ))
742 # The AND group should share the same Alias.
744 # Actually, maybe it doesn't matter. We use the same alias and it
745 # works itself out? (er.. different.)
747 # Steal more from _ProcessRestrictions
749 # FIXME: Maybe look at the previous FooLimit call, and if it was a
750 # TransLimit and EntryAggregator == AND, reuse the Aliases?
752 # Or better - store the aliases on a per subclause basis - since
753 # those are going to be the things we want to relate to each other,
756 # maybe we should not allow certain kinds of aggregation of these
757 # clauses and do a psuedo regex instead? - the problem is getting
758 # them all into the same subclause when you have (A op B op C) - the
759 # way they get parsed in the tree they're in different subclauses.
761 my ( $self, $field, $op, $value, %rest ) = @_;
762 $field = 'Content' if $field =~ /\W/;
764 my $config = RT->Config->Get('FullTextSearch') || {};
765 unless ( $config->{'Enable'} ) {
766 $self->_SQLLimit( %rest, FIELD => 'id', VALUE => 0 );
770 my $txn_alias = $self->JoinTransactions;
771 unless ( defined $self->{_sql_trattachalias} ) {
772 $self->{_sql_trattachalias} = $self->_SQLJoin(
773 TYPE => 'LEFT', # not all txns have an attachment
774 ALIAS1 => $txn_alias,
776 TABLE2 => 'Attachments',
777 FIELD2 => 'TransactionId',
782 if ( $config->{'Indexed'} ) {
783 my $db_type = RT->Config->Get('DatabaseType');
786 if ( $config->{'Table'} and $config->{'Table'} ne "Attachments") {
787 $alias = $self->{'_sql_aliases'}{'full_text'} ||= $self->_SQLJoin(
789 ALIAS1 => $self->{'_sql_trattachalias'},
791 TABLE2 => $config->{'Table'},
795 $alias = $self->{'_sql_trattachalias'};
798 #XXX: handle negative searches
799 my $index = $config->{'Column'};
800 if ( $db_type eq 'Oracle' ) {
801 my $dbh = $RT::Handle->dbh;
802 my $alias = $self->{_sql_trattachalias};
805 FUNCTION => "CONTAINS( $alias.$field, ".$dbh->quote($value) .")",
811 # this is required to trick DBIx::SB's LEFT JOINS optimizer
812 # into deciding that join is redundant as it is
814 ENTRYAGGREGATOR => 'AND',
815 ALIAS => $self->{_sql_trattachalias},
817 OPERATOR => 'IS NOT',
821 elsif ( $db_type eq 'Pg' ) {
822 my $dbh = $RT::Handle->dbh;
828 VALUE => 'plainto_tsquery('. $dbh->quote($value) .')',
832 elsif ( $db_type eq 'mysql' ) {
833 # XXX: We could theoretically skip the join to Attachments,
834 # and have Sphinx simply index and group by the TicketId,
835 # and join Ticket.id to that attribute, which would be much
836 # more efficient -- however, this is only a possibility if
837 # there are no other transaction limits.
839 # This is a special character. Note that \ does not escape
840 # itself (in Sphinx 2.1.0, at least), so 'foo\;bar' becoming
841 # 'foo\\;bar' is not a vulnerability, and is still parsed as
842 # "foo, \, ;, then bar". Happily, the default mode is
843 # "all", meaning that boolean operators are not special.
846 my $max = $config->{'MaxMatches'};
852 VALUE => "$value;limit=$max;maxmatches=$max",
858 ALIAS => $self->{_sql_trattachalias},
865 if ( RT->Config->Get('DontSearchFileAttachments') ) {
867 ENTRYAGGREGATOR => 'AND',
868 ALIAS => $self->{_sql_trattachalias},
879 Handle watcher limits. (Requestor, CC, etc..)
895 my $meta = $FIELD_METADATA{ $field };
896 my $type = $meta->[1] || '';
897 my $class = $meta->[2] || 'Ticket';
899 # Bail if the subfield is not allowed
901 and not grep { $_ eq $rest{SUBKEY} } @{$SEARCHABLE_SUBFIELDS{'User'}})
903 die "Invalid watcher subfield: '$rest{SUBKEY}'";
906 # Owner was ENUM field, so "Owner = 'xxx'" allowed user to
907 # search by id and Name at the same time, this is workaround
908 # to preserve backward compatibility
909 if ( $field eq 'Owner' ) {
910 if ( $op =~ /^!?=$/ && (!$rest{'SUBKEY'} || $rest{'SUBKEY'} eq 'Name' || $rest{'SUBKEY'} eq 'EmailAddress') ) {
911 my $o = RT::User->new( $self->CurrentUser );
912 my $method = ($rest{'SUBKEY'}||'') eq 'EmailAddress' ? 'LoadByEmail': 'Load';
913 $o->$method( $value );
922 if ( ($rest{'SUBKEY'}||'') eq 'id' ) {
932 $rest{SUBKEY} ||= 'EmailAddress';
934 my $groups = $self->_RoleGroupsJoin( Type => $type, Class => $class, New => !$type );
937 if ( $op =~ /^IS(?: NOT)?$/ ) {
938 # is [not] empty case
940 my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
941 # to avoid joining the table Users into the query, we just join GM
942 # and make sure we don't match records where group is member of itself
944 LEFTJOIN => $group_members,
947 VALUE => "$group_members.MemberId",
951 ALIAS => $group_members,
958 elsif ( $op =~ /^!=$|^NOT\s+/i ) {
959 # negative condition case
962 $op =~ s/!|NOT\s+//i;
964 # XXX: we have no way to build correct "Watcher.X != 'Y'" when condition
965 # "X = 'Y'" matches more then one user so we try to fetch two records and
966 # do the right thing when there is only one exist and semi-working solution
968 my $users_obj = RT::Users->new( $self->CurrentUser );
970 FIELD => $rest{SUBKEY},
975 $users_obj->RowsPerPage(2);
976 my @users = @{ $users_obj->ItemsArrayRef };
978 my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
981 $uid = $users[0]->id if @users;
983 LEFTJOIN => $group_members,
984 ALIAS => $group_members,
990 ALIAS => $group_members,
997 LEFTJOIN => $group_members,
1000 VALUE => "$group_members.MemberId",
1003 my $users = $self->Join(
1005 ALIAS1 => $group_members,
1006 FIELD1 => 'MemberId',
1010 $self->SUPER::Limit(
1013 FIELD => $rest{SUBKEY},
1027 # positive condition case
1029 my $group_members = $self->_GroupMembersJoin(
1030 GroupsAlias => $groups, New => 1, Left => 0
1032 my $users = $self->Join(
1034 ALIAS1 => $group_members,
1035 FIELD1 => 'MemberId',
1042 FIELD => $rest{'SUBKEY'},
1051 sub _RoleGroupsJoin {
1053 my %args = (New => 0, Class => 'Ticket', Type => '', @_);
1054 return $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $args{'Type'} }
1055 if $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $args{'Type'} }
1058 # we always have watcher groups for ticket, so we use INNER join
1059 my $groups = $self->Join(
1061 FIELD1 => $args{'Class'} eq 'Queue'? 'Queue': 'id',
1063 FIELD2 => 'Instance',
1064 ENTRYAGGREGATOR => 'AND',
1066 $self->SUPER::Limit(
1067 LEFTJOIN => $groups,
1070 VALUE => 'RT::'. $args{'Class'} .'-Role',
1072 $self->SUPER::Limit(
1073 LEFTJOIN => $groups,
1076 VALUE => $args{'Type'},
1079 $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $args{'Type'} } = $groups
1080 unless $args{'New'};
1085 sub _GroupMembersJoin {
1087 my %args = (New => 1, GroupsAlias => undef, Left => 1, @_);
1089 return $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} }
1090 if $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} }
1093 my $alias = $self->Join(
1094 $args{'Left'} ? (TYPE => 'LEFT') : (),
1095 ALIAS1 => $args{'GroupsAlias'},
1097 TABLE2 => 'CachedGroupMembers',
1098 FIELD2 => 'GroupId',
1099 ENTRYAGGREGATOR => 'AND',
1101 $self->SUPER::Limit(
1102 $args{'Left'} ? (LEFTJOIN => $alias) : (),
1104 FIELD => 'Disabled',
1108 $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} } = $alias
1109 unless $args{'New'};
1116 Helper function which provides joins to a watchers table both for limits
1123 my $type = shift || '';
1126 my $groups = $self->_RoleGroupsJoin( Type => $type );
1127 my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
1128 # XXX: work around, we must hide groups that
1129 # are members of the role group we search in,
1130 # otherwise them result in wrong NULLs in Users
1131 # table and break ordering. Now, we know that
1132 # RT doesn't allow to add groups as members of the
1133 # ticket roles, so we just hide entries in CGM table
1134 # with MemberId == GroupId from results
1135 $self->SUPER::Limit(
1136 LEFTJOIN => $group_members,
1139 VALUE => "$group_members.MemberId",
1142 my $users = $self->Join(
1144 ALIAS1 => $group_members,
1145 FIELD1 => 'MemberId',
1149 return ($groups, $group_members, $users);
1152 =head2 _WatcherMembershipLimit
1154 Handle watcher membership limits, i.e. whether the watcher belongs to a
1155 specific group or not.
1158 1: Field to query on
1160 SELECT DISTINCT main.*
1164 CachedGroupMembers CachedGroupMembers_2,
1167 (main.EffectiveId = main.id)
1169 (main.Status != 'deleted')
1171 (main.Type = 'ticket')
1174 (Users_3.EmailAddress = '22')
1176 (Groups_1.Domain = 'RT::Ticket-Role')
1178 (Groups_1.Type = 'RequestorGroup')
1181 Groups_1.Instance = main.id
1183 Groups_1.id = CachedGroupMembers_2.GroupId
1185 CachedGroupMembers_2.MemberId = Users_3.id
1186 ORDER BY main.id ASC
1191 sub _WatcherMembershipLimit {
1192 my ( $self, $field, $op, $value, @rest ) = @_;
1197 my $groups = $self->NewAlias('Groups');
1198 my $groupmembers = $self->NewAlias('CachedGroupMembers');
1199 my $users = $self->NewAlias('Users');
1200 my $memberships = $self->NewAlias('CachedGroupMembers');
1202 if ( ref $field ) { # gross hack
1203 my @bundle = @$field;
1205 for my $chunk (@bundle) {
1206 ( $field, $op, $value, @rest ) = @$chunk;
1208 ALIAS => $memberships,
1219 ALIAS => $memberships,
1227 # Tie to groups for tickets we care about
1231 VALUE => 'RT::Ticket-Role',
1232 ENTRYAGGREGATOR => 'AND'
1237 FIELD1 => 'Instance',
1244 # If we care about which sort of watcher
1245 my $meta = $FIELD_METADATA{$field};
1246 my $type = ( defined $meta->[1] ? $meta->[1] : undef );
1253 ENTRYAGGREGATOR => 'AND'
1260 ALIAS2 => $groupmembers,
1265 ALIAS1 => $groupmembers,
1266 FIELD1 => 'MemberId',
1272 ALIAS => $groupmembers,
1273 FIELD => 'Disabled',
1278 ALIAS1 => $memberships,
1279 FIELD1 => 'MemberId',
1285 ALIAS => $memberships,
1286 FIELD => 'Disabled',
1295 =head2 _CustomFieldDecipher
1297 Try and turn a CF descriptor into (cfid, cfname) object pair.
1301 sub _CustomFieldDecipher {
1302 my ($self, $string) = @_;
1304 my ($queue, $field, $column) = ($string =~ /^(?:(.+?)\.)?{(.+)}(?:\.(Content|LargeContent))?$/);
1305 $field ||= ($string =~ /^{(.*?)}$/)[0] || $string;
1309 my $q = RT::Queue->new( $self->CurrentUser );
1313 # $queue = $q->Name; # should we normalize the queue?
1314 $cf = $q->CustomField( $field );
1317 $RT::Logger->warning("Queue '$queue' doesn't exist, parsed from '$string'");
1321 elsif ( $field =~ /\D/ ) {
1323 my $cfs = RT::CustomFields->new( $self->CurrentUser );
1324 $cfs->Limit( FIELD => 'Name', VALUE => $field );
1325 $cfs->LimitToLookupType('RT::Queue-RT::Ticket');
1327 # if there is more then one field the current user can
1328 # see with the same name then we shouldn't return cf object
1329 # as we don't know which one to use
1332 $cf = undef if $cfs->Next;
1336 $cf = RT::CustomField->new( $self->CurrentUser );
1337 $cf->Load( $field );
1340 return ($queue, $field, $cf, $column);
1343 =head2 _CustomFieldJoin
1345 Factor out the Join of custom fields so we can use it for sorting too
1349 sub _CustomFieldJoin {
1350 my ($self, $cfkey, $cfid, $field) = @_;
1351 # Perform one Join per CustomField
1352 if ( $self->{_sql_object_cfv_alias}{$cfkey} ||
1353 $self->{_sql_cf_alias}{$cfkey} )
1355 return ( $self->{_sql_object_cfv_alias}{$cfkey},
1356 $self->{_sql_cf_alias}{$cfkey} );
1359 my ($TicketCFs, $CFs);
1361 $TicketCFs = $self->{_sql_object_cfv_alias}{$cfkey} = $self->Join(
1365 TABLE2 => 'ObjectCustomFieldValues',
1366 FIELD2 => 'ObjectId',
1368 $self->SUPER::Limit(
1369 LEFTJOIN => $TicketCFs,
1370 FIELD => 'CustomField',
1372 ENTRYAGGREGATOR => 'AND'
1376 my $ocfalias = $self->Join(
1379 TABLE2 => 'ObjectCustomFields',
1380 FIELD2 => 'ObjectId',
1383 $self->SUPER::Limit(
1384 LEFTJOIN => $ocfalias,
1385 ENTRYAGGREGATOR => 'OR',
1386 FIELD => 'ObjectId',
1390 $CFs = $self->{_sql_cf_alias}{$cfkey} = $self->Join(
1392 ALIAS1 => $ocfalias,
1393 FIELD1 => 'CustomField',
1394 TABLE2 => 'CustomFields',
1397 $self->SUPER::Limit(
1399 ENTRYAGGREGATOR => 'AND',
1400 FIELD => 'LookupType',
1401 VALUE => 'RT::Queue-RT::Ticket',
1403 $self->SUPER::Limit(
1405 ENTRYAGGREGATOR => 'AND',
1410 $TicketCFs = $self->{_sql_object_cfv_alias}{$cfkey} = $self->Join(
1414 TABLE2 => 'ObjectCustomFieldValues',
1415 FIELD2 => 'CustomField',
1417 $self->SUPER::Limit(
1418 LEFTJOIN => $TicketCFs,
1419 FIELD => 'ObjectId',
1422 ENTRYAGGREGATOR => 'AND',
1425 $self->SUPER::Limit(
1426 LEFTJOIN => $TicketCFs,
1427 FIELD => 'ObjectType',
1428 VALUE => 'RT::Ticket',
1429 ENTRYAGGREGATOR => 'AND'
1431 $self->SUPER::Limit(
1432 LEFTJOIN => $TicketCFs,
1433 FIELD => 'Disabled',
1436 ENTRYAGGREGATOR => 'AND'
1439 return ($TicketCFs, $CFs);
1442 =head2 _CustomFieldLimit
1444 Limit based on CustomFields
1451 use Regexp::Common qw(RE_net_IPv4);
1452 use Regexp::Common::net::CIDR;
1455 sub _CustomFieldLimit {
1456 my ( $self, $_field, $op, $value, %rest ) = @_;
1458 my $field = $rest{'SUBKEY'} || die "No field specified";
1460 # For our sanity, we can only limit on one queue at a time
1462 my ($queue, $cfid, $cf, $column);
1463 ($queue, $field, $cf, $column) = $self->_CustomFieldDecipher( $field );
1464 $cfid = $cf ? $cf->id : 0 ;
1466 # If we're trying to find custom fields that don't match something, we
1467 # want tickets where the custom field has no value at all. Note that
1468 # we explicitly don't include the "IS NULL" case, since we would
1469 # otherwise end up with a redundant clause.
1471 my ($negative_op, $null_op, $inv_op, $range_op)
1472 = $self->ClassifySQLOperation( $op );
1475 return @_ unless RT->Config->Get('DatabaseType') eq 'Oracle';
1478 return %args unless $args{'FIELD'} eq 'LargeContent';
1480 my $op = $args{'OPERATOR'};
1482 $args{'OPERATOR'} = 'MATCHES';
1484 elsif ( $op eq '!=' ) {
1485 $args{'OPERATOR'} = 'NOT MATCHES';
1487 elsif ( $op =~ /^[<>]=?$/ ) {
1488 $args{'FUNCTION'} = "TO_CHAR( $args{'ALIAS'}.LargeContent )";
1493 if ( $cf && $cf->Type eq 'IPAddress' ) {
1494 my $parsed = RT::ObjectCustomFieldValue->ParseIP($value);
1499 $RT::Logger->warn("$value is not a valid IPAddress");
1503 if ( $cf && $cf->Type eq 'IPAddressRange' ) {
1505 if ( $value =~ /^\s*$RE{net}{CIDR}{IPv4}{-keep}\s*$/o ) {
1507 # convert incomplete 192.168/24 to 192.168.0.0/24 format
1509 join( '.', map $_ || 0, ( split /\./, $1 )[ 0 .. 3 ] ) . "/$2"
1513 my ( $start_ip, $end_ip ) =
1514 RT::ObjectCustomFieldValue->ParseIPRange($value);
1515 if ( $start_ip && $end_ip ) {
1516 if ( $op =~ /^([<>])=?$/ ) {
1517 my $is_less = $1 eq '<' ? 1 : 0;
1526 $value = join '-', $start_ip, $end_ip;
1530 $RT::Logger->warn("$value is not a valid IPAddressRange");
1534 my $single_value = !$cf || !$cfid || $cf->SingleValue;
1536 my $cfkey = $cfid ? $cfid : "$queue.$field";
1538 if ( $null_op && !$column ) {
1539 # IS[ NOT] NULL without column is the same as has[ no] any CF value,
1540 # we can reuse our default joins for this operation
1541 # with column specified we have different situation
1542 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
1545 ALIAS => $TicketCFs,
1554 OPERATOR => 'IS NOT',
1557 ENTRYAGGREGATOR => 'AND',
1561 elsif ( $op !~ /^[<>]=?$/ && ( $cf && $cf->Type eq 'IPAddressRange')) {
1563 my ($start_ip, $end_ip) = split /-/, $value;
1566 if ( $op !~ /NOT|!=|<>/i ) { # positive equation
1567 $self->_CustomFieldLimit(
1568 'CF', '<=', $end_ip, %rest,
1569 SUBKEY => $rest{'SUBKEY'}. '.Content',
1571 $self->_CustomFieldLimit(
1572 'CF', '>=', $start_ip, %rest,
1573 SUBKEY => $rest{'SUBKEY'}. '.LargeContent',
1574 ENTRYAGGREGATOR => 'AND',
1576 # as well limit borders so DB optimizers can use better
1577 # estimations and scan less rows
1578 # have to disable this tweak because of ipv6
1579 # $self->_CustomFieldLimit(
1580 # $field, '>=', '000.000.000.000', %rest,
1581 # SUBKEY => $rest{'SUBKEY'}. '.Content',
1582 # ENTRYAGGREGATOR => 'AND',
1584 # $self->_CustomFieldLimit(
1585 # $field, '<=', '255.255.255.255', %rest,
1586 # SUBKEY => $rest{'SUBKEY'}. '.LargeContent',
1587 # ENTRYAGGREGATOR => 'AND',
1590 else { # negative equation
1591 $self->_CustomFieldLimit($field, '>', $end_ip, %rest);
1592 $self->_CustomFieldLimit(
1593 $field, '<', $start_ip, %rest,
1594 SUBKEY => $rest{'SUBKEY'}. '.LargeContent',
1595 ENTRYAGGREGATOR => 'OR',
1597 # TODO: as well limit borders so DB optimizers can use better
1598 # estimations and scan less rows, but it's harder to do
1599 # as we have OR aggregator
1603 elsif ( !$negative_op || $single_value ) {
1604 $cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++ if !$single_value && !$range_op;
1605 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
1612 # if column is defined then deal only with it
1613 # otherwise search in Content and in LargeContent
1615 $self->_SQLLimit( $fix_op->(
1616 ALIAS => $TicketCFs,
1627 # need special treatment for Date
1628 if ( $cf and $cf->Type eq 'DateTime' and $op eq '=' ) {
1630 if ( $value =~ /:/ ) {
1631 # there is time speccified.
1632 my $date = RT::Date->new( $self->CurrentUser );
1633 $date->Set( Format => 'unknown', Value => $value );
1635 ALIAS => $TicketCFs,
1638 VALUE => $date->ISO,
1643 # no time specified, that means we want everything on a
1644 # particular day. in the database, we need to check for >
1645 # and < the edges of that day.
1646 my $date = RT::Date->new( $self->CurrentUser );
1647 $date->Set( Format => 'unknown', Value => $value );
1648 $date->SetToMidnight( Timezone => 'server' );
1649 my $daystart = $date->ISO;
1651 my $dayend = $date->ISO;
1656 ALIAS => $TicketCFs,
1664 ALIAS => $TicketCFs,
1669 ENTRYAGGREGATOR => 'AND',
1675 elsif ( $op eq '=' || $op eq '!=' || $op eq '<>' ) {
1676 if ( length( Encode::encode_utf8($value) ) < 256 ) {
1678 ALIAS => $TicketCFs,
1688 ALIAS => $TicketCFs,
1692 ENTRYAGGREGATOR => 'OR'
1695 ALIAS => $TicketCFs,
1699 ENTRYAGGREGATOR => 'OR'
1702 $self->_SQLLimit( $fix_op->(
1703 ALIAS => $TicketCFs,
1704 FIELD => 'LargeContent',
1707 ENTRYAGGREGATOR => 'AND',
1713 ALIAS => $TicketCFs,
1723 ALIAS => $TicketCFs,
1727 ENTRYAGGREGATOR => 'OR'
1730 ALIAS => $TicketCFs,
1734 ENTRYAGGREGATOR => 'OR'
1737 $self->_SQLLimit( $fix_op->(
1738 ALIAS => $TicketCFs,
1739 FIELD => 'LargeContent',
1742 ENTRYAGGREGATOR => 'AND',
1748 # XXX: if we join via CustomFields table then
1749 # because of order of left joins we get NULLs in
1750 # CF table and then get nulls for those records
1751 # in OCFVs table what result in wrong results
1752 # as decifer method now tries to load a CF then
1753 # we fall into this situation only when there
1754 # are more than one CF with the name in the DB.
1755 # the same thing applies to order by call.
1756 # TODO: reorder joins T <- OCFVs <- CFs <- OCFs if
1757 # we want treat IS NULL as (not applies or has
1762 OPERATOR => 'IS NOT',
1765 ENTRYAGGREGATOR => 'AND',
1771 ALIAS => $TicketCFs,
1772 FIELD => $column || 'Content',
1776 ENTRYAGGREGATOR => 'OR',
1784 $cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++;
1785 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
1788 $op =~ s/!|NOT\s+//i;
1790 # if column is defined then deal only with it
1791 # otherwise search in Content and in LargeContent
1793 $self->SUPER::Limit( $fix_op->(
1794 LEFTJOIN => $TicketCFs,
1795 ALIAS => $TicketCFs,
1802 $self->SUPER::Limit(
1803 LEFTJOIN => $TicketCFs,
1804 ALIAS => $TicketCFs,
1812 ALIAS => $TicketCFs,
1821 sub _HasAttributeLimit {
1822 my ( $self, $field, $op, $value, %rest ) = @_;
1824 my $alias = $self->Join(
1828 TABLE2 => 'Attributes',
1829 FIELD2 => 'ObjectId',
1831 $self->SUPER::Limit(
1833 FIELD => 'ObjectType',
1834 VALUE => 'RT::Ticket',
1835 ENTRYAGGREGATOR => 'AND'
1837 $self->SUPER::Limit(
1842 ENTRYAGGREGATOR => 'AND'
1848 OPERATOR => $FIELD_METADATA{$field}->[1]? 'IS NOT': 'IS',
1855 # End Helper Functions
1857 # End of SQL Stuff -------------------------------------------------
1860 =head2 OrderByCols ARRAY
1862 A modified version of the OrderBy method which automatically joins where
1863 C<ALIAS> is set to the name of a watcher type.
1874 foreach my $row (@args) {
1875 if ( $row->{ALIAS} ) {
1879 if ( $row->{FIELD} !~ /\./ ) {
1880 my $meta = $self->FIELDS->{ $row->{FIELD} };
1886 if ( $meta->[0] eq 'ENUM' && ($meta->[1]||'') eq 'Queue' ) {
1887 my $alias = $self->Join(
1890 FIELD1 => $row->{'FIELD'},
1894 push @res, { %$row, ALIAS => $alias, FIELD => "Name" };
1895 } elsif ( ( $meta->[0] eq 'ENUM' && ($meta->[1]||'') eq 'User' )
1896 || ( $meta->[0] eq 'WATCHERFIELD' && ($meta->[1]||'') eq 'Owner' )
1898 my $alias = $self->Join(
1901 FIELD1 => $row->{'FIELD'},
1905 push @res, { %$row, ALIAS => $alias, FIELD => "Name" };
1912 my ( $field, $subkey ) = split /\./, $row->{FIELD}, 2;
1913 my $meta = $self->FIELDS->{$field};
1914 if ( defined $meta->[0] && $meta->[0] eq 'WATCHERFIELD' ) {
1915 # cache alias as we want to use one alias per watcher type for sorting
1916 my $users = $self->{_sql_u_watchers_alias_for_sort}{ $meta->[1] };
1918 $self->{_sql_u_watchers_alias_for_sort}{ $meta->[1] }
1919 = $users = ( $self->_WatcherJoin( $meta->[1] ) )[2];
1921 push @res, { %$row, ALIAS => $users, FIELD => $subkey };
1922 } elsif ( defined $meta->[0] && $meta->[0] eq 'CUSTOMFIELD' ) {
1923 my ($queue, $field, $cf_obj, $column) = $self->_CustomFieldDecipher( $subkey );
1924 my $cfkey = $cf_obj ? $cf_obj->id : "$queue.$field";
1925 $cfkey .= ".ordering" if !$cf_obj || ($cf_obj->MaxValues||0) != 1;
1926 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, ($cf_obj ?$cf_obj->id :0) , $field );
1927 # this is described in _CustomFieldLimit
1931 OPERATOR => 'IS NOT',
1934 ENTRYAGGREGATOR => 'AND',
1937 # For those cases where we are doing a join against the
1938 # CF name, and don't have a CFid, use Unique to make sure
1939 # we don't show duplicate tickets. NOTE: I'm pretty sure
1940 # this will stay mixed in for the life of the
1941 # class/package, and not just for the life of the object.
1942 # Potential performance issue.
1943 require DBIx::SearchBuilder::Unique;
1944 DBIx::SearchBuilder::Unique->import;
1946 my $CFvs = $self->Join(
1948 ALIAS1 => $TicketCFs,
1949 FIELD1 => 'CustomField',
1950 TABLE2 => 'CustomFieldValues',
1951 FIELD2 => 'CustomField',
1953 $self->SUPER::Limit(
1957 VALUE => $TicketCFs . ".Content",
1958 ENTRYAGGREGATOR => 'AND'
1961 push @res, { %$row, ALIAS => $CFvs, FIELD => 'SortOrder' };
1962 push @res, { %$row, ALIAS => $TicketCFs, FIELD => 'Content' };
1963 } elsif ( $field eq "Custom" && $subkey eq "Ownership") {
1964 # PAW logic is "reversed"
1966 if (exists $row->{ORDER} ) {
1967 my $o = $row->{ORDER};
1968 delete $row->{ORDER};
1969 $order = "DESC" if $o =~ /asc/i;
1972 # Ticket.Owner 1 0 X
1973 # Unowned Tickets 0 1 X
1976 foreach my $uid ( $self->CurrentUser->Id, RT->Nobody->Id ) {
1977 if ( RT->Config->Get('DatabaseType') eq 'Oracle' ) {
1978 my $f = ($row->{'ALIAS'} || 'main') .'.Owner';
1983 FUNCTION => "CASE WHEN $f=$uid THEN 1 ELSE 0 END",
1990 FUNCTION => "Owner=$uid",
1996 push @res, { %$row, FIELD => "Priority", ORDER => $order } ;
2002 return $self->SUPER::OrderByCols(@res);
2010 Takes a paramhash with the fields FIELD, OPERATOR, VALUE and DESCRIPTION
2011 Generally best called from LimitFoo methods
2021 DESCRIPTION => undef,
2024 $args{'DESCRIPTION'} = $self->loc(
2025 "[_1] [_2] [_3]", $args{'FIELD'},
2026 $args{'OPERATOR'}, $args{'VALUE'}
2028 if ( !defined $args{'DESCRIPTION'} );
2030 my $index = $self->_NextIndex;
2032 # make the TicketRestrictions hash the equivalent of whatever we just passed in;
2034 %{ $self->{'TicketRestrictions'}{$index} } = %args;
2036 $self->{'RecalcTicketLimits'} = 1;
2038 # If we're looking at the effective id, we don't want to append the other clause
2039 # which limits us to tickets where id = effective id
2040 if ( $args{'FIELD'} eq 'EffectiveId'
2041 && ( !$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) )
2043 $self->{'looking_at_effective_id'} = 1;
2046 if ( $args{'FIELD'} eq 'Type'
2047 && ( !$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) )
2049 $self->{'looking_at_type'} = 1;
2060 LimitQueue takes a paramhash with the fields OPERATOR and VALUE.
2061 OPERATOR is one of = or !=. (It defaults to =).
2062 VALUE is a queue id or Name.
2075 #TODO VALUE should also take queue objects
2076 if ( defined $args{'VALUE'} && $args{'VALUE'} !~ /^\d+$/ ) {
2077 my $queue = RT::Queue->new( $self->CurrentUser );
2078 $queue->Load( $args{'VALUE'} );
2079 $args{'VALUE'} = $queue->Id;
2082 # What if they pass in an Id? Check for isNum() and convert to
2085 #TODO check for a valid queue here
2089 VALUE => $args{'VALUE'},
2090 OPERATOR => $args{'OPERATOR'},
2091 DESCRIPTION => join(
2092 ' ', $self->loc('Queue'), $args{'OPERATOR'}, $args{'VALUE'},
2102 Takes a paramhash with the fields OPERATOR and VALUE.
2103 OPERATOR is one of = or !=.
2106 RT adds Status != 'deleted' until object has
2107 allow_deleted_search internal property set.
2108 $tickets->{'allow_deleted_search'} = 1;
2109 $tickets->LimitStatus( VALUE => 'deleted' );
2121 VALUE => $args{'VALUE'},
2122 OPERATOR => $args{'OPERATOR'},
2123 DESCRIPTION => join( ' ',
2124 $self->loc('Status'), $args{'OPERATOR'},
2125 $self->loc( $args{'VALUE'} ) ),
2133 If called, this search will not automatically limit the set of results found
2134 to tickets of type "Ticket". Tickets of other types, such as "project" and
2135 "approval" will be found.
2142 # Instead of faking a Limit that later gets ignored, fake up the
2143 # fact that we're already looking at type, so that the check in
2144 # Tickets_SQL/FromSQL goes down the right branch
2146 # $self->LimitType(VALUE => '__any');
2147 $self->{looking_at_type} = 1;
2154 Takes a paramhash with the fields OPERATOR and VALUE.
2155 OPERATOR is one of = or !=, it defaults to "=".
2156 VALUE is a string to search for in the type of the ticket.
2171 VALUE => $args{'VALUE'},
2172 OPERATOR => $args{'OPERATOR'},
2173 DESCRIPTION => join( ' ',
2174 $self->loc('Type'), $args{'OPERATOR'}, $args{'Limit'}, ),
2184 Takes a paramhash with the fields OPERATOR and VALUE.
2185 OPERATOR is one of = or !=.
2186 VALUE is a string to search for in the subject of the ticket.
2195 VALUE => $args{'VALUE'},
2196 OPERATOR => $args{'OPERATOR'},
2197 DESCRIPTION => join( ' ',
2198 $self->loc('Subject'), $args{'OPERATOR'}, $args{'VALUE'}, ),
2204 # Things that can be > < = !=
2209 Takes a paramhash with the fields OPERATOR and VALUE.
2210 OPERATOR is one of =, >, < or !=.
2211 VALUE is a ticket Id to search for
2224 VALUE => $args{'VALUE'},
2225 OPERATOR => $args{'OPERATOR'},
2227 join( ' ', $self->loc('Id'), $args{'OPERATOR'}, $args{'VALUE'}, ),
2233 =head2 LimitPriority
2235 Takes a paramhash with the fields OPERATOR and VALUE.
2236 OPERATOR is one of =, >, < or !=.
2237 VALUE is a value to match the ticket\'s priority against
2245 FIELD => 'Priority',
2246 VALUE => $args{'VALUE'},
2247 OPERATOR => $args{'OPERATOR'},
2248 DESCRIPTION => join( ' ',
2249 $self->loc('Priority'),
2250 $args{'OPERATOR'}, $args{'VALUE'}, ),
2256 =head2 LimitInitialPriority
2258 Takes a paramhash with the fields OPERATOR and VALUE.
2259 OPERATOR is one of =, >, < or !=.
2260 VALUE is a value to match the ticket\'s initial priority against
2265 sub LimitInitialPriority {
2269 FIELD => 'InitialPriority',
2270 VALUE => $args{'VALUE'},
2271 OPERATOR => $args{'OPERATOR'},
2272 DESCRIPTION => join( ' ',
2273 $self->loc('Initial Priority'), $args{'OPERATOR'},
2280 =head2 LimitFinalPriority
2282 Takes a paramhash with the fields OPERATOR and VALUE.
2283 OPERATOR is one of =, >, < or !=.
2284 VALUE is a value to match the ticket\'s final priority against
2288 sub LimitFinalPriority {
2292 FIELD => 'FinalPriority',
2293 VALUE => $args{'VALUE'},
2294 OPERATOR => $args{'OPERATOR'},
2295 DESCRIPTION => join( ' ',
2296 $self->loc('Final Priority'), $args{'OPERATOR'},
2303 =head2 LimitTimeWorked
2305 Takes a paramhash with the fields OPERATOR and VALUE.
2306 OPERATOR is one of =, >, < or !=.
2307 VALUE is a value to match the ticket's TimeWorked attribute
2311 sub LimitTimeWorked {
2315 FIELD => 'TimeWorked',
2316 VALUE => $args{'VALUE'},
2317 OPERATOR => $args{'OPERATOR'},
2318 DESCRIPTION => join( ' ',
2319 $self->loc('Time Worked'),
2320 $args{'OPERATOR'}, $args{'VALUE'}, ),
2326 =head2 LimitTimeLeft
2328 Takes a paramhash with the fields OPERATOR and VALUE.
2329 OPERATOR is one of =, >, < or !=.
2330 VALUE is a value to match the ticket's TimeLeft attribute
2338 FIELD => 'TimeLeft',
2339 VALUE => $args{'VALUE'},
2340 OPERATOR => $args{'OPERATOR'},
2341 DESCRIPTION => join( ' ',
2342 $self->loc('Time Left'),
2343 $args{'OPERATOR'}, $args{'VALUE'}, ),
2353 Takes a paramhash with the fields OPERATOR and VALUE.
2354 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2355 VALUE is a string to search for in the body of the ticket
2364 VALUE => $args{'VALUE'},
2365 OPERATOR => $args{'OPERATOR'},
2366 DESCRIPTION => join( ' ',
2367 $self->loc('Ticket content'), $args{'OPERATOR'},
2374 =head2 LimitFilename
2376 Takes a paramhash with the fields OPERATOR and VALUE.
2377 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2378 VALUE is a string to search for in the body of the ticket
2386 FIELD => 'Filename',
2387 VALUE => $args{'VALUE'},
2388 OPERATOR => $args{'OPERATOR'},
2389 DESCRIPTION => join( ' ',
2390 $self->loc('Attachment filename'), $args{'OPERATOR'},
2396 =head2 LimitContentType
2398 Takes a paramhash with the fields OPERATOR and VALUE.
2399 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2400 VALUE is a content type to search ticket attachments for
2404 sub LimitContentType {
2408 FIELD => 'ContentType',
2409 VALUE => $args{'VALUE'},
2410 OPERATOR => $args{'OPERATOR'},
2411 DESCRIPTION => join( ' ',
2412 $self->loc('Ticket content type'), $args{'OPERATOR'},
2423 Takes a paramhash with the fields OPERATOR and VALUE.
2424 OPERATOR is one of = or !=.
2436 my $owner = RT::User->new( $self->CurrentUser );
2437 $owner->Load( $args{'VALUE'} );
2439 # FIXME: check for a valid $owner
2442 VALUE => $args{'VALUE'},
2443 OPERATOR => $args{'OPERATOR'},
2444 DESCRIPTION => join( ' ',
2445 $self->loc('Owner'), $args{'OPERATOR'}, $owner->Name(), ),
2455 Takes a paramhash with the fields OPERATOR, TYPE and VALUE.
2456 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2457 VALUE is a value to match the ticket\'s watcher email addresses against
2458 TYPE is the sort of watchers you want to match against. Leave it undef if you want to search all of them
2472 #build us up a description
2473 my ( $watcher_type, $desc );
2474 if ( $args{'TYPE'} ) {
2475 $watcher_type = $args{'TYPE'};
2478 $watcher_type = "Watcher";
2482 FIELD => $watcher_type,
2483 VALUE => $args{'VALUE'},
2484 OPERATOR => $args{'OPERATOR'},
2485 TYPE => $args{'TYPE'},
2486 DESCRIPTION => join( ' ',
2487 $self->loc($watcher_type),
2488 $args{'OPERATOR'}, $args{'VALUE'}, ),
2497 =head2 LimitLinkedTo
2499 LimitLinkedTo takes a paramhash with two fields: TYPE and TARGET
2500 TYPE limits the sort of link we want to search on
2502 TYPE = { RefersTo, MemberOf, DependsOn }
2504 TARGET is the id or URI of the TARGET of the link
2518 FIELD => 'LinkedTo',
2520 TARGET => $args{'TARGET'},
2521 TYPE => $args{'TYPE'},
2522 DESCRIPTION => $self->loc(
2523 "Tickets [_1] by [_2]",
2524 $self->loc( $args{'TYPE'} ),
2527 OPERATOR => $args{'OPERATOR'},
2533 =head2 LimitLinkedFrom
2535 LimitLinkedFrom takes a paramhash with two fields: TYPE and BASE
2536 TYPE limits the sort of link we want to search on
2539 BASE is the id or URI of the BASE of the link
2543 sub LimitLinkedFrom {
2552 # translate RT2 From/To naming to RT3 TicketSQL naming
2553 my %fromToMap = qw(DependsOn DependentOn
2555 RefersTo ReferredToBy);
2557 my $type = $args{'TYPE'};
2558 $type = $fromToMap{$type} if exists( $fromToMap{$type} );
2561 FIELD => 'LinkedTo',
2563 BASE => $args{'BASE'},
2565 DESCRIPTION => $self->loc(
2566 "Tickets [_1] [_2]",
2567 $self->loc( $args{'TYPE'} ),
2570 OPERATOR => $args{'OPERATOR'},
2577 my $ticket_id = shift;
2578 return $self->LimitLinkedTo(
2580 TARGET => $ticket_id,
2586 sub LimitHasMember {
2588 my $ticket_id = shift;
2589 return $self->LimitLinkedFrom(
2591 BASE => "$ticket_id",
2592 TYPE => 'HasMember',
2599 sub LimitDependsOn {
2601 my $ticket_id = shift;
2602 return $self->LimitLinkedTo(
2604 TARGET => $ticket_id,
2605 TYPE => 'DependsOn',
2612 sub LimitDependedOnBy {
2614 my $ticket_id = shift;
2615 return $self->LimitLinkedFrom(
2618 TYPE => 'DependentOn',
2627 my $ticket_id = shift;
2628 return $self->LimitLinkedTo(
2630 TARGET => $ticket_id,
2638 sub LimitReferredToBy {
2640 my $ticket_id = shift;
2641 return $self->LimitLinkedFrom(
2644 TYPE => 'ReferredToBy',
2652 =head2 LimitDate (FIELD => 'DateField', OPERATOR => $oper, VALUE => $ISODate)
2654 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
2656 OPERATOR is one of > or <
2657 VALUE is a date and time in ISO format in GMT
2658 FIELD is one of Starts, Started, Told, Created, Resolved, LastUpdated
2660 There are also helper functions of the form LimitFIELD that eliminate
2661 the need to pass in a FIELD argument.
2675 #Set the description if we didn't get handed it above
2676 unless ( $args{'DESCRIPTION'} ) {
2677 $args{'DESCRIPTION'} = $args{'FIELD'} . " "
2678 . $args{'OPERATOR'} . " "
2679 . $args{'VALUE'} . " GMT";
2682 $self->Limit(%args);
2689 $self->LimitDate( FIELD => 'Created', @_ );
2694 $self->LimitDate( FIELD => 'Due', @_ );
2700 $self->LimitDate( FIELD => 'Starts', @_ );
2706 $self->LimitDate( FIELD => 'Started', @_ );
2711 $self->LimitDate( FIELD => 'Resolved', @_ );
2716 $self->LimitDate( FIELD => 'Told', @_ );
2719 sub LimitLastUpdated {
2721 $self->LimitDate( FIELD => 'LastUpdated', @_ );
2726 =head2 LimitTransactionDate (OPERATOR => $oper, VALUE => $ISODate)
2728 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
2730 OPERATOR is one of > or <
2731 VALUE is a date and time in ISO format in GMT
2736 sub LimitTransactionDate {
2739 FIELD => 'TransactionDate',
2746 # <20021217042756.GK28744@pallas.fsck.com>
2747 # "Kill It" - Jesse.
2749 #Set the description if we didn't get handed it above
2750 unless ( $args{'DESCRIPTION'} ) {
2751 $args{'DESCRIPTION'} = $args{'FIELD'} . " "
2752 . $args{'OPERATOR'} . " "
2753 . $args{'VALUE'} . " GMT";
2756 $self->Limit(%args);
2763 =head2 LimitCustomField
2765 Takes a paramhash of key/value pairs with the following keys:
2769 =item CUSTOMFIELD - CustomField name or id. If a name is passed, an additional parameter QUEUE may also be passed to distinguish the custom field.
2771 =item OPERATOR - The usual Limit operators
2773 =item VALUE - The value to compare against
2779 sub LimitCustomField {
2783 CUSTOMFIELD => undef,
2785 DESCRIPTION => undef,
2786 FIELD => 'CustomFieldValue',
2791 my $CF = RT::CustomField->new( $self->CurrentUser );
2792 if ( $args{CUSTOMFIELD} =~ /^\d+$/ ) {
2793 $CF->Load( $args{CUSTOMFIELD} );
2796 $CF->LoadByNameAndQueue(
2797 Name => $args{CUSTOMFIELD},
2798 Queue => $args{QUEUE}
2800 $args{CUSTOMFIELD} = $CF->Id;
2803 #If we are looking to compare with a null value.
2804 if ( $args{'OPERATOR'} =~ /^is$/i ) {
2805 $args{'DESCRIPTION'}
2806 ||= $self->loc( "Custom field [_1] has no value.", $CF->Name );
2808 elsif ( $args{'OPERATOR'} =~ /^is not$/i ) {
2809 $args{'DESCRIPTION'}
2810 ||= $self->loc( "Custom field [_1] has a value.", $CF->Name );
2813 # if we're not looking to compare with a null value
2815 $args{'DESCRIPTION'} ||= $self->loc( "Custom field [_1] [_2] [_3]",
2816 $CF->Name, $args{OPERATOR}, $args{VALUE} );
2819 if ( defined $args{'QUEUE'} && $args{'QUEUE'} =~ /\D/ ) {
2820 my $QueueObj = RT::Queue->new( $self->CurrentUser );
2821 $QueueObj->Load( $args{'QUEUE'} );
2822 $args{'QUEUE'} = $QueueObj->Id;
2824 delete $args{'QUEUE'} unless defined $args{'QUEUE'} && length $args{'QUEUE'};
2827 @rest = ( ENTRYAGGREGATOR => 'AND' )
2828 if ( $CF->Type eq 'SelectMultiple' );
2831 VALUE => $args{VALUE},
2833 .(defined $args{'QUEUE'}? ".{$args{'QUEUE'}}" : '' )
2834 .".{" . $CF->Name . "}",
2835 OPERATOR => $args{OPERATOR},
2840 $self->{'RecalcTicketLimits'} = 1;
2847 Keep track of the counter for the array of restrictions
2853 return ( $self->{'restriction_index'}++ );
2861 $self->{'table'} = "Tickets";
2862 $self->{'RecalcTicketLimits'} = 1;
2863 $self->{'looking_at_effective_id'} = 0;
2864 $self->{'looking_at_type'} = 0;
2865 $self->{'restriction_index'} = 1;
2866 $self->{'primary_key'} = "id";
2867 delete $self->{'items_array'};
2868 delete $self->{'item_map'};
2869 delete $self->{'columns_to_display'};
2870 $self->SUPER::_Init(@_);
2879 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2880 return ( $self->SUPER::Count() );
2886 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2887 return ( $self->SUPER::CountAll() );
2892 =head2 ItemsArrayRef
2894 Returns a reference to the set of all items found in this search
2901 return $self->{'items_array'} if $self->{'items_array'};
2903 my $placeholder = $self->_ItemsCounter;
2904 $self->GotoFirstItem();
2905 while ( my $item = $self->Next ) {
2906 push( @{ $self->{'items_array'} }, $item );
2908 $self->GotoItem($placeholder);
2909 $self->{'items_array'}
2910 = $self->ItemsOrderBy( $self->{'items_array'} );
2912 return $self->{'items_array'};
2915 sub ItemsArrayRefWindow {
2919 my @old = ($self->_ItemsCounter, $self->RowsPerPage, $self->FirstRow+1);
2921 $self->RowsPerPage( $window );
2923 $self->GotoFirstItem;
2926 while ( my $item = $self->Next ) {
2930 $self->RowsPerPage( $old[1] );
2931 $self->FirstRow( $old[2] );
2932 $self->GotoItem( $old[0] );
2941 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2943 my $Ticket = $self->SUPER::Next;
2944 return $Ticket unless $Ticket;
2946 if ( $Ticket->__Value('Status') eq 'deleted'
2947 && !$self->{'allow_deleted_search'} )
2951 elsif ( RT->Config->Get('UseSQLForACLChecks') ) {
2952 # if we found a ticket with this option enabled then
2953 # all tickets we found are ACLed, cache this fact
2954 my $key = join ";:;", $self->CurrentUser->id, 'ShowTicket', 'RT::Ticket-'. $Ticket->id;
2955 $RT::Principal::_ACL_CACHE->set( $key => 1 );
2958 elsif ( $Ticket->CurrentUserHasRight('ShowTicket') ) {
2963 # If the user doesn't have the right to show this ticket
2970 $self->CurrentUserCanSee if RT->Config->Get('UseSQLForACLChecks');
2971 return $self->SUPER::_DoSearch( @_ );
2976 $self->CurrentUserCanSee if RT->Config->Get('UseSQLForACLChecks');
2977 return $self->SUPER::_DoCount( @_ );
2983 my $cache_key = 'RolesHasRight;:;ShowTicket';
2985 if ( my $cached = $RT::Principal::_ACL_CACHE->fetch( $cache_key ) ) {
2989 my $ACL = RT::ACL->new( RT->SystemUser );
2990 $ACL->Limit( FIELD => 'RightName', VALUE => 'ShowTicket' );
2991 $ACL->Limit( FIELD => 'PrincipalType', OPERATOR => '!=', VALUE => 'Group' );
2992 my $principal_alias = $ACL->Join(
2994 FIELD1 => 'PrincipalId',
2995 TABLE2 => 'Principals',
2998 $ACL->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3001 foreach my $ACE ( @{ $ACL->ItemsArrayRef } ) {
3002 my $role = $ACE->__Value('PrincipalType');
3003 my $type = $ACE->__Value('ObjectType');
3004 if ( $type eq 'RT::System' ) {
3007 elsif ( $type eq 'RT::Queue' ) {
3008 next if $res{ $role } && !ref $res{ $role };
3009 push @{ $res{ $role } ||= [] }, $ACE->__Value('ObjectId');
3012 $RT::Logger->error('ShowTicket right is granted on unsupported object');
3015 $RT::Principal::_ACL_CACHE->set( $cache_key => \%res );
3019 sub _DirectlyCanSeeIn {
3021 my $id = $self->CurrentUser->id;
3023 my $cache_key = 'User-'. $id .';:;ShowTicket;:;DirectlyCanSeeIn';
3024 if ( my $cached = $RT::Principal::_ACL_CACHE->fetch( $cache_key ) ) {
3028 my $ACL = RT::ACL->new( RT->SystemUser );
3029 $ACL->Limit( FIELD => 'RightName', VALUE => 'ShowTicket' );
3030 my $principal_alias = $ACL->Join(
3032 FIELD1 => 'PrincipalId',
3033 TABLE2 => 'Principals',
3036 $ACL->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3037 my $cgm_alias = $ACL->Join(
3039 FIELD1 => 'PrincipalId',
3040 TABLE2 => 'CachedGroupMembers',
3041 FIELD2 => 'GroupId',
3043 $ACL->Limit( ALIAS => $cgm_alias, FIELD => 'MemberId', VALUE => $id );
3044 $ACL->Limit( ALIAS => $cgm_alias, FIELD => 'Disabled', VALUE => 0 );
3047 foreach my $ACE ( @{ $ACL->ItemsArrayRef } ) {
3048 my $type = $ACE->__Value('ObjectType');
3049 if ( $type eq 'RT::System' ) {
3050 # If user is direct member of a group that has the right
3051 # on the system then he can see any ticket
3052 $RT::Principal::_ACL_CACHE->set( $cache_key => [-1] );
3055 elsif ( $type eq 'RT::Queue' ) {
3056 push @res, $ACE->__Value('ObjectId');
3059 $RT::Logger->error('ShowTicket right is granted on unsupported object');
3062 $RT::Principal::_ACL_CACHE->set( $cache_key => \@res );
3066 sub CurrentUserCanSee {
3068 return if $self->{'_sql_current_user_can_see_applied'};
3070 return $self->{'_sql_current_user_can_see_applied'} = 1
3071 if $self->CurrentUser->UserObj->HasRight(
3072 Right => 'SuperUser', Object => $RT::System
3075 my $id = $self->CurrentUser->id;
3077 # directly can see in all queues then we have nothing to do
3078 my @direct_queues = $self->_DirectlyCanSeeIn;
3079 return $self->{'_sql_current_user_can_see_applied'} = 1
3080 if @direct_queues && $direct_queues[0] == -1;
3082 my %roles = $self->_RolesCanSee;
3084 my %skip = map { $_ => 1 } @direct_queues;
3085 foreach my $role ( keys %roles ) {
3086 next unless ref $roles{ $role };
3088 my @queues = grep !$skip{$_}, @{ $roles{ $role } };
3090 $roles{ $role } = \@queues;
3092 delete $roles{ $role };
3097 # there is no global watchers, only queues and tickes, if at
3098 # some point we will add global roles then it's gonna blow
3099 # the idea here is that if the right is set globaly for a role
3100 # and user plays this role for a queue directly not a ticket
3101 # then we have to check in advance
3102 if ( my @tmp = grep $_ ne 'Owner' && !ref $roles{ $_ }, keys %roles ) {
3104 my $groups = RT::Groups->new( RT->SystemUser );
3105 $groups->Limit( FIELD => 'Domain', VALUE => 'RT::Queue-Role' );
3107 $groups->Limit( FIELD => 'Type', VALUE => $_ );
3109 my $principal_alias = $groups->Join(
3112 TABLE2 => 'Principals',
3115 $groups->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3116 my $cgm_alias = $groups->Join(
3119 TABLE2 => 'CachedGroupMembers',
3120 FIELD2 => 'GroupId',
3122 $groups->Limit( ALIAS => $cgm_alias, FIELD => 'MemberId', VALUE => $id );
3123 $groups->Limit( ALIAS => $cgm_alias, FIELD => 'Disabled', VALUE => 0 );
3124 while ( my $group = $groups->Next ) {
3125 push @direct_queues, $group->Instance;
3129 unless ( @direct_queues || keys %roles ) {
3130 $self->SUPER::Limit(
3135 ENTRYAGGREGATOR => 'AND',
3137 return $self->{'_sql_current_user_can_see_applied'} = 1;
3141 my $join_roles = keys %roles;
3142 $join_roles = 0 if $join_roles == 1 && $roles{'Owner'};
3143 my ($role_group_alias, $cgm_alias);
3144 if ( $join_roles ) {
3145 $role_group_alias = $self->_RoleGroupsJoin( New => 1 );
3146 $cgm_alias = $self->_GroupMembersJoin( GroupsAlias => $role_group_alias );
3147 $self->SUPER::Limit(
3148 LEFTJOIN => $cgm_alias,
3149 FIELD => 'MemberId',
3154 my $limit_queues = sub {
3158 return unless @queues;
3159 if ( @queues == 1 ) {
3160 $self->SUPER::Limit(
3165 ENTRYAGGREGATOR => $ea,
3168 $self->SUPER::_OpenParen('ACL');
3169 foreach my $q ( @queues ) {
3170 $self->SUPER::Limit(
3175 ENTRYAGGREGATOR => $ea,
3179 $self->SUPER::_CloseParen('ACL');
3184 $self->SUPER::_OpenParen('ACL');
3186 $ea = 'OR' if $limit_queues->( $ea, @direct_queues );
3187 while ( my ($role, $queues) = each %roles ) {
3188 $self->SUPER::_OpenParen('ACL');
3189 if ( $role eq 'Owner' ) {
3190 $self->SUPER::Limit(
3194 ENTRYAGGREGATOR => $ea,
3198 $self->SUPER::Limit(
3200 ALIAS => $cgm_alias,
3201 FIELD => 'MemberId',
3202 OPERATOR => 'IS NOT',
3205 ENTRYAGGREGATOR => $ea,
3207 $self->SUPER::Limit(
3209 ALIAS => $role_group_alias,
3212 ENTRYAGGREGATOR => 'AND',
3215 $limit_queues->( 'AND', @$queues ) if ref $queues;
3216 $ea = 'OR' if $ea eq 'AND';
3217 $self->SUPER::_CloseParen('ACL');
3219 $self->SUPER::_CloseParen('ACL');
3221 return $self->{'_sql_current_user_can_see_applied'} = 1;
3228 =head2 LoadRestrictions
3230 LoadRestrictions takes a string which can fully populate the TicketRestrictons hash.
3231 TODO It is not yet implemented
3237 =head2 DescribeRestrictions
3240 Returns a hash keyed by restriction id.
3241 Each element of the hash is currently a one element hash that contains DESCRIPTION which
3242 is a description of the purpose of that TicketRestriction
3246 sub DescribeRestrictions {
3251 foreach my $row ( keys %{ $self->{'TicketRestrictions'} } ) {
3252 $listing{$row} = $self->{'TicketRestrictions'}{$row}{'DESCRIPTION'};
3259 =head2 RestrictionValues FIELD
3261 Takes a restriction field and returns a list of values this field is restricted
3266 sub RestrictionValues {
3269 map $self->{'TicketRestrictions'}{$_}{'VALUE'}, grep {
3270 $self->{'TicketRestrictions'}{$_}{'FIELD'} eq $field
3271 && $self->{'TicketRestrictions'}{$_}{'OPERATOR'} eq "="
3273 keys %{ $self->{'TicketRestrictions'} };
3278 =head2 ClearRestrictions
3280 Removes all restrictions irretrievably
3284 sub ClearRestrictions {
3286 delete $self->{'TicketRestrictions'};
3287 $self->{'looking_at_effective_id'} = 0;
3288 $self->{'looking_at_type'} = 0;
3289 $self->{'RecalcTicketLimits'} = 1;
3294 =head2 DeleteRestriction
3296 Takes the row Id of a restriction (From DescribeRestrictions' output, for example.
3297 Removes that restriction from the session's limits.
3301 sub DeleteRestriction {
3304 delete $self->{'TicketRestrictions'}{$row};
3306 $self->{'RecalcTicketLimits'} = 1;
3308 #make the underlying easysearch object forget all its preconceptions
3313 # Convert a set of oldstyle SB Restrictions to Clauses for RQL
3315 sub _RestrictionsToClauses {
3319 foreach my $row ( keys %{ $self->{'TicketRestrictions'} } ) {
3320 my $restriction = $self->{'TicketRestrictions'}{$row};
3322 # We need to reimplement the subclause aggregation that SearchBuilder does.
3323 # Default Subclause is ALIAS.FIELD, and default ALIAS is 'main',
3324 # Then SB AND's the different Subclauses together.
3326 # So, we want to group things into Subclauses, convert them to
3327 # SQL, and then join them with the appropriate DefaultEA.
3328 # Then join each subclause group with AND.
3330 my $field = $restriction->{'FIELD'};
3331 my $realfield = $field; # CustomFields fake up a fieldname, so
3332 # we need to figure that out
3335 # Rewrite LinkedTo meta field to the real field
3336 if ( $field =~ /LinkedTo/ ) {
3337 $realfield = $field = $restriction->{'TYPE'};
3341 # Handle subkey fields with a different real field
3342 if ( $field =~ /^(\w+)\./ ) {
3346 die "I don't know about $field yet"
3347 unless ( exists $FIELD_METADATA{$realfield}
3348 or $restriction->{CUSTOMFIELD} );
3350 my $type = $FIELD_METADATA{$realfield}->[0];
3351 my $op = $restriction->{'OPERATOR'};
3355 map { $restriction->{$_} } qw(VALUE TICKET BASE TARGET)
3358 # this performs the moral equivalent of defined or/dor/C<//>,
3359 # without the short circuiting.You need to use a 'defined or'
3360 # type thing instead of just checking for truth values, because
3361 # VALUE could be 0.(i.e. "false")
3363 # You could also use this, but I find it less aesthetic:
3364 # (although it does short circuit)
3365 #( defined $restriction->{'VALUE'}? $restriction->{VALUE} :
3366 # defined $restriction->{'TICKET'} ?
3367 # $restriction->{TICKET} :
3368 # defined $restriction->{'BASE'} ?
3369 # $restriction->{BASE} :
3370 # defined $restriction->{'TARGET'} ?
3371 # $restriction->{TARGET} )
3373 my $ea = $restriction->{ENTRYAGGREGATOR}
3374 || $DefaultEA{$type}
3377 die "Invalid operator $op for $field ($type)"
3378 unless exists $ea->{$op};
3382 # Each CustomField should be put into a different Clause so they
3383 # are ANDed together.
3384 if ( $restriction->{CUSTOMFIELD} ) {
3385 $realfield = $field;
3388 exists $clause{$realfield} or $clause{$realfield} = [];
3391 $field =~ s!(['\\])!\\$1!g;
3392 $value =~ s!(['\\])!\\$1!g;
3393 my $data = [ $ea, $type, $field, $op, $value ];
3395 # here is where we store extra data, say if it's a keyword or
3396 # something. (I.e. "TYPE SPECIFIC STUFF")
3398 if (lc $ea eq 'none') {
3399 $clause{$realfield} = [ $data ];
3401 push @{ $clause{$realfield} }, $data;
3409 =head2 _ProcessRestrictions PARAMHASH
3411 # The new _ProcessRestrictions is somewhat dependent on the SQL stuff,
3412 # but isn't quite generic enough to move into Tickets_SQL.
3416 sub _ProcessRestrictions {
3419 #Blow away ticket aliases since we'll need to regenerate them for
3421 delete $self->{'TicketAliases'};
3422 delete $self->{'items_array'};
3423 delete $self->{'item_map'};
3424 delete $self->{'raw_rows'};
3425 delete $self->{'rows'};
3426 delete $self->{'count_all'};
3428 my $sql = $self->Query; # Violating the _SQL namespace
3429 if ( !$sql || $self->{'RecalcTicketLimits'} ) {
3431 # "Restrictions to Clauses Branch\n";
3432 my $clauseRef = eval { $self->_RestrictionsToClauses; };
3434 $RT::Logger->error( "RestrictionsToClauses: " . $@ );
3438 $sql = $self->ClausesToSQL($clauseRef);
3439 $self->FromSQL($sql) if $sql;
3443 $self->{'RecalcTicketLimits'} = 0;
3447 =head2 _BuildItemMap
3449 Build up a L</ItemMap> of first/last/next/prev items, so that we can
3450 display search nav quickly.
3457 my $window = RT->Config->Get('TicketsItemMapSize');
3459 $self->{'item_map'} = {};
3461 my $items = $self->ItemsArrayRefWindow( $window );
3462 return unless $items && @$items;
3465 $self->{'item_map'}{'first'} = $items->[0]->EffectiveId;
3466 for ( my $i = 0; $i < @$items; $i++ ) {
3467 my $item = $items->[$i];
3468 my $id = $item->EffectiveId;
3469 $self->{'item_map'}{$id}{'defined'} = 1;
3470 $self->{'item_map'}{$id}{'prev'} = $prev;
3471 $self->{'item_map'}{$id}{'next'} = $items->[$i+1]->EffectiveId
3475 $self->{'item_map'}{'last'} = $prev
3476 if !$window || @$items < $window;
3481 Returns an a map of all items found by this search. The map is a hash
3485 first => <first ticket id found>,
3486 last => <last ticket id found or undef>,
3489 prev => <the ticket id found before>,
3490 next => <the ticket id found after>,
3502 $self->_BuildItemMap unless $self->{'item_map'};
3503 return $self->{'item_map'};
3509 =head2 PrepForSerialization
3511 You don't want to serialize a big tickets object, as
3512 the {items} hash will be instantly invalid _and_ eat
3517 sub PrepForSerialization {
3519 delete $self->{'items'};
3520 delete $self->{'items_array'};
3521 $self->RedoSearch();
3526 RT::Tickets supports several flags which alter search behavior:
3529 allow_deleted_search (Otherwise never show deleted tickets in search results)
3530 looking_at_type (otherwise limit to type=ticket)
3532 These flags are set by calling
3534 $tickets->{'flagname'} = 1;
3536 BUG: There should be an API for this
3546 Returns an empty new RT::Ticket item
3552 return(RT::Ticket->new($self->CurrentUser));
3554 RT::Base->_ImportOverlays();