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 $self->_CurrentUserCan($privacy, Right => 'Delete'); 335} 336 337sub CurrentUserCanSubscribe { 338 my $self = shift; 339 my $privacy = shift; 340 341 $self->_CurrentUserCan($privacy, FullRight => 'SubscribeDashboard'); 342} 343 344=head2 Subscription 345 346Returns the L<RT::Attribute> representing the current user's subscription 347to this dashboard if there is one; otherwise, returns C<undef>. 348 349=cut 350 351sub Subscription { 352 my $self = shift; 353 354 # no subscription to unloaded dashboards 355 return unless $self->id; 356 357 for my $sub ($self->CurrentUser->UserObj->Attributes->Named('Subscription')) { 358 return $sub if $sub->SubValue('DashboardId') == $self->id; 359 } 360 361 return; 362} 363 364sub ObjectsForLoading { 365 my $self = shift; 366 my %args = ( 367 IncludeSuperuserGroups => 1, 368 @_ 369 ); 370 my @objects; 371 372 # If you've been granted the SeeOwnDashboard global right (which you 373 # could have by way of global user right or global group right), you 374 # get to see your own dashboards 375 my $CurrentUser = $self->CurrentUser; 376 push @objects, $CurrentUser->UserObj 377 if $CurrentUser->HasRight(Object => $RT::System, Right => 'SeeOwnDashboard'); 378 379 # Find groups for which: (a) you are a member of the group, and (b) 380 # you have been granted SeeGroupDashboard on (by any means), and (c) 381 # have at least one dashboard 382 my $groups = RT::Groups->new($CurrentUser); 383 $groups->LimitToUserDefinedGroups; 384 $groups->ForWhichCurrentUserHasRight( 385 Right => 'SeeGroupDashboard', 386 IncludeSuperusers => $args{IncludeSuperuserGroups}, 387 ); 388 $groups->WithCurrentUser; 389 my $attrs = $groups->Join( 390 ALIAS1 => 'main', 391 FIELD1 => 'id', 392 TABLE2 => 'Attributes', 393 FIELD2 => 'ObjectId', 394 ); 395 $groups->Limit( 396 ALIAS => $attrs, 397 FIELD => 'ObjectType', 398 VALUE => 'RT::Group', 399 ); 400 $groups->Limit( 401 ALIAS => $attrs, 402 FIELD => 'Name', 403 VALUE => 'Dashboard', 404 ); 405 push @objects, @{ $groups->ItemsArrayRef }; 406 407 # Finally, if you have been granted the SeeDashboard right (which 408 # you could have by way of global user right or global group right), 409 # you can see system dashboards. 410 push @objects, RT::System->new($CurrentUser) 411 if $CurrentUser->HasRight(Object => $RT::System, Right => 'SeeDashboard'); 412 413 return @objects; 414} 415 416sub CurrentUserCanCreateAny { 417 my $self = shift; 418 my @objects; 419 420 my $CurrentUser = $self->CurrentUser; 421 return 1 422 if $CurrentUser->HasRight(Object => $RT::System, Right => 'CreateOwnDashboard'); 423 424 my $groups = RT::Groups->new($CurrentUser); 425 $groups->LimitToUserDefinedGroups; 426 $groups->ForWhichCurrentUserHasRight( 427 Right => 'CreateGroupDashboard', 428 IncludeSuperusers => 1, 429 ); 430 return 1 if $groups->Count; 431 432 return 1 433 if $CurrentUser->HasRight(Object => $RT::System, Right => 'CreateDashboard'); 434 435 return 0; 436} 437 438=head2 Delete 439 440Deletes the dashboard and related subscriptions. 441Returns a tuple of status and message, where status is true upon success. 442 443=cut 444 445sub Delete { 446 my $self = shift; 447 my $id = $self->id; 448 my ( $status, $msg ) = $self->SUPER::Delete(@_); 449 if ( $status ) { 450 # delete all the subscriptions 451 my $subscriptions = RT::Attributes->new( RT->SystemUser ); 452 $subscriptions->Limit( 453 FIELD => 'Name', 454 VALUE => 'Subscription', 455 ); 456 $subscriptions->Limit( 457 FIELD => 'Description', 458 VALUE => "Subscription to dashboard $id", 459 ); 460 while ( my $subscription = $subscriptions->Next ) { 461 $subscription->Delete(); 462 } 463 } 464 465 return ( $status, $msg ); 466} 467 468RT::Base->_ImportOverlays(); 469 4701; 471