1# BEGIN BPS TAGGED BLOCK {{{
2#
3# COPYRIGHT:
4#
5# This software is Copyright (c) 1996-2021 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
49package RT::Attribute;
50
51use strict;
52use warnings;
53
54use base 'RT::Record';
55
56sub Table {'Attributes'}
57
58use Storable qw/nfreeze thaw/;
59use MIME::Base64;
60use RT::URI::attribute;
61
62
63=head1 NAME
64
65  RT::Attribute_Overlay
66
67=head1 Content
68
69=cut
70
71# 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
72
73our $ACL_MAP = {
74    SavedSearch => { create => 'EditSavedSearches',
75                     update => 'EditSavedSearches',
76                     delete => 'EditSavedSearches',
77                     display => 'ShowSavedSearches' },
78
79};
80
81# There are a number of attributes that users should be able to modify for themselves, such as saved searches
82#  we could do this with a different set of "update" rights, but that gets very hacky very fast. this is even faster and even
83# hackier. we're hardcoding that a different set of rights are needed for attributes on oneself
84our $PERSONAL_ACL_MAP = {
85    SavedSearch => { create => 'ModifySelf',
86                     update => 'ModifySelf',
87                     delete => 'ModifySelf',
88                     display => 'allow' },
89
90};
91
92=head2 LookupObjectRight { ObjectType => undef, ObjectId => undef, Name => undef, Right => { create, update, delete, display } }
93
94Returns 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.
95
96=cut
97
98sub LookupObjectRight {
99    my $self = shift;
100    my %args = ( ObjectType => undef,
101                 ObjectId => undef,
102                 Right => undef,
103                 Name => undef,
104                 @_);
105
106    # if it's an attribute on oneself, check the personal acl map
107    if (($args{'ObjectType'} eq 'RT::User') && ($args{'ObjectId'} eq $self->CurrentUser->Id)) {
108    return('allow') unless ($PERSONAL_ACL_MAP->{$args{'Name'}});
109    return('allow') unless ($PERSONAL_ACL_MAP->{$args{'Name'}}->{$args{'Right'}});
110    return($PERSONAL_ACL_MAP->{$args{'Name'}}->{$args{'Right'}});
111
112    }
113   # otherwise check the main ACL map
114    else {
115    return('allow') unless ($ACL_MAP->{$args{'Name'}});
116    return('allow') unless ($ACL_MAP->{$args{'Name'}}->{$args{'Right'}});
117    return($ACL_MAP->{$args{'Name'}}->{$args{'Right'}});
118    }
119}
120
121
122
123
124=head2 Create PARAMHASH
125
126Create takes a hash of values and creates a row in the database:
127
128  varchar(200) 'Name'.
129  varchar(255) 'Content'.
130  varchar(16) 'ContentType',
131  varchar(64) 'ObjectType'.
132  int(11) 'ObjectId'.
133
134You may pass a C<Object> instead of C<ObjectType> and C<ObjectId>.
135
136=cut
137
138
139
140
141sub Create {
142    my $self = shift;
143    my %args = (
144                Name => '',
145                Description => '',
146                Content => '',
147                ContentType => '',
148                Object => undef,
149                RecordTransaction => undef,
150                @_);
151
152    if ($args{Object} and UNIVERSAL::can($args{Object}, 'Id')) {
153        $args{ObjectType} = $args{Object}->isa("RT::CurrentUser") ? "RT::User" : ref($args{Object});
154        $args{ObjectId} = $args{Object}->Id;
155    } else {
156        return(0, $self->loc("Required parameter '[_1]' not specified", 'Object'));
157
158    }
159
160    # object_right is the right that the user has to have on the object for them to have $right on this attribute
161    my $object_right = $self->LookupObjectRight(
162        Right      => 'create',
163        ObjectId   => $args{'ObjectId'},
164        ObjectType => $args{'ObjectType'},
165        Name       => $args{'Name'}
166    );
167    if ($object_right eq 'deny') {
168        return (0, $self->loc('Permission Denied'));
169    }
170    elsif ($object_right eq 'allow') {
171        # do nothing, we're ok
172    }
173    elsif (!$self->CurrentUser->HasRight( Object => $args{Object}, Right => $object_right)) {
174        return (0, $self->loc('Permission Denied'));
175    }
176
177
178    if (ref ($args{'Content'}) ) {
179        eval  {$args{'Content'} = $self->_SerializeContent($args{'Content'}); };
180        if ($@) {
181         return(0, $@);
182        }
183        $args{'ContentType'} = 'storable';
184    }
185
186    $args{'RecordTransaction'} //= 1 if $args{'Name'} =~ /^(?:SavedSearch|Dashboard|Subscription)$/;
187
188    $RT::Handle->BeginTransaction if $args{'RecordTransaction'};
189    my @return = $self->SUPER::Create(
190        Name        => $args{'Name'},
191        Content     => $args{'Content'},
192        ContentType => $args{'ContentType'},
193        Description => $args{'Description'},
194        ObjectType  => $args{'ObjectType'},
195        ObjectId    => $args{'ObjectId'},
196    );
197
198
199    if ( $args{'RecordTransaction'} ) {
200        if ( $return[0] ) {
201            my ( $ret, $msg ) = $self->_NewTransaction( Type => 'Create' );
202            if ($ret) {
203                ( $ret, $msg ) = $self->AddAttribute(
204                    Name    => 'ContentHistory',
205                    Content => $self->_DeserializeContent( $args{'Content'} ) || {},
206                );
207            }
208
209            @return = ( $ret, $msg ) unless $ret;
210        }
211
212        if ( $return[0] ) {
213            $RT::Handle->Commit;
214        }
215        else {
216            $RT::Handle->Rollback;
217        }
218    }
219
220    $self->_SyncLinks if $return[0];
221    return wantarray ? @return : $return[0];
222}
223
224
225
226=head2  LoadByNameAndObject (Object => OBJECT, Name => NAME)
227
228Loads the Attribute named NAME for Object OBJECT.
229
230=cut
231
232sub LoadByNameAndObject {
233    my $self = shift;
234    my %args = (
235        Object => undef,
236        Name  => undef,
237        @_,
238    );
239
240    return (
241        $self->LoadByCols(
242            Name => $args{'Name'},
243            ObjectType => ref($args{'Object'}),
244            ObjectId => $args{'Object'}->Id,
245        )
246    );
247
248}
249
250
251
252=head2 _DeserializeContent
253
254DeserializeContent returns this Attribute's "Content" as a hashref.
255
256
257=cut
258
259sub _DeserializeContent {
260    my $self = shift;
261    my $content = shift;
262
263    my $hashref;
264    eval {$hashref  = thaw(decode_base64($content))} ;
265    if ($@) {
266        $RT::Logger->error("Deserialization of attribute ".$self->Id. " failed");
267    }
268
269    return($hashref);
270
271}
272
273
274=head2 Content
275
276Returns this attribute's content. If it's a scalar, returns a scalar
277If it's data structure returns a ref to that data structure.
278
279=cut
280
281sub Content {
282    my $self = shift;
283    # Here we call _Value to get the ACL check.
284    my $content = $self->_Value('Content');
285    if ( ($self->__Value('ContentType') || '') eq 'storable') {
286        eval {$content = $self->_DeserializeContent($content); };
287        if ($@) {
288            $RT::Logger->error("Deserialization of content for attribute ".$self->Id. " failed. Attribute was: ".$content);
289        }
290    }
291
292    return($content);
293
294}
295
296sub _SerializeContent {
297    my $self = shift;
298    my $content = shift;
299    local $Storable::canonical = 1;
300    return( encode_base64(nfreeze($content)));
301}
302
303
304sub SetContent {
305    my $self = shift;
306    my $content = shift;
307
308    # Call __Value to avoid ACL check.
309    if ( ($self->__Value('ContentType')||'') eq 'storable' ) {
310        # We eval the serialization because it will lose on a coderef.
311        $content = eval { $self->_SerializeContent($content) };
312        if ($@) {
313            $RT::Logger->error("Content couldn't be frozen: $@");
314            return(0, "Content couldn't be frozen");
315        }
316    }
317    my ($ok, $msg) = $self->_Set( Field => 'Content', Value => $content );
318    if ($ok) {
319        $self->_SyncLinks;
320        return ( $ok, $self->loc("Attribute updated") );
321    }
322    return ($ok, $msg);
323}
324
325=head2 SubValue KEY
326
327Returns the subvalue for $key.
328
329
330=cut
331
332sub SubValue {
333    my $self = shift;
334    my $key = shift;
335    my $values = $self->Content();
336    return undef unless ref($values);
337    return($values->{$key});
338}
339
340=head2 DeleteSubValue NAME
341
342Deletes the subvalue with the key NAME
343
344=cut
345
346sub DeleteSubValue {
347    my $self = shift;
348    my $key = shift;
349    my $values = $self->Content();
350    delete $values->{$key};
351    $self->SetContent($values);
352}
353
354
355=head2 DeleteAllSubValues
356
357Deletes all subvalues for this attribute
358
359=cut
360
361
362sub DeleteAllSubValues {
363    my $self = shift;
364    $self->SetContent({});
365}
366
367=head2 SetSubValues  {  }
368
369Takes a hash of keys and values and stores them in the content of this attribute.
370
371Each key B<replaces> the existing key with the same name
372
373Returns a tuple of (status, message)
374
375=cut
376
377
378sub SetSubValues {
379   my $self = shift;
380   my %args = (@_);
381   my $values = ($self->Content() || {} );
382   foreach my $key (keys %args) {
383    $values->{$key} = $args{$key};
384   }
385
386   $self->SetContent($values);
387
388}
389
390
391sub Object {
392    my $self = shift;
393    my $object_type = $self->__Value('ObjectType');
394    my $object;
395    eval { $object = $object_type->new($self->CurrentUser) };
396    unless(UNIVERSAL::isa($object, $object_type)) {
397        $RT::Logger->error("Attribute ".$self->Id." has a bogus object type - $object_type (".$@.")");
398        return(undef);
399     }
400    $object->Load($self->__Value('ObjectId'));
401
402    return($object);
403
404}
405
406
407sub Delete {
408    my $self = shift;
409    my %args = (
410        RecordTransaction => undef,
411        @_,
412    );
413
414    unless ( $self->CurrentUserHasRight('delete') ) {
415        return ( 0, $self->loc('Permission Denied') );
416    }
417
418    # Get values even if current user doesn't have right to see
419    my $name = $self->__Value('Name');
420    my @links;
421    if ( $name =~ /^(Dashboard|(?:Pref-)?HomepageSettings)$/ ) {
422        push @links, @{ $self->DependsOn->ItemsArrayRef };
423    }
424    elsif ( $name eq 'SavedSearch' ) {
425        push @links, @{ $self->DependedOnBy->ItemsArrayRef };
426    }
427
428    $args{'RecordTransaction'} //= 1 if $name =~ /^(?:SavedSearch|Dashboard|Subscription)$/;
429    $RT::Handle->BeginTransaction if $args{'RecordTransaction'};
430
431    my @return = $self->SUPER::Delete(@_);
432
433    if ( $args{'RecordTransaction'} ) {
434        if ( $return[0] ) {
435            my $txn = RT::Transaction->new( $self->CurrentUser );
436            my ( $ret, $msg ) = $txn->Create(
437                ObjectId   => $self->Id,
438                ObjectType => ref($self),
439                Type       => 'Delete',
440            );
441            @return = ( $ret, $msg ) unless $ret;
442        }
443
444        if ( $return[0] ) {
445            $RT::Handle->Commit;
446        }
447        else {
448            $RT::Handle->Rollback;
449        }
450    }
451
452    if ( $return[0] ) {
453        for my $link (@links) {
454            my ( $ret, $msg ) = $link->Delete;
455            if ( !$ret ) {
456                RT->Logger->error( "Couldn't delete link #" . $link->id . ": $msg" );
457            }
458        }
459    }
460
461    return @return;
462}
463
464
465sub _Value {
466    my $self = shift;
467    unless ($self->CurrentUserHasRight('display')) {
468        return (0,$self->loc('Permission Denied'));
469    }
470
471    return($self->SUPER::_Value(@_));
472
473
474}
475
476
477sub _Set {
478    my $self = shift;
479    my %args = (
480        Field             => undef,
481        Value             => undef,
482        RecordTransaction => undef,
483        TransactionType   => 'Set',
484        @_
485    );
486
487    unless ( $self->CurrentUserHasRight('update') ) {
488        return ( 0, $self->loc('Permission Denied') );
489    }
490
491    # Get values even if current user doesn't have right to see
492    $args{'RecordTransaction'} //= 1 if $self->__Value('Name') =~ /^(?:SavedSearch|Dashboard|Subscription)$/;
493    my $old_value = $self->__Value( $args{'Field'} ) if $args{'RecordTransaction'};
494
495    $RT::Handle->BeginTransaction if $args{'RecordTransaction'};
496
497    # Set the new value
498    my @return = $self->SUPER::_Set(
499        Field => $args{'Field'},
500        Value => $args{'Value'},
501    );
502
503    if ( $args{'RecordTransaction'} ) {
504        if ( $return[0] ) {
505            my ( $new_ref, $old_ref );
506
507            my %opt = (
508                Type  => $args{'TransactionType'},
509                Field => $args{'Field'},
510            );
511            if ( $args{'Field'} eq 'Content' ) {
512
513                $opt{ReferenceType} = 'RT::Attribute';
514
515                my $attrs = $self->Attributes;
516                $attrs->Limit( FIELD => 'Name', VALUE => 'ContentHistory' );
517                $attrs->OrderByCols( { FIELD => 'id', ORDER => 'DESC' } );
518                if ( my $old_content = $attrs->First ) {
519                    $opt{OldReference} = $old_content->id;
520                }
521                else {
522                    RT->Logger->debug("Couldn't find ContentHistory, creating one from old value");
523                    my ( $ret, $msg ) = $self->AddAttribute(
524                        Name    => 'ContentHistory',
525                        Content => $self->__Value('ContentType') eq 'storable'
526                        ? $self->_DeserializeContent($old_value)
527                        : $old_value,
528                    );
529                    if ($ret) {
530                        $opt{OldReference} = $ret;
531                    }
532                    else {
533                        @return = ( $ret, $msg );
534                    }
535                }
536
537                if ( $return[0] ) {
538                    my ( $ret, $msg ) = $self->AddAttribute(
539                        Name    => 'ContentHistory',
540                        Content => $self->_DeserializeContent( $args{'Value'} ),
541                        Content => $self->__Value('ContentType') eq 'storable'
542                        ? $self->_DeserializeContent( $args{'Value'} )
543                        : $args{'Value'},
544                    );
545
546                    if ($ret) {
547                        $opt{NewReference} = $ret;
548                    }
549                    else {
550                        @return = ( $ret, $msg );
551                    }
552                }
553            }
554            else {
555                $opt{'OldValue'} = $old_value;
556                $opt{'NewValue'} = $args{'Value'};
557            }
558
559            if ( $return[0] ) {
560                my ( $ret, $msg ) = $self->_NewTransaction(%opt);
561                @return = ( $ret, $msg ) unless $ret;
562            }
563        }
564
565        if ( $return[0] ) {
566            $RT::Handle->Commit;
567        }
568        else {
569            $RT::Handle->Rollback;
570        }
571    }
572
573    return wantarray ? @return : $return[0];
574}
575
576
577=head2 CurrentUserHasRight
578
579One 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.
580
581=cut
582
583sub CurrentUserHasRight {
584    my $self = shift;
585    my $right = shift;
586
587    # object_right is the right that the user has to have on the object for them to have $right on this attribute
588    my $object_right = $self->LookupObjectRight(
589        Right      => $right,
590        ObjectId   => $self->__Value('ObjectId'),
591        ObjectType => $self->__Value('ObjectType'),
592        Name       => $self->__Value('Name')
593    );
594
595    return (1) if ($object_right eq 'allow');
596    return (0) if ($object_right eq 'deny');
597    return(1) if ($self->CurrentUser->HasRight( Object => $self->Object, Right => $object_right));
598    return(0);
599
600}
601
602
603=head1 TODO
604
605We should be deserializing the content on load and then never again, rather than at every access
606
607=cut
608
609
610
611
612
613
614
615
616=head2 id
617
618Returns the current value of id.
619(In the database, id is stored as int(11).)
620
621
622=cut
623
624
625=head2 Name
626
627Returns the current value of Name.
628(In the database, Name is stored as varchar(255).)
629
630
631
632=head2 SetName VALUE
633
634
635Set Name to VALUE.
636Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
637(In the database, Name will be stored as a varchar(255).)
638
639
640=cut
641
642
643=head2 Description
644
645Returns the current value of Description.
646(In the database, Description is stored as varchar(255).)
647
648
649
650=head2 SetDescription VALUE
651
652
653Set Description to VALUE.
654Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
655(In the database, Description will be stored as a varchar(255).)
656
657
658=cut
659
660
661=head2 Content
662
663Returns the current value of Content.
664(In the database, Content is stored as blob.)
665
666
667
668=head2 SetContent VALUE
669
670
671Set Content to VALUE.
672Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
673(In the database, Content will be stored as a blob.)
674
675
676=cut
677
678
679=head2 ContentType
680
681Returns the current value of ContentType.
682(In the database, ContentType is stored as varchar(16).)
683
684
685
686=head2 SetContentType VALUE
687
688
689Set ContentType to VALUE.
690Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
691(In the database, ContentType will be stored as a varchar(16).)
692
693
694=cut
695
696
697=head2 ObjectType
698
699Returns the current value of ObjectType.
700(In the database, ObjectType is stored as varchar(64).)
701
702
703
704=head2 SetObjectType VALUE
705
706
707Set ObjectType to VALUE.
708Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
709(In the database, ObjectType will be stored as a varchar(64).)
710
711
712=cut
713
714
715=head2 ObjectId
716
717Returns the current value of ObjectId.
718(In the database, ObjectId is stored as int(11).)
719
720
721
722=head2 SetObjectId VALUE
723
724
725Set ObjectId to VALUE.
726Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
727(In the database, ObjectId will be stored as a int(11).)
728
729
730=cut
731
732
733=head2 Creator
734
735Returns the current value of Creator.
736(In the database, Creator is stored as int(11).)
737
738
739=cut
740
741
742=head2 Created
743
744Returns the current value of Created.
745(In the database, Created is stored as datetime.)
746
747
748=cut
749
750
751=head2 LastUpdatedBy
752
753Returns the current value of LastUpdatedBy.
754(In the database, LastUpdatedBy is stored as int(11).)
755
756
757=cut
758
759
760=head2 LastUpdated
761
762Returns the current value of LastUpdated.
763(In the database, LastUpdated is stored as datetime.)
764
765
766=cut
767
768
769
770sub _CoreAccessible {
771    {
772
773        id =>
774                {read => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
775        Name =>
776                {read => 1, write => 1, sql_type => 12, length => 255,  is_blob => 0,  is_numeric => 0,  type => 'varchar(255)', default => ''},
777        Description =>
778                {read => 1, write => 1, sql_type => 12, length => 255,  is_blob => 0,  is_numeric => 0,  type => 'varchar(255)', default => ''},
779        Content =>
780                {read => 1, write => 1, sql_type => -4, length => 0,  is_blob => 1,  is_numeric => 0,  type => 'blob', default => ''},
781        ContentType =>
782                {read => 1, write => 1, sql_type => 12, length => 16,  is_blob => 0,  is_numeric => 0,  type => 'varchar(16)', default => ''},
783        ObjectType =>
784                {read => 1, write => 1, sql_type => 12, length => 64,  is_blob => 0,  is_numeric => 0,  type => 'varchar(64)', default => ''},
785        ObjectId =>
786                {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
787        Creator =>
788                {read => 1, auto => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
789        Created =>
790                {read => 1, auto => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
791        LastUpdatedBy =>
792                {read => 1, auto => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
793        LastUpdated =>
794                {read => 1, auto => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
795
796 }
797};
798
799sub FindDependencies {
800    my $self = shift;
801    my ($walker, $deps) = @_;
802
803    $self->SUPER::FindDependencies($walker, $deps);
804    $deps->Add( out => $self->Object );
805
806    # dashboards in menu attribute has dependencies on each of its dashboards
807    if ($self->Name eq RT::User::_PrefName("DashboardsInMenu")) {
808        my $content = $self->Content;
809        for my $pane (values %{ $content || {} }) {
810            for my $dash_id (@$pane) {
811                my $attr = RT::Attribute->new($self->CurrentUser);
812                $attr->LoadById($dash_id);
813                $deps->Add( out => $attr );
814            }
815        }
816    }
817    # homepage settings attribute has dependencies on each of the searches in it
818    elsif ($self->Name eq RT::User::_PrefName("HomepageSettings")) {
819        my $content = $self->Content;
820        for my $pane (values %{ $content || {} }) {
821            for my $component (@$pane) {
822                # this hairy code mirrors what's in the saved search loader
823                # in /Elements/ShowSearch
824                if ($component->{type} eq 'saved') {
825                    if ($component->{name} =~ /^(.*?)-(\d+)-SavedSearch-(\d+)$/) {
826                        my $attr = RT::Attribute->new($self->CurrentUser);
827                        $attr->LoadById($3);
828                        $deps->Add( out => $attr );
829                    }
830                }
831                elsif ($component->{type} eq 'system') {
832                    my ($search) = RT::System->new($self->CurrentUser)->Attributes->Named( 'Search - ' . $component->{name} );
833                    unless ( $search && $search->Id ) {
834                        my (@custom_searches) = RT::System->new($self->CurrentUser)->Attributes->Named('SavedSearch');
835                        foreach my $custom (@custom_searches) {
836                            if ($custom->Description eq $component->{name}) { $search = $custom; last }
837                        }
838                    }
839                    $deps->Add( out => $search ) if $search;
840                }
841            }
842        }
843    }
844    # dashboards have dependencies on all the searches and dashboards they use
845    elsif ($self->Name eq 'Dashboard') {
846        my $content = $self->Content;
847        for my $pane (values %{ $content->{Panes} || {} }) {
848            for my $component (@$pane) {
849                if ($component->{portlet_type} eq 'search' || $component->{portlet_type} eq 'dashboard') {
850                    my $attr = RT::Attribute->new($self->CurrentUser);
851                    $attr->LoadById($component->{id});
852                    $deps->Add( out => $attr );
853                }
854            }
855        }
856    }
857    # each subscription depends on its dashboard
858    elsif ($self->Name eq 'Subscription') {
859        my $content = $self->Content;
860        my $attr = RT::Attribute->new($self->CurrentUser);
861        $attr->LoadById($content->{DashboardId});
862        $deps->Add( out => $attr );
863    }
864
865    # Links
866    my $links = RT::Links->new( $self->CurrentUser );
867    $links->Limit(
868        SUBCLAUSE       => "either",
869        FIELD           => $_,
870        VALUE           => $self->URI,
871        ENTRYAGGREGATOR => 'OR',
872        )
873        for qw/Base Target/;
874    $deps->Add( in => $links );
875}
876
877sub PreInflate {
878    my $class = shift;
879    my ($importer, $uid, $data) = @_;
880
881    if ($data->{Object} and ref $data->{Object}) {
882        my $on_uid = ${ $data->{Object} };
883
884        # skip attributes of objects we're not inflating
885        # exception: we don't inflate RT->System, but we want RT->System's searches
886        unless ($on_uid eq RT->System->UID && $data->{Name} =~ /Search/) {
887            return if $importer->ShouldSkipTransaction($on_uid);
888        }
889    }
890
891    return $class->SUPER::PreInflate( $importer, $uid, $data );
892}
893
894# this method will be called repeatedly to fix up this attribute's contents
895# (a list of searches, dashboards) during the import process, as the
896# ordinary dependency resolution system can't quite handle the subtlety
897# involved (e.g. a user simply declares out-dependencies on all of her
898# attributes, but those attributes (e.g. dashboards, saved searches,
899# dashboards in menu preferences) have dependencies amongst themselves).
900# if this attribute (e.g. a user's dashboard) fails to load an attribute
901# (e.g. a user's saved search) then it postpones and repeats the postinflate
902# process again when that user's saved search has been imported
903# this method updates Content each time through, each time getting closer and
904# closer to the fully inflated attribute
905sub PostInflateFixup {
906    my $self     = shift;
907    my $importer = shift;
908    my $spec     = shift;
909
910    # decode UIDs to be raw dashboard IDs
911    if ($self->Name eq RT::User::_PrefName("DashboardsInMenu")) {
912        my $content = $self->Content;
913
914        for my $pane (values %{ $content || {} }) {
915            for (@$pane) {
916                if (ref($_) eq 'SCALAR') {
917                    my $attr = $importer->LookupObj($$_);
918                    if ($attr) {
919                        $_ = $attr->Id;
920                    }
921                    else {
922                        $importer->Postpone(
923                            for    => $$_,
924                            uid    => $spec->{uid},
925                            method => 'PostInflateFixup',
926                        );
927                    }
928                }
929            }
930        }
931        $self->SetContent($content);
932    }
933    # decode UIDs to be saved searches
934    elsif ($self->Name eq RT::User::_PrefName("HomepageSettings")) {
935        my $content = $self->Content;
936
937        for my $pane (values %{ $content || {} }) {
938            for (@$pane) {
939                if (ref($_->{uid}) eq 'SCALAR') {
940                    my $uid = $_->{uid};
941                    my $attr = $importer->LookupObj($$uid);
942
943                    if ($attr) {
944                        if ($_->{type} eq 'saved') {
945                            $_->{name} = join '-', $attr->ObjectType, $attr->ObjectId, 'SavedSearch', $attr->id;
946                        }
947                        # if type is system, name doesn't need to change
948                        # if type is anything else, pass it through as is
949                        delete $_->{uid};
950                    }
951                    else {
952                        $importer->Postpone(
953                            for    => $$uid,
954                            uid    => $spec->{uid},
955                            method => 'PostInflateFixup',
956                        );
957                    }
958                }
959            }
960        }
961        $self->SetContent($content);
962    }
963    elsif ($self->Name eq 'Dashboard') {
964        my $content = $self->Content;
965
966        for my $pane (values %{ $content->{Panes} || {} }) {
967            for (@$pane) {
968                if (ref($_->{uid}) eq 'SCALAR') {
969                    my $uid = $_->{uid};
970                    my $attr = $importer->LookupObj($$uid);
971
972                    if ($attr) {
973                        # update with the new id numbers assigned to us
974                        $_->{id} = $attr->Id;
975                        $_->{privacy} = join '-', $attr->ObjectType, $attr->ObjectId;
976                        delete $_->{uid};
977                    }
978                    else {
979                        $importer->Postpone(
980                            for    => $$uid,
981                            uid    => $spec->{uid},
982                            method => 'PostInflateFixup',
983                        );
984                    }
985                }
986            }
987        }
988        $self->SetContent($content);
989    }
990    elsif ($self->Name eq 'Subscription') {
991        my $content = $self->Content;
992        if (ref($content->{DashboardId}) eq 'SCALAR') {
993            my $attr = $importer->LookupObj(${ $content->{DashboardId} });
994            if ($attr) {
995                $content->{DashboardId} = $attr->Id;
996            }
997            else {
998                $importer->Postpone(
999                    for    => ${ $content->{DashboardId} },
1000                    uid    => $spec->{uid},
1001                    method => 'PostInflateFixup',
1002                );
1003            }
1004        }
1005        $self->SetContent($content);
1006    }
1007}
1008
1009sub PostInflate {
1010    my $self = shift;
1011    my ($importer, $uid) = @_;
1012
1013    $self->SUPER::PostInflate( $importer, $uid );
1014
1015    # this method is separate because it needs to be callable multple times,
1016    # and we can't guarantee that SUPER::PostInflate can deal with that
1017    $self->PostInflateFixup($importer, { uid => $uid });
1018}
1019
1020sub Serialize {
1021    my $self = shift;
1022    my %args = (@_);
1023    my %store = $self->SUPER::Serialize(@_);
1024
1025    # encode raw dashboard IDs to be UIDs
1026    if ($store{Name} eq RT::User::_PrefName("DashboardsInMenu")) {
1027        my $content = $self->_DeserializeContent($store{Content});
1028        for my $pane (values %{ $content || {} }) {
1029            for (@$pane) {
1030                my $attr = RT::Attribute->new($self->CurrentUser);
1031                $attr->LoadById($_);
1032                $_ = \($attr->UID);
1033            }
1034        }
1035        $store{Content} = $self->_SerializeContent($content);
1036    }
1037    # encode saved searches to be UIDs
1038    elsif ($store{Name} eq RT::User::_PrefName("HomepageSettings")) {
1039        my $content = $self->_DeserializeContent($store{Content});
1040        for my $pane (values %{ $content || {} }) {
1041            for (@$pane) {
1042                # this hairy code mirrors what's in the saved search loader
1043                # in /Elements/ShowSearch
1044                if ($_->{type} eq 'saved') {
1045                    if ($_->{name} =~ /^(.*?)-(\d+)-SavedSearch-(\d+)$/) {
1046                        my $attr = RT::Attribute->new($self->CurrentUser);
1047                        $attr->LoadById($3);
1048                        $_->{uid} = \($attr->UID);
1049                    }
1050                    # if we can't parse the name, just pass it through
1051                }
1052                elsif ($_->{type} eq 'system') {
1053                    my ($search) = RT::System->new($self->CurrentUser)->Attributes->Named( 'Search - ' . $_->{name} );
1054                    unless ( $search && $search->Id ) {
1055                        my (@custom_searches) = RT::System->new($self->CurrentUser)->Attributes->Named('SavedSearch');
1056                        foreach my $custom (@custom_searches) {
1057                            if ($custom->Description eq $_->{name}) { $search = $custom; last }
1058                        }
1059                    }
1060                    # if we can't load the search, just pass it through
1061                    if ($search) {
1062                        $_->{uid} = \($search->UID);
1063                    }
1064                }
1065                # pass through everything else (e.g. component)
1066            }
1067        }
1068        $store{Content} = $self->_SerializeContent($content);
1069    }
1070    # encode saved searches and dashboards to be UIDs
1071    elsif ($store{Name} eq 'Dashboard') {
1072        my $content = $self->_DeserializeContent($store{Content}) || {};
1073        for my $pane (values %{ $content->{Panes} || {} }) {
1074            for (@$pane) {
1075                if ($_->{portlet_type} eq 'search' || $_->{portlet_type} eq 'dashboard') {
1076                    my $attr = RT::Attribute->new($self->CurrentUser);
1077                    $attr->LoadById($_->{id});
1078                    $_->{uid} = \($attr->UID);
1079                }
1080                # pass through everything else (e.g. component)
1081            }
1082        }
1083        $store{Content} = $self->_SerializeContent($content);
1084    }
1085    # encode subscriptions to have dashboard UID
1086    elsif ($store{Name} eq 'Subscription') {
1087        my $content = $self->_DeserializeContent($store{Content});
1088        my $attr = RT::Attribute->new($self->CurrentUser);
1089        $attr->LoadById($content->{DashboardId});
1090        $content->{DashboardId} = \($attr->UID);
1091        $store{Content} = $self->_SerializeContent($content);
1092    }
1093
1094    return %store;
1095}
1096
1097=head2 URI
1098
1099Returns this attribute's URI
1100
1101=cut
1102
1103sub URI {
1104    my $self = shift;
1105    my $uri  = RT::URI::attribute->new( $self->CurrentUser );
1106    return $uri->URIForObject($self);
1107}
1108
1109
1110=head2 _SyncLinks
1111
1112For dashboard and homepage attributes, keep links to saved searches they
1113include up to date. It does nothing for other attributes.
1114
1115Returns 1 on success and 0 on failure.
1116
1117=cut
1118
1119sub _SyncLinks {
1120    my $self = shift;
1121    my $name = $self->__Value('Name');
1122
1123    my $success;
1124
1125    if ( $name =~ /^(Dashboard|(?:Pref-)?HomepageSettings)$/ ) {
1126        my $type    = $1;
1127        my $content = $self->_DeserializeContent( $self->__Value('Content') );
1128
1129        my %searches;
1130        if ( $type eq 'Dashboard' ) {
1131            %searches
1132                = map { $_->{id} => 1 } grep { $_->{portlet_type} eq 'search' } @{ $content->{Panes}{body} },
1133                @{ $content->{Panes}{sidebar} };
1134        }
1135        else {
1136            for my $item ( @{ $content->{body} }, @{ $content->{sidebar} } ) {
1137                if ( $item->{type} eq 'saved' ) {
1138                    if ( $item->{name} =~ /SavedSearch-(\d+)/ ) {
1139                        $searches{$1} ||= 1;
1140                    }
1141                }
1142                elsif ( $item->{type} eq 'system' ) {
1143                    if ( my $attr
1144                        = RT::System->new( $self->CurrentUser )->FirstAttribute( 'Search - ' . $item->{name} ) )
1145                    {
1146                        $searches{ $attr->id } ||= 1;
1147                    }
1148                    else {
1149                        my $attrs = RT::System->new( $self->CurrentUser )->Attributes;
1150                        $attrs->Limit( FIELD => 'Name',        VALUE => 'SavedSearch' );
1151                        $attrs->Limit( FIELD => 'Description', VALUE => $item->{name} );
1152                        if ( my $attr = $attrs->First ) {
1153                            $searches{ $attr->id } ||= 1;
1154                        }
1155
1156                    }
1157                }
1158            }
1159        }
1160
1161        my $links = $self->DependsOn;
1162        while ( my $link = $links->Next ) {
1163            next if delete $searches{ $link->TargetObj->id };
1164            my ( $ret, $msg ) = $link->Delete;
1165            if ( !$ret ) {
1166                RT->Logger->error( "Couldn't delete link #" . $link->id . ": $msg" );
1167                $success //= 0;
1168            }
1169        }
1170
1171        for my $id ( keys %searches ) {
1172            my $link = RT::Link->new( $self->CurrentUser );
1173            my $attribute = RT::Attribute->new( $self->CurrentUser );
1174            $attribute->Load($id);
1175            if ( $attribute->id ) {
1176                my ( $ret, $msg )
1177                    = $link->Create( Type => 'DependsOn', Base => 'attribute:' . $self->id, Target => "attribute:$id" );
1178                if ( !$ret ) {
1179                    RT->Logger->error( "Couldn't create link for attribute #:" . $self->id . ": $msg" );
1180                    $success //= 0;
1181                }
1182            }
1183        }
1184    }
1185    return $success // 1;
1186}
1187
1188=head2 CurrentUserCanSee
1189
1190Shortcut of CurrentUserHasRight('display').
1191
1192=cut
1193
1194sub CurrentUserCanSee {
1195    my $self = shift;
1196    return $self->CurrentUserHasRight('display');
1197}
1198
1199RT::Base->_ImportOverlays();
1200
12011;
1202