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