File Coverage

blib/lib/Catalyst/View/JSON.pm
Criterion Covered Total %
statement 80 85 94.1
branch 36 40 90.0
condition 13 15 86.6
subroutine 16 17 94.1
pod 3 5 60.0
total 148 162 91.3


line stmt bran cond sub pod time code
1             package Catalyst::View::JSON;
2              
3 2     2   1444080 use strict;
  2         3  
  2         48  
4 2     2   7 use warnings;
  2         1  
  2         79  
5             our $VERSION = '0.36';
6 2     2   36 use 5.008_001;
  2         8  
7              
8 2     2   6 use base qw( Catalyst::View );
  2         3  
  2         787  
9 2     2   419373 use Encode ();
  2         7525  
  2         34  
10 2     2   8 use MRO::Compat;
  2         3  
  2         29  
11 2     2   405 use Catalyst::Exception;
  2         112280  
  2         1477  
12              
13             __PACKAGE__->mk_accessors(qw(
14             allow_callback callback_param expose_stash
15             encoding json_dumper no_x_json_header json_encoder_args
16             use_force_bom));
17              
18             sub new {
19 2     2 1 19227 my($class, $c, $arguments) = @_;
20 2         8 my $self = $class->next::method($c);
21              
22 2         4721 for my $field (keys %$arguments) {
23            
24             # Remove catalyst_component_name (and future Cat specific params)
25 6 100       868 next if $field =~ /^catalyst/;
26            
27             # no longer supported
28 4 50       8 warn('json_driver is no longer supported'), next if $field eq 'json_driver';
29              
30 4 50       23 if ($self->can($field)) {
31 4         17 $self->$field($arguments->{$field});
32             } else {
33 0         0 $c->log->debug("Unknown config parameter '$field'");
34             }
35             }
36              
37 2 100       13 if (my $method = $self->can('encode_json')) {
38             $self->json_dumper( sub {
39 1     1   76 my($data, $self, $c) = @_;
40 1         5 $method->($self, $c, $data);
41 1         9 } );
42             } else {
43 1         546 require JSON::MaybeXS;
44 1 50       3150 my %args = (utf8=>1, %{$self->json_encoder_args ||+{}});
  1         9  
45 1         98 my $json = JSON::MaybeXS->new(%args);
46 1     10   27 $self->json_dumper(sub { $json->encode($_[0]) });
  10         837  
47             }
48              
49 2         462 return $self;
50             }
51              
52             sub process {
53 12     12 1 197115 my($self, $c) = @_;
54              
55             # get the response data from stash
56 12     4   52 my $cond = sub { 1 };
  4         10  
57              
58 12         16 my $single_key;
59 12 100       41 if (my $expose = $self->expose_stash) {
60 11 100       835 if (ref($expose) eq 'Regexp') {
    50          
    100          
61 9     15   33 $cond = sub { $_[0] =~ $expose };
  15         79  
62             } elsif (ref($expose) eq 'ARRAY') {
63 0         0 my %match = map { $_ => 1 } @$expose;
  0         0  
64 0     0   0 $cond = sub { $match{$_[0]} };
  0         0  
65             } elsif (!ref($expose)) {
66 1         2 $single_key = $expose;
67             } else {
68 1         4 $c->log->warn("expose_stash should be an array reference, Regexp object, or key for a single stash entry.");
69 1         331 $c->log->warn("Returning all stash entries");
70             }
71             }
72              
73 12         389 my $data;
74 12 100       18 if ($single_key) {
75 1         4 $data = $c->stash->{$single_key};
76             } else {
77 19 100       779 $data = { map { $cond->($_) ? ($_ => $c->stash->{$_}) : () }
78 11         15 keys %{$c->stash} };
  11         34  
79             }
80              
81 12 100 50     559 my $cb_param = $self->allow_callback
82             ? ($self->callback_param || 'callback') : undef;
83 12 100       1541 my $cb = $cb_param ? $c->req->param($cb_param) : undef;
84 12 100       892 $self->validate_callback_param($cb) if $cb;
85              
86             # When you set encoding option in View::JSON, this plugin DWIMs
87 11   100     39 my $encoding = $self->encoding || 'utf-8';
88              
89 11         763 $c->res->content_type("application/json; charset=$encoding");
90              
91 11 100 100     2236 if ($c->req->header('X-Prototype-Version') && !$self->no_x_json_header) {
92 1         158 $c->res->header('X-JSON' => 'eval("("+this.transport.responseText+")")');
93             }
94              
95 11         1133 my $json = $self->render($c, $data);
96 11         21 my $output;
97              
98             ## add UTF-8 BOM if the client meets a test and the application wants it.
99 11 100 100     38 if ($self->use_force_bom && $encoding eq 'utf-8') {
100 9   100     642 my $user_agent = $c->req->user_agent || '';
101 9 100       636 if ($self->user_agent_bom_test($user_agent)) {
102 2         5 $output = "\xEF\xBB\xBF";
103             }
104             }
105              
106 11 100       161 $output .= "$cb(" if $cb;
107 11         21 $output .= $json;
108 11 100       21 $output .= ");" if $cb;
109              
110 11         28 $c->res->output($output);
111             }
112              
113             # allow for called as $c, $template, $data || $c, $data so that we are compatible
114             # with the semi standard render method that a lot of views use.
115              
116             sub render {
117 11     11 1 19 my $self = shift;
118 11         27 my $c = shift;
119 11         16 my $data = pop;
120              
121 11         35 return $self->json_dumper->($data, $self, $c); # weird order to be backward compat
122             }
123              
124             sub user_agent_bom_test {
125 9     9 0 16 my ($self, $user_agent) = @_;
126 9   66     48 return(($user_agent =~ m/\bSafari\b/) and ($user_agent !~ m/\bChrome\b/));
127             }
128              
129              
130             sub validate_callback_param {
131 2     2 0 3 my($self, $param) = @_;
132 2 100       25 $param =~ /^[a-zA-Z0-9\.\_\[\]]+$/
133             or Catalyst::Exception->throw("Invalid callback parameter $param");
134             }
135              
136             1;
137             __END__
138              
139             =head1 NAME
140              
141             Catalyst::View::JSON - JSON view for your data
142              
143             =head1 SYNOPSIS
144              
145             # lib/MyApp/View/JSON.pm
146             package MyApp::View::JSON;
147             use base qw( Catalyst::View::JSON );
148             1;
149              
150             # configure in lib/MyApp.pm
151             MyApp->config({
152             ...
153             'View::JSON' => {
154             allow_callback => 1, # defaults to 0
155             callback_param => 'cb', # defaults to 'callback'
156             expose_stash => [ qw(foo bar) ], # defaults to everything
157             },
158             });
159              
160             sub hello : Local {
161             my($self, $c) = @_;
162             $c->stash->{message} = 'Hello World!';
163             $c->forward('View::JSON');
164             }
165              
166             =head1 DESCRIPTION
167              
168             Catalyst::View::JSON is a Catalyst View handler that returns stash
169             data in JSON format.
170              
171             =head1 CONFIG VARIABLES
172              
173             =over 4
174              
175             =item allow_callback
176              
177             Flag to allow callbacks by adding C<callback=function>. Defaults to 0
178             (doesn't allow callbacks). See L</CALLBACKS> for details.
179              
180             =item callback_param
181              
182             Name of URI parameter to specify JSON callback function name. Defaults
183             to C<callback>. Only effective when C<allow_callback> is turned on.
184              
185             =item expose_stash
186              
187             Scalar, List or regular expression object, to specify which stash keys are
188             exposed as a JSON response. Defaults to everything. Examples configuration:
189              
190             # use 'json_data' value as a data to return
191             expose_stash => 'json_data',
192              
193             # only exposes keys 'foo' and 'bar'
194             expose_stash => [ qw( foo bar ) ],
195              
196             # only exposes keys that matches with /^json_/
197             expose_stash => qr/^json_/,
198              
199             Suppose you have data structure of the following.
200              
201             $c->stash->{foo} = [ 1, 2 ];
202             $c->stash->{bar} = 2;
203              
204             By default, this view will return:
205              
206             {"foo":[1,2],"bar":2}
207              
208             When you set C<< expose_stash => [ 'foo' ] >>, it'll return
209              
210             {"foo":[1,2]}
211              
212             and in the case of C<< expose_stash => 'foo' >>, it'll just return
213              
214             [1,2]
215              
216             instead of the whole object (hashref in perl). This option will be
217             useful when you share the method with different views (e.g. TT) and
218             don't want to expose non-irrelevant stash variables as in JSON.
219              
220             =item no_x_json_header
221              
222             no_x_json_header: 1
223              
224             By default this plugin sets X-JSON header if the requested client is a
225             Prototype.js with X-JSON support. By setting 1, you can opt-out this
226             behavior so that you can do eval() by your own. Defaults to 0.
227              
228             =item json_encoder_args
229              
230             An optional hashref that supplies arguments to L<JSON::MaybeXS> used when creating
231             a new object.
232              
233             =item use_force_bom
234              
235             If versions of this view older than 0.36, there was some code that added a UTF-8 BOM
236             marker to the end of the JSON string when the user agent was Safari. After looking
237             at a lot of existing code I don't think this is needed anymore so we removed it by
238             default. However if this turns out to be a problem you can re enable it by setting
239             this attribute to true. Possible a breaking change so we offer this workaround.
240              
241             You may also override the method 'user_agent_bom_test' which received the current
242             request user agent string to try and better determine if this is needed. Patches
243             for this welcomed.
244              
245             =back
246              
247             =head1 METHODS
248              
249             =head2 process
250              
251             Standard target of $c->forward used to prepare a response
252              
253             =head2 render
254              
255             The methods accepts either of the following argument signatures in order to promote
256             compatibility with the semi standard render method as define in numerous L<Catalyst>
257             views on CPAN:
258              
259             my $json_string = $c->view('JSON')->render($c, undef, $data);
260             my $json_string = $c->view('JSON')->render($c, $data);
261              
262             Given '$data' returns the JSON serialized version, or throws and error.
263              
264             =head1 OVERRIDING JSON ENCODER
265              
266             By default it uses L<JSON::MaybeXS::encode_json> to serialize perl data structure into
267             JSON data format. If you want to avoid this and encode with your own
268             encoder (like passing different options to L<JSON::MaybeXS> etc.), you can implement
269             the C<encode_json> method in your View class.
270              
271             package MyApp::View::JSON;
272             use base qw( Catalyst::View::JSON );
273              
274             use JSON::MaybeXS ();
275              
276             sub encode_json {
277             my($self, $c, $data) = @_;
278             my $encoder = JSON::MaybeXS->new->(ascii => 1, pretty => 1, allow_nonref => 1);
279             $encoder->encode($data);
280             }
281              
282             1;
283              
284             =head1 ENCODINGS
285              
286             B<NOTE> Starting in release v5.90080 L<Catalyst> encodes all text
287             like body returns as UTF8. It however ignores content types like
288             application/json and assumes that a correct JSON serializer is
289             doing what it is supposed to do, which is encode UTF8 automatically.
290             In general this is what this view does so you shoulding need to
291             mess with the encoding flag here unless you have some odd case.
292              
293             Also, the comment aboe regard 'browser gotcha's' was written a
294             number of years ago and I can't say one way or another if those
295             gotchas continue to be common in the wild.
296              
297             B<NOTE> Setting this configuation has no bearing on how the actual
298             serialized string is encoded. This ONLY sets the content type
299             header in your response. By default we set the 'utf8' flag on
300             L<JSON::MaybeXS> so that the string generated and set to your response
301             body is proper UTF8 octets that can be transmitted over HTTP. If
302             you are planning to do some alternative encoding you should turn off
303             this default via the C<json_encoder_args>:
304              
305             MyApp::View::JSON->config(
306             json_encoder_args => +{utf8=>0} );
307              
308             B<NOTE> In 2015 the use of UTF8 as encoding is widely standard so it
309             is very likely you should need to do nothing to get the correct
310             encoding. The following documention will remain for historical
311             value and backcompat needs.
312              
313             Due to the browser gotchas like those of Safari and Opera, sometimes
314             you have to specify a valid charset value in the response's
315             Content-Type header, e.g. C<text/javascript; charset=utf-8>.
316              
317             Catalyst::View::JSON comes with the configuration variable C<encoding>
318             which defaults to utf-8. You can change it via C<< YourApp->config >>
319             or even runtime, using C<component>.
320              
321             $c->component('View::JSON')->encoding('euc-jp');
322              
323             This assumes you set your stash data in raw euc-jp bytes, or Unicode
324             flagged variable. In case of Unicode flagged variable,
325             Catalyst::View::JSON automatically encodes the data into your
326             C<encoding> value (euc-jp in this case) before emitting the data to
327             the browser.
328              
329             Another option would be to use I<JavaScript-UCS> as an encoding (and
330             pass Unicode flagged string to the stash). That way all non-ASCII
331             characters in the output JSON will be automatically encoded to
332             JavaScript Unicode encoding like I<\uXXXX>. You have to install
333             L<Encode::JavaScript::UCS> to use the encoding.
334              
335             =head1 CALLBACKS
336              
337             By default it returns raw JSON data so your JavaScript app can deal
338             with using XMLHttpRequest calls. Adding callbacks (JSONP) to the API
339             gives more flexibility to the end users of the API: overcome the
340             cross-domain restrictions of XMLHttpRequest. It can be done by
341             appending I<script> node with dynamic DOM manipulation, and associate
342             callback handler to the returned data.
343              
344             For example, suppose you have the following code.
345              
346             sub end : Private {
347             my($self, $c) = @_;
348             if ($c->req->param('output') eq 'json') {
349             $c->forward('View::JSON');
350             } else {
351             ...
352             }
353             }
354              
355             C</foo/bar?output=json> will just return the data set in
356             C<< $c->stash >> as JSON format, like:
357              
358             { result: "foo", message: "Hello" }
359              
360             but C</foo/bar?output=json&callback=handle_result> will give you:
361              
362             handle_result({ result: "foo", message: "Hello" });
363              
364             and you can write a custom C<handle_result> function to handle the
365             returned data asynchronously.
366              
367             The valid characters you can use in the callback function are
368              
369             [a-zA-Z0-9\.\_\[\]]
370              
371             but you can customize the behaviour by overriding the
372             C<validate_callback_param> method in your View::JSON class.
373              
374             See L<http://developer.yahoo.net/common/json.html> and
375             L<http://ajaxian.com/archives/jsonp-json-with-padding> for more about
376             JSONP.
377              
378             B<NOTE> For another way to enable JSONP in your application take a look
379             at L<Plack::Middleware::JSONP>
380              
381             =head1 INTEROPERABILITY
382              
383             JSON use is still developing and has not been standardized. This
384             section provides some notes on various libraries.
385              
386             Dojo Toolkit: Setting dojo.io.bind's mimetype to 'text/json' in
387             the JavaScript request will instruct dojo.io.bind to expect JSON
388             data in the response body and auto-eval it. Dojo ignores the
389             server response Content-Type. This works transparently with
390             Catalyst::View::JSON.
391              
392             Prototype.js: prototype.js will auto-eval JSON data that is
393             returned in the custom X-JSON header. The reason given for this is
394             to allow a separate HTML fragment in the response body, however
395             this of limited use because IE 6 has a max header length that will
396             cause the JSON evaluation to silently fail when reached. The
397             recommend approach is to use Catalyst::View::JSON which will JSON
398             format all the response data and return it in the response body.
399              
400             In at least prototype 1.5.0 rc0 and above, prototype.js will send the
401             X-Prototype-Version header. If this is encountered, a JavaScript eval
402             will be returned in the X-JSON response header to automatically eval
403             the response body, unless you set I<no_x_json_header> to 1. If your
404             version of prototype does not send this header, you can manually eval
405             the response body using the following JavaScript:
406              
407             evalJSON: function(request) {
408             try {
409             return eval('(' + request.responseText + ')');
410             } catch (e) {}
411             }
412             // elsewhere
413             var json = this.evalJSON(request);
414              
415             B<NOTE> The above comments were written a number of years ago and
416             I would take then with a grain of salt so to speak. For now I will
417             leave them in place but not sure they are meaningful in 2015.
418              
419             =head1 SECURITY CONSIDERATION
420              
421             Catalyst::View::JSON makes the data available as a (sort of)
422             JavaScript to the client, so you might want to be careful about the
423             security of your data.
424              
425             =head2 Use callbacks only for public data
426              
427             When you enable callbacks (JSONP) by setting C<allow_callback>, all
428             your JSON data will be available cross-site. This means embedding
429             private data of logged-in user to JSON is considered bad.
430              
431             # MyApp.yaml
432             View::JSON:
433             allow_callback: 1
434              
435             sub foo : Local {
436             my($self, $c) = @_;
437             $c->stash->{address} = $c->user->street_address; # BAD
438             $c->forward('View::JSON');
439             }
440              
441             If you want to enable callbacks in a controller (for public API) and
442             disable in another, you need to create two different View classes,
443             like MyApp::View::JSON and MyApp::View::JSONP, because
444             C<allow_callback> is a static configuration of the View::JSON class.
445              
446             See L<http://ajaxian.com/archives/gmail-csrf-security-flaw> for more.
447              
448             =head2 Avoid valid cross-site JSON requests
449              
450             Even if you disable the callbacks, the nature of JavaScript still has
451             a possibility to access private JSON data cross-site, by overriding
452             Array constructor C<[]>.
453              
454             # MyApp.yaml
455             View::JSON:
456             expose_stash: json
457              
458             sub foo : Local {
459             my($self, $c) = @_;
460             $c->stash->{json} = [ $c->user->street_address ]; # BAD
461             $c->forward('View::JSON');
462             }
463              
464             When you return logged-in user's private data to the response JSON,
465             you might want to disable GET requests (because I<script> tag invokes
466             GET requests), or include a random digest string and validate it.
467              
468             See
469             L<http://jeremiahgrossman.blogspot.com/2006/01/advanced-web-attack-techniques-using.html>
470             for more.
471              
472             =head1 AUTHOR
473              
474             Tatsuhiko Miyagawa E<lt>miyagawa@bulknews.netE<gt>
475              
476             =head1 LICENSE
477              
478             This library is free software; you can redistribute it and/or modify
479             it under the same terms as Perl itself.
480              
481             =head1 CONTRIBUTORS
482              
483             Following people has been contributing patches, bug reports and
484             suggestions for the improvement of Catalyst::View::JSON.
485              
486             John Wang
487             kazeburo
488             Daisuke Murase
489             Jun Kuriyama
490             Tomas Doran
491              
492             =head1 SEE ALSO
493              
494             L<Catalyst>, L<JSON::MaybeXS>, L<Encode::JavaScript::UCS>
495              
496             L<http://www.prototypejs.org/learn/json>
497             L<http://docs.jquery.com/Ajax/jQuery.getJSON>
498             L<http://manual.dojotoolkit.org/json.html>
499             L<http://developer.yahoo.com/yui/json/>
500              
501             =cut