import rt 2.0.14
[freeside.git] / rt / lib / RT / Transaction.pm
diff --git a/rt/lib/RT/Transaction.pm b/rt/lib/RT/Transaction.pm
new file mode 100755 (executable)
index 0000000..ee1f069
--- /dev/null
@@ -0,0 +1,783 @@
+# $Header: /home/cvs/cvsroot/freeside/rt/lib/RT/Transaction.pm,v 1.1 2002-08-12 06:17:07 ivan Exp $
+# Copyright 1999-2001 Jesse Vincent <jesse@fsck.com>
+# Released under the terms of the GNU Public License
+
+=head1 NAME
+
+  RT::Transaction - RT\'s transaction object
+
+=head1 SYNOPSIS
+
+  use RT::Transaction;
+
+
+=head1 DESCRIPTION
+
+
+Each RT::Transaction describes an atomic change to a ticket object 
+or an update to an RT::Ticket object.
+It can have arbitrary MIME attachments.
+
+
+=head1 METHODS
+
+=begin testing
+
+ok(require RT::TestHarness);
+ok(require RT::Transaction);
+
+=end testing
+
+=cut
+
+package RT::Transaction;
+
+use RT::Record;
+@ISA= qw(RT::Record);
+    
+use RT::Attachments;
+
+# {{{ sub _Init 
+sub _Init  {
+    my $self = shift;
+  $self->{'table'} = "Transactions";
+  return ($self->SUPER::_Init(@_));
+
+}
+# }}}
+
+# {{{ sub Create 
+
+=head2 Create
+
+Create a new transaction.
+
+This routine should _never_ be called anything other Than RT::Ticket. It should not be called 
+from client code. Ever. Not ever.  If you do this, we will hunt you down. and break your kneecaps.
+Then the unpleasant stuff will start.
+
+TODO: Document what gets passed to this
+
+=cut
+
+sub Create  {
+    my $self = shift;
+    my %args = ( id => undef,
+                TimeTaken => 0,
+                Ticket => 0 ,
+                Type => 'undefined',
+                Data => '',
+                Field => undef,
+                OldValue => undef,
+                NewValue => undef,
+                MIMEObj => undef,
+                ActivateScrips => 1,
+                @_
+              );
+    
+    #if we didn't specify a ticket, we need to bail
+    unless ( $args{'Ticket'} ) {
+       return(0, "RT::Transaction->Create couldn't, as you didn't specify a ticket id");
+    }
+        
+    #lets create our transaction
+    my $id = $self->SUPER::Create(Ticket => $args{'Ticket'},
+                                 TimeTaken => $args{'TimeTaken'},
+                                 Type => $args{'Type'},
+                                 Data => $args{'Data'},
+                                 Field => $args{'Field'},
+                                 OldValue => $args{'OldValue'},
+                                 NewValue => $args{'NewValue'},
+                                 Created => $args{'Created'}
+                                );
+    $self->Load($id);
+    $self->_Attach($args{'MIMEObj'})
+      if defined $args{'MIMEObj'};
+    
+    #Provide a way to turn off scrips if we need to
+    if ($args{'ActivateScrips'}) {
+
+       #We're really going to need a non-acled ticket for the scrips to work
+       my $TicketAsSystem = RT::Ticket->new($RT::SystemUser);
+       $TicketAsSystem->Load($args{'Ticket'}) || 
+         $RT::Logger->err("$self couldn't load ticket $args{'Ticket'}\n");
+       
+       my $TransAsSystem = RT::Transaction->new($RT::SystemUser);
+       $TransAsSystem->Load($self->id) ||
+         $RT::Logger->err("$self couldn't load a copy of itself as superuser\n");
+       
+       # {{{ Deal with Scrips
+    
+    #Load a scripscopes object
+    use RT::Scrips;
+    my $PossibleScrips = RT::Scrips->new($RT::SystemUser);
+    
+    $PossibleScrips->LimitToQueue($TicketAsSystem->QueueObj->Id); #Limit it to  $Ticket->QueueObj->Id
+    $PossibleScrips->LimitToGlobal(); # or to "global"
+    my $ConditionsAlias = $PossibleScrips->NewAlias('ScripConditions');
+    
+    $PossibleScrips->Join(ALIAS1 => 'main',  FIELD1 => 'ScripCondition',
+                         ALIAS2 => $ConditionsAlias, FIELD2=> 'id');
+    
+    
+    #We only want things where the scrip applies to this sort of transaction
+    $PossibleScrips->Limit(ALIAS=> $ConditionsAlias,
+                          FIELD=>'ApplicableTransTypes',
+                          OPERATOR => 'LIKE',
+                          VALUE => $args{'Type'},
+                          ENTRYAGGREGATOR => 'OR',
+                         );
+    
+    # Or where the scrip applies to any transaction
+    $PossibleScrips->Limit(ALIAS=> $ConditionsAlias,
+                          FIELD=>'ApplicableTransTypes',
+                          OPERATOR => 'LIKE',
+                          VALUE => "Any",
+                          ENTRYAGGREGATOR => 'OR',
+                         );                        
+    
+    #Iterate through each script and check it's applicability.
+    
+    while (my $Scrip = $PossibleScrips->Next()) {
+      
+      #TODO: properly deal with errors raised in this scrip loop
+       
+      #$RT::Logger->debug("$self now dealing with ".$Scrip->Id. "\n");      
+       eval {
+         local $SIG{__DIE__} = sub { $RT::Logger->error($_[0])};
+         
+         
+         #Load the scrip's Condition object
+         $Scrip->ConditionObj->LoadCondition(TicketObj => $TicketAsSystem, 
+                                             TransactionObj => $TransAsSystem);          
+         
+         
+         #If it's applicable, prepare and commit it
+         
+       $RT::Logger->debug ("$self: Checking condition ".$Scrip->ConditionObj->Name. "...\n");
+         
+         if ( $Scrip->IsApplicable() ) {
+             
+               $RT::Logger->debug ("$self: Matches condition ".$Scrip->ConditionObj->Name. "...\n");
+             #TODO: handle some errors here
+             
+             $Scrip->ActionObj->LoadAction(TicketObj => $TicketAsSystem, 
+                                          TransactionObj => $TransAsSystem);
+         
+             
+             if ($Scrip->Prepare()) {
+                 $RT::Logger->debug("$self: Prepared " .
+                                  $Scrip->ActionObj->Name . "\n");
+                 if ($Scrip->Commit()) {
+                       $RT::Logger->debug("$self: Committed " .
+                                          $Scrip->ActionObj->Name . "\n");
+                 }
+                 else {
+                       $RT::Logger->info("$self: Failed to commit ".
+                                          $Scrip->ActionObj->Name . "\n");
+                 } 
+             }
+             else {
+                 $RT::Logger->info("$self: Failed to prepare " .
+                                    $Scrip->ActionObj->Name . "\n");
+             }
+
+             #We're done with it. lets clean up.
+             #TODO: something else isn't letting these get garbage collected. check em out.
+             $Scrip->ActionObj->DESTROY();
+             $Scrip->ConditionObj->DESTROY;
+         }
+         
+         
+       else {
+           $RT::Logger->debug ("$self: Doesn't match condition ".$Scrip->ConditionObj->Name. "...\n");
+
+           # TODO: why doesn't this catch all the ScripObjs we create. 
+           # and why do we explictly need to destroy them?
+           $Scrip->ConditionObj->DESTROY;
+       }
+      }        
+    }
+
+    # }}}
+       
+    }
+
+    return ($id, "Transaction Created");
+}
+
+# }}}
+
+# {{{ sub Delete
+
+sub Delete {
+    my $self = shift;
+    return (0, 'Deleting this object could break referential integrity');
+}
+
+# }}}
+
+# {{{ Routines dealing with Attachments
+
+# {{{ sub Message 
+
+=head2 Message
+
+  Returns the RT::Attachments Object which contains the "top-level" object
+  attachment for this transaction
+
+=cut
+
+sub Message  {
+
+    my $self = shift;
+    
+    if (!defined ($self->{'message'}) ){
+       
+       $self->{'message'} = new RT::Attachments($self->CurrentUser);
+       $self->{'message'}->Limit(FIELD => 'TransactionId',
+                                 VALUE => $self->Id);
+       
+       $self->{'message'}->ChildrenOf(0);
+    } 
+    return($self->{'message'});
+}
+# }}}
+
+# {{{ sub Content
+
+=head2 Content PARAMHASH
+
+If this transaction has attached mime objects, returns the first text/ part.
+Otherwise, returns undef.
+
+Takes a paramhash.  If the $args{'Quote'} parameter is set, wraps this message 
+at $args{'Wrap'}.  $args{'Wrap'} defaults to 70.
+
+
+=cut
+
+sub Content {
+    my $self = shift;
+    my %args = ( Quote => 0,
+                Wrap => 70,
+                @_ );
+
+    my $content = undef;
+
+    # If we don\'t have any content, return undef now.
+    unless ($self->Message->First) {
+       return (undef);
+    }  
+    
+    # Get the set of toplevel attachments to this transaction.
+    my $MIMEObj = $self->Message->First();
+    
+    # If it's a message or a plain part, just return the
+    # body. 
+    if ($MIMEObj->ContentType() =~ '^(text|message)(/|$)') {
+       $content = $MIMEObj->Content();
+    }
+    
+    # If it's a multipart object, first try returning the first 
+    # text/plain part. 
+    
+    elsif ($MIMEObj->ContentType() =~ '^multipart/') {
+       my $plain_parts = $MIMEObj->Children();
+       $plain_parts->ContentType(VALUE => 'text/plain');
+       
+       # If we actully found a part, return its content
+       if ($plain_parts->First && 
+        $plain_parts->First->Content ne '') {
+           $content = $plain_parts->First->Content;            
+       }       
+       
+       # If that fails, return the  first text/ or message/ part 
+       # which has some content.
+    
+       else {
+           my $all_parts = $MIMEObj->Children();
+           while (($content == undef) && 
+                  (my $part = $all_parts->Next)) {
+               if (($part->ContentType() =~ '^(text|message)(/|$)') and
+                   ($part->Content())) {
+                   $content = $part->Content;
+               }       
+           }
+       }       
+
+    }
+    # If all else fails, return a message that we couldn't find
+    # any content
+    else { 
+        $content = 'This transaction appears to have no content';
+    }  
+
+    if ($args{'Quote'}) {
+       # Remove quoted signature.
+       $content =~ s/\n-- \n(.*)$//s;
+
+       # What's the longest line like?
+       foreach (split (/\n/,$content)) {
+           $max=length if ( length > $max);
+       }
+
+       if ($max>76) {
+           require Text::Wrapper;
+           my $wrapper=new Text::Wrapper
+               (
+                columns => $args{'Wrap'}, 
+                body_start => ($max > 70*3 ? '   ' : ''),
+                par_start => ''
+                );
+           $content=$wrapper->wrap($content);
+       }
+
+       $content =~ s/^/> /gm;
+       $content = '[' . $self->CreatorObj->Name() . ' - ' . $self->CreatedAsString()
+                   . "]:\n\n"
+               . $content . "\n\n";
+
+    }
+
+    return ($content); 
+}
+# }}}
+
+# {{{ sub Subject
+
+=head2 Subject
+
+If this transaction has attached mime objects, returns the first one's subject
+Otherwise, returns null
+  
+=cut
+
+sub Subject {
+    my $self = shift;
+    if ($self->Message->First) {
+       return ($self->Message->First->Subject);
+    }
+    else {
+       return (undef);
+    }
+}
+# }}}
+
+# {{{ sub Attachments 
+
+=head2 Attachments
+
+  Returns all the RT::Attachment objects which are attached
+to this transaction. Takes an optional parameter, which is
+a ContentType that Attachments should be restricted to.
+
+=cut
+
+
+sub Attachments  {
+    my $self = shift;
+    my $Types = '';
+    $Types = shift if (@_);
+
+    my $Attachments = new RT::Attachments($self->CurrentUser);
+    
+    #If it's a comment, return an empty object if they don't have the right to see it
+    if ($self->Type eq 'Comment') {
+       unless ($self->CurrentUserHasRight('ShowTicketComments')) {
+           return ($Attachments);
+       }
+    }  
+    #if they ain't got rights to see, return an empty object
+    else {
+       unless ($self->CurrentUserHasRight('ShowTicket')) {
+           return ($Attachments);
+       }
+    }
+    
+    $Attachments->Limit(FIELD => 'TransactionId',
+                       VALUE => $self->Id);
+
+    # Get the attachments in the order they're put into
+    # the database.  Arguably, we should be returning a tree
+    # of attachments, not a set...but no current app seems to need
+    # it. 
+
+    $Attachments->OrderBy(ALIAS => 'main', 
+                         FIELD => 'Id',
+                         ORDER => 'asc');
+
+    if ($Types) {
+       $Attachments->ContentType( VALUE => "$Types",
+                                  OPERATOR => "LIKE");
+    }
+    
+    
+    return($Attachments);
+    
+}
+
+# }}}
+
+# {{{ sub _Attach 
+
+=head2 _Attach
+
+A private method used to attach a mime object to this transaction.
+
+=cut
+
+sub _Attach  {
+    my $self = shift;
+    my $MIMEObject = shift;
+    
+    if (!defined($MIMEObject)) {
+       $RT::Logger->error("$self _Attach: We can't attach a mime object if you don't give us one.\n");
+       return(0, "$self: no attachment specified");
+    }
+    
+  
+    use RT::Attachment;
+    my $Attachment = new RT::Attachment ($self->CurrentUser);
+    $Attachment->Create(TransactionId => $self->Id,
+                       Attachment => $MIMEObject);
+    return ($Attachment, "Attachment created");
+    
+}
+
+# }}}
+
+# }}}
+
+# {{{ Routines dealing with Transaction Attributes
+
+# {{{ sub TicketObj
+
+=head2 TicketObj
+
+Returns this transaction's ticket object.
+
+=cut
+
+sub TicketObj {
+    my $self = shift;
+    if (! exists $self->{'TicketObj'}) {
+       $self->{'TicketObj'} = new RT::Ticket($self->CurrentUser);
+       $self->{'TicketObj'}->Load($self->Ticket);
+    }
+    
+    return $self->{'TicketObj'};
+}
+# }}}
+
+# {{{ sub Description 
+
+=head2 Description
+
+Returns a text string which describes this transaction
+
+=cut
+
+
+sub Description  {
+    my $self = shift;
+
+    #Check those ACLs
+    #If it's a comment, we need to be extra special careful
+    if ($self->__Value('Type') eq 'Comment') {
+       unless ($self->CurrentUserHasRight('ShowTicketComments')) {
+           return (0, "Permission Denied");
+       }
+    }  
+
+    #if they ain't got rights to see, don't let em
+    else {
+       unless ($self->CurrentUserHasRight('ShowTicket')) {
+           return (0, "Permission Denied");
+       }
+    }
+
+    if (!defined($self->Type)) {
+       return("No transaction type specified");
+    }
+    
+    return ($self->BriefDescription . " by " . $self->CreatorObj->Name);
+}
+
+# }}}
+
+# {{{ sub BriefDescription 
+
+=head2 BriefDescription
+
+Returns a text string which briefly describes this transaction
+
+=cut
+
+
+sub BriefDescription  {
+    my $self = shift;
+
+    #Check those ACLs
+    #If it's a comment, we need to be extra special careful
+    if ($self->__Value('Type') eq 'Comment') {
+       unless ($self->CurrentUserHasRight('ShowTicketComments')) {
+           return (0, "Permission Denied");
+       }
+    }  
+
+    #if they ain't got rights to see, don't let em
+    else {
+       unless ($self->CurrentUserHasRight('ShowTicket')) {
+           return (0, "Permission Denied");
+       }
+    }
+
+    if (!defined($self->Type)) {
+       return("No transaction type specified");
+    }
+    
+    if ($self->Type eq 'Create'){
+       return("Ticket created");
+    }
+    elsif ($self->Type =~ /Status/) {
+       if ($self->Field eq 'Status') {
+           if ($self->NewValue eq 'dead') {
+               return ("Ticket killed");
+      }
+           else {
+               return( "Status changed from ".  $self->OldValue . 
+                       " to ". $self->NewValue);
+
+           }
+       }
+       # Generic:
+       return ($self->Field." changed from ".($self->OldValue||"(empty value)").
+         " to ".$self->NewValue );
+      }
+    
+    if ($self->Type eq 'Correspond')    {
+       return("Correspondence added");
+    }
+    
+    elsif ($self->Type eq 'Comment')  {
+       return( "Comments added");
+    }
+    
+    elsif ($self->Type eq 'Keyword') {
+
+       my $field = 'Keyword';
+
+       if ($self->Field) {
+           my $keywordsel = new RT::KeywordSelect ($self->CurrentUser);
+           $keywordsel->Load($self->Field);
+           $field = $keywordsel->Name();
+       }
+
+       if ($self->OldValue eq '') {
+           return ($field." ".$self->NewValue." added");
+       }
+       elsif ($self->NewValue eq '') {
+           return ($field." ".$self->OldValue." deleted"); 
+           
+       }
+       else {
+           return ($field." ".$self->OldValue . " changed to ". 
+                    $self->NewValue);
+       }       
+    }
+    
+    elsif ($self->Type eq 'Untake'){
+           return( "Untaken");
+       }
+    
+    elsif ($self->Type eq "Take") {
+       return( "Taken");
+    }
+    
+    elsif ($self->Type eq "Force") {
+        my $Old = RT::User->new($self->CurrentUser);
+        $Old->Load($self->OldValue);
+        my $New = RT::User->new($self->CurrentUser);
+        $New->Load($self->NewValue);
+       return "Owner forcibly changed from ".$Old->Name . " to ". $New->Name;
+    }
+    elsif ($self->Type eq "Steal") {
+       my $Old = RT::User->new($self->CurrentUser);
+       $Old->Load($self->OldValue);
+       return "Stolen from ".$Old->Name;
+    }
+    
+    elsif ($self->Type eq "Give") {
+       my $New = RT::User->new($self->CurrentUser);
+       $New->Load($self->NewValue);
+       return( "Given to ".$New->Name);
+    }
+    
+    elsif ($self->Type eq 'AddWatcher'){
+       return( $self->Field." ". $self->NewValue ." added");
+    }
+    
+    elsif ($self->Type eq 'DelWatcher'){
+       return( $self->Field." ".$self->OldValue ." deleted");
+    }
+    
+    elsif ($self->Type eq 'Subject') {
+       return( "Subject changed to ".$self->Data);
+    }
+    elsif ($self->Type eq 'Told') {
+       return( "User notified");
+    }
+    
+    elsif ($self->Type eq 'AddLink') {
+       return ($self->Data);
+    }
+    elsif ($self->Type eq 'DeleteLink') {
+       return ($self->Data);
+    }
+    elsif ($self->Type eq 'Set') {
+       if ($self->Field eq 'Queue') {
+           my $q1 = new RT::Queue($self->CurrentUser);
+           $q1->Load($self->OldValue);
+           my $q2 = new RT::Queue($self->CurrentUser);
+           $q2->Load($self->NewValue);
+           return ($self->Field . " changed from " . $q1->Name . " to ".
+                   $q2->Name);
+       }
+
+        # Write the date/time change at local time:                    
+    elsif ($self->Field =~  /Due|Starts|Started|Told/) {           
+        my $t1 = new RT::Date($self->CurrentUser);                 
+        $t1->Set(Format => 'ISO', Value => $self->NewValue);       
+        my $t2 = new RT::Date($self->CurrentUser);                 
+        $t2->Set(Format => 'ISO', Value => $self->OldValue);       
+        return ($self->Field . " changed from " . $t2->AsString .  
+                    " to ".$t1->AsString);      
+    }                
+       else {
+           return ($self->Field . " changed from " . $self->OldValue . 
+                   " to ".$self->NewValue);
+       }       
+    }
+    elsif ($self->Type eq 'PurgeTransaction') {
+       return ("Transaction ".$self->Data. " purged");
+    }
+    else {
+       return ("Default: ". $self->Type ."/". $self->Field . 
+               " changed from " . $self->OldValue . 
+               " to ".$self->NewValue);
+       
+    }
+}
+
+# }}}
+
+# {{{ Utility methods
+
+# {{{ sub IsInbound
+
+=head2 IsInbound
+
+Returns true if the creator of the transaction is a requestor of the ticket.
+Returns false otherwise
+
+=cut
+
+sub IsInbound {
+    my $self=shift;
+    return ($self->TicketObj->IsRequestor($self->CreatorObj));
+}
+
+# }}}
+
+# }}}
+
+# {{{ sub _Accessible 
+
+sub _Accessible  {
+  my $self = shift;
+  my %Cols = (
+             TimeTaken => 'read',
+             Ticket => 'read/public',
+             Type=> 'read',
+             Field => 'read',
+             Data => 'read',
+             NewValue => 'read',
+             OldValue => 'read',
+             Creator => 'read/auto',
+             Created => 'read/auto',
+            );
+  return $self->SUPER::_Accessible(@_, %Cols);
+}
+
+# }}}
+
+# }}}
+
+# {{{ sub _Set
+
+sub _Set {
+    my $self = shift;
+    return(0, 'Transactions are immutable');
+}
+
+# }}}
+
+# {{{ sub _Value 
+
+=head2 _Value
+
+Takes the name of a table column.
+Returns its value as a string, if the user passes an ACL check
+
+=cut
+
+sub _Value  {
+
+    my $self = shift;
+    my $field = shift;
+    
+    
+    #if the field is public, return it.
+    if ($self->_Accessible($field, 'public')) {
+       return($self->__Value($field));
+       
+    }
+    #If it's a comment, we need to be extra special careful
+    if ($self->__Value('Type') eq 'Comment') {
+       unless ($self->CurrentUserHasRight('ShowTicketComments')) {
+           return (undef);
+       }
+    }  
+    #if they ain't got rights to see, don't let em
+    else {
+       unless ($self->CurrentUserHasRight('ShowTicket')) {
+           return (undef);
+       }
+    }  
+    
+    return($self->__Value($field));
+    
+}
+
+# }}}
+
+# {{{ sub CurrentUserHasRight
+
+=head2 CurrentUserHasRight RIGHT
+
+Calls $self->CurrentUser->HasQueueRight for the right passed in here.
+passed in here.
+
+=cut
+
+sub CurrentUserHasRight {
+    my $self = shift;
+    my $right = shift;
+    return ($self->CurrentUser->HasQueueRight(Right => "$right", 
+                                              TicketObj => $self->TicketObj));            
+}
+
+# }}}
+
+1;