1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2011 Best Practical Solutions, LLC
6 # <sales@bestpractical.com>
8 # (Except where explicitly superseded by other copyright notices)
13 # This work is made available to you under the terms of Version 2 of
14 # the GNU General Public License. A copy of that license should have
15 # been provided with this software, but in any event can be snarfed
18 # This work is distributed in the hope that it will be useful, but
19 # WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
21 # General Public License for more details.
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
26 # 02110-1301 or visit their web page on the internet at
27 # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
30 # CONTRIBUTION SUBMISSION POLICY:
32 # (The following paragraph is not intended to limit the rights granted
33 # to you to modify and distribute this software under the terms of
34 # the GNU General Public License and is only of importance to you if
35 # you choose to contribute your changes and enhancements to the
36 # community by submitting them to Best Practical Solutions, LLC.)
38 # By intentionally submitting any modifications, corrections or
39 # derivatives to this work, or any other work intended for use with
40 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
41 # you are the copyright holder for those contributions and you grant
42 # Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
43 # royalty-free, perpetual, license to use, copy, create derivative
44 # works based on those contributions, and sublicense and distribute
45 # those contributions and any derivatives thereof.
47 # END BPS TAGGED BLOCK }}}
54 my $ticket = new RT::Ticket($CurrentUser);
55 $ticket->Load($ticket_id);
59 This module lets you manipulate RT\'s ticket object.
71 no warnings qw(redefine);
82 use RT::URI::fsck_com_rt;
88 # A helper table for links mapping to make it easier
89 # to build and parse links between tickets
92 MemberOf => { Type => 'MemberOf',
94 Parents => { Type => 'MemberOf',
96 Members => { Type => 'MemberOf',
98 Children => { Type => 'MemberOf',
100 HasMember => { Type => 'MemberOf',
102 RefersTo => { Type => 'RefersTo',
104 ReferredToBy => { Type => 'RefersTo',
106 DependsOn => { Type => 'DependsOn',
108 DependedOnBy => { Type => 'DependsOn',
110 MergedInto => { Type => 'MergedInto',
118 # A helper table for links mapping to make it easier
119 # to build and parse links between tickets
122 MemberOf => { Base => 'MemberOf',
123 Target => 'HasMember', },
124 RefersTo => { Base => 'RefersTo',
125 Target => 'ReferredToBy', },
126 DependsOn => { Base => 'DependsOn',
127 Target => 'DependedOnBy', },
128 MergedInto => { Base => 'MergedInto',
129 Target => 'MergedInto', },
135 sub LINKTYPEMAP { return \%LINKTYPEMAP }
136 sub LINKDIRMAP { return \%LINKDIRMAP }
147 Takes a single argument. This can be a ticket id, ticket alias or
148 local ticket uri. If the ticket can't be loaded, returns undef.
149 Otherwise, returns the ticket id.
156 $id = '' unless defined $id;
158 # TODO: modify this routine to look at EffectiveId and
159 # do the recursive load thing. be careful to cache all
160 # the interim tickets we try so we don't loop forever.
162 # FIXME: there is no TicketBaseURI option in config
163 my $base_uri = RT->Config->Get('TicketBaseURI') || '';
164 #If it's a local URI, turn it into a ticket id
165 if ( $base_uri && $id =~ /^$base_uri(\d+)$/ ) {
169 unless ( $id =~ /^\d+$/ ) {
170 $RT::Logger->debug("Tried to load a bogus ticket id: '$id'");
174 $id = $MERGE_CACHE{'effective'}{ $id }
175 if $MERGE_CACHE{'effective'}{ $id };
177 my ($ticketid, $msg) = $self->LoadById( $id );
178 unless ( $self->Id ) {
179 $RT::Logger->debug("$self tried to load a bogus ticket: $id");
183 #If we're merged, resolve the merge.
184 if ( $self->EffectiveId && $self->EffectiveId != $self->Id ) {
186 "We found a merged ticket. "
187 . $self->id ."/". $self->EffectiveId
189 my $real_id = $self->Load( $self->EffectiveId );
190 $MERGE_CACHE{'effective'}{ $id } = $real_id;
194 #Ok. we're loaded. lets get outa here.
204 Arguments: ARGS is a hash of named parameters. Valid parameters are:
207 Queue - Either a Queue object or a Queue Name
208 Requestor - A reference to a list of email addresses or RT user Names
209 Cc - A reference to a list of email addresses or Names
210 AdminCc - A reference to a list of email addresses or Names
211 SquelchMailTo - A reference to a list of email addresses -
212 who should this ticket not mail
213 Type -- The ticket\'s type. ignore this for now
214 Owner -- This ticket\'s owner. either an RT::User object or this user\'s id
215 Subject -- A string describing the subject of the ticket
216 Priority -- an integer from 0 to 99
217 InitialPriority -- an integer from 0 to 99
218 FinalPriority -- an integer from 0 to 99
219 Status -- any valid status (Defined in RT::Queue)
220 TimeEstimated -- an integer. estimated time for this task in minutes
221 TimeWorked -- an integer. time worked so far in minutes
222 TimeLeft -- an integer. time remaining in minutes
223 Starts -- an ISO date describing the ticket\'s start date and time in GMT
224 Due -- an ISO date describing the ticket\'s due date and time in GMT
225 MIMEObj -- a MIME::Entity object with the content of the initial ticket request.
226 CustomField-<n> -- a scalar or array of values for the customfield with the id <n>
228 Ticket links can be set up during create by passing the link type as a hask key and
229 the ticket id to be linked to as a value (or a URI when linking to other objects).
230 Multiple links of the same type can be created by passing an array ref. For example:
233 DependsOn => [ 15, 22 ],
234 RefersTo => 'http://www.bestpractical.com',
236 Supported link types are C<MemberOf>, C<HasMember>, C<RefersTo>, C<ReferredToBy>,
237 C<DependsOn> and C<DependedOnBy>. Also, C<Parents> is alias for C<MemberOf> and
238 C<Members> and C<Children> are aliases for C<HasMember>.
240 Returns: TICKETID, Transaction Object, Error Message
250 EffectiveId => undef,
255 SquelchMailTo => undef,
259 InitialPriority => undef,
260 FinalPriority => undef,
271 _RecordTransaction => 1,
276 my ($ErrStr, @non_fatal_errors);
278 my $QueueObj = RT::Queue->new( $RT::SystemUser );
279 if ( ref $args{'Queue'} eq 'RT::Queue' ) {
280 $QueueObj->Load( $args{'Queue'}->Id );
282 elsif ( $args{'Queue'} ) {
283 $QueueObj->Load( $args{'Queue'} );
286 $RT::Logger->debug("'". ( $args{'Queue'} ||''). "' not a recognised queue object." );
289 #Can't create a ticket without a queue.
290 unless ( $QueueObj->Id ) {
291 $RT::Logger->debug("$self No queue given for ticket creation.");
292 return ( 0, 0, $self->loc('Could not create ticket. Queue not set') );
296 #Now that we have a queue, Check the ACLS
298 $self->CurrentUser->HasRight(
299 Right => 'CreateTicket',
306 $self->loc( "No permission to create tickets in the queue '[_1]'", $QueueObj->Name));
309 unless ( $QueueObj->IsValidStatus( $args{'Status'} ) ) {
310 return ( 0, 0, $self->loc('Invalid value for status') );
313 #Since we have a queue, we can set queue defaults
316 # If there's no queue default initial priority and it's not set, set it to 0
317 $args{'InitialPriority'} = $QueueObj->InitialPriority || 0
318 unless defined $args{'InitialPriority'};
321 # If there's no queue default final priority and it's not set, set it to 0
322 $args{'FinalPriority'} = $QueueObj->FinalPriority || 0
323 unless defined $args{'FinalPriority'};
325 # Priority may have changed from InitialPriority, for the case
326 # where we're importing tickets (eg, from an older RT version.)
327 $args{'Priority'} = $args{'InitialPriority'}
328 unless defined $args{'Priority'};
331 #TODO we should see what sort of due date we're getting, rather +
332 # than assuming it's in ISO format.
334 #Set the due date. if we didn't get fed one, use the queue default due in
335 my $Due = new RT::Date( $self->CurrentUser );
336 if ( defined $args{'Due'} ) {
337 $Due->Set( Format => 'ISO', Value => $args{'Due'} );
339 elsif ( my $due_in = $QueueObj->DefaultDueIn ) {
341 $Due->AddDays( $due_in );
344 my $Starts = new RT::Date( $self->CurrentUser );
345 if ( defined $args{'Starts'} ) {
346 $Starts->Set( Format => 'ISO', Value => $args{'Starts'} );
349 my $Started = new RT::Date( $self->CurrentUser );
350 if ( defined $args{'Started'} ) {
351 $Started->Set( Format => 'ISO', Value => $args{'Started'} );
353 elsif ( $args{'Status'} ne 'new' ) {
357 my $Resolved = new RT::Date( $self->CurrentUser );
358 if ( defined $args{'Resolved'} ) {
359 $Resolved->Set( Format => 'ISO', Value => $args{'Resolved'} );
362 #If the status is an inactive status, set the resolved date
363 elsif ( $QueueObj->IsInactiveStatus( $args{'Status'} ) )
365 $RT::Logger->debug( "Got a ". $args{'Status'}
366 ."(inactive) ticket with undefined resolved date. Setting to now."
373 # {{{ Dealing with time fields
375 $args{'TimeEstimated'} = 0 unless defined $args{'TimeEstimated'};
376 $args{'TimeWorked'} = 0 unless defined $args{'TimeWorked'};
377 $args{'TimeLeft'} = 0 unless defined $args{'TimeLeft'};
381 # {{{ Deal with setting the owner
384 if ( ref( $args{'Owner'} ) eq 'RT::User' ) {
385 if ( $args{'Owner'}->id ) {
386 $Owner = $args{'Owner'};
388 $RT::Logger->error('passed not loaded owner object');
389 push @non_fatal_errors, $self->loc("Invalid owner object");
394 #If we've been handed something else, try to load the user.
395 elsif ( $args{'Owner'} ) {
396 $Owner = RT::User->new( $self->CurrentUser );
397 $Owner->Load( $args{'Owner'} );
398 $Owner->LoadByEmail( $args{'Owner'} )
400 unless ( $Owner->Id ) {
401 push @non_fatal_errors,
402 $self->loc("Owner could not be set.") . " "
403 . $self->loc( "User '[_1]' could not be found.", $args{'Owner'} );
408 #If we have a proposed owner and they don't have the right
409 #to own a ticket, scream about it and make them not the owner
412 if ( $Owner && $Owner->Id != $RT::Nobody->Id
413 && !$Owner->HasRight( Object => $QueueObj, Right => 'OwnTicket' ) )
415 $DeferOwner = $Owner;
417 $RT::Logger->debug('going to deffer setting owner');
421 #If we haven't been handed a valid owner, make it nobody.
422 unless ( defined($Owner) && $Owner->Id ) {
423 $Owner = new RT::User( $self->CurrentUser );
424 $Owner->Load( $RT::Nobody->Id );
429 # We attempt to load or create each of the people who might have a role for this ticket
430 # _outside_ the transaction, so we don't get into ticket creation races
431 foreach my $type ( "Cc", "AdminCc", "Requestor" ) {
432 $args{ $type } = [ $args{ $type } ] unless ref $args{ $type };
433 foreach my $watcher ( splice @{ $args{$type} } ) {
434 next unless $watcher;
435 if ( $watcher =~ /^\d+$/ ) {
436 push @{ $args{$type} }, $watcher;
438 my @addresses = RT::EmailParser->ParseEmailAddress( $watcher );
439 foreach my $address( @addresses ) {
440 my $user = RT::User->new( $RT::SystemUser );
441 my ($uid, $msg) = $user->LoadOrCreateByEmail( $address );
443 push @non_fatal_errors,
444 $self->loc("Couldn't load or create user: [_1]", $msg);
446 push @{ $args{$type} }, $user->id;
453 $RT::Handle->BeginTransaction();
456 Queue => $QueueObj->Id,
458 Subject => $args{'Subject'},
459 InitialPriority => $args{'InitialPriority'},
460 FinalPriority => $args{'FinalPriority'},
461 Priority => $args{'Priority'},
462 Status => $args{'Status'},
463 TimeWorked => $args{'TimeWorked'},
464 TimeEstimated => $args{'TimeEstimated'},
465 TimeLeft => $args{'TimeLeft'},
466 Type => $args{'Type'},
467 Starts => $Starts->ISO,
468 Started => $Started->ISO,
469 Resolved => $Resolved->ISO,
473 # Parameters passed in during an import that we probably don't want to touch, otherwise
474 foreach my $attr (qw(id Creator Created LastUpdated LastUpdatedBy)) {
475 $params{$attr} = $args{$attr} if $args{$attr};
478 # Delete null integer parameters
480 (qw(TimeWorked TimeLeft TimeEstimated InitialPriority FinalPriority))
482 delete $params{$attr}
483 unless ( exists $params{$attr} && $params{$attr} );
486 # Delete the time worked if we're counting it in the transaction
487 delete $params{'TimeWorked'} if $args{'_RecordTransaction'};
489 my ($id,$ticket_message) = $self->SUPER::Create( %params );
491 $RT::Logger->crit( "Couldn't create a ticket: " . $ticket_message );
492 $RT::Handle->Rollback();
494 $self->loc("Ticket could not be created due to an internal error")
498 #Set the ticket's effective ID now that we've created it.
499 my ( $val, $msg ) = $self->__Set(
500 Field => 'EffectiveId',
501 Value => ( $args{'EffectiveId'} || $id )
504 $RT::Logger->crit("Couldn't set EffectiveId: $msg");
505 $RT::Handle->Rollback;
507 $self->loc("Ticket could not be created due to an internal error")
511 my $create_groups_ret = $self->_CreateTicketGroups();
512 unless ($create_groups_ret) {
513 $RT::Logger->crit( "Couldn't create ticket groups for ticket "
515 . ". aborting Ticket creation." );
516 $RT::Handle->Rollback();
518 $self->loc("Ticket could not be created due to an internal error")
522 # Set the owner in the Groups table
523 # We denormalize it into the Ticket table too because doing otherwise would
524 # kill performance, bigtime. It gets kept in lockstep thanks to the magic of transactionalization
525 $self->OwnerGroup->_AddMember(
526 PrincipalId => $Owner->PrincipalId,
527 InsideTransaction => 1
528 ) unless $DeferOwner;
532 # {{{ Deal with setting up watchers
534 foreach my $type ( "Cc", "AdminCc", "Requestor" ) {
535 # we know it's an array ref
536 foreach my $watcher ( @{ $args{$type} } ) {
538 # Note that we're using AddWatcher, rather than _AddWatcher, as we
539 # actually _want_ that ACL check. Otherwise, random ticket creators
540 # could make themselves adminccs and maybe get ticket rights. that would
542 my $method = $type eq 'AdminCc'? 'AddWatcher': '_AddWatcher';
544 my ($val, $msg) = $self->$method(
546 PrincipalId => $watcher,
549 push @non_fatal_errors, $self->loc("Couldn't set [_1] watcher: [_2]", $type, $msg)
554 if ($args{'SquelchMailTo'}) {
555 my @squelch = ref( $args{'SquelchMailTo'} ) ? @{ $args{'SquelchMailTo'} }
556 : $args{'SquelchMailTo'};
557 $self->_SquelchMailTo( @squelch );
563 # {{{ Add all the custom fields
565 foreach my $arg ( keys %args ) {
566 next unless $arg =~ /^CustomField-(\d+)$/i;
570 UNIVERSAL::isa( $args{$arg} => 'ARRAY' ) ? @{ $args{$arg} } : ( $args{$arg} ) )
572 next unless defined $value && length $value;
574 # Allow passing in uploaded LargeContent etc by hash reference
575 my ($status, $msg) = $self->_AddCustomFieldValue(
576 (UNIVERSAL::isa( $value => 'HASH' )
581 RecordTransaction => 0,
583 push @non_fatal_errors, $msg unless $status;
589 # {{{ Deal with setting up links
591 # TODO: Adding link may fire scrips on other end and those scrips
592 # could create transactions on this ticket before 'Create' transaction.
594 # We should implement different schema: record 'Create' transaction,
595 # create links and only then fire create transaction's scrips.
597 # Ideal variant: add all links without firing scrips, record create
598 # transaction and only then fire scrips on the other ends of links.
602 foreach my $type ( keys %LINKTYPEMAP ) {
603 next unless ( defined $args{$type} );
605 ref( $args{$type} ) ? @{ $args{$type} } : ( $args{$type} ) )
607 # Check rights on the other end of the link if we must
608 # then run _AddLink that doesn't check for ACLs
609 if ( RT->Config->Get( 'StrictLinkACL' ) ) {
610 my ($val, $msg, $obj) = $self->__GetTicketFromURI( URI => $link );
612 push @non_fatal_errors, $msg;
615 if ( $obj && !$obj->CurrentUserHasRight('ModifyTicket') ) {
616 push @non_fatal_errors, $self->loc('Linking. Permission denied');
621 my ( $wval, $wmsg ) = $self->_AddLink(
622 Type => $LINKTYPEMAP{$type}->{'Type'},
623 $LINKTYPEMAP{$type}->{'Mode'} => $link,
624 Silent => !$args{'_RecordTransaction'},
625 'Silent'. ( $LINKTYPEMAP{$type}->{'Mode'} eq 'Base'? 'Target': 'Base' )
629 push @non_fatal_errors, $wmsg unless ($wval);
634 # Now that we've created the ticket and set up its metadata, we can actually go and check OwnTicket on the ticket itself.
635 # This might be different than before in cases where extensions like RTIR are doing clever things with RT's ACL system
637 if (!$DeferOwner->HasRight( Object => $self, Right => 'OwnTicket')) {
639 $RT::Logger->warning( "User " . $DeferOwner->Name . "(" . $DeferOwner->id
640 . ") was proposed as a ticket owner but has no rights to own "
641 . "tickets in " . $QueueObj->Name );
642 push @non_fatal_errors, $self->loc(
643 "Owner '[_1]' does not have rights to own this ticket.",
647 $Owner = $DeferOwner;
648 $self->__Set(Field => 'Owner', Value => $Owner->id);
651 $self->OwnerGroup->_AddMember(
652 PrincipalId => $Owner->PrincipalId,
653 InsideTransaction => 1
657 if ( $args{'_RecordTransaction'} ) {
659 # {{{ Add a transaction for the create
660 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
662 TimeTaken => $args{'TimeWorked'},
663 MIMEObj => $args{'MIMEObj'},
664 CommitScrips => !$args{'DryRun'},
667 if ( $self->Id && $Trans ) {
669 $TransObj->UpdateCustomFields(ARGSRef => \%args);
671 $RT::Logger->info( "Ticket " . $self->Id . " created in queue '" . $QueueObj->Name . "' by " . $self->CurrentUser->Name );
672 $ErrStr = $self->loc( "Ticket [_1] created in queue '[_2]'", $self->Id, $QueueObj->Name );
673 $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
676 $RT::Handle->Rollback();
678 $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
679 $RT::Logger->error("Ticket couldn't be created: $ErrStr");
680 return ( 0, 0, $self->loc( "Ticket could not be created due to an internal error"));
683 if ( $args{'DryRun'} ) {
684 $RT::Handle->Rollback();
685 return ($self->id, $TransObj, $ErrStr);
687 $RT::Handle->Commit();
688 return ( $self->Id, $TransObj->Id, $ErrStr );
694 # Not going to record a transaction
695 $RT::Handle->Commit();
696 $ErrStr = $self->loc( "Ticket [_1] created in queue '[_2]'", $self->Id, $QueueObj->Name );
697 $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
698 return ( $self->Id, 0, $ErrStr );
706 # {{{ _Parse822HeadersForAttributes Content
708 =head2 _Parse822HeadersForAttributes Content
710 Takes an RFC822 style message and parses its attributes into a hash.
714 sub _Parse822HeadersForAttributes {
719 my @lines = ( split ( /\n/, $content ) );
720 while ( defined( my $line = shift @lines ) ) {
721 if ( $line =~ /^(.*?):(?:\s+(.*))?$/ ) {
726 if ( defined( $args{$tag} ) )
727 { #if we're about to get a second value, make it an array
728 $args{$tag} = [ $args{$tag} ];
730 if ( ref( $args{$tag} ) )
731 { #If it's an array, we want to push the value
732 push @{ $args{$tag} }, $value;
734 else { #if there's nothing there, just set the value
735 $args{$tag} = $value;
737 } elsif ($line =~ /^$/) {
739 #TODO: this won't work, since "" isn't of the form "foo:value"
741 while ( defined( my $l = shift @lines ) ) {
742 push @{ $args{'content'} }, $l;
748 foreach my $date (qw(due starts started resolved)) {
749 my $dateobj = RT::Date->new($RT::SystemUser);
750 if ( defined ($args{$date}) and $args{$date} =~ /^\d+$/ ) {
751 $dateobj->Set( Format => 'unix', Value => $args{$date} );
754 $dateobj->Set( Format => 'unknown', Value => $args{$date} );
756 $args{$date} = $dateobj->ISO;
758 $args{'mimeobj'} = MIME::Entity->new();
759 $args{'mimeobj'}->build(
760 Type => ( $args{'contenttype'} || 'text/plain' ),
761 Data => ($args{'content'} || '')
771 =head2 Import PARAMHASH
774 Doesn\'t create a transaction.
775 Doesn\'t supply queue defaults, etc.
783 my ( $ErrStr, $QueueObj, $Owner );
787 EffectiveId => undef,
791 Owner => $RT::Nobody->Id,
792 Subject => '[no subject]',
793 InitialPriority => undef,
794 FinalPriority => undef,
805 if ( ( defined( $args{'Queue'} ) ) && ( !ref( $args{'Queue'} ) ) ) {
806 $QueueObj = RT::Queue->new($RT::SystemUser);
807 $QueueObj->Load( $args{'Queue'} );
809 #TODO error check this and return 0 if it\'s not loading properly +++
811 elsif ( ref( $args{'Queue'} ) eq 'RT::Queue' ) {
812 $QueueObj = RT::Queue->new($RT::SystemUser);
813 $QueueObj->Load( $args{'Queue'}->Id );
817 "$self " . $args{'Queue'} . " not a recognised queue object." );
820 #Can't create a ticket without a queue.
821 unless ( defined($QueueObj) and $QueueObj->Id ) {
822 $RT::Logger->debug("$self No queue given for ticket creation.");
823 return ( 0, $self->loc('Could not create ticket. Queue not set') );
826 #Now that we have a queue, Check the ACLS
828 $self->CurrentUser->HasRight(
829 Right => 'CreateTicket',
835 $self->loc("No permission to create tickets in the queue '[_1]'"
839 # {{{ Deal with setting the owner
841 # Attempt to take user object, user name or user id.
842 # Assign to nobody if lookup fails.
843 if ( defined( $args{'Owner'} ) ) {
844 if ( ref( $args{'Owner'} ) ) {
845 $Owner = $args{'Owner'};
848 $Owner = new RT::User( $self->CurrentUser );
849 $Owner->Load( $args{'Owner'} );
850 if ( !defined( $Owner->id ) ) {
851 $Owner->Load( $RT::Nobody->id );
856 #If we have a proposed owner and they don't have the right
857 #to own a ticket, scream about it and make them not the owner
860 and ( $Owner->Id != $RT::Nobody->Id )
870 $RT::Logger->warning( "$self user "
874 . "as a ticket owner but has no rights to own "
876 . $QueueObj->Name . "'" );
881 #If we haven't been handed a valid owner, make it nobody.
882 unless ( defined($Owner) ) {
883 $Owner = new RT::User( $self->CurrentUser );
884 $Owner->Load( $RT::Nobody->UserObj->Id );
889 unless ( $self->ValidateStatus( $args{'Status'} ) ) {
890 return ( 0, $self->loc("'[_1]' is an invalid value for status", $args{'Status'}) );
893 $self->{'_AccessibleCache'}{Created} = { 'read' => 1, 'write' => 1 };
894 $self->{'_AccessibleCache'}{Creator} = { 'read' => 1, 'auto' => 1 };
895 $self->{'_AccessibleCache'}{LastUpdated} = { 'read' => 1, 'write' => 1 };
896 $self->{'_AccessibleCache'}{LastUpdatedBy} = { 'read' => 1, 'auto' => 1 };
898 # If we're coming in with an id, set that now.
899 my $EffectiveId = undef;
901 $EffectiveId = $args{'id'};
905 my $id = $self->SUPER::Create(
907 EffectiveId => $EffectiveId,
908 Queue => $QueueObj->Id,
910 Subject => $args{'Subject'}, # loc
911 InitialPriority => $args{'InitialPriority'}, # loc
912 FinalPriority => $args{'FinalPriority'}, # loc
913 Priority => $args{'InitialPriority'}, # loc
914 Status => $args{'Status'}, # loc
915 TimeWorked => $args{'TimeWorked'}, # loc
916 Type => $args{'Type'}, # loc
917 Created => $args{'Created'}, # loc
918 Told => $args{'Told'}, # loc
919 LastUpdated => $args{'Updated'}, # loc
920 Resolved => $args{'Resolved'}, # loc
921 Due => $args{'Due'}, # loc
924 # If the ticket didn't have an id
925 # Set the ticket's effective ID now that we've created it.
927 $self->Load( $args{'id'} );
931 $self->__Set( Field => 'EffectiveId', Value => $id );
935 $self . "->Import couldn't set EffectiveId: $msg" );
939 my $create_groups_ret = $self->_CreateTicketGroups();
940 unless ($create_groups_ret) {
942 "Couldn't create ticket groups for ticket " . $self->Id );
945 $self->OwnerGroup->_AddMember( PrincipalId => $Owner->PrincipalId );
947 foreach my $watcher ( @{ $args{'Cc'} } ) {
948 $self->_AddWatcher( Type => 'Cc', Email => $watcher, Silent => 1 );
950 foreach my $watcher ( @{ $args{'AdminCc'} } ) {
951 $self->_AddWatcher( Type => 'AdminCc', Email => $watcher,
954 foreach my $watcher ( @{ $args{'Requestor'} } ) {
955 $self->_AddWatcher( Type => 'Requestor', Email => $watcher,
959 return ( $self->Id, $ErrStr );
964 # {{{ Routines dealing with watchers.
966 # {{{ _CreateTicketGroups
968 =head2 _CreateTicketGroups
970 Create the ticket groups and links for this ticket.
971 This routine expects to be called from Ticket->Create _inside of a transaction_
973 It will create four groups for this ticket: Requestor, Cc, AdminCc and Owner.
975 It will return true on success and undef on failure.
981 sub _CreateTicketGroups {
984 my @types = qw(Requestor Owner Cc AdminCc);
986 foreach my $type (@types) {
987 my $type_obj = RT::Group->new($self->CurrentUser);
988 my ($id, $msg) = $type_obj->CreateRoleGroup(Domain => 'RT::Ticket-Role',
989 Instance => $self->Id,
992 $RT::Logger->error("Couldn't create a ticket group of type '$type' for ticket ".
993 $self->Id.": ".$msg);
1003 # {{{ sub OwnerGroup
1007 A constructor which returns an RT::Group object containing the owner of this ticket.
1013 my $owner_obj = RT::Group->new($self->CurrentUser);
1014 $owner_obj->LoadTicketRoleGroup( Ticket => $self->Id, Type => 'Owner');
1015 return ($owner_obj);
1021 # {{{ sub AddWatcher
1025 AddWatcher takes a parameter hash. The keys are as follows:
1027 Type One of Requestor, Cc, AdminCc
1029 PrincipalId The RT::Principal id of the user or group that's being added as a watcher
1031 Email The email address of the new watcher. If a user with this
1032 email address can't be found, a new nonprivileged user will be created.
1034 If the watcher you\'re trying to set has an RT account, set the PrincipalId paremeter to their User Id. Otherwise, set the Email parameter to their Email address.
1042 PrincipalId => undef,
1047 # ModifyTicket works in any case
1048 return $self->_AddWatcher( %args )
1049 if $self->CurrentUserHasRight('ModifyTicket');
1050 if ( $args{'Email'} ) {
1051 my ($addr) = RT::EmailParser->ParseEmailAddress( $args{'Email'} );
1052 return (0, $self->loc("Couldn't parse address from '[_1]' string", $args{'Email'} ))
1055 if ( lc $self->CurrentUser->UserObj->EmailAddress
1056 eq lc RT::User->CanonicalizeEmailAddress( $addr->address ) )
1058 $args{'PrincipalId'} = $self->CurrentUser->id;
1059 delete $args{'Email'};
1063 # If the watcher isn't the current user then the current user has no right
1065 unless ( $args{'PrincipalId'} && $self->CurrentUser->id == $args{'PrincipalId'} ) {
1066 return ( 0, $self->loc("Permission Denied") );
1069 # If it's an AdminCc and they don't have 'WatchAsAdminCc', bail
1070 if ( $args{'Type'} eq 'AdminCc' ) {
1071 unless ( $self->CurrentUserHasRight('WatchAsAdminCc') ) {
1072 return ( 0, $self->loc('Permission Denied') );
1076 # If it's a Requestor or Cc and they don't have 'Watch', bail
1077 elsif ( $args{'Type'} eq 'Cc' || $args{'Type'} eq 'Requestor' ) {
1078 unless ( $self->CurrentUserHasRight('Watch') ) {
1079 return ( 0, $self->loc('Permission Denied') );
1083 $RT::Logger->warning( "AddWatcher got passed a bogus type");
1084 return ( 0, $self->loc('Error in parameters to Ticket->AddWatcher') );
1087 return $self->_AddWatcher( %args );
1090 #This contains the meat of AddWatcher. but can be called from a routine like
1091 # Create, which doesn't need the additional acl check
1097 PrincipalId => undef,
1103 my $principal = RT::Principal->new($self->CurrentUser);
1104 if ($args{'Email'}) {
1105 if ( RT::EmailParser->IsRTAddress( $args{'Email'} ) ) {
1106 return (0, $self->loc("[_1] is an address RT receives mail at. Adding it as a '[_2]' would create a mail loop", $args{'Email'}, $self->loc($args{'Type'})));
1108 my $user = RT::User->new($RT::SystemUser);
1109 my ($pid, $msg) = $user->LoadOrCreateByEmail( $args{'Email'} );
1110 $args{'PrincipalId'} = $pid if $pid;
1112 if ($args{'PrincipalId'}) {
1113 $principal->Load($args{'PrincipalId'});
1114 if ( $principal->id and $principal->IsUser and my $email = $principal->Object->EmailAddress ) {
1115 return (0, $self->loc("[_1] is an address RT receives mail at. Adding it as a '[_2]' would create a mail loop", $email, $self->loc($args{'Type'})))
1116 if RT::EmailParser->IsRTAddress( $email );
1122 # If we can't find this watcher, we need to bail.
1123 unless ($principal->Id) {
1124 $RT::Logger->error("Could not load create a user with the email address '".$args{'Email'}. "' to add as a watcher for ticket ".$self->Id);
1125 return(0, $self->loc("Could not find or create that user"));
1129 my $group = RT::Group->new($self->CurrentUser);
1130 $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->Id);
1131 unless ($group->id) {
1132 return(0,$self->loc("Group not found"));
1135 if ( $group->HasMember( $principal)) {
1137 return ( 0, $self->loc('That principal is already a [_1] for this ticket', $self->loc($args{'Type'})) );
1141 my ( $m_id, $m_msg ) = $group->_AddMember( PrincipalId => $principal->Id,
1142 InsideTransaction => 1 );
1144 $RT::Logger->error("Failed to add ".$principal->Id." as a member of group ".$group->Id.": ".$m_msg);
1146 return ( 0, $self->loc('Could not make that principal a [_1] for this ticket', $self->loc($args{'Type'})) );
1149 unless ( $args{'Silent'} ) {
1150 $self->_NewTransaction(
1151 Type => 'AddWatcher',
1152 NewValue => $principal->Id,
1153 Field => $args{'Type'}
1157 return ( 1, $self->loc('Added principal as a [_1] for this ticket', $self->loc($args{'Type'})) );
1163 # {{{ sub DeleteWatcher
1165 =head2 DeleteWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL_ADDRESS }
1168 Deletes a Ticket watcher. Takes two arguments:
1170 Type (one of Requestor,Cc,AdminCc)
1174 PrincipalId (an RT::Principal Id of the watcher you want to remove)
1176 Email (the email address of an existing wathcer)
1185 my %args = ( Type => undef,
1186 PrincipalId => undef,
1190 unless ( $args{'PrincipalId'} || $args{'Email'} ) {
1191 return ( 0, $self->loc("No principal specified") );
1193 my $principal = RT::Principal->new( $self->CurrentUser );
1194 if ( $args{'PrincipalId'} ) {
1196 $principal->Load( $args{'PrincipalId'} );
1199 my $user = RT::User->new( $self->CurrentUser );
1200 $user->LoadByEmail( $args{'Email'} );
1201 $principal->Load( $user->Id );
1204 # If we can't find this watcher, we need to bail.
1205 unless ( $principal->Id ) {
1206 return ( 0, $self->loc("Could not find that principal") );
1209 my $group = RT::Group->new( $self->CurrentUser );
1210 $group->LoadTicketRoleGroup( Type => $args{'Type'}, Ticket => $self->Id );
1211 unless ( $group->id ) {
1212 return ( 0, $self->loc("Group not found") );
1216 #If the watcher we're trying to add is for the current user
1217 if ( $self->CurrentUser->PrincipalId == $principal->id ) {
1219 # If it's an AdminCc and they don't have
1220 # 'WatchAsAdminCc' or 'ModifyTicket', bail
1221 if ( $args{'Type'} eq 'AdminCc' ) {
1222 unless ( $self->CurrentUserHasRight('ModifyTicket')
1223 or $self->CurrentUserHasRight('WatchAsAdminCc') ) {
1224 return ( 0, $self->loc('Permission Denied') );
1228 # If it's a Requestor or Cc and they don't have
1229 # 'Watch' or 'ModifyTicket', bail
1230 elsif ( ( $args{'Type'} eq 'Cc' ) or ( $args{'Type'} eq 'Requestor' ) )
1232 unless ( $self->CurrentUserHasRight('ModifyTicket')
1233 or $self->CurrentUserHasRight('Watch') ) {
1234 return ( 0, $self->loc('Permission Denied') );
1238 $RT::Logger->warn("$self -> DeleteWatcher got passed a bogus type");
1240 $self->loc('Error in parameters to Ticket->DeleteWatcher') );
1244 # If the watcher isn't the current user
1245 # and the current user doesn't have 'ModifyTicket' bail
1247 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1248 return ( 0, $self->loc("Permission Denied") );
1254 # see if this user is already a watcher.
1256 unless ( $group->HasMember($principal) ) {
1258 $self->loc( 'That principal is not a [_1] for this ticket',
1262 my ( $m_id, $m_msg ) = $group->_DeleteMember( $principal->Id );
1264 $RT::Logger->error( "Failed to delete "
1266 . " as a member of group "
1272 'Could not remove that principal as a [_1] for this ticket',
1276 unless ( $args{'Silent'} ) {
1277 $self->_NewTransaction( Type => 'DelWatcher',
1278 OldValue => $principal->Id,
1279 Field => $args{'Type'} );
1283 $self->loc( "[_1] is no longer a [_2] for this ticket.",
1284 $principal->Object->Name,
1293 =head2 SquelchMailTo [EMAIL]
1295 Takes an optional email address to never email about updates to this ticket.
1298 Returns an array of the RT::Attribute objects for this ticket's 'SquelchMailTo' attributes.
1306 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1310 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1315 return $self->_SquelchMailTo(@_);
1318 sub _SquelchMailTo {
1322 $self->AddAttribute( Name => 'SquelchMailTo', Content => $attr )
1323 unless grep { $_->Content eq $attr }
1324 $self->Attributes->Named('SquelchMailTo');
1326 my @attributes = $self->Attributes->Named('SquelchMailTo');
1327 return (@attributes);
1331 =head2 UnsquelchMailTo ADDRESS
1333 Takes an address and removes it from this ticket's "SquelchMailTo" list. If an address appears multiple times, each instance is removed.
1335 Returns a tuple of (status, message)
1339 sub UnsquelchMailTo {
1342 my $address = shift;
1343 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1344 return ( 0, $self->loc("Permission Denied") );
1347 my ($val, $msg) = $self->Attributes->DeleteEntry ( Name => 'SquelchMailTo', Content => $address);
1348 return ($val, $msg);
1352 # {{{ a set of [foo]AsString subs that will return the various sorts of watchers for a ticket/queue as a comma delineated string
1354 =head2 RequestorAddresses
1356 B<Returns> String: All Ticket Requestor email addresses as a string.
1360 sub RequestorAddresses {
1363 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1367 return ( $self->Requestors->MemberEmailAddressesAsString );
1371 =head2 AdminCcAddresses
1373 returns String: All Ticket AdminCc email addresses as a string
1377 sub AdminCcAddresses {
1380 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1384 return ( $self->AdminCc->MemberEmailAddressesAsString )
1390 returns String: All Ticket Ccs as a string of email addresses
1397 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1400 return ( $self->Cc->MemberEmailAddressesAsString);
1406 # {{{ Routines that return RT::Watchers objects of Requestors, Ccs and AdminCcs
1408 # {{{ sub Requestors
1413 Returns this ticket's Requestors as an RT::Group object
1420 my $group = RT::Group->new($self->CurrentUser);
1421 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1422 $group->LoadTicketRoleGroup(Type => 'Requestor', Ticket => $self->Id);
1435 Returns an RT::Group object which contains this ticket's Ccs.
1436 If the user doesn't have "ShowTicket" permission, returns an empty group
1443 my $group = RT::Group->new($self->CurrentUser);
1444 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1445 $group->LoadTicketRoleGroup(Type => 'Cc', Ticket => $self->Id);
1458 Returns an RT::Group object which contains this ticket's AdminCcs.
1459 If the user doesn't have "ShowTicket" permission, returns an empty group
1466 my $group = RT::Group->new($self->CurrentUser);
1467 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1468 $group->LoadTicketRoleGroup(Type => 'AdminCc', Ticket => $self->Id);
1478 # {{{ IsWatcher,IsRequestor,IsCc, IsAdminCc
1481 # a generic routine to be called by IsRequestor, IsCc and IsAdminCc
1483 =head2 IsWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL }
1485 Takes a param hash with the attributes Type and either PrincipalId or Email
1487 Type is one of Requestor, Cc, AdminCc and Owner
1489 PrincipalId is an RT::Principal id, and Email is an email address.
1491 Returns true if the specified principal (or the one corresponding to the
1492 specified address) is a member of the group Type for this ticket.
1494 XX TODO: This should be Memoized.
1501 my %args = ( Type => 'Requestor',
1502 PrincipalId => undef,
1507 # Load the relevant group.
1508 my $group = RT::Group->new($self->CurrentUser);
1509 $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->id);
1511 # Find the relevant principal.
1512 if (!$args{PrincipalId} && $args{Email}) {
1513 # Look up the specified user.
1514 my $user = RT::User->new($self->CurrentUser);
1515 $user->LoadByEmail($args{Email});
1517 $args{PrincipalId} = $user->PrincipalId;
1520 # A non-existent user can't be a group member.
1525 # Ask if it has the member in question
1526 return $group->HasMember( $args{'PrincipalId'} );
1531 # {{{ sub IsRequestor
1533 =head2 IsRequestor PRINCIPAL_ID
1535 Takes an L<RT::Principal> id.
1537 Returns true if the principal is a requestor of the current ticket.
1545 return ( $self->IsWatcher( Type => 'Requestor', PrincipalId => $person ) );
1553 =head2 IsCc PRINCIPAL_ID
1555 Takes an RT::Principal id.
1556 Returns true if the principal is a Cc of the current ticket.
1565 return ( $self->IsWatcher( Type => 'Cc', PrincipalId => $cc ) );
1573 =head2 IsAdminCc PRINCIPAL_ID
1575 Takes an RT::Principal id.
1576 Returns true if the principal is an AdminCc of the current ticket.
1584 return ( $self->IsWatcher( Type => 'AdminCc', PrincipalId => $person ) );
1594 Takes an RT::User object. Returns true if that user is this ticket's owner.
1595 returns undef otherwise
1603 # no ACL check since this is used in acl decisions
1604 # unless ($self->CurrentUserHasRight('ShowTicket')) {
1608 #Tickets won't yet have owners when they're being created.
1609 unless ( $self->OwnerObj->id ) {
1613 if ( $person->id == $self->OwnerObj->id ) {
1628 =head2 TransactionAddresses
1630 Returns a composite hashref of the results of L<RT::Transaction/Addresses> for
1631 all this ticket's Create, Comment or Correspond transactions. The keys are
1632 stringified email addresses. Each value is an L<Email::Address> object.
1634 NOTE: For performance reasons, this method might want to skip transactions and go straight for attachments. But to make that work right, we're going to need to go and walk around the access control in Attachment.pm's sub _Value.
1639 sub TransactionAddresses {
1641 my $txns = $self->Transactions;
1644 foreach my $type (qw(Create Comment Correspond)) {
1645 $txns->Limit(FIELD => 'Type', OPERATOR => '=', VALUE => $type , ENTRYAGGREGATOR => 'OR', CASESENSITIVE => 1);
1648 while (my $txn = $txns->Next) {
1649 my $txnaddrs = $txn->Addresses;
1650 foreach my $addrlist ( values %$txnaddrs ) {
1651 foreach my $addr (@$addrlist) {
1652 # Skip addresses without a phrase (things that are just raw addresses) if we have a phrase
1653 next if ($addresses{$addr->address} && $addresses{$addr->address}->phrase && not $addr->phrase);
1654 # skips "comment-only" addresses
1655 next unless ($addr->address);
1656 $addresses{$addr->address} = $addr;
1668 # {{{ Routines dealing with queues
1670 # {{{ sub ValidateQueue
1677 $RT::Logger->warning( " RT:::Queue::ValidateQueue called with a null value. this isn't ok.");
1681 my $QueueObj = RT::Queue->new( $self->CurrentUser );
1682 my $id = $QueueObj->Load($Value);
1698 my $NewQueue = shift;
1700 #Redundant. ACL gets checked in _Set;
1701 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1702 return ( 0, $self->loc("Permission Denied") );
1705 my $NewQueueObj = RT::Queue->new( $self->CurrentUser );
1706 $NewQueueObj->Load($NewQueue);
1708 unless ( $NewQueueObj->Id() ) {
1709 return ( 0, $self->loc("That queue does not exist") );
1712 if ( $NewQueueObj->Id == $self->QueueObj->Id ) {
1713 return ( 0, $self->loc('That is the same value') );
1716 $self->CurrentUser->HasRight(
1717 Right => 'CreateTicket',
1718 Object => $NewQueueObj
1722 return ( 0, $self->loc("You may not create requests in that queue.") );
1726 $self->OwnerObj->HasRight(
1727 Right => 'OwnTicket',
1728 Object => $NewQueueObj
1732 my $clone = RT::Ticket->new( $RT::SystemUser );
1733 $clone->Load( $self->Id );
1734 unless ( $clone->Id ) {
1735 return ( 0, $self->loc("Couldn't load copy of ticket #[_1].", $self->Id) );
1737 my ($status, $msg) = $clone->SetOwner( $RT::Nobody->Id, 'Force' );
1738 $RT::Logger->error("Couldn't set owner on queue change: $msg") unless $status;
1741 my ($status, $msg) = $self->_Set( Field => 'Queue', Value => $NewQueueObj->Id() );
1744 # On queue change, change queue for reminders too
1745 my $reminder_collection = $self->Reminders->Collection;
1746 while ( my $reminder = $reminder_collection->Next ) {
1747 my ($status, $msg) = $reminder->SetQueue($NewQueue);
1748 $RT::Logger->error('Queue change failed for reminder #' . $reminder->Id . ': ' . $msg) unless $status;
1752 return ($status, $msg);
1761 Takes nothing. returns this ticket's queue object
1768 my $queue_obj = RT::Queue->new( $self->CurrentUser );
1770 #We call __Value so that we can avoid the ACL decision and some deep recursion
1771 my ($result) = $queue_obj->Load( $self->__Value('Queue') );
1772 return ($queue_obj);
1779 # {{{ Date printing routines
1785 Returns an RT::Date object containing this ticket's due date
1792 my $time = new RT::Date( $self->CurrentUser );
1794 # -1 is RT::Date slang for never
1795 if ( my $due = $self->Due ) {
1796 $time->Set( Format => 'sql', Value => $due );
1799 $time->Set( Format => 'unix', Value => -1 );
1807 # {{{ sub DueAsString
1811 Returns this ticket's due date as a human readable string
1817 return $self->DueObj->AsString();
1822 # {{{ sub ResolvedObj
1826 Returns an RT::Date object of this ticket's 'resolved' time.
1833 my $time = new RT::Date( $self->CurrentUser );
1834 $time->Set( Format => 'sql', Value => $self->Resolved );
1840 # {{{ sub SetStarted
1844 Takes a date in ISO format or undef
1845 Returns a transaction id and a message
1846 The client calls "Start" to note that the project was started on the date in $date.
1847 A null date means "now"
1853 my $time = shift || 0;
1855 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1856 return ( 0, $self->loc("Permission Denied") );
1859 #We create a date object to catch date weirdness
1860 my $time_obj = new RT::Date( $self->CurrentUser() );
1862 $time_obj->Set( Format => 'ISO', Value => $time );
1865 $time_obj->SetToNow();
1868 #Now that we're starting, open this ticket
1869 #TODO do we really want to force this as policy? it should be a scrip
1871 #We need $TicketAsSystem, in case the current user doesn't have
1874 my $TicketAsSystem = new RT::Ticket($RT::SystemUser);
1875 $TicketAsSystem->Load( $self->Id );
1876 if ( $TicketAsSystem->Status eq 'new' ) {
1877 $TicketAsSystem->Open();
1880 return ( $self->_Set( Field => 'Started', Value => $time_obj->ISO ) );
1886 # {{{ sub StartedObj
1890 Returns an RT::Date object which contains this ticket's
1898 my $time = new RT::Date( $self->CurrentUser );
1899 $time->Set( Format => 'sql', Value => $self->Started );
1909 Returns an RT::Date object which contains this ticket's
1917 my $time = new RT::Date( $self->CurrentUser );
1918 $time->Set( Format => 'sql', Value => $self->Starts );
1928 Returns an RT::Date object which contains this ticket's
1936 my $time = new RT::Date( $self->CurrentUser );
1937 $time->Set( Format => 'sql', Value => $self->Told );
1943 # {{{ sub ToldAsString
1947 A convenience method that returns ToldObj->AsString
1949 TODO: This should be deprecated
1955 if ( $self->Told ) {
1956 return $self->ToldObj->AsString();
1965 # {{{ sub TimeWorkedAsString
1967 =head2 TimeWorkedAsString
1969 Returns the amount of time worked on this ticket as a Text String
1973 sub TimeWorkedAsString {
1975 my $value = $self->TimeWorked;
1977 # return the # of minutes worked turned into seconds and written as
1978 # a simple text string, this is not really a date object, but if we
1979 # diff a number of seconds vs the epoch, we'll get a nice description
1981 return "" unless $value;
1982 return RT::Date->new( $self->CurrentUser )
1983 ->DurationAsString( $value * 60 );
1988 # {{{ sub TimeLeftAsString
1990 =head2 TimeLeftAsString
1992 Returns the amount of time left on this ticket as a Text String
1996 sub TimeLeftAsString {
1998 my $value = $self->TimeLeft;
1999 return "" unless $value;
2000 return RT::Date->new( $self->CurrentUser )
2001 ->DurationAsString( $value * 60 );
2006 # {{{ Routines dealing with correspondence/comments
2012 Comment on this ticket.
2013 Takes a hash with the following attributes:
2014 If MIMEObj is undefined, Content will be used to build a MIME::Entity for this
2017 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
2019 If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
2020 They will, however, be prepared and you'll be able to access them through the TransactionObj
2022 Returns: Transaction id, Error Message, Transaction Object
2023 (note the different order from Create()!)
2030 my %args = ( CcMessageTo => undef,
2031 BccMessageTo => undef,
2038 unless ( ( $self->CurrentUserHasRight('CommentOnTicket') )
2039 or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
2040 return ( 0, $self->loc("Permission Denied"), undef );
2042 $args{'NoteType'} = 'Comment';
2044 if ($args{'DryRun'}) {
2045 $RT::Handle->BeginTransaction();
2046 $args{'CommitScrips'} = 0;
2049 my @results = $self->_RecordNote(%args);
2050 if ($args{'DryRun'}) {
2051 $RT::Handle->Rollback();
2058 # {{{ sub Correspond
2062 Correspond on this ticket.
2063 Takes a hashref with the following attributes:
2066 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
2068 if there's no MIMEObj, Content is used to build a MIME::Entity object
2070 If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
2071 They will, however, be prepared and you'll be able to access them through the TransactionObj
2073 Returns: Transaction id, Error Message, Transaction Object
2074 (note the different order from Create()!)
2081 my %args = ( CcMessageTo => undef,
2082 BccMessageTo => undef,
2088 unless ( ( $self->CurrentUserHasRight('ReplyToTicket') )
2089 or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
2090 return ( 0, $self->loc("Permission Denied"), undef );
2093 $args{'NoteType'} = 'Correspond';
2094 if ($args{'DryRun'}) {
2095 $RT::Handle->BeginTransaction();
2096 $args{'CommitScrips'} = 0;
2099 my @results = $self->_RecordNote(%args);
2101 #Set the last told date to now if this isn't mail from the requestor.
2102 #TODO: Note that this will wrongly ack mail from any non-requestor as a "told"
2103 $self->_SetTold unless ( $self->IsRequestor($self->CurrentUser->id));
2105 if ($args{'DryRun'}) {
2106 $RT::Handle->Rollback();
2115 # {{{ sub _RecordNote
2119 the meat of both comment and correspond.
2121 Performs no access control checks. hence, dangerous.
2128 CcMessageTo => undef,
2129 BccMessageTo => undef,
2134 NoteType => 'Correspond',
2140 unless ( $args{'MIMEObj'} || $args{'Content'} ) {
2141 return ( 0, $self->loc("No message attached"), undef );
2144 unless ( $args{'MIMEObj'} ) {
2145 $args{'MIMEObj'} = MIME::Entity->build(
2146 Data => ( ref $args{'Content'}? $args{'Content'}: [ $args{'Content'} ] )
2150 # convert text parts into utf-8
2151 RT::I18N::SetMIMEEntityToUTF8( $args{'MIMEObj'} );
2153 # If we've been passed in CcMessageTo and BccMessageTo fields,
2154 # add them to the mime object for passing on to the transaction handler
2155 # The "NotifyOtherRecipients" scripAction will look for RT-Send-Cc: and
2156 # RT-Send-Bcc: headers
2159 foreach my $type (qw/Cc Bcc/) {
2160 if ( defined $args{ $type . 'MessageTo' } ) {
2162 my $addresses = join ', ', (
2163 map { RT::User->CanonicalizeEmailAddress( $_->address ) }
2164 Email::Address->parse( $args{ $type . 'MessageTo' } ) );
2165 $args{'MIMEObj'}->head->add( 'RT-Send-' . $type, Encode::encode_utf8( $addresses ) );
2169 foreach my $argument (qw(Encrypt Sign)) {
2170 $args{'MIMEObj'}->head->add(
2171 "X-RT-$argument" => Encode::encode_utf8( $args{ $argument } )
2172 ) if defined $args{ $argument };
2175 # If this is from an external source, we need to come up with its
2176 # internal Message-ID now, so all emails sent because of this
2177 # message have a common Message-ID
2178 my $org = RT->Config->Get('Organization');
2179 my $msgid = $args{'MIMEObj'}->head->get('Message-ID');
2180 unless (defined $msgid && $msgid =~ /<(rt-.*?-\d+-\d+)\.(\d+-0-0)\@\Q$org\E>/) {
2181 $args{'MIMEObj'}->head->set(
2182 'RT-Message-ID' => RT::Interface::Email::GenMessageId( Ticket => $self )
2186 #Record the correspondence (write the transaction)
2187 my ( $Trans, $msg, $TransObj ) = $self->_NewTransaction(
2188 Type => $args{'NoteType'},
2189 Data => ( $args{'MIMEObj'}->head->get('subject') || 'No Subject' ),
2190 TimeTaken => $args{'TimeTaken'},
2191 MIMEObj => $args{'MIMEObj'},
2192 CommitScrips => $args{'CommitScrips'},
2196 $RT::Logger->err("$self couldn't init a transaction $msg");
2197 return ( $Trans, $self->loc("Message could not be recorded"), undef );
2200 return ( $Trans, $self->loc("Message recorded"), $TransObj );
2212 #TODO: Field isn't the right thing here. but I ahave no idea what mnemonic ---
2215 my $type = shift || "";
2217 my $cache_key = "$field$type";
2218 return $self->{ $cache_key } if $self->{ $cache_key };
2220 my $links = $self->{ $cache_key }
2221 = RT::Links->new( $self->CurrentUser );
2222 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
2223 $links->Limit( FIELD => 'id', VALUE => 0 );
2227 # Maybe this ticket is a merge ticket
2228 my $limit_on = 'Local'. $field;
2229 # at least to myself
2233 ENTRYAGGREGATOR => 'OR',
2238 ENTRYAGGREGATOR => 'OR',
2239 ) foreach $self->Merged;
2250 # {{{ sub DeleteLink
2254 Delete a link. takes a paramhash of Base, Target, Type, Silent,
2255 SilentBase and SilentTarget. Either Base or Target must be null.
2256 The null value will be replaced with this ticket\'s id.
2258 If Silent is true then no transaction would be recorded, in other
2259 case you can control creation of transactions on both base and
2260 target with SilentBase and SilentTarget respectively. By default
2261 both transactions are created.
2272 SilentBase => undef,
2273 SilentTarget => undef,
2277 unless ( $args{'Target'} || $args{'Base'} ) {
2278 $RT::Logger->error("Base or Target must be specified");
2279 return ( 0, $self->loc('Either base or target must be specified') );
2284 $right++ if $self->CurrentUserHasRight('ModifyTicket');
2285 if ( !$right && RT->Config->Get( 'StrictLinkACL' ) ) {
2286 return ( 0, $self->loc("Permission Denied") );
2289 # If the other URI is an RT::Ticket, we want to make sure the user
2290 # can modify it too...
2291 my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} );
2292 return (0, $msg) unless $status;
2293 if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) {
2296 if ( ( !RT->Config->Get( 'StrictLinkACL' ) && $right == 0 ) ||
2297 ( RT->Config->Get( 'StrictLinkACL' ) && $right < 2 ) )
2299 return ( 0, $self->loc("Permission Denied") );
2302 my ($val, $Msg) = $self->SUPER::_DeleteLink(%args);
2303 return ( 0, $Msg ) unless $val;
2305 return ( $val, $Msg ) if $args{'Silent'};
2307 my ($direction, $remote_link);
2309 if ( $args{'Base'} ) {
2310 $remote_link = $args{'Base'};
2311 $direction = 'Target';
2313 elsif ( $args{'Target'} ) {
2314 $remote_link = $args{'Target'};
2315 $direction = 'Base';
2318 my $remote_uri = RT::URI->new( $self->CurrentUser );
2319 $remote_uri->FromURI( $remote_link );
2321 unless ( $args{ 'Silent'. $direction } ) {
2322 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
2323 Type => 'DeleteLink',
2324 Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
2325 OldValue => $remote_uri->URI || $remote_link,
2328 $RT::Logger->error("Couldn't create transaction: $Msg") unless $Trans;
2331 if ( !$args{ 'Silent'. ( $direction eq 'Target'? 'Base': 'Target' ) } && $remote_uri->IsLocal ) {
2332 my $OtherObj = $remote_uri->Object;
2333 my ( $val, $Msg ) = $OtherObj->_NewTransaction(
2334 Type => 'DeleteLink',
2335 Field => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
2336 : $LINKDIRMAP{$args{'Type'}}->{Target},
2337 OldValue => $self->URI,
2338 ActivateScrips => !RT->Config->Get('LinkTransactionsRun1Scrip'),
2341 $RT::Logger->error("Couldn't create transaction: $Msg") unless $val;
2344 return ( $val, $Msg );
2353 Takes a paramhash of Type and one of Base or Target. Adds that link to this ticket.
2355 If Silent is true then no transaction would be recorded, in other
2356 case you can control creation of transactions on both base and
2357 target with SilentBase and SilentTarget respectively. By default
2358 both transactions are created.
2364 my %args = ( Target => '',
2368 SilentBase => undef,
2369 SilentTarget => undef,
2372 unless ( $args{'Target'} || $args{'Base'} ) {
2373 $RT::Logger->error("Base or Target must be specified");
2374 return ( 0, $self->loc('Either base or target must be specified') );
2378 $right++ if $self->CurrentUserHasRight('ModifyTicket');
2379 if ( !$right && RT->Config->Get( 'StrictLinkACL' ) ) {
2380 return ( 0, $self->loc("Permission Denied") );
2383 # If the other URI is an RT::Ticket, we want to make sure the user
2384 # can modify it too...
2385 my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} );
2386 return (0, $msg) unless $status;
2387 if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) {
2390 if ( ( !RT->Config->Get( 'StrictLinkACL' ) && $right == 0 ) ||
2391 ( RT->Config->Get( 'StrictLinkACL' ) && $right < 2 ) )
2393 return ( 0, $self->loc("Permission Denied") );
2396 return $self->_AddLink(%args);
2399 sub __GetTicketFromURI {
2401 my %args = ( URI => '', @_ );
2403 # If the other URI is an RT::Ticket, we want to make sure the user
2404 # can modify it too...
2405 my $uri_obj = RT::URI->new( $self->CurrentUser );
2406 $uri_obj->FromURI( $args{'URI'} );
2408 unless ( $uri_obj->Resolver && $uri_obj->Scheme ) {
2409 my $msg = $self->loc( "Couldn't resolve '[_1]' into a URI.", $args{'URI'} );
2410 $RT::Logger->warning( $msg );
2413 my $obj = $uri_obj->Resolver->Object;
2414 unless ( UNIVERSAL::isa($obj, 'RT::Ticket') && $obj->id ) {
2415 return (1, 'Found not a ticket', undef);
2417 return (1, 'Found ticket', $obj);
2422 Private non-acled variant of AddLink so that links can be added during create.
2428 my %args = ( Target => '',
2432 SilentBase => undef,
2433 SilentTarget => undef,
2436 my ($val, $msg, $exist) = $self->SUPER::_AddLink(%args);
2437 return ($val, $msg) if !$val || $exist;
2438 return ($val, $msg) if $args{'Silent'};
2440 my ($direction, $remote_link);
2441 if ( $args{'Target'} ) {
2442 $remote_link = $args{'Target'};
2443 $direction = 'Base';
2444 } elsif ( $args{'Base'} ) {
2445 $remote_link = $args{'Base'};
2446 $direction = 'Target';
2449 my $remote_uri = RT::URI->new( $self->CurrentUser );
2450 $remote_uri->FromURI( $remote_link );
2452 unless ( $args{ 'Silent'. $direction } ) {
2453 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
2455 Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
2456 NewValue => $remote_uri->URI || $remote_link,
2459 $RT::Logger->error("Couldn't create transaction: $Msg") unless $Trans;
2462 if ( !$args{ 'Silent'. ( $direction eq 'Target'? 'Base': 'Target' ) } && $remote_uri->IsLocal ) {
2463 my $OtherObj = $remote_uri->Object;
2464 my ( $val, $msg ) = $OtherObj->_NewTransaction(
2466 Field => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
2467 : $LINKDIRMAP{$args{'Type'}}->{Target},
2468 NewValue => $self->URI,
2469 ActivateScrips => !RT->Config->Get('LinkTransactionsRun1Scrip'),
2472 $RT::Logger->error("Couldn't create transaction: $msg") unless $val;
2475 return ( $val, $msg );
2485 MergeInto take the id of the ticket to merge this ticket into.
2491 my $ticket_id = shift;
2493 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2494 return ( 0, $self->loc("Permission Denied") );
2497 # Load up the new ticket.
2498 my $MergeInto = RT::Ticket->new($self->CurrentUser);
2499 $MergeInto->Load($ticket_id);
2501 # make sure it exists.
2502 unless ( $MergeInto->Id ) {
2503 return ( 0, $self->loc("New ticket doesn't exist") );
2506 # Make sure the current user can modify the new ticket.
2507 unless ( $MergeInto->CurrentUserHasRight('ModifyTicket') ) {
2508 return ( 0, $self->loc("Permission Denied") );
2511 delete $MERGE_CACHE{'effective'}{ $self->id };
2512 delete @{ $MERGE_CACHE{'merged'} }{
2513 $ticket_id, $MergeInto->id, $self->id
2516 $RT::Handle->BeginTransaction();
2518 # We use EffectiveId here even though it duplicates information from
2519 # the links table becasue of the massive performance hit we'd take
2520 # by trying to do a separate database query for merge info everytime
2523 #update this ticket's effective id to the new ticket's id.
2524 my ( $id_val, $id_msg ) = $self->__Set(
2525 Field => 'EffectiveId',
2526 Value => $MergeInto->Id()
2530 $RT::Handle->Rollback();
2531 return ( 0, $self->loc("Merge failed. Couldn't set EffectiveId") );
2535 if ( $self->__Value('Status') ne 'resolved' ) {
2537 my ( $status_val, $status_msg )
2538 = $self->__Set( Field => 'Status', Value => 'resolved' );
2540 unless ($status_val) {
2541 $RT::Handle->Rollback();
2544 "[_1] couldn't set status to resolved. RT's Database may be inconsistent.",
2548 return ( 0, $self->loc("Merge failed. Couldn't set Status") );
2552 # update all the links that point to that old ticket
2553 my $old_links_to = RT::Links->new($self->CurrentUser);
2554 $old_links_to->Limit(FIELD => 'Target', VALUE => $self->URI);
2557 while (my $link = $old_links_to->Next) {
2558 if (exists $old_seen{$link->Base."-".$link->Type}) {
2561 elsif ($link->Base eq $MergeInto->URI) {
2564 # First, make sure the link doesn't already exist. then move it over.
2565 my $tmp = RT::Link->new($RT::SystemUser);
2566 $tmp->LoadByCols(Base => $link->Base, Type => $link->Type, LocalTarget => $MergeInto->id);
2570 $link->SetTarget($MergeInto->URI);
2571 $link->SetLocalTarget($MergeInto->id);
2573 $old_seen{$link->Base."-".$link->Type} =1;
2578 my $old_links_from = RT::Links->new($self->CurrentUser);
2579 $old_links_from->Limit(FIELD => 'Base', VALUE => $self->URI);
2581 while (my $link = $old_links_from->Next) {
2582 if (exists $old_seen{$link->Type."-".$link->Target}) {
2585 if ($link->Target eq $MergeInto->URI) {
2588 # First, make sure the link doesn't already exist. then move it over.
2589 my $tmp = RT::Link->new($RT::SystemUser);
2590 $tmp->LoadByCols(Target => $link->Target, Type => $link->Type, LocalBase => $MergeInto->id);
2594 $link->SetBase($MergeInto->URI);
2595 $link->SetLocalBase($MergeInto->id);
2596 $old_seen{$link->Type."-".$link->Target} =1;
2602 # Update time fields
2603 foreach my $type (qw(TimeEstimated TimeWorked TimeLeft)) {
2605 my $mutator = "Set$type";
2606 $MergeInto->$mutator(
2607 ( $MergeInto->$type() || 0 ) + ( $self->$type() || 0 ) );
2610 #add all of this ticket's watchers to that ticket.
2611 foreach my $watcher_type (qw(Requestors Cc AdminCc)) {
2613 my $people = $self->$watcher_type->MembersObj;
2614 my $addwatcher_type = $watcher_type;
2615 $addwatcher_type =~ s/s$//;
2617 while ( my $watcher = $people->Next ) {
2619 my ($val, $msg) = $MergeInto->_AddWatcher(
2620 Type => $addwatcher_type,
2622 PrincipalId => $watcher->MemberId
2625 $RT::Logger->warning($msg);
2631 #find all of the tickets that were merged into this ticket.
2632 my $old_mergees = new RT::Tickets( $self->CurrentUser );
2633 $old_mergees->Limit(
2634 FIELD => 'EffectiveId',
2639 # update their EffectiveId fields to the new ticket's id
2640 while ( my $ticket = $old_mergees->Next() ) {
2641 my ( $val, $msg ) = $ticket->__Set(
2642 Field => 'EffectiveId',
2643 Value => $MergeInto->Id()
2647 #make a new link: this ticket is merged into that other ticket.
2648 $self->AddLink( Type => 'MergedInto', Target => $MergeInto->Id());
2650 $MergeInto->_SetLastUpdated;
2652 $RT::Handle->Commit();
2653 return ( 1, $self->loc("Merge Successful") );
2658 Returns list of tickets' ids that's been merged into this ticket.
2666 return @{ $MERGE_CACHE{'merged'}{ $id } }
2667 if $MERGE_CACHE{'merged'}{ $id };
2669 my $mergees = RT::Tickets->new( $self->CurrentUser );
2671 FIELD => 'EffectiveId',
2679 return @{ $MERGE_CACHE{'merged'}{ $id } ||= [] }
2680 = map $_->id, @{ $mergees->ItemsArrayRef || [] };
2687 # {{{ Routines dealing with ownership
2693 Takes nothing and returns an RT::User object of
2701 #If this gets ACLed, we lose on a rights check in User.pm and
2702 #get deep recursion. if we need ACLs here, we need
2703 #an equiv without ACLs
2705 my $owner = new RT::User( $self->CurrentUser );
2706 $owner->Load( $self->__Value('Owner') );
2708 #Return the owner object
2714 # {{{ sub OwnerAsString
2716 =head2 OwnerAsString
2718 Returns the owner's email address
2724 return ( $self->OwnerObj->EmailAddress );
2734 Takes two arguments:
2735 the Id or Name of the owner
2736 and (optionally) the type of the SetOwner Transaction. It defaults
2737 to 'Give'. 'Steal' is also a valid option.
2744 my $NewOwner = shift;
2745 my $Type = shift || "Give";
2747 $RT::Handle->BeginTransaction();
2749 $self->_SetLastUpdated(); # lock the ticket
2750 $self->Load( $self->id ); # in case $self changed while waiting for lock
2752 my $OldOwnerObj = $self->OwnerObj;
2754 my $NewOwnerObj = RT::User->new( $self->CurrentUser );
2755 $NewOwnerObj->Load( $NewOwner );
2756 unless ( $NewOwnerObj->Id ) {
2757 $RT::Handle->Rollback();
2758 return ( 0, $self->loc("That user does not exist") );
2762 # must have ModifyTicket rights
2763 # or TakeTicket/StealTicket and $NewOwner is self
2764 # see if it's a take
2765 if ( $OldOwnerObj->Id == $RT::Nobody->Id ) {
2766 unless ( $self->CurrentUserHasRight('ModifyTicket')
2767 || $self->CurrentUserHasRight('TakeTicket') ) {
2768 $RT::Handle->Rollback();
2769 return ( 0, $self->loc("Permission Denied") );
2773 # see if it's a steal
2774 elsif ( $OldOwnerObj->Id != $RT::Nobody->Id
2775 && $OldOwnerObj->Id != $self->CurrentUser->id ) {
2777 unless ( $self->CurrentUserHasRight('ModifyTicket')
2778 || $self->CurrentUserHasRight('StealTicket') ) {
2779 $RT::Handle->Rollback();
2780 return ( 0, $self->loc("Permission Denied") );
2784 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2785 $RT::Handle->Rollback();
2786 return ( 0, $self->loc("Permission Denied") );
2790 # If we're not stealing and the ticket has an owner and it's not
2792 if ( $Type ne 'Steal' and $Type ne 'Force'
2793 and $OldOwnerObj->Id != $RT::Nobody->Id
2794 and $OldOwnerObj->Id != $self->CurrentUser->Id )
2796 $RT::Handle->Rollback();
2797 return ( 0, $self->loc("You can only take tickets that are unowned") )
2798 if $NewOwnerObj->id == $self->CurrentUser->id;
2801 $self->loc("You can only reassign tickets that you own or that are unowned" )
2805 #If we've specified a new owner and that user can't modify the ticket
2806 elsif ( !$NewOwnerObj->HasRight( Right => 'OwnTicket', Object => $self ) ) {
2807 $RT::Handle->Rollback();
2808 return ( 0, $self->loc("That user may not own tickets in that queue") );
2811 # If the ticket has an owner and it's the new owner, we don't need
2813 elsif ( $NewOwnerObj->Id == $OldOwnerObj->Id ) {
2814 $RT::Handle->Rollback();
2815 return ( 0, $self->loc("That user already owns that ticket") );
2818 # Delete the owner in the owner group, then add a new one
2819 # TODO: is this safe? it's not how we really want the API to work
2820 # for most things, but it's fast.
2821 my ( $del_id, $del_msg );
2822 for my $owner (@{$self->OwnerGroup->MembersObj->ItemsArrayRef}) {
2823 ($del_id, $del_msg) = $owner->Delete();
2824 last unless ($del_id);
2828 $RT::Handle->Rollback();
2829 return ( 0, $self->loc("Could not change owner: [_1]", $del_msg) );
2832 my ( $add_id, $add_msg ) = $self->OwnerGroup->_AddMember(
2833 PrincipalId => $NewOwnerObj->PrincipalId,
2834 InsideTransaction => 1 );
2836 $RT::Handle->Rollback();
2837 return ( 0, $self->loc("Could not change owner: [_1]", $add_msg ) );
2840 # We call set twice with slightly different arguments, so
2841 # as to not have an SQL transaction span two RT transactions
2843 my ( $val, $msg ) = $self->_Set(
2845 RecordTransaction => 0,
2846 Value => $NewOwnerObj->Id,
2848 TransactionType => $Type,
2849 CheckACL => 0, # don't check acl
2853 $RT::Handle->Rollback;
2854 return ( 0, $self->loc("Could not change owner: [_1]", $msg) );
2857 ($val, $msg) = $self->_NewTransaction(
2860 NewValue => $NewOwnerObj->Id,
2861 OldValue => $OldOwnerObj->Id,
2866 $msg = $self->loc( "Owner changed from [_1] to [_2]",
2867 $OldOwnerObj->Name, $NewOwnerObj->Name );
2870 $RT::Handle->Rollback();
2874 $RT::Handle->Commit();
2876 return ( $val, $msg );
2885 A convenince method to set the ticket's owner to the current user
2891 return ( $self->SetOwner( $self->CurrentUser->Id, 'Take' ) );
2900 Convenience method to set the owner to 'nobody' if the current user is the owner.
2906 return ( $self->SetOwner( $RT::Nobody->UserObj->Id, 'Untake' ) );
2915 A convenience method to change the owner of the current ticket to the
2916 current user. Even if it's owned by another user.
2923 if ( $self->IsOwner( $self->CurrentUser ) ) {
2924 return ( 0, $self->loc("You already own this ticket") );
2927 return ( $self->SetOwner( $self->CurrentUser->Id, 'Steal' ) );
2937 # {{{ Routines dealing with status
2939 # {{{ sub ValidateStatus
2941 =head2 ValidateStatus STATUS
2943 Takes a string. Returns true if that status is a valid status for this ticket.
2944 Returns false otherwise.
2948 sub ValidateStatus {
2952 #Make sure the status passed in is valid
2953 unless ( $self->QueueObj->IsValidStatus($status) ) {
2965 =head2 SetStatus STATUS
2967 Set this ticket\'s status. STATUS can be one of: new, open, stalled, resolved, rejected or deleted.
2969 Alternatively, you can pass in a list of named parameters (Status => STATUS, Force => FORCE). If FORCE is true, ignore unresolved dependencies and force a status change.
2980 $args{Status} = shift;
2987 if ( $args{Status} eq 'deleted') {
2988 unless ($self->CurrentUserHasRight('DeleteTicket')) {
2989 return ( 0, $self->loc('Permission Denied') );
2992 unless ($self->CurrentUserHasRight('ModifyTicket')) {
2993 return ( 0, $self->loc('Permission Denied') );
2997 if (!$args{Force} && ($args{'Status'} eq 'resolved') && $self->HasUnresolvedDependencies) {
2998 return (0, $self->loc('That ticket has unresolved dependencies'));
3001 my $now = RT::Date->new( $self->CurrentUser );
3004 #If we're changing the status from new, record that we've started
3005 if ( $self->Status eq 'new' && $args{Status} ne 'new' ) {
3007 #Set the Started time to "now"
3008 $self->_Set( Field => 'Started',
3010 RecordTransaction => 0 );
3013 #When we close a ticket, set the 'Resolved' attribute to now.
3014 # It's misnamed, but that's just historical.
3015 if ( $self->QueueObj->IsInactiveStatus($args{Status}) ) {
3016 $self->_Set( Field => 'Resolved',
3018 RecordTransaction => 0 );
3021 #Actually update the status
3022 my ($val, $msg)= $self->_Set( Field => 'Status',
3023 Value => $args{Status},
3026 TransactionType => 'Status' );
3037 Takes no arguments. Marks this ticket for garbage collection
3043 return ( $self->SetStatus('deleted') );
3045 # TODO: garbage collection
3054 Sets this ticket's status to stalled
3060 return ( $self->SetStatus('stalled') );
3069 Sets this ticket's status to rejected
3075 return ( $self->SetStatus('rejected') );
3084 Sets this ticket\'s status to Open
3090 return ( $self->SetStatus('open') );
3099 Sets this ticket\'s status to Resolved
3105 return ( $self->SetStatus('resolved') );
3113 # {{{ Actions + Routines dealing with transactions
3115 # {{{ sub SetTold and _SetTold
3117 =head2 SetTold ISO [TIMETAKEN]
3119 Updates the told and records a transaction
3126 $told = shift if (@_);
3127 my $timetaken = shift || 0;
3129 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
3130 return ( 0, $self->loc("Permission Denied") );
3133 my $datetold = new RT::Date( $self->CurrentUser );
3135 $datetold->Set( Format => 'iso',
3139 $datetold->SetToNow();
3142 return ( $self->_Set( Field => 'Told',
3143 Value => $datetold->ISO,
3144 TimeTaken => $timetaken,
3145 TransactionType => 'Told' ) );
3150 Updates the told without a transaction or acl check. Useful when we're sending replies.
3157 my $now = new RT::Date( $self->CurrentUser );
3160 #use __Set to get no ACLs ;)
3161 return ( $self->__Set( Field => 'Told',
3162 Value => $now->ISO ) );
3172 my $uid = $self->CurrentUser->id;
3173 my $attr = $self->FirstAttribute( "User-". $uid ."-SeenUpTo" );
3174 return if $attr && $attr->Content gt $self->LastUpdated;
3176 my $txns = $self->Transactions;
3177 $txns->Limit( FIELD => 'Type', VALUE => 'Comment' );
3178 $txns->Limit( FIELD => 'Type', VALUE => 'Correspond' );
3179 $txns->Limit( FIELD => 'Creator', OPERATOR => '!=', VALUE => $uid );
3183 VALUE => $attr->Content
3185 $txns->RowsPerPage(1);
3186 return $txns->First;
3191 =head2 TransactionBatch
3193 Returns an array reference of all transactions created on this ticket during
3194 this ticket object's lifetime or since last application of a batch, or undef
3197 Only works when the C<UseTransactionBatch> config option is set to true.
3201 sub TransactionBatch {
3203 return $self->{_TransactionBatch};
3206 =head2 ApplyTransactionBatch
3208 Applies scrips on the current batch of transactions and shinks it. Usually
3209 batch is applied when object is destroyed, but in some cases it's too late.
3213 sub ApplyTransactionBatch {
3216 my $batch = $self->TransactionBatch;
3217 return unless $batch && @$batch;
3219 $self->_ApplyTransactionBatch;
3221 $self->{_TransactionBatch} = [];
3224 sub _ApplyTransactionBatch {
3226 my $batch = $self->TransactionBatch;
3229 my $types = join ',', grep !$seen{$_}++, grep defined, map $_->__Value('Type'), grep defined, @{$batch};
3232 RT::Scrips->new($RT::SystemUser)->Apply(
3233 Stage => 'TransactionBatch',
3235 TransactionObj => $batch->[0],
3239 # Entry point of the rule system
3240 my $rules = RT::Ruleset->FindAllRules(
3241 Stage => 'TransactionBatch',
3243 TransactionObj => $batch->[0],
3246 RT::Ruleset->CommitRules($rules);
3252 # DESTROY methods need to localize $@, or it may unset it. This
3253 # causes $m->abort to not bubble all of the way up. See perlbug
3254 # http://rt.perl.org/rt3/Ticket/Display.html?id=17650
3257 # The following line eliminates reentrancy.
3258 # It protects against the fact that perl doesn't deal gracefully
3259 # when an object's refcount is changed in its destructor.
3260 return if $self->{_Destroyed}++;
3262 my $batch = $self->TransactionBatch;
3263 return unless $batch && @$batch;
3265 return $self->_ApplyTransactionBatch;
3270 # {{{ PRIVATE UTILITY METHODS. Mostly needed so Ticket can be a DBIx::Record
3272 # {{{ sub _OverlayAccessible
3274 sub _OverlayAccessible {
3276 EffectiveId => { 'read' => 1, 'write' => 1, 'public' => 1 },
3277 Queue => { 'read' => 1, 'write' => 1 },
3278 Requestors => { 'read' => 1, 'write' => 1 },
3279 Owner => { 'read' => 1, 'write' => 1 },
3280 Subject => { 'read' => 1, 'write' => 1 },
3281 InitialPriority => { 'read' => 1, 'write' => 1 },
3282 FinalPriority => { 'read' => 1, 'write' => 1 },
3283 Priority => { 'read' => 1, 'write' => 1 },
3284 Status => { 'read' => 1, 'write' => 1 },
3285 TimeEstimated => { 'read' => 1, 'write' => 1 },
3286 TimeWorked => { 'read' => 1, 'write' => 1 },
3287 TimeLeft => { 'read' => 1, 'write' => 1 },
3288 Told => { 'read' => 1, 'write' => 1 },
3289 Resolved => { 'read' => 1 },
3290 Type => { 'read' => 1 },
3291 Starts => { 'read' => 1, 'write' => 1 },
3292 Started => { 'read' => 1, 'write' => 1 },
3293 Due => { 'read' => 1, 'write' => 1 },
3294 Creator => { 'read' => 1, 'auto' => 1 },
3295 Created => { 'read' => 1, 'auto' => 1 },
3296 LastUpdatedBy => { 'read' => 1, 'auto' => 1 },
3297 LastUpdated => { 'read' => 1, 'auto' => 1 }
3309 my %args = ( Field => undef,
3312 RecordTransaction => 1,
3315 TransactionType => 'Set',
3318 if ($args{'CheckACL'}) {
3319 unless ( $self->CurrentUserHasRight('ModifyTicket')) {
3320 return ( 0, $self->loc("Permission Denied"));
3324 unless ($args{'UpdateTicket'} || $args{'RecordTransaction'}) {
3325 $RT::Logger->error("Ticket->_Set called without a mandate to record an update or update the ticket");
3326 return(0, $self->loc("Internal Error"));
3329 #if the user is trying to modify the record
3331 #Take care of the old value we really don't want to get in an ACL loop.
3332 # so ask the super::_Value
3333 my $Old = $self->SUPER::_Value("$args{'Field'}");
3336 if ( $args{'UpdateTicket'} ) {
3339 ( $ret, $msg ) = $self->SUPER::_Set( Field => $args{'Field'},
3340 Value => $args{'Value'} );
3342 #If we can't actually set the field to the value, don't record
3343 # a transaction. instead, get out of here.
3344 return ( 0, $msg ) unless $ret;
3347 if ( $args{'RecordTransaction'} == 1 ) {
3349 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
3350 Type => $args{'TransactionType'},
3351 Field => $args{'Field'},
3352 NewValue => $args{'Value'},
3354 TimeTaken => $args{'TimeTaken'},
3356 return ( $Trans, scalar $TransObj->BriefDescription );
3359 return ( $ret, $msg );
3369 Takes the name of a table column.
3370 Returns its value as a string, if the user passes an ACL check
3379 #if the field is public, return it.
3380 if ( $self->_Accessible( $field, 'public' ) ) {
3382 #$RT::Logger->debug("Skipping ACL check for $field");
3383 return ( $self->SUPER::_Value($field) );
3387 #If the current user doesn't have ACLs, don't let em at it.
3389 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
3392 return ( $self->SUPER::_Value($field) );
3398 # {{{ sub _UpdateTimeTaken
3400 =head2 _UpdateTimeTaken
3402 This routine will increment the timeworked counter. it should
3403 only be called from _NewTransaction
3407 sub _UpdateTimeTaken {
3409 my $Minutes = shift;
3412 $Total = $self->SUPER::_Value("TimeWorked");
3413 $Total = ( $Total || 0 ) + ( $Minutes || 0 );
3415 Field => "TimeWorked",
3426 # {{{ Routines dealing with ACCESS CONTROL
3428 # {{{ sub CurrentUserHasRight
3430 =head2 CurrentUserHasRight
3432 Takes the textual name of a Ticket scoped right (from RT::ACE) and returns
3433 1 if the user has that right. It returns 0 if the user doesn't have that right.
3437 sub CurrentUserHasRight {
3441 return $self->CurrentUser->PrincipalObj->HasRight(
3453 Takes a paramhash with the attributes 'Right' and 'Principal'
3454 'Right' is a ticket-scoped textual right from RT::ACE
3455 'Principal' is an RT::User object
3457 Returns 1 if the principal has the right. Returns undef if not.
3469 unless ( ( defined $args{'Principal'} ) and ( ref( $args{'Principal'} ) ) )
3471 Carp::cluck("Principal attrib undefined for Ticket::HasRight");
3472 $RT::Logger->crit("Principal attrib undefined for Ticket::HasRight");
3477 $args{'Principal'}->HasRight(
3479 Right => $args{'Right'}
3490 Return the Reminders object for this ticket. (It's an RT::Reminders object.)
3491 It isn't acutally a searchbuilder collection itself.
3498 unless ($self->{'__reminders'}) {
3499 $self->{'__reminders'} = RT::Reminders->new($self->CurrentUser);
3500 $self->{'__reminders'}->Ticket($self->id);
3502 return $self->{'__reminders'};
3508 # {{{ sub Transactions
3512 Returns an RT::Transactions object of all transactions on this ticket
3519 my $transactions = RT::Transactions->new( $self->CurrentUser );
3521 #If the user has no rights, return an empty object
3522 if ( $self->CurrentUserHasRight('ShowTicket') ) {
3523 $transactions->LimitToTicket($self->id);
3525 # if the user may not see comments do not return them
3526 unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
3527 $transactions->Limit(
3533 $transactions->Limit(
3537 VALUE => "CommentEmailRecord",
3538 ENTRYAGGREGATOR => 'AND'
3543 $transactions->Limit(
3547 ENTRYAGGREGATOR => 'AND'
3551 return ($transactions);
3557 # {{{ TransactionCustomFields
3559 =head2 TransactionCustomFields
3561 Returns the custom fields that transactions on tickets will have.
3565 sub TransactionCustomFields {
3567 return $self->QueueObj->TicketTransactionCustomFields;
3572 # {{{ sub CustomFieldValues
3574 =head2 CustomFieldValues
3576 # Do name => id mapping (if needed) before falling back to
3577 # RT::Record's CustomFieldValues
3583 sub CustomFieldValues {
3587 return $self->SUPER::CustomFieldValues( $field ) if !$field || $field =~ /^\d+$/;
3589 my $cf = RT::CustomField->new( $self->CurrentUser );
3590 $cf->SetContextObject( $self );
3591 $cf->LoadByNameAndQueue( Name => $field, Queue => $self->Queue );
3592 unless ( $cf->id ) {
3593 $cf->LoadByNameAndQueue( Name => $field, Queue => 0 );
3596 # If we didn't find a valid cfid, give up.
3597 return RT::ObjectCustomFieldValues->new( $self->CurrentUser ) unless $cf->id;
3599 return $self->SUPER::CustomFieldValues( $cf->id );
3604 # {{{ sub CustomFieldLookupType
3606 =head2 CustomFieldLookupType
3608 Returns the RT::Ticket lookup type, which can be passed to
3609 RT::CustomField->Create() via the 'LookupType' hash key.
3615 sub CustomFieldLookupType {
3616 "RT::Queue-RT::Ticket";
3619 =head2 ACLEquivalenceObjects
3621 This method returns a list of objects for which a user's rights also apply
3622 to this ticket. Generally, this is only the ticket's queue, but some RT
3623 extensions may make other objects available too.
3625 This method is called from L<RT::Principal/HasRight>.
3629 sub ACLEquivalenceObjects {
3631 return $self->QueueObj;
3640 Jesse Vincent, jesse@bestpractical.com