rt 4.0.23
[freeside.git] / rt / lib / RT / Attribute.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 package RT::Attribute;
50
51 use strict;
52 use warnings;
53
54 use base 'RT::Record';
55
56 sub Table {'Attributes'}
57
58 use Storable qw/nfreeze thaw/;
59 use MIME::Base64;
60
61
62 =head1 NAME
63
64   RT::Attribute_Overlay 
65
66 =head1 Content
67
68 =cut
69
70 # 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
71
72 our $ACL_MAP = {
73     SavedSearch => { create => 'EditSavedSearches',
74                      update => 'EditSavedSearches',
75                      delete => 'EditSavedSearches',
76                      display => 'ShowSavedSearches' },
77
78 };
79
80 # There are a number of attributes that users should be able to modify for themselves, such as saved searches
81 #  we could do this with a different set of "update" rights, but that gets very hacky very fast. this is even faster and even
82 # hackier. we're hardcoding that a different set of rights are needed for attributes on oneself
83 our $PERSONAL_ACL_MAP = { 
84     SavedSearch => { create => 'ModifySelf',
85                      update => 'ModifySelf',
86                      delete => 'ModifySelf',
87                      display => 'allow' },
88
89 };
90
91 =head2 LookupObjectRight { ObjectType => undef, ObjectId => undef, Name => undef, Right => { create, update, delete, display } }
92
93 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.
94
95 =cut
96
97 sub LookupObjectRight { 
98     my $self = shift;
99     my %args = ( ObjectType => undef,
100                  ObjectId => undef,
101                  Right => undef,
102                  Name => undef,
103                  @_);
104
105     # if it's an attribute on oneself, check the personal acl map
106     if (($args{'ObjectType'} eq 'RT::User') && ($args{'ObjectId'} eq $self->CurrentUser->Id)) {
107     return('allow') unless ($PERSONAL_ACL_MAP->{$args{'Name'}});
108     return('allow') unless ($PERSONAL_ACL_MAP->{$args{'Name'}}->{$args{'Right'}});
109     return($PERSONAL_ACL_MAP->{$args{'Name'}}->{$args{'Right'}}); 
110
111     }
112    # otherwise check the main ACL map
113     else {
114     return('allow') unless ($ACL_MAP->{$args{'Name'}});
115     return('allow') unless ($ACL_MAP->{$args{'Name'}}->{$args{'Right'}});
116     return($ACL_MAP->{$args{'Name'}}->{$args{'Right'}}); 
117     }
118 }
119
120
121
122
123 =head2 Create PARAMHASH
124
125 Create takes a hash of values and creates a row in the database:
126
127   varchar(200) 'Name'.
128   varchar(255) 'Content'.
129   varchar(16) 'ContentType',
130   varchar(64) 'ObjectType'.
131   int(11) 'ObjectId'.
132
133 You may pass a C<Object> instead of C<ObjectType> and C<ObjectId>.
134
135 =cut
136
137
138
139
140 sub Create {
141     my $self = shift;
142     my %args = ( 
143                 Name => '',
144                 Description => '',
145                 Content => '',
146                 ContentType => '',
147                 Object => undef,
148                   @_);
149
150     if ($args{Object} and UNIVERSAL::can($args{Object}, 'Id')) {
151             $args{ObjectType} = $args{Object}->isa("RT::CurrentUser") ? "RT::User" : ref($args{Object});
152             $args{ObjectId} = $args{Object}->Id;
153     } else {
154         return(0, $self->loc("Required parameter '[_1]' not specified", 'Object'));
155
156     }
157    
158     # object_right is the right that the user has to have on the object for them to have $right on this attribute
159     my $object_right = $self->LookupObjectRight(
160         Right      => 'create',
161         ObjectId   => $args{'ObjectId'},
162         ObjectType => $args{'ObjectType'},
163         Name       => $args{'Name'}
164     );
165     if ($object_right eq 'deny') { 
166         return (0, $self->loc('Permission Denied'));
167     } 
168     elsif ($object_right eq 'allow') {
169         # do nothing, we're ok
170     }
171     elsif (!$self->CurrentUser->HasRight( Object => $args{Object}, Right => $object_right)) {
172         return (0, $self->loc('Permission Denied'));
173     }
174
175    
176     if (ref ($args{'Content'}) ) { 
177         eval  {$args{'Content'} = $self->_SerializeContent($args{'Content'}); };
178         if ($@) {
179          return(0, $@);
180         }
181         $args{'ContentType'} = 'storable';
182     }
183
184     
185     $self->SUPER::Create(
186                          Name => $args{'Name'},
187                          Content => $args{'Content'},
188                          ContentType => $args{'ContentType'},
189                          Description => $args{'Description'},
190                          ObjectType => $args{'ObjectType'},
191                          ObjectId => $args{'ObjectId'},
192 );
193
194 }
195
196
197
198 =head2  LoadByNameAndObject (Object => OBJECT, Name => NAME)
199
200 Loads the Attribute named NAME for Object OBJECT.
201
202 =cut
203
204 sub LoadByNameAndObject {
205     my $self = shift;
206     my %args = (
207         Object => undef,
208         Name  => undef,
209         @_,
210     );
211
212     return (
213         $self->LoadByCols(
214             Name => $args{'Name'},
215             ObjectType => ref($args{'Object'}),
216             ObjectId => $args{'Object'}->Id,
217         )
218     );
219
220 }
221
222
223
224 =head2 _DeserializeContent
225
226 DeserializeContent returns this Attribute's "Content" as a hashref.
227
228
229 =cut
230
231 sub _DeserializeContent {
232     my $self = shift;
233     my $content = shift;
234
235     my $hashref;
236     eval {$hashref  = thaw(decode_base64($content))} ; 
237     if ($@) {
238         $RT::Logger->error("Deserialization of attribute ".$self->Id. " failed");
239     }
240
241     return($hashref);
242
243 }
244
245
246 =head2 Content
247
248 Returns this attribute's content. If it's a scalar, returns a scalar
249 If it's data structure returns a ref to that data structure.
250
251 =cut
252
253 sub Content {
254     my $self = shift;
255     # Here we call _Value to get the ACL check.
256     my $content = $self->_Value('Content');
257     if ( ($self->__Value('ContentType') || '') eq 'storable') {
258         eval {$content = $self->_DeserializeContent($content); };
259         if ($@) {
260             $RT::Logger->error("Deserialization of content for attribute ".$self->Id. " failed. Attribute was: ".$content);
261         }
262     } 
263
264     return($content);
265
266 }
267
268 sub _SerializeContent {
269     my $self = shift;
270     my $content = shift;
271         return( encode_base64(nfreeze($content))); 
272 }
273
274
275 sub SetContent {
276     my $self = shift;
277     my $content = shift;
278
279     # Call __Value to avoid ACL check.
280     if ( ($self->__Value('ContentType')||'') eq 'storable' ) {
281         # We eval the serialization because it will lose on a coderef.
282         $content = eval { $self->_SerializeContent($content) };
283         if ($@) {
284             $RT::Logger->error("Content couldn't be frozen: $@");
285             return(0, "Content couldn't be frozen");
286         }
287     }
288     return $self->_Set( Field => 'Content', Value => $content );
289 }
290
291 =head2 SubValue KEY
292
293 Returns the subvalue for $key.
294
295
296 =cut
297
298 sub SubValue {
299     my $self = shift;
300     my $key = shift;
301     my $values = $self->Content();
302     return undef unless ref($values);
303     return($values->{$key});
304 }
305
306 =head2 DeleteSubValue NAME
307
308 Deletes the subvalue with the key NAME
309
310 =cut
311
312 sub DeleteSubValue {
313     my $self = shift;
314     my $key = shift;
315     my $values = $self->Content();
316     delete $values->{$key};
317     $self->SetContent($values);
318 }
319
320
321 =head2 DeleteAllSubValues 
322
323 Deletes all subvalues for this attribute
324
325 =cut
326
327
328 sub DeleteAllSubValues {
329     my $self = shift; 
330     $self->SetContent({});
331 }
332
333 =head2 SetSubValues  {  }
334
335 Takes a hash of keys and values and stores them in the content of this attribute.
336
337 Each key B<replaces> the existing key with the same name
338
339 Returns a tuple of (status, message)
340
341 =cut
342
343
344 sub SetSubValues {
345    my $self = shift;
346    my %args = (@_); 
347    my $values = ($self->Content() || {} );
348    foreach my $key (keys %args) {
349     $values->{$key} = $args{$key};
350    }
351
352    $self->SetContent($values);
353
354 }
355
356
357 sub Object {
358     my $self = shift;
359     my $object_type = $self->__Value('ObjectType');
360     my $object;
361     eval { $object = $object_type->new($self->CurrentUser) };
362     unless(UNIVERSAL::isa($object, $object_type)) {
363         $RT::Logger->error("Attribute ".$self->Id." has a bogus object type - $object_type (".$@.")");
364         return(undef);
365      }
366     $object->Load($self->__Value('ObjectId'));
367
368     return($object);
369
370 }
371
372
373 sub Delete {
374     my $self = shift;
375     unless ($self->CurrentUserHasRight('delete')) {
376         return (0,$self->loc('Permission Denied'));
377     }
378     return($self->SUPER::Delete(@_));
379 }
380
381
382 sub _Value {
383     my $self = shift;
384     unless ($self->CurrentUserHasRight('display')) {
385         return (0,$self->loc('Permission Denied'));
386     }
387
388     return($self->SUPER::_Value(@_));
389
390
391 }
392
393
394 sub _Set {
395     my $self = shift;
396     unless ($self->CurrentUserHasRight('update')) {
397
398         return (0,$self->loc('Permission Denied'));
399     }
400     return($self->SUPER::_Set(@_));
401
402 }
403
404
405 =head2 CurrentUserHasRight
406
407 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.
408
409 =cut
410
411 sub CurrentUserHasRight {
412     my $self = shift;
413     my $right = shift;
414
415     # object_right is the right that the user has to have on the object for them to have $right on this attribute
416     my $object_right = $self->LookupObjectRight(
417         Right      => $right,
418         ObjectId   => $self->__Value('ObjectId'),
419         ObjectType => $self->__Value('ObjectType'),
420         Name       => $self->__Value('Name')
421     );
422    
423     return (1) if ($object_right eq 'allow');
424     return (0) if ($object_right eq 'deny');
425     return(1) if ($self->CurrentUser->HasRight( Object => $self->Object, Right => $object_right));
426     return(0);
427
428 }
429
430
431 =head1 TODO
432
433 We should be deserializing the content on load and then enver again, rather than at every access
434
435 =cut
436
437
438
439
440
441
442
443
444 =head2 id
445
446 Returns the current value of id.
447 (In the database, id is stored as int(11).)
448
449
450 =cut
451
452
453 =head2 Name
454
455 Returns the current value of Name.
456 (In the database, Name is stored as varchar(255).)
457
458
459
460 =head2 SetName VALUE
461
462
463 Set Name to VALUE.
464 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
465 (In the database, Name will be stored as a varchar(255).)
466
467
468 =cut
469
470
471 =head2 Description
472
473 Returns the current value of Description.
474 (In the database, Description is stored as varchar(255).)
475
476
477
478 =head2 SetDescription VALUE
479
480
481 Set Description to VALUE.
482 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
483 (In the database, Description will be stored as a varchar(255).)
484
485
486 =cut
487
488
489 =head2 Content
490
491 Returns the current value of Content.
492 (In the database, Content is stored as blob.)
493
494
495
496 =head2 SetContent VALUE
497
498
499 Set Content to VALUE.
500 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
501 (In the database, Content will be stored as a blob.)
502
503
504 =cut
505
506
507 =head2 ContentType
508
509 Returns the current value of ContentType.
510 (In the database, ContentType is stored as varchar(16).)
511
512
513
514 =head2 SetContentType VALUE
515
516
517 Set ContentType to VALUE.
518 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
519 (In the database, ContentType will be stored as a varchar(16).)
520
521
522 =cut
523
524
525 =head2 ObjectType
526
527 Returns the current value of ObjectType.
528 (In the database, ObjectType is stored as varchar(64).)
529
530
531
532 =head2 SetObjectType VALUE
533
534
535 Set ObjectType to VALUE.
536 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
537 (In the database, ObjectType will be stored as a varchar(64).)
538
539
540 =cut
541
542
543 =head2 ObjectId
544
545 Returns the current value of ObjectId.
546 (In the database, ObjectId is stored as int(11).)
547
548
549
550 =head2 SetObjectId VALUE
551
552
553 Set ObjectId to VALUE.
554 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
555 (In the database, ObjectId will be stored as a int(11).)
556
557
558 =cut
559
560
561 =head2 Creator
562
563 Returns the current value of Creator.
564 (In the database, Creator is stored as int(11).)
565
566
567 =cut
568
569
570 =head2 Created
571
572 Returns the current value of Created.
573 (In the database, Created is stored as datetime.)
574
575
576 =cut
577
578
579 =head2 LastUpdatedBy
580
581 Returns the current value of LastUpdatedBy.
582 (In the database, LastUpdatedBy is stored as int(11).)
583
584
585 =cut
586
587
588 =head2 LastUpdated
589
590 Returns the current value of LastUpdated.
591 (In the database, LastUpdated is stored as datetime.)
592
593
594 =cut
595
596
597
598 sub _CoreAccessible {
599     {
600
601         id =>
602                 {read => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
603         Name =>
604                 {read => 1, write => 1, sql_type => 12, length => 255,  is_blob => 0,  is_numeric => 0,  type => 'varchar(255)', default => ''},
605         Description =>
606                 {read => 1, write => 1, sql_type => 12, length => 255,  is_blob => 0,  is_numeric => 0,  type => 'varchar(255)', default => ''},
607         Content =>
608                 {read => 1, write => 1, sql_type => -4, length => 0,  is_blob => 1,  is_numeric => 0,  type => 'blob', default => ''},
609         ContentType =>
610                 {read => 1, write => 1, sql_type => 12, length => 16,  is_blob => 0,  is_numeric => 0,  type => 'varchar(16)', default => ''},
611         ObjectType =>
612                 {read => 1, write => 1, sql_type => 12, length => 64,  is_blob => 0,  is_numeric => 0,  type => 'varchar(64)', default => ''},
613         ObjectId =>
614                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
615         Creator =>
616                 {read => 1, auto => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
617         Created =>
618                 {read => 1, auto => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
619         LastUpdatedBy =>
620                 {read => 1, auto => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
621         LastUpdated =>
622                 {read => 1, auto => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
623
624  }
625 };
626
627 RT::Base->_ImportOverlays();
628
629 1;