1# BEGIN BPS TAGGED BLOCK {{{
2#
3# COPYRIGHT:
4#
5# This software is Copyright (c) 1996-2021 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
49package RT::REST2::Resource::Ticket;
50use strict;
51use warnings;
52
53use Moose;
54use namespace::autoclean;
55
56extends 'RT::REST2::Resource::Record';
57with (
58    'RT::REST2::Resource::Record::Readable',
59    'RT::REST2::Resource::Record::Hypermedia'
60        => { -alias => { hypermedia_links => '_default_hypermedia_links' } },
61    'RT::REST2::Resource::Record::Deletable',
62    'RT::REST2::Resource::Record::Writable'
63        => { -alias => { create_record => '_create_record', update_record => '_update_record' } },
64);
65
66has 'action' => (
67    is  => 'ro',
68    isa => 'Str',
69);
70
71sub dispatch_rules {
72    Path::Dispatcher::Rule::Regex->new(
73        regex => qr{^/ticket/?$},
74        block => sub { { record_class => 'RT::Ticket' } },
75    ),
76    Path::Dispatcher::Rule::Regex->new(
77        regex => qr{^/ticket/(\d+)/?$},
78        block => sub { { record_class => 'RT::Ticket', record_id => shift->pos(1) } },
79    ),
80    Path::Dispatcher::Rule::Regex->new(
81        regex => qr{^/ticket/(\d+)/(take|untake|steal)$},
82        block => sub { { record_class => 'RT::Ticket', record_id => $_[0]->pos(1), action => $_[0]->pos(2) } },
83    ),
84}
85
86sub create_record {
87    my $self = shift;
88    my $data = shift;
89
90    # Check for any bad input data before creating a ticket
91    my ($ok, $msg, $return_code) = $self->validate_input(Data => $data, Action => 'create');
92    if (!$ok) {
93        return (\$return_code, $msg);
94    }
95
96    if ( defined $data->{Content} || defined $data->{Attachments} ) {
97        $data->{MIMEObj} = HTML::Mason::Commands::MakeMIMEEntity(
98            Interface => 'REST',
99            Subject   => $data->{Subject},
100            Body      => delete $data->{Content},
101            Type      => delete $data->{ContentType} || 'text/plain',
102        );
103        if ( defined $data->{Attachments} ) {
104            return (\400, "Attachments must be an array") unless ref($data->{Attachments}) eq 'ARRAY';
105            foreach my $attachment (@{$data->{Attachments}}) {
106                return (\400, "Each element of Attachments must be a hash") unless ref($attachment) eq 'HASH';
107                foreach my $field (qw(FileName FileType FileContent)) {
108                    return (\400, "Field $field is required for each attachment in Attachments") unless $attachment->{$field};
109                }
110                $data->{MIMEObj}->attach(
111                    Type     => $attachment->{FileType},
112                    Filename => $attachment->{FileName},
113                    Data     => MIME::Base64::decode_base64($attachment->{FileContent}));
114            }
115            delete $data->{Attachments};
116        }
117    }
118
119    my ($txn);
120    ($ok, $txn, $msg) = $self->_create_record($data);
121    return ($ok, $msg);
122}
123
124sub update_record {
125    my $self = shift;
126    my $data = shift;
127
128    my @results;
129
130    if ( my $action = $self->action ) {
131        my $method = ucfirst $action;
132        my ( $ok, $msg ) = $self->record->$method();
133        push @results, $msg;
134    }
135
136    push @results, $self->_update_record($data);
137    if ( my $ticket_id = delete $data->{MergeInto} ) {
138        my ( $ok, $msg ) = $self->record->MergeInto($ticket_id);
139        push @results, $msg;
140    }
141    return @results;
142}
143
144sub forbidden {
145    my $self = shift;
146    return 0 unless $self->record->id;
147    return !$self->record->CurrentUserHasRight('ShowTicket');
148}
149
150sub lifecycle_hypermedia_links {
151    my $self = shift;
152    my $self_link = $self->_self_link;
153    my $ticket = $self->record;
154    my @links;
155
156    # lifecycle actions
157    my $lifecycle = $ticket->LifecycleObj;
158    my $current = $ticket->Status;
159    my $hide_resolve_with_deps = RT->Config->Get('HideResolveActionsWithDependencies')
160        && $ticket->HasUnresolvedDependencies;
161
162    for my $info ( $lifecycle->Actions($current) ) {
163        my $next = $info->{'to'};
164        next unless $lifecycle->IsTransition( $current => $next );
165
166        my $check = $lifecycle->CheckRight( $current => $next );
167        next unless $ticket->CurrentUserHasRight($check);
168
169        next if $hide_resolve_with_deps
170            && $lifecycle->IsInactive($next)
171            && !$lifecycle->IsInactive($current);
172
173        my $url = $self_link->{_url};
174        $url .= '/correspond' if ($info->{update}||'') eq 'Respond';
175        $url .= '/comment' if ($info->{update}||'') eq 'Comment';
176
177        push @links, {
178            %$info,
179            label => $self->current_user->loc($info->{'label'} || ucfirst($next)),
180            ref   => 'lifecycle',
181            _url  => $url,
182        };
183    }
184
185    return @links;
186}
187
188sub hypermedia_links {
189    my $self = shift;
190    my $self_link = $self->_self_link;
191    my $links = $self->_default_hypermedia_links(@_);
192    my $ticket = $self->record;
193
194    push @$links, $self->_transaction_history_link;
195
196    push @$links, {
197            ref     => 'correspond',
198            _url    => $self_link->{_url} . '/correspond',
199    } if $ticket->CurrentUserHasRight('ReplyToTicket');
200
201    push @$links, {
202        ref     => 'comment',
203        _url    => $self_link->{_url} . '/comment',
204    } if $ticket->CurrentUserHasRight('CommentOnTicket');
205
206    push @$links, $self->lifecycle_hypermedia_links;
207
208    return $links;
209}
210
211sub validate_input {
212    my $self = shift;
213    my %args = ( Data    => '',
214                 Action  => '',
215                 @_ );
216    my $data = $args{'Data'};
217
218    if ( $args{'Action'} eq 'create' ) {
219        return (0, "Could not create ticket. Queue not set", 400) if !$data->{Queue};
220
221        my $queue = RT::Queue->new(RT->SystemUser);
222        $queue->Load($data->{Queue});
223
224        return (0, "Unable to find queue", 400) if !$queue->Id;
225
226        return (0, $self->record->loc("No permission to create tickets in the queue '[_1]'", $queue->Name), 403)
227            unless $self->record->CurrentUser->HasRight(
228                Right  => 'CreateTicket',
229                Object => $queue,
230            ) and $queue->Disabled != 1;
231    }
232
233    if ( $args{'Action'} eq 'update' ) {
234        # Add pre-update input validation
235    }
236
237    return (1, "Validation passed");
238}
239
240
241__PACKAGE__->meta->make_immutable;
242
2431;
244