]>
Commit | Line | Data |
---|---|---|
84fb5b46 MKG |
1 | # BEGIN BPS TAGGED BLOCK {{{ |
2 | # | |
3 | # COPYRIGHT: | |
4 | # | |
5 | # This software is Copyright (c) 1996-2012 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} = 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 | } | |
322 | ||
323 | ||
324 | =head2 DeleteAllSubValues | |
325 | ||
326 | Deletes all subvalues for this attribute | |
327 | ||
328 | =cut | |
329 | ||
330 | ||
331 | sub DeleteAllSubValues { | |
332 | my $self = shift; | |
333 | $self->SetContent({}); | |
334 | } | |
335 | ||
336 | =head2 SetSubValues { } | |
337 | ||
338 | Takes a hash of keys and values and stores them in the content of this attribute. | |
339 | ||
340 | Each key B<replaces> the existing key with the same name | |
341 | ||
342 | Returns a tuple of (status, message) | |
343 | ||
344 | =cut | |
345 | ||
346 | ||
347 | sub SetSubValues { | |
348 | my $self = shift; | |
349 | my %args = (@_); | |
350 | my $values = ($self->Content() || {} ); | |
351 | foreach my $key (keys %args) { | |
352 | $values->{$key} = $args{$key}; | |
353 | } | |
354 | ||
355 | $self->SetContent($values); | |
356 | ||
357 | } | |
358 | ||
359 | ||
360 | sub Object { | |
361 | my $self = shift; | |
362 | my $object_type = $self->__Value('ObjectType'); | |
363 | my $object; | |
364 | eval { $object = $object_type->new($self->CurrentUser) }; | |
365 | unless(UNIVERSAL::isa($object, $object_type)) { | |
366 | $RT::Logger->error("Attribute ".$self->Id." has a bogus object type - $object_type (".$@.")"); | |
367 | return(undef); | |
368 | } | |
369 | $object->Load($self->__Value('ObjectId')); | |
370 | ||
371 | return($object); | |
372 | ||
373 | } | |
374 | ||
375 | ||
376 | sub Delete { | |
377 | my $self = shift; | |
378 | unless ($self->CurrentUserHasRight('delete')) { | |
379 | return (0,$self->loc('Permission Denied')); | |
380 | } | |
381 | return($self->SUPER::Delete(@_)); | |
382 | } | |
383 | ||
384 | ||
385 | sub _Value { | |
386 | my $self = shift; | |
387 | unless ($self->CurrentUserHasRight('display')) { | |
388 | return (0,$self->loc('Permission Denied')); | |
389 | } | |
390 | ||
391 | return($self->SUPER::_Value(@_)); | |
392 | ||
393 | ||
394 | } | |
395 | ||
396 | ||
397 | sub _Set { | |
398 | my $self = shift; | |
399 | unless ($self->CurrentUserHasRight('update')) { | |
400 | ||
401 | return (0,$self->loc('Permission Denied')); | |
402 | } | |
403 | return($self->SUPER::_Set(@_)); | |
404 | ||
405 | } | |
406 | ||
407 | ||
408 | =head2 CurrentUserHasRight | |
409 | ||
410 | 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. | |
411 | ||
412 | =cut | |
413 | ||
414 | sub CurrentUserHasRight { | |
415 | my $self = shift; | |
416 | my $right = shift; | |
417 | ||
418 | # object_right is the right that the user has to have on the object for them to have $right on this attribute | |
419 | my $object_right = $self->LookupObjectRight( | |
420 | Right => $right, | |
421 | ObjectId => $self->__Value('ObjectId'), | |
422 | ObjectType => $self->__Value('ObjectType'), | |
423 | Name => $self->__Value('Name') | |
424 | ); | |
425 | ||
426 | return (1) if ($object_right eq 'allow'); | |
427 | return (0) if ($object_right eq 'deny'); | |
428 | return(1) if ($self->CurrentUser->HasRight( Object => $self->Object, Right => $object_right)); | |
429 | return(0); | |
430 | ||
431 | } | |
432 | ||
433 | ||
434 | =head1 TODO | |
435 | ||
436 | We should be deserializing the content on load and then enver again, rather than at every access | |
437 | ||
438 | =cut | |
439 | ||
440 | ||
441 | ||
442 | ||
443 | ||
444 | ||
445 | ||
446 | ||
447 | =head2 id | |
448 | ||
449 | Returns the current value of id. | |
450 | (In the database, id is stored as int(11).) | |
451 | ||
452 | ||
453 | =cut | |
454 | ||
455 | ||
456 | =head2 Name | |
457 | ||
458 | Returns the current value of Name. | |
459 | (In the database, Name is stored as varchar(255).) | |
460 | ||
461 | ||
462 | ||
463 | =head2 SetName VALUE | |
464 | ||
465 | ||
466 | Set Name to VALUE. | |
467 | Returns (1, 'Status message') on success and (0, 'Error Message') on failure. | |
468 | (In the database, Name will be stored as a varchar(255).) | |
469 | ||
470 | ||
471 | =cut | |
472 | ||
473 | ||
474 | =head2 Description | |
475 | ||
476 | Returns the current value of Description. | |
477 | (In the database, Description is stored as varchar(255).) | |
478 | ||
479 | ||
480 | ||
481 | =head2 SetDescription VALUE | |
482 | ||
483 | ||
484 | Set Description to VALUE. | |
485 | Returns (1, 'Status message') on success and (0, 'Error Message') on failure. | |
486 | (In the database, Description will be stored as a varchar(255).) | |
487 | ||
488 | ||
489 | =cut | |
490 | ||
491 | ||
492 | =head2 Content | |
493 | ||
494 | Returns the current value of Content. | |
495 | (In the database, Content is stored as blob.) | |
496 | ||
497 | ||
498 | ||
499 | =head2 SetContent VALUE | |
500 | ||
501 | ||
502 | Set Content to VALUE. | |
503 | Returns (1, 'Status message') on success and (0, 'Error Message') on failure. | |
504 | (In the database, Content will be stored as a blob.) | |
505 | ||
506 | ||
507 | =cut | |
508 | ||
509 | ||
510 | =head2 ContentType | |
511 | ||
512 | Returns the current value of ContentType. | |
513 | (In the database, ContentType is stored as varchar(16).) | |
514 | ||
515 | ||
516 | ||
517 | =head2 SetContentType VALUE | |
518 | ||
519 | ||
520 | Set ContentType to VALUE. | |
521 | Returns (1, 'Status message') on success and (0, 'Error Message') on failure. | |
522 | (In the database, ContentType will be stored as a varchar(16).) | |
523 | ||
524 | ||
525 | =cut | |
526 | ||
527 | ||
528 | =head2 ObjectType | |
529 | ||
530 | Returns the current value of ObjectType. | |
531 | (In the database, ObjectType is stored as varchar(64).) | |
532 | ||
533 | ||
534 | ||
535 | =head2 SetObjectType VALUE | |
536 | ||
537 | ||
538 | Set ObjectType to VALUE. | |
539 | Returns (1, 'Status message') on success and (0, 'Error Message') on failure. | |
540 | (In the database, ObjectType will be stored as a varchar(64).) | |
541 | ||
542 | ||
543 | =cut | |
544 | ||
545 | ||
546 | =head2 ObjectId | |
547 | ||
548 | Returns the current value of ObjectId. | |
549 | (In the database, ObjectId is stored as int(11).) | |
550 | ||
551 | ||
552 | ||
553 | =head2 SetObjectId VALUE | |
554 | ||
555 | ||
556 | Set ObjectId to VALUE. | |
557 | Returns (1, 'Status message') on success and (0, 'Error Message') on failure. | |
558 | (In the database, ObjectId will be stored as a int(11).) | |
559 | ||
560 | ||
561 | =cut | |
562 | ||
563 | ||
564 | =head2 Creator | |
565 | ||
566 | Returns the current value of Creator. | |
567 | (In the database, Creator is stored as int(11).) | |
568 | ||
569 | ||
570 | =cut | |
571 | ||
572 | ||
573 | =head2 Created | |
574 | ||
575 | Returns the current value of Created. | |
576 | (In the database, Created is stored as datetime.) | |
577 | ||
578 | ||
579 | =cut | |
580 | ||
581 | ||
582 | =head2 LastUpdatedBy | |
583 | ||
584 | Returns the current value of LastUpdatedBy. | |
585 | (In the database, LastUpdatedBy is stored as int(11).) | |
586 | ||
587 | ||
588 | =cut | |
589 | ||
590 | ||
591 | =head2 LastUpdated | |
592 | ||
593 | Returns the current value of LastUpdated. | |
594 | (In the database, LastUpdated is stored as datetime.) | |
595 | ||
596 | ||
597 | =cut | |
598 | ||
599 | ||
600 | ||
601 | sub _CoreAccessible { | |
602 | { | |
603 | ||
604 | id => | |
605 | {read => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''}, | |
606 | Name => | |
607 | {read => 1, write => 1, sql_type => 12, length => 255, is_blob => 0, is_numeric => 0, type => 'varchar(255)', default => ''}, | |
608 | Description => | |
609 | {read => 1, write => 1, sql_type => 12, length => 255, is_blob => 0, is_numeric => 0, type => 'varchar(255)', default => ''}, | |
610 | Content => | |
611 | {read => 1, write => 1, sql_type => -4, length => 0, is_blob => 1, is_numeric => 0, type => 'blob', default => ''}, | |
612 | ContentType => | |
613 | {read => 1, write => 1, sql_type => 12, length => 16, is_blob => 0, is_numeric => 0, type => 'varchar(16)', default => ''}, | |
614 | ObjectType => | |
615 | {read => 1, write => 1, sql_type => 12, length => 64, is_blob => 0, is_numeric => 0, type => 'varchar(64)', default => ''}, | |
616 | ObjectId => | |
617 | {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''}, | |
618 | Creator => | |
619 | {read => 1, auto => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'}, | |
620 | Created => | |
621 | {read => 1, auto => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''}, | |
622 | LastUpdatedBy => | |
623 | {read => 1, auto => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'}, | |
624 | LastUpdated => | |
625 | {read => 1, auto => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''}, | |
626 | ||
627 | } | |
628 | }; | |
629 | ||
630 | RT::Base->_ImportOverlays(); | |
631 | ||
632 | 1; |