Upgrade to 4.2.8
[usit-rt.git] / lib / RT / Dashboard.pm
1 # BEGIN BPS TAGGED BLOCK {{{
2 #
3 # COPYRIGHT:
4 #
5 # This software is Copyright (c) 1996-2014 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
68 package RT::Dashboard;
69
70 use strict;
71 use warnings;
72
73 use base qw/RT::SharedSetting/;
74
75 use RT::SavedSearch;
76
77 use 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
93 An object of this class is called "dashboard"
94
95 =cut
96
97 sub ObjectName { "dashboard" } # loc
98
99 sub 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
111 sub 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
146 Ensure that the ID corresponds to an actual dashboard object, since it's all
147 attributes under the hood.
148
149 =cut
150
151 sub 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
159 Returns a hashref of pane name to portlets
160
161 =cut
162
163 sub 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
171 Returns the list of this dashboard's portlets, each a hashref with key
172 C<portlet_type> being C<search> or C<component>.
173
174 =cut
175
176 sub Portlets {
177     my $self = shift;
178     return map { @$_ } values %{ $self->Panes };
179 }
180
181 =head2 Dashboards
182
183 Returns a list of loaded sub-dashboards
184
185 =cut
186
187 sub 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
198 Returns a list of loaded saved searches
199
200 =cut
201
202 sub 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
213 Returns an array for one saved search, suitable for passing to
214 /Elements/ShowSearch.
215
216 =cut
217
218 sub 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
231 This will return a list of saved searches that are potentially not visible by
232 all users for whom the dashboard is visible. You may pass in a privacy to
233 use instead of the dashboard's privacy.
234
235 =cut
236
237 sub 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
248 sub _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->WithMember( PrincipalId => $CurrentUser->Id,
259                          Recursively => 1 );
260     push @objects, @{ $groups->ItemsArrayRef };
261
262     push @objects, RT::System->new($CurrentUser);
263
264     return @objects;
265 }
266
267 # ACLs
268
269 sub _CurrentUserCan {
270     my $self    = shift;
271     my $privacy = shift || $self->Privacy;
272     my %args    = @_;
273
274     if (!defined($privacy)) {
275         $RT::Logger->debug("No privacy provided to $self->_CurrentUserCan");
276         return 0;
277     }
278
279     my $object = $self->_GetObject($privacy);
280     return 0 unless $object;
281
282     my $level;
283
284        if ($object->isa('RT::User'))   { $level = 'Own' }
285     elsif ($object->isa('RT::Group'))  { $level = 'Group' }
286     elsif ($object->isa('RT::System')) { $level = '' }
287     else {
288         $RT::Logger->error("Unknown object $object from privacy $privacy");
289         return 0;
290     }
291
292     # users are mildly special-cased, since we actually have to check that
293     # the user is operating on himself
294     if ($object->isa('RT::User')) {
295         return 0 unless $object->Id == $self->CurrentUser->Id;
296     }
297
298     my $right = $args{FullRight}
299              || join('', $args{Right}, $level, 'Dashboard');
300
301     # all rights, except group rights, are global
302     $object = $RT::System unless $object->isa('RT::Group');
303
304     return $self->CurrentUser->HasRight(
305         Right  => $right,
306         Object => $object,
307     );
308 }
309
310 sub CurrentUserCanSee {
311     my $self    = shift;
312     my $privacy = shift;
313
314     $self->_CurrentUserCan($privacy, Right => 'See');
315 }
316
317 sub CurrentUserCanCreate {
318     my $self    = shift;
319     my $privacy = shift;
320
321     $self->_CurrentUserCan($privacy, Right => 'Create');
322 }
323
324 sub CurrentUserCanModify {
325     my $self    = shift;
326     my $privacy = shift;
327
328     $self->_CurrentUserCan($privacy, Right => 'Modify');
329 }
330
331 sub CurrentUserCanDelete {
332     my $self    = shift;
333     my $privacy = shift;
334
335     $self->_CurrentUserCan($privacy, Right => 'Delete');
336 }
337
338 sub CurrentUserCanSubscribe {
339     my $self    = shift;
340     my $privacy = shift;
341
342     $self->_CurrentUserCan($privacy, FullRight => 'SubscribeDashboard');
343 }
344
345 =head2 Subscription
346
347 Returns the L<RT::Attribute> representing the current user's subscription
348 to this dashboard if there is one; otherwise, returns C<undef>.
349
350 =cut
351
352 sub Subscription {
353     my $self = shift;
354
355     # no subscription to unloaded dashboards
356     return unless $self->id;
357
358     for my $sub ($self->CurrentUser->UserObj->Attributes->Named('Subscription')) {
359         return $sub if $sub->SubValue('DashboardId') == $self->id;
360     }
361
362     return;
363 }
364
365 sub ObjectsForLoading {
366     my $self = shift;
367     my %args = (
368         IncludeSuperuserGroups => 1,
369         @_
370     );
371     my @objects;
372
373     # If you've been granted the SeeOwnDashboard global right (which you
374     # could have by way of global user right or global group right), you
375     # get to see your own dashboards
376     my $CurrentUser = $self->CurrentUser;
377     push @objects, $CurrentUser->UserObj
378         if $CurrentUser->HasRight(Object => $RT::System, Right => 'SeeOwnDashboard');
379
380     # Find groups for which: (a) you are a member of the group, and (b)
381     # you have been granted SeeGroupDashboard on (by any means), and (c)
382     # have at least one dashboard
383     my $groups = RT::Groups->new($CurrentUser);
384     $groups->LimitToUserDefinedGroups;
385     $groups->ForWhichCurrentUserHasRight(
386         Right             => 'SeeGroupDashboard',
387         IncludeSuperusers => $args{IncludeSuperuserGroups},
388     );
389     $groups->WithMember(
390         Recursively => 1,
391         PrincipalId => $CurrentUser->UserObj->PrincipalId
392     );
393     my $attrs = $groups->Join(
394         ALIAS1 => 'main',
395         FIELD1 => 'id',
396         TABLE2 => 'Attributes',
397         FIELD2 => 'ObjectId',
398     );
399     $groups->Limit(
400         ALIAS => $attrs,
401         FIELD => 'ObjectType',
402         VALUE => 'RT::Group',
403     );
404     $groups->Limit(
405         ALIAS => $attrs,
406         FIELD => 'Name',
407         VALUE => 'Dashboard',
408     );
409     push @objects, @{ $groups->ItemsArrayRef };
410
411     # Finally, if you have been granted the SeeDashboard right (which
412     # you could have by way of global user right or global group right),
413     # you can see system dashboards.
414     push @objects, RT::System->new($CurrentUser)
415         if $CurrentUser->HasRight(Object => $RT::System, Right => 'SeeDashboard');
416
417     return @objects;
418 }
419
420 sub CurrentUserCanCreateAny {
421     my $self = shift;
422     my @objects;
423
424     my $CurrentUser = $self->CurrentUser;
425     return 1
426         if $CurrentUser->HasRight(Object => $RT::System, Right => 'CreateOwnDashboard');
427
428     my $groups = RT::Groups->new($CurrentUser);
429     $groups->LimitToUserDefinedGroups;
430     $groups->ForWhichCurrentUserHasRight(
431         Right             => 'CreateGroupDashboard',
432         IncludeSuperusers => 1,
433     );
434     return 1 if $groups->Count;
435
436     return 1
437         if $CurrentUser->HasRight(Object => $RT::System, Right => 'CreateDashboard');
438
439     return 0;
440 }
441
442 =head2 Delete
443
444 Deletes the dashboard and related subscriptions.
445 Returns a tuple of status and message, where status is true upon success.
446
447 =cut
448
449 sub Delete {
450     my $self = shift;
451     my $id = $self->id;
452     my ( $status, $msg ) = $self->SUPER::Delete(@_);
453     if ( $status ) {
454         # delete all the subscriptions
455         my $subscriptions = RT::Attributes->new( RT->SystemUser );
456         $subscriptions->Limit(
457             FIELD => 'Name',
458             VALUE => 'Subscription',
459         );
460         $subscriptions->Limit(
461             FIELD => 'Description',
462             VALUE => "Subscription to dashboard $id",
463         );
464         while ( my $subscription = $subscriptions->Next ) {
465             $subscription->Delete();
466         }
467     }
468
469     return ( $status, $msg );
470 }
471
472 RT::Base->_ImportOverlays();
473
474 1;