File Coverage

blib/lib/Catalyst/View/JSON.pm
Criterion Covered Total %
statement 16 18 88.8
branch n/a
condition n/a
subroutine 6 6 100.0
pod n/a
total 22 24 91.6


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