File Coverage

blib/lib/Catalyst/ResponseHelpers.pm
Criterion Covered Total %
statement 35 97 36.0
branch 0 20 0.0
condition 0 33 0.0
subroutine 12 26 46.1
pod 14 14 100.0
total 61 190 32.1


line stmt bran cond sub pod time code
1 1     1   22797 use 5.010;
  1         4  
2 1     1   6 use strict;
  1         2  
  1         30  
3 1     1   4 use warnings;
  1         2  
  1         38  
4 1     1   804 use utf8;
  1         12  
  1         5  
5             package Catalyst::ResponseHelpers;
6 1     1   685 use parent qw< Exporter::Tiny >;
  1         359  
  1         6  
7 1     1   5170 use HTTP::Status qw< :constants :is status_message >;
  1         3427  
  1         344  
8 1     1   680 use Path::Tiny;
  1         9755  
  1         57  
9 1     1   428 use Safe::Isa qw< $_isa >;
  1         356  
  1         96  
10 1     1   468 use Encode qw< encode_utf8 >;
  1         6856  
  1         55  
11 1     1   421 use IO::String;
  1         2577  
  1         24  
12 1     1   520 use URI;
  1         2940  
  1         22  
13 1     1   386 use URI::QueryParam;
  1         470  
  1         763  
14              
15             our $VERSION = '1.02';
16              
17             =encoding utf-8
18              
19             =head1 NAME
20              
21             Catalyst::ResponseHelpers - Concise response constructors for Catalyst controllers
22              
23             =head1 SYNOPSIS
24              
25             use Catalyst::ResponseHelpers qw< :helpers :status >;
26              
27             sub show_user : Chained('/') PathPart('user') Args(1) {
28             my ($c, $id) = @_;
29             my $user = load_user($id)
30             or return NotFound($c, "The user id <$id> couldn't be found.");
31             ...
32             }
33              
34             =head1 DESCRIPTION
35              
36             Various helper functions for setting up the current L<Catalyst::Response>
37             object. All response helpers call C<Catalyst/detach> to stop request
38             processing. For clarity in your controller actions, it is nevertheless
39             recommended that you call these helpers as values to L<return()|perlfunc/return>.
40              
41             =head1 EXPORTS
42              
43             By default, only the helper methods documented below are exported. You may
44             explicitly request them using the C<:helpers> tag.
45              
46             You may also request C<:status>, which re-exports the C<:constants> from
47             L<HTTP::Status> into your package. This is useful for custom status codes.
48              
49             =cut
50              
51             our %EXPORT_TAGS = (
52             status => $HTTP::Status::EXPORT_TAGS{constants},
53             helpers => [qw[
54             Ok
55             Forbidden
56             NotFound
57             ClientError
58             ServerError
59             TextPlain
60             AsJSON
61             FromFile
62             FromCharString
63             FromByteString
64             FromHandle
65             Redirect
66             RedirectToUrl
67             ReturnWithMsg
68             ]],
69             );
70             our @EXPORT = @{ $EXPORT_TAGS{helpers} };
71             our @EXPORT_OK = map { @$_ } values %EXPORT_TAGS;
72              
73             =head1 FUNCTIONS
74              
75             =head2 ReturnWithMsg($c, $mid)
76              
77             Redirects to the request’s C<return> parameter, or C</> if no such parameter
78             exists or if the given URI appears to be external to the app. The given
79             C<$mid> is set as a query parameter, and should be the result of a
80             C<< $c->set_status_msg >> or C<< $c->set_error_msg >> call. These context
81             methods are normally provided by L<Catalyst::Plugin::StatusMessage>.
82              
83             =head2 Redirect($c, $action_or_action_path, @args?)
84              
85             Passes arguments to L<Catalyst/uri_for_action> and redirects to the returned
86             URL.
87              
88             =head2 RedirectToUrl($c, $url, $status?)
89              
90             Redirects to the given URL, with an optional custom status. Status defaults to
91             302 (HTTP_FOUND).
92              
93             =cut
94              
95             sub ReturnWithMsg {
96 0     0 1   my ($c, $mid) = @_;
97 0           my $base = $c->req->base;
98 0           my $return = URI->new( $c->req->param('return') );
99 0 0 0       $return = $c->uri_for('/') unless $return and $return =~ m{^\Q$base\E}i;
100 0           $return->query_param_append( mid => $mid );
101 0           RedirectToUrl($c, $return);
102             }
103              
104             sub Redirect {
105 0     0 1   my ($c, $action, @rest) = @_;
106 0           RedirectToUrl($c, $c->uri_for_action($action, @rest));
107             }
108              
109             sub RedirectToUrl {
110 0     0 1   my ($c, $url, $status) = @_;
111 0           $c->response->redirect($url, $status);
112 0           $c->detach;
113             }
114              
115             =head2 Ok($c, $status?, $msg?)
116              
117             Sets a body-less 204 No Content response by default, switching to a 200 OK with
118             a body via L</TextPlain> iff a message is provided. Both the status and
119             message may be omitted or provided. If the message is omitted, a body-less
120             response is set.
121              
122             Note that if you're using L<Catalyst::Action::RenderView> and you specify a
123             status other than 204 but don't provide a message (e.g. C<Ok($c, 200)>),
124             RenderView will intercept the response and try to render a template. This
125             probably isn't what you wanted. A workaround is to use the proper status code
126             when sending no content (204) or specify a message (the empty string is OK).
127              
128             =cut
129              
130             sub Ok {
131 0     0 1   my ($c, $status, $msg) = @_;
132 0 0 0       ($status, $msg) = (undef, $status)
133             if @_ == 2 and not is_success($status);
134              
135 0 0         if (defined $msg) {
136 0   0       $status //= HTTP_OK;
137 0           TextPlain($c, $status, $msg);
138             } else {
139 0   0       $status //= HTTP_NO_CONTENT;
140 0           $c->response->status($status);
141 0           $c->response->body(undef);
142 0           $c->detach;
143             }
144             }
145              
146             =head2 Forbidden($c, $msg?)
147              
148             Sets a plain text 403 Forbidden response, with an optional custom message.
149              
150             =head2 NotFound($c, $msg?)
151              
152             Sets a plain text 404 Not Found response, with an optional custom message.
153              
154             =cut
155              
156             sub Forbidden {
157 0     0 1   my ($c, $msg) = @_;
158 0           TextPlain($c, HTTP_FORBIDDEN, $msg);
159             }
160              
161             sub NotFound {
162 0     0 1   my ($c, $msg) = @_;
163 0           TextPlain($c, HTTP_NOT_FOUND, $msg);
164             }
165              
166             =head2 ClientError($c, $status?, $msg?)
167              
168             Sets a plain text 400 Bad Request response by default, with an optional
169             custom message. Both the status and message may be omitted or provided.
170              
171             =head2 ServerError($c, $status?, $msg?)
172              
173             Sets a plain text 500 Internal Server Error response by default, with an
174             optional custom message. Both the status and message may be omitted or
175             provided. The error is logged via L<Catalyst/log>.
176              
177             =cut
178              
179             sub ClientError {
180 0     0 1   my ($c, $status, $msg) = @_;
181 0 0 0       ($status, $msg) = (undef, $status)
182             if @_ == 2 and not is_client_error($status);
183 0   0       TextPlain($c, $status // HTTP_BAD_REQUEST, $msg);
184             }
185              
186             sub ServerError {
187 0     0 1   my ($c, $status, $msg) = @_;
188 0 0 0       ($status, $msg) = (undef, $status)
189             if @_ == 2 and not is_server_error($status);
190 0   0       $status //= HTTP_INTERNAL_SERVER_ERROR;
191 0           $c->log->error("HTTP $status: $msg");
192 0           TextPlain($c, $status, $msg);
193             }
194              
195             =head2 TextPlain($c, $status?, $msg?)
196              
197             Sets a plain text 200 OK response by default, with an optional custom
198             message. Both the status and message may be omitted or provided.
199              
200             =cut
201              
202             sub TextPlain {
203 0     0 1   my ($c, $status, $msg) = @_;
204 0 0 0       ($status, $msg) = (undef, $status)
205             if @_ == 2 and not status_message($status);
206 0   0       $status //= HTTP_OK;
207 0           $c->response->status($status);
208 0           $c->response->content_type("text/plain");
209 0   0       $c->response->body($msg // status_message($status));
210 0           $c->detach;
211             }
212              
213             =head2 AsJSON($c, $status?, $data)
214              
215             Sets a JSON 200 OK response by default, with an optional custom status. Data
216             should be serializable by a view named C<JSON> provided by your application
217             (e.g. via L<Catalyst::View::JSON>).
218              
219             =cut
220              
221             sub AsJSON {
222 0     0 1   my ($c, $status, $data) = @_;
223 0 0         ($status, $data) = (undef, $status)
224             if @_ == 2;
225 0   0       $status //= HTTP_OK;
226 0           $c->response->status($status);
227 0           $c->stash( json => $data );
228 0           $c->view('JSON')->process($c);
229 0           $c->detach;
230             }
231              
232             =head2 FromFile($c, $filename, $mime_type, $headers?)
233              
234             Sets a response from the contents of the filename using the specified MIME
235             type. C<Content-Length> and C<Last-Modified> are set from the file.
236              
237             The C<Content-Disposition> is set to C<attachment> by default, usually forcing
238             a download.
239              
240             An optional arrayref of additional headers may also be provided, which is
241             passed through to L</FromHandle>.
242              
243             =head2 FromCharString($c, $string, $mime_type, $headers?)
244              
245             Sets a response from the contents of a B<character> string using the specified
246             MIME type. The character string will be encoded as UTF-8 bytes.
247              
248             The C<Content-Disposition> is set to C<attachment> by default, usually forcing
249             a download.
250              
251             An optional arrayref of additional headers may also be provided, which is
252             passed through to L</FromHandle>.
253              
254             =head2 FromByteString($c, $string, $mime_type, $headers?)
255              
256             Sets a response from the contents of a B<byte> string using the specified
257             MIME type. The character string will B<NOT> be encoded.
258              
259             The C<Content-Disposition> is set to C<attachment> by default, usually forcing
260             a download.
261              
262             An optional arrayref of additional headers may also be provided, which is
263             passed through to L</FromHandle>.
264              
265             =head2 FromHandle($c, $handle, $mime_type, $headers?)
266              
267             Sets a response from the contents of the filehandle using the specified MIME
268             type. An optional arrayref of additional headers may also be provided, which
269             is passed to L<the response’s|Catalyst::Response> L<HTTP::Headers> object.
270              
271             The C<Content-Disposition> is set to C<attachment> by default, usually forcing
272             a download.
273              
274             =cut
275              
276             sub FromFile {
277 0     0 1   my ($c, $file) = (shift, shift);
278 0 0         $file = path($file)
279             unless $file->$_isa("Path::Tiny");
280 0           return FromHandle($c, $file->openr_raw, @_);
281             }
282              
283             sub FromByteString {
284 0     0 1   my ($c, $string) = (shift, shift);
285 0           my $handle = IO::String->new( $string );
286 0           return FromHandle($c, $handle, @_);
287             }
288              
289             sub FromCharString {
290 0     0 1   my ($c, $string) = (shift, shift);
291 0           return FromByteString($c, encode_utf8($string), @_);
292             }
293              
294             sub FromHandle {
295 0     0 1   my ($c, $handle, $mime, $headers) = @_;
296 0           my $h = $c->response->headers;
297              
298 0           $c->response->body( $handle );
299 0           $c->response->header('Content-Disposition' => 'attachment');
300              
301             # Default to UTF-8 for text content unless otherwise specified
302 0           $h->content_type( $mime );
303 0 0 0       $h->content_type( "$mime; charset=utf-8" )
304             if $h->content_is_text and not $h->content_type_charset;
305              
306 0 0         $h->header( @$headers )
307             if $headers;
308 0           $c->detach;
309             }
310              
311             =head1 AUTHOR
312              
313             Thomas Sibley E<lt>trsibley@uw.eduE<gt>
314              
315             =head1 THANKS
316              
317             Inspired in part by seeing John Napiorkowski’s (jnap)
318             L<experimental response helpers in CatalystX::Example::Todo|https://github.com/jjn1056/CatalystX-Example-Todo/blob/master/lib/Catalyst/ResponseHelpers.pm>.
319              
320             =head1 COPYRIGHT
321              
322             Copyright 2015- by the University of Washington
323              
324             =head1 LICENSE
325              
326             This library is free software; you can redistribute it and/or modify
327             it under the same terms as Perl itself.
328              
329             =cut
330              
331             1;