File Coverage

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


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