+ my @results = $self->_RecordNote(%args);
+
+ unless ( $results[0] ) {
+ $RT::Handle->Rollback();
+ return @results;
+ }
+
+ #Set the last told date to now if this isn't mail from the requestor.
+ #TODO: Note that this will wrongly ack mail from any non-requestor as a "told"
+ unless ( $self->IsRequestor($self->CurrentUser->id) ) {
+ my %squelch;
+ $squelch{$_}++ for map {$_->Content} $self->SquelchMailTo, $results[2]->SquelchMailTo;
+ $self->_SetTold
+ if grep {not $squelch{$_}} $self->Requestors->MemberEmailAddresses;
+ }
+
+ if ($args{'DryRun'}) {
+ $RT::Handle->Rollback();
+ } else {
+ $RT::Handle->Commit();
+ }
+
+ return (@results);
+
+}
+
+
+
+=head2 _RecordNote
+
+the meat of both comment and correspond.
+
+Performs no access control checks. hence, dangerous.
+
+=cut
+
+sub _RecordNote {
+ my $self = shift;
+ my %args = (
+ CcMessageTo => undef,
+ BccMessageTo => undef,
+ Encrypt => undef,
+ Sign => undef,
+ MIMEObj => undef,
+ Content => undef,
+ NoteType => 'Correspond',
+ TimeTaken => 0,
+ CommitScrips => 1,
+ SquelchMailTo => undef,
+ CustomFields => {},
+ @_
+ );
+
+ unless ( $args{'MIMEObj'} || $args{'Content'} ) {
+ return ( 0, $self->loc("No message attached"), undef );
+ }
+
+ unless ( $args{'MIMEObj'} ) {
+ my $data = ref $args{'Content'}? $args{'Content'} : [ $args{'Content'} ];
+ $args{'MIMEObj'} = MIME::Entity->build(
+ Type => "text/plain",
+ Charset => "UTF-8",
+ Data => [ map {Encode::encode("UTF-8", $_)} @{$data} ],
+ );
+ }
+
+ $args{'MIMEObj'}->head->replace('X-RT-Interface' => 'API')
+ unless $args{'MIMEObj'}->head->get('X-RT-Interface');
+
+ # convert text parts into utf-8
+ RT::I18N::SetMIMEEntityToUTF8( $args{'MIMEObj'} );
+
+ # If we've been passed in CcMessageTo and BccMessageTo fields,
+ # add them to the mime object for passing on to the transaction handler
+ # The "NotifyOtherRecipients" scripAction will look for RT-Send-Cc: and
+ # RT-Send-Bcc: headers
+
+
+ foreach my $type (qw/Cc Bcc/) {
+ if ( defined $args{ $type . 'MessageTo' } ) {
+
+ my $addresses = join ', ', (
+ map { RT::User->CanonicalizeEmailAddress( $_->address ) }
+ Email::Address->parse( $args{ $type . 'MessageTo' } ) );
+ $args{'MIMEObj'}->head->replace( 'RT-Send-' . $type, Encode::encode( "UTF-8", $addresses ) );
+ }
+ }
+
+ foreach my $argument (qw(Encrypt Sign)) {
+ $args{'MIMEObj'}->head->replace(
+ "X-RT-$argument" => $args{ $argument } ? 1 : 0
+ ) if defined $args{ $argument };
+ }
+
+ # If this is from an external source, we need to come up with its
+ # internal Message-ID now, so all emails sent because of this
+ # message have a common Message-ID
+ my $org = RT->Config->Get('Organization');
+ my $msgid = Encode::decode( "UTF-8", $args{'MIMEObj'}->head->get('Message-ID') );
+ unless (defined $msgid && $msgid =~ /<(rt-.*?-\d+-\d+)\.(\d+-0-0)\@\Q$org\E>/) {
+ $args{'MIMEObj'}->head->replace(
+ 'RT-Message-ID' => Encode::encode( "UTF-8",
+ RT::Interface::Email::GenMessageId( Ticket => $self )
+ )
+ );
+ }
+
+ #Record the correspondence (write the transaction)
+ my ( $Trans, $msg, $TransObj ) = $self->_NewTransaction(
+ Type => $args{'NoteType'},
+ Data => ( Encode::decode( "UTF-8", $args{'MIMEObj'}->head->get('Subject') ) || 'No Subject' ),
+ TimeTaken => $args{'TimeTaken'},
+ MIMEObj => $args{'MIMEObj'},
+ CommitScrips => $args{'CommitScrips'},
+ SquelchMailTo => $args{'SquelchMailTo'},
+ CustomFields => $args{'CustomFields'},
+ );
+
+ unless ($Trans) {
+ $RT::Logger->err("$self couldn't init a transaction $msg");
+ return ( $Trans, $self->loc("Message could not be recorded"), undef );
+ }
+
+ if ($args{NoteType} eq "Comment") {
+ $msg = $self->loc("Comments added");
+ } else {
+ $msg = $self->loc("Correspondence added");
+ }
+ return ( $Trans, $msg, $TransObj );
+}
+
+
+=head2 DryRun
+
+Builds a MIME object from the given C<UpdateSubject> and
+C<UpdateContent>, then calls L</Comment> or L</Correspond> with
+C<< DryRun => 1 >>, and returns the transaction so produced.
+
+=cut
+
+sub DryRun {
+ my $self = shift;
+ my %args = @_;
+ my $action;
+ if (($args{'UpdateType'} || $args{Action}) =~ /^respon(d|se)$/i ) {
+ $action = 'Correspond';
+ } else {
+ $action = 'Comment';
+ }
+
+ my $Message = MIME::Entity->build(
+ Subject => defined $args{UpdateSubject} ? Encode::encode( "UTF-8", $args{UpdateSubject} ) : "",
+ Type => 'text/plain',
+ Charset => 'UTF-8',
+ Data => Encode::encode("UTF-8", $args{'UpdateContent'} || ""),
+ );
+
+ my ( $Transaction, $Description, $Object ) = $self->$action(
+ CcMessageTo => $args{'UpdateCc'},
+ BccMessageTo => $args{'UpdateBcc'},
+ MIMEObj => $Message,
+ TimeTaken => $args{'UpdateTimeWorked'},
+ DryRun => 1,
+ SquelchMailTo => $args{'SquelchMailTo'},
+ );
+ unless ( $Transaction ) {
+ $RT::Logger->error("Couldn't fire '$action' action: $Description");
+ }
+
+ return $Object;
+}
+
+=head2 DryRunCreate
+
+Prepares a MIME mesage with the given C<Subject>, C<Cc>, and
+C<Content>, then calls L</Create> with C<< DryRun => 1 >> and returns
+the resulting L<RT::Transaction>.
+
+=cut
+
+sub DryRunCreate {
+ my $self = shift;
+ my %args = @_;
+ my $Message = MIME::Entity->build(
+ Subject => defined $args{Subject} ? Encode::encode( "UTF-8", $args{'Subject'} ) : "",
+ (defined $args{'Cc'} ?
+ ( Cc => Encode::encode( "UTF-8", $args{'Cc'} ) ) : ()),
+ Type => 'text/plain',
+ Charset => 'UTF-8',
+ Data => Encode::encode( "UTF-8", $args{'Content'} || ""),
+ );
+
+ my ( $Transaction, $Object, $Description ) = $self->Create(
+ Type => $args{'Type'} || 'ticket',
+ Queue => $args{'Queue'},
+ Owner => $args{'Owner'},
+ Requestor => $args{'Requestors'},
+ Cc => $args{'Cc'},
+ AdminCc => $args{'AdminCc'},
+ InitialPriority => $args{'InitialPriority'},
+ FinalPriority => $args{'FinalPriority'},
+ TimeLeft => $args{'TimeLeft'},
+ TimeEstimated => $args{'TimeEstimated'},
+ TimeWorked => $args{'TimeWorked'},
+ Subject => $args{'Subject'},
+ Status => $args{'Status'},
+ MIMEObj => $Message,
+ DryRun => 1,
+ );
+ unless ( $Transaction ) {
+ $RT::Logger->error("Couldn't fire Create action: $Description");
+ }
+
+ return $Object;
+}
+
+
+
+sub _Links {
+ my $self = shift;
+
+ #TODO: Field isn't the right thing here. but I ahave no idea what mnemonic ---
+ #tobias meant by $f
+ my $field = shift;
+ my $type = shift || "";
+
+ my $cache_key = "$field$type";
+ return $self->{ $cache_key } if $self->{ $cache_key };
+
+ my $links = $self->{ $cache_key }
+ = RT::Links->new( $self->CurrentUser );
+ unless ( $self->CurrentUserHasRight('ShowTicket') ) {
+ $links->Limit( FIELD => 'id', VALUE => 0, SUBCLAUSE => 'acl' );
+ return $links;
+ }
+
+ # Maybe this ticket is a merge ticket
+ my $limit_on = 'Local'. $field;
+ # at least to myself
+ $links->Limit(
+ FIELD => $limit_on,
+ OPERATOR => 'IN',
+ VALUE => [ $self->id, $self->Merged ],
+ );
+ $links->Limit(
+ FIELD => 'Type',
+ VALUE => $type,
+ ) if $type;
+
+ return $links;
+}
+
+=head2 MergeInto
+
+MergeInto take the id of the ticket to merge this ticket into.
+
+=cut
+
+sub MergeInto {
+ my $self = shift;
+ my $ticket_id = shift;
+
+ unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
+ return ( 0, $self->loc("Permission Denied") );
+ }
+
+ # Load up the new ticket.
+ my $MergeInto = RT::Ticket->new($self->CurrentUser);
+ $MergeInto->Load($ticket_id);
+
+ # make sure it exists.
+ unless ( $MergeInto->Id ) {
+ return ( 0, $self->loc("New ticket doesn't exist") );
+ }
+
+ # Can't merge into yourself
+ if ( $MergeInto->Id == $self->Id ) {
+ return ( 0, $self->loc("Can't merge a ticket into itself") );
+ }
+
+ # Make sure the current user can modify the new ticket.
+ unless ( $MergeInto->CurrentUserHasRight('ModifyTicket') ) {
+ return ( 0, $self->loc("Permission Denied") );
+ }
+
+ delete $MERGE_CACHE{'effective'}{ $self->id };
+ delete @{ $MERGE_CACHE{'merged'} }{
+ $ticket_id, $MergeInto->id, $self->id
+ };
+
+ $RT::Handle->BeginTransaction();
+
+ my ($ok, $msg) = $self->_MergeInto( $MergeInto );
+
+ $RT::Handle->Commit() if $ok;
+
+ return ($ok, $msg);
+}
+
+sub _MergeInto {
+ my $self = shift;
+ my $MergeInto = shift;
+
+
+ # We use EffectiveId here even though it duplicates information from
+ # the links table becasue of the massive performance hit we'd take
+ # by trying to do a separate database query for merge info everytime
+ # loaded a ticket.
+
+ #update this ticket's effective id to the new ticket's id.
+ my ( $id_val, $id_msg ) = $self->__Set(
+ Field => 'EffectiveId',
+ Value => $MergeInto->Id()
+ );
+
+ unless ($id_val) {
+ $RT::Handle->Rollback();
+ return ( 0, $self->loc("Merge failed. Couldn't set EffectiveId") );
+ }
+
+ ( $id_val, $id_msg ) = $self->__Set( Field => 'IsMerged', Value => 1 );
+ unless ($id_val) {
+ $RT::Handle->Rollback();
+ return ( 0, $self->loc("Merge failed. Couldn't set IsMerged") );
+ }
+
+ my $force_status = $self->LifecycleObj->DefaultOnMerge;
+ if ( $force_status && $force_status ne $self->__Value('Status') ) {
+ my ( $status_val, $status_msg )
+ = $self->__Set( Field => 'Status', Value => $force_status );
+
+ unless ($status_val) {
+ $RT::Handle->Rollback();
+ $RT::Logger->error(
+ "Couldn't set status to $force_status. RT's Database may be inconsistent."
+ );
+ return ( 0, $self->loc("Merge failed. Couldn't set Status") );
+ }
+ }
+
+ # update all the links that point to that old ticket
+ my $old_links_to = RT::Links->new($self->CurrentUser);
+ $old_links_to->Limit(FIELD => 'Target', VALUE => $self->URI);
+
+ my %old_seen;
+ while (my $link = $old_links_to->Next) {
+ if (exists $old_seen{$link->Base."-".$link->Type}) {
+ $link->Delete;
+ }
+ elsif ($link->Base eq $MergeInto->URI) {
+ $link->Delete;
+ } else {
+ # First, make sure the link doesn't already exist. then move it over.
+ my $tmp = RT::Link->new(RT->SystemUser);
+ $tmp->LoadByCols(Base => $link->Base, Type => $link->Type, LocalTarget => $MergeInto->id);
+ if ($tmp->id) {
+ $link->Delete;
+ } else {
+ $link->SetTarget($MergeInto->URI);
+ $link->SetLocalTarget($MergeInto->id);
+ }
+ $old_seen{$link->Base."-".$link->Type} =1;
+ }
+
+ }
+
+ my $old_links_from = RT::Links->new($self->CurrentUser);
+ $old_links_from->Limit(FIELD => 'Base', VALUE => $self->URI);
+
+ while (my $link = $old_links_from->Next) {
+ if (exists $old_seen{$link->Type."-".$link->Target}) {
+ $link->Delete;
+ }
+ if ($link->Target eq $MergeInto->URI) {
+ $link->Delete;
+ } else {
+ # First, make sure the link doesn't already exist. then move it over.
+ my $tmp = RT::Link->new(RT->SystemUser);
+ $tmp->LoadByCols(Target => $link->Target, Type => $link->Type, LocalBase => $MergeInto->id);
+ if ($tmp->id) {
+ $link->Delete;
+ } else {
+ $link->SetBase($MergeInto->URI);
+ $link->SetLocalBase($MergeInto->id);
+ $old_seen{$link->Type."-".$link->Target} =1;
+ }
+ }
+
+ }
+
+ # Update time fields
+ foreach my $type (qw(TimeEstimated TimeWorked TimeLeft)) {
+ $MergeInto->_Set(
+ Field => $type,
+ Value => ( $MergeInto->$type() || 0 ) + ( $self->$type() || 0 ),
+ RecordTransaction => 0,
+ );
+ }
+
+ # add all of this ticket's watchers to that ticket.
+ for my $role ($self->Roles) {
+ next if $self->RoleGroup($role)->SingleMemberRoleGroup;
+ my $people = $self->RoleGroup($role)->MembersObj;
+ while ( my $watcher = $people->Next ) {
+ my ($val, $msg) = $MergeInto->AddRoleMember(
+ Type => $role,
+ Silent => 1,
+ PrincipalId => $watcher->MemberId,
+ InsideTransaction => 1,
+ );
+ unless ($val) {
+ $RT::Logger->debug($msg);
+ }
+ }
+ }
+
+ #find all of the tickets that were merged into this ticket.
+ my $old_mergees = RT::Tickets->new( $self->CurrentUser );
+ $old_mergees->Limit(
+ FIELD => 'EffectiveId',
+ OPERATOR => '=',
+ VALUE => $self->Id
+ );
+
+ # update their EffectiveId fields to the new ticket's id
+ while ( my $ticket = $old_mergees->Next() ) {
+ my ( $val, $msg ) = $ticket->__Set(
+ Field => 'EffectiveId',
+ Value => $MergeInto->Id()
+ );
+ }
+
+ #make a new link: this ticket is merged into that other ticket.
+ $self->AddLink( Type => 'MergedInto', Target => $MergeInto->Id());
+
+ $MergeInto->_SetLastUpdated;
+
+ return ( 1, $self->loc("Merge Successful") );
+}
+
+=head2 Merged
+
+Returns list of tickets' ids that's been merged into this ticket.
+
+=cut
+
+sub Merged {
+ my $self = shift;
+
+ my $id = $self->id;
+ return @{ $MERGE_CACHE{'merged'}{ $id } }
+ if $MERGE_CACHE{'merged'}{ $id };
+
+ my $mergees = RT::Tickets->new( $self->CurrentUser );
+ $mergees->LimitField(
+ FIELD => 'EffectiveId',
+ VALUE => $id,
+ );
+ $mergees->LimitField(
+ FIELD => 'id',
+ OPERATOR => '!=',
+ VALUE => $id,
+ );
+ return @{ $MERGE_CACHE{'merged'}{ $id } ||= [] }
+ = map $_->id, @{ $mergees->ItemsArrayRef || [] };
+}
+
+
+
+
+
+=head2 OwnerObj
+
+Takes nothing and returns an RT::User object of
+this ticket's owner
+
+=cut
+
+sub OwnerObj {
+ my $self = shift;
+
+ #If this gets ACLed, we lose on a rights check in User.pm and
+ #get deep recursion. if we need ACLs here, we need
+ #an equiv without ACLs
+
+ my $owner = RT::User->new( $self->CurrentUser );
+ $owner->Load( $self->__Value('Owner') );
+
+ #Return the owner object
+ return ($owner);
+}
+
+
+
+=head2 OwnerAsString
+
+Returns the owner's email address
+
+=cut
+
+sub OwnerAsString {
+ my $self = shift;
+ return ( $self->OwnerObj->EmailAddress );
+
+}
+
+
+
+=head2 SetOwner
+
+Takes two arguments:
+ the Id or Name of the owner
+and (optionally) the type of the SetOwner Transaction. It defaults
+to 'Set'. 'Steal' is also a valid option.
+
+
+=cut
+
+sub SetOwner {
+ my $self = shift;
+ my $NewOwner = shift;
+ my $Type = shift || "Set";
+
+ $RT::Handle->BeginTransaction();
+
+ $self->_SetLastUpdated(); # lock the ticket
+ $self->Load( $self->id ); # in case $self changed while waiting for lock
+
+ my $OldOwnerObj = $self->OwnerObj;
+
+ my $NewOwnerObj = RT::User->new( $self->CurrentUser );
+ $NewOwnerObj->Load( $NewOwner );
+
+ my ( $val, $msg ) = $self->CurrentUserCanSetOwner(
+ NewOwnerObj => $NewOwnerObj,
+ Type => $Type );
+
+ unless ($val) {
+ $RT::Handle->Rollback();
+ return ( $val, $msg );
+ }
+
+ ($val, $msg ) = $self->OwnerGroup->_AddMember(
+ PrincipalId => $NewOwnerObj->PrincipalId,
+ InsideTransaction => 1,
+ Object => $self,
+ );
+ unless ($val) {
+ $RT::Handle->Rollback;
+ return ( 0, $self->loc("Could not change owner: [_1]", $msg) );
+ }
+
+ $msg = $self->loc( "Owner changed from [_1] to [_2]",
+ $OldOwnerObj->Name, $NewOwnerObj->Name );
+
+ $RT::Handle->Commit();
+
+ return ( $val, $msg );
+}
+
+=head2 CurrentUserCanSetOwner
+
+Confirm the current user can set the owner of the current ticket.
+
+There are several different rights to manage owner changes and
+this method evaluates these rights, guided by parameters provided.
+
+This method evaluates these rights in the context of the state of
+the current ticket. For example, it evaluates Take for tickets that
+are owned by Nobody because that is the context appropriate for the
+TakeTicket right. If you need to strictly test a user for a right,
+use HasRight to check for the right directly.
+
+For some custom types of owner changes (C<Take> and C<Steal>), it also
+verifies that those actions are possible given the current ticket owner.
+
+=head3 Rights to Set Owner
+
+The current user can set or change the Owner field in the following
+cases:
+
+=over
+
+=item *
+
+ReassignTicket unconditionally grants the right to set the owner
+to any user who has OwnTicket. This can be used to break an
+Owner lock held by another user (see below) and can be a convenient
+right for managers or administrators who need to assign tickets
+without necessarily owning them.
+
+=item *
+
+ModifyTicket grants the right to set the owner to any user who
+has OwnTicket, provided the ticket is currently owned by the current
+user or is not owned (owned by Nobody). (See the details on the Force
+parameter below for exceptions to this.)
+
+=item *
+
+If the ticket is currently not owned (owned by Nobody),
+TakeTicket is sufficient to set the owner to yourself (but not
+an arbitrary person), but only if you have OwnTicket. It is
+thus a subset of the possible changes provided by ModifyTicket.
+This exists to allow granting TakeTicket freely, and
+the broader ModifyTicket only to Owners.
+
+=item *
+
+If the ticket is currently owned by someone who is not you or
+Nobody, StealTicket is sufficient to set the owner to yourself,
+but only if you have OwnTicket. This is hence non-overlapping
+with the changes provided by ModifyTicket, and is used to break
+a lock held by another user.
+
+=back
+
+=head3 Parameters
+
+This method returns ($result, $message) with $result containing
+true or false indicating if the current user can set owner and $message
+containing a message, typically in the case of a false response.
+
+If called with no parameters, this method determines if the current
+user could set the owner of the current ticket given any
+permutation of the rights described above. This can be useful
+when determining whether to make owner-setting options available
+in the GUI.
+
+This method accepts the following parameters as a paramshash:
+
+=over
+
+=item C<NewOwnerObj>
+
+Optional; an L<RT::User> object representing the proposed new owner of
+the ticket.
+
+=item C<Type>
+
+Optional; the type of set owner operation. Valid values are C<Take>,
+C<Steal>, or C<Force>. Note that if the type is C<Take>, this method
+will return false if the current user is already the owner; similarly,
+it will return false for C<Steal> if the ticket has no owner or the
+owner is the current user.
+
+=back
+
+As noted above, there are exceptions to the standard ticket-based rights
+described here. The Force option allows for these and is used
+when moving tickets between queues, for reminders (because the full
+owner rights system is too complex for them), and optionally during
+bulk update.
+
+=cut
+
+sub CurrentUserCanSetOwner {
+ my $self = shift;
+ my %args = ( Type => '',
+ @_);
+ my $OldOwnerObj = $self->OwnerObj;
+
+ $args{NewOwnerObj} ||= $self->CurrentUser->UserObj
+ if $args{Type} eq "Take" or $args{Type} eq "Steal";
+
+ # Confirm rights for new owner if we got one
+ if ( $args{'NewOwnerObj'} ){
+ my ($ok, $message) = $self->_NewOwnerCanOwnTicket($args{'NewOwnerObj'}, $OldOwnerObj);
+ return ($ok, $message) if not $ok;
+ }
+
+ # ReassignTicket allows you to SetOwner, but we also need to check ticket's
+ # current owner for Take and Steal Types
+ return ( 1, undef ) if $self->CurrentUserHasRight('ReassignTicket')
+ && $args{Type} ne 'Take' && $args{Type} ne 'Steal';
+
+ # Ticket is unowned
+ if ( $OldOwnerObj->Id == RT->Nobody->Id ) {
+
+ # Steal is not applicable for unowned tickets.
+ if ( $args{'Type'} eq 'Steal' ){
+ return ( 0, $self->loc("You can only steal a ticket owned by someone else") )
+ }
+
+ # Can set owner to yourself with ModifyTicket, ReassignTicket,
+ # or TakeTicket; in all of these cases, OwnTicket is checked by
+ # _NewOwnerCanOwnTicket above.
+ if ( $args{'Type'} eq 'Take'
+ or ( $args{'NewOwnerObj'}
+ and $args{'NewOwnerObj'}->id == $self->CurrentUser->id )) {
+ unless ( $self->CurrentUserHasRight('ModifyTicket')
+ or $self->CurrentUserHasRight('ReassignTicket')
+ or $self->CurrentUserHasRight('TakeTicket') ) {
+ return ( 0, $self->loc("Permission Denied") );
+ }
+ } else {
+ # Nobody -> someone else requires ModifyTicket or ReassignTicket
+ unless ( $self->CurrentUserHasRight('ModifyTicket')
+ or $self->CurrentUserHasRight('ReassignTicket') ) {
+ return ( 0, $self->loc("Permission Denied") );
+ }
+ }
+ }
+
+ # Ticket is owned by someone else
+ # Can set owner to yourself with ModifyTicket or StealTicket
+ # and OwnTicket.
+ elsif ( $OldOwnerObj->Id != RT->Nobody->Id
+ && $OldOwnerObj->Id != $self->CurrentUser->id ) {
+
+ unless ( $self->CurrentUserHasRight('ModifyTicket')
+ || $self->CurrentUserHasRight('ReassignTicket')
+ || $self->CurrentUserHasRight('StealTicket') ) {
+ return ( 0, $self->loc("Permission Denied") )
+ }
+
+ if ( $args{'Type'} eq 'Steal' || $args{'Type'} eq 'Force' ){
+ return ( 1, undef ) if $self->CurrentUserHasRight('OwnTicket');
+ return ( 0, $self->loc("Permission Denied") );
+ }
+
+ # Not a steal or force
+ if ( $args{'Type'} eq 'Take'
+ or ( $args{'NewOwnerObj'}
+ and $args{'NewOwnerObj'}->id == $self->CurrentUser->id )) {
+ return ( 0, $self->loc("You can only take tickets that are unowned") );
+ }
+
+ unless ( $self->CurrentUserHasRight('ReassignTicket') ) {
+ return ( 0, $self->loc( "You can only reassign tickets that you own or that are unowned"));
+ }
+
+ }
+ # You own the ticket
+ # Untake falls through to here, so we don't need to explicitly handle that Type
+ else {
+ if ( $args{'Type'} eq 'Take' || $args{'Type'} eq 'Steal' ) {
+ return ( 0, $self->loc("You already own this ticket") );
+ }
+
+ unless ( $self->CurrentUserHasRight('ModifyTicket')
+ || $self->CurrentUserHasRight('ReassignTicket') ) {
+ return ( 0, $self->loc("Permission Denied") );
+ }
+ }
+
+ return ( 1, undef );
+}
+
+# Verify the proposed new owner can own the ticket.
+
+sub _NewOwnerCanOwnTicket {
+ my $self = shift;
+ my $NewOwnerObj = shift;
+ my $OldOwnerObj = shift;
+
+ unless ( $NewOwnerObj->Id ) {
+ return ( 0, $self->loc("That user does not exist") );
+ }
+
+ # The proposed new owner can't own the ticket
+ if ( !$NewOwnerObj->HasRight( Right => 'OwnTicket', Object => $self ) ){
+ return ( 0, $self->loc("That user may not own tickets in that queue") );
+ }
+
+ # Ticket's current owner is the same as the new owner, nothing to do
+ elsif ( $NewOwnerObj->Id == $OldOwnerObj->Id ) {
+ return ( 0, $self->loc("That user already owns that ticket") );
+ }
+
+ return (1, undef);
+}
+
+=head2 Take
+
+A convenince method to set the ticket's owner to the current user
+
+=cut
+
+sub Take {
+ my $self = shift;
+ return ( $self->SetOwner( $self->CurrentUser->Id, 'Take' ) );
+}
+
+
+
+=head2 Untake
+
+Convenience method to set the owner to 'nobody' if the current user is the owner.
+
+=cut
+
+sub Untake {
+ my $self = shift;
+ return ( $self->SetOwner( RT->Nobody->UserObj->Id, 'Untake' ) );
+}
+
+
+
+=head2 Steal
+
+A convenience method to change the owner of the current ticket to the
+current user. Even if it's owned by another user.
+
+=cut
+
+sub Steal {
+ my $self = shift;
+
+ if ( $self->IsOwner( $self->CurrentUser ) ) {
+ return ( 0, $self->loc("You already own this ticket") );
+ }
+ else {
+ return ( $self->SetOwner( $self->CurrentUser->Id, 'Steal' ) );
+
+ }
+
+}
+
+=head2 SetStatus STATUS
+
+Set this ticket's status.
+
+Alternatively, you can pass in a list of named parameters (Status => STATUS, Force => FORCE, SetStarted => SETSTARTED ).
+If FORCE is true, ignore unresolved dependencies and force a status change.
+if SETSTARTED is true (it's the default value), set Started to current datetime if Started
+is not set and the status is changed from initial to not initial.
+
+=cut
+
+sub SetStatus {
+ my $self = shift;
+ my %args;
+ if (@_ == 1) {
+ $args{Status} = shift;
+ }
+ else {
+ %args = (@_);
+ }
+
+ # this only allows us to SetStarted, not we must SetStarted.
+ # this option was added for rtir initially
+ $args{SetStarted} = 1 unless exists $args{SetStarted};
+
+ my ($valid, $msg) = $self->ValidateStatusChange($args{Status});
+ return ($valid, $msg) unless $valid;
+
+ my $lifecycle = $self->LifecycleObj;
+
+ if ( !$args{Force}
+ && !$lifecycle->IsInactive($self->Status)
+ && $lifecycle->IsInactive($args{Status})
+ && $self->HasUnresolvedDependencies )
+ {
+ return ( 0, $self->loc('That ticket has unresolved dependencies') );
+ }
+
+ return $self->_SetStatus(
+ Status => $args{Status},
+ SetStarted => $args{SetStarted},
+ );
+}
+
+sub _SetStatus {
+ my $self = shift;
+ my %args = (
+ Status => undef,
+ SetStarted => 1,
+ RecordTransaction => 1,
+ Lifecycle => $self->LifecycleObj,
+ @_,
+ );
+ $args{Status} = lc $args{Status} if defined $args{Status};
+ $args{NewLifecycle} ||= $args{Lifecycle};
+
+ my $now = RT::Date->new( $self->CurrentUser );
+ $now->SetToNow();
+
+ my $raw_started = RT::Date->new(RT->SystemUser);
+ $raw_started->Set(Format => 'ISO', Value => $self->__Value('Started'));
+
+ my $old = $self->__Value('Status');
+
+ # If we're changing the status from new, record that we've started
+ if ( $args{SetStarted}
+ && $args{Lifecycle}->IsInitial($old)
+ && !$args{NewLifecycle}->IsInitial($args{Status})
+ && !$raw_started->IsSet) {
+ # Set the Started time to "now"
+ $self->_Set(
+ Field => 'Started',
+ Value => $now->ISO,
+ RecordTransaction => 0
+ );
+ }
+
+ # When we close a ticket, set the 'Resolved' attribute to now.
+ # It's misnamed, but that's just historical.
+ if ( $args{NewLifecycle}->IsInactive($args{Status}) ) {
+ $self->_Set(
+ Field => 'Resolved',
+ Value => $now->ISO,
+ RecordTransaction => 0,
+ );
+ }
+
+ # Actually update the status
+ my ($val, $msg)= $self->_Set(
+ Field => 'Status',
+ Value => $args{Status},
+ TimeTaken => 0,
+ CheckACL => 0,
+ TransactionType => 'Status',
+ RecordTransaction => $args{RecordTransaction},
+ );
+ return ($val, $msg);
+}
+
+sub SetTimeWorked {
+ my $self = shift;
+ my $value = shift;
+
+ my $taken = ($value||0) - ($self->__Value('TimeWorked')||0);
+
+ return $self->_Set(
+ Field => 'TimeWorked',
+ Value => $value,
+ TimeTaken => $taken,
+ );
+}
+
+=head2 Delete
+
+Takes no arguments. Marks this ticket for garbage collection
+
+=cut
+
+sub Delete {
+ my $self = shift;
+ unless ( $self->LifecycleObj->IsValid('deleted') ) {
+ return (0, $self->loc('Delete operation is disabled by lifecycle configuration') ); #loc
+ }
+ return ( $self->SetStatus('deleted') );
+}
+
+
+=head2 SetTold ISO [TIMETAKEN]
+
+Updates the told and records a transaction
+
+=cut
+
+sub SetTold {
+ my $self = shift;
+ my $told;
+ $told = shift if (@_);
+ my $timetaken = shift || 0;
+
+ unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
+ return ( 0, $self->loc("Permission Denied") );