# $Header: /raid/cvsroot/rt/lib/RT/Transaction.pm,v 1.6 2002/07/10 17:59:46 jesse 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;
