6dd21cb33d928bb488e25f5c5231c0ddb267ba38
[usit-rt.git] / lib / RT / SharedSetting.pm
1 # BEGIN BPS TAGGED BLOCK {{{
2 #
3 # COPYRIGHT:
4 #
5 # This software is Copyright (c) 1996-2013 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::SharedSetting - an API for settings that belong to an RT::User or RT::Group
52
53 =head1 SYNOPSIS
54
55   use RT::SharedSetting;
56
57 =head1 DESCRIPTION
58
59 A RT::SharedSetting is an object that can belong to an L<RT::User> or an <RT::Group>.
60 It consists of an ID, a name, and some arbitrary data.
61
62 =cut
63
64 package RT::SharedSetting;
65 use strict;
66 use warnings;
67
68 use RT::Attribute;
69 use Scalar::Util 'blessed';
70 use base qw/RT::Base/;
71
72 =head1 METHODS
73
74 =head2 new
75
76 Returns a new L<RT::SharedSetting> object.
77 Takes the current user, see also L<RT::Base>.
78
79 =cut
80
81 sub new  {
82     my $proto = shift;
83     my $class = ref($proto) || $proto;
84     my $self  = {};
85     $self->{'Id'} = 0;
86     bless ($self, $class);
87     $self->CurrentUser(@_);
88     return $self;
89 }
90
91 =head2 Load
92
93 Takes a privacy specification and a shared-setting ID.  Loads the given object
94 ID if it belongs to the stated user or group. Calls the L</PostLoad> method on
95 success for any further initialization. Returns a tuple of status and message,
96 where status is true on success.
97
98 =cut
99
100 sub Load {
101     my $self = shift;
102     my ($privacy, $id) = @_;
103     my $object = $self->_GetObject($privacy);
104
105     if ($object) {
106         $self->{'Attribute'} = $object->Attributes->WithId($id);
107         if ($self->{'Attribute'}->Id) {
108             $self->{'Id'} = $self->{'Attribute'}->Id;
109             $self->{'Privacy'} = $privacy;
110             $self->PostLoad();
111
112             return (0, $self->loc("Permission denied"))
113                 unless $self->CurrentUserCanSee;
114
115             my ($ok, $msg) = $self->PostLoadValidate;
116             return ($ok, $msg) if !$ok;
117
118             return (1, $self->loc("Loaded [_1] [_2]", $self->ObjectName, $self->Name));
119         } else {
120             $RT::Logger->error("Could not load attribute " . $id
121                     . " for object " . $privacy);
122             return (0, $self->loc("Failed to load [_1] [_2]", $self->ObjectName, $id))
123         }
124     } else {
125         $RT::Logger->warning("Could not load object $privacy when loading " . $self->ObjectName);
126         return (0, $self->loc("Could not load object for [_1]", $privacy));
127     }
128 }
129
130 =head2 LoadById
131
132 First loads up the L<RT::Attribute> for this shared setting by ID, then calls
133 L</Load> with the correct parameters. Returns a tuple of status and message,
134 where status is true on success.
135
136 =cut
137
138 sub LoadById {
139     my $self = shift;
140     my $id   = shift;
141
142     my $attr = RT::Attribute->new($self->CurrentUser);
143     my ($ok, $msg) = $attr->LoadById($id);
144
145     if (!$ok) {
146         return (0, $self->loc("Failed to load [_1] [_2]: [_3]", $self->ObjectName, $id, $msg))
147     }
148
149     my $privacy = $self->_build_privacy($attr->ObjectType, $attr->ObjectId);
150     return (0, $self->loc("Bad privacy for attribute [_1]", $id))
151         if !$privacy;
152
153     return $self->Load($privacy, $id);
154 }
155
156 =head2 PostLoad
157
158 Called after a successful L</Load>.
159
160 =cut
161
162 sub PostLoad { }
163
164 =head2 PostLoadValidate
165
166 Called just before returning success from L</Load>; may be used to validate
167 that the record is correct. This method is expected to return a (ok, msg)
168 pair.
169
170 =cut
171
172 sub PostLoadValidate {
173     return 1;
174 }
175
176 =head2 Save
177
178 Creates a new shared setting. Takes a privacy, a name, and any other arguments.
179 Saves the given parameters to the appropriate user/group object, and loads the
180 resulting object. Arguments are passed to the L</SaveAttribute> method, which
181 does the actual update. Returns a tuple of status and message, where status is
182 true on success. Defaults are:
183
184   Privacy:  CurrentUser only
185   Name:     "new (ObjectName)"
186
187 =cut
188
189 sub Save {
190     my $self = shift;
191     my %args = (
192         'Privacy' => 'RT::User-' . $self->CurrentUser->Id,
193         'Name'    => "new " . $self->ObjectName,
194                 @_,
195     );
196
197     my $privacy = $args{'Privacy'};
198     my $name    = $args{'Name'},
199     my $object  = $self->_GetObject($privacy);
200
201     return (0, $self->loc("Failed to load object for [_1]", $privacy))
202         unless $object;
203
204     return (0, $self->loc("Permission denied"))
205         unless $self->CurrentUserCanCreate($privacy);
206
207     my ($att_id, $att_msg) = $self->SaveAttribute($object, \%args);
208
209     if ($att_id) {
210         $self->{'Attribute'} = $object->Attributes->WithId($att_id);
211         $self->{'Id'}        = $att_id;
212         $self->{'Privacy'}   = $privacy;
213         return ( 1, $self->loc( "Saved [_1] [_2]", $self->ObjectName, $name ) );
214     }
215     else {
216         $RT::Logger->error($self->ObjectName . " save failure: $att_msg");
217         return ( 0, $self->loc("Failed to create [_1] attribute", $self->ObjectName) );
218     }
219 }
220
221 =head2 SaveAttribute
222
223 An empty method for subclassing. Called from L</Save> method.
224
225 =cut
226
227 sub SaveAttribute { }
228
229 =head2 Update
230
231 Updates the parameters of an existing shared setting. Any arguments are passed
232 to the L</UpdateAttribute> method. Returns a tuple of status and message, where
233 status is true on success.
234
235 =cut
236
237 sub Update {
238     my $self = shift;
239     my %args = @_;
240
241     return(0, $self->loc("No [_1] loaded", $self->ObjectName)) unless $self->Id;
242     return(0, $self->loc("Could not load [_1] attribute", $self->ObjectName))
243         unless $self->{'Attribute'}->Id;
244
245     return (0, $self->loc("Permission denied"))
246         unless $self->CurrentUserCanModify;
247
248     my ($status, $msg) = $self->UpdateAttribute(\%args);
249
250     return (1, $self->loc("[_1] update: Nothing changed", ucfirst($self->ObjectName)))
251         if !defined $msg;
252
253     # prevent useless warnings
254     return (1, $self->loc("[_1] updated"), ucfirst($self->ObjectName))
255         if $msg =~ /That is already the current value/;
256
257     return ($status, $self->loc("[_1] update: [_2]", ucfirst($self->ObjectName), $msg));
258 }
259
260 =head2 UpdateAttribute
261
262 An empty method for subclassing. Called from L</Update> method.
263
264 =cut
265
266 sub UpdateAttribute { }
267
268 =head2 Delete
269     
270 Deletes the existing shared setting. Returns a tuple of status and message,
271 where status is true upon success.
272
273 =cut
274
275 sub Delete {
276     my $self = shift;
277     return (0, $self->loc("Permission denied"))
278         unless $self->CurrentUserCanDelete;
279
280     my ($status, $msg) = $self->{'Attribute'}->Delete;
281     $self->CurrentUser->ClearAttributes; # force the current user's attribute cache to be cleaned up
282     if ($status) {
283         return (1, $self->loc("Deleted [_1]", $self->ObjectName));
284     } else {
285         return (0, $self->loc("Delete failed: [_1]", $msg));
286     }
287 }
288
289 ### Accessor methods
290
291 =head2 Name
292
293 Returns the name of this shared setting.
294
295 =cut
296
297 sub Name {
298     my $self = shift;
299     return unless ref($self->{'Attribute'}) eq 'RT::Attribute';
300     return $self->{'Attribute'}->Description();
301 }
302
303 =head2 Id
304
305 Returns the numerical ID of this shared setting.
306
307 =cut
308
309 sub Id {
310     my $self = shift;
311     return $self->{'Id'};
312 }
313
314 *id = \&Id;
315
316
317 =head2 Privacy
318
319 Returns the principal object to whom this shared setting belongs, in a string
320 "<class>-<id>", e.g. "RT::Group-16".
321
322 =cut
323
324 sub Privacy {
325     my $self = shift;
326     return $self->{'Privacy'};
327 }
328
329 =head2 GetParameter
330
331 Returns the given named parameter of the setting.
332
333 =cut
334
335 sub GetParameter {
336     my $self = shift;
337     my $param = shift;
338     return unless ref($self->{'Attribute'}) eq 'RT::Attribute';
339     return $self->{'Attribute'}->SubValue($param);
340 }
341
342 =head2 IsVisibleTo Privacy
343
344 Returns true if the setting is visible to all principals of the given privacy.
345 This does not deal with ACLs, this only looks at membership.
346
347 =cut
348
349 sub IsVisibleTo {
350     my $self    = shift;
351     my $to      = shift;
352     my $privacy = $self->Privacy || '';
353
354     # if the privacies are the same, then they can be seen. this handles
355     # a personal setting being visible to that user.
356     return 1 if $privacy eq $to;
357
358     # If the setting is systemwide, then any user can see it.
359     return 1 if $privacy =~ /^RT::System/;
360
361     # Only privacies that are RT::System can be seen by everyone.
362     return 0 if $to =~ /^RT::System/;
363
364     # If the setting is group-wide...
365     if ($privacy =~ /^RT::Group-(\d+)$/) {
366         my $setting_group = RT::Group->new($self->CurrentUser);
367         $setting_group->Load($1);
368
369         if ($to =~ /-(\d+)$/) {
370             my $to_id = $1;
371
372             # then any principal that is a member of the setting's group can see
373             # the setting
374             return $setting_group->HasMemberRecursively($to_id);
375         }
376     }
377
378     return 0;
379 }
380
381 sub CurrentUserCanSee    { 1 }
382 sub CurrentUserCanCreate { 1 }
383 sub CurrentUserCanModify { 1 }
384 sub CurrentUserCanDelete { 1 }
385
386 ### Internal methods
387
388 # _GetObject: helper routine to load the correct object whose parameters
389 #  have been passed.
390
391 sub _GetObject {
392     my $self = shift;
393     my $privacy = shift;
394
395     # short circuit: if they pass the object we want anyway, just return it
396     if (blessed($privacy) && $privacy->isa('RT::Record')) {
397         return $privacy;
398     }
399
400     my ($obj_type, $obj_id) = split(/\-/, ($privacy || ''));
401
402     unless ($obj_type && $obj_id) {
403         $privacy = '(undef)' if !defined($privacy);
404         $RT::Logger->debug("Invalid privacy string '$privacy'");
405         return undef;
406     }
407
408     my $object = $self->_load_privacy_object($obj_type, $obj_id);
409
410     unless (ref($object) eq $obj_type) {
411         $RT::Logger->error("Could not load object of type $obj_type with ID $obj_id, got object of type " . (ref($object) || 'undef'));
412         return undef;
413     }
414
415     # Do not allow the loading of a user object other than the current
416     # user, or of a group object of which the current user is not a member.
417
418     if ($obj_type eq 'RT::User' && $object->Id != $self->CurrentUser->UserObj->Id) {
419         $RT::Logger->debug("Permission denied for user other than self");
420         return undef;
421     }
422
423     if (   $obj_type eq 'RT::Group'
424         && !$object->HasMemberRecursively($self->CurrentUser->PrincipalObj)
425         && !$self->CurrentUser->HasRight( Object => $RT::System, Right => 'SuperUser' ) ) {
426         $RT::Logger->debug("Permission denied, ".$self->CurrentUser->Name.
427                            " is not a member of group");
428         return undef;
429     }
430
431     return $object;
432 }
433
434 sub _load_privacy_object {
435     my ($self, $obj_type, $obj_id) = @_;
436     if ( $obj_type eq 'RT::User' ) {
437         if ( $obj_id == $self->CurrentUser->Id ) {
438             return $self->CurrentUser->UserObj;
439         } else {
440             $RT::Logger->warning("User #". $self->CurrentUser->Id ." tried to load container user #". $obj_id);
441             return undef;
442         }
443     }
444     elsif ($obj_type eq 'RT::Group') {
445         my $group = RT::Group->new($self->CurrentUser);
446         $group->Load($obj_id);
447         return $group;
448     }
449     elsif ($obj_type eq 'RT::System') {
450         return RT::System->new($self->CurrentUser);
451     }
452
453     $RT::Logger->error(
454         "Tried to load a ". $self->ObjectName 
455         ." belonging to an $obj_type, which is neither a user nor a group"
456     );
457
458     return undef;
459 }
460
461 sub _build_privacy {
462     my ($self, $obj_type, $obj_id) = @_;
463
464     # allow passing in just an object to find its privacy string
465     if (ref($obj_type)) {
466         my $Object = $obj_type;
467         return $Object->isa('RT::User')   ? 'RT::User-'   . $Object->Id
468              : $Object->isa('RT::Group')  ? 'RT::Group-'  . $Object->Id
469              : $Object->isa('RT::System') ? 'RT::System-' . $Object->Id
470              : undef;
471     }
472
473     return undef unless ($obj_type);  # undef workaround
474     return $obj_type eq 'RT::User'   ? "$obj_type-$obj_id"
475          : $obj_type eq 'RT::Group'  ? "$obj_type-$obj_id"
476          : $obj_type eq 'RT::System' ? "$obj_type-$obj_id"
477          : undef;
478 }
479
480 =head2 ObjectsForLoading
481
482 Returns a list of objects that can be used to load this shared setting. It
483 is ACL checked.
484
485 =cut
486
487 sub ObjectsForLoading {
488     my $self = shift;
489     return grep { $self->CurrentUserCanSee($_) } $self->_PrivacyObjects;
490 }
491
492 =head2 ObjectsForCreating
493
494 Returns a list of objects that can be used to create this shared setting. It
495 is ACL checked.
496
497 =cut
498
499 sub ObjectsForCreating {
500     my $self = shift;
501     return grep { $self->CurrentUserCanCreate($_) } $self->_PrivacyObjects;
502 }
503
504 =head2 ObjectsForModifying
505
506 Returns a list of objects that can be used to modify this shared setting. It
507 is ACL checked.
508
509 =cut
510
511 sub ObjectsForModifying {
512     my $self = shift;
513     return grep { $self->CurrentUserCanModify($_) } $self->_PrivacyObjects;
514 }
515
516 RT::Base->_ImportOverlays();
517
518 1;