root/Catalyst-View-JSON/trunk/lib/Catalyst/View/JSON.pm

Revision 2322 (checked in by miyagawa, 13 years ago)

Checking in changes prior to tagging of version 0.20. Changelog diff is:

=== Changes
==================================================================
--- Changes (revision 6837)
+++ Changes (local)
@@ -1,5 +1,9 @@

Revision history for Perl extension Catalyst::View
JSON

+0.20 Wed Aug 29 03:23:27 PDT 2007
+ - Fixed POD config 'V::JSON' to 'View::JSON'
+ (Thanks to Matt S Trout)
+

0.19 Fri Aug 3 15:32:04 PDT 2007

- Store JSON
Any object into json_dumper to prevent other
modules to change JSON::Any backend like WWW::Facebook
API does.

  • Property svn:keywords set to Id Revision
Line 
1 package Catalyst::View::JSON;
2
3 use strict;
4 our $VERSION = '0.20';
5
6 use base qw( Catalyst::View );
7 use Encode ();
8 use NEXT;
9 use Catalyst::Exception;
10 require JSON::Any;
11
12 __PACKAGE__->mk_accessors(qw( allow_callback callback_param expose_stash encoding json_dumper no_x_json_header ));
13
14 sub new {
15     my($class, $c, $arguments) = @_;
16     my $self = $class->NEXT::new($c);
17
18     for my $field (keys %$arguments) {
19         next if $field eq 'json_driver';
20         if ($self->can($field)) {
21             $self->$field($arguments->{$field});
22         } else {
23             $c->log->debug("Unknown config parameter '$field'");
24         }
25     }
26
27     my $driver = $arguments->{json_driver} || 'JSON';
28     $driver =~ s/^JSON:://; #backward compatibility
29
30     eval {
31         JSON::Any->import($driver);
32         my $json = JSON::Any->new; # create the copy of JSON handler
33         $self->json_dumper(sub { $json->objToJson($_[0]) });
34     };
35
36     if (my $error = $@) {
37         die $error;
38     }
39
40     return $self;
41 }
42
43 sub process {
44     my($self, $c) = @_;
45
46     # get the response data from stash
47     my $cond = sub { 1 };
48
49     my $single_key;
50     if (my $expose = $self->expose_stash) {
51         if (ref($expose) eq 'Regexp') {
52             $cond = sub { $_[0] =~ $expose };
53         } elsif (ref($expose) eq 'ARRAY') {
54             my %match = map { $_ => 1 } @$expose;
55             $cond = sub { $match{$_[0]} };
56         } elsif (!ref($expose)) {
57             $single_key = $expose;
58         } else {
59             $c->log->warn("expose_stash should be an array referernce or Regexp object.");
60         }
61     }
62
63     my $data;
64     if ($single_key) {
65         $data = $c->stash->{$single_key};
66     } else {
67         $data = { map { $cond->($_) ? ($_ => $c->stash->{$_}) : () }
68                   keys %{$c->stash} };
69     }
70
71     my $cb_param = $self->allow_callback
72         ? ($self->callback_param || 'callback') : undef;
73     my $cb = $cb_param ? $c->req->param($cb_param) : undef;
74     $self->validate_callback_param($cb) if $cb;
75
76     my $json = $self->json_dumper->($data);
77
78     # When you set encoding option in View::JSON, this plugin DWIMs
79     my $encoding = $self->encoding || 'utf-8';
80
81     # if you pass a valid Unicode flagged string in the stash,
82     # this view automatically transcodes to the encoding you set.
83     # Otherwise it just bypasses the stash data in JSON format
84     if ( Encode::is_utf8($json) ) {
85         $json = Encode::encode($encoding, $json);
86     }
87
88     if (($c->req->user_agent || '') =~ /Opera/) {
89         $c->res->content_type("application/x-javascript; charset=$encoding");
90     } else {
91         $c->res->content_type("application/json; charset=$encoding");
92     }
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 (($c->req->user_agent || '') =~ m/Safari/ and $encoding eq 'utf-8') {
102         $output = "\xEF\xBB\xBF";
103     }
104
105     $output .= "$cb(" if $cb;
106     $output .= $json;
107     $output .= ");"   if $cb;
108
109     $c->res->output($output);
110 }
111
112 sub validate_callback_param {
113     my($self, $param) = @_;
114     $param =~ /^[a-zA-Z0-9\.\_\[\]]+$/
115         or Catalyst::Exception->throw("Invalid callback parameter $param");
116 }
117
118 1;
119 __END__
120
121 =head1 NAME
122
123 Catalyst::View::JSON - JSON view for your data
124
125 =head1 SYNOPSIS
126
127   # lib/MyApp/View/JSON.pm
128   package MyApp::View::JSON;
129   use base qw( Catalyst::View::JSON );
130   1;
131
132   # configure in lib/MyApp.pm
133   MyApp->config({
134       ...
135       'View::JSON' => {
136           allow_callback  => 1,    # defaults to 0
137           callback_param  => 'cb', # defaults to 'callback'
138           expose_stash    => [ qw(foo bar) ], # defaults to everything
139       },
140   });
141
142   sub hello : Local {
143       my($self, $c) = @_;
144       $c->stash->{message} = 'Hello World!';
145       $c->forward('MyApp::View::JSON');
146   }
147
148 =head1 DESCRIPTION
149
150 Catalyst::View::JSON is a Catalyst View handler that returns stash
151 data in JSON format.
152
153 =head1 CONFIG VARIABLES
154
155 =over 4
156
157 =item allow_callback
158
159 Flag to allow callbacks by adding C<callback=function>. Defaults to 0
160 (doesn't allow callbacks). See L</CALLBACKS> for details.
161
162 =item callback_param
163
164 Name of URI parameter to specify JSON callback function name. Defaults
165 to C<callback>. Only effective when C<allow_callback> is turned on.
166
167 =item expose_stash
168
169 Scalar, List or regular expression object, to specify which stash keys are
170 exposed as a JSON response. Defaults to everything. Examples configuration:
171
172   # use 'json_data' value as a data to return
173   expose_stash => 'json_data',
174
175   # only exposes keys 'foo' and 'bar'
176   expose_stash => [ qw( foo bar ) ],
177
178   # only exposes keys that matches with /^json_/
179   expose_stash => qr/^json_/,
180
181 Suppose you have data structure of the following.
182
183   $c->stash->{foo} = [ 1, 2 ];
184   $c->stash->{bar} = [ 3, 4 ];
185
186 By default, this view will return:
187
188   {"foo":[1,2],"bar":2}
189
190 When you set C<< expose_stash => [ 'foo' ] >>, it'll return
191
192   {"foo":[1,2]}
193
194 and in the case of C<< expose_stash => 'foo' >>, it'll just return
195
196   [1,2]
197
198 instead of the whole object (hashref in perl). This option will be
199 useful when you share the method with different views (e.g. TT) and
200 don't want to expose non-irrelevant stash variables as in JSON.
201
202 =item json_driver
203
204   json_driver: JSON::Syck
205
206 By default this plugin uses JSON to encode the object, but you can
207 switch to the other drivers like JSON::Syck. For now, JSON::Syck is
208 the only alternative encoding driver.
209
210 =item no_x_json_header
211
212   no_x_json_header: 1
213
214 By default this plugin sets X-JSON header if the requested client is a
215 Prototype.js with X-JSON support. By setting 1, you can opt-out this
216 behavior so that you can do eval() by your own. Defaults to 0.
217
218 =back
219
220 =head2 ENCODINGS
221
222 Due to the browser gotchas like those of Safari and Opera, sometimes
223 you have to specify a valid charset value in the response's
224 Content-Type header, e.g. C<text/javascript; charset=utf-8>.
225
226 Catalyst::View::JSON comes with the configuration variable C<encoding>
227 which defaults to utf-8. You can change it via C<< YourApp->config >>
228 or even runtime, using C<component>.
229
230   $c->component('View::JSON')->encoding('euc-jp');
231
232 This assumes you set your stash data in raw euc-jp bytes, or Unicode
233 flagged variable. In case of Unicode flagged variable,
234 Catalyst::View::JSON automatically encodes the data into your
235 C<encoding> value (euc-jp in this case) before emitting the data to
236 the browser.
237
238 Another option would be to use I<JavaScript-UCS> as an encoding (and
239 pass Unicode flagged string to the stash). That way all non-ASCII
240 characters in the output JSON will be automatically encoded to
241 JavaScript Unicode encoding like I<\uXXXX>. You have to install
242 L<Encode::JavaScript::UCS> to use the encoding.
243
244 =head2 CALLBACKS
245
246 By default it returns raw JSON data so your JavaScript app can deal
247 with using XMLHttpRequest calls. Adding callbacks (JSONP) to the API
248 gives more flexibility to the end users of the API: overcome the
249 cross-domain restrictions of XMLHttpRequest. It can be done by
250 appending I<script> node with dynamic DOM manipulation, and associate
251 callback handler to the returned data.
252
253 For example, suppose you have the following code.
254
255   sub end : Private {
256       my($self, $c) = @_;
257       if ($c->req->param('output') eq 'json') {
258           $c->forward('MyApp::View::JSON');
259       } else {
260           ...
261       }
262   }
263
264 C</foo/bar?output=json> will just return the data set in
265 C<< $c->stash >> as JSON format, like:
266
267   { result: "foo", message: "Hello" }
268
269 but C</foo/bar?output=json&callback=handle_result> will give you:
270
271   handle_result({ result: "foo", message: "Hello" });
272
273 and you can write a custom C<handle_result> function to handle the
274 returned data asynchronously.
275
276 The valid characters you can use in the callback function are
277
278   [a-zA-Z0-9\.\_\[\]]
279
280 but you can customize the behaviour by overriding the
281 C<validate_callback_param> method in your View::JSON class.
282
283 See L<http://developer.yahoo.net/common/json.html> and
284 L<http://ajaxian.com/archives/jsonp-json-with-padding> for more about
285 JSONP.
286
287 =head1 INTEROPERABILITY
288
289 JSON use is still developing and has not been standardized. This
290 section provides some notes on various libraries.
291
292 Dojo Toolkit: Setting dojo.io.bind's mimetype to 'text/json' in
293 the JavaScript request will instruct dojo.io.bind to expect JSON
294 data in the response body and auto-eval it. Dojo ignores the
295 server response Content-Type. This works transparently with
296 Catalyst::View::JSON.
297
298 Prototype.js: prototype.js will auto-eval JSON data that is
299 returned in the custom X-JSON header. The reason given for this is
300 to allow a separate HTML fragment in the response body, however
301 this of limited use because IE 6 has a max header length that will
302 cause the JSON evaluation to silently fail when reached. The
303 recommened approach is to use Catalyst::View::JSON which will JSON
304 format all the response data and return it in the response body.
305
306 In at least prototype 1.5.0 rc0 and above, prototype.js will send the
307 X-Prototype-Version header. If this is encountered, a JavaScript eval
308 will be returned in the X-JSON resonse header to automatically eval
309 the response body, unless you set I<no_x_json_header> to 1. If your
310 version of prototype does not send this header, you can manually eval
311 the response body using the following JavaScript:
312
313   evalJSON: function(request) {
314     try {
315       return eval('(' + request.responseText + ')');
316     } catch (e) {}
317   }
318   // elsewhere
319   var json = this.evalJSON(request);
320
321 =head1 SECURITY CONSIDERATION
322
323 Catalyst::View::JSON makes the data available as a (sort of)
324 JavaScript to the client, so you might want to be careful about the
325 security of your data.
326
327 =head2 Use callbacks only for public data
328
329 When you enable callbacks (JSONP) by setting C<allow_callbacks>, all
330 your JSON data will be available cross-site. This means embedding
331 private data of logged-in user to JSON is considered bad.
332
333   # MyApp.yaml
334   View::JSON:
335     allow_callbacks: 1
336
337   sub foo : Local {
338       my($self, $c) = @_;
339       $c->stash->{address} = $c->user->street_address; # BAD
340       $c->forward('View::JSON');
341   }
342
343 If you want to enable callbacks in a controller (for public API) and
344 disable in another, you need to create two different View classes,
345 like MyApp::View::JSON and MyApp::View::JSONP, because
346 C<allow_callbacks> is a static configuration of the View::JSON class.
347
348 See L<http://ajaxian.com/archives/gmail-csrf-security-flaw> for more.
349
350 =head2 Avoid valid cross-site JSON requests
351
352 Even if you disable the callbacks, the nature of JavaScript still has
353 a possiblity to access private JSON data cross-site, by overriding
354 Array constructor C<[]>.
355
356   # MyApp.yaml
357   View::JSON:
358     expose_stash: json
359
360   sub foo : Local {
361       my($self, $c) = @_;
362       $c->stash->{json} = [ $c->user->street_address ]; # BAD
363       $c->forward('View::JSON');
364   }
365
366 When you return logged-in user's private data to the response JSON,
367 you might want to disable GET requests (because I<script> tag invokes
368 GET requests), or include a random digest string and validate it.
369
370 See
371 L<http://jeremiahgrossman.blogspot.com/2006/01/advanced-web-attack-techniques-using.html>
372 for more.
373
374 =head1 AUTHOR
375
376 Tatsuhiko Miyagawa E<lt>miyagawa@bulknews.netE<gt>
377
378 =head1 LICENSE
379
380 This library is free software; you can redistribute it and/or modify
381 it under the same terms as Perl itself.
382
383 =head1 CONTRIBUTORS
384
385 Following people has been contributing patches, bug reports and
386 suggestions for the improvement of Catalyst::View::JSON.
387
388 John Wang
389 kazeburo
390 Daisuke Murase
391 Jun Kuriyama
392
393 =head1 SEE ALSO
394
395 L<Catalyst>, L<JSON>, L<Encode::JavaScript::UCS>
396
397 =cut
Note: See TracBrowser for help on using the browser.