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