rt 4.2.13 ticket#13852
[freeside.git] / rt / lib / RT / Attribute.pm
1 # BEGIN BPS TAGGED BLOCK {{{
2 #
3 # COPYRIGHT:
4 #
5 # This software is Copyright (c) 1996-2016 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     $self->SUPER::Create(
185                          Name => $args{'Name'},
186                          Content => $args{'Content'},
187                          ContentType => $args{'ContentType'},
188                          Description => $args{'Description'},
189                          ObjectType => $args{'ObjectType'},
190                          ObjectId => $args{'ObjectId'},
191 );
192
193 }
194
195
196
197 =head2  LoadByNameAndObject (Object => OBJECT, Name => NAME)
198
199 Loads the Attribute named NAME for Object OBJECT.
200
201 =cut
202
203 sub LoadByNameAndObject {
204     my $self = shift;
205     my %args = (
206         Object => undef,
207         Name  => undef,
208         @_,
209     );
210
211     return (
212         $self->LoadByCols(
213             Name => $args{'Name'},
214             ObjectType => ref($args{'Object'}),
215             ObjectId => $args{'Object'}->Id,
216         )
217     );
218
219 }
220
221
222
223 =head2 _DeserializeContent
224
225 DeserializeContent returns this Attribute's "Content" as a hashref.
226
227
228 =cut
229
230 sub _DeserializeContent {
231     my $self = shift;
232     my $content = shift;
233
234     my $hashref;
235     eval {$hashref  = thaw(decode_base64($content))} ; 
236     if ($@) {
237         $RT::Logger->error("Deserialization of attribute ".$self->Id. " failed");
238     }
239
240     return($hashref);
241
242 }
243
244
245 =head2 Content
246
247 Returns this attribute's content. If it's a scalar, returns a scalar
248 If it's data structure returns a ref to that data structure.
249
250 =cut
251
252 sub Content {
253     my $self = shift;
254     # Here we call _Value to get the ACL check.
255     my $content = $self->_Value('Content');
256     if ( ($self->__Value('ContentType') || '') eq 'storable') {
257         eval {$content = $self->_DeserializeContent($content); };
258         if ($@) {
259             $RT::Logger->error("Deserialization of content for attribute ".$self->Id. " failed. Attribute was: ".$content);
260         }
261     } 
262
263     return($content);
264
265 }
266
267 sub _SerializeContent {
268     my $self = shift;
269     my $content = shift;
270         return( encode_base64(nfreeze($content))); 
271 }
272
273
274 sub SetContent {
275     my $self = shift;
276     my $content = shift;
277
278     # Call __Value to avoid ACL check.
279     if ( ($self->__Value('ContentType')||'') eq 'storable' ) {
280         # We eval the serialization because it will lose on a coderef.
281         $content = eval { $self->_SerializeContent($content) };
282         if ($@) {
283             $RT::Logger->error("Content couldn't be frozen: $@");
284             return(0, "Content couldn't be frozen");
285         }
286     }
287     my ($ok, $msg) = $self->_Set( Field => 'Content', Value => $content );
288     return ($ok, $self->loc("Attribute updated")) if $ok;
289     return ($ok, $msg);
290 }
291
292 =head2 SubValue KEY
293
294 Returns the subvalue for $key.
295
296
297 =cut
298
299 sub SubValue {
300     my $self = shift;
301     my $key = shift;
302     my $values = $self->Content();
303     return undef unless ref($values);
304     return($values->{$key});
305 }
306
307 =head2 DeleteSubValue NAME
308
309 Deletes the subvalue with the key NAME
310
311 =cut
312
313 sub DeleteSubValue {
314     my $self = shift;
315     my $key = shift;
316     my $values = $self->Content();
317     delete $values->{$key};
318     $self->SetContent($values);
319 }
320
321
322 =head2 DeleteAllSubValues 
323
324 Deletes all subvalues for this attribute
325
326 =cut
327
328
329 sub DeleteAllSubValues {
330     my $self = shift; 
331     $self->SetContent({});
332 }
333
334 =head2 SetSubValues  {  }
335
336 Takes a hash of keys and values and stores them in the content of this attribute.
337
338 Each key B<replaces> the existing key with the same name
339
340 Returns a tuple of (status, message)
341
342 =cut
343
344
345 sub SetSubValues {
346    my $self = shift;
347    my %args = (@_); 
348    my $values = ($self->Content() || {} );
349    foreach my $key (keys %args) {
350     $values->{$key} = $args{$key};
351    }
352
353    $self->SetContent($values);
354
355 }
356
357
358 sub Object {
359     my $self = shift;
360     my $object_type = $self->__Value('ObjectType');
361     my $object;
362     eval { $object = $object_type->new($self->CurrentUser) };
363     unless(UNIVERSAL::isa($object, $object_type)) {
364         $RT::Logger->error("Attribute ".$self->Id." has a bogus object type - $object_type (".$@.")");
365         return(undef);
366      }
367     $object->Load($self->__Value('ObjectId'));
368
369     return($object);
370
371 }
372
373
374 sub Delete {
375     my $self = shift;
376     unless ($self->CurrentUserHasRight('delete')) {
377         return (0,$self->loc('Permission Denied'));
378     }
379
380     return($self->SUPER::Delete(@_));
381 }
382
383
384 sub _Value {
385     my $self = shift;
386     unless ($self->CurrentUserHasRight('display')) {
387         return (0,$self->loc('Permission Denied'));
388     }
389
390     return($self->SUPER::_Value(@_));
391
392
393 }
394
395
396 sub _Set {
397     my $self = shift;
398     unless ($self->CurrentUserHasRight('update')) {
399
400         return (0,$self->loc('Permission Denied'));
401     }
402     return($self->SUPER::_Set(@_));
403
404 }
405
406
407 =head2 CurrentUserHasRight
408
409 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.
410
411 =cut
412
413 sub CurrentUserHasRight {
414     my $self = shift;
415     my $right = shift;
416
417     # object_right is the right that the user has to have on the object for them to have $right on this attribute
418     my $object_right = $self->LookupObjectRight(
419         Right      => $right,
420         ObjectId   => $self->__Value('ObjectId'),
421         ObjectType => $self->__Value('ObjectType'),
422         Name       => $self->__Value('Name')
423     );
424    
425     return (1) if ($object_right eq 'allow');
426     return (0) if ($object_right eq 'deny');
427     return(1) if ($self->CurrentUser->HasRight( Object => $self->Object, Right => $object_right));
428     return(0);
429
430 }
431
432
433 =head1 TODO
434
435 We should be deserializing the content on load and then enver again, rather than at every access
436
437 =cut
438
439
440
441
442
443
444
445
446 =head2 id
447
448 Returns the current value of id.
449 (In the database, id is stored as int(11).)
450
451
452 =cut
453
454
455 =head2 Name
456
457 Returns the current value of Name.
458 (In the database, Name is stored as varchar(255).)
459
460
461
462 =head2 SetName VALUE
463
464
465 Set Name to VALUE.
466 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
467 (In the database, Name will be stored as a varchar(255).)
468
469
470 =cut
471
472
473 =head2 Description
474
475 Returns the current value of Description.
476 (In the database, Description is stored as varchar(255).)
477
478
479
480 =head2 SetDescription VALUE
481
482
483 Set Description to VALUE.
484 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
485 (In the database, Description will be stored as a varchar(255).)
486
487
488 =cut
489
490
491 =head2 Content
492
493 Returns the current value of Content.
494 (In the database, Content is stored as blob.)
495
496
497
498 =head2 SetContent VALUE
499
500
501 Set Content to VALUE.
502 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
503 (In the database, Content will be stored as a blob.)
504
505
506 =cut
507
508
509 =head2 ContentType
510
511 Returns the current value of ContentType.
512 (In the database, ContentType is stored as varchar(16).)
513
514
515
516 =head2 SetContentType VALUE
517
518
519 Set ContentType to VALUE.
520 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
521 (In the database, ContentType will be stored as a varchar(16).)
522
523
524 =cut
525
526
527 =head2 ObjectType
528
529 Returns the current value of ObjectType.
530 (In the database, ObjectType is stored as varchar(64).)
531
532
533
534 =head2 SetObjectType VALUE
535
536
537 Set ObjectType to VALUE.
538 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
539 (In the database, ObjectType will be stored as a varchar(64).)
540
541
542 =cut
543
544
545 =head2 ObjectId
546
547 Returns the current value of ObjectId.
548 (In the database, ObjectId is stored as int(11).)
549
550
551
552 =head2 SetObjectId VALUE
553
554
555 Set ObjectId to VALUE.
556 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
557 (In the database, ObjectId will be stored as a int(11).)
558
559
560 =cut
561
562
563 =head2 Creator
564
565 Returns the current value of Creator.
566 (In the database, Creator is stored as int(11).)
567
568
569 =cut
570
571
572 =head2 Created
573
574 Returns the current value of Created.
575 (In the database, Created is stored as datetime.)
576
577
578 =cut
579
580
581 =head2 LastUpdatedBy
582
583 Returns the current value of LastUpdatedBy.
584 (In the database, LastUpdatedBy is stored as int(11).)
585
586
587 =cut
588
589
590 =head2 LastUpdated
591
592 Returns the current value of LastUpdated.
593 (In the database, LastUpdated is stored as datetime.)
594
595
596 =cut
597
598
599
600 sub _CoreAccessible {
601     {
602
603         id =>
604                 {read => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
605         Name =>
606                 {read => 1, write => 1, sql_type => 12, length => 255,  is_blob => 0,  is_numeric => 0,  type => 'varchar(255)', default => ''},
607         Description =>
608                 {read => 1, write => 1, sql_type => 12, length => 255,  is_blob => 0,  is_numeric => 0,  type => 'varchar(255)', default => ''},
609         Content =>
610                 {read => 1, write => 1, sql_type => -4, length => 0,  is_blob => 1,  is_numeric => 0,  type => 'blob', default => ''},
611         ContentType =>
612                 {read => 1, write => 1, sql_type => 12, length => 16,  is_blob => 0,  is_numeric => 0,  type => 'varchar(16)', default => ''},
613         ObjectType =>
614                 {read => 1, write => 1, sql_type => 12, length => 64,  is_blob => 0,  is_numeric => 0,  type => 'varchar(64)', default => ''},
615         ObjectId =>
616                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
617         Creator =>
618                 {read => 1, auto => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
619         Created =>
620                 {read => 1, auto => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
621         LastUpdatedBy =>
622                 {read => 1, auto => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
623         LastUpdated =>
624                 {read => 1, auto => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
625
626  }
627 };
628
629 sub FindDependencies {
630     my $self = shift;
631     my ($walker, $deps) = @_;
632
633     $self->SUPER::FindDependencies($walker, $deps);
634     $deps->Add( out => $self->Object );
635 }
636
637 sub PreInflate {
638     my $class = shift;
639     my ($importer, $uid, $data) = @_;
640
641     if ($data->{Object} and ref $data->{Object}) {
642         my $on_uid = ${ $data->{Object} };
643         return if $importer->ShouldSkipTransaction($on_uid);
644     }
645     return $class->SUPER::PreInflate( $importer, $uid, $data );
646 }
647
648 RT::Base->_ImportOverlays();
649
650 1;