--- /dev/null
+# {{{ BEGIN BPS TAGGED BLOCK
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2004 Best Practical Solutions, LLC
+# <jesse@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., 675 Mass Ave, Cambridge, MA 02139, USA.
+#
+#
+# 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;
+no warnings qw(redefine);
+use Storable qw/nfreeze thaw/;
+use MIME::Base64;
+
+
+=head1 NAME
+
+ RT::Attribute_Overlay
+
+=head1 Content
+
+=cut
+
+# the acl map is a map of "name of attribute" and "what right the user must have on the associated object to see/edit it
+
+our $ACL_MAP = {
+ SavedSearch => { create => 'EditSavedSearches',
+ update => 'EditSavedSearches',
+ delete => 'EditSavedSearches',
+ display => 'ShowSavedSearches' },
+
+};
+
+# There are a number of attributes that users should be able to modify for themselves, such as saved searches
+# we could do this with a different set of "modify" rights, but that gets very hacky very fast. this is even faster and even
+# hackier. we're hardcoding that a different set of rights are needed for attributes on oneself
+our $PERSONAL_ACL_MAP = {
+ SavedSearch => { create => 'ModifySelf',
+ update => 'ModifySelf',
+ delete => 'ModifySelf',
+ display => 'allow' },
+
+};
+
+=head2 LookupObjectRight { ObjectType => undef, ObjectId => undef, Name => undef, Right => { create, update, delete, display } }
+
+Returns the right that the user needs to have on this attribute's object to perform the related attribute operation. Returns "allow" if the right is otherwise unspecified.
+
+=cut
+
+sub LookupObjectRight {
+ my $self = shift;
+ my %args = ( ObjectType => undef,
+ ObjectId => undef,
+ Right => undef,
+ Name => undef,
+ @_);
+
+ # if it's an attribute on oneself, check the personal acl map
+ if (($args{'ObjectType'} eq 'RT::User') && ($args{'ObjectId'} eq $self->CurrentUser->Id)) {
+ return('allow') unless ($PERSONAL_ACL_MAP->{$args{'Name'}});
+ return('allow') unless ($PERSONAL_ACL_MAP->{$args{'Name'}}->{$args{'Right'}});
+ return($PERSONAL_ACL_MAP->{$args{'Name'}}->{$args{'Right'}});
+
+ }
+ # otherwise check the main ACL map
+ else {
+ return('allow') unless ($ACL_MAP->{$args{'Name'}});
+ return('allow') unless ($ACL_MAP->{$args{'Name'}}->{$args{'Right'}});
+ return($ACL_MAP->{$args{'Name'}}->{$args{'Right'}});
+ }
+}
+
+
+
+
+=head2 Create PARAMHASH
+
+Create takes a hash of values and creates a row in the database:
+
+ varchar(200) 'Name'.
+ varchar(255) 'Content'.
+ varchar(16) 'ContentType',
+ varchar(64) 'ObjectType'.
+ int(11) 'ObjectId'.
+
+You may pass a C<Object> instead of C<ObjectType> and C<ObjectId>.
+
+=cut
+
+
+
+
+sub Create {
+ my $self = shift;
+ my %args = (
+ Name => '',
+ Description => '',
+ Content => '',
+ ContentType => '',
+ Object => undef,
+ @_);
+
+ if ($args{Object} and UNIVERSAL::can($args{Object}, 'Id')) {
+ $args{ObjectType} = ref($args{Object});
+ $args{ObjectId} = $args{Object}->Id;
+ } else {
+ return(0, $self->loc("Required parameter '[_1]' not specified", 'Object'));
+
+ }
+
+ # object_right is the right that the user has to have on the object for them to have $right on this attribute
+ my $object_right = $self->LookupObjectRight(
+ Right => 'create',
+ ObjectId => $args{'ObjectId'},
+ ObjectType => $args{'ObjectType'},
+ Name => $args{'Name'}
+ );
+ if ($object_right eq 'deny') {
+ return (0, $self->loc('Permission Denied'));
+ }
+ elsif ($object_right eq 'allow') {
+ # do nothing, we're ok
+ }
+ elsif (!$self->CurrentUser->HasRight( Object => $args{Object}, Right => $object_right)) {
+ return (0, $self->loc('Permission Denied'));
+ }
+
+
+ if (ref ($args{'Content'}) ) {
+ eval {$args{'Content'} = $self->_SerializeContent($args{'Content'}); };
+ if ($@) {
+ return(0, $@);
+ }
+ $args{'ContentType'} = 'storable';
+ }
+
+
+ $self->SUPER::Create(
+ Name => $args{'Name'},
+ Content => $args{'Content'},
+ ContentType => $args{'ContentType'},
+ Description => $args{'Description'},
+ ObjectType => $args{'ObjectType'},
+ ObjectId => $args{'ObjectId'},
+);
+
+}
+
+
+# {{{ sub LoadByNameAndObject
+
+=head2 LoadByNameAndObject (Object => OBJECT, Name => NAME)
+
+Loads the Attribute named NAME for Object OBJECT.
+
+=cut
+
+sub LoadByNameAndObject {
+ my $self = shift;
+ my %args = (
+ Object => undef,
+ Name => undef,
+ @_,
+ );
+
+ return (
+ $self->LoadByCols(
+ Name => $args{'Name'},
+ ObjectType => ref($args{'Object'}),
+ ObjectId => $args{'Object'}->Id,
+ )
+ );
+
+}
+
+# }}}
+
+
+=head2 _DeserializeContent
+
+DeserializeContent returns this Attribute's "Content" as a hashref.
+
+
+=cut
+
+sub _DeserializeContent {
+ my $self = shift;
+ my $content = shift;
+
+ my $hashref;
+ eval {$hashref = thaw(decode_base64($content))} ;
+ if ($@) {
+ $RT::Logger->error("Deserialization of attribute ".$self->Id. " failed");
+ }
+
+ return($hashref);
+
+}
+
+
+=head2 Content
+
+Returns this attribute's content. If it's a scalar, returns a scalar
+If it's data structure returns a ref to that data structure.
+
+=cut
+
+sub Content {
+ my $self = shift;
+ # Here we call _Value to get the ACL check.
+ my $content = $self->_Value('Content');
+ if ($self->__Value('ContentType') eq 'storable') {
+ eval {$content = $self->_DeserializeContent($content); };
+ if ($@) {
+ $RT::Logger->error("Deserialization of content for attribute ".$self->Id. " failed. Attribute was: ".$content);
+ }
+ }
+
+ return($content);
+
+}
+
+sub _SerializeContent {
+ my $self = shift;
+ my $content = shift;
+ return( encode_base64(nfreeze($content)));
+}
+
+
+sub SetContent {
+ my $self = shift;
+ my $content = shift;
+
+ # Call __Value to avoid ACL check.
+ if ($self->__Value('ContentType') eq 'storable') {
+ # We eval the serialization because it will lose on a coderef.
+ eval {$content = $self->_SerializeContent($content); };
+ if ($@) {
+ $RT::Logger->error("For some reason, content couldn't be frozen");
+ return(0, $@);
+ }
+ }
+ return ($self->SUPER::SetContent($content));
+}
+
+=head2 SubValue KEY
+
+Returns the subvalue for $key.
+
+=begin testing
+
+my $user = $RT::SystemUser;
+my ($id, $msg) = $user->AddAttribute(Name => 'SavedSearch', Content => { Query => 'Foo'} );
+ok ($id, $msg);
+my $attr = RT::Attribute->new($RT::SystemUser);
+$attr->Load($id);
+ok($attr->Name eq 'SavedSearch');
+$attr->SetSubValues( Format => 'baz');
+
+my $format = $attr->SubValue('Format');
+is ($format , 'baz');
+
+$attr->SetSubValues( Format => 'bar');
+$format = $attr->SubValue('Format');
+is ($format , 'bar');
+
+$attr->DeleteAllSubValues();
+$format = $attr->SubValue('Format');
+is ($format, undef);
+
+$attr->SetSubValues(Format => 'This is a format');
+
+my $attr2 = RT::Attribute->new($RT::SystemUser);
+$attr2->Load($id);
+is ($attr2->SubValue('Format'), 'This is a format');
+
+
+=end testing
+
+=cut
+
+sub SubValue {
+ my $self = shift;
+ my $key = shift;
+ my $values = $self->Content();
+ return undef unless ref($values);
+ return($values->{$key});
+}
+
+=head2 DeleteSubValue NAME
+
+Deletes the subvalue with the key NAME
+
+=cut
+
+sub DeleteSubValue {
+ my $self = shift;
+ my $key = shift;
+ my %values = $self->Content();
+ delete $values{$key};
+ $self->SetContent(%values);
+
+
+
+}
+
+
+=head2 DeleteAllSubValues
+
+Deletes all subvalues for this attribute
+
+=cut
+
+
+sub DeleteAllSubValues {
+ my $self = shift;
+ $self->SetContent({});
+}
+
+=head2 SetSubValues { }
+
+Takes a hash of keys and values and stores them in the content of this attribute.
+
+Each key B<replaces> the existing key with the same name
+
+Returns a tuple of (status, message)
+
+=cut
+
+
+sub SetSubValues {
+ my $self = shift;
+ my %args = (@_);
+ my $values = ($self->Content() || {} );
+ foreach my $key (keys %args) {
+ $values->{$key} = $args{$key};
+ }
+
+ $self->SetContent($values);
+
+}
+
+
+sub Object {
+ my $self = shift;
+ my $object_type = $self->__Value('ObjectType');
+ my $object;
+ eval { $object = $object_type->new($self->CurrentUser) };
+ unless(UNIVERSAL::isa($object, $object_type)) {
+ $RT::Logger->error("Attribute ".$self->Id." has a bogus object type - $object_type (".$@.")");
+ return(undef);
+ }
+ $object->Load($self->__Value('ObjectId'));
+
+ return($object);
+
+}
+
+
+sub Delete {
+ my $self = shift;
+ unless ($self->CurrentUserHasRight('delete')) {
+ return (0,$self->loc('Permission Denied'));
+ }
+ return($self->SUPER::Delete(@_));
+}
+
+
+sub _Value {
+ my $self = shift;
+ unless ($self->CurrentUserHasRight('display')) {
+ return (0,$self->loc('Permission Denied'));
+ }
+
+ return($self->SUPER::_Value(@_));
+
+
+}
+
+
+sub _Set {
+ my $self = shift;
+ unless ($self->CurrentUserHasRight('modify')) {
+
+ return (0,$self->loc('Permission Denied'));
+ }
+ return($self->SUPER::_Set(@_));
+
+}
+
+
+=head2 CurrentUserHasRight
+
+One of "display" "modify" "delete" or "create" and returns 1 if the user has that right for attributes of this name for this object.Returns undef otherwise.
+
+=cut
+
+sub CurrentUserHasRight {
+ my $self = shift;
+ my $right = shift;
+
+ # object_right is the right that the user has to have on the object for them to have $right on this attribute
+ my $object_right = $self->LookupObjectRight(
+ Right => $right,
+ ObjectId => $self->__Value('ObjectId'),
+ ObjectType => $self->__Value('ObjectType'),
+ Name => $self->__Value('Name')
+ );
+
+ return (1) if ($object_right eq 'allow');
+ return (0) if ($object_right eq 'deny');
+ return(1) if ($self->CurrentUser->HasRight( Object => $self->Object, Right => $object_right));
+ return(0);
+
+}
+
+
+=head1 TODO
+
+We should be deserializing the content on load and then enver again, rather than at every access
+
+=cut
+
+
+1;