X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=blobdiff_plain;f=rt%2Flib%2FRT%2FAttribute.pm;h=0850894b1430e00aa5afa243f6158e342a9e069f;hp=24c89dd3861fc9c08554264b5482795d133d217d;hb=187086c479a09629b7d180eec513fb7657f4e291;hpb=73a6a80a9ca5edbd43d139b7cb25bfee4abfd35e diff --git a/rt/lib/RT/Attribute.pm b/rt/lib/RT/Attribute.pm index 24c89dd38..0850894b1 100644 --- a/rt/lib/RT/Attribute.pm +++ b/rt/lib/RT/Attribute.pm @@ -2,7 +2,7 @@ # # COPYRIGHT: # -# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC +# This software is Copyright (c) 1996-2018 Best Practical Solutions, LLC # # # (Except where explicitly superseded by other copyright notices) @@ -145,11 +145,11 @@ sub Create { Content => '', ContentType => '', Object => undef, - @_); + @_); if ($args{Object} and UNIVERSAL::can($args{Object}, 'Id')) { - $args{ObjectType} = $args{Object}->isa("RT::CurrentUser") ? "RT::User" : ref($args{Object}); - $args{ObjectId} = $args{Object}->Id; + $args{ObjectType} = $args{Object}->isa("RT::CurrentUser") ? "RT::User" : ref($args{Object}); + $args{ObjectId} = $args{Object}->Id; } else { return(0, $self->loc("Required parameter '[_1]' not specified", 'Object')); @@ -181,7 +181,6 @@ sub Create { $args{'ContentType'} = 'storable'; } - $self->SUPER::Create( Name => $args{'Name'}, Content => $args{'Content'}, @@ -210,11 +209,11 @@ sub LoadByNameAndObject { ); return ( - $self->LoadByCols( - Name => $args{'Name'}, - ObjectType => ref($args{'Object'}), - ObjectId => $args{'Object'}->Id, - ) + $self->LoadByCols( + Name => $args{'Name'}, + ObjectType => ref($args{'Object'}), + ObjectId => $args{'Object'}->Id, + ) ); } @@ -285,7 +284,9 @@ sub SetContent { return(0, "Content couldn't be frozen"); } } - return $self->_Set( Field => 'Content', Value => $content ); + my ($ok, $msg) = $self->_Set( Field => 'Content', Value => $content ); + return ($ok, $self->loc("Attribute updated")) if $ok; + return ($ok, $msg); } =head2 SubValue KEY @@ -375,6 +376,7 @@ sub Delete { unless ($self->CurrentUserHasRight('delete')) { return (0,$self->loc('Permission Denied')); } + return($self->SUPER::Delete(@_)); } @@ -430,7 +432,7 @@ sub CurrentUserHasRight { =head1 TODO -We should be deserializing the content on load and then enver again, rather than at every access +We should be deserializing the content on load and then never again, rather than at every access =cut @@ -599,31 +601,318 @@ sub _CoreAccessible { { id => - {read => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''}, + {read => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''}, Name => - {read => 1, write => 1, sql_type => 12, length => 255, is_blob => 0, is_numeric => 0, type => 'varchar(255)', default => ''}, + {read => 1, write => 1, sql_type => 12, length => 255, is_blob => 0, is_numeric => 0, type => 'varchar(255)', default => ''}, Description => - {read => 1, write => 1, sql_type => 12, length => 255, is_blob => 0, is_numeric => 0, type => 'varchar(255)', default => ''}, + {read => 1, write => 1, sql_type => 12, length => 255, is_blob => 0, is_numeric => 0, type => 'varchar(255)', default => ''}, Content => - {read => 1, write => 1, sql_type => -4, length => 0, is_blob => 1, is_numeric => 0, type => 'blob', default => ''}, + {read => 1, write => 1, sql_type => -4, length => 0, is_blob => 1, is_numeric => 0, type => 'blob', default => ''}, ContentType => - {read => 1, write => 1, sql_type => 12, length => 16, is_blob => 0, is_numeric => 0, type => 'varchar(16)', default => ''}, + {read => 1, write => 1, sql_type => 12, length => 16, is_blob => 0, is_numeric => 0, type => 'varchar(16)', default => ''}, ObjectType => - {read => 1, write => 1, sql_type => 12, length => 64, is_blob => 0, is_numeric => 0, type => 'varchar(64)', default => ''}, + {read => 1, write => 1, sql_type => 12, length => 64, is_blob => 0, is_numeric => 0, type => 'varchar(64)', default => ''}, ObjectId => - {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''}, + {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''}, Creator => - {read => 1, auto => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'}, + {read => 1, auto => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'}, Created => - {read => 1, auto => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''}, + {read => 1, auto => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''}, LastUpdatedBy => - {read => 1, auto => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'}, + {read => 1, auto => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'}, LastUpdated => - {read => 1, auto => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''}, + {read => 1, auto => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''}, } }; +sub FindDependencies { + my $self = shift; + my ($walker, $deps) = @_; + + $self->SUPER::FindDependencies($walker, $deps); + $deps->Add( out => $self->Object ); + + # dashboards in menu attribute has dependencies on each of its dashboards + if ($self->Name eq RT::User::_PrefName("DashboardsInMenu")) { + my $content = $self->Content; + for my $pane (values %{ $content || {} }) { + for my $dash_id (@$pane) { + my $attr = RT::Attribute->new($self->CurrentUser); + $attr->LoadById($dash_id); + $deps->Add( out => $attr ); + } + } + } + # homepage settings attribute has dependencies on each of the searches in it + elsif ($self->Name eq RT::User::_PrefName("HomepageSettings")) { + my $content = $self->Content; + for my $pane (values %{ $content || {} }) { + for my $component (@$pane) { + # this hairy code mirrors what's in the saved search loader + # in /Elements/ShowSearch + if ($component->{type} eq 'saved') { + if ($component->{name} =~ /^(.*?)-(\d+)-SavedSearch-(\d+)$/) { + my $attr = RT::Attribute->new($self->CurrentUser); + $attr->LoadById($3); + $deps->Add( out => $attr ); + } + } + elsif ($component->{type} eq 'system') { + my ($search) = RT::System->new($self->CurrentUser)->Attributes->Named( 'Search - ' . $component->{name} ); + unless ( $search && $search->Id ) { + my (@custom_searches) = RT::System->new($self->CurrentUser)->Attributes->Named('SavedSearch'); + foreach my $custom (@custom_searches) { + if ($custom->Description eq $component->{name}) { $search = $custom; last } + } + } + $deps->Add( out => $search ) if $search; + } + } + } + } + # dashboards have dependencies on all the searches and dashboards they use + elsif ($self->Name eq 'Dashboard') { + my $content = $self->Content; + for my $pane (values %{ $content->{Panes} || {} }) { + for my $component (@$pane) { + if ($component->{portlet_type} eq 'search' || $component->{portlet_type} eq 'dashboard') { + my $attr = RT::Attribute->new($self->CurrentUser); + $attr->LoadById($component->{id}); + $deps->Add( out => $attr ); + } + } + } + } + # each subscription depends on its dashboard + elsif ($self->Name eq 'Subscription') { + my $content = $self->Content; + my $attr = RT::Attribute->new($self->CurrentUser); + $attr->LoadById($content->{DashboardId}); + $deps->Add( out => $attr ); + } +} + +sub PreInflate { + my $class = shift; + my ($importer, $uid, $data) = @_; + + if ($data->{Object} and ref $data->{Object}) { + my $on_uid = ${ $data->{Object} }; + + # skip attributes of objects we're not inflating + # exception: we don't inflate RT->System, but we want RT->System's searches + unless ($on_uid eq RT->System->UID && $data->{Name} =~ /Search/) { + return if $importer->ShouldSkipTransaction($on_uid); + } + } + + return $class->SUPER::PreInflate( $importer, $uid, $data ); +} + +# this method will be called repeatedly to fix up this attribute's contents +# (a list of searches, dashboards) during the import process, as the +# ordinary dependency resolution system can't quite handle the subtlety +# involved (e.g. a user simply declares out-dependencies on all of her +# attributes, but those attributes (e.g. dashboards, saved searches, +# dashboards in menu preferences) have dependencies amongst themselves). +# if this attribute (e.g. a user's dashboard) fails to load an attribute +# (e.g. a user's saved search) then it postpones and repeats the postinflate +# process again when that user's saved search has been imported +# this method updates Content each time through, each time getting closer and +# closer to the fully inflated attribute +sub PostInflateFixup { + my $self = shift; + my $importer = shift; + my $spec = shift; + + # decode UIDs to be raw dashboard IDs + if ($self->Name eq RT::User::_PrefName("DashboardsInMenu")) { + my $content = $self->Content; + + for my $pane (values %{ $content || {} }) { + for (@$pane) { + if (ref($_) eq 'SCALAR') { + my $attr = $importer->LookupObj($$_); + if ($attr) { + $_ = $attr->Id; + } + else { + $importer->Postpone( + for => $$_, + uid => $spec->{uid}, + method => 'PostInflateFixup', + ); + } + } + } + } + $self->SetContent($content); + } + # decode UIDs to be saved searches + elsif ($self->Name eq RT::User::_PrefName("HomepageSettings")) { + my $content = $self->Content; + + for my $pane (values %{ $content || {} }) { + for (@$pane) { + if (ref($_->{uid}) eq 'SCALAR') { + my $uid = $_->{uid}; + my $attr = $importer->LookupObj($$uid); + + if ($attr) { + if ($_->{type} eq 'saved') { + $_->{name} = join '-', $attr->ObjectType, $attr->ObjectId, 'SavedSearch', $attr->id; + } + # if type is system, name doesn't need to change + # if type is anything else, pass it through as is + delete $_->{uid}; + } + else { + $importer->Postpone( + for => $$uid, + uid => $spec->{uid}, + method => 'PostInflateFixup', + ); + } + } + } + } + $self->SetContent($content); + } + elsif ($self->Name eq 'Dashboard') { + my $content = $self->Content; + + for my $pane (values %{ $content->{Panes} || {} }) { + for (@$pane) { + if (ref($_->{uid}) eq 'SCALAR') { + my $uid = $_->{uid}; + my $attr = $importer->LookupObj($$uid); + + if ($attr) { + # update with the new id numbers assigned to us + $_->{id} = $attr->Id; + $_->{privacy} = join '-', $attr->ObjectType, $attr->ObjectId; + delete $_->{uid}; + } + else { + $importer->Postpone( + for => $$uid, + uid => $spec->{uid}, + method => 'PostInflateFixup', + ); + } + } + } + } + $self->SetContent($content); + } + elsif ($self->Name eq 'Subscription') { + my $content = $self->Content; + if (ref($content->{DashboardId}) eq 'SCALAR') { + my $attr = $importer->LookupObj(${ $content->{DashboardId} }); + if ($attr) { + $content->{DashboardId} = $attr->Id; + } + else { + $importer->Postpone( + for => ${ $content->{DashboardId} }, + uid => $spec->{uid}, + method => 'PostInflateFixup', + ); + } + } + $self->SetContent($content); + } +} + +sub PostInflate { + my $self = shift; + my ($importer, $uid) = @_; + + $self->SUPER::PostInflate( $importer, $uid ); + + # this method is separate because it needs to be callable multple times, + # and we can't guarantee that SUPER::PostInflate can deal with that + $self->PostInflateFixup($importer, { uid => $uid }); +} + +sub Serialize { + my $self = shift; + my %args = (@_); + my %store = $self->SUPER::Serialize(@_); + + # encode raw dashboard IDs to be UIDs + if ($store{Name} eq RT::User::_PrefName("DashboardsInMenu")) { + my $content = $self->_DeserializeContent($store{Content}); + for my $pane (values %{ $content || {} }) { + for (@$pane) { + my $attr = RT::Attribute->new($self->CurrentUser); + $attr->LoadById($_); + $_ = \($attr->UID); + } + } + $store{Content} = $self->_SerializeContent($content); + } + # encode saved searches to be UIDs + elsif ($store{Name} eq RT::User::_PrefName("HomepageSettings")) { + my $content = $self->_DeserializeContent($store{Content}); + for my $pane (values %{ $content || {} }) { + for (@$pane) { + # this hairy code mirrors what's in the saved search loader + # in /Elements/ShowSearch + if ($_->{type} eq 'saved') { + if ($_->{name} =~ /^(.*?)-(\d+)-SavedSearch-(\d+)$/) { + my $attr = RT::Attribute->new($self->CurrentUser); + $attr->LoadById($3); + $_->{uid} = \($attr->UID); + } + # if we can't parse the name, just pass it through + } + elsif ($_->{type} eq 'system') { + my ($search) = RT::System->new($self->CurrentUser)->Attributes->Named( 'Search - ' . $_->{name} ); + unless ( $search && $search->Id ) { + my (@custom_searches) = RT::System->new($self->CurrentUser)->Attributes->Named('SavedSearch'); + foreach my $custom (@custom_searches) { + if ($custom->Description eq $_->{name}) { $search = $custom; last } + } + } + # if we can't load the search, just pass it through + if ($search) { + $_->{uid} = \($search->UID); + } + } + # pass through everything else (e.g. component) + } + } + $store{Content} = $self->_SerializeContent($content); + } + # encode saved searches and dashboards to be UIDs + elsif ($store{Name} eq 'Dashboard') { + my $content = $self->_DeserializeContent($store{Content}) || {}; + for my $pane (values %{ $content->{Panes} || {} }) { + for (@$pane) { + if ($_->{portlet_type} eq 'search' || $_->{portlet_type} eq 'dashboard') { + my $attr = RT::Attribute->new($self->CurrentUser); + $attr->LoadById($_->{id}); + $_->{uid} = \($attr->UID); + } + # pass through everything else (e.g. component) + } + } + $store{Content} = $self->_SerializeContent($content); + } + # encode subscriptions to have dashboard UID + elsif ($store{Name} eq 'Subscription') { + my $content = $self->_DeserializeContent($store{Content}); + my $attr = RT::Attribute->new($self->CurrentUser); + $attr->LoadById($content->{DashboardId}); + $content->{DashboardId} = \($attr->UID); + $store{Content} = $self->_SerializeContent($content); + } + + return %store; +} + RT::Base->_ImportOverlays(); 1;