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