diff options
Diffstat (limited to 'rt/lib/RT/Record')
-rw-r--r-- | rt/lib/RT/Record/AddAndSort.pm | 621 | ||||
-rw-r--r-- | rt/lib/RT/Record/Role.pm | 78 | ||||
-rw-r--r-- | rt/lib/RT/Record/Role/Lifecycle.pm | 219 | ||||
-rw-r--r-- | rt/lib/RT/Record/Role/Links.pm | 174 | ||||
-rw-r--r-- | rt/lib/RT/Record/Role/Rights.pm | 133 | ||||
-rw-r--r-- | rt/lib/RT/Record/Role/Roles.pm | 633 | ||||
-rw-r--r-- | rt/lib/RT/Record/Role/Status.pm | 314 |
7 files changed, 2172 insertions, 0 deletions
diff --git a/rt/lib/RT/Record/AddAndSort.pm b/rt/lib/RT/Record/AddAndSort.pm new file mode 100644 index 000000000..4b5d7ac28 --- /dev/null +++ b/rt/lib/RT/Record/AddAndSort.pm @@ -0,0 +1,621 @@ +# 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::Record::AddAndSort; +use base 'RT::Record'; + +=head1 NAME + +RT::Record::AddAndSort - base class for records that can be added and sorted + +=head1 DESCRIPTION + +Base class for L<RT::ObjectCustomField> and L<RT::ObjectScrip> that unifies +application of L<RT::CustomField>s and L<RT::Scrip>s to various objects. Also, +deals with order of the records. + +=head1 METHODS + +=head2 Meta information + +=head3 CollectionClass + +Returns class representing collection for this record class. Basicly adds 's' +at the end. Should be overriden if default doesn't work. + +For example returns L<RT::ObjectCustomFields> when called on L<RT::ObjectCustomField>. + +=cut + +sub CollectionClass { + return (ref($_[0]) || $_[0]).'s'; +} + +=head3 TargetField + +Returns name of the field in the table where id of object we add is stored. +By default deletes everything up to '::Object' from class name. +This method allows to use friendlier argument names and methods. + +For example returns 'Scrip' for L<RT::ObjectScrip>. + +=cut + +sub TargetField { + my $class = ref($_[0]) || $_[0]; + $class =~ s/.*::Object// or return undef; + return $class; +} + +=head3 ObjectCollectionClass + +Takes an object under L</TargetField> name and should return class +name representing collection the object can be added to. + +Must be overriden by sub classes. + + +See L<RT::ObjectScrip/ObjectCollectionClass> and L<RT::ObjectCustomField/CollectionClass>. + +=cut + +sub ObjectCollectionClass { die "should be subclassed" } + +=head2 Manipulation + +=head3 Create + +Takes 'ObjectId' with id of an object we can be added to, object we can +add to under L</TargetField> name, Disabled and SortOrder. + +This method doesn't create duplicates. If record already exists then it's not created, but +loaded instead. Note that nothing is updated if record exist. + +If SortOrder is not defined then it's calculated to place new record last. If it's +provided then it's caller's duty to make sure it is correct value. + +Example: + + my $ocf = RT::ObjectCustomField->new( RT->SystemUser ); + my ($id, $msg) = $ocf->Create( CustomField => 1, ObjectId => 0 ); + +See L</Add> which has more error checks. Also, L<RT::Scrip> and L<RT::CustomField> +have more appropriate methods that B<should be> prefered over calling this directly. + +=cut + +sub Create { + my $self = shift; + my %args = ( + ObjectId => 0, + SortOrder => undef, + @_ + ); + + my $tfield = $self->TargetField; + + my $target = $self->TargetObj( $args{ $tfield } ); + unless ( $target->id ) { + $RT::Logger->error("Couldn't load ". ref($target) ." '$args{$tfield}'"); + return 0; + } + + my $exist = $self->new($self->CurrentUser); + $exist->LoadByCols( ObjectId => $args{'ObjectId'}, $tfield => $target->id ); + if ( $exist->id ) { + $self->Load( $exist->id ); + return $self->id; + } + + unless ( defined $args{'SortOrder'} ) { + $args{'SortOrder'} = $self->NextSortOrder( + %args, + $tfield => $target, + ); + } + + return $self->SUPER::Create( + %args, + $tfield => $target->id, + ); +} + +=head3 Add + +Helper method that wraps L</Create> and does more checks to make sure +result is consistent. Doesn't allow adding a record to an object if the +record is already global. Removes record from particular objects when +asked to add the record globally. + +=cut + +sub Add { + my $self = shift; + my %args = (@_); + + my $field = $self->TargetField; + + my $tid = $args{ $field }; + $tid = $tid->id if ref $tid; + $tid ||= $self->TargetObj->id; + + my $oid = $args{'ObjectId'}; + $oid = $oid->id if ref $oid; + $oid ||= 0; + + if ( $self->IsAdded( $tid => $oid ) ) { + return ( 0, $self->loc("Is already added to the object") ); + } + + if ( $oid ) { + # adding locally + return (0, $self->loc("Couldn't add as it's global already") ) + if $self->IsAdded( $tid => 0 ); + } + else { + $self->DeleteAll( $field => $tid ); + } + + return $self->Create( + %args, $field => $tid, ObjectId => $oid, + ); +} + +sub IsAdded { + my $self = shift; + my ($tid, $oid) = @_; + my $record = $self->new( $self->CurrentUser ); + $record->LoadByCols( $self->TargetField => $tid, ObjectId => $oid ); + return $record->id; +} + +=head3 AddedTo + +Returns collection with objects target of this record is added to. +Class of the collection depends on L</ObjectCollectionClass>. +See all L</NotAddedTo>. + +For example returns L<RT::Queues> collection if the target is L<RT::Scrip>. + +Returns empty collection if target is added globally. + +=cut + +sub AddedTo { + my $self = shift; + + my ($res, $alias) = $self->_AddedTo( @_ ); + return $res unless $res; + + $res->Limit( + ALIAS => $alias, + FIELD => 'id', + OPERATOR => 'IS NOT', + VALUE => 'NULL', + ); + + return $res; +} + +=head3 NotAddedTo + +Returns collection with objects target of this record is not added to. +Class of the collection depends on L</ObjectCollectionClass>. +See all L</AddedTo>. + +Returns empty collection if target is added globally. + +=cut + +sub NotAddedTo { + my $self = shift; + + my ($res, $alias) = $self->_AddedTo( @_ ); + return $res unless $res; + + $res->Limit( + ALIAS => $alias, + FIELD => 'id', + OPERATOR => 'IS', + VALUE => 'NULL', + ); + + return $res; +} + +sub _AddedTo { + my $self = shift; + my %args = (@_); + + my $field = $self->TargetField; + my $target = $args{ $field } || $self->TargetObj; + + my ($class) = $self->ObjectCollectionClass( $field => $target ); + return undef unless $class; + + my $res = $class->new( $self->CurrentUser ); + + # If target added to a Group, only display user-defined groups + $res->LimitToUserDefinedGroups if $class eq 'RT::Groups'; + + $res->OrderBy( FIELD => 'Name' ); + my $alias = $res->Join( + TYPE => 'LEFT', + ALIAS1 => 'main', + FIELD1 => 'id', + TABLE2 => $self->Table, + FIELD2 => 'ObjectId', + ); + $res->Limit( + LEFTJOIN => $alias, + ALIAS => $alias, + FIELD => $field, + VALUE => $target->id, + ); + return ($res, $alias); +} + +=head3 Delete + +Deletes this record. + +=cut + +sub Delete { + my $self = shift; + + return $self->SUPER::Delete if $self->IsSortOrderShared; + + # Move everything below us up + my $siblings = $self->Neighbors; + $siblings->Limit( FIELD => 'SortOrder', OPERATOR => '>=', VALUE => $self->SortOrder ); + $siblings->OrderBy( FIELD => 'SortOrder', ORDER => 'ASC' ); + foreach my $record ( @{ $siblings->ItemsArrayRef } ) { + $record->SetSortOrder($record->SortOrder - 1); + } + + return $self->SUPER::Delete; +} + +=head3 DeleteAll + +Helper method to delete all applications for one target (Scrip, CustomField, ...). +Target can be provided in arguments. If it's not then L</TargetObj> is used. + + $object_scrip->DeleteAll; + + $object_scrip->DeleteAll( Scrip => $scrip ); + +=cut + +sub DeleteAll { + my $self = shift; + my %args = (@_); + + my $field = $self->TargetField; + + my $id = $args{ $field }; + $id = $id->id if ref $id; + $id ||= $self->TargetObj->id; + + my $list = $self->CollectionClass->new( $self->CurrentUser ); + $list->Limit( FIELD => $field, VALUE => $id ); + $_->Delete foreach @{ $list->ItemsArrayRef }; +} + +=head3 MoveUp + +Moves record up. + +=cut + +sub MoveUp { return shift->Move( Up => @_ ) } + +=head3 MoveDown + +Moves record down. + +=cut + +sub MoveDown { return shift->Move( Down => @_ ) } + +=head3 Move + +Takes 'up' or 'down'. One method that implements L</MoveUp> and L</MoveDown>. + +=cut + +sub Move { + my $self = shift; + my $dir = lc(shift || 'up'); + + my %meta; + if ( $dir eq 'down' ) { + %meta = qw( + next_op > + next_order ASC + prev_op <= + diff +1 + ); + } else { + %meta = qw( + next_op < + next_order DESC + prev_op >= + diff -1 + ); + } + + my $siblings = $self->Siblings; + $siblings->Limit( FIELD => 'SortOrder', OPERATOR => $meta{'next_op'}, VALUE => $self->SortOrder ); + $siblings->OrderBy( FIELD => 'SortOrder', ORDER => $meta{'next_order'} ); + + my @next = ($siblings->Next, $siblings->Next); + unless ($next[0]) { + return $dir eq 'down' + ? (0, "Can not move down. It's already at the bottom") + : (0, "Can not move up. It's already at the top") + ; + } + + my ($new_sort_order, $move); + + unless ( $self->ObjectId ) { + # moving global, it can not share sort order, so just move it + # on place of next global and move everything in between one number + + $new_sort_order = $next[0]->SortOrder; + $move = $self->Neighbors; + $move->Limit( + FIELD => 'SortOrder', OPERATOR => $meta{'next_op'}, VALUE => $self->SortOrder, + ); + $move->Limit( + FIELD => 'SortOrder', OPERATOR => $meta{'prev_op'}, VALUE => $next[0]->SortOrder, + ENTRYAGGREGATOR => 'AND', + ); + } + elsif ( $next[0]->ObjectId == $self->ObjectId ) { + # moving two locals, just swap them, they should follow 'so = so+/-1' rule + $new_sort_order = $next[0]->SortOrder; + $move = $next[0]; + } + else { + # moving local behind global + unless ( $self->IsSortOrderShared ) { + # not shared SO allows us to swap + $new_sort_order = $next[0]->SortOrder; + $move = $next[0]; + } + elsif ( $next[1] ) { + # more records there and shared SO, we have to move everything + $new_sort_order = $next[0]->SortOrder; + $move = $self->Neighbors; + $move->Limit( + FIELD => 'SortOrder', OPERATOR => $meta{prev_op}, VALUE => $next[0]->SortOrder, + ); + } + else { + # shared SO and place after is free, so just jump + $new_sort_order = $next[0]->SortOrder + $meta{'diff'}; + } + } + + if ( $move ) { + foreach my $record ( $move->isa('RT::Record')? ($move) : @{ $move->ItemsArrayRef } ) { + my ($status, $msg) = $record->SetSortOrder( + $record->SortOrder - $meta{'diff'} + ); + return (0, "Couldn't move: $msg") unless $status; + } + } + + my ($status, $msg) = $self->SetSortOrder( $new_sort_order ); + unless ( $status ) { + return (0, "Couldn't move: $msg"); + } + + return (1,"Moved"); +} + +=head2 Accessors, instrospection and traversing. + +=head3 TargetObj + +Returns target object of this record. Returns L<RT::Scrip> object for +L<RT::ObjectScrip>. + +=cut + +sub TargetObj { + my $self = shift; + my $id = shift; + + my $method = $self->TargetField .'Obj'; + return $self->$method( $id ); +} + +=head3 NextSortOrder + +Returns next available SortOrder value in the L<neighborhood|/Neighbors>. +Pass arguments to L</Neighbors> and can take optional ObjectId argument, +calls ObjectId if it's not provided. + +=cut + +sub NextSortOrder { + my $self = shift; + my %args = (@_); + + my $oid = $args{'ObjectId'}; + $oid = $self->ObjectId unless defined $oid; + $oid ||= 0; + + my $neighbors = $self->Neighbors( %args ); + if ( $oid ) { + $neighbors->LimitToObjectId( $oid ); + $neighbors->LimitToObjectId( 0 ); + } elsif ( !$neighbors->_isLimited ) { + $neighbors->UnLimit; + } + $neighbors->OrderBy( FIELD => 'SortOrder', ORDER => 'DESC' ); + return 0 unless my $first = $neighbors->First; + return $first->SortOrder + 1; +} + +=head3 IsSortOrderShared + +Returns true if this record shares SortOrder value with a L<neighbor|/Neighbors>. + +=cut + +sub IsSortOrderShared { + my $self = shift; + return 0 unless $self->ObjectId; + + my $neighbors = $self->Neighbors; + $neighbors->Limit( FIELD => 'id', OPERATOR => '!=', VALUE => $self->id ); + $neighbors->Limit( FIELD => 'SortOrder', VALUE => $self->SortOrder ); + return $neighbors->Count; +} + +=head2 Neighbors and Siblings + +These two methods should only be understood by developers who wants +to implement new classes of records that can be added to other records +and sorted. + +Main purpose is to maintain SortOrder values. + +Let's take a look at custom fields. A custom field can be created for tickets, +queues, transactions, users... Custom fields created for tickets can +be added globally or to particular set of queues. Custom fields for +tickets are neighbors. Neighbor custom fields added to the same objects +are siblings. Custom fields added globally are sibling to all neighbors. + +For scrips Stage defines neighborhood. + +Let's look at the three scrips in create stage S1, S2 and S3, queues Q1 and Q2 and +G for global. + + S1@Q1, S3@Q2 0 + S2@G 1 + S1@Q2 2 + +Above table says that S2 is added globally, S1 is added to Q1 and executed +before S2 in this queue, also S1 is added to Q1, but exectued after S2 in this +queue, S3 is only added to Q2 and executed before S2 and S1. + +Siblings are scrips added to an object including globally added or only +globally added. In our example there are three different collection +of siblings: (S2) - global, (S1, S2) for Q1, (S3, S2, S1) for Q2. + +Sort order can be shared between neighbors, but can not be shared between siblings. + +Here is what happens with sort order if we move S1@Q2 one position up: + + S3@Q2 0 + S1@Q1, S1@Q2 1 + S2@G 2 + +One position more: + + S1@Q2 0 + S1@Q1, S3@Q2 1 + S2@G 2 + +Hopefuly it's enough to understand how it works. + +Targets from different neighborhood can not be sorted against each other. + +=head3 Neighbors + +Returns collection of records of this class with all +neighbors. By default all possible targets are neighbors. + +Takes the same arguments as L</Create> method. If arguments are not passed +then uses the current record. + +See L</Neighbors and Siblings> for detailed description. + +See L<RT::ObjectCustomField/Neighbors> for example. + +=cut + +sub Neighbors { + my $self = shift; + return $self->CollectionClass->new( $self->CurrentUser ); +} + +=head3 Siblings + +Returns collection of records of this class with siblings. + +Takes the same arguments as L</Neighbors>. Siblings is subset of L</Neighbors>. + +=cut + +sub Siblings { + my $self = shift; + my %args = @_; + + my $oid = $args{'ObjectId'}; + $oid = $self->ObjectId unless defined $oid; + $oid ||= 0; + + my $res = $self->Neighbors( %args ); + $res->LimitToObjectId( $oid ); + $res->LimitToObjectId( 0 ) if $oid; + return $res; +} + +RT::Base->_ImportOverlays(); + +1; diff --git a/rt/lib/RT/Record/Role.pm b/rt/lib/RT/Record/Role.pm new file mode 100644 index 000000000..9d95ea152 --- /dev/null +++ b/rt/lib/RT/Record/Role.pm @@ -0,0 +1,78 @@ +# 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::Record::Role; +use Role::Basic; + +=head1 NAME + +RT::Record::Role - Common requirements for roles which are consumed by records + +=head1 DESCRIPTION + +Various L<RT::Record> (and by inheritance L<DBIx::SearchBuilder::Record>) +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::Record::Role::> namespace). + +=cut + +requires $_ for qw( + id + loc + CurrentUser + + _Set + _Accessible + _NewTransaction +); + +1; diff --git a/rt/lib/RT/Record/Role/Lifecycle.pm b/rt/lib/RT/Record/Role/Lifecycle.pm new file mode 100644 index 000000000..0474a06e1 --- /dev/null +++ b/rt/lib/RT/Record/Role/Lifecycle.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::Record::Role::Lifecycle; +use Role::Basic; +use Scalar::Util qw(blessed); + +=head1 NAME + +RT::Record::Role::Lifecycle - Common methods for records which have a Lifecycle column + +=head1 REQUIRES + +=head2 L<RT::Record::Role> + +=head2 LifecycleType + +Used as a role parameter. Must return a string of the type of lifecycles the +record consumes, i.e. I<ticket> for L<RT::Queue>. + +=head2 Lifecycle + +A Lifecycle method which returns a lifecycle name is required. Currently +unenforced at compile-time due to poor interactions with +L<DBIx::SearchBuilder::Record/AUTOLOAD>. You'll hit run-time errors if this +method isn't available in consuming classes, however. + +=cut + +with 'RT::Record::Role'; +requires 'LifecycleType'; + +# XXX: can't require column methods due to DBIx::SB::Record's AUTOLOAD +#requires 'Lifecycle'; + +=head1 PROVIDES + +=head2 LifecycleObj + +Returns an L<RT::Lifecycle> object for this record's C<Lifecycle>. If called +as a class method, returns an L<RT::Lifecycle> object which is an aggregation +of all lifecycles of the appropriate type. + +=cut + +sub LifecycleObj { + my $self = shift; + my $type = $self->LifecycleType; + my $fallback = $self->_Accessible( Lifecycle => "default" ); + + unless (blessed($self) and $self->id) { + return RT::Lifecycle->Load( Type => $type ); + } + + my $name = $self->Lifecycle || $fallback; + my $res = RT::Lifecycle->Load( Name => $name, Type => $type ); + unless ( $res ) { + RT->Logger->error( + sprintf "Lifecycle '%s' of type %s for %s #%d doesn't exist", + $name, $type, ref($self), $self->id); + return RT::Lifecycle->Load( Name => $fallback, Type => $type ); + } + return $res; +} + +=head2 SetLifecycle + +Validates that the specified lifecycle exists before updating the record. + +Takes a lifecycle name. + +=cut + +sub SetLifecycle { + my $self = shift; + my $value = shift || $self->_Accessible( Lifecycle => "default" ); + + return (0, $self->loc('[_1] is not a valid lifecycle', $value)) + unless $self->ValidateLifecycle($value); + + return $self->_Set( Field => 'Lifecycle', Value => $value, @_ ); +} + +=head2 ValidateLifecycle + +Takes a lifecycle name. Returns true if it's an OK name and such lifecycle is +configured. Returns false otherwise. + +=cut + +sub ValidateLifecycle { + my $self = shift; + my $value = shift; + return unless $value; + return unless RT::Lifecycle->Load( Name => $value, Type => $self->LifecycleType ); + return 1; +} + +=head2 ActiveStatusArray + +Returns an array of all ActiveStatuses for the lifecycle + +=cut + +sub ActiveStatusArray { + my $self = shift; + return $self->LifecycleObj->Valid('initial', 'active'); +} + +=head2 InactiveStatusArray + +Returns an array of all InactiveStatuses for the lifecycle + +=cut + +sub InactiveStatusArray { + my $self = shift; + return $self->LifecycleObj->Inactive; +} + +=head2 StatusArray + +Returns an array of all statuses for the lifecycle + +=cut + +sub StatusArray { + my $self = shift; + return $self->LifecycleObj->Valid( @_ ); +} + +=head2 IsValidStatus + +Takes a status. + +Returns true if STATUS is a valid status. Otherwise, returns 0. + +=cut + +sub IsValidStatus { + my $self = shift; + return $self->LifecycleObj->IsValid( shift ); +} + +=head2 IsActiveStatus + +Takes a status. + +Returns true if STATUS is a Active status. Otherwise, returns 0 + +=cut + +sub IsActiveStatus { + my $self = shift; + return $self->LifecycleObj->IsValid( shift, 'initial', 'active'); +} + +=head2 IsInactiveStatus + +Takes a status. + +Returns true if STATUS is a Inactive status. Otherwise, returns 0 + +=cut + +sub IsInactiveStatus { + my $self = shift; + return $self->LifecycleObj->IsInactive( shift ); +} + +1; diff --git a/rt/lib/RT/Record/Role/Links.pm b/rt/lib/RT/Record/Role/Links.pm new file mode 100644 index 000000000..f865090df --- /dev/null +++ b/rt/lib/RT/Record/Role/Links.pm @@ -0,0 +1,174 @@ +# 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::Record::Role::Links; +use Role::Basic; + +=head1 NAME + +RT::Record::Role::Links - Common methods for records which handle links + +=head1 REQUIRES + +=head2 L<RT::Record::Role> + +=head2 _AddLink + +Usually provided by L<RT::Record/_AddLink>. + +=head2 _DeleteLink + +Usually provided by L<RT::Record/_DeleteLink>. + +=head2 ModifyLinkRight + +The right name to check in L<AddLink> and L<DeleteLink>. + +=head2 CurrentUserHasRight + +=cut + +with 'RT::Record::Role'; + +requires '_AddLink'; +requires '_DeleteLink'; + +requires 'ModifyLinkRight'; +requires 'CurrentUserHasRight'; + +=head1 PROVIDES + +=head2 _AddLinksOnCreate + +Calls _AddLink (usually L<RT::Record/_AddLink>) for all valid link types and +aliases found in the hash. Refer to L<RT::Link/%TYPEMAP> for details of link +types. Key values may be a single URI or an arrayref of URIs. + +Takes two hashrefs. The first is the argument hash provided to the consuming +class's Create method. The second is optional and contains extra arguments to +pass to _AddLink. + +By default records a transaction on the link's destination object (if any), but +not on the origin object. + +Returns an array of localized error messages, if any. + +=cut + +sub _AddLinksOnCreate { + my $self = shift; + my %args = %{shift || {}}; + my %AddLink = %{shift || {}}; + my @results; + + foreach my $type ( keys %RT::Link::TYPEMAP ) { + next unless defined $args{$type}; + + my $links = $args{$type}; + $links = [$links] unless ref $links; + + for my $link (@$links) { + my $typemap = $RT::Link::TYPEMAP{$type}; + my $opposite_mode = $typemap->{Mode} eq "Base" ? "Target" : "Base"; + my ($ok, $msg) = $self->_AddLink( + Type => $typemap->{Type}, + $typemap->{Mode} => $link, + "Silent$opposite_mode" => 1, + %AddLink, + ); + push @results, + $self->loc("Unable to add [_1] link: [_2]", $self->loc($type), $msg) + unless $ok; + } + } + return @results; +} + +=head2 AddLink + +Takes a paramhash of Type and one of Base or Target. Adds that link to this +record. + +Refer to L<RT::Record/_AddLink> for full documentation. This method implements +permissions and ticket validity checks before calling into L<RT::Record> +(usually). + +=cut + +sub AddLink { + my $self = shift; + + return (0, $self->loc("Permission Denied")) + unless $self->CurrentUserHasRight($self->ModifyLinkRight); + + return $self->_AddLink(@_); +} + +=head2 DeleteLink + +Takes a paramhash of Type and one of Base or Target. Removes that link from the +record. + +Refer to L<RT::Record/_DeleteLink> for full documentation. This method +implements permission checks before calling into L<RT::Record> (usually). + +=cut + +sub DeleteLink { + my $self = shift; + + return (0, $self->loc("Permission Denied")) + unless $self->CurrentUserHasRight($self->ModifyLinkRight); + + return $self->_DeleteLink(@_); +} + +1; diff --git a/rt/lib/RT/Record/Role/Rights.pm b/rt/lib/RT/Record/Role/Rights.pm new file mode 100644 index 000000000..cd2d60adf --- /dev/null +++ b/rt/lib/RT/Record/Role/Rights.pm @@ -0,0 +1,133 @@ +# 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::Record::Role::Rights; +use Role::Basic; +use Scalar::Util qw(blessed); + +=head1 NAME + +RT::Record::Role::Rights - Common methods for records which can provide rights + +=head1 DESCRIPTION + +=head1 REQUIRES + +=head2 L<RT::Record::Role> + +=cut + +with 'RT::Record::Role'; + +=head1 PROVIDES + +=cut + +=head2 AddRight C<CATEGORY>, C<RIGHT>, C<DESCRIPTION> + +Adds the given rights to the list of possible rights. This method +should be called during server startup, not at runtime. + +=cut + +sub AddRight { + my $class = shift; + $class = ref($class) || $class; + my ($category, $name, $description) = @_; + + require RT::ACE; + if (exists $RT::ACE::RIGHTS{$class}{lc $name}) { + warn "Duplicate right '$name' found"; + return; + } + + $RT::ACE::RIGHTS{$class}{lc $name} = { + Name => $name, + Category => $category, + Description => $description, + }; +} + +=head2 AvailableRights + +Returns a hashref of available rights for this object. The keys are the +right names and the values are a description of what the rights do. + +=cut + +sub AvailableRights { + my $self = shift; + my $class = ref($self) || $self; + + my %rights; + $rights{$_->{Name}} = $_->{Description} + for values %{$RT::ACE::RIGHTS{$class} || {} }; + return \%rights; +} + +=head2 RightCategories + +Returns a hashref where the keys are rights for this type of object and the +values are the category (General, Staff, Admin) the right falls into. + +=cut + +sub RightCategories { + my $self = shift; + my $class = ref($self) || $self; + + my %rights; + $rights{$_->{Name}} = $_->{Category} + for values %{ $RT::ACE::RIGHTS{$class} || {} }; + return \%rights; +} + +1; diff --git a/rt/lib/RT/Record/Role/Roles.pm b/rt/lib/RT/Record/Role/Roles.pm new file mode 100644 index 000000000..725c0d76e --- /dev/null +++ b/rt/lib/RT/Record/Role/Roles.pm @@ -0,0 +1,633 @@ +# 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::Record::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::Record::Role> + +=cut + +with 'RT::Record::Role'; + +require RT::System; +require RT::Principal; +require RT::Group; +require RT::User; + +require RT::EmailParser; + +=head1 PROVIDES + +=head2 RegisterRole + +Registers an RT role which applies to this class for role-based access control. +Arguments: + +=over 4 + +=item Name + +Required. The role name (i.e. Requestor, Owner, AdminCc, etc). + +=item EquivClasses + +Optional. Array ref of classes through which this role percolates up to +L<RT::System>. You can think of this list as: + + map { ref } $record_object->ACLEquivalenceObjects; + +You should not include L<RT::System> itself in this list. + +Simply calls RegisterRole on each equivalent class. + +=item Single + +Optional. A true value indicates that this role may only contain a single user +as a member at any given time. When adding a new member to a Single role, any +existing member will be removed. If all members are removed, L<RT/Nobody> is +added automatically. + +=item Column + +Optional, implies Single. Specifies a column on the announcing class into +which the single role member's user ID is denormalized. The column will be +kept updated automatically as the role member changes. This is used, for +example, for ticket owners and makes searching simpler (among other benefits). + +=item ACLOnly + +Optional. A true value indicates this role is only used for ACLs and should +not be populated with members. + +This flag is advisory only, and the Perl API still allows members to be added +to ACLOnly roles. + +=item ACLOnlyInEquiv + +Optional. Automatically sets the ACLOnly flag for all EquivClasses, but not +the announcing class. + +=item SortOrder + +Optional. A numeric value indicating the position of this role when sorted +ascending with other roles in a list. Roles with the same sort order are +ordered alphabetically by name within themselves. + +=back + +=cut + +sub RegisterRole { + my $self = shift; + my $class = ref($self) || $self; + my %role = ( + Name => undef, + EquivClasses => [], + SortOrder => 0, + @_ + ); + return unless $role{Name}; + + # Keep track of the class this role came from originally + $role{ Class } ||= $class; + + # Some groups are limited to a single user + $role{ Single } = 1 if $role{Column}; + + # Stash the role on ourself + $class->_ROLES->{ $role{Name} } = { %role }; + + # Register it with any equivalent classes... + my $equiv = delete $role{EquivClasses} || []; + + # ... and globally unless we ARE global + unless ($class eq "RT::System") { + push @$equiv, "RT::System"; + } + + # ... marked as "for ACLs only" if flagged as such by the announcing class + $role{ACLOnly} = 1 if delete $role{ACLOnlyInEquiv}; + + $_->RegisterRole(%role) for @$equiv; + + # XXX TODO: Register which classes have roles on them somewhere? + + return 1; +} + +=head2 UnregisterRole + +Removes an RT role which applies to this class for role-based access control. +Any roles on equivalent classes (via EquivClasses passed to L</RegisterRole>) +are also unregistered. + +Takes a role name as the sole argument. + +B<Use this carefully:> Objects created after a role is unregistered will not +have an associated L<RT::Group> for the removed role. If you later decide to +stop unregistering the role, operations on those objects created in the +meantime will fail when trying to interact with the missing role groups. + +B<Unregistering a role may break code which assumes the role exists.> + +=cut + +sub UnregisterRole { + my $self = shift; + my $class = ref($self) || $self; + my $name = shift + or return; + + my $role = delete $self->_ROLES->{$name} + or return; + + $_->UnregisterRole($name) + for "RT::System", reverse @{$role->{EquivClasses}}; +} + +=head2 Role + +Takes a role name; returns a hashref describing the role. This hashref +contains the same attributes used to register the role (see L</RegisterRole>), +as well as some extras, including: + +=over + +=item Class + +The original class which announced the role. This is set automatically by +L</RegisterRole> and is the same across all EquivClasses. + +=back + +Returns an empty hashref if the role doesn't exist. + +=cut + +sub Role { + return \%{ $_[0]->_ROLES->{$_[1]} || {} }; +} + +=head2 Roles + +Returns a list of role names registered for this class, sorted ascending by +SortOrder and then alphabetically by name. + +Optionally takes a hash specifying attributes the returned roles must possess +or lack. Testing is done on a simple truthy basis and the actual values of +the role attributes and arguments you pass are not compared string-wise or +numerically; they must simply evaluate to the same truthiness. + +For example: + + # Return role names which are not only for ACL purposes + $object->Roles( ACLOnly => 0 ); + + # Return role names which are denormalized into a column; note that the + # role's Column attribute contains a string. + $object->Roles( Column => 1 ); + +=cut + +sub Roles { + my $self = shift; + my %attr = @_; + + return map { $_->[0] } + sort { $a->[1]{SortOrder} <=> $b->[1]{SortOrder} + or $a->[0] cmp $b->[0] } + grep { + my $ok = 1; + for my $k (keys %attr) { + $ok = 0, last if $attr{$k} xor $_->[1]{$k}; + } + $ok } + map { [ $_, $self->Role($_) ] } + keys %{ $self->_ROLES }; +} + +{ + my %ROLES; + sub _ROLES { + my $class = ref($_[0]) || $_[0]; + return $ROLES{$class} ||= {}; + } +} + +=head2 HasRole + +Returns true if the name provided is a registered role for this class. +Otherwise returns false. + +=cut + +sub HasRole { + my $self = shift; + my $type = shift; + return scalar grep { $type eq $_ } $self->Roles; +} + +=head2 RoleGroup + +Expects a role name as the first parameter which is used to load the +L<RT::Group> for the specified role on this record. Returns an unloaded +L<RT::Group> object on failure. + +=cut + +sub RoleGroup { + my $self = shift; + my $name = shift; + my $group = RT::Group->new( $self->CurrentUser ); + + if ($self->HasRole($name)) { + $group->LoadRoleGroup( + Object => $self, + Name => $name, + ); + } + return $group; +} + +=head2 AddRoleMember + +Adds the described L<RT::Principal> to the specified role group for this record. + +Takes a set of key-value pairs: + +=over 4 + +=item PrincipalId + +Optional. The ID of the L<RT::Principal> object to add. + +=item User + +Optional. The Name or EmailAddress of an L<RT::User> to use as the +principal. If an email address is given, but a user matching it cannot +be found, a new user will be created. + +=item Group + +Optional. The Name of an L<RT::Group> to use as the principal. + +=item Type + +Required. One of the valid roles for this record, as returned by L</Roles>. + +=item ACL + +Optional. A subroutine reference which will be passed the role type and +principal being added. If it returns false, the method will fail with a +status of "Permission denied". + +=back + +One, and only one, of I<PrincipalId>, I<User>, or I<Group> is required. + +Returns a tuple of (principal object which was added, message). + +=cut + +sub AddRoleMember { + my $self = shift; + my %args = (@_); + + return (0, $self->loc("One, and only one, of PrincipalId/User/Group is required")) + if 1 != grep { $_ } @args{qw/PrincipalId User Group/}; + + my $type = delete $args{Type}; + return (0, $self->loc("No valid Type specified")) + unless $type and $self->HasRole($type); + + if ($args{PrincipalId}) { + # Check the PrincipalId for loops + my $principal = RT::Principal->new( $self->CurrentUser ); + $principal->Load($args{'PrincipalId'}); + if ( $principal->id and $principal->IsUser and my $email = $principal->Object->EmailAddress ) { + return (0, $self->loc("[_1] is an address RT receives mail at. Adding it as a '[_2]' would create a mail loop", + $email, $self->loc($type))) + if RT::EmailParser->IsRTAddress( $email ); + } + } else { + if ($args{User}) { + my $name = delete $args{User}; + # Sanity check the address + return (0, $self->loc("[_1] is an address RT receives mail at. Adding it as a '[_2]' would create a mail loop", + $name, $self->loc($type) )) + if RT::EmailParser->IsRTAddress( $name ); + + # Create as the SystemUser, not the current user + my $user = RT::User->new(RT->SystemUser); + my ($ok, $msg); + if ($name =~ /@/) { + ($ok, $msg) = $user->LoadOrCreateByEmail( $name ); + } else { + ($ok, $msg) = $user->Load( $name ); + } + unless ($user->Id) { + # If we can't find this watcher, we need to bail. + $RT::Logger->error("Could not load or create a user '$name' to add as a watcher: $msg"); + return (0, $self->loc("Could not find or create user '[_1]'", $name)); + } + $args{PrincipalId} = $user->PrincipalId; + } + elsif ($args{Group}) { + my $name = delete $args{Group}; + my $group = RT::Group->new( $self->CurrentUser ); + $group->LoadUserDefinedGroup($name); + unless ($group->id) { + $RT::Logger->error("Could not load group '$name' to add as a watcher"); + return (0, $self->loc("Could not find group '[_1]'", $name)); + } + $args{PrincipalId} = $group->PrincipalObj->id; + } + } + + my $principal = RT::Principal->new( $self->CurrentUser ); + $principal->Load( $args{PrincipalId} ); + + my $acl = delete $args{ACL}; + return (0, $self->loc("Permission denied")) + if $acl and not $acl->($type => $principal); + + my $group = $self->RoleGroup( $type ); + return (0, $self->loc("Role group '[_1]' not found", $type)) + unless $group->id; + + return (0, $self->loc('[_1] is already a [_2]', + $principal->Object->Name, $self->loc($type)) ) + if $group->HasMember( $principal ); + + return (0, $self->loc('[_1] cannot be a group', $self->loc($type)) ) + if $group->SingleMemberRoleGroup and $principal->IsGroup; + + my ( $ok, $msg ) = $group->_AddMember( %args, RecordTransaction => !$args{Silent} ); + unless ($ok) { + $RT::Logger->error("Failed to add $args{PrincipalId} as a member of group ".$group->Id.": ".$msg); + + return ( 0, $self->loc('Could not make [_1] a [_2]', + $principal->Object->Name, $self->loc($type)) ); + } + + return ($principal, $msg); +} + +=head2 DeleteRoleMember + +Removes the specified L<RT::Principal> from the specified role group for this +record. + +Takes a set of key-value pairs: + +=over 4 + +=item PrincipalId + +Optional. The ID of the L<RT::Principal> object to remove. + +=item User + +Optional. The Name or EmailAddress of an L<RT::User> to use as the +principal + +=item Type + +Required. One of the valid roles for this record, as returned by L</Roles>. + +=item ACL + +Optional. A subroutine reference which will be passed the role type and +principal being removed. If it returns false, the method will fail with a +status of "Permission denied". + +=back + +One, and only one, of I<PrincipalId> or I<User> is required. + +Returns a tuple of (principal object that was removed, message). + +=cut + +sub DeleteRoleMember { + my $self = shift; + my %args = (@_); + + return (0, $self->loc("No valid Type specified")) + unless $args{Type} and $self->HasRole($args{Type}); + + if ($args{User}) { + my $user = RT::User->new( $self->CurrentUser ); + $user->LoadByEmail( $args{User} ); + $user->Load( $args{User} ) unless $user->id; + return (0, $self->loc("Could not load user '[_1]'", $args{User}) ) + unless $user->id; + $args{PrincipalId} = $user->PrincipalId; + } + + return (0, $self->loc("No valid PrincipalId")) + unless $args{PrincipalId}; + + my $principal = RT::Principal->new( $self->CurrentUser ); + $principal->Load( $args{PrincipalId} ); + + my $acl = delete $args{ACL}; + return (0, $self->loc("Permission denied")) + if $acl and not $acl->($args{Type} => $principal); + + my $group = $self->RoleGroup( $args{Type} ); + return (0, $self->loc("Role group '[_1]' not found", $args{Type})) + unless $group->id; + + return ( 0, $self->loc( '[_1] is not a [_2]', + $principal->Object->Name, $self->loc($args{Type}) ) ) + unless $group->HasMember($principal); + + my ($ok, $msg) = $group->_DeleteMember($args{PrincipalId}, RecordTransaction => !$args{Silent}); + unless ($ok) { + $RT::Logger->error("Failed to remove $args{PrincipalId} as a member of group ".$group->Id.": ".$msg); + + return ( 0, $self->loc('Could not remove [_1] as a [_2]', + $principal->Object->Name, $self->loc($args{Type})) ); + } + + return ($principal, $msg); +} + +sub _ResolveRoles { + my $self = shift; + my ($roles, %args) = (@_); + + my @errors; + for my $role ($self->Roles) { + if ($self->_ROLES->{$role}{Single}) { + # Default to nobody if unspecified + my $value = $args{$role} || RT->Nobody; + $value = $value->[0] if ref $value eq 'ARRAY'; + if (Scalar::Util::blessed($value) and $value->isa("RT::User")) { + # Accept a user; it may not be loaded, which we catch below + $roles->{$role} = $value->PrincipalObj; + } else { + # Try loading by id, name, then email. If all fail, catch that below + my $user = RT::User->new( $self->CurrentUser ); + $user->Load( $value ); + # XXX: LoadOrCreateByEmail ? + $user->LoadByEmail( $value ) unless $user->id; + $roles->{$role} = $user->PrincipalObj; + } + unless (Scalar::Util::blessed($roles->{$role}) and $roles->{$role}->id) { + push @errors, $self->loc("Invalid value for [_1]",$self->loc($role)); + $roles->{$role} = RT->Nobody->PrincipalObj; + } + # For consistency, we always return an arrayref + $roles->{$role} = [ $roles->{$role} ]; + } else { + $roles->{$role} = []; + my @values = ref $args{ $role } ? @{ $args{$role} } : ($args{$role}); + for my $value (grep {defined} @values) { + if ( $value =~ /^\d+$/ ) { + # This implicitly allows groups, if passed by id. + my $principal = RT::Principal->new( $self->CurrentUser ); + my ($ok, $msg) = $principal->Load( $value ); + if ($ok) { + push @{ $roles->{$role} }, $principal; + } else { + push @errors, + $self->loc("Couldn't load principal: [_1]", $msg); + } + } else { + my @addresses = RT::EmailParser->ParseEmailAddress( $value ); + for my $address ( @addresses ) { + my $user = RT::User->new( RT->SystemUser ); + my ($id, $msg) = $user->LoadOrCreateByEmail( $address ); + if ( $id ) { + # Load it back as us, not as the system + # user, to be completely safe. + $user = RT::User->new( $self->CurrentUser ); + $user->Load( $id ); + push @{ $roles->{$role} }, $user->PrincipalObj; + } else { + push @errors, + $self->loc("Couldn't load or create user: [_1]", $msg); + } + } + } + } + } + } + return (@errors); +} + +sub _CreateRoleGroups { + my $self = shift; + my %args = (@_); + for my $name ($self->Roles) { + my $type_obj = RT::Group->new($self->CurrentUser); + my ($id, $msg) = $type_obj->CreateRoleGroup( + Name => $name, + Object => $self, + %args, + ); + unless ($id) { + $RT::Logger->error("Couldn't create a role group of type '$name' for ".ref($self)." ". + $self->id.": ".$msg); + return(undef); + } + } + return(1); +} + +sub _AddRolesOnCreate { + my $self = shift; + my ($roles, %acls) = @_; + + my @errors; + { + my $changed = 0; + + for my $role (keys %{$roles}) { + my $group = $self->RoleGroup($role); + my @left; + for my $principal (@{$roles->{$role}}) { + if ($acls{$role}->($principal)) { + next if $group->HasMember($principal); + my ($ok, $msg) = $group->_AddMember( + PrincipalId => $principal->id, + InsideTransaction => 1, + RecordTransaction => 0, + Object => $self, + ); + push @errors, $self->loc("Couldn't set [_1] watcher: [_2]", $role, $msg) + unless $ok; + $changed++; + } else { + push @left, $principal; + } + } + $roles->{$role} = [ @left ]; + } + + redo if $changed; + } + + return @errors; +} + + +1; diff --git a/rt/lib/RT/Record/Role/Status.pm b/rt/lib/RT/Record/Role/Status.pm new file mode 100644 index 000000000..98f699cd0 --- /dev/null +++ b/rt/lib/RT/Record/Role/Status.pm @@ -0,0 +1,314 @@ +# 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::Record::Role::Status; +use Role::Basic; +use Scalar::Util qw(blessed); + +=head1 NAME + +RT::Record::Role::Status - Common methods for records which have a Status column + +=head1 DESCRIPTION + +Lifecycles are generally set on container records, and Statuses on records +which belong to one of those containers. L<RT::Record::Role::Lifecycle> +handles the containers with the I<Lifecycle> column. This role is for the +records with a I<Status> column within those containers. It includes +convenience methods for grabbing an L<RT::Lifecycle> object as well setters for +validating I<Status> and the column which points to the container object. + +=head1 REQUIRES + +=head2 L<RT::Record::Role> + +=head2 LifecycleColumn + +Used as a role parameter. Must return a string of the column name which points +to the container object that consumes L<RT::Record::Role::Lifecycle> (or +conforms to it). The resulting string is used to construct two method names: +as-is to fetch the column value and suffixed with "Obj" to fetch the object. + +=head2 Status + +A Status method which returns a lifecycle name is required. Currently +unenforced at compile-time due to poor interactions with +L<DBIx::SearchBuilder::Record/AUTOLOAD>. You'll hit run-time errors if this +method isn't available in consuming classes, however. + +=cut + +with 'RT::Record::Role'; +requires 'LifecycleColumn'; + +=head1 PROVIDES + +=head2 Status + +Returns the Status for this record, in the canonical casing. + +=cut + +sub Status { + my $self = shift; + my $value = $self->_Value( 'Status' ); + my $lifecycle = $self->LifecycleObj; + return $value unless $lifecycle; + return $lifecycle->CanonicalCase( $value ); +} + +=head2 LifecycleObj + +Returns an L<RT::Lifecycle> object for this record's C<Lifecycle>. If called +as a class method, returns an L<RT::Lifecycle> object which is an aggregation +of all lifecycles of the appropriate type. + +=cut + +sub LifecycleObj { + my $self = shift; + my $obj = $self->LifecycleColumn . "Obj"; + return $self->$obj->LifecycleObj; +} + +=head2 Lifecycle + +Returns the L<RT::Lifecycle/Name> of this record's L</LifecycleObj>. + +=cut + +sub Lifecycle { + my $self = shift; + return $self->LifecycleObj->Name; +} + +=head2 ValidateStatus + +Takes a status. Returns true if that status is a valid status for this record, +otherwise returns false. + +=cut + +sub ValidateStatus { + my $self = shift; + return $self->LifecycleObj->IsValid(@_); +} + +=head2 ValidateStatusChange + +Validates the new status with the current lifecycle. Returns a tuple of (OK, +message). + +Expected to be called from this role's L</SetStatus> or the consuming class' +equivalent. + +=cut + +sub ValidateStatusChange { + my $self = shift; + my $new = shift; + my $old = $self->Status; + + my $lifecycle = $self->LifecycleObj; + + unless ( $lifecycle->IsValid( $new ) ) { + return (0, $self->loc("Status '[_1]' isn't a valid status for this [_2].", $self->loc($new), $self->loc($lifecycle->Type))); + } + + unless ( $lifecycle->IsTransition( $old => $new ) ) { + return (0, $self->loc("You can't change status from '[_1]' to '[_2]'.", $self->loc($old), $self->loc($new))); + } + + my $check_right = $lifecycle->CheckRight( $old => $new ); + unless ( $self->CurrentUser->HasRight( Right => $check_right, Object => $self ) ) { + return ( 0, $self->loc('Permission Denied') ); + } + + return 1; +} + +=head2 SetStatus + +Validates the status transition before updating the Status column. This method +may want to be overridden by a more specific method in the consuming class. + +=cut + +sub SetStatus { + my $self = shift; + my $new = shift; + + my ($valid, $error) = $self->ValidateStatusChange($new); + return ($valid, $error) unless $valid; + + return $self->_SetStatus( Status => $new ); +} + +=head2 _SetStatus + +Sets the Status column without validating the change. Intended to be used +as-is by methods provided by the role, or overridden in the consuming class to +take additional action. For example, L<RT::Ticket/_SetStatus> sets the Started +and Resolved dates on the ticket as necessary. + +Takes a paramhash where the only required key is Status. Other keys may +include Lifecycle and NewLifecycle when called from L</_SetLifecycleColumn>, +which may assist consuming classes. NewLifecycle defaults to Lifecycle if not +provided; this indicates the lifecycle isn't changing. + +=cut + +sub _SetStatus { + my $self = shift; + my %args = ( + Status => undef, + Lifecycle => $self->LifecycleObj, + @_, + ); + $args{Status} = lc $args{Status} if defined $args{Status}; + $args{NewLifecycle} ||= $args{Lifecycle}; + + return $self->_Set( + Field => 'Status', + Value => $args{Status}, + ); +} + +=head2 _SetLifecycleColumn + +Validates and updates the column named by L</LifecycleColumn>. The Status +column is also updated if necessary (via lifecycle transition maps). + +On success, returns a tuple of (1, I<message>, I<new status>) where I<new +status> is the status that was transitioned to, if any. On failure, returns +(0, I<error message>). + +Takes a paramhash with keys I<Value> and (optionally) I<RequireRight>. +I<RequireRight> is a right name which the current user must have on the new +L</LifecycleColumn> object in order for the method to succeed. + +This method is expected to be used from within another method such as +L<RT::Ticket/SetQueue>. + +=cut + +sub _SetLifecycleColumn { + my $self = shift; + my %args = @_; + + my $column = $self->LifecycleColumn; + my $column_obj = "${column}Obj"; + + my $current = $self->$column_obj; + my $class = blessed($current); + + my $new = $class->new( $self->CurrentUser ); + $new->Load($args{Value}); + + return (0, $self->loc("[_1] [_2] does not exist", $self->loc($column), $args{Value})) + unless $new->id; + + my $name = eval { $current->Name } || $current->id; + + return (0, $self->loc("[_1] [_2] is disabled", $self->loc($column), $name)) + if $new->Disabled; + + return (0, $self->loc("[_1] is already set to [_2]", $self->loc($column), $name)) + if $new->id == $current->id; + + return (0, $self->loc("Permission Denied")) + if $args{RequireRight} and not $self->CurrentUser->HasRight( + Right => $args{RequireRight}, + Object => $new, + ); + + my $new_status; + my $old_lifecycle = $current->LifecycleObj; + my $new_lifecycle = $new->LifecycleObj; + if ( $old_lifecycle->Name ne $new_lifecycle->Name ) { + unless ( $old_lifecycle->HasMoveMap( $new_lifecycle ) ) { + return ( 0, $self->loc("There is no mapping for statuses between lifecycle [_1] and [_2]. Contact your system administrator.", $old_lifecycle->Name, $new_lifecycle->Name) ); + } + $new_status = $old_lifecycle->MoveMap( $new_lifecycle )->{ lc $self->Status }; + return ( 0, $self->loc("Mapping between lifecycle [_1] and [_2] is incomplete. Contact your system administrator.", $old_lifecycle->Name, $new_lifecycle->Name) ) + unless $new_status; + } + + my ($ok, $msg) = $self->_Set( Field => $column, Value => $new->id ); + if ($ok) { + if ( $new_status and $new_status ne $self->Status ) { + my $as_system = blessed($self)->new( RT->SystemUser ); + $as_system->Load( $self->Id ); + unless ( $as_system->Id ) { + return ( 0, $self->loc("Couldn't load copy of [_1] #[_2]", blessed($self), $self->Id) ); + } + + my ($val, $msg) = $as_system->_SetStatus( + Lifecycle => $old_lifecycle, + NewLifecycle => $new_lifecycle, + Status => $new_status, + ); + + if ($val) { + # Pick up the change made by the clone above + $self->Load( $self->id ); + } else { + RT->Logger->error("Status change to $new_status failed on $column change: $msg"); + undef $new_status; + } + } + return (1, $msg, $new_status); + } else { + return (0, $msg); + } +} + +1; |