summaryrefslogtreecommitdiff
path: root/rt/lib/RT/SearchBuilder/Role/Roles.pm
diff options
context:
space:
mode:
Diffstat (limited to 'rt/lib/RT/SearchBuilder/Role/Roles.pm')
-rw-r--r--rt/lib/RT/SearchBuilder/Role/Roles.pm399
1 files changed, 399 insertions, 0 deletions
diff --git a/rt/lib/RT/SearchBuilder/Role/Roles.pm b/rt/lib/RT/SearchBuilder/Role/Roles.pm
new file mode 100644
index 000000000..914c74bc9
--- /dev/null
+++ b/rt/lib/RT/SearchBuilder/Role/Roles.pm
@@ -0,0 +1,399 @@
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC
+# <sales@bestpractical.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
+#
+# This work is made available to you under the terms of Version 2 of
+# the GNU General Public License. A copy of that license should have
+# been provided with this software, but in any event can be snarfed
+# from www.gnu.org.
+#
+# This work is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
+#
+# CONTRIBUTION SUBMISSION POLICY:
+#
+# (The following paragraph is not intended to limit the rights granted
+# to you to modify and distribute this software under the terms of
+# the GNU General Public License and is only of importance to you if
+# you choose to contribute your changes and enhancements to the
+# community by submitting them to Best Practical Solutions, LLC.)
+#
+# By intentionally submitting any modifications, corrections or
+# derivatives to this work, or any other work intended for use with
+# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+# you are the copyright holder for those contributions and you grant
+# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+# royalty-free, perpetual, license to use, copy, create derivative
+# works based on those contributions, and sublicense and distribute
+# those contributions and any derivatives thereof.
+#
+# END BPS TAGGED BLOCK }}}
+
+use strict;
+use warnings;
+
+package RT::SearchBuilder::Role::Roles;
+use Role::Basic;
+use Scalar::Util qw(blessed);
+
+=head1 NAME
+
+RT::Record::Role::Roles - Common methods for records which "watchers" or "roles"
+
+=head1 REQUIRES
+
+=head2 L<RT::SearchBuilder::Role>
+
+=cut
+
+with 'RT::SearchBuilder::Role';
+
+require RT::System;
+require RT::Principal;
+require RT::Group;
+require RT::User;
+
+require RT::EmailParser;
+
+=head1 PROVIDES
+
+=head2 _RoleGroupClass
+
+Returns the class name on which role searches should be based. This relates to
+the internal L<RT::Group/Domain> and distinguishes between roles on the objects
+being searched and their counterpart roles on containing classes. For example,
+limiting on L<RT::Queue> roles while searching for L<RT::Ticket>s.
+
+The default implementation is:
+
+ $self->RecordClass
+
+which is the class that this collection object searches and instatiates objects
+for. If you're doing something hinky, you may need to override this method.
+
+=cut
+
+sub _RoleGroupClass {
+ my $self = shift;
+ return $self->RecordClass;
+}
+
+sub _RoleGroupsJoin {
+ my $self = shift;
+ my %args = (New => 0, Class => '', Name => '', @_);
+
+ $args{'Class'} ||= $self->_RoleGroupClass;
+
+ my $name = $args{'Name'};
+ if ( exists $args{'Type'} ) {
+ RT->Deprecated( Arguments => 'Type', Instead => 'Name', Remove => '4.4' );
+ $name = $args{'Type'};
+ }
+
+ return $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $name }
+ if $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $name }
+ && !$args{'New'};
+
+ # If we're looking at a role group on a class that "contains" this record
+ # (i.e. roles on queues for tickets), then we assume that the current
+ # record has a column named after the containing class (i.e.
+ # Tickets.Queue).
+ my $instance = $self->_RoleGroupClass eq $args{Class} ? "id" : $args{Class};
+ $instance =~ s/^RT:://;
+
+ # Watcher groups are always created for each record, so we use INNER join.
+ my $groups = $self->Join(
+ ALIAS1 => 'main',
+ FIELD1 => $instance,
+ TABLE2 => 'Groups',
+ FIELD2 => 'Instance',
+ ENTRYAGGREGATOR => 'AND',
+ DISTINCT => !!$args{'Type'},
+ );
+ $self->Limit(
+ LEFTJOIN => $groups,
+ ALIAS => $groups,
+ FIELD => 'Domain',
+ VALUE => $args{'Class'} .'-Role',
+ CASESENSITIVE => 0,
+ );
+ $self->Limit(
+ LEFTJOIN => $groups,
+ ALIAS => $groups,
+ FIELD => 'Name',
+ VALUE => $name,
+ CASESENSITIVE => 0,
+ ) if $name;
+
+ $self->{'_sql_role_group_aliases'}{ $args{'Class'} .'-'. $name } = $groups
+ unless $args{'New'};
+
+ return $groups;
+}
+
+sub _GroupMembersJoin {
+ my $self = shift;
+ my %args = (New => 1, GroupsAlias => undef, Left => 1, @_);
+
+ return $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} }
+ if $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} }
+ && !$args{'New'};
+
+ my $alias = $self->Join(
+ $args{'Left'} ? (TYPE => 'LEFT') : (),
+ ALIAS1 => $args{'GroupsAlias'},
+ FIELD1 => 'id',
+ TABLE2 => 'CachedGroupMembers',
+ FIELD2 => 'GroupId',
+ ENTRYAGGREGATOR => 'AND',
+ );
+ $self->Limit(
+ LEFTJOIN => $alias,
+ ALIAS => $alias,
+ FIELD => 'Disabled',
+ VALUE => 0,
+ );
+
+ $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} } = $alias
+ unless $args{'New'};
+
+ return $alias;
+}
+
+=head2 _WatcherJoin
+
+Helper function which provides joins to a watchers table both for limits
+and for ordering.
+
+=cut
+
+sub _WatcherJoin {
+ my $self = shift;
+
+ my $groups = $self->_RoleGroupsJoin(@_);
+ my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
+ # XXX: work around, we must hide groups that
+ # are members of the role group we search in,
+ # otherwise them result in wrong NULLs in Users
+ # table and break ordering. Now, we know that
+ # RT doesn't allow to add groups as members of the
+ # ticket roles, so we just hide entries in CGM table
+ # with MemberId == GroupId from results
+ $self->Limit(
+ LEFTJOIN => $group_members,
+ FIELD => 'GroupId',
+ OPERATOR => '!=',
+ VALUE => "$group_members.MemberId",
+ QUOTEVALUE => 0,
+ );
+ my $users = $self->Join(
+ TYPE => 'LEFT',
+ ALIAS1 => $group_members,
+ FIELD1 => 'MemberId',
+ TABLE2 => 'Users',
+ FIELD2 => 'id',
+ );
+ return ($groups, $group_members, $users);
+}
+
+
+sub RoleLimit {
+ my $self = shift;
+ my %args = (
+ TYPE => '',
+ CLASS => '',
+ FIELD => undef,
+ OPERATOR => '=',
+ VALUE => undef,
+ @_
+ );
+
+ my $class = $args{CLASS} || $self->_RoleGroupClass;
+
+ $args{FIELD} ||= 'id' if $args{VALUE} =~ /^\d+$/;
+
+ my $type = delete $args{TYPE};
+ if ($type and not $class->HasRole($type)) {
+ RT->Logger->warn("RoleLimit called with invalid role $type for $class");
+ return;
+ }
+
+ my $column = $type ? $class->Role($type)->{Column} : undef;
+
+ # if it's equality op and search by Email or Name then we can preload user
+ # we do it to help some DBs better estimate number of rows and get better plans
+ if ( $args{OPERATOR} =~ /^!?=$/
+ && (!$args{FIELD} || $args{FIELD} eq 'Name' || $args{FIELD} eq 'EmailAddress') ) {
+ my $o = RT::User->new( $self->CurrentUser );
+ my $method =
+ !$args{FIELD}
+ ? ($column ? 'Load' : 'LoadByEmail')
+ : $args{FIELD} eq 'EmailAddress' ? 'LoadByEmail': 'Load';
+ $o->$method( $args{VALUE} );
+ $args{FIELD} = 'id';
+ $args{VALUE} = $o->id || 0;
+ }
+
+ if ( $column and $args{FIELD} and $args{FIELD} eq 'id' ) {
+ $self->Limit(
+ %args,
+ FIELD => $column,
+ );
+ return;
+ }
+
+ $args{FIELD} ||= 'EmailAddress';
+
+ my ($groups, $group_members, $users);
+ if ( $args{'BUNDLE'} ) {
+ ($groups, $group_members, $users) = @{ $args{'BUNDLE'} };
+ } else {
+ $groups = $self->_RoleGroupsJoin( Name => $type, Class => $class, New => !$type );
+ }
+
+ $self->_OpenParen( $args{SUBCLAUSE} ) if $args{SUBCLAUSE};
+ if ( $args{OPERATOR} =~ /^IS(?: NOT)?$/i ) {
+ # is [not] empty case
+
+ $group_members ||= $self->_GroupMembersJoin( GroupsAlias => $groups );
+ # to avoid joining the table Users into the query, we just join GM
+ # and make sure we don't match records where group is member of itself
+ $self->Limit(
+ LEFTJOIN => $group_members,
+ FIELD => 'GroupId',
+ OPERATOR => '!=',
+ VALUE => "$group_members.MemberId",
+ QUOTEVALUE => 0,
+ );
+ $self->Limit(
+ %args,
+ ALIAS => $group_members,
+ FIELD => 'GroupId',
+ OPERATOR => $args{OPERATOR},
+ VALUE => $args{VALUE},
+ );
+ }
+ elsif ( $args{OPERATOR} =~ /^!=$|^NOT\s+/i ) {
+ # negative condition case
+
+ # reverse op
+ $args{OPERATOR} =~ s/!|NOT\s+//i;
+
+ # XXX: we have no way to build correct "Watcher.X != 'Y'" when condition
+ # "X = 'Y'" matches more then one user so we try to fetch two records and
+ # do the right thing when there is only one exist and semi-working solution
+ # otherwise.
+ my $users_obj = RT::Users->new( $self->CurrentUser );
+ $users_obj->Limit(
+ FIELD => $args{FIELD},
+ OPERATOR => $args{OPERATOR},
+ VALUE => $args{VALUE},
+ );
+ $users_obj->OrderBy;
+ $users_obj->RowsPerPage(2);
+ my @users = @{ $users_obj->ItemsArrayRef };
+
+ $group_members ||= $self->_GroupMembersJoin( GroupsAlias => $groups );
+ if ( @users <= 1 ) {
+ my $uid = 0;
+ $uid = $users[0]->id if @users;
+ $self->Limit(
+ LEFTJOIN => $group_members,
+ ALIAS => $group_members,
+ FIELD => 'MemberId',
+ VALUE => $uid,
+ );
+ $self->Limit(
+ %args,
+ ALIAS => $group_members,
+ FIELD => 'id',
+ OPERATOR => 'IS',
+ VALUE => 'NULL',
+ );
+ } else {
+ $self->Limit(
+ LEFTJOIN => $group_members,
+ FIELD => 'GroupId',
+ OPERATOR => '!=',
+ VALUE => "$group_members.MemberId",
+ QUOTEVALUE => 0,
+ );
+ $users ||= $self->Join(
+ TYPE => 'LEFT',
+ ALIAS1 => $group_members,
+ FIELD1 => 'MemberId',
+ TABLE2 => 'Users',
+ FIELD2 => 'id',
+ );
+ $self->Limit(
+ LEFTJOIN => $users,
+ ALIAS => $users,
+ FIELD => $args{FIELD},
+ OPERATOR => $args{OPERATOR},
+ VALUE => $args{VALUE},
+ CASESENSITIVE => 0,
+ );
+ $self->Limit(
+ %args,
+ ALIAS => $users,
+ FIELD => 'id',
+ OPERATOR => 'IS',
+ VALUE => 'NULL',
+ );
+ }
+ } else {
+ # positive condition case
+
+ $group_members ||= $self->_GroupMembersJoin(
+ GroupsAlias => $groups, New => 1, Left => 0
+ );
+ if ($args{FIELD} eq "id") {
+ # Save a left join to Users, if possible
+ $self->Limit(
+ %args,
+ ALIAS => $group_members,
+ FIELD => "MemberId",
+ OPERATOR => $args{OPERATOR},
+ VALUE => $args{VALUE},
+ CASESENSITIVE => 0,
+ );
+ } else {
+ $users ||= $self->Join(
+ TYPE => 'LEFT',
+ ALIAS1 => $group_members,
+ FIELD1 => 'MemberId',
+ TABLE2 => 'Users',
+ FIELD2 => 'id',
+ );
+ $self->Limit(
+ %args,
+ ALIAS => $users,
+ FIELD => $args{FIELD},
+ OPERATOR => $args{OPERATOR},
+ VALUE => $args{VALUE},
+ CASESENSITIVE => 0,
+ );
+ }
+ }
+ $self->_CloseParen( $args{SUBCLAUSE} ) if $args{SUBCLAUSE};
+ return ($groups, $group_members, $users);
+}
+
+1;