File Coverage

lib/Mojolicious/Plugin/Qooxdoo/JsonRpcController.pm
Criterion Covered Total %
statement 126 142 88.7
branch 34 48 70.8
condition 9 15 60.0
subroutine 16 17 94.1
pod 0 6 0.0
total 185 228 81.1


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