summaryrefslogtreecommitdiff
path: root/rt/lib/RT/SearchBuilder
diff options
context:
space:
mode:
authorIvan Kohler <ivan@freeside.biz>2015-07-09 22:18:55 -0700
committerIvan Kohler <ivan@freeside.biz>2015-07-09 22:27:04 -0700
commite131b1f71f08b69abb832c1687d1f29682d171f8 (patch)
tree490167e41d9fe05b760e7b21a96ee35a86f8edda /rt/lib/RT/SearchBuilder
parentd05d7346bb2387fd9d0354923d577275c5c7f019 (diff)
RT 4.2.11, ticket#13852
Diffstat (limited to 'rt/lib/RT/SearchBuilder')
-rw-r--r--rt/lib/RT/SearchBuilder/AddAndSort.pm219
-rw-r--r--rt/lib/RT/SearchBuilder/Role.pm77
-rw-r--r--rt/lib/RT/SearchBuilder/Role/Roles.pm399
3 files changed, 695 insertions, 0 deletions
diff --git a/rt/lib/RT/SearchBuilder/AddAndSort.pm b/rt/lib/RT/SearchBuilder/AddAndSort.pm
new file mode 100644
index 000000000..abe8aa6d1
--- /dev/null
+++ b/rt/lib/RT/SearchBuilder/AddAndSort.pm
@@ -0,0 +1,219 @@
+# 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::AddAndSort;
+use base 'RT::SearchBuilder';
+
+=head1 NAME
+
+RT::SearchBuilder::AddAndSort - base class for 'add and sort' collections
+
+=head1 DESCRIPTION
+
+Base class for collections where records can be added to objects with order.
+See also L<RT::Record::AddAndSort>. Used by L<RT::ObjectScrips> and
+L<RT::ObjectCustomFields>.
+
+As it's about sorting then collection is sorted by SortOrder field.
+
+=head1 METHODS
+
+=cut
+
+sub _Init {
+ my $self = shift;
+
+ # By default, order by SortOrder
+ $self->OrderByCols(
+ { ALIAS => 'main',
+ FIELD => 'SortOrder',
+ ORDER => 'ASC' },
+ { ALIAS => 'main',
+ FIELD => 'id',
+ ORDER => 'ASC' },
+ );
+
+ return $self->SUPER::_Init(@_);
+}
+
+=head2 LimitToObjectId
+
+Takes id of an object and limits collection.
+
+=cut
+
+sub LimitToObjectId {
+ my $self = shift;
+ my $id = shift || 0;
+ $self->Limit( FIELD => 'ObjectId', VALUE => $id );
+}
+
+=head1 METHODS FOR TARGETS
+
+Rather than implementing a base class for targets (L<RT::Scrip>,
+L<RT::CustomField>) and its collections. This class provides
+class methods to limit target collections.
+
+=head2 LimitTargetToNotAdded
+
+Takes a collection object and optional list of object ids. Limits the
+collection to records not added to listed objects or if the list is
+empty then any object. Use 0 (zero) to mean global.
+
+=cut
+
+sub LimitTargetToNotAdded {
+ my $self = shift;
+ my $collection = shift;
+ my @ids = @_;
+
+ my $alias = $self->JoinTargetToAdded($collection => @ids);
+
+ $collection->Limit(
+ ENTRYAGGREGATOR => 'AND',
+ ALIAS => $alias,
+ FIELD => 'id',
+ OPERATOR => 'IS',
+ VALUE => 'NULL',
+ );
+ return $alias;
+}
+
+=head2 LimitTargetToAdded
+
+L</LimitTargetToNotAdded> with reverse meaning. Takes the same
+arguments.
+
+=cut
+
+sub LimitTargetToAdded {
+ my $self = shift;
+ my $collection = shift;
+ my @ids = @_;
+
+ my $alias = $self->JoinTargetToAdded($collection => @ids);
+
+ $collection->Limit(
+ ENTRYAGGREGATOR => 'AND',
+ ALIAS => $alias,
+ FIELD => 'id',
+ OPERATOR => 'IS NOT',
+ VALUE => 'NULL',
+ );
+ return $alias;
+}
+
+=head2 JoinTargetToAdded
+
+Joins collection to this table using left join, limits joined table
+by ids if those are provided.
+
+Returns alias of the joined table. Join is cached and re-used for
+multiple calls.
+
+=cut
+
+sub JoinTargetToAdded {
+ my $self = shift;
+ my $collection = shift;
+ my @ids = @_;
+
+ my $alias = $self->JoinTargetToThis( $collection, New => 0, Left => 1 );
+ return $alias unless @ids;
+
+ # XXX: we need different EA in join clause, but DBIx::SB
+ # doesn't support them, use IN (X) instead
+ my $dbh = $self->_Handle->dbh;
+ $collection->Limit(
+ LEFTJOIN => $alias,
+ ALIAS => $alias,
+ FIELD => 'ObjectId',
+ OPERATOR => 'IN',
+ VALUE => [ @ids ],
+ );
+
+ return $alias;
+}
+
+=head2 JoinTargetToThis
+
+Joins target collection to this table using TargetField.
+
+Takes New and Left arguments. Use New to avoid caching and re-using
+this join. Use Left to create LEFT JOIN rather than inner.
+
+=cut
+
+sub JoinTargetToThis {
+ my $self = shift;
+ my $collection = shift;
+ my %args = ( New => 0, Left => 0, Distinct => 0, @_ );
+
+ my $table = $self->Table;
+ my $key = "_sql_${table}_alias";
+
+ return $collection->{ $key } if $collection->{ $key } && !$args{'New'};
+
+ my $alias = $collection->Join(
+ $args{'Left'} ? (TYPE => 'LEFT') : (),
+ ALIAS1 => 'main',
+ FIELD1 => 'id',
+ TABLE2 => $table,
+ FIELD2 => $self->RecordClass->TargetField,
+ DISTINCT => $args{Distinct},
+ );
+ return $alias if $args{'New'};
+ return $collection->{ $key } = $alias;
+}
+
+RT::Base->_ImportOverlays();
+
+1;
diff --git a/rt/lib/RT/SearchBuilder/Role.pm b/rt/lib/RT/SearchBuilder/Role.pm
new file mode 100644
index 000000000..ec20de287
--- /dev/null
+++ b/rt/lib/RT/SearchBuilder/Role.pm
@@ -0,0 +1,77 @@
+# 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;
+use Role::Basic;
+
+=head1 NAME
+
+RT::SearchBuilder::Role - Common requirements for roles which are consumed by collections
+
+=head1 DESCRIPTION
+
+Various L<RT::SearchBuilder> (and by inheritance L<DBIx::SearchBuilder>)
+methods are required by this role. It provides no methods on its own but is
+simply a contract for other roles to require (usually under the
+I<RT::SearchBuilder::Role::> namespace).
+
+=cut
+
+requires $_ for qw(
+ Join
+ Limit
+ NewItem
+ CurrentUser
+ _OpenParen
+ _CloseParen
+);
+
+1;
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;