rt 4.2.16
[freeside.git] / rt / lib / RT / Attribute.pm
index cd0b54e..e8d1b47 100644 (file)
@@ -2,7 +2,7 @@
 #
 # COPYRIGHT:
 #
-# This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC
+# This software is Copyright (c) 1996-2019 Best Practical Solutions, LLC
 #                                          <sales@bestpractical.com>
 #
 # (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} = 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;