1# BEGIN BPS TAGGED BLOCK {{{ 2# 3# COPYRIGHT: 4# 5# This software is Copyright (c) 1996-2021 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=head1 NAME 50 51 RT::Record - Base class for RT record objects 52 53=head1 SYNOPSIS 54 55 56=head1 DESCRIPTION 57 58 59 60=head1 METHODS 61 62=cut 63 64package RT::Record; 65 66use strict; 67use warnings; 68 69use RT; 70use base RT->Config->Get('RecordBaseClass'); 71use base 'RT::Base'; 72 73require RT::Date; 74require RT::User; 75require RT::Attributes; 76require RT::Transactions; 77require RT::Link; 78use RT::Shredder::Dependencies; 79use RT::Shredder::Constants; 80use RT::Shredder::Exceptions; 81 82our $_TABLE_ATTR = { }; 83 84 85sub _Init { 86 my $self = shift; 87 $self->_BuildTableAttributes unless ($_TABLE_ATTR->{ref($self)}); 88 $self->CurrentUser(@_); 89} 90 91 92 93=head2 _PrimaryKeys 94 95The primary keys for RT classes is 'id' 96 97=cut 98 99sub _PrimaryKeys { return ['id'] } 100# short circuit many, many thousands of calls from searchbuilder 101sub _PrimaryKey { 'id' } 102 103=head2 Id 104 105Override L<DBIx::SearchBuilder/Id> to avoid a few lookups RT doesn't do 106on a very common codepath 107 108C<id> is an alias to C<Id> and is the preferred way to call this method. 109 110=cut 111 112sub Id { 113 return shift->{'values'}->{id}; 114} 115 116*id = \&Id; 117 118=head2 Delete 119 120Delete this record object from the database. 121 122=cut 123 124sub Delete { 125 my $self = shift; 126 my ($rv) = $self->SUPER::Delete; 127 if ($rv) { 128 return ($rv, $self->loc("Object deleted")); 129 } else { 130 return (0, $self->loc("Object could not be deleted")); 131 } 132} 133 134=head2 RecordType 135 136Returns a string which is this record's type. It's not localized and by 137default last part (everything after last ::) of class name is returned. 138 139=cut 140 141sub RecordType { 142 my $res = ref($_[0]) || $_[0]; 143 $res =~ s/.*:://; 144 return $res; 145} 146 147=head2 Attributes 148 149Return this object's attributes as an RT::Attributes object 150 151=cut 152 153sub Attributes { 154 my $self = shift; 155 unless ($self->{'attributes'}) { 156 $self->{'attributes'} = RT::Attributes->new($self->CurrentUser); 157 $self->{'attributes'}->LimitToObject($self); 158 $self->{'attributes'}->OrderByCols({FIELD => 'id'}); 159 } 160 return ($self->{'attributes'}); 161} 162 163 164=head2 AddAttribute { Name, Description, Content } 165 166Adds a new attribute for this object. 167 168=cut 169 170sub AddAttribute { 171 my $self = shift; 172 my %args = ( Name => undef, 173 Description => undef, 174 Content => undef, 175 @_ ); 176 177 my $attr = RT::Attribute->new( $self->CurrentUser ); 178 my ( $id, $msg ) = $attr->Create( 179 Object => $self, 180 Name => $args{'Name'}, 181 Description => $args{'Description'}, 182 Content => $args{'Content'} ); 183 184 185 # XXX TODO: Why won't RedoSearch work here? 186 $self->Attributes->_DoSearch; 187 188 return ($id, $msg); 189} 190 191 192=head2 SetAttribute { Name, Description, Content } 193 194Like AddAttribute, but replaces all existing attributes with the same Name. 195 196=cut 197 198sub SetAttribute { 199 my $self = shift; 200 my %args = ( Name => undef, 201 Description => undef, 202 Content => undef, 203 @_ ); 204 205 my @AttributeObjs = $self->Attributes->Named( $args{'Name'} ) 206 or return $self->AddAttribute( %args ); 207 208 my $AttributeObj = pop( @AttributeObjs ); 209 $_->Delete foreach @AttributeObjs; 210 211 $AttributeObj->SetDescription( $args{'Description'} ); 212 $AttributeObj->SetContent( $args{'Content'} ); 213 214 $self->Attributes->RedoSearch; 215 return 1; 216} 217 218=head2 DeleteAttribute NAME 219 220Deletes all attributes with the matching name for this object. 221 222=cut 223 224sub DeleteAttribute { 225 my $self = shift; 226 my $name = shift; 227 my ($val,$msg) = $self->Attributes->DeleteEntry( Name => $name ); 228 $self->ClearAttributes; 229 return ($val,$msg); 230} 231 232=head2 FirstAttribute NAME 233 234Returns the first attribute with the matching name for this object (as an 235L<RT::Attribute> object), or C<undef> if no such attributes exist. 236If there is more than one attribute with the matching name on the 237object, the first value that was set is returned. 238 239=cut 240 241sub FirstAttribute { 242 my $self = shift; 243 my $name = shift; 244 return ($self->Attributes->Named( $name ))[0]; 245} 246 247 248sub ClearAttributes { 249 my $self = shift; 250 delete $self->{'attributes'}; 251 252} 253 254sub _Handle { return $RT::Handle } 255 256 257 258=head2 Create PARAMHASH 259 260Takes a PARAMHASH of Column -> Value pairs. 261If any Column has a Validate$PARAMNAME subroutine defined and the 262value provided doesn't pass validation, this routine returns 263an error. 264 265If this object's table has any of the following atetributes defined as 266'Auto', this routine will automatically fill in their values. 267 268=over 269 270=item Created 271 272=item Creator 273 274=item LastUpdated 275 276=item LastUpdatedBy 277 278=back 279 280=cut 281 282sub Create { 283 my $self = shift; 284 my %attribs = (@_); 285 foreach my $key ( keys %attribs ) { 286 if (my $method = $self->can("Validate$key")) { 287 if (! $method->( $self, $attribs{$key} ) ) { 288 if (wantarray) { 289 return ( 0, $self->loc('Invalid value for [_1]', $key) ); 290 } 291 else { 292 return (0); 293 } 294 } 295 } 296 } 297 298 299 300 my ($sec,$min,$hour,$mday,$mon,$year,$wday,$ydaym,$isdst,$offset) = gmtime(); 301 302 my $now_iso = 303 sprintf("%04d-%02d-%02d %02d:%02d:%02d", ($year+1900), ($mon+1), $mday, $hour, $min, $sec); 304 305 $attribs{'Created'} = $now_iso if ( $self->_Accessible( 'Created', 'auto' ) && !$attribs{'Created'}); 306 307 if ($self->_Accessible( 'Creator', 'auto' ) && !$attribs{'Creator'}) { 308 $attribs{'Creator'} = $self->CurrentUser->id || '0'; 309 } 310 $attribs{'LastUpdated'} = $now_iso 311 if ( $self->_Accessible( 'LastUpdated', 'auto' ) && !$attribs{'LastUpdated'}); 312 313 $attribs{'LastUpdatedBy'} = $self->CurrentUser->id || '0' 314 if ( $self->_Accessible( 'LastUpdatedBy', 'auto' ) && !$attribs{'LastUpdatedBy'}); 315 316 my $id = $self->SUPER::Create(%attribs); 317 if ( UNIVERSAL::isa( $id, 'Class::ReturnValue' ) ) { 318 if ( $id->errno ) { 319 if (wantarray) { 320 return ( 0, 321 $self->loc( "Internal Error: [_1]", $id->{error_message} ) ); 322 } 323 else { 324 return (0); 325 } 326 } 327 } 328 # If the object was created in the database, 329 # load it up now, so we're sure we get what the database 330 # has. Arguably, this should not be necessary, but there 331 # isn't much we can do about it. 332 333 unless ($id) { 334 if (wantarray) { 335 return ( $id, $self->loc('Object could not be created') ); 336 } 337 else { 338 return ($id); 339 } 340 341 } 342 343 if (UNIVERSAL::isa('errno',$id)) { 344 return(undef); 345 } 346 347 $self->Load($id) if ($id); 348 349 350 351 if (wantarray) { 352 return ( $id, $self->loc('Object created') ); 353 } 354 else { 355 return ($id); 356 } 357 358} 359 360 361 362=head2 LoadByCols 363 364Override DBIx::SearchBuilder::LoadByCols to do case-insensitive loads if the 365DB is case sensitive 366 367=cut 368 369sub LoadByCols { 370 my $self = shift; 371 372 # We don't want to hang onto this 373 $self->ClearAttributes; 374 delete $self->{_Roles}; 375 376 unless ( $self->_Handle->CaseSensitive ) { 377 my ( $ret, $msg ) = $self->SUPER::LoadByCols( @_ ); 378 return wantarray ? ( $ret, $msg ) : $ret; 379 } 380 381 # If this database is case sensitive we need to uncase objects for 382 # explicit loading 383 my %hash = (@_); 384 foreach my $key ( keys %hash ) { 385 386 # If we've been passed an empty value, we can't do the lookup. 387 # We don't need to explicitly downcase integers or an id. 388 if ( $key ne 'id' && defined $hash{ $key } && $hash{ $key } !~ /^\d+$/ ) { 389 my ($op, $val, $func); 390 ($key, $op, $val, $func) = 391 $self->_Handle->_MakeClauseCaseInsensitive( $key, '=', delete $hash{ $key } ); 392 $hash{$key}->{operator} = $op; 393 $hash{$key}->{value} = $val; 394 $hash{$key}->{function} = $func; 395 } 396 } 397 my ( $ret, $msg ) = $self->SUPER::LoadByCols( %hash ); 398 return wantarray ? ( $ret, $msg ) : $ret; 399} 400 401 402 403# There is room for optimizations in most of those subs: 404 405 406sub LastUpdatedObj { 407 my $self = shift; 408 my $obj = RT::Date->new( $self->CurrentUser ); 409 410 $obj->Set( Format => 'sql', Value => $self->LastUpdated ); 411 return $obj; 412} 413 414 415 416sub CreatedObj { 417 my $self = shift; 418 my $obj = RT::Date->new( $self->CurrentUser ); 419 420 $obj->Set( Format => 'sql', Value => $self->Created ); 421 422 return $obj; 423} 424 425 426sub LastUpdatedAsString { 427 my $self = shift; 428 if ( $self->LastUpdated ) { 429 return ( $self->LastUpdatedObj->AsString() ); 430 } else { 431 return "never"; 432 } 433} 434 435sub CreatedAsString { 436 my $self = shift; 437 return ( $self->CreatedObj->AsString() ); 438} 439 440sub _Set { 441 my $self = shift; 442 443 my %args = ( 444 Field => undef, 445 Value => undef, 446 IsSQL => undef, 447 @_ 448 ); 449 450 #if the user is trying to modify the record 451 # TODO: document _why_ this code is here 452 453 if ( ( !defined( $args{'Field'} ) ) || ( !defined( $args{'Value'} ) ) ) { 454 $args{'Value'} = 0; 455 } 456 457 my $old_val = $self->__Value($args{'Field'}); 458 $self->_SetLastUpdated(); 459 my $ret = $self->SUPER::_Set( 460 Field => $args{'Field'}, 461 Value => $args{'Value'}, 462 IsSQL => $args{'IsSQL'} 463 ); 464 my ($status, $msg) = $ret->as_array(); 465 466 # @values has two values, a status code and a message. 467 468 # $ret is a Class::ReturnValue object. as such, in a boolean context, it's a bool 469 # we want to change the standard "success" message 470 if ($status) { 471 if ($self->SQLType( $args{'Field'}) =~ /text/) { 472 $msg = $self->loc( 473 "[_1] updated", 474 $self->loc( $args{'Field'} ), 475 ); 476 } else { 477 $msg = $self->loc( 478 "[_1] changed from [_2] to [_3]", 479 $self->loc( $args{'Field'} ), 480 ( $old_val ? '"' . $old_val . '"' : $self->loc("(no value)") ), 481 '"' . $self->__Value( $args{'Field'}) . '"', 482 ); 483 } 484 } else { 485 $msg = $self->CurrentUser->loc_fuzzy($msg); 486 } 487 488 return wantarray ? ($status, $msg) : $ret; 489} 490 491 492 493=head2 _SetLastUpdated 494 495This routine updates the LastUpdated and LastUpdatedBy columns of the row in question 496It takes no options. Arguably, this is a bug 497 498=cut 499 500sub _SetLastUpdated { 501 my $self = shift; 502 my $now = RT::Date->new( $self->CurrentUser ); 503 $now->SetToNow(); 504 505 if ( $self->_Accessible( 'LastUpdated', 'auto' ) ) { 506 my ( $msg, $val ) = $self->__Set( 507 Field => 'LastUpdated', 508 Value => $now->ISO 509 ); 510 } 511 if ( $self->_Accessible( 'LastUpdatedBy', 'auto' ) ) { 512 my ( $msg, $val ) = $self->__Set( 513 Field => 'LastUpdatedBy', 514 Value => $self->CurrentUser->id 515 ); 516 } 517} 518 519 520 521=head2 CreatorObj 522 523Returns an RT::User object with the RT account of the creator of this row 524 525=cut 526 527sub CreatorObj { 528 my $self = shift; 529 unless ( exists $self->{'CreatorObj'} ) { 530 531 $self->{'CreatorObj'} = RT::User->new( $self->CurrentUser ); 532 $self->{'CreatorObj'}->Load( $self->Creator ); 533 } 534 return ( $self->{'CreatorObj'} ); 535} 536 537 538 539=head2 LastUpdatedByObj 540 541 Returns an RT::User object of the last user to touch this object 542 543=cut 544 545sub LastUpdatedByObj { 546 my $self = shift; 547 unless ( exists $self->{LastUpdatedByObj} ) { 548 $self->{'LastUpdatedByObj'} = RT::User->new( $self->CurrentUser ); 549 $self->{'LastUpdatedByObj'}->Load( $self->LastUpdatedBy ); 550 } 551 return $self->{'LastUpdatedByObj'}; 552} 553 554 555 556=head2 URI 557 558Returns this record's URI 559 560=cut 561 562sub URI { 563 my $self = shift; 564 my $uri = RT::URI::fsck_com_rt->new($self->CurrentUser); 565 return($uri->URIForObject($self)); 566} 567 568 569=head2 ValidateName NAME 570 571Validate the name of the record we're creating. Mostly, just make sure it's not a numeric ID, which is invalid for Name 572 573=cut 574 575sub ValidateName { 576 my $self = shift; 577 my $value = shift; 578 if (defined $value && $value=~ /^\d+$/) { 579 return(0); 580 } else { 581 return(1); 582 } 583} 584 585 586 587=head2 SQLType attribute 588 589return the SQL type for the attribute 'attribute' as stored in _ClassAccessible 590 591=cut 592 593sub SQLType { 594 my $self = shift; 595 my $field = shift; 596 597 return ($self->_Accessible($field, 'type')); 598 599 600} 601 602sub __Value { 603 my $self = shift; 604 my $field = shift; 605 my %args = ( decode_utf8 => 1, @_ ); 606 607 unless ($field) { 608 $RT::Logger->error("__Value called with undef field"); 609 } 610 611 my $value = $self->SUPER::__Value($field); 612 return $value if ref $value; 613 614 return undef if (!defined $value); 615 616 # Pg returns character columns as character strings; mysql and 617 # sqlite return them as bytes. While mysql can be made to return 618 # characters, using the mysql_enable_utf8 flag, the "Content" column 619 # is bytes on mysql and characters on Postgres, making true 620 # consistency impossible. 621 if ( $args{'decode_utf8'} ) { 622 if ( !utf8::is_utf8($value) ) { # mysql/sqlite 623 utf8::decode($value); 624 } 625 } else { 626 if ( utf8::is_utf8($value) ) { 627 utf8::encode($value); 628 } 629 } 630 631 return $value; 632 633} 634 635# Set up defaults for DBIx::SearchBuilder::Record::Cachable 636 637sub _CacheConfig { 638 { 639 'cache_for_sec' => 30, 640 } 641} 642 643 644 645sub _BuildTableAttributes { 646 my $self = shift; 647 my $class = ref($self) || $self; 648 649 my $attributes; 650 if ( UNIVERSAL::can( $self, '_CoreAccessible' ) ) { 651 $attributes = $self->_CoreAccessible(); 652 } elsif ( UNIVERSAL::can( $self, '_ClassAccessible' ) ) { 653 $attributes = $self->_ClassAccessible(); 654 655 } 656 657 foreach my $column (keys %$attributes) { 658 foreach my $attr ( keys %{ $attributes->{$column} } ) { 659 $_TABLE_ATTR->{$class}->{$column}->{$attr} = $attributes->{$column}->{$attr}; 660 } 661 } 662 foreach my $method ( qw(_OverlayAccessible _VendorAccessible _LocalAccessible) ) { 663 next unless UNIVERSAL::can( $self, $method ); 664 $attributes = $self->$method(); 665 666 foreach my $column ( keys %$attributes ) { 667 foreach my $attr ( keys %{ $attributes->{$column} } ) { 668 $_TABLE_ATTR->{$class}->{$column}->{$attr} = $attributes->{$column}->{$attr}; 669 } 670 } 671 } 672} 673 674 675=head2 _ClassAccessible 676 677Overrides the "core" _ClassAccessible using $_TABLE_ATTR. Behaves identical to the version in 678DBIx::SearchBuilder::Record 679 680=cut 681 682sub _ClassAccessible { 683 my $self = shift; 684 return $_TABLE_ATTR->{ref($self) || $self}; 685} 686 687=head2 _Accessible COLUMN ATTRIBUTE 688 689returns the value of ATTRIBUTE for COLUMN 690 691 692=cut 693 694sub _Accessible { 695 my $self = shift; 696 my $column = shift; 697 my $attribute = lc(shift); 698 699 my $class = ref($self) || $self; 700 $class->_BuildTableAttributes unless ($_TABLE_ATTR->{$class}); 701 702 return 0 unless defined ($_TABLE_ATTR->{$class}->{$column}); 703 return $_TABLE_ATTR->{$class}->{$column}->{$attribute} || 0; 704 705} 706 707=head2 _EncodeLOB BODY MIME_TYPE FILENAME 708 709Takes a potentially large attachment. Returns (ContentEncoding, 710EncodedBody, MimeType, Filename, NoteArgs) based on system configuration and 711selected database. Returns a custom (short) text/plain message if 712DropLongAttachments causes an attachment to not be stored. 713 714Encodes your data as base64 or Quoted-Printable as needed based on your 715Databases's restrictions and the UTF-8ness of the data being passed in. Since 716we are storing in columns marked UTF8, we must ensure that binary data is 717encoded on databases which are strict. 718 719This function expects to receive an octet string in order to properly 720evaluate and encode it. It will return an octet string. 721 722NoteArgs is currently used to indicate caller that the message is too long and 723is truncated or dropped. It's a hashref which is expected to be passed to 724L<RT::Record/_NewTransaction>. 725 726=cut 727 728sub _EncodeLOB { 729 my $self = shift; 730 my $Body = shift; 731 my $MIMEType = shift || ''; 732 my $Filename = shift; 733 734 my $ContentEncoding = 'none'; 735 my $note_args; 736 737 RT::Util::assert_bytes( $Body ); 738 739 #get the max attachment length from RT 740 my $MaxSize = RT->Config->Get('MaxAttachmentSize'); 741 742 #if the current attachment contains nulls and the 743 #database doesn't support embedded nulls 744 745 if ( ( !$RT::Handle->BinarySafeBLOBs ) && ( $Body =~ /\x00/ ) ) { 746 747 # set a flag telling us to mimencode the attachment 748 $ContentEncoding = 'base64'; 749 750 #cut the max attchment size by 25% (for mime-encoding overhead. 751 $RT::Logger->debug("Max size is $MaxSize"); 752 $MaxSize = $MaxSize * 3 / 4; 753 # Some databases (postgres) can't handle non-utf8 data 754 } elsif ( !$RT::Handle->BinarySafeBLOBs 755 && $Body =~ /\P{ASCII}/ 756 && !Encode::is_utf8( $Body, 1 ) ) { 757 $ContentEncoding = 'quoted-printable'; 758 } 759 760 #if the attachment is larger than the maximum size 761 if ( ($MaxSize) and ( $MaxSize < length($Body) ) ) { 762 763 my $size = length $Body; 764 # if we're supposed to truncate large attachments 765 if (RT->Config->Get('TruncateLongAttachments')) { 766 767 $RT::Logger->info("$self: Truncated an attachment of size $size"); 768 769 # truncate the attachment to that length. 770 $Body = substr( $Body, 0, $MaxSize ); 771 $note_args = { 772 Type => 'AttachmentTruncate', 773 Data => $Filename, 774 OldValue => $size, 775 NewValue => $MaxSize, 776 ActivateScrips => 0, 777 }; 778 779 } 780 781 # elsif we're supposed to drop large attachments on the floor, 782 elsif (RT->Config->Get('DropLongAttachments')) { 783 784 # drop the attachment on the floor 785 $RT::Logger->info( "$self: Dropped an attachment of size $size" ); 786 $RT::Logger->info( "It started: " . substr( $Body, 0, 60 ) ); 787 $note_args = { 788 Type => 'AttachmentDrop', 789 Data => $Filename, 790 OldValue => $size, 791 NewValue => $MaxSize, 792 ActivateScrips => 0, 793 }; 794 $Filename .= ".txt" if $Filename && $Filename !~ /\.txt$/; 795 return ("none", "Large attachment dropped", "text/plain", $Filename, $note_args ); 796 } 797 } 798 799 # if we need to mimencode the attachment 800 if ( $ContentEncoding eq 'base64' ) { 801 # base64 encode the attachment 802 $Body = MIME::Base64::encode_base64($Body); 803 804 } elsif ($ContentEncoding eq 'quoted-printable') { 805 $Body = MIME::QuotedPrint::encode($Body); 806 } 807 808 809 return ($ContentEncoding, $Body, $MIMEType, $Filename, $note_args ); 810} 811 812=head2 _DecodeLOB C<ContentType>, C<ContentEncoding>, C<Content> 813 814This function reverses the effects of L</_EncodeLOB>, by unpacking the 815data, provided as bytes (not characters!), from the database. This 816data may also be Base64 or Quoted-Printable encoded, as given by 817C<Content-Encoding>. This encoding layer exists because the 818underlying database column is "text", which rejects non-UTF-8 byte 819sequences. 820 821Alternatively, if the data lives in external storage, it will be read 822(or downloaded) and returned. 823 824This function differs in one important way from being the inverse of 825L</_EncodeLOB>: for textual data (as judged via 826L<RT::I18N/IsTextualContentType> applied to the given C<ContentType>), 827C<_DecodeLOB> returns character strings, not bytes. The character set 828used in decoding is taken from the C<ContentType>, or UTF-8 if not 829provided; however, for all textual content inserted by current code, 830the character set used for storage is always UTF-8. 831 832This decoding step is done using L<Encode>'s PERLQQ filter, which 833replaces invalid byte sequences with C<\x{HH}>. This mirrors how data 834from query parameters are parsed in L<RT::Interface::Web/DecodeARGS>. 835Since RT is now strict about the bytes it inserts, substitution 836characters should only be needed for data inserted by older versions 837of RT, or for C<ContentType>s which are now believed to be textual, 838but were not considered so on insertion (and thus not transcoded). 839 840=cut 841 842sub _DecodeLOB { 843 my $self = shift; 844 my $ContentType = shift || ''; 845 my $ContentEncoding = shift || 'none'; 846 my $Content = shift; 847 848 RT::Util::assert_bytes( $Content ); 849 850 if ( $ContentEncoding eq 'base64' ) { 851 $Content = MIME::Base64::decode_base64($Content); 852 } 853 elsif ( $ContentEncoding eq 'quoted-printable' ) { 854 $Content = MIME::QuotedPrint::decode($Content); 855 } 856 elsif ( $ContentEncoding eq 'external' ) { 857 my $Digest = $Content; 858 my $Storage = RT->System->ExternalStorage; 859 unless ($Storage) { 860 RT->Logger->error( "Failed to load $Content; external storage not configured" ); 861 return (""); 862 }; 863 864 ($Content, my $msg) = $Storage->Get( $Digest ); 865 unless (defined $Content) { 866 RT->Logger->error( "Failed to load $Digest from external storage: $msg" ); 867 return (""); 868 } 869 } 870 elsif ( $ContentEncoding && $ContentEncoding ne 'none' ) { 871 return ( $self->loc( "Unknown ContentEncoding [_1]", $ContentEncoding ) ); 872 } 873 874 if ( RT::I18N::IsTextualContentType($ContentType) ) { 875 my $entity = MIME::Entity->new(); 876 $entity->head->add("Content-Type", $ContentType); 877 $entity->bodyhandle( MIME::Body::Scalar->new( $Content ) ); 878 my $charset = RT::I18N::_FindOrGuessCharset($entity); 879 $charset = 'utf-8' if not $charset or not Encode::find_encoding($charset); 880 881 $Content = Encode::decode($charset,$Content,Encode::FB_PERLQQ); 882 } 883 return ($Content); 884} 885 886=head2 Update ARGSHASH 887 888Updates fields on an object for you using the proper Set methods, 889skipping unchanged values. 890 891 ARGSRef => a hashref of attributes => value for the update 892 AttributesRef => an arrayref of keys in ARGSRef that should be updated 893 AttributePrefix => a prefix that should be added to the attributes in AttributesRef 894 when looking up values in ARGSRef 895 Bare attributes are tried before prefixed attributes 896 897Returns a list of localized results of the update 898 899=cut 900 901sub Update { 902 my $self = shift; 903 904 my %args = ( 905 ARGSRef => undef, 906 AttributesRef => undef, 907 AttributePrefix => undef, 908 @_ 909 ); 910 911 my $attributes = $args{'AttributesRef'}; 912 my $ARGSRef = $args{'ARGSRef'}; 913 my %new_values; 914 915 # gather all new values 916 foreach my $attribute (@$attributes) { 917 my $value; 918 if ( defined $ARGSRef->{$attribute} ) { 919 $value = $ARGSRef->{$attribute}; 920 } 921 elsif ( 922 defined( $args{'AttributePrefix'} ) 923 && defined( 924 $ARGSRef->{ $args{'AttributePrefix'} . "-" . $attribute } 925 ) 926 ) { 927 $value = $ARGSRef->{ $args{'AttributePrefix'} . "-" . $attribute }; 928 929 } 930 else { 931 next; 932 } 933 934 $value =~ s/\r\n/\n/gs; 935 936 my $truncated_value = $self->TruncateValue($attribute, $value); 937 938 # If Queue is 'General', we want to resolve the queue name for 939 # the object. 940 941 # This is in an eval block because $object might not exist. 942 # and might not have a Name method. But "can" won't find autoloaded 943 # items. If it fails, we don't care 944 do { 945 no warnings "uninitialized"; 946 947 if ( $attribute ne 'Lifecycle' ) { 948 local $@; 949 my $name = eval { 950 my $object = $attribute . "Obj"; 951 $self->$object->Name; 952 }; 953 unless ($@) { 954 next if $name eq $value || $name eq ( $value || 0 ); 955 } 956 } 957 958 next if $truncated_value eq $self->$attribute(); 959 next if ( $truncated_value || 0 ) eq $self->$attribute(); 960 }; 961 962 $new_values{$attribute} = $value; 963 } 964 965 return $self->_UpdateAttributes( 966 Attributes => $attributes, 967 NewValues => \%new_values, 968 ); 969} 970 971sub _UpdateAttributes { 972 my $self = shift; 973 my %args = ( 974 Attributes => [], 975 NewValues => {}, 976 @_, 977 ); 978 979 my @results; 980 981 foreach my $attribute (@{ $args{Attributes} }) { 982 next if !exists($args{NewValues}{$attribute}); 983 984 my $value = $args{NewValues}{$attribute}; 985 my $method = "Set$attribute"; 986 my ( $code, $msg ) = $self->$method($value); 987 my ($prefix) = ref($self) =~ /RT(?:.*)::(\w+)/; 988 989 # Default to $id, but use name if we can get it. 990 my $label = $self->id; 991 $label = $self->Name if (UNIVERSAL::can($self,'Name')); 992 # this requires model names to be loc'ed. 993 994=for loc 995 996 "Ticket" # loc 997 "User" # loc 998 "Group" # loc 999 "Queue" # loc 1000 1001=cut 1002 1003 push @results, $self->loc( $prefix ) . " $label: ". $msg; 1004 1005=for loc 1006 1007 "[_1] could not be set to [_2].", # loc 1008 "That is already the current value", # loc 1009 "No value sent to _Set!", # loc 1010 "Illegal value for [_1]", # loc 1011 "The new value has been set.", # loc 1012 "No column specified", # loc 1013 "Immutable field", # loc 1014 "Nonexistant field?", # loc 1015 "Invalid data", # loc 1016 "Couldn't find row", # loc 1017 "Missing a primary key?: [_1]", # loc 1018 "Found Object", # loc 1019 1020=cut 1021 1022 } 1023 1024 return @results; 1025} 1026 1027 1028 1029 1030=head2 Members 1031 1032 This returns an RT::Links object which references all the tickets 1033which are 'MembersOf' this ticket 1034 1035=cut 1036 1037sub Members { 1038 my $self = shift; 1039 return ( $self->_Links( 'Target', 'MemberOf' ) ); 1040} 1041 1042 1043 1044=head2 MemberOf 1045 1046 This returns an RT::Links object which references all the tickets that this 1047ticket is a 'MemberOf' 1048 1049=cut 1050 1051sub MemberOf { 1052 my $self = shift; 1053 return ( $self->_Links( 'Base', 'MemberOf' ) ); 1054} 1055 1056 1057 1058=head2 RefersTo 1059 1060 This returns an RT::Links object which shows all references for which this ticket is a base 1061 1062=cut 1063 1064sub RefersTo { 1065 my $self = shift; 1066 return ( $self->_Links( 'Base', 'RefersTo' ) ); 1067} 1068 1069 1070 1071=head2 ReferredToBy 1072 1073This returns an L<RT::Links> object which shows all references for which this ticket is a target 1074 1075=cut 1076 1077sub ReferredToBy { 1078 my $self = shift; 1079 return ( $self->_Links( 'Target', 'RefersTo' ) ); 1080} 1081 1082 1083 1084=head2 DependedOnBy 1085 1086 This returns an RT::Links object which references all the tickets that depend on this one 1087 1088=cut 1089 1090sub DependedOnBy { 1091 my $self = shift; 1092 return ( $self->_Links( 'Target', 'DependsOn' ) ); 1093} 1094 1095 1096 1097 1098=head2 HasUnresolvedDependencies 1099 1100Takes a paramhash of Type (default to '__any'). Returns the number of 1101unresolved dependencies, if $self->UnresolvedDependencies returns an 1102object with one or more members of that type. Returns false 1103otherwise. 1104 1105=cut 1106 1107sub HasUnresolvedDependencies { 1108 my $self = shift; 1109 my %args = ( 1110 Type => undef, 1111 @_ 1112 ); 1113 1114 my $deps = $self->UnresolvedDependencies; 1115 1116 if ($args{Type}) { 1117 $deps->LimitType( VALUE => $args{Type} ); 1118 } else { 1119 $deps->IgnoreType; 1120 } 1121 1122 if ($deps->Count > 0) { 1123 return $deps->Count; 1124 } 1125 else { 1126 return (undef); 1127 } 1128} 1129 1130 1131 1132=head2 UnresolvedDependencies 1133 1134Returns an RT::Tickets object of tickets which this ticket depends on 1135and which have a status of new, open or stalled. (That list comes from 1136RT::Queue->ActiveStatusArray 1137 1138=cut 1139 1140 1141sub UnresolvedDependencies { 1142 my $self = shift; 1143 my $deps = RT::Tickets->new($self->CurrentUser); 1144 1145 $deps->LimitToActiveStatus; 1146 $deps->LimitDependedOnBy($self->Id); 1147 1148 return($deps); 1149 1150} 1151 1152 1153 1154=head2 AllDependedOnBy 1155 1156Returns an array of RT::Ticket objects which (directly or indirectly) 1157depends on this ticket; takes an optional 'Type' argument in the param 1158hash, which will limit returned tickets to that type, as well as cause 1159tickets with that type to serve as 'leaf' nodes that stops the recursive 1160dependency search. 1161 1162=cut 1163 1164sub AllDependedOnBy { 1165 my $self = shift; 1166 return $self->_AllLinkedTickets( LinkType => 'DependsOn', 1167 Direction => 'Target', @_ ); 1168} 1169 1170=head2 AllDependsOn 1171 1172Returns an array of RT::Ticket objects which this ticket (directly or 1173indirectly) depends on; takes an optional 'Type' argument in the param 1174hash, which will limit returned tickets to that type, as well as cause 1175tickets with that type to serve as 'leaf' nodes that stops the 1176recursive dependency search. 1177 1178=cut 1179 1180sub AllDependsOn { 1181 my $self = shift; 1182 return $self->_AllLinkedTickets( LinkType => 'DependsOn', 1183 Direction => 'Base', @_ ); 1184} 1185 1186sub _AllLinkedTickets { 1187 my $self = shift; 1188 1189 my %args = ( 1190 LinkType => undef, 1191 Direction => undef, 1192 Type => undef, 1193 _found => {}, 1194 _top => 1, 1195 @_ 1196 ); 1197 1198 my $dep = $self->_Links( $args{Direction}, $args{LinkType}); 1199 while (my $link = $dep->Next()) { 1200 my $uri = $args{Direction} eq 'Target' ? $link->BaseURI : $link->TargetURI; 1201 next unless ($uri->IsLocal()); 1202 my $obj = $args{Direction} eq 'Target' ? $link->BaseObj : $link->TargetObj; 1203 next if $args{_found}{$obj->Id}; 1204 1205 if (!$args{Type}) { 1206 $args{_found}{$obj->Id} = $obj; 1207 $obj->_AllLinkedTickets( %args, _top => 0 ); 1208 } 1209 elsif ($obj->Type and $obj->Type eq $args{Type}) { 1210 $args{_found}{$obj->Id} = $obj; 1211 } 1212 else { 1213 $obj->_AllLinkedTickets( %args, _top => 0 ); 1214 } 1215 } 1216 1217 if ($args{_top}) { 1218 return map { $args{_found}{$_} } sort keys %{$args{_found}}; 1219 } 1220 else { 1221 return 1; 1222 } 1223} 1224 1225 1226 1227=head2 DependsOn 1228 1229 This returns an RT::Links object which references all the tickets that this ticket depends on 1230 1231=cut 1232 1233sub DependsOn { 1234 my $self = shift; 1235 return ( $self->_Links( 'Base', 'DependsOn' ) ); 1236} 1237 1238 1239 1240 1241 1242 1243=head2 Links DIRECTION [TYPE] 1244 1245Return links (L<RT::Links>) to/from this object. 1246 1247DIRECTION is either 'Base' or 'Target'. 1248 1249TYPE is a type of links to return, it can be omitted to get 1250links of any type. 1251 1252=cut 1253 1254sub Links { shift->_Links(@_) } 1255 1256sub _Links { 1257 my $self = shift; 1258 1259 #TODO: Field isn't the right thing here. but I ahave no idea what mnemonic --- 1260 #tobias meant by $f 1261 my $field = shift; 1262 my $type = shift || ""; 1263 1264 unless ( $self->{"$field$type"} ) { 1265 $self->{"$field$type"} = RT::Links->new( $self->CurrentUser ); 1266 # at least to myself 1267 $self->{"$field$type"}->Limit( FIELD => $field, 1268 VALUE => $self->URI, 1269 ENTRYAGGREGATOR => 'OR' ); 1270 $self->{"$field$type"}->Limit( FIELD => 'Type', 1271 VALUE => $type ) 1272 if ($type); 1273 } 1274 return ( $self->{"$field$type"} ); 1275} 1276 1277 1278 1279 1280=head2 FormatType 1281 1282Takes a Type and returns a string that is more human readable. 1283 1284=cut 1285 1286sub FormatType{ 1287 my $self = shift; 1288 my %args = ( Type => '', 1289 @_ 1290 ); 1291 $args{Type} =~ s/([A-Z])/" " . lc $1/ge; 1292 $args{Type} =~ s/^\s+//; 1293 return $args{Type}; 1294} 1295 1296 1297 1298 1299=head2 FormatLink 1300 1301Takes either a Target or a Base and returns a string of human friendly text. 1302 1303=cut 1304 1305sub FormatLink { 1306 my $self = shift; 1307 my %args = ( Object => undef, 1308 FallBack => '', 1309 @_ 1310 ); 1311 my $text = "URI " . $args{FallBack}; 1312 if ($args{Object} && $args{Object}->isa("RT::Ticket")) { 1313 $text = "Ticket " . $args{Object}->id; 1314 } 1315 return $text; 1316} 1317 1318=head2 _AddLink 1319 1320Takes a paramhash of Type and one of Base or Target. Adds that link to this object. 1321 1322If Silent is true then no transactions will be recorded. You can individually 1323control transactions on both base and target and with SilentBase and 1324SilentTarget respectively. By default both transactions are created. 1325 1326If the link destination is a local object and does the 1327L<RT::Record::Role::Status> role, this method ensures object Status is not 1328"deleted". Linking to deleted objects is forbidden. 1329 1330If the link destination (i.e. not C<$self>) is a local object and the 1331C<$StrictLinkACL> option is enabled, this method checks the appropriate right 1332on the destination object (if any, as returned by the L</ModifyLinkRight> 1333method). B<< The subclass is expected to check the appropriate right on the 1334source object (i.e. C<$self>) before calling this method. >> This allows a 1335different right to be used on the source object during creation, for example. 1336 1337Returns a tuple of (link ID, message, flag if link already existed). 1338 1339=cut 1340 1341sub _AddLink { 1342 my $self = shift; 1343 my %args = ( 1344 Target => '', 1345 Base => '', 1346 Type => '', 1347 Silent => undef, 1348 Silent => undef, 1349 SilentBase => undef, 1350 SilentTarget => undef, 1351 @_ 1352 ); 1353 1354 # Remote_link is the URI of the object that is not this ticket 1355 my $remote_link; 1356 my $direction; 1357 1358 if ( $args{'Base'} and $args{'Target'} ) { 1359 $RT::Logger->debug( "$self tried to create a link. both base and target were specified" ); 1360 return ( 0, $self->loc("Can't specify both base and target") ); 1361 } 1362 elsif ( $args{'Base'} ) { 1363 $args{'Target'} = $self->URI(); 1364 $remote_link = $args{'Base'}; 1365 $direction = 'Target'; 1366 } 1367 elsif ( $args{'Target'} ) { 1368 $args{'Base'} = $self->URI(); 1369 $remote_link = $args{'Target'}; 1370 $direction = 'Base'; 1371 } 1372 else { 1373 return ( 0, $self->loc('Either base or target must be specified') ); 1374 } 1375 1376 my $remote_uri = RT::URI->new( $self->CurrentUser ); 1377 if ($remote_uri->FromURI( $remote_link )) { 1378 my $remote_obj = $remote_uri->IsLocal ? $remote_uri->Object : undef; 1379 if ($remote_obj and $remote_obj->id) { 1380 # Enforce the remote end of StrictLinkACL 1381 if (RT->Config->Get("StrictLinkACL")) { 1382 my $right = $remote_obj->ModifyLinkRight; 1383 1384 return (0, $self->loc("Permission denied")) 1385 if $right and 1386 not $self->CurrentUser->HasRight( Right => $right, Object => $remote_obj ); 1387 } 1388 1389 # Prevent linking to deleted objects 1390 if ($remote_obj->DOES("RT::Record::Role::Status") 1391 and $remote_obj->Status eq "deleted") { 1392 return (0, $self->loc("Linking to a deleted [_1] is not allowed", $self->loc(lc($remote_obj->RecordType)))); 1393 } 1394 } 1395 } else { 1396 return (0, $self->loc("Couldn't resolve '[_1]' into a link.", $remote_link)); 1397 } 1398 1399 # Check if the link already exists - we don't want duplicates 1400 my $old_link = RT::Link->new( $self->CurrentUser ); 1401 $old_link->LoadByParams( Base => $args{'Base'}, 1402 Type => $args{'Type'}, 1403 Target => $args{'Target'} ); 1404 if ( $old_link->Id ) { 1405 $RT::Logger->debug("$self Somebody tried to duplicate a link"); 1406 return ( $old_link->id, $self->loc("Link already exists"), 1 ); 1407 } 1408 1409 if ( $args{'Type'} =~ /^(?:DependsOn|MemberOf)$/ ) { 1410 1411 my @tickets = $self->_AllLinkedTickets( 1412 LinkType => $args{'Type'}, 1413 Direction => $direction eq 'Target' ? 'Base' : 'Target', 1414 ); 1415 if ( grep { $_->id == ( $direction eq 'Target' ? $args{'Base'} : $args{'Target'} ) } @tickets ) { 1416 return ( 0, $self->loc("Refused to add link which would create a circular relationship") ); 1417 } 1418 } 1419 1420 # Storing the link in the DB. 1421 my $link = RT::Link->new( $self->CurrentUser ); 1422 my ($linkid, $linkmsg) = $link->Create( Target => $args{Target}, 1423 Base => $args{Base}, 1424 Type => $args{Type} ); 1425 1426 unless ($linkid) { 1427 $RT::Logger->error("Link could not be created: ".$linkmsg); 1428 return ( 0, $self->loc("Link could not be created: [_1]", $linkmsg) ); 1429 } 1430 1431 my $basetext = $self->FormatLink(Object => $link->BaseObj, 1432 FallBack => $args{Base}); 1433 my $targettext = $self->FormatLink(Object => $link->TargetObj, 1434 FallBack => $args{Target}); 1435 my $typetext = $self->FormatType(Type => $args{Type}); 1436 my $TransString = "$basetext $typetext $targettext."; 1437 1438 # No transactions for you! 1439 return ($linkid, $TransString) if $args{'Silent'}; 1440 1441 my $opposite_direction = $direction eq 'Target' ? 'Base': 'Target'; 1442 1443 # Some transactions? 1444 unless ( $args{ 'Silent'. $direction } ) { 1445 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction( 1446 Type => 'AddLink', 1447 Field => $RT::Link::DIRMAP{$args{'Type'}}->{$direction}, 1448 NewValue => $remote_uri->URI || $remote_link, 1449 TimeTaken => 0 1450 ); 1451 $RT::Logger->error("Couldn't create transaction: $Msg") unless $Trans; 1452 } 1453 1454 if ( !$args{"Silent$opposite_direction"} && $remote_uri->IsLocal ) { 1455 my $OtherObj = $remote_uri->Object; 1456 my ( $val, $msg ) = $OtherObj->_NewTransaction( 1457 Type => 'AddLink', 1458 Field => $RT::Link::DIRMAP{$args{'Type'}}->{$opposite_direction}, 1459 NewValue => $self->URI, 1460 TimeTaken => 0, 1461 ); 1462 $RT::Logger->error("Couldn't create transaction: $msg") unless $val; 1463 } 1464 1465 return ($linkid, $TransString); 1466} 1467 1468=head2 _DeleteLink 1469 1470Takes a paramhash of Type and one of Base or Target. Removes that link from this object. 1471 1472If Silent is true then no transactions will be recorded. You can individually 1473control transactions on both base and target and with SilentBase and 1474SilentTarget respectively. By default both transactions are created. 1475 1476If the link destination (i.e. not C<$self>) is a local object and the 1477C<$StrictLinkACL> option is enabled, this method checks the appropriate right 1478on the destination object (if any, as returned by the L</ModifyLinkRight> 1479method). B<< The subclass is expected to check the appropriate right on the 1480source object (i.e. C<$self>) before calling this method. >> 1481 1482Returns a tuple of (status flag, message). 1483 1484=cut 1485 1486sub _DeleteLink { 1487 my $self = shift; 1488 my %args = ( 1489 Base => undef, 1490 Target => undef, 1491 Type => undef, 1492 Silent => undef, 1493 SilentBase => undef, 1494 SilentTarget => undef, 1495 @_ 1496 ); 1497 1498 # We want one of base and target. We don't care which but we only want _one_. 1499 my $direction; 1500 my $remote_link; 1501 1502 if ( $args{'Base'} and $args{'Target'} ) { 1503 $RT::Logger->debug("$self ->_DeleteLink. got both Base and Target"); 1504 return ( 0, $self->loc("Can't specify both base and target") ); 1505 } 1506 elsif ( $args{'Base'} ) { 1507 $args{'Target'} = $self->URI(); 1508 $remote_link = $args{'Base'}; 1509 $direction = 'Target'; 1510 } 1511 elsif ( $args{'Target'} ) { 1512 $args{'Base'} = $self->URI(); 1513 $remote_link = $args{'Target'}; 1514 $direction = 'Base'; 1515 } 1516 else { 1517 $RT::Logger->error("Base or Target must be specified"); 1518 return ( 0, $self->loc('Either base or target must be specified') ); 1519 } 1520 1521 my $remote_uri = RT::URI->new( $self->CurrentUser ); 1522 if ($remote_uri->FromURI( $remote_link )) { 1523 # Enforce the remote end of StrictLinkACL 1524 my $remote_obj = $remote_uri->IsLocal ? $remote_uri->Object : undef; 1525 if ($remote_obj and $remote_obj->id and RT->Config->Get("StrictLinkACL")) { 1526 my $right = $remote_obj->ModifyLinkRight; 1527 1528 return (0, $self->loc("Permission denied")) 1529 if $right and 1530 not $self->CurrentUser->HasRight( Right => $right, Object => $remote_obj ); 1531 } 1532 } else { 1533 return (0, $self->loc("Couldn't resolve '[_1]' into a link.", $remote_link)); 1534 } 1535 1536 my $link = RT::Link->new( $self->CurrentUser ); 1537 $RT::Logger->debug( "Trying to load link: " 1538 . $args{'Base'} . " " 1539 . $args{'Type'} . " " 1540 . $args{'Target'} ); 1541 1542 $link->LoadByParams( 1543 Base => $args{'Base'}, 1544 Type => $args{'Type'}, 1545 Target => $args{'Target'} 1546 ); 1547 1548 unless ($link->id) { 1549 $RT::Logger->debug("Couldn't find that link"); 1550 return ( 0, $self->loc("Link not found") ); 1551 } 1552 1553 my $basetext = $self->FormatLink(Object => $link->BaseObj, 1554 FallBack => $args{Base}); 1555 my $targettext = $self->FormatLink(Object => $link->TargetObj, 1556 FallBack => $args{Target}); 1557 my $typetext = $self->FormatType(Type => $args{Type}); 1558 my $TransString = "$basetext no longer $typetext $targettext."; 1559 1560 my ($ok, $msg) = $link->Delete(); 1561 unless ($ok) { 1562 RT->Logger->error("Link could not be deleted: $msg"); 1563 return ( 0, $self->loc("Link could not be deleted: [_1]", $msg) ); 1564 } 1565 1566 # No transactions for you! 1567 return (1, $TransString) if $args{'Silent'}; 1568 1569 my $opposite_direction = $direction eq 'Target' ? 'Base': 'Target'; 1570 1571 # Some transactions? 1572 unless ( $args{ 'Silent'. $direction } ) { 1573 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction( 1574 Type => 'DeleteLink', 1575 Field => $RT::Link::DIRMAP{$args{'Type'}}->{$direction}, 1576 OldValue => $remote_uri->URI || $remote_link, 1577 TimeTaken => 0 1578 ); 1579 $RT::Logger->error("Couldn't create transaction: $Msg") unless $Trans; 1580 } 1581 1582 if ( !$args{"Silent$opposite_direction"} && $remote_uri->IsLocal ) { 1583 my $OtherObj = $remote_uri->Object; 1584 my ( $val, $msg ) = $OtherObj->_NewTransaction( 1585 Type => 'DeleteLink', 1586 Field => $RT::Link::DIRMAP{$args{'Type'}}->{$opposite_direction}, 1587 OldValue => $self->URI, 1588 TimeTaken => 0, 1589 ); 1590 $RT::Logger->error("Couldn't create transaction: $msg") unless $val; 1591 } 1592 1593 return (1, $TransString); 1594} 1595 1596=head1 LockForUpdate 1597 1598In a database transaction, gains an exclusive lock on the row, to 1599prevent race conditions. On SQLite, this is a "RESERVED" lock on the 1600entire database. 1601 1602=cut 1603 1604sub LockForUpdate { 1605 my $self = shift; 1606 1607 my $pk = $self->_PrimaryKey; 1608 my $id = @_ ? $_[0] : $self->$pk; 1609 $self->_expire if $self->isa("DBIx::SearchBuilder::Record::Cachable"); 1610 if (RT->Config->Get('DatabaseType') eq "SQLite") { 1611 # SQLite does DB-level locking, upgrading the transaction to 1612 # "RESERVED" on the first UPDATE/INSERT/DELETE. Do a no-op 1613 # UPDATE to force the upgade. 1614 return RT->DatabaseHandle->dbh->do( 1615 "UPDATE " .$self->Table. 1616 " SET $pk = $pk WHERE 1 = 0"); 1617 } else { 1618 return $self->_LoadFromSQL( 1619 "SELECT * FROM ".$self->Table 1620 ." WHERE $pk = ? FOR UPDATE", 1621 $id, 1622 ); 1623 } 1624} 1625 1626=head2 _NewTransaction PARAMHASH 1627 1628Private function to create a new RT::Transaction object for this ticket update 1629 1630=cut 1631 1632sub _NewTransaction { 1633 my $self = shift; 1634 my %args = ( 1635 TimeTaken => undef, 1636 Type => undef, 1637 OldValue => undef, 1638 NewValue => undef, 1639 OldReference => undef, 1640 NewReference => undef, 1641 ReferenceType => undef, 1642 Data => undef, 1643 Field => undef, 1644 MIMEObj => undef, 1645 ActivateScrips => 1, 1646 SquelchMailTo => undef, 1647 @_ 1648 ); 1649 1650 my $in_txn = RT->DatabaseHandle->TransactionDepth; 1651 RT->DatabaseHandle->BeginTransaction unless $in_txn; 1652 1653 $self->LockForUpdate; 1654 1655 my $old_ref = $args{'OldReference'}; 1656 my $new_ref = $args{'NewReference'}; 1657 my $ref_type = $args{'ReferenceType'}; 1658 if ($old_ref or $new_ref) { 1659 $ref_type ||= ref($old_ref) || ref($new_ref); 1660 if (!$ref_type) { 1661 $RT::Logger->error("Reference type not specified for transaction"); 1662 return; 1663 } 1664 $old_ref = $old_ref->Id if ref($old_ref); 1665 $new_ref = $new_ref->Id if ref($new_ref); 1666 } 1667 1668 require RT::Transaction; 1669 my $trans = RT::Transaction->new( $self->CurrentUser ); 1670 my ( $transaction, $msg ) = $trans->Create( 1671 ObjectId => $self->Id, 1672 ObjectType => ref($self), 1673 TimeTaken => $args{'TimeTaken'}, 1674 Type => $args{'Type'}, 1675 Data => $args{'Data'}, 1676 Field => $args{'Field'}, 1677 NewValue => $args{'NewValue'}, 1678 OldValue => $args{'OldValue'}, 1679 NewReference => $new_ref, 1680 OldReference => $old_ref, 1681 ReferenceType => $ref_type, 1682 MIMEObj => $args{'MIMEObj'}, 1683 ActivateScrips => $args{'ActivateScrips'}, 1684 DryRun => $self->{DryRun}, 1685 SquelchMailTo => $args{'SquelchMailTo'} || $self->{TransSquelchMailTo}, 1686 ); 1687 1688 # Rationalize the object since we may have done things to it during the caching. 1689 $self->Load($self->Id); 1690 1691 $RT::Logger->warning($msg) unless $transaction; 1692 1693 $self->_SetLastUpdated; 1694 1695 if ( defined $args{'TimeTaken'} and $self->can('_UpdateTimeTaken')) { 1696 $self->_UpdateTimeTaken( $args{'TimeTaken'}, Transaction => $trans ); 1697 } 1698 if ( RT->Config->Get('UseTransactionBatch') and $transaction ) { 1699 push @{$self->{_TransactionBatch}}, $trans; 1700 } 1701 1702 RT->DatabaseHandle->Commit unless $in_txn; 1703 1704 return ( $transaction, $msg, $trans ); 1705} 1706 1707 1708 1709=head2 Transactions 1710 1711Returns an L<RT::Transactions> object of all transactions on this record object 1712 1713=cut 1714 1715sub Transactions { 1716 my $self = shift; 1717 1718 my $transactions = RT::Transactions->new( $self->CurrentUser ); 1719 $transactions->Limit( 1720 FIELD => 'ObjectId', 1721 VALUE => $self->id, 1722 ); 1723 $transactions->Limit( 1724 FIELD => 'ObjectType', 1725 VALUE => ref($self), 1726 ); 1727 1728 return $transactions; 1729} 1730 1731=head2 SortedTransactions 1732 1733Returns the result of L</Transactions> ordered per the 1734I<OldestTransactionsFirst> preference/option. 1735 1736Pass an optional value 'ASC' or 'DESC' to force a specific 1737order. 1738 1739=cut 1740 1741sub SortedTransactions { 1742 my $self = shift; 1743 my $order = shift || 0; 1744 my $txns = $self->Transactions; 1745 1746 if ( $order && ( $order eq 'ASC' || $order eq 'DESC' ) ) { 1747 # Use provided value 1748 } 1749 else { 1750 $order = RT->Config->Get("OldestTransactionsFirst", $self->CurrentUser) 1751 ? 'ASC' : 'DESC'; 1752 } 1753 1754 $txns->OrderByCols( 1755 { FIELD => 'Created', ORDER => $order }, 1756 { FIELD => 'id', ORDER => $order }, 1757 ); 1758 return $txns; 1759} 1760 1761our %TRANSACTION_CLASSIFICATION = ( 1762 Create => 'message', 1763 Correspond => 'message', 1764 Comment => 'message', 1765 1766 AddWatcher => 'people', 1767 DelWatcher => 'people', 1768 1769 Take => 'people', 1770 Untake => 'people', 1771 Force => 'people', 1772 Steal => 'people', 1773 Give => 'people', 1774 1775 AddLink => 'links', 1776 DeleteLink => 'links', 1777 1778 Status => 'basics', 1779 Set => { 1780 __default => 'basics', 1781 map( { $_ => 'dates' } qw( 1782 Told Starts Started Due LastUpdated Created LastUpdated 1783 ) ), 1784 map( { $_ => 'people' } qw( 1785 Owner Creator LastUpdatedBy 1786 ) ), 1787 }, 1788 CustomField => 'cfs', 1789 SystemError => 'error', 1790 AttachmentTruncate => 'attachment-truncate', 1791 AttachmentDrop => 'attachment-drop', 1792 AttachmentError => 'error', 1793 __default => 'other', 1794); 1795 1796sub ClassifyTransaction { 1797 my $self = shift; 1798 my $txn = shift; 1799 1800 my $type = $txn->Type; 1801 1802 my $res = $TRANSACTION_CLASSIFICATION{ $type }; 1803 return $res || $TRANSACTION_CLASSIFICATION{ '__default' } 1804 unless ref $res; 1805 1806 return $res->{ $txn->Field } || $res->{'__default'} 1807 || $TRANSACTION_CLASSIFICATION{ '__default' }; 1808} 1809 1810=head2 Attachments 1811 1812Returns an L<RT::Attachments> object of all attachments on this record object 1813(for all its L</Transactions>). 1814 1815By default Content and Headers of attachments are not fetched right away from 1816database. Use C<WithContent> and C<WithHeaders> options to override this. 1817 1818=cut 1819 1820sub Attachments { 1821 my $self = shift; 1822 my %args = ( 1823 WithHeaders => 0, 1824 WithContent => 0, 1825 @_ 1826 ); 1827 my @columns = grep { not /^(Headers|Content)$/ } 1828 RT::Attachment->ReadableAttributes; 1829 push @columns, 'Headers' if $args{'WithHeaders'}; 1830 push @columns, 'Content' if $args{'WithContent'}; 1831 1832 my $res = RT::Attachments->new( $self->CurrentUser ); 1833 $res->Columns( @columns ); 1834 my $txn_alias = $res->TransactionAlias; 1835 $res->Limit( 1836 ALIAS => $txn_alias, 1837 FIELD => 'ObjectType', 1838 VALUE => ref($self), 1839 ); 1840 $res->Limit( 1841 ALIAS => $txn_alias, 1842 FIELD => 'ObjectId', 1843 VALUE => $self->id, 1844 ); 1845 return $res; 1846} 1847 1848=head2 TextAttachments 1849 1850Returns an L<RT::Attachments> object of all attachments, like L<Attachments>, 1851but only those that are text. 1852 1853By default Content and Headers are fetched. Use C<WithContent> and 1854C<WithHeaders> options to override this. 1855 1856=cut 1857 1858sub TextAttachments { 1859 my $self = shift; 1860 my $res = $self->Attachments( 1861 WithHeaders => 1, 1862 WithContent => 1, 1863 @_ 1864 ); 1865 $res->Limit( FIELD => 'ContentType', OPERATOR => '=', VALUE => 'text/plain'); 1866 $res->Limit( FIELD => 'ContentType', OPERATOR => 'STARTSWITH', VALUE => 'message/'); 1867 $res->Limit( FIELD => 'ContentType', OPERATOR => '=', VALUE => 'text'); 1868 $res->Limit( FIELD => 'Filename', OPERATOR => 'IS', VALUE => 'NULL') 1869 if RT->Config->Get( 'SuppressInlineTextFiles', $self->CurrentUser ); 1870 return $res; 1871} 1872 1873sub CustomFields { 1874 my $self = shift; 1875 my $cfs = RT::CustomFields->new( $self->CurrentUser ); 1876 1877 $cfs->SetContextObject( $self ); 1878 # XXX handle multiple types properly 1879 $cfs->LimitToLookupType( $self->CustomFieldLookupType ); 1880 $cfs->LimitToGlobalOrObjectId( $self->CustomFieldLookupId ); 1881 $cfs->ApplySortOrder; 1882 1883 return $cfs; 1884} 1885 1886# TODO: This _only_ works for RT::Foo classes. it doesn't work, for 1887# example, for RT::IR::Foo classes. 1888 1889sub CustomFieldLookupId { 1890 my $self = shift; 1891 my $lookup = shift || $self->CustomFieldLookupType; 1892 my @classes = ($lookup =~ /RT::(\w+)-/g); 1893 1894 # Work on "RT::Queue", for instance 1895 return $self->Id unless @classes; 1896 1897 my $object = $self; 1898 # Save a ->Load call by not calling ->FooObj->Id, just ->Foo 1899 my $final = shift @classes; 1900 foreach my $class (reverse @classes) { 1901 my $method = "${class}Obj"; 1902 $object = $object->$method; 1903 } 1904 1905 my $id = $object->$final; 1906 unless (defined $id) { 1907 my $method = "${final}Obj"; 1908 $id = $object->$method->Id; 1909 } 1910 return $id; 1911} 1912 1913 1914=head2 CustomFieldLookupType 1915 1916Returns the path RT uses to figure out which custom fields apply to this object. 1917 1918=cut 1919 1920sub CustomFieldLookupType { 1921 my $self = shift; 1922 return ref($self) || $self; 1923} 1924 1925 1926=head2 AddCustomFieldValue { Field => FIELD, Value => VALUE } 1927 1928VALUE should be a string. FIELD can be any identifier of a CustomField 1929supported by L</LoadCustomFieldByIdentifier> method. 1930 1931Adds VALUE as a value of CustomField FIELD. If this is a single-value custom field, 1932deletes the old value. 1933If VALUE is not a valid value for the custom field, returns 1934(0, 'Error message' ) otherwise, returns ($id, 'Success Message') where 1935$id is ID of created L<ObjectCustomFieldValue> object. 1936 1937=cut 1938 1939sub AddCustomFieldValue { 1940 my $self = shift; 1941 $self->_AddCustomFieldValue(@_); 1942} 1943 1944sub _AddCustomFieldValue { 1945 my $self = shift; 1946 my %args = ( 1947 Field => undef, 1948 Value => undef, 1949 LargeContent => undef, 1950 ContentType => undef, 1951 RecordTransaction => 1, 1952 ForCreation => 0, 1953 @_ 1954 ); 1955 1956 my $cf = $self->LoadCustomFieldByIdentifier($args{'Field'}); 1957 $cf->{include_set_initial} = 1 if $args{'ForCreation'}; 1958 1959 unless ( $cf->Id ) { 1960 return ( 0, $self->loc( "Custom field [_1] not found", $args{'Field'} ) ); 1961 } 1962 1963 my $OCFs = $self->CustomFields; 1964 $OCFs->Limit( FIELD => 'id', VALUE => $cf->Id ); 1965 unless ( $OCFs->Count ) { 1966 return ( 1967 0, 1968 $self->loc( 1969 "Custom field [_1] does not apply to this object", 1970 ref $args{'Field'} ? $args{'Field'}->id : $args{'Field'} 1971 ) 1972 ); 1973 } 1974 1975 # empty string is not correct value of any CF, so undef it 1976 foreach ( qw(Value LargeContent) ) { 1977 $args{ $_ } = undef if defined $args{ $_ } && !length $args{ $_ }; 1978 } 1979 1980 unless ( $cf->ValidateValue( $args{'Value'} ) ) { 1981 return ( 0, $self->loc("Invalid value for custom field") ); 1982 } 1983 1984 # If the custom field only accepts a certain # of values, delete the existing 1985 # value and record a "changed from foo to bar" transaction 1986 unless ( $cf->UnlimitedValues ) { 1987 1988 # Load up a ObjectCustomFieldValues object for this custom field and this ticket 1989 my $values = $cf->ValuesForObject($self); 1990 1991 # We need to whack any old values here. In most cases, the custom field should 1992 # only have one value to delete. In the pathalogical case, this custom field 1993 # used to be a multiple and we have many values to whack.... 1994 my $cf_values = $values->Count; 1995 1996 if ( $cf_values > $cf->MaxValues ) { 1997 my $i = 0; #We want to delete all but the max we can currently have , so we can then 1998 # execute the same code to "change" the value from old to new 1999 while ( my $value = $values->Next ) { 2000 $i++; 2001 if ( $i < $cf_values ) { 2002 my ( $val, $msg ) = $cf->DeleteValueForObject( 2003 Object => $self, 2004 Id => $value->id, 2005 ); 2006 unless ($val) { 2007 return ( 0, $msg ); 2008 } 2009 my ( $TransactionId, $Msg, $TransactionObj ) = 2010 $self->_NewTransaction( 2011 Type => 'CustomField', 2012 Field => $cf->Id, 2013 OldReference => $value, 2014 ); 2015 } 2016 } 2017 $values->RedoSearch if $i; # redo search if have deleted at least one value 2018 } 2019 2020 if ( my $entry = $values->HasEntry($args{'Value'}, $args{'LargeContent'}) ) { 2021 return $entry->id; 2022 } 2023 2024 my $old_value = $values->First; 2025 my $old_content; 2026 $old_content = $old_value->Content if $old_value; 2027 2028 my ( $new_value_id, $value_msg ) = $cf->AddValueForObject( 2029 Object => $self, 2030 Content => $args{'Value'}, 2031 LargeContent => $args{'LargeContent'}, 2032 ContentType => $args{'ContentType'}, 2033 ForCreation => $args{'ForCreation'}, 2034 ); 2035 2036 unless ( $new_value_id ) { 2037 return ( 0, $self->loc( "Could not add new custom field value: [_1]", $value_msg ) ); 2038 } 2039 2040 my $new_value = RT::ObjectCustomFieldValue->new( $self->CurrentUser ); 2041 $new_value->{include_set_initial} = 1 if $args{'ForCreation'}; 2042 $new_value->Load( $new_value_id ); 2043 2044 if ( $args{'RecordTransaction'} ) { 2045 my ( $TransactionId, $Msg, $TransactionObj ) = 2046 $self->_NewTransaction( 2047 Type => 'CustomField', 2048 Field => $cf->Id, 2049 OldReference => $old_value, 2050 NewReference => $new_value, 2051 ); 2052 } 2053 2054 my $new_content = $new_value->Content; 2055 2056 # For datetime, we need to display them in "human" format in result message 2057 #XXX TODO how about date without time? 2058 if ($cf->Type eq 'DateTime') { 2059 my $DateObj = RT::Date->new( $self->CurrentUser ); 2060 $DateObj->Set( 2061 Format => 'ISO', 2062 Value => $new_content, 2063 ); 2064 $new_content = $DateObj->AsString; 2065 2066 if ( defined $old_content && length $old_content ) { 2067 $DateObj->Set( 2068 Format => 'ISO', 2069 Value => $old_content, 2070 ); 2071 $old_content = $DateObj->AsString; 2072 } 2073 } 2074 2075 unless ( defined $old_content && length $old_content ) { 2076 return ( $new_value_id, $self->loc( "[_1] [_2] added", $cf->Name, $new_content )); 2077 } 2078 elsif ( !defined $new_content || !length $new_content ) { 2079 return ( $new_value_id, 2080 $self->loc( "[_1] [_2] deleted", $cf->Name, $old_content ) ); 2081 } 2082 else { 2083 return ( $new_value_id, $self->loc( "[_1] [_2] changed to [_3]", $cf->Name, $old_content, $new_content)); 2084 } 2085 2086 } 2087 2088 # otherwise, just add a new value and record "new value added" 2089 else { 2090 my $values = $cf->ValuesForObject($self); 2091 if ( my $entry = $values->HasEntry($args{'Value'}, $args{'LargeContent'}) ) { 2092 return $entry->id; 2093 } 2094 2095 my ($new_value_id, $msg) = $cf->AddValueForObject( 2096 Object => $self, 2097 Content => $args{'Value'}, 2098 LargeContent => $args{'LargeContent'}, 2099 ContentType => $args{'ContentType'}, 2100 ForCreation => $args{'ForCreation'}, 2101 ); 2102 2103 unless ( $new_value_id ) { 2104 return ( 0, $self->loc( "Could not add new custom field value: [_1]", $msg ) ); 2105 } 2106 2107 if ( $args{'RecordTransaction'} ) { 2108 my ( $tid, $msg ) = $self->_NewTransaction( 2109 Type => 'CustomField', 2110 Field => $cf->Id, 2111 NewReference => $new_value_id, 2112 ReferenceType => 'RT::ObjectCustomFieldValue', 2113 ); 2114 unless ( $tid ) { 2115 return ( 0, $self->loc( "Couldn't create a transaction: [_1]", $msg ) ); 2116 } 2117 } 2118 return ( $new_value_id, $self->loc( "[_1] added as a value for [_2]", $args{'Value'}, $cf->Name ) ); 2119 } 2120} 2121 2122=head2 AddCustomFieldDefaultValues 2123 2124Add default values to object's empty custom fields. 2125 2126=cut 2127 2128sub AddCustomFieldDefaultValues { 2129 my $self = shift; 2130 2131 my $object = ( ref $self )->new( RT->SystemUser ); 2132 $object->Load( $self->id ); 2133 2134 my $cfs = $object->CustomFields; 2135 my @msgs; 2136 while ( my $cf = $cfs->Next ) { 2137 next if $object->CustomFieldValues($cf->id)->Count || !$cf->SupportDefaultValues; 2138 my ( $on ) = grep { $_->isa( $cf->RecordClassFromLookupType ) } $cf->ACLEquivalenceObjects; 2139 my $values = $cf->DefaultValues( Object => $on || RT->System ); 2140 foreach my $value ( UNIVERSAL::isa( $values => 'ARRAY' ) ? @$values : $values ) { 2141 next if $object->CustomFieldValueIsEmpty( 2142 Field => $cf, 2143 Value => $value, 2144 ); 2145 2146 my ( $status, $msg ) = $object->_AddCustomFieldValue( 2147 Field => $cf->id, 2148 Value => $value, 2149 RecordTransaction => 0, 2150 ); 2151 push @msgs, $msg unless $status; 2152 } 2153 } 2154 return ( 0, @msgs ) if @msgs; 2155 return 1; 2156} 2157 2158=head2 CustomFieldValueIsEmpty { Field => FIELD, Value => VALUE } 2159 2160Check if the custom field value is empty. 2161 2162Some custom fields could have other special empty values, e.g. "1970-01-01" is empty for Date cf 2163 2164Return 1 if it is empty, 0 otherwise. 2165 2166=cut 2167 2168 2169sub CustomFieldValueIsEmpty { 2170 my $self = shift; 2171 my %args = ( 2172 Field => undef, 2173 Value => undef, 2174 @_ 2175 ); 2176 my $value = $args{Value}; 2177 return 1 unless defined $value && length $value; 2178 2179 my $cf = ref($args{'Field'}) 2180 ? $args{'Field'} 2181 : $self->LoadCustomFieldByIdentifier( $args{'Field'} ); 2182 2183 if ($cf) { 2184 if ( $cf->__Value('Type') =~ /^Date(?:Time)?$/ ) { 2185 my $DateObj = RT::Date->new( $self->CurrentUser ); 2186 $DateObj->Set( 2187 Format => 'unknown', 2188 Value => $value, 2189 $cf->Type eq 'Date' ? ( Timezone => 'UTC' ) : (), 2190 ); 2191 return 1 unless $DateObj->IsSet; 2192 } 2193 } 2194 return 0; 2195} 2196 2197=head2 DeleteCustomFieldValue { Field => FIELD, Value => VALUE } 2198 2199Deletes VALUE as a value of CustomField FIELD. 2200 2201VALUE can be a string, a CustomFieldValue or a ObjectCustomFieldValue. 2202 2203If VALUE is not a valid value for the custom field, returns 2204(0, 'Error message' ) otherwise, returns (1, 'Success Message') 2205 2206=cut 2207 2208sub DeleteCustomFieldValue { 2209 my $self = shift; 2210 my %args = ( 2211 Field => undef, 2212 Value => undef, 2213 ValueId => undef, 2214 @_ 2215 ); 2216 2217 my $cf = $self->LoadCustomFieldByIdentifier($args{'Field'}); 2218 unless ( $cf->Id ) { 2219 return ( 0, $self->loc( "Custom field [_1] not found", $args{'Field'} ) ); 2220 } 2221 2222 my ( $val, $msg ) = $cf->DeleteValueForObject( 2223 Object => $self, 2224 Id => $args{'ValueId'}, 2225 Content => $args{'Value'}, 2226 ); 2227 unless ($val) { 2228 return ( 0, $msg ); 2229 } 2230 2231 my ( $TransactionId, $Msg, $TransactionObj ) = $self->_NewTransaction( 2232 Type => 'CustomField', 2233 Field => $cf->Id, 2234 OldReference => $val, 2235 ReferenceType => 'RT::ObjectCustomFieldValue', 2236 ); 2237 unless ($TransactionId) { 2238 return ( 0, $self->loc( "Couldn't create a transaction: [_1]", $Msg ) ); 2239 } 2240 2241 my $old_value = $TransactionObj->OldValue; 2242 # For datetime, we need to display them in "human" format in result message 2243 if ( $cf->Type eq 'DateTime' ) { 2244 my $DateObj = RT::Date->new( $self->CurrentUser ); 2245 $DateObj->Set( 2246 Format => 'ISO', 2247 Value => $old_value, 2248 ); 2249 $old_value = $DateObj->AsString; 2250 } 2251 return ( 2252 $TransactionId, 2253 $self->loc( 2254 "[_1] is no longer a value for custom field [_2]", 2255 $old_value, $cf->Name 2256 ) 2257 ); 2258} 2259 2260 2261 2262=head2 FirstCustomFieldValue FIELD 2263 2264Return the content of the first value of CustomField FIELD for this ticket 2265Takes a field id or name 2266 2267=cut 2268 2269sub FirstCustomFieldValue { 2270 my $self = shift; 2271 my $field = shift; 2272 2273 my $values = $self->CustomFieldValues( $field ); 2274 return undef unless my $first = $values->First; 2275 return $first->Content; 2276} 2277 2278=head2 CustomFieldValuesAsString FIELD 2279 2280Return the content of the CustomField FIELD for this ticket. 2281If this is a multi-value custom field, values will be joined with newlines. 2282 2283Takes a field id or name as the first argument 2284 2285Takes an optional Separator => "," second and third argument 2286if you want to join the values using something other than a newline 2287 2288=cut 2289 2290sub CustomFieldValuesAsString { 2291 my $self = shift; 2292 my $field = shift; 2293 my %args = @_; 2294 my $separator = $args{Separator} || "\n"; 2295 2296 my $values = $self->CustomFieldValues( $field ); 2297 return join ($separator, grep { defined $_ } 2298 map { $_->Content } @{$values->ItemsArrayRef}); 2299} 2300 2301 2302 2303=head2 CustomFieldValues FIELD 2304 2305Return a ObjectCustomFieldValues object of all values of the CustomField whose 2306id or Name is FIELD for this record. 2307 2308Returns an RT::ObjectCustomFieldValues object 2309 2310=cut 2311 2312sub CustomFieldValues { 2313 my $self = shift; 2314 my $field = shift; 2315 2316 if ( $field ) { 2317 my $cf = $self->LoadCustomFieldByIdentifier( $field ); 2318 2319 # we were asked to search on a custom field we couldn't find 2320 unless ( $cf->id ) { 2321 $RT::Logger->warning("Couldn't load custom field by '$field' identifier"); 2322 return RT::ObjectCustomFieldValues->new( $self->CurrentUser ); 2323 } 2324 return ( $cf->ValuesForObject($self) ); 2325 } 2326 2327 # we're not limiting to a specific custom field; 2328 my $ocfs = RT::ObjectCustomFieldValues->new( $self->CurrentUser ); 2329 $ocfs->LimitToObject( $self ); 2330 return $ocfs; 2331} 2332 2333=head2 LoadCustomFieldByIdentifier IDENTIFER 2334 2335Find the custom field has id or name IDENTIFIER for this object. 2336 2337If no valid field is found, returns an empty RT::CustomField object. 2338 2339=cut 2340 2341sub LoadCustomFieldByIdentifier { 2342 my $self = shift; 2343 my $field = shift; 2344 2345 my $cf; 2346 if ( UNIVERSAL::isa( $field, "RT::CustomField" ) ) { 2347 $cf = RT::CustomField->new($self->CurrentUser); 2348 $cf->SetContextObject( $self ); 2349 $cf->LoadById( $field->id ); 2350 } 2351 elsif ($field =~ /^\d+$/) { 2352 $cf = RT::CustomField->new($self->CurrentUser); 2353 $cf->SetContextObject( $self ); 2354 $cf->LoadById($field); 2355 } else { 2356 2357 my $cfs = $self->CustomFields($self->CurrentUser); 2358 $cfs->SetContextObject( $self ); 2359 $cfs->Limit(FIELD => 'Name', VALUE => $field, CASESENSITIVE => 0); 2360 $cf = $cfs->First || RT::CustomField->new($self->CurrentUser); 2361 } 2362 return $cf; 2363} 2364 2365sub ACLEquivalenceObjects { } 2366 2367=head2 HasRight 2368 2369 Takes a paramhash with the attributes 'Right' and 'Principal' 2370 'Right' is a ticket-scoped textual right from RT::ACE 2371 'Principal' is an RT::User object 2372 2373 Returns 1 if the principal has the right. Returns undef if not. 2374 2375=cut 2376 2377sub HasRight { 2378 my $self = shift; 2379 my %args = ( 2380 Right => undef, 2381 Principal => undef, 2382 @_ 2383 ); 2384 2385 $args{Principal} ||= $self->CurrentUser->PrincipalObj; 2386 2387 return $args{'Principal'}->HasRight( 2388 Object => $self->Id ? $self : $RT::System, 2389 Right => $args{'Right'} 2390 ); 2391} 2392 2393sub CurrentUserHasRight { 2394 my $self = shift; 2395 return $self->HasRight( Right => @_ ); 2396} 2397 2398sub ModifyLinkRight { } 2399 2400=head2 ColumnMapClassName 2401 2402ColumnMap needs a massaged collection class name to load the correct list 2403display. Equivalent to L<RT::SearchBuilder/ColumnMapClassName>, but provided 2404for a record instead of a collection. 2405 2406Returns a string. May be called as a package method. 2407 2408=cut 2409 2410sub ColumnMapClassName { 2411 my $self = shift; 2412 my $Class = ref($self) || $self; 2413 $Class =~ s/:/_/g; 2414 return $Class; 2415} 2416 2417sub BasicColumns { } 2418 2419sub WikiBase { 2420 return RT->Config->Get('WebPath'). "/index.html?q="; 2421} 2422 2423# Matches one field in "field - field" style range specs. Subclasses 2424# that can participate in custom date ranges should override this method 2425# to match their additional date fields. Be sure to call this superclass 2426# method to get "now", datetime columns and CF parsing. 2427 2428sub _CustomDateRangeFieldParser { 2429 my $self = shift; 2430 my $regex = qr{ 2431 now 2432 | cf\. (?: \{ .*? \} | \S+ ) 2433 }xi; 2434 2435 for my $column ( keys %{ $_TABLE_ATTR->{ ref $self || $self} } ) { 2436 my $entry = $_TABLE_ATTR->{ ref $self || $self}{$column}; 2437 next unless $entry->{read} && ( $entry->{type} // '' ) eq 'datetime'; 2438 $regex .= '|' . qr{$column}i; 2439 } 2440 return $regex; 2441} 2442 2443# Returns an RT::Date instantiated with this record's value for the parsed 2444# field name. Includes the $range_name parameter only for diagnostics. 2445# Subclasses should override this to instantiate the fields they added in 2446# _CustomDateRangeFieldParser. 2447 2448sub _DateForCustomDateRangeField { 2449 my $self = shift; 2450 my $field = shift; 2451 my $range_name = shift; 2452 2453 my $date = RT::Date->new($self->CurrentUser); 2454 2455 if (lc($field) eq 'now') { 2456 $date->Set(Format => 'unix', Value => time); 2457 } 2458 elsif ($field =~ m{^ cf\. (?: \{ (.*?) \} | (\S+) ) $}xi) { 2459 my $name = $1 || $2; 2460 my $value = $self->FirstCustomFieldValue($name); 2461 2462 if (!$value) { 2463 # no CF value for this record, so bail out 2464 return; 2465 } 2466 2467 $date->Set(Format => 'unknown', Value => $value, Timezone => 'UTC'); 2468 } 2469 else { 2470 if ( my ($column) = grep { lc $field eq lc $_ } keys %{ $_TABLE_ATTR->{ ref $self || $self } } ) { 2471 my $method = $column . 'Obj'; 2472 if ( $self->can($method) ) { 2473 $date = $self->$method; 2474 } 2475 else { 2476 RT->Logger->error( "Missing $method in " . ref $self ); 2477 return; 2478 } 2479 } 2480 else { 2481 RT->Logger->error("Unable to parse '$field' as a field name in CustomDateRanges '$range_name'"); 2482 return; 2483 } 2484 } 2485 2486 return $date; 2487} 2488 2489# Parses custom date range spec and returns a hash containing parsed info. 2490# Returns the empty list if there's an error. 2491 2492sub _ParseCustomDateRangeSpec { 2493 my $self = shift; 2494 my $name = shift; 2495 my $spec = shift; 2496 2497 my $calculation; 2498 my $format; 2499 2500 if (ref($spec)) { 2501 $calculation = $spec->{value} || join( ' - ', $spec->{to}, $spec->{from} ); 2502 $format = $spec->{format}; 2503 } 2504 else { 2505 $calculation = $spec; 2506 } 2507 2508 if (!$calculation || ref($calculation)) { 2509 RT->Logger->error("CustomDateRanges '$name' 'value' must be a string"); 2510 return; 2511 } 2512 2513 if ($format && ref($format) ne 'CODE') { 2514 RT->Logger->error("CustomDateRanges '$name' 'format' must be a CODE reference"); 2515 return; 2516 } 2517 2518 # class-specific matcher for now, created, CF.{foo bar}, CF.baz, etc. 2519 my $field_parser = $self->_CustomDateRangeFieldParser; 2520 2521 # regex parses "field - field" (intentionally very strict) 2522 my $calculation_parser = qr{ 2523 ^ 2524 ($field_parser) # to field name 2525 \s+ - \s+ # space, operator, more space 2526 ($field_parser) # from field name 2527 $ 2528 }x; 2529 2530 my @matches = $calculation =~ $calculation_parser; 2531 2532 if (!@matches) { 2533 RT->Logger->error("Unable to parse '$calculation' as a calculated value in CustomDateRanges '$name'"); 2534 return; 2535 } 2536 2537 if ( ref $spec ) { 2538 for my $type ( qw/from to/ ) { 2539 if ( $spec->{"${type}_fallback"} && $spec->{"${type}_fallback"} !~ /^$field_parser$/ ) { 2540 RT->Logger->error( "Invalid ${type}_fallback field: " . $spec->{"${type}_fallback"} ); 2541 return; 2542 } 2543 } 2544 } 2545 2546 my %date_range_spec = ( from => $matches[1], to => $matches[0], ref $spec ? %$spec : () ); 2547 return %date_range_spec; 2548} 2549 2550=head2 CustomDateRange name, spec 2551 2552Takes a L<RT_Config/%CustomDateRanges>-style spec string and its name (for 2553diagnostics). Returns a localized string evaluating the calculation. If either 2554date is unset, or anything fails to parse, this returns C<undef>. 2555 2556=cut 2557 2558sub CustomDateRange { 2559 my $self = shift; 2560 my $name = shift; 2561 my $spec = shift; 2562 2563 my %date_range_spec = $self->_ParseCustomDateRangeSpec($name, $spec); 2564 2565 # parse failed; render no value 2566 return unless $date_range_spec{from} && $date_range_spec{to}; 2567 2568 my $end_dt = $self->_DateForCustomDateRangeField($date_range_spec{to}, $name); 2569 my $start_dt = $self->_DateForCustomDateRangeField($date_range_spec{from}, $name); 2570 2571 unless ( $start_dt && $start_dt->IsSet ) { 2572 if ( ref $spec && $date_range_spec{from_fallback} ) { 2573 $start_dt = $self->_DateForCustomDateRangeField( $date_range_spec{from_fallback}, $name ); 2574 } 2575 } 2576 2577 unless ( $end_dt && $end_dt->IsSet ) { 2578 if ( ref $spec && $date_range_spec{to_fallback} ) { 2579 $end_dt = $self->_DateForCustomDateRangeField( $date_range_spec{to_fallback}, $name ); 2580 } 2581 } 2582 2583 # RT::Date instantiation failed; render no value 2584 return unless $start_dt && $start_dt->IsSet 2585 && $end_dt && $end_dt->IsSet; 2586 2587 my $duration; 2588 if ( $date_range_spec{business_time} ) { 2589 my $schedule; 2590 my $timezone; 2591 2592 # Prefer the schedule/timezone specified in %ServiceAgreements for current object 2593 if ( $self->isa('RT::Ticket') && !$self->QueueObj->SLADisabled && $self->SLA ) { 2594 if ( my $config = RT->Config->Get('ServiceAgreements') ) { 2595 if ( ref( $config->{QueueDefault}{ $self->QueueObj->Name } ) eq 'HASH' ) { 2596 $timezone = $config->{QueueDefault}{ $self->QueueObj->Name }{Timezone}; 2597 } 2598 2599 # Each SLA could have its own schedule and timezone 2600 if ( my $agreement = $config->{Levels}{ $self->SLA } ) { 2601 $schedule = $agreement->{BusinessHours}; 2602 $timezone ||= $agreement->{Timezone}; 2603 } 2604 } 2605 } 2606 $timezone ||= RT->Config->Get('Timezone'); 2607 $schedule ||= 'Default'; 2608 2609 { 2610 local $ENV{'TZ'} = $ENV{'TZ'}; 2611 if ( $timezone ne ( $ENV{'TZ'} || '' ) ) { 2612 $ENV{'TZ'} = $timezone; 2613 require POSIX; 2614 POSIX::tzset(); 2615 } 2616 2617 my $bhours = RT::SLA->BusinessHours($schedule); 2618 $duration = $bhours->between( 2619 $start_dt->Unix <= $end_dt->Unix 2620 ? ( $start_dt->Unix, $end_dt->Unix ) 2621 : ( $end_dt->Unix, $start_dt->Unix ) 2622 ); 2623 $duration *= -1 if $start_dt->Unix > $end_dt->Unix; 2624 } 2625 2626 if ( $timezone ne ( $ENV{'TZ'} || '' ) ) { 2627 POSIX::tzset(); 2628 } 2629 } 2630 2631 $duration //= $end_dt->Diff($start_dt); 2632 2633 # _ParseCustomDateRangeSpec guarantees $format is a coderef 2634 if ($date_range_spec{format}) { 2635 return $date_range_spec{format}->($duration, $end_dt, $start_dt, $self); 2636 } 2637 else { 2638 my $max_unit = $date_range_spec{business_time} ? 'hour' : 'year'; 2639 2640 # "x days ago" is strongly suggestive of comparing with the current 2641 # time; but if we're comparing two arbitrary times, "x days prior" 2642 # reads better 2643 if ($duration < 0) { 2644 $duration *= -1; 2645 return $self->loc('[_1] prior', $end_dt->DurationAsString($duration, MaxUnit => $max_unit)); 2646 } 2647 else { 2648 return $end_dt->DurationAsString($duration, MaxUnit => $max_unit); 2649 } 2650 } 2651} 2652 2653=head2 CustomDateRanges 2654 2655Return all of the custom date ranges of current class. 2656 2657=cut 2658 2659sub CustomDateRanges { 2660 my $self = shift; 2661 my %args = ( 2662 Type => undef, 2663 ExcludeSystem => undef, 2664 ExcludeUsers => undef, 2665 ExcludeUser => undef, 2666 @_, 2667 ); 2668 2669 my $type = $args{Type} || ref $self || $self,; 2670 my %ranges; 2671 2672 if ( !$args{ExcludeSystem} ) { 2673 if ( my $config = RT->Config->Get('CustomDateRanges') ) { 2674 for my $name ( keys %{ $config->{$type} || {} } ) { 2675 $ranges{$name} ||= $config->{$type}{$name}; 2676 } 2677 } 2678 2679 if ( my $db_config = RT->Config->Get('CustomDateRangesUI') ) { 2680 for my $name ( keys %{ $db_config->{$type} || {} } ) { 2681 $ranges{$name} ||= $db_config->{$type}{$name}; 2682 } 2683 } 2684 } 2685 2686 if ( !$args{ExcludeUsers} ) { 2687 my $attributes = RT::Attributes->new( RT->SystemUser ); 2688 $attributes->Limit( FIELD => 'Name', VALUE => 'Pref-CustomDateRanges' ); 2689 $attributes->Limit( FIELD => 'ObjectType', VALUE => 'RT::User' ); 2690 if ( $args{ExcludeUser} ) { 2691 $attributes->Limit( FIELD => 'Creator', OPERATOR => '!=', VALUE => $args{ExcludeUser} ); 2692 } 2693 $attributes->OrderBy( FIELD => 'id' ); 2694 2695 while ( my $attribute = $attributes->Next ) { 2696 if ( my $content = $attribute->Content ) { 2697 for my $name ( keys %{ $content->{$type} || {} } ) { 2698 $ranges{$name} ||= $content->{$type}{$name}; 2699 } 2700 } 2701 } 2702 } 2703 return %ranges; 2704} 2705 2706=head2 CustomDateRangeFields 2707 2708Return all of the fields custom date range could use for current class. 2709 2710=cut 2711 2712sub CustomDateRangeFields { 2713 my $self = shift; 2714 my $type = ref $self || $self; 2715 2716 my @fields = 'now'; 2717 2718 for my $column ( keys %{ $_TABLE_ATTR->{ ref $self || $self } } ) { 2719 my $entry = $_TABLE_ATTR->{ ref $self || $self }{$column}; 2720 next unless $entry->{read} && ( $entry->{type} // '' ) eq 'datetime'; 2721 push @fields, $column; 2722 } 2723 2724 my $cfs = RT::CustomFields->new( ref $self ? $self->CurrentUser : RT->SystemUser ); 2725 $cfs->Limit( FIELD => 'Type', VALUE => [ 'Date', 'DateTime' ], OPERATOR => 'IN' ); 2726 while ( my $cf = $cfs->Next ) { 2727 push @fields, 'CF.{' . $cf->Name . '}'; 2728 } 2729 return sort { lc $a cmp lc $b } @fields; 2730} 2731 2732sub UID { 2733 my $self = shift; 2734 return undef unless defined $self->Id; 2735 return "@{[ref $self]}-$RT::Organization-@{[$self->Id]}"; 2736} 2737 2738sub FindDependencies { 2739 my $self = shift; 2740 my ($walker, $deps) = @_; 2741 for my $col (qw/Creator LastUpdatedBy/) { 2742 if ( $self->_Accessible( $col, 'read' ) ) { 2743 next unless $self->$col; 2744 my $obj = RT::Principal->new( $self->CurrentUser ); 2745 $obj->Load( $self->$col ); 2746 $deps->Add( out => $obj->Object ); 2747 } 2748 } 2749 2750 my $objs; 2751 2752 # Object attributes, we have to check on every object 2753 # attributes of attributes are not supported yet though. 2754 if ( !$self->isa('RT::Attribute') ) { 2755 $objs = $self->Attributes; 2756 $deps->Add( in => $objs ); 2757 } 2758 2759 # Transactions 2760 if ( $self->isa("RT::Ticket") 2761 or $self->isa("RT::User") 2762 or $self->isa("RT::Group") 2763 or $self->isa("RT::Article") 2764 or $self->isa("RT::Asset") 2765 or $self->isa("RT::Catalog") 2766 or $self->isa("RT::Attribute") 2767 or $self->isa("RT::Queue") ) 2768 { 2769 $objs = RT::Transactions->new( $self->CurrentUser ); 2770 $objs->Limit( FIELD => 'ObjectType', VALUE => ref $self ); 2771 $objs->Limit( FIELD => 'ObjectId', VALUE => $self->id ); 2772 $deps->Add( in => $objs ); 2773 } 2774 2775 # Object custom field values 2776 if (( $self->isa("RT::Transaction") 2777 or $self->isa("RT::Ticket") 2778 or $self->isa("RT::User") 2779 or $self->isa("RT::Group") 2780 or $self->isa("RT::Asset") 2781 or $self->isa("RT::Queue") 2782 or $self->isa("RT::Article") ) 2783 and $self->can("CustomFieldValues") ) 2784 { 2785 $objs = $self->CustomFieldValues; # Actually OCFVs 2786 $objs->{find_disabled_rows} = 1; 2787 $deps->Add( in => $objs ); 2788 } 2789 2790 # ACE records 2791 if ( $self->isa("RT::Group") 2792 or $self->isa("RT::Class") 2793 or $self->isa("RT::Queue") 2794 or $self->isa("RT::CustomField") ) 2795 { 2796 $objs = RT::ACL->new( $self->CurrentUser ); 2797 $objs->LimitToObject( $self ); 2798 $deps->Add( in => $objs ); 2799 } 2800} 2801 2802sub Serialize { 2803 my $self = shift; 2804 my %args = ( 2805 Methods => {}, 2806 UIDs => 1, 2807 @_, 2808 ); 2809 my %methods = ( 2810 Creator => "CreatorObj", 2811 LastUpdatedBy => "LastUpdatedByObj", 2812 %{ $args{Methods} || {} }, 2813 ); 2814 2815 my %values = %{$self->{values}}; 2816 2817 my %ca = %{ $self->_ClassAccessible }; 2818 my @cols = grep {exists $values{lc $_} and defined $values{lc $_}} keys %ca; 2819 2820 my %store; 2821 $store{$_} = $values{lc $_} for @cols; 2822 $store{id} = $values{id}; # Explicitly necessary in some cases 2823 2824 # Un-apply the _transfer_ encoding, but don't mess with the octets 2825 # themselves. Calling ->Content directly would, in some cases, 2826 # decode from some mostly-unknown character set -- which reversing 2827 # on the far end would be complicated. 2828 if ($ca{ContentEncoding} and $ca{ContentType}) { 2829 my ($content_col) = grep {exists $ca{$_}} qw/LargeContent Content/; 2830 $store{$content_col} = $self->_DecodeLOB( 2831 "application/octet-stream", # Lie so that we get bytes, not characters 2832 $self->ContentEncoding, 2833 $self->_Value( $content_col, decode_utf8 => 0 ) 2834 ); 2835 delete $store{ContentEncoding}; 2836 } 2837 return %store unless $args{UIDs}; 2838 2839 # Use FooObj to turn Foo into a reference to the UID 2840 for my $col ( grep {$store{$_}} @cols ) { 2841 my $method = $methods{$col}; 2842 if (not $method) { 2843 $method = $col; 2844 $method =~ s/(Id)?$/Obj/; 2845 } 2846 next unless $self->can($method); 2847 2848 my $obj = $self->$method; 2849 next unless $obj and $obj->isa("RT::Record"); 2850 $store{$col} = \($obj->UID); 2851 } 2852 2853 # Anything on an object should get the UID stored instead 2854 if ($store{ObjectType} and $store{ObjectId} and $self->can("Object")) { 2855 delete $store{$_} for qw/ObjectType ObjectId/; 2856 $store{Object} = \($self->Object->UID); 2857 } 2858 2859 return %store; 2860} 2861 2862sub PreInflate { 2863 my $class = shift; 2864 my ($importer, $uid, $data) = @_; 2865 2866 my $ca = $class->_ClassAccessible; 2867 my %ca = %{ $ca }; 2868 2869 if ($ca{ContentEncoding} and $ca{ContentType}) { 2870 my ($content_col) = grep {exists $ca{$_}} qw/LargeContent Content/; 2871 if (defined $data->{$content_col}) { 2872 my ($ContentEncoding, $Content) = $class->_EncodeLOB( 2873 $data->{$content_col}, $data->{ContentType}, 2874 ); 2875 $data->{ContentEncoding} = $ContentEncoding; 2876 $data->{$content_col} = $Content; 2877 } 2878 } 2879 2880 if ($data->{Object} and not $ca{Object}) { 2881 my $ref_uid = ${ delete $data->{Object} }; 2882 my $ref = $importer->Lookup( $ref_uid ); 2883 if ($ref) { 2884 my ($class, $id) = @{$ref}; 2885 $data->{ObjectId} = $id; 2886 $data->{ObjectType} = $class; 2887 } else { 2888 $data->{ObjectId} = 0; 2889 $data->{ObjectType} = ""; 2890 $importer->Postpone( 2891 for => $ref_uid, 2892 uid => $uid, 2893 column => "ObjectId", 2894 classcolumn => "ObjectType", 2895 ); 2896 } 2897 } 2898 2899 for my $col (keys %{$data}) { 2900 if (ref $data->{$col}) { 2901 my $ref_uid = ${ $data->{$col} }; 2902 my $ref = $importer->Lookup( $ref_uid ); 2903 if ($ref) { 2904 my (undef, $id) = @{$ref}; 2905 $data->{$col} = $id; 2906 } else { 2907 $data->{$col} = 0; 2908 $importer->Postpone( 2909 for => $ref_uid, 2910 uid => $uid, 2911 column => $col, 2912 ); 2913 } 2914 } 2915 } 2916 2917 return 1; 2918} 2919 2920sub PostInflate { 2921} 2922 2923=head2 _AsInsertQuery 2924 2925Returns INSERT query string that duplicates current record and 2926can be used to insert record back into DB after delete. 2927 2928=cut 2929 2930sub _AsInsertQuery 2931{ 2932 my $self = shift; 2933 2934 my $dbh = $RT::Handle->dbh; 2935 2936 my $res = "INSERT INTO ". $self->Table; 2937 my $values = $self->{'values'}; 2938 $res .= "(". join( ",", map { $dbh->quote_identifier( $_ ) } sort keys %$values ) .")"; 2939 $res .= " VALUES"; 2940 $res .= "(". join( ",", map { $dbh->quote( $values->{$_} ) } sort keys %$values ) .")"; 2941 $res .= ";"; 2942 2943 return $res; 2944} 2945 2946sub BeforeWipeout { return 1 } 2947 2948=head2 Dependencies 2949 2950Returns L<RT::Shredder::Dependencies> object. 2951 2952=cut 2953 2954sub Dependencies 2955{ 2956 my $self = shift; 2957 my %args = ( 2958 Shredder => undef, 2959 Flags => RT::Shredder::Constants::DEPENDS_ON, 2960 @_, 2961 ); 2962 2963 unless( $self->id ) { 2964 RT::Shredder::Exception->throw('Object is not loaded'); 2965 } 2966 2967 my $deps = RT::Shredder::Dependencies->new(); 2968 if( $args{'Flags'} & RT::Shredder::Constants::DEPENDS_ON ) { 2969 $self->__DependsOn( %args, Dependencies => $deps ); 2970 } 2971 return $deps; 2972} 2973 2974sub __DependsOn 2975{ 2976 my $self = shift; 2977 my %args = ( 2978 Shredder => undef, 2979 Dependencies => undef, 2980 @_, 2981 ); 2982 my $deps = $args{'Dependencies'}; 2983 my $list = []; 2984 2985# Object custom field values 2986 my $objs = $self->CustomFieldValues; 2987 $objs->{'find_disabled_rows'} = 1; 2988 push( @$list, $objs ); 2989 2990# Object attributes 2991 $objs = $self->Attributes; 2992 push( @$list, $objs ); 2993 2994# Transactions 2995 $objs = RT::Transactions->new( $self->CurrentUser ); 2996 $objs->Limit( FIELD => 'ObjectType', VALUE => ref $self ); 2997 $objs->Limit( FIELD => 'ObjectId', VALUE => $self->id ); 2998 push( @$list, $objs ); 2999 3000 if ( $self->isa( 'RT::CustomField' ) ) { 3001 $objs = RT::Transactions->new( $self->CurrentUser ); 3002 $objs->Limit( FIELD => 'Type', VALUE => 'CustomField' ); 3003 $objs->Limit( FIELD => 'Field', VALUE => $self->id ); 3004 push( @$list, $objs ); 3005 } 3006 3007# Links 3008 if ( $self->can('Links') ) { 3009 # make sure we don't skip any record 3010 no warnings 'redefine'; 3011 local *RT::Links::IsValidLink = sub { 1 }; 3012 3013 foreach ( qw(Base Target) ) { 3014 my $objs = $self->Links( $_ ); 3015 $objs->_DoSearch; 3016 push @$list, $objs->ItemsArrayRef; 3017 } 3018 } 3019 3020# ACE records 3021 $objs = RT::ACL->new( $self->CurrentUser ); 3022 $objs->LimitToObject( $self ); 3023 push( @$list, $objs ); 3024 3025 $deps->_PushDependencies( 3026 BaseObject => $self, 3027 Flags => RT::Shredder::Constants::DEPENDS_ON, 3028 TargetObjects => $list, 3029 Shredder => $args{'Shredder'} 3030 ); 3031 return; 3032} 3033 3034# implement proxy method because some RT classes 3035# override Delete method 3036sub __Wipeout 3037{ 3038 my $self = shift; 3039 my $msg = $self->UID ." wiped out"; 3040 $self->SUPER::Delete; 3041 $RT::Logger->info( $msg ); 3042 return; 3043} 3044 3045RT::Base->_ImportOverlays(); 3046 30471; 3048