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