import rt 3.6.4
[freeside.git] / rt / lib / RT / Attribute_Overlay.pm
1 # BEGIN BPS TAGGED BLOCK {{{
2
3 # COPYRIGHT:
4 #  
5 # This software is Copyright (c) 1996-2007 Best Practical Solutions, LLC 
6 #                                          <jesse@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/copyleft/gpl.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 package RT::Attribute;
49
50 use strict;
51 no warnings qw(redefine);
52 use Storable qw/nfreeze thaw/;
53 use MIME::Base64;
54
55
56 =head1 NAME
57
58   RT::Attribute_Overlay 
59
60 =head1 Content
61
62 =cut
63
64 # the acl map is a map of "name of attribute" and "what right the user must have on the associated object to see/edit it
65
66 our $ACL_MAP = {
67     SavedSearch => { create => 'EditSavedSearches',
68                      update => 'EditSavedSearches',
69                      delete => 'EditSavedSearches',
70                      display => 'ShowSavedSearches' },
71
72 };
73
74 # There are a number of attributes that users should be able to modify for themselves, such as saved searches
75 #  we could do this with a different set of "modify" rights, but that gets very hacky very fast. this is even faster and even
76 # hackier. we're hardcoding that a different set of rights are needed for attributes on oneself
77 our $PERSONAL_ACL_MAP = { 
78     SavedSearch => { create => 'ModifySelf',
79                      update => 'ModifySelf',
80                      delete => 'ModifySelf',
81                      display => 'allow' },
82
83 };
84
85 =head2 LookupObjectRight { ObjectType => undef, ObjectId => undef, Name => undef, Right => { create, update, delete, display } }
86
87 Returns the right that the user needs to have on this attribute's object to perform the related attribute operation. Returns "allow" if the right is otherwise unspecified.
88
89 =cut
90
91 sub LookupObjectRight { 
92     my $self = shift;
93     my %args = ( ObjectType => undef,
94                  ObjectId => undef,
95                  Right => undef,
96                  Name => undef,
97                  @_);
98
99     # if it's an attribute on oneself, check the personal acl map
100     if (($args{'ObjectType'} eq 'RT::User') && ($args{'ObjectId'} eq $self->CurrentUser->Id)) {
101     return('allow') unless ($PERSONAL_ACL_MAP->{$args{'Name'}});
102     return('allow') unless ($PERSONAL_ACL_MAP->{$args{'Name'}}->{$args{'Right'}});
103     return($PERSONAL_ACL_MAP->{$args{'Name'}}->{$args{'Right'}}); 
104
105     }
106    # otherwise check the main ACL map
107     else {
108     return('allow') unless ($ACL_MAP->{$args{'Name'}});
109     return('allow') unless ($ACL_MAP->{$args{'Name'}}->{$args{'Right'}});
110     return($ACL_MAP->{$args{'Name'}}->{$args{'Right'}}); 
111     }
112 }
113
114
115
116
117 =head2 Create PARAMHASH
118
119 Create takes a hash of values and creates a row in the database:
120
121   varchar(200) 'Name'.
122   varchar(255) 'Content'.
123   varchar(16) 'ContentType',
124   varchar(64) 'ObjectType'.
125   int(11) 'ObjectId'.
126
127 You may pass a C<Object> instead of C<ObjectType> and C<ObjectId>.
128
129 =cut
130
131
132
133
134 sub Create {
135     my $self = shift;
136     my %args = ( 
137                 Name => '',
138                 Description => '',
139                 Content => '',
140                 ContentType => '',
141                 Object => undef,
142                   @_);
143
144     if ($args{Object} and UNIVERSAL::can($args{Object}, 'Id')) {
145             $args{ObjectType} = ref($args{Object});
146             $args{ObjectId} = $args{Object}->Id;
147     } else {
148         return(0, $self->loc("Required parameter '[_1]' not specified", 'Object'));
149
150     }
151    
152     # object_right is the right that the user has to have on the object for them to have $right on this attribute
153     my $object_right = $self->LookupObjectRight(
154         Right      => 'create',
155         ObjectId   => $args{'ObjectId'},
156         ObjectType => $args{'ObjectType'},
157         Name       => $args{'Name'}
158     );
159     if ($object_right eq 'deny') { 
160         return (0, $self->loc('Permission Denied'));
161     } 
162     elsif ($object_right eq 'allow') {
163         # do nothing, we're ok
164     }
165     elsif (!$self->CurrentUser->HasRight( Object => $args{Object}, Right => $object_right)) {
166         return (0, $self->loc('Permission Denied'));
167     }
168
169    
170     if (ref ($args{'Content'}) ) { 
171         eval  {$args{'Content'} = $self->_SerializeContent($args{'Content'}); };
172         if ($@) {
173          return(0, $@);
174         }
175         $args{'ContentType'} = 'storable';
176     }
177
178     
179     $self->SUPER::Create(
180                          Name => $args{'Name'},
181                          Content => $args{'Content'},
182                          ContentType => $args{'ContentType'},
183                          Description => $args{'Description'},
184                          ObjectType => $args{'ObjectType'},
185                          ObjectId => $args{'ObjectId'},
186 );
187
188 }
189
190
191 # {{{ sub LoadByNameAndObject
192
193 =head2  LoadByNameAndObject (Object => OBJECT, Name => NAME)
194
195 Loads the Attribute named NAME for Object OBJECT.
196
197 =cut
198
199 sub LoadByNameAndObject {
200     my $self = shift;
201     my %args = (
202         Object => undef,
203         Name  => undef,
204         @_,
205     );
206
207     return (
208         $self->LoadByCols(
209             Name => $args{'Name'},
210             ObjectType => ref($args{'Object'}),
211             ObjectId => $args{'Object'}->Id,
212         )
213     );
214
215 }
216
217 # }}}
218
219
220 =head2 _DeserializeContent
221
222 DeserializeContent returns this Attribute's "Content" as a hashref.
223
224
225 =cut
226
227 sub _DeserializeContent {
228     my $self = shift;
229     my $content = shift;
230
231     my $hashref;
232     eval {$hashref  = thaw(decode_base64($content))} ; 
233     if ($@) {
234         $RT::Logger->error("Deserialization of attribute ".$self->Id. " failed");
235     }
236
237     return($hashref);
238
239 }
240
241
242 =head2 Content
243
244 Returns this attribute's content. If it's a scalar, returns a scalar
245 If it's data structure returns a ref to that data structure.
246
247 =cut
248
249 sub Content {
250     my $self = shift;
251     # Here we call _Value to get the ACL check.
252     my $content = $self->_Value('Content');
253     if ($self->__Value('ContentType') eq 'storable') {
254         eval {$content = $self->_DeserializeContent($content); };
255         if ($@) {
256             $RT::Logger->error("Deserialization of content for attribute ".$self->Id. " failed. Attribute was: ".$content);
257         }
258     } 
259
260     return($content);
261
262 }
263
264 sub _SerializeContent {
265     my $self = shift;
266     my $content = shift;
267         return( encode_base64(nfreeze($content))); 
268 }
269
270
271 sub SetContent {
272     my $self = shift;
273     my $content = shift;
274     
275     # Call __Value to avoid ACL check.
276     if ($self->__Value('ContentType') eq 'storable') {
277     # We eval the serialization because it will lose on a coderef.
278     eval  {$content = $self->_SerializeContent($content); };
279     if ($@) {
280         $RT::Logger->error("For some reason, content couldn't be frozen");
281         return(0, $@);
282     }
283     }
284     return ($self->SUPER::SetContent($content));
285 }
286
287 =head2 SubValue KEY
288
289 Returns the subvalue for $key.
290
291 =begin testing
292
293 my $user = $RT::SystemUser;
294 my ($id, $msg) =  $user->AddAttribute(Name => 'SavedSearch', Content => { Query => 'Foo'} );
295 ok ($id, $msg);
296 my $attr = RT::Attribute->new($RT::SystemUser);
297 $attr->Load($id);
298 ok($attr->Name eq 'SavedSearch');
299 $attr->SetSubValues( Format => 'baz');
300
301 my $format = $attr->SubValue('Format');
302 is ($format , 'baz');
303
304 $attr->SetSubValues( Format => 'bar');
305 $format = $attr->SubValue('Format');
306 is ($format , 'bar');
307
308 $attr->DeleteAllSubValues();
309 $format = $attr->SubValue('Format');
310 is ($format, undef);
311
312 $attr->SetSubValues(Format => 'This is a format');
313
314 my $attr2 = RT::Attribute->new($RT::SystemUser);
315 $attr2->Load($id);
316 is ($attr2->SubValue('Format'), 'This is a format');
317 $attr2->Delete;
318 my $attr3 = RT::Attribute->new($RT::SystemUser);
319 my ($id) = $attr3->Load($id);
320 is ($id, 0);
321
322 =end testing
323
324 =cut
325
326 sub SubValue {
327     my $self = shift;
328     my $key = shift;
329     my $values = $self->Content();
330     return undef unless ref($values);
331     return($values->{$key});
332 }
333
334 =head2 DeleteSubValue NAME
335
336 Deletes the subvalue with the key NAME
337
338 =cut
339
340 sub DeleteSubValue {
341     my $self = shift;
342     my $key = shift;
343     my %values = $self->Content();
344     delete $values{$key};
345     $self->SetContent(%values);
346
347     
348
349 }
350
351
352 =head2 DeleteAllSubValues 
353
354 Deletes all subvalues for this attribute
355
356 =cut
357
358
359 sub DeleteAllSubValues {
360     my $self = shift; 
361     $self->SetContent({});
362 }
363
364 =head2 SetSubValues  {  }
365
366 Takes a hash of keys and values and stores them in the content of this attribute.
367
368 Each key B<replaces> the existing key with the same name
369
370 Returns a tuple of (status, message)
371
372 =cut
373
374
375 sub SetSubValues {
376    my $self = shift;
377    my %args = (@_); 
378    my $values = ($self->Content() || {} );
379    foreach my $key (keys %args) {
380     $values->{$key} = $args{$key};
381    }
382
383    $self->SetContent($values);
384
385 }
386
387
388 sub Object {
389     my $self = shift;
390     my $object_type = $self->__Value('ObjectType');
391     my $object;
392     eval { $object = $object_type->new($self->CurrentUser) };
393     unless(UNIVERSAL::isa($object, $object_type)) {
394         $RT::Logger->error("Attribute ".$self->Id." has a bogus object type - $object_type (".$@.")");
395         return(undef);
396      }
397     $object->Load($self->__Value('ObjectId'));
398
399     return($object);
400
401 }
402
403
404 sub Delete {
405     my $self = shift;
406     unless ($self->CurrentUserHasRight('delete')) {
407         return (0,$self->loc('Permission Denied'));
408     }
409     return($self->SUPER::Delete(@_));
410 }
411
412
413 sub _Value {
414     my $self = shift;
415     unless ($self->CurrentUserHasRight('display')) {
416         return (0,$self->loc('Permission Denied'));
417     }
418
419     return($self->SUPER::_Value(@_));
420
421
422 }
423
424
425 sub _Set {
426     my $self = shift;
427     unless ($self->CurrentUserHasRight('modify')) {
428
429         return (0,$self->loc('Permission Denied'));
430     }
431     return($self->SUPER::_Set(@_));
432
433 }
434
435
436 =head2 CurrentUserHasRight
437
438 One of "display" "modify" "delete" or "create" and returns 1 if the user has that right for attributes of this name for this object.Returns undef otherwise.
439
440 =cut
441
442 sub CurrentUserHasRight {
443     my $self = shift;
444     my $right = shift;
445
446     # object_right is the right that the user has to have on the object for them to have $right on this attribute
447     my $object_right = $self->LookupObjectRight(
448         Right      => $right,
449         ObjectId   => $self->__Value('ObjectId'),
450         ObjectType => $self->__Value('ObjectType'),
451         Name       => $self->__Value('Name')
452     );
453    
454     return (1) if ($object_right eq 'allow');
455     return (0) if ($object_right eq 'deny');
456     return(1) if ($self->CurrentUser->HasRight( Object => $self->Object, Right => $object_right));
457     return(0);
458
459 }
460
461
462 =head1 TODO
463
464 We should be deserializing the content on load and then enver again, rather than at every access
465
466 =cut
467
468
469 1;