rt 4.2.15
[freeside.git] / rt / lib / RT / SearchBuilder / Role / Roles.pm
1 # BEGIN BPS TAGGED BLOCK {{{
2 #
3 # COPYRIGHT:
4 #
5 # This software is Copyright (c) 1996-2018 Best Practical Solutions, LLC
6 #                                          <sales@bestpractical.com>
7 #
8 # (Except where explicitly superseded by other copyright notices)
9 #
10 #
11 # LICENSE:
12 #
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
16 # from www.gnu.org.
17 #
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.
22 #
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.
28 #
29 #
30 # CONTRIBUTION SUBMISSION POLICY:
31 #
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.)
37 #
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.
46 #
47 # END BPS TAGGED BLOCK }}}
48
49 use strict;
50 use warnings;
51
52 package RT::SearchBuilder::Role::Roles;
53 use Role::Basic;
54 use Scalar::Util qw(blessed);
55
56 =head1 NAME
57
58 RT::Record::Role::Roles - Common methods for records which "watchers" or "roles"
59
60 =head1 REQUIRES
61
62 =head2 L<RT::SearchBuilder::Role>
63
64 =cut
65
66 with 'RT::SearchBuilder::Role';
67
68 require RT::System;
69 require RT::Principal;
70 require RT::Group;
71 require RT::User;
72
73 require RT::EmailParser;
74
75 =head1 PROVIDES
76
77 =head2 _RoleGroupClass
78
79 Returns the class name on which role searches should be based.  This relates to
80 the internal L<RT::Group/Domain> and distinguishes between roles on the objects
81 being searched and their counterpart roles on containing classes.  For example,
82 limiting on L<RT::Queue> roles while searching for L<RT::Ticket>s.
83
84 The default implementation is:
85
86     $self->RecordClass
87
88 which is the class that this collection object searches and instatiates objects
89 for.  If you're doing something hinky, you may need to override this method.
90
91 =cut
92
93 sub _RoleGroupClass {
94     my $self = shift;
95     return $self->RecordClass;
96 }
97
98 sub _RoleGroupsJoin {
99     my $self = shift;
100     my %args = (New => 0, Class => '', Name => '', @_);
101
102     $args{'Class'} ||= $self->_RoleGroupClass;
103
104     my $name = $args{'Name'};
105     if ( exists $args{'Type'} ) {
106         RT->Deprecated( Arguments => 'Type', Instead => 'Name', Remove => '4.4' );
107         $name = $args{'Type'};
108     }
109
110     return $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $name }
111         if $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $name }
112            && !$args{'New'};
113
114     # If we're looking at a role group on a class that "contains" this record
115     # (i.e. roles on queues for tickets), then we assume that the current
116     # record has a column named after the containing class (i.e.
117     # Tickets.Queue).
118     my $instance = $self->_RoleGroupClass eq $args{Class} ? "id" : $args{Class};
119        $instance =~ s/^RT:://;
120
121     # Watcher groups are always created for each record, so we use INNER join.
122     my $groups = $self->Join(
123         ALIAS1          => 'main',
124         FIELD1          => $instance,
125         TABLE2          => 'Groups',
126         FIELD2          => 'Instance',
127         ENTRYAGGREGATOR => 'AND',
128         DISTINCT        => !!$args{'Type'},
129     );
130     $self->Limit(
131         LEFTJOIN        => $groups,
132         ALIAS           => $groups,
133         FIELD           => 'Domain',
134         VALUE           => $args{'Class'} .'-Role',
135         CASESENSITIVE   => 0,
136     );
137     $self->Limit(
138         LEFTJOIN        => $groups,
139         ALIAS           => $groups,
140         FIELD           => 'Name',
141         VALUE           => $name,
142         CASESENSITIVE   => 0,
143     ) if $name;
144
145     $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $name } = $groups
146         unless $args{'New'};
147
148     return $groups;
149 }
150
151 sub _GroupMembersJoin {
152     my $self = shift;
153     my %args = (New => 1, GroupsAlias => undef, Left => 1, @_);
154
155     return $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} }
156         if $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} }
157             && !$args{'New'};
158
159     my $alias = $self->Join(
160         $args{'Left'} ? (TYPE            => 'LEFT') : (),
161         ALIAS1          => $args{'GroupsAlias'},
162         FIELD1          => 'id',
163         TABLE2          => 'CachedGroupMembers',
164         FIELD2          => 'GroupId',
165         ENTRYAGGREGATOR => 'AND',
166     );
167     $self->Limit(
168         LEFTJOIN => $alias,
169         ALIAS => $alias,
170         FIELD => 'Disabled',
171         VALUE => 0,
172     );
173
174     $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} } = $alias
175         unless $args{'New'};
176
177     return $alias;
178 }
179
180 =head2 _WatcherJoin
181
182 Helper function which provides joins to a watchers table both for limits
183 and for ordering.
184
185 =cut
186
187 sub _WatcherJoin {
188     my $self = shift;
189
190     my $groups = $self->_RoleGroupsJoin(@_);
191     my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
192     # XXX: work around, we must hide groups that
193     # are members of the role group we search in,
194     # otherwise them result in wrong NULLs in Users
195     # table and break ordering. Now, we know that
196     # RT doesn't allow to add groups as members of the
197     # ticket roles, so we just hide entries in CGM table
198     # with MemberId == GroupId from results
199     $self->Limit(
200         LEFTJOIN   => $group_members,
201         FIELD      => 'GroupId',
202         OPERATOR   => '!=',
203         VALUE      => "$group_members.MemberId",
204         QUOTEVALUE => 0,
205     );
206     my $users = $self->Join(
207         TYPE            => 'LEFT',
208         ALIAS1          => $group_members,
209         FIELD1          => 'MemberId',
210         TABLE2          => 'Users',
211         FIELD2          => 'id',
212     );
213     return ($groups, $group_members, $users);
214 }
215
216
217 sub RoleLimit {
218     my $self = shift;
219     my %args = (
220         TYPE => '',
221         CLASS => '',
222         FIELD => undef,
223         OPERATOR => '=',
224         VALUE => undef,
225         @_
226     );
227
228     my $class = $args{CLASS} || $self->_RoleGroupClass;
229
230     $args{FIELD} ||= 'id' if $args{VALUE} =~ /^\d+$/;
231
232     my $type = delete $args{TYPE};
233     if ($type and not $class->HasRole($type)) {
234         RT->Logger->warn("RoleLimit called with invalid role $type for $class");
235         return;
236     }
237
238     my $column = $type ? $class->Role($type)->{Column} : undef;
239
240     # if it's equality op and search by Email or Name then we can preload user
241     # we do it to help some DBs better estimate number of rows and get better plans
242     if ( $args{OPERATOR} =~ /^!?=$/
243              && (!$args{FIELD} || $args{FIELD} eq 'Name' || $args{FIELD} eq 'EmailAddress') ) {
244         my $o = RT::User->new( $self->CurrentUser );
245         my $method =
246             !$args{FIELD}
247             ? ($column ? 'Load' : 'LoadByEmail')
248             : $args{FIELD} eq 'EmailAddress' ? 'LoadByEmail': 'Load';
249         $o->$method( $args{VALUE} );
250         $args{FIELD} = 'id';
251         $args{VALUE} = $o->id || 0;
252     }
253
254     if ( $column and $args{FIELD} and $args{FIELD} eq 'id' ) {
255         $self->Limit(
256             %args,
257             FIELD => $column,
258         );
259         return;
260     }
261
262     $args{FIELD} ||= 'EmailAddress';
263
264     my ($groups, $group_members, $users);
265     if ( $args{'BUNDLE'} ) {
266         ($groups, $group_members, $users) = @{ $args{'BUNDLE'} };
267     } else {
268         $groups = $self->_RoleGroupsJoin( Name => $type, Class => $class, New => !$type );
269     }
270
271     $self->_OpenParen( $args{SUBCLAUSE} ) if $args{SUBCLAUSE};
272     if ( $args{OPERATOR} =~ /^IS(?: NOT)?$/i ) {
273         # is [not] empty case
274
275         $group_members ||= $self->_GroupMembersJoin( GroupsAlias => $groups );
276         # to avoid joining the table Users into the query, we just join GM
277         # and make sure we don't match records where group is member of itself
278         $self->Limit(
279             LEFTJOIN   => $group_members,
280             FIELD      => 'GroupId',
281             OPERATOR   => '!=',
282             VALUE      => "$group_members.MemberId",
283             QUOTEVALUE => 0,
284         );
285         $self->Limit(
286             %args,
287             ALIAS         => $group_members,
288             FIELD         => 'GroupId',
289             OPERATOR      => $args{OPERATOR},
290             VALUE         => $args{VALUE},
291         );
292     }
293     elsif ( $args{OPERATOR} =~ /^!=$|^NOT\s+/i ) {
294         # negative condition case
295
296         # reverse op
297         $args{OPERATOR} =~ s/!|NOT\s+//i;
298
299         # XXX: we have no way to build correct "Watcher.X != 'Y'" when condition
300         # "X = 'Y'" matches more then one user so we try to fetch two records and
301         # do the right thing when there is only one exist and semi-working solution
302         # otherwise.
303         my $users_obj = RT::Users->new( $self->CurrentUser );
304         $users_obj->Limit(
305             FIELD         => $args{FIELD},
306             OPERATOR      => $args{OPERATOR},
307             VALUE         => $args{VALUE},
308         );
309         $users_obj->OrderBy;
310         $users_obj->RowsPerPage(2);
311         my @users = @{ $users_obj->ItemsArrayRef };
312
313         $group_members ||= $self->_GroupMembersJoin( GroupsAlias => $groups );
314         if ( @users <= 1 ) {
315             my $uid = 0;
316             $uid = $users[0]->id if @users;
317             $self->Limit(
318                 LEFTJOIN      => $group_members,
319                 ALIAS         => $group_members,
320                 FIELD         => 'MemberId',
321                 VALUE         => $uid,
322             );
323             $self->Limit(
324                 %args,
325                 ALIAS           => $group_members,
326                 FIELD           => 'id',
327                 OPERATOR        => 'IS',
328                 VALUE           => 'NULL',
329             );
330         } else {
331             $self->Limit(
332                 LEFTJOIN   => $group_members,
333                 FIELD      => 'GroupId',
334                 OPERATOR   => '!=',
335                 VALUE      => "$group_members.MemberId",
336                 QUOTEVALUE => 0,
337             );
338             $users ||= $self->Join(
339                 TYPE            => 'LEFT',
340                 ALIAS1          => $group_members,
341                 FIELD1          => 'MemberId',
342                 TABLE2          => 'Users',
343                 FIELD2          => 'id',
344             );
345             $self->Limit(
346                 LEFTJOIN      => $users,
347                 ALIAS         => $users,
348                 FIELD         => $args{FIELD},
349                 OPERATOR      => $args{OPERATOR},
350                 VALUE         => $args{VALUE},
351                 CASESENSITIVE => 0,
352             );
353             $self->Limit(
354                 %args,
355                 ALIAS         => $users,
356                 FIELD         => 'id',
357                 OPERATOR      => 'IS',
358                 VALUE         => 'NULL',
359             );
360         }
361     } else {
362         # positive condition case
363
364         $group_members ||= $self->_GroupMembersJoin(
365             GroupsAlias => $groups, New => 1, Left => 0
366         );
367         if ($args{FIELD} eq "id") {
368             # Save a left join to Users, if possible
369             $self->Limit(
370                 %args,
371                 ALIAS           => $group_members,
372                 FIELD           => "MemberId",
373                 OPERATOR        => $args{OPERATOR},
374                 VALUE           => $args{VALUE},
375                 CASESENSITIVE   => 0,
376             );
377         } else {
378             $users ||= $self->Join(
379                 TYPE            => 'LEFT',
380                 ALIAS1          => $group_members,
381                 FIELD1          => 'MemberId',
382                 TABLE2          => 'Users',
383                 FIELD2          => 'id',
384             );
385             $self->Limit(
386                 %args,
387                 ALIAS           => $users,
388                 FIELD           => $args{FIELD},
389                 OPERATOR        => $args{OPERATOR},
390                 VALUE           => $args{VALUE},
391                 CASESENSITIVE   => 0,
392             );
393         }
394     }
395     $self->_CloseParen( $args{SUBCLAUSE} ) if $args{SUBCLAUSE};
396     return ($groups, $group_members, $users);
397 }
398
399 1;