rt 4.2.16
[freeside.git] / rt / lib / RT / Attribute.pm
index a46f4d8..e8d1b47 100644 (file)
@@ -1,38 +1,40 @@
 # BEGIN BPS TAGGED BLOCK {{{
-# 
+#
 # COPYRIGHT:
-#  
-# This software is Copyright (c) 1996-2005 Best Practical Solutions, LLC 
-#                                          <jesse@bestpractical.com>
-# 
+#
+# This software is Copyright (c) 1996-2019 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., 675 Mass Ave, Cambridge, MA 02139, USA.
-# 
-# 
+# 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
 # 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 }}}
-# Autogenerated by DBIx::SearchBuilder factory (by <jesse@bestpractical.com>)
-# WARNING: THIS FILE IS AUTOGENERATED. ALL CHANGES TO THIS FILE WILL BE LOST.  
-# 
-# !! DO NOT EDIT THIS FILE !!
 #
+# END BPS TAGGED BLOCK }}}
+
+package RT::Attribute;
 
 use strict;
+use warnings;
 
+use base 'RT::Record';
 
-=head1 NAME
+sub Table {'Attributes'}
 
-RT::Attribute
+use Storable qw/nfreeze thaw/;
+use MIME::Base64;
 
 
-=head1 SYNOPSIS
+=head1 NAME
 
-=head1 DESCRIPTION
+  RT::Attribute_Overlay 
 
-=head1 METHODS
+=head1 Content
 
 =cut
 
-package RT::Attribute;
-use RT::Record; 
+# 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' },
 
-use vars qw( @ISA );
-@ISA= qw( RT::Record );
+};
 
-sub _Init {
-  my $self = shift; 
+# 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 "update" 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' },
 
-  $self->Table('Attributes');
-  $self->SUPER::_Init(@_);
-}
+};
+
+=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'}}); 
+    }
+}
 
 
 
@@ -87,13 +124,14 @@ sub _Init {
 
 Create takes a hash of values and creates a row in the database:
 
-  varchar(255) 'Name'.
-  varchar(255) 'Description'.
-  text 'Content'.
-  varchar(16) 'ContentType'.
+  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
 
 
@@ -106,15 +144,48 @@ sub Create {
                 Description => '',
                 Content => '',
                 ContentType => '',
-                ObjectType => '',
-                ObjectId => '',
+                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;
+    } 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'},
-                         Description => $args{'Description'},
                          Content => $args{'Content'},
                          ContentType => $args{'ContentType'},
+                         Description => $args{'Description'},
                          ObjectType => $args{'ObjectType'},
                          ObjectId => $args{'ObjectId'},
 );
@@ -123,9 +194,258 @@ sub Create {
 
 
 
+=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.
+        $content = eval { $self->_SerializeContent($content) };
+        if ($@) {
+            $RT::Logger->error("Content couldn't be frozen: $@");
+            return(0, "Content couldn't be frozen");
+        }
+    }
+    my ($ok, $msg) = $self->_Set( Field => 'Content', Value => $content );
+    return ($ok, $self->loc("Attribute updated")) if $ok;
+    return ($ok, $msg);
+}
+
+=head2 SubValue KEY
+
+Returns the subvalue for $key.
+
+
+=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('update')) {
+
+        return (0,$self->loc('Permission Denied'));
+    }
+    return($self->SUPER::_Set(@_));
+
+}
+
+
+=head2 CurrentUserHasRight
+
+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.
+
+=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 never again, rather than at every access
+
+=cut
+
+
+
+
+
+
+
+
 =head2 id
 
-Returns the current value of id. 
+Returns the current value of id.
 (In the database, id is stored as int(11).)
 
 
@@ -134,7 +454,7 @@ Returns the current value of id.
 
 =head2 Name
 
-Returns the current value of Name. 
+Returns the current value of Name.
 (In the database, Name is stored as varchar(255).)
 
 
@@ -142,7 +462,7 @@ Returns the current value of Name.
 =head2 SetName VALUE
 
 
-Set Name to VALUE. 
+Set Name to VALUE.
 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
 (In the database, Name will be stored as a varchar(255).)
 
@@ -152,7 +472,7 @@ Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
 
 =head2 Description
 
-Returns the current value of Description. 
+Returns the current value of Description.
 (In the database, Description is stored as varchar(255).)
 
 
@@ -160,7 +480,7 @@ Returns the current value of Description.
 =head2 SetDescription VALUE
 
 
-Set Description to VALUE. 
+Set Description to VALUE.
 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
 (In the database, Description will be stored as a varchar(255).)
 
@@ -170,17 +490,17 @@ Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
 
 =head2 Content
 
-Returns the current value of Content. 
-(In the database, Content is stored as text.)
+Returns the current value of Content.
+(In the database, Content is stored as blob.)
 
 
 
 =head2 SetContent VALUE
 
 
-Set Content to VALUE. 
+Set Content to VALUE.
 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
-(In the database, Content will be stored as a text.)
+(In the database, Content will be stored as a blob.)
 
 
 =cut
@@ -188,7 +508,7 @@ Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
 
 =head2 ContentType
 
-Returns the current value of ContentType. 
+Returns the current value of ContentType.
 (In the database, ContentType is stored as varchar(16).)
 
 
@@ -196,7 +516,7 @@ Returns the current value of ContentType.
 =head2 SetContentType VALUE
 
 
-Set ContentType to VALUE. 
+Set ContentType to VALUE.
 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
 (In the database, ContentType will be stored as a varchar(16).)
 
@@ -206,7 +526,7 @@ Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
 
 =head2 ObjectType
 
-Returns the current value of ObjectType. 
+Returns the current value of ObjectType.
 (In the database, ObjectType is stored as varchar(64).)
 
 
@@ -214,7 +534,7 @@ Returns the current value of ObjectType.
 =head2 SetObjectType VALUE
 
 
-Set ObjectType to VALUE. 
+Set ObjectType to VALUE.
 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
 (In the database, ObjectType will be stored as a varchar(64).)
 
@@ -224,7 +544,7 @@ Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
 
 =head2 ObjectId
 
-Returns the current value of ObjectId. 
+Returns the current value of ObjectId.
 (In the database, ObjectId is stored as int(11).)
 
 
@@ -232,7 +552,7 @@ Returns the current value of ObjectId.
 =head2 SetObjectId VALUE
 
 
-Set ObjectId to VALUE. 
+Set ObjectId to VALUE.
 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
 (In the database, ObjectId will be stored as a int(11).)
 
@@ -242,7 +562,7 @@ Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
 
 =head2 Creator
 
-Returns the current value of Creator. 
+Returns the current value of Creator.
 (In the database, Creator is stored as int(11).)
 
 
@@ -251,7 +571,7 @@ Returns the current value of Creator.
 
 =head2 Created
 
-Returns the current value of Created. 
+Returns the current value of Created.
 (In the database, Created is stored as datetime.)
 
 
@@ -260,7 +580,7 @@ Returns the current value of Created.
 
 =head2 LastUpdatedBy
 
-Returns the current value of LastUpdatedBy. 
+Returns the current value of LastUpdatedBy.
 (In the database, LastUpdatedBy is stored as int(11).)
 
 
@@ -269,7 +589,7 @@ Returns the current value of LastUpdatedBy.
 
 =head2 LastUpdated
 
-Returns the current value of LastUpdated. 
+Returns the current value of LastUpdated.
 (In the database, LastUpdated is stored as datetime.)
 
 
@@ -279,69 +599,320 @@ Returns the current value of LastUpdated.
 
 sub _CoreAccessible {
     {
-     
+
         id =>
-               {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 => ''},
-        Description => 
-               {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 => 'text', default => ''},
-        ContentType => 
-               {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 => ''},
-        ObjectId => 
-               {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'},
-        Created => 
-               {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'},
-        LastUpdated => 
-               {read => 1, auto => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', 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 => ''},
+        Description =>
+                {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 => ''},
+        ContentType =>
+                {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 => ''},
+        ObjectId =>
+                {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'},
+        Created =>
+                {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'},
+        LastUpdated =>
+                {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 );
+    }
+}
 
-        eval "require RT::Attribute_Overlay";
-        if ($@ && $@ !~ qr{^Can't locate RT/Attribute_Overlay.pm}) {
-            die $@;
-        };
-
-        eval "require RT::Attribute_Vendor";
-        if ($@ && $@ !~ qr{^Can't locate RT/Attribute_Vendor.pm}) {
-            die $@;
-        };
-
-        eval "require RT::Attribute_Local";
-        if ($@ && $@ !~ qr{^Can't locate RT/Attribute_Local.pm}) {
-            die $@;
-        };
-
-
-
+sub PreInflate {
+    my $class = shift;
+    my ($importer, $uid, $data) = @_;
 
-=head1 SEE ALSO
+    if ($data->{Object} and ref $data->{Object}) {
+        my $on_uid = ${ $data->{Object} };
 
-This class allows "overlay" methods to be placed
-into the following files _Overlay is for a System overlay by the original author,
-_Vendor is for 3rd-party vendor add-ons, while _Local is for site-local customizations.  
+        # 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);
+        }
+    }
 
-These overlay files can contain new subs or subs to replace existing subs in this module.
+    return $class->SUPER::PreInflate( $importer, $uid, $data );
+}
 
-Each of these files should begin with the line 
+# 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);
+    }
+}
 
-   no warnings qw(redefine);
+sub PostInflate {
+    my $self = shift;
+    my ($importer, $uid) = @_;
 
-so that perl does not kick and scream when you redefine a subroutine or variable in your overlay.
+    $self->SUPER::PostInflate( $importer, $uid );
 
-RT::Attribute_Overlay, RT::Attribute_Vendor, RT::Attribute_Local
+    # 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 });
+}
 
-=cut
+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;