1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2013 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 # Lower Case version of FIELDS, for case insensitivity
158 our %LOWER_CASE_FIELDS = map { ( lc($_) => $_ ) } (keys %FIELD_METADATA);
160 our %SEARCHABLE_SUBFIELDS = (
162 EmailAddress Name RealName Nickname Organization Address1 Address2
163 WorkPhone HomePhone MobilePhone PagerPhone id
167 # Mapping of Field Type to Function
169 ENUM => \&_EnumLimit,
172 LINK => \&_LinkLimit,
173 DATE => \&_DateLimit,
174 STRING => \&_StringLimit,
175 TRANSFIELD => \&_TransLimit,
176 TRANSCONTENT => \&_TransContentLimit,
177 TRANSDATE => \&_TransDateLimit,
178 WATCHERFIELD => \&_WatcherLimit,
179 MEMBERSHIPFIELD => \&_WatcherMembershipLimit,
180 CUSTOMFIELD => \&_CustomFieldLimit,
181 HASATTRIBUTE => \&_HasAttributeLimit,
183 our %can_bundle = ();# WATCHERFIELD => "yes", );
185 # Default EntryAggregator per type
186 # if you specify OP, you must specify all valid OPs
227 # Helper functions for passing the above lexically scoped tables above
229 sub FIELDS { return \%FIELD_METADATA }
230 sub dispatch { return \%dispatch }
231 sub can_bundle { return \%can_bundle }
233 # Bring in the clowns.
234 require RT::Tickets_SQL;
237 our @SORTFIELDS = qw(id Status
239 Owner Created Due Starts Started
241 Resolved LastUpdated Priority TimeWorked TimeLeft);
245 Returns the list of fields that lists of tickets can easily be sorted by
251 return (@SORTFIELDS);
255 # BEGIN SQL STUFF *********************************
260 $self->SUPER::CleanSlate( @_ );
261 delete $self->{$_} foreach qw(
263 _sql_group_members_aliases
264 _sql_object_cfv_alias
265 _sql_role_group_aliases
267 _sql_u_watchers_alias_for_sort
268 _sql_u_watchers_aliases
269 _sql_current_user_can_see_applied
273 =head1 Limit Helper Routines
275 These routines are the targets of a dispatch table depending on the
276 type of field. They all share the same signature:
278 my ($self,$field,$op,$value,@rest) = @_;
280 The values in @rest should be suitable for passing directly to
281 DBIx::SearchBuilder::Limit.
283 Essentially they are an expanded/broken out (and much simplified)
284 version of what ProcessRestrictions used to do. They're also much
285 more clearly delineated by the TYPE of field being processed.
294 my ( $sb, $field, $op, $value, @rest ) = @_;
296 if ( $value eq '__Bookmarked__' ) {
297 return $sb->_BookmarkLimit( $field, $op, $value, @rest );
299 return $sb->_IntLimit( $field, $op, $value, @rest );
304 my ( $sb, $field, $op, $value, @rest ) = @_;
306 die "Invalid operator $op for __Bookmarked__ search on $field"
307 unless $op =~ /^(=|!=)$/;
310 my $tmp = $sb->CurrentUser->UserObj->FirstAttribute('Bookmarks');
311 $tmp = $tmp->Content if $tmp;
316 return $sb->_SQLLimit(
323 # as bookmarked tickets can be merged we have to use a join
324 # but it should be pretty lightweight
325 my $tickets_alias = $sb->Join(
330 FIELD2 => 'EffectiveId',
334 my $ea = $op eq '='? 'OR': 'AND';
335 foreach my $id ( sort @bookmarks ) {
337 ALIAS => $tickets_alias,
341 $first? (@rest): ( ENTRYAGGREGATOR => $ea )
343 $first = 0 if $first;
350 Handle Fields which are limited to certain values, and potentially
351 need to be looked up from another class.
353 This subroutine actually handles two different kinds of fields. For
354 some the user is responsible for limiting the values. (i.e. Status,
357 For others, the value specified by the user will be looked by via
361 name of class to lookup in (Optional)
366 my ( $sb, $field, $op, $value, @rest ) = @_;
368 # SQL::Statement changes != to <>. (Can we remove this now?)
369 $op = "!=" if $op eq "<>";
371 die "Invalid Operation: $op for $field"
375 my $meta = $FIELD_METADATA{$field};
376 if ( defined $meta->[1] && defined $value && $value !~ /^\d+$/ ) {
377 my $class = "RT::" . $meta->[1];
378 my $o = $class->new( $sb->CurrentUser );
380 $value = $o->Id || 0;
392 Handle fields where the values are limited to integers. (For example,
393 Priority, TimeWorked.)
401 my ( $sb, $field, $op, $value, @rest ) = @_;
403 die "Invalid Operator $op for $field"
404 unless $op =~ /^(=|!=|>|<|>=|<=)$/;
416 Handle fields which deal with links between tickets. (MemberOf, DependsOn)
419 1: Direction (From, To)
420 2: Link Type (MemberOf, DependsOn, RefersTo)
425 my ( $sb, $field, $op, $value, @rest ) = @_;
427 my $meta = $FIELD_METADATA{$field};
428 die "Invalid Operator $op for $field" unless $op =~ /^(=|!=|IS|IS NOT)$/io;
431 if ( $op eq '!=' || $op =~ /\bNOT\b/i ) {
435 $is_null = 1 if !$value || $value =~ /^null$/io;
438 $value = RT::URI->new( $sb->CurrentUser )->CanonicalizeURI( $value );
441 my $direction = $meta->[1] || '';
442 my ($matchfield, $linkfield) = ('', '');
443 if ( $direction eq 'To' ) {
444 ($matchfield, $linkfield) = ("Target", "Base");
446 elsif ( $direction eq 'From' ) {
447 ($matchfield, $linkfield) = ("Base", "Target");
449 elsif ( $direction ) {
450 die "Invalid link direction '$direction' for $field\n";
453 $sb->_LinkLimit( 'LinkedTo', $op, $value, @rest );
455 'LinkedFrom', $op, $value, @rest,
456 ENTRYAGGREGATOR => (($is_negative && $is_null) || (!$is_null && !$is_negative))? 'OR': 'AND',
464 $op = ($op =~ /^(=|IS)$/i)? 'IS': 'IS NOT';
466 elsif ( $value =~ /\D/ ) {
469 $matchfield = "Local$matchfield" if $is_local;
471 #For doing a left join to find "unlinked tickets" we want to generate a query that looks like this
472 # SELECT main.* FROM Tickets main
473 # LEFT JOIN Links Links_1 ON ( (Links_1.Type = 'MemberOf')
474 # AND(main.id = Links_1.LocalTarget))
475 # WHERE Links_1.LocalBase IS NULL;
478 my $linkalias = $sb->Join(
483 FIELD2 => 'Local' . $linkfield
486 LEFTJOIN => $linkalias,
494 FIELD => $matchfield,
501 my $linkalias = $sb->Join(
506 FIELD2 => 'Local' . $linkfield
509 LEFTJOIN => $linkalias,
515 LEFTJOIN => $linkalias,
516 FIELD => $matchfield,
523 FIELD => $matchfield,
524 OPERATOR => $is_negative? 'IS': 'IS NOT',
533 Handle date fields. (Created, LastTold..)
536 1: type of link. (Probably not necessary.)
541 my ( $sb, $field, $op, $value, @rest ) = @_;
543 die "Invalid Date Op: $op"
544 unless $op =~ /^(=|>|<|>=|<=)$/;
546 my $meta = $FIELD_METADATA{$field};
547 die "Incorrect Meta Data for $field"
548 unless ( defined $meta->[1] );
550 my $date = RT::Date->new( $sb->CurrentUser );
551 $date->Set( Format => 'unknown', Value => $value );
555 # if we're specifying =, that means we want everything on a
556 # particular single day. in the database, we need to check for >
557 # and < the edges of that day.
559 $date->SetToMidnight( Timezone => 'server' );
560 my $daystart = $date->ISO;
562 my $dayend = $date->ISO;
578 ENTRYAGGREGATOR => 'AND',
596 Handle simple fields which are just strings. (Subject,Type)
604 my ( $sb, $field, $op, $value, @rest ) = @_;
608 # =, !=, LIKE, NOT LIKE
609 if ( RT->Config->Get('DatabaseType') eq 'Oracle'
610 && (!defined $value || !length $value)
611 && lc($op) ne 'is' && lc($op) ne 'is not'
613 if ($op eq '!=' || $op =~ /^NOT\s/i) {
630 =head2 _TransDateLimit
632 Handle fields limiting based on Transaction Date.
634 The inpupt value must be in a format parseable by Time::ParseDate
641 # This routine should really be factored into translimit.
642 sub _TransDateLimit {
643 my ( $sb, $field, $op, $value, @rest ) = @_;
645 # See the comments for TransLimit, they apply here too
647 my $txn_alias = $sb->JoinTransactions;
649 my $date = RT::Date->new( $sb->CurrentUser );
650 $date->Set( Format => 'unknown', Value => $value );
655 # if we're specifying =, that means we want everything on a
656 # particular single day. in the database, we need to check for >
657 # and < the edges of that day.
659 $date->SetToMidnight( Timezone => 'server' );
660 my $daystart = $date->ISO;
662 my $dayend = $date->ISO;
677 ENTRYAGGREGATOR => 'AND',
682 # not searching for a single day
685 #Search for the right field
700 Limit based on the ContentType or the Filename of a transaction.
705 my ( $self, $field, $op, $value, %rest ) = @_;
707 my $txn_alias = $self->JoinTransactions;
708 unless ( defined $self->{_sql_trattachalias} ) {
709 $self->{_sql_trattachalias} = $self->_SQLJoin(
710 TYPE => 'LEFT', # not all txns have an attachment
711 ALIAS1 => $txn_alias,
713 TABLE2 => 'Attachments',
714 FIELD2 => 'TransactionId',
720 ALIAS => $self->{_sql_trattachalias},
728 =head2 _TransContentLimit
730 Limit based on the Content of a transaction.
734 sub _TransContentLimit {
738 # If only this was this simple. We've got to do something
741 #Basically, we want to make sure that the limits apply to
742 #the same attachment, rather than just another attachment
743 #for the same ticket, no matter how many clauses we lump
744 #on. We put them in TicketAliases so that they get nuked
745 #when we redo the join.
747 # In the SQL, we might have
748 # (( Content = foo ) or ( Content = bar AND Content = baz ))
749 # The AND group should share the same Alias.
751 # Actually, maybe it doesn't matter. We use the same alias and it
752 # works itself out? (er.. different.)
754 # Steal more from _ProcessRestrictions
756 # FIXME: Maybe look at the previous FooLimit call, and if it was a
757 # TransLimit and EntryAggregator == AND, reuse the Aliases?
759 # Or better - store the aliases on a per subclause basis - since
760 # those are going to be the things we want to relate to each other,
763 # maybe we should not allow certain kinds of aggregation of these
764 # clauses and do a psuedo regex instead? - the problem is getting
765 # them all into the same subclause when you have (A op B op C) - the
766 # way they get parsed in the tree they're in different subclauses.
768 my ( $self, $field, $op, $value, %rest ) = @_;
769 $field = 'Content' if $field =~ /\W/;
771 my $config = RT->Config->Get('FullTextSearch') || {};
772 unless ( $config->{'Enable'} ) {
773 $self->_SQLLimit( %rest, FIELD => 'id', VALUE => 0 );
777 my $txn_alias = $self->JoinTransactions;
778 unless ( defined $self->{_sql_trattachalias} ) {
779 $self->{_sql_trattachalias} = $self->_SQLJoin(
780 TYPE => 'LEFT', # not all txns have an attachment
781 ALIAS1 => $txn_alias,
783 TABLE2 => 'Attachments',
784 FIELD2 => 'TransactionId',
789 if ( $config->{'Indexed'} ) {
790 my $db_type = RT->Config->Get('DatabaseType');
793 if ( $config->{'Table'} and $config->{'Table'} ne "Attachments") {
794 $alias = $self->{'_sql_aliases'}{'full_text'} ||= $self->_SQLJoin(
796 ALIAS1 => $self->{'_sql_trattachalias'},
798 TABLE2 => $config->{'Table'},
802 $alias = $self->{'_sql_trattachalias'};
805 #XXX: handle negative searches
806 my $index = $config->{'Column'};
807 if ( $db_type eq 'Oracle' ) {
808 my $dbh = $RT::Handle->dbh;
809 my $alias = $self->{_sql_trattachalias};
812 FUNCTION => "CONTAINS( $alias.$field, ".$dbh->quote($value) .")",
818 # this is required to trick DBIx::SB's LEFT JOINS optimizer
819 # into deciding that join is redundant as it is
821 ENTRYAGGREGATOR => 'AND',
822 ALIAS => $self->{_sql_trattachalias},
824 OPERATOR => 'IS NOT',
828 elsif ( $db_type eq 'Pg' ) {
829 my $dbh = $RT::Handle->dbh;
835 VALUE => 'plainto_tsquery('. $dbh->quote($value) .')',
839 elsif ( $db_type eq 'mysql' ) {
840 # XXX: We could theoretically skip the join to Attachments,
841 # and have Sphinx simply index and group by the TicketId,
842 # and join Ticket.id to that attribute, which would be much
843 # more efficient -- however, this is only a possibility if
844 # there are no other transaction limits.
846 # This is a special character. Note that \ does not escape
847 # itself (in Sphinx 2.1.0, at least), so 'foo\;bar' becoming
848 # 'foo\\;bar' is not a vulnerability, and is still parsed as
849 # "foo, \, ;, then bar". Happily, the default mode is
850 # "all", meaning that boolean operators are not special.
853 my $max = $config->{'MaxMatches'};
859 VALUE => "$value;limit=$max;maxmatches=$max",
865 ALIAS => $self->{_sql_trattachalias},
872 if ( RT->Config->Get('DontSearchFileAttachments') ) {
874 ENTRYAGGREGATOR => 'AND',
875 ALIAS => $self->{_sql_trattachalias},
886 Handle watcher limits. (Requestor, CC, etc..)
902 my $meta = $FIELD_METADATA{ $field };
903 my $type = $meta->[1] || '';
904 my $class = $meta->[2] || 'Ticket';
906 # Bail if the subfield is not allowed
908 and not grep { $_ eq $rest{SUBKEY} } @{$SEARCHABLE_SUBFIELDS{'User'}})
910 die "Invalid watcher subfield: '$rest{SUBKEY}'";
913 # Owner was ENUM field, so "Owner = 'xxx'" allowed user to
914 # search by id and Name at the same time, this is workaround
915 # to preserve backward compatibility
916 if ( $field eq 'Owner' ) {
917 if ( $op =~ /^!?=$/ && (!$rest{'SUBKEY'} || $rest{'SUBKEY'} eq 'Name' || $rest{'SUBKEY'} eq 'EmailAddress') ) {
918 my $o = RT::User->new( $self->CurrentUser );
919 my $method = ($rest{'SUBKEY'}||'') eq 'EmailAddress' ? 'LoadByEmail': 'Load';
920 $o->$method( $value );
929 if ( ($rest{'SUBKEY'}||'') eq 'id' ) {
939 $rest{SUBKEY} ||= 'EmailAddress';
941 my $groups = $self->_RoleGroupsJoin( Type => $type, Class => $class, New => !$type );
944 if ( $op =~ /^IS(?: NOT)?$/i ) {
945 # is [not] empty case
947 my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
948 # to avoid joining the table Users into the query, we just join GM
949 # and make sure we don't match records where group is member of itself
951 LEFTJOIN => $group_members,
954 VALUE => "$group_members.MemberId",
958 ALIAS => $group_members,
965 elsif ( $op =~ /^!=$|^NOT\s+/i ) {
966 # negative condition case
969 $op =~ s/!|NOT\s+//i;
971 # XXX: we have no way to build correct "Watcher.X != 'Y'" when condition
972 # "X = 'Y'" matches more then one user so we try to fetch two records and
973 # do the right thing when there is only one exist and semi-working solution
975 my $users_obj = RT::Users->new( $self->CurrentUser );
977 FIELD => $rest{SUBKEY},
982 $users_obj->RowsPerPage(2);
983 my @users = @{ $users_obj->ItemsArrayRef };
985 my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
988 $uid = $users[0]->id if @users;
990 LEFTJOIN => $group_members,
991 ALIAS => $group_members,
997 ALIAS => $group_members,
1003 $self->SUPER::Limit(
1004 LEFTJOIN => $group_members,
1007 VALUE => "$group_members.MemberId",
1010 my $users = $self->Join(
1012 ALIAS1 => $group_members,
1013 FIELD1 => 'MemberId',
1017 $self->SUPER::Limit(
1020 FIELD => $rest{SUBKEY},
1034 # positive condition case
1036 my $group_members = $self->_GroupMembersJoin(
1037 GroupsAlias => $groups, New => 1, Left => 0
1039 my $users = $self->Join(
1041 ALIAS1 => $group_members,
1042 FIELD1 => 'MemberId',
1049 FIELD => $rest{'SUBKEY'},
1058 sub _RoleGroupsJoin {
1060 my %args = (New => 0, Class => 'Ticket', Type => '', @_);
1061 return $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $args{'Type'} }
1062 if $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $args{'Type'} }
1065 # we always have watcher groups for ticket, so we use INNER join
1066 my $groups = $self->Join(
1068 FIELD1 => $args{'Class'} eq 'Queue'? 'Queue': 'id',
1070 FIELD2 => 'Instance',
1071 ENTRYAGGREGATOR => 'AND',
1073 $self->SUPER::Limit(
1074 LEFTJOIN => $groups,
1077 VALUE => 'RT::'. $args{'Class'} .'-Role',
1079 $self->SUPER::Limit(
1080 LEFTJOIN => $groups,
1083 VALUE => $args{'Type'},
1086 $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $args{'Type'} } = $groups
1087 unless $args{'New'};
1092 sub _GroupMembersJoin {
1094 my %args = (New => 1, GroupsAlias => undef, Left => 1, @_);
1096 return $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} }
1097 if $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} }
1100 my $alias = $self->Join(
1101 $args{'Left'} ? (TYPE => 'LEFT') : (),
1102 ALIAS1 => $args{'GroupsAlias'},
1104 TABLE2 => 'CachedGroupMembers',
1105 FIELD2 => 'GroupId',
1106 ENTRYAGGREGATOR => 'AND',
1108 $self->SUPER::Limit(
1109 $args{'Left'} ? (LEFTJOIN => $alias) : (),
1111 FIELD => 'Disabled',
1115 $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} } = $alias
1116 unless $args{'New'};
1123 Helper function which provides joins to a watchers table both for limits
1130 my $type = shift || '';
1133 my $groups = $self->_RoleGroupsJoin( Type => $type );
1134 my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
1135 # XXX: work around, we must hide groups that
1136 # are members of the role group we search in,
1137 # otherwise them result in wrong NULLs in Users
1138 # table and break ordering. Now, we know that
1139 # RT doesn't allow to add groups as members of the
1140 # ticket roles, so we just hide entries in CGM table
1141 # with MemberId == GroupId from results
1142 $self->SUPER::Limit(
1143 LEFTJOIN => $group_members,
1146 VALUE => "$group_members.MemberId",
1149 my $users = $self->Join(
1151 ALIAS1 => $group_members,
1152 FIELD1 => 'MemberId',
1156 return ($groups, $group_members, $users);
1159 =head2 _WatcherMembershipLimit
1161 Handle watcher membership limits, i.e. whether the watcher belongs to a
1162 specific group or not.
1165 1: Field to query on
1167 SELECT DISTINCT main.*
1171 CachedGroupMembers CachedGroupMembers_2,
1174 (main.EffectiveId = main.id)
1176 (main.Status != 'deleted')
1178 (main.Type = 'ticket')
1181 (Users_3.EmailAddress = '22')
1183 (Groups_1.Domain = 'RT::Ticket-Role')
1185 (Groups_1.Type = 'RequestorGroup')
1188 Groups_1.Instance = main.id
1190 Groups_1.id = CachedGroupMembers_2.GroupId
1192 CachedGroupMembers_2.MemberId = Users_3.id
1193 ORDER BY main.id ASC
1198 sub _WatcherMembershipLimit {
1199 my ( $self, $field, $op, $value, @rest ) = @_;
1204 my $groups = $self->NewAlias('Groups');
1205 my $groupmembers = $self->NewAlias('CachedGroupMembers');
1206 my $users = $self->NewAlias('Users');
1207 my $memberships = $self->NewAlias('CachedGroupMembers');
1209 if ( ref $field ) { # gross hack
1210 my @bundle = @$field;
1212 for my $chunk (@bundle) {
1213 ( $field, $op, $value, @rest ) = @$chunk;
1215 ALIAS => $memberships,
1226 ALIAS => $memberships,
1234 # Tie to groups for tickets we care about
1238 VALUE => 'RT::Ticket-Role',
1239 ENTRYAGGREGATOR => 'AND'
1244 FIELD1 => 'Instance',
1251 # If we care about which sort of watcher
1252 my $meta = $FIELD_METADATA{$field};
1253 my $type = ( defined $meta->[1] ? $meta->[1] : undef );
1260 ENTRYAGGREGATOR => 'AND'
1267 ALIAS2 => $groupmembers,
1272 ALIAS1 => $groupmembers,
1273 FIELD1 => 'MemberId',
1279 ALIAS => $groupmembers,
1280 FIELD => 'Disabled',
1285 ALIAS1 => $memberships,
1286 FIELD1 => 'MemberId',
1292 ALIAS => $memberships,
1293 FIELD => 'Disabled',
1302 =head2 _CustomFieldDecipher
1304 Try and turn a CF descriptor into (cfid, cfname) object pair.
1308 sub _CustomFieldDecipher {
1309 my ($self, $string) = @_;
1311 my ($queue, $field, $column) = ($string =~ /^(?:(.+?)\.)?{(.+)}(?:\.(Content|LargeContent))?$/);
1312 $field ||= ($string =~ /^{(.*?)}$/)[0] || $string;
1316 my $q = RT::Queue->new( $self->CurrentUser );
1320 # $queue = $q->Name; # should we normalize the queue?
1321 $cf = $q->CustomField( $field );
1324 $RT::Logger->warning("Queue '$queue' doesn't exist, parsed from '$string'");
1328 elsif ( $field =~ /\D/ ) {
1330 my $cfs = RT::CustomFields->new( $self->CurrentUser );
1331 $cfs->Limit( FIELD => 'Name', VALUE => $field );
1332 $cfs->LimitToLookupType('RT::Queue-RT::Ticket');
1334 # if there is more then one field the current user can
1335 # see with the same name then we shouldn't return cf object
1336 # as we don't know which one to use
1339 $cf = undef if $cfs->Next;
1343 $cf = RT::CustomField->new( $self->CurrentUser );
1344 $cf->Load( $field );
1347 return ($queue, $field, $cf, $column);
1350 =head2 _CustomFieldJoin
1352 Factor out the Join of custom fields so we can use it for sorting too
1356 sub _CustomFieldJoin {
1357 my ($self, $cfkey, $cfid, $field) = @_;
1358 # Perform one Join per CustomField
1359 if ( $self->{_sql_object_cfv_alias}{$cfkey} ||
1360 $self->{_sql_cf_alias}{$cfkey} )
1362 return ( $self->{_sql_object_cfv_alias}{$cfkey},
1363 $self->{_sql_cf_alias}{$cfkey} );
1366 my ($TicketCFs, $CFs);
1368 $TicketCFs = $self->{_sql_object_cfv_alias}{$cfkey} = $self->Join(
1372 TABLE2 => 'ObjectCustomFieldValues',
1373 FIELD2 => 'ObjectId',
1375 $self->SUPER::Limit(
1376 LEFTJOIN => $TicketCFs,
1377 FIELD => 'CustomField',
1379 ENTRYAGGREGATOR => 'AND'
1383 my $ocfalias = $self->Join(
1386 TABLE2 => 'ObjectCustomFields',
1387 FIELD2 => 'ObjectId',
1390 $self->SUPER::Limit(
1391 LEFTJOIN => $ocfalias,
1392 ENTRYAGGREGATOR => 'OR',
1393 FIELD => 'ObjectId',
1397 $CFs = $self->{_sql_cf_alias}{$cfkey} = $self->Join(
1399 ALIAS1 => $ocfalias,
1400 FIELD1 => 'CustomField',
1401 TABLE2 => 'CustomFields',
1404 $self->SUPER::Limit(
1406 ENTRYAGGREGATOR => 'AND',
1407 FIELD => 'LookupType',
1408 VALUE => 'RT::Queue-RT::Ticket',
1410 $self->SUPER::Limit(
1412 ENTRYAGGREGATOR => 'AND',
1417 $TicketCFs = $self->{_sql_object_cfv_alias}{$cfkey} = $self->Join(
1421 TABLE2 => 'ObjectCustomFieldValues',
1422 FIELD2 => 'CustomField',
1424 $self->SUPER::Limit(
1425 LEFTJOIN => $TicketCFs,
1426 FIELD => 'ObjectId',
1429 ENTRYAGGREGATOR => 'AND',
1432 $self->SUPER::Limit(
1433 LEFTJOIN => $TicketCFs,
1434 FIELD => 'ObjectType',
1435 VALUE => 'RT::Ticket',
1436 ENTRYAGGREGATOR => 'AND'
1438 $self->SUPER::Limit(
1439 LEFTJOIN => $TicketCFs,
1440 FIELD => 'Disabled',
1443 ENTRYAGGREGATOR => 'AND'
1446 return ($TicketCFs, $CFs);
1449 =head2 _CustomFieldLimit
1451 Limit based on CustomFields
1458 use Regexp::Common qw(RE_net_IPv4);
1459 use Regexp::Common::net::CIDR;
1462 sub _CustomFieldLimit {
1463 my ( $self, $_field, $op, $value, %rest ) = @_;
1465 my $field = $rest{'SUBKEY'} || die "No field specified";
1467 # For our sanity, we can only limit on one queue at a time
1469 my ($queue, $cfid, $cf, $column);
1470 ($queue, $field, $cf, $column) = $self->_CustomFieldDecipher( $field );
1471 $cfid = $cf ? $cf->id : 0 ;
1473 # If we're trying to find custom fields that don't match something, we
1474 # want tickets where the custom field has no value at all. Note that
1475 # we explicitly don't include the "IS NULL" case, since we would
1476 # otherwise end up with a redundant clause.
1478 my ($negative_op, $null_op, $inv_op, $range_op)
1479 = $self->ClassifySQLOperation( $op );
1482 return @_ unless RT->Config->Get('DatabaseType') eq 'Oracle';
1485 return %args unless $args{'FIELD'} eq 'LargeContent';
1487 my $op = $args{'OPERATOR'};
1489 $args{'OPERATOR'} = 'MATCHES';
1491 elsif ( $op eq '!=' ) {
1492 $args{'OPERATOR'} = 'NOT MATCHES';
1494 elsif ( $op =~ /^[<>]=?$/ ) {
1495 $args{'FUNCTION'} = "TO_CHAR( $args{'ALIAS'}.LargeContent )";
1500 if ( $cf && $cf->Type eq 'IPAddress' ) {
1501 my $parsed = RT::ObjectCustomFieldValue->ParseIP($value);
1506 $RT::Logger->warn("$value is not a valid IPAddress");
1510 if ( $cf && $cf->Type eq 'IPAddressRange' ) {
1512 if ( $value =~ /^\s*$RE{net}{CIDR}{IPv4}{-keep}\s*$/o ) {
1514 # convert incomplete 192.168/24 to 192.168.0.0/24 format
1516 join( '.', map $_ || 0, ( split /\./, $1 )[ 0 .. 3 ] ) . "/$2"
1520 my ( $start_ip, $end_ip ) =
1521 RT::ObjectCustomFieldValue->ParseIPRange($value);
1522 if ( $start_ip && $end_ip ) {
1523 if ( $op =~ /^([<>])=?$/ ) {
1524 my $is_less = $1 eq '<' ? 1 : 0;
1533 $value = join '-', $start_ip, $end_ip;
1537 $RT::Logger->warn("$value is not a valid IPAddressRange");
1541 my $single_value = !$cf || !$cfid || $cf->SingleValue;
1543 my $cfkey = $cfid ? $cfid : "$queue.$field";
1545 if ( $null_op && !$column ) {
1546 # IS[ NOT] NULL without column is the same as has[ no] any CF value,
1547 # we can reuse our default joins for this operation
1548 # with column specified we have different situation
1549 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
1552 ALIAS => $TicketCFs,
1561 OPERATOR => 'IS NOT',
1564 ENTRYAGGREGATOR => 'AND',
1568 elsif ( $op !~ /^[<>]=?$/ && ( $cf && $cf->Type eq 'IPAddressRange')) {
1570 my ($start_ip, $end_ip) = split /-/, $value;
1573 if ( $op !~ /NOT|!=|<>/i ) { # positive equation
1574 $self->_CustomFieldLimit(
1575 'CF', '<=', $end_ip, %rest,
1576 SUBKEY => $rest{'SUBKEY'}. '.Content',
1578 $self->_CustomFieldLimit(
1579 'CF', '>=', $start_ip, %rest,
1580 SUBKEY => $rest{'SUBKEY'}. '.LargeContent',
1581 ENTRYAGGREGATOR => 'AND',
1583 # as well limit borders so DB optimizers can use better
1584 # estimations and scan less rows
1585 # have to disable this tweak because of ipv6
1586 # $self->_CustomFieldLimit(
1587 # $field, '>=', '000.000.000.000', %rest,
1588 # SUBKEY => $rest{'SUBKEY'}. '.Content',
1589 # ENTRYAGGREGATOR => 'AND',
1591 # $self->_CustomFieldLimit(
1592 # $field, '<=', '255.255.255.255', %rest,
1593 # SUBKEY => $rest{'SUBKEY'}. '.LargeContent',
1594 # ENTRYAGGREGATOR => 'AND',
1597 else { # negative equation
1598 $self->_CustomFieldLimit($field, '>', $end_ip, %rest);
1599 $self->_CustomFieldLimit(
1600 $field, '<', $start_ip, %rest,
1601 SUBKEY => $rest{'SUBKEY'}. '.LargeContent',
1602 ENTRYAGGREGATOR => 'OR',
1604 # TODO: as well limit borders so DB optimizers can use better
1605 # estimations and scan less rows, but it's harder to do
1606 # as we have OR aggregator
1610 elsif ( !$negative_op || $single_value ) {
1611 $cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++ if !$single_value && !$range_op;
1612 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
1619 # if column is defined then deal only with it
1620 # otherwise search in Content and in LargeContent
1622 $self->_SQLLimit( $fix_op->(
1623 ALIAS => $TicketCFs,
1635 # need special treatment for Date
1636 if ( $cf and $cf->Type eq 'DateTime' and $op eq '=' ) {
1638 if ( $value =~ /:/ ) {
1639 # there is time speccified.
1640 my $date = RT::Date->new( $self->CurrentUser );
1641 $date->Set( Format => 'unknown', Value => $value );
1643 ALIAS => $TicketCFs,
1646 VALUE => $date->ISO,
1651 # no time specified, that means we want everything on a
1652 # particular day. in the database, we need to check for >
1653 # and < the edges of that day.
1654 my $date = RT::Date->new( $self->CurrentUser );
1655 $date->Set( Format => 'unknown', Value => $value );
1656 $date->SetToMidnight( Timezone => 'server' );
1657 my $daystart = $date->ISO;
1659 my $dayend = $date->ISO;
1664 ALIAS => $TicketCFs,
1672 ALIAS => $TicketCFs,
1677 ENTRYAGGREGATOR => 'AND',
1683 elsif ( $op eq '=' || $op eq '!=' || $op eq '<>' ) {
1684 if ( length( Encode::encode_utf8($value) ) < 256 ) {
1686 ALIAS => $TicketCFs,
1697 ALIAS => $TicketCFs,
1701 ENTRYAGGREGATOR => 'OR'
1704 ALIAS => $TicketCFs,
1708 ENTRYAGGREGATOR => 'OR'
1711 $self->_SQLLimit( $fix_op->(
1712 ALIAS => $TicketCFs,
1713 FIELD => 'LargeContent',
1716 ENTRYAGGREGATOR => 'AND',
1723 ALIAS => $TicketCFs,
1734 ALIAS => $TicketCFs,
1738 ENTRYAGGREGATOR => 'OR'
1741 ALIAS => $TicketCFs,
1745 ENTRYAGGREGATOR => 'OR'
1748 $self->_SQLLimit( $fix_op->(
1749 ALIAS => $TicketCFs,
1750 FIELD => 'LargeContent',
1753 ENTRYAGGREGATOR => 'AND',
1760 # XXX: if we join via CustomFields table then
1761 # because of order of left joins we get NULLs in
1762 # CF table and then get nulls for those records
1763 # in OCFVs table what result in wrong results
1764 # as decifer method now tries to load a CF then
1765 # we fall into this situation only when there
1766 # are more than one CF with the name in the DB.
1767 # the same thing applies to order by call.
1768 # TODO: reorder joins T <- OCFVs <- CFs <- OCFs if
1769 # we want treat IS NULL as (not applies or has
1774 OPERATOR => 'IS NOT',
1777 ENTRYAGGREGATOR => 'AND',
1783 ALIAS => $TicketCFs,
1784 FIELD => $column || 'Content',
1788 ENTRYAGGREGATOR => 'OR',
1796 $cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++;
1797 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
1800 $op =~ s/!|NOT\s+//i;
1802 # if column is defined then deal only with it
1803 # otherwise search in Content and in LargeContent
1805 $self->SUPER::Limit( $fix_op->(
1806 LEFTJOIN => $TicketCFs,
1807 ALIAS => $TicketCFs,
1815 $self->SUPER::Limit(
1816 LEFTJOIN => $TicketCFs,
1817 ALIAS => $TicketCFs,
1826 ALIAS => $TicketCFs,
1835 sub _HasAttributeLimit {
1836 my ( $self, $field, $op, $value, %rest ) = @_;
1838 my $alias = $self->Join(
1842 TABLE2 => 'Attributes',
1843 FIELD2 => 'ObjectId',
1845 $self->SUPER::Limit(
1847 FIELD => 'ObjectType',
1848 VALUE => 'RT::Ticket',
1849 ENTRYAGGREGATOR => 'AND'
1851 $self->SUPER::Limit(
1856 ENTRYAGGREGATOR => 'AND'
1862 OPERATOR => $FIELD_METADATA{$field}->[1]? 'IS NOT': 'IS',
1869 # End Helper Functions
1871 # End of SQL Stuff -------------------------------------------------
1874 =head2 OrderByCols ARRAY
1876 A modified version of the OrderBy method which automatically joins where
1877 C<ALIAS> is set to the name of a watcher type.
1888 foreach my $row (@args) {
1889 if ( $row->{ALIAS} ) {
1893 if ( $row->{FIELD} !~ /\./ ) {
1894 my $meta = $self->FIELDS->{ $row->{FIELD} };
1900 if ( $meta->[0] eq 'ENUM' && ($meta->[1]||'') eq 'Queue' ) {
1901 my $alias = $self->Join(
1904 FIELD1 => $row->{'FIELD'},
1908 push @res, { %$row, ALIAS => $alias, FIELD => "Name" };
1909 } elsif ( ( $meta->[0] eq 'ENUM' && ($meta->[1]||'') eq 'User' )
1910 || ( $meta->[0] eq 'WATCHERFIELD' && ($meta->[1]||'') eq 'Owner' )
1912 my $alias = $self->Join(
1915 FIELD1 => $row->{'FIELD'},
1919 push @res, { %$row, ALIAS => $alias, FIELD => "Name" };
1926 my ( $field, $subkey ) = split /\./, $row->{FIELD}, 2;
1927 my $meta = $self->FIELDS->{$field};
1928 if ( defined $meta->[0] && $meta->[0] eq 'WATCHERFIELD' ) {
1929 # cache alias as we want to use one alias per watcher type for sorting
1930 my $users = $self->{_sql_u_watchers_alias_for_sort}{ $meta->[1] };
1932 $self->{_sql_u_watchers_alias_for_sort}{ $meta->[1] }
1933 = $users = ( $self->_WatcherJoin( $meta->[1] ) )[2];
1935 push @res, { %$row, ALIAS => $users, FIELD => $subkey };
1936 } elsif ( defined $meta->[0] && $meta->[0] eq 'CUSTOMFIELD' ) {
1937 my ($queue, $field, $cf_obj, $column) = $self->_CustomFieldDecipher( $subkey );
1938 my $cfkey = $cf_obj ? $cf_obj->id : "$queue.$field";
1939 $cfkey .= ".ordering" if !$cf_obj || ($cf_obj->MaxValues||0) != 1;
1940 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, ($cf_obj ?$cf_obj->id :0) , $field );
1941 # this is described in _CustomFieldLimit
1945 OPERATOR => 'IS NOT',
1948 ENTRYAGGREGATOR => 'AND',
1951 # For those cases where we are doing a join against the
1952 # CF name, and don't have a CFid, use Unique to make sure
1953 # we don't show duplicate tickets. NOTE: I'm pretty sure
1954 # this will stay mixed in for the life of the
1955 # class/package, and not just for the life of the object.
1956 # Potential performance issue.
1957 require DBIx::SearchBuilder::Unique;
1958 DBIx::SearchBuilder::Unique->import;
1960 my $CFvs = $self->Join(
1962 ALIAS1 => $TicketCFs,
1963 FIELD1 => 'CustomField',
1964 TABLE2 => 'CustomFieldValues',
1965 FIELD2 => 'CustomField',
1967 $self->SUPER::Limit(
1971 VALUE => $TicketCFs . ".Content",
1972 ENTRYAGGREGATOR => 'AND'
1975 push @res, { %$row, ALIAS => $CFvs, FIELD => 'SortOrder' };
1976 push @res, { %$row, ALIAS => $TicketCFs, FIELD => 'Content' };
1977 } elsif ( $field eq "Custom" && $subkey eq "Ownership") {
1978 # PAW logic is "reversed"
1980 if (exists $row->{ORDER} ) {
1981 my $o = $row->{ORDER};
1982 delete $row->{ORDER};
1983 $order = "DESC" if $o =~ /asc/i;
1986 # Ticket.Owner 1 0 X
1987 # Unowned Tickets 0 1 X
1990 foreach my $uid ( $self->CurrentUser->Id, RT->Nobody->Id ) {
1991 if ( RT->Config->Get('DatabaseType') eq 'Oracle' ) {
1992 my $f = ($row->{'ALIAS'} || 'main') .'.Owner';
1997 FUNCTION => "CASE WHEN $f=$uid THEN 1 ELSE 0 END",
2004 FUNCTION => "Owner=$uid",
2010 push @res, { %$row, FIELD => "Priority", ORDER => $order } ;
2016 return $self->SUPER::OrderByCols(@res);
2024 Takes a paramhash with the fields FIELD, OPERATOR, VALUE and DESCRIPTION
2025 Generally best called from LimitFoo methods
2035 DESCRIPTION => undef,
2038 $args{'DESCRIPTION'} = $self->loc(
2039 "[_1] [_2] [_3]", $args{'FIELD'},
2040 $args{'OPERATOR'}, $args{'VALUE'}
2042 if ( !defined $args{'DESCRIPTION'} );
2044 my $index = $self->_NextIndex;
2046 # make the TicketRestrictions hash the equivalent of whatever we just passed in;
2048 %{ $self->{'TicketRestrictions'}{$index} } = %args;
2050 $self->{'RecalcTicketLimits'} = 1;
2052 # If we're looking at the effective id, we don't want to append the other clause
2053 # which limits us to tickets where id = effective id
2054 if ( $args{'FIELD'} eq 'EffectiveId'
2055 && ( !$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) )
2057 $self->{'looking_at_effective_id'} = 1;
2060 if ( $args{'FIELD'} eq 'Type'
2061 && ( !$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) )
2063 $self->{'looking_at_type'} = 1;
2074 LimitQueue takes a paramhash with the fields OPERATOR and VALUE.
2075 OPERATOR is one of = or !=. (It defaults to =).
2076 VALUE is a queue id or Name.
2089 #TODO VALUE should also take queue objects
2090 if ( defined $args{'VALUE'} && $args{'VALUE'} !~ /^\d+$/ ) {
2091 my $queue = RT::Queue->new( $self->CurrentUser );
2092 $queue->Load( $args{'VALUE'} );
2093 $args{'VALUE'} = $queue->Id;
2096 # What if they pass in an Id? Check for isNum() and convert to
2099 #TODO check for a valid queue here
2103 VALUE => $args{'VALUE'},
2104 OPERATOR => $args{'OPERATOR'},
2105 DESCRIPTION => join(
2106 ' ', $self->loc('Queue'), $args{'OPERATOR'}, $args{'VALUE'},
2116 Takes a paramhash with the fields OPERATOR and VALUE.
2117 OPERATOR is one of = or !=.
2120 RT adds Status != 'deleted' until object has
2121 allow_deleted_search internal property set.
2122 $tickets->{'allow_deleted_search'} = 1;
2123 $tickets->LimitStatus( VALUE => 'deleted' );
2135 VALUE => $args{'VALUE'},
2136 OPERATOR => $args{'OPERATOR'},
2137 DESCRIPTION => join( ' ',
2138 $self->loc('Status'), $args{'OPERATOR'},
2139 $self->loc( $args{'VALUE'} ) ),
2147 If called, this search will not automatically limit the set of results found
2148 to tickets of type "Ticket". Tickets of other types, such as "project" and
2149 "approval" will be found.
2156 # Instead of faking a Limit that later gets ignored, fake up the
2157 # fact that we're already looking at type, so that the check in
2158 # Tickets_SQL/FromSQL goes down the right branch
2160 # $self->LimitType(VALUE => '__any');
2161 $self->{looking_at_type} = 1;
2168 Takes a paramhash with the fields OPERATOR and VALUE.
2169 OPERATOR is one of = or !=, it defaults to "=".
2170 VALUE is a string to search for in the type of the ticket.
2185 VALUE => $args{'VALUE'},
2186 OPERATOR => $args{'OPERATOR'},
2187 DESCRIPTION => join( ' ',
2188 $self->loc('Type'), $args{'OPERATOR'}, $args{'Limit'}, ),
2198 Takes a paramhash with the fields OPERATOR and VALUE.
2199 OPERATOR is one of = or !=.
2200 VALUE is a string to search for in the subject of the ticket.
2209 VALUE => $args{'VALUE'},
2210 OPERATOR => $args{'OPERATOR'},
2211 DESCRIPTION => join( ' ',
2212 $self->loc('Subject'), $args{'OPERATOR'}, $args{'VALUE'}, ),
2218 # Things that can be > < = !=
2223 Takes a paramhash with the fields OPERATOR and VALUE.
2224 OPERATOR is one of =, >, < or !=.
2225 VALUE is a ticket Id to search for
2238 VALUE => $args{'VALUE'},
2239 OPERATOR => $args{'OPERATOR'},
2241 join( ' ', $self->loc('Id'), $args{'OPERATOR'}, $args{'VALUE'}, ),
2247 =head2 LimitPriority
2249 Takes a paramhash with the fields OPERATOR and VALUE.
2250 OPERATOR is one of =, >, < or !=.
2251 VALUE is a value to match the ticket's priority against
2259 FIELD => 'Priority',
2260 VALUE => $args{'VALUE'},
2261 OPERATOR => $args{'OPERATOR'},
2262 DESCRIPTION => join( ' ',
2263 $self->loc('Priority'),
2264 $args{'OPERATOR'}, $args{'VALUE'}, ),
2270 =head2 LimitInitialPriority
2272 Takes a paramhash with the fields OPERATOR and VALUE.
2273 OPERATOR is one of =, >, < or !=.
2274 VALUE is a value to match the ticket's initial priority against
2279 sub LimitInitialPriority {
2283 FIELD => 'InitialPriority',
2284 VALUE => $args{'VALUE'},
2285 OPERATOR => $args{'OPERATOR'},
2286 DESCRIPTION => join( ' ',
2287 $self->loc('Initial Priority'), $args{'OPERATOR'},
2294 =head2 LimitFinalPriority
2296 Takes a paramhash with the fields OPERATOR and VALUE.
2297 OPERATOR is one of =, >, < or !=.
2298 VALUE is a value to match the ticket's final priority against
2302 sub LimitFinalPriority {
2306 FIELD => 'FinalPriority',
2307 VALUE => $args{'VALUE'},
2308 OPERATOR => $args{'OPERATOR'},
2309 DESCRIPTION => join( ' ',
2310 $self->loc('Final Priority'), $args{'OPERATOR'},
2317 =head2 LimitTimeWorked
2319 Takes a paramhash with the fields OPERATOR and VALUE.
2320 OPERATOR is one of =, >, < or !=.
2321 VALUE is a value to match the ticket's TimeWorked attribute
2325 sub LimitTimeWorked {
2329 FIELD => 'TimeWorked',
2330 VALUE => $args{'VALUE'},
2331 OPERATOR => $args{'OPERATOR'},
2332 DESCRIPTION => join( ' ',
2333 $self->loc('Time Worked'),
2334 $args{'OPERATOR'}, $args{'VALUE'}, ),
2340 =head2 LimitTimeLeft
2342 Takes a paramhash with the fields OPERATOR and VALUE.
2343 OPERATOR is one of =, >, < or !=.
2344 VALUE is a value to match the ticket's TimeLeft attribute
2352 FIELD => 'TimeLeft',
2353 VALUE => $args{'VALUE'},
2354 OPERATOR => $args{'OPERATOR'},
2355 DESCRIPTION => join( ' ',
2356 $self->loc('Time Left'),
2357 $args{'OPERATOR'}, $args{'VALUE'}, ),
2367 Takes a paramhash with the fields OPERATOR and VALUE.
2368 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2369 VALUE is a string to search for in the body of the ticket
2378 VALUE => $args{'VALUE'},
2379 OPERATOR => $args{'OPERATOR'},
2380 DESCRIPTION => join( ' ',
2381 $self->loc('Ticket content'), $args{'OPERATOR'},
2388 =head2 LimitFilename
2390 Takes a paramhash with the fields OPERATOR and VALUE.
2391 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2392 VALUE is a string to search for in the body of the ticket
2400 FIELD => 'Filename',
2401 VALUE => $args{'VALUE'},
2402 OPERATOR => $args{'OPERATOR'},
2403 DESCRIPTION => join( ' ',
2404 $self->loc('Attachment filename'), $args{'OPERATOR'},
2410 =head2 LimitContentType
2412 Takes a paramhash with the fields OPERATOR and VALUE.
2413 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2414 VALUE is a content type to search ticket attachments for
2418 sub LimitContentType {
2422 FIELD => 'ContentType',
2423 VALUE => $args{'VALUE'},
2424 OPERATOR => $args{'OPERATOR'},
2425 DESCRIPTION => join( ' ',
2426 $self->loc('Ticket content type'), $args{'OPERATOR'},
2437 Takes a paramhash with the fields OPERATOR and VALUE.
2438 OPERATOR is one of = or !=.
2450 my $owner = RT::User->new( $self->CurrentUser );
2451 $owner->Load( $args{'VALUE'} );
2453 # FIXME: check for a valid $owner
2456 VALUE => $args{'VALUE'},
2457 OPERATOR => $args{'OPERATOR'},
2458 DESCRIPTION => join( ' ',
2459 $self->loc('Owner'), $args{'OPERATOR'}, $owner->Name(), ),
2469 Takes a paramhash with the fields OPERATOR, TYPE and VALUE.
2470 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2471 VALUE is a value to match the ticket's watcher email addresses against
2472 TYPE is the sort of watchers you want to match against. Leave it undef if you want to search all of them
2486 #build us up a description
2487 my ( $watcher_type, $desc );
2488 if ( $args{'TYPE'} ) {
2489 $watcher_type = $args{'TYPE'};
2492 $watcher_type = "Watcher";
2496 FIELD => $watcher_type,
2497 VALUE => $args{'VALUE'},
2498 OPERATOR => $args{'OPERATOR'},
2499 TYPE => $args{'TYPE'},
2500 DESCRIPTION => join( ' ',
2501 $self->loc($watcher_type),
2502 $args{'OPERATOR'}, $args{'VALUE'}, ),
2511 =head2 LimitLinkedTo
2513 LimitLinkedTo takes a paramhash with two fields: TYPE and TARGET
2514 TYPE limits the sort of link we want to search on
2516 TYPE = { RefersTo, MemberOf, DependsOn }
2518 TARGET is the id or URI of the TARGET of the link
2532 FIELD => 'LinkedTo',
2534 TARGET => $args{'TARGET'},
2535 TYPE => $args{'TYPE'},
2536 DESCRIPTION => $self->loc(
2537 "Tickets [_1] by [_2]",
2538 $self->loc( $args{'TYPE'} ),
2541 OPERATOR => $args{'OPERATOR'},
2547 =head2 LimitLinkedFrom
2549 LimitLinkedFrom takes a paramhash with two fields: TYPE and BASE
2550 TYPE limits the sort of link we want to search on
2553 BASE is the id or URI of the BASE of the link
2557 sub LimitLinkedFrom {
2566 # translate RT2 From/To naming to RT3 TicketSQL naming
2567 my %fromToMap = qw(DependsOn DependentOn
2569 RefersTo ReferredToBy);
2571 my $type = $args{'TYPE'};
2572 $type = $fromToMap{$type} if exists( $fromToMap{$type} );
2575 FIELD => 'LinkedTo',
2577 BASE => $args{'BASE'},
2579 DESCRIPTION => $self->loc(
2580 "Tickets [_1] [_2]",
2581 $self->loc( $args{'TYPE'} ),
2584 OPERATOR => $args{'OPERATOR'},
2591 my $ticket_id = shift;
2592 return $self->LimitLinkedTo(
2594 TARGET => $ticket_id,
2600 sub LimitHasMember {
2602 my $ticket_id = shift;
2603 return $self->LimitLinkedFrom(
2605 BASE => "$ticket_id",
2606 TYPE => 'HasMember',
2613 sub LimitDependsOn {
2615 my $ticket_id = shift;
2616 return $self->LimitLinkedTo(
2618 TARGET => $ticket_id,
2619 TYPE => 'DependsOn',
2626 sub LimitDependedOnBy {
2628 my $ticket_id = shift;
2629 return $self->LimitLinkedFrom(
2632 TYPE => 'DependentOn',
2641 my $ticket_id = shift;
2642 return $self->LimitLinkedTo(
2644 TARGET => $ticket_id,
2652 sub LimitReferredToBy {
2654 my $ticket_id = shift;
2655 return $self->LimitLinkedFrom(
2658 TYPE => 'ReferredToBy',
2666 =head2 LimitDate (FIELD => 'DateField', OPERATOR => $oper, VALUE => $ISODate)
2668 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
2670 OPERATOR is one of > or <
2671 VALUE is a date and time in ISO format in GMT
2672 FIELD is one of Starts, Started, Told, Created, Resolved, LastUpdated
2674 There are also helper functions of the form LimitFIELD that eliminate
2675 the need to pass in a FIELD argument.
2689 #Set the description if we didn't get handed it above
2690 unless ( $args{'DESCRIPTION'} ) {
2691 $args{'DESCRIPTION'} = $args{'FIELD'} . " "
2692 . $args{'OPERATOR'} . " "
2693 . $args{'VALUE'} . " GMT";
2696 $self->Limit(%args);
2703 $self->LimitDate( FIELD => 'Created', @_ );
2708 $self->LimitDate( FIELD => 'Due', @_ );
2714 $self->LimitDate( FIELD => 'Starts', @_ );
2720 $self->LimitDate( FIELD => 'Started', @_ );
2725 $self->LimitDate( FIELD => 'Resolved', @_ );
2730 $self->LimitDate( FIELD => 'Told', @_ );
2733 sub LimitLastUpdated {
2735 $self->LimitDate( FIELD => 'LastUpdated', @_ );
2740 =head2 LimitTransactionDate (OPERATOR => $oper, VALUE => $ISODate)
2742 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
2744 OPERATOR is one of > or <
2745 VALUE is a date and time in ISO format in GMT
2750 sub LimitTransactionDate {
2753 FIELD => 'TransactionDate',
2760 # <20021217042756.GK28744@pallas.fsck.com>
2761 # "Kill It" - Jesse.
2763 #Set the description if we didn't get handed it above
2764 unless ( $args{'DESCRIPTION'} ) {
2765 $args{'DESCRIPTION'} = $args{'FIELD'} . " "
2766 . $args{'OPERATOR'} . " "
2767 . $args{'VALUE'} . " GMT";
2770 $self->Limit(%args);
2777 =head2 LimitCustomField
2779 Takes a paramhash of key/value pairs with the following keys:
2783 =item CUSTOMFIELD - CustomField name or id. If a name is passed, an additional parameter QUEUE may also be passed to distinguish the custom field.
2785 =item OPERATOR - The usual Limit operators
2787 =item VALUE - The value to compare against
2793 sub LimitCustomField {
2797 CUSTOMFIELD => undef,
2799 DESCRIPTION => undef,
2800 FIELD => 'CustomFieldValue',
2805 my $CF = RT::CustomField->new( $self->CurrentUser );
2806 if ( $args{CUSTOMFIELD} =~ /^\d+$/ ) {
2807 $CF->Load( $args{CUSTOMFIELD} );
2810 $CF->LoadByNameAndQueue(
2811 Name => $args{CUSTOMFIELD},
2812 Queue => $args{QUEUE}
2814 $args{CUSTOMFIELD} = $CF->Id;
2817 #If we are looking to compare with a null value.
2818 if ( $args{'OPERATOR'} =~ /^is$/i ) {
2819 $args{'DESCRIPTION'}
2820 ||= $self->loc( "Custom field [_1] has no value.", $CF->Name );
2822 elsif ( $args{'OPERATOR'} =~ /^is not$/i ) {
2823 $args{'DESCRIPTION'}
2824 ||= $self->loc( "Custom field [_1] has a value.", $CF->Name );
2827 # if we're not looking to compare with a null value
2829 $args{'DESCRIPTION'} ||= $self->loc( "Custom field [_1] [_2] [_3]",
2830 $CF->Name, $args{OPERATOR}, $args{VALUE} );
2833 if ( defined $args{'QUEUE'} && $args{'QUEUE'} =~ /\D/ ) {
2834 my $QueueObj = RT::Queue->new( $self->CurrentUser );
2835 $QueueObj->Load( $args{'QUEUE'} );
2836 $args{'QUEUE'} = $QueueObj->Id;
2838 delete $args{'QUEUE'} unless defined $args{'QUEUE'} && length $args{'QUEUE'};
2841 @rest = ( ENTRYAGGREGATOR => 'AND' )
2842 if ( $CF->Type eq 'SelectMultiple' );
2845 VALUE => $args{VALUE},
2847 .(defined $args{'QUEUE'}? ".{$args{'QUEUE'}}" : '' )
2848 .".{" . $CF->Name . "}",
2849 OPERATOR => $args{OPERATOR},
2854 $self->{'RecalcTicketLimits'} = 1;
2861 Keep track of the counter for the array of restrictions
2867 return ( $self->{'restriction_index'}++ );
2875 $self->{'table'} = "Tickets";
2876 $self->{'RecalcTicketLimits'} = 1;
2877 $self->{'looking_at_effective_id'} = 0;
2878 $self->{'looking_at_type'} = 0;
2879 $self->{'restriction_index'} = 1;
2880 $self->{'primary_key'} = "id";
2881 delete $self->{'items_array'};
2882 delete $self->{'item_map'};
2883 delete $self->{'columns_to_display'};
2884 $self->SUPER::_Init(@_);
2893 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2894 return ( $self->SUPER::Count() );
2900 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2901 return ( $self->SUPER::CountAll() );
2906 =head2 ItemsArrayRef
2908 Returns a reference to the set of all items found in this search
2915 return $self->{'items_array'} if $self->{'items_array'};
2917 my $placeholder = $self->_ItemsCounter;
2918 $self->GotoFirstItem();
2919 while ( my $item = $self->Next ) {
2920 push( @{ $self->{'items_array'} }, $item );
2922 $self->GotoItem($placeholder);
2923 $self->{'items_array'}
2924 = $self->ItemsOrderBy( $self->{'items_array'} );
2926 return $self->{'items_array'};
2929 sub ItemsArrayRefWindow {
2933 my @old = ($self->_ItemsCounter, $self->RowsPerPage, $self->FirstRow+1);
2935 $self->RowsPerPage( $window );
2937 $self->GotoFirstItem;
2940 while ( my $item = $self->Next ) {
2944 $self->RowsPerPage( $old[1] );
2945 $self->FirstRow( $old[2] );
2946 $self->GotoItem( $old[0] );
2955 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2957 my $Ticket = $self->SUPER::Next;
2958 return $Ticket unless $Ticket;
2960 if ( $Ticket->__Value('Status') eq 'deleted'
2961 && !$self->{'allow_deleted_search'} )
2965 elsif ( RT->Config->Get('UseSQLForACLChecks') ) {
2966 # if we found a ticket with this option enabled then
2967 # all tickets we found are ACLed, cache this fact
2968 my $key = join ";:;", $self->CurrentUser->id, 'ShowTicket', 'RT::Ticket-'. $Ticket->id;
2969 $RT::Principal::_ACL_CACHE->set( $key => 1 );
2972 elsif ( $Ticket->CurrentUserHasRight('ShowTicket') ) {
2977 # If the user doesn't have the right to show this ticket
2984 $self->CurrentUserCanSee if RT->Config->Get('UseSQLForACLChecks');
2985 return $self->SUPER::_DoSearch( @_ );
2990 $self->CurrentUserCanSee if RT->Config->Get('UseSQLForACLChecks');
2991 return $self->SUPER::_DoCount( @_ );
2997 my $cache_key = 'RolesHasRight;:;ShowTicket';
2999 if ( my $cached = $RT::Principal::_ACL_CACHE->fetch( $cache_key ) ) {
3003 my $ACL = RT::ACL->new( RT->SystemUser );
3004 $ACL->Limit( FIELD => 'RightName', VALUE => 'ShowTicket' );
3005 $ACL->Limit( FIELD => 'PrincipalType', OPERATOR => '!=', VALUE => 'Group' );
3006 my $principal_alias = $ACL->Join(
3008 FIELD1 => 'PrincipalId',
3009 TABLE2 => 'Principals',
3012 $ACL->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3015 foreach my $ACE ( @{ $ACL->ItemsArrayRef } ) {
3016 my $role = $ACE->__Value('PrincipalType');
3017 my $type = $ACE->__Value('ObjectType');
3018 if ( $type eq 'RT::System' ) {
3021 elsif ( $type eq 'RT::Queue' ) {
3022 next if $res{ $role } && !ref $res{ $role };
3023 push @{ $res{ $role } ||= [] }, $ACE->__Value('ObjectId');
3026 $RT::Logger->error('ShowTicket right is granted on unsupported object');
3029 $RT::Principal::_ACL_CACHE->set( $cache_key => \%res );
3033 sub _DirectlyCanSeeIn {
3035 my $id = $self->CurrentUser->id;
3037 my $cache_key = 'User-'. $id .';:;ShowTicket;:;DirectlyCanSeeIn';
3038 if ( my $cached = $RT::Principal::_ACL_CACHE->fetch( $cache_key ) ) {
3042 my $ACL = RT::ACL->new( RT->SystemUser );
3043 $ACL->Limit( FIELD => 'RightName', VALUE => 'ShowTicket' );
3044 my $principal_alias = $ACL->Join(
3046 FIELD1 => 'PrincipalId',
3047 TABLE2 => 'Principals',
3050 $ACL->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3051 my $cgm_alias = $ACL->Join(
3053 FIELD1 => 'PrincipalId',
3054 TABLE2 => 'CachedGroupMembers',
3055 FIELD2 => 'GroupId',
3057 $ACL->Limit( ALIAS => $cgm_alias, FIELD => 'MemberId', VALUE => $id );
3058 $ACL->Limit( ALIAS => $cgm_alias, FIELD => 'Disabled', VALUE => 0 );
3061 foreach my $ACE ( @{ $ACL->ItemsArrayRef } ) {
3062 my $type = $ACE->__Value('ObjectType');
3063 if ( $type eq 'RT::System' ) {
3064 # If user is direct member of a group that has the right
3065 # on the system then he can see any ticket
3066 $RT::Principal::_ACL_CACHE->set( $cache_key => [-1] );
3069 elsif ( $type eq 'RT::Queue' ) {
3070 push @res, $ACE->__Value('ObjectId');
3073 $RT::Logger->error('ShowTicket right is granted on unsupported object');
3076 $RT::Principal::_ACL_CACHE->set( $cache_key => \@res );
3080 sub CurrentUserCanSee {
3082 return if $self->{'_sql_current_user_can_see_applied'};
3084 return $self->{'_sql_current_user_can_see_applied'} = 1
3085 if $self->CurrentUser->UserObj->HasRight(
3086 Right => 'SuperUser', Object => $RT::System
3089 my $id = $self->CurrentUser->id;
3091 # directly can see in all queues then we have nothing to do
3092 my @direct_queues = $self->_DirectlyCanSeeIn;
3093 return $self->{'_sql_current_user_can_see_applied'} = 1
3094 if @direct_queues && $direct_queues[0] == -1;
3096 my %roles = $self->_RolesCanSee;
3098 my %skip = map { $_ => 1 } @direct_queues;
3099 foreach my $role ( keys %roles ) {
3100 next unless ref $roles{ $role };
3102 my @queues = grep !$skip{$_}, @{ $roles{ $role } };
3104 $roles{ $role } = \@queues;
3106 delete $roles{ $role };
3111 # there is no global watchers, only queues and tickes, if at
3112 # some point we will add global roles then it's gonna blow
3113 # the idea here is that if the right is set globaly for a role
3114 # and user plays this role for a queue directly not a ticket
3115 # then we have to check in advance
3116 if ( my @tmp = grep $_ ne 'Owner' && !ref $roles{ $_ }, keys %roles ) {
3118 my $groups = RT::Groups->new( RT->SystemUser );
3119 $groups->Limit( FIELD => 'Domain', VALUE => 'RT::Queue-Role' );
3121 $groups->Limit( FIELD => 'Type', VALUE => $_ );
3123 my $principal_alias = $groups->Join(
3126 TABLE2 => 'Principals',
3129 $groups->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3130 my $cgm_alias = $groups->Join(
3133 TABLE2 => 'CachedGroupMembers',
3134 FIELD2 => 'GroupId',
3136 $groups->Limit( ALIAS => $cgm_alias, FIELD => 'MemberId', VALUE => $id );
3137 $groups->Limit( ALIAS => $cgm_alias, FIELD => 'Disabled', VALUE => 0 );
3138 while ( my $group = $groups->Next ) {
3139 push @direct_queues, $group->Instance;
3143 unless ( @direct_queues || keys %roles ) {
3144 $self->SUPER::Limit(
3149 ENTRYAGGREGATOR => 'AND',
3151 return $self->{'_sql_current_user_can_see_applied'} = 1;
3155 my $join_roles = keys %roles;
3156 $join_roles = 0 if $join_roles == 1 && $roles{'Owner'};
3157 my ($role_group_alias, $cgm_alias);
3158 if ( $join_roles ) {
3159 $role_group_alias = $self->_RoleGroupsJoin( New => 1 );
3160 $cgm_alias = $self->_GroupMembersJoin( GroupsAlias => $role_group_alias );
3161 $self->SUPER::Limit(
3162 LEFTJOIN => $cgm_alias,
3163 FIELD => 'MemberId',
3168 my $limit_queues = sub {
3172 return unless @queues;
3173 if ( @queues == 1 ) {
3174 $self->SUPER::Limit(
3179 ENTRYAGGREGATOR => $ea,
3182 $self->SUPER::_OpenParen('ACL');
3183 foreach my $q ( @queues ) {
3184 $self->SUPER::Limit(
3189 ENTRYAGGREGATOR => $ea,
3193 $self->SUPER::_CloseParen('ACL');
3198 $self->SUPER::_OpenParen('ACL');
3200 $ea = 'OR' if $limit_queues->( $ea, @direct_queues );
3201 while ( my ($role, $queues) = each %roles ) {
3202 $self->SUPER::_OpenParen('ACL');
3203 if ( $role eq 'Owner' ) {
3204 $self->SUPER::Limit(
3208 ENTRYAGGREGATOR => $ea,
3212 $self->SUPER::Limit(
3214 ALIAS => $cgm_alias,
3215 FIELD => 'MemberId',
3216 OPERATOR => 'IS NOT',
3219 ENTRYAGGREGATOR => $ea,
3221 $self->SUPER::Limit(
3223 ALIAS => $role_group_alias,
3226 ENTRYAGGREGATOR => 'AND',
3229 $limit_queues->( 'AND', @$queues ) if ref $queues;
3230 $ea = 'OR' if $ea eq 'AND';
3231 $self->SUPER::_CloseParen('ACL');
3233 $self->SUPER::_CloseParen('ACL');
3235 return $self->{'_sql_current_user_can_see_applied'} = 1;
3242 =head2 LoadRestrictions
3244 LoadRestrictions takes a string which can fully populate the TicketRestrictons hash.
3245 TODO It is not yet implemented
3251 =head2 DescribeRestrictions
3254 Returns a hash keyed by restriction id.
3255 Each element of the hash is currently a one element hash that contains DESCRIPTION which
3256 is a description of the purpose of that TicketRestriction
3260 sub DescribeRestrictions {
3265 foreach my $row ( keys %{ $self->{'TicketRestrictions'} } ) {
3266 $listing{$row} = $self->{'TicketRestrictions'}{$row}{'DESCRIPTION'};
3273 =head2 RestrictionValues FIELD
3275 Takes a restriction field and returns a list of values this field is restricted
3280 sub RestrictionValues {
3283 map $self->{'TicketRestrictions'}{$_}{'VALUE'}, grep {
3284 $self->{'TicketRestrictions'}{$_}{'FIELD'} eq $field
3285 && $self->{'TicketRestrictions'}{$_}{'OPERATOR'} eq "="
3287 keys %{ $self->{'TicketRestrictions'} };
3292 =head2 ClearRestrictions
3294 Removes all restrictions irretrievably
3298 sub ClearRestrictions {
3300 delete $self->{'TicketRestrictions'};
3301 $self->{'looking_at_effective_id'} = 0;
3302 $self->{'looking_at_type'} = 0;
3303 $self->{'RecalcTicketLimits'} = 1;
3308 =head2 DeleteRestriction
3310 Takes the row Id of a restriction (From DescribeRestrictions' output, for example.
3311 Removes that restriction from the session's limits.
3315 sub DeleteRestriction {
3318 delete $self->{'TicketRestrictions'}{$row};
3320 $self->{'RecalcTicketLimits'} = 1;
3322 #make the underlying easysearch object forget all its preconceptions
3327 # Convert a set of oldstyle SB Restrictions to Clauses for RQL
3329 sub _RestrictionsToClauses {
3333 foreach my $row ( keys %{ $self->{'TicketRestrictions'} } ) {
3334 my $restriction = $self->{'TicketRestrictions'}{$row};
3336 # We need to reimplement the subclause aggregation that SearchBuilder does.
3337 # Default Subclause is ALIAS.FIELD, and default ALIAS is 'main',
3338 # Then SB AND's the different Subclauses together.
3340 # So, we want to group things into Subclauses, convert them to
3341 # SQL, and then join them with the appropriate DefaultEA.
3342 # Then join each subclause group with AND.
3344 my $field = $restriction->{'FIELD'};
3345 my $realfield = $field; # CustomFields fake up a fieldname, so
3346 # we need to figure that out
3349 # Rewrite LinkedTo meta field to the real field
3350 if ( $field =~ /LinkedTo/ ) {
3351 $realfield = $field = $restriction->{'TYPE'};
3355 # Handle subkey fields with a different real field
3356 if ( $field =~ /^(\w+)\./ ) {
3360 die "I don't know about $field yet"
3361 unless ( exists $FIELD_METADATA{$realfield}
3362 or $restriction->{CUSTOMFIELD} );
3364 my $type = $FIELD_METADATA{$realfield}->[0];
3365 my $op = $restriction->{'OPERATOR'};
3369 map { $restriction->{$_} } qw(VALUE TICKET BASE TARGET)
3372 # this performs the moral equivalent of defined or/dor/C<//>,
3373 # without the short circuiting.You need to use a 'defined or'
3374 # type thing instead of just checking for truth values, because
3375 # VALUE could be 0.(i.e. "false")
3377 # You could also use this, but I find it less aesthetic:
3378 # (although it does short circuit)
3379 #( defined $restriction->{'VALUE'}? $restriction->{VALUE} :
3380 # defined $restriction->{'TICKET'} ?
3381 # $restriction->{TICKET} :
3382 # defined $restriction->{'BASE'} ?
3383 # $restriction->{BASE} :
3384 # defined $restriction->{'TARGET'} ?
3385 # $restriction->{TARGET} )
3387 my $ea = $restriction->{ENTRYAGGREGATOR}
3388 || $DefaultEA{$type}
3391 die "Invalid operator $op for $field ($type)"
3392 unless exists $ea->{$op};
3396 # Each CustomField should be put into a different Clause so they
3397 # are ANDed together.
3398 if ( $restriction->{CUSTOMFIELD} ) {
3399 $realfield = $field;
3402 exists $clause{$realfield} or $clause{$realfield} = [];
3405 $field =~ s!(['\\])!\\$1!g;
3406 $value =~ s!(['\\])!\\$1!g;
3407 my $data = [ $ea, $type, $field, $op, $value ];
3409 # here is where we store extra data, say if it's a keyword or
3410 # something. (I.e. "TYPE SPECIFIC STUFF")
3412 if (lc $ea eq 'none') {
3413 $clause{$realfield} = [ $data ];
3415 push @{ $clause{$realfield} }, $data;
3423 =head2 _ProcessRestrictions PARAMHASH
3425 # The new _ProcessRestrictions is somewhat dependent on the SQL stuff,
3426 # but isn't quite generic enough to move into Tickets_SQL.
3430 sub _ProcessRestrictions {
3433 #Blow away ticket aliases since we'll need to regenerate them for
3435 delete $self->{'TicketAliases'};
3436 delete $self->{'items_array'};
3437 delete $self->{'item_map'};
3438 delete $self->{'raw_rows'};
3439 delete $self->{'rows'};
3440 delete $self->{'count_all'};
3442 my $sql = $self->Query; # Violating the _SQL namespace
3443 if ( !$sql || $self->{'RecalcTicketLimits'} ) {
3445 # "Restrictions to Clauses Branch\n";
3446 my $clauseRef = eval { $self->_RestrictionsToClauses; };
3448 $RT::Logger->error( "RestrictionsToClauses: " . $@ );
3452 $sql = $self->ClausesToSQL($clauseRef);
3453 $self->FromSQL($sql) if $sql;
3457 $self->{'RecalcTicketLimits'} = 0;
3461 =head2 _BuildItemMap
3463 Build up a L</ItemMap> of first/last/next/prev items, so that we can
3464 display search nav quickly.
3471 my $window = RT->Config->Get('TicketsItemMapSize');
3473 $self->{'item_map'} = {};
3475 my $items = $self->ItemsArrayRefWindow( $window );
3476 return unless $items && @$items;
3479 $self->{'item_map'}{'first'} = $items->[0]->EffectiveId;
3480 for ( my $i = 0; $i < @$items; $i++ ) {
3481 my $item = $items->[$i];
3482 my $id = $item->EffectiveId;
3483 $self->{'item_map'}{$id}{'defined'} = 1;
3484 $self->{'item_map'}{$id}{'prev'} = $prev;
3485 $self->{'item_map'}{$id}{'next'} = $items->[$i+1]->EffectiveId
3489 $self->{'item_map'}{'last'} = $prev
3490 if !$window || @$items < $window;
3495 Returns an a map of all items found by this search. The map is a hash
3499 first => <first ticket id found>,
3500 last => <last ticket id found or undef>,
3503 prev => <the ticket id found before>,
3504 next => <the ticket id found after>,
3516 $self->_BuildItemMap unless $self->{'item_map'};
3517 return $self->{'item_map'};
3523 =head2 PrepForSerialization
3525 You don't want to serialize a big tickets object, as
3526 the {items} hash will be instantly invalid _and_ eat
3531 sub PrepForSerialization {
3533 delete $self->{'items'};
3534 delete $self->{'items_array'};
3535 $self->RedoSearch();
3540 RT::Tickets supports several flags which alter search behavior:
3543 allow_deleted_search (Otherwise never show deleted tickets in search results)
3544 looking_at_type (otherwise limit to type=ticket)
3546 These flags are set by calling
3548 $tickets->{'flagname'} = 1;
3550 BUG: There should be an API for this
3560 Returns an empty new RT::Ticket item
3566 return(RT::Ticket->new($self->CurrentUser));
3568 RT::Base->_ImportOverlays();