File Coverage

lib/CallBackery/Controller/RpcService.pm
Criterion Covered Total %
statement 80 211 37.9
branch 21 60 35.0
condition 3 9 33.3
subroutine 14 30 46.6
pod 16 21 76.1
total 134 331 40.4


line stmt bran cond sub pod time code
1             package CallBackery::Controller::RpcService;
2              
3 1         7 use Mojo::Base qw(Mojolicious::Plugin::Qooxdoo::JsonRpcController),
4 1     1   878176 -signatures,-async_await;
  1         2  
5 1     1   24553 use CallBackery::Exception qw(mkerror);
  1         2  
  1         75  
6 1     1   520 use CallBackery::Translate qw(trm);
  1         2  
  1         72  
7 1     1   6 use Mojo::JSON qw(encode_json decode_json from_json);
  1         1  
  1         47  
8 1     1   640 use Syntax::Keyword::Try;
  1         1351  
  1         5  
9 1     1   104 use Scalar::Util qw(blessed weaken);
  1         1  
  1         6016  
10              
11             =head1 NAME
12              
13             CallBackery::RpcService - RPC services for CallBackery
14              
15             =head1 SYNOPSIS
16              
17             This module gets instantiated by L and provides backend
18             functionality.
19              
20             =head1 DESCRIPTION
21              
22             This module provides the following methods
23              
24             =cut
25              
26             # the name of the service we provide
27             has service => 'default';
28              
29             =head2 allow_rpc_access(method)
30              
31             is this method accessible?
32              
33             =cut
34              
35             my %allow = (
36             getBaseConfig => 1,
37             login => 1,
38             logout => 1,
39             ping => 1,
40             getUserConfig => 2,
41             getPluginConfig => 3,
42             validatePluginData => 3,
43             processPluginData => 3,
44             getPluginData => 3,
45             getSessionCookie => 2
46             );
47              
48             has config => sub ($self) {
49             $self->app->config;
50             }, weak => 1;
51              
52             has user => sub ($self) {
53             my $obj = $self->app->userObject->new(app=>$self->app,controller=>$self,log=>$self->log);
54             return $obj;
55             };
56              
57             has pluginMap => sub ($self) {
58             my $map = $self->config->cfgHash->{PLUGIN};
59             return $map;
60             }, weak => 1;
61              
62              
63 2     2 1 10718 sub allow_rpc_access ($self,$method) {
  2         6  
  2         4  
  2         5  
64 2 50       175 if (not $self->req->method eq 'POST') {
65             # sorry we do not allow GET requests
66 0         0 $self->log->error("refused ".$self->req->method." request");
67 0         0 return 0;
68             }
69 2 50       68 if (not exists $allow{$method}){
70 0         0 return 0;
71             }
72 2         7 for ($allow{$method}){
73 2 50       17 /1/ && return 1;
74 0 0       0 return 1 if ($self->user->isUserAuthenticated);
75 0 0       0 /3/ && do {
76 0         0 my $plugin = $self->rpcParams->[0];
77 0 0       0 if ($self->config->instantiatePlugin($plugin,$self->user)->mayAnonymous){
78 0         0 return 1;
79             }
80             };
81             }
82 0         0 return 0;
83             };
84              
85             has passMatch => sub ($self) {
86             qr{(?i)(?:password|_pass)};
87             };
88              
89 5     5 0 11 sub perMethodCleaner ($self,$method=undef) {
  5         8  
  5         14  
  5         11  
90 5 100       26 $method or return;
91             return {
92             login => sub {
93 1     1   4 my $data = shift;
94 1 50       5 if (ref $data eq 'ARRAY'){
95 1         3 $data->[1] = 'xxx';
96             }
97 1         3 return;
98             }
99 2         22 }->{$method};
100             };
101              
102 5     5 0 9881 sub dataCleaner ($self,$data,$method=undef) {
  5         12  
  5         9  
  5         13  
  5         10  
103 5 100       20 if (my $perMethodCleaner = $self->perMethodCleaner($method)){
104 1         5 return $perMethodCleaner->($data);
105             }
106              
107 4         24 my $match = $self->passMatch;
108 4         26 my $type = ref $data;
109 4         13 for ($type) {
110 4 100       18 /ARRAY/ && do {
111 1         4 $self->dataCleaner($_) for @$data;
112             };
113 4 100       21 /HASH/ && do {
114 3         12 for my $key (keys %$data) {
115 4         13 my $value = $data->{$key};
116 4 100       70 if ($key =~ /$match/){
    100          
117 1         9 $data->{$key} = 'xxx';
118             }
119             elsif (ref $value){
120 1         6 $self->dataCleaner($value);
121             }
122             }
123             }
124             }
125             }
126              
127             =head2 logRpcCall
128              
129             Set CALLBACKERY_RPC_LOG for extensive logging messages. Note that all
130             values with keys matching /password|_pass/ do get replaced with 'xxx'
131             in the output.
132              
133             =cut
134              
135             # our own logging
136             sub logRpcCall {
137 2     2 1 251 my $self = shift;
138 2 100       14 if ($ENV{CALLBACKERY_RPC_LOG}){
139 1         4 my $method = shift;
140 1         3 my $data = shift;
141 1         7 $self->dataCleaner($data,$method);
142 1   50     3 my $userId = eval { $self->user->loginName } // '*UNKNOWN*';
  1         5  
143 1         9 my $remoteAddr = $self->tx->remote_address;
144 1         36 $self->log->debug("[$userId|$remoteAddr] CALL $method(".encode_json($data).")");
145             }
146             else {
147 1         13 $self->SUPER::logRpcCall(@_);
148             }
149             }
150              
151             =head2 logRpcReturn
152              
153             Set CALLBACKERY_RPC_LOG for extensive logging messages. Note that all
154             values with keys matching /password|_pass/ do get replaced with 'xxx'
155             in the output.
156              
157             =cut
158              
159             # our own logging
160             sub logRpcReturn {
161 2     2 1 233 my $self = shift;
162 2 100       13 if ($ENV{CALLBACKERY_RPC_LOG}){
163 1         2 my $data = shift;
164 1         7 $self->dataCleaner($data);
165 1   50     3 my $userId = eval { $self->user->loginName } // '*UNKNOWN*';
  1         5  
166 1         727 my $remoteAddr = $self->tx->remote_address;
167 1         34 $self->log->debug("[$userId|$remoteAddr] RETURN ".encode_json($data).")");
168             }
169             else {
170 1         10 $self->SUPER::logRpcReturn(@_);
171             }
172              
173             }
174              
175             =head2 ping()
176              
177             check if the server is happy with our authentication state
178              
179             =cut
180              
181             sub ping {
182 2     2 1 140 return 'pong';
183             }
184              
185             =head2 getSessionCookie()
186              
187             Return a timeestamped session cookie. For use in the X-Session-Cookie header or as a xsc field
188             in form submissions. Note that session cookies for form submissions are only valid for 2 seconds.
189             So you have to get a fresh one from the server before submitting your form.
190              
191             =cut
192              
193             sub getSessionCookie {
194 0     0 1 0 shift->user->makeSessionCookie();
195             }
196              
197             =head2 getConfig()
198              
199             get some gloabal configuration information into the interface
200              
201             =cut
202              
203             sub getBaseConfig {
204 0     0 0 0 my $self = shift;
205 0         0 return $self->config->cfgHash->{FRONTEND};
206             }
207              
208             =head2 login(user,password)
209              
210             Check login and provide the user specific interface configuration as a response.
211              
212             =cut
213              
214 0     0 1 0 async sub login { ## no critic (RequireArgUnpacking)
215 0         0 my $self = shift;
216 0         0 my $login = shift;
217 0         0 my $password = shift;
218 0         0 my $cfg = $self->config->cfgHash->{BACKEND};
219 0 0       0 if (my $ok =
220             await $self->config->promisify($self->user->login($login,$password))){
221             return {
222 0         0 sessionCookie => $self->user->makeSessionCookie()
223             }
224             } else {
225 0         0 return;
226             }
227             }
228              
229             =head2 logout
230              
231             Kill the session.
232              
233             =cut
234              
235             sub logout {
236 0     0 1 0 my $self = shift;
237 0         0 $self->session(expires=>1);
238 0         0 return 'http://youtu.be/KGsTNugVctI';
239             }
240              
241              
242              
243             =head2 instantiatePlugin_p
244              
245             get an instance for the given plugin
246              
247             =cut
248              
249 0     0 1 0 async sub instantiatePlugin_p {
250 0         0 my $self = shift;
251 0         0 my $name = shift;
252 0         0 my $args = shift;
253 0         0 my $user = $self->user;
254 0         0 my $plugin = await $self->config->instantiatePlugin_p($name,$user,$args);
255 0         0 $plugin->log($self->log);
256 0         0 return $plugin;
257             }
258              
259             sub instantiatePlugin {
260 0     0 0 0 my $self = shift;
261 0         0 my $name = shift;
262 0         0 my $args = shift;
263 0         0 my $user = $self->user;
264 0         0 my $plugin = $self->config->instantiatePlugin($name,$user,$args);
265 0         0 $plugin->log($self->log);
266 0         0 return $plugin;
267             }
268              
269             =head2 processPluginData(plugin,args)
270              
271             handle form sumissions
272              
273             =cut
274              
275 0     0 1 0 async sub processPluginData {
276 0         0 my $self = shift;
277 0         0 my $plugin = shift;
278             # "Localizing" required as it seems to be changed somewhere.
279 0         0 my @args = @_;
280             # Creating two statements will make things easier to debug since
281             # there is only one thing that can go wrong per line.
282 0         0 my $instance = await $self->instantiatePlugin_p($plugin);
283 0         0 return $instance->processData(@args);
284             }
285              
286             =head2 validateField(plugin,args)
287              
288             validate the content of the given field for the given plugin
289              
290             =cut
291              
292 0     0 0 0 async sub validatePluginData {
293 0         0 my $self = shift;
294 0         0 my $plugin = shift;
295             # "Localizing" required as it seems to be changed somewhere.
296 0         0 my @args = @_;
297 0         0 return (await $self->instantiatePlugin_p($plugin))
298             ->validateData(@args);
299             }
300              
301             =head2 getPluginData(plugin,args);
302              
303             return the current value for the given field
304              
305             =cut
306              
307 0     0 1 0 async sub getPluginData {
308 0         0 my $self = shift;
309 0         0 my $plugin = shift;
310             # "Localizing" required as it seems to be changed somewhere.
311 0         0 my @args = @_;
312 0         0 return (await $self->instantiatePlugin_p($plugin))
313             ->getData(@args);
314             }
315              
316              
317             =head2 getUserConfig
318              
319             returns user specific configuration information
320              
321             =cut
322              
323              
324 0     0 1 0 async sub getUserConfig {
325 0         0 my $self = shift;
326 0         0 my $args = shift;
327 0         0 my @plugins;
328 0         0 for my $plugin ($self->pluginMap->{list}->@*){
329             try {
330             my $obj = await $self->instantiatePlugin_p($plugin,$args);
331             #$obj = $self->instantiatePlugin($plugin,$args);
332             # $self->log->debug("instanciate $plugin");
333             push @plugins, {
334             tabName => $obj->tabName,
335             name => $obj->name,
336             instantiationMode => $obj->instantiationMode
337             };
338 0         0 } catch ($error) {
339             warn "$error";
340             }
341             }
342 0         0 my $userConfig = await $self->config->promisify($self->user->userInfo);
343             return {
344 0         0 userInfo => $userConfig,
345             plugins => \@plugins,
346             };
347             }
348              
349             =head2 getPluginConfig(plugin,args)
350              
351             Returns a plugin configuration removing all the 'back end' keys and
352             non ARRAY or HASH references in the process.
353              
354             =cut
355              
356 0     0 1 0 async sub getPluginConfig {
357 0         0 my $self = shift;
358 0         0 my $plugin = shift;
359 0         0 my $args = shift;
360 0         0 my $obj = await $self->instantiatePlugin_p($plugin,$args);
361 0         0 return $obj->filterHashKey($obj->screenCfg,'backend');
362             }
363              
364             =head2 runEventActions(event[,args])
365              
366             Call the eventAction handlers of all configured plugins. Currently the
367             following events are known:
368              
369             changeConfig
370              
371             =cut
372              
373             sub runEventActions {
374 0     0 1 0 my $self = shift;
375 0         0 my $event = shift;
376 0         0 my @args = @_;
377 0         0 for my $obj (@{$self->config->configPlugins}){
  0         0  
378 0 0       0 if (my $action = $obj->eventActions->{$event}){
379 0         0 $action->(@args)
380             }
381             }
382             }
383              
384             =head2 setPreDestroyAction(key,callback);
385              
386             This can be used to have tasks completed at the end of a webtransaction since
387             the controller gets instantiated per transaction.
388             An example application would be backing up the configuration changes only
389             once even if more than one configChange event has occured.
390              
391             =cut
392              
393             my $runPreDestroyActions = sub {
394             my $self = shift;
395             my $actions = $self->{preDestroyActions} // {};
396             $self->log->debug('destroying controller');
397             for my $key (keys %$actions){
398             $self->log->debug('running preDestroyAction '.$key);
399             eval {
400             $actions->{$key}->();
401             };
402             if ($@){
403             $self->log->error("preDestoryAction $key: ".$@);
404             }
405             # and thus hopefully releasing the controller
406             delete $actions->{$key};
407             }
408             delete $self->{preDestroyActions}
409             };
410              
411             sub setPreDestroyAction {
412 0     0 1 0 my $self = shift;
413 0         0 my $key = shift;
414 0         0 my $cb = shift;
415 0 0       0 if (not $self->{preDestroyActions}){
416             # we want to run these pretty soon, basically as soon as
417             # controll returns to the ioloop
418 0     0   0 Mojo::IOLoop->timer("0.2" => sub{ $self->$runPreDestroyActions });
  0         0  
419             }
420 0         0 $self->{preDestroyActions}{$key} = $cb;
421             }
422              
423             =head2 handleUpload
424              
425             Process incoming upload request. This is getting called via a route and NOT
426             in the usual way, hence we have to render our own response!
427              
428             =cut
429              
430              
431 0     0 1 0 async sub handleUpload {
432 0         0 my $self = shift;
433 0         0 $self->render_later;
434 0 0       0 if (not $self->user->isUserAuthenticated){
435 0         0 return $self->render(json => {exception=>{
436             message=>trm('Access Denied'),code=>4922}});
437             }
438 0         0 my $name = $self->req->param('name');
439 0 0       0 if (not $name){
440 0         0 return $self->render(json => {exception=>{
441             message=>trm('Plugin Name missing'),code=>3934}});
442             }
443              
444 0         0 my $upload = $self->req->upload('file');
445 0 0       0 if (not $upload){
446 0         0 return $self->render(json => {exception=>{
447             message=>trm('Upload Missing'),code=>9384}});
448             }
449 0         0 my $obj = await $self->instantiatePlugin_p($name);
450              
451 0         0 my $form;
452 0 0       0 if (my $formData = $self->req->param('formData')){
453 0         0 $form = eval { decode_json($formData) };
  0         0  
454 0 0       0 if ($@){
455 0         0 return $self->render(json=>{exception=>{
456             message=>trm('Data Decoding Problem %1',$@),code=>7932}});
457             }
458             }
459 0         0 $form->{uploadObj} = $upload;
460              
461 0         0 my $return;
462             try {
463             $return = await $self->config->promisify($obj->processData({
464             key => $self->req->param('key'),
465             formData => $form,
466             }));
467 0         0 } catch ($error) {
468             if (blessed $error){
469             if ($error->isa('CallBackery::Exception')){
470             return $self->render(json=>{exception=>{
471             message=>$error->message,code=>$error->code}});
472             }
473             elsif ($error->isa('Mojo::Exception')){
474             return $self->render(json=>{exception=>{message=>$error->message,code=>9999}});
475             }
476             }
477             return $self->render(json=>{exception=>{message=>$error,code=>9999}});
478              
479             }
480 0         0 return $self->render(json=>$return);
481             }
482              
483             =head2 handleDownload
484              
485             Process incoming download request. The handler expects two parameters: name
486             for the plugin instance and formData for the data of the webform.
487              
488             The handler getting the request must return a hash with the following elements:
489              
490             =over
491              
492             =item filename
493              
494             name of the download when saved
495              
496             =item type
497              
498             mime-type of the download
499              
500             =item asset
501              
502             a L. eg Cnew(path => '/etc/passwd')>.
503              
504             =back
505              
506             =cut
507              
508 0     0 1 0 async sub handleDownload {
509 0         0 my $self = shift;
510              
511 0 0       0 if (not $self->user->isUserAuthenticated){
512 0         0 return $self->render(json=>{exception=>{
513             message=>trm('Access Denied'),code=>3928}});
514             }
515              
516 0         0 my $name = $self->param('name');
517 0         0 my $key = $self->param('key');
518 0 0       0 if (not $name){
519 0         0 return $self->render(json=>{exception=>{
520             message=>trm('Plugin Name missing'),code=>3923}});
521             }
522 0         0 $self->render_later;
523 0         0 my $obj = await $self->instantiatePlugin_p($name);
524              
525 0         0 my $form;
526 0 0       0 if (my $formData = $self->req->param('formData')){
527 0         0 $form = eval { from_json($formData) };
  0         0  
528 0 0       0 if ($@){
529 0         0 return $self->render(json=>{exception=>{
530             message=>trm('Data Decoding Problem %1',$@),code=>3923}});
531             }
532             }
533 0         0 my $map;
534             try {
535             $map = await $self->config->promisify($obj->processData({
536             key => $key,
537             formData => $form,
538             }));
539 0         0 } catch ($error) {
540             if (blessed $error){
541             if ($error->isa('CallBackery::Exception')){
542             return $self->render(json=>{exception=>{
543             message=>$error->message,code=>$error->code}});
544             }
545             elsif ($error->isa('Mojo::Exception')){
546             return $self->render(json=>{exception=>{
547             message=>$error->message,code=>9999}});
548             }
549             }
550             return $self->render(json=>{exception=>{message=>$error,code=>9999}});
551             }
552              
553 0         0 $self->res->headers->content_type($map->{type}.';name=' .$map->{filename});
554 0 0       0 if (not $self->param('display')) {
555 0         0 $self->res->headers->content_disposition('attachment;filename='.$map->{filename});
556             }
557 0         0 $self->res->content->asset($map->{asset});
558 0         0 $self->rendered(200);
559             }
560              
561 3     3   2991 sub DESTROY ($self) {
  3         9  
  3         6  
562             # we are only interested in objects that get destroyed during
563             # global destruction as this is a potential problem
564 3   50     34 my $class = ref($self) // "child of ". __PACKAGE__;
565 3 50       19 if (${^GLOBAL_PHASE} ne 'DESTRUCT') {
566             # $self->log->debug($class." DESTROYed");
567 3         33 return;
568             }
569 0 0 0       if (blessed $self && ref $self->log){
570 0           $self->log->debug("late destruction of $class object during global destruction");
571 0           return;
572             }
573 0           warn "extra late destruction of $class object during global destruction\n";
574             }
575              
576             1;
577             __END__