## $Header: /raid/cvsroot/rt/lib/RT/Interface/Web.pm,v 1.12 2002/07/12 17:31:41 jesse Exp $

## Portions Copyright 2000 Tobias Brox <tobix@fsck.com>
## Copyright 1996-2002 Jesse Vincent <jesse@bestpractical.com>

## This is a library of static subs to be used by the Mason web
## interface to RT

package RT::Interface::Web;

# {{{ sub NewParser

=head2 NewParser

  Returns a new Mason::Parser object. Takes a param hash of things 
  that get passed to HTML::Mason::Parser. Currently hard coded to only
  take the parameter 'allow_globals'.

=cut

sub NewParser {
    my %args = (
        allow_globals => undef,
        @_
    );

    my $parser = new HTML::Mason::Parser(
        default_escape_flags => 'h',
        allow_globals        => $args{'allow_globals'}
    );
    return ($parser);
}

# }}}

# {{{ sub NewInterp

=head2 NewInterp 

  Takes a paremeter hash. Needs a param called 'parser' which is a reference
  to an HTML::Mason::Parser.
  returns a new Mason::Interp object

=cut

sub NewInterp {
    my %params = (
        comp_root                    => [
            [ local    => $RT::MasonLocalComponentRoot ],
            [ standard => $RT::MasonComponentRoot ]
        ],
        data_dir => "$RT::MasonDataDir",
        @_
    );

    #We allow recursive autohandlers to allow for RT auth.

    use HTML::Mason::Interp;
    my $interp = new HTML::Mason::Interp(%params);

}

# }}}

# {{{ sub NewApacheHandler 

=head2 NewApacheHandler

  Takes a Mason::Interp object
  Returns a new Mason::ApacheHandler object

=cut

sub NewApacheHandler {
    my $interp = shift;
    my $ah = new HTML::Mason::ApacheHandler( interp => $interp );
    return ($ah);
}

# }}}


# {{{ sub NewMason11ApacheHandler

=head2 NewMason11ApacheHandler

  Returns a new Mason::ApacheHandler object

=cut

sub NewMason11ApacheHandler {
        my %args = ( default_escape_flags => 'h',
                    allow_globals        => [%session],
        comp_root                    => [
            [ local    => $RT::MasonLocalComponentRoot ],
            [ standard => $RT::MasonComponentRoot ]
        ],
        data_dir => "$RT::MasonDataDir",
        args_method => 'CGI'
    );
    my $ah = new HTML::Mason::ApacheHandler(%args);
    return ($ah);
}

# }}}





# }}}

package HTML::Mason::Commands;

# {{{ sub Abort
# Error - calls Error and aborts
sub Abort {

    if ( $session{'ErrorDocument'} && $session{'ErrorDocumentType'} ) {
        SetContentType( $session{'ErrorDocumentType'} );
        $m->comp( $session{'ErrorDocument'}, Why => shift );
        $m->abort;
    }
    else {
        SetContentType('text/html');
        $m->comp( "/Elements/Error", Why => shift );
        $m->abort;
    }
}

# }}}

# {{{ sub CreateTicket 

=head2 CreateTicket ARGS

Create a new ticket, using Mason's %ARGS.  returns @results.
=cut

sub CreateTicket {
    my %ARGS = (@_);

    my (@Actions);

    my $Ticket = new RT::Ticket( $session{'CurrentUser'} );

    my $Queue = new RT::Queue( $session{'CurrentUser'} );
    unless ( $Queue->Load( $ARGS{'Queue'} ) ) {
        Abort('Queue not found');
    }

    unless ( $Queue->CurrentUserHasRight('CreateTicket') ) {
        Abort('You have no permission to create tickets in that queue.');
    }

    my $due = new RT::Date( $session{'CurrentUser'} );
    $due->Set( Format => 'unknown', Value => $ARGS{'Due'} );
    my $starts = new RT::Date( $session{'CurrentUser'} );
    $starts->Set( Format => 'unknown', Value => $ARGS{'Starts'} );

    my @Requestors = split ( /,/, $ARGS{'Requestors'} );
    my @Cc         = split ( /,/, $ARGS{'Cc'} );
    my @AdminCc    = split ( /,/, $ARGS{'AdminCc'} );

    my $MIMEObj = MakeMIMEEntity(
        Subject             => $ARGS{'Subject'},
        From                => $ARGS{'From'},
        Cc                  => $ARGS{'Cc'},
        Body                => $ARGS{'Content'},
        AttachmentFieldName => 'Attach'
    );

    my %create_args = (
        Queue           => $ARGS{Queue},
        Owner           => $ARGS{Owner},
        InitialPriority => $ARGS{InitialPriority},
        FinalPriority   => $ARGS{FinalPriority},
        TimeLeft        => $ARGS{TimeLeft},
        TimeWorked      => $ARGS{TimeWorked},
        Requestor       => \@Requestors,
        Cc              => \@Cc,
        AdminCc         => \@AdminCc,
        Subject         => $ARGS{Subject},
        Status          => $ARGS{Status},
        Due             => $due->ISO,
        Starts          => $starts->ISO,
        MIMEObj         => $MIMEObj
    );

    # we need to get any KeywordSelect-<integer> fields into %create_args..
    grep { $_ =~ /^KeywordSelect-/ &&{ $create_args{$_} = $ARGS{$_} } } %ARGS;

    my ( $id, $Trans, $ErrMsg ) = $Ticket->Create(%create_args);
    unless ( $id && $Trans ) {
        Abort($ErrMsg);
    }
    my @linktypes = qw( DependsOn MemberOf RefersTo );

    foreach my $linktype (@linktypes) {
        foreach my $luri ( split ( / /, $ARGS{"new-$linktype"} ) ) {
            $luri =~ s/\s*$//;    # Strip trailing whitespace
            my ( $val, $msg ) = $Ticket->AddLink(
                Target => $luri,
                Type   => $linktype
            );
            push ( @Actions, $msg ) unless ($val);
        }

        foreach my $luri ( split ( / /, $ARGS{"$linktype-new"} ) ) {
            my ( $val, $msg ) = $Ticket->AddLink(
                Base => $luri,
                Type => $linktype
            );

            push ( @Actions, $msg ) unless ($val);
        }
    }

    push ( @Actions, $ErrMsg );
    unless ( $Ticket->CurrentUserHasRight('ShowTicket') ) {
        Abort( "No permission to view newly created ticket #"
            . $Ticket->id . "." );
    }
    return ( $Ticket, @Actions );

}

# }}}

# {{{ sub LoadTicket - loads a ticket

=head2  LoadTicket id

Takes a ticket id as its only variable. if it's handed an array, it takes
the first value.

Returns an RT::Ticket object as the current user.

=cut

sub LoadTicket {
    my $id = shift;

    if ( ref($id) eq "ARRAY" ) {
        $id = $id->[0];
    }

    unless ($id) {
        Abort("No ticket specified");
    }

    my $Ticket = RT::Ticket->new( $session{'CurrentUser'} );
    $Ticket->Load($id);
    unless ( $Ticket->id ) {
        Abort("Could not load ticket $id");
    }
    return $Ticket;
}

# }}}

# {{{ sub ProcessUpdateMessage

sub ProcessUpdateMessage {

    #TODO document what else this takes.
    my %args = (
        ARGSRef   => undef,
        Actions   => undef,
        TicketObj => undef,
        @_
    );

    #Make the update content have no 'weird' newlines in it
    if ( $args{ARGSRef}->{'UpdateContent'} ) {

        if (
            $args{ARGSRef}->{'UpdateSubject'} eq $args{'TicketObj'}->Subject() )
        {
            $args{ARGSRef}->{'UpdateSubject'} = undef;
        }

        my $Message = MakeMIMEEntity(
            Subject             => $args{ARGSRef}->{'UpdateSubject'},
            Body                => $args{ARGSRef}->{'UpdateContent'},
            AttachmentFieldName => 'UpdateAttachment'
        );

	## Check whether this was a refresh or not.  

	# Match Correspondence or Comments.
        my $trans_flag = -2;
	my $trans_type = undef;
	my $orig_trans = $args{ARGSRef}->{'UpdateType'};
        if ( $orig_trans =~ /^(private|public)$/ ) {
	    $trans_type = "Comment";
        }elsif ( $orig_trans eq 'response' ) {
	    $trans_type = "Correspond";
	}

	# Do we have a transaction that we need to update on? session
	if( defined( $trans_type ) ){
	    $trans_flag = 0;

	    # Prepare a checksum.
	    # See perldoc -f unpack for example of this.
	    my $this_checksum = unpack("%32C*", $Message->body_as_string ) % 65535;

	    # The above *could* generate duplicate checksums.  Crosscheck with
	    # the length.
	    my $this_length = length( $Message->body_as_string );

	    # Don't forget the ticket id.
	    my $this_id = $args{TicketObj}->id;

	    # Check whether the previous transaction in the
	    # ticket is the same as the current transaction.
	    if( defined( $session{'prev_trans_type'} ) && defined( $session{'prev_trans_chksum'} ) && defined( $session{'prev_trans_length'} ) && defined( $session{'prev_trans_tickid'} ) ){
		if( $session{'prev_trans_type'} eq $orig_trans && $session{'prev_trans_chksum'} == $this_checksum && $session{'prev_trans_length'} == $this_length && $session{'prev_trans_tickid'} == $this_id ){
		    # Its the same as the previous transaction for this user.
		    $trans_flag = -1;
		}
	    }

	    # Store them for next time.
	    $session{'prev_trans_type'} = $orig_trans;
	    $session{'prev_trans_chksum'} = $this_checksum;
	    $session{'prev_trans_length'} = $this_length;
	    $session{'prev_trans_tickid'} = $this_id;

	    if( $trans_flag == -1 ){
                push ( @{ $args{'Actions'} },
"This appears to be a duplicate of your previous update (please do not refresh this page)" );
	    }


            if ( $trans_type eq 'Comment' && $trans_flag >= 0 ) {
                my ( $Transaction, $Description ) = $args{TicketObj}->Comment(
                    CcMessageTo  => $args{ARGSRef}->{'UpdateCc'},
                    BccMessageTo => $args{ARGSRef}->{'UpdateBcc'},
                    MIMEObj      => $Message,
                    TimeTaken    => $args{ARGSRef}->{'UpdateTimeWorked'}
                );
                push ( @{ $args{Actions} }, $Description );
            }
            elsif ( $trans_type eq 'Correspond' && $trans_flag >= 0 ) {
                my ( $Transaction, $Description ) = $args{TicketObj}->Correspond(
                    CcMessageTo  => $args{ARGSRef}->{'UpdateCc'},
                    BccMessageTo => $args{ARGSRef}->{'UpdateBcc'},
                    MIMEObj      => $Message,
                    TimeTaken    => $args{ARGSRef}->{'UpdateTimeWorked'}
                );
                push ( @{ $args{Actions} }, $Description );
            }
	}
        else {
            push ( @{ $args{'Actions'} },
    "Update type was neither correspondence nor comment. Update not recorded"
                );
        }
    }
}

# }}}

# {{{ sub MakeMIMEEntity

=head2 MakeMIMEEntity PARAMHASH

Takes a paramhash Subject, Body and AttachmentFieldName.

  Returns a MIME::Entity.

=cut

sub MakeMIMEEntity {

    #TODO document what else this takes.
    my %args = (
        Subject             => undef,
        From                => undef,
        Cc                  => undef,
        Body                => undef,
        AttachmentFieldName => undef,
        @_
    );

    #Make the update content have no 'weird' newlines in it

    $args{'Body'} =~ s/\r\n/\n/gs;
    my $Message = MIME::Entity->build(
        Subject => $args{'Subject'} || "",
        From    => $args{'From'},
        Cc      => $args{'Cc'},
        Data    => [ $args{'Body'} ]
    );

    my $cgi_object = CGIObject();
    if ( $cgi_object->param( $args{'AttachmentFieldName'} ) ) {

        my $cgi_filehandle =
          $cgi_object->upload( $args{'AttachmentFieldName'} );

        use File::Temp qw(tempfile tempdir);

        #foreach my $filehandle (@filenames) {

        # my ( $fh, $temp_file ) = tempfile();

        #$binmode $fh;    #thank you, windows

        # We're having trouble with tempfiles not getting created. Let's try it with 
        # a scalar instead

        my ( $buffer, @file );

        while ( my $bytesread = read( $cgi_filehandle, $buffer, 4096 ) ) {
            push ( @file, $buffer );
        }

        $RT::Logger->debug($file);
        my $filename = "$cgi_filehandle";
        $filename =~ s#^(.*)/##;
        $filename =~ s#^(.*)\\##;
        my $uploadinfo = $cgi_object->uploadInfo($cgi_filehandle);
        $Message->attach(
            Data => \@file,

            #Path     => $temp_file,
            Filename => $filename,
            Type     => $uploadinfo->{'Content-Type'}
        );

        #close($fh);
        #unlink($temp_file);

        #	}
    }
    $Message->make_singlepart();
    return ($Message);

}

# }}}

# {{{ sub ProcessSearchQuery

=head2 ProcessSearchQuery

  Takes a form such as the one filled out in webrt/Search/Elements/PickRestriction and turns it into something that RT::Tickets can understand.

TODO Doc exactly what comes in the paramhash


=cut

sub ProcessSearchQuery {
    my %args = @_;

    ## TODO: The only parameter here is %ARGS.  Maybe it would be
    ## cleaner to load this parameter as $ARGS, and use $ARGS->{...}
    ## instead of $args{ARGS}->{...} ? :)

    #Searches are sticky.
    if ( defined $session{'tickets'} ) {

        # Reset the old search
        $session{'tickets'}->GotoFirstItem;
    }
    else {

        # Init a new search
        $session{'tickets'} = RT::Tickets->new( $session{'CurrentUser'} );
    }

    #Import a bookmarked search if we have one
    if ( defined $args{ARGS}->{'Bookmark'} ) {
        $session{'tickets'}->ThawLimits( $args{ARGS}->{'Bookmark'} );
    }

    # {{{ Goto next/prev page
    if ( $args{ARGS}->{'GotoPage'} eq 'Next' ) {
        $session{'tickets'}->NextPage;
    }
    elsif ( $args{ARGS}->{'GotoPage'} eq 'Prev' ) {
        $session{'tickets'}->PrevPage;
    }

    # }}}

    # {{{ Deal with limiting the search

    if ( $args{ARGS}->{'RefreshSearchInterval'} ) {
        $session{'tickets_refresh_interval'} =
          $args{ARGS}->{'RefreshSearchInterval'};
    }

    if ( $args{ARGS}->{'TicketsSortBy'} ) {
        $session{'tickets_sort_by'}    = $args{ARGS}->{'TicketsSortBy'};
        $session{'tickets_sort_order'} = $args{ARGS}->{'TicketsSortOrder'};
        $session{'tickets'}->OrderBy(
            FIELD => $args{ARGS}->{'TicketsSortBy'},
            ORDER => $args{ARGS}->{'TicketsSortOrder'}
        );
    }

    # }}}

    # {{{ Set the query limit
    if ( defined $args{ARGS}->{'RowsPerPage'} ) {
        $RT::Logger->debug(
            "limiting to " . $args{ARGS}->{'RowsPerPage'} . " rows" );

        $session{'tickets_rows_per_page'} = $args{ARGS}->{'RowsPerPage'};
        $session{'tickets'}->RowsPerPage( $args{ARGS}->{'RowsPerPage'} );
    }

    # }}}
    # {{{ Limit priority
    if ( $args{ARGS}->{'ValueOfPriority'} ne '' ) {
        $session{'tickets'}->LimitPriority(
            VALUE    => $args{ARGS}->{'ValueOfPriority'},
            OPERATOR => $args{ARGS}->{'PriorityOp'}
        );
    }

    # }}}
    # {{{ Limit owner
    if ( $args{ARGS}->{'ValueOfOwner'} ne '' ) {
        $session{'tickets'}->LimitOwner(
            VALUE    => $args{ARGS}->{'ValueOfOwner'},
            OPERATOR => $args{ARGS}->{'OwnerOp'}
        );
    }

    # }}}
    # {{{ Limit requestor email

    if ( $args{ARGS}->{'ValueOfRequestor'} ne '' ) {
        my $alias = $session{'tickets'}->LimitRequestor(
            VALUE    => $args{ARGS}->{'ValueOfRequestor'},
            OPERATOR => $args{ARGS}->{'RequestorOp'},
        );

    }

    # }}}
    # {{{ Limit Queue
    if ( $args{ARGS}->{'ValueOfQueue'} ne '' ) {
        $session{'tickets'}->LimitQueue(
            VALUE    => $args{ARGS}->{'ValueOfQueue'},
            OPERATOR => $args{ARGS}->{'QueueOp'}
        );
    }

    # }}}
    # {{{ Limit Status
    if ( $args{ARGS}->{'ValueOfStatus'} ne '' ) {
        if ( ref( $args{ARGS}->{'ValueOfStatus'} ) ) {
            foreach my $value ( @{ $args{ARGS}->{'ValueOfStatus'} } ) {
                $session{'tickets'}->LimitStatus(
                    VALUE    => $value,
                    OPERATOR => $args{ARGS}->{'StatusOp'},
                );
            }
        }
        else {
            $session{'tickets'}->LimitStatus(
                VALUE    => $args{ARGS}->{'ValueOfStatus'},
                OPERATOR => $args{ARGS}->{'StatusOp'},
            );
        }

    }

    # }}}
    # {{{ Limit Subject
    if ( $args{ARGS}->{'ValueOfSubject'} ne '' ) {
        $session{'tickets'}->LimitSubject(
            VALUE    => $args{ARGS}->{'ValueOfSubject'},
            OPERATOR => $args{ARGS}->{'SubjectOp'},
        );
    }

    # }}}    
    # {{{ Limit Dates
    if ( $args{ARGS}->{'ValueOfDate'} ne '' ) {

        my $date = ParseDateToISO( $args{ARGS}->{'ValueOfDate'} );
        $args{ARGS}->{'DateType'} =~ s/_Date$//;

        $session{'tickets'}->LimitDate(
            FIELD    => $args{ARGS}->{'DateType'},
            VALUE    => $date,
            OPERATOR => $args{ARGS}->{'DateOp'},
        );
    }

    # }}}    
    # {{{ Limit Content
    if ( $args{ARGS}->{'ValueOfContent'} ne '' ) {
        $session{'tickets'}->LimitContent(
            VALUE    => $args{ARGS}->{'ValueOfContent'},
            OPERATOR => $args{ARGS}->{'ContentOp'},
        );
    }

    # }}}   
    # {{{ Limit KeywordSelects

    foreach my $KeywordSelectId (
        map { /^KeywordSelect(\d+)$/; $1 }
        grep { /^KeywordSelect(\d+)$/; } keys %{ $args{ARGS} }
      )
    {
        my $form = $args{ARGS}->{"KeywordSelect$KeywordSelectId"};
        my $oper = $args{ARGS}->{"KeywordSelectOp$KeywordSelectId"};
        foreach my $KeywordId ( ref($form) ? @{$form} : ($form) ) {
            next unless ($KeywordId);
            my $quote = 1;
            if ( $KeywordId =~ /^null$/i ) {

                #Don't quote the string 'null'
                $quote = 0;

                # Convert the operator to something apropriate for nulls
                $oper = 'IS'     if ( $oper eq '=' );
                $oper = 'IS NOT' if ( $oper eq '!=' );
            }
            $session{'tickets'}->LimitKeyword(
                KEYWORDSELECT => $KeywordSelectId,
                OPERATOR      => $oper,
                QUOTEVALUE    => $quote,
                KEYWORD       => $KeywordId
            );
        }
    }

    # }}}

}

# }}}

# {{{ sub ParseDateToISO

=head2 ParseDateToISO

Takes a date in an arbitrary format.
Returns an ISO date and time in GMT

=cut

sub ParseDateToISO {
    my $date = shift;

    my $date_obj = new RT::Date($CurrentUser);
    $date_obj->Set(
        Format => 'unknown',
        Value  => $date
    );
    return ( $date_obj->ISO );
}

# }}}

# {{{ sub Config 
# TODO: This might eventually read the cookies, user configuration
# information from the DB, queue configuration information from the
# DB, etc.

sub Config {
    my $args = shift;
    my $key  = shift;
    return $args->{$key} || $RT::WebOptions{$key};
}

# }}}

# {{{ sub ProcessACLChanges

sub ProcessACLChanges {
    my $ACLref  = shift;
    my $ARGSref = shift;

    my @CheckACL = @$ACLref;
    my %ARGS     = %$ARGSref;

    my ( $ACL, @results );

    # {{{ Add rights
    foreach $ACL (@CheckACL) {
        my ($Principal);

        next unless ($ACL);

        # Parse out what we're really talking about. 
        if ( $ACL =~ /^(.*?)-(\d+)-(.*?)-(\d+)/ ) {
            my $PrincipalType = $1;
            my $PrincipalId   = $2;
            my $Scope         = $3;
            my $AppliesTo     = $4;

            # {{{ Create an object called Principal
            # so we can do rights operations

            if ( $PrincipalType eq 'User' ) {
                $Principal = new RT::User( $session{'CurrentUser'} );
            }
            elsif ( $PrincipalType eq 'Group' ) {
                $Principal = new RT::Group( $session{'CurrentUser'} );
            }
            else {
                Abort("$PrincipalType unknown principal type");
            }

            $Principal->Load($PrincipalId)
              || Abort("$PrincipalType $PrincipalId couldn't be loaded");

            # }}}

            # {{{ load up an RT::ACL object with the same current vals of this ACL

            my $CurrentACL = new RT::ACL( $session{'CurrentUser'} );
            if ( $Scope eq 'Queue' ) {
                $CurrentACL->LimitToQueue($AppliesTo);
            }
            elsif ( $Scope eq 'System' ) {
                $CurrentACL->LimitToSystem();
            }

            $CurrentACL->LimitPrincipalToType($PrincipalType);
            $CurrentACL->LimitPrincipalToId($PrincipalId);

            # }}}

            # {{{ Get the values of the select we're working with 
            # into an array. it will contain all the new rights that have 
            # been granted
            #Hack to turn the ACL returned into an array
            my @rights =
              ref( $ARGS{"GrantACE-$ACL"} ) eq 'ARRAY'
              ? @{ $ARGS{"GrantACE-$ACL"} }
              : ( $ARGS{"GrantACE-$ACL"} );

            # }}}

            # {{{ Add any rights we need.

            foreach my $right (@rights) {
                next unless ($right);

                #if the right that's been selected wasn't there before, add it.
                unless (
                    $CurrentACL->HasEntry(
                        RightScope     => "$Scope",
                        RightName      => "$right",
                        RightAppliesTo => "$AppliesTo",
                        PrincipalType  => $PrincipalType,
                        PrincipalId    => $Principal->Id
                    )
                  )
                {

                    #Add new entry to list of rights.
                    if ( $Scope eq 'Queue' ) {
                        my $Queue = new RT::Queue( $session{'CurrentUser'} );
                        $Queue->Load($AppliesTo);
                        unless ( $Queue->id ) {
                            Abort("Couldn't find a queue called $AppliesTo");
                        }

                        my ( $val, $msg ) = $Principal->GrantQueueRight(
                            RightAppliesTo => $Queue->id,
                            RightName      => "$right"
                        );

                        if ($val) {
                            push ( @results,
                                "Granted right $right to "
                                  . $Principal->Name
                                  . " for queue "
                                  . $Queue->Name );
                        }
                        else {
                            push ( @results, $msg );
                        }
                    }
                    elsif ( $Scope eq 'System' ) {
                        my ( $val, $msg ) = $Principal->GrantSystemRight(
                            RightAppliesTo => $AppliesTo,
                            RightName      => "$right"
                        );
                        if ($val) {
                            push ( @results, "Granted system right '$right' to "
                                  . $Principal->Name );
                        }
                        else {
                            push ( @results, $msg );
                        }
                    }
                }
            }

            # }}}
        }
    }

    # }}} Add rights

    # {{{ remove any rights that have been deleted

    my @RevokeACE =
      ref( $ARGS{"RevokeACE"} ) eq 'ARRAY' 
      ? @{ $ARGS{"RevokeACE"} }
      : ( $ARGS{"RevokeACE"} );

    foreach my $aceid (@RevokeACE) {

        my $right = new RT::ACE( $session{'CurrentUser'} );
        $right->Load($aceid);
        next unless ( $right->id );

        my $phrase = "Revoked "
          . $right->PrincipalType . " "
          . $right->PrincipalObj->Name
          . "'s right to "
          . $right->RightName;

        if ( $right->RightScope eq 'System' ) {
            $phrase .= ' across all queues.';
        }
        else {
            $phrase .= ' for the queue ' . $right->AppliesToObj->Name . '.';
        }
        my ( $val, $msg ) = $right->Delete();
        if ($val) {
            push ( @results, $phrase );
        }
        else {
            push ( @results, $msg );
        }
    }

    # }}}

    return (@results);
}

# }}}

# {{{ sub UpdateRecordObj

=head2 UpdateRecordObj ( ARGSRef => \%ARGS, Object => RT::Record, AttributesRef => \@attribs)

@attribs is a list of ticket fields to check and update if they differ from the  B<Object>'s current values. ARGSRef is a ref to HTML::Mason's %ARGS.

Returns an array of success/failure messages

=cut

sub UpdateRecordObject {
    my %args = (
        ARGSRef       => undef,
        AttributesRef => undef,
        Object        => undef,
        @_
    );

    my (@results);

    my $object     = $args{'Object'};
    my $attributes = $args{'AttributesRef'};
    my $ARGSRef    = $args{'ARGSRef'};

    foreach $attribute (@$attributes) {
        if ( ( defined $ARGSRef->{"$attribute"} )
            and ( $ARGSRef->{"$attribute"} ne $object->$attribute() ) )
        {
            $ARGSRef->{"$attribute"} =~ s/\r\n/\n/gs;

            my $method = "Set$attribute";
            my ( $code, $msg ) = $object->$method( $ARGSRef->{"$attribute"} );
            push @results, "$attribute: $msg";
        }
    }
    return (@results);
}

# }}}

# {{{ sub ProcessTicketBasics

=head2 ProcessTicketBasics ( TicketObj => $Ticket, ARGSRef => \%ARGS );

Returns an array of results messages.

=cut

sub ProcessTicketBasics {

    my %args = (
        TicketObj => undef,
        ARGSRef   => undef,
        @_
    );

    my $TicketObj = $args{'TicketObj'};
    my $ARGSRef   = $args{'ARGSRef'};

    # {{{ Set basic fields 
    my @attribs = qw(
      Subject
      FinalPriority
      Priority
      TimeWorked
      TimeLeft
      Status
      Queue
    );

    if ( $ARGSRef->{'Queue'} and ( $ARGSRef->{'Queue'} !~ /^(\d+)$/ ) ) {
        my $tempqueue = RT::Queue->new($RT::SystemUser);
        $tempqueue->Load( $ARGSRef->{'Queue'} );
        if ( $tempqueue->id ) {
            $ARGSRef->{'Queue'} = $tempqueue->Id();
        }
    }

    my @results = UpdateRecordObject(
        AttributesRef => \@attribs,
        Object        => $TicketObj,
        ARGSRef       => $ARGSRef
    );

    # We special case owner changing, so we can use ForceOwnerChange
    if ( $ARGSRef->{'Owner'} && ( $TicketObj->Owner ne $ARGSRef->{'Owner'} ) ) {
        my ($ChownType);
        if ( $ARGSRef->{'ForceOwnerChange'} ) {
            $ChownType = "Force";
        }
        else {
            $ChownType = "Give";
        }

        my ( $val, $msg ) =
          $TicketObj->SetOwner( $ARGSRef->{'Owner'}, $ChownType );
        push ( @results, "$msg" );
    }

    # }}}

    return (@results);
}

# }}}

# {{{ sub ProcessTicketWatchers

=head2 ProcessTicketWatchers ( TicketObj => $Ticket, ARGSRef => \%ARGS );

Returns an array of results messages.

=cut

sub ProcessTicketWatchers {
    my %args = (
        TicketObj => undef,
        ARGSRef   => undef,
        @_
    );
    my (@results);

    my $Ticket  = $args{'TicketObj'};
    my $ARGSRef = $args{'ARGSRef'};

    # {{{ Munge watchers

    foreach my $key ( keys %$ARGSRef ) {

        # Delete deletable watchers
        if ( ( $key =~ /^DelWatcher(\d*)$/ ) and ( $ARGSRef->{$key} ) ) {
            my ( $code, $msg ) = $Ticket->DeleteWatcher($1);
            push @results, $msg;
        }

        # Delete watchers in the simple style demanded by the bulk manipulator
        elsif ( $key =~ /^Delete(Requestor|Cc|AdminCc)$/ ) {
            my ( $code, $msg ) = $Ticket->DeleteWatcher( $ARGSRef->{$key}, $1 );
            push @results, $msg;
        }

        # Add new wathchers by email address      
        elsif ( ( $ARGSRef->{$key} =~ /^(AdminCc|Cc|Requestor)$/ )
            and ( $key =~ /^WatcherTypeEmail(\d*)$/ ) )
        {

            #They're in this order because otherwise $1 gets clobbered :/
            my ( $code, $msg ) = $Ticket->AddWatcher(
                Type  => $ARGSRef->{$key},
                Email => $ARGSRef->{ "WatcherAddressEmail" . $1 }
            );
            push @results, $msg;
        }

        #Add requestors in the simple style demanded by the bulk manipulator
        elsif ( $key =~ /^Add(Requestor|Cc|AdminCc)$/ ) {
            my ( $code, $msg ) = $Ticket->AddWatcher(
                Type  => $1,
                Email => $ARGSRef->{$key}
            );
            push @results, $msg;
        }

        # Add new  watchers by owner
        elsif ( ( $ARGSRef->{$key} =~ /^(AdminCc|Cc|Requestor)$/ )
            and ( $key =~ /^WatcherTypeUser(\d*)$/ ) )
        {

            #They're in this order because otherwise $1 gets clobbered :/
            my ( $code, $msg ) =
              $Ticket->AddWatcher( Type => $ARGSRef->{$key}, Owner => $1 );
            push @results, $msg;
        }
    }

    # }}}

    return (@results);
}

# }}}

# {{{ sub ProcessTicketDates

=head2 ProcessTicketDates ( TicketObj => $Ticket, ARGSRef => \%ARGS );

Returns an array of results messages.

=cut

sub ProcessTicketDates {
    my %args = (
        TicketObj => undef,
        ARGSRef   => undef,
        @_
    );

    my $Ticket  = $args{'TicketObj'};
    my $ARGSRef = $args{'ARGSRef'};

    my (@results);

    # {{{ Set date fields
    my @date_fields = qw(
      Told
      Resolved
      Starts
      Started
      Due
    );

    #Run through each field in this list. update the value if apropriate
    foreach $field (@date_fields) {
        my ( $code, $msg );

        my $DateObj = RT::Date->new( $session{'CurrentUser'} );

        #If it's something other than just whitespace
        if ( $ARGSRef->{ $field . '_Date' } ne '' ) {
            $DateObj->Set(
                Format => 'unknown',
                Value  => $ARGSRef->{ $field . '_Date' }
            );
            my $obj = $field . "Obj";
            if ( ( defined $DateObj->Unix )
                and ( $DateObj->Unix ne $Ticket->$obj()->Unix() ) )
            {
                my $method = "Set$field";
                my ( $code, $msg ) = $Ticket->$method( $DateObj->ISO );
                push @results, "$msg";
            }
        }
    }

    # }}}
    return (@results);
}

# }}}

# {{{ sub ProcessTicketLinks

=head2 ProcessTicketLinks ( TicketObj => $Ticket, ARGSRef => \%ARGS );

Returns an array of results messages.

=cut

sub ProcessTicketLinks {
    my %args = (
        TicketObj => undef,
        ARGSRef   => undef,
        @_
    );

    my $Ticket  = $args{'TicketObj'};
    my $ARGSRef = $args{'ARGSRef'};

    my (@results);

    # Delete links that are gone gone gone.
    foreach my $arg ( keys %$ARGSRef ) {
        if ( $arg =~ /DeleteLink-(.*?)-(DependsOn|MemberOf|RefersTo)-(.*)$/ ) {
            my $base   = $1;
            my $type   = $2;
            my $target = $3;

            push @results,
              "Trying to delete: Base: $base Target: $target  Type $type";
            my ( $val, $msg ) = $Ticket->DeleteLink(
                Base   => $base,
                Type   => $type,
                Target => $target
            );

            push @results, $msg;

        }

    }

    my @linktypes = qw( DependsOn MemberOf RefersTo );

    foreach my $linktype (@linktypes) {

        for my $luri ( split ( / /, $ARGSRef->{ $Ticket->Id . "-$linktype" } ) )
        {
            $luri =~ s/\s*$//;    # Strip trailing whitespace
            my ( $val, $msg ) = $Ticket->AddLink(
                Target => $luri,
                Type   => $linktype
            );
            push @results, $msg;
        }

        for my $luri ( split ( / /, $ARGSRef->{ "$linktype-" . $Ticket->Id } ) )
        {
            my ( $val, $msg ) = $Ticket->AddLink(
                Base => $luri,
                Type => $linktype
            );

            push @results, $msg;
        }
    }

    #Merge if we need to
    if ( $ARGSRef->{ $Ticket->Id . "-MergeInto" } ) {
        my ( $val, $msg ) =
          $Ticket->MergeInto( $ARGSRef->{ $Ticket->Id . "-MergeInto" } );
        push @results, $msg;
    }

    return (@results);
}

# }}}

# {{{ sub ProcessTicketObjectKeywords

=head2 ProcessTicketObjectKeywords ( TicketObj => $Ticket, ARGSRef => \%ARGS );

Returns an array of results messages.

=cut

sub ProcessTicketObjectKeywords {
    my %args = (
        TicketObj => undef,
        ARGSRef   => undef,
        @_
    );

    my $TicketObj = $args{'TicketObj'};
    my $ARGSRef   = $args{'ARGSRef'};

    my (@results);

    # {{{ set ObjectKeywords.

    my $KeywordSelects = $TicketObj->QueueObj->KeywordSelects;

    # iterate through all the keyword selects for this queue
    while ( my $KeywordSelect = $KeywordSelects->Next ) {

        # {{{ do some setup

        # if we have KeywordSelectMagic for this keywordselect:
        next
          unless
          defined $ARGSRef->{ 'KeywordSelectMagic' . $KeywordSelect->id };

        # Lets get a hash of the possible values to work with
        my $value = $ARGSRef->{ 'KeywordSelect' . $KeywordSelect->id } || [];

        #lets get all those values in a hash. regardless of # of entries
        #we'll use this for adding and deleting keywords from this object.
        my %values = map { $_ => 1 } ref($value) ? @{$value} : ($value);

        # Load up the ObjectKeywords for this KeywordSelect for this ticket
        my $ObjectKeys = $TicketObj->KeywordsObj( $KeywordSelect->id );

        # }}}
        # {{{ add new keywords

        foreach my $key ( keys %values ) {

            #unless the ticket has that keyword for that keyword select,
            unless ( $ObjectKeys->HasEntry($key) ) {

                #Add the keyword
                my ( $result, $msg ) = $TicketObj->AddKeyword(
                    Keyword       => $key,
                    KeywordSelect => $KeywordSelect->id
                );
                push ( @results, $msg );
            }
        }

        # }}}
        # {{{ Delete unused keywords

        #redo this search, so we don't ask it to delete things that are already gone
        # such as when a single keyword select gets its value changed.
        $ObjectKeys = $TicketObj->KeywordsObj( $KeywordSelect->id );

        while ( my $TicketKey = $ObjectKeys->Next ) {

            # if the hash defined above doesn\'t contain the keyword mentioned,
            unless ( $values{ $TicketKey->Keyword } ) {

                #I'd really love to just call $keyword->Delete, but then 
                # we wouldn't get a transaction recorded
                my ( $result, $msg ) = $TicketObj->DeleteKeyword(
                    Keyword       => $TicketKey->Keyword,
                    KeywordSelect => $KeywordSelect->id
                );
                push ( @results, $msg );
            }
        }

        # }}}
    }

    #Iterate through the keyword selects for BulkManipulator style access
    while ( my $KeywordSelect = $KeywordSelects->Next ) {
        if ( $ARGSRef->{ "AddToKeywordSelect" . $KeywordSelect->Id } ) {

            #Add the keyword
            my ( $result, $msg ) = $TicketObj->AddKeyword(
                Keyword =>
                $ARGSRef->{ "AddToKeywordSelect" . $KeywordSelect->Id },
                KeywordSelect => $KeywordSelect->id
            );
            push ( @results, $msg );
        }
        if ( $ARGSRef->{ "DeleteFromKeywordSelect" . $KeywordSelect->Id } ) {

            #Delete the keyword
            my ( $result, $msg ) = $TicketObj->DeleteKeyword(
                Keyword =>
                $ARGSRef->{ "DeleteFromKeywordSelect" . $KeywordSelect->Id },
                KeywordSelect => $KeywordSelect->id
            );
            push ( @results, $msg );
        }
    }

    # }}}

    return (@results);
}

# }}}

1;
