File Coverage

lib/CallBackery/Controller/RpcService.pm
Criterion Covered Total %
statement 74 198 37.3
branch 20 58 34.4
condition 2 4 50.0
subroutine 13 28 46.4
pod 16 20 80.0
total 125 308 40.5


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