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