File Coverage

lib/Mojolicious/Plugin/Qooxdoo/JsonRpcController.pm
Criterion Covered Total %
statement 130 146 89.0
branch 35 50 70.0
condition 9 15 60.0
subroutine 17 18 94.4
pod 0 6 0.0
total 191 235 81.2


line stmt bran cond sub pod time code
1             package Mojolicious::Plugin::Qooxdoo::JsonRpcController;
2              
3 1     1   1210 use strict;
  1         2  
  1         32  
4 1     1   5 use warnings;
  1         1  
  1         30  
5              
6 1     1   5 use Mojo::JSON qw(encode_json decode_json);
  1         2  
  1         53  
7 1     1   6 use Mojo::Base 'Mojolicious::Controller';
  1         1  
  1         6  
8 1     1   9739 use Mojo::Promise;
  1         2  
  1         13  
9 1     1   33 use Storable qw(dclone);
  1         2  
  1         49  
10              
11 1     1   6 use Encode;
  1         2  
  1         936  
12              
13              
14             has toUTF8 => sub { find_encoding('utf8') };
15              
16             our $VERSION = '1.0.13';
17              
18             has 'service';
19              
20             has 'crossDomain';
21              
22             has 'requestId';
23              
24             has 'methodName';
25              
26             has 'rpcParams';
27              
28             sub dispatch {
29 16     16 0 24144 my $self = shift;
30            
31             # We have to differentiate between POST and GET requests, because
32             # the data is not sent in the same place..
33 16         65 my $log = $self->log;
34              
35             # send warnings to log file preserving the origin
36             local $SIG{__WARN__} = sub {
37 0     0   0 my $message = shift;
38 0         0 $message =~ s/\n$//;
39 0         0 @_ = ($log, $message);
40 0         0 goto &Mojo::Log::warn;
41 16         448 };
42 16         39 my $data;
43 16         60 for ( $self->req->method ){
44 16 100       333 /^POST$/ && do {
45             # Data comes as JSON object, so fetch a reference to it
46 13   100     43 my $type = $self->req->headers->content_type//'*missing header*';
47 13 100       376 if ($type !~ m{^application/json\b}i) {
48 1         7 $log->error("unexpected Content-Type header: $type (should be application/json)");
49 1         50 $self->render(text => "invalid payload format announcement", status=>500);
50 1         376 return;
51             }
52 12         29 $data = eval { decode_json($self->req->body) };
  12         414  
53 12 100       3360 if ($@) {
54 1         11 my $error = "Invalid json string: " . $@;
55 1         30 $log->error($error);
56 1         11 $self->render(text => "invalid payload format", status=>500);
57 1         374 return;
58             };
59 11         63 $self->requestId($data->{id});
60 11         102 $self->crossDomain(0);
61 11         66 last;
62             };
63 3 100       19 /^GET$/ && do {
64             # not checking the content header here since we are trying to
65             # to a cross domain request ... all sorts of things may have
66             # happened to the data since this
67 2         8 $data= eval { decode_json($self->param('_ScriptTransport_data')) };
  2         86  
68              
69 2 100       2236 if ($@) {
70 1         11 my $error = "Invalid json string: " . $@;
71 1         35 $log->error($error);
72 1         24 $self->render(text => $error, status=>500);
73 1         543 return;
74             };
75              
76 1         7 $self->requestId($self->param('_ScriptTransport_id')) ;
77 1         81 $self->crossDomain(1);
78 1         8 last;
79             };
80              
81 1         4 my $error = "request must be POST or GET. Can't handle '".$self->req->method."'";
82 1         19 $log->error($error);
83 1         19 $self->render(text => $error, status=>500);
84 1         423 return;
85             }
86 12 100       38 if (not defined $self->requestId){
87 1         8 my $error = "Missing 'id' property in JsonRPC request.";
88 1         6 $log->error($error);
89 1         38 $self->render(text => $error, status=>500);
90 1         363 return;
91             }
92              
93              
94             # Check if service is property is available
95 11 100       77 my $service = $data->{service} or do {
96 1         2 my $error = "Missing service property in JsonRPC request.";
97 1         5 $log->error($error);
98 1         11 $self->render(text => $error, status=>500);
99 1         365 return;
100             };
101              
102             # Check if method is specified in the request
103 10 100       37 my $method = $data->{method} or do {
104 1         2 my $error = "Missing method property in JsonRPC request.";
105 1         39 $log->error($error);
106 1         17 $self->render(text => $error, status=>500);
107 1         365 return;
108             };
109 9         47 $self->methodName($method);
110              
111 9   100     97 $self->rpcParams($data->{params} // []);
112            
113             # invocation of method in class according to request
114 9         66 my $reply = eval {
115             # make sure there are not foreign signal handlers
116             # messing with our problems
117 9         39 local $SIG{__DIE__};
118             # Getting available services from stash
119              
120              
121 9 100       30 die {
122             origin => 1,
123             message => "service $service not available",
124             code=> 2
125             } if not $self->service eq $service;
126              
127 8 50       31 die {
128             origin => 1,
129             message => "your rpc service controller (".ref($self).") must provide an allow_rpc_access method",
130             code=> 2
131             } unless $self->can('allow_rpc_access');
132              
133            
134 8 100       169 die {
135             origin => 1,
136             message => "rpc access to method $method denied",
137             code=> 6
138             } unless $self->allow_rpc_access($method);
139              
140 7 50       27 die {
141             origin => 1,
142             message => "method $method does not exist.",
143             code=> 4
144             } if not $self->can($method);
145              
146 7         143 $self->logRpcCall($method,dclone($self->rpcParams));
147            
148             # reply
149 1     1   9 no strict 'refs';
  1         2  
  1         1222  
150 7         243 return $self->$method(@{$self->rpcParams});
  7         23  
151             };
152 9 100       227 if ($@){
153 3         18 $self->renderJsonRpcError($@);
154             }
155             else {
156 6 100       13 if (eval { $reply->isa('Mojo::Promise') }){
  6         60  
157             $reply->then(
158             sub {
159 1     1   423 my $ret = shift;
160 1         7 $self->renderJsonRpcResult($ret);
161             },
162             sub {
163 1     1   431 my $err = shift;
164 1         8 $self->renderJsonRpcError($err);
165             }
166 2         60 );
167 2         163 $self->render_later;
168             }
169             else {
170             # do NOT render if
171 4 100       16 if (not $self->stash->{'mojo.rendered'}){
172 2         32 $self->renderJsonRpcResult($reply);
173             }
174             }
175             }
176             }
177              
178             sub logRpcCall {
179 7     7 0 356 my $self = shift;
180 7 50       28 if ($self->log->level eq 'debug'){
181 0         0 my $method = shift;
182 0         0 my $request = encode_json(shift);
183 0 0       0 if (not $ENV{MOJO_QX_FULL_RPC_DETAILS}){
184 0 0       0 if (length($request) > 60){
185 0         0 $request = substr($request,0,60) . ' [...]';
186             }
187             }
188 0         0 $self->log->debug("call $method(".$request.")");
189             }
190             }
191              
192             sub renderJsonRpcResult {
193 4     4 0 17 my $self = shift;
194 4         13 my $data = shift;
195 4         26 my $reply = { id => $self->requestId, result => $data };
196 4         188 $self->logRpcReturn(dclone($reply));
197 4         200 $self->finalizeJsonRpcReply(encode_json($reply));
198             }
199              
200             sub logRpcReturn {
201 4     4 0 15 my $self = shift;
202 4 50       21 if ($self->log->level eq 'debug'){
203 0         0 my $debug = encode_json(shift);
204 0 0       0 if (not $ENV{MOJO_QX_FULL_RPC_DETAILS}){
205 0 0       0 if (length($debug) > 60){
206 0         0 $debug = substr($debug,0,60) . ' [...]';
207             }
208             }
209 0         0 $self->log->debug("return ".$debug);
210             }
211             }
212              
213             sub renderJsonRpcError {
214 5     5 0 34 my $self = shift;
215 5         12 my $exception = shift;
216 5         11 my $error;
217 5         20 for (ref $exception){
218 5 50 66     40 /HASH/ && $exception->{message} && do {
219             $error = {
220             origin => $exception->{origin} || 2,
221             message => $exception->{message},
222             code=>$exception->{code}
223 2   50     12 };
224 2         7 last;
225             };
226 3 50 33     75 /.+/ && $exception->can('message') && $exception->can('code') && do {
      33        
227 3         18 $error = {
228             origin => 2,
229             message => $exception->message(),
230             code=>$exception->code()
231             };
232 3         43 last;
233             };
234 0         0 $error = {
235             origin => 2,
236             message => "error while processing ".$self->service."::".$self->methodName.": $exception",
237             code=> 9999
238             };
239             }
240 5         24 $self->log->error("JsonRPC Error $error->{code}: $error->{message}");
241 5         245 $self->finalizeJsonRpcReply(encode_json({ id => $self->requestId, error => $error}));
242             }
243              
244             sub finalizeJsonRpcReply {
245 9     9 0 1074 my $self = shift;
246 9         27 my $reply = shift;
247 9 100       39 if ($self->crossDomain){
248             # for GET requests, qooxdoo expects us to send a javascript method
249             # and to wrap our json a litte bit more
250 1         11 $self->res->headers->content_type('application/javascript; charset=utf-8');
251 1         35 $reply = "qx.io.remote.transport.Script._requestFinished( ".$self->requestId.", " . $reply . ");";
252             } else {
253 8         73 $self->res->headers->content_type('application/json; charset=utf-8');
254             }
255             # the render takes care of encoding the output, so make sure we re-decode
256             # the json stuf
257 9         349 $self->render(text => $self->toUTF8->decode($reply));
258             }
259              
260             sub DESTROY {
261 16     16   8326 local($., $@, $!, $^E, $?);
262 16 50       84 return if ${^GLOBAL_PHASE} eq 'DESTRUCT';
263 16         36 my $self = shift;
264 16         55 $self->log->debug("Destroying ".__PACKAGE__);
265             }
266              
267              
268             1;
269              
270              
271             =head1 NAME
272              
273             Mojolicious::Plugin::Qooxdoo::JsonRpcController - A controller base class for Qooxdoo JSON-RPC Calls
274              
275             =head1 SYNOPSIS
276              
277             # lib/MyApp.pm
278              
279             use base 'Mojolicious';
280            
281             sub startup {
282             my $self = shift;
283            
284             # add a route to the Qooxdoo dispatcher and route to it
285             my $r = $self->routes;
286             $r->route('/RpcService') -> to(
287             controller => 'MyJsonRpcController',
288             action => 'dispatch',
289             );
290             }
291              
292             package MyApp::MyJsonRpcController;
293              
294             use Mojo::Base qw(Mojolicious::Plugin::Qooxdoo::JsonRpcController);
295             use Mojo::Promise;
296              
297             has service => sub { 'Test' };
298            
299             out %allow = ( echo => 1, bad => 1, async => 1);
300              
301             sub allow_rpc_access {
302             my $self = shift;
303             my $method = shift;
304             return $allow{$method};;
305             }
306              
307             sub echo {
308             my $self = shift;
309             my $text = shift;
310             return $text;
311             }
312              
313             sub bad {
314              
315             die MyException->new(code=>1323,message=>'I died');
316              
317             die { code => 1234, message => 'another way to die' };
318             }
319              
320             sub async {
321             my $self=shift;
322             $self->render_later;
323             xyzWithCallback(callback=>sub{
324             eval {
325             local $SIG{__DIE__};
326             $self->renderJsonRpcResult('Late Reply');
327             }
328             if ($@) {
329             $self->renderJsonRpcError($@);
330             }
331             });
332             }
333              
334             sub async_p {
335             my $self=shift;
336             my $p = Mojo::Promise->new;
337             xyzWithCallback(callback => sub {
338             eval {
339             local $SIG{__DIE__};
340             $p->resolve('Late Reply');
341             }
342             if ($@) {
343             $p->reject($@);
344             }
345             });
346             return $p;
347             }
348              
349             package MyException;
350              
351             use Mojo::Base -base;
352             has 'code';
353             has 'message';
354             1;
355              
356             =head1 DESCRIPTION
357              
358             All you have todo to process incoming JSON-RPC requests from a qooxdoo
359             application, is to make your controller a child of
360             L. And then route all
361             incoming requests to the inherited dispatch method in the new controller.
362              
363             If you want your Mojolicious app to also serve the qooxdoo application
364             files, you can use L to have everything setup for you.
365              
366             =head2 Exception processing
367              
368             Errors within the methods of your controller are handled by an eval call,
369             encapsulating the method call. So if you run into trouble, just C. If
370             if you die with a object providing a C and C property or with
371             a hash containing a C and C key, this information will be
372             used to populate the JSON-RPC error object returned to the caller.
373              
374             =head2 Security
375              
376             The C method provided by
377             L calls the C
378             method to check if rpc access should be allowed. The result of this request
379             is NOT cached, so you can use this method to provide dynamic access control
380             or even do initialization tasks that are required before handling each
381             request.
382              
383             =head2 Async Processing
384              
385             If you want to do async data processing, call the C method
386             to let the dispatcher know that it should not bother with trying to render anyting.
387             In the callback, call the C method to render your result. Note
388             that you have to take care of any exceptions in the callback yourself and use
389             the C method to send the exception to the client.
390              
391             =head2 Mojo::Promise Support
392              
393             If your method returns a promise, all will workout as expected. See the example above.
394              
395             =head2 Debugging
396              
397             To see full details of your rpc request and the answers sent back to the
398             browser in your debug log, set the MOJO_QX_FULL_RPC_DETAILS environment
399             variable to 1. Otherwise you will only see the first 60 characters even
400             when logging at debug level.
401              
402             =head1 AUTHOR
403              
404             Smatthias@puffin.chE>,
405             Stobi@oetiker.chE>.
406              
407             This Module is sponsored by OETIKER+PARTNER AG.
408              
409             =head1 COPYRIGHT
410              
411             Copyright (C) 2010,2013
412              
413             =head1 LICENSE
414              
415             This library is free software; you can redistribute it and/or modify
416             it under the same terms as Perl itself, either Perl version 5.8.8 or,
417             at your option, any later version of Perl 5 you may have available.
418              
419             =cut