Upgrade to 4.0.8 with mod of ExternalAuth + absolute paths to ticket-menu.
[usit-rt.git] / lib / RT / Ticket.pm
CommitLineData
84fb5b46
MKG
1# BEGIN BPS TAGGED BLOCK {{{
2#
3# COPYRIGHT:
4#
5# This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC
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
57This module lets you manipulate RT\'s ticket object.
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
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
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
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
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.
787Doesn\'t create a transaction.
788Doesn\'t supply queue defaults, etc.
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
822 #TODO error check this and return 0 if it\'s not loading properly +++
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
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.
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
1143 return ( 0, $self->loc('That principal is already a [_1] for this ticket', $self->loc($args{'Type'})) );
1144 }
1145
1146
1147 my ( $m_id, $m_msg ) = $group->_AddMember( PrincipalId => $principal->Id,
1148 InsideTransaction => 1 );
1149 unless ($m_id) {
1150 $RT::Logger->error("Failed to add ".$principal->Id." as a member of group ".$group->Id.": ".$m_msg);
1151
1152 return ( 0, $self->loc('Could not make that principal a [_1] for this ticket', $self->loc($args{'Type'})) );
1153 }
1154
1155 unless ( $args{'Silent'} ) {
1156 $self->_NewTransaction(
1157 Type => 'AddWatcher',
1158 NewValue => $principal->Id,
1159 Field => $args{'Type'}
1160 );
1161 }
1162
1163 return ( 1, $self->loc('Added principal as a [_1] for this ticket', $self->loc($args{'Type'})) );
1164}
1165
1166
1167
1168
1169=head2 DeleteWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL_ADDRESS }
1170
1171
1172Deletes a Ticket watcher. Takes two arguments:
1173
1174Type (one of Requestor,Cc,AdminCc)
1175
1176and one of
1177
1178PrincipalId (an RT::Principal Id of the watcher you want to remove)
1179 OR
1180Email (the email address of an existing wathcer)
1181
1182
1183=cut
1184
1185
1186sub DeleteWatcher {
1187 my $self = shift;
1188
1189 my %args = ( Type => undef,
1190 PrincipalId => undef,
1191 Email => undef,
1192 @_ );
1193
1194 unless ( $args{'PrincipalId'} || $args{'Email'} ) {
1195 return ( 0, $self->loc("No principal specified") );
1196 }
1197 my $principal = RT::Principal->new( $self->CurrentUser );
1198 if ( $args{'PrincipalId'} ) {
1199
1200 $principal->Load( $args{'PrincipalId'} );
1201 }
1202 else {
1203 my $user = RT::User->new( $self->CurrentUser );
1204 $user->LoadByEmail( $args{'Email'} );
1205 $principal->Load( $user->Id );
1206 }
1207
1208 # If we can't find this watcher, we need to bail.
1209 unless ( $principal->Id ) {
1210 return ( 0, $self->loc("Could not find that principal") );
1211 }
1212
1213 my $group = RT::Group->new( $self->CurrentUser );
1214 $group->LoadTicketRoleGroup( Type => $args{'Type'}, Ticket => $self->Id );
1215 unless ( $group->id ) {
1216 return ( 0, $self->loc("Group not found") );
1217 }
1218
1219 # Check ACLS
1220 #If the watcher we're trying to add is for the current user
1221 if ( $self->CurrentUser->PrincipalId == $principal->id ) {
1222
1223 # If it's an AdminCc and they don't have
1224 # 'WatchAsAdminCc' or 'ModifyTicket', bail
1225 if ( $args{'Type'} eq 'AdminCc' ) {
1226 unless ( $self->CurrentUserHasRight('ModifyTicket')
1227 or $self->CurrentUserHasRight('WatchAsAdminCc') ) {
1228 return ( 0, $self->loc('Permission Denied') );
1229 }
1230 }
1231
1232 # If it's a Requestor or Cc and they don't have
1233 # 'Watch' or 'ModifyTicket', bail
1234 elsif ( ( $args{'Type'} eq 'Cc' ) or ( $args{'Type'} eq 'Requestor' ) )
1235 {
1236 unless ( $self->CurrentUserHasRight('ModifyTicket')
1237 or $self->CurrentUserHasRight('Watch') ) {
1238 return ( 0, $self->loc('Permission Denied') );
1239 }
1240 }
1241 else {
dab09ea8 1242 $RT::Logger->warning("$self -> DeleteWatcher got passed a bogus type");
84fb5b46
MKG
1243 return ( 0,
1244 $self->loc('Error in parameters to Ticket->DeleteWatcher') );
1245 }
1246 }
1247
1248 # If the watcher isn't the current user
1249 # and the current user doesn't have 'ModifyTicket' bail
1250 else {
1251 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1252 return ( 0, $self->loc("Permission Denied") );
1253 }
1254 }
1255
1256 # }}}
1257
1258 # see if this user is already a watcher.
1259
1260 unless ( $group->HasMember($principal) ) {
1261 return ( 0,
1262 $self->loc( 'That principal is not a [_1] for this ticket',
1263 $args{'Type'} ) );
1264 }
1265
1266 my ( $m_id, $m_msg ) = $group->_DeleteMember( $principal->Id );
1267 unless ($m_id) {
1268 $RT::Logger->error( "Failed to delete "
1269 . $principal->Id
1270 . " as a member of group "
1271 . $group->Id . ": "
1272 . $m_msg );
1273
1274 return (0,
1275 $self->loc(
1276 'Could not remove that principal as a [_1] for this ticket',
1277 $args{'Type'} ) );
1278 }
1279
1280 unless ( $args{'Silent'} ) {
1281 $self->_NewTransaction( Type => 'DelWatcher',
1282 OldValue => $principal->Id,
1283 Field => $args{'Type'} );
1284 }
1285
1286 return ( 1,
1287 $self->loc( "[_1] is no longer a [_2] for this ticket.",
1288 $principal->Object->Name,
1289 $args{'Type'} ) );
1290}
1291
1292
1293
1294
1295
1296=head2 SquelchMailTo [EMAIL]
1297
1298Takes an optional email address to never email about updates to this ticket.
1299
1300
1301Returns an array of the RT::Attribute objects for this ticket's 'SquelchMailTo' attributes.
1302
1303
1304=cut
1305
1306sub SquelchMailTo {
1307 my $self = shift;
1308 if (@_) {
1309 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1310 return ();
1311 }
1312 } else {
1313 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1314 return ();
1315 }
1316
1317 }
1318 return $self->_SquelchMailTo(@_);
1319}
1320
1321sub _SquelchMailTo {
1322 my $self = shift;
1323 if (@_) {
1324 my $attr = shift;
1325 $self->AddAttribute( Name => 'SquelchMailTo', Content => $attr )
1326 unless grep { $_->Content eq $attr }
1327 $self->Attributes->Named('SquelchMailTo');
1328 }
1329 my @attributes = $self->Attributes->Named('SquelchMailTo');
1330 return (@attributes);
1331}
1332
1333
1334=head2 UnsquelchMailTo ADDRESS
1335
1336Takes an address and removes it from this ticket's "SquelchMailTo" list. If an address appears multiple times, each instance is removed.
1337
1338Returns a tuple of (status, message)
1339
1340=cut
1341
1342sub UnsquelchMailTo {
1343 my $self = shift;
1344
1345 my $address = shift;
1346 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1347 return ( 0, $self->loc("Permission Denied") );
1348 }
1349
1350 my ($val, $msg) = $self->Attributes->DeleteEntry ( Name => 'SquelchMailTo', Content => $address);
1351 return ($val, $msg);
1352}
1353
1354
1355
1356=head2 RequestorAddresses
1357
1358 B<Returns> String: All Ticket Requestor email addresses as a string.
1359
1360=cut
1361
1362sub RequestorAddresses {
1363 my $self = shift;
1364
1365 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1366 return undef;
1367 }
1368
1369 return ( $self->Requestors->MemberEmailAddressesAsString );
1370}
1371
1372
1373=head2 AdminCcAddresses
1374
1375returns String: All Ticket AdminCc email addresses as a string
1376
1377=cut
1378
1379sub AdminCcAddresses {
1380 my $self = shift;
1381
1382 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1383 return undef;
1384 }
1385
1386 return ( $self->AdminCc->MemberEmailAddressesAsString )
1387
1388}
1389
1390=head2 CcAddresses
1391
1392returns String: All Ticket Ccs as a string of email addresses
1393
1394=cut
1395
1396sub CcAddresses {
1397 my $self = shift;
1398
1399 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1400 return undef;
1401 }
1402 return ( $self->Cc->MemberEmailAddressesAsString);
1403
1404}
1405
1406
1407
1408
1409=head2 Requestors
1410
1411Takes nothing.
1412Returns this ticket's Requestors as an RT::Group object
1413
1414=cut
1415
1416sub Requestors {
1417 my $self = shift;
1418
1419 my $group = RT::Group->new($self->CurrentUser);
1420 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1421 $group->LoadTicketRoleGroup(Type => 'Requestor', Ticket => $self->Id);
1422 }
1423 return ($group);
1424
1425}
1426
1427
1428
1429=head2 Cc
1430
1431Takes nothing.
1432Returns an RT::Group object which contains this ticket's Ccs.
1433If the user doesn't have "ShowTicket" permission, returns an empty group
1434
1435=cut
1436
1437sub Cc {
1438 my $self = shift;
1439
1440 my $group = RT::Group->new($self->CurrentUser);
1441 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1442 $group->LoadTicketRoleGroup(Type => 'Cc', Ticket => $self->Id);
1443 }
1444 return ($group);
1445
1446}
1447
1448
1449
1450=head2 AdminCc
1451
1452Takes nothing.
1453Returns an RT::Group object which contains this ticket's AdminCcs.
1454If the user doesn't have "ShowTicket" permission, returns an empty group
1455
1456=cut
1457
1458sub AdminCc {
1459 my $self = shift;
1460
1461 my $group = RT::Group->new($self->CurrentUser);
1462 if ( $self->CurrentUserHasRight('ShowTicket') ) {
1463 $group->LoadTicketRoleGroup(Type => 'AdminCc', Ticket => $self->Id);
1464 }
1465 return ($group);
1466
1467}
1468
1469
1470
1471
1472# a generic routine to be called by IsRequestor, IsCc and IsAdminCc
1473
1474=head2 IsWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL }
1475
1476Takes a param hash with the attributes Type and either PrincipalId or Email
1477
1478Type is one of Requestor, Cc, AdminCc and Owner
1479
1480PrincipalId is an RT::Principal id, and Email is an email address.
1481
1482Returns true if the specified principal (or the one corresponding to the
1483specified address) is a member of the group Type for this ticket.
1484
1485XX TODO: This should be Memoized.
1486
1487=cut
1488
1489sub IsWatcher {
1490 my $self = shift;
1491
1492 my %args = ( Type => 'Requestor',
1493 PrincipalId => undef,
1494 Email => undef,
1495 @_
1496 );
1497
1498 # Load the relevant group.
1499 my $group = RT::Group->new($self->CurrentUser);
1500 $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->id);
1501
1502 # Find the relevant principal.
1503 if (!$args{PrincipalId} && $args{Email}) {
1504 # Look up the specified user.
1505 my $user = RT::User->new($self->CurrentUser);
1506 $user->LoadByEmail($args{Email});
1507 if ($user->Id) {
1508 $args{PrincipalId} = $user->PrincipalId;
1509 }
1510 else {
1511 # A non-existent user can't be a group member.
1512 return 0;
1513 }
1514 }
1515
1516 # Ask if it has the member in question
1517 return $group->HasMember( $args{'PrincipalId'} );
1518}
1519
1520
1521
1522=head2 IsRequestor PRINCIPAL_ID
1523
1524Takes an L<RT::Principal> id.
1525
1526Returns true if the principal is a requestor of the current ticket.
1527
1528=cut
1529
1530sub IsRequestor {
1531 my $self = shift;
1532 my $person = shift;
1533
1534 return ( $self->IsWatcher( Type => 'Requestor', PrincipalId => $person ) );
1535
1536};
1537
1538
1539
1540=head2 IsCc PRINCIPAL_ID
1541
1542 Takes an RT::Principal id.
1543 Returns true if the principal is a Cc of the current ticket.
1544
1545
1546=cut
1547
1548sub IsCc {
1549 my $self = shift;
1550 my $cc = shift;
1551
1552 return ( $self->IsWatcher( Type => 'Cc', PrincipalId => $cc ) );
1553
1554}
1555
1556
1557
1558=head2 IsAdminCc PRINCIPAL_ID
1559
1560 Takes an RT::Principal id.
1561 Returns true if the principal is an AdminCc of the current ticket.
1562
1563=cut
1564
1565sub IsAdminCc {
1566 my $self = shift;
1567 my $person = shift;
1568
1569 return ( $self->IsWatcher( Type => 'AdminCc', PrincipalId => $person ) );
1570
1571}
1572
1573
1574
1575=head2 IsOwner
1576
1577 Takes an RT::User object. Returns true if that user is this ticket's owner.
1578returns undef otherwise
1579
1580=cut
1581
1582sub IsOwner {
1583 my $self = shift;
1584 my $person = shift;
1585
1586 # no ACL check since this is used in acl decisions
1587 # unless ($self->CurrentUserHasRight('ShowTicket')) {
1588 # return(undef);
1589 # }
1590
1591 #Tickets won't yet have owners when they're being created.
1592 unless ( $self->OwnerObj->id ) {
1593 return (undef);
1594 }
1595
1596 if ( $person->id == $self->OwnerObj->id ) {
1597 return (1);
1598 }
1599 else {
1600 return (undef);
1601 }
1602}
1603
1604
1605
1606
1607
1608=head2 TransactionAddresses
1609
1610Returns a composite hashref of the results of L<RT::Transaction/Addresses> for
1611all this ticket's Create, Comment or Correspond transactions. The keys are
1612stringified email addresses. Each value is an L<Email::Address> object.
1613
1614NOTE: 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.
1615
1616=cut
1617
1618
1619sub TransactionAddresses {
1620 my $self = shift;
1621 my $txns = $self->Transactions;
1622
1623 my %addresses = ();
1624
1625 my $attachments = RT::Attachments->new( $self->CurrentUser );
1626 $attachments->LimitByTicket( $self->id );
1627 $attachments->Columns( qw( id Headers TransactionId));
1628
1629
1630 foreach my $type (qw(Create Comment Correspond)) {
1631 $attachments->Limit( ALIAS => $attachments->TransactionAlias,
1632 FIELD => 'Type',
1633 OPERATOR => '=',
1634 VALUE => $type,
1635 ENTRYAGGREGATOR => 'OR',
1636 CASESENSITIVE => 1
1637 );
1638 }
1639
1640 while ( my $att = $attachments->Next ) {
1641 foreach my $addrlist ( values %{$att->Addresses } ) {
1642 foreach my $addr (@$addrlist) {
1643
1644# Skip addresses without a phrase (things that are just raw addresses) if we have a phrase
1645 next
1646 if ( $addresses{ $addr->address }
1647 && $addresses{ $addr->address }->phrase
1648 && not $addr->phrase );
1649
1650 # skips "comment-only" addresses
1651 next unless ( $addr->address );
1652 $addresses{ $addr->address } = $addr;
1653 }
1654 }
1655 }
1656
1657 return \%addresses;
1658
1659}
1660
1661
1662
1663
1664
1665
1666sub ValidateQueue {
1667 my $self = shift;
1668 my $Value = shift;
1669
1670 if ( !$Value ) {
1671 $RT::Logger->warning( " RT:::Queue::ValidateQueue called with a null value. this isn't ok.");
1672 return (1);
1673 }
1674
1675 my $QueueObj = RT::Queue->new( $self->CurrentUser );
1676 my $id = $QueueObj->Load($Value);
1677
1678 if ($id) {
1679 return (1);
1680 }
1681 else {
1682 return (undef);
1683 }
1684}
1685
1686
1687
1688sub SetQueue {
1689 my $self = shift;
1690 my $NewQueue = shift;
1691
1692 #Redundant. ACL gets checked in _Set;
1693 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1694 return ( 0, $self->loc("Permission Denied") );
1695 }
1696
1697 my $NewQueueObj = RT::Queue->new( $self->CurrentUser );
1698 $NewQueueObj->Load($NewQueue);
1699
1700 unless ( $NewQueueObj->Id() ) {
1701 return ( 0, $self->loc("That queue does not exist") );
1702 }
1703
1704 if ( $NewQueueObj->Id == $self->QueueObj->Id ) {
1705 return ( 0, $self->loc('That is the same value') );
1706 }
1707 unless ( $self->CurrentUser->HasRight( Right => 'CreateTicket', Object => $NewQueueObj)) {
1708 return ( 0, $self->loc("You may not create requests in that queue.") );
1709 }
1710
1711 my $new_status;
1712 my $old_lifecycle = $self->QueueObj->Lifecycle;
1713 my $new_lifecycle = $NewQueueObj->Lifecycle;
1714 if ( $old_lifecycle->Name ne $new_lifecycle->Name ) {
1715 unless ( $old_lifecycle->HasMoveMap( $new_lifecycle ) ) {
1716 return ( 0, $self->loc("There is no mapping for statuses between these queues. Contact your system administrator.") );
1717 }
1718 $new_status = $old_lifecycle->MoveMap( $new_lifecycle )->{ $self->Status };
1719 return ( 0, $self->loc("Mapping between queues' lifecycles is incomplete. Contact your system administrator.") )
1720 unless $new_status;
1721 }
1722
1723 if ( $new_status ) {
1724 my $clone = RT::Ticket->new( RT->SystemUser );
1725 $clone->Load( $self->Id );
1726 unless ( $clone->Id ) {
1727 return ( 0, $self->loc("Couldn't load copy of ticket #[_1].", $self->Id) );
1728 }
1729
1730 my $now = RT::Date->new( $self->CurrentUser );
1731 $now->SetToNow;
1732
1733 my $old_status = $clone->Status;
1734
1735 #If we're changing the status from initial in old to not intial in new,
1736 # record that we've started
1737 if ( $old_lifecycle->IsInitial($old_status) && !$new_lifecycle->IsInitial($new_status) && $clone->StartedObj->Unix == 0 ) {
1738 #Set the Started time to "now"
1739 $clone->_Set(
1740 Field => 'Started',
1741 Value => $now->ISO,
1742 RecordTransaction => 0
1743 );
1744 }
1745
1746 #When we close a ticket, set the 'Resolved' attribute to now.
1747 # It's misnamed, but that's just historical.
1748 if ( $new_lifecycle->IsInactive($new_status) ) {
1749 $clone->_Set(
1750 Field => 'Resolved',
1751 Value => $now->ISO,
1752 RecordTransaction => 0,
1753 );
1754 }
1755
1756 #Actually update the status
1757 my ($val, $msg)= $clone->_Set(
1758 Field => 'Status',
1759 Value => $new_status,
1760 RecordTransaction => 0,
1761 );
1762 $RT::Logger->error( 'Status change failed on queue change: '. $msg )
1763 unless $val;
1764 }
1765
1766 my ($status, $msg) = $self->_Set( Field => 'Queue', Value => $NewQueueObj->Id() );
1767
1768 if ( $status ) {
1769 # Clear the queue object cache;
1770 $self->{_queue_obj} = undef;
1771
1772 # Untake the ticket if we have no permissions in the new queue
1773 unless ( $self->OwnerObj->HasRight( Right => 'OwnTicket', Object => $NewQueueObj ) ) {
1774 my $clone = RT::Ticket->new( RT->SystemUser );
1775 $clone->Load( $self->Id );
1776 unless ( $clone->Id ) {
1777 return ( 0, $self->loc("Couldn't load copy of ticket #[_1].", $self->Id) );
1778 }
1779 my ($status, $msg) = $clone->SetOwner( RT->Nobody->Id, 'Force' );
1780 $RT::Logger->error("Couldn't set owner on queue change: $msg") unless $status;
1781 }
1782
1783 # On queue change, change queue for reminders too
1784 my $reminder_collection = $self->Reminders->Collection;
1785 while ( my $reminder = $reminder_collection->Next ) {
1786 my ($status, $msg) = $reminder->SetQueue($NewQueue);
1787 $RT::Logger->error('Queue change failed for reminder #' . $reminder->Id . ': ' . $msg) unless $status;
1788 }
1789 }
1790
1791 return ($status, $msg);
1792}
1793
1794
1795
1796=head2 QueueObj
1797
1798Takes nothing. returns this ticket's queue object
1799
1800=cut
1801
1802sub QueueObj {
1803 my $self = shift;
1804
1805 if(!$self->{_queue_obj} || ! $self->{_queue_obj}->id) {
1806
1807 $self->{_queue_obj} = RT::Queue->new( $self->CurrentUser );
1808
1809 #We call __Value so that we can avoid the ACL decision and some deep recursion
1810 my ($result) = $self->{_queue_obj}->Load( $self->__Value('Queue') );
1811 }
1812 return ($self->{_queue_obj});
1813}
1814
1815=head2 SubjectTag
1816
1817Takes nothing. Returns SubjectTag for this ticket. Includes
1818queue's subject tag or rtname if that is not set, ticket
1819id and braces, for example:
1820
1821 [support.example.com #123456]
1822
1823=cut
1824
1825sub SubjectTag {
1826 my $self = shift;
1827 return
1828 '['
1829 . ($self->QueueObj->SubjectTag || RT->Config->Get('rtname'))
1830 .' #'. $self->id
1831 .']'
1832 ;
1833}
1834
1835
1836=head2 DueObj
1837
1838 Returns an RT::Date object containing this ticket's due date
1839
1840=cut
1841
1842sub DueObj {
1843 my $self = shift;
1844
1845 my $time = RT::Date->new( $self->CurrentUser );
1846
1847 # -1 is RT::Date slang for never
1848 if ( my $due = $self->Due ) {
1849 $time->Set( Format => 'sql', Value => $due );
1850 }
1851 else {
1852 $time->Set( Format => 'unix', Value => -1 );
1853 }
1854
1855 return $time;
1856}
1857
1858
1859
1860=head2 DueAsString
1861
1862Returns this ticket's due date as a human readable string
1863
1864=cut
1865
1866sub DueAsString {
1867 my $self = shift;
1868 return $self->DueObj->AsString();
1869}
1870
1871
1872
1873=head2 ResolvedObj
1874
1875 Returns an RT::Date object of this ticket's 'resolved' time.
1876
1877=cut
1878
1879sub ResolvedObj {
1880 my $self = shift;
1881
1882 my $time = RT::Date->new( $self->CurrentUser );
1883 $time->Set( Format => 'sql', Value => $self->Resolved );
1884 return $time;
1885}
1886
1887
1888=head2 FirstActiveStatus
1889
1890Returns the first active status that the ticket could transition to,
1891according to its current Queue's lifecycle. May return undef if there
1892is no such possible status to transition to, or we are already in it.
1893This is used in L<RT::Action::AutoOpen>, for instance.
1894
1895=cut
1896
1897sub FirstActiveStatus {
1898 my $self = shift;
1899
1900 my $lifecycle = $self->QueueObj->Lifecycle;
1901 my $status = $self->Status;
1902 my @active = $lifecycle->Active;
1903 # no change if no active statuses in the lifecycle
1904 return undef unless @active;
1905
1906 # no change if the ticket is already has first status from the list of active
1907 return undef if lc $status eq lc $active[0];
1908
1909 my ($next) = grep $lifecycle->IsActive($_), $lifecycle->Transitions($status);
1910 return $next;
1911}
1912
dab09ea8
MKG
1913=head2 FirstInactiveStatus
1914
1915Returns the first inactive status that the ticket could transition to,
1916according to its current Queue's lifecycle. May return undef if there
1917is no such possible status to transition to, or we are already in it.
1918This is used in resolve action in UnsafeEmailCommands, for instance.
1919
1920=cut
1921
1922sub FirstInactiveStatus {
1923 my $self = shift;
1924
1925 my $lifecycle = $self->QueueObj->Lifecycle;
1926 my $status = $self->Status;
1927 my @inactive = $lifecycle->Inactive;
1928 # no change if no inactive statuses in the lifecycle
1929 return undef unless @inactive;
1930
1931 # no change if the ticket is already has first status from the list of inactive
1932 return undef if lc $status eq lc $inactive[0];
1933
1934 my ($next) = grep $lifecycle->IsInactive($_), $lifecycle->Transitions($status);
1935 return $next;
1936}
1937
84fb5b46
MKG
1938=head2 SetStarted
1939
1940Takes a date in ISO format or undef
1941Returns a transaction id and a message
1942The client calls "Start" to note that the project was started on the date in $date.
1943A null date means "now"
1944
1945=cut
1946
1947sub SetStarted {
1948 my $self = shift;
1949 my $time = shift || 0;
1950
1951 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1952 return ( 0, $self->loc("Permission Denied") );
1953 }
1954
1955 #We create a date object to catch date weirdness
1956 my $time_obj = RT::Date->new( $self->CurrentUser() );
1957 if ( $time ) {
1958 $time_obj->Set( Format => 'ISO', Value => $time );
1959 }
1960 else {
1961 $time_obj->SetToNow();
1962 }
1963
1964 # We need $TicketAsSystem, in case the current user doesn't have
1965 # ShowTicket
1966 my $TicketAsSystem = RT::Ticket->new(RT->SystemUser);
1967 $TicketAsSystem->Load( $self->Id );
1968 # Now that we're starting, open this ticket
1969 # TODO: do we really want to force this as policy? it should be a scrip
1970 my $next = $TicketAsSystem->FirstActiveStatus;
1971
1972 $self->SetStatus( $next ) if defined $next;
1973
1974 return ( $self->_Set( Field => 'Started', Value => $time_obj->ISO ) );
1975
1976}
1977
1978
1979
1980=head2 StartedObj
1981
1982 Returns an RT::Date object which contains this ticket's
1983'Started' time.
1984
1985=cut
1986
1987sub StartedObj {
1988 my $self = shift;
1989
1990 my $time = RT::Date->new( $self->CurrentUser );
1991 $time->Set( Format => 'sql', Value => $self->Started );
1992 return $time;
1993}
1994
1995
1996
1997=head2 StartsObj
1998
1999 Returns an RT::Date object which contains this ticket's
2000'Starts' time.
2001
2002=cut
2003
2004sub StartsObj {
2005 my $self = shift;
2006
2007 my $time = RT::Date->new( $self->CurrentUser );
2008 $time->Set( Format => 'sql', Value => $self->Starts );
2009 return $time;
2010}
2011
2012
2013
2014=head2 ToldObj
2015
2016 Returns an RT::Date object which contains this ticket's
2017'Told' time.
2018
2019=cut
2020
2021sub ToldObj {
2022 my $self = shift;
2023
2024 my $time = RT::Date->new( $self->CurrentUser );
2025 $time->Set( Format => 'sql', Value => $self->Told );
2026 return $time;
2027}
2028
2029
2030
2031=head2 ToldAsString
2032
2033A convenience method that returns ToldObj->AsString
2034
2035TODO: This should be deprecated
2036
2037=cut
2038
2039sub ToldAsString {
2040 my $self = shift;
2041 if ( $self->Told ) {
2042 return $self->ToldObj->AsString();
2043 }
2044 else {
2045 return ("Never");
2046 }
2047}
2048
2049
2050
2051=head2 TimeWorkedAsString
2052
2053Returns the amount of time worked on this ticket as a Text String
2054
2055=cut
2056
2057sub TimeWorkedAsString {
2058 my $self = shift;
2059 my $value = $self->TimeWorked;
2060
2061 # return the # of minutes worked turned into seconds and written as
2062 # a simple text string, this is not really a date object, but if we
2063 # diff a number of seconds vs the epoch, we'll get a nice description
2064 # of time worked.
2065 return "" unless $value;
2066 return RT::Date->new( $self->CurrentUser )
2067 ->DurationAsString( $value * 60 );
2068}
2069
2070
2071
2072=head2 TimeLeftAsString
2073
2074Returns the amount of time left on this ticket as a Text String
2075
2076=cut
2077
2078sub TimeLeftAsString {
2079 my $self = shift;
2080 my $value = $self->TimeLeft;
2081 return "" unless $value;
2082 return RT::Date->new( $self->CurrentUser )
2083 ->DurationAsString( $value * 60 );
2084}
2085
2086
2087
2088
2089=head2 Comment
2090
2091Comment on this ticket.
2092Takes a hash with the following attributes:
2093If MIMEObj is undefined, Content will be used to build a MIME::Entity for this
2094comment.
2095
2096MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
2097
2098If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
2099They will, however, be prepared and you'll be able to access them through the TransactionObj
2100
2101Returns: Transaction id, Error Message, Transaction Object
2102(note the different order from Create()!)
2103
2104=cut
2105
2106sub Comment {
2107 my $self = shift;
2108
2109 my %args = ( CcMessageTo => undef,
2110 BccMessageTo => undef,
2111 MIMEObj => undef,
2112 Content => undef,
2113 TimeTaken => 0,
2114 DryRun => 0,
2115 @_ );
2116
2117 unless ( ( $self->CurrentUserHasRight('CommentOnTicket') )
2118 or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
2119 return ( 0, $self->loc("Permission Denied"), undef );
2120 }
2121 $args{'NoteType'} = 'Comment';
2122
dab09ea8 2123 $RT::Handle->BeginTransaction();
84fb5b46 2124 if ($args{'DryRun'}) {
84fb5b46
MKG
2125 $args{'CommitScrips'} = 0;
2126 }
2127
2128 my @results = $self->_RecordNote(%args);
2129 if ($args{'DryRun'}) {
2130 $RT::Handle->Rollback();
dab09ea8
MKG
2131 } else {
2132 $RT::Handle->Commit();
84fb5b46
MKG
2133 }
2134
2135 return(@results);
2136}
2137
2138
2139=head2 Correspond
2140
2141Correspond on this ticket.
2142Takes a hashref with the following attributes:
2143
2144
2145MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
2146
2147if there's no MIMEObj, Content is used to build a MIME::Entity object
2148
2149If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
2150They will, however, be prepared and you'll be able to access them through the TransactionObj
2151
2152Returns: Transaction id, Error Message, Transaction Object
2153(note the different order from Create()!)
2154
2155
2156=cut
2157
2158sub Correspond {
2159 my $self = shift;
2160 my %args = ( CcMessageTo => undef,
2161 BccMessageTo => undef,
2162 MIMEObj => undef,
2163 Content => undef,
2164 TimeTaken => 0,
2165 @_ );
2166
2167 unless ( ( $self->CurrentUserHasRight('ReplyToTicket') )
2168 or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
2169 return ( 0, $self->loc("Permission Denied"), undef );
2170 }
dab09ea8 2171 $args{'NoteType'} = 'Correspond';
84fb5b46 2172
dab09ea8 2173 $RT::Handle->BeginTransaction();
84fb5b46 2174 if ($args{'DryRun'}) {
84fb5b46
MKG
2175 $args{'CommitScrips'} = 0;
2176 }
2177
2178 my @results = $self->_RecordNote(%args);
2179
2180 #Set the last told date to now if this isn't mail from the requestor.
2181 #TODO: Note that this will wrongly ack mail from any non-requestor as a "told"
2182 unless ( $self->IsRequestor($self->CurrentUser->id) ) {
2183 my %squelch;
2184 $squelch{$_}++ for map {$_->Content} $self->SquelchMailTo, $results[2]->SquelchMailTo;
2185 $self->_SetTold
2186 if grep {not $squelch{$_}} $self->Requestors->MemberEmailAddresses;
2187 }
2188
2189 if ($args{'DryRun'}) {
2190 $RT::Handle->Rollback();
dab09ea8
MKG
2191 } else {
2192 $RT::Handle->Commit();
84fb5b46
MKG
2193 }
2194
2195 return (@results);
2196
2197}
2198
2199
2200
2201=head2 _RecordNote
2202
2203the meat of both comment and correspond.
2204
2205Performs no access control checks. hence, dangerous.
2206
2207=cut
2208
2209sub _RecordNote {
2210 my $self = shift;
2211 my %args = (
2212 CcMessageTo => undef,
2213 BccMessageTo => undef,
2214 Encrypt => undef,
2215 Sign => undef,
2216 MIMEObj => undef,
2217 Content => undef,
2218 NoteType => 'Correspond',
2219 TimeTaken => 0,
2220 CommitScrips => 1,
2221 SquelchMailTo => undef,
2222 @_
2223 );
2224
2225 unless ( $args{'MIMEObj'} || $args{'Content'} ) {
2226 return ( 0, $self->loc("No message attached"), undef );
2227 }
2228
2229 unless ( $args{'MIMEObj'} ) {
2230 $args{'MIMEObj'} = MIME::Entity->build(
2231 Data => ( ref $args{'Content'}? $args{'Content'}: [ $args{'Content'} ] )
2232 );
2233 }
2234
2235 # convert text parts into utf-8
2236 RT::I18N::SetMIMEEntityToUTF8( $args{'MIMEObj'} );
2237
2238 # If we've been passed in CcMessageTo and BccMessageTo fields,
2239 # add them to the mime object for passing on to the transaction handler
2240 # The "NotifyOtherRecipients" scripAction will look for RT-Send-Cc: and
2241 # RT-Send-Bcc: headers
2242
2243
2244 foreach my $type (qw/Cc Bcc/) {
2245 if ( defined $args{ $type . 'MessageTo' } ) {
2246
2247 my $addresses = join ', ', (
2248 map { RT::User->CanonicalizeEmailAddress( $_->address ) }
2249 Email::Address->parse( $args{ $type . 'MessageTo' } ) );
2250 $args{'MIMEObj'}->head->replace( 'RT-Send-' . $type, Encode::encode_utf8( $addresses ) );
2251 }
2252 }
2253
2254 foreach my $argument (qw(Encrypt Sign)) {
2255 $args{'MIMEObj'}->head->replace(
2256 "X-RT-$argument" => Encode::encode_utf8( $args{ $argument } )
2257 ) if defined $args{ $argument };
2258 }
2259
2260 # If this is from an external source, we need to come up with its
2261 # internal Message-ID now, so all emails sent because of this
2262 # message have a common Message-ID
2263 my $org = RT->Config->Get('Organization');
2264 my $msgid = $args{'MIMEObj'}->head->get('Message-ID');
2265 unless (defined $msgid && $msgid =~ /<(rt-.*?-\d+-\d+)\.(\d+-0-0)\@\Q$org\E>/) {
2266 $args{'MIMEObj'}->head->set(
dab09ea8
MKG
2267 'RT-Message-ID' => Encode::encode_utf8(
2268 RT::Interface::Email::GenMessageId( Ticket => $self )
2269 )
84fb5b46
MKG
2270 );
2271 }
2272
2273 #Record the correspondence (write the transaction)
2274 my ( $Trans, $msg, $TransObj ) = $self->_NewTransaction(
2275 Type => $args{'NoteType'},
2276 Data => ( $args{'MIMEObj'}->head->get('subject') || 'No Subject' ),
2277 TimeTaken => $args{'TimeTaken'},
2278 MIMEObj => $args{'MIMEObj'},
2279 CommitScrips => $args{'CommitScrips'},
2280 SquelchMailTo => $args{'SquelchMailTo'},
2281 );
2282
2283 unless ($Trans) {
2284 $RT::Logger->err("$self couldn't init a transaction $msg");
2285 return ( $Trans, $self->loc("Message could not be recorded"), undef );
2286 }
2287
2288 return ( $Trans, $self->loc("Message recorded"), $TransObj );
2289}
2290
2291
2292=head2 DryRun
2293
2294Builds a MIME object from the given C<UpdateSubject> and
2295C<UpdateContent>, then calls L</Comment> or L</Correspond> with
2296C<< DryRun => 1 >>, and returns the transaction so produced.
2297
2298=cut
2299
2300sub DryRun {
2301 my $self = shift;
2302 my %args = @_;
2303 my $action;
2304 if (($args{'UpdateType'} || $args{Action}) =~ /^respon(d|se)$/i ) {
2305 $action = 'Correspond';
2306 } else {
2307 $action = 'Comment';
2308 }
2309
2310 my $Message = MIME::Entity->build(
2311 Type => 'text/plain',
2312 Subject => defined $args{UpdateSubject} ? Encode::encode_utf8( $args{UpdateSubject} ) : "",
2313 Charset => 'UTF-8',
2314 Data => $args{'UpdateContent'} || "",
2315 );
2316
2317 my ( $Transaction, $Description, $Object ) = $self->$action(
2318 CcMessageTo => $args{'UpdateCc'},
2319 BccMessageTo => $args{'UpdateBcc'},
2320 MIMEObj => $Message,
2321 TimeTaken => $args{'UpdateTimeWorked'},
2322 DryRun => 1,
2323 );
2324 unless ( $Transaction ) {
2325 $RT::Logger->error("Couldn't fire '$action' action: $Description");
2326 }
2327
2328 return $Object;
2329}
2330
2331=head2 DryRunCreate
2332
2333Prepares a MIME mesage with the given C<Subject>, C<Cc>, and
2334C<Content>, then calls L</Create> with C<< DryRun => 1 >> and returns
2335the resulting L<RT::Transaction>.
2336
2337=cut
2338
2339sub DryRunCreate {
2340 my $self = shift;
2341 my %args = @_;
2342 my $Message = MIME::Entity->build(
2343 Type => 'text/plain',
2344 Subject => defined $args{Subject} ? Encode::encode_utf8( $args{'Subject'} ) : "",
2345 (defined $args{'Cc'} ?
2346 ( Cc => Encode::encode_utf8( $args{'Cc'} ) ) : ()),
2347 Charset => 'UTF-8',
2348 Data => $args{'Content'} || "",
2349 );
2350
2351 my ( $Transaction, $Object, $Description ) = $self->Create(
2352 Type => $args{'Type'} || 'ticket',
2353 Queue => $args{'Queue'},
2354 Owner => $args{'Owner'},
2355 Requestor => $args{'Requestors'},
2356 Cc => $args{'Cc'},
2357 AdminCc => $args{'AdminCc'},
2358 InitialPriority => $args{'InitialPriority'},
2359 FinalPriority => $args{'FinalPriority'},
2360 TimeLeft => $args{'TimeLeft'},
2361 TimeEstimated => $args{'TimeEstimated'},
2362 TimeWorked => $args{'TimeWorked'},
2363 Subject => $args{'Subject'},
2364 Status => $args{'Status'},
2365 MIMEObj => $Message,
2366 DryRun => 1,
2367 );
2368 unless ( $Transaction ) {
2369 $RT::Logger->error("Couldn't fire Create action: $Description");
2370 }
2371
2372 return $Object;
2373}
2374
2375
2376
2377sub _Links {
2378 my $self = shift;
2379
2380 #TODO: Field isn't the right thing here. but I ahave no idea what mnemonic ---
2381 #tobias meant by $f
2382 my $field = shift;
2383 my $type = shift || "";
2384
2385 my $cache_key = "$field$type";
2386 return $self->{ $cache_key } if $self->{ $cache_key };
2387
2388 my $links = $self->{ $cache_key }
2389 = RT::Links->new( $self->CurrentUser );
2390 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
2391 $links->Limit( FIELD => 'id', VALUE => 0, SUBCLAUSE => 'acl' );
2392 return $links;
2393 }
2394
2395 # Maybe this ticket is a merge ticket
2396 my $limit_on = 'Local'. $field;
2397 # at least to myself
2398 $links->Limit(
2399 FIELD => $limit_on,
2400 VALUE => $self->id,
2401 ENTRYAGGREGATOR => 'OR',
2402 );
2403 $links->Limit(
2404 FIELD => $limit_on,
2405 VALUE => $_,
2406 ENTRYAGGREGATOR => 'OR',
2407 ) foreach $self->Merged;
2408 $links->Limit(
2409 FIELD => 'Type',
2410 VALUE => $type,
2411 ) if $type;
2412
2413 return $links;
2414}
2415
2416
2417
2418=head2 DeleteLink
2419
2420Delete a link. takes a paramhash of Base, Target, Type, Silent,
2421SilentBase and SilentTarget. Either Base or Target must be null.
2422The null value will be replaced with this ticket\'s id.
2423
2424If Silent is true then no transaction would be recorded, in other
2425case you can control creation of transactions on both base and
2426target with SilentBase and SilentTarget respectively. By default
2427both transactions are created.
2428
2429=cut
2430
2431sub DeleteLink {
2432 my $self = shift;
2433 my %args = (
2434 Base => undef,
2435 Target => undef,
2436 Type => undef,
2437 Silent => undef,
2438 SilentBase => undef,
2439 SilentTarget => undef,
2440 @_
2441 );
2442
2443 unless ( $args{'Target'} || $args{'Base'} ) {
2444 $RT::Logger->error("Base or Target must be specified");
2445 return ( 0, $self->loc('Either base or target must be specified') );
2446 }
2447
2448 #check acls
2449 my $right = 0;
2450 $right++ if $self->CurrentUserHasRight('ModifyTicket');
2451 if ( !$right && RT->Config->Get( 'StrictLinkACL' ) ) {
2452 return ( 0, $self->loc("Permission Denied") );
2453 }
2454
2455 # If the other URI is an RT::Ticket, we want to make sure the user
2456 # can modify it too...
2457 my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} );
2458 return (0, $msg) unless $status;
2459 if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) {
2460 $right++;
2461 }
2462 if ( ( !RT->Config->Get( 'StrictLinkACL' ) && $right == 0 ) ||
2463 ( RT->Config->Get( 'StrictLinkACL' ) && $right < 2 ) )
2464 {
2465 return ( 0, $self->loc("Permission Denied") );
2466 }
2467
2468 my ($val, $Msg) = $self->SUPER::_DeleteLink(%args);
2469 return ( 0, $Msg ) unless $val;
2470
2471 return ( $val, $Msg ) if $args{'Silent'};
2472
2473 my ($direction, $remote_link);
2474
2475 if ( $args{'Base'} ) {
2476 $remote_link = $args{'Base'};
2477 $direction = 'Target';
2478 }
2479 elsif ( $args{'Target'} ) {
2480 $remote_link = $args{'Target'};
2481 $direction = 'Base';
2482 }
2483
2484 my $remote_uri = RT::URI->new( $self->CurrentUser );
2485 $remote_uri->FromURI( $remote_link );
2486
2487 unless ( $args{ 'Silent'. $direction } ) {
2488 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
2489 Type => 'DeleteLink',
2490 Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
2491 OldValue => $remote_uri->URI || $remote_link,
2492 TimeTaken => 0
2493 );
2494 $RT::Logger->error("Couldn't create transaction: $Msg") unless $Trans;
2495 }
2496
2497 if ( !$args{ 'Silent'. ( $direction eq 'Target'? 'Base': 'Target' ) } && $remote_uri->IsLocal ) {
2498 my $OtherObj = $remote_uri->Object;
2499 my ( $val, $Msg ) = $OtherObj->_NewTransaction(
2500 Type => 'DeleteLink',
2501 Field => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
2502 : $LINKDIRMAP{$args{'Type'}}->{Target},
2503 OldValue => $self->URI,
2504 ActivateScrips => !RT->Config->Get('LinkTransactionsRun1Scrip'),
2505 TimeTaken => 0,
2506 );
2507 $RT::Logger->error("Couldn't create transaction: $Msg") unless $val;
2508 }
2509
2510 return ( $val, $Msg );
2511}
2512
2513
2514
2515=head2 AddLink
2516
2517Takes a paramhash of Type and one of Base or Target. Adds that link to this ticket.
2518
2519If Silent is true then no transaction would be recorded, in other
2520case you can control creation of transactions on both base and
2521target with SilentBase and SilentTarget respectively. By default
2522both transactions are created.
2523
2524=cut
2525
2526sub AddLink {
2527 my $self = shift;
2528 my %args = ( Target => '',
2529 Base => '',
2530 Type => '',
2531 Silent => undef,
2532 SilentBase => undef,
2533 SilentTarget => undef,
2534 @_ );
2535
2536 unless ( $args{'Target'} || $args{'Base'} ) {
2537 $RT::Logger->error("Base or Target must be specified");
2538 return ( 0, $self->loc('Either base or target must be specified') );
2539 }
2540
2541 my $right = 0;
2542 $right++ if $self->CurrentUserHasRight('ModifyTicket');
2543 if ( !$right && RT->Config->Get( 'StrictLinkACL' ) ) {
2544 return ( 0, $self->loc("Permission Denied") );
2545 }
2546
2547 # If the other URI is an RT::Ticket, we want to make sure the user
2548 # can modify it too...
2549 my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} );
2550 return (0, $msg) unless $status;
2551 if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) {
2552 $right++;
2553 }
2554 if ( ( !RT->Config->Get( 'StrictLinkACL' ) && $right == 0 ) ||
2555 ( RT->Config->Get( 'StrictLinkACL' ) && $right < 2 ) )
2556 {
2557 return ( 0, $self->loc("Permission Denied") );
2558 }
2559
2560 return ( 0, "Can't link to a deleted ticket" )
2561 if $other_ticket && $other_ticket->Status eq 'deleted';
2562
2563 return $self->_AddLink(%args);
2564}
2565
2566sub __GetTicketFromURI {
2567 my $self = shift;
2568 my %args = ( URI => '', @_ );
2569
2570 # If the other URI is an RT::Ticket, we want to make sure the user
2571 # can modify it too...
2572 my $uri_obj = RT::URI->new( $self->CurrentUser );
2573 $uri_obj->FromURI( $args{'URI'} );
2574
2575 unless ( $uri_obj->Resolver && $uri_obj->Scheme ) {
2576 my $msg = $self->loc( "Couldn't resolve '[_1]' into a URI.", $args{'URI'} );
2577 $RT::Logger->warning( $msg );
2578 return( 0, $msg );
2579 }
2580 my $obj = $uri_obj->Resolver->Object;
2581 unless ( UNIVERSAL::isa($obj, 'RT::Ticket') && $obj->id ) {
2582 return (1, 'Found not a ticket', undef);
2583 }
2584 return (1, 'Found ticket', $obj);
2585}
2586
2587=head2 _AddLink
2588
2589Private non-acled variant of AddLink so that links can be added during create.
2590
2591=cut
2592
2593sub _AddLink {
2594 my $self = shift;
2595 my %args = ( Target => '',
2596 Base => '',
2597 Type => '',
2598 Silent => undef,
2599 SilentBase => undef,
2600 SilentTarget => undef,
2601 @_ );
2602
2603 my ($val, $msg, $exist) = $self->SUPER::_AddLink(%args);
2604 return ($val, $msg) if !$val || $exist;
2605 return ($val, $msg) if $args{'Silent'};
2606
2607 my ($direction, $remote_link);
2608 if ( $args{'Target'} ) {
2609 $remote_link = $args{'Target'};
2610 $direction = 'Base';
2611 } elsif ( $args{'Base'} ) {
2612 $remote_link = $args{'Base'};
2613 $direction = 'Target';
2614 }
2615
2616 my $remote_uri = RT::URI->new( $self->CurrentUser );
2617 $remote_uri->FromURI( $remote_link );
2618
2619 unless ( $args{ 'Silent'. $direction } ) {
2620 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
2621 Type => 'AddLink',
2622 Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
2623 NewValue => $remote_uri->URI || $remote_link,
2624 TimeTaken => 0
2625 );
2626 $RT::Logger->error("Couldn't create transaction: $Msg") unless $Trans;
2627 }
2628
2629 if ( !$args{ 'Silent'. ( $direction eq 'Target'? 'Base': 'Target' ) } && $remote_uri->IsLocal ) {
2630 my $OtherObj = $remote_uri->Object;
2631 my ( $val, $msg ) = $OtherObj->_NewTransaction(
2632 Type => 'AddLink',
2633 Field => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
2634 : $LINKDIRMAP{$args{'Type'}}->{Target},
2635 NewValue => $self->URI,
2636 ActivateScrips => !RT->Config->Get('LinkTransactionsRun1Scrip'),
2637 TimeTaken => 0,
2638 );
2639 $RT::Logger->error("Couldn't create transaction: $msg") unless $val;
2640 }
2641
2642 return ( $val, $msg );
2643}
2644
2645
2646
2647
2648=head2 MergeInto
2649
2650MergeInto take the id of the ticket to merge this ticket into.
2651
2652=cut
2653
2654sub MergeInto {
2655 my $self = shift;
2656 my $ticket_id = shift;
2657
2658 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2659 return ( 0, $self->loc("Permission Denied") );
2660 }
2661
2662 # Load up the new ticket.
2663 my $MergeInto = RT::Ticket->new($self->CurrentUser);
2664 $MergeInto->Load($ticket_id);
2665
2666 # make sure it exists.
2667 unless ( $MergeInto->Id ) {
2668 return ( 0, $self->loc("New ticket doesn't exist") );
2669 }
2670
2671 # Make sure the current user can modify the new ticket.
2672 unless ( $MergeInto->CurrentUserHasRight('ModifyTicket') ) {
2673 return ( 0, $self->loc("Permission Denied") );
2674 }
2675
2676 delete $MERGE_CACHE{'effective'}{ $self->id };
2677 delete @{ $MERGE_CACHE{'merged'} }{
2678 $ticket_id, $MergeInto->id, $self->id
2679 };
2680
2681 $RT::Handle->BeginTransaction();
2682
2683 $self->_MergeInto( $MergeInto );
2684
2685 $RT::Handle->Commit();
2686
2687 return ( 1, $self->loc("Merge Successful") );
2688}
2689
2690sub _MergeInto {
2691 my $self = shift;
2692 my $MergeInto = shift;
2693
2694
2695 # We use EffectiveId here even though it duplicates information from
2696 # the links table becasue of the massive performance hit we'd take
2697 # by trying to do a separate database query for merge info everytime
2698 # loaded a ticket.
2699
2700 #update this ticket's effective id to the new ticket's id.
2701 my ( $id_val, $id_msg ) = $self->__Set(
2702 Field => 'EffectiveId',
2703 Value => $MergeInto->Id()
2704 );
2705
2706 unless ($id_val) {
2707 $RT::Handle->Rollback();
2708 return ( 0, $self->loc("Merge failed. Couldn't set EffectiveId") );
2709 }
2710
2711
2712 my $force_status = $self->QueueObj->Lifecycle->DefaultOnMerge;
2713 if ( $force_status && $force_status ne $self->__Value('Status') ) {
2714 my ( $status_val, $status_msg )
2715 = $self->__Set( Field => 'Status', Value => $force_status );
2716
2717 unless ($status_val) {
2718 $RT::Handle->Rollback();
2719 $RT::Logger->error(
2720 "Couldn't set status to $force_status. RT's Database may be inconsistent."
2721 );
2722 return ( 0, $self->loc("Merge failed. Couldn't set Status") );
2723 }
2724 }
2725
2726 # update all the links that point to that old ticket
2727 my $old_links_to = RT::Links->new($self->CurrentUser);
2728 $old_links_to->Limit(FIELD => 'Target', VALUE => $self->URI);
2729
2730 my %old_seen;
2731 while (my $link = $old_links_to->Next) {
2732 if (exists $old_seen{$link->Base."-".$link->Type}) {
2733 $link->Delete;
2734 }
2735 elsif ($link->Base eq $MergeInto->URI) {
2736 $link->Delete;
2737 } else {
2738 # First, make sure the link doesn't already exist. then move it over.
2739 my $tmp = RT::Link->new(RT->SystemUser);
2740 $tmp->LoadByCols(Base => $link->Base, Type => $link->Type, LocalTarget => $MergeInto->id);
2741 if ($tmp->id) {
2742 $link->Delete;
2743 } else {
2744 $link->SetTarget($MergeInto->URI);
2745 $link->SetLocalTarget($MergeInto->id);
2746 }
2747 $old_seen{$link->Base."-".$link->Type} =1;
2748 }
2749
2750 }
2751
2752 my $old_links_from = RT::Links->new($self->CurrentUser);
2753 $old_links_from->Limit(FIELD => 'Base', VALUE => $self->URI);
2754
2755 while (my $link = $old_links_from->Next) {
2756 if (exists $old_seen{$link->Type."-".$link->Target}) {
2757 $link->Delete;
2758 }
2759 if ($link->Target eq $MergeInto->URI) {
2760 $link->Delete;
2761 } else {
2762 # First, make sure the link doesn't already exist. then move it over.
2763 my $tmp = RT::Link->new(RT->SystemUser);
2764 $tmp->LoadByCols(Target => $link->Target, Type => $link->Type, LocalBase => $MergeInto->id);
2765 if ($tmp->id) {
2766 $link->Delete;
2767 } else {
2768 $link->SetBase($MergeInto->URI);
2769 $link->SetLocalBase($MergeInto->id);
2770 $old_seen{$link->Type."-".$link->Target} =1;
2771 }
2772 }
2773
2774 }
2775
2776 # Update time fields
2777 foreach my $type (qw(TimeEstimated TimeWorked TimeLeft)) {
2778
2779 my $mutator = "Set$type";
2780 $MergeInto->$mutator(
2781 ( $MergeInto->$type() || 0 ) + ( $self->$type() || 0 ) );
2782
2783 }
2784#add all of this ticket's watchers to that ticket.
2785 foreach my $watcher_type (qw(Requestors Cc AdminCc)) {
2786
2787 my $people = $self->$watcher_type->MembersObj;
2788 my $addwatcher_type = $watcher_type;
2789 $addwatcher_type =~ s/s$//;
2790
2791 while ( my $watcher = $people->Next ) {
2792
2793 my ($val, $msg) = $MergeInto->_AddWatcher(
2794 Type => $addwatcher_type,
2795 Silent => 1,
2796 PrincipalId => $watcher->MemberId
2797 );
2798 unless ($val) {
2799 $RT::Logger->debug($msg);
2800 }
2801 }
2802
2803 }
2804
2805 #find all of the tickets that were merged into this ticket.
2806 my $old_mergees = RT::Tickets->new( $self->CurrentUser );
2807 $old_mergees->Limit(
2808 FIELD => 'EffectiveId',
2809 OPERATOR => '=',
2810 VALUE => $self->Id
2811 );
2812
2813 # update their EffectiveId fields to the new ticket's id
2814 while ( my $ticket = $old_mergees->Next() ) {
2815 my ( $val, $msg ) = $ticket->__Set(
2816 Field => 'EffectiveId',
2817 Value => $MergeInto->Id()
2818 );
2819 }
2820
2821 #make a new link: this ticket is merged into that other ticket.
2822 $self->AddLink( Type => 'MergedInto', Target => $MergeInto->Id());
2823
2824 $MergeInto->_SetLastUpdated;
2825}
2826
2827=head2 Merged
2828
2829Returns list of tickets' ids that's been merged into this ticket.
2830
2831=cut
2832
2833sub Merged {
2834 my $self = shift;
2835
2836 my $id = $self->id;
2837 return @{ $MERGE_CACHE{'merged'}{ $id } }
2838 if $MERGE_CACHE{'merged'}{ $id };
2839
2840 my $mergees = RT::Tickets->new( $self->CurrentUser );
2841 $mergees->Limit(
2842 FIELD => 'EffectiveId',
2843 VALUE => $id,
2844 );
2845 $mergees->Limit(
2846 FIELD => 'id',
2847 OPERATOR => '!=',
2848 VALUE => $id,
2849 );
2850 return @{ $MERGE_CACHE{'merged'}{ $id } ||= [] }
2851 = map $_->id, @{ $mergees->ItemsArrayRef || [] };
2852}
2853
2854
2855
2856
2857
2858=head2 OwnerObj
2859
2860Takes nothing and returns an RT::User object of
2861this ticket's owner
2862
2863=cut
2864
2865sub OwnerObj {
2866 my $self = shift;
2867
2868 #If this gets ACLed, we lose on a rights check in User.pm and
2869 #get deep recursion. if we need ACLs here, we need
2870 #an equiv without ACLs
2871
2872 my $owner = RT::User->new( $self->CurrentUser );
2873 $owner->Load( $self->__Value('Owner') );
2874
2875 #Return the owner object
2876 return ($owner);
2877}
2878
2879
2880
2881=head2 OwnerAsString
2882
2883Returns the owner's email address
2884
2885=cut
2886
2887sub OwnerAsString {
2888 my $self = shift;
2889 return ( $self->OwnerObj->EmailAddress );
2890
2891}
2892
2893
2894
2895=head2 SetOwner
2896
2897Takes two arguments:
2898 the Id or Name of the owner
2899and (optionally) the type of the SetOwner Transaction. It defaults
2900to 'Set'. 'Steal' is also a valid option.
2901
2902
2903=cut
2904
2905sub SetOwner {
2906 my $self = shift;
2907 my $NewOwner = shift;
2908 my $Type = shift || "Set";
2909
2910 $RT::Handle->BeginTransaction();
2911
2912 $self->_SetLastUpdated(); # lock the ticket
2913 $self->Load( $self->id ); # in case $self changed while waiting for lock
2914
2915 my $OldOwnerObj = $self->OwnerObj;
2916
2917 my $NewOwnerObj = RT::User->new( $self->CurrentUser );
2918 $NewOwnerObj->Load( $NewOwner );
2919 unless ( $NewOwnerObj->Id ) {
2920 $RT::Handle->Rollback();
2921 return ( 0, $self->loc("That user does not exist") );
2922 }
2923
2924
2925 # must have ModifyTicket rights
2926 # or TakeTicket/StealTicket and $NewOwner is self
2927 # see if it's a take
2928 if ( $OldOwnerObj->Id == RT->Nobody->Id ) {
2929 unless ( $self->CurrentUserHasRight('ModifyTicket')
2930 || $self->CurrentUserHasRight('TakeTicket') ) {
2931 $RT::Handle->Rollback();
2932 return ( 0, $self->loc("Permission Denied") );
2933 }
2934 }
2935
2936 # see if it's a steal
2937 elsif ( $OldOwnerObj->Id != RT->Nobody->Id
2938 && $OldOwnerObj->Id != $self->CurrentUser->id ) {
2939
2940 unless ( $self->CurrentUserHasRight('ModifyTicket')
2941 || $self->CurrentUserHasRight('StealTicket') ) {
2942 $RT::Handle->Rollback();
2943 return ( 0, $self->loc("Permission Denied") );
2944 }
2945 }
2946 else {
2947 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2948 $RT::Handle->Rollback();
2949 return ( 0, $self->loc("Permission Denied") );
2950 }
2951 }
2952
2953 # If we're not stealing and the ticket has an owner and it's not
2954 # the current user
2955 if ( $Type ne 'Steal' and $Type ne 'Force'
2956 and $OldOwnerObj->Id != RT->Nobody->Id
2957 and $OldOwnerObj->Id != $self->CurrentUser->Id )
2958 {
2959 $RT::Handle->Rollback();
2960 return ( 0, $self->loc("You can only take tickets that are unowned") )
2961 if $NewOwnerObj->id == $self->CurrentUser->id;
2962 return (
2963 0,
2964 $self->loc("You can only reassign tickets that you own or that are unowned" )
2965 );
2966 }
2967
2968 #If we've specified a new owner and that user can't modify the ticket
2969 elsif ( !$NewOwnerObj->HasRight( Right => 'OwnTicket', Object => $self ) ) {
2970 $RT::Handle->Rollback();
2971 return ( 0, $self->loc("That user may not own tickets in that queue") );
2972 }
2973
2974 # If the ticket has an owner and it's the new owner, we don't need
2975 # To do anything
2976 elsif ( $NewOwnerObj->Id == $OldOwnerObj->Id ) {
2977 $RT::Handle->Rollback();
2978 return ( 0, $self->loc("That user already owns that ticket") );
2979 }
2980
2981 # Delete the owner in the owner group, then add a new one
2982 # TODO: is this safe? it's not how we really want the API to work
2983 # for most things, but it's fast.
2984 my ( $del_id, $del_msg );
2985 for my $owner (@{$self->OwnerGroup->MembersObj->ItemsArrayRef}) {
2986 ($del_id, $del_msg) = $owner->Delete();
2987 last unless ($del_id);
2988 }
2989
2990 unless ($del_id) {
2991 $RT::Handle->Rollback();
2992 return ( 0, $self->loc("Could not change owner: [_1]", $del_msg) );
2993 }
2994
2995 my ( $add_id, $add_msg ) = $self->OwnerGroup->_AddMember(
2996 PrincipalId => $NewOwnerObj->PrincipalId,
2997 InsideTransaction => 1 );
2998 unless ($add_id) {
2999 $RT::Handle->Rollback();
3000 return ( 0, $self->loc("Could not change owner: [_1]", $add_msg ) );
3001 }
3002
3003 # We call set twice with slightly different arguments, so
3004 # as to not have an SQL transaction span two RT transactions
3005
3006 my ( $val, $msg ) = $self->_Set(
3007 Field => 'Owner',
3008 RecordTransaction => 0,
3009 Value => $NewOwnerObj->Id,
3010 TimeTaken => 0,
3011 TransactionType => 'Set',
3012 CheckACL => 0, # don't check acl
3013 );
3014
3015 unless ($val) {
3016 $RT::Handle->Rollback;
3017 return ( 0, $self->loc("Could not change owner: [_1]", $msg) );
3018 }
3019
3020 ($val, $msg) = $self->_NewTransaction(
3021 Type => 'Set',
3022 Field => 'Owner',
3023 NewValue => $NewOwnerObj->Id,
3024 OldValue => $OldOwnerObj->Id,
3025 TimeTaken => 0,
3026 );
3027
3028 if ( $val ) {
3029 $msg = $self->loc( "Owner changed from [_1] to [_2]",
3030 $OldOwnerObj->Name, $NewOwnerObj->Name );
3031 }
3032 else {
3033 $RT::Handle->Rollback();
3034 return ( 0, $msg );
3035 }
3036
3037 $RT::Handle->Commit();
3038
3039 return ( $val, $msg );
3040}
3041
3042
3043
3044=head2 Take
3045
3046A convenince method to set the ticket's owner to the current user
3047
3048=cut
3049
3050sub Take {
3051 my $self = shift;
3052 return ( $self->SetOwner( $self->CurrentUser->Id, 'Take' ) );
3053}
3054
3055
3056
3057=head2 Untake
3058
3059Convenience method to set the owner to 'nobody' if the current user is the owner.
3060
3061=cut
3062
3063sub Untake {
3064 my $self = shift;
3065 return ( $self->SetOwner( RT->Nobody->UserObj->Id, 'Untake' ) );
3066}
3067
3068
3069
3070=head2 Steal
3071
3072A convenience method to change the owner of the current ticket to the
3073current user. Even if it's owned by another user.
3074
3075=cut
3076
3077sub Steal {
3078 my $self = shift;
3079
3080 if ( $self->IsOwner( $self->CurrentUser ) ) {
3081 return ( 0, $self->loc("You already own this ticket") );
3082 }
3083 else {
3084 return ( $self->SetOwner( $self->CurrentUser->Id, 'Steal' ) );
3085
3086 }
3087
3088}
3089
3090
3091
3092
3093
3094=head2 ValidateStatus STATUS
3095
3096Takes a string. Returns true if that status is a valid status for this ticket.
3097Returns false otherwise.
3098
3099=cut
3100
3101sub ValidateStatus {
3102 my $self = shift;
3103 my $status = shift;
3104
3105 #Make sure the status passed in is valid
3106 return 1 if $self->QueueObj->IsValidStatus($status);
3107
3108 my $i = 0;
3109 while ( my $caller = (caller($i++))[3] ) {
3110 return 1 if $caller eq 'RT::Ticket::SetQueue';
3111 }
3112
3113 return 0;
3114}
3115
3116
3117
3118=head2 SetStatus STATUS
3119
3120Set this ticket\'s status. STATUS can be one of: new, open, stalled, resolved, rejected or deleted.
3121
3122Alternatively, you can pass in a list of named parameters (Status => STATUS, Force => FORCE, SetStarted => SETSTARTED ).
3123If FORCE is true, ignore unresolved dependencies and force a status change.
3124if SETSTARTED is true( it's the default value), set Started to current datetime if Started
3125is not set and the status is changed from initial to not initial.
3126
3127=cut
3128
3129sub SetStatus {
3130 my $self = shift;
3131 my %args;
3132 if (@_ == 1) {
3133 $args{Status} = shift;
3134 }
3135 else {
3136 %args = (@_);
3137 }
3138
3139 # this only allows us to SetStarted, not we must SetStarted.
3140 # this option was added for rtir initially
3141 $args{SetStarted} = 1 unless exists $args{SetStarted};
3142
3143
3144 my $lifecycle = $self->QueueObj->Lifecycle;
3145
3146 my $new = $args{'Status'};
3147 unless ( $lifecycle->IsValid( $new ) ) {
3148 return (0, $self->loc("Status '[_1]' isn't a valid status for tickets in this queue.", $self->loc($new)));
3149 }
3150
3151 my $old = $self->__Value('Status');
3152 unless ( $lifecycle->IsTransition( $old => $new ) ) {
3153 return (0, $self->loc("You can't change status from '[_1]' to '[_2]'.", $self->loc($old), $self->loc($new)));
3154 }
3155
3156 my $check_right = $lifecycle->CheckRight( $old => $new );
3157 unless ( $self->CurrentUserHasRight( $check_right ) ) {
3158 return ( 0, $self->loc('Permission Denied') );
3159 }
3160
3161 if ( !$args{Force} && $lifecycle->IsInactive( $new ) && $self->HasUnresolvedDependencies) {
3162 return (0, $self->loc('That ticket has unresolved dependencies'));
3163 }
3164
3165 my $now = RT::Date->new( $self->CurrentUser );
3166 $now->SetToNow();
3167
3168 my $raw_started = RT::Date->new(RT->SystemUser);
3169 $raw_started->Set(Format => 'ISO', Value => $self->__Value('Started'));
3170
3171 #If we're changing the status from new, record that we've started
3172 if ( $args{SetStarted} && $lifecycle->IsInitial($old) && !$lifecycle->IsInitial($new) && !$raw_started->Unix) {
3173 #Set the Started time to "now"
3174 $self->_Set(
3175 Field => 'Started',
3176 Value => $now->ISO,
3177 RecordTransaction => 0
3178 );
3179 }
3180
3181 #When we close a ticket, set the 'Resolved' attribute to now.
3182 # It's misnamed, but that's just historical.
3183 if ( $lifecycle->IsInactive($new) ) {
3184 $self->_Set(
3185 Field => 'Resolved',
3186 Value => $now->ISO,
3187 RecordTransaction => 0,
3188 );
3189 }
3190
3191 #Actually update the status
3192 my ($val, $msg)= $self->_Set(
3193 Field => 'Status',
3194 Value => $args{Status},
3195 TimeTaken => 0,
3196 CheckACL => 0,
3197 TransactionType => 'Status',
3198 );
3199 return ($val, $msg);
3200}
3201
3202
3203
3204=head2 Delete
3205
3206Takes no arguments. Marks this ticket for garbage collection
3207
3208=cut
3209
3210sub Delete {
3211 my $self = shift;
3212 unless ( $self->QueueObj->Lifecycle->IsValid('deleted') ) {
3213 return (0, $self->loc('Delete operation is disabled by lifecycle configuration') ); #loc
3214 }
3215 return ( $self->SetStatus('deleted') );
3216}
3217
3218
3219=head2 SetTold ISO [TIMETAKEN]
3220
3221Updates the told and records a transaction
3222
3223=cut
3224
3225sub SetTold {
3226 my $self = shift;
3227 my $told;
3228 $told = shift if (@_);
3229 my $timetaken = shift || 0;
3230
3231 unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
3232 return ( 0, $self->loc("Permission Denied") );
3233 }
3234
3235 my $datetold = RT::Date->new( $self->CurrentUser );
3236 if ($told) {
3237 $datetold->Set( Format => 'iso',
3238 Value => $told );
3239 }
3240 else {
3241 $datetold->SetToNow();
3242 }
3243
3244 return ( $self->_Set( Field => 'Told',
3245 Value => $datetold->ISO,
3246 TimeTaken => $timetaken,
3247 TransactionType => 'Told' ) );
3248}
3249
3250=head2 _SetTold
3251
3252Updates the told without a transaction or acl check. Useful when we're sending replies.
3253
3254=cut
3255
3256sub _SetTold {
3257 my $self = shift;
3258
3259 my $now = RT::Date->new( $self->CurrentUser );
3260 $now->SetToNow();
3261
3262 #use __Set to get no ACLs ;)
3263 return ( $self->__Set( Field => 'Told',
3264 Value => $now->ISO ) );
3265}
3266
3267=head2 SeenUpTo
3268
3269
3270=cut
3271
3272sub SeenUpTo {
3273 my $self = shift;
3274 my $uid = $self->CurrentUser->id;
3275 my $attr = $self->FirstAttribute( "User-". $uid ."-SeenUpTo" );
3276 return if $attr && $attr->Content gt $self->LastUpdated;
3277
3278 my $txns = $self->Transactions;
3279 $txns->Limit( FIELD => 'Type', VALUE => 'Comment' );
3280 $txns->Limit( FIELD => 'Type', VALUE => 'Correspond' );
3281 $txns->Limit( FIELD => 'Creator', OPERATOR => '!=', VALUE => $uid );
3282 $txns->Limit(
3283 FIELD => 'Created',
3284 OPERATOR => '>',
3285 VALUE => $attr->Content
3286 ) if $attr;
3287 $txns->RowsPerPage(1);
3288 return $txns->First;
3289}
3290
dab09ea8
MKG
3291=head2 RanTransactionBatch
3292
3293Acts as a guard around running TransactionBatch scrips.
3294
3295Should be false until you enter the code that runs TransactionBatch scrips
3296
3297Accepts an optional argument to indicate that TransactionBatch Scrips should no longer be run on this object.
3298
3299=cut
3300
3301sub RanTransactionBatch {
3302 my $self = shift;
3303 my $val = shift;
3304
3305 if ( defined $val ) {
3306 return $self->{_RanTransactionBatch} = $val;
3307 } else {
3308 return $self->{_RanTransactionBatch};
3309 }
3310
3311}
3312
84fb5b46
MKG
3313
3314=head2 TransactionBatch
3315
3316Returns an array reference of all transactions created on this ticket during
3317this ticket object's lifetime or since last application of a batch, or undef
3318if there were none.
3319
3320Only works when the C<UseTransactionBatch> config option is set to true.
3321
3322=cut
3323
3324sub TransactionBatch {
3325 my $self = shift;
3326 return $self->{_TransactionBatch};
3327}
3328
3329=head2 ApplyTransactionBatch
3330
3331Applies scrips on the current batch of transactions and shinks it. Usually
3332batch is applied when object is destroyed, but in some cases it's too late.
3333
3334=cut
3335
3336sub ApplyTransactionBatch {
3337 my $self = shift;
3338
3339 my $batch = $self->TransactionBatch;
3340 return unless $batch && @$batch;
3341
3342 $self->_ApplyTransactionBatch;
3343
3344 $self->{_TransactionBatch} = [];
3345}
3346
3347sub _ApplyTransactionBatch {
3348 my $self = shift;
dab09ea8
MKG
3349
3350 return if $self->RanTransactionBatch;
3351 $self->RanTransactionBatch(1);
3352
3353 my $still_exists = RT::Ticket->new( RT->SystemUser );
3354 $still_exists->Load( $self->Id );
3355 if (not $still_exists->Id) {
3356 # The ticket has been removed from the database, but we still
3357 # have pending TransactionBatch txns for it. Unfortunately,
3358 # because it isn't in the DB anymore, attempting to run scrips
3359 # on it may produce unpredictable results; simply drop the
3360 # batched transactions.
3361 $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.");
3362 return;
3363 }
3364
84fb5b46
MKG
3365 my $batch = $self->TransactionBatch;
3366
3367 my %seen;
3368 my $types = join ',', grep !$seen{$_}++, grep defined, map $_->__Value('Type'), grep defined, @{$batch};
3369
3370 require RT::Scrips;
3371 RT::Scrips->new(RT->SystemUser)->Apply(
3372 Stage => 'TransactionBatch',
3373 TicketObj => $self,
3374 TransactionObj => $batch->[0],
3375 Type => $types,
3376 );
3377
3378 # Entry point of the rule system
3379 my $rules = RT::Ruleset->FindAllRules(
3380 Stage => 'TransactionBatch',
3381 TicketObj => $self,
3382 TransactionObj => $batch->[0],
3383 Type => $types,
3384 );
3385 RT::Ruleset->CommitRules($rules);
3386}
3387
3388sub DESTROY {
3389 my $self = shift;
3390
3391 # DESTROY methods need to localize $@, or it may unset it. This
3392 # causes $m->abort to not bubble all of the way up. See perlbug
3393 # http://rt.perl.org/rt3/Ticket/Display.html?id=17650
3394 local $@;
3395
3396 # The following line eliminates reentrancy.
3397 # It protects against the fact that perl doesn't deal gracefully
3398 # when an object's refcount is changed in its destructor.
3399 return if $self->{_Destroyed}++;
3400
3401 if (in_global_destruction()) {
3402 unless ($ENV{'HARNESS_ACTIVE'}) {
3403 warn "Too late to safely run transaction-batch scrips!"
3404 ." This is typically caused by using ticket objects"
3405 ." at the top-level of a script which uses the RT API."
3406 ." Be sure to explicitly undef such ticket objects,"
3407 ." or put them inside of a lexical scope.";
3408 }
3409 return;
3410 }
3411
dab09ea8 3412 return $self->ApplyTransactionBatch;
84fb5b46
MKG
3413}
3414
3415
3416
3417
3418sub _OverlayAccessible {
3419 {
3420 EffectiveId => { 'read' => 1, 'write' => 1, 'public' => 1 },
3421 Queue => { 'read' => 1, 'write' => 1 },
3422 Requestors => { 'read' => 1, 'write' => 1 },
3423 Owner => { 'read' => 1, 'write' => 1 },
3424 Subject => { 'read' => 1, 'write' => 1 },
3425 InitialPriority => { 'read' => 1, 'write' => 1 },
3426 FinalPriority => { 'read' => 1, 'write' => 1 },
3427 Priority => { 'read' => 1, 'write' => 1 },
3428 Status => { 'read' => 1, 'write' => 1 },
3429 TimeEstimated => { 'read' => 1, 'write' => 1 },
3430 TimeWorked => { 'read' => 1, 'write' => 1 },
3431 TimeLeft => { 'read' => 1, 'write' => 1 },
3432 Told => { 'read' => 1, 'write' => 1 },
3433 Resolved => { 'read' => 1 },
3434 Type => { 'read' => 1 },
3435 Starts => { 'read' => 1, 'write' => 1 },
3436 Started => { 'read' => 1, 'write' => 1 },
3437 Due => { 'read' => 1, 'write' => 1 },
3438 Creator => { 'read' => 1, 'auto' => 1 },
3439 Created => { 'read' => 1, 'auto' => 1 },
3440 LastUpdatedBy => { 'read' => 1, 'auto' => 1 },
3441 LastUpdated => { 'read' => 1, 'auto' => 1 }
3442 };
3443
3444}
3445
3446
3447
3448sub _Set {
3449 my $self = shift;
3450
3451 my %args = ( Field => undef,
3452 Value => undef,
3453 TimeTaken => 0,
3454 RecordTransaction => 1,
3455 UpdateTicket => 1,
3456 CheckACL => 1,
3457 TransactionType => 'Set',
3458 @_ );
3459
3460 if ($args{'CheckACL'}) {
3461 unless ( $self->CurrentUserHasRight('ModifyTicket')) {
3462 return ( 0, $self->loc("Permission Denied"));
3463 }
3464 }
3465
3466 unless ($args{'UpdateTicket'} || $args{'RecordTransaction'}) {
3467 $RT::Logger->error("Ticket->_Set called without a mandate to record an update or update the ticket");
3468 return(0, $self->loc("Internal Error"));
3469 }
3470
3471 #if the user is trying to modify the record
3472
3473 #Take care of the old value we really don't want to get in an ACL loop.
3474 # so ask the super::_Value
3475 my $Old = $self->SUPER::_Value("$args{'Field'}");
3476
3477 my ($ret, $msg);
3478 if ( $args{'UpdateTicket'} ) {
3479
3480 #Set the new value
3481 ( $ret, $msg ) = $self->SUPER::_Set( Field => $args{'Field'},
3482 Value => $args{'Value'} );
3483
3484 #If we can't actually set the field to the value, don't record
3485 # a transaction. instead, get out of here.
3486 return ( 0, $msg ) unless $ret;
3487 }
3488
3489 if ( $args{'RecordTransaction'} == 1 ) {
3490
3491 my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
3492 Type => $args{'TransactionType'},
3493 Field => $args{'Field'},
3494 NewValue => $args{'Value'},
3495 OldValue => $Old,
3496 TimeTaken => $args{'TimeTaken'},
3497 );
3498 return ( $Trans, scalar $TransObj->BriefDescription );
3499 }
3500 else {
3501 return ( $ret, $msg );
3502 }
3503}
3504
3505
3506
3507=head2 _Value
3508
3509Takes the name of a table column.
3510Returns its value as a string, if the user passes an ACL check
3511
3512=cut
3513
3514sub _Value {
3515
3516 my $self = shift;
3517 my $field = shift;
3518
3519 #if the field is public, return it.
3520 if ( $self->_Accessible( $field, 'public' ) ) {
3521
3522 #$RT::Logger->debug("Skipping ACL check for $field");
3523 return ( $self->SUPER::_Value($field) );
3524
3525 }
3526
3527 #If the current user doesn't have ACLs, don't let em at it.
3528
3529 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
3530 return (undef);
3531 }
3532 return ( $self->SUPER::_Value($field) );
3533
3534}
3535
3536
3537
3538=head2 _UpdateTimeTaken
3539
3540This routine will increment the timeworked counter. it should
3541only be called from _NewTransaction
3542
3543=cut
3544
3545sub _UpdateTimeTaken {
3546 my $self = shift;
3547 my $Minutes = shift;
3548 my ($Total);
3549
3550 $Total = $self->SUPER::_Value("TimeWorked");
3551 $Total = ( $Total || 0 ) + ( $Minutes || 0 );
3552 $self->SUPER::_Set(
3553 Field => "TimeWorked",
3554 Value => $Total
3555 );
3556
3557 return ($Total);
3558}
3559
3560
3561
3562
3563
3564=head2 CurrentUserHasRight
3565
3566 Takes the textual name of a Ticket scoped right (from RT::ACE) and returns
35671 if the user has that right. It returns 0 if the user doesn't have that right.
3568
3569=cut
3570
3571sub CurrentUserHasRight {
3572 my $self = shift;
3573 my $right = shift;
3574
3575 return $self->CurrentUser->PrincipalObj->HasRight(
3576 Object => $self,
3577 Right => $right,
3578 )
3579}
3580
3581
3582=head2 CurrentUserCanSee
3583
3584Returns true if the current user can see the ticket, using ShowTicket
3585
3586=cut
3587
3588sub CurrentUserCanSee {
3589 my $self = shift;
3590 return $self->CurrentUserHasRight('ShowTicket');
3591}
3592
3593=head2 HasRight
3594
3595 Takes a paramhash with the attributes 'Right' and 'Principal'
3596 'Right' is a ticket-scoped textual right from RT::ACE
3597 'Principal' is an RT::User object
3598
3599 Returns 1 if the principal has the right. Returns undef if not.
3600
3601=cut
3602
3603sub HasRight {
3604 my $self = shift;
3605 my %args = (
3606 Right => undef,
3607 Principal => undef,
3608 @_
3609 );
3610
3611 unless ( ( defined $args{'Principal'} ) and ( ref( $args{'Principal'} ) ) )
3612 {
3613 Carp::cluck("Principal attrib undefined for Ticket::HasRight");
3614 $RT::Logger->crit("Principal attrib undefined for Ticket::HasRight");
3615 return(undef);
3616 }
3617
3618 return (
3619 $args{'Principal'}->HasRight(
3620 Object => $self,
3621 Right => $args{'Right'}
3622 )
3623 );
3624}
3625
3626
3627
3628=head2 Reminders
3629
3630Return the Reminders object for this ticket. (It's an RT::Reminders object.)
3631It isn't acutally a searchbuilder collection itself.
3632
3633=cut
3634
3635sub Reminders {
3636 my $self = shift;
3637
3638 unless ($self->{'__reminders'}) {
3639 $self->{'__reminders'} = RT::Reminders->new($self->CurrentUser);
3640 $self->{'__reminders'}->Ticket($self->id);
3641 }
3642 return $self->{'__reminders'};
3643
3644}
3645
3646
3647
3648
3649=head2 Transactions
3650
3651 Returns an RT::Transactions object of all transactions on this ticket
3652
3653=cut
3654
3655sub Transactions {
3656 my $self = shift;
3657
3658 my $transactions = RT::Transactions->new( $self->CurrentUser );
3659
3660 #If the user has no rights, return an empty object
3661 if ( $self->CurrentUserHasRight('ShowTicket') ) {
3662 $transactions->LimitToTicket($self->id);
3663
3664 # if the user may not see comments do not return them
3665 unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
3666 $transactions->Limit(
3667 SUBCLAUSE => 'acl',
3668 FIELD => 'Type',
3669 OPERATOR => '!=',
3670 VALUE => "Comment"
3671 );
3672 $transactions->Limit(
3673 SUBCLAUSE => 'acl',
3674 FIELD => 'Type',
3675 OPERATOR => '!=',
3676 VALUE => "CommentEmailRecord",
3677 ENTRYAGGREGATOR => 'AND'
3678 );
3679
3680 }
3681 } else {
3682 $transactions->Limit(
3683 SUBCLAUSE => 'acl',
3684 FIELD => 'id',
3685 VALUE => 0,
3686 ENTRYAGGREGATOR => 'AND'
3687 );
3688 }
3689
3690 return ($transactions);
3691}
3692
3693
3694
3695
3696=head2 TransactionCustomFields
3697
3698 Returns the custom fields that transactions on tickets will have.
3699
3700=cut
3701
3702sub TransactionCustomFields {
3703 my $self = shift;
3704 my $cfs = $self->QueueObj->TicketTransactionCustomFields;
3705 $cfs->SetContextObject( $self );
3706 return $cfs;
3707}
3708
3709
3710
3711=head2 CustomFieldValues
3712
3713# Do name => id mapping (if needed) before falling back to
3714# RT::Record's CustomFieldValues
3715
3716See L<RT::Record>
3717
3718=cut
3719
3720sub CustomFieldValues {
3721 my $self = shift;
3722 my $field = shift;
3723
3724 return $self->SUPER::CustomFieldValues( $field ) if !$field || $field =~ /^\d+$/;
3725
3726 my $cf = RT::CustomField->new( $self->CurrentUser );
3727 $cf->SetContextObject( $self );
3728 $cf->LoadByNameAndQueue( Name => $field, Queue => $self->Queue );
3729 unless ( $cf->id ) {
3730 $cf->LoadByNameAndQueue( Name => $field, Queue => 0 );
3731 }
3732
3733 # If we didn't find a valid cfid, give up.
3734 return RT::ObjectCustomFieldValues->new( $self->CurrentUser ) unless $cf->id;
3735
3736 return $self->SUPER::CustomFieldValues( $cf->id );
3737}
3738
3739
3740
3741=head2 CustomFieldLookupType
3742
3743Returns the RT::Ticket lookup type, which can be passed to
3744RT::CustomField->Create() via the 'LookupType' hash key.
3745
3746=cut
3747
3748
3749sub CustomFieldLookupType {
3750 "RT::Queue-RT::Ticket";
3751}
3752
3753=head2 ACLEquivalenceObjects
3754
3755This method returns a list of objects for which a user's rights also apply
3756to this ticket. Generally, this is only the ticket's queue, but some RT
3757extensions may make other objects available too.
3758
3759This method is called from L<RT::Principal/HasRight>.
3760
3761=cut
3762
3763sub ACLEquivalenceObjects {
3764 my $self = shift;
3765 return $self->QueueObj;
3766
3767}
3768
3769
37701;
3771
3772=head1 AUTHOR
3773
3774Jesse Vincent, jesse@bestpractical.com
3775
3776=head1 SEE ALSO
3777
3778RT
3779
3780=cut
3781
3782
3783use RT::Queue;
3784use base 'RT::Record';
3785
3786sub Table {'Tickets'}
3787
3788
3789
3790
3791
3792
3793=head2 id
3794
3795Returns the current value of id.
3796(In the database, id is stored as int(11).)
3797
3798
3799=cut
3800
3801
3802=head2 EffectiveId
3803
3804Returns the current value of EffectiveId.
3805(In the database, EffectiveId is stored as int(11).)
3806
3807
3808
3809=head2 SetEffectiveId VALUE
3810
3811
3812Set EffectiveId to VALUE.
3813Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3814(In the database, EffectiveId will be stored as a int(11).)
3815
3816
3817=cut
3818
3819
3820=head2 Queue
3821
3822Returns the current value of Queue.
3823(In the database, Queue is stored as int(11).)
3824
3825
3826
3827=head2 SetQueue VALUE
3828
3829
3830Set Queue to VALUE.
3831Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3832(In the database, Queue will be stored as a int(11).)
3833
3834
3835=cut
3836
3837
3838=head2 Type
3839
3840Returns the current value of Type.
3841(In the database, Type is stored as varchar(16).)
3842
3843
3844
3845=head2 SetType VALUE
3846
3847
3848Set Type to VALUE.
3849Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3850(In the database, Type will be stored as a varchar(16).)
3851
3852
3853=cut
3854
3855
3856=head2 IssueStatement
3857
3858Returns the current value of IssueStatement.
3859(In the database, IssueStatement is stored as int(11).)
3860
3861
3862
3863=head2 SetIssueStatement VALUE
3864
3865
3866Set IssueStatement to VALUE.
3867Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3868(In the database, IssueStatement will be stored as a int(11).)
3869
3870
3871=cut
3872
3873
3874=head2 Resolution
3875
3876Returns the current value of Resolution.
3877(In the database, Resolution is stored as int(11).)
3878
3879
3880
3881=head2 SetResolution VALUE
3882
3883
3884Set Resolution to VALUE.
3885Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3886(In the database, Resolution will be stored as a int(11).)
3887
3888
3889=cut
3890
3891
3892=head2 Owner
3893
3894Returns the current value of Owner.
3895(In the database, Owner is stored as int(11).)
3896
3897
3898
3899=head2 SetOwner VALUE
3900
3901
3902Set Owner to VALUE.
3903Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3904(In the database, Owner will be stored as a int(11).)
3905
3906
3907=cut
3908
3909
3910=head2 Subject
3911
3912Returns the current value of Subject.
3913(In the database, Subject is stored as varchar(200).)
3914
3915
3916
3917=head2 SetSubject VALUE
3918
3919
3920Set Subject to VALUE.
3921Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3922(In the database, Subject will be stored as a varchar(200).)
3923
3924
3925=cut
3926
3927
3928=head2 InitialPriority
3929
3930Returns the current value of InitialPriority.
3931(In the database, InitialPriority is stored as int(11).)
3932
3933
3934
3935=head2 SetInitialPriority VALUE
3936
3937
3938Set InitialPriority to VALUE.
3939Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3940(In the database, InitialPriority will be stored as a int(11).)
3941
3942
3943=cut
3944
3945
3946=head2 FinalPriority
3947
3948Returns the current value of FinalPriority.
3949(In the database, FinalPriority is stored as int(11).)
3950
3951
3952
3953=head2 SetFinalPriority VALUE
3954
3955
3956Set FinalPriority to VALUE.
3957Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3958(In the database, FinalPriority will be stored as a int(11).)
3959
3960
3961=cut
3962
3963
3964=head2 Priority
3965
3966Returns the current value of Priority.
3967(In the database, Priority is stored as int(11).)
3968
3969
3970
3971=head2 SetPriority VALUE
3972
3973
3974Set Priority to VALUE.
3975Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3976(In the database, Priority will be stored as a int(11).)
3977
3978
3979=cut
3980
3981
3982=head2 TimeEstimated
3983
3984Returns the current value of TimeEstimated.
3985(In the database, TimeEstimated is stored as int(11).)
3986
3987
3988
3989=head2 SetTimeEstimated VALUE
3990
3991
3992Set TimeEstimated to VALUE.
3993Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3994(In the database, TimeEstimated will be stored as a int(11).)
3995
3996
3997=cut
3998