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