import rt 3.6.4
[freeside.git] / rt / lib / RT / Record.pm
1 # BEGIN BPS TAGGED BLOCK {{{
2
3 # COPYRIGHT:
4 #  
5 # This software is Copyright (c) 1996-2007 Best Practical Solutions, LLC 
6 #                                          <jesse@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/copyleft/gpl.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 =head1 NAME
49
50   RT::Record - Base class for RT record objects
51
52 =head1 SYNOPSIS
53
54
55 =head1 DESCRIPTION
56
57
58 =begin testing
59
60 ok (require RT::Record);
61
62 =end testing
63
64 =head1 METHODS
65
66 =cut
67
68 package RT::Record;
69
70 use strict;
71 use warnings;
72
73 our @ISA;
74 use base qw(RT::Base);
75
76 use RT::Date;
77 use RT::User;
78 use RT::Attributes;
79 use DBIx::SearchBuilder::Record::Cachable;
80 use Encode qw();
81
82 our $_TABLE_ATTR = { };
83
84
85 if ( $RT::DontCacheSearchBuilderRecords ) {
86     push (@ISA, 'DBIx::SearchBuilder::Record');
87 } else {
88     push (@ISA, 'DBIx::SearchBuilder::Record::Cachable');
89
90 }
91
92 # {{{ sub _Init 
93
94 sub _Init {
95     my $self = shift;
96     $self->_BuildTableAttributes unless ($_TABLE_ATTR->{ref($self)});
97     $self->CurrentUser(@_);
98 }
99
100 # }}}
101
102 # {{{ _PrimaryKeys
103
104 =head2 _PrimaryKeys
105
106 The primary keys for RT classes is 'id'
107
108 =cut
109
110 sub _PrimaryKeys {
111     my $self = shift;
112     return ( ['id'] );
113 }
114
115 # }}}
116
117 =head2 Delete
118
119 Delete this record object from the database.
120
121 =cut
122
123 sub Delete {
124     my $self = shift;
125     my ($rv) = $self->SUPER::Delete;
126     if ($rv) {
127         return ($rv, $self->loc("Object deleted"));
128     } else {
129
130         return(0, $self->loc("Object could not be deleted"))
131     } 
132 }
133
134 =head2 ObjectTypeStr
135
136 Returns a string which is this object's type.  The type is the class,
137 without the "RT::" prefix.
138
139 =begin testing
140
141 my $ticket = RT::Ticket->new($RT::SystemUser);
142 my $group = RT::Group->new($RT::SystemUser);
143 is($ticket->ObjectTypeStr, 'Ticket', "Ticket returns correct typestring");
144 is($group->ObjectTypeStr, 'Group', "Group returns correct typestring");
145
146 =end testing
147
148 =cut
149
150 sub ObjectTypeStr {
151     my $self = shift;
152     if (ref($self) =~ /^.*::(\w+)$/) {
153         return $self->loc($1);
154     } else {
155         return $self->loc(ref($self));
156     }
157 }
158
159 =head2 Attributes
160
161 Return this object's attributes as an RT::Attributes object
162
163 =cut
164
165 sub Attributes {
166     my $self = shift;
167     
168     unless ($self->{'attributes'}) {
169         $self->{'attributes'} = RT::Attributes->new($self->CurrentUser);     
170        $self->{'attributes'}->LimitToObject($self); 
171     }
172     return ($self->{'attributes'}); 
173
174 }
175
176
177 =head2 AddAttribute { Name, Description, Content }
178
179 Adds a new attribute for this object.
180
181 =cut
182
183 sub AddAttribute {
184     my $self = shift;
185     my %args = ( Name        => undef,
186                  Description => undef,
187                  Content     => undef,
188                  @_ );
189
190     my $attr = RT::Attribute->new( $self->CurrentUser );
191     my ( $id, $msg ) = $attr->Create( 
192                                       Object    => $self,
193                                       Name        => $args{'Name'},
194                                       Description => $args{'Description'},
195                                       Content     => $args{'Content'} );
196
197
198     # XXX TODO: Why won't RedoSearch work here?                                     
199     $self->Attributes->_DoSearch;
200     
201     return ($id, $msg);
202 }
203
204
205 =head2 SetAttribute { Name, Description, Content }
206
207 Like AddAttribute, but replaces all existing attributes with the same Name.
208
209 =cut
210
211 sub SetAttribute {
212     my $self = shift;
213     my %args = ( Name        => undef,
214                  Description => undef,
215                  Content     => undef,
216                  @_ );
217
218     my @AttributeObjs = $self->Attributes->Named( $args{'Name'} )
219         or return $self->AddAttribute( %args );
220
221     my $AttributeObj = pop( @AttributeObjs );
222     $_->Delete foreach @AttributeObjs;
223
224     $AttributeObj->SetDescription( $args{'Description'} );
225     $AttributeObj->SetContent( $args{'Content'} );
226
227     $self->Attributes->RedoSearch;
228     return 1;
229 }
230
231 =head2 DeleteAttribute NAME
232
233 Deletes all attributes with the matching name for this object.
234
235 =cut
236
237 sub DeleteAttribute {
238     my $self = shift;
239     my $name = shift;
240     return $self->Attributes->DeleteEntry( Name => $name );
241 }
242
243 =head2 FirstAttribute NAME
244
245 Returns the first attribute with the matching name for this object (as an
246 L<RT::Attribute> object), or C<undef> if no such attributes exist.
247
248 Note that if there is more than one attribute with the matching name on the
249 object, the choice of which one to return is basically arbitrary.  This may be
250 made well-defined in the future.
251
252 =cut
253
254 sub FirstAttribute {
255     my $self = shift;
256     my $name = shift;
257     return ($self->Attributes->Named( $name ))[0];
258 }
259
260
261 # {{{ sub _Handle 
262 sub _Handle {
263     my $self = shift;
264     return ($RT::Handle);
265 }
266
267 # }}}
268
269 # {{{ sub Create 
270
271 =head2  Create PARAMHASH
272
273 Takes a PARAMHASH of Column -> Value pairs.
274 If any Column has a Validate$PARAMNAME subroutine defined and the 
275 value provided doesn't pass validation, this routine returns
276 an error.
277
278 If this object's table has any of the following atetributes defined as
279 'Auto', this routine will automatically fill in their values.
280
281 =cut
282
283 sub Create {
284     my $self    = shift;
285     my %attribs = (@_);
286     foreach my $key ( keys %attribs ) {
287         my $method = "Validate$key";
288         unless ( $self->$method( $attribs{$key} ) ) {
289             if (wantarray) {
290                 return ( 0, $self->loc('Invalid value for [_1]', $key) );
291             }
292             else {
293                 return (0);
294             }
295         }
296     }
297     my $now = RT::Date->new( $self->CurrentUser );
298     $now->Set( Format => 'unix', Value => time );
299     $attribs{'Created'} = $now->ISO() if ( $self->_Accessible( 'Created', 'auto' ) && !$attribs{'Created'});
300
301     if ($self->_Accessible( 'Creator', 'auto' ) && !$attribs{'Creator'}) {
302          $attribs{'Creator'} = $self->CurrentUser->id || '0'; 
303     }
304     $attribs{'LastUpdated'} = $now->ISO()
305       if ( $self->_Accessible( 'LastUpdated', 'auto' ) && !$attribs{'LastUpdated'});
306
307     $attribs{'LastUpdatedBy'} = $self->CurrentUser->id || '0'
308       if ( $self->_Accessible( 'LastUpdatedBy', 'auto' ) && !$attribs{'LastUpdatedBy'});
309
310     my $id = $self->SUPER::Create(%attribs);
311     if ( UNIVERSAL::isa( $id, 'Class::ReturnValue' ) ) {
312         if ( $id->errno ) {
313             if (wantarray) {
314                 return ( 0,
315                     $self->loc( "Internal Error: [_1]", $id->{error_message} ) );
316             }
317             else {
318                 return (0);
319             }
320         }
321     }
322     # If the object was created in the database, 
323     # load it up now, so we're sure we get what the database 
324     # has.  Arguably, this should not be necessary, but there
325     # isn't much we can do about it.
326
327    unless ($id) { 
328     if (wantarray) {
329         return ( $id, $self->loc('Object could not be created') );
330     }
331     else {
332         return ($id);
333     }
334
335    }
336
337     if  (UNIVERSAL::isa('errno',$id)) {
338         exit(0);
339        warn "It's here!";
340         return(undef);
341     }
342
343     $self->Load($id) if ($id);
344
345
346
347     if (wantarray) {
348         return ( $id, $self->loc('Object created') );
349     }
350     else {
351         return ($id);
352     }
353
354 }
355
356 # }}}
357
358 # {{{ sub LoadByCols
359
360 =head2 LoadByCols
361
362 Override DBIx::SearchBuilder::LoadByCols to do case-insensitive loads if the 
363 DB is case sensitive
364
365 =cut
366
367 sub LoadByCols {
368     my $self = shift;
369     my %hash = (@_);
370
371     # We don't want to hang onto this
372     delete $self->{'attributes'};
373
374     # If this database is case sensitive we need to uncase objects for
375     # explicit loading
376     if ( $self->_Handle->CaseSensitive ) {
377         my %newhash;
378         foreach my $key ( keys %hash ) {
379
380             # If we've been passed an empty value, we can't do the lookup. 
381             # We don't need to explicitly downcase integers or an id.
382             if ( $key =~ '^id$'
383                 || !defined( $hash{$key} )
384                 || $hash{$key} =~ /^\d+$/
385                  )
386             {
387                 $newhash{$key} = $hash{$key};
388             }
389             else {
390                 my ($op, $val, $func);
391                 ($key, $op, $val, $func) = $self->_Handle->_MakeClauseCaseInsensitive($key, '=', $hash{$key});
392                 $newhash{$key}->{operator} = $op;
393                 $newhash{$key}->{value} = $val;
394                 $newhash{$key}->{function} = $func;
395             }
396         }
397
398         # We've clobbered everything we care about. bash the old hash
399         # and replace it with the new hash
400         %hash = %newhash;
401     }
402     $self->SUPER::LoadByCols(%hash);
403 }
404
405 # }}}
406
407 # {{{ Datehandling
408
409 # There is room for optimizations in most of those subs:
410
411 # {{{ LastUpdatedObj
412
413 sub LastUpdatedObj {
414     my $self = shift;
415     my $obj  = new RT::Date( $self->CurrentUser );
416
417     $obj->Set( Format => 'sql', Value => $self->LastUpdated );
418     return $obj;
419 }
420
421 # }}}
422
423 # {{{ CreatedObj
424
425 sub CreatedObj {
426     my $self = shift;
427     my $obj  = new RT::Date( $self->CurrentUser );
428
429     $obj->Set( Format => 'sql', Value => $self->Created );
430
431     return $obj;
432 }
433
434 # }}}
435
436 # {{{ AgeAsString
437 #
438 # TODO: This should be deprecated
439 #
440 sub AgeAsString {
441     my $self = shift;
442     return ( $self->CreatedObj->AgeAsString() );
443 }
444
445 # }}}
446
447 # {{{ LastUpdatedAsString
448
449 # TODO this should be deprecated
450
451 sub LastUpdatedAsString {
452     my $self = shift;
453     if ( $self->LastUpdated ) {
454         return ( $self->LastUpdatedObj->AsString() );
455
456     }
457     else {
458         return "never";
459     }
460 }
461
462 # }}}
463
464 # {{{ CreatedAsString
465 #
466 # TODO This should be deprecated 
467 #
468 sub CreatedAsString {
469     my $self = shift;
470     return ( $self->CreatedObj->AsString() );
471 }
472
473 # }}}
474
475 # {{{ LongSinceUpdateAsString
476 #
477 # TODO This should be deprecated
478 #
479 sub LongSinceUpdateAsString {
480     my $self = shift;
481     if ( $self->LastUpdated ) {
482
483         return ( $self->LastUpdatedObj->AgeAsString() );
484
485     }
486     else {
487         return "never";
488     }
489 }
490
491 # }}}
492
493 # }}} Datehandling
494
495 # {{{ sub _Set 
496 #
497 sub _Set {
498     my $self = shift;
499
500     my %args = (
501         Field => undef,
502         Value => undef,
503         IsSQL => undef,
504         @_
505     );
506
507     #if the user is trying to modify the record
508     # TODO: document _why_ this code is here
509
510     if ( ( !defined( $args{'Field'} ) ) || ( !defined( $args{'Value'} ) ) ) {
511         $args{'Value'} = 0;
512     }
513
514     my $old_val = $self->__Value($args{'Field'});
515      $self->_SetLastUpdated();
516     my $ret = $self->SUPER::_Set(
517         Field => $args{'Field'},
518         Value => $args{'Value'},
519         IsSQL => $args{'IsSQL'}
520     );
521         my ($status, $msg) =  $ret->as_array();
522
523         # @values has two values, a status code and a message.
524
525     # $ret is a Class::ReturnValue object. as such, in a boolean context, it's a bool
526     # we want to change the standard "success" message
527     if ($status) {
528         $msg =
529           $self->loc(
530             "[_1] changed from [_2] to [_3]",
531             $args{'Field'},
532             ( $old_val ? "'$old_val'" : $self->loc("(no value)") ),
533             '"' . $self->__Value( $args{'Field'}) . '"' 
534           );
535       } else {
536
537           $msg = $self->CurrentUser->loc_fuzzy($msg);
538     }
539     return wantarray ? ($status, $msg) : $ret;     
540
541 }
542
543 # }}}
544
545 # {{{ sub _SetLastUpdated
546
547 =head2 _SetLastUpdated
548
549 This routine updates the LastUpdated and LastUpdatedBy columns of the row in question
550 It takes no options. Arguably, this is a bug
551
552 =cut
553
554 sub _SetLastUpdated {
555     my $self = shift;
556     use RT::Date;
557     my $now = new RT::Date( $self->CurrentUser );
558     $now->SetToNow();
559
560     if ( $self->_Accessible( 'LastUpdated', 'auto' ) ) {
561         my ( $msg, $val ) = $self->__Set(
562             Field => 'LastUpdated',
563             Value => $now->ISO
564         );
565     }
566     if ( $self->_Accessible( 'LastUpdatedBy', 'auto' ) ) {
567         my ( $msg, $val ) = $self->__Set(
568             Field => 'LastUpdatedBy',
569             Value => $self->CurrentUser->id
570         );
571     }
572 }
573
574 # }}}
575
576 # {{{ sub CreatorObj 
577
578 =head2 CreatorObj
579
580 Returns an RT::User object with the RT account of the creator of this row
581
582 =cut
583
584 sub CreatorObj {
585     my $self = shift;
586     unless ( exists $self->{'CreatorObj'} ) {
587
588         $self->{'CreatorObj'} = RT::User->new( $self->CurrentUser );
589         $self->{'CreatorObj'}->Load( $self->Creator );
590     }
591     return ( $self->{'CreatorObj'} );
592 }
593
594 # }}}
595
596 # {{{ sub LastUpdatedByObj
597
598 =head2 LastUpdatedByObj
599
600   Returns an RT::User object of the last user to touch this object
601
602 =cut
603
604 sub LastUpdatedByObj {
605     my $self = shift;
606     unless ( exists $self->{LastUpdatedByObj} ) {
607         $self->{'LastUpdatedByObj'} = RT::User->new( $self->CurrentUser );
608         $self->{'LastUpdatedByObj'}->Load( $self->LastUpdatedBy );
609     }
610     return $self->{'LastUpdatedByObj'};
611 }
612
613 # }}}
614
615 # {{{ sub URI 
616
617 =head2 URI
618
619 Returns this record's URI
620
621 =cut
622
623 sub URI {
624     my $self = shift;
625     my $uri = RT::URI::fsck_com_rt->new($self->CurrentUser);
626     return($uri->URIForObject($self));
627 }
628
629 # }}}
630
631 =head2 ValidateName NAME
632
633 Validate the name of the record we're creating. Mostly, just make sure it's not a numeric ID, which is invalid for Name
634
635 =cut
636
637 sub ValidateName {
638     my $self = shift;
639     my $value = shift;
640     if ($value && $value=~ /^\d+$/) {
641         return(0);
642     } else  {
643          return (1);
644     }
645 }
646
647
648
649 =head2 SQLType attribute
650
651 return the SQL type for the attribute 'attribute' as stored in _ClassAccessible
652
653 =cut
654
655 sub SQLType {
656     my $self = shift;
657     my $field = shift;
658
659     return ($self->_Accessible($field, 'type'));
660
661
662 }
663
664
665 sub __Value {
666     my $self  = shift;
667     my $field = shift;
668     my %args = ( decode_utf8 => 1,
669                  @_ );
670
671     unless (defined $field && $field) {
672         $RT::Logger->error("$self __Value called with undef field");
673     }
674     my $value = $self->SUPER::__Value($field);
675
676     return('') if ( !defined($value) || $value eq '');
677
678     if( $args{'decode_utf8'} ) {
679         # XXX: is_utf8 check should be here unless Encode bug would be fixed
680         # see http://rt.cpan.org/NoAuth/Bug.html?id=14559 
681         return Encode::decode_utf8($value) unless Encode::is_utf8($value);
682     } else {
683         # check is_utf8 here just to be shure
684         return Encode::encode_utf8($value) if Encode::is_utf8($value);
685     }
686     return $value;
687 }
688
689 # Set up defaults for DBIx::SearchBuilder::Record::Cachable
690
691 sub _CacheConfig {
692   {
693      'cache_p'        => 1,
694      'cache_for_sec'  => 30,
695   }
696 }
697
698
699
700 sub _BuildTableAttributes {
701     my $self = shift;
702
703     my $attributes;
704     if ( UNIVERSAL::can( $self, '_CoreAccessible' ) ) {
705        $attributes = $self->_CoreAccessible();
706     } elsif ( UNIVERSAL::can( $self, '_ClassAccessible' ) ) {
707        $attributes = $self->_ClassAccessible();
708
709     }
710
711     foreach my $column (%$attributes) {
712         foreach my $attr ( %{ $attributes->{$column} } ) {
713             $_TABLE_ATTR->{ref($self)}->{$column}->{$attr} = $attributes->{$column}->{$attr};
714         }
715     }
716     if ( UNIVERSAL::can( $self, '_OverlayAccessible' ) ) {
717         $attributes = $self->_OverlayAccessible();
718
719         foreach my $column (%$attributes) {
720             foreach my $attr ( %{ $attributes->{$column} } ) {
721                 $_TABLE_ATTR->{ref($self)}->{$column}->{$attr} = $attributes->{$column}->{$attr};
722             }
723         }
724     }
725     if ( UNIVERSAL::can( $self, '_VendorAccessible' ) ) {
726         $attributes = $self->_VendorAccessible();
727
728         foreach my $column (%$attributes) {
729             foreach my $attr ( %{ $attributes->{$column} } ) {
730                 $_TABLE_ATTR->{ref($self)}->{$column}->{$attr} = $attributes->{$column}->{$attr};
731             }
732         }
733     }
734     if ( UNIVERSAL::can( $self, '_LocalAccessible' ) ) {
735         $attributes = $self->_LocalAccessible();
736
737         foreach my $column (%$attributes) {
738             foreach my $attr ( %{ $attributes->{$column} } ) {
739                 $_TABLE_ATTR->{ref($self)}->{$column}->{$attr} = $attributes->{$column}->{$attr};
740             }
741         }
742     }
743
744 }
745
746
747 =head2 _ClassAccessible 
748
749 Overrides the "core" _ClassAccessible using $_TABLE_ATTR. Behaves identical to the version in
750 DBIx::SearchBuilder::Record
751
752 =cut
753
754 sub _ClassAccessible {
755     my $self = shift;
756     return $_TABLE_ATTR->{ref($self)};
757 }
758
759 =head2 _Accessible COLUMN ATTRIBUTE
760
761 returns the value of ATTRIBUTE for COLUMN
762
763
764 =cut 
765
766 sub _Accessible  {
767   my $self = shift;
768   my $column = shift;
769   my $attribute = lc(shift);
770   return 0 unless defined ($_TABLE_ATTR->{ref($self)}->{$column});
771   return $_TABLE_ATTR->{ref($self)}->{$column}->{$attribute} || 0;
772
773 }
774
775 =head2 _EncodeLOB BODY MIME_TYPE
776
777 Takes a potentially large attachment. Returns (ContentEncoding, EncodedBody) based on system configuration and selected database
778
779 =cut
780
781 sub _EncodeLOB {
782         my $self = shift;
783         my $Body = shift;
784         my $MIMEType = shift;
785
786         my $ContentEncoding = 'none';
787
788         #get the max attachment length from RT
789         my $MaxSize = $RT::MaxAttachmentSize;
790
791         #if the current attachment contains nulls and the
792         #database doesn't support embedded nulls
793
794         if ( $RT::AlwaysUseBase64 or
795              ( !$RT::Handle->BinarySafeBLOBs ) && ( $Body =~ /\x00/ ) ) {
796
797             # set a flag telling us to mimencode the attachment
798             $ContentEncoding = 'base64';
799
800             #cut the max attchment size by 25% (for mime-encoding overhead.
801             $RT::Logger->debug("Max size is $MaxSize\n");
802             $MaxSize = $MaxSize * 3 / 4;
803         # Some databases (postgres) can't handle non-utf8 data
804         } elsif (    !$RT::Handle->BinarySafeBLOBs
805                   && $MIMEType !~ /text\/plain/gi
806                   && !Encode::is_utf8( $Body, 1 ) ) {
807               $ContentEncoding = 'quoted-printable';
808         }
809
810         #if the attachment is larger than the maximum size
811         if ( ($MaxSize) and ( $MaxSize < length($Body) ) ) {
812
813             # if we're supposed to truncate large attachments
814             if ($RT::TruncateLongAttachments) {
815
816                 # truncate the attachment to that length.
817                 $Body = substr( $Body, 0, $MaxSize );
818
819             }
820
821             # elsif we're supposed to drop large attachments on the floor,
822             elsif ($RT::DropLongAttachments) {
823
824                 # drop the attachment on the floor
825                 $RT::Logger->info( "$self: Dropped an attachment of size "
826                                    . length($Body) . "\n"
827                                    . "It started: " . substr( $Body, 0, 60 ) . "\n"
828                                  );
829                 return ("none", "Large attachment dropped" );
830             }
831         }
832
833         # if we need to mimencode the attachment
834         if ( $ContentEncoding eq 'base64' ) {
835
836             # base64 encode the attachment
837             Encode::_utf8_off($Body);
838             $Body = MIME::Base64::encode_base64($Body);
839
840         } elsif ($ContentEncoding eq 'quoted-printable') {
841             Encode::_utf8_off($Body);
842             $Body = MIME::QuotedPrint::encode($Body);
843         }
844
845
846         return ($ContentEncoding, $Body);
847
848 }
849
850 sub _DecodeLOB {
851     my $self            = shift;
852     my $ContentType     = shift;
853     my $ContentEncoding = shift;
854     my $Content         = shift;
855
856     if ( $ContentEncoding eq 'base64' ) {
857         $Content = MIME::Base64::decode_base64($Content);
858     }
859     elsif ( $ContentEncoding eq 'quoted-printable' ) {
860         $Content = MIME::QuotedPrint::decode($Content);
861     }
862     elsif ( $ContentEncoding && $ContentEncoding ne 'none' ) {
863         return ( $self->loc( "Unknown ContentEncoding [_1]", $ContentEncoding ) );
864     }
865     if ( $ContentType eq 'text/plain' ) {
866        $Content = Encode::decode_utf8($Content) unless Encode::is_utf8($Content);
867     }
868         return ($Content);
869 }
870
871 # {{{ LINKDIRMAP
872 # A helper table for links mapping to make it easier
873 # to build and parse links between tickets
874
875 use vars '%LINKDIRMAP';
876
877 %LINKDIRMAP = (
878     MemberOf => { Base => 'MemberOf',
879                   Target => 'HasMember', },
880     RefersTo => { Base => 'RefersTo',
881                 Target => 'ReferredToBy', },
882     DependsOn => { Base => 'DependsOn',
883                    Target => 'DependedOnBy', },
884     MergedInto => { Base => 'MergedInto',
885                    Target => 'MergedInto', },
886
887 );
888
889 sub Update {
890     my $self = shift;
891
892     my %args = (
893         ARGSRef         => undef,
894         AttributesRef   => undef,
895         AttributePrefix => undef,
896         @_
897     );
898
899     my $attributes = $args{'AttributesRef'};
900     my $ARGSRef    = $args{'ARGSRef'};
901     my @results;
902
903     foreach my $attribute (@$attributes) {
904         my $value;
905         if ( defined $ARGSRef->{$attribute} ) {
906             $value = $ARGSRef->{$attribute};
907         }
908         elsif (
909             defined( $args{'AttributePrefix'} )
910             && defined(
911                 $ARGSRef->{ $args{'AttributePrefix'} . "-" . $attribute }
912             )
913           ) {
914             $value = $ARGSRef->{ $args{'AttributePrefix'} . "-" . $attribute };
915
916         }
917         else {
918             next;
919         }
920
921         $value =~ s/\r\n/\n/gs;
922
923
924         # If Queue is 'General', we want to resolve the queue name for
925         # the object.
926
927         # This is in an eval block because $object might not exist.
928         # and might not have a Name method. But "can" won't find autoloaded
929         # items. If it fails, we don't care
930         eval {
931             my $object = $attribute . "Obj";
932             next if ($self->$object->Name eq $value);
933         };
934         next if ( $value eq $self->$attribute() );
935         my $method = "Set$attribute";
936         my ( $code, $msg ) = $self->$method($value);
937         my ($prefix) = ref($self) =~ /RT(?:.*)::(\w+)/;
938
939         # Default to $id, but use name if we can get it.
940         my $label = $self->id;
941         $label = $self->Name if (UNIVERSAL::can($self,'Name'));
942         push @results, $self->loc( "$prefix [_1]", $label ) . ': '. $msg;
943
944 =for loc
945
946                                    "[_1] could not be set to [_2].",       # loc
947                                    "That is already the current value",    # loc
948                                    "No value sent to _Set!\n",             # loc
949                                    "Illegal value for [_1]",               # loc
950                                    "The new value has been set.",          # loc
951                                    "No column specified",                  # loc
952                                    "Immutable field",                      # loc
953                                    "Nonexistant field?",                   # loc
954                                    "Invalid data",                         # loc
955                                    "Couldn't find row",                    # loc
956                                    "Missing a primary key?: [_1]",         # loc
957                                    "Found Object",                         # loc
958
959 =cut
960
961     }
962
963     return @results;
964 }
965
966 # {{{ Routines dealing with Links
967
968 # {{{ Link Collections
969
970 # {{{ sub Members
971
972 =head2 Members
973
974   This returns an RT::Links object which references all the tickets 
975 which are 'MembersOf' this ticket
976
977 =cut
978
979 sub Members {
980     my $self = shift;
981     return ( $self->_Links( 'Target', 'MemberOf' ) );
982 }
983
984 # }}}
985
986 # {{{ sub MemberOf
987
988 =head2 MemberOf
989
990   This returns an RT::Links object which references all the tickets that this
991 ticket is a 'MemberOf'
992
993 =cut
994
995 sub MemberOf {
996     my $self = shift;
997     return ( $self->_Links( 'Base', 'MemberOf' ) );
998 }
999
1000 # }}}
1001
1002 # {{{ RefersTo
1003
1004 =head2 RefersTo
1005
1006   This returns an RT::Links object which shows all references for which this ticket is a base
1007
1008 =cut
1009
1010 sub RefersTo {
1011     my $self = shift;
1012     return ( $self->_Links( 'Base', 'RefersTo' ) );
1013 }
1014
1015 # }}}
1016
1017 # {{{ ReferredToBy
1018
1019 =head2 ReferredToBy
1020
1021   This returns an RT::Links object which shows all references for which this ticket is a target
1022
1023 =cut
1024
1025 sub ReferredToBy {
1026     my $self = shift;
1027     return ( $self->_Links( 'Target', 'RefersTo' ) );
1028 }
1029
1030 # }}}
1031
1032 # {{{ DependedOnBy
1033
1034 =head2 DependedOnBy
1035
1036   This returns an RT::Links object which references all the tickets that depend on this one
1037
1038 =cut
1039
1040 sub DependedOnBy {
1041     my $self = shift;
1042     return ( $self->_Links( 'Target', 'DependsOn' ) );
1043 }
1044
1045 # }}}
1046
1047
1048
1049 =head2 HasUnresolvedDependencies
1050
1051   Takes a paramhash of Type (default to '__any').  Returns true if
1052 $self->UnresolvedDependencies returns an object with one or more members
1053 of that type.  Returns false otherwise
1054
1055
1056 =begin testing
1057
1058 my $t1 = RT::Ticket->new($RT::SystemUser);
1059 my ($id, $trans, $msg) = $t1->Create(Subject => 'DepTest1', Queue => 'general');
1060 ok($id, "Created dep test 1 - $msg");
1061
1062 my $t2 = RT::Ticket->new($RT::SystemUser);
1063 my ($id2, $trans, $msg2) = $t2->Create(Subject => 'DepTest2', Queue => 'general');
1064 ok($id2, "Created dep test 2 - $msg2");
1065 my $t3 = RT::Ticket->new($RT::SystemUser);
1066 my ($id3, $trans, $msg3) = $t3->Create(Subject => 'DepTest3', Queue => 'general', Type => 'approval');
1067 ok($id3, "Created dep test 3 - $msg3");
1068 my ($addid, $addmsg);
1069 ok (($addid, $addmsg) =$t1->AddLink( Type => 'DependsOn', Target => $t2->id));
1070 ok ($addid, $addmsg);
1071 ok (($addid, $addmsg) =$t1->AddLink( Type => 'DependsOn', Target => $t3->id));
1072
1073 ok ($addid, $addmsg);
1074 my $link = RT::Link->new($RT::SystemUser);
1075 my ($rv, $msg) = $link->Load($addid);
1076 ok ($rv, $msg);
1077 ok ($link->LocalTarget == $t3->id, "Link LocalTarget is correct");
1078 ok ($link->LocalBase   == $t1->id, "Link LocalBase   is correct");
1079
1080 ok ($t1->HasUnresolvedDependencies, "Ticket ".$t1->Id." has unresolved deps");
1081 ok (!$t1->HasUnresolvedDependencies( Type => 'blah' ), "Ticket ".$t1->Id." has no unresolved blahs");
1082 ok ($t1->HasUnresolvedDependencies( Type => 'approval' ), "Ticket ".$t1->Id." has unresolved approvals");
1083 ok (!$t2->HasUnresolvedDependencies, "Ticket ".$t2->Id." has no unresolved deps");
1084 ;
1085
1086 my ($rid, $rmsg)= $t1->Resolve();
1087 ok(!$rid, $rmsg);
1088 my ($rid2, $rmsg2) = $t2->Resolve();
1089 ok ($rid2, $rmsg2);
1090 ($rid, $rmsg)= $t1->Resolve();
1091 ok(!$rid, $rmsg);
1092 my ($rid3,$rmsg3) = $t3->Resolve;
1093 ok ($rid3,$rmsg3);
1094 ($rid, $rmsg)= $t1->Resolve();
1095 ok($rid, $rmsg);
1096
1097
1098 =end testing
1099
1100 =cut
1101
1102 sub HasUnresolvedDependencies {
1103     my $self = shift;
1104     my %args = (
1105         Type   => undef,
1106         @_
1107     );
1108
1109     my $deps = $self->UnresolvedDependencies;
1110
1111     if ($args{Type}) {
1112         $deps->Limit( FIELD => 'Type', 
1113               OPERATOR => '=',
1114               VALUE => $args{Type}); 
1115     }
1116     else {
1117             $deps->IgnoreType;
1118     }
1119
1120     if ($deps->Count > 0) {
1121         return 1;
1122     }
1123     else {
1124         return (undef);
1125     }
1126 }
1127
1128
1129 # {{{ UnresolvedDependencies 
1130
1131 =head2 UnresolvedDependencies
1132
1133 Returns an RT::Tickets object of tickets which this ticket depends on
1134 and which have a status of new, open or stalled. (That list comes from
1135 RT::Queue->ActiveStatusArray
1136
1137 =cut
1138
1139
1140 sub UnresolvedDependencies {
1141     my $self = shift;
1142     my $deps = RT::Tickets->new($self->CurrentUser);
1143
1144     my @live_statuses = RT::Queue->ActiveStatusArray();
1145     foreach my $status (@live_statuses) {
1146         $deps->LimitStatus(VALUE => $status);
1147     }
1148     $deps->LimitDependedOnBy($self->Id);
1149
1150     return($deps);
1151
1152 }
1153
1154 # }}}
1155
1156 # {{{ AllDependedOnBy
1157
1158 =head2 AllDependedOnBy
1159
1160 Returns an array of RT::Ticket objects which (directly or indirectly)
1161 depends on this ticket; takes an optional 'Type' argument in the param
1162 hash, which will limit returned tickets to that type, as well as cause
1163 tickets with that type to serve as 'leaf' nodes that stops the recursive
1164 dependency search.
1165
1166 =cut
1167
1168 sub AllDependedOnBy {
1169     my $self = shift;
1170     my $dep = $self->DependedOnBy;
1171     my %args = (
1172         Type   => undef,
1173         _found => {},
1174         _top   => 1,
1175         @_
1176     );
1177
1178     while (my $link = $dep->Next()) {
1179         next unless ($link->BaseURI->IsLocal());
1180         next if $args{_found}{$link->BaseObj->Id};
1181
1182         if (!$args{Type}) {
1183             $args{_found}{$link->BaseObj->Id} = $link->BaseObj;
1184             $link->BaseObj->AllDependedOnBy( %args, _top => 0 );
1185         }
1186         elsif ($link->BaseObj->Type eq $args{Type}) {
1187             $args{_found}{$link->BaseObj->Id} = $link->BaseObj;
1188         }
1189         else {
1190             $link->BaseObj->AllDependedOnBy( %args, _top => 0 );
1191         }
1192     }
1193
1194     if ($args{_top}) {
1195         return map { $args{_found}{$_} } sort keys %{$args{_found}};
1196     }
1197     else {
1198         return 1;
1199     }
1200 }
1201
1202 # }}}
1203
1204 # {{{ DependsOn
1205
1206 =head2 DependsOn
1207
1208   This returns an RT::Links object which references all the tickets that this ticket depends on
1209
1210 =cut
1211
1212 sub DependsOn {
1213     my $self = shift;
1214     return ( $self->_Links( 'Base', 'DependsOn' ) );
1215 }
1216
1217 # }}}
1218
1219
1220
1221
1222 # {{{ sub _Links 
1223
1224 =head2 Links DIRECTION TYPE 
1225
1226 return links to/from this object. 
1227
1228 =cut
1229
1230 *Links = \&_Links;
1231
1232 sub _Links {
1233     my $self = shift;
1234
1235     #TODO: Field isn't the right thing here. but I ahave no idea what mnemonic ---
1236     #tobias meant by $f
1237     my $field = shift;
1238     my $type  = shift || "";
1239
1240     unless ( $self->{"$field$type"} ) {
1241         $self->{"$field$type"} = new RT::Links( $self->CurrentUser );
1242             # at least to myself
1243             $self->{"$field$type"}->Limit( FIELD => $field,
1244                                            VALUE => $self->URI,
1245                                            ENTRYAGGREGATOR => 'OR' );
1246             $self->{"$field$type"}->Limit( FIELD => 'Type',
1247                                            VALUE => $type )
1248               if ($type);
1249     }
1250     return ( $self->{"$field$type"} );
1251 }
1252
1253 # }}}
1254
1255 # }}}
1256
1257 # {{{ sub _AddLink
1258
1259 =head2 _AddLink
1260
1261 Takes a paramhash of Type and one of Base or Target. Adds that link to this object.
1262
1263 Returns C<link id>, C<message> and C<exist> flag.
1264
1265
1266 =cut
1267
1268
1269 sub _AddLink {
1270     my $self = shift;
1271     my %args = ( Target => '',
1272                  Base   => '',
1273                  Type   => '',
1274                  Silent => undef,
1275                  @_ );
1276
1277
1278     # Remote_link is the URI of the object that is not this ticket
1279     my $remote_link;
1280     my $direction;
1281
1282     if ( $args{'Base'} and $args{'Target'} ) {
1283         $RT::Logger->debug( "$self tried to create a link. both base and target were specified\n" );
1284         return ( 0, $self->loc("Can't specifiy both base and target") );
1285     }
1286     elsif ( $args{'Base'} ) {
1287         $args{'Target'} = $self->URI();
1288         $remote_link    = $args{'Base'};
1289         $direction      = 'Target';
1290     }
1291     elsif ( $args{'Target'} ) {
1292         $args{'Base'} = $self->URI();
1293         $remote_link  = $args{'Target'};
1294         $direction    = 'Base';
1295     }
1296     else {
1297         return ( 0, $self->loc('Either base or target must be specified') );
1298     }
1299
1300     # {{{ Check if the link already exists - we don't want duplicates
1301     use RT::Link;
1302     my $old_link = RT::Link->new( $self->CurrentUser );
1303     $old_link->LoadByParams( Base   => $args{'Base'},
1304                              Type   => $args{'Type'},
1305                              Target => $args{'Target'} );
1306     if ( $old_link->Id ) {
1307         $RT::Logger->debug("$self Somebody tried to duplicate a link");
1308         return ( $old_link->id, $self->loc("Link already exists"), 1 );
1309     }
1310
1311     # }}}
1312
1313
1314     # Storing the link in the DB.
1315     my $link = RT::Link->new( $self->CurrentUser );
1316     my ($linkid, $linkmsg) = $link->Create( Target => $args{Target},
1317                                   Base   => $args{Base},
1318                                   Type   => $args{Type} );
1319
1320     unless ($linkid) {
1321         $RT::Logger->error("Link could not be created: ".$linkmsg);
1322         return ( 0, $self->loc("Link could not be created") );
1323     }
1324
1325     my $TransString =
1326       "Record $args{'Base'} $args{Type} record $args{'Target'}.";
1327
1328     return ( $linkid, $self->loc( "Link created ([_1])", $TransString ) );
1329 }
1330
1331 # }}}
1332
1333 # {{{ sub _DeleteLink 
1334
1335 =head2 _DeleteLink
1336
1337 Delete a link. takes a paramhash of Base, Target and Type.
1338 Either Base or Target must be null. The null value will 
1339 be replaced with this ticket\'s id
1340
1341 =cut 
1342
1343 sub _DeleteLink {
1344     my $self = shift;
1345     my %args = (
1346         Base   => undef,
1347         Target => undef,
1348         Type   => undef,
1349         @_
1350     );
1351
1352     #we want one of base and target. we don't care which
1353     #but we only want _one_
1354
1355     my $direction;
1356     my $remote_link;
1357
1358     if ( $args{'Base'} and $args{'Target'} ) {
1359         $RT::Logger->debug("$self ->_DeleteLink. got both Base and Target\n");
1360         return ( 0, $self->loc("Can't specifiy 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         $RT::Logger->error("Base or Target must be specified\n");
1374         return ( 0, $self->loc('Either base or target must be specified') );
1375     }
1376
1377     my $link = new RT::Link( $self->CurrentUser );
1378     $RT::Logger->debug( "Trying to load link: " . $args{'Base'} . " " . $args{'Type'} . " " . $args{'Target'} . "\n" );
1379
1380
1381     $link->LoadByParams( Base=> $args{'Base'}, Type=> $args{'Type'}, Target=>  $args{'Target'} );
1382     #it's a real link. 
1383     if ( $link->id ) {
1384
1385         my $linkid = $link->id;
1386         $link->Delete();
1387
1388         my $TransString = "Record $args{'Base'} no longer $args{Type} record $args{'Target'}.";
1389         return ( 1, $self->loc("Link deleted ([_1])", $TransString));
1390     }
1391
1392     #if it's not a link we can find
1393     else {
1394         $RT::Logger->debug("Couldn't find that link\n");
1395         return ( 0, $self->loc("Link not found") );
1396     }
1397 }
1398
1399 # }}}
1400
1401 # }}}
1402
1403 # {{{ Routines dealing with transactions
1404
1405 # {{{ sub _NewTransaction
1406
1407 =head2 _NewTransaction  PARAMHASH
1408
1409 Private function to create a new RT::Transaction object for this ticket update
1410
1411 =cut
1412
1413 sub _NewTransaction {
1414     my $self = shift;
1415     my %args = (
1416         TimeTaken => undef,
1417         Type      => undef,
1418         OldValue  => undef,
1419         NewValue  => undef,
1420         OldReference  => undef,
1421         NewReference  => undef,
1422         ReferenceType => undef,
1423         Data      => undef,
1424         Field     => undef,
1425         MIMEObj   => undef,
1426         ActivateScrips => 1,
1427         CommitScrips => 1,
1428         @_
1429     );
1430
1431     my $old_ref = $args{'OldReference'};
1432     my $new_ref = $args{'NewReference'};
1433     my $ref_type = $args{'ReferenceType'};
1434     if ($old_ref or $new_ref) {
1435         $ref_type ||= ref($old_ref) || ref($new_ref);
1436         if (!$ref_type) {
1437             $RT::Logger->error("Reference type not specified for transaction");
1438             return;
1439         }
1440         $old_ref = $old_ref->Id if ref($old_ref);
1441         $new_ref = $new_ref->Id if ref($new_ref);
1442     }
1443
1444     require RT::Transaction;
1445     my $trans = new RT::Transaction( $self->CurrentUser );
1446     my ( $transaction, $msg ) = $trans->Create(
1447         ObjectId  => $self->Id,
1448         ObjectType => ref($self),
1449         TimeTaken => $args{'TimeTaken'},
1450         Type      => $args{'Type'},
1451         Data      => $args{'Data'},
1452         Field     => $args{'Field'},
1453         NewValue  => $args{'NewValue'},
1454         OldValue  => $args{'OldValue'},
1455         NewReference  => $new_ref,
1456         OldReference  => $old_ref,
1457         ReferenceType => $ref_type,
1458         MIMEObj   => $args{'MIMEObj'},
1459         ActivateScrips => $args{'ActivateScrips'},
1460         CommitScrips => $args{'CommitScrips'},
1461     );
1462
1463     # Rationalize the object since we may have done things to it during the caching.
1464     $self->Load($self->Id);
1465
1466     $RT::Logger->warning($msg) unless $transaction;
1467
1468     $self->_SetLastUpdated;
1469
1470     if ( defined $args{'TimeTaken'} and $self->can('_UpdateTimeTaken')) {
1471         $self->_UpdateTimeTaken( $args{'TimeTaken'} );
1472     }
1473     if ( $RT::UseTransactionBatch and $transaction ) {
1474             push @{$self->{_TransactionBatch}}, $trans if $args{'CommitScrips'};
1475     }
1476     return ( $transaction, $msg, $trans );
1477 }
1478
1479 # }}}
1480
1481 # {{{ sub Transactions 
1482
1483 =head2 Transactions
1484
1485   Returns an RT::Transactions object of all transactions on this record object
1486
1487 =cut
1488
1489 sub Transactions {
1490     my $self = shift;
1491
1492     use RT::Transactions;
1493     my $transactions = RT::Transactions->new( $self->CurrentUser );
1494
1495     #If the user has no rights, return an empty object
1496     $transactions->Limit(
1497         FIELD => 'ObjectId',
1498         VALUE => $self->id,
1499     );
1500     $transactions->Limit(
1501         FIELD => 'ObjectType',
1502         VALUE => ref($self),
1503     );
1504
1505     return ($transactions);
1506 }
1507
1508 # }}}
1509 # }}}
1510 #
1511 # {{{ Routines dealing with custom fields
1512
1513 sub CustomFields {
1514     my $self = shift;
1515     my $cfs  = RT::CustomFields->new( $self->CurrentUser );
1516
1517     # XXX handle multiple types properly
1518     $cfs->LimitToLookupType( $self->CustomFieldLookupType );
1519     $cfs->LimitToGlobalOrObjectId(
1520         $self->_LookupId( $self->CustomFieldLookupType ) );
1521
1522     return $cfs;
1523 }
1524
1525 # TODO: This _only_ works for RT::Class classes. it doesn't work, for example, for RT::FM classes.
1526
1527 sub _LookupId {
1528     my $self = shift;
1529     my $lookup = shift;
1530     my @classes = ($lookup =~ /RT::(\w+)-/g);
1531
1532     my $object = $self;
1533     foreach my $class (reverse @classes) {
1534         my $method = "${class}Obj";
1535         $object = $object->$method;
1536     }
1537
1538     return $object->Id;
1539 }
1540
1541
1542 =head2 CustomFieldLookupType 
1543
1544 Returns the path RT uses to figure out which custom fields apply to this object.
1545
1546 =cut
1547
1548 sub CustomFieldLookupType {
1549     my $self = shift;
1550     return ref($self);
1551 }
1552
1553 #TODO Deprecated API. Destroy in 3.6
1554 sub _LookupTypes { 
1555     my  $self = shift;
1556     $RT::Logger->warning("_LookupTypes call is deprecated at (". join(":",caller)."). Replace with CustomFieldLookupType");
1557
1558     return($self->CustomFieldLookupType);
1559
1560 }
1561
1562 # {{{ AddCustomFieldValue
1563
1564 =head2 AddCustomFieldValue { Field => FIELD, Value => VALUE }
1565
1566 VALUE should be a string.
1567 FIELD can be a CustomField object OR a CustomField ID.
1568
1569
1570 Adds VALUE as a value of CustomField FIELD.  If this is a single-value custom field,
1571 deletes the old value. 
1572 If VALUE is not a valid value for the custom field, returns 
1573 (0, 'Error message' ) otherwise, returns (1, 'Success Message')
1574
1575 =cut
1576
1577 sub AddCustomFieldValue {
1578     my $self = shift;
1579     $self->_AddCustomFieldValue(@_);
1580 }
1581
1582 sub _AddCustomFieldValue {
1583     my $self = shift;
1584     my %args = (
1585         Field             => undef,
1586         Value             => undef,
1587         RecordTransaction => 1,
1588         @_
1589     );
1590
1591     my $cf = $self->LoadCustomFieldByIdentifier($args{'Field'});
1592
1593     unless ( $cf->Id ) {
1594         return ( 0, $self->loc( "Custom field [_1] not found", $args{'Field'} ) );
1595     }
1596
1597     my $OCFs = $self->CustomFields;
1598     $OCFs->Limit( FIELD => 'id', VALUE => $cf->Id );
1599     unless ( $OCFs->Count ) {
1600         return (
1601             0,
1602             $self->loc(
1603                 "Custom field [_1] does not apply to this object",
1604                 $args{'Field'}
1605             )
1606         );
1607     }
1608     # Load up a ObjectCustomFieldValues object for this custom field and this ticket
1609     my $values = $cf->ValuesForObject($self);
1610
1611     unless ( $cf->ValidateValue( $args{'Value'} ) ) {
1612         return ( 0, $self->loc("Invalid value for custom field") );
1613     }
1614
1615     # If the custom field only accepts a certain # of values, delete the existing
1616     # value and record a "changed from foo to bar" transaction
1617     unless ( $cf->UnlimitedValues) {
1618
1619  # We need to whack any old values here.  In most cases, the custom field should
1620  # only have one value to delete.  In the pathalogical case, this custom field
1621  # used to be a multiple and we have many values to whack....
1622         my $cf_values = $values->Count;
1623
1624         if ( $cf_values > $cf->MaxValues ) {
1625             my $i = 0;   #We want to delete all but the max we can currently have , so we can then
1626                  # execute the same code to "change" the value from old to new
1627             while ( my $value = $values->Next ) {
1628                 $i++;
1629                 if ( $i < $cf_values ) {
1630                     my ( $val, $msg ) = $cf->DeleteValueForObject(
1631                         Object  => $self,
1632                         Content => $value->Content
1633                     );
1634                     unless ($val) {
1635                         return ( 0, $msg );
1636                     }
1637                     my ( $TransactionId, $Msg, $TransactionObj ) =
1638                       $self->_NewTransaction(
1639                         Type         => 'CustomField',
1640                         Field        => $cf->Id,
1641                         OldReference => $value,
1642                       );
1643                 }
1644             }
1645             $values->RedoSearch if $i; # redo search if have deleted at least one value
1646         }
1647
1648         my ( $old_value, $old_content );
1649         if ( $old_value = $values->First ) {
1650             $old_content = $old_value->Content();
1651             return (1) if( $old_content eq $args{'Value'} && $old_value->LargeContent eq $args{'LargeContent'});;
1652         }
1653
1654         my ( $new_value_id, $value_msg ) = $cf->AddValueForObject(
1655             Object       => $self,
1656             Content      => $args{'Value'},
1657             LargeContent => $args{'LargeContent'},
1658             ContentType  => $args{'ContentType'},
1659         );
1660
1661         unless ($new_value_id) {
1662             return ( 0, $self->loc( "Could not add new custom field value: [_1]", $value_msg) );
1663         }
1664
1665         my $new_value = RT::ObjectCustomFieldValue->new( $self->CurrentUser );
1666         $new_value->Load($new_value_id);
1667
1668         # now that adding the new value was successful, delete the old one
1669         if ($old_value) {
1670             my ( $val, $msg ) = $old_value->Delete();
1671             unless ($val) {
1672                 return ( 0, $msg );
1673             }
1674         }
1675
1676         if ( $args{'RecordTransaction'} ) {
1677             my ( $TransactionId, $Msg, $TransactionObj ) =
1678               $self->_NewTransaction(
1679                 Type         => 'CustomField',
1680                 Field        => $cf->Id,
1681                 OldReference => $old_value,
1682                 NewReference => $new_value,
1683               );
1684         }
1685
1686         if ( $old_value eq '' ) {
1687             return ( 1, $self->loc( "[_1] [_2] added", $cf->Name, $new_value->Content ));
1688         }
1689         elsif ( $new_value->Content eq '' ) {
1690             return ( 1,
1691                 $self->loc( "[_1] [_2] deleted", $cf->Name, $old_value->Content ) );
1692         }
1693         else {
1694             return ( 1, $self->loc( "[_1] [_2] changed to [_3]", $cf->Name, $old_content,                $new_value->Content));
1695         }
1696
1697     }
1698
1699     # otherwise, just add a new value and record "new value added"
1700     else {
1701         my ($new_value_id, $value_msg) = $cf->AddValueForObject(
1702             Object       => $self,
1703             Content      => $args{'Value'},
1704             LargeContent => $args{'LargeContent'},
1705             ContentType  => $args{'ContentType'},
1706         );
1707
1708         unless ($new_value_id) {
1709             return ( 0, $self->loc( "Could not add new custom field value: [_1]", $value_msg) );
1710         }
1711         if ( $args{'RecordTransaction'} ) {
1712             my ( $TransactionId, $Msg, $TransactionObj ) =
1713               $self->_NewTransaction(
1714                 Type          => 'CustomField',
1715                 Field         => $cf->Id,
1716                 NewReference  => $new_value_id,
1717                 ReferenceType => 'RT::ObjectCustomFieldValue',
1718               );
1719             unless ($TransactionId) {
1720                 return ( 0,
1721                     $self->loc( "Couldn't create a transaction: [_1]", $Msg ) );
1722             }
1723         }
1724         return ( 1, $self->loc( "[_1] added as a value for [_2]", $args{'Value'}, $cf->Name));
1725     }
1726
1727 }
1728
1729 # }}}
1730
1731 # {{{ DeleteCustomFieldValue
1732
1733 =head2 DeleteCustomFieldValue { Field => FIELD, Value => VALUE }
1734
1735 Deletes VALUE as a value of CustomField FIELD. 
1736
1737 VALUE can be a string, a CustomFieldValue or a ObjectCustomFieldValue.
1738
1739 If VALUE is not a valid value for the custom field, returns 
1740 (0, 'Error message' ) otherwise, returns (1, 'Success Message')
1741
1742 =cut
1743
1744 sub DeleteCustomFieldValue {
1745     my $self = shift;
1746     my %args = (
1747         Field   => undef,
1748         Value   => undef,
1749         ValueId => undef,
1750         @_
1751     );
1752
1753     my $cf = $self->LoadCustomFieldByIdentifier($args{'Field'});
1754
1755     unless ( $cf->Id ) {
1756         return ( 0, $self->loc( "Custom field [_1] not found", $args{'Field'} ) );
1757     }
1758     my ( $val, $msg ) = $cf->DeleteValueForObject(
1759         Object  => $self,
1760         Id      => $args{'ValueId'},
1761         Content => $args{'Value'},
1762     );
1763     unless ($val) {
1764         return ( 0, $msg );
1765     }
1766     my ( $TransactionId, $Msg, $TransactionObj ) = $self->_NewTransaction(
1767         Type          => 'CustomField',
1768         Field         => $cf->Id,
1769         OldReference  => $val,
1770         ReferenceType => 'RT::ObjectCustomFieldValue',
1771     );
1772     unless ($TransactionId) {
1773         return ( 0, $self->loc( "Couldn't create a transaction: [_1]", $Msg ) );
1774     }
1775
1776     return (
1777         $TransactionId,
1778         $self->loc(
1779             "[_1] is no longer a value for custom field [_2]",
1780             $TransactionObj->OldValue, $cf->Name
1781         )
1782     );
1783 }
1784
1785 # }}}
1786
1787 # {{{ FirstCustomFieldValue
1788
1789 =head2 FirstCustomFieldValue FIELD
1790
1791 Return the content of the first value of CustomField FIELD for this ticket
1792 Takes a field id or name
1793
1794 =cut
1795
1796 sub FirstCustomFieldValue {
1797     my $self = shift;
1798     my $field = shift;
1799     my $values = $self->CustomFieldValues($field);
1800     if ($values->First) {
1801         return $values->First->Content;
1802     } else {
1803         return undef;
1804     }
1805
1806 }
1807
1808
1809
1810 # {{{ CustomFieldValues
1811
1812 =head2 CustomFieldValues FIELD
1813
1814 Return a ObjectCustomFieldValues object of all values of the CustomField whose 
1815 id or Name is FIELD for this record.
1816
1817 Returns an RT::ObjectCustomFieldValues object
1818
1819 =cut
1820
1821 sub CustomFieldValues {
1822     my $self  = shift;
1823     my $field = shift;
1824
1825     if ($field) {
1826         my $cf = $self->LoadCustomFieldByIdentifier($field);
1827
1828         # we were asked to search on a custom field we couldn't fine
1829         unless ( $cf->id ) {
1830             return RT::ObjectCustomFieldValues->new( $self->CurrentUser );
1831         }
1832         return ( $cf->ValuesForObject($self) );
1833     }
1834
1835     # we're not limiting to a specific custom field;
1836     my $ocfs = RT::ObjectCustomFieldValues->new( $self->CurrentUser );
1837     $ocfs->LimitToObject($self);
1838     return $ocfs;
1839
1840 }
1841
1842 =head2 CustomField IDENTIFER
1843
1844 Find the custom field has id or name IDENTIFIER for this object.
1845
1846 If no valid field is found, returns an empty RT::CustomField object.
1847
1848 =cut
1849
1850 sub LoadCustomFieldByIdentifier {
1851     my $self = shift;
1852     my $field = shift;
1853     
1854     my $cf = RT::CustomField->new($self->CurrentUser);
1855
1856     if ( UNIVERSAL::isa( $field, "RT::CustomField" ) ) {
1857         $cf->LoadById( $field->id );
1858     }
1859     elsif ($field =~ /^\d+$/) {
1860         $cf = RT::CustomField->new($self->CurrentUser);
1861         $cf->Load($field); 
1862     } else {
1863
1864         my $cfs = $self->CustomFields($self->CurrentUser);
1865         $cfs->Limit(FIELD => 'Name', VALUE => $field);
1866         $cf = $cfs->First || RT::CustomField->new($self->CurrentUser);
1867     }
1868     return $cf;
1869 }
1870
1871
1872 # }}}
1873
1874 # }}}
1875
1876 # }}}
1877
1878 sub BasicColumns {
1879 }
1880
1881 sub WikiBase {
1882   return $RT::WebPath. "/index.html?q=";
1883 }
1884
1885 eval "require RT::Record_Vendor";
1886 die $@ if ($@ && $@ !~ qr{^Can't locate RT/Record_Vendor.pm});
1887 eval "require RT::Record_Local";
1888 die $@ if ($@ && $@ !~ qr{^Can't locate RT/Record_Local.pm});
1889
1890 1;