Dev to 4.0.11
[usit-rt.git] / lib / RT / Ticket.pm
CommitLineData
84fb5b46
MKG
1# BEGIN BPS TAGGED BLOCK {{{
2#
3# COPYRIGHT:
4#
403d7b0b 5# This software is Copyright (c) 1996-2013 Best Practical Solutions, LLC
84fb5b46
MKG
6# <sales@bestpractical.com>
7#
8# (Except where explicitly superseded by other copyright notices)
9#
10#
11# LICENSE:
12#
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
16# from www.gnu.org.
17#
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.
22#
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.
28#
29#
30# CONTRIBUTION SUBMISSION POLICY:
31#
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.)
37#
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.
46#
47# END BPS TAGGED BLOCK }}}
48
49=head1 SYNOPSIS
50
51 use RT::Ticket;
52 my $ticket = RT::Ticket->new($CurrentUser);
53 $ticket->Load($ticket_id);
54
55=head1 DESCRIPTION
56
403d7b0b 57This module lets you manipulate RT's ticket object.
84fb5b46
MKG
58
59
60=head1 METHODS
61
62
63=cut
64
65
66package RT::Ticket;
67
68use strict;
69use warnings;
70
71
72use RT::Queue;
73use RT::User;
74use RT::Record;
75use RT::Links;
76use RT::Date;
77use RT::CustomFields;
78use RT::Tickets;
79use RT::Transactions;
80use RT::Reminders;
81use RT::URI::fsck_com_rt;
82use RT::URI;
83use MIME::Entity;
84use Devel::GlobalDestruction;
85
86
87# A helper table for links mapping to make it easier
88# to build and parse links between tickets
89
90our %LINKTYPEMAP = (
91 MemberOf => { Type => 'MemberOf',
92 Mode => 'Target', },
93 Parents => { Type => 'MemberOf',
94 Mode => 'Target', },
95 Members => { Type => 'MemberOf',
96 Mode => 'Base', },
97 Children => { Type => 'MemberOf',
98 Mode => 'Base', },
99 HasMember => { Type => 'MemberOf',
100 Mode => 'Base', },
101 RefersTo => { Type => 'RefersTo',
102 Mode => 'Target', },
103 ReferredToBy => { Type => 'RefersTo',
104 Mode => 'Base', },
105 DependsOn => { Type => 'DependsOn',
106 Mode => 'Target', },
107 DependedOnBy => { Type => 'DependsOn',
108 Mode => 'Base', },
109 MergedInto => { Type => 'MergedInto',
110 Mode => 'Target', },
111
112);
113
114
115# A helper table for links mapping to make it easier
116# to build and parse links between tickets
117
118our %LINKDIRMAP = (
119 MemberOf => { Base => 'MemberOf',
120 Target => 'HasMember', },
121 RefersTo => { Base => 'RefersTo',
122 Target => 'ReferredToBy', },
123 DependsOn => { Base => 'DependsOn',
124 Target => 'DependedOnBy', },
125 MergedInto => { Base => 'MergedInto',
126 Target => 'MergedInto', },
127
128);
129
130
131sub LINKTYPEMAP { return \%LINKTYPEMAP }
132sub LINKDIRMAP { return \%LINKDIRMAP }
133
134our %MERGE_CACHE = (
135 effective => {},
136 merged => {},
137);
138
139
140=head2 Load
141
142Takes a single argument. This can be a ticket id, ticket alias or
143local ticket uri. If the ticket can't be loaded, returns undef.
144Otherwise, returns the ticket id.
145
146=cut
147
148sub Load {
149 my $self = shift;
150 my $id = shift;
151 $id = '' unless defined $id;
152
153 # TODO: modify this routine to look at EffectiveId and
154 # do the recursive load thing. be careful to cache all
155 # the interim tickets we try so we don't loop forever.
156
157 unless ( $id =~ /^\d+$/ ) {
158 $RT::Logger->debug("Tried to load a bogus ticket id: '$id'");
159 return (undef);
160 }
161
162 $id = $MERGE_CACHE{'effective'}{ $id }
163 if $MERGE_CACHE{'effective'}{ $id };
164
165 my ($ticketid, $msg) = $self->LoadById( $id );
166 unless ( $self->Id ) {
167 $RT::Logger->debug("$self tried to load a bogus ticket: $id");
168 return (undef);
169 }
170
171 #If we're merged, resolve the merge.
172 if ( $self->EffectiveId && $self->EffectiveId != $self->Id ) {
173 $RT::Logger->debug(
174 "We found a merged ticket. "
175 . $self->id ."/". $self->EffectiveId
176 );
177 my $real_id = $self->Load( $self->EffectiveId );
178 $MERGE_CACHE{'effective'}{ $id } = $real_id;
179 return $real_id;
180 }
181
182 #Ok. we're loaded. lets get outa here.
183 return $self->Id;
184}
185
186
187
188=head2 Create (ARGS)
189
190Arguments: ARGS is a hash of named parameters. Valid parameters are:
191
192 id
193 Queue - Either a Queue object or a Queue Name
194 Requestor - A reference to a list of email addresses or RT user Names
195 Cc - A reference to a list of email addresses or Names
196 AdminCc - A reference to a list of email addresses or Names
197 SquelchMailTo - A reference to a list of email addresses -
198 who should this ticket not mail
403d7b0b
MKG
199 Type -- The ticket's type. ignore this for now
200 Owner -- This ticket's owner. either an RT::User object or this user's id
84fb5b46
MKG
201 Subject -- A string describing the subject of the ticket
202 Priority -- an integer from 0 to 99
203 InitialPriority -- an integer from 0 to 99
204 FinalPriority -- an integer from 0 to 99
205 Status -- any valid status (Defined in RT::Queue)
206 TimeEstimated -- an integer. estimated time for this task in minutes
207 TimeWorked -- an integer. time worked so far in minutes
208 TimeLeft -- an integer. time remaining in minutes
403d7b0b
MKG
209 Starts -- an ISO date describing the ticket's start date and time in GMT
210 Due -- an ISO date describing the ticket's due date and time in GMT
84fb5b46
MKG
211 MIMEObj -- a MIME::Entity object with the content of the initial ticket request.
212 CustomField-<n> -- a scalar or array of values for the customfield with the id <n>
213
214Ticket links can be set up during create by passing the link type as a hask key and
215the ticket id to be linked to as a value (or a URI when linking to other objects).
216Multiple links of the same type can be created by passing an array ref. For example:
217
218 Parents => 45,
219 DependsOn => [ 15, 22 ],
220 RefersTo => 'http://www.bestpractical.com',
221
222Supported link types are C<MemberOf>, C<HasMember>, C<RefersTo>, C<ReferredToBy>,
223C<DependsOn> and C<DependedOnBy>. Also, C<Parents> is alias for C<MemberOf> and
224C<Members> and C<Children> are aliases for C<HasMember>.
225
226Returns: TICKETID, Transaction Object, Error Message
227
228
229=cut
230
231sub Create {
232 my $self = shift;
233
234 my %args = (
235 id => undef,
236 EffectiveId => undef,
237 Queue => undef,
238 Requestor => undef,
239 Cc => undef,
240 AdminCc => undef,
241 SquelchMailTo => undef,
242 TransSquelchMailTo => undef,
243 Type => 'ticket',
244 Owner => undef,
245 Subject => '',
246 InitialPriority => undef,
247 FinalPriority => undef,
248 Priority => undef,
249 Status => undef,
250 TimeWorked => "0",
251 TimeLeft => 0,
252 TimeEstimated => 0,
253 Due => undef,
254 Starts => undef,
255 Started => undef,
256 Resolved => undef,
257 MIMEObj => undef,
258 _RecordTransaction => 1,
259 DryRun => 0,
260 @_
261 );
262
263 my ($ErrStr, @non_fatal_errors);
264
265 my $QueueObj = RT::Queue->new( RT->SystemUser );
266 if ( ref $args{'Queue'} eq 'RT::Queue' ) {
267 $QueueObj->Load( $args{'Queue'}->Id );
268 }
269 elsif ( $args{'Queue'} ) {
270 $QueueObj->Load( $args{'Queue'} );
271 }
272 else {
273 $RT::Logger->debug("'". ( $args{'Queue'} ||''). "' not a recognised queue object." );
274 }
275
276 #Can't create a ticket without a queue.
277 unless ( $QueueObj->Id ) {
278 $RT::Logger->debug("$self No queue given for ticket creation.");
279 return ( 0, 0, $self->loc('Could not create ticket. Queue not set') );
280 }
281
282
283 #Now that we have a queue, Check the ACLS
284 unless (
285 $self->CurrentUser->HasRight(
286 Right => 'CreateTicket',
287 Object => $QueueObj
288 )
289 )
290 {
291 return (
292 0, 0,
293 $self->loc( "No permission to create tickets in the queue '[_1]'", $QueueObj->Name));
294 }
295
296 my $cycle = $QueueObj->Lifecycle;
297 unless ( defined $args{'Status'} && length $args{'Status'} ) {
298 $args{'Status'} = $cycle->DefaultOnCreate;
299 }
300
301 unless ( $cycle->IsValid( $args{'Status'} ) ) {
302 return ( 0, 0,
303 $self->loc("Status '[_1]' isn't a valid status for tickets in this queue.",
304 $self->loc($args{'Status'}))
305 );
306 }
307
308 unless ( $cycle->IsTransition( '' => $args{'Status'} ) ) {
309 return ( 0, 0,
310 $self->loc("New tickets can not have status '[_1]' in this queue.",
311 $self->loc($args{'Status'}))
312 );
313 }
314
315
316
317 #Since we have a queue, we can set queue defaults
318
319 #Initial Priority
320 # If there's no queue default initial priority and it's not set, set it to 0
321 $args{'InitialPriority'} = $QueueObj->InitialPriority || 0
322 unless defined $args{'InitialPriority'};
323
324 #Final priority
325 # If there's no queue default final priority and it's not set, set it to 0
326 $args{'FinalPriority'} = $QueueObj->FinalPriority || 0
327 unless defined $args{'FinalPriority'};
328
329 # Priority may have changed from InitialPriority, for the case
330 # where we're importing tickets (eg, from an older RT version.)
331 $args{'Priority'} = $args{'InitialPriority'}
332 unless defined $args{'Priority'};
333
334 # Dates
335 #TODO we should see what sort of due date we're getting, rather +
336 # than assuming it's in ISO format.
337
338 #Set the due date. if we didn't get fed one, use the queue default due in
339 my $Due = RT::Date->new( $self->CurrentUser );
340 if ( defined $args{'Due'} ) {
341 $Due->Set( Format => 'ISO', Value => $args{'Due'} );
342 }
343 elsif ( my $due_in = $QueueObj->DefaultDueIn ) {
344 $Due->SetToNow;
345 $Due->AddDays( $due_in );
346 }
347
348 my $Starts = RT::Date->new( $self->CurrentUser );
349 if ( defined $args{'Starts'} ) {
350 $Starts->Set( Format => 'ISO', Value => $args{'Starts'} );
351 }
352
353 my $Started = RT::Date->new( $self->CurrentUser );
354 if ( defined $args{'Started'} ) {
355 $Started->Set( Format => 'ISO', Value => $args{'Started'} );
356 }
357
358 # If the status is not an initial status, set the started date
359 elsif ( !$cycle->IsInitial($args{'Status'}) ) {
360 $Started->SetToNow;
361 }
362
363 my $Resolved = RT::Date->new( $self->CurrentUser );
364 if ( defined $args{'Resolved'} ) {
365 $Resolved->Set( Format => 'ISO', Value => $args{'Resolved'} );
366 }
367
368 #If the status is an inactive status, set the resolved date
369 elsif ( $cycle->IsInactive( $args{'Status'} ) )
370 {
371 $RT::Logger->debug( "Got a ". $args{'Status'}
372 ."(inactive) ticket with undefined resolved date. Setting to now."
373 );
374 $Resolved->SetToNow;
375 }
376
377 # }}}
378
379 # Dealing with time fields
380
381 $args{'TimeEstimated'} = 0 unless defined $args{'TimeEstimated'};
382 $args{'TimeWorked'} = 0 unless defined $args{'TimeWorked'};
383 $args{'TimeLeft'} = 0 unless defined $args{'TimeLeft'};
384
385 # }}}
386
387 # Deal with setting the owner
388
389 my $Owner;
390 if ( ref( $args{'Owner'} ) eq 'RT::User' ) {
391 if ( $args{'Owner'}->id ) {
392 $Owner = $args{'Owner'};
393 } else {
394 $RT::Logger->error('Passed an empty RT::User for owner');
395 push @non_fatal_errors,
396 $self->loc("Owner could not be set.") . " ".
397 $self->loc("Invalid value for [_1]",loc('owner'));
398 $Owner = undef;
399 }
400 }
401
402 #If we've been handed something else, try to load the user.
403 elsif ( $args{'Owner'} ) {
404 $Owner = RT::User->new( $self->CurrentUser );
405 $Owner->Load( $args{'Owner'} );
406 if (!$Owner->id) {
407 $Owner->LoadByEmail( $args{'Owner'} )
408 }
409 unless ( $Owner->Id ) {
410 push @non_fatal_errors,
411 $self->loc("Owner could not be set.") . " "
412 . $self->loc( "User '[_1]' could not be found.", $args{'Owner'} );
413 $Owner = undef;
414 }
415 }
416
417 #If we have a proposed owner and they don't have the right
418 #to own a ticket, scream about it and make them not the owner
419
420 my $DeferOwner;
421 if ( $Owner && $Owner->Id != RT->Nobody->Id
422 && !$Owner->HasRight( Object => $QueueObj, Right => 'OwnTicket' ) )
423 {
424 $DeferOwner = $Owner;
425 $Owner = undef;
426 $RT::Logger->debug('going to deffer setting owner');
427
428 }
429
430 #If we haven't been handed a valid owner, make it nobody.
431 unless ( defined($Owner) && $Owner->Id ) {
432 $Owner = RT::User->new( $self->CurrentUser );
433 $Owner->Load( RT->Nobody->Id );
434 }
435
436 # }}}
437
438# We attempt to load or create each of the people who might have a role for this ticket
439# _outside_ the transaction, so we don't get into ticket creation races
440 foreach my $type ( "Cc", "AdminCc", "Requestor" ) {
441 $args{ $type } = [ $args{ $type } ] unless ref $args{ $type };
442 foreach my $watcher ( splice @{ $args{$type} } ) {
443 next unless $watcher;
444 if ( $watcher =~ /^\d+$/ ) {
445 push @{ $args{$type} }, $watcher;
446 } else {
447 my @addresses = RT::EmailParser->ParseEmailAddress( $watcher );
448 foreach my $address( @addresses ) {
449 my $user = RT::User->new( RT->SystemUser );
450 my ($uid, $msg) = $user->LoadOrCreateByEmail( $address );
451 unless ( $uid ) {
452 push @non_fatal_errors,
453 $self->loc("Couldn't load or create user: [_1]", $msg);
454 } else {
455 push @{ $args{$type} }, $user->id;
456 }
457 }
458 }
459 }
460 }
461
462 $RT::Handle->BeginTransaction();
463
464 my %params = (
465 Queue => $QueueObj->Id,
466 Owner => $Owner->Id,
467 Subject => $args{'Subject'},
468 InitialPriority => $args{'InitialPriority'},
469 FinalPriority => $args{'FinalPriority'},
470 Priority => $args{'Priority'},
471 Status => $args{'Status'},
472 TimeWorked => $args{'TimeWorked'},
473 TimeEstimated => $args{'TimeEstimated'},
474 TimeLeft => $args{'TimeLeft'},
475 Type => $args{'Type'},
476 Starts => $Starts->ISO,
477 Started => $Started->ISO,
478 Resolved => $Resolved->ISO,
479 Due => $Due->ISO
480 );
481
482# Parameters passed in during an import that we probably don't want to touch, otherwise
483 foreach my $attr (qw(id Creator Created LastUpdated LastUpdatedBy)) {
484 $params{$attr} = $args{$attr} if $args{$attr};
485 }
486
487 # Delete null integer parameters
488 foreach my $attr
489 (qw(TimeWorked TimeLeft TimeEstimated InitialPriority FinalPriority))
490 {
491 delete $params{$attr}
492 unless ( exists $params{$attr} && $params{$attr} );
493 }
494
495 # Delete the time worked if we're counting it in the transaction
496 delete $params{'TimeWorked'} if $args{'_RecordTransaction'};
497
498 my ($id,$ticket_message) = $self->SUPER::Create( %params );
499 unless ($id) {
500 $RT::Logger->crit( "Couldn't create a ticket: " . $ticket_message );
501 $RT::Handle->Rollback();
502 return ( 0, 0,
503 $self->loc("Ticket could not be created due to an internal error")
504 );
505 }
506
507 #Set the ticket's effective ID now that we've created it.
508 my ( $val, $msg ) = $self->__Set(
509 Field => 'EffectiveId',
510 Value => ( $args{'EffectiveId'} || $id )
511 );
512 unless ( $val ) {
513 $RT::Logger->crit("Couldn't set EffectiveId: $msg");
514 $RT::Handle->Rollback;
515 return ( 0, 0,
516 $self->loc("Ticket could not be created due to an internal error")
517 );
518 }
519
520 my $create_groups_ret = $self->_CreateTicketGroups();
521 unless ($create_groups_ret) {
522 $RT::Logger->crit( "Couldn't create ticket groups for ticket "
523 . $self->Id
524 . ". aborting Ticket creation." );
525 $RT::Handle->Rollback();
526 return ( 0, 0,
527 $self->loc("Ticket could not be created due to an internal error")
528 );
529 }
530
531 # Set the owner in the Groups table
532 # We denormalize it into the Ticket table too because doing otherwise would
533 # kill performance, bigtime. It gets kept in lockstep thanks to the magic of transactionalization
534 $self->OwnerGroup->_AddMember(
535 PrincipalId => $Owner->PrincipalId,
536 InsideTransaction => 1
537 ) unless $DeferOwner;
538
539
540
541 # Deal with setting up watchers
542
543 foreach my $type ( "Cc", "AdminCc", "Requestor" ) {
544 # we know it's an array ref
545 foreach my $watcher ( @{ $args{$type} } ) {
546
547 # Note that we're using AddWatcher, rather than _AddWatcher, as we
548 # actually _want_ that ACL check. Otherwise, random ticket creators
549 # could make themselves adminccs and maybe get ticket rights. that would
550 # be poor
551 my $method = $type eq 'AdminCc'? 'AddWatcher': '_AddWatcher';
552
553 my ($val, $msg) = $self->$method(
554 Type => $type,
555 PrincipalId => $watcher,
556 Silent => 1,
557 );
558 push @non_fatal_errors, $self->loc("Couldn't set [_1] watcher: [_2]", $type, $msg)
559 unless $val;
560 }
561 }
562
563 if ($args{'SquelchMailTo'}) {
564 my @squelch = ref( $args{'SquelchMailTo'} ) ? @{ $args{'SquelchMailTo'} }
565 : $args{'SquelchMailTo'};
566 $self->_SquelchMailTo( @squelch );
567 }
568
569
570 # }}}
571
572 # Add all the custom fields
573
574 foreach my $arg ( keys %args ) {
575 next unless $arg =~ /^CustomField-(\d+)$/i;
576 my $cfid = $1;
577
578 foreach my $value (
579 UNIVERSAL::isa( $args{$arg} => 'ARRAY' ) ? @{ $args{$arg} } : ( $args{$arg} ) )
580 {
581 next unless defined $value && length $value;
582
583 # Allow passing in uploaded LargeContent etc by hash reference
584 my ($status, $msg) = $self->_AddCustomFieldValue(
585 (UNIVERSAL::isa( $value => 'HASH' )
586 ? %$value
587 : (Value => $value)
588 ),
589 Field => $cfid,
590 RecordTransaction => 0,
591 );
592 push @non_fatal_errors, $msg unless $status;
593 }
594 }
595
596 # }}}
597
598 # Deal with setting up links
599
600 # TODO: Adding link may fire scrips on other end and those scrips
601 # could create transactions on this ticket before 'Create' transaction.
602 #
603 # We should implement different lifecycle: record 'Create' transaction,
604 # create links and only then fire create transaction's scrips.
605 #
606 # Ideal variant: add all links without firing scrips, record create
607 # transaction and only then fire scrips on the other ends of links.
608 #
609 # //RUZ
610
611 foreach my $type ( keys %LINKTYPEMAP ) {
612 next unless ( defined $args{$type} );
613 foreach my $link (
614 ref( $args{$type} ) ? @{ $args{$type} } : ( $args{$type} ) )
615 {
616 my ( $val, $msg, $obj ) = $self->__GetTicketFromURI( URI => $link );
617 unless ($val) {
618 push @non_fatal_errors, $msg;
619 next;
620 }
621
622 # Check rights on the other end of the link if we must
623 # then run _AddLink that doesn't check for ACLs
624 if ( RT->Config->Get( 'StrictLinkACL' ) ) {
625 if ( $obj && !$obj->CurrentUserHasRight('ModifyTicket') ) {
626 push @non_fatal_errors, $self->loc('Linking. Permission denied');
627 next;
628 }
629 }
630
631 if ( $obj && $obj->Status eq 'deleted' ) {
632 push @non_fatal_errors,
633 $self->loc("Linking. Can't link to a deleted ticket");
634 next;
635 }
636
637 my ( $wval, $wmsg ) = $self->_AddLink(
638 Type => $LINKTYPEMAP{$type}->{'Type'},
639 $LINKTYPEMAP{$type}->{'Mode'} => $link,
640 Silent => !$args{'_RecordTransaction'} || $self->Type eq 'reminder',
641 'Silent'. ( $LINKTYPEMAP{$type}->{'Mode'} eq 'Base'? 'Target': 'Base' )
642 => 1,
643 );
644
645 push @non_fatal_errors, $wmsg unless ($wval);
646 }
647 }
648
649 # }}}
650 # Now that we've created the ticket and set up its metadata, we can actually go and check OwnTicket on the ticket itself.
651 # This might be different than before in cases where extensions like RTIR are doing clever things with RT's ACL system
652 if ( $DeferOwner ) {
653 if (!$DeferOwner->HasRight( Object => $self, Right => 'OwnTicket')) {
654
655 $RT::Logger->warning( "User " . $DeferOwner->Name . "(" . $DeferOwner->id
656 . ") was proposed as a ticket owner but has no rights to own "
657 . "tickets in " . $QueueObj->Name );
658 push @non_fatal_errors, $self->loc(
659 "Owner '[_1]' does not have rights to own this ticket.",
660 $DeferOwner->Name
661 );
662 } else {
663 $Owner = $DeferOwner;
664 $self->__Set(Field => 'Owner', Value => $Owner->id);
665
666 }
667 $self->OwnerGroup->_AddMember(
668 PrincipalId => $Owner->PrincipalId,
669 InsideTransaction => 1
670 );
671 }
672
673 if ( $args{'_RecordTransaction'} ) {
674
675 # Add a transaction for the create
676 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
677 Type => "Create",
678 TimeTaken => $args{'TimeWorked'},
679 MIMEObj => $args{'MIMEObj'},
680 CommitScrips => !$args{'DryRun'},
681 SquelchMailTo => $args{'TransSquelchMailTo'},
682 );
683
684 if ( $self->Id && $Trans ) {
685
686 $TransObj->UpdateCustomFields(ARGSRef => \%args);
687
688 $RT::Logger->info( "Ticket " . $self->Id . " created in queue '" . $QueueObj->Name . "' by " . $self->CurrentUser->Name );
689 $ErrStr = $self->loc( "Ticket [_1] created in queue '[_2]'", $self->Id, $QueueObj->Name );
690 $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
691 }
692 else {
693 $RT::Handle->Rollback();
694
695 $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
696 $RT::Logger->error("Ticket couldn't be created: $ErrStr");
697 return ( 0, 0, $self->loc( "Ticket could not be created due to an internal error"));
698 }
699
700 if ( $args{'DryRun'} ) {
701 $RT::Handle->Rollback();
702 return ($self->id, $TransObj, $ErrStr);
703 }
704 $RT::Handle->Commit();
705 return ( $self->Id, $TransObj->Id, $ErrStr );
706
707 # }}}
708 }
709 else {
710
711 # Not going to record a transaction
712 $RT::Handle->Commit();
713 $ErrStr = $self->loc( "Ticket [_1] created in queue '[_2]'", $self->Id, $QueueObj->Name );
714 $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
715 return ( $self->Id, 0, $ErrStr );
716
717 }
718}
719
720
721
722
723=head2 _Parse822HeadersForAttributes Content
724
725Takes an RFC822 style message and parses its attributes into a hash.
726
727=cut
728
729sub _Parse822HeadersForAttributes {
730 my $self = shift;
731 my $content = shift;
732 my %args;
733
734 my @lines = ( split ( /\n/, $content ) );
735 while ( defined( my $line = shift @lines ) ) {
736 if ( $line =~ /^(.*?):(?:\s+(.*))?$/ ) {
737 my $value = $2;
738 my $tag = lc($1);
739
740 $tag =~ s/-//g;
741 if ( defined( $args{$tag} ) )
742 { #if we're about to get a second value, make it an array
743 $args{$tag} = [ $args{$tag} ];
744 }
745 if ( ref( $args{$tag} ) )
746 { #If it's an array, we want to push the value
747 push @{ $args{$tag} }, $value;
748 }
749 else { #if there's nothing there, just set the value
750 $args{$tag} = $value;
751 }
752 } elsif ($line =~ /^$/) {
753
754 #TODO: this won't work, since "" isn't of the form "foo:value"
755
756 while ( defined( my $l = shift @lines ) ) {
757 push @{ $args{'content'} }, $l;
758 }
759 }
760
761 }
762
763 foreach my $date (qw(due starts started resolved)) {
764 my $dateobj = RT::Date->new(RT->SystemUser);
765 if ( defined ($args{$date}) and $args{$date} =~ /^\d+$/ ) {
766 $dateobj->Set( Format => 'unix', Value => $args{$date} );
767 }
768 else {
769 $dateobj->Set( Format => 'unknown', Value => $args{$date} );
770 }
771 $args{$date} = $dateobj->ISO;
772 }
773 $args{'mimeobj'} = MIME::Entity->new();
774 $args{'mimeobj'}->build(
775 Type => ( $args{'contenttype'} || 'text/plain' ),
776 Data => ($args{'content'} || '')
777 );
778
779 return (%args);
780}
781
782
783
784=head2 Import PARAMHASH
785
786Import a ticket.
403d7b0b
MKG
787Doesn't create a transaction.
788Doesn't supply queue defaults, etc.
84fb5b46
MKG
789
790Returns: TICKETID
791
792=cut
793
794sub Import {
795 my $self = shift;
796 my ( $ErrStr, $QueueObj, $Owner );
797
798 my %args = (
799 id => undef,
800 EffectiveId => undef,
801 Queue => undef,
802 Requestor => undef,
803 Type => 'ticket',
804 Owner => RT->Nobody->Id,
805 Subject => '[no subject]',
806 InitialPriority => undef,
807 FinalPriority => undef,
808 Status => 'new',
809 TimeWorked => "0",
810 Due => undef,
811 Created => undef,
812 Updated => undef,
813 Resolved => undef,
814 Told => undef,
815 @_
816 );
817
818 if ( ( defined( $args{'Queue'} ) ) && ( !ref( $args{'Queue'} ) ) ) {
819 $QueueObj = RT::Queue->new(RT->SystemUser);
820 $QueueObj->Load( $args{'Queue'} );
821
403d7b0b 822 #TODO error check this and return 0 if it's not loading properly +++
84fb5b46
MKG
823 }
824 elsif ( ref( $args{'Queue'} ) eq 'RT::Queue' ) {
825 $QueueObj = RT::Queue->new(RT->SystemUser);
826 $QueueObj->Load( $args{'Queue'}->Id );
827 }
828 else {
829 $RT::Logger->debug(
830 "$self " . $args{'Queue'} . " not a recognised queue object." );
831 }
832
833 #Can't create a ticket without a queue.
834 unless ( defined($QueueObj) and $QueueObj->Id ) {
835 $RT::Logger->debug("$self No queue given for ticket creation.");
836 return ( 0, $self->loc('Could not create ticket. Queue not set') );
837 }
838
839 #Now that we have a queue, Check the ACLS
840 unless (
841 $self->CurrentUser->HasRight(
842 Right => 'CreateTicket',
843 Object => $QueueObj
844 )
845 )
846 {
847 return ( 0,
848 $self->loc("No permission to create tickets in the queue '[_1]'"
849 , $QueueObj->Name));
850 }
851
852 # Deal with setting the owner
853
854 # Attempt to take user object, user name or user id.
855 # Assign to nobody if lookup fails.
856 if ( defined( $args{'Owner'} ) ) {
857 if ( ref( $args{'Owner'} ) ) {
858 $Owner = $args{'Owner'};
859 }
860 else {
861 $Owner = RT::User->new( $self->CurrentUser );
862 $Owner->Load( $args{'Owner'} );
863 if ( !defined( $Owner->id ) ) {
864 $Owner->Load( RT->Nobody->id );
865 }
866 }
867 }
868
869 #If we have a proposed owner and they don't have the right
870 #to own a ticket, scream about it and make them not the owner
871 if (
872 ( defined($Owner) )
873 and ( $Owner->Id != RT->Nobody->Id )
874 and (
875 !$Owner->HasRight(
876 Object => $QueueObj,
877 Right => 'OwnTicket'
878 )
879 )
880 )
881 {
882
883 $RT::Logger->warning( "$self user "
884 . $Owner->Name . "("
885 . $Owner->id
886 . ") was proposed "
887 . "as a ticket owner but has no rights to own "
888 . "tickets in '"
889 . $QueueObj->Name . "'" );
890
891 $Owner = undef;
892 }
893
894 #If we haven't been handed a valid owner, make it nobody.
895 unless ( defined($Owner) ) {
896 $Owner = RT::User->new( $self->CurrentUser );
897 $Owner->Load( RT->Nobody->UserObj->Id );
898 }
899
900 # }}}
901
902 unless ( $self->ValidateStatus( $args{'Status'} ) ) {
903 return ( 0, $self->loc("'[_1]' is an invalid value for status", $args{'Status'}) );
904 }
905
906 $self->{'_AccessibleCache'}{Created} = { 'read' => 1, 'write' => 1 };
907 $self->{'_AccessibleCache'}{Creator} = { 'read' => 1, 'auto' => 1 };
908 $self->{'_AccessibleCache'}{LastUpdated} = { 'read' => 1, 'write' => 1 };
909 $self->{'_AccessibleCache'}{LastUpdatedBy} = { 'read' => 1, 'auto' => 1 };
910
911 # If we're coming in with an id, set that now.
912 my $EffectiveId = undef;
913 if ( $args{'id'} ) {
914 $EffectiveId = $args{'id'};
915
916 }
917
918 my $id = $self->SUPER::Create(
919 id => $args{'id'},
920 EffectiveId => $EffectiveId,
921 Queue => $QueueObj->Id,
922 Owner => $Owner->Id,
923 Subject => $args{'Subject'}, # loc
924 InitialPriority => $args{'InitialPriority'}, # loc
925 FinalPriority => $args{'FinalPriority'}, # loc
926 Priority => $args{'InitialPriority'}, # loc
927 Status => $args{'Status'}, # loc
928 TimeWorked => $args{'TimeWorked'}, # loc
929 Type => $args{'Type'}, # loc
930 Created => $args{'Created'}, # loc
931 Told => $args{'Told'}, # loc
932 LastUpdated => $args{'Updated'}, # loc
933 Resolved => $args{'Resolved'}, # loc
934 Due => $args{'Due'}, # loc
935 );
936
937 # If the ticket didn't have an id
938 # Set the ticket's effective ID now that we've created it.
939 if ( $args{'id'} ) {
940 $self->Load( $args{'id'} );
941 }
942 else {
943 my ( $val, $msg ) =
944 $self->__Set( Field => 'EffectiveId', Value => $id );
945
946 unless ($val) {
947 $RT::Logger->err(
948 $self . "->Import couldn't set EffectiveId: $msg" );
949 }
950 }
951
952 my $create_groups_ret = $self->_CreateTicketGroups();
953 unless ($create_groups_ret) {
954 $RT::Logger->crit(
955 "Couldn't create ticket groups for ticket " . $self->Id );
956 }
957
958 $self->OwnerGroup->_AddMember( PrincipalId => $Owner->PrincipalId );
959
960 foreach my $watcher ( @{ $args{'Cc'} } ) {
961 $self->_AddWatcher( Type => 'Cc', Email => $watcher, Silent => 1 );
962 }
963 foreach my $watcher ( @{ $args{'AdminCc'} } ) {
964 $self->_AddWatcher( Type => 'AdminCc', Email => $watcher,
965 Silent => 1 );
966 }
967 foreach my $watcher ( @{ $args{'Requestor'} } ) {
968 $self->_AddWatcher( Type => 'Requestor', Email => $watcher,
969 Silent => 1 );
970 }
971
972 return ( $self->Id, $ErrStr );
973}
974
975
976
977
978=head2 _CreateTicketGroups
979
980Create the ticket groups and links for this ticket.
981This routine expects to be called from Ticket->Create _inside of a transaction_
982
983It will create four groups for this ticket: Requestor, Cc, AdminCc and Owner.
984
985It will return true on success and undef on failure.
986
987
988=cut
989
990
991sub _CreateTicketGroups {
992 my $self = shift;
993
994 my @types = (qw(Requestor Owner Cc AdminCc));
995
996 foreach my $type (@types) {
997 my $type_obj = RT::Group->new($self->CurrentUser);
998 my ($id, $msg) = $type_obj->CreateRoleGroup(Domain => 'RT::Ticket-Role',
999 Instance => $self->Id,
1000 Type => $type);
1001 unless ($id) {
1002 $RT::Logger->error("Couldn't create a ticket group of type '$type' for ticket ".
1003 $self->Id.": ".$msg);
1004 return(undef);
1005 }
1006 }
1007 return(1);
1008
1009}
1010
1011
1012
1013=head2 OwnerGroup
1014
1015A constructor which returns an RT::Group object containing the owner of this ticket.
1016
1017=cut
1018
1019sub OwnerGroup {
1020 my $self = shift;
1021 my $owner_obj = RT::Group->new($self->CurrentUser);
1022 $owner_obj->LoadTicketRoleGroup( Ticket => $self->Id, Type => 'Owner');
1023 return ($owner_obj);
1024}
1025
1026
1027
1028
1029=head2 AddWatcher
1030
1031AddWatcher takes a parameter hash. The keys are as follows:
1032
1033Type One of Requestor, Cc, AdminCc
1034
1035PrincipalId The RT::Principal id of the user or group that's being added as a watcher
1036
1037Email The email address of the new watcher. If a user with this
1038 email address can't be found, a new nonprivileged user will be created.
1039
403d7b0b 1040If 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.
84fb5b46
MKG
1041
1042=cut
1043
1044sub AddWatcher {
1045 my $self = shift;
1046 my %args = (
1047 Type => undef,
1048 PrincipalId => undef,
1049 Email => undef,
1050 @_
1051 );
1052
1053 # ModifyTicket works in any case
1054 return $self->_AddWatcher( %args )
1055 if $self->CurrentUserHasRight('ModifyTicket');
1056 if ( $args{'Email'} ) {
1057 my ($addr) = RT::EmailParser->ParseEmailAddress( $args{'Email'} );
1058 return (0, $self->loc("Couldn't parse address from '[_1]' string", $args{'Email'} ))
1059 unless $addr;
1060
dab09ea8 1061 if ( lc $self->CurrentUser->EmailAddress
84fb5b46
MKG
1062 eq lc RT::User->CanonicalizeEmailAddress( $addr->address ) )
1063 {
1064 $args{'PrincipalId'} = $self->CurrentUser->id;
1065 delete $args{'Email'};
1066 }
1067 }
1068
1069 # If the watcher isn't the current user then the current user has no right
1070 # bail
1071 unless ( $args{'PrincipalId'} && $self->CurrentUser->id == $args{'PrincipalId'} ) {
1072 return ( 0, $self->loc("Permission Denied") );
1073 }
1074
1075 # If it's an AdminCc and they don't have 'WatchAsAdminCc', bail
1076 if ( $args{'Type'} eq 'AdminCc' ) {
1077 unless ( $self->CurrentUserHasRight('WatchAsAdminCc') ) {
1078 return ( 0, $self->loc('Permission Denied') );
1079 }
1080 }
1081
1082 # If it's a Requestor or Cc and they don't have 'Watch', bail
1083 elsif ( $args{'Type'} eq 'Cc' || $args{'Type'} eq 'Requestor' ) {
1084 unless ( $self->CurrentUserHasRight('Watch') ) {
1085 return ( 0, $self->loc('Permission Denied') );
1086 }
1087 }
1088 else {
1089 $RT::Logger->warning( "AddWatcher got passed a bogus type");
1090 return ( 0, $self->loc('Error in parameters to Ticket->AddWatcher') );
1091 }
1092
1093 return $self->_AddWatcher( %args );
1094}
1095
1096#This contains the meat of AddWatcher. but can be called from a routine like
1097# Create, which doesn't need the additional acl check
1098sub _AddWatcher {
1099 my $self = shift;
1100 my %args = (
1101 Type => undef,
1102 Silent => undef,
1103 PrincipalId => undef,
1104 Email => undef,
1105 @_
1106 );
1107
1108
1109 my $principal = RT::Principal->new($self->CurrentUser);
1110 if ($args{'Email'}) {
1111 if ( RT::EmailParser->IsRTAddress( $args{'Email'} ) ) {
1112 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'})));
1113 }
1114 my $user = RT::User->new(RT->SystemUser);
1115 my ($pid, $msg) = $user->LoadOrCreateByEmail( $args{'Email'} );
1116 $args{'PrincipalId'} = $pid if $pid;
1117 }
1118 if ($args{'PrincipalId'}) {
1119 $principal->Load($args{'PrincipalId'});
1120 if ( $principal->id and $principal->IsUser and my $email = $principal->Object->EmailAddress ) {
1121 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'})))
1122 if RT::EmailParser->IsRTAddress( $email );
1123
1124 }
1125 }
1126
1127
1128 # If we can't find this watcher, we need to bail.
1129 unless ($principal->Id) {
1130 $RT::Logger->error("Could not load create a user with the email address '".$args{'Email'}. "' to add as a watcher for ticket ".$self->Id);
1131 return(0, $self->loc("Could not find or create that user"));
1132 }
1133
1134
1135 my $group = RT::Group->new($self->CurrentUser);
1136 $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->Id);
1137 unless ($group->id) {
1138 return(0,$self->loc("Group not found"));
1139 }
1140
1141 if ( $group->HasMember( $principal)) {
1142
403d7b0b
MKG
1143 return ( 0, $self->loc('[_1] is already a [_2] for this ticket',
1144 $principal->Object->Name, $self->loc($args{'Type'})) );
84fb5b46
MKG
1145 }
1146
1147
1148 my ( $m_id, $m_msg ) = $group->_AddMember( PrincipalId => $principal->Id,
1149 InsideTransaction => 1 );
1150 unless ($m_id) {
1151 $RT::Logger->error("Failed to add ".$principal->Id." as a member of group ".$group->Id.": ".$m_msg);
1152
403d7b0b
MKG
1153 return ( 0, $self->loc('Could not make [_1] a [_2] for this ticket',
1154 $principal->Object->Name, $self->loc($args{'Type'})) );
84fb5b46
MKG
1155 }
1156
1157 unless ( $args{'Silent'} ) {
1158 $self->_NewTransaction(
1159 Type => 'AddWatcher',
1160 NewValue => $principal->Id,
1161 Field => $args{'Type'}
1162 );
1163 }
1164
403d7b0b
MKG
1165 return ( 1, $self->loc('Added [_1] as a [_2] for this ticket',
1166 $principal->Object->Name, $self->loc($args{'Type'})) );
84fb5b46
MKG
1167}
1168
1169
1170
1171
1172=head2 DeleteWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL_ADDRESS }
1173
1174
1175Deletes a Ticket watcher. Takes two arguments:
1176
1177Type (one of Requestor,Cc,AdminCc)
1178
1179and one of
1180
1181PrincipalId (an RT::Principal Id of the watcher you want to remove)
1182 OR
1183Email (the email address of an existing wathcer)
1184
1185
1186=cut
1187
1188
1189sub DeleteWatcher {
1190 my $self = shift;
1191
1192 my %args = ( Type => undef,
1193 PrincipalId => undef,
1194 Email => undef,
1195 @_ );
1196
1197 unless ( $args{'PrincipalId'} || $args{'Email'} ) {
1198 return ( 0, $self->loc("No principal specified") );
1199 }
1200 my $principal = RT::Principal->new( $self->CurrentUser );
1201 if ( $args{'PrincipalId'} ) {
1202
1203 $principal->Load( $args{'PrincipalId'} );
1204 }
1205 else {
1206 my $user = RT::User->new( $self->CurrentUser );
1207 $user->LoadByEmail( $args{'Email'} );
1208 $principal->Load( $user->Id );
1209 }
1210
1211 # If we can't find this watcher, we need to bail.
1212 unless ( $principal->Id ) {
1213 return ( 0, $self->loc("Could not find that principal") );
1214 }
1215
1216 my $group = RT::Group->new( $self->CurrentUser );
1217 $group->LoadTicketRoleGroup( Type => $args{'Type'}, Ticket => $self->Id );
1218 unless ( $group->id ) {
1219 return ( 0, $self->loc("Group not found") );
1220 }
1221
1222 # Check ACLS
1223 #If the watcher we're trying to add is for the current user
1224 if ( $self->CurrentUser->PrincipalId == $principal->id ) {
1225
1226 # If it's an AdminCc and they don't have
1227 # 'WatchAsAdminCc' or 'ModifyTicket', bail
1228 if ( $args{'Type'} eq 'AdminCc' ) {
1229 unless ( $self->CurrentUserHasRight('ModifyTicket')
1230 or $self->CurrentUserHasRight('WatchAsAdminCc') ) {
1231 return ( 0, $self->loc('Permission Denied') );
1232 }
1233 }
1234
1235 # If it's a Requestor or Cc and they don't have
1236 # 'Watch' or 'ModifyTicket', bail
1237 elsif ( ( $args{'Type'} eq 'Cc' ) or ( $args{'Type'} eq 'Requestor' ) )
1238 {
1239 unless ( $self->CurrentUserHasRight('ModifyTicket')
1240 or $self->CurrentUserHasRight('Watch') ) {
1241 return ( 0, $self->loc('Permission Denied') );
1242 }
1243 }
1244 else {
dab09ea8 1245 $RT::Logger->warning("$self -> DeleteWatcher got passed a bogus type");
84fb5b46
MKG
1246 return ( 0,
1247 $self->loc('Error in parameters to Ticket->DeleteWatcher') );
1248 }
1249 }
1250
1251 # If the watcher isn't the current user
1252 # and the current user doesn't have 'ModifyTicket' bail
1253 else {
1254 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1255 return ( 0, $self->loc("Permission Denied") );
1256 }
1257 }
1258
1259 # }}}
1260
1261 # see if this user is already a watcher.
1262
1263 unless ( $group->HasMember($principal) ) {
1264 return ( 0,
403d7b0b
MKG
1265 $self->loc( '[_1] is not a [_2] for this ticket',
1266 $principal->Object->Name, $args{'Type'} ) );
84fb5b46
MKG
1267 }
1268
1269 my ( $m_id, $m_msg ) = $group->_DeleteMember( $principal->Id );
1270 unless ($m_id) {
1271 $RT::Logger->error( "Failed to delete "
1272 . $principal->Id
1273 . " as a member of group "
1274 . $group->Id . ": "
1275 . $m_msg );
1276
1277 return (0,
1278 $self->loc(
403d7b0b
MKG
1279 'Could not remove [_1] as a [_2] for this ticket',
1280 $principal->Object->Name, $args{'Type'} ) );
84fb5b46
MKG
1281 }
1282
1283 unless ( $args{'Silent'} ) {
1284 $self->_NewTransaction( Type => 'DelWatcher',
1285 OldValue => $principal->Id,
1286 Field => $args{'Type'} );
1287 }
1288
1289 return ( 1,
1290 $self->loc( "[_1] is no longer a [_2] for this ticket.",
1291 $principal->Object->Name,
1292 $args{'Type'} ) );
1293}
1294
1295
1296
1297
1298
1299=head2 SquelchMailTo [EMAIL]
1300
1301Takes an optional email address to never email about updates to this ticket.
1302
1303
1304Returns an array of the RT::Attribute objects for this ticket's 'SquelchMailTo' attributes.
1305
1306
1307=cut
1308
1309sub SquelchMailTo {
1310 my $self = shift;
1311 if (@_) {
1312 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1313 return ();
1314 }
1315 } else {
1316 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1317 return ();
1318 }
1319
1320 }
1321 return $self->_SquelchMailTo(@_);
1322}
1323
1324sub _SquelchMailTo {
1325 my $self = shift;
1326 if (@_) {
1327 my $attr = shift;
1328 $self->AddAttribute( Name => 'SquelchMailTo', Content => $attr )
1329 unless grep { $_->Content eq $attr }
1330 $self->Attributes->Named('SquelchMailTo');
1331 }
1332 my @attributes = $self->Attributes->Named('SquelchMailTo');
1333 return (@attributes);
1334}
1335
1336
1337=head2 UnsquelchMailTo ADDRESS
1338
1339Takes an address and removes it from this ticket's "SquelchMailTo" list. If an address appears multiple times, each instance is removed.
1340
1341Returns a tuple of (status, message)
1342
1343=cut
1344
1345sub UnsquelchMailTo {
1346 my $self = shift;
1347
1348 my $address = shift;
1349 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1350 return ( 0, $self->loc("Permission Denied") );
1351 }
1352
1353 my ($val, $msg) = $self->Attributes->DeleteEntry ( Name => 'SquelchMailTo', Content => $address);
1354 return ($val, $msg);
1355}
1356
1357
1358
1359=head2 RequestorAddresses
1360
403d7b0b 1361B<Returns> String: All Ticket Requestor email addresses as a string.
84fb5b46
MKG
1362
1363=cut
1364
1365sub RequestorAddresses {
1366 my $self = shift;
1367
1368 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1369 return undef;
1370 }
1371
1372 return ( $self->Requestors->MemberEmailAddressesAsString );
1373}
1374
1375
1376=head2 AdminCcAddresses
1377
1378returns String: All Ticket AdminCc email addresses as a string
1379
1380=cut
1381
1382sub AdminCcAddresses {
1383 my $self = shift;
1384
1385 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1386 return undef;
1387 }
1388
1389 return ( $self->AdminCc->MemberEmailAddressesAsString )
1390
1391}
1392
1393=head2 CcAddresses
1394
1395returns String: All Ticket Ccs as a string of email addresses
1396
1397=cut
1398
1399sub CcAddresses {
1400 my $self = shift;
1401
1402 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1403 return undef;
1404 }
1405 return ( $self->Cc->MemberEmailAddressesAsString);
1406
1407}
1408
1409
1410
1411
1412=head2 Requestors
1413
1414Takes nothing.
1415Returns this ticket's Requestors as an RT::Group object
1416
1417=cut
1418
1419sub Requestors {
1420 my $self = shift;
1421
1422 my $group = RT::Group->new($self->CurrentUser);
1423 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1424 $group->LoadTicketRoleGroup(Type => 'Requestor', Ticket => $self->Id);
1425 }
1426 return ($group);
1427
1428}
1429
1430
1431
1432=head2 Cc
1433
1434Takes nothing.
1435Returns an RT::Group object which contains this ticket's Ccs.
1436If the user doesn't have "ShowTicket" permission, returns an empty group
1437
1438=cut
1439
1440sub Cc {
1441 my $self = shift;
1442
1443 my $group = RT::Group->new($self->CurrentUser);
1444 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1445 $group->LoadTicketRoleGroup(Type => 'Cc', Ticket => $self->Id);
1446 }
1447 return ($group);
1448
1449}
1450
1451
1452
1453=head2 AdminCc
1454
1455Takes nothing.
1456Returns an RT::Group object which contains this ticket's AdminCcs.
1457If the user doesn't have "ShowTicket" permission, returns an empty group
1458
1459=cut
1460
1461sub AdminCc {
1462 my $self = shift;
1463
1464 my $group = RT::Group->new($self->CurrentUser);
1465 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1466 $group->LoadTicketRoleGroup(Type => 'AdminCc', Ticket => $self->Id);
1467 }
1468 return ($group);
1469
1470}
1471
1472
1473
1474
1475# a generic routine to be called by IsRequestor, IsCc and IsAdminCc
1476
1477=head2 IsWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL }
1478
1479Takes a param hash with the attributes Type and either PrincipalId or Email
1480
1481Type is one of Requestor, Cc, AdminCc and Owner
1482
1483PrincipalId is an RT::Principal id, and Email is an email address.
1484
1485Returns true if the specified principal (or the one corresponding to the
1486specified address) is a member of the group Type for this ticket.
1487
1488XX TODO: This should be Memoized.
1489
1490=cut
1491
1492sub IsWatcher {
1493 my $self = shift;
1494
1495 my %args = ( Type => 'Requestor',
1496 PrincipalId => undef,
1497 Email => undef,
1498 @_
1499 );
1500
1501 # Load the relevant group.
1502 my $group = RT::Group->new($self->CurrentUser);
1503 $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->id);
1504
1505 # Find the relevant principal.
1506 if (!$args{PrincipalId} && $args{Email}) {
1507 # Look up the specified user.
1508 my $user = RT::User->new($self->CurrentUser);
1509 $user->LoadByEmail($args{Email});
1510 if ($user->Id) {
1511 $args{PrincipalId} = $user->PrincipalId;
1512 }
1513 else {
1514 # A non-existent user can't be a group member.
1515 return 0;
1516 }
1517 }
1518
1519 # Ask if it has the member in question
1520 return $group->HasMember( $args{'PrincipalId'} );
1521}
1522
1523
1524
1525=head2 IsRequestor PRINCIPAL_ID
1526
1527Takes an L<RT::Principal> id.
1528
1529Returns true if the principal is a requestor of the current ticket.
1530
1531=cut
1532
1533sub IsRequestor {
1534 my $self = shift;
1535 my $person = shift;
1536
1537 return ( $self->IsWatcher( Type => 'Requestor', PrincipalId => $person ) );
1538
1539};
1540
1541
1542
1543=head2 IsCc PRINCIPAL_ID
1544
1545 Takes an RT::Principal id.
1546 Returns true if the principal is a Cc of the current ticket.
1547
1548
1549=cut
1550
1551sub IsCc {
1552 my $self = shift;
1553 my $cc = shift;
1554
1555 return ( $self->IsWatcher( Type => 'Cc', PrincipalId => $cc ) );
1556
1557}
1558
1559
1560
1561=head2 IsAdminCc PRINCIPAL_ID
1562
1563 Takes an RT::Principal id.
1564 Returns true if the principal is an AdminCc of the current ticket.
1565
1566=cut
1567
1568sub IsAdminCc {
1569 my $self = shift;
1570 my $person = shift;
1571
1572 return ( $self->IsWatcher( Type => 'AdminCc', PrincipalId => $person ) );
1573
1574}
1575
1576
1577
1578=head2 IsOwner
1579
1580 Takes an RT::User object. Returns true if that user is this ticket's owner.
1581returns undef otherwise
1582
1583=cut
1584
1585sub IsOwner {
1586 my $self = shift;
1587 my $person = shift;
1588
1589 # no ACL check since this is used in acl decisions
1590 # unless ($self->CurrentUserHasRight('ShowTicket')) {
1591 # return(undef);
1592 # }
1593
1594 #Tickets won't yet have owners when they're being created.
1595 unless ( $self->OwnerObj->id ) {
1596 return (undef);
1597 }
1598
1599 if ( $person->id == $self->OwnerObj->id ) {
1600 return (1);
1601 }
1602 else {
1603 return (undef);
1604 }
1605}
1606
1607
1608
1609
1610
1611=head2 TransactionAddresses
1612
1613Returns a composite hashref of the results of L<RT::Transaction/Addresses> for
1614all this ticket's Create, Comment or Correspond transactions. The keys are
1615stringified email addresses. Each value is an L<Email::Address> object.
1616
1617NOTE: 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.
1618
1619=cut
1620
1621
1622sub TransactionAddresses {
1623 my $self = shift;
1624 my $txns = $self->Transactions;
1625
1626 my %addresses = ();
1627
1628 my $attachments = RT::Attachments->new( $self->CurrentUser );
1629 $attachments->LimitByTicket( $self->id );
1630 $attachments->Columns( qw( id Headers TransactionId));
1631
1632
1633 foreach my $type (qw(Create Comment Correspond)) {
1634 $attachments->Limit( ALIAS => $attachments->TransactionAlias,
1635 FIELD => 'Type',
1636 OPERATOR => '=',
1637 VALUE => $type,
1638 ENTRYAGGREGATOR => 'OR',
1639 CASESENSITIVE => 1
1640 );
1641 }
1642
1643 while ( my $att = $attachments->Next ) {
1644 foreach my $addrlist ( values %{$att->Addresses } ) {
1645 foreach my $addr (@$addrlist) {
1646
1647# Skip addresses without a phrase (things that are just raw addresses) if we have a phrase
1648 next
1649 if ( $addresses{ $addr->address }
1650 && $addresses{ $addr->address }->phrase
1651 && not $addr->phrase );
1652
1653 # skips "comment-only" addresses
1654 next unless ( $addr->address );
1655 $addresses{ $addr->address } = $addr;
1656 }
1657 }
1658 }
1659
1660 return \%addresses;
1661
1662}
1663
1664
1665
1666
1667
1668
1669sub ValidateQueue {
1670 my $self = shift;
1671 my $Value = shift;
1672
1673 if ( !$Value ) {
1674 $RT::Logger->warning( " RT:::Queue::ValidateQueue called with a null value. this isn't ok.");
1675 return (1);
1676 }
1677
1678 my $QueueObj = RT::Queue->new( $self->CurrentUser );
1679 my $id = $QueueObj->Load($Value);
1680
1681 if ($id) {
1682 return (1);
1683 }
1684 else {
1685 return (undef);
1686 }
1687}
1688
1689
1690
1691sub SetQueue {
1692 my $self = shift;
1693 my $NewQueue = shift;
1694
1695 #Redundant. ACL gets checked in _Set;
1696 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1697 return ( 0, $self->loc("Permission Denied") );
1698 }
1699
1700 my $NewQueueObj = RT::Queue->new( $self->CurrentUser );
1701 $NewQueueObj->Load($NewQueue);
1702
1703 unless ( $NewQueueObj->Id() ) {
1704 return ( 0, $self->loc("That queue does not exist") );
1705 }
1706
1707 if ( $NewQueueObj->Id == $self->QueueObj->Id ) {
1708 return ( 0, $self->loc('That is the same value') );
1709 }
1710 unless ( $self->CurrentUser->HasRight( Right => 'CreateTicket', Object => $NewQueueObj)) {
1711 return ( 0, $self->loc("You may not create requests in that queue.") );
1712 }
1713
1714 my $new_status;
1715 my $old_lifecycle = $self->QueueObj->Lifecycle;
1716 my $new_lifecycle = $NewQueueObj->Lifecycle;
1717 if ( $old_lifecycle->Name ne $new_lifecycle->Name ) {
1718 unless ( $old_lifecycle->HasMoveMap( $new_lifecycle ) ) {
1719 return ( 0, $self->loc("There is no mapping for statuses between these queues. Contact your system administrator.") );
1720 }
1721 $new_status = $old_lifecycle->MoveMap( $new_lifecycle )->{ $self->Status };
1722 return ( 0, $self->loc("Mapping between queues' lifecycles is incomplete. Contact your system administrator.") )
1723 unless $new_status;
1724 }
1725
1726 if ( $new_status ) {
1727 my $clone = RT::Ticket->new( RT->SystemUser );
1728 $clone->Load( $self->Id );
1729 unless ( $clone->Id ) {
1730 return ( 0, $self->loc("Couldn't load copy of ticket #[_1].", $self->Id) );
1731 }
1732
1733 my $now = RT::Date->new( $self->CurrentUser );
1734 $now->SetToNow;
1735
1736 my $old_status = $clone->Status;
1737
1738 #If we're changing the status from initial in old to not intial in new,
1739 # record that we've started
1740 if ( $old_lifecycle->IsInitial($old_status) && !$new_lifecycle->IsInitial($new_status) && $clone->StartedObj->Unix == 0 ) {
1741 #Set the Started time to "now"
1742 $clone->_Set(
1743 Field => 'Started',
1744 Value => $now->ISO,
1745 RecordTransaction => 0
1746 );
1747 }
1748
1749 #When we close a ticket, set the 'Resolved' attribute to now.
1750 # It's misnamed, but that's just historical.
1751 if ( $new_lifecycle->IsInactive($new_status) ) {
1752 $clone->_Set(
1753 Field => 'Resolved',
1754 Value => $now->ISO,
1755 RecordTransaction => 0,
1756 );
1757 }
1758
1759 #Actually update the status
1760 my ($val, $msg)= $clone->_Set(
1761 Field => 'Status',
1762 Value => $new_status,
1763 RecordTransaction => 0,
1764 );
1765 $RT::Logger->error( 'Status change failed on queue change: '. $msg )
1766 unless $val;
1767 }
1768
1769 my ($status, $msg) = $self->_Set( Field => 'Queue', Value => $NewQueueObj->Id() );
1770
1771 if ( $status ) {
1772 # Clear the queue object cache;
1773 $self->{_queue_obj} = undef;
1774
1775 # Untake the ticket if we have no permissions in the new queue
1776 unless ( $self->OwnerObj->HasRight( Right => 'OwnTicket', Object => $NewQueueObj ) ) {
1777 my $clone = RT::Ticket->new( RT->SystemUser );
1778 $clone->Load( $self->Id );
1779 unless ( $clone->Id ) {
1780 return ( 0, $self->loc("Couldn't load copy of ticket #[_1].", $self->Id) );
1781 }
1782 my ($status, $msg) = $clone->SetOwner( RT->Nobody->Id, 'Force' );
1783 $RT::Logger->error("Couldn't set owner on queue change: $msg") unless $status;
1784 }
1785
1786 # On queue change, change queue for reminders too
1787 my $reminder_collection = $self->Reminders->Collection;
1788 while ( my $reminder = $reminder_collection->Next ) {
1789 my ($status, $msg) = $reminder->SetQueue($NewQueue);
1790 $RT::Logger->error('Queue change failed for reminder #' . $reminder->Id . ': ' . $msg) unless $status;
1791 }
1792 }
1793
1794 return ($status, $msg);
1795}
1796
1797
1798
1799=head2 QueueObj
1800
1801Takes nothing. returns this ticket's queue object
1802
1803=cut
1804
1805sub QueueObj {
1806 my $self = shift;
1807
1808 if(!$self->{_queue_obj} || ! $self->{_queue_obj}->id) {
1809
1810 $self->{_queue_obj} = RT::Queue->new( $self->CurrentUser );
1811
1812 #We call __Value so that we can avoid the ACL decision and some deep recursion
1813 my ($result) = $self->{_queue_obj}->Load( $self->__Value('Queue') );
1814 }
1815 return ($self->{_queue_obj});
1816}
1817
1818=head2 SubjectTag
1819
1820Takes nothing. Returns SubjectTag for this ticket. Includes
1821queue's subject tag or rtname if that is not set, ticket
1822id and braces, for example:
1823
1824 [support.example.com #123456]
1825
1826=cut
1827
1828sub SubjectTag {
1829 my $self = shift;
1830 return
1831 '['
1832 . ($self->QueueObj->SubjectTag || RT->Config->Get('rtname'))
1833 .' #'. $self->id
1834 .']'
1835 ;
1836}
1837
1838
1839=head2 DueObj
1840
1841 Returns an RT::Date object containing this ticket's due date
1842
1843=cut
1844
1845sub DueObj {
1846 my $self = shift;
1847
1848 my $time = RT::Date->new( $self->CurrentUser );
1849
1850 # -1 is RT::Date slang for never
1851 if ( my $due = $self->Due ) {
1852 $time->Set( Format => 'sql', Value => $due );
1853 }
1854 else {
1855 $time->Set( Format => 'unix', Value => -1 );
1856 }
1857
1858 return $time;
1859}
1860
1861
1862
1863=head2 DueAsString
1864
1865Returns this ticket's due date as a human readable string
1866
1867=cut
1868
1869sub DueAsString {
1870 my $self = shift;
1871 return $self->DueObj->AsString();
1872}
1873
1874
1875
1876=head2 ResolvedObj
1877
1878 Returns an RT::Date object of this ticket's 'resolved' time.
1879
1880=cut
1881
1882sub ResolvedObj {
1883 my $self = shift;
1884
1885 my $time = RT::Date->new( $self->CurrentUser );
1886 $time->Set( Format => 'sql', Value => $self->Resolved );
1887 return $time;
1888}
1889
1890
1891=head2 FirstActiveStatus
1892
1893Returns the first active status that the ticket could transition to,
1894according to its current Queue's lifecycle. May return undef if there
1895is no such possible status to transition to, or we are already in it.
1896This is used in L<RT::Action::AutoOpen>, for instance.
1897
1898=cut
1899
1900sub FirstActiveStatus {
1901 my $self = shift;
1902
1903 my $lifecycle = $self->QueueObj->Lifecycle;
1904 my $status = $self->Status;
1905 my @active = $lifecycle->Active;
1906 # no change if no active statuses in the lifecycle
1907 return undef unless @active;
1908
1909 # no change if the ticket is already has first status from the list of active
1910 return undef if lc $status eq lc $active[0];
1911
1912 my ($next) = grep $lifecycle->IsActive($_), $lifecycle->Transitions($status);
1913 return $next;
1914}
1915
dab09ea8
MKG
1916=head2 FirstInactiveStatus
1917
1918Returns the first inactive status that the ticket could transition to,
1919according to its current Queue's lifecycle. May return undef if there
1920is no such possible status to transition to, or we are already in it.
1921This is used in resolve action in UnsafeEmailCommands, for instance.
1922
1923=cut
1924
1925sub FirstInactiveStatus {
1926 my $self = shift;
1927
1928 my $lifecycle = $self->QueueObj->Lifecycle;
1929 my $status = $self->Status;
1930 my @inactive = $lifecycle->Inactive;
1931 # no change if no inactive statuses in the lifecycle
1932 return undef unless @inactive;
1933
1934 # no change if the ticket is already has first status from the list of inactive
1935 return undef if lc $status eq lc $inactive[0];
1936
1937 my ($next) = grep $lifecycle->IsInactive($_), $lifecycle->Transitions($status);
1938 return $next;
1939}
1940
84fb5b46
MKG
1941=head2 SetStarted
1942
1943Takes a date in ISO format or undef
1944Returns a transaction id and a message
1945The client calls "Start" to note that the project was started on the date in $date.
1946A null date means "now"
1947
1948=cut
1949
1950sub SetStarted {
1951 my $self = shift;
1952 my $time = shift || 0;
1953
1954 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1955 return ( 0, $self->loc("Permission Denied") );
1956 }
1957
1958 #We create a date object to catch date weirdness
1959 my $time_obj = RT::Date->new( $self->CurrentUser() );
1960 if ( $time ) {
1961 $time_obj->Set( Format => 'ISO', Value => $time );
1962 }
1963 else {
1964 $time_obj->SetToNow();
1965 }
1966
1967 # We need $TicketAsSystem, in case the current user doesn't have
1968 # ShowTicket
1969 my $TicketAsSystem = RT::Ticket->new(RT->SystemUser);
1970 $TicketAsSystem->Load( $self->Id );
1971 # Now that we're starting, open this ticket
1972 # TODO: do we really want to force this as policy? it should be a scrip
1973 my $next = $TicketAsSystem->FirstActiveStatus;
1974
1975 $self->SetStatus( $next ) if defined $next;
1976
1977 return ( $self->_Set( Field => 'Started', Value => $time_obj->ISO ) );
1978
1979}
1980
1981
1982
1983=head2 StartedObj
1984
1985 Returns an RT::Date object which contains this ticket's
1986'Started' time.
1987
1988=cut
1989
1990sub StartedObj {
1991 my $self = shift;
1992
1993 my $time = RT::Date->new( $self->CurrentUser );
1994 $time->Set( Format => 'sql', Value => $self->Started );
1995 return $time;
1996}
1997
1998
1999
2000=head2 StartsObj
2001
2002 Returns an RT::Date object which contains this ticket's
2003'Starts' time.
2004
2005=cut
2006
2007sub StartsObj {
2008 my $self = shift;
2009
2010 my $time = RT::Date->new( $self->CurrentUser );
2011 $time->Set( Format => 'sql', Value => $self->Starts );
2012 return $time;
2013}
2014
2015
2016
2017=head2 ToldObj
2018
2019 Returns an RT::Date object which contains this ticket's
2020'Told' time.
2021
2022=cut
2023
2024sub ToldObj {
2025 my $self = shift;
2026
2027 my $time = RT::Date->new( $self->CurrentUser );
2028 $time->Set( Format => 'sql', Value => $self->Told );
2029 return $time;
2030}
2031
2032
2033
2034=head2 ToldAsString
2035
2036A convenience method that returns ToldObj->AsString
2037
2038TODO: This should be deprecated
2039
2040=cut
2041
2042sub ToldAsString {
2043 my $self = shift;
2044 if ( $self->Told ) {
2045 return $self->ToldObj->AsString();
2046 }
2047 else {
2048 return ("Never");
2049 }
2050}
2051
2052
2053
2054=head2 TimeWorkedAsString
2055
2056Returns the amount of time worked on this ticket as a Text String
2057
2058=cut
2059
2060sub TimeWorkedAsString {
2061 my $self = shift;
2062 my $value = $self->TimeWorked;
2063
2064 # return the # of minutes worked turned into seconds and written as
2065 # a simple text string, this is not really a date object, but if we
2066 # diff a number of seconds vs the epoch, we'll get a nice description
2067 # of time worked.
2068 return "" unless $value;
2069 return RT::Date->new( $self->CurrentUser )
2070 ->DurationAsString( $value * 60 );
2071}
2072
2073
2074
2075=head2 TimeLeftAsString
2076
2077Returns the amount of time left on this ticket as a Text String
2078
2079=cut
2080
2081sub TimeLeftAsString {
2082 my $self = shift;
2083 my $value = $self->TimeLeft;
2084 return "" unless $value;
2085 return RT::Date->new( $self->CurrentUser )
2086 ->DurationAsString( $value * 60 );
2087}
2088
2089
2090
2091
2092=head2 Comment
2093
2094Comment on this ticket.
2095Takes a hash with the following attributes:
2096If MIMEObj is undefined, Content will be used to build a MIME::Entity for this
2097comment.
2098
2099MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
2100
2101If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
2102They will, however, be prepared and you'll be able to access them through the TransactionObj
2103
2104Returns: Transaction id, Error Message, Transaction Object
2105(note the different order from Create()!)
2106
2107=cut
2108
2109sub Comment {
2110 my $self = shift;
2111
2112 my %args = ( CcMessageTo => undef,
2113 BccMessageTo => undef,
2114 MIMEObj => undef,
2115 Content => undef,
2116 TimeTaken => 0,
2117 DryRun => 0,
2118 @_ );
2119
2120 unless ( ( $self->CurrentUserHasRight('CommentOnTicket') )
2121 or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
2122 return ( 0, $self->loc("Permission Denied"), undef );
2123 }
2124 $args{'NoteType'} = 'Comment';
2125
dab09ea8 2126 $RT::Handle->BeginTransaction();
84fb5b46 2127 if ($args{'DryRun'}) {
84fb5b46
MKG
2128 $args{'CommitScrips'} = 0;
2129 }
2130
2131 my @results = $self->_RecordNote(%args);
2132 if ($args{'DryRun'}) {
2133 $RT::Handle->Rollback();
dab09ea8
MKG
2134 } else {
2135 $RT::Handle->Commit();
84fb5b46
MKG
2136 }
2137
2138 return(@results);
2139}
2140
2141
2142=head2 Correspond
2143
2144Correspond on this ticket.
2145Takes a hashref with the following attributes:
2146
2147
2148MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
2149
2150if there's no MIMEObj, Content is used to build a MIME::Entity object
2151
2152If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
2153They will, however, be prepared and you'll be able to access them through the TransactionObj
2154
2155Returns: Transaction id, Error Message, Transaction Object
2156(note the different order from Create()!)
2157
2158
2159=cut
2160
2161sub Correspond {
2162 my $self = shift;
2163 my %args = ( CcMessageTo => undef,
2164 BccMessageTo => undef,
2165 MIMEObj => undef,
2166 Content => undef,
2167 TimeTaken => 0,
2168 @_ );
2169
2170 unless ( ( $self->CurrentUserHasRight('ReplyToTicket') )
2171 or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
2172 return ( 0, $self->loc("Permission Denied"), undef );
2173 }
dab09ea8 2174 $args{'NoteType'} = 'Correspond';
84fb5b46 2175
dab09ea8 2176 $RT::Handle->BeginTransaction();
84fb5b46 2177 if ($args{'DryRun'}) {
84fb5b46
MKG
2178 $args{'CommitScrips'} = 0;
2179 }
2180
2181 my @results = $self->_RecordNote(%args);
2182
403d7b0b
MKG
2183 unless ( $results[0] ) {
2184 $RT::Handle->Rollback();
2185 return @results;
2186 }
2187
84fb5b46
MKG
2188 #Set the last told date to now if this isn't mail from the requestor.
2189 #TODO: Note that this will wrongly ack mail from any non-requestor as a "told"
2190 unless ( $self->IsRequestor($self->CurrentUser->id) ) {
2191 my %squelch;
2192 $squelch{$_}++ for map {$_->Content} $self->SquelchMailTo, $results[2]->SquelchMailTo;
2193 $self->_SetTold
2194 if grep {not $squelch{$_}} $self->Requestors->MemberEmailAddresses;
2195 }
2196
2197 if ($args{'DryRun'}) {
2198 $RT::Handle->Rollback();
dab09ea8
MKG
2199 } else {
2200 $RT::Handle->Commit();
84fb5b46
MKG
2201 }
2202
2203 return (@results);
2204
2205}
2206
2207
2208
2209=head2 _RecordNote
2210
2211the meat of both comment and correspond.
2212
2213Performs no access control checks. hence, dangerous.
2214
2215=cut
2216
2217sub _RecordNote {
2218 my $self = shift;
2219 my %args = (
2220 CcMessageTo => undef,
2221 BccMessageTo => undef,
2222 Encrypt => undef,
2223 Sign => undef,
2224 MIMEObj => undef,
2225 Content => undef,
2226 NoteType => 'Correspond',
2227 TimeTaken => 0,
2228 CommitScrips => 1,
2229 SquelchMailTo => undef,
2230 @_
2231 );
2232
2233 unless ( $args{'MIMEObj'} || $args{'Content'} ) {
2234 return ( 0, $self->loc("No message attached"), undef );
2235 }
2236
2237 unless ( $args{'MIMEObj'} ) {
2238 $args{'MIMEObj'} = MIME::Entity->build(
2239 Data => ( ref $args{'Content'}? $args{'Content'}: [ $args{'Content'} ] )
2240 );
2241 }
2242
403d7b0b
MKG
2243 $args{'MIMEObj'}->head->replace('X-RT-Interface' => 'API')
2244 unless $args{'MIMEObj'}->head->get('X-RT-Interface');
2245
84fb5b46
MKG
2246 # convert text parts into utf-8
2247 RT::I18N::SetMIMEEntityToUTF8( $args{'MIMEObj'} );
2248
2249 # If we've been passed in CcMessageTo and BccMessageTo fields,
2250 # add them to the mime object for passing on to the transaction handler
2251 # The "NotifyOtherRecipients" scripAction will look for RT-Send-Cc: and
2252 # RT-Send-Bcc: headers
2253
2254
2255 foreach my $type (qw/Cc Bcc/) {
2256 if ( defined $args{ $type . 'MessageTo' } ) {
2257
2258 my $addresses = join ', ', (
2259 map { RT::User->CanonicalizeEmailAddress( $_->address ) }
2260 Email::Address->parse( $args{ $type . 'MessageTo' } ) );
2261 $args{'MIMEObj'}->head->replace( 'RT-Send-' . $type, Encode::encode_utf8( $addresses ) );
2262 }
2263 }
2264
2265 foreach my $argument (qw(Encrypt Sign)) {
2266 $args{'MIMEObj'}->head->replace(
2267 "X-RT-$argument" => Encode::encode_utf8( $args{ $argument } )
2268 ) if defined $args{ $argument };
2269 }
2270
2271 # If this is from an external source, we need to come up with its
2272 # internal Message-ID now, so all emails sent because of this
2273 # message have a common Message-ID
2274 my $org = RT->Config->Get('Organization');
2275 my $msgid = $args{'MIMEObj'}->head->get('Message-ID');
2276 unless (defined $msgid && $msgid =~ /<(rt-.*?-\d+-\d+)\.(\d+-0-0)\@\Q$org\E>/) {
2277 $args{'MIMEObj'}->head->set(
dab09ea8
MKG
2278 'RT-Message-ID' => Encode::encode_utf8(
2279 RT::Interface::Email::GenMessageId( Ticket => $self )
2280 )
84fb5b46
MKG
2281 );
2282 }
2283
2284 #Record the correspondence (write the transaction)
2285 my ( $Trans, $msg, $TransObj ) = $self->_NewTransaction(
2286 Type => $args{'NoteType'},
2287 Data => ( $args{'MIMEObj'}->head->get('subject') || 'No Subject' ),
2288 TimeTaken => $args{'TimeTaken'},
2289 MIMEObj => $args{'MIMEObj'},
2290 CommitScrips => $args{'CommitScrips'},
2291 SquelchMailTo => $args{'SquelchMailTo'},
2292 );
2293
2294 unless ($Trans) {
2295 $RT::Logger->err("$self couldn't init a transaction $msg");
2296 return ( $Trans, $self->loc("Message could not be recorded"), undef );
2297 }
2298
2299 return ( $Trans, $self->loc("Message recorded"), $TransObj );
2300}
2301
2302
2303=head2 DryRun
2304
2305Builds a MIME object from the given C<UpdateSubject> and
2306C<UpdateContent>, then calls L</Comment> or L</Correspond> with
2307C<< DryRun => 1 >>, and returns the transaction so produced.
2308
2309=cut
2310
2311sub DryRun {
2312 my $self = shift;
2313 my %args = @_;
2314 my $action;
2315 if (($args{'UpdateType'} || $args{Action}) =~ /^respon(d|se)$/i ) {
2316 $action = 'Correspond';
2317 } else {
2318 $action = 'Comment';
2319 }
2320
2321 my $Message = MIME::Entity->build(
2322 Type => 'text/plain',
2323 Subject => defined $args{UpdateSubject} ? Encode::encode_utf8( $args{UpdateSubject} ) : "",
2324 Charset => 'UTF-8',
2325 Data => $args{'UpdateContent'} || "",
2326 );
2327
2328 my ( $Transaction, $Description, $Object ) = $self->$action(
2329 CcMessageTo => $args{'UpdateCc'},
2330 BccMessageTo => $args{'UpdateBcc'},
2331 MIMEObj => $Message,
2332 TimeTaken => $args{'UpdateTimeWorked'},
2333 DryRun => 1,
2334 );
2335 unless ( $Transaction ) {
2336 $RT::Logger->error("Couldn't fire '$action' action: $Description");
2337 }
2338
2339 return $Object;
2340}
2341
2342=head2 DryRunCreate
2343
2344Prepares a MIME mesage with the given C<Subject>, C<Cc>, and
2345C<Content>, then calls L</Create> with C<< DryRun => 1 >> and returns
2346the resulting L<RT::Transaction>.
2347
2348=cut
2349
2350sub DryRunCreate {
2351 my $self = shift;
2352 my %args = @_;
2353 my $Message = MIME::Entity->build(
2354 Type => 'text/plain',
2355 Subject => defined $args{Subject} ? Encode::encode_utf8( $args{'Subject'} ) : "",
2356 (defined $args{'Cc'} ?
2357 ( Cc => Encode::encode_utf8( $args{'Cc'} ) ) : ()),
2358 Charset => 'UTF-8',
2359 Data => $args{'Content'} || "",
2360 );
2361
2362 my ( $Transaction, $Object, $Description ) = $self->Create(
2363 Type => $args{'Type'} || 'ticket',
2364 Queue => $args{'Queue'},
2365 Owner => $args{'Owner'},
2366 Requestor => $args{'Requestors'},
2367 Cc => $args{'Cc'},
2368 AdminCc => $args{'AdminCc'},
2369 InitialPriority => $args{'InitialPriority'},
2370 FinalPriority => $args{'FinalPriority'},
2371 TimeLeft => $args{'TimeLeft'},
2372 TimeEstimated => $args{'TimeEstimated'},
2373 TimeWorked => $args{'TimeWorked'},
2374 Subject => $args{'Subject'},
2375 Status => $args{'Status'},
2376 MIMEObj => $Message,
2377 DryRun => 1,
2378 );
2379 unless ( $Transaction ) {
2380 $RT::Logger->error("Couldn't fire Create action: $Description");
2381 }
2382
2383 return $Object;
2384}
2385
2386
2387
2388sub _Links {
2389 my $self = shift;
2390
2391 #TODO: Field isn't the right thing here. but I ahave no idea what mnemonic ---
2392 #tobias meant by $f
2393 my $field = shift;
2394 my $type = shift || "";
2395
2396 my $cache_key = "$field$type";
2397 return $self->{ $cache_key } if $self->{ $cache_key };
2398
2399 my $links = $self->{ $cache_key }
2400 = RT::Links->new( $self->CurrentUser );
2401 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
2402 $links->Limit( FIELD => 'id', VALUE => 0, SUBCLAUSE => 'acl' );
2403 return $links;
2404 }
2405
2406 # Maybe this ticket is a merge ticket
2407 my $limit_on = 'Local'. $field;
2408 # at least to myself
2409 $links->Limit(
2410 FIELD => $limit_on,
2411 VALUE => $self->id,
2412 ENTRYAGGREGATOR => 'OR',
2413 );
2414 $links->Limit(
2415 FIELD => $limit_on,
2416 VALUE => $_,
2417 ENTRYAGGREGATOR => 'OR',
2418 ) foreach $self->Merged;
2419 $links->Limit(
2420 FIELD => 'Type',
2421 VALUE => $type,
2422 ) if $type;
2423
2424 return $links;
2425}
2426
2427
2428
2429=head2 DeleteLink
2430
2431Delete a link. takes a paramhash of Base, Target, Type, Silent,
2432SilentBase and SilentTarget. Either Base or Target must be null.
403d7b0b 2433The null value will be replaced with this ticket's id.
84fb5b46
MKG
2434
2435If Silent is true then no transaction would be recorded, in other
2436case you can control creation of transactions on both base and
2437target with SilentBase and SilentTarget respectively. By default
2438both transactions are created.
2439
2440=cut
2441
2442sub DeleteLink {
2443 my $self = shift;
2444 my %args = (
2445 Base => undef,
2446 Target => undef,
2447 Type => undef,
2448 Silent => undef,
2449 SilentBase => undef,
2450 SilentTarget => undef,
2451 @_
2452 );
2453
2454 unless ( $args{'Target'} || $args{'Base'} ) {
2455 $RT::Logger->error("Base or Target must be specified");
2456 return ( 0, $self->loc('Either base or target must be specified') );
2457 }
2458
2459 #check acls
2460 my $right = 0;
2461 $right++ if $self->CurrentUserHasRight('ModifyTicket');
2462 if ( !$right && RT->Config->Get( 'StrictLinkACL' ) ) {
2463 return ( 0, $self->loc("Permission Denied") );
2464 }
2465
2466 # If the other URI is an RT::Ticket, we want to make sure the user
2467 # can modify it too...
2468 my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} );
2469 return (0, $msg) unless $status;
2470 if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) {
2471 $right++;
2472 }
2473 if ( ( !RT->Config->Get( 'StrictLinkACL' ) && $right == 0 ) ||
2474 ( RT->Config->Get( 'StrictLinkACL' ) && $right < 2 ) )
2475 {
2476 return ( 0, $self->loc("Permission Denied") );
2477 }
2478
2479 my ($val, $Msg) = $self->SUPER::_DeleteLink(%args);
2480 return ( 0, $Msg ) unless $val;
2481
2482 return ( $val, $Msg ) if $args{'Silent'};
2483
2484 my ($direction, $remote_link);
2485
2486 if ( $args{'Base'} ) {
2487 $remote_link = $args{'Base'};
2488 $direction = 'Target';
2489 }
2490 elsif ( $args{'Target'} ) {
2491 $remote_link = $args{'Target'};
2492 $direction = 'Base';
2493 }
2494
2495 my $remote_uri = RT::URI->new( $self->CurrentUser );
2496 $remote_uri->FromURI( $remote_link );
2497
2498 unless ( $args{ 'Silent'. $direction } ) {
2499 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
2500 Type => 'DeleteLink',
2501 Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
2502 OldValue => $remote_uri->URI || $remote_link,
2503 TimeTaken => 0
2504 );
2505 $RT::Logger->error("Couldn't create transaction: $Msg") unless $Trans;
2506 }
2507
2508 if ( !$args{ 'Silent'. ( $direction eq 'Target'? 'Base': 'Target' ) } && $remote_uri->IsLocal ) {
2509 my $OtherObj = $remote_uri->Object;
2510 my ( $val, $Msg ) = $OtherObj->_NewTransaction(
2511 Type => 'DeleteLink',
2512 Field => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
2513 : $LINKDIRMAP{$args{'Type'}}->{Target},
2514 OldValue => $self->URI,
2515 ActivateScrips => !RT->Config->Get('LinkTransactionsRun1Scrip'),
2516 TimeTaken => 0,
2517 );
2518 $RT::Logger->error("Couldn't create transaction: $Msg") unless $val;
2519 }
2520
2521 return ( $val, $Msg );
2522}
2523
2524
2525
2526=head2 AddLink
2527
2528Takes a paramhash of Type and one of Base or Target. Adds that link to this ticket.
2529
2530If Silent is true then no transaction would be recorded, in other
2531case you can control creation of transactions on both base and
2532target with SilentBase and SilentTarget respectively. By default
2533both transactions are created.
2534
2535=cut
2536
2537sub AddLink {
2538 my $self = shift;
2539 my %args = ( Target => '',
2540 Base => '',
2541 Type => '',
2542 Silent => undef,
2543 SilentBase => undef,
2544 SilentTarget => undef,
2545 @_ );
2546
2547 unless ( $args{'Target'} || $args{'Base'} ) {
2548 $RT::Logger->error("Base or Target must be specified");
2549 return ( 0, $self->loc('Either base or target must be specified') );
2550 }
2551
2552 my $right = 0;
2553 $right++ if $self->CurrentUserHasRight('ModifyTicket');
2554 if ( !$right && RT->Config->Get( 'StrictLinkACL' ) ) {
2555 return ( 0, $self->loc("Permission Denied") );
2556 }
2557
2558 # If the other URI is an RT::Ticket, we want to make sure the user
2559 # can modify it too...
2560 my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} );
2561 return (0, $msg) unless $status;
2562 if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) {
2563 $right++;
2564 }
2565 if ( ( !RT->Config->Get( 'StrictLinkACL' ) && $right == 0 ) ||
2566 ( RT->Config->Get( 'StrictLinkACL' ) && $right < 2 ) )
2567 {
2568 return ( 0, $self->loc("Permission Denied") );
2569 }
2570
2571 return ( 0, "Can't link to a deleted ticket" )
2572 if $other_ticket && $other_ticket->Status eq 'deleted';
2573
2574 return $self->_AddLink(%args);
2575}
2576
2577sub __GetTicketFromURI {
2578 my $self = shift;
2579 my %args = ( URI => '', @_ );
2580
2581 # If the other URI is an RT::Ticket, we want to make sure the user
2582 # can modify it too...
2583 my $uri_obj = RT::URI->new( $self->CurrentUser );
c36a7e1d 2584 unless ($uri_obj->FromURI( $args{'URI'} )) {
84fb5b46
MKG
2585 my $msg = $self->loc( "Couldn't resolve '[_1]' into a URI.", $args{'URI'} );
2586 $RT::Logger->warning( $msg );
2587 return( 0, $msg );
2588 }
2589 my $obj = $uri_obj->Resolver->Object;
2590 unless ( UNIVERSAL::isa($obj, 'RT::Ticket') && $obj->id ) {
2591 return (1, 'Found not a ticket', undef);
2592 }
2593 return (1, 'Found ticket', $obj);
2594}
2595
2596=head2 _AddLink
2597
2598Private non-acled variant of AddLink so that links can be added during create.
2599
2600=cut
2601
2602sub _AddLink {
2603 my $self = shift;
2604 my %args = ( Target => '',
2605 Base => '',
2606 Type => '',
2607 Silent => undef,
2608 SilentBase => undef,
2609 SilentTarget => undef,
2610 @_ );
2611
2612 my ($val, $msg, $exist) = $self->SUPER::_AddLink(%args);
2613 return ($val, $msg) if !$val || $exist;
2614 return ($val, $msg) if $args{'Silent'};
2615
2616 my ($direction, $remote_link);
2617 if ( $args{'Target'} ) {
2618 $remote_link = $args{'Target'};
2619 $direction = 'Base';
2620 } elsif ( $args{'Base'} ) {
2621 $remote_link = $args{'Base'};
2622 $direction = 'Target';
2623 }
2624
2625 my $remote_uri = RT::URI->new( $self->CurrentUser );
2626 $remote_uri->FromURI( $remote_link );
2627
2628 unless ( $args{ 'Silent'. $direction } ) {
2629 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
2630 Type => 'AddLink',
2631 Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
2632 NewValue => $remote_uri->URI || $remote_link,
2633 TimeTaken => 0
2634 );
2635 $RT::Logger->error("Couldn't create transaction: $Msg") unless $Trans;
2636 }
2637
2638 if ( !$args{ 'Silent'. ( $direction eq 'Target'? 'Base': 'Target' ) } && $remote_uri->IsLocal ) {
2639 my $OtherObj = $remote_uri->Object;
2640 my ( $val, $msg ) = $OtherObj->_NewTransaction(
2641 Type => 'AddLink',
2642 Field => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
2643 : $LINKDIRMAP{$args{'Type'}}->{Target},
2644 NewValue => $self->URI,
2645 ActivateScrips => !RT->Config->Get('LinkTransactionsRun1Scrip'),
2646 TimeTaken => 0,
2647 );
2648 $RT::Logger->error("Couldn't create transaction: $msg") unless $val;
2649 }
2650
2651 return ( $val, $msg );
2652}
2653
2654
2655
2656
2657=head2 MergeInto
2658
2659MergeInto take the id of the ticket to merge this ticket into.
2660
2661=cut
2662
2663sub MergeInto {
2664 my $self = shift;
2665 my $ticket_id = shift;
2666
2667 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2668 return ( 0, $self->loc("Permission Denied") );
2669 }
2670
2671 # Load up the new ticket.
2672 my $MergeInto = RT::Ticket->new($self->CurrentUser);
2673 $MergeInto->Load($ticket_id);
2674
2675 # make sure it exists.
2676 unless ( $MergeInto->Id ) {
2677 return ( 0, $self->loc("New ticket doesn't exist") );
2678 }
2679
2680 # Make sure the current user can modify the new ticket.
2681 unless ( $MergeInto->CurrentUserHasRight('ModifyTicket') ) {
2682 return ( 0, $self->loc("Permission Denied") );
2683 }
2684
2685 delete $MERGE_CACHE{'effective'}{ $self->id };
2686 delete @{ $MERGE_CACHE{'merged'} }{
2687 $ticket_id, $MergeInto->id, $self->id
2688 };
2689
2690 $RT::Handle->BeginTransaction();
2691
2692 $self->_MergeInto( $MergeInto );
2693
2694 $RT::Handle->Commit();
2695
2696 return ( 1, $self->loc("Merge Successful") );
2697}
2698
2699sub _MergeInto {
2700 my $self = shift;
2701 my $MergeInto = shift;
2702
2703
2704 # We use EffectiveId here even though it duplicates information from
2705 # the links table becasue of the massive performance hit we'd take
2706 # by trying to do a separate database query for merge info everytime
2707 # loaded a ticket.
2708
2709 #update this ticket's effective id to the new ticket's id.
2710 my ( $id_val, $id_msg ) = $self->__Set(
2711 Field => 'EffectiveId',
2712 Value => $MergeInto->Id()
2713 );
2714
2715 unless ($id_val) {
2716 $RT::Handle->Rollback();
2717 return ( 0, $self->loc("Merge failed. Couldn't set EffectiveId") );
2718 }
2719
2720
2721 my $force_status = $self->QueueObj->Lifecycle->DefaultOnMerge;
2722 if ( $force_status && $force_status ne $self->__Value('Status') ) {
2723 my ( $status_val, $status_msg )
2724 = $self->__Set( Field => 'Status', Value => $force_status );
2725
2726 unless ($status_val) {
2727 $RT::Handle->Rollback();
2728 $RT::Logger->error(
2729 "Couldn't set status to $force_status. RT's Database may be inconsistent."
2730 );
2731 return ( 0, $self->loc("Merge failed. Couldn't set Status") );
2732 }
2733 }
2734
2735 # update all the links that point to that old ticket
2736 my $old_links_to = RT::Links->new($self->CurrentUser);
2737 $old_links_to->Limit(FIELD => 'Target', VALUE => $self->URI);
2738
2739 my %old_seen;
2740 while (my $link = $old_links_to->Next) {
2741 if (exists $old_seen{$link->Base."-".$link->Type}) {
2742 $link->Delete;
2743 }
2744 elsif ($link->Base eq $MergeInto->URI) {
2745 $link->Delete;
2746 } else {
2747 # First, make sure the link doesn't already exist. then move it over.
2748 my $tmp = RT::Link->new(RT->SystemUser);
2749 $tmp->LoadByCols(Base => $link->Base, Type => $link->Type, LocalTarget => $MergeInto->id);
2750 if ($tmp->id) {
2751 $link->Delete;
2752 } else {
2753 $link->SetTarget($MergeInto->URI);
2754 $link->SetLocalTarget($MergeInto->id);
2755 }
2756 $old_seen{$link->Base."-".$link->Type} =1;
2757 }
2758
2759 }
2760
2761 my $old_links_from = RT::Links->new($self->CurrentUser);
2762 $old_links_from->Limit(FIELD => 'Base', VALUE => $self->URI);
2763
2764 while (my $link = $old_links_from->Next) {
2765 if (exists $old_seen{$link->Type."-".$link->Target}) {
2766 $link->Delete;
2767 }
2768 if ($link->Target eq $MergeInto->URI) {
2769 $link->Delete;
2770 } else {
2771 # First, make sure the link doesn't already exist. then move it over.
2772 my $tmp = RT::Link->new(RT->SystemUser);
2773 $tmp->LoadByCols(Target => $link->Target, Type => $link->Type, LocalBase => $MergeInto->id);
2774 if ($tmp->id) {
2775 $link->Delete;
2776 } else {
2777 $link->SetBase($MergeInto->URI);
2778 $link->SetLocalBase($MergeInto->id);
2779 $old_seen{$link->Type."-".$link->Target} =1;
2780 }
2781 }
2782
2783 }
2784
2785 # Update time fields
2786 foreach my $type (qw(TimeEstimated TimeWorked TimeLeft)) {
2787
2788 my $mutator = "Set$type";
2789 $MergeInto->$mutator(
2790 ( $MergeInto->$type() || 0 ) + ( $self->$type() || 0 ) );
2791
2792 }
2793#add all of this ticket's watchers to that ticket.
2794 foreach my $watcher_type (qw(Requestors Cc AdminCc)) {
2795
2796 my $people = $self->$watcher_type->MembersObj;
2797 my $addwatcher_type = $watcher_type;
2798 $addwatcher_type =~ s/s$//;
2799
2800 while ( my $watcher = $people->Next ) {
2801
2802 my ($val, $msg) = $MergeInto->_AddWatcher(
2803 Type => $addwatcher_type,
2804 Silent => 1,
2805 PrincipalId => $watcher->MemberId
2806 );
2807 unless ($val) {
2808 $RT::Logger->debug($msg);
2809 }
2810 }
2811
2812 }
2813
2814 #find all of the tickets that were merged into this ticket.
2815 my $old_mergees = RT::Tickets->new( $self->CurrentUser );
2816 $old_mergees->Limit(
2817 FIELD => 'EffectiveId',
2818 OPERATOR => '=',
2819 VALUE => $self->Id
2820 );
2821
2822 # update their EffectiveId fields to the new ticket's id
2823 while ( my $ticket = $old_mergees->Next() ) {
2824 my ( $val, $msg ) = $ticket->__Set(
2825 Field => 'EffectiveId',
2826 Value => $MergeInto->Id()
2827 );
2828 }
2829
2830 #make a new link: this ticket is merged into that other ticket.
2831 $self->AddLink( Type => 'MergedInto', Target => $MergeInto->Id());
2832
2833 $MergeInto->_SetLastUpdated;
2834}
2835
2836=head2 Merged
2837
2838Returns list of tickets' ids that's been merged into this ticket.
2839
2840=cut
2841
2842sub Merged {
2843 my $self = shift;
2844
2845 my $id = $self->id;
2846 return @{ $MERGE_CACHE{'merged'}{ $id } }
2847 if $MERGE_CACHE{'merged'}{ $id };
2848
2849 my $mergees = RT::Tickets->new( $self->CurrentUser );
2850 $mergees->Limit(
2851 FIELD => 'EffectiveId',
2852 VALUE => $id,
2853 );
2854 $mergees->Limit(
2855 FIELD => 'id',
2856 OPERATOR => '!=',
2857 VALUE => $id,
2858 );
2859 return @{ $MERGE_CACHE{'merged'}{ $id } ||= [] }
2860 = map $_->id, @{ $mergees->ItemsArrayRef || [] };
2861}
2862
2863
2864
2865
2866
2867=head2 OwnerObj
2868
2869Takes nothing and returns an RT::User object of
2870this ticket's owner
2871
2872=cut
2873
2874sub OwnerObj {
2875 my $self = shift;
2876
2877 #If this gets ACLed, we lose on a rights check in User.pm and
2878 #get deep recursion. if we need ACLs here, we need
2879 #an equiv without ACLs
2880
2881 my $owner = RT::User->new( $self->CurrentUser );
2882 $owner->Load( $self->__Value('Owner') );
2883
2884 #Return the owner object
2885 return ($owner);
2886}
2887
2888
2889
2890=head2 OwnerAsString
2891
2892Returns the owner's email address
2893
2894=cut
2895
2896sub OwnerAsString {
2897 my $self = shift;
2898 return ( $self->OwnerObj->EmailAddress );
2899
2900}
2901
2902
2903
2904=head2 SetOwner
2905
2906Takes two arguments:
2907 the Id or Name of the owner
2908and (optionally) the type of the SetOwner Transaction. It defaults
2909to 'Set'. 'Steal' is also a valid option.
2910
2911
2912=cut
2913
2914sub SetOwner {
2915 my $self = shift;
2916 my $NewOwner = shift;
2917 my $Type = shift || "Set";
2918
2919 $RT::Handle->BeginTransaction();
2920
2921 $self->_SetLastUpdated(); # lock the ticket
2922 $self->Load( $self->id ); # in case $self changed while waiting for lock
2923
2924 my $OldOwnerObj = $self->OwnerObj;
2925
2926 my $NewOwnerObj = RT::User->new( $self->CurrentUser );
2927 $NewOwnerObj->Load( $NewOwner );
2928 unless ( $NewOwnerObj->Id ) {
2929 $RT::Handle->Rollback();
2930 return ( 0, $self->loc("That user does not exist") );
2931 }
2932
2933
2934 # must have ModifyTicket rights
2935 # or TakeTicket/StealTicket and $NewOwner is self
2936 # see if it's a take
2937 if ( $OldOwnerObj->Id == RT->Nobody->Id ) {
2938 unless ( $self->CurrentUserHasRight('ModifyTicket')
2939 || $self->CurrentUserHasRight('TakeTicket') ) {
2940 $RT::Handle->Rollback();
2941 return ( 0, $self->loc("Permission Denied") );
2942 }
2943 }
2944
2945 # see if it's a steal
2946 elsif ( $OldOwnerObj->Id != RT->Nobody->Id
2947 && $OldOwnerObj->Id != $self->CurrentUser->id ) {
2948
2949 unless ( $self->CurrentUserHasRight('ModifyTicket')
2950 || $self->CurrentUserHasRight('StealTicket') ) {
2951 $RT::Handle->Rollback();
2952 return ( 0, $self->loc("Permission Denied") );
2953 }
2954 }
2955 else {
2956 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2957 $RT::Handle->Rollback();
2958 return ( 0, $self->loc("Permission Denied") );
2959 }
2960 }
2961
2962 # If we're not stealing and the ticket has an owner and it's not
2963 # the current user
2964 if ( $Type ne 'Steal' and $Type ne 'Force'
2965 and $OldOwnerObj->Id != RT->Nobody->Id
2966 and $OldOwnerObj->Id != $self->CurrentUser->Id )
2967 {
2968 $RT::Handle->Rollback();
2969 return ( 0, $self->loc("You can only take tickets that are unowned") )
2970 if $NewOwnerObj->id == $self->CurrentUser->id;
2971 return (
2972 0,
2973 $self->loc("You can only reassign tickets that you own or that are unowned" )
2974 );
2975 }
2976
2977 #If we've specified a new owner and that user can't modify the ticket
2978 elsif ( !$NewOwnerObj->HasRight( Right => 'OwnTicket', Object => $self ) ) {
2979 $RT::Handle->Rollback();
2980 return ( 0, $self->loc("That user may not own tickets in that queue") );
2981 }
2982
2983 # If the ticket has an owner and it's the new owner, we don't need
2984 # To do anything
2985 elsif ( $NewOwnerObj->Id == $OldOwnerObj->Id ) {
2986 $RT::Handle->Rollback();
2987 return ( 0, $self->loc("That user already owns that ticket") );
2988 }
2989
2990 # Delete the owner in the owner group, then add a new one
2991 # TODO: is this safe? it's not how we really want the API to work
2992 # for most things, but it's fast.
2993 my ( $del_id, $del_msg );
2994 for my $owner (@{$self->OwnerGroup->MembersObj->ItemsArrayRef}) {
2995 ($del_id, $del_msg) = $owner->Delete();
2996 last unless ($del_id);
2997 }
2998
2999 unless ($del_id) {
3000 $RT::Handle->Rollback();
3001 return ( 0, $self->loc("Could not change owner: [_1]", $del_msg) );
3002 }
3003
3004 my ( $add_id, $add_msg ) = $self->OwnerGroup->_AddMember(
3005 PrincipalId => $NewOwnerObj->PrincipalId,
3006 InsideTransaction => 1 );
3007 unless ($add_id) {
3008 $RT::Handle->Rollback();
3009 return ( 0, $self->loc("Could not change owner: [_1]", $add_msg ) );
3010 }
3011
3012 # We call set twice with slightly different arguments, so
3013 # as to not have an SQL transaction span two RT transactions
3014
3015 my ( $val, $msg ) = $self->_Set(
3016 Field => 'Owner',
3017 RecordTransaction => 0,
3018 Value => $NewOwnerObj->Id,
3019 TimeTaken => 0,
3020 TransactionType => 'Set',
3021 CheckACL => 0, # don't check acl
3022 );
3023
3024 unless ($val) {
3025 $RT::Handle->Rollback;
3026 return ( 0, $self->loc("Could not change owner: [_1]", $msg) );
3027 }
3028
3029 ($val, $msg) = $self->_NewTransaction(
3030 Type => 'Set',
3031 Field => 'Owner',
3032 NewValue => $NewOwnerObj->Id,
3033 OldValue => $OldOwnerObj->Id,
3034 TimeTaken => 0,
3035 );
3036
3037 if ( $val ) {
3038 $msg = $self->loc( "Owner changed from [_1] to [_2]",
3039 $OldOwnerObj->Name, $NewOwnerObj->Name );
3040 }
3041 else {
3042 $RT::Handle->Rollback();
3043 return ( 0, $msg );
3044 }
3045
3046 $RT::Handle->Commit();
3047
3048 return ( $val, $msg );
3049}
3050
3051
3052
3053=head2 Take
3054
3055A convenince method to set the ticket's owner to the current user
3056
3057=cut
3058
3059sub Take {
3060 my $self = shift;
3061 return ( $self->SetOwner( $self->CurrentUser->Id, 'Take' ) );
3062}
3063
3064
3065
3066=head2 Untake
3067
3068Convenience method to set the owner to 'nobody' if the current user is the owner.
3069
3070=cut
3071
3072sub Untake {
3073 my $self = shift;
3074 return ( $self->SetOwner( RT->Nobody->UserObj->Id, 'Untake' ) );
3075}
3076
3077
3078
3079=head2 Steal
3080
3081A convenience method to change the owner of the current ticket to the
3082current user. Even if it's owned by another user.
3083
3084=cut
3085
3086sub Steal {
3087 my $self = shift;
3088
3089 if ( $self->IsOwner( $self->CurrentUser ) ) {
3090 return ( 0, $self->loc("You already own this ticket") );
3091 }
3092 else {
3093 return ( $self->SetOwner( $self->CurrentUser->Id, 'Steal' ) );
3094
3095 }
3096
3097}
3098
3099
3100
3101
3102
3103=head2 ValidateStatus STATUS
3104
3105Takes a string. Returns true if that status is a valid status for this ticket.
3106Returns false otherwise.
3107
3108=cut
3109
3110sub ValidateStatus {
3111 my $self = shift;
3112 my $status = shift;
3113
3114 #Make sure the status passed in is valid
3115 return 1 if $self->QueueObj->IsValidStatus($status);
3116
3117 my $i = 0;
3118 while ( my $caller = (caller($i++))[3] ) {
3119 return 1 if $caller eq 'RT::Ticket::SetQueue';
3120 }
3121
3122 return 0;
3123}
3124
3125
3126
3127=head2 SetStatus STATUS
3128
403d7b0b 3129Set this ticket's status. STATUS can be one of: new, open, stalled, resolved, rejected or deleted.
84fb5b46
MKG
3130
3131Alternatively, you can pass in a list of named parameters (Status => STATUS, Force => FORCE, SetStarted => SETSTARTED ).
3132If FORCE is true, ignore unresolved dependencies and force a status change.
3133if SETSTARTED is true( it's the default value), set Started to current datetime if Started
3134is not set and the status is changed from initial to not initial.
3135
3136=cut
3137
3138sub SetStatus {
3139 my $self = shift;
3140 my %args;
3141 if (@_ == 1) {
3142 $args{Status} = shift;
3143 }
3144 else {
3145 %args = (@_);
3146 }
3147
3148 # this only allows us to SetStarted, not we must SetStarted.
3149 # this option was added for rtir initially
3150 $args{SetStarted} = 1 unless exists $args{SetStarted};
3151
3152
3153 my $lifecycle = $self->QueueObj->Lifecycle;
3154
3155 my $new = $args{'Status'};
3156 unless ( $lifecycle->IsValid( $new ) ) {
3157 return (0, $self->loc("Status '[_1]' isn't a valid status for tickets in this queue.", $self->loc($new)));
3158 }
3159
3160 my $old = $self->__Value('Status');
3161 unless ( $lifecycle->IsTransition( $old => $new ) ) {
3162 return (0, $self->loc("You can't change status from '[_1]' to '[_2]'.", $self->loc($old), $self->loc($new)));
3163 }
3164
3165 my $check_right = $lifecycle->CheckRight( $old => $new );
3166 unless ( $self->CurrentUserHasRight( $check_right ) ) {
3167 return ( 0, $self->loc('Permission Denied') );
3168 }
3169
3170 if ( !$args{Force} && $lifecycle->IsInactive( $new ) && $self->HasUnresolvedDependencies) {
3171 return (0, $self->loc('That ticket has unresolved dependencies'));
3172 }
3173
3174 my $now = RT::Date->new( $self->CurrentUser );
3175 $now->SetToNow();
3176
3177 my $raw_started = RT::Date->new(RT->SystemUser);
3178 $raw_started->Set(Format => 'ISO', Value => $self->__Value('Started'));
3179
3180 #If we're changing the status from new, record that we've started
3181 if ( $args{SetStarted} && $lifecycle->IsInitial($old) && !$lifecycle->IsInitial($new) && !$raw_started->Unix) {
3182 #Set the Started time to "now"
3183 $self->_Set(
3184 Field => 'Started',
3185 Value => $now->ISO,
3186 RecordTransaction => 0
3187 );
3188 }
3189
3190 #When we close a ticket, set the 'Resolved' attribute to now.
3191 # It's misnamed, but that's just historical.
3192 if ( $lifecycle->IsInactive($new) ) {
3193 $self->_Set(
3194 Field => 'Resolved',
3195 Value => $now->ISO,
3196 RecordTransaction => 0,
3197 );
3198 }
3199
3200 #Actually update the status
3201 my ($val, $msg)= $self->_Set(
3202 Field => 'Status',
3203 Value => $args{Status},
3204 TimeTaken => 0,
3205 CheckACL => 0,
3206 TransactionType => 'Status',
3207 );
3208 return ($val, $msg);
3209}
3210
3211
3212
3213=head2 Delete
3214
3215Takes no arguments. Marks this ticket for garbage collection
3216
3217=cut
3218
3219sub Delete {
3220 my $self = shift;
3221 unless ( $self->QueueObj->Lifecycle->IsValid('deleted') ) {
3222 return (0, $self->loc('Delete operation is disabled by lifecycle configuration') ); #loc
3223 }
3224 return ( $self->SetStatus('deleted') );
3225}
3226
3227
3228=head2 SetTold ISO [TIMETAKEN]
3229
3230Updates the told and records a transaction
3231
3232=cut
3233
3234sub SetTold {
3235 my $self = shift;
3236 my $told;
3237 $told = shift if (@_);
3238 my $timetaken = shift || 0;
3239
3240 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
3241 return ( 0, $self->loc("Permission Denied") );
3242 }
3243
3244 my $datetold = RT::Date->new( $self->CurrentUser );
3245 if ($told) {
3246 $datetold->Set( Format => 'iso',
3247 Value => $told );
3248 }
3249 else {
3250 $datetold->SetToNow();
3251 }
3252
3253 return ( $self->_Set( Field => 'Told',
3254 Value => $datetold->ISO,
3255 TimeTaken => $timetaken,
3256 TransactionType => 'Told' ) );
3257}
3258
3259=head2 _SetTold
3260
3261Updates the told without a transaction or acl check. Useful when we're sending replies.
3262
3263=cut
3264
3265sub _SetTold {
3266 my $self = shift;
3267
3268 my $now = RT::Date->new( $self->CurrentUser );
3269 $now->SetToNow();
3270
3271 #use __Set to get no ACLs ;)
3272 return ( $self->__Set( Field => 'Told',
3273 Value => $now->ISO ) );
3274}
3275
3276=head2 SeenUpTo
3277
3278
3279=cut
3280
3281sub SeenUpTo {
3282 my $self = shift;
3283 my $uid = $self->CurrentUser->id;
3284 my $attr = $self->FirstAttribute( "User-". $uid ."-SeenUpTo" );
3285 return if $attr && $attr->Content gt $self->LastUpdated;
3286
3287 my $txns = $self->Transactions;
3288 $txns->Limit( FIELD => 'Type', VALUE => 'Comment' );
3289 $txns->Limit( FIELD => 'Type', VALUE => 'Correspond' );
3290 $txns->Limit( FIELD => 'Creator', OPERATOR => '!=', VALUE => $uid );
3291 $txns->Limit(
3292 FIELD => 'Created',
3293 OPERATOR => '>',
3294 VALUE => $attr->Content
3295 ) if $attr;
3296 $txns->RowsPerPage(1);
3297 return $txns->First;
3298}
3299
dab09ea8
MKG
3300=head2 RanTransactionBatch
3301
3302Acts as a guard around running TransactionBatch scrips.
3303
3304Should be false until you enter the code that runs TransactionBatch scrips
3305
3306Accepts an optional argument to indicate that TransactionBatch Scrips should no longer be run on this object.
3307
3308=cut
3309
3310sub RanTransactionBatch {
3311 my $self = shift;
3312 my $val = shift;
3313
3314 if ( defined $val ) {
3315 return $self->{_RanTransactionBatch} = $val;
3316 } else {
3317 return $self->{_RanTransactionBatch};
3318 }
3319
3320}
3321
84fb5b46
MKG
3322
3323=head2 TransactionBatch
3324
3325Returns an array reference of all transactions created on this ticket during
3326this ticket object's lifetime or since last application of a batch, or undef
3327if there were none.
3328
3329Only works when the C<UseTransactionBatch> config option is set to true.
3330
3331=cut
3332
3333sub TransactionBatch {
3334 my $self = shift;
3335 return $self->{_TransactionBatch};
3336}
3337
3338=head2 ApplyTransactionBatch
3339
3340Applies scrips on the current batch of transactions and shinks it. Usually
3341batch is applied when object is destroyed, but in some cases it's too late.
3342
3343=cut
3344
3345sub ApplyTransactionBatch {
3346 my $self = shift;
3347
3348 my $batch = $self->TransactionBatch;
3349 return unless $batch && @$batch;
3350
3351 $self->_ApplyTransactionBatch;
3352
3353 $self->{_TransactionBatch} = [];
3354}
3355
3356sub _ApplyTransactionBatch {
3357 my $self = shift;
dab09ea8
MKG
3358
3359 return if $self->RanTransactionBatch;
3360 $self->RanTransactionBatch(1);
3361
3362 my $still_exists = RT::Ticket->new( RT->SystemUser );
3363 $still_exists->Load( $self->Id );
3364 if (not $still_exists->Id) {
3365 # The ticket has been removed from the database, but we still
3366 # have pending TransactionBatch txns for it. Unfortunately,
3367 # because it isn't in the DB anymore, attempting to run scrips
3368 # on it may produce unpredictable results; simply drop the
3369 # batched transactions.
3370 $RT::Logger->warning("TransactionBatch was fired on a ticket that no longer exists; unable to run scrips! Call ->ApplyTransactionBatch before shredding the ticket, for consistent results.");
3371 return;
3372 }
3373
84fb5b46
MKG
3374 my $batch = $self->TransactionBatch;
3375
3376 my %seen;
3377 my $types = join ',', grep !$seen{$_}++, grep defined, map $_->__Value('Type'), grep defined, @{$batch};
3378
3379 require RT::Scrips;
3380 RT::Scrips->new(RT->SystemUser)->Apply(
3381 Stage => 'TransactionBatch',
3382 TicketObj => $self,
3383 TransactionObj => $batch->[0],
3384 Type => $types,
3385 );
3386
3387 # Entry point of the rule system
3388 my $rules = RT::Ruleset->FindAllRules(
3389 Stage => 'TransactionBatch',
3390 TicketObj => $self,
3391 TransactionObj => $batch->[0],
3392 Type => $types,
3393 );
3394 RT::Ruleset->CommitRules($rules);
3395}
3396
3397sub DESTROY {
3398 my $self = shift;
3399
3400 # DESTROY methods need to localize $@, or it may unset it. This
3401 # causes $m->abort to not bubble all of the way up. See perlbug
3402 # http://rt.perl.org/rt3/Ticket/Display.html?id=17650
3403 local $@;
3404
3405 # The following line eliminates reentrancy.
3406 # It protects against the fact that perl doesn't deal gracefully
3407 # when an object's refcount is changed in its destructor.
3408 return if $self->{_Destroyed}++;
3409
3410 if (in_global_destruction()) {
3411 unless ($ENV{'HARNESS_ACTIVE'}) {
3412 warn "Too late to safely run transaction-batch scrips!"
3413 ." This is typically caused by using ticket objects"
3414 ." at the top-level of a script which uses the RT API."
3415 ." Be sure to explicitly undef such ticket objects,"
3416 ." or put them inside of a lexical scope.";
3417 }
3418 return;
3419 }
3420
dab09ea8 3421 return $self->ApplyTransactionBatch;
84fb5b46
MKG
3422}
3423
3424
3425
3426
3427sub _OverlayAccessible {
3428 {
3429 EffectiveId => { 'read' => 1, 'write' => 1, 'public' => 1 },
3430 Queue => { 'read' => 1, 'write' => 1 },
3431 Requestors => { 'read' => 1, 'write' => 1 },
3432 Owner => { 'read' => 1, 'write' => 1 },
3433 Subject => { 'read' => 1, 'write' => 1 },
3434 InitialPriority => { 'read' => 1, 'write' => 1 },
3435 FinalPriority => { 'read' => 1, 'write' => 1 },
3436 Priority => { 'read' => 1, 'write' => 1 },
3437 Status => { 'read' => 1, 'write' => 1 },
3438 TimeEstimated => { 'read' => 1, 'write' => 1 },
3439 TimeWorked => { 'read' => 1, 'write' => 1 },
3440 TimeLeft => { 'read' => 1, 'write' => 1 },
3441 Told => { 'read' => 1, 'write' => 1 },
3442 Resolved => { 'read' => 1 },
3443 Type => { 'read' => 1 },
3444 Starts => { 'read' => 1, 'write' => 1 },
3445 Started => { 'read' => 1, 'write' => 1 },
3446 Due => { 'read' => 1, 'write' => 1 },
3447 Creator => { 'read' => 1, 'auto' => 1 },
3448 Created => { 'read' => 1, 'auto' => 1 },
3449 LastUpdatedBy => { 'read' => 1, 'auto' => 1 },
3450 LastUpdated => { 'read' => 1, 'auto' => 1 }
3451 };
3452
3453}
3454
3455
3456
3457sub _Set {
3458 my $self = shift;
3459
3460 my %args = ( Field => undef,
3461 Value => undef,
3462 TimeTaken => 0,
3463 RecordTransaction => 1,
3464 UpdateTicket => 1,
3465 CheckACL => 1,
3466 TransactionType => 'Set',
3467 @_ );
3468
3469 if ($args{'CheckACL'}) {
3470 unless ( $self->CurrentUserHasRight('ModifyTicket')) {
3471 return ( 0, $self->loc("Permission Denied"));
3472 }
3473 }
3474
3475 unless ($args{'UpdateTicket'} || $args{'RecordTransaction'}) {
3476 $RT::Logger->error("Ticket->_Set called without a mandate to record an update or update the ticket");
3477 return(0, $self->loc("Internal Error"));
3478 }
3479
3480 #if the user is trying to modify the record
3481
3482 #Take care of the old value we really don't want to get in an ACL loop.
3483 # so ask the super::_Value
3484 my $Old = $self->SUPER::_Value("$args{'Field'}");
3485
3486 my ($ret, $msg);
3487 if ( $args{'UpdateTicket'} ) {
3488
3489 #Set the new value
3490 ( $ret, $msg ) = $self->SUPER::_Set( Field => $args{'Field'},
3491 Value => $args{'Value'} );
3492
3493 #If we can't actually set the field to the value, don't record
3494 # a transaction. instead, get out of here.
3495 return ( 0, $msg ) unless $ret;
3496 }
3497
3498 if ( $args{'RecordTransaction'} == 1 ) {
3499
3500 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
3501 Type => $args{'TransactionType'},
3502 Field => $args{'Field'},
3503 NewValue => $args{'Value'},
3504 OldValue => $Old,
3505 TimeTaken => $args{'TimeTaken'},
3506 );
403d7b0b
MKG
3507 # Ensure that we can read the transaction, even if the change
3508 # just made the ticket unreadable to us
3509 $TransObj->{ _object_is_readable } = 1;
84fb5b46
MKG
3510 return ( $Trans, scalar $TransObj->BriefDescription );
3511 }
3512 else {
3513 return ( $ret, $msg );
3514 }
3515}
3516
3517
3518
3519=head2 _Value
3520
3521Takes the name of a table column.
3522Returns its value as a string, if the user passes an ACL check
3523
3524=cut
3525
3526sub _Value {
3527
3528 my $self = shift;
3529 my $field = shift;
3530
3531 #if the field is public, return it.
3532 if ( $self->_Accessible( $field, 'public' ) ) {
3533
3534 #$RT::Logger->debug("Skipping ACL check for $field");
3535 return ( $self->SUPER::_Value($field) );
3536
3537 }
3538
3539 #If the current user doesn't have ACLs, don't let em at it.
3540
3541 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
3542 return (undef);
3543 }
3544 return ( $self->SUPER::_Value($field) );
3545
3546}
3547
3548
3549
3550=head2 _UpdateTimeTaken
3551
3552This routine will increment the timeworked counter. it should
3553only be called from _NewTransaction
3554
3555=cut
3556
3557sub _UpdateTimeTaken {
3558 my $self = shift;
3559 my $Minutes = shift;
3560 my ($Total);
3561
3562 $Total = $self->SUPER::_Value("TimeWorked");
3563 $Total = ( $Total || 0 ) + ( $Minutes || 0 );
3564 $self->SUPER::_Set(
3565 Field => "TimeWorked",
3566 Value => $Total
3567 );
3568
3569 return ($Total);
3570}
3571
3572
3573
3574
3575
3576=head2 CurrentUserHasRight
3577
3578 Takes the textual name of a Ticket scoped right (from RT::ACE) and returns
35791 if the user has that right. It returns 0 if the user doesn't have that right.
3580
3581=cut
3582
3583sub CurrentUserHasRight {
3584 my $self = shift;
3585 my $right = shift;
3586
3587 return $self->CurrentUser->PrincipalObj->HasRight(
3588 Object => $self,
3589 Right => $right,
3590 )
3591}
3592
3593
3594=head2 CurrentUserCanSee
3595
3596Returns true if the current user can see the ticket, using ShowTicket
3597
3598=cut
3599
3600sub CurrentUserCanSee {
3601 my $self = shift;
3602 return $self->CurrentUserHasRight('ShowTicket');
3603}
3604
3605=head2 HasRight
3606
3607 Takes a paramhash with the attributes 'Right' and 'Principal'
3608 'Right' is a ticket-scoped textual right from RT::ACE
3609 'Principal' is an RT::User object
3610
3611 Returns 1 if the principal has the right. Returns undef if not.
3612
3613=cut
3614
3615sub HasRight {
3616 my $self = shift;
3617 my %args = (
3618 Right => undef,
3619 Principal => undef,
3620 @_
3621 );
3622
3623 unless ( ( defined $args{'Principal'} ) and ( ref( $args{'Principal'} ) ) )
3624 {
3625 Carp::cluck("Principal attrib undefined for Ticket::HasRight");
3626 $RT::Logger->crit("Principal attrib undefined for Ticket::HasRight");
3627 return(undef);
3628 }
3629
3630 return (
3631 $args{'Principal'}->HasRight(
3632 Object => $self,
3633 Right => $args{'Right'}
3634 )
3635 );
3636}
3637
3638
3639
3640=head2 Reminders
3641
3642Return the Reminders object for this ticket. (It's an RT::Reminders object.)
3643It isn't acutally a searchbuilder collection itself.
3644
3645=cut
3646
3647sub Reminders {
3648 my $self = shift;
3649
3650 unless ($self->{'__reminders'}) {
3651 $self->{'__reminders'} = RT::Reminders->new($self->CurrentUser);
3652 $self->{'__reminders'}->Ticket($self->id);
3653 }
3654 return $self->{'__reminders'};
3655
3656}
3657
3658
3659
3660
3661=head2 Transactions
3662
3663 Returns an RT::Transactions object of all transactions on this ticket
3664
3665=cut
3666
3667sub Transactions {
3668 my $self = shift;
3669
3670 my $transactions = RT::Transactions->new( $self->CurrentUser );
3671
3672 #If the user has no rights, return an empty object
3673 if ( $self->CurrentUserHasRight('ShowTicket') ) {
3674 $transactions->LimitToTicket($self->id);
3675
3676 # if the user may not see comments do not return them
3677 unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
3678 $transactions->Limit(
3679 SUBCLAUSE => 'acl',
3680 FIELD => 'Type',
3681 OPERATOR => '!=',
3682 VALUE => "Comment"
3683 );
3684 $transactions->Limit(
3685 SUBCLAUSE => 'acl',
3686 FIELD => 'Type',
3687 OPERATOR => '!=',
3688 VALUE => "CommentEmailRecord",
3689 ENTRYAGGREGATOR => 'AND'
3690 );
3691
3692 }
3693 } else {
3694 $transactions->Limit(
3695 SUBCLAUSE => 'acl',
3696 FIELD => 'id',
3697 VALUE => 0,
3698 ENTRYAGGREGATOR => 'AND'
3699 );
3700 }
3701
3702 return ($transactions);
3703}
3704
3705
3706
3707
3708=head2 TransactionCustomFields
3709
3710 Returns the custom fields that transactions on tickets will have.
3711
3712=cut
3713
3714sub TransactionCustomFields {
3715 my $self = shift;
3716 my $cfs = $self->QueueObj->TicketTransactionCustomFields;
3717 $cfs->SetContextObject( $self );
3718 return $cfs;
3719}
3720
3721
403d7b0b 3722=head2 LoadCustomFieldByIdentifier
84fb5b46 3723
403d7b0b
MKG
3724Finds and returns the custom field of the given name for the ticket,
3725overriding L<RT::Record/LoadCustomFieldByIdentifier> to look for
3726queue-specific CFs before global ones.
84fb5b46
MKG
3727
3728=cut
3729
403d7b0b 3730sub LoadCustomFieldByIdentifier {
84fb5b46
MKG
3731 my $self = shift;
3732 my $field = shift;
3733
403d7b0b
MKG
3734 return $self->SUPER::LoadCustomFieldByIdentifier($field)
3735 if ref $field or $field =~ /^\d+$/;
84fb5b46
MKG
3736
3737 my $cf = RT::CustomField->new( $self->CurrentUser );
3738 $cf->SetContextObject( $self );
3739 $cf->LoadByNameAndQueue( Name => $field, Queue => $self->Queue );
403d7b0b
MKG
3740 $cf->LoadByNameAndQueue( Name => $field, Queue => 0 ) unless $cf->id;
3741 return $cf;
84fb5b46
MKG
3742}
3743
3744
84fb5b46
MKG
3745=head2 CustomFieldLookupType
3746
3747Returns the RT::Ticket lookup type, which can be passed to
3748RT::CustomField->Create() via the 'LookupType' hash key.
3749
3750=cut
3751
3752
3753sub CustomFieldLookupType {
3754 "RT::Queue-RT::Ticket";
3755}
3756
3757=head2 ACLEquivalenceObjects
3758
3759This method returns a list of objects for which a user's rights also apply
3760to this ticket. Generally, this is only the ticket's queue, but some RT
3761extensions may make other objects available too.
3762
3763This method is called from L<RT::Principal/HasRight>.
3764
3765=cut
3766
3767sub ACLEquivalenceObjects {
3768 my $self = shift;
3769 return $self->QueueObj;
3770
3771}
3772
3773
37741;
3775
3776=head1 AUTHOR
3777
3778Jesse Vincent, jesse@bestpractical.com
3779
3780=head1 SEE ALSO
3781
3782RT
3783
3784=cut
3785
3786
3787use RT::Queue;
3788use base 'RT::Record';
3789
3790sub Table {'Tickets'}
3791
3792
3793
3794
3795
3796
3797=head2 id
3798
3799Returns the current value of id.
3800(In the database, id is stored as int(11).)
3801
3802
3803=cut
3804
3805
3806=head2 EffectiveId
3807
3808Returns the current value of EffectiveId.
3809(In the database, EffectiveId is stored as int(11).)
3810
3811
3812
3813=head2 SetEffectiveId VALUE
3814
3815
3816Set EffectiveId to VALUE.
3817Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3818(In the database, EffectiveId will be stored as a int(11).)
3819
3820
3821=cut
3822
3823
3824=head2 Queue
3825
3826Returns the current value of Queue.
3827(In the database, Queue is stored as int(11).)
3828
3829
3830
3831=head2 SetQueue VALUE
3832
3833
3834Set Queue to VALUE.
3835Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3836(In the database, Queue will be stored as a int(11).)
3837
3838
3839=cut
3840
3841
3842=head2 Type
3843
3844Returns the current value of Type.
3845(In the database, Type is stored as varchar(16).)
3846
3847
3848
3849=head2 SetType VALUE
3850
3851
3852Set Type to VALUE.
3853Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3854(In the database, Type will be stored as a varchar(16).)
3855
3856
3857=cut
3858
3859
3860=head2 IssueStatement
3861
3862Returns the current value of IssueStatement.
3863(In the database, IssueStatement is stored as int(11).)
3864
3865
3866
3867=head2 SetIssueStatement VALUE
3868
3869
3870Set IssueStatement to VALUE.
3871Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3872(In the database, IssueStatement will be stored as a int(11).)
3873
3874
3875=cut
3876
3877
3878=head2 Resolution
3879
3880Returns the current value of Resolution.
3881(In the database, Resolution is stored as int(11).)
3882
3883
3884
3885=head2 SetResolution VALUE
3886
3887
3888Set Resolution to VALUE.
3889Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3890(In the database, Resolution will be stored as a int(11).)
3891
3892
3893=cut
3894
3895
3896=head2 Owner
3897
3898Returns the current value of Owner.
3899(In the database, Owner is stored as int(11).)
3900
3901
3902
3903=head2 SetOwner VALUE
3904
3905
3906Set Owner to VALUE.
3907Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3908(In the database, Owner will be stored as a int(11).)
3909
3910
3911=cut
3912
3913
3914=head2 Subject
3915
3916Returns the current value of Subject.
3917(In the database, Subject is stored as varchar(200).)
3918
3919
3920
3921=head2 SetSubject VALUE
3922
3923
3924Set Subject to VALUE.
3925Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3926(In the database, Subject will be stored as a varchar(200).)
3927
3928
3929=cut
3930
3931
3932=head2 InitialPriority
3933
3934Returns the current value of InitialPriority.
3935(In the database, InitialPriority is stored as int(11).)
3936
3937
3938
3939=head2 SetInitialPriority VALUE
3940
3941
3942Set InitialPriority to VALUE.
3943Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3944(In the database, InitialPriority will be stored as a int(11).)
3945
3946
3947=cut
3948
3949
3950=head2 FinalPriority
3951
3952Returns the current value of FinalPriority.
3953(In the database, FinalPriority is stored as int(11).)
3954
3955
3956
3957=head2 SetFinalPriority VALUE
3958
3959