rt 4.2.14 (#13852)
[freeside.git] / rt / lib / RT / Attribute.pm
1 # BEGIN BPS TAGGED BLOCK {{{
2 #
3 # COPYRIGHT:
4 #
5 # This software is Copyright (c) 1996-2017 Best Practical Solutions, LLC
6 #                                          <sales@bestpractical.com>
7 #
8 # (Except where explicitly superseded by other copyright notices)
9 #
10 #
11 # LICENSE:
12 #
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
16 # from www.gnu.org.
17 #
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.
22 #
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.
28 #
29 #
30 # CONTRIBUTION SUBMISSION POLICY:
31 #
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.)
37 #
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.
46 #
47 # END BPS TAGGED BLOCK }}}
48
49 package RT::Attribute;
50
51 use strict;
52 use warnings;
53
54 use base 'RT::Record';
55
56 sub Table {'Attributes'}
57
58 use Storable qw/nfreeze thaw/;
59 use MIME::Base64;
60
61
62 =head1 NAME
63
64   RT::Attribute_Overlay 
65
66 =head1 Content
67
68 =cut
69
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
71
72 our $ACL_MAP = {
73     SavedSearch => { create => 'EditSavedSearches',
74                      update => 'EditSavedSearches',
75                      delete => 'EditSavedSearches',
76                      display => 'ShowSavedSearches' },
77
78 };
79
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',
87                      display => 'allow' },
88
89 };
90
91 =head2 LookupObjectRight { ObjectType => undef, ObjectId => undef, Name => undef, Right => { create, update, delete, display } }
92
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.
94
95 =cut
96
97 sub LookupObjectRight { 
98     my $self = shift;
99     my %args = ( ObjectType => undef,
100                  ObjectId => undef,
101                  Right => undef,
102                  Name => undef,
103                  @_);
104
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'}}); 
110
111     }
112    # otherwise check the main ACL map
113     else {
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'}}); 
117     }
118 }
119
120
121
122
123 =head2 Create PARAMHASH
124
125 Create takes a hash of values and creates a row in the database:
126
127   varchar(200) 'Name'.
128   varchar(255) 'Content'.
129   varchar(16) 'ContentType',
130   varchar(64) 'ObjectType'.
131   int(11) 'ObjectId'.
132
133 You may pass a C<Object> instead of C<ObjectType> and C<ObjectId>.
134
135 =cut
136
137
138
139
140 sub Create {
141     my $self = shift;
142     my %args = ( 
143                 Name => '',
144                 Description => '',
145                 Content => '',
146                 ContentType => '',
147                 Object => undef,
148                 @_);
149
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;
153     } else {
154         return(0, $self->loc("Required parameter '[_1]' not specified", 'Object'));
155
156     }
157    
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(
160         Right      => 'create',
161         ObjectId   => $args{'ObjectId'},
162         ObjectType => $args{'ObjectType'},
163         Name       => $args{'Name'}
164     );
165     if ($object_right eq 'deny') { 
166         return (0, $self->loc('Permission Denied'));
167     } 
168     elsif ($object_right eq 'allow') {
169         # do nothing, we're ok
170     }
171     elsif (!$self->CurrentUser->HasRight( Object => $args{Object}, Right => $object_right)) {
172         return (0, $self->loc('Permission Denied'));
173     }
174
175    
176     if (ref ($args{'Content'}) ) { 
177         eval  {$args{'Content'} = $self->_SerializeContent($args{'Content'}); };
178         if ($@) {
179          return(0, $@);
180         }
181         $args{'ContentType'} = 'storable';
182     }
183
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'},
191 );
192
193 }
194
195
196
197 =head2  LoadByNameAndObject (Object => OBJECT, Name => NAME)
198
199 Loads the Attribute named NAME for Object OBJECT.
200
201 =cut
202
203 sub LoadByNameAndObject {
204     my $self = shift;
205     my %args = (
206         Object => undef,
207         Name  => undef,
208         @_,
209     );
210
211     return (
212         $self->LoadByCols(
213             Name => $args{'Name'},
214             ObjectType => ref($args{'Object'}),
215             ObjectId => $args{'Object'}->Id,
216         )
217     );
218
219 }
220
221
222
223 =head2 _DeserializeContent
224
225 DeserializeContent returns this Attribute's "Content" as a hashref.
226
227
228 =cut
229
230 sub _DeserializeContent {
231     my $self = shift;
232     my $content = shift;
233
234     my $hashref;
235     eval {$hashref  = thaw(decode_base64($content))} ; 
236     if ($@) {
237         $RT::Logger->error("Deserialization of attribute ".$self->Id. " failed");
238     }
239
240     return($hashref);
241
242 }
243
244
245 =head2 Content
246
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.
249
250 =cut
251
252 sub Content {
253     my $self = shift;
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); };
258         if ($@) {
259             $RT::Logger->error("Deserialization of content for attribute ".$self->Id. " failed. Attribute was: ".$content);
260         }
261     } 
262
263     return($content);
264
265 }
266
267 sub _SerializeContent {
268     my $self = shift;
269     my $content = shift;
270         return( encode_base64(nfreeze($content))); 
271 }
272
273
274 sub SetContent {
275     my $self = shift;
276     my $content = shift;
277
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) };
282         if ($@) {
283             $RT::Logger->error("Content couldn't be frozen: $@");
284             return(0, "Content couldn't be frozen");
285         }
286     }
287     my ($ok, $msg) = $self->_Set( Field => 'Content', Value => $content );
288     return ($ok, $self->loc("Attribute updated")) if $ok;
289     return ($ok, $msg);
290 }
291
292 =head2 SubValue KEY
293
294 Returns the subvalue for $key.
295
296
297 =cut
298
299 sub SubValue {
300     my $self = shift;
301     my $key = shift;
302     my $values = $self->Content();
303     return undef unless ref($values);
304     return($values->{$key});
305 }
306
307 =head2 DeleteSubValue NAME
308
309 Deletes the subvalue with the key NAME
310
311 =cut
312
313 sub DeleteSubValue {
314     my $self = shift;
315     my $key = shift;
316     my $values = $self->Content();
317     delete $values->{$key};
318     $self->SetContent($values);
319 }
320
321
322 =head2 DeleteAllSubValues 
323
324 Deletes all subvalues for this attribute
325
326 =cut
327
328
329 sub DeleteAllSubValues {
330     my $self = shift; 
331     $self->SetContent({});
332 }
333
334 =head2 SetSubValues  {  }
335
336 Takes a hash of keys and values and stores them in the content of this attribute.
337
338 Each key B<replaces> the existing key with the same name
339
340 Returns a tuple of (status, message)
341
342 =cut
343
344
345 sub SetSubValues {
346    my $self = shift;
347    my %args = (@_); 
348    my $values = ($self->Content() || {} );
349    foreach my $key (keys %args) {
350     $values->{$key} = $args{$key};
351    }
352
353    $self->SetContent($values);
354
355 }
356
357
358 sub Object {
359     my $self = shift;
360     my $object_type = $self->__Value('ObjectType');
361     my $object;
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 (".$@.")");
365         return(undef);
366      }
367     $object->Load($self->__Value('ObjectId'));
368
369     return($object);
370
371 }
372
373
374 sub Delete {
375     my $self = shift;
376     unless ($self->CurrentUserHasRight('delete')) {
377         return (0,$self->loc('Permission Denied'));
378     }
379
380     return($self->SUPER::Delete(@_));
381 }
382
383
384 sub _Value {
385     my $self = shift;
386     unless ($self->CurrentUserHasRight('display')) {
387         return (0,$self->loc('Permission Denied'));
388     }
389
390     return($self->SUPER::_Value(@_));
391
392
393 }
394
395
396 sub _Set {
397     my $self = shift;
398     unless ($self->CurrentUserHasRight('update')) {
399
400         return (0,$self->loc('Permission Denied'));
401     }
402     return($self->SUPER::_Set(@_));
403
404 }
405
406
407 =head2 CurrentUserHasRight
408
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.
410
411 =cut
412
413 sub CurrentUserHasRight {
414     my $self = shift;
415     my $right = shift;
416
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(
419         Right      => $right,
420         ObjectId   => $self->__Value('ObjectId'),
421         ObjectType => $self->__Value('ObjectType'),
422         Name       => $self->__Value('Name')
423     );
424    
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));
428     return(0);
429
430 }
431
432
433 =head1 TODO
434
435 We should be deserializing the content on load and then enver again, rather than at every access
436
437 =cut
438
439
440
441
442
443
444
445
446 =head2 id
447
448 Returns the current value of id.
449 (In the database, id is stored as int(11).)
450
451
452 =cut
453
454
455 =head2 Name
456
457 Returns the current value of Name.
458 (In the database, Name is stored as varchar(255).)
459
460
461
462 =head2 SetName VALUE
463
464
465 Set Name to VALUE.
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).)
468
469
470 =cut
471
472
473 =head2 Description
474
475 Returns the current value of Description.
476 (In the database, Description is stored as varchar(255).)
477
478
479
480 =head2 SetDescription VALUE
481
482
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).)
486
487
488 =cut
489
490
491 =head2 Content
492
493 Returns the current value of Content.
494 (In the database, Content is stored as blob.)
495
496
497
498 =head2 SetContent VALUE
499
500
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.)
504
505
506 =cut
507
508
509 =head2 ContentType
510
511 Returns the current value of ContentType.
512 (In the database, ContentType is stored as varchar(16).)
513
514
515
516 =head2 SetContentType VALUE
517
518
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).)
522
523
524 =cut
525
526
527 =head2 ObjectType
528
529 Returns the current value of ObjectType.
530 (In the database, ObjectType is stored as varchar(64).)
531
532
533
534 =head2 SetObjectType VALUE
535
536
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).)
540
541
542 =cut
543
544
545 =head2 ObjectId
546
547 Returns the current value of ObjectId.
548 (In the database, ObjectId is stored as int(11).)
549
550
551
552 =head2 SetObjectId VALUE
553
554
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).)
558
559
560 =cut
561
562
563 =head2 Creator
564
565 Returns the current value of Creator.
566 (In the database, Creator is stored as int(11).)
567
568
569 =cut
570
571
572 =head2 Created
573
574 Returns the current value of Created.
575 (In the database, Created is stored as datetime.)
576
577
578 =cut
579
580
581 =head2 LastUpdatedBy
582
583 Returns the current value of LastUpdatedBy.
584 (In the database, LastUpdatedBy is stored as int(11).)
585
586
587 =cut
588
589
590 =head2 LastUpdated
591
592 Returns the current value of LastUpdated.
593 (In the database, LastUpdated is stored as datetime.)
594
595
596 =cut
597
598
599
600 sub _CoreAccessible {
601     {
602
603         id =>
604                 {read => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
605         Name =>
606                 {read => 1, write => 1, sql_type => 12, length => 255,  is_blob => 0,  is_numeric => 0,  type => 'varchar(255)', default => ''},
607         Description =>
608                 {read => 1, write => 1, sql_type => 12, length => 255,  is_blob => 0,  is_numeric => 0,  type => 'varchar(255)', default => ''},
609         Content =>
610                 {read => 1, write => 1, sql_type => -4, length => 0,  is_blob => 1,  is_numeric => 0,  type => 'blob', default => ''},
611         ContentType =>
612                 {read => 1, write => 1, sql_type => 12, length => 16,  is_blob => 0,  is_numeric => 0,  type => 'varchar(16)', default => ''},
613         ObjectType =>
614                 {read => 1, write => 1, sql_type => 12, length => 64,  is_blob => 0,  is_numeric => 0,  type => 'varchar(64)', default => ''},
615         ObjectId =>
616                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
617         Creator =>
618                 {read => 1, auto => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
619         Created =>
620                 {read => 1, auto => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
621         LastUpdatedBy =>
622                 {read => 1, auto => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
623         LastUpdated =>
624                 {read => 1, auto => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
625
626  }
627 };
628
629 sub FindDependencies {
630     my $self = shift;
631     my ($walker, $deps) = @_;
632
633     $self->SUPER::FindDependencies($walker, $deps);
634     $deps->Add( out => $self->Object );
635
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 );
644             }
645         }
646     }
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);
657                         $attr->LoadById($3);
658                         $deps->Add( out => $attr );
659                     }
660                 }
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 }
667                         }
668                     }
669                     $deps->Add( out => $search ) if $search;
670                 }
671             }
672         }
673     }
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 );
683                 }
684             }
685         }
686     }
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 );
693     }
694 }
695
696 sub PreInflate {
697     my $class = shift;
698     my ($importer, $uid, $data) = @_;
699
700     if ($data->{Object} and ref $data->{Object}) {
701         my $on_uid = ${ $data->{Object} };
702
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);
707         }
708     }
709
710     return $class->SUPER::PreInflate( $importer, $uid, $data );
711 }
712
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 {
725     my $self     = shift;
726     my $importer = shift;
727     my $spec     = shift;
728
729     # decode UIDs to be raw dashboard IDs
730     if ($self->Name eq RT::User::_PrefName("DashboardsInMenu")) {
731         my $content = $self->Content;
732
733         for my $pane (values %{ $content || {} }) {
734             for (@$pane) {
735                 if (ref($_) eq 'SCALAR') {
736                     my $attr = $importer->LookupObj($$_);
737                     if ($attr) {
738                         $_ = $attr->Id;
739                     }
740                     else {
741                         $importer->Postpone(
742                             for    => $$_,
743                             uid    => $spec->{uid},
744                             method => 'PostInflateFixup',
745                         );
746                     }
747                 }
748             }
749         }
750         $self->SetContent($content);
751     }
752     # decode UIDs to be saved searches
753     elsif ($self->Name eq RT::User::_PrefName("HomepageSettings")) {
754         my $content = $self->Content;
755
756         for my $pane (values %{ $content || {} }) {
757             for (@$pane) {
758                 if (ref($_->{uid}) eq 'SCALAR') {
759                     my $uid = $_->{uid};
760                     my $attr = $importer->LookupObj($$uid);
761
762                     if ($attr) {
763                         if ($_->{type} eq 'saved') {
764                             $_->{name} = join '-', $attr->ObjectType, $attr->ObjectId, 'SavedSearch', $attr->id;
765                         }
766                         # if type is system, name doesn't need to change
767                         # if type is anything else, pass it through as is
768                         delete $_->{uid};
769                     }
770                     else {
771                         $importer->Postpone(
772                             for    => $$uid,
773                             uid    => $spec->{uid},
774                             method => 'PostInflateFixup',
775                         );
776                     }
777                 }
778             }
779         }
780         $self->SetContent($content);
781     }
782     elsif ($self->Name eq 'Dashboard') {
783         my $content = $self->Content;
784
785         for my $pane (values %{ $content->{Panes} || {} }) {
786             for (@$pane) {
787                 if (ref($_->{uid}) eq 'SCALAR') {
788                     my $uid = $_->{uid};
789                     my $attr = $importer->LookupObj($$uid);
790
791                     if ($attr) {
792                         # update with the new id numbers assigned to us
793                         $_->{id} = $attr->Id;
794                         $_->{privacy} = join '-', $attr->ObjectType, $attr->ObjectId;
795                         delete $_->{uid};
796                     }
797                     else {
798                         $importer->Postpone(
799                             for    => $$uid,
800                             uid    => $spec->{uid},
801                             method => 'PostInflateFixup',
802                         );
803                     }
804                 }
805             }
806         }
807         $self->SetContent($content);
808     }
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} });
813             if ($attr) {
814                 $content->{DashboardId} = $attr->Id;
815             }
816             else {
817                 $importer->Postpone(
818                     for    => ${ $content->{DashboardId} },
819                     uid    => $spec->{uid},
820                     method => 'PostInflateFixup',
821                 );
822             }
823         }
824         $self->SetContent($content);
825     }
826 }
827
828 sub PostInflate {
829     my $self = shift;
830     my ($importer, $uid) = @_;
831
832     $self->SUPER::PostInflate( $importer, $uid );
833
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 });
837 }
838
839 sub Serialize {
840     my $self = shift;
841     my %args = (@_);
842     my %store = $self->SUPER::Serialize(@_);
843
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 || {} }) {
848             for (@$pane) {
849                 my $attr = RT::Attribute->new($self->CurrentUser);
850                 $attr->LoadById($_);
851                 $_ = \($attr->UID);
852             }
853         }
854         $store{Content} = $self->_SerializeContent($content);
855     }
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 || {} }) {
860             for (@$pane) {
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);
866                         $attr->LoadById($3);
867                         $_->{uid} = \($attr->UID);
868                     }
869                     # if we can't parse the name, just pass it through
870                 }
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 }
877                         }
878                     }
879                     # if we can't load the search, just pass it through
880                     if ($search) {
881                         $_->{uid} = \($search->UID);
882                     }
883                 }
884                 # pass through everything else (e.g. component)
885             }
886         }
887         $store{Content} = $self->_SerializeContent($content);
888     }
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} || {} }) {
893             for (@$pane) {
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);
898                 }
899                 # pass through everything else (e.g. component)
900             }
901         }
902         $store{Content} = $self->_SerializeContent($content);
903     }
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);
911     }
912
913     return %store;
914 }
915
916 RT::Base->_ImportOverlays();
917
918 1;