1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2011 Best Practical Solutions, LLC
6 # <sales@bestpractical.com>
8 # (Except where explicitly superseded by other copyright notices)
13 # This work is made available to you under the terms of Version 2 of
14 # the GNU General Public License. A copy of that license should have
15 # been provided with this software, but in any event can be snarfed
18 # This work is distributed in the hope that it will be useful, but
19 # WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
21 # General Public License for more details.
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
26 # 02110-1301 or visit their web page on the internet at
27 # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
30 # CONTRIBUTION SUBMISSION POLICY:
32 # (The following paragraph is not intended to limit the rights granted
33 # to you to modify and distribute this software under the terms of
34 # the GNU General Public License and is only of importance to you if
35 # you choose to contribute your changes and enhancements to the
36 # community by submitting them to Best Practical Solutions, LLC.)
38 # By intentionally submitting any modifications, corrections or
39 # derivatives to this work, or any other work intended for use with
40 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
41 # you are the copyright holder for those contributions and you grant
42 # Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
43 # royalty-free, perpetual, license to use, copy, create derivative
44 # works based on those contributions, and sublicense and distribute
45 # those contributions and any derivatives thereof.
47 # END BPS TAGGED BLOCK }}}
51 # - Decimated ProcessRestrictions and broke it into multiple
52 # functions joined by a LUT
53 # - Semi-Generic SQL stuff moved to another file
55 # Known Issues: FIXME!
57 # - ClearRestrictions and Reinitialization is messy and unclear. The
58 # only good way to do it is to create a new RT::Tickets object.
62 RT::Tickets - A collection of Ticket objects
68 my $tickets = new RT::Tickets($CurrentUser);
72 A collection of RT::Tickets.
82 no warnings qw(redefine);
85 use DBIx::SearchBuilder::Unique;
87 # Configuration Tables:
89 # FIELD_METADATA is a mapping of searchable Field name, to Type, and other
92 our %FIELD_METADATA = (
93 Status => [ 'ENUM', ], #loc_left_pair
94 Queue => [ 'ENUM' => 'Queue', ], #loc_left_pair
95 Type => [ 'ENUM', ], #loc_left_pair
96 Creator => [ 'ENUM' => 'User', ], #loc_left_pair
97 LastUpdatedBy => [ 'ENUM' => 'User', ], #loc_left_pair
98 Owner => [ 'WATCHERFIELD' => 'Owner', ], #loc_left_pair
99 EffectiveId => [ 'INT', ], #loc_left_pair
100 id => [ 'ID', ], #loc_left_pair
101 InitialPriority => [ 'INT', ], #loc_left_pair
102 FinalPriority => [ 'INT', ], #loc_left_pair
103 Priority => [ 'INT', ], #loc_left_pair
104 TimeLeft => [ 'INT', ], #loc_left_pair
105 TimeWorked => [ 'INT', ], #loc_left_pair
106 TimeEstimated => [ 'INT', ], #loc_left_pair
108 Linked => [ 'LINK' ], #loc_left_pair
109 LinkedTo => [ 'LINK' => 'To' ], #loc_left_pair
110 LinkedFrom => [ 'LINK' => 'From' ], #loc_left_pair
111 MemberOf => [ 'LINK' => To => 'MemberOf', ], #loc_left_pair
112 DependsOn => [ 'LINK' => To => 'DependsOn', ], #loc_left_pair
113 RefersTo => [ 'LINK' => To => 'RefersTo', ], #loc_left_pair
114 HasMember => [ 'LINK' => From => 'MemberOf', ], #loc_left_pair
115 DependentOn => [ 'LINK' => From => 'DependsOn', ], #loc_left_pair
116 DependedOnBy => [ 'LINK' => From => 'DependsOn', ], #loc_left_pair
117 ReferredToBy => [ 'LINK' => From => 'RefersTo', ], #loc_left_pair
118 Told => [ 'DATE' => 'Told', ], #loc_left_pair
119 Starts => [ 'DATE' => 'Starts', ], #loc_left_pair
120 Started => [ 'DATE' => 'Started', ], #loc_left_pair
121 Due => [ 'DATE' => 'Due', ], #loc_left_pair
122 Resolved => [ 'DATE' => 'Resolved', ], #loc_left_pair
123 LastUpdated => [ 'DATE' => 'LastUpdated', ], #loc_left_pair
124 Created => [ 'DATE' => 'Created', ], #loc_left_pair
125 Subject => [ 'STRING', ], #loc_left_pair
126 Content => [ 'TRANSFIELD', ], #loc_left_pair
127 ContentType => [ 'TRANSFIELD', ], #loc_left_pair
128 Filename => [ 'TRANSFIELD', ], #loc_left_pair
129 TransactionDate => [ 'TRANSDATE', ], #loc_left_pair
130 Requestor => [ 'WATCHERFIELD' => 'Requestor', ], #loc_left_pair
131 Requestors => [ 'WATCHERFIELD' => 'Requestor', ], #loc_left_pair
132 Cc => [ 'WATCHERFIELD' => 'Cc', ], #loc_left_pair
133 AdminCc => [ 'WATCHERFIELD' => 'AdminCc', ], #loc_left_pair
134 Watcher => [ 'WATCHERFIELD', ], #loc_left_pair
135 QueueCc => [ 'WATCHERFIELD' => 'Cc' => 'Queue', ], #loc_left_pair
136 QueueAdminCc => [ 'WATCHERFIELD' => 'AdminCc' => 'Queue', ], #loc_left_pair
137 QueueWatcher => [ 'WATCHERFIELD' => undef => 'Queue', ], #loc_left_pair
138 CustomFieldValue => [ 'CUSTOMFIELD', ], #loc_left_pair
139 CustomField => [ 'CUSTOMFIELD', ], #loc_left_pair
140 CF => [ 'CUSTOMFIELD', ], #loc_left_pair
141 Updated => [ 'TRANSDATE', ], #loc_left_pair
142 RequestorGroup => [ 'MEMBERSHIPFIELD' => 'Requestor', ], #loc_left_pair
143 CCGroup => [ 'MEMBERSHIPFIELD' => 'Cc', ], #loc_left_pair
144 AdminCCGroup => [ 'MEMBERSHIPFIELD' => 'AdminCc', ], #loc_left_pair
145 WatcherGroup => [ 'MEMBERSHIPFIELD', ], #loc_left_pair
146 HasAttribute => [ 'HASATTRIBUTE', 1 ],
147 HasNoAttribute => [ 'HASATTRIBUTE', 0 ],
149 Customer => [ 'FREESIDEFIELD' => 'Customer' ],
150 Service => [ 'FREESIDEFIELD' => 'Service' ],
151 WillResolve => [ 'DATE' => 'WillResolve', ], #loc_left_pair
154 our %SEARCHABLE_SUBFIELDS = (
156 EmailAddress Name RealName Nickname Organization Address1 Address2
157 WorkPhone HomePhone MobilePhone PagerPhone id
161 # Mapping of Field Type to Function
163 ENUM => \&_EnumLimit,
166 LINK => \&_LinkLimit,
167 DATE => \&_DateLimit,
168 STRING => \&_StringLimit,
169 TRANSFIELD => \&_TransLimit,
170 TRANSDATE => \&_TransDateLimit,
171 WATCHERFIELD => \&_WatcherLimit,
172 MEMBERSHIPFIELD => \&_WatcherMembershipLimit,
173 CUSTOMFIELD => \&_CustomFieldLimit,
174 HASATTRIBUTE => \&_HasAttributeLimit,
175 FREESIDEFIELD => \&_FreesideFieldLimit,
177 our %can_bundle = ();# WATCHERFIELD => "yes", );
179 # Default EntryAggregator per type
180 # if you specify OP, you must specify all valid OPs
221 # Helper functions for passing the above lexically scoped tables above
222 # into Tickets_Overlay_SQL.
223 sub FIELDS { return \%FIELD_METADATA }
224 sub dispatch { return \%dispatch }
225 sub can_bundle { return \%can_bundle }
227 # Bring in the clowns.
228 require RT::Tickets_Overlay_SQL;
232 our @SORTFIELDS = qw(id Status
234 Owner Created Due Starts Started
236 Resolved LastUpdated Priority TimeWorked TimeLeft);
240 Returns the list of fields that lists of tickets can easily be sorted by
246 return (@SORTFIELDS);
251 # BEGIN SQL STUFF *********************************
256 $self->SUPER::CleanSlate( @_ );
257 delete $self->{$_} foreach qw(
259 _sql_group_members_aliases
260 _sql_object_cfv_alias
261 _sql_role_group_aliases
264 _sql_u_watchers_alias_for_sort
265 _sql_u_watchers_aliases
266 _sql_current_user_can_see_applied
270 =head1 Limit Helper Routines
272 These routines are the targets of a dispatch table depending on the
273 type of field. They all share the same signature:
275 my ($self,$field,$op,$value,@rest) = @_;
277 The values in @rest should be suitable for passing directly to
278 DBIx::SearchBuilder::Limit.
280 Essentially they are an expanded/broken out (and much simplified)
281 version of what ProcessRestrictions used to do. They're also much
282 more clearly delineated by the TYPE of field being processed.
291 my ( $sb, $field, $op, $value, @rest ) = @_;
293 return $sb->_IntLimit( $field, $op, $value, @rest ) unless $value eq '__Bookmarked__';
295 die "Invalid operator $op for __Bookmarked__ search on $field"
296 unless $op =~ /^(=|!=)$/;
299 my $tmp = $sb->CurrentUser->UserObj->FirstAttribute('Bookmarks');
300 $tmp = $tmp->Content if $tmp;
305 return $sb->_SQLLimit(
312 # as bookmarked tickets can be merged we have to use a join
313 # but it should be pretty lightweight
314 my $tickets_alias = $sb->Join(
319 FIELD2 => 'EffectiveId',
323 my $ea = $op eq '='? 'OR': 'AND';
324 foreach my $id ( sort @bookmarks ) {
326 ALIAS => $tickets_alias,
330 $first? (@rest): ( ENTRYAGGREGATOR => $ea )
338 Handle Fields which are limited to certain values, and potentially
339 need to be looked up from another class.
341 This subroutine actually handles two different kinds of fields. For
342 some the user is responsible for limiting the values. (i.e. Status,
345 For others, the value specified by the user will be looked by via
349 name of class to lookup in (Optional)
354 my ( $sb, $field, $op, $value, @rest ) = @_;
356 # SQL::Statement changes != to <>. (Can we remove this now?)
357 $op = "!=" if $op eq "<>";
359 die "Invalid Operation: $op for $field"
363 my $meta = $FIELD_METADATA{$field};
364 if ( defined $meta->[1] && defined $value && $value !~ /^\d+$/ ) {
365 my $class = "RT::" . $meta->[1];
366 my $o = $class->new( $sb->CurrentUser );
380 Handle fields where the values are limited to integers. (For example,
381 Priority, TimeWorked.)
389 my ( $sb, $field, $op, $value, @rest ) = @_;
391 die "Invalid Operator $op for $field"
392 unless $op =~ /^(=|!=|>|<|>=|<=)$/;
404 Handle fields which deal with links between tickets. (MemberOf, DependsOn)
407 1: Direction (From, To)
408 2: Link Type (MemberOf, DependsOn, RefersTo)
413 my ( $sb, $field, $op, $value, @rest ) = @_;
415 my $meta = $FIELD_METADATA{$field};
416 die "Invalid Operator $op for $field" unless $op =~ /^(=|!=|IS|IS NOT)$/io;
419 if ( $op eq '!=' || $op =~ /\bNOT\b/i ) {
423 $is_null = 1 if !$value || $value =~ /^null$/io;
425 my $direction = $meta->[1] || '';
426 my ($matchfield, $linkfield) = ('', '');
427 if ( $direction eq 'To' ) {
428 ($matchfield, $linkfield) = ("Target", "Base");
430 elsif ( $direction eq 'From' ) {
431 ($matchfield, $linkfield) = ("Base", "Target");
433 elsif ( $direction ) {
434 die "Invalid link direction '$direction' for $field\n";
437 $sb->_LinkLimit( 'LinkedTo', $op, $value, @rest );
439 'LinkedFrom', $op, $value, @rest,
440 ENTRYAGGREGATOR => (($is_negative && $is_null) || (!$is_null && !$is_negative))? 'OR': 'AND',
448 $op = ($op =~ /^(=|IS)$/)? 'IS': 'IS NOT';
450 elsif ( $value =~ /\D/ ) {
453 $matchfield = "Local$matchfield" if $is_local;
455 #For doing a left join to find "unlinked tickets" we want to generate a query that looks like this
456 # SELECT main.* FROM Tickets main
457 # LEFT JOIN Links Links_1 ON ( (Links_1.Type = 'MemberOf')
458 # AND(main.id = Links_1.LocalTarget))
459 # WHERE Links_1.LocalBase IS NULL;
462 my $linkalias = $sb->Join(
467 FIELD2 => 'Local' . $linkfield
470 LEFTJOIN => $linkalias,
478 FIELD => $matchfield,
485 my $linkalias = $sb->Join(
490 FIELD2 => 'Local' . $linkfield
493 LEFTJOIN => $linkalias,
499 LEFTJOIN => $linkalias,
500 FIELD => $matchfield,
507 FIELD => $matchfield,
508 OPERATOR => $is_negative? 'IS': 'IS NOT',
517 Handle date fields. (Created, LastTold..)
520 1: type of link. (Probably not necessary.)
525 my ( $sb, $field, $op, $value, @rest ) = @_;
527 die "Invalid Date Op: $op"
528 unless $op =~ /^(=|>|<|>=|<=)$/;
530 my $meta = $FIELD_METADATA{$field};
531 die "Incorrect Meta Data for $field"
532 unless ( defined $meta->[1] );
534 $sb->_DateFieldLimit( $meta->[1], $op, $value, @rest );
537 # Factor this out for use by custom fields
539 sub _DateFieldLimit {
540 my ( $sb, $field, $op, $value, @rest ) = @_;
542 my $date = RT::Date->new( $sb->CurrentUser );
543 $date->Set( Format => 'unknown', Value => $value );
547 # if we're specifying =, that means we want everything on a
548 # particular single day. in the database, we need to check for >
549 # and < the edges of that day.
551 # Except if the value is 'this month' or 'last month', check
552 # > and < the edges of the month.
554 my ($daystart, $dayend);
555 if ( lc($value) eq 'this month' ) {
557 $date->SetToStart('month', Timezone => 'server');
558 $daystart = $date->ISO;
559 $date->AddMonth(Timezone => 'server');
560 $dayend = $date->ISO;
562 elsif ( lc($value) eq 'last month' ) {
564 $date->SetToStart('month', Timezone => 'server');
565 $dayend = $date->ISO;
567 $date->SetToStart('month', Timezone => 'server');
568 $daystart = $date->ISO;
571 $date->SetToMidnight( Timezone => 'server' );
572 $daystart = $date->ISO;
574 $dayend = $date->ISO;
591 ENTRYAGGREGATOR => 'AND',
609 Handle simple fields which are just strings. (Subject,Type)
617 my ( $sb, $field, $op, $value, @rest ) = @_;
621 # =, !=, LIKE, NOT LIKE
622 if ( (!defined $value || !length $value)
623 && lc($op) ne 'is' && lc($op) ne 'is not'
624 && RT->Config->Get('DatabaseType') eq 'Oracle'
626 my $negative = 1 if $op eq '!=' || $op =~ /^NOT\s/;
627 $op = $negative? 'IS NOT': 'IS';
640 =head2 _TransDateLimit
642 Handle fields limiting based on Transaction Date.
644 The inpupt value must be in a format parseable by Time::ParseDate
651 # This routine should really be factored into translimit.
652 sub _TransDateLimit {
653 my ( $sb, $field, $op, $value, @rest ) = @_;
655 # See the comments for TransLimit, they apply here too
657 unless ( $sb->{_sql_transalias} ) {
658 $sb->{_sql_transalias} = $sb->Join(
661 TABLE2 => 'Transactions',
662 FIELD2 => 'ObjectId',
665 ALIAS => $sb->{_sql_transalias},
666 FIELD => 'ObjectType',
667 VALUE => 'RT::Ticket',
668 ENTRYAGGREGATOR => 'AND',
672 my $date = RT::Date->new( $sb->CurrentUser );
673 $date->Set( Format => 'unknown', Value => $value );
678 # if we're specifying =, that means we want everything on a
679 # particular single day. in the database, we need to check for >
680 # and < the edges of that day.
682 $date->SetToMidnight( Timezone => 'server' );
683 my $daystart = $date->ISO;
685 my $dayend = $date->ISO;
688 ALIAS => $sb->{_sql_transalias},
696 ALIAS => $sb->{_sql_transalias},
702 ENTRYAGGREGATOR => 'AND',
707 # not searching for a single day
710 #Search for the right field
712 ALIAS => $sb->{_sql_transalias},
726 Limit based on the Content of a transaction or the ContentType.
735 # Content, ContentType, Filename
737 # If only this was this simple. We've got to do something
740 #Basically, we want to make sure that the limits apply to
741 #the same attachment, rather than just another attachment
742 #for the same ticket, no matter how many clauses we lump
743 #on. We put them in TicketAliases so that they get nuked
744 #when we redo the join.
746 # In the SQL, we might have
747 # (( Content = foo ) or ( Content = bar AND Content = baz ))
748 # The AND group should share the same Alias.
750 # Actually, maybe it doesn't matter. We use the same alias and it
751 # works itself out? (er.. different.)
753 # Steal more from _ProcessRestrictions
755 # FIXME: Maybe look at the previous FooLimit call, and if it was a
756 # TransLimit and EntryAggregator == AND, reuse the Aliases?
758 # Or better - store the aliases on a per subclause basis - since
759 # those are going to be the things we want to relate to each other,
762 # maybe we should not allow certain kinds of aggregation of these
763 # clauses and do a psuedo regex instead? - the problem is getting
764 # them all into the same subclause when you have (A op B op C) - the
765 # way they get parsed in the tree they're in different subclauses.
767 my ( $self, $field, $op, $value, %rest ) = @_;
769 unless ( $self->{_sql_transalias} ) {
770 $self->{_sql_transalias} = $self->Join(
773 TABLE2 => 'Transactions',
774 FIELD2 => 'ObjectId',
777 ALIAS => $self->{_sql_transalias},
778 FIELD => 'ObjectType',
779 VALUE => 'RT::Ticket',
780 ENTRYAGGREGATOR => 'AND',
783 unless ( defined $self->{_sql_trattachalias} ) {
784 $self->{_sql_trattachalias} = $self->_SQLJoin(
785 TYPE => 'LEFT', # not all txns have an attachment
786 ALIAS1 => $self->{_sql_transalias},
788 TABLE2 => 'Attachments',
789 FIELD2 => 'TransactionId',
793 #Search for the right field
794 if ( $field eq 'Content' and RT->Config->Get('DontSearchFileAttachments') ) {
798 ALIAS => $self->{_sql_trattachalias},
805 ENTRYAGGREGATOR => 'AND',
806 ALIAS => $self->{_sql_trattachalias},
815 ALIAS => $self->{_sql_trattachalias},
828 Handle watcher limits. (Requestor, CC, etc..)
844 my $meta = $FIELD_METADATA{ $field };
845 my $type = $meta->[1] || '';
846 my $class = $meta->[2] || 'Ticket';
848 # Bail if the subfield is not allowed
850 and not grep { $_ eq $rest{SUBKEY} } @{$SEARCHABLE_SUBFIELDS{'User'}})
852 die "Invalid watcher subfield: '$rest{SUBKEY}'";
855 # Owner was ENUM field, so "Owner = 'xxx'" allowed user to
856 # search by id and Name at the same time, this is workaround
857 # to preserve backward compatibility
858 if ( $field eq 'Owner' ) {
859 if ( $op =~ /^!?=$/ && (!$rest{'SUBKEY'} || $rest{'SUBKEY'} eq 'Name' || $rest{'SUBKEY'} eq 'EmailAddress') ) {
860 my $o = RT::User->new( $self->CurrentUser );
861 my $method = ($rest{'SUBKEY'}||'') eq 'EmailAddress' ? 'LoadByEmail': 'Load';
862 $o->$method( $value );
871 if ( ($rest{'SUBKEY'}||'') eq 'id' ) {
881 $rest{SUBKEY} ||= 'EmailAddress';
883 my $groups = $self->_RoleGroupsJoin( Type => $type, Class => $class );
886 if ( $op =~ /^IS(?: NOT)?$/ ) {
887 my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
888 # to avoid joining the table Users into the query, we just join GM
889 # and make sure we don't match records where group is member of itself
891 LEFTJOIN => $group_members,
894 VALUE => "$group_members.MemberId",
898 ALIAS => $group_members,
905 elsif ( $op =~ /^!=$|^NOT\s+/i ) {
907 $op =~ s/!|NOT\s+//i;
909 # XXX: we have no way to build correct "Watcher.X != 'Y'" when condition
910 # "X = 'Y'" matches more then one user so we try to fetch two records and
911 # do the right thing when there is only one exist and semi-working solution
913 my $users_obj = RT::Users->new( $self->CurrentUser );
915 FIELD => $rest{SUBKEY},
920 $users_obj->RowsPerPage(2);
921 my @users = @{ $users_obj->ItemsArrayRef };
923 my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
926 $uid = $users[0]->id if @users;
928 LEFTJOIN => $group_members,
929 ALIAS => $group_members,
935 ALIAS => $group_members,
942 LEFTJOIN => $group_members,
945 VALUE => "$group_members.MemberId",
948 my $users = $self->Join(
950 ALIAS1 => $group_members,
951 FIELD1 => 'MemberId',
958 FIELD => $rest{SUBKEY},
972 my $group_members = $self->_GroupMembersJoin(
973 GroupsAlias => $groups,
977 my $users = $self->{'_sql_u_watchers_aliases'}{$group_members};
979 $users = $self->{'_sql_u_watchers_aliases'}{$group_members} =
980 $self->NewAlias('Users');
982 LEFTJOIN => $group_members,
983 ALIAS => $group_members,
985 VALUE => "$users.id",
990 # we join users table without adding some join condition between tables,
991 # the only conditions we have are conditions on the table iteslf,
992 # for example Users.EmailAddress = 'x'. We should add this condition to
993 # the top level of the query and bundle it with another similar conditions,
994 # for example "Users.EmailAddress = 'x' OR Users.EmailAddress = 'Y'".
995 # To achive this goal we use own SUBCLAUSE for conditions on the users table.
998 SUBCLAUSE => '_sql_u_watchers_'. $users,
1000 FIELD => $rest{'SUBKEY'},
1005 # A condition which ties Users and Groups (role groups) is a left join condition
1006 # of CachedGroupMembers table. To get correct results of the query we check
1007 # if there are matches in CGM table or not using 'cgm.id IS NOT NULL'.
1010 ALIAS => $group_members,
1012 OPERATOR => 'IS NOT',
1019 sub _RoleGroupsJoin {
1021 my %args = (New => 0, Class => 'Ticket', Type => '', @_);
1022 return $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $args{'Type'} }
1023 if $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $args{'Type'} }
1026 # we always have watcher groups for ticket, so we use INNER join
1027 my $groups = $self->Join(
1029 FIELD1 => $args{'Class'} eq 'Queue'? 'Queue': 'id',
1031 FIELD2 => 'Instance',
1032 ENTRYAGGREGATOR => 'AND',
1034 $self->SUPER::Limit(
1035 LEFTJOIN => $groups,
1038 VALUE => 'RT::'. $args{'Class'} .'-Role',
1040 $self->SUPER::Limit(
1041 LEFTJOIN => $groups,
1044 VALUE => $args{'Type'},
1047 $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $args{'Type'} } = $groups
1048 unless $args{'New'};
1053 sub _GroupMembersJoin {
1055 my %args = (New => 1, GroupsAlias => undef, @_);
1057 return $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} }
1058 if $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} }
1061 my $alias = $self->Join(
1063 ALIAS1 => $args{'GroupsAlias'},
1065 TABLE2 => 'CachedGroupMembers',
1066 FIELD2 => 'GroupId',
1067 ENTRYAGGREGATOR => 'AND',
1070 $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} } = $alias
1071 unless $args{'New'};
1078 Helper function which provides joins to a watchers table both for limits
1085 my $type = shift || '';
1088 my $groups = $self->_RoleGroupsJoin( Type => $type );
1089 my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
1090 # XXX: work around, we must hide groups that
1091 # are members of the role group we search in,
1092 # otherwise them result in wrong NULLs in Users
1093 # table and break ordering. Now, we know that
1094 # RT doesn't allow to add groups as members of the
1095 # ticket roles, so we just hide entries in CGM table
1096 # with MemberId == GroupId from results
1097 $self->SUPER::Limit(
1098 LEFTJOIN => $group_members,
1101 VALUE => "$group_members.MemberId",
1104 my $users = $self->Join(
1106 ALIAS1 => $group_members,
1107 FIELD1 => 'MemberId',
1111 return ($groups, $group_members, $users);
1114 =head2 _WatcherMembershipLimit
1116 Handle watcher membership limits, i.e. whether the watcher belongs to a
1117 specific group or not.
1120 1: Field to query on
1122 SELECT DISTINCT main.*
1126 CachedGroupMembers CachedGroupMembers_2,
1129 (main.EffectiveId = main.id)
1131 (main.Status != 'deleted')
1133 (main.Type = 'ticket')
1136 (Users_3.EmailAddress = '22')
1138 (Groups_1.Domain = 'RT::Ticket-Role')
1140 (Groups_1.Type = 'RequestorGroup')
1143 Groups_1.Instance = main.id
1145 Groups_1.id = CachedGroupMembers_2.GroupId
1147 CachedGroupMembers_2.MemberId = Users_3.id
1148 ORDER BY main.id ASC
1153 sub _WatcherMembershipLimit {
1154 my ( $self, $field, $op, $value, @rest ) = @_;
1159 my $groups = $self->NewAlias('Groups');
1160 my $groupmembers = $self->NewAlias('CachedGroupMembers');
1161 my $users = $self->NewAlias('Users');
1162 my $memberships = $self->NewAlias('CachedGroupMembers');
1164 if ( ref $field ) { # gross hack
1165 my @bundle = @$field;
1167 for my $chunk (@bundle) {
1168 ( $field, $op, $value, @rest ) = @$chunk;
1170 ALIAS => $memberships,
1181 ALIAS => $memberships,
1189 # {{{ Tie to groups for tickets we care about
1193 VALUE => 'RT::Ticket-Role',
1194 ENTRYAGGREGATOR => 'AND'
1199 FIELD1 => 'Instance',
1206 # If we care about which sort of watcher
1207 my $meta = $FIELD_METADATA{$field};
1208 my $type = ( defined $meta->[1] ? $meta->[1] : undef );
1215 ENTRYAGGREGATOR => 'AND'
1222 ALIAS2 => $groupmembers,
1227 ALIAS1 => $groupmembers,
1228 FIELD1 => 'MemberId',
1234 ALIAS1 => $memberships,
1235 FIELD1 => 'MemberId',
1244 =head2 _CustomFieldDecipher
1246 Try and turn a CF descriptor into (cfid, cfname) object pair.
1250 sub _CustomFieldDecipher {
1251 my ($self, $string) = @_;
1253 my ($queue, $field, $column) = ($string =~ /^(?:(.+?)\.)?{(.+)}(?:\.(Content|LargeContent))?$/);
1254 $field ||= ($string =~ /^{(.*?)}$/)[0] || $string;
1258 my $q = RT::Queue->new( $self->CurrentUser );
1262 # $queue = $q->Name; # should we normalize the queue?
1263 $cf = $q->CustomField( $field );
1266 $RT::Logger->warning("Queue '$queue' doesn't exist, parsed from '$string'");
1270 elsif ( $field =~ /\D/ ) {
1272 my $cfs = RT::CustomFields->new( $self->CurrentUser );
1273 $cfs->Limit( FIELD => 'Name', VALUE => $field );
1274 $cfs->LimitToLookupType('RT::Queue-RT::Ticket');
1276 # if there is more then one field the current user can
1277 # see with the same name then we shouldn't return cf object
1278 # as we don't know which one to use
1281 $cf = undef if $cfs->Next;
1285 $cf = RT::CustomField->new( $self->CurrentUser );
1286 $cf->Load( $field );
1289 return ($queue, $field, $cf, $column);
1292 =head2 _CustomFieldJoin
1294 Factor out the Join of custom fields so we can use it for sorting too
1298 sub _CustomFieldJoin {
1299 my ($self, $cfkey, $cfid, $field) = @_;
1300 # Perform one Join per CustomField
1301 if ( $self->{_sql_object_cfv_alias}{$cfkey} ||
1302 $self->{_sql_cf_alias}{$cfkey} )
1304 return ( $self->{_sql_object_cfv_alias}{$cfkey},
1305 $self->{_sql_cf_alias}{$cfkey} );
1308 my ($TicketCFs, $CFs);
1310 $TicketCFs = $self->{_sql_object_cfv_alias}{$cfkey} = $self->Join(
1314 TABLE2 => 'ObjectCustomFieldValues',
1315 FIELD2 => 'ObjectId',
1317 $self->SUPER::Limit(
1318 LEFTJOIN => $TicketCFs,
1319 FIELD => 'CustomField',
1321 ENTRYAGGREGATOR => 'AND'
1325 my $ocfalias = $self->Join(
1328 TABLE2 => 'ObjectCustomFields',
1329 FIELD2 => 'ObjectId',
1332 $self->SUPER::Limit(
1333 LEFTJOIN => $ocfalias,
1334 ENTRYAGGREGATOR => 'OR',
1335 FIELD => 'ObjectId',
1339 $CFs = $self->{_sql_cf_alias}{$cfkey} = $self->Join(
1341 ALIAS1 => $ocfalias,
1342 FIELD1 => 'CustomField',
1343 TABLE2 => 'CustomFields',
1346 $self->SUPER::Limit(
1348 ENTRYAGGREGATOR => 'AND',
1349 FIELD => 'LookupType',
1350 VALUE => 'RT::Queue-RT::Ticket',
1352 $self->SUPER::Limit(
1354 ENTRYAGGREGATOR => 'AND',
1359 $TicketCFs = $self->{_sql_object_cfv_alias}{$cfkey} = $self->Join(
1363 TABLE2 => 'ObjectCustomFieldValues',
1364 FIELD2 => 'CustomField',
1366 $self->SUPER::Limit(
1367 LEFTJOIN => $TicketCFs,
1368 FIELD => 'ObjectId',
1371 ENTRYAGGREGATOR => 'AND',
1374 $self->SUPER::Limit(
1375 LEFTJOIN => $TicketCFs,
1376 FIELD => 'ObjectType',
1377 VALUE => 'RT::Ticket',
1378 ENTRYAGGREGATOR => 'AND'
1380 $self->SUPER::Limit(
1381 LEFTJOIN => $TicketCFs,
1382 FIELD => 'Disabled',
1385 ENTRYAGGREGATOR => 'AND'
1388 return ($TicketCFs, $CFs);
1391 =head2 _CustomFieldLimit
1393 Limit based on CustomFields
1400 sub _CustomFieldLimit {
1401 my ( $self, $_field, $op, $value, %rest ) = @_;
1403 my $field = $rest{'SUBKEY'} || die "No field specified";
1405 # For our sanity, we can only limit on one queue at a time
1407 my ($queue, $cfid, $cf, $column);
1408 ($queue, $field, $cf, $column) = $self->_CustomFieldDecipher( $field );
1409 $cfid = $cf ? $cf->id : 0 ;
1411 # If we're trying to find custom fields that don't match something, we
1412 # want tickets where the custom field has no value at all. Note that
1413 # we explicitly don't include the "IS NULL" case, since we would
1414 # otherwise end up with a redundant clause.
1416 my ($negative_op, $null_op, $inv_op, $range_op)
1417 = $self->ClassifySQLOperation( $op );
1421 return $op unless RT->Config->Get('DatabaseType') eq 'Oracle';
1422 return 'MATCHES' if $op eq '=';
1423 return 'NOT MATCHES' if $op eq '!=';
1427 my $single_value = !$cf || !$cfid || $cf->SingleValue;
1429 my $cfkey = $cfid ? $cfid : "$queue.$field";
1431 if ( $null_op && !$column ) {
1432 # IS[ NOT] NULL without column is the same as has[ no] any CF value,
1433 # we can reuse our default joins for this operation
1434 # with column specified we have different situation
1435 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
1438 ALIAS => $TicketCFs,
1447 OPERATOR => 'IS NOT',
1450 ENTRYAGGREGATOR => 'AND',
1454 elsif ( !$negative_op || $single_value ) {
1455 $cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++ if !$single_value && !$range_op;
1456 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
1463 # if column is defined then deal only with it
1464 # otherwise search in Content and in LargeContent
1467 ALIAS => $TicketCFs,
1469 OPERATOR => ($column ne 'LargeContent'? $op : $fix_op->($op)),
1474 elsif ( $cfid and $cf->Type eq 'Date' ) {
1475 $self->_DateFieldLimit(
1479 ALIAS => $TicketCFs,
1483 elsif ( $op eq '=' || $op eq '!=' || $op eq '<>' ) {
1484 unless ( length( Encode::encode_utf8($value) ) > 255 ) {
1486 ALIAS => $TicketCFs,
1495 ALIAS => $TicketCFs,
1499 ENTRYAGGREGATOR => 'OR'
1502 ALIAS => $TicketCFs,
1506 ENTRYAGGREGATOR => 'OR'
1510 ALIAS => $TicketCFs,
1511 FIELD => 'LargeContent',
1512 OPERATOR => $fix_op->($op),
1514 ENTRYAGGREGATOR => 'AND',
1520 ALIAS => $TicketCFs,
1530 ALIAS => $TicketCFs,
1534 ENTRYAGGREGATOR => 'OR'
1537 ALIAS => $TicketCFs,
1541 ENTRYAGGREGATOR => 'OR'
1545 ALIAS => $TicketCFs,
1546 FIELD => 'LargeContent',
1547 OPERATOR => $fix_op->($op),
1549 ENTRYAGGREGATOR => 'AND',
1555 # XXX: if we join via CustomFields table then
1556 # because of order of left joins we get NULLs in
1557 # CF table and then get nulls for those records
1558 # in OCFVs table what result in wrong results
1559 # as decifer method now tries to load a CF then
1560 # we fall into this situation only when there
1561 # are more than one CF with the name in the DB.
1562 # the same thing applies to order by call.
1563 # TODO: reorder joins T <- OCFVs <- CFs <- OCFs if
1564 # we want treat IS NULL as (not applies or has
1569 OPERATOR => 'IS NOT',
1572 ENTRYAGGREGATOR => 'AND',
1578 ALIAS => $TicketCFs,
1579 FIELD => $column || 'Content',
1583 ENTRYAGGREGATOR => 'OR',
1590 $cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++;
1591 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
1594 $op =~ s/!|NOT\s+//i;
1596 # if column is defined then deal only with it
1597 # otherwise search in Content and in LargeContent
1599 $self->SUPER::Limit(
1600 LEFTJOIN => $TicketCFs,
1601 ALIAS => $TicketCFs,
1603 OPERATOR => ($column ne 'LargeContent'? $op : $fix_op->($op)),
1608 $self->SUPER::Limit(
1609 LEFTJOIN => $TicketCFs,
1610 ALIAS => $TicketCFs,
1618 ALIAS => $TicketCFs,
1627 sub _HasAttributeLimit {
1628 my ( $self, $field, $op, $value, %rest ) = @_;
1630 my $alias = $self->Join(
1634 TABLE2 => 'Attributes',
1635 FIELD2 => 'ObjectId',
1637 $self->SUPER::Limit(
1639 FIELD => 'ObjectType',
1640 VALUE => 'RT::Ticket',
1641 ENTRYAGGREGATOR => 'AND'
1643 $self->SUPER::Limit(
1648 ENTRYAGGREGATOR => 'AND'
1654 OPERATOR => $FIELD_METADATA{$field}->[1]? 'IS NOT': 'IS',
1660 # End Helper Functions
1662 # End of SQL Stuff -------------------------------------------------
1664 # {{{ Allow sorting on watchers
1666 =head2 OrderByCols ARRAY
1668 A modified version of the OrderBy method which automatically joins where
1669 C<ALIAS> is set to the name of a watcher type.
1680 foreach my $row (@args) {
1681 if ( $row->{ALIAS} ) {
1685 if ( $row->{FIELD} !~ /\./ ) {
1686 my $meta = $self->FIELDS->{ $row->{FIELD} };
1692 if ( $meta->[0] eq 'ENUM' && ($meta->[1]||'') eq 'Queue' ) {
1693 my $alias = $self->Join(
1696 FIELD1 => $row->{'FIELD'},
1700 push @res, { %$row, ALIAS => $alias, FIELD => "Name" };
1701 } elsif ( ( $meta->[0] eq 'ENUM' && ($meta->[1]||'') eq 'User' )
1702 || ( $meta->[0] eq 'WATCHERFIELD' && ($meta->[1]||'') eq 'Owner' )
1704 my $alias = $self->Join(
1707 FIELD1 => $row->{'FIELD'},
1711 push @res, { %$row, ALIAS => $alias, FIELD => "Name" };
1718 my ( $field, $subkey ) = split /\./, $row->{FIELD}, 2;
1719 my $meta = $self->FIELDS->{$field};
1720 if ( defined $meta->[0] && $meta->[0] eq 'WATCHERFIELD' ) {
1721 # cache alias as we want to use one alias per watcher type for sorting
1722 my $users = $self->{_sql_u_watchers_alias_for_sort}{ $meta->[1] };
1724 $self->{_sql_u_watchers_alias_for_sort}{ $meta->[1] }
1725 = $users = ( $self->_WatcherJoin( $meta->[1] ) )[2];
1727 push @res, { %$row, ALIAS => $users, FIELD => $subkey };
1728 } elsif ( defined $meta->[0] && $meta->[0] eq 'CUSTOMFIELD' ) {
1729 my ($queue, $field, $cf_obj, $column) = $self->_CustomFieldDecipher( $subkey );
1730 my $cfkey = $cf_obj ? $cf_obj->id : "$queue.$field";
1731 $cfkey .= ".ordering" if !$cf_obj || ($cf_obj->MaxValues||0) != 1;
1732 my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, ($cf_obj ?$cf_obj->id :0) , $field );
1733 # this is described in _CustomFieldLimit
1737 OPERATOR => 'IS NOT',
1740 ENTRYAGGREGATOR => 'AND',
1743 # For those cases where we are doing a join against the
1744 # CF name, and don't have a CFid, use Unique to make sure
1745 # we don't show duplicate tickets. NOTE: I'm pretty sure
1746 # this will stay mixed in for the life of the
1747 # class/package, and not just for the life of the object.
1748 # Potential performance issue.
1749 require DBIx::SearchBuilder::Unique;
1750 DBIx::SearchBuilder::Unique->import;
1752 my $CFvs = $self->Join(
1754 ALIAS1 => $TicketCFs,
1755 FIELD1 => 'CustomField',
1756 TABLE2 => 'CustomFieldValues',
1757 FIELD2 => 'CustomField',
1759 $self->SUPER::Limit(
1763 VALUE => $TicketCFs . ".Content",
1764 ENTRYAGGREGATOR => 'AND'
1767 push @res, { %$row, ALIAS => $CFvs, FIELD => 'SortOrder' };
1768 push @res, { %$row, ALIAS => $TicketCFs, FIELD => 'Content' };
1769 } elsif ( $field eq "Custom" && $subkey eq "Ownership") {
1770 # PAW logic is "reversed"
1772 if (exists $row->{ORDER} ) {
1773 my $o = $row->{ORDER};
1774 delete $row->{ORDER};
1775 $order = "DESC" if $o =~ /asc/i;
1778 # Ticket.Owner 1 0 X
1779 # Unowned Tickets 0 1 X
1782 foreach my $uid ( $self->CurrentUser->Id, $RT::Nobody->Id ) {
1783 if ( RT->Config->Get('DatabaseType') eq 'Oracle' ) {
1784 my $f = ($row->{'ALIAS'} || 'main') .'.Owner';
1789 FUNCTION => "CASE WHEN $f=$uid THEN 1 ELSE 0 END",
1796 FUNCTION => "Owner=$uid",
1802 push @res, { %$row, FIELD => "Priority", ORDER => $order } ;
1804 } elsif ( $field eq 'Customer' ) { #Freeside
1805 # OrderBy(FIELD => expression) doesn't work, it has to be
1806 # an actual field, so we have to do the join even if sorting
1808 my $custalias = $self->JoinToCustomer;
1809 my $cust_field = lc($subkey);
1810 if ( !$cust_field or $cust_field eq 'number' ) {
1811 $cust_field = 'custnum';
1813 elsif ( $cust_field eq 'name' ) {
1814 $cust_field = "COALESCE( $custalias.company,
1815 $custalias.last || ', ' || $custalias.first
1818 else { # order by cust_main fields directly: 'Customer.agentnum'
1819 $cust_field = $subkey;
1821 push @res, { %$row, ALIAS => $custalias, FIELD => $cust_field };
1823 } elsif ( $field eq 'Service' ) {
1825 my $svcalias = $self->JoinToService;
1826 my $svc_field = lc($subkey);
1827 if ( !$svc_field or $svc_field eq 'number' ) {
1828 $svc_field = 'svcnum';
1830 push @res, { %$row, ALIAS => $svcalias, FIELD => $svc_field };
1838 return $self->SUPER::OrderByCols(@res);
1843 sub JoinToCustLinks {
1844 # Set up join to links (id = localbase),
1845 # limit link type to 'MemberOf',
1846 # and target value to any Freeside custnum URI.
1847 # Return the linkalias for further join/limit action,
1848 # and an sql expression to retrieve the custnum.
1850 # only join once for each RT::Tickets object
1851 my $linkalias = $self->{cust_main_linkalias};
1853 $linkalias = $self->Join(
1858 FIELD2 => 'LocalBase',
1861 $self->SUPER::Limit(
1862 LEFTJOIN => $linkalias,
1865 VALUE => 'MemberOf',
1867 $self->SUPER::Limit(
1868 LEFTJOIN => $linkalias,
1870 OPERATOR => 'STARTSWITH',
1871 VALUE => 'freeside://freeside/cust_main/',
1873 $self->{cust_main_linkalias} = $linkalias;
1875 my $custnum_sql = "CAST(SUBSTR($linkalias.Target,31) AS ";
1876 if ( RT->Config->Get('DatabaseType') eq 'mysql' ) {
1877 $custnum_sql .= 'SIGNED INTEGER)';
1880 $custnum_sql .= 'INTEGER)';
1882 return ($linkalias, $custnum_sql);
1885 sub JoinToCustomer {
1887 my ($linkalias, $custnum_sql) = $self->JoinToCustLinks;
1888 # don't reuse this join, though--negative queries need
1890 my $custalias = $self->Join(
1892 EXPRESSION => $custnum_sql,
1893 TABLE2 => 'cust_main',
1894 FIELD2 => 'custnum',
1899 sub JoinToSvcLinks {
1901 my $linkalias = $self->{cust_svc_linkalias};
1903 $linkalias = $self->Join(
1908 FIELD2 => 'LocalBase',
1911 $self->SUPER::Limit(
1912 LEFTJOIN => $linkalias,
1915 VALUE => 'MemberOf',
1917 $self->SUPER::Limit(
1918 LEFTJOIN => $linkalias,
1920 OPERATOR => 'STARTSWITH',
1921 VALUE => 'freeside://freeside/cust_svc/',
1923 $self->{cust_svc_linkalias} = $linkalias;
1925 my $svcnum_sql = "CAST(SUBSTR($linkalias.Target,30) AS ";
1926 if ( RT->Config->Get('DatabaseType') eq 'mysql' ) {
1927 $svcnum_sql .= 'SIGNED INTEGER)';
1930 $svcnum_sql .= 'INTEGER)';
1932 return ($linkalias, $svcnum_sql);
1937 my ($linkalias, $svcnum_sql) = $self->JoinToSvcLinks;
1940 EXPRESSION => $svcnum_sql,
1941 TABLE2 => 'cust_svc',
1946 # This creates an alternate left join path to cust_main via cust_svc.
1947 # _FreesideFieldLimit needs to add this as a separate, independent join
1948 # and include all tickets that have a matching cust_main record via
1950 sub JoinToCustomerViaService {
1952 my $svcalias = $self->JoinToService;
1953 my $cust_pkg = $self->Join(
1955 ALIAS1 => $svcalias,
1957 TABLE2 => 'cust_pkg',
1960 my $cust_main = $self->Join(
1962 ALIAS1 => $cust_pkg,
1963 FIELD1 => 'custnum',
1964 TABLE2 => 'cust_main',
1965 FIELD2 => 'custnum',
1970 sub _FreesideFieldLimit {
1971 my ( $self, $field, $op, $value, %rest ) = @_;
1972 my $is_negative = 0;
1973 if ( $op eq '!=' || $op =~ /\bNOT\b/i ) {
1974 # if the op is negative, do the join as though
1975 # the op were positive, then accept only records
1976 # where the right-side join key is null.
1978 $op = '=' if $op eq '!=';
1982 my (@alias, $table2, $subfield, $pkey);
1983 if ( $field eq 'Customer' ) {
1984 push @alias, $self->JoinToCustomer;
1985 push @alias, $self->JoinToCustomerViaService;
1988 elsif ( $field eq 'Service' ) {
1989 push @alias, $self->JoinToService;
1993 die "malformed Freeside query: $field";
1996 $subfield = $rest{SUBKEY} || $pkey;
1997 # compound subkey: separate into table name and field in that table
1998 # (must be linked by custnum)
1999 $subfield = lc($subfield);
2000 ($table2, $subfield) = ($1, $2) if $subfield =~ /^(\w+)?\.(\w+)$/;
2001 $subfield = $pkey if $subfield eq 'number';
2003 # if it's compound, create a join from cust_main or cust_svc to that
2004 # table, using custnum or svcnum, and Limit on that table instead.
2005 foreach my $a (@alias) {
2016 # do the actual Limit
2017 $self->SUPER::Limit(
2022 ENTRYAGGREGATOR => 'AND',
2023 # no SUBCLAUSE needed, limits on different aliases across left joins
2024 # are inherently independent
2027 # then, since it's a left join, exclude tickets for which there is now
2028 # no matching record in the table we just limited on. (Or where there
2029 # is a matching record, if $is_negative.)
2030 # For a cust_main query (where there are two different aliases), this
2031 # will produce a subclause: "cust_main_1.custnum IS NOT NULL OR
2032 # cust_main_2.custnum IS NOT NULL" (or "IS NULL AND..." for a negative
2038 OPERATOR => $is_negative ? 'IS' : 'IS NOT',
2041 ENTRYAGGREGATOR => $is_negative ? 'AND' : 'OR',
2042 SUBCLAUSE => 'fs_limit',
2051 # {{{ Limit the result set based on content
2057 Takes a paramhash with the fields FIELD, OPERATOR, VALUE and DESCRIPTION
2058 Generally best called from LimitFoo methods
2068 DESCRIPTION => undef,
2071 $args{'DESCRIPTION'} = $self->loc(
2072 "[_1] [_2] [_3]", $args{'FIELD'},
2073 $args{'OPERATOR'}, $args{'VALUE'}
2075 if ( !defined $args{'DESCRIPTION'} );
2077 my $index = $self->_NextIndex;
2079 # make the TicketRestrictions hash the equivalent of whatever we just passed in;
2081 %{ $self->{'TicketRestrictions'}{$index} } = %args;
2083 $self->{'RecalcTicketLimits'} = 1;
2085 # If we're looking at the effective id, we don't want to append the other clause
2086 # which limits us to tickets where id = effective id
2087 if ( $args{'FIELD'} eq 'EffectiveId'
2088 && ( !$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) )
2090 $self->{'looking_at_effective_id'} = 1;
2093 if ( $args{'FIELD'} eq 'Type'
2094 && ( !$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) )
2096 $self->{'looking_at_type'} = 1;
2106 Returns a frozen string suitable for handing back to ThawLimits.
2110 sub _FreezeThawKeys {
2111 'TicketRestrictions', 'restriction_index', 'looking_at_effective_id',
2115 # {{{ sub FreezeLimits
2120 require MIME::Base64;
2121 MIME::Base64::base64_encode(
2122 Storable::freeze( \@{$self}{ $self->_FreezeThawKeys } ) );
2129 Take a frozen Limits string generated by FreezeLimits and make this tickets
2130 object have that set of limits.
2134 # {{{ sub ThawLimits
2140 #if we don't have $in, get outta here.
2141 return undef unless ($in);
2143 $self->{'RecalcTicketLimits'} = 1;
2146 require MIME::Base64;
2148 #We don't need to die if the thaw fails.
2149 @{$self}{ $self->_FreezeThawKeys }
2150 = eval { @{ Storable::thaw( MIME::Base64::base64_decode($in) ) }; };
2152 $RT::Logger->error($@) if $@;
2158 # {{{ Limit by enum or foreign key
2160 # {{{ sub LimitQueue
2164 LimitQueue takes a paramhash with the fields OPERATOR and VALUE.
2165 OPERATOR is one of = or !=. (It defaults to =).
2166 VALUE is a queue id or Name.
2179 #TODO VALUE should also take queue objects
2180 if ( defined $args{'VALUE'} && $args{'VALUE'} !~ /^\d+$/ ) {
2181 my $queue = new RT::Queue( $self->CurrentUser );
2182 $queue->Load( $args{'VALUE'} );
2183 $args{'VALUE'} = $queue->Id;
2186 # What if they pass in an Id? Check for isNum() and convert to
2189 #TODO check for a valid queue here
2193 VALUE => $args{'VALUE'},
2194 OPERATOR => $args{'OPERATOR'},
2195 DESCRIPTION => join(
2196 ' ', $self->loc('Queue'), $args{'OPERATOR'}, $args{'VALUE'},
2204 # {{{ sub LimitStatus
2208 Takes a paramhash with the fields OPERATOR and VALUE.
2209 OPERATOR is one of = or !=.
2212 RT adds Status != 'deleted' until object has
2213 allow_deleted_search internal property set.
2214 $tickets->{'allow_deleted_search'} = 1;
2215 $tickets->LimitStatus( VALUE => 'deleted' );
2227 VALUE => $args{'VALUE'},
2228 OPERATOR => $args{'OPERATOR'},
2229 DESCRIPTION => join( ' ',
2230 $self->loc('Status'), $args{'OPERATOR'},
2231 $self->loc( $args{'VALUE'} ) ),
2237 # {{{ sub IgnoreType
2241 If called, this search will not automatically limit the set of results found
2242 to tickets of type "Ticket". Tickets of other types, such as "project" and
2243 "approval" will be found.
2250 # Instead of faking a Limit that later gets ignored, fake up the
2251 # fact that we're already looking at type, so that the check in
2252 # Tickets_Overlay_SQL/FromSQL goes down the right branch
2254 # $self->LimitType(VALUE => '__any');
2255 $self->{looking_at_type} = 1;
2264 Takes a paramhash with the fields OPERATOR and VALUE.
2265 OPERATOR is one of = or !=, it defaults to "=".
2266 VALUE is a string to search for in the type of the ticket.
2281 VALUE => $args{'VALUE'},
2282 OPERATOR => $args{'OPERATOR'},
2283 DESCRIPTION => join( ' ',
2284 $self->loc('Type'), $args{'OPERATOR'}, $args{'Limit'}, ),
2292 # {{{ Limit by string field
2294 # {{{ sub LimitSubject
2298 Takes a paramhash with the fields OPERATOR and VALUE.
2299 OPERATOR is one of = or !=.
2300 VALUE is a string to search for in the subject of the ticket.
2309 VALUE => $args{'VALUE'},
2310 OPERATOR => $args{'OPERATOR'},
2311 DESCRIPTION => join( ' ',
2312 $self->loc('Subject'), $args{'OPERATOR'}, $args{'VALUE'}, ),
2320 # {{{ Limit based on ticket numerical attributes
2321 # Things that can be > < = !=
2327 Takes a paramhash with the fields OPERATOR and VALUE.
2328 OPERATOR is one of =, >, < or !=.
2329 VALUE is a ticket Id to search for
2342 VALUE => $args{'VALUE'},
2343 OPERATOR => $args{'OPERATOR'},
2345 join( ' ', $self->loc('Id'), $args{'OPERATOR'}, $args{'VALUE'}, ),
2351 # {{{ sub LimitPriority
2353 =head2 LimitPriority
2355 Takes a paramhash with the fields OPERATOR and VALUE.
2356 OPERATOR is one of =, >, < or !=.
2357 VALUE is a value to match the ticket\'s priority against
2365 FIELD => 'Priority',
2366 VALUE => $args{'VALUE'},
2367 OPERATOR => $args{'OPERATOR'},
2368 DESCRIPTION => join( ' ',
2369 $self->loc('Priority'),
2370 $args{'OPERATOR'}, $args{'VALUE'}, ),
2376 # {{{ sub LimitInitialPriority
2378 =head2 LimitInitialPriority
2380 Takes a paramhash with the fields OPERATOR and VALUE.
2381 OPERATOR is one of =, >, < or !=.
2382 VALUE is a value to match the ticket\'s initial priority against
2387 sub LimitInitialPriority {
2391 FIELD => 'InitialPriority',
2392 VALUE => $args{'VALUE'},
2393 OPERATOR => $args{'OPERATOR'},
2394 DESCRIPTION => join( ' ',
2395 $self->loc('Initial Priority'), $args{'OPERATOR'},
2402 # {{{ sub LimitFinalPriority
2404 =head2 LimitFinalPriority
2406 Takes a paramhash with the fields OPERATOR and VALUE.
2407 OPERATOR is one of =, >, < or !=.
2408 VALUE is a value to match the ticket\'s final priority against
2412 sub LimitFinalPriority {
2416 FIELD => 'FinalPriority',
2417 VALUE => $args{'VALUE'},
2418 OPERATOR => $args{'OPERATOR'},
2419 DESCRIPTION => join( ' ',
2420 $self->loc('Final Priority'), $args{'OPERATOR'},
2427 # {{{ sub LimitTimeWorked
2429 =head2 LimitTimeWorked
2431 Takes a paramhash with the fields OPERATOR and VALUE.
2432 OPERATOR is one of =, >, < or !=.
2433 VALUE is a value to match the ticket's TimeWorked attribute
2437 sub LimitTimeWorked {
2441 FIELD => 'TimeWorked',
2442 VALUE => $args{'VALUE'},
2443 OPERATOR => $args{'OPERATOR'},
2444 DESCRIPTION => join( ' ',
2445 $self->loc('Time Worked'),
2446 $args{'OPERATOR'}, $args{'VALUE'}, ),
2452 # {{{ sub LimitTimeLeft
2454 =head2 LimitTimeLeft
2456 Takes a paramhash with the fields OPERATOR and VALUE.
2457 OPERATOR is one of =, >, < or !=.
2458 VALUE is a value to match the ticket's TimeLeft attribute
2466 FIELD => 'TimeLeft',
2467 VALUE => $args{'VALUE'},
2468 OPERATOR => $args{'OPERATOR'},
2469 DESCRIPTION => join( ' ',
2470 $self->loc('Time Left'),
2471 $args{'OPERATOR'}, $args{'VALUE'}, ),
2479 # {{{ Limiting based on attachment attributes
2481 # {{{ sub LimitContent
2485 Takes a paramhash with the fields OPERATOR and VALUE.
2486 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2487 VALUE is a string to search for in the body of the ticket
2496 VALUE => $args{'VALUE'},
2497 OPERATOR => $args{'OPERATOR'},
2498 DESCRIPTION => join( ' ',
2499 $self->loc('Ticket content'), $args{'OPERATOR'},
2506 # {{{ sub LimitFilename
2508 =head2 LimitFilename
2510 Takes a paramhash with the fields OPERATOR and VALUE.
2511 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2512 VALUE is a string to search for in the body of the ticket
2520 FIELD => 'Filename',
2521 VALUE => $args{'VALUE'},
2522 OPERATOR => $args{'OPERATOR'},
2523 DESCRIPTION => join( ' ',
2524 $self->loc('Attachment filename'), $args{'OPERATOR'},
2530 # {{{ sub LimitContentType
2532 =head2 LimitContentType
2534 Takes a paramhash with the fields OPERATOR and VALUE.
2535 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2536 VALUE is a content type to search ticket attachments for
2540 sub LimitContentType {
2544 FIELD => 'ContentType',
2545 VALUE => $args{'VALUE'},
2546 OPERATOR => $args{'OPERATOR'},
2547 DESCRIPTION => join( ' ',
2548 $self->loc('Ticket content type'), $args{'OPERATOR'},
2557 # {{{ Limiting based on people
2559 # {{{ sub LimitOwner
2563 Takes a paramhash with the fields OPERATOR and VALUE.
2564 OPERATOR is one of = or !=.
2576 my $owner = new RT::User( $self->CurrentUser );
2577 $owner->Load( $args{'VALUE'} );
2579 # FIXME: check for a valid $owner
2582 VALUE => $args{'VALUE'},
2583 OPERATOR => $args{'OPERATOR'},
2584 DESCRIPTION => join( ' ',
2585 $self->loc('Owner'), $args{'OPERATOR'}, $owner->Name(), ),
2592 # {{{ Limiting watchers
2594 # {{{ sub LimitWatcher
2598 Takes a paramhash with the fields OPERATOR, TYPE and VALUE.
2599 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2600 VALUE is a value to match the ticket\'s watcher email addresses against
2601 TYPE is the sort of watchers you want to match against. Leave it undef if you want to search all of them
2615 #build us up a description
2616 my ( $watcher_type, $desc );
2617 if ( $args{'TYPE'} ) {
2618 $watcher_type = $args{'TYPE'};
2621 $watcher_type = "Watcher";
2625 FIELD => $watcher_type,
2626 VALUE => $args{'VALUE'},
2627 OPERATOR => $args{'OPERATOR'},
2628 TYPE => $args{'TYPE'},
2629 DESCRIPTION => join( ' ',
2630 $self->loc($watcher_type),
2631 $args{'OPERATOR'}, $args{'VALUE'}, ),
2641 # {{{ Limiting based on links
2645 =head2 LimitLinkedTo
2647 LimitLinkedTo takes a paramhash with two fields: TYPE and TARGET
2648 TYPE limits the sort of link we want to search on
2650 TYPE = { RefersTo, MemberOf, DependsOn }
2652 TARGET is the id or URI of the TARGET of the link
2666 FIELD => 'LinkedTo',
2668 TARGET => $args{'TARGET'},
2669 TYPE => $args{'TYPE'},
2670 DESCRIPTION => $self->loc(
2671 "Tickets [_1] by [_2]",
2672 $self->loc( $args{'TYPE'} ),
2675 OPERATOR => $args{'OPERATOR'},
2681 # {{{ LimitLinkedFrom
2683 =head2 LimitLinkedFrom
2685 LimitLinkedFrom takes a paramhash with two fields: TYPE and BASE
2686 TYPE limits the sort of link we want to search on
2689 BASE is the id or URI of the BASE of the link
2693 sub LimitLinkedFrom {
2702 # translate RT2 From/To naming to RT3 TicketSQL naming
2703 my %fromToMap = qw(DependsOn DependentOn
2705 RefersTo ReferredToBy);
2707 my $type = $args{'TYPE'};
2708 $type = $fromToMap{$type} if exists( $fromToMap{$type} );
2711 FIELD => 'LinkedTo',
2713 BASE => $args{'BASE'},
2715 DESCRIPTION => $self->loc(
2716 "Tickets [_1] [_2]",
2717 $self->loc( $args{'TYPE'} ),
2720 OPERATOR => $args{'OPERATOR'},
2729 my $ticket_id = shift;
2730 return $self->LimitLinkedTo(
2732 TARGET => $ticket_id,
2739 # {{{ LimitHasMember
2740 sub LimitHasMember {
2742 my $ticket_id = shift;
2743 return $self->LimitLinkedFrom(
2745 BASE => "$ticket_id",
2746 TYPE => 'HasMember',
2753 # {{{ LimitDependsOn
2755 sub LimitDependsOn {
2757 my $ticket_id = shift;
2758 return $self->LimitLinkedTo(
2760 TARGET => $ticket_id,
2761 TYPE => 'DependsOn',
2768 # {{{ LimitDependedOnBy
2770 sub LimitDependedOnBy {
2772 my $ticket_id = shift;
2773 return $self->LimitLinkedFrom(
2776 TYPE => 'DependentOn',
2787 my $ticket_id = shift;
2788 return $self->LimitLinkedTo(
2790 TARGET => $ticket_id,
2798 # {{{ LimitReferredToBy
2800 sub LimitReferredToBy {
2802 my $ticket_id = shift;
2803 return $self->LimitLinkedFrom(
2806 TYPE => 'ReferredToBy',
2814 # {{{ limit based on ticket date attribtes
2818 =head2 LimitDate (FIELD => 'DateField', OPERATOR => $oper, VALUE => $ISODate)
2820 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
2822 OPERATOR is one of > or <
2823 VALUE is a date and time in ISO format in GMT
2824 FIELD is one of Starts, Started, Told, Created, Resolved, LastUpdated
2826 There are also helper functions of the form LimitFIELD that eliminate
2827 the need to pass in a FIELD argument.
2841 #Set the description if we didn't get handed it above
2842 unless ( $args{'DESCRIPTION'} ) {
2843 $args{'DESCRIPTION'} = $args{'FIELD'} . " "
2844 . $args{'OPERATOR'} . " "
2845 . $args{'VALUE'} . " GMT";
2848 $self->Limit(%args);
2856 $self->LimitDate( FIELD => 'Created', @_ );
2861 $self->LimitDate( FIELD => 'Due', @_ );
2867 $self->LimitDate( FIELD => 'Starts', @_ );
2873 $self->LimitDate( FIELD => 'Started', @_ );
2878 $self->LimitDate( FIELD => 'Resolved', @_ );
2883 $self->LimitDate( FIELD => 'Told', @_ );
2886 sub LimitLastUpdated {
2888 $self->LimitDate( FIELD => 'LastUpdated', @_ );
2892 # {{{ sub LimitTransactionDate
2894 =head2 LimitTransactionDate (OPERATOR => $oper, VALUE => $ISODate)
2896 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
2898 OPERATOR is one of > or <
2899 VALUE is a date and time in ISO format in GMT
2904 sub LimitTransactionDate {
2907 FIELD => 'TransactionDate',
2914 # <20021217042756.GK28744@pallas.fsck.com>
2915 # "Kill It" - Jesse.
2917 #Set the description if we didn't get handed it above
2918 unless ( $args{'DESCRIPTION'} ) {
2919 $args{'DESCRIPTION'} = $args{'FIELD'} . " "
2920 . $args{'OPERATOR'} . " "
2921 . $args{'VALUE'} . " GMT";
2924 $self->Limit(%args);
2932 # {{{ Limit based on custom fields
2933 # {{{ sub LimitCustomField
2935 =head2 LimitCustomField
2937 Takes a paramhash of key/value pairs with the following keys:
2941 =item CUSTOMFIELD - CustomField name or id. If a name is passed, an additional parameter QUEUE may also be passed to distinguish the custom field.
2943 =item OPERATOR - The usual Limit operators
2945 =item VALUE - The value to compare against
2951 sub LimitCustomField {
2955 CUSTOMFIELD => undef,
2957 DESCRIPTION => undef,
2958 FIELD => 'CustomFieldValue',
2963 my $CF = RT::CustomField->new( $self->CurrentUser );
2964 if ( $args{CUSTOMFIELD} =~ /^\d+$/ ) {
2965 $CF->Load( $args{CUSTOMFIELD} );
2968 $CF->LoadByNameAndQueue(
2969 Name => $args{CUSTOMFIELD},
2970 Queue => $args{QUEUE}
2972 $args{CUSTOMFIELD} = $CF->Id;
2975 #If we are looking to compare with a null value.
2976 if ( $args{'OPERATOR'} =~ /^is$/i ) {
2977 $args{'DESCRIPTION'}
2978 ||= $self->loc( "Custom field [_1] has no value.", $CF->Name );
2980 elsif ( $args{'OPERATOR'} =~ /^is not$/i ) {
2981 $args{'DESCRIPTION'}
2982 ||= $self->loc( "Custom field [_1] has a value.", $CF->Name );
2985 # if we're not looking to compare with a null value
2987 $args{'DESCRIPTION'} ||= $self->loc( "Custom field [_1] [_2] [_3]",
2988 $CF->Name, $args{OPERATOR}, $args{VALUE} );
2991 if ( defined $args{'QUEUE'} && $args{'QUEUE'} =~ /\D/ ) {
2992 my $QueueObj = RT::Queue->new( $self->CurrentUser );
2993 $QueueObj->Load( $args{'QUEUE'} );
2994 $args{'QUEUE'} = $QueueObj->Id;
2996 delete $args{'QUEUE'} unless defined $args{'QUEUE'} && length $args{'QUEUE'};
2999 @rest = ( ENTRYAGGREGATOR => 'AND' )
3000 if ( $CF->Type eq 'SelectMultiple' );
3003 VALUE => $args{VALUE},
3005 .(defined $args{'QUEUE'}? ".{$args{'QUEUE'}}" : '' )
3006 .".{" . $CF->Name . "}",
3007 OPERATOR => $args{OPERATOR},
3012 $self->{'RecalcTicketLimits'} = 1;
3018 # {{{ sub _NextIndex
3022 Keep track of the counter for the array of restrictions
3028 return ( $self->{'restriction_index'}++ );
3035 # {{{ Core bits to make this a DBIx::SearchBuilder object
3040 $self->{'table'} = "Tickets";
3041 $self->{'RecalcTicketLimits'} = 1;
3042 $self->{'looking_at_effective_id'} = 0;
3043 $self->{'looking_at_type'} = 0;
3044 $self->{'restriction_index'} = 1;
3045 $self->{'primary_key'} = "id";
3046 delete $self->{'items_array'};
3047 delete $self->{'item_map'};
3048 delete $self->{'columns_to_display'};
3049 $self->SUPER::_Init(@_);
3060 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
3061 return ( $self->SUPER::Count() );
3069 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
3070 return ( $self->SUPER::CountAll() );
3075 # {{{ sub ItemsArrayRef
3077 =head2 ItemsArrayRef
3079 Returns a reference to the set of all items found in this search
3086 return $self->{'items_array'} if $self->{'items_array'};
3088 my $placeholder = $self->_ItemsCounter;
3089 $self->GotoFirstItem();
3090 while ( my $item = $self->Next ) {
3091 push( @{ $self->{'items_array'} }, $item );
3093 $self->GotoItem($placeholder);
3094 $self->{'items_array'}
3095 = $self->ItemsOrderBy( $self->{'items_array'} );
3097 return $self->{'items_array'};
3100 sub ItemsArrayRefWindow {
3104 my @old = ($self->_ItemsCounter, $self->RowsPerPage, $self->FirstRow+1);
3106 $self->RowsPerPage( $window );
3108 $self->GotoFirstItem;
3111 while ( my $item = $self->Next ) {
3115 $self->RowsPerPage( $old[1] );
3116 $self->FirstRow( $old[2] );
3117 $self->GotoItem( $old[0] );
3128 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
3130 my $Ticket = $self->SUPER::Next;
3131 return $Ticket unless $Ticket;
3133 if ( $Ticket->__Value('Status') eq 'deleted'
3134 && !$self->{'allow_deleted_search'} )
3138 elsif ( RT->Config->Get('UseSQLForACLChecks') ) {
3139 # if we found a ticket with this option enabled then
3140 # all tickets we found are ACLed, cache this fact
3141 my $key = join ";:;", $self->CurrentUser->id, 'ShowTicket', 'RT::Ticket-'. $Ticket->id;
3142 $RT::Principal::_ACL_CACHE->set( $key => 1 );
3145 elsif ( $Ticket->CurrentUserHasRight('ShowTicket') ) {
3150 # If the user doesn't have the right to show this ticket
3157 $self->CurrentUserCanSee if RT->Config->Get('UseSQLForACLChecks');
3158 return $self->SUPER::_DoSearch( @_ );
3163 $self->CurrentUserCanSee if RT->Config->Get('UseSQLForACLChecks');
3164 return $self->SUPER::_DoCount( @_ );
3170 my $cache_key = 'RolesHasRight;:;ShowTicket';
3172 if ( my $cached = $RT::Principal::_ACL_CACHE->fetch( $cache_key ) ) {
3176 my $ACL = RT::ACL->new( $RT::SystemUser );
3177 $ACL->Limit( FIELD => 'RightName', VALUE => 'ShowTicket' );
3178 $ACL->Limit( FIELD => 'PrincipalType', OPERATOR => '!=', VALUE => 'Group' );
3179 my $principal_alias = $ACL->Join(
3181 FIELD1 => 'PrincipalId',
3182 TABLE2 => 'Principals',
3185 $ACL->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3188 while ( my $ACE = $ACL->Next ) {
3189 my $role = $ACE->PrincipalType;
3190 my $type = $ACE->ObjectType;
3191 if ( $type eq 'RT::System' ) {
3194 elsif ( $type eq 'RT::Queue' ) {
3195 next if $res{ $role } && !ref $res{ $role };
3196 push @{ $res{ $role } ||= [] }, $ACE->ObjectId;
3199 $RT::Logger->error('ShowTicket right is granted on unsupported object');
3202 $RT::Principal::_ACL_CACHE->set( $cache_key => \%res );
3206 sub _DirectlyCanSeeIn {
3208 my $id = $self->CurrentUser->id;
3210 my $cache_key = 'User-'. $id .';:;ShowTicket;:;DirectlyCanSeeIn';
3211 if ( my $cached = $RT::Principal::_ACL_CACHE->fetch( $cache_key ) ) {
3215 my $ACL = RT::ACL->new( $RT::SystemUser );
3216 $ACL->Limit( FIELD => 'RightName', VALUE => 'ShowTicket' );
3217 my $principal_alias = $ACL->Join(
3219 FIELD1 => 'PrincipalId',
3220 TABLE2 => 'Principals',
3223 $ACL->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3224 my $cgm_alias = $ACL->Join(
3226 FIELD1 => 'PrincipalId',
3227 TABLE2 => 'CachedGroupMembers',
3228 FIELD2 => 'GroupId',
3230 $ACL->Limit( ALIAS => $cgm_alias, FIELD => 'MemberId', VALUE => $id );
3231 $ACL->Limit( ALIAS => $cgm_alias, FIELD => 'Disabled', VALUE => 0 );
3234 while ( my $ACE = $ACL->Next ) {
3235 my $type = $ACE->ObjectType;
3236 if ( $type eq 'RT::System' ) {
3237 # If user is direct member of a group that has the right
3238 # on the system then he can see any ticket
3239 $RT::Principal::_ACL_CACHE->set( $cache_key => [-1] );
3242 elsif ( $type eq 'RT::Queue' ) {
3243 push @res, $ACE->ObjectId;
3246 $RT::Logger->error('ShowTicket right is granted on unsupported object');
3249 $RT::Principal::_ACL_CACHE->set( $cache_key => \@res );
3253 sub CurrentUserCanSee {
3255 return if $self->{'_sql_current_user_can_see_applied'};
3257 return $self->{'_sql_current_user_can_see_applied'} = 1
3258 if $self->CurrentUser->UserObj->HasRight(
3259 Right => 'SuperUser', Object => $RT::System
3262 my $id = $self->CurrentUser->id;
3264 # directly can see in all queues then we have nothing to do
3265 my @direct_queues = $self->_DirectlyCanSeeIn;
3266 return $self->{'_sql_current_user_can_see_applied'} = 1
3267 if @direct_queues && $direct_queues[0] == -1;
3269 my %roles = $self->_RolesCanSee;
3271 my %skip = map { $_ => 1 } @direct_queues;
3272 foreach my $role ( keys %roles ) {
3273 next unless ref $roles{ $role };
3275 my @queues = grep !$skip{$_}, @{ $roles{ $role } };
3277 $roles{ $role } = \@queues;
3279 delete $roles{ $role };
3284 # there is no global watchers, only queues and tickes, if at
3285 # some point we will add global roles then it's gonna blow
3286 # the idea here is that if the right is set globaly for a role
3287 # and user plays this role for a queue directly not a ticket
3288 # then we have to check in advance
3289 if ( my @tmp = grep $_ ne 'Owner' && !ref $roles{ $_ }, keys %roles ) {
3291 my $groups = RT::Groups->new( $RT::SystemUser );
3292 $groups->Limit( FIELD => 'Domain', VALUE => 'RT::Queue-Role' );
3294 $groups->Limit( FIELD => 'Type', VALUE => $_ );
3296 my $principal_alias = $groups->Join(
3299 TABLE2 => 'Principals',
3302 $groups->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3303 my $cgm_alias = $groups->Join(
3306 TABLE2 => 'CachedGroupMembers',
3307 FIELD2 => 'GroupId',
3309 $groups->Limit( ALIAS => $cgm_alias, FIELD => 'MemberId', VALUE => $id );
3310 $groups->Limit( ALIAS => $cgm_alias, FIELD => 'Disabled', VALUE => 0 );
3311 while ( my $group = $groups->Next ) {
3312 push @direct_queues, $group->Instance;
3316 unless ( @direct_queues || keys %roles ) {
3317 $self->SUPER::Limit(
3322 ENTRYAGGREGATOR => 'AND',
3324 return $self->{'_sql_current_user_can_see_applied'} = 1;
3328 my $join_roles = keys %roles;
3329 $join_roles = 0 if $join_roles == 1 && $roles{'Owner'};
3330 my ($role_group_alias, $cgm_alias);
3331 if ( $join_roles ) {
3332 $role_group_alias = $self->_RoleGroupsJoin( New => 1 );
3333 $cgm_alias = $self->_GroupMembersJoin( GroupsAlias => $role_group_alias );
3334 $self->SUPER::Limit(
3335 LEFTJOIN => $cgm_alias,
3336 FIELD => 'MemberId',
3341 my $limit_queues = sub {
3345 return unless @queues;
3346 if ( @queues == 1 ) {
3347 $self->SUPER::Limit(
3352 ENTRYAGGREGATOR => $ea,
3355 $self->SUPER::_OpenParen('ACL');
3356 foreach my $q ( @queues ) {
3357 $self->SUPER::Limit(
3362 ENTRYAGGREGATOR => $ea,
3366 $self->SUPER::_CloseParen('ACL');
3371 $self->SUPER::_OpenParen('ACL');
3373 $ea = 'OR' if $limit_queues->( $ea, @direct_queues );
3374 while ( my ($role, $queues) = each %roles ) {
3375 $self->SUPER::_OpenParen('ACL');
3376 if ( $role eq 'Owner' ) {
3377 $self->SUPER::Limit(
3381 ENTRYAGGREGATOR => $ea,
3385 $self->SUPER::Limit(
3387 ALIAS => $cgm_alias,
3388 FIELD => 'MemberId',
3389 OPERATOR => 'IS NOT',
3392 ENTRYAGGREGATOR => $ea,
3394 $self->SUPER::Limit(
3396 ALIAS => $role_group_alias,
3399 ENTRYAGGREGATOR => 'AND',
3402 $limit_queues->( 'AND', @$queues ) if ref $queues;
3403 $ea = 'OR' if $ea eq 'AND';
3404 $self->SUPER::_CloseParen('ACL');
3406 $self->SUPER::_CloseParen('ACL');
3408 return $self->{'_sql_current_user_can_see_applied'} = 1;
3415 # {{{ Deal with storing and restoring restrictions
3417 # {{{ sub LoadRestrictions
3419 =head2 LoadRestrictions
3421 LoadRestrictions takes a string which can fully populate the TicketRestrictons hash.
3422 TODO It is not yet implemented
3428 # {{{ sub DescribeRestrictions
3430 =head2 DescribeRestrictions
3433 Returns a hash keyed by restriction id.
3434 Each element of the hash is currently a one element hash that contains DESCRIPTION which
3435 is a description of the purpose of that TicketRestriction
3439 sub DescribeRestrictions {
3444 foreach my $row ( keys %{ $self->{'TicketRestrictions'} } ) {
3445 $listing{$row} = $self->{'TicketRestrictions'}{$row}{'DESCRIPTION'};
3452 # {{{ sub RestrictionValues
3454 =head2 RestrictionValues FIELD
3456 Takes a restriction field and returns a list of values this field is restricted
3461 sub RestrictionValues {
3464 map $self->{'TicketRestrictions'}{$_}{'VALUE'}, grep {
3465 $self->{'TicketRestrictions'}{$_}{'FIELD'} eq $field
3466 && $self->{'TicketRestrictions'}{$_}{'OPERATOR'} eq "="
3468 keys %{ $self->{'TicketRestrictions'} };
3473 # {{{ sub ClearRestrictions
3475 =head2 ClearRestrictions
3477 Removes all restrictions irretrievably
3481 sub ClearRestrictions {
3483 delete $self->{'TicketRestrictions'};
3484 $self->{'looking_at_effective_id'} = 0;
3485 $self->{'looking_at_type'} = 0;
3486 $self->{'RecalcTicketLimits'} = 1;
3491 # {{{ sub DeleteRestriction
3493 =head2 DeleteRestriction
3495 Takes the row Id of a restriction (From DescribeRestrictions' output, for example.
3496 Removes that restriction from the session's limits.
3500 sub DeleteRestriction {
3503 delete $self->{'TicketRestrictions'}{$row};
3505 $self->{'RecalcTicketLimits'} = 1;
3507 #make the underlying easysearch object forget all its preconceptions
3512 # {{{ sub _RestrictionsToClauses
3514 # Convert a set of oldstyle SB Restrictions to Clauses for RQL
3516 sub _RestrictionsToClauses {
3520 foreach my $row ( keys %{ $self->{'TicketRestrictions'} } ) {
3521 my $restriction = $self->{'TicketRestrictions'}{$row};
3523 # We need to reimplement the subclause aggregation that SearchBuilder does.
3524 # Default Subclause is ALIAS.FIELD, and default ALIAS is 'main',
3525 # Then SB AND's the different Subclauses together.
3527 # So, we want to group things into Subclauses, convert them to
3528 # SQL, and then join them with the appropriate DefaultEA.
3529 # Then join each subclause group with AND.
3531 my $field = $restriction->{'FIELD'};
3532 my $realfield = $field; # CustomFields fake up a fieldname, so
3533 # we need to figure that out
3536 # Rewrite LinkedTo meta field to the real field
3537 if ( $field =~ /LinkedTo/ ) {
3538 $realfield = $field = $restriction->{'TYPE'};
3542 # Handle subkey fields with a different real field
3543 if ( $field =~ /^(\w+)\./ ) {
3547 die "I don't know about $field yet"
3548 unless ( exists $FIELD_METADATA{$realfield}
3549 or $restriction->{CUSTOMFIELD} );
3551 my $type = $FIELD_METADATA{$realfield}->[0];
3552 my $op = $restriction->{'OPERATOR'};
3556 map { $restriction->{$_} } qw(VALUE TICKET BASE TARGET)
3559 # this performs the moral equivalent of defined or/dor/C<//>,
3560 # without the short circuiting.You need to use a 'defined or'
3561 # type thing instead of just checking for truth values, because
3562 # VALUE could be 0.(i.e. "false")
3564 # You could also use this, but I find it less aesthetic:
3565 # (although it does short circuit)
3566 #( defined $restriction->{'VALUE'}? $restriction->{VALUE} :
3567 # defined $restriction->{'TICKET'} ?
3568 # $restriction->{TICKET} :
3569 # defined $restriction->{'BASE'} ?
3570 # $restriction->{BASE} :
3571 # defined $restriction->{'TARGET'} ?
3572 # $restriction->{TARGET} )
3574 my $ea = $restriction->{ENTRYAGGREGATOR}
3575 || $DefaultEA{$type}
3578 die "Invalid operator $op for $field ($type)"
3579 unless exists $ea->{$op};
3583 # Each CustomField should be put into a different Clause so they
3584 # are ANDed together.
3585 if ( $restriction->{CUSTOMFIELD} ) {
3586 $realfield = $field;
3589 exists $clause{$realfield} or $clause{$realfield} = [];
3592 $field =~ s!(['"])!\\$1!g;
3593 $value =~ s!(['"])!\\$1!g;
3594 my $data = [ $ea, $type, $field, $op, $value ];
3596 # here is where we store extra data, say if it's a keyword or
3597 # something. (I.e. "TYPE SPECIFIC STUFF")
3599 push @{ $clause{$realfield} }, $data;
3606 # {{{ sub _ProcessRestrictions
3608 =head2 _ProcessRestrictions PARAMHASH
3610 # The new _ProcessRestrictions is somewhat dependent on the SQL stuff,
3611 # but isn't quite generic enough to move into Tickets_Overlay_SQL.
3615 sub _ProcessRestrictions {
3618 #Blow away ticket aliases since we'll need to regenerate them for
3620 delete $self->{'TicketAliases'};
3621 delete $self->{'items_array'};
3622 delete $self->{'item_map'};
3623 delete $self->{'raw_rows'};
3624 delete $self->{'rows'};
3625 delete $self->{'count_all'};
3627 my $sql = $self->Query; # Violating the _SQL namespace
3628 if ( !$sql || $self->{'RecalcTicketLimits'} ) {
3630 # "Restrictions to Clauses Branch\n";
3631 my $clauseRef = eval { $self->_RestrictionsToClauses; };
3633 $RT::Logger->error( "RestrictionsToClauses: " . $@ );
3637 $sql = $self->ClausesToSQL($clauseRef);
3638 $self->FromSQL($sql) if $sql;
3642 $self->{'RecalcTicketLimits'} = 0;
3646 =head2 _BuildItemMap
3648 Build up a L</ItemMap> of first/last/next/prev items, so that we can
3649 display search nav quickly.
3656 my $window = RT->Config->Get('TicketsItemMapSize');
3658 $self->{'item_map'} = {};
3660 my $items = $self->ItemsArrayRefWindow( $window );
3661 return unless $items && @$items;
3664 $self->{'item_map'}{'first'} = $items->[0]->EffectiveId;
3665 for ( my $i = 0; $i < @$items; $i++ ) {
3666 my $item = $items->[$i];
3667 my $id = $item->EffectiveId;
3668 $self->{'item_map'}{$id}{'defined'} = 1;
3669 $self->{'item_map'}{$id}{'prev'} = $prev;
3670 $self->{'item_map'}{$id}{'next'} = $items->[$i+1]->EffectiveId
3674 $self->{'item_map'}{'last'} = $prev
3675 if !$window || @$items < $window;
3680 Returns an a map of all items found by this search. The map is a hash
3684 first => <first ticket id found>,
3685 last => <last ticket id found or undef>,
3688 prev => <the ticket id found before>,
3689 next => <the ticket id found after>,
3701 $self->_BuildItemMap unless $self->{'item_map'};
3702 return $self->{'item_map'};
3710 =head2 PrepForSerialization
3712 You don't want to serialize a big tickets object, as
3713 the {items} hash will be instantly invalid _and_ eat
3718 sub PrepForSerialization {
3720 delete $self->{'items'};
3721 delete $self->{'items_array'};
3722 $self->RedoSearch();
3727 RT::Tickets supports several flags which alter search behavior:
3730 allow_deleted_search (Otherwise never show deleted tickets in search results)
3731 looking_at_type (otherwise limit to type=ticket)
3733 These flags are set by calling
3735 $tickets->{'flagname'} = 1;
3737 BUG: There should be an API for this