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