1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2017 Best Practical Solutions, LLC
6 # <sales@bestpractical.com>
8 # (Except where explicitly superseded by other copyright notices)
13 # This work is made available to you under the terms of Version 2 of
14 # the GNU General Public License. A copy of that license should have
15 # been provided with this software, but in any event can be snarfed
18 # This work is distributed in the hope that it will be useful, but
19 # WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
21 # General Public License for more details.
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
26 # 02110-1301 or visit their web page on the internet at
27 # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
30 # CONTRIBUTION SUBMISSION POLICY:
32 # (The following paragraph is not intended to limit the rights granted
33 # to you to modify and distribute this software under the terms of
34 # the GNU General Public License and is only of importance to you if
35 # you choose to contribute your changes and enhancements to the
36 # community by submitting them to Best Practical Solutions, LLC.)
38 # By intentionally submitting any modifications, corrections or
39 # derivatives to this work, or any other work intended for use with
40 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
41 # you are the copyright holder for those contributions and you grant
42 # Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
43 # royalty-free, perpetual, license to use, copy, create derivative
44 # works based on those contributions, and sublicense and distribute
45 # those contributions and any derivatives thereof.
47 # END BPS TAGGED BLOCK }}}
49 package RT::Attribute;
54 use base 'RT::Record';
56 sub Table {'Attributes'}
58 use Storable qw/nfreeze thaw/;
70 # 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
73 SavedSearch => { create => 'EditSavedSearches',
74 update => 'EditSavedSearches',
75 delete => 'EditSavedSearches',
76 display => 'ShowSavedSearches' },
80 # There are a number of attributes that users should be able to modify for themselves, such as saved searches
81 # we could do this with a different set of "update" rights, but that gets very hacky very fast. this is even faster and even
82 # hackier. we're hardcoding that a different set of rights are needed for attributes on oneself
83 our $PERSONAL_ACL_MAP = {
84 SavedSearch => { create => 'ModifySelf',
85 update => 'ModifySelf',
86 delete => 'ModifySelf',
91 =head2 LookupObjectRight { ObjectType => undef, ObjectId => undef, Name => undef, Right => { create, update, delete, display } }
93 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.
97 sub LookupObjectRight {
99 my %args = ( ObjectType => undef,
105 # if it's an attribute on oneself, check the personal acl map
106 if (($args{'ObjectType'} eq 'RT::User') && ($args{'ObjectId'} eq $self->CurrentUser->Id)) {
107 return('allow') unless ($PERSONAL_ACL_MAP->{$args{'Name'}});
108 return('allow') unless ($PERSONAL_ACL_MAP->{$args{'Name'}}->{$args{'Right'}});
109 return($PERSONAL_ACL_MAP->{$args{'Name'}}->{$args{'Right'}});
112 # otherwise check the main ACL map
114 return('allow') unless ($ACL_MAP->{$args{'Name'}});
115 return('allow') unless ($ACL_MAP->{$args{'Name'}}->{$args{'Right'}});
116 return($ACL_MAP->{$args{'Name'}}->{$args{'Right'}});
123 =head2 Create PARAMHASH
125 Create takes a hash of values and creates a row in the database:
128 varchar(255) 'Content'.
129 varchar(16) 'ContentType',
130 varchar(64) 'ObjectType'.
133 You may pass a C<Object> instead of C<ObjectType> and C<ObjectId>.
150 if ($args{Object} and UNIVERSAL::can($args{Object}, 'Id')) {
151 $args{ObjectType} = $args{Object}->isa("RT::CurrentUser") ? "RT::User" : ref($args{Object});
152 $args{ObjectId} = $args{Object}->Id;
154 return(0, $self->loc("Required parameter '[_1]' not specified", 'Object'));
158 # object_right is the right that the user has to have on the object for them to have $right on this attribute
159 my $object_right = $self->LookupObjectRight(
161 ObjectId => $args{'ObjectId'},
162 ObjectType => $args{'ObjectType'},
163 Name => $args{'Name'}
165 if ($object_right eq 'deny') {
166 return (0, $self->loc('Permission Denied'));
168 elsif ($object_right eq 'allow') {
169 # do nothing, we're ok
171 elsif (!$self->CurrentUser->HasRight( Object => $args{Object}, Right => $object_right)) {
172 return (0, $self->loc('Permission Denied'));
176 if (ref ($args{'Content'}) ) {
177 eval {$args{'Content'} = $self->_SerializeContent($args{'Content'}); };
181 $args{'ContentType'} = 'storable';
184 $self->SUPER::Create(
185 Name => $args{'Name'},
186 Content => $args{'Content'},
187 ContentType => $args{'ContentType'},
188 Description => $args{'Description'},
189 ObjectType => $args{'ObjectType'},
190 ObjectId => $args{'ObjectId'},
197 =head2 LoadByNameAndObject (Object => OBJECT, Name => NAME)
199 Loads the Attribute named NAME for Object OBJECT.
203 sub LoadByNameAndObject {
213 Name => $args{'Name'},
214 ObjectType => ref($args{'Object'}),
215 ObjectId => $args{'Object'}->Id,
223 =head2 _DeserializeContent
225 DeserializeContent returns this Attribute's "Content" as a hashref.
230 sub _DeserializeContent {
235 eval {$hashref = thaw(decode_base64($content))} ;
237 $RT::Logger->error("Deserialization of attribute ".$self->Id. " failed");
247 Returns this attribute's content. If it's a scalar, returns a scalar
248 If it's data structure returns a ref to that data structure.
254 # Here we call _Value to get the ACL check.
255 my $content = $self->_Value('Content');
256 if ( ($self->__Value('ContentType') || '') eq 'storable') {
257 eval {$content = $self->_DeserializeContent($content); };
259 $RT::Logger->error("Deserialization of content for attribute ".$self->Id. " failed. Attribute was: ".$content);
267 sub _SerializeContent {
270 return( encode_base64(nfreeze($content)));
278 # Call __Value to avoid ACL check.
279 if ( ($self->__Value('ContentType')||'') eq 'storable' ) {
280 # We eval the serialization because it will lose on a coderef.
281 $content = eval { $self->_SerializeContent($content) };
283 $RT::Logger->error("Content couldn't be frozen: $@");
284 return(0, "Content couldn't be frozen");
287 my ($ok, $msg) = $self->_Set( Field => 'Content', Value => $content );
288 return ($ok, $self->loc("Attribute updated")) if $ok;
294 Returns the subvalue for $key.
302 my $values = $self->Content();
303 return undef unless ref($values);
304 return($values->{$key});
307 =head2 DeleteSubValue NAME
309 Deletes the subvalue with the key NAME
316 my $values = $self->Content();
317 delete $values->{$key};
318 $self->SetContent($values);
322 =head2 DeleteAllSubValues
324 Deletes all subvalues for this attribute
329 sub DeleteAllSubValues {
331 $self->SetContent({});
334 =head2 SetSubValues { }
336 Takes a hash of keys and values and stores them in the content of this attribute.
338 Each key B<replaces> the existing key with the same name
340 Returns a tuple of (status, message)
348 my $values = ($self->Content() || {} );
349 foreach my $key (keys %args) {
350 $values->{$key} = $args{$key};
353 $self->SetContent($values);
360 my $object_type = $self->__Value('ObjectType');
362 eval { $object = $object_type->new($self->CurrentUser) };
363 unless(UNIVERSAL::isa($object, $object_type)) {
364 $RT::Logger->error("Attribute ".$self->Id." has a bogus object type - $object_type (".$@.")");
367 $object->Load($self->__Value('ObjectId'));
376 unless ($self->CurrentUserHasRight('delete')) {
377 return (0,$self->loc('Permission Denied'));
380 return($self->SUPER::Delete(@_));
386 unless ($self->CurrentUserHasRight('display')) {
387 return (0,$self->loc('Permission Denied'));
390 return($self->SUPER::_Value(@_));
398 unless ($self->CurrentUserHasRight('update')) {
400 return (0,$self->loc('Permission Denied'));
402 return($self->SUPER::_Set(@_));
407 =head2 CurrentUserHasRight
409 One of "display" "update" "delete" or "create" and returns 1 if the user has that right for attributes of this name for this object.Returns undef otherwise.
413 sub CurrentUserHasRight {
417 # object_right is the right that the user has to have on the object for them to have $right on this attribute
418 my $object_right = $self->LookupObjectRight(
420 ObjectId => $self->__Value('ObjectId'),
421 ObjectType => $self->__Value('ObjectType'),
422 Name => $self->__Value('Name')
425 return (1) if ($object_right eq 'allow');
426 return (0) if ($object_right eq 'deny');
427 return(1) if ($self->CurrentUser->HasRight( Object => $self->Object, Right => $object_right));
435 We should be deserializing the content on load and then enver again, rather than at every access
448 Returns the current value of id.
449 (In the database, id is stored as int(11).)
457 Returns the current value of Name.
458 (In the database, Name is stored as varchar(255).)
466 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
467 (In the database, Name will be stored as a varchar(255).)
475 Returns the current value of Description.
476 (In the database, Description is stored as varchar(255).)
480 =head2 SetDescription VALUE
483 Set Description to VALUE.
484 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
485 (In the database, Description will be stored as a varchar(255).)
493 Returns the current value of Content.
494 (In the database, Content is stored as blob.)
498 =head2 SetContent VALUE
501 Set Content to VALUE.
502 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
503 (In the database, Content will be stored as a blob.)
511 Returns the current value of ContentType.
512 (In the database, ContentType is stored as varchar(16).)
516 =head2 SetContentType VALUE
519 Set ContentType to VALUE.
520 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
521 (In the database, ContentType will be stored as a varchar(16).)
529 Returns the current value of ObjectType.
530 (In the database, ObjectType is stored as varchar(64).)
534 =head2 SetObjectType VALUE
537 Set ObjectType to VALUE.
538 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
539 (In the database, ObjectType will be stored as a varchar(64).)
547 Returns the current value of ObjectId.
548 (In the database, ObjectId is stored as int(11).)
552 =head2 SetObjectId VALUE
555 Set ObjectId to VALUE.
556 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
557 (In the database, ObjectId will be stored as a int(11).)
565 Returns the current value of Creator.
566 (In the database, Creator is stored as int(11).)
574 Returns the current value of Created.
575 (In the database, Created is stored as datetime.)
583 Returns the current value of LastUpdatedBy.
584 (In the database, LastUpdatedBy is stored as int(11).)
592 Returns the current value of LastUpdated.
593 (In the database, LastUpdated is stored as datetime.)
600 sub _CoreAccessible {
604 {read => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''},
606 {read => 1, write => 1, sql_type => 12, length => 255, is_blob => 0, is_numeric => 0, type => 'varchar(255)', default => ''},
608 {read => 1, write => 1, sql_type => 12, length => 255, is_blob => 0, is_numeric => 0, type => 'varchar(255)', default => ''},
610 {read => 1, write => 1, sql_type => -4, length => 0, is_blob => 1, is_numeric => 0, type => 'blob', default => ''},
612 {read => 1, write => 1, sql_type => 12, length => 16, is_blob => 0, is_numeric => 0, type => 'varchar(16)', default => ''},
614 {read => 1, write => 1, sql_type => 12, length => 64, is_blob => 0, is_numeric => 0, type => 'varchar(64)', default => ''},
616 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''},
618 {read => 1, auto => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
620 {read => 1, auto => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
622 {read => 1, auto => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
624 {read => 1, auto => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
629 sub FindDependencies {
631 my ($walker, $deps) = @_;
633 $self->SUPER::FindDependencies($walker, $deps);
634 $deps->Add( out => $self->Object );
636 # dashboards in menu attribute has dependencies on each of its dashboards
637 if ($self->Name eq RT::User::_PrefName("DashboardsInMenu")) {
638 my $content = $self->Content;
639 for my $pane (values %{ $content || {} }) {
640 for my $dash_id (@$pane) {
641 my $attr = RT::Attribute->new($self->CurrentUser);
642 $attr->LoadById($dash_id);
643 $deps->Add( out => $attr );
647 # homepage settings attribute has dependencies on each of the searches in it
648 elsif ($self->Name eq RT::User::_PrefName("HomepageSettings")) {
649 my $content = $self->Content;
650 for my $pane (values %{ $content || {} }) {
651 for my $component (@$pane) {
652 # this hairy code mirrors what's in the saved search loader
653 # in /Elements/ShowSearch
654 if ($component->{type} eq 'saved') {
655 if ($component->{name} =~ /^(.*?)-(\d+)-SavedSearch-(\d+)$/) {
656 my $attr = RT::Attribute->new($self->CurrentUser);
658 $deps->Add( out => $attr );
661 elsif ($component->{type} eq 'system') {
662 my ($search) = RT::System->new($self->CurrentUser)->Attributes->Named( 'Search - ' . $component->{name} );
663 unless ( $search && $search->Id ) {
664 my (@custom_searches) = RT::System->new($self->CurrentUser)->Attributes->Named('SavedSearch');
665 foreach my $custom (@custom_searches) {
666 if ($custom->Description eq $component->{name}) { $search = $custom; last }
669 $deps->Add( out => $search ) if $search;
674 # dashboards have dependencies on all the searches and dashboards they use
675 elsif ($self->Name eq 'Dashboard') {
676 my $content = $self->Content;
677 for my $pane (values %{ $content->{Panes} || {} }) {
678 for my $component (@$pane) {
679 if ($component->{portlet_type} eq 'search' || $component->{portlet_type} eq 'dashboard') {
680 my $attr = RT::Attribute->new($self->CurrentUser);
681 $attr->LoadById($component->{id});
682 $deps->Add( out => $attr );
687 # each subscription depends on its dashboard
688 elsif ($self->Name eq 'Subscription') {
689 my $content = $self->Content;
690 my $attr = RT::Attribute->new($self->CurrentUser);
691 $attr->LoadById($content->{DashboardId});
692 $deps->Add( out => $attr );
698 my ($importer, $uid, $data) = @_;
700 if ($data->{Object} and ref $data->{Object}) {
701 my $on_uid = ${ $data->{Object} };
703 # skip attributes of objects we're not inflating
704 # exception: we don't inflate RT->System, but we want RT->System's searches
705 unless ($on_uid eq RT->System->UID && $data->{Name} =~ /Search/) {
706 return if $importer->ShouldSkipTransaction($on_uid);
710 return $class->SUPER::PreInflate( $importer, $uid, $data );
713 # this method will be called repeatedly to fix up this attribute's contents
714 # (a list of searches, dashboards) during the import process, as the
715 # ordinary dependency resolution system can't quite handle the subtlety
716 # involved (e.g. a user simply declares out-dependencies on all of her
717 # attributes, but those attributes (e.g. dashboards, saved searches,
718 # dashboards in menu preferences) have dependencies amongst themselves).
719 # if this attribute (e.g. a user's dashboard) fails to load an attribute
720 # (e.g. a user's saved search) then it postpones and repeats the postinflate
721 # process again when that user's saved search has been imported
722 # this method updates Content each time through, each time getting closer and
723 # closer to the fully inflated attribute
724 sub PostInflateFixup {
726 my $importer = shift;
729 # decode UIDs to be raw dashboard IDs
730 if ($self->Name eq RT::User::_PrefName("DashboardsInMenu")) {
731 my $content = $self->Content;
733 for my $pane (values %{ $content || {} }) {
735 if (ref($_) eq 'SCALAR') {
736 my $attr = $importer->LookupObj($$_);
744 method => 'PostInflateFixup',
750 $self->SetContent($content);
752 # decode UIDs to be saved searches
753 elsif ($self->Name eq RT::User::_PrefName("HomepageSettings")) {
754 my $content = $self->Content;
756 for my $pane (values %{ $content || {} }) {
758 if (ref($_->{uid}) eq 'SCALAR') {
760 my $attr = $importer->LookupObj($$uid);
763 if ($_->{type} eq 'saved') {
764 $_->{name} = join '-', $attr->ObjectType, $attr->ObjectId, 'SavedSearch', $attr->id;
766 # if type is system, name doesn't need to change
767 # if type is anything else, pass it through as is
774 method => 'PostInflateFixup',
780 $self->SetContent($content);
782 elsif ($self->Name eq 'Dashboard') {
783 my $content = $self->Content;
785 for my $pane (values %{ $content->{Panes} || {} }) {
787 if (ref($_->{uid}) eq 'SCALAR') {
789 my $attr = $importer->LookupObj($$uid);
792 # update with the new id numbers assigned to us
793 $_->{id} = $attr->Id;
794 $_->{privacy} = join '-', $attr->ObjectType, $attr->ObjectId;
801 method => 'PostInflateFixup',
807 $self->SetContent($content);
809 elsif ($self->Name eq 'Subscription') {
810 my $content = $self->Content;
811 if (ref($content->{DashboardId}) eq 'SCALAR') {
812 my $attr = $importer->LookupObj(${ $content->{DashboardId} });
814 $content->{DashboardId} = $attr->Id;
818 for => ${ $content->{DashboardId} },
820 method => 'PostInflateFixup',
824 $self->SetContent($content);
830 my ($importer, $uid) = @_;
832 $self->SUPER::PostInflate( $importer, $uid );
834 # this method is separate because it needs to be callable multple times,
835 # and we can't guarantee that SUPER::PostInflate can deal with that
836 $self->PostInflateFixup($importer, { uid => $uid });
842 my %store = $self->SUPER::Serialize(@_);
844 # encode raw dashboard IDs to be UIDs
845 if ($store{Name} eq RT::User::_PrefName("DashboardsInMenu")) {
846 my $content = $self->_DeserializeContent($store{Content});
847 for my $pane (values %{ $content || {} }) {
849 my $attr = RT::Attribute->new($self->CurrentUser);
854 $store{Content} = $self->_SerializeContent($content);
856 # encode saved searches to be UIDs
857 elsif ($store{Name} eq RT::User::_PrefName("HomepageSettings")) {
858 my $content = $self->_DeserializeContent($store{Content});
859 for my $pane (values %{ $content || {} }) {
861 # this hairy code mirrors what's in the saved search loader
862 # in /Elements/ShowSearch
863 if ($_->{type} eq 'saved') {
864 if ($_->{name} =~ /^(.*?)-(\d+)-SavedSearch-(\d+)$/) {
865 my $attr = RT::Attribute->new($self->CurrentUser);
867 $_->{uid} = \($attr->UID);
869 # if we can't parse the name, just pass it through
871 elsif ($_->{type} eq 'system') {
872 my ($search) = RT::System->new($self->CurrentUser)->Attributes->Named( 'Search - ' . $_->{name} );
873 unless ( $search && $search->Id ) {
874 my (@custom_searches) = RT::System->new($self->CurrentUser)->Attributes->Named('SavedSearch');
875 foreach my $custom (@custom_searches) {
876 if ($custom->Description eq $_->{name}) { $search = $custom; last }
879 # if we can't load the search, just pass it through
881 $_->{uid} = \($search->UID);
884 # pass through everything else (e.g. component)
887 $store{Content} = $self->_SerializeContent($content);
889 # encode saved searches and dashboards to be UIDs
890 elsif ($store{Name} eq 'Dashboard') {
891 my $content = $self->_DeserializeContent($store{Content}) || {};
892 for my $pane (values %{ $content->{Panes} || {} }) {
894 if ($_->{portlet_type} eq 'search' || $_->{portlet_type} eq 'dashboard') {
895 my $attr = RT::Attribute->new($self->CurrentUser);
896 $attr->LoadById($_->{id});
897 $_->{uid} = \($attr->UID);
899 # pass through everything else (e.g. component)
902 $store{Content} = $self->_SerializeContent($content);
904 # encode subscriptions to have dashboard UID
905 elsif ($store{Name} eq 'Subscription') {
906 my $content = $self->_DeserializeContent($store{Content});
907 my $attr = RT::Attribute->new($self->CurrentUser);
908 $attr->LoadById($content->{DashboardId});
909 $content->{DashboardId} = \($attr->UID);
910 $store{Content} = $self->_SerializeContent($content);
916 RT::Base->_ImportOverlays();