Ticket.pm 96.5 KB
Newer Older
1
# BEGIN BPS TAGGED BLOCK {{{
Jesse Vincent's avatar
Jesse Vincent committed
2
#
3
# COPYRIGHT:
Jesse Vincent's avatar
Jesse Vincent committed
4
#
5
# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
Kevin Falcone's avatar
Kevin Falcone committed
6
#                                          <sales@bestpractical.com>
Jesse Vincent's avatar
Jesse Vincent committed
7
#
8
# (Except where explicitly superseded by other copyright notices)
Jesse Vincent's avatar
Jesse Vincent committed
9
10
#
#
11
# LICENSE:
Jesse Vincent's avatar
Jesse Vincent committed
12
#
13
14
15
# This work is made available to you under the terms of Version 2 of
# the GNU General Public License. A copy of that license should have
# been provided with this software, but in any event can be snarfed
16
# from www.gnu.org.
Jesse Vincent's avatar
Jesse Vincent committed
17
#
18
19
20
21
# This work is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
Jesse Vincent's avatar
Jesse Vincent committed
22
#
23
24
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
Jesse Vincent's avatar
Jesse Vincent committed
25
26
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 or visit their web page on the internet at
Ruslan Zakirov's avatar
Ruslan Zakirov committed
27
# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
Jesse Vincent's avatar
Jesse Vincent committed
28
29
#
#
30
# CONTRIBUTION SUBMISSION POLICY:
Jesse Vincent's avatar
Jesse Vincent committed
31
#
32
33
34
35
36
# (The following paragraph is not intended to limit the rights granted
# to you to modify and distribute this software under the terms of
# the GNU General Public License and is only of importance to you if
# you choose to contribute your changes and enhancements to the
# community by submitting them to Best Practical Solutions, LLC.)
Jesse Vincent's avatar
Jesse Vincent committed
37
#
38
39
40
41
42
43
44
45
# By intentionally submitting any modifications, corrections or
# derivatives to this work, or any other work intended for use with
# Request Tracker, to Best Practical Solutions, LLC, you confirm that
# you are the copyright holder for those contributions and you grant
# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
# royalty-free, perpetual, license to use, copy, create derivative
# works based on those contributions, and sublicense and distribute
# those contributions and any derivatives thereof.
Jesse Vincent's avatar
Jesse Vincent committed
46
#
47
# END BPS TAGGED BLOCK }}}
48

Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
49
50
51
=head1 SYNOPSIS

  use RT::Ticket;
52
  my $ticket = RT::Ticket->new($CurrentUser);
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
53
54
55
56
  $ticket->Load($ticket_id);

=head1 DESCRIPTION

57
This module lets you manipulate RT's ticket object.
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
58
59
60
61


=head1 METHODS

Jesse Vincent's avatar
Jesse Vincent committed
62

Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
63
64
=cut

65
66
67

package RT::Ticket;

68
use strict;
69
use warnings;
70
use base 'RT::Record';
71

72
use Role::Basic 'with';
73
74
75
76

# SetStatus and _SetStatus are reimplemented below (using other pieces of the
# role) to deal with ACLs, moving tickets between queues, and automatically
# setting dates.
77
78
79
with "RT::Record::Role::Status" => { -excludes => [qw(SetStatus _SetStatus)] },
     "RT::Record::Role::Links",
     "RT::Record::Role::Roles";
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
80
81
82
83

use RT::Queue;
use RT::User;
use RT::Record;
84
use RT::Link;
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
85
86
use RT::Links;
use RT::Date;
Jesse Vincent's avatar
Jesse Vincent committed
87
use RT::CustomFields;
88
use RT::Tickets;
89
use RT::Transactions;
Jesse Vincent's avatar
Jesse Vincent committed
90
use RT::Reminders;
Jesse Vincent's avatar
Jesse Vincent committed
91
92
use RT::URI::fsck_com_rt;
use RT::URI;
93
use MIME::Entity;
94
use Devel::GlobalDestruction;
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
95

96
97
sub LifecycleColumn { "Queue" }

98
99
100
101
102
103
104
105
106
my %ROLES = (
    # name    =>  description
    Owner     => 'The owner of a ticket',                             # loc_pair
    Requestor => 'The requestor of a ticket',                         # loc_pair
    Cc        => 'The CC of a ticket',                                # loc_pair
    AdminCc   => 'The administrative CC of a ticket',                 # loc_pair
);

for my $role (sort keys %ROLES) {
107
108
109
    RT::Ticket->RegisterRole(
        Name            => $role,
        EquivClasses    => ['RT::Queue'],
110
111
        ( $role eq "Owner" ? ( Column => "Owner")   : () ),
        ( $role !~ /Cc/    ? ( ACLOnlyInEquiv => 1) : () ),
112
113
114
    );
}

115
116
117
118
119
our %MERGE_CACHE = (
    effective => {},
    merged => {},
);

Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
120
121
122
123
124
125
126
127
128
129

=head2 Load

Takes a single argument. This can be a ticket id, ticket alias or 
local ticket uri.  If the ticket can't be loaded, returns undef.
Otherwise, returns the ticket id.

=cut

sub Load {
130
131
    my $self = shift;
    my $id   = shift;
132
    $id = '' unless defined $id;
133

134
135
136
    # TODO: modify this routine to look at EffectiveId and
    # do the recursive load thing. be careful to cache all
    # the interim tickets we try so we don't loop forever.
137

138
139
    unless ( $id =~ /^\d+$/ ) {
        $RT::Logger->debug("Tried to load a bogus ticket id: '$id'");
140
141
142
        return (undef);
    }

143
144
145
    $id = $MERGE_CACHE{'effective'}{ $id }
        if $MERGE_CACHE{'effective'}{ $id };

146
147
148
    my ($ticketid, $msg) = $self->LoadById( $id );
    unless ( $self->Id ) {
        $RT::Logger->debug("$self tried to load a bogus ticket: $id");
149
150
151
152
        return (undef);
    }

    #If we're merged, resolve the merge.
153
    if ( $self->EffectiveId && $self->EffectiveId != $self->Id ) {
154
155
156
157
        $RT::Logger->debug(
            "We found a merged ticket. "
            . $self->id ."/". $self->EffectiveId
        );
158
159
160
        my $real_id = $self->Load( $self->EffectiveId );
        $MERGE_CACHE{'effective'}{ $id } = $real_id;
        return $real_id;
161
162
163
    }

    #Ok. we're loaded. lets get outa here.
164
    return $self->Id;
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
165
166
167
168
169
170
171
172
173
174
}



=head2 Create (ARGS)

Arguments: ARGS is a hash of named parameters.  Valid parameters are:

  id 
  Queue  - Either a Queue object or a Queue Name
Jesse Vincent's avatar
Jesse Vincent committed
175
176
177
  Requestor -  A reference to a list of  email addresses or RT user Names
  Cc  - A reference to a list of  email addresses or Names
  AdminCc  - A reference to a  list of  email addresses or Names
178
179
  SquelchMailTo - A reference to a list of email addresses - 
                  who should this ticket not mail
180
181
  Type -- The ticket's type. ignore this for now
  Owner -- This ticket's owner. either an RT::User object or this user's id
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
182
  Subject -- A string describing the subject of the ticket
183
  Priority -- an integer from 0 to 99
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
184
185
  InitialPriority -- an integer from 0 to 99
  FinalPriority -- an integer from 0 to 99
186
  Status -- any valid status for Queue's Lifecycle, otherwises uses on_create from Lifecycle default
Jesse Vincent's avatar
Jesse Vincent committed
187
188
189
  TimeEstimated -- an integer. estimated time for this task in minutes
  TimeWorked -- an integer. time worked so far in minutes
  TimeLeft -- an integer. time remaining in minutes
190
191
  Starts -- an ISO date describing the ticket's start date and time in GMT
  Due -- an ISO date describing the ticket's due date and time in GMT
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
192
  MIMEObj -- a MIME::Entity object with the content of the initial ticket request.
193
  CustomField-<n> -- a scalar or array of values for the customfield with the id <n>
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
194

Ruslan Zakirov's avatar
Ruslan Zakirov committed
195
196
197
Ticket links can be set up during create by passing the link type as a hask key and
the ticket id to be linked to as a value (or a URI when linking to other objects).
Multiple links of the same type can be created by passing an array ref. For example:
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
198

199
  Parents => 45,
Ruslan Zakirov's avatar
Ruslan Zakirov committed
200
201
202
203
204
205
  DependsOn => [ 15, 22 ],
  RefersTo => 'http://www.bestpractical.com',

Supported link types are C<MemberOf>, C<HasMember>, C<RefersTo>, C<ReferredToBy>,
C<DependsOn> and C<DependedOnBy>. Also, C<Parents> is alias for C<MemberOf> and
C<Members> and C<Children> are aliases for C<HasMember>.
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
206

Ruslan Zakirov's avatar
Ruslan Zakirov committed
207
Returns: TICKETID, Transaction Object, Error Message
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
208
209
210
211
212
213


=cut

sub Create {
    my $self = shift;
214

Jesse Vincent's avatar
Jesse Vincent committed
215
216
    my %args = (
        id                 => undef,
217
218
219
220
221
        EffectiveId        => undef,
        Queue              => undef,
        Requestor          => undef,
        Cc                 => undef,
        AdminCc            => undef,
222
        SquelchMailTo      => undef,
223
        TransSquelchMailTo => undef,
224
225
226
227
228
229
        Type               => 'ticket',
        Owner              => undef,
        Subject            => '',
        InitialPriority    => undef,
        FinalPriority      => undef,
        Priority           => undef,
230
        Status             => undef,
231
232
233
234
235
236
237
238
239
        TimeWorked         => "0",
        TimeLeft           => 0,
        TimeEstimated      => 0,
        Due                => undef,
        Starts             => undef,
        Started            => undef,
        Resolved           => undef,
        MIMEObj            => undef,
        _RecordTransaction => 1,
240
        DryRun             => 0,
Jesse Vincent's avatar
Jesse Vincent committed
241
242
        @_
    );
243

Ruslan Zakirov's avatar
Ruslan Zakirov committed
244
    my ($ErrStr, @non_fatal_errors);
245

246
    my $QueueObj = RT::Queue->new( RT->SystemUser );
Ruslan Zakirov's avatar
Ruslan Zakirov committed
247
    if ( ref $args{'Queue'} eq 'RT::Queue' ) {
248
        $QueueObj->Load( $args{'Queue'}->Id );
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
249
    }
Ruslan Zakirov's avatar
Ruslan Zakirov committed
250
251
252
    elsif ( $args{'Queue'} ) {
        $QueueObj->Load( $args{'Queue'} );
    }
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
253
    else {
254
        $RT::Logger->debug("'". ( $args{'Queue'} ||''). "' not a recognised queue object." );
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
255
    }
256

Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
257
    #Can't create a ticket without a queue.
Ruslan Zakirov's avatar
Ruslan Zakirov committed
258
    unless ( $QueueObj->Id ) {
259
        $RT::Logger->debug("$self No queue given for ticket creation.");
260
        return ( 0, 0, $self->loc('Could not create ticket. Queue not set') );
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
261
    }
262

263

Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
264
    #Now that we have a queue, Check the ACLS
Jesse Vincent's avatar
Jesse Vincent committed
265
266
267
268
    unless (
        $self->CurrentUser->HasRight(
            Right  => 'CreateTicket',
            Object => $QueueObj
269
        ) and $QueueObj->Disabled != 1
Jesse Vincent's avatar
Jesse Vincent committed
270
271
272
273
      )
    {
        return (
            0, 0,
274
            $self->loc( "No permission to create tickets in the queue '[_1]'", $QueueObj->Name));
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
275
    }
276

277
    my $cycle = $QueueObj->LifecycleObj;
278
    unless ( defined $args{'Status'} && length $args{'Status'} ) {
279
        $args{'Status'} = $cycle->DefaultOnCreate;
280
281
    }

282
    $args{'Status'} = lc $args{'Status'};
283
    unless ( $cycle->IsValid( $args{'Status'} ) ) {
284
285
        return ( 0, 0,
            $self->loc("Status '[_1]' isn't a valid status for tickets in this queue.",
286
287
                $self->loc($args{'Status'}))
        );
Jesse Vincent's avatar
Jesse Vincent committed
288
289
    }

290
291
292
293
294
295
    unless ( $cycle->IsTransition( '' => $args{'Status'} ) ) {
        return ( 0, 0,
            $self->loc("New tickets can not have status '[_1]' in this queue.",
                $self->loc($args{'Status'}))
        );
    }
Jesse Vincent's avatar
Jesse Vincent committed
296
297
298



Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
299
300
    #Since we have a queue, we can set queue defaults

Ruslan Zakirov's avatar
Ruslan Zakirov committed
301
    #Initial Priority
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
302
    # If there's no queue default initial priority and it's not set, set it to 0
303
304
    $args{'InitialPriority'} = $QueueObj->InitialPriority || 0
        unless defined $args{'InitialPriority'};
305

Jesse Vincent's avatar
Jesse Vincent committed
306
    #Final priority
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
307
    # If there's no queue default final priority and it's not set, set it to 0
308
309
    $args{'FinalPriority'} = $QueueObj->FinalPriority || 0
        unless defined $args{'FinalPriority'};
310

311
312
    # Priority may have changed from InitialPriority, for the case
    # where we're importing tickets (eg, from an older RT version.)
313
314
    $args{'Priority'} = $args{'InitialPriority'}
        unless defined $args{'Priority'};
315

316
    # Dates
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
317
318
    #TODO we should see what sort of due date we're getting, rather +
    # than assuming it's in ISO format.
319

Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
320
    #Set the due date. if we didn't get fed one, use the queue default due in
321
    my $Due = RT::Date->new( $self->CurrentUser );
322
    if ( defined $args{'Due'} ) {
323
        $Due->Set( Format => 'ISO', Value => $args{'Due'} );
324
    }
Ruslan Zakirov's avatar
minor    
Ruslan Zakirov committed
325
    elsif ( my $due_in = $QueueObj->DefaultDueIn ) {
Jesse Vincent's avatar
Jesse Vincent committed
326
        $Due->SetToNow;
Ruslan Zakirov's avatar
minor    
Ruslan Zakirov committed
327
        $Due->AddDays( $due_in );
328
329
    }

330
    my $Starts = RT::Date->new( $self->CurrentUser );
331
    if ( defined $args{'Starts'} ) {
332
        $Starts->Set( Format => 'ISO', Value => $args{'Starts'} );
Jesse Vincent's avatar
Jesse Vincent committed
333
334
    }

335
    my $Started = RT::Date->new( $self->CurrentUser );
Jesse Vincent's avatar
Jesse Vincent committed
336
    if ( defined $args{'Started'} ) {
337
        $Started->Set( Format => 'ISO', Value => $args{'Started'} );
Jesse Vincent's avatar
Jesse Vincent committed
338
    }
339

340
341
    # If the status is not an initial status, set the started date
    elsif ( !$cycle->IsInitial($args{'Status'}) ) {
Ruslan Zakirov's avatar
Ruslan Zakirov committed
342
        $Started->SetToNow;
343
    }
Jesse Vincent's avatar
Jesse Vincent committed
344

345
    my $Resolved = RT::Date->new( $self->CurrentUser );
Jesse Vincent's avatar
Jesse Vincent committed
346
    if ( defined $args{'Resolved'} ) {
347
        $Resolved->Set( Format => 'ISO', Value => $args{'Resolved'} );
Jesse Vincent's avatar
Jesse Vincent committed
348
349
350
    }

    #If the status is an inactive status, set the resolved date
351
    elsif ( $cycle->IsInactive( $args{'Status'} ) )
Jesse Vincent's avatar
Jesse Vincent committed
352
    {
353
        $RT::Logger->debug( "Got a ". $args{'Status'}
354
            ."(inactive) ticket with undefined resolved date. Setting to now."
355
        );
Jesse Vincent's avatar
Jesse Vincent committed
356
        $Resolved->SetToNow;
357
358
    }

359
    # Dealing with time fields
360
361
362
363
    $args{'TimeEstimated'} = 0 unless defined $args{'TimeEstimated'};
    $args{'TimeWorked'}    = 0 unless defined $args{'TimeWorked'};
    $args{'TimeLeft'}      = 0 unless defined $args{'TimeLeft'};

364
365
366
    # Figure out users for roles
    my $roles = {};
    push @non_fatal_errors, $self->_ResolveRoles( $roles, %args );
367

368
369
370
    $args{'Type'} = lc $args{'Type'}
        if $args{'Type'} =~ /^(ticket|approval|reminder)$/i;

371
372
    $args{'Subject'} =~ s/\n//g;

373
374
    $RT::Handle->BeginTransaction();

Jesse Vincent's avatar
Jesse Vincent committed
375
376
    my %params = (
        Queue           => $QueueObj->Id,
377
378
379
        Subject         => $args{'Subject'},
        InitialPriority => $args{'InitialPriority'},
        FinalPriority   => $args{'FinalPriority'},
380
        Priority        => $args{'Priority'},
381
382
383
384
385
386
387
388
        Status          => $args{'Status'},
        TimeWorked      => $args{'TimeWorked'},
        TimeEstimated   => $args{'TimeEstimated'},
        TimeLeft        => $args{'TimeLeft'},
        Type            => $args{'Type'},
        Starts          => $Starts->ISO,
        Started         => $Started->ISO,
        Resolved        => $Resolved->ISO,
Jesse Vincent's avatar
Jesse Vincent committed
389
390
        Due             => $Due->ISO
    );
391

Jesse Vincent's avatar
Jesse Vincent committed
392
# Parameters passed in during an import that we probably don't want to touch, otherwise
393
    foreach my $attr (qw(id Creator Created LastUpdated LastUpdatedBy)) {
Ruslan Zakirov's avatar
Ruslan Zakirov committed
394
        $params{$attr} = $args{$attr} if $args{$attr};
Jesse Vincent's avatar
Jesse Vincent committed
395
396
    }

Jesse Vincent's avatar
Jesse Vincent committed
397
    # Delete null integer parameters
Jesse Vincent's avatar
Jesse Vincent committed
398
    foreach my $attr
399
        (qw(TimeWorked TimeLeft TimeEstimated InitialPriority FinalPriority))
Ruslan Zakirov's avatar
Ruslan Zakirov committed
400
    {
Jesse Vincent's avatar
Jesse Vincent committed
401
402
        delete $params{$attr}
          unless ( exists $params{$attr} && $params{$attr} );
Jesse Vincent's avatar
Jesse Vincent committed
403
404
    }

Alex Vandiver's avatar
Alex Vandiver committed
405
    # Delete the time worked if we're counting it in the transaction
Ruslan Zakirov's avatar
Ruslan Zakirov committed
406
    delete $params{'TimeWorked'} if $args{'_RecordTransaction'};
Ruslan Zakirov's avatar
Ruslan Zakirov committed
407
408

    my ($id,$ticket_message) = $self->SUPER::Create( %params );
409
    unless ($id) {
410
        $RT::Logger->crit( "Couldn't create a ticket: " . $ticket_message );
411
        $RT::Handle->Rollback();
Jesse Vincent's avatar
Jesse Vincent committed
412
413
414
        return ( 0, 0,
            $self->loc("Ticket could not be created due to an internal error")
        );
415
416
    }

Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
417
    #Set the ticket's effective ID now that we've created it.
Jesse Vincent's avatar
Jesse Vincent committed
418
419
420
421
    my ( $val, $msg ) = $self->__Set(
        Field => 'EffectiveId',
        Value => ( $args{'EffectiveId'} || $id )
    );
Ruslan Zakirov's avatar
Ruslan Zakirov committed
422
    unless ( $val ) {
423
        $RT::Logger->crit("Couldn't set EffectiveId: $msg");
Ruslan Zakirov's avatar
Ruslan Zakirov committed
424
        $RT::Handle->Rollback;
Jesse Vincent's avatar
Jesse Vincent committed
425
426
427
        return ( 0, 0,
            $self->loc("Ticket could not be created due to an internal error")
        );
428
    }
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
429

430
431
    # Create (empty) role groups
    my $create_groups_ret = $self->_CreateRoleGroups();
432
    unless ($create_groups_ret) {
Jesse Vincent's avatar
Jesse Vincent committed
433
        $RT::Logger->crit( "Couldn't create ticket groups for ticket "
434
435
              . $self->Id
              . ". aborting Ticket creation." );
436
        $RT::Handle->Rollback();
Jesse Vincent's avatar
Jesse Vincent committed
437
        return ( 0, 0,
Jesse Vincent's avatar
Jesse Vincent committed
438
439
            $self->loc("Ticket could not be created due to an internal error")
        );
440
441
    }

442
443
444
445
446
447
448
    # Codify what it takes to add each kind of group
    my %acls = (
        Cc        => sub { 1 },
        Requestor => sub { 1 },
        AdminCc   => sub {
            my $principal = shift;
            return 1 if $self->CurrentUserHasRight('ModifyTicket');
449
450
451
            return unless $self->CurrentUserHasRight("WatchAsAdminCc");
            return unless $principal->id == $self->CurrentUser->PrincipalId;
            return 1;
452
453
454
455
456
457
458
        },
        Owner     => sub {
            my $principal = shift;
            return 1 if $principal->id == RT->Nobody->PrincipalId;
            return $principal->HasRight( Object => $self, Right => 'OwnTicket' );
        },
    );
Ruslan Zakirov's avatar
Ruslan Zakirov committed
459

460
461
    # Populate up the role groups.  This call modifies $roles.
    push @non_fatal_errors, $self->_AddRolesOnCreate( $roles, %acls );
462

463
    # Squelching
464
465
466
467
    if ($args{'SquelchMailTo'}) {
       my @squelch = ref( $args{'SquelchMailTo'} ) ? @{ $args{'SquelchMailTo'} }
        : $args{'SquelchMailTo'};
        $self->_SquelchMailTo( @squelch );
468
469
    }

470
    # Add all the custom fields
Ruslan Zakirov's avatar
Ruslan Zakirov committed
471
    foreach my $arg ( keys %args ) {
Ruslan Zakirov's avatar
Ruslan Zakirov committed
472
        next unless $arg =~ /^CustomField-(\d+)$/i;
Ruslan Zakirov's avatar
Ruslan Zakirov committed
473
        my $cfid = $1;
474
475
        my $cf = $self->LoadCustomFieldByIdentifier($cfid);
        next unless $cf->ObjectTypeFromLookupType->isa(ref $self);
Ruslan Zakirov's avatar
Ruslan Zakirov committed
476
477
478

        foreach my $value (
            UNIVERSAL::isa( $args{$arg} => 'ARRAY' ) ? @{ $args{$arg} } : ( $args{$arg} ) )
Ruslan Zakirov's avatar
Ruslan Zakirov committed
479
        {
Ruslan Zakirov's avatar
Ruslan Zakirov committed
480
            next unless defined $value && length $value;
Ruslan Zakirov's avatar
Ruslan Zakirov committed
481
482

            # Allow passing in uploaded LargeContent etc by hash reference
Ruslan Zakirov's avatar
Ruslan Zakirov committed
483
            my ($status, $msg) = $self->_AddCustomFieldValue(
Ruslan Zakirov's avatar
Ruslan Zakirov committed
484
485
486
487
488
489
490
                (UNIVERSAL::isa( $value => 'HASH' )
                    ? %$value
                    : (Value => $value)
                ),
                Field             => $cfid,
                RecordTransaction => 0,
            );
Ruslan Zakirov's avatar
Ruslan Zakirov committed
491
            push @non_fatal_errors, $msg unless $status;
Ruslan Zakirov's avatar
Ruslan Zakirov committed
492
493
494
        }
    }

495
    # Deal with setting up links
Jesse Vincent's avatar
Jesse Vincent committed
496

Ruslan Zakirov's avatar
Ruslan Zakirov committed
497
498
499
    # TODO: Adding link may fire scrips on other end and those scrips
    # could create transactions on this ticket before 'Create' transaction.
    #
500
    # We should implement different lifecycle: record 'Create' transaction,
Ruslan Zakirov's avatar
Ruslan Zakirov committed
501
502
503
504
505
506
    # create links and only then fire create transaction's scrips.
    #
    # Ideal variant: add all links without firing scrips, record create
    # transaction and only then fire scrips on the other ends of links.
    #
    # //RUZ
507
508
509
    push @non_fatal_errors, $self->_AddLinksOnCreate(\%args, {
        Silent => !$args{'_RecordTransaction'} || ($self->Type || '') eq 'reminder',
    });
510

511
512
513
514
515
516
517
518
    # Try to add roles once more.
    push @non_fatal_errors, $self->_AddRolesOnCreate( $roles, %acls );

    # Anything left is failure of ACLs; Cc and Requestor have no ACLs,
    # so we don't bother checking them.
    if (@{ $roles->{Owner} }) {
        my $owner = $roles->{Owner}[0]->Object;
        $RT::Logger->warning( "User " . $owner->Name . "(" . $owner->id
519
520
                . ") was proposed as a ticket owner but has no rights to own "
                . "tickets in " . $QueueObj->Name );
521
522
523
524
525
526
527
528
529
        push @non_fatal_errors, $self->loc(
            "Owner '[_1]' does not have rights to own this ticket.",
            $owner->Name
        );
    }
    for my $principal (@{ $roles->{AdminCc} }) {
        push @non_fatal_errors, $self->loc(
            "No rights to add '[_1]' as an AdminCc on this ticket",
            $principal->Object->Name
530
        );
531
    }
Jesse Vincent's avatar
Jesse Vincent committed
532

Jesse Vincent's avatar
Jesse Vincent committed
533
    if ( $args{'_RecordTransaction'} ) {
Jesse Vincent's avatar
Jesse Vincent committed
534

535
        # Add a transaction for the create
Jesse Vincent's avatar
Jesse Vincent committed
536
        my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
537
538
539
540
            Type         => "Create",
            TimeTaken    => $args{'TimeWorked'},
            MIMEObj      => $args{'MIMEObj'},
            CommitScrips => !$args{'DryRun'},
541
            SquelchMailTo => $args{'TransSquelchMailTo'},
Jesse Vincent's avatar
Jesse Vincent committed
542
543
544
        );

        if ( $self->Id && $Trans ) {
545

546
            $TransObj->UpdateCustomFields( %args );
547

Jesse Vincent's avatar
Jesse Vincent committed
548
            $RT::Logger->info( "Ticket " . $self->Id . " created in queue '" . $QueueObj->Name . "' by " . $self->CurrentUser->Name );
549
            $ErrStr = $self->loc( "Ticket [_1] created in queue '[_2]'", $self->Id, $QueueObj->Name );
Jesse Vincent's avatar
Jesse Vincent committed
550
            $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
Jesse Vincent's avatar
Jesse Vincent committed
551
552
553
554
        }
        else {
            $RT::Handle->Rollback();

Jesse Vincent's avatar
Jesse Vincent committed
555
            $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
Jesse Vincent's avatar
Jesse Vincent committed
556
            $RT::Logger->error("Ticket couldn't be created: $ErrStr");
557
            return ( 0, 0, $self->loc( "Ticket could not be created due to an internal error"));
Jesse Vincent's avatar
Jesse Vincent committed
558
559
        }

560
561
562
563
        if ( $args{'DryRun'} ) {
            $RT::Handle->Rollback();
            return ($self->id, $TransObj, $ErrStr);
        }
Jesse Vincent's avatar
Jesse Vincent committed
564
565
        $RT::Handle->Commit();
        return ( $self->Id, $TransObj->Id, $ErrStr );
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
566
567
    }
    else {
Jesse Vincent's avatar
Jesse Vincent committed
568

Jesse Vincent's avatar
Jesse Vincent committed
569
570
571
        # Not going to record a transaction
        $RT::Handle->Commit();
        $ErrStr = $self->loc( "Ticket [_1] created in queue '[_2]'", $self->Id, $QueueObj->Name );
Jesse Vincent's avatar
Jesse Vincent committed
572
        $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
Jesse Vincent's avatar
Jesse Vincent committed
573
        return ( $self->Id, 0, $ErrStr );
574

Jesse Vincent's avatar
Jesse Vincent committed
575
    }
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
576
577
}

578
579
580
581
582
583
584
585
586
sub SetType {
    my $self = shift;
    my $value = shift;

    # Force lowercase on internal RT types
    $value = lc $value
        if $value =~ /^(ticket|approval|reminder)$/i;
    return $self->_Set(Field => 'Type', Value => $value, @_);
}
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
587

588
589
590
591
592
593
594
595
=head2 OwnerGroup

A constructor which returns an RT::Group object containing the owner of this ticket.

=cut

sub OwnerGroup {
    my $self = shift;
596
    return $self->RoleGroup( 'Owner' );
597
598
599
}


600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
sub _HasModifyWatcherRight {
    my $self = shift;
    my ($type, $principal) = @_;

    # ModifyTicket works in any case
    return 1 if $self->CurrentUserHasRight('ModifyTicket');
    # If the watcher isn't the current user then the current user has no right
    return 0 unless $self->CurrentUser->PrincipalId == $principal->id;
    # If it's an AdminCc and they don't have 'WatchAsAdminCc', bail
    return 0 if $type eq 'AdminCc' and not $self->CurrentUserHasRight('WatchAsAdminCc');
    # If it's a Requestor or Cc and they don't have 'Watch', bail
    return 0 if ($type eq "Cc" or $type eq 'Requestor')
        and not $self->CurrentUserHasRight('Watch');
    return 1;
}
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
615
616
617
618


=head2 AddWatcher

619
620
621
Applies access control checking, then calls L<RT::Record/AddRoleMember>.
Additionally, C<Email> is accepted as an alternative argument name for
C<User>.
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
622

623
Returns a tuple of (status, message).
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
624
625
626
627
628

=cut

sub AddWatcher {
    my $self = shift;
629
630
    my %args = (
        Type  => undef,
631
632
        PrincipalId => undef,
        Email => undef,
633
634
        @_
    );
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
635

636
    $args{ACL} = sub { $self->_HasModifyWatcherRight( @_ ) };
637
638
639
640
641
642
    $args{User} ||= delete $args{Email};
    my ($principal, $msg) = $self->AddRoleMember(
        %args,
        InsideTransaction => 1,
    );
    return ( 0, $msg) unless $principal;
643

644
645
    return ( 1, $self->loc('Added [_1] as a [_2] for this ticket',
                $principal->Object->Name, $self->loc($args{'Type'})) );
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
646
647
648
}


649
=head2 DeleteWatcher
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
650

651
652
653
Applies access control checking, then calls L<RT::Record/DeleteRoleMember>.
Additionally, C<Email> is accepted as an alternative argument name for
C<User>.
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
654

655
Returns a tuple of (status, message).
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
656
657
658
659
660
661

=cut


sub DeleteWatcher {
    my $self = shift;
662

663
    my %args = ( Type        => undef,
664
                 PrincipalId => undef,
665
                 Email       => undef,
666
                 @_ );
667

668
    $args{ACL} = sub { $self->_HasModifyWatcherRight( @_ ) };
669
670
671
    $args{User} ||= delete $args{Email};
    my ($principal, $msg) = $self->DeleteRoleMember( %args );
    return ( 0, $msg ) unless $principal;
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
672

673
674
675
    return ( 1,
             $self->loc( "[_1] is no longer a [_2] for this ticket.",
                         $principal->Object->Name,
676
                         $self->loc($args{'Type'}) ) );
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
677
678
679
680
681
682
}





683
684
685
686
687
688
689
690
691
692
693
694
695
=head2 SquelchMailTo [EMAIL]

Takes an optional email address to never email about updates to this ticket.


Returns an array of the RT::Attribute objects for this ticket's 'SquelchMailTo' attributes.


=cut

sub SquelchMailTo {
    my $self = shift;
    if (@_) {
696
        unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
697
            return ();
698
        }
699
700
    } else {
        unless ( $self->CurrentUserHasRight('ShowTicket') ) {
701
            return ();
702
        }
703
704

    }
705
706
707
708
709
710
711
712
713
714
    return $self->_SquelchMailTo(@_);
}

sub _SquelchMailTo {
    my $self = shift;
    if (@_) {
        my $attr = shift;
        $self->AddAttribute( Name => 'SquelchMailTo', Content => $attr )
            unless grep { $_->Content eq $attr }
                $self->Attributes->Named('SquelchMailTo');
715
716
717
    }
    my @attributes = $self->Attributes->Named('SquelchMailTo');
    return (@attributes);
718
719
720
}


721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
=head2 UnsquelchMailTo ADDRESS

Takes an address and removes it from this ticket's "SquelchMailTo" list. If an address appears multiple times, each instance is removed.

Returns a tuple of (status, message)

=cut

sub UnsquelchMailTo {
    my $self = shift;

    my $address = shift;
    unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
        return ( 0, $self->loc("Permission Denied") );
    }

737
738
    my ($val, $msg) = $self->Attributes->DeleteEntry ( Name => 'SquelchMailTo', Content => $address);
    return ($val, $msg);
739
740
}

741

Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
742

743
=head2 RequestorAddresses
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
744

745
B<Returns> String: All Ticket Requestor email addresses as a string.
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
746
747
748

=cut

749
sub RequestorAddresses {
750
    my $self = shift;
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
751

752
    unless ( $self->CurrentUserHasRight('ShowTicket') ) {
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
753
754
        return undef;
    }
755

756
    return ( $self->Requestors->MemberEmailAddressesAsString );
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
757
758
759
}


760
=head2 AdminCcAddresses
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
761
762
763
764
765

returns String: All Ticket AdminCc email addresses as a string

=cut

766
sub AdminCcAddresses {
767
    my $self = shift;
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
768

769
770
    unless ( $self->CurrentUserHasRight('ShowTicket') ) {
        return undef;
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
771
    }
772

773
    return ( $self->AdminCc->MemberEmailAddressesAsString )
774

Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
775
776
}

777
=head2 CcAddresses
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
778
779
780
781
782

returns String: All Ticket Ccs as a string of email addresses

=cut

783
sub CcAddresses {
784
    my $self = shift;
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
785

786
787
    unless ( $self->CurrentUserHasRight('ShowTicket') ) {
        return undef;
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
788
    }
789
    return ( $self->Cc->MemberEmailAddressesAsString);
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
790
791
792
793
794
795

}




796
=head2 Requestor
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
797
798

Takes nothing.
799
Returns this ticket's Requestors as an RT::Group object
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
800
801
802

=cut

803
sub Requestor {
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
804
    my $self = shift;
805
806
807
    return RT::Group->new($self->CurrentUser)
        unless $self->CurrentUserHasRight('ShowTicket');
    return $self->RoleGroup( 'Requestor' );
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
808
809
}

810
811
812
813
814
sub Requestors {
    my $self = shift;
    return $self->Requestor;
}

Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
815
816
817
818
819


=head2 Cc

Takes nothing.
820
821
Returns an RT::Group object which contains this ticket's Ccs.
If the user doesn't have "ShowTicket" permission, returns an empty group
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
822
823
824
825
826

=cut

sub Cc {
    my $self = shift;
827

828
829
830
    return RT::Group->new($self->CurrentUser)
        unless $self->CurrentUserHasRight('ShowTicket');
    return $self->RoleGroup( 'Cc' );
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
831
832
833
834
835
836
837
}



=head2 AdminCc

Takes nothing.
838
839
Returns an RT::Group object which contains this ticket's AdminCcs.
If the user doesn't have "ShowTicket" permission, returns an empty group
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
840
841
842
843
844

=cut

sub AdminCc {
    my $self = shift;
845

846
847
848
    return RT::Group->new($self->CurrentUser)
        unless $self->CurrentUserHasRight('ShowTicket');
    return $self->RoleGroup( 'AdminCc' );
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
849
850
851
852
853
854
855
}




# a generic routine to be called by IsRequestor, IsCc and IsAdminCc

856
=head2 IsWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL }
857

858
Takes a param hash with the attributes Type and either PrincipalId or Email
859
860
861

Type is one of Requestor, Cc, AdminCc and Owner

862
PrincipalId is an RT::Principal id, and Email is an email address.
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
863

864
865
Returns true if the specified principal (or the one corresponding to the
specified address) is a member of the group Type for this ticket.
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
866

867
868
XX TODO: This should be Memoized. 

Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
869
870
871
872
873
=cut

sub IsWatcher {
    my $self = shift;

874
875
    my %args = ( Type  => 'Requestor',
        PrincipalId    => undef,
876
        Email          => undef,
877
878
879
        @_
    );

880
881
    # Load the relevant group.
    my $group = $self->RoleGroup( $args{'Type'} );
882

883
884
885
886
887
888
889
890
891
892
893
894
895
    # Find the relevant principal.
    if (!$args{PrincipalId} && $args{Email}) {
        # Look up the specified user.
        my $user = RT::User->new($self->CurrentUser);
        $user->LoadByEmail($args{Email});
        if ($user->Id) {
            $args{PrincipalId} = $user->PrincipalId;
        }
        else {
            # A non-existent user can't be a group member.
            return 0;
        }
    }
896

897
    # Ask if it has the member in question
898
    return $group->HasMember( $args{'PrincipalId'} );
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
899
}
900

Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
901
902


903
=head2 IsRequestor PRINCIPAL_ID
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
904
  
Ruslan Zakirov's avatar
pod fix    
Ruslan Zakirov committed
905
Takes an L<RT::Principal> id.
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
906

Ruslan Zakirov's avatar
pod fix    
Ruslan Zakirov committed
907
Returns true if the principal is a requestor of the current ticket.
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
908
909
910
911

=cut

sub IsRequestor {
912
    my $self   = shift;
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
913
914
    my $person = shift;

915
    return ( $self->IsWatcher( Type => 'Requestor', PrincipalId => $person ) );
916

Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
917
918
919
920
};



921
922
923
=head2 IsCc PRINCIPAL_ID

  Takes an RT::Principal id.
924
  Returns true if the principal is a Cc of the current ticket.
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
925
926
927
928
929


=cut

sub IsCc {
930
931
932
    my $self = shift;
    my $cc   = shift;

933
    return ( $self->IsWatcher( Type => 'Cc', PrincipalId => $cc ) );
934

Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
935
936
937
938
}



939
=head2 IsAdminCc PRINCIPAL_ID
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
940

941
  Takes an RT::Principal id.
942
  Returns true if the principal is an AdminCc of the current ticket.
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
943
944
945
946

=cut

sub IsAdminCc {
947
948
949
    my $self   = shift;
    my $person = shift;

950
    return ( $self->IsWatcher( Type => 'AdminCc', PrincipalId => $person ) );
951

Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
952
953
954
955
956
957
958
959
960
961
962
963
}



=head2 IsOwner

  Takes an RT::User object. Returns true if that user is this ticket's owner.
returns undef otherwise

=cut

sub IsOwner {
964
    my $self   = shift;
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
965
966
967
968
    my $person = shift;

    # no ACL check since this is used in acl decisions
    # unless ($self->CurrentUserHasRight('ShowTicket')) {
969
970
    #    return(undef);
    #   }    
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
971
972

    #Tickets won't yet have owners when they're being created.
973
974
    unless ( $self->OwnerObj->id ) {
        return (undef);
Jesse Vincent's avatar
rt.2.1  
Jesse Vincent committed
975
    }
976
977
978

    if ( $person->id == $self