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 # if it's equality op and search by Email or Name then we can preload user
914 # we do it to help some DBs better estimate number of rows and get better plans
915 if ( $op =~ /^!?=$/ && (!$rest{'SUBKEY'} || $rest{'SUBKEY'} eq 'Name' || $rest{'SUBKEY'} eq 'EmailAddress') ) {
916 my $o = RT::User->new( $self->CurrentUser );
919 ? $field eq 'Owner'? 'Load' : 'LoadByEmail'
920 : $rest{'SUBKEY'} eq 'EmailAddress' ? 'LoadByEmail': 'Load';
921 $o->$method( $value );
922 $rest{'SUBKEY'} = 'id';
923 $value = $o->id || 0;
926 # Owner was ENUM field, so "Owner = 'xxx'" allowed user to
927 # search by id and Name at the same time, this is workaround
928 # to preserve backward compatibility
929 if ( $field eq 'Owner' ) {
930 if ( ($rest{'SUBKEY'}||'') eq 'id' ) {
940 $rest{SUBKEY} ||= 'EmailAddress';
942 my $groups = $self->_RoleGroupsJoin( Type => $type, Class => $class, New => !$type );
945 if ( $op =~ /^IS(?: NOT)?$/i ) {
946 # is [not] empty case
948 my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
949 # to avoid joining the table Users into the query, we just join GM
950 # and make sure we don't match records where group is member of itself
952 LEFTJOIN => $group_members,
955 VALUE => "$group_members.MemberId",
959 ALIAS => $group_members,
966 elsif ( $op =~ /^!=$|^NOT\s+/i ) {
967 # negative condition case
970 $op =~ s/!|NOT\s+//i;
972 # XXX: we have no way to build correct "Watcher.X != 'Y'" when condition
973 # "X = 'Y'" matches more then one user so we try to fetch two records and
974 # do the right thing when there is only one exist and semi-working solution
976 my $users_obj = RT::Users->new( $self->CurrentUser );
978 FIELD => $rest{SUBKEY},
983 $users_obj->RowsPerPage(2);
984 my @users = @{ $users_obj->ItemsArrayRef };
986 my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
989 $uid = $users[0]->id if @users;
991 LEFTJOIN => $group_members,
992 ALIAS => $group_members,
998 ALIAS => $group_members,
1004 $self->SUPER::Limit(
1005 LEFTJOIN => $group_members,
1008 VALUE => "$group_members.MemberId",
1011 my $users = $self->Join(
1013 ALIAS1 => $group_members,
1014 FIELD1 => 'MemberId',
1018 $self->SUPER::Limit(
1021 FIELD => $rest{SUBKEY},
1035 # positive condition case
1037 my $group_members = $self->_GroupMembersJoin(
1038 GroupsAlias => $groups, New => 1, Left => 0
1040 my $users = $self->Join(
1042 ALIAS1 => $group_members,
1043 FIELD1 => 'MemberId',
1050 FIELD => $rest{'SUBKEY'},
1059 sub _RoleGroupsJoin {
1061 my %args = (New => 0, Class => 'Ticket', Type => '', @_);
1062 return $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $args{'Type'} }
1063 if $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $args{'Type'} }
1066 # we always have watcher groups for ticket, so we use INNER join
1067 my $groups = $self->Join(
1069 FIELD1 => $args{'Class'} eq 'Queue'? 'Queue': 'id',
1071 FIELD2 => 'Instance',
1072 ENTRYAGGREGATOR => 'AND',
1074 $self->SUPER::Limit(
1075 LEFTJOIN => $groups,
1078 VALUE => 'RT::'. $args{'Class'} .'-Role',
1080 $self->SUPER::Limit(
1081 LEFTJOIN => $groups,
1084 VALUE => $args{'Type'},
1087 $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $args{'Type'} } = $groups
1088 unless $args{'New'};
1093 sub _GroupMembersJoin {
1095 my %args = (New => 1, GroupsAlias => undef, Left => 1, @_);
1097 return $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} }
1098 if $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} }
1101 my $alias = $self->Join(
1102 $args{'Left'} ? (TYPE => 'LEFT') : (),
1103 ALIAS1 => $args{'GroupsAlias'},
1105 TABLE2 => 'CachedGroupMembers',
1106 FIELD2 => 'GroupId',
1107 ENTRYAGGREGATOR => 'AND',
1109 $self->SUPER::Limit(
1110 $args{'Left'} ? (LEFTJOIN => $alias) : (),
1112 FIELD => 'Disabled',
1116 $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} } = $alias
1117 unless $args{'New'};
1124 Helper function which provides joins to a watchers table both for limits
1131 my $type = shift || '';
1134 my $groups = $self->_RoleGroupsJoin( Type => $type );
1135 my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
1136 # XXX: work around, we must hide groups that
1137 # are members of the role group we search in,
1138 # otherwise them result in wrong NULLs in Users
1139 # table and break ordering. Now, we know that
1140 # RT doesn't allow to add groups as members of the
1141 # ticket roles, so we just hide entries in CGM table
1142 # with MemberId == GroupId from results
1143 $self->SUPER::Limit(
1144 LEFTJOIN => $group_members,
1147 VALUE => "$group_members.MemberId",
1150 my $users = $self->Join(
1152 ALIAS1 => $group_members,
1153 FIELD1 => 'MemberId',
1157 return ($groups, $group_members, $users);
1160 =head2 _WatcherMembershipLimit
1162 Handle watcher membership limits, i.e. whether the watcher belongs to a
1163 specific group or not.
1166 1: Field to query on
1168 SELECT DISTINCT main.*
1172 CachedGroupMembers CachedGroupMembers_2,
1175 (main.EffectiveId = main.id)
1177 (main.Status != 'deleted')
1179 (main.Type = 'ticket')
1182 (Users_3.EmailAddress = '22')
1184 (Groups_1.Domain = 'RT::Ticket-Role')
1186 (Groups_1.Type = 'RequestorGroup')
1189 Groups_1.Instance = main.id
1191 Groups_1.id = CachedGroupMembers_2.GroupId
1193 CachedGroupMembers_2.MemberId = Users_3.id
1194 ORDER BY main.id ASC
1199 sub _WatcherMembershipLimit {
1200 my ( $self, $field, $op, $value, @rest ) = @_;
1205 my $groups = $self->NewAlias('Groups');
1206 my $groupmembers = $self->NewAlias('CachedGroupMembers');
1207 my $users = $self->NewAlias('Users');
1208 my $memberships = $self->NewAlias('CachedGroupMembers');
1210 if ( ref $field ) { # gross hack
1211 my @bundle = @$field;
1213 for my $chunk (@bundle) {
1214 ( $field, $op, $value, @rest ) = @$chunk;
1216 ALIAS => $memberships,
1227 ALIAS => $memberships,
1235 # Tie to groups for tickets we care about
1239 VALUE => 'RT::Ticket-Role',
1240 ENTRYAGGREGATOR => 'AND'
1245 FIELD1 => 'Instance',
1252 # If we care about which sort of watcher
1253 my $meta = $FIELD_METADATA{$field};
1254 my $type = ( defined $meta->[1] ? $meta->[1] : undef );
1261 ENTRYAGGREGATOR => 'AND'
1268 ALIAS2 => $groupmembers,
1273 ALIAS1 => $groupmembers,
1274 FIELD1 => 'MemberId',
1280 ALIAS => $groupmembers,
1281 FIELD => 'Disabled',
1286 ALIAS1 => $memberships,
1287 FIELD1 => 'MemberId',
1293 ALIAS => $memberships,
1294 FIELD => 'Disabled',
1303 =head2 _CustomFieldDecipher
1305 Try and turn a CF descriptor into (cfid, cfname) object pair.
1309 sub _CustomFieldDecipher {
1310 my ($self, $string) = @_;
1312 my ($queue, $field, $column) = ($string =~ /^(?:(.+?)\.)?{(.+)}(?:\.(Content|LargeContent))?$/);
1313 $field ||= ($string =~ /^{(.*?)}$/)[0] || $string;
1317 my $q = RT::Queue->new( $self->CurrentUser );
1321 # $queue = $q->Name; # should we normalize the queue?
1322 $cf = $q->CustomField( $field );
1325 $RT::Logger->warning("Queue '$queue' doesn't exist, parsed from '$string'");
1329 elsif ( $field =~ /\D/ ) {
1331 my $cfs = RT::CustomFields->new( $self->CurrentUser );
1332 $cfs->Limit( FIELD => 'Name', VALUE => $field );
1333 $cfs->LimitToLookupType('RT::Queue-RT::Ticket');
1335 # if there is more then one field the current user can
1336 # see with the same name then we shouldn't return cf object
1337 # as we don't know which one to use
1340 $cf = undef if $cfs->Next;
1344 $cf = RT::CustomField->new( $self->CurrentUser );
1345 $cf->Load( $field );
1348 return ($queue, $field, $cf, $column);
1351 =head2 _CustomFieldJoin
1353 Factor out the Join of custom fields so we can use it for sorting too
1357 sub _CustomFieldJoin {
1358 my ($self, $cfkey, $cfid, $field) = @_;
1359 # Perform one Join per CustomField
1360 if ( $self->{_sql_object_cfv_alias}{$cfkey} ||
1361 $self->{_sql_cf_alias}{$cfkey} )
1363 return ( $self->{_sql_object_cfv_alias}{$cfkey},
1364 $self->{_sql_cf_alias}{$cfkey} );
1367 my ($TicketCFs, $CFs);
1369 $TicketCFs = $self->{_sql_object_cfv_alias}{$cfkey} = $self->Join(
1373 TABLE2 => 'ObjectCustomFieldValues',
1374 FIELD2 => 'ObjectId',
1376 $self->SUPER::Limit(
1377 LEFTJOIN => $TicketCFs,
1378 FIELD => 'CustomField',
1380 ENTRYAGGREGATOR => 'AND'
1384 my $ocfalias = $self->Join(
1387 TABLE2 => 'ObjectCustomFields',
1388 FIELD2 => 'ObjectId',
1391 $self->SUPER::Limit(
1392 LEFTJOIN => $ocfalias,
1393 ENTRYAGGREGATOR => 'OR',
1394 FIELD => 'ObjectId',
1398 $CFs = $self->{_sql_cf_alias}{$cfkey} = $self->Join(
1400 ALIAS1 => $ocfalias,
1401 FIELD1 => 'CustomField',
1402 TABLE2 => 'CustomFields',
1405 $self->SUPER::Limit(
1407 ENTRYAGGREGATOR => 'AND',
1408 FIELD => 'LookupType',
1409 VALUE => 'RT::Queue-RT::Ticket',
1411 $self->SUPER::Limit(
1413 ENTRYAGGREGATOR => 'AND',
1418 $TicketCFs = $self->{_sql_object_cfv_alias}{$cfkey} = $self->Join(
1422 TABLE2 => 'ObjectCustomFieldValues',
1423 FIELD2 => 'CustomField',
1425 $self->SUPER::Limit(
1426 LEFTJOIN => $TicketCFs,
1427 FIELD => 'ObjectId',
1430 ENTRYAGGREGATOR => 'AND',
1433 $self->SUPER::Limit(
1434 LEFTJOIN => $TicketCFs,
1435 FIELD => 'ObjectType',
1436 VALUE => 'RT::Ticket',
1437 ENTRYAGGREGATOR => 'AND'
1439 $self->SUPER::Limit(
1440 LEFTJOIN => $TicketCFs,
1441 FIELD => 'Disabled',
1444 ENTRYAGGREGATOR => 'AND'
1447 return ($TicketCFs, $CFs);
1450 =head2 _CustomFieldLimit
1452 Limit based on CustomFields
1459 use Regexp::Common qw(RE_net_IPv4);
1460 use Regexp::Common::net::CIDR;
1463 sub _CustomFieldLimit {
1464 my ( $self, $_field, $op, $value, %rest ) = @_;
1466 my $field = $rest{'SUBKEY'} || die "No field specified";
1468 # For our sanity, we can only limit on one queue at a time
1470 my ($queue, $cfid, $cf, $column);
1471 ($queue, $field, $cf, $column) = $self->_CustomFieldDecipher( $field );
1472 $cfid = $cf ? $cf->id : 0 ;
1474 # If we're trying to find custom fields that don't match something, we
1475 # want tickets where the custom field has no value at all. Note that
1476 # we explicitly don't include the "IS NULL" case, since we would
1477 # otherwise end up with a redundant clause.
1479 my ($negative_op, $null_op, $inv_op, $range_op)
1480 = $self->ClassifySQLOperation( $op );
1483 return @_ unless RT->Config->Get('DatabaseType') eq 'Oracle';
1486 return %args unless $args{'FIELD'} eq 'LargeContent';
1488 my $op = $args{'OPERATOR'};
1490 $args{'OPERATOR'} = 'MATCHES';
1492 elsif ( $op eq '!=' ) {
1493 $args{'OPERATOR'} = 'NOT MATCHES';
1495 elsif ( $op =~ /^[<>]=?$/ ) {
1496 $args{'FUNCTION'} = "TO_CHAR( $args{'ALIAS'}.LargeContent )";
1501 if ( $cf && $cf->Type eq 'IPAddress' ) {
1502 my $parsed = RT::ObjectCustomFieldValue->ParseIP($value);
1507 $RT::Logger->warn("$value is not a valid IPAddress");
1511 if ( $cf && $cf->Type eq 'IPAddressRange' ) {
1513 if ( $value =~ /^\s*$RE{net}{CIDR}{IPv4}{-keep}\s*$/o ) {
1515 # convert incomplete 192.168/24 to 192.168.0.0/24 format
1517 join( '.', map $_ || 0, ( split /\./, $1 )[ 0 .. 3 ] ) . "/$2"
1521 my ( $start_ip, $end_ip ) =
1522 RT::ObjectCustomFieldValue->ParseIPRange($value);
1523 if ( $start_ip && $end_ip ) {
1524 if ( $op =~ /^([<>])=?$/ ) {
1525 my $is_less = $1 eq '<' ? 1 : 0;
1534 $value = join '-', $start_ip, $end_ip;
1538 $RT::Logger->warn("$value is not a valid IPAddressRange");
1542 my $single_value = !$cf || !$cfid || $cf->SingleValue;
1544 my $cfkey = $cfid ? $cfid : "$queue.$field";
1546 if ( $null_op && !$column ) {
1547 # IS[ NOT] NULL without column is the same as has[ no] any CF value,
1548 # we can reuse our default joins for this operation
1549 # with column specified we have different situation
1550 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
1553 ALIAS => $TicketCFs,
1562 OPERATOR => 'IS NOT',
1565 ENTRYAGGREGATOR => 'AND',
1569 elsif ( $op !~ /^[<>]=?$/ && ( $cf && $cf->Type eq 'IPAddressRange')) {
1571 my ($start_ip, $end_ip) = split /-/, $value;
1574 if ( $op !~ /NOT|!=|<>/i ) { # positive equation
1575 $self->_CustomFieldLimit(
1576 'CF', '<=', $end_ip, %rest,
1577 SUBKEY => $rest{'SUBKEY'}. '.Content',
1579 $self->_CustomFieldLimit(
1580 'CF', '>=', $start_ip, %rest,
1581 SUBKEY => $rest{'SUBKEY'}. '.LargeContent',
1582 ENTRYAGGREGATOR => 'AND',
1584 # as well limit borders so DB optimizers can use better
1585 # estimations and scan less rows
1586 # have to disable this tweak because of ipv6
1587 # $self->_CustomFieldLimit(
1588 # $field, '>=', '000.000.000.000', %rest,
1589 # SUBKEY => $rest{'SUBKEY'}. '.Content',
1590 # ENTRYAGGREGATOR => 'AND',
1592 # $self->_CustomFieldLimit(
1593 # $field, '<=', '255.255.255.255', %rest,
1594 # SUBKEY => $rest{'SUBKEY'}. '.LargeContent',
1595 # ENTRYAGGREGATOR => 'AND',
1598 else { # negative equation
1599 $self->_CustomFieldLimit($field, '>', $end_ip, %rest);
1600 $self->_CustomFieldLimit(
1601 $field, '<', $start_ip, %rest,
1602 SUBKEY => $rest{'SUBKEY'}. '.LargeContent',
1603 ENTRYAGGREGATOR => 'OR',
1605 # TODO: as well limit borders so DB optimizers can use better
1606 # estimations and scan less rows, but it's harder to do
1607 # as we have OR aggregator
1611 elsif ( !$negative_op || $single_value ) {
1612 $cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++ if !$single_value && !$range_op;
1613 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
1620 # if column is defined then deal only with it
1621 # otherwise search in Content and in LargeContent
1623 $self->_SQLLimit( $fix_op->(
1624 ALIAS => $TicketCFs,
1636 # need special treatment for Date
1637 if ( $cf and $cf->Type eq 'DateTime' and $op eq '=' ) {
1639 if ( $value =~ /:/ ) {
1640 # there is time speccified.
1641 my $date = RT::Date->new( $self->CurrentUser );
1642 $date->Set( Format => 'unknown', Value => $value );
1644 ALIAS => $TicketCFs,
1647 VALUE => $date->ISO,
1652 # no time specified, that means we want everything on a
1653 # particular day. in the database, we need to check for >
1654 # and < the edges of that day.
1655 my $date = RT::Date->new( $self->CurrentUser );
1656 $date->Set( Format => 'unknown', Value => $value );
1657 $date->SetToMidnight( Timezone => 'server' );
1658 my $daystart = $date->ISO;
1660 my $dayend = $date->ISO;
1665 ALIAS => $TicketCFs,
1673 ALIAS => $TicketCFs,
1678 ENTRYAGGREGATOR => 'AND',
1684 elsif ( $op eq '=' || $op eq '!=' || $op eq '<>' ) {
1685 if ( length( Encode::encode_utf8($value) ) < 256 ) {
1687 ALIAS => $TicketCFs,
1698 ALIAS => $TicketCFs,
1702 ENTRYAGGREGATOR => 'OR'
1705 ALIAS => $TicketCFs,
1709 ENTRYAGGREGATOR => 'OR'
1712 $self->_SQLLimit( $fix_op->(
1713 ALIAS => $TicketCFs,
1714 FIELD => 'LargeContent',
1717 ENTRYAGGREGATOR => 'AND',
1724 ALIAS => $TicketCFs,
1735 ALIAS => $TicketCFs,
1739 ENTRYAGGREGATOR => 'OR'
1742 ALIAS => $TicketCFs,
1746 ENTRYAGGREGATOR => 'OR'
1749 $self->_SQLLimit( $fix_op->(
1750 ALIAS => $TicketCFs,
1751 FIELD => 'LargeContent',
1754 ENTRYAGGREGATOR => 'AND',
1761 # XXX: if we join via CustomFields table then
1762 # because of order of left joins we get NULLs in
1763 # CF table and then get nulls for those records
1764 # in OCFVs table what result in wrong results
1765 # as decifer method now tries to load a CF then
1766 # we fall into this situation only when there
1767 # are more than one CF with the name in the DB.
1768 # the same thing applies to order by call.
1769 # TODO: reorder joins T <- OCFVs <- CFs <- OCFs if
1770 # we want treat IS NULL as (not applies or has
1775 OPERATOR => 'IS NOT',
1778 ENTRYAGGREGATOR => 'AND',
1784 ALIAS => $TicketCFs,
1785 FIELD => $column || 'Content',
1789 ENTRYAGGREGATOR => 'OR',
1797 $cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++;
1798 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
1801 $op =~ s/!|NOT\s+//i;
1803 # if column is defined then deal only with it
1804 # otherwise search in Content and in LargeContent
1806 $self->SUPER::Limit( $fix_op->(
1807 LEFTJOIN => $TicketCFs,
1808 ALIAS => $TicketCFs,
1816 $self->SUPER::Limit(
1817 LEFTJOIN => $TicketCFs,
1818 ALIAS => $TicketCFs,
1827 ALIAS => $TicketCFs,
1836 sub _HasAttributeLimit {
1837 my ( $self, $field, $op, $value, %rest ) = @_;
1839 my $alias = $self->Join(
1843 TABLE2 => 'Attributes',
1844 FIELD2 => 'ObjectId',
1846 $self->SUPER::Limit(
1848 FIELD => 'ObjectType',
1849 VALUE => 'RT::Ticket',
1850 ENTRYAGGREGATOR => 'AND'
1852 $self->SUPER::Limit(
1857 ENTRYAGGREGATOR => 'AND'
1863 OPERATOR => $FIELD_METADATA{$field}->[1]? 'IS NOT': 'IS',
1870 # End Helper Functions
1872 # End of SQL Stuff -------------------------------------------------
1875 =head2 OrderByCols ARRAY
1877 A modified version of the OrderBy method which automatically joins where
1878 C<ALIAS> is set to the name of a watcher type.
1889 foreach my $row (@args) {
1890 if ( $row->{ALIAS} ) {
1894 if ( $row->{FIELD} !~ /\./ ) {
1895 my $meta = $self->FIELDS->{ $row->{FIELD} };
1901 if ( $meta->[0] eq 'ENUM' && ($meta->[1]||'') eq 'Queue' ) {
1902 my $alias = $self->Join(
1905 FIELD1 => $row->{'FIELD'},
1909 push @res, { %$row, ALIAS => $alias, FIELD => "Name" };
1910 } elsif ( ( $meta->[0] eq 'ENUM' && ($meta->[1]||'') eq 'User' )
1911 || ( $meta->[0] eq 'WATCHERFIELD' && ($meta->[1]||'') eq 'Owner' )
1913 my $alias = $self->Join(
1916 FIELD1 => $row->{'FIELD'},
1920 push @res, { %$row, ALIAS => $alias, FIELD => "Name" };
1927 my ( $field, $subkey ) = split /\./, $row->{FIELD}, 2;
1928 my $meta = $self->FIELDS->{$field};
1929 if ( defined $meta->[0] && $meta->[0] eq 'WATCHERFIELD' ) {
1930 # cache alias as we want to use one alias per watcher type for sorting
1931 my $users = $self->{_sql_u_watchers_alias_for_sort}{ $meta->[1] };
1933 $self->{_sql_u_watchers_alias_for_sort}{ $meta->[1] }
1934 = $users = ( $self->_WatcherJoin( $meta->[1] ) )[2];
1936 push @res, { %$row, ALIAS => $users, FIELD => $subkey };
1937 } elsif ( defined $meta->[0] && $meta->[0] eq 'CUSTOMFIELD' ) {
1938 my ($queue, $field, $cf_obj, $column) = $self->_CustomFieldDecipher( $subkey );
1939 my $cfkey = $cf_obj ? $cf_obj->id : "$queue.$field";
1940 $cfkey .= ".ordering" if !$cf_obj || ($cf_obj->MaxValues||0) != 1;
1941 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, ($cf_obj ?$cf_obj->id :0) , $field );
1942 # this is described in _CustomFieldLimit
1946 OPERATOR => 'IS NOT',
1949 ENTRYAGGREGATOR => 'AND',
1952 # For those cases where we are doing a join against the
1953 # CF name, and don't have a CFid, use Unique to make sure
1954 # we don't show duplicate tickets. NOTE: I'm pretty sure
1955 # this will stay mixed in for the life of the
1956 # class/package, and not just for the life of the object.
1957 # Potential performance issue.
1958 require DBIx::SearchBuilder::Unique;
1959 DBIx::SearchBuilder::Unique->import;
1961 my $CFvs = $self->Join(
1963 ALIAS1 => $TicketCFs,
1964 FIELD1 => 'CustomField',
1965 TABLE2 => 'CustomFieldValues',
1966 FIELD2 => 'CustomField',
1968 $self->SUPER::Limit(
1972 VALUE => $TicketCFs . ".Content",
1973 ENTRYAGGREGATOR => 'AND'
1976 push @res, { %$row, ALIAS => $CFvs, FIELD => 'SortOrder' };
1977 push @res, { %$row, ALIAS => $TicketCFs, FIELD => 'Content' };
1978 } elsif ( $field eq "Custom" && $subkey eq "Ownership") {
1979 # PAW logic is "reversed"
1981 if (exists $row->{ORDER} ) {
1982 my $o = $row->{ORDER};
1983 delete $row->{ORDER};
1984 $order = "DESC" if $o =~ /asc/i;
1987 # Ticket.Owner 1 0 X
1988 # Unowned Tickets 0 1 X
1991 foreach my $uid ( $self->CurrentUser->Id, RT->Nobody->Id ) {
1992 if ( RT->Config->Get('DatabaseType') eq 'Oracle' ) {
1993 my $f = ($row->{'ALIAS'} || 'main') .'.Owner';
1998 FUNCTION => "CASE WHEN $f=$uid THEN 1 ELSE 0 END",
2005 FUNCTION => "Owner=$uid",
2011 push @res, { %$row, FIELD => "Priority", ORDER => $order } ;
2017 return $self->SUPER::OrderByCols(@res);
2025 Takes a paramhash with the fields FIELD, OPERATOR, VALUE and DESCRIPTION
2026 Generally best called from LimitFoo methods
2036 DESCRIPTION => undef,
2039 $args{'DESCRIPTION'} = $self->loc(
2040 "[_1] [_2] [_3]", $args{'FIELD'},
2041 $args{'OPERATOR'}, $args{'VALUE'}
2043 if ( !defined $args{'DESCRIPTION'} );
2045 my $index = $self->_NextIndex;
2047 # make the TicketRestrictions hash the equivalent of whatever we just passed in;
2049 %{ $self->{'TicketRestrictions'}{$index} } = %args;
2051 $self->{'RecalcTicketLimits'} = 1;
2053 # If we're looking at the effective id, we don't want to append the other clause
2054 # which limits us to tickets where id = effective id
2055 if ( $args{'FIELD'} eq 'EffectiveId'
2056 && ( !$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) )
2058 $self->{'looking_at_effective_id'} = 1;
2061 if ( $args{'FIELD'} eq 'Type'
2062 && ( !$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) )
2064 $self->{'looking_at_type'} = 1;
2075 LimitQueue takes a paramhash with the fields OPERATOR and VALUE.
2076 OPERATOR is one of = or !=. (It defaults to =).
2077 VALUE is a queue id or Name.
2090 #TODO VALUE should also take queue objects
2091 if ( defined $args{'VALUE'} && $args{'VALUE'} !~ /^\d+$/ ) {
2092 my $queue = RT::Queue->new( $self->CurrentUser );
2093 $queue->Load( $args{'VALUE'} );
2094 $args{'VALUE'} = $queue->Id;
2097 # What if they pass in an Id? Check for isNum() and convert to
2100 #TODO check for a valid queue here
2104 VALUE => $args{'VALUE'},
2105 OPERATOR => $args{'OPERATOR'},
2106 DESCRIPTION => join(
2107 ' ', $self->loc('Queue'), $args{'OPERATOR'}, $args{'VALUE'},
2117 Takes a paramhash with the fields OPERATOR and VALUE.
2118 OPERATOR is one of = or !=.
2121 RT adds Status != 'deleted' until object has
2122 allow_deleted_search internal property set.
2123 $tickets->{'allow_deleted_search'} = 1;
2124 $tickets->LimitStatus( VALUE => 'deleted' );
2136 VALUE => $args{'VALUE'},
2137 OPERATOR => $args{'OPERATOR'},
2138 DESCRIPTION => join( ' ',
2139 $self->loc('Status'), $args{'OPERATOR'},
2140 $self->loc( $args{'VALUE'} ) ),
2148 If called, this search will not automatically limit the set of results found
2149 to tickets of type "Ticket". Tickets of other types, such as "project" and
2150 "approval" will be found.
2157 # Instead of faking a Limit that later gets ignored, fake up the
2158 # fact that we're already looking at type, so that the check in
2159 # Tickets_SQL/FromSQL goes down the right branch
2161 # $self->LimitType(VALUE => '__any');
2162 $self->{looking_at_type} = 1;
2169 Takes a paramhash with the fields OPERATOR and VALUE.
2170 OPERATOR is one of = or !=, it defaults to "=".
2171 VALUE is a string to search for in the type of the ticket.
2186 VALUE => $args{'VALUE'},
2187 OPERATOR => $args{'OPERATOR'},
2188 DESCRIPTION => join( ' ',
2189 $self->loc('Type'), $args{'OPERATOR'}, $args{'VALUE'}, ),
2199 Takes a paramhash with the fields OPERATOR and VALUE.
2200 OPERATOR is one of = or !=.
2201 VALUE is a string to search for in the subject of the ticket.
2210 VALUE => $args{'VALUE'},
2211 OPERATOR => $args{'OPERATOR'},
2212 DESCRIPTION => join( ' ',
2213 $self->loc('Subject'), $args{'OPERATOR'}, $args{'VALUE'}, ),
2219 # Things that can be > < = !=
2224 Takes a paramhash with the fields OPERATOR and VALUE.
2225 OPERATOR is one of =, >, < or !=.
2226 VALUE is a ticket Id to search for
2239 VALUE => $args{'VALUE'},
2240 OPERATOR => $args{'OPERATOR'},
2242 join( ' ', $self->loc('Id'), $args{'OPERATOR'}, $args{'VALUE'}, ),
2248 =head2 LimitPriority
2250 Takes a paramhash with the fields OPERATOR and VALUE.
2251 OPERATOR is one of =, >, < or !=.
2252 VALUE is a value to match the ticket's priority against
2260 FIELD => 'Priority',
2261 VALUE => $args{'VALUE'},
2262 OPERATOR => $args{'OPERATOR'},
2263 DESCRIPTION => join( ' ',
2264 $self->loc('Priority'),
2265 $args{'OPERATOR'}, $args{'VALUE'}, ),
2271 =head2 LimitInitialPriority
2273 Takes a paramhash with the fields OPERATOR and VALUE.
2274 OPERATOR is one of =, >, < or !=.
2275 VALUE is a value to match the ticket's initial priority against
2280 sub LimitInitialPriority {
2284 FIELD => 'InitialPriority',
2285 VALUE => $args{'VALUE'},
2286 OPERATOR => $args{'OPERATOR'},
2287 DESCRIPTION => join( ' ',
2288 $self->loc('Initial Priority'), $args{'OPERATOR'},
2295 =head2 LimitFinalPriority
2297 Takes a paramhash with the fields OPERATOR and VALUE.
2298 OPERATOR is one of =, >, < or !=.
2299 VALUE is a value to match the ticket's final priority against
2303 sub LimitFinalPriority {
2307 FIELD => 'FinalPriority',
2308 VALUE => $args{'VALUE'},
2309 OPERATOR => $args{'OPERATOR'},
2310 DESCRIPTION => join( ' ',
2311 $self->loc('Final Priority'), $args{'OPERATOR'},
2318 =head2 LimitTimeWorked
2320 Takes a paramhash with the fields OPERATOR and VALUE.
2321 OPERATOR is one of =, >, < or !=.
2322 VALUE is a value to match the ticket's TimeWorked attribute
2326 sub LimitTimeWorked {
2330 FIELD => 'TimeWorked',
2331 VALUE => $args{'VALUE'},
2332 OPERATOR => $args{'OPERATOR'},
2333 DESCRIPTION => join( ' ',
2334 $self->loc('Time Worked'),
2335 $args{'OPERATOR'}, $args{'VALUE'}, ),
2341 =head2 LimitTimeLeft
2343 Takes a paramhash with the fields OPERATOR and VALUE.
2344 OPERATOR is one of =, >, < or !=.
2345 VALUE is a value to match the ticket's TimeLeft attribute
2353 FIELD => 'TimeLeft',
2354 VALUE => $args{'VALUE'},
2355 OPERATOR => $args{'OPERATOR'},
2356 DESCRIPTION => join( ' ',
2357 $self->loc('Time Left'),
2358 $args{'OPERATOR'}, $args{'VALUE'}, ),
2368 Takes a paramhash with the fields OPERATOR and VALUE.
2369 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2370 VALUE is a string to search for in the body of the ticket
2379 VALUE => $args{'VALUE'},
2380 OPERATOR => $args{'OPERATOR'},
2381 DESCRIPTION => join( ' ',
2382 $self->loc('Ticket content'), $args{'OPERATOR'},
2389 =head2 LimitFilename
2391 Takes a paramhash with the fields OPERATOR and VALUE.
2392 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2393 VALUE is a string to search for in the body of the ticket
2401 FIELD => 'Filename',
2402 VALUE => $args{'VALUE'},
2403 OPERATOR => $args{'OPERATOR'},
2404 DESCRIPTION => join( ' ',
2405 $self->loc('Attachment filename'), $args{'OPERATOR'},
2411 =head2 LimitContentType
2413 Takes a paramhash with the fields OPERATOR and VALUE.
2414 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2415 VALUE is a content type to search ticket attachments for
2419 sub LimitContentType {
2423 FIELD => 'ContentType',
2424 VALUE => $args{'VALUE'},
2425 OPERATOR => $args{'OPERATOR'},
2426 DESCRIPTION => join( ' ',
2427 $self->loc('Ticket content type'), $args{'OPERATOR'},
2438 Takes a paramhash with the fields OPERATOR and VALUE.
2439 OPERATOR is one of = or !=.
2451 my $owner = RT::User->new( $self->CurrentUser );
2452 $owner->Load( $args{'VALUE'} );
2454 # FIXME: check for a valid $owner
2457 VALUE => $args{'VALUE'},
2458 OPERATOR => $args{'OPERATOR'},
2459 DESCRIPTION => join( ' ',
2460 $self->loc('Owner'), $args{'OPERATOR'}, $owner->Name(), ),
2470 Takes a paramhash with the fields OPERATOR, TYPE and VALUE.
2471 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2472 VALUE is a value to match the ticket's watcher email addresses against
2473 TYPE is the sort of watchers you want to match against. Leave it undef if you want to search all of them
2487 #build us up a description
2488 my ( $watcher_type, $desc );
2489 if ( $args{'TYPE'} ) {
2490 $watcher_type = $args{'TYPE'};
2493 $watcher_type = "Watcher";
2497 FIELD => $watcher_type,
2498 VALUE => $args{'VALUE'},
2499 OPERATOR => $args{'OPERATOR'},
2500 TYPE => $args{'TYPE'},
2501 DESCRIPTION => join( ' ',
2502 $self->loc($watcher_type),
2503 $args{'OPERATOR'}, $args{'VALUE'}, ),
2512 =head2 LimitLinkedTo
2514 LimitLinkedTo takes a paramhash with two fields: TYPE and TARGET
2515 TYPE limits the sort of link we want to search on
2517 TYPE = { RefersTo, MemberOf, DependsOn }
2519 TARGET is the id or URI of the TARGET of the link
2533 FIELD => 'LinkedTo',
2535 TARGET => $args{'TARGET'},
2536 TYPE => $args{'TYPE'},
2537 DESCRIPTION => $self->loc(
2538 "Tickets [_1] by [_2]",
2539 $self->loc( $args{'TYPE'} ),
2542 OPERATOR => $args{'OPERATOR'},
2548 =head2 LimitLinkedFrom
2550 LimitLinkedFrom takes a paramhash with two fields: TYPE and BASE
2551 TYPE limits the sort of link we want to search on
2554 BASE is the id or URI of the BASE of the link
2558 sub LimitLinkedFrom {
2567 # translate RT2 From/To naming to RT3 TicketSQL naming
2568 my %fromToMap = qw(DependsOn DependentOn
2570 RefersTo ReferredToBy);
2572 my $type = $args{'TYPE'};
2573 $type = $fromToMap{$type} if exists( $fromToMap{$type} );
2576 FIELD => 'LinkedTo',
2578 BASE => $args{'BASE'},
2580 DESCRIPTION => $self->loc(
2581 "Tickets [_1] [_2]",
2582 $self->loc( $args{'TYPE'} ),
2585 OPERATOR => $args{'OPERATOR'},
2592 my $ticket_id = shift;
2593 return $self->LimitLinkedTo(
2595 TARGET => $ticket_id,
2601 sub LimitHasMember {
2603 my $ticket_id = shift;
2604 return $self->LimitLinkedFrom(
2606 BASE => "$ticket_id",
2607 TYPE => 'HasMember',
2614 sub LimitDependsOn {
2616 my $ticket_id = shift;
2617 return $self->LimitLinkedTo(
2619 TARGET => $ticket_id,
2620 TYPE => 'DependsOn',
2627 sub LimitDependedOnBy {
2629 my $ticket_id = shift;
2630 return $self->LimitLinkedFrom(
2633 TYPE => 'DependentOn',
2642 my $ticket_id = shift;
2643 return $self->LimitLinkedTo(
2645 TARGET => $ticket_id,
2653 sub LimitReferredToBy {
2655 my $ticket_id = shift;
2656 return $self->LimitLinkedFrom(
2659 TYPE => 'ReferredToBy',
2667 =head2 LimitDate (FIELD => 'DateField', OPERATOR => $oper, VALUE => $ISODate)
2669 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
2671 OPERATOR is one of > or <
2672 VALUE is a date and time in ISO format in GMT
2673 FIELD is one of Starts, Started, Told, Created, Resolved, LastUpdated
2675 There are also helper functions of the form LimitFIELD that eliminate
2676 the need to pass in a FIELD argument.
2690 #Set the description if we didn't get handed it above
2691 unless ( $args{'DESCRIPTION'} ) {
2692 $args{'DESCRIPTION'} = $args{'FIELD'} . " "
2693 . $args{'OPERATOR'} . " "
2694 . $args{'VALUE'} . " GMT";
2697 $self->Limit(%args);
2704 $self->LimitDate( FIELD => 'Created', @_ );
2709 $self->LimitDate( FIELD => 'Due', @_ );
2715 $self->LimitDate( FIELD => 'Starts', @_ );
2721 $self->LimitDate( FIELD => 'Started', @_ );
2726 $self->LimitDate( FIELD => 'Resolved', @_ );
2731 $self->LimitDate( FIELD => 'Told', @_ );
2734 sub LimitLastUpdated {
2736 $self->LimitDate( FIELD => 'LastUpdated', @_ );
2741 =head2 LimitTransactionDate (OPERATOR => $oper, VALUE => $ISODate)
2743 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
2745 OPERATOR is one of > or <
2746 VALUE is a date and time in ISO format in GMT
2751 sub LimitTransactionDate {
2754 FIELD => 'TransactionDate',
2761 # <20021217042756.GK28744@pallas.fsck.com>
2762 # "Kill It" - Jesse.
2764 #Set the description if we didn't get handed it above
2765 unless ( $args{'DESCRIPTION'} ) {
2766 $args{'DESCRIPTION'} = $args{'FIELD'} . " "
2767 . $args{'OPERATOR'} . " "
2768 . $args{'VALUE'} . " GMT";
2771 $self->Limit(%args);
2778 =head2 LimitCustomField
2780 Takes a paramhash of key/value pairs with the following keys:
2784 =item CUSTOMFIELD - CustomField name or id. If a name is passed, an additional parameter QUEUE may also be passed to distinguish the custom field.
2786 =item OPERATOR - The usual Limit operators
2788 =item VALUE - The value to compare against
2794 sub LimitCustomField {
2798 CUSTOMFIELD => undef,
2800 DESCRIPTION => undef,
2801 FIELD => 'CustomFieldValue',
2806 my $CF = RT::CustomField->new( $self->CurrentUser );
2807 if ( $args{CUSTOMFIELD} =~ /^\d+$/ ) {
2808 $CF->Load( $args{CUSTOMFIELD} );
2811 $CF->LoadByNameAndQueue(
2812 Name => $args{CUSTOMFIELD},
2813 Queue => $args{QUEUE}
2815 $args{CUSTOMFIELD} = $CF->Id;
2818 #If we are looking to compare with a null value.
2819 if ( $args{'OPERATOR'} =~ /^is$/i ) {
2820 $args{'DESCRIPTION'}
2821 ||= $self->loc( "Custom field [_1] has no value.", $CF->Name );
2823 elsif ( $args{'OPERATOR'} =~ /^is not$/i ) {
2824 $args{'DESCRIPTION'}
2825 ||= $self->loc( "Custom field [_1] has a value.", $CF->Name );
2828 # if we're not looking to compare with a null value
2830 $args{'DESCRIPTION'} ||= $self->loc( "Custom field [_1] [_2] [_3]",
2831 $CF->Name, $args{OPERATOR}, $args{VALUE} );
2834 if ( defined $args{'QUEUE'} && $args{'QUEUE'} =~ /\D/ ) {
2835 my $QueueObj = RT::Queue->new( $self->CurrentUser );
2836 $QueueObj->Load( $args{'QUEUE'} );
2837 $args{'QUEUE'} = $QueueObj->Id;
2839 delete $args{'QUEUE'} unless defined $args{'QUEUE'} && length $args{'QUEUE'};
2842 @rest = ( ENTRYAGGREGATOR => 'AND' )
2843 if ( $CF->Type eq 'SelectMultiple' );
2846 VALUE => $args{VALUE},
2848 .(defined $args{'QUEUE'}? ".{$args{'QUEUE'}}" : '' )
2849 .".{" . $CF->Name . "}",
2850 OPERATOR => $args{OPERATOR},
2855 $self->{'RecalcTicketLimits'} = 1;
2862 Keep track of the counter for the array of restrictions
2868 return ( $self->{'restriction_index'}++ );
2876 $self->{'table'} = "Tickets";
2877 $self->{'RecalcTicketLimits'} = 1;
2878 $self->{'looking_at_effective_id'} = 0;
2879 $self->{'looking_at_type'} = 0;
2880 $self->{'restriction_index'} = 1;
2881 $self->{'primary_key'} = "id";
2882 delete $self->{'items_array'};
2883 delete $self->{'item_map'};
2884 delete $self->{'columns_to_display'};
2885 $self->SUPER::_Init(@_);
2894 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2895 return ( $self->SUPER::Count() );
2901 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2902 return ( $self->SUPER::CountAll() );
2907 =head2 ItemsArrayRef
2909 Returns a reference to the set of all items found in this search
2916 return $self->{'items_array'} if $self->{'items_array'};
2918 my $placeholder = $self->_ItemsCounter;
2919 $self->GotoFirstItem();
2920 while ( my $item = $self->Next ) {
2921 push( @{ $self->{'items_array'} }, $item );
2923 $self->GotoItem($placeholder);
2924 $self->{'items_array'}
2925 = $self->ItemsOrderBy( $self->{'items_array'} );
2927 return $self->{'items_array'};
2930 sub ItemsArrayRefWindow {
2934 my @old = ($self->_ItemsCounter, $self->RowsPerPage, $self->FirstRow+1);
2936 $self->RowsPerPage( $window );
2938 $self->GotoFirstItem;
2941 while ( my $item = $self->Next ) {
2945 $self->RowsPerPage( $old[1] );
2946 $self->FirstRow( $old[2] );
2947 $self->GotoItem( $old[0] );
2956 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
2958 my $Ticket = $self->SUPER::Next;
2959 return $Ticket unless $Ticket;
2961 if ( $Ticket->__Value('Status') eq 'deleted'
2962 && !$self->{'allow_deleted_search'} )
2966 elsif ( RT->Config->Get('UseSQLForACLChecks') ) {
2967 # if we found a ticket with this option enabled then
2968 # all tickets we found are ACLed, cache this fact
2969 my $key = join ";:;", $self->CurrentUser->id, 'ShowTicket', 'RT::Ticket-'. $Ticket->id;
2970 $RT::Principal::_ACL_CACHE->set( $key => 1 );
2973 elsif ( $Ticket->CurrentUserHasRight('ShowTicket') ) {
2978 # If the user doesn't have the right to show this ticket
2985 $self->CurrentUserCanSee if RT->Config->Get('UseSQLForACLChecks');
2986 return $self->SUPER::_DoSearch( @_ );
2991 $self->CurrentUserCanSee if RT->Config->Get('UseSQLForACLChecks');
2992 return $self->SUPER::_DoCount( @_ );
2998 my $cache_key = 'RolesHasRight;:;ShowTicket';
3000 if ( my $cached = $RT::Principal::_ACL_CACHE->fetch( $cache_key ) ) {
3004 my $ACL = RT::ACL->new( RT->SystemUser );
3005 $ACL->Limit( FIELD => 'RightName', VALUE => 'ShowTicket' );
3006 $ACL->Limit( FIELD => 'PrincipalType', OPERATOR => '!=', VALUE => 'Group' );
3007 my $principal_alias = $ACL->Join(
3009 FIELD1 => 'PrincipalId',
3010 TABLE2 => 'Principals',
3013 $ACL->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3016 foreach my $ACE ( @{ $ACL->ItemsArrayRef } ) {
3017 my $role = $ACE->__Value('PrincipalType');
3018 my $type = $ACE->__Value('ObjectType');
3019 if ( $type eq 'RT::System' ) {
3022 elsif ( $type eq 'RT::Queue' ) {
3023 next if $res{ $role } && !ref $res{ $role };
3024 push @{ $res{ $role } ||= [] }, $ACE->__Value('ObjectId');
3027 $RT::Logger->error('ShowTicket right is granted on unsupported object');
3030 $RT::Principal::_ACL_CACHE->set( $cache_key => \%res );
3034 sub _DirectlyCanSeeIn {
3036 my $id = $self->CurrentUser->id;
3038 my $cache_key = 'User-'. $id .';:;ShowTicket;:;DirectlyCanSeeIn';
3039 if ( my $cached = $RT::Principal::_ACL_CACHE->fetch( $cache_key ) ) {
3043 my $ACL = RT::ACL->new( RT->SystemUser );
3044 $ACL->Limit( FIELD => 'RightName', VALUE => 'ShowTicket' );
3045 my $principal_alias = $ACL->Join(
3047 FIELD1 => 'PrincipalId',
3048 TABLE2 => 'Principals',
3051 $ACL->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3052 my $cgm_alias = $ACL->Join(
3054 FIELD1 => 'PrincipalId',
3055 TABLE2 => 'CachedGroupMembers',
3056 FIELD2 => 'GroupId',
3058 $ACL->Limit( ALIAS => $cgm_alias, FIELD => 'MemberId', VALUE => $id );
3059 $ACL->Limit( ALIAS => $cgm_alias, FIELD => 'Disabled', VALUE => 0 );
3062 foreach my $ACE ( @{ $ACL->ItemsArrayRef } ) {
3063 my $type = $ACE->__Value('ObjectType');
3064 if ( $type eq 'RT::System' ) {
3065 # If user is direct member of a group that has the right
3066 # on the system then he can see any ticket
3067 $RT::Principal::_ACL_CACHE->set( $cache_key => [-1] );
3070 elsif ( $type eq 'RT::Queue' ) {
3071 push @res, $ACE->__Value('ObjectId');
3074 $RT::Logger->error('ShowTicket right is granted on unsupported object');
3077 $RT::Principal::_ACL_CACHE->set( $cache_key => \@res );
3081 sub CurrentUserCanSee {
3083 return if $self->{'_sql_current_user_can_see_applied'};
3085 return $self->{'_sql_current_user_can_see_applied'} = 1
3086 if $self->CurrentUser->UserObj->HasRight(
3087 Right => 'SuperUser', Object => $RT::System
3090 my $id = $self->CurrentUser->id;
3092 # directly can see in all queues then we have nothing to do
3093 my @direct_queues = $self->_DirectlyCanSeeIn;
3094 return $self->{'_sql_current_user_can_see_applied'} = 1
3095 if @direct_queues && $direct_queues[0] == -1;
3097 my %roles = $self->_RolesCanSee;
3099 my %skip = map { $_ => 1 } @direct_queues;
3100 foreach my $role ( keys %roles ) {
3101 next unless ref $roles{ $role };
3103 my @queues = grep !$skip{$_}, @{ $roles{ $role } };
3105 $roles{ $role } = \@queues;
3107 delete $roles{ $role };
3112 # there is no global watchers, only queues and tickes, if at
3113 # some point we will add global roles then it's gonna blow
3114 # the idea here is that if the right is set globaly for a role
3115 # and user plays this role for a queue directly not a ticket
3116 # then we have to check in advance
3117 if ( my @tmp = grep $_ ne 'Owner' && !ref $roles{ $_ }, keys %roles ) {
3119 my $groups = RT::Groups->new( RT->SystemUser );
3120 $groups->Limit( FIELD => 'Domain', VALUE => 'RT::Queue-Role' );
3122 $groups->Limit( FIELD => 'Type', VALUE => $_ );
3124 my $principal_alias = $groups->Join(
3127 TABLE2 => 'Principals',
3130 $groups->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3131 my $cgm_alias = $groups->Join(
3134 TABLE2 => 'CachedGroupMembers',
3135 FIELD2 => 'GroupId',
3137 $groups->Limit( ALIAS => $cgm_alias, FIELD => 'MemberId', VALUE => $id );
3138 $groups->Limit( ALIAS => $cgm_alias, FIELD => 'Disabled', VALUE => 0 );
3139 while ( my $group = $groups->Next ) {
3140 push @direct_queues, $group->Instance;
3144 unless ( @direct_queues || keys %roles ) {
3145 $self->SUPER::Limit(
3150 ENTRYAGGREGATOR => 'AND',
3152 return $self->{'_sql_current_user_can_see_applied'} = 1;
3156 my $join_roles = keys %roles;
3157 $join_roles = 0 if $join_roles == 1 && $roles{'Owner'};
3158 my ($role_group_alias, $cgm_alias);
3159 if ( $join_roles ) {
3160 $role_group_alias = $self->_RoleGroupsJoin( New => 1 );
3161 $cgm_alias = $self->_GroupMembersJoin( GroupsAlias => $role_group_alias );
3162 $self->SUPER::Limit(
3163 LEFTJOIN => $cgm_alias,
3164 FIELD => 'MemberId',
3169 my $limit_queues = sub {
3173 return unless @queues;
3174 if ( @queues == 1 ) {
3175 $self->SUPER::Limit(
3180 ENTRYAGGREGATOR => $ea,
3183 $self->SUPER::_OpenParen('ACL');
3184 foreach my $q ( @queues ) {
3185 $self->SUPER::Limit(
3190 ENTRYAGGREGATOR => $ea,
3194 $self->SUPER::_CloseParen('ACL');
3199 $self->SUPER::_OpenParen('ACL');
3201 $ea = 'OR' if $limit_queues->( $ea, @direct_queues );
3202 while ( my ($role, $queues) = each %roles ) {
3203 $self->SUPER::_OpenParen('ACL');
3204 if ( $role eq 'Owner' ) {
3205 $self->SUPER::Limit(
3209 ENTRYAGGREGATOR => $ea,
3213 $self->SUPER::Limit(
3215 ALIAS => $cgm_alias,
3216 FIELD => 'MemberId',
3217 OPERATOR => 'IS NOT',
3220 ENTRYAGGREGATOR => $ea,
3222 $self->SUPER::Limit(
3224 ALIAS => $role_group_alias,
3227 ENTRYAGGREGATOR => 'AND',
3230 $limit_queues->( 'AND', @$queues ) if ref $queues;
3231 $ea = 'OR' if $ea eq 'AND';
3232 $self->SUPER::_CloseParen('ACL');
3234 $self->SUPER::_CloseParen('ACL');
3236 return $self->{'_sql_current_user_can_see_applied'} = 1;
3243 =head2 LoadRestrictions
3245 LoadRestrictions takes a string which can fully populate the TicketRestrictons hash.
3246 TODO It is not yet implemented
3252 =head2 DescribeRestrictions
3255 Returns a hash keyed by restriction id.
3256 Each element of the hash is currently a one element hash that contains DESCRIPTION which
3257 is a description of the purpose of that TicketRestriction
3261 sub DescribeRestrictions {
3266 foreach my $row ( keys %{ $self->{'TicketRestrictions'} } ) {
3267 $listing{$row} = $self->{'TicketRestrictions'}{$row}{'DESCRIPTION'};
3274 =head2 RestrictionValues FIELD
3276 Takes a restriction field and returns a list of values this field is restricted
3281 sub RestrictionValues {
3284 map $self->{'TicketRestrictions'}{$_}{'VALUE'}, grep {
3285 $self->{'TicketRestrictions'}{$_}{'FIELD'} eq $field
3286 && $self->{'TicketRestrictions'}{$_}{'OPERATOR'} eq "="
3288 keys %{ $self->{'TicketRestrictions'} };
3293 =head2 ClearRestrictions
3295 Removes all restrictions irretrievably
3299 sub ClearRestrictions {
3301 delete $self->{'TicketRestrictions'};
3302 $self->{'looking_at_effective_id'} = 0;
3303 $self->{'looking_at_type'} = 0;
3304 $self->{'RecalcTicketLimits'} = 1;
3309 =head2 DeleteRestriction
3311 Takes the row Id of a restriction (From DescribeRestrictions' output, for example.
3312 Removes that restriction from the session's limits.
3316 sub DeleteRestriction {
3319 delete $self->{'TicketRestrictions'}{$row};
3321 $self->{'RecalcTicketLimits'} = 1;
3323 #make the underlying easysearch object forget all its preconceptions
3328 # Convert a set of oldstyle SB Restrictions to Clauses for RQL
3330 sub _RestrictionsToClauses {
3334 foreach my $row ( keys %{ $self->{'TicketRestrictions'} } ) {
3335 my $restriction = $self->{'TicketRestrictions'}{$row};
3337 # We need to reimplement the subclause aggregation that SearchBuilder does.
3338 # Default Subclause is ALIAS.FIELD, and default ALIAS is 'main',
3339 # Then SB AND's the different Subclauses together.
3341 # So, we want to group things into Subclauses, convert them to
3342 # SQL, and then join them with the appropriate DefaultEA.
3343 # Then join each subclause group with AND.
3345 my $field = $restriction->{'FIELD'};
3346 my $realfield = $field; # CustomFields fake up a fieldname, so
3347 # we need to figure that out
3350 # Rewrite LinkedTo meta field to the real field
3351 if ( $field =~ /LinkedTo/ ) {
3352 $realfield = $field = $restriction->{'TYPE'};
3356 # Handle subkey fields with a different real field
3357 if ( $field =~ /^(\w+)\./ ) {
3361 die "I don't know about $field yet"
3362 unless ( exists $FIELD_METADATA{$realfield}
3363 or $restriction->{CUSTOMFIELD} );
3365 my $type = $FIELD_METADATA{$realfield}->[0];
3366 my $op = $restriction->{'OPERATOR'};
3370 map { $restriction->{$_} } qw(VALUE TICKET BASE TARGET)
3373 # this performs the moral equivalent of defined or/dor/C<//>,
3374 # without the short circuiting.You need to use a 'defined or'
3375 # type thing instead of just checking for truth values, because
3376 # VALUE could be 0.(i.e. "false")
3378 # You could also use this, but I find it less aesthetic:
3379 # (although it does short circuit)
3380 #( defined $restriction->{'VALUE'}? $restriction->{VALUE} :
3381 # defined $restriction->{'TICKET'} ?
3382 # $restriction->{TICKET} :
3383 # defined $restriction->{'BASE'} ?
3384 # $restriction->{BASE} :
3385 # defined $restriction->{'TARGET'} ?
3386 # $restriction->{TARGET} )
3388 my $ea = $restriction->{ENTRYAGGREGATOR}
3389 || $DefaultEA{$type}
3392 die "Invalid operator $op for $field ($type)"
3393 unless exists $ea->{$op};
3397 # Each CustomField should be put into a different Clause so they
3398 # are ANDed together.
3399 if ( $restriction->{CUSTOMFIELD} ) {
3400 $realfield = $field;
3403 exists $clause{$realfield} or $clause{$realfield} = [];
3406 $field =~ s!(['\\])!\\$1!g;
3407 $value =~ s!(['\\])!\\$1!g;
3408 my $data = [ $ea, $type, $field, $op, $value ];
3410 # here is where we store extra data, say if it's a keyword or
3411 # something. (I.e. "TYPE SPECIFIC STUFF")
3413 if (lc $ea eq 'none') {
3414 $clause{$realfield} = [ $data ];
3416 push @{ $clause{$realfield} }, $data;
3424 =head2 _ProcessRestrictions PARAMHASH
3426 # The new _ProcessRestrictions is somewhat dependent on the SQL stuff,
3427 # but isn't quite generic enough to move into Tickets_SQL.
3431 sub _ProcessRestrictions {
3434 #Blow away ticket aliases since we'll need to regenerate them for
3436 delete $self->{'TicketAliases'};
3437 delete $self->{'items_array'};
3438 delete $self->{'item_map'};
3439 delete $self->{'raw_rows'};
3440 delete $self->{'rows'};
3441 delete $self->{'count_all'};
3443 my $sql = $self->Query; # Violating the _SQL namespace
3444 if ( !$sql || $self->{'RecalcTicketLimits'} ) {
3446 # "Restrictions to Clauses Branch\n";
3447 my $clauseRef = eval { $self->_RestrictionsToClauses; };
3449 $RT::Logger->error( "RestrictionsToClauses: " . $@ );
3453 $sql = $self->ClausesToSQL($clauseRef);
3454 $self->FromSQL($sql) if $sql;
3458 $self->{'RecalcTicketLimits'} = 0;
3462 =head2 _BuildItemMap
3464 Build up a L</ItemMap> of first/last/next/prev items, so that we can
3465 display search nav quickly.
3472 my $window = RT->Config->Get('TicketsItemMapSize');
3474 $self->{'item_map'} = {};
3476 my $items = $self->ItemsArrayRefWindow( $window );
3477 return unless $items && @$items;
3480 $self->{'item_map'}{'first'} = $items->[0]->EffectiveId;
3481 for ( my $i = 0; $i < @$items; $i++ ) {
3482 my $item = $items->[$i];
3483 my $id = $item->EffectiveId;
3484 $self->{'item_map'}{$id}{'defined'} = 1;
3485 $self->{'item_map'}{$id}{'prev'} = $prev;
3486 $self->{'item_map'}{$id}{'next'} = $items->[$i+1]->EffectiveId
3490 $self->{'item_map'}{'last'} = $prev
3491 if !$window || @$items < $window;
3496 Returns an a map of all items found by this search. The map is a hash
3500 first => <first ticket id found>,
3501 last => <last ticket id found or undef>,
3504 prev => <the ticket id found before>,
3505 next => <the ticket id found after>,
3517 $self->_BuildItemMap unless $self->{'item_map'};
3518 return $self->{'item_map'};
3524 =head2 PrepForSerialization
3526 You don't want to serialize a big tickets object, as
3527 the {items} hash will be instantly invalid _and_ eat
3532 sub PrepForSerialization {
3534 delete $self->{'items'};
3535 delete $self->{'items_array'};
3536 $self->RedoSearch();
3541 RT::Tickets supports several flags which alter search behavior:
3544 allow_deleted_search (Otherwise never show deleted tickets in search results)
3545 looking_at_type (otherwise limit to type=ticket)
3547 These flags are set by calling
3549 $tickets->{'flagname'} = 1;
3551 BUG: There should be an API for this
3561 Returns an empty new RT::Ticket item
3567 return(RT::Ticket->new($self->CurrentUser));
3569 RT::Base->_ImportOverlays();