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