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