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;
1998 # compound subkey: separate into table name and field in that table
1999 # (must be linked by custnum)
2000 $subfield = lc($subfield);
2001 ($table2, $subfield) = ($1, $2) if $subfield =~ /^(\w+)?\.(\w+)$/;
2002 $subfield = $pkey if $subfield eq 'number';
2004 # if it's compound, create a join from cust_main or cust_svc to that
2005 # table, using custnum or svcnum, and Limit on that table instead.
2006 foreach my $a (@alias) {
2017 # do the actual Limit
2018 $self->SUPER::Limit(
2023 ENTRYAGGREGATOR => 'AND',
2024 # no SUBCLAUSE needed, limits on different aliases across left joins
2025 # are inherently independent
2028 # then, since it's a left join, exclude tickets for which there is now
2029 # no matching record in the table we just limited on. (Or where there
2030 # is a matching record, if $is_negative.)
2031 # For a cust_main query (where there are two different aliases), this
2032 # will produce a subclause: "cust_main_1.custnum IS NOT NULL OR
2033 # cust_main_2.custnum IS NOT NULL" (or "IS NULL AND..." for a negative
2039 OPERATOR => $is_negative ? 'IS' : 'IS NOT',
2042 ENTRYAGGREGATOR => $is_negative ? 'AND' : 'OR',
2043 SUBCLAUSE => 'fs_limit',
2052 # {{{ Limit the result set based on content
2058 Takes a paramhash with the fields FIELD, OPERATOR, VALUE and DESCRIPTION
2059 Generally best called from LimitFoo methods
2069 DESCRIPTION => undef,
2072 $args{'DESCRIPTION'} = $self->loc(
2073 "[_1] [_2] [_3]", $args{'FIELD'},
2074 $args{'OPERATOR'}, $args{'VALUE'}
2076 if ( !defined $args{'DESCRIPTION'} );
2078 my $index = $self->_NextIndex;
2080 # make the TicketRestrictions hash the equivalent of whatever we just passed in;
2082 %{ $self->{'TicketRestrictions'}{$index} } = %args;
2084 $self->{'RecalcTicketLimits'} = 1;
2086 # If we're looking at the effective id, we don't want to append the other clause
2087 # which limits us to tickets where id = effective id
2088 if ( $args{'FIELD'} eq 'EffectiveId'
2089 && ( !$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) )
2091 $self->{'looking_at_effective_id'} = 1;
2094 if ( $args{'FIELD'} eq 'Type'
2095 && ( !$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) )
2097 $self->{'looking_at_type'} = 1;
2107 Returns a frozen string suitable for handing back to ThawLimits.
2111 sub _FreezeThawKeys {
2112 'TicketRestrictions', 'restriction_index', 'looking_at_effective_id',
2116 # {{{ sub FreezeLimits
2121 require MIME::Base64;
2122 MIME::Base64::base64_encode(
2123 Storable::freeze( \@{$self}{ $self->_FreezeThawKeys } ) );
2130 Take a frozen Limits string generated by FreezeLimits and make this tickets
2131 object have that set of limits.
2135 # {{{ sub ThawLimits
2141 #if we don't have $in, get outta here.
2142 return undef unless ($in);
2144 $self->{'RecalcTicketLimits'} = 1;
2147 require MIME::Base64;
2149 #We don't need to die if the thaw fails.
2150 @{$self}{ $self->_FreezeThawKeys }
2151 = eval { @{ Storable::thaw( MIME::Base64::base64_decode($in) ) }; };
2153 $RT::Logger->error($@) if $@;
2159 # {{{ Limit by enum or foreign key
2161 # {{{ sub LimitQueue
2165 LimitQueue takes a paramhash with the fields OPERATOR and VALUE.
2166 OPERATOR is one of = or !=. (It defaults to =).
2167 VALUE is a queue id or Name.
2180 #TODO VALUE should also take queue objects
2181 if ( defined $args{'VALUE'} && $args{'VALUE'} !~ /^\d+$/ ) {
2182 my $queue = new RT::Queue( $self->CurrentUser );
2183 $queue->Load( $args{'VALUE'} );
2184 $args{'VALUE'} = $queue->Id;
2187 # What if they pass in an Id? Check for isNum() and convert to
2190 #TODO check for a valid queue here
2194 VALUE => $args{'VALUE'},
2195 OPERATOR => $args{'OPERATOR'},
2196 DESCRIPTION => join(
2197 ' ', $self->loc('Queue'), $args{'OPERATOR'}, $args{'VALUE'},
2205 # {{{ sub LimitStatus
2209 Takes a paramhash with the fields OPERATOR and VALUE.
2210 OPERATOR is one of = or !=.
2213 RT adds Status != 'deleted' until object has
2214 allow_deleted_search internal property set.
2215 $tickets->{'allow_deleted_search'} = 1;
2216 $tickets->LimitStatus( VALUE => 'deleted' );
2228 VALUE => $args{'VALUE'},
2229 OPERATOR => $args{'OPERATOR'},
2230 DESCRIPTION => join( ' ',
2231 $self->loc('Status'), $args{'OPERATOR'},
2232 $self->loc( $args{'VALUE'} ) ),
2238 # {{{ sub IgnoreType
2242 If called, this search will not automatically limit the set of results found
2243 to tickets of type "Ticket". Tickets of other types, such as "project" and
2244 "approval" will be found.
2251 # Instead of faking a Limit that later gets ignored, fake up the
2252 # fact that we're already looking at type, so that the check in
2253 # Tickets_Overlay_SQL/FromSQL goes down the right branch
2255 # $self->LimitType(VALUE => '__any');
2256 $self->{looking_at_type} = 1;
2265 Takes a paramhash with the fields OPERATOR and VALUE.
2266 OPERATOR is one of = or !=, it defaults to "=".
2267 VALUE is a string to search for in the type of the ticket.
2282 VALUE => $args{'VALUE'},
2283 OPERATOR => $args{'OPERATOR'},
2284 DESCRIPTION => join( ' ',
2285 $self->loc('Type'), $args{'OPERATOR'}, $args{'Limit'}, ),
2293 # {{{ Limit by string field
2295 # {{{ sub LimitSubject
2299 Takes a paramhash with the fields OPERATOR and VALUE.
2300 OPERATOR is one of = or !=.
2301 VALUE is a string to search for in the subject of the ticket.
2310 VALUE => $args{'VALUE'},
2311 OPERATOR => $args{'OPERATOR'},
2312 DESCRIPTION => join( ' ',
2313 $self->loc('Subject'), $args{'OPERATOR'}, $args{'VALUE'}, ),
2321 # {{{ Limit based on ticket numerical attributes
2322 # Things that can be > < = !=
2328 Takes a paramhash with the fields OPERATOR and VALUE.
2329 OPERATOR is one of =, >, < or !=.
2330 VALUE is a ticket Id to search for
2343 VALUE => $args{'VALUE'},
2344 OPERATOR => $args{'OPERATOR'},
2346 join( ' ', $self->loc('Id'), $args{'OPERATOR'}, $args{'VALUE'}, ),
2352 # {{{ sub LimitPriority
2354 =head2 LimitPriority
2356 Takes a paramhash with the fields OPERATOR and VALUE.
2357 OPERATOR is one of =, >, < or !=.
2358 VALUE is a value to match the ticket\'s priority against
2366 FIELD => 'Priority',
2367 VALUE => $args{'VALUE'},
2368 OPERATOR => $args{'OPERATOR'},
2369 DESCRIPTION => join( ' ',
2370 $self->loc('Priority'),
2371 $args{'OPERATOR'}, $args{'VALUE'}, ),
2377 # {{{ sub LimitInitialPriority
2379 =head2 LimitInitialPriority
2381 Takes a paramhash with the fields OPERATOR and VALUE.
2382 OPERATOR is one of =, >, < or !=.
2383 VALUE is a value to match the ticket\'s initial priority against
2388 sub LimitInitialPriority {
2392 FIELD => 'InitialPriority',
2393 VALUE => $args{'VALUE'},
2394 OPERATOR => $args{'OPERATOR'},
2395 DESCRIPTION => join( ' ',
2396 $self->loc('Initial Priority'), $args{'OPERATOR'},
2403 # {{{ sub LimitFinalPriority
2405 =head2 LimitFinalPriority
2407 Takes a paramhash with the fields OPERATOR and VALUE.
2408 OPERATOR is one of =, >, < or !=.
2409 VALUE is a value to match the ticket\'s final priority against
2413 sub LimitFinalPriority {
2417 FIELD => 'FinalPriority',
2418 VALUE => $args{'VALUE'},
2419 OPERATOR => $args{'OPERATOR'},
2420 DESCRIPTION => join( ' ',
2421 $self->loc('Final Priority'), $args{'OPERATOR'},
2428 # {{{ sub LimitTimeWorked
2430 =head2 LimitTimeWorked
2432 Takes a paramhash with the fields OPERATOR and VALUE.
2433 OPERATOR is one of =, >, < or !=.
2434 VALUE is a value to match the ticket's TimeWorked attribute
2438 sub LimitTimeWorked {
2442 FIELD => 'TimeWorked',
2443 VALUE => $args{'VALUE'},
2444 OPERATOR => $args{'OPERATOR'},
2445 DESCRIPTION => join( ' ',
2446 $self->loc('Time Worked'),
2447 $args{'OPERATOR'}, $args{'VALUE'}, ),
2453 # {{{ sub LimitTimeLeft
2455 =head2 LimitTimeLeft
2457 Takes a paramhash with the fields OPERATOR and VALUE.
2458 OPERATOR is one of =, >, < or !=.
2459 VALUE is a value to match the ticket's TimeLeft attribute
2467 FIELD => 'TimeLeft',
2468 VALUE => $args{'VALUE'},
2469 OPERATOR => $args{'OPERATOR'},
2470 DESCRIPTION => join( ' ',
2471 $self->loc('Time Left'),
2472 $args{'OPERATOR'}, $args{'VALUE'}, ),
2480 # {{{ Limiting based on attachment attributes
2482 # {{{ sub LimitContent
2486 Takes a paramhash with the fields OPERATOR and VALUE.
2487 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2488 VALUE is a string to search for in the body of the ticket
2497 VALUE => $args{'VALUE'},
2498 OPERATOR => $args{'OPERATOR'},
2499 DESCRIPTION => join( ' ',
2500 $self->loc('Ticket content'), $args{'OPERATOR'},
2507 # {{{ sub LimitFilename
2509 =head2 LimitFilename
2511 Takes a paramhash with the fields OPERATOR and VALUE.
2512 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2513 VALUE is a string to search for in the body of the ticket
2521 FIELD => 'Filename',
2522 VALUE => $args{'VALUE'},
2523 OPERATOR => $args{'OPERATOR'},
2524 DESCRIPTION => join( ' ',
2525 $self->loc('Attachment filename'), $args{'OPERATOR'},
2531 # {{{ sub LimitContentType
2533 =head2 LimitContentType
2535 Takes a paramhash with the fields OPERATOR and VALUE.
2536 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2537 VALUE is a content type to search ticket attachments for
2541 sub LimitContentType {
2545 FIELD => 'ContentType',
2546 VALUE => $args{'VALUE'},
2547 OPERATOR => $args{'OPERATOR'},
2548 DESCRIPTION => join( ' ',
2549 $self->loc('Ticket content type'), $args{'OPERATOR'},
2558 # {{{ Limiting based on people
2560 # {{{ sub LimitOwner
2564 Takes a paramhash with the fields OPERATOR and VALUE.
2565 OPERATOR is one of = or !=.
2577 my $owner = new RT::User( $self->CurrentUser );
2578 $owner->Load( $args{'VALUE'} );
2580 # FIXME: check for a valid $owner
2583 VALUE => $args{'VALUE'},
2584 OPERATOR => $args{'OPERATOR'},
2585 DESCRIPTION => join( ' ',
2586 $self->loc('Owner'), $args{'OPERATOR'}, $owner->Name(), ),
2593 # {{{ Limiting watchers
2595 # {{{ sub LimitWatcher
2599 Takes a paramhash with the fields OPERATOR, TYPE and VALUE.
2600 OPERATOR is one of =, LIKE, NOT LIKE or !=.
2601 VALUE is a value to match the ticket\'s watcher email addresses against
2602 TYPE is the sort of watchers you want to match against. Leave it undef if you want to search all of them
2616 #build us up a description
2617 my ( $watcher_type, $desc );
2618 if ( $args{'TYPE'} ) {
2619 $watcher_type = $args{'TYPE'};
2622 $watcher_type = "Watcher";
2626 FIELD => $watcher_type,
2627 VALUE => $args{'VALUE'},
2628 OPERATOR => $args{'OPERATOR'},
2629 TYPE => $args{'TYPE'},
2630 DESCRIPTION => join( ' ',
2631 $self->loc($watcher_type),
2632 $args{'OPERATOR'}, $args{'VALUE'}, ),
2642 # {{{ Limiting based on links
2646 =head2 LimitLinkedTo
2648 LimitLinkedTo takes a paramhash with two fields: TYPE and TARGET
2649 TYPE limits the sort of link we want to search on
2651 TYPE = { RefersTo, MemberOf, DependsOn }
2653 TARGET is the id or URI of the TARGET of the link
2667 FIELD => 'LinkedTo',
2669 TARGET => $args{'TARGET'},
2670 TYPE => $args{'TYPE'},
2671 DESCRIPTION => $self->loc(
2672 "Tickets [_1] by [_2]",
2673 $self->loc( $args{'TYPE'} ),
2676 OPERATOR => $args{'OPERATOR'},
2682 # {{{ LimitLinkedFrom
2684 =head2 LimitLinkedFrom
2686 LimitLinkedFrom takes a paramhash with two fields: TYPE and BASE
2687 TYPE limits the sort of link we want to search on
2690 BASE is the id or URI of the BASE of the link
2694 sub LimitLinkedFrom {
2703 # translate RT2 From/To naming to RT3 TicketSQL naming
2704 my %fromToMap = qw(DependsOn DependentOn
2706 RefersTo ReferredToBy);
2708 my $type = $args{'TYPE'};
2709 $type = $fromToMap{$type} if exists( $fromToMap{$type} );
2712 FIELD => 'LinkedTo',
2714 BASE => $args{'BASE'},
2716 DESCRIPTION => $self->loc(
2717 "Tickets [_1] [_2]",
2718 $self->loc( $args{'TYPE'} ),
2721 OPERATOR => $args{'OPERATOR'},
2730 my $ticket_id = shift;
2731 return $self->LimitLinkedTo(
2733 TARGET => $ticket_id,
2740 # {{{ LimitHasMember
2741 sub LimitHasMember {
2743 my $ticket_id = shift;
2744 return $self->LimitLinkedFrom(
2746 BASE => "$ticket_id",
2747 TYPE => 'HasMember',
2754 # {{{ LimitDependsOn
2756 sub LimitDependsOn {
2758 my $ticket_id = shift;
2759 return $self->LimitLinkedTo(
2761 TARGET => $ticket_id,
2762 TYPE => 'DependsOn',
2769 # {{{ LimitDependedOnBy
2771 sub LimitDependedOnBy {
2773 my $ticket_id = shift;
2774 return $self->LimitLinkedFrom(
2777 TYPE => 'DependentOn',
2788 my $ticket_id = shift;
2789 return $self->LimitLinkedTo(
2791 TARGET => $ticket_id,
2799 # {{{ LimitReferredToBy
2801 sub LimitReferredToBy {
2803 my $ticket_id = shift;
2804 return $self->LimitLinkedFrom(
2807 TYPE => 'ReferredToBy',
2815 # {{{ limit based on ticket date attribtes
2819 =head2 LimitDate (FIELD => 'DateField', OPERATOR => $oper, VALUE => $ISODate)
2821 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
2823 OPERATOR is one of > or <
2824 VALUE is a date and time in ISO format in GMT
2825 FIELD is one of Starts, Started, Told, Created, Resolved, LastUpdated
2827 There are also helper functions of the form LimitFIELD that eliminate
2828 the need to pass in a FIELD argument.
2842 #Set the description if we didn't get handed it above
2843 unless ( $args{'DESCRIPTION'} ) {
2844 $args{'DESCRIPTION'} = $args{'FIELD'} . " "
2845 . $args{'OPERATOR'} . " "
2846 . $args{'VALUE'} . " GMT";
2849 $self->Limit(%args);
2857 $self->LimitDate( FIELD => 'Created', @_ );
2862 $self->LimitDate( FIELD => 'Due', @_ );
2868 $self->LimitDate( FIELD => 'Starts', @_ );
2874 $self->LimitDate( FIELD => 'Started', @_ );
2879 $self->LimitDate( FIELD => 'Resolved', @_ );
2884 $self->LimitDate( FIELD => 'Told', @_ );
2887 sub LimitLastUpdated {
2889 $self->LimitDate( FIELD => 'LastUpdated', @_ );
2893 # {{{ sub LimitTransactionDate
2895 =head2 LimitTransactionDate (OPERATOR => $oper, VALUE => $ISODate)
2897 Takes a paramhash with the fields FIELD OPERATOR and VALUE.
2899 OPERATOR is one of > or <
2900 VALUE is a date and time in ISO format in GMT
2905 sub LimitTransactionDate {
2908 FIELD => 'TransactionDate',
2915 # <20021217042756.GK28744@pallas.fsck.com>
2916 # "Kill It" - Jesse.
2918 #Set the description if we didn't get handed it above
2919 unless ( $args{'DESCRIPTION'} ) {
2920 $args{'DESCRIPTION'} = $args{'FIELD'} . " "
2921 . $args{'OPERATOR'} . " "
2922 . $args{'VALUE'} . " GMT";
2925 $self->Limit(%args);
2933 # {{{ Limit based on custom fields
2934 # {{{ sub LimitCustomField
2936 =head2 LimitCustomField
2938 Takes a paramhash of key/value pairs with the following keys:
2942 =item CUSTOMFIELD - CustomField name or id. If a name is passed, an additional parameter QUEUE may also be passed to distinguish the custom field.
2944 =item OPERATOR - The usual Limit operators
2946 =item VALUE - The value to compare against
2952 sub LimitCustomField {
2956 CUSTOMFIELD => undef,
2958 DESCRIPTION => undef,
2959 FIELD => 'CustomFieldValue',
2964 my $CF = RT::CustomField->new( $self->CurrentUser );
2965 if ( $args{CUSTOMFIELD} =~ /^\d+$/ ) {
2966 $CF->Load( $args{CUSTOMFIELD} );
2969 $CF->LoadByNameAndQueue(
2970 Name => $args{CUSTOMFIELD},
2971 Queue => $args{QUEUE}
2973 $args{CUSTOMFIELD} = $CF->Id;
2976 #If we are looking to compare with a null value.
2977 if ( $args{'OPERATOR'} =~ /^is$/i ) {
2978 $args{'DESCRIPTION'}
2979 ||= $self->loc( "Custom field [_1] has no value.", $CF->Name );
2981 elsif ( $args{'OPERATOR'} =~ /^is not$/i ) {
2982 $args{'DESCRIPTION'}
2983 ||= $self->loc( "Custom field [_1] has a value.", $CF->Name );
2986 # if we're not looking to compare with a null value
2988 $args{'DESCRIPTION'} ||= $self->loc( "Custom field [_1] [_2] [_3]",
2989 $CF->Name, $args{OPERATOR}, $args{VALUE} );
2992 if ( defined $args{'QUEUE'} && $args{'QUEUE'} =~ /\D/ ) {
2993 my $QueueObj = RT::Queue->new( $self->CurrentUser );
2994 $QueueObj->Load( $args{'QUEUE'} );
2995 $args{'QUEUE'} = $QueueObj->Id;
2997 delete $args{'QUEUE'} unless defined $args{'QUEUE'} && length $args{'QUEUE'};
3000 @rest = ( ENTRYAGGREGATOR => 'AND' )
3001 if ( $CF->Type eq 'SelectMultiple' );
3004 VALUE => $args{VALUE},
3006 .(defined $args{'QUEUE'}? ".{$args{'QUEUE'}}" : '' )
3007 .".{" . $CF->Name . "}",
3008 OPERATOR => $args{OPERATOR},
3013 $self->{'RecalcTicketLimits'} = 1;
3019 # {{{ sub _NextIndex
3023 Keep track of the counter for the array of restrictions
3029 return ( $self->{'restriction_index'}++ );
3036 # {{{ Core bits to make this a DBIx::SearchBuilder object
3041 $self->{'table'} = "Tickets";
3042 $self->{'RecalcTicketLimits'} = 1;
3043 $self->{'looking_at_effective_id'} = 0;
3044 $self->{'looking_at_type'} = 0;
3045 $self->{'restriction_index'} = 1;
3046 $self->{'primary_key'} = "id";
3047 delete $self->{'items_array'};
3048 delete $self->{'item_map'};
3049 delete $self->{'columns_to_display'};
3050 $self->SUPER::_Init(@_);
3061 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
3062 return ( $self->SUPER::Count() );
3070 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
3071 return ( $self->SUPER::CountAll() );
3076 # {{{ sub ItemsArrayRef
3078 =head2 ItemsArrayRef
3080 Returns a reference to the set of all items found in this search
3087 return $self->{'items_array'} if $self->{'items_array'};
3089 my $placeholder = $self->_ItemsCounter;
3090 $self->GotoFirstItem();
3091 while ( my $item = $self->Next ) {
3092 push( @{ $self->{'items_array'} }, $item );
3094 $self->GotoItem($placeholder);
3095 $self->{'items_array'}
3096 = $self->ItemsOrderBy( $self->{'items_array'} );
3098 return $self->{'items_array'};
3101 sub ItemsArrayRefWindow {
3105 my @old = ($self->_ItemsCounter, $self->RowsPerPage, $self->FirstRow+1);
3107 $self->RowsPerPage( $window );
3109 $self->GotoFirstItem;
3112 while ( my $item = $self->Next ) {
3116 $self->RowsPerPage( $old[1] );
3117 $self->FirstRow( $old[2] );
3118 $self->GotoItem( $old[0] );
3129 $self->_ProcessRestrictions() if ( $self->{'RecalcTicketLimits'} == 1 );
3131 my $Ticket = $self->SUPER::Next;
3132 return $Ticket unless $Ticket;
3134 if ( $Ticket->__Value('Status') eq 'deleted'
3135 && !$self->{'allow_deleted_search'} )
3139 elsif ( RT->Config->Get('UseSQLForACLChecks') ) {
3140 # if we found a ticket with this option enabled then
3141 # all tickets we found are ACLed, cache this fact
3142 my $key = join ";:;", $self->CurrentUser->id, 'ShowTicket', 'RT::Ticket-'. $Ticket->id;
3143 $RT::Principal::_ACL_CACHE->set( $key => 1 );
3146 elsif ( $Ticket->CurrentUserHasRight('ShowTicket') ) {
3151 # If the user doesn't have the right to show this ticket
3158 $self->CurrentUserCanSee if RT->Config->Get('UseSQLForACLChecks');
3159 return $self->SUPER::_DoSearch( @_ );
3164 $self->CurrentUserCanSee if RT->Config->Get('UseSQLForACLChecks');
3165 return $self->SUPER::_DoCount( @_ );
3171 my $cache_key = 'RolesHasRight;:;ShowTicket';
3173 if ( my $cached = $RT::Principal::_ACL_CACHE->fetch( $cache_key ) ) {
3177 my $ACL = RT::ACL->new( $RT::SystemUser );
3178 $ACL->Limit( FIELD => 'RightName', VALUE => 'ShowTicket' );
3179 $ACL->Limit( FIELD => 'PrincipalType', OPERATOR => '!=', VALUE => 'Group' );
3180 my $principal_alias = $ACL->Join(
3182 FIELD1 => 'PrincipalId',
3183 TABLE2 => 'Principals',
3186 $ACL->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3189 while ( my $ACE = $ACL->Next ) {
3190 my $role = $ACE->PrincipalType;
3191 my $type = $ACE->ObjectType;
3192 if ( $type eq 'RT::System' ) {
3195 elsif ( $type eq 'RT::Queue' ) {
3196 next if $res{ $role } && !ref $res{ $role };
3197 push @{ $res{ $role } ||= [] }, $ACE->ObjectId;
3200 $RT::Logger->error('ShowTicket right is granted on unsupported object');
3203 $RT::Principal::_ACL_CACHE->set( $cache_key => \%res );
3207 sub _DirectlyCanSeeIn {
3209 my $id = $self->CurrentUser->id;
3211 my $cache_key = 'User-'. $id .';:;ShowTicket;:;DirectlyCanSeeIn';
3212 if ( my $cached = $RT::Principal::_ACL_CACHE->fetch( $cache_key ) ) {
3216 my $ACL = RT::ACL->new( $RT::SystemUser );
3217 $ACL->Limit( FIELD => 'RightName', VALUE => 'ShowTicket' );
3218 my $principal_alias = $ACL->Join(
3220 FIELD1 => 'PrincipalId',
3221 TABLE2 => 'Principals',
3224 $ACL->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3225 my $cgm_alias = $ACL->Join(
3227 FIELD1 => 'PrincipalId',
3228 TABLE2 => 'CachedGroupMembers',
3229 FIELD2 => 'GroupId',
3231 $ACL->Limit( ALIAS => $cgm_alias, FIELD => 'MemberId', VALUE => $id );
3232 $ACL->Limit( ALIAS => $cgm_alias, FIELD => 'Disabled', VALUE => 0 );
3235 while ( my $ACE = $ACL->Next ) {
3236 my $type = $ACE->ObjectType;
3237 if ( $type eq 'RT::System' ) {
3238 # If user is direct member of a group that has the right
3239 # on the system then he can see any ticket
3240 $RT::Principal::_ACL_CACHE->set( $cache_key => [-1] );
3243 elsif ( $type eq 'RT::Queue' ) {
3244 push @res, $ACE->ObjectId;
3247 $RT::Logger->error('ShowTicket right is granted on unsupported object');
3250 $RT::Principal::_ACL_CACHE->set( $cache_key => \@res );
3254 sub CurrentUserCanSee {
3256 return if $self->{'_sql_current_user_can_see_applied'};
3258 return $self->{'_sql_current_user_can_see_applied'} = 1
3259 if $self->CurrentUser->UserObj->HasRight(
3260 Right => 'SuperUser', Object => $RT::System
3263 my $id = $self->CurrentUser->id;
3265 # directly can see in all queues then we have nothing to do
3266 my @direct_queues = $self->_DirectlyCanSeeIn;
3267 return $self->{'_sql_current_user_can_see_applied'} = 1
3268 if @direct_queues && $direct_queues[0] == -1;
3270 my %roles = $self->_RolesCanSee;
3272 my %skip = map { $_ => 1 } @direct_queues;
3273 foreach my $role ( keys %roles ) {
3274 next unless ref $roles{ $role };
3276 my @queues = grep !$skip{$_}, @{ $roles{ $role } };
3278 $roles{ $role } = \@queues;
3280 delete $roles{ $role };
3285 # there is no global watchers, only queues and tickes, if at
3286 # some point we will add global roles then it's gonna blow
3287 # the idea here is that if the right is set globaly for a role
3288 # and user plays this role for a queue directly not a ticket
3289 # then we have to check in advance
3290 if ( my @tmp = grep $_ ne 'Owner' && !ref $roles{ $_ }, keys %roles ) {
3292 my $groups = RT::Groups->new( $RT::SystemUser );
3293 $groups->Limit( FIELD => 'Domain', VALUE => 'RT::Queue-Role' );
3295 $groups->Limit( FIELD => 'Type', VALUE => $_ );
3297 my $principal_alias = $groups->Join(
3300 TABLE2 => 'Principals',
3303 $groups->Limit( ALIAS => $principal_alias, FIELD => 'Disabled', VALUE => 0 );
3304 my $cgm_alias = $groups->Join(
3307 TABLE2 => 'CachedGroupMembers',
3308 FIELD2 => 'GroupId',
3310 $groups->Limit( ALIAS => $cgm_alias, FIELD => 'MemberId', VALUE => $id );
3311 $groups->Limit( ALIAS => $cgm_alias, FIELD => 'Disabled', VALUE => 0 );
3312 while ( my $group = $groups->Next ) {
3313 push @direct_queues, $group->Instance;
3317 unless ( @direct_queues || keys %roles ) {
3318 $self->SUPER::Limit(
3323 ENTRYAGGREGATOR => 'AND',
3325 return $self->{'_sql_current_user_can_see_applied'} = 1;
3329 my $join_roles = keys %roles;
3330 $join_roles = 0 if $join_roles == 1 && $roles{'Owner'};
3331 my ($role_group_alias, $cgm_alias);
3332 if ( $join_roles ) {
3333 $role_group_alias = $self->_RoleGroupsJoin( New => 1 );
3334 $cgm_alias = $self->_GroupMembersJoin( GroupsAlias => $role_group_alias );
3335 $self->SUPER::Limit(
3336 LEFTJOIN => $cgm_alias,
3337 FIELD => 'MemberId',
3342 my $limit_queues = sub {
3346 return unless @queues;
3347 if ( @queues == 1 ) {
3348 $self->SUPER::Limit(
3353 ENTRYAGGREGATOR => $ea,
3356 $self->SUPER::_OpenParen('ACL');
3357 foreach my $q ( @queues ) {
3358 $self->SUPER::Limit(
3363 ENTRYAGGREGATOR => $ea,
3367 $self->SUPER::_CloseParen('ACL');
3372 $self->SUPER::_OpenParen('ACL');
3374 $ea = 'OR' if $limit_queues->( $ea, @direct_queues );
3375 while ( my ($role, $queues) = each %roles ) {
3376 $self->SUPER::_OpenParen('ACL');
3377 if ( $role eq 'Owner' ) {
3378 $self->SUPER::Limit(
3382 ENTRYAGGREGATOR => $ea,
3386 $self->SUPER::Limit(
3388 ALIAS => $cgm_alias,
3389 FIELD => 'MemberId',
3390 OPERATOR => 'IS NOT',
3393 ENTRYAGGREGATOR => $ea,
3395 $self->SUPER::Limit(
3397 ALIAS => $role_group_alias,
3400 ENTRYAGGREGATOR => 'AND',
3403 $limit_queues->( 'AND', @$queues ) if ref $queues;
3404 $ea = 'OR' if $ea eq 'AND';
3405 $self->SUPER::_CloseParen('ACL');
3407 $self->SUPER::_CloseParen('ACL');
3409 return $self->{'_sql_current_user_can_see_applied'} = 1;
3416 # {{{ Deal with storing and restoring restrictions
3418 # {{{ sub LoadRestrictions
3420 =head2 LoadRestrictions
3422 LoadRestrictions takes a string which can fully populate the TicketRestrictons hash.
3423 TODO It is not yet implemented
3429 # {{{ sub DescribeRestrictions
3431 =head2 DescribeRestrictions
3434 Returns a hash keyed by restriction id.
3435 Each element of the hash is currently a one element hash that contains DESCRIPTION which
3436 is a description of the purpose of that TicketRestriction
3440 sub DescribeRestrictions {
3445 foreach my $row ( keys %{ $self->{'TicketRestrictions'} } ) {
3446 $listing{$row} = $self->{'TicketRestrictions'}{$row}{'DESCRIPTION'};
3453 # {{{ sub RestrictionValues
3455 =head2 RestrictionValues FIELD
3457 Takes a restriction field and returns a list of values this field is restricted
3462 sub RestrictionValues {
3465 map $self->{'TicketRestrictions'}{$_}{'VALUE'}, grep {
3466 $self->{'TicketRestrictions'}{$_}{'FIELD'} eq $field
3467 && $self->{'TicketRestrictions'}{$_}{'OPERATOR'} eq "="
3469 keys %{ $self->{'TicketRestrictions'} };
3474 # {{{ sub ClearRestrictions
3476 =head2 ClearRestrictions
3478 Removes all restrictions irretrievably
3482 sub ClearRestrictions {
3484 delete $self->{'TicketRestrictions'};
3485 $self->{'looking_at_effective_id'} = 0;
3486 $self->{'looking_at_type'} = 0;
3487 $self->{'RecalcTicketLimits'} = 1;
3492 # {{{ sub DeleteRestriction
3494 =head2 DeleteRestriction
3496 Takes the row Id of a restriction (From DescribeRestrictions' output, for example.
3497 Removes that restriction from the session's limits.
3501 sub DeleteRestriction {
3504 delete $self->{'TicketRestrictions'}{$row};
3506 $self->{'RecalcTicketLimits'} = 1;
3508 #make the underlying easysearch object forget all its preconceptions
3513 # {{{ sub _RestrictionsToClauses
3515 # Convert a set of oldstyle SB Restrictions to Clauses for RQL
3517 sub _RestrictionsToClauses {
3521 foreach my $row ( keys %{ $self->{'TicketRestrictions'} } ) {
3522 my $restriction = $self->{'TicketRestrictions'}{$row};
3524 # We need to reimplement the subclause aggregation that SearchBuilder does.
3525 # Default Subclause is ALIAS.FIELD, and default ALIAS is 'main',
3526 # Then SB AND's the different Subclauses together.
3528 # So, we want to group things into Subclauses, convert them to
3529 # SQL, and then join them with the appropriate DefaultEA.
3530 # Then join each subclause group with AND.
3532 my $field = $restriction->{'FIELD'};
3533 my $realfield = $field; # CustomFields fake up a fieldname, so
3534 # we need to figure that out
3537 # Rewrite LinkedTo meta field to the real field
3538 if ( $field =~ /LinkedTo/ ) {
3539 $realfield = $field = $restriction->{'TYPE'};
3543 # Handle subkey fields with a different real field
3544 if ( $field =~ /^(\w+)\./ ) {
3548 die "I don't know about $field yet"
3549 unless ( exists $FIELD_METADATA{$realfield}
3550 or $restriction->{CUSTOMFIELD} );
3552 my $type = $FIELD_METADATA{$realfield}->[0];
3553 my $op = $restriction->{'OPERATOR'};
3557 map { $restriction->{$_} } qw(VALUE TICKET BASE TARGET)
3560 # this performs the moral equivalent of defined or/dor/C<//>,
3561 # without the short circuiting.You need to use a 'defined or'
3562 # type thing instead of just checking for truth values, because
3563 # VALUE could be 0.(i.e. "false")
3565 # You could also use this, but I find it less aesthetic:
3566 # (although it does short circuit)
3567 #( defined $restriction->{'VALUE'}? $restriction->{VALUE} :
3568 # defined $restriction->{'TICKET'} ?
3569 # $restriction->{TICKET} :
3570 # defined $restriction->{'BASE'} ?
3571 # $restriction->{BASE} :
3572 # defined $restriction->{'TARGET'} ?
3573 # $restriction->{TARGET} )
3575 my $ea = $restriction->{ENTRYAGGREGATOR}
3576 || $DefaultEA{$type}
3579 die "Invalid operator $op for $field ($type)"
3580 unless exists $ea->{$op};
3584 # Each CustomField should be put into a different Clause so they
3585 # are ANDed together.
3586 if ( $restriction->{CUSTOMFIELD} ) {
3587 $realfield = $field;
3590 exists $clause{$realfield} or $clause{$realfield} = [];
3593 $field =~ s!(['"])!\\$1!g;
3594 $value =~ s!(['"])!\\$1!g;
3595 my $data = [ $ea, $type, $field, $op, $value ];
3597 # here is where we store extra data, say if it's a keyword or
3598 # something. (I.e. "TYPE SPECIFIC STUFF")
3600 push @{ $clause{$realfield} }, $data;
3607 # {{{ sub _ProcessRestrictions
3609 =head2 _ProcessRestrictions PARAMHASH
3611 # The new _ProcessRestrictions is somewhat dependent on the SQL stuff,
3612 # but isn't quite generic enough to move into Tickets_Overlay_SQL.
3616 sub _ProcessRestrictions {
3619 #Blow away ticket aliases since we'll need to regenerate them for
3621 delete $self->{'TicketAliases'};
3622 delete $self->{'items_array'};
3623 delete $self->{'item_map'};
3624 delete $self->{'raw_rows'};
3625 delete $self->{'rows'};
3626 delete $self->{'count_all'};
3628 my $sql = $self->Query; # Violating the _SQL namespace
3629 if ( !$sql || $self->{'RecalcTicketLimits'} ) {
3631 # "Restrictions to Clauses Branch\n";
3632 my $clauseRef = eval { $self->_RestrictionsToClauses; };
3634 $RT::Logger->error( "RestrictionsToClauses: " . $@ );
3638 $sql = $self->ClausesToSQL($clauseRef);
3639 $self->FromSQL($sql) if $sql;
3643 $self->{'RecalcTicketLimits'} = 0;
3647 =head2 _BuildItemMap
3649 Build up a L</ItemMap> of first/last/next/prev items, so that we can
3650 display search nav quickly.
3657 my $window = RT->Config->Get('TicketsItemMapSize');
3659 $self->{'item_map'} = {};
3661 my $items = $self->ItemsArrayRefWindow( $window );
3662 return unless $items && @$items;
3665 $self->{'item_map'}{'first'} = $items->[0]->EffectiveId;
3666 for ( my $i = 0; $i < @$items; $i++ ) {
3667 my $item = $items->[$i];
3668 my $id = $item->EffectiveId;
3669 $self->{'item_map'}{$id}{'defined'} = 1;
3670 $self->{'item_map'}{$id}{'prev'} = $prev;
3671 $self->{'item_map'}{$id}{'next'} = $items->[$i+1]->EffectiveId
3675 $self->{'item_map'}{'last'} = $prev
3676 if !$window || @$items < $window;
3681 Returns an a map of all items found by this search. The map is a hash
3685 first => <first ticket id found>,
3686 last => <last ticket id found or undef>,
3689 prev => <the ticket id found before>,
3690 next => <the ticket id found after>,
3702 $self->_BuildItemMap unless $self->{'item_map'};
3703 return $self->{'item_map'};
3711 =head2 PrepForSerialization
3713 You don't want to serialize a big tickets object, as
3714 the {items} hash will be instantly invalid _and_ eat
3719 sub PrepForSerialization {
3721 delete $self->{'items'};
3722 delete $self->{'items_array'};
3723 $self->RedoSearch();
3728 RT::Tickets supports several flags which alter search behavior:
3731 allow_deleted_search (Otherwise never show deleted tickets in search results)
3732 looking_at_type (otherwise limit to type=ticket)
3734 These flags are set by calling
3736 $tickets->{'flagname'} = 1;
3738 BUG: There should be an API for this