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 49=head1 NAME 50 51 RT::Dashboard - an API for saving and retrieving dashboards 52 53=head1 SYNOPSIS 54 55 use RT::Dashboard 56 57=head1 DESCRIPTION 58 59 Dashboard is an object that can belong to either an RT::User or an 60 RT::Group. It consists of an ID, a name, and a number of 61 saved searches and portlets. 62 63=head1 METHODS 64 65 66=cut 67 68package RT::Dashboard; 69 70use strict; 71use warnings; 72 73use base qw/RT::SharedSetting/; 74 75use RT::SavedSearch; 76 77use RT::System; 78'RT::System'->AddRight( Staff => SubscribeDashboard => 'Subscribe to dashboards'); # loc 79 80'RT::System'->AddRight( General => SeeDashboard => 'View system dashboards'); # loc 81'RT::System'->AddRight( Admin => CreateDashboard => 'Create system dashboards'); # loc 82'RT::System'->AddRight( Admin => ModifyDashboard => 'Modify system dashboards'); # loc 83'RT::System'->AddRight( Admin => DeleteDashboard => 'Delete system dashboards'); # loc 84 85'RT::System'->AddRight( Staff => SeeOwnDashboard => 'View personal dashboards'); # loc 86'RT::System'->AddRight( Staff => CreateOwnDashboard => 'Create personal dashboards'); # loc 87'RT::System'->AddRight( Staff => ModifyOwnDashboard => 'Modify personal dashboards'); # loc 88'RT::System'->AddRight( Staff => DeleteOwnDashboard => 'Delete personal dashboards'); # loc 89 90 91=head2 ObjectName 92 93An object of this class is called "dashboard" 94 95=cut 96 97sub ObjectName { "dashboard" } # loc 98 99sub SaveAttribute { 100 my $self = shift; 101 my $object = shift; 102 my $args = shift; 103 104 return $object->AddAttribute( 105 'Name' => 'Dashboard', 106 'Description' => $args->{'Name'}, 107 'Content' => {Panes => $args->{'Panes'}}, 108 ); 109} 110 111sub UpdateAttribute { 112 my $self = shift; 113 my $args = shift; 114 115 my ($status, $msg) = (1, undef); 116 if (defined $args->{'Panes'}) { 117 ($status, $msg) = $self->{'Attribute'}->SetSubValues( 118 Panes => $args->{'Panes'}, 119 ); 120 } 121 122 if ($status && $args->{'Name'}) { 123 ($status, $msg) = $self->{'Attribute'}->SetDescription($args->{'Name'}) 124 unless $self->Name eq $args->{'Name'}; 125 } 126 127 if ($status && $args->{'Privacy'}) { 128 my ($new_obj_type, $new_obj_id) = split /-/, $args->{'Privacy'}; 129 my ($obj_type, $obj_id) = split /-/, $self->Privacy; 130 131 my $attr = $self->{'Attribute'}; 132 if ($new_obj_type ne $obj_type) { 133 ($status, $msg) = $attr->SetObjectType($new_obj_type); 134 } 135 if ($status && $new_obj_id != $obj_id ) { 136 ($status, $msg) = $attr->SetObjectId($new_obj_id); 137 } 138 $self->{'Privacy'} = $args->{'Privacy'} if $status; 139 } 140 141 return ($status, $msg); 142} 143 144=head2 PostLoadValidate 145 146Ensure that the ID corresponds to an actual dashboard object, since it's all 147attributes under the hood. 148 149=cut 150 151sub PostLoadValidate { 152 my $self = shift; 153 return (0, "Invalid object type") unless $self->{'Attribute'}->Name eq 'Dashboard'; 154 return 1; 155} 156 157=head2 Panes 158 159Returns a hashref of pane name to portlets 160 161=cut 162 163sub Panes { 164 my $self = shift; 165 return unless ref($self->{'Attribute'}) eq 'RT::Attribute'; 166 return $self->{'Attribute'}->SubValue('Panes') || {}; 167} 168 169=head2 Portlets 170 171Returns the list of this dashboard's portlets, each a hashref with key 172C<portlet_type> being C<search> or C<component>. 173 174=cut 175 176sub Portlets { 177 my $self = shift; 178 return map { @$_ } values %{ $self->Panes }; 179} 180 181=head2 Dashboards 182 183Returns a list of loaded sub-dashboards 184 185=cut 186 187sub Dashboards { 188 my $self = shift; 189 return map { 190 my $search = RT::Dashboard->new($self->CurrentUser); 191 $search->LoadById($_->{id}); 192 $search 193 } grep { $_->{portlet_type} eq 'dashboard' } $self->Portlets; 194} 195 196=head2 Searches 197 198Returns a list of loaded saved searches 199 200=cut 201 202sub Searches { 203 my $self = shift; 204 return map { 205 my $search = RT::SavedSearch->new($self->CurrentUser); 206 $search->Load($_->{privacy}, $_->{id}); 207 $search 208 } grep { $_->{portlet_type} eq 'search' } $self->Portlets; 209} 210 211=head2 ShowSearchName Portlet 212 213Returns an array for one saved search, suitable for passing to 214/Elements/ShowSearch. 215 216=cut 217 218sub ShowSearchName { 219 my $self = shift; 220 my $portlet = shift; 221 222 if ($portlet->{privacy} eq 'RT::System') { 223 return Name => $portlet->{description}; 224 } 225 226 return SavedSearch => join('-', $portlet->{privacy}, 'SavedSearch', $portlet->{id}); 227} 228 229=head2 PossibleHiddenSearches 230 231This will return a list of saved searches that are potentially not visible by 232all users for whom the dashboard is visible. You may pass in a privacy to 233use instead of the dashboard's privacy. 234 235=cut 236 237sub PossibleHiddenSearches { 238 my $self = shift; 239 my $privacy = shift || $self->Privacy; 240 241 return grep { !$_->IsVisibleTo($privacy) } $self->Searches, $self->Dashboards; 242} 243 244# _PrivacyObjects: returns a list of objects that can be used to load 245# dashboards from. You probably want to use the wrapper methods like 246# ObjectsForLoading, ObjectsForCreating, etc. 247 248sub _PrivacyObjects { 249 my $self = shift; 250 251 my @objects; 252 253 my $CurrentUser = $self->CurrentUser; 254 push @objects, $CurrentUser->UserObj; 255 256 my $groups = RT::Groups->new($CurrentUser); 257 $groups->LimitToUserDefinedGroups; 258 $groups->WithCurrentUser; 259 push @objects, @{ $groups->ItemsArrayRef }; 260 261 push @objects, RT::System->new($CurrentUser); 262 263 return @objects; 264} 265 266# ACLs 267 268sub _CurrentUserCan { 269 my $self = shift; 270 my $privacy = shift || $self->Privacy; 271 my %args = @_; 272 273 if (!defined($privacy)) { 274 $RT::Logger->debug("No privacy provided to $self->_CurrentUserCan"); 275 return 0; 276 } 277 278 my $object = $self->_GetObject($privacy); 279 return 0 unless $object; 280 281 my $level; 282 283 if ($object->isa('RT::User')) { $level = 'Own' } 284 elsif ($object->isa('RT::Group')) { $level = 'Group' } 285 elsif ($object->isa('RT::System')) { $level = '' } 286 else { 287 $RT::Logger->error("Unknown object $object from privacy $privacy"); 288 return 0; 289 } 290 291 # users are mildly special-cased, since we actually have to check that 292 # the user is operating on himself 293 if ($object->isa('RT::User')) { 294 return 0 unless $object->Id == $self->CurrentUser->Id; 295 } 296 297 my $right = $args{FullRight} 298 || join('', $args{Right}, $level, 'Dashboard'); 299 300 # all rights, except group rights, are global 301 $object = $RT::System unless $object->isa('RT::Group'); 302 303 return $self->CurrentUser->HasRight( 304 Right => $right, 305 Object => $object, 306 ); 307} 308 309sub CurrentUserCanSee { 310 my $self = shift; 311 my $privacy = shift; 312 313 $self->_CurrentUserCan($privacy, Right => 'See'); 314} 315 316sub CurrentUserCanCreate { 317 my $self = shift; 318 my $privacy = shift; 319 320 $self->_CurrentUserCan($privacy, Right => 'Create'); 321} 322 323sub CurrentUserCanModify { 324 my $self = shift; 325 my $privacy = shift; 326 327 $self->_CurrentUserCan($privacy, Right => 'Modify'); 328} 329 330sub CurrentUserCanDelete { 331 my $self = shift; 332 my $privacy = shift; 333 334 my $can = $self->_CurrentUserCan($privacy, Right => 'Delete'); 335 336 # Don't allow to delete system default dashboard 337 if ($can) { 338 my ($system_default) = RT::System->new( RT->SystemUser )->Attributes->Named('DefaultDashboard'); 339 if ( $system_default && $system_default->Content && $system_default->Content == $self->Id ) { 340 return 0; 341 } 342 } 343 344 return $can; 345} 346 347sub CurrentUserCanSubscribe { 348 my $self = shift; 349 my $privacy = shift; 350 351 $self->_CurrentUserCan($privacy, FullRight => 'SubscribeDashboard'); 352} 353 354=head2 Subscription 355 356Returns the L<RT::Attribute> representing the current user's subscription 357to this dashboard if there is one; otherwise, returns C<undef>. 358 359=cut 360 361sub Subscription { 362 my $self = shift; 363 364 # no subscription to unloaded dashboards 365 return unless $self->id; 366 367 for my $sub ($self->CurrentUser->UserObj->Attributes->Named('Subscription')) { 368 return $sub if $sub->SubValue('DashboardId') == $self->id; 369 } 370 371 return; 372} 373 374sub ObjectsForLoading { 375 my $self = shift; 376 my %args = ( 377 IncludeSuperuserGroups => 1, 378 @_ 379 ); 380 my @objects; 381 382 # If you've been granted the SeeOwnDashboard global right (which you 383 # could have by way of global user right or global group right), you 384 # get to see your own dashboards 385 my $CurrentUser = $self->CurrentUser; 386 push @objects, $CurrentUser->UserObj 387 if $CurrentUser->HasRight(Object => $RT::System, Right => 'SeeOwnDashboard'); 388 389 # Find groups for which: (a) you are a member of the group, and (b) 390 # you have been granted SeeGroupDashboard on (by any means), and (c) 391 # have at least one dashboard 392 my $groups = RT::Groups->new($CurrentUser); 393 $groups->LimitToUserDefinedGroups; 394 $groups->ForWhichCurrentUserHasRight( 395 Right => 'SeeGroupDashboard', 396 IncludeSuperusers => $args{IncludeSuperuserGroups}, 397 ); 398 $groups->WithCurrentUser; 399 my $attrs = $groups->Join( 400 ALIAS1 => 'main', 401 FIELD1 => 'id', 402 TABLE2 => 'Attributes', 403 FIELD2 => 'ObjectId', 404 ); 405 $groups->Limit( 406 ALIAS => $attrs, 407 FIELD => 'ObjectType', 408 VALUE => 'RT::Group', 409 ); 410 $groups->Limit( 411 ALIAS => $attrs, 412 FIELD => 'Name', 413 VALUE => 'Dashboard', 414 ); 415 push @objects, @{ $groups->ItemsArrayRef }; 416 417 # Finally, if you have been granted the SeeDashboard right (which 418 # you could have by way of global user right or global group right), 419 # you can see system dashboards. 420 push @objects, RT::System->new($CurrentUser) 421 if $CurrentUser->HasRight(Object => $RT::System, Right => 'SeeDashboard'); 422 423 return @objects; 424} 425 426sub CurrentUserCanCreateAny { 427 my $self = shift; 428 my @objects; 429 430 my $CurrentUser = $self->CurrentUser; 431 return 1 432 if $CurrentUser->HasRight(Object => $RT::System, Right => 'CreateOwnDashboard'); 433 434 my $groups = RT::Groups->new($CurrentUser); 435 $groups->LimitToUserDefinedGroups; 436 $groups->ForWhichCurrentUserHasRight( 437 Right => 'CreateGroupDashboard', 438 IncludeSuperusers => 1, 439 ); 440 return 1 if $groups->Count; 441 442 return 1 443 if $CurrentUser->HasRight(Object => $RT::System, Right => 'CreateDashboard'); 444 445 return 0; 446} 447 448=head2 Delete 449 450Deletes the dashboard and related subscriptions. 451Returns a tuple of status and message, where status is true upon success. 452 453=cut 454 455sub Delete { 456 my $self = shift; 457 my $id = $self->id; 458 my ( $status, $msg ) = $self->SUPER::Delete(@_); 459 if ( $status ) { 460 # delete all the subscriptions 461 my $subscriptions = RT::Attributes->new( RT->SystemUser ); 462 $subscriptions->Limit( 463 FIELD => 'Name', 464 VALUE => 'Subscription', 465 ); 466 $subscriptions->Limit( 467 FIELD => 'Description', 468 VALUE => "Subscription to dashboard $id", 469 ); 470 while ( my $subscription = $subscriptions->Next ) { 471 $subscription->Delete(); 472 } 473 } 474 475 return ( $status, $msg ); 476} 477 478RT::Base->_ImportOverlays(); 479 4801; 481