File Coverage

lib/CallBackery/GuiPlugin/Abstract.pm
Criterion Covered Total %
statement 73 189 38.6
branch 4 68 5.8
condition 2 21 9.5
subroutine 20 34 58.8
pod 12 13 92.3
total 111 325 34.1


line stmt bran cond sub pod time code
1             package CallBackery::GuiPlugin::Abstract;
2 1     1   1009 use strict;
  1         3  
  1         50  
3 1     1   6 use warnings;
  1         3  
  1         61  
4 1     1   5 use Carp qw(carp croak);
  1         2  
  1         73  
5 1     1   8 use Storable qw(dclone);
  1         2  
  1         62  
6 1     1   7 use Data::Dumper;
  1         2  
  1         78  
7 1     1   6 use Mojo::Template;
  1         2  
  1         12  
8 1     1   54 use Mojo::Util qw(monkey_patch);
  1         2  
  1         61  
9 1     1   6 use CallBackery::Exception qw(mkerror);
  1         2  
  1         68  
10 1     1   7 use autodie;
  1         2  
  1         3934  
11 1     1   7469 use Scalar::Util 'blessed';
  1         4  
  1         97  
12 1     1   825 use IPC::Open3;
  1         6936  
  1         170  
13 1     1   11 use POSIX qw;
  1         3  
  1         9  
14 1     1   151 use Time::HiRes qw(usleep);
  1         2  
  1         14  
15 1     1   109 use Mojo::JSON qw(encode_json decode_json true false);
  1         3  
  1         85  
16 1     1   8 use Mojo::File;
  1         3  
  1         57  
17 1     1   8 use Scalar::Util 'weaken';
  1         2  
  1         130  
18             # disable warnings below, otherwise testing will give warnings
19             eval { local $^W=0; require "sys/ioctl.ph" };
20              
21             =head1 NAME
22              
23             CallBackery::GuiPlugin::Abstract - GuiPlugin base class
24              
25             =head1 SYNOPSIS
26              
27             use Mojo::Base 'CallBackery::GuiPlugin::Abstract';
28              
29             =head1 DESCRIPTION
30              
31             The abstract base class for callbackery gui classes.
32              
33             =cut
34              
35 1     1   7 use Mojo::Base -base, -signatures, -async_await;
  1         2  
  1         12  
36              
37              
38             =head1 ATTRIBUTES
39              
40             =head2 config
41              
42             The Plugin instance specific config section from the master config file.
43              
44             =cut
45              
46             has 'config';
47              
48             =head2 name
49              
50             The PLUGIN instance 'name' as specified in the C<*** PLUGIN:... ***> section.
51              
52             =cut
53              
54             has 'name';
55              
56             =head2 user
57              
58             The current user object
59              
60             =cut
61              
62             has 'user';
63              
64              
65             has 'dbHandle' => sub ($self) {
66             $self->user->db;
67             };
68              
69            
70             =head2 tabName
71              
72             What should the tab holding this plugin be called
73              
74             =cut
75              
76             has tabName => sub {
77             return shift->config->{'tab-name'};
78             };
79              
80             =head2 instantiationMode
81              
82             Should the plugin in the webui be instantiated immediately or only when the tab gets selected
83              
84             =cut
85              
86             has instantiationMode => sub {
87             return 'onTabSelection'; # or onStartup
88             };
89              
90             =head2 grammar
91              
92             Returns the L parser for the configuration of this plugin.
93              
94             =cut
95              
96             has grammar => sub ($self) {
97             return {
98             _doc => 'Base class documentation string. Should be overwritten by the child class',
99             _vars => [qw(tab-name)],
100             _mandatory => [qw(tab-name)],
101             'tab-name' => {
102             _doc => 'Title of the Plugin Tab'
103             },
104             };
105             };
106              
107             =head2 schema
108              
109             A very simple minded grammar to json-schema convertor with no magic.
110             Better supply a proper schema.
111              
112             =cut
113              
114             has schema => sub {
115             my $self = shift;
116             my $grammar = $self->grammar;
117             return {
118             type => 'object',
119             properties => {
120             module => {
121             type => 'string'
122             },
123             unlisted => {
124             type => 'boolean'
125             },
126             map {
127             $_ => {
128             type => 'string',
129             $grammar->{$_}{_doc} ?
130             ( description => $grammar->{$_}{_doc} ) : (),
131             $grammar->{$_}{_re} ?
132             ( pattern => $grammar->{$_}{_re} ) : (),
133             $grammar->{$_}{_default} ?
134             ( default => $grammar->{$_}{_default} ) : (),
135             }
136             } @{$grammar->{_vars}},
137             map {
138             $_ => {
139             type => 'object',
140             $grammar->{$_}{_doc} ?
141             ( description => $grammar->{$_}{_doc} ) : (),
142             }
143             } @{$grammar->{_sections}},
144             },
145             required => [
146             'module',
147             ( $grammar->{_mandatory} ? (
148             @{$grammar->{_mandatory}} ) : ()
149             )
150             ],
151             additionalProperties => false
152             }
153             };
154              
155             =head2 controller
156              
157             the current controller
158              
159             =cut
160              
161             has controller => sub ($self) {
162             return $self->user->controller if $self->user;
163             };
164              
165             =head2 app
166              
167             the app object
168              
169             =cut
170              
171             has app => sub ($self) {
172             return $self->user->app if $self->user;
173             }, weak => 1;
174              
175             =head2 log
176              
177             the log object
178              
179             =cut
180              
181             has log => sub ($self) {
182             return $self->controller->log if $self->controller;
183             return $self->app->log if $self->app
184             };
185              
186             =head2 args
187              
188             some meta information provided when instantiating the plugin.
189             for example when buidling the response to getUserConfig, args will contain the output of getUrlConfig from the frontend in the key urlConfig, which will allow to pass information from the url to calls like checkAccess.
190              
191             =cut
192              
193             has 'args' => sub { {} };
194              
195             =head2 screenCfg
196              
197             Returns the information for building a plugin configuration screen.
198              
199             =cut
200              
201             has screenCfg => sub {
202             return {
203             type => '*unknown*',
204             options => {},
205             # followed by type dependent keys
206             }
207             };
208              
209             =head2 checkAccess()
210              
211             Check if the current user may access the Plugin. Override in the Child
212             class to limit accessibility. By default plugins are not accessible
213             unless you have numeric UID or the word C<__CONFIG>.
214              
215             The L sets the userId to C<__SHELL>. If a
216             plugin should be configurable interactively it must allow access to
217             the C<__SHELL> user.
218              
219             checkAccess can also return a promise or be an async method
220              
221             =cut
222              
223             has checkAccess => sub {
224             my $self = shift;
225             my $userId = $self->user->userId;
226             return (defined $userId and ($userId eq '__CONFIG' or $userId =~ /^\d+$/));
227             };
228              
229             =head2 mayAnonymous
230              
231             may this gui plugin run for unauthenticated users ?
232              
233             =cut
234              
235             has mayAnonymous => sub {
236             return 0;
237             };
238              
239             =head2 stateFiles
240              
241             A list of files that contain the state of the settings configured by
242             this plugin this is used both for backup purposes and to replicate the
243             settings to a second installation.
244              
245             =cut
246              
247             has stateFiles => sub {
248             [];
249             };
250              
251             =head2 unconfigureFiles
252              
253             a list of files to be removed when 'unConfiguring' a device
254              
255             =cut
256              
257             has unConfigureFiles => sub {
258             [];
259             };
260              
261             =head2 eventActions
262              
263             A map of callbacks that will be called according to events in the
264             system. The following events are available:
265              
266             configChanged
267              
268             =cut
269              
270             has eventActions => sub {
271             {};
272             };
273              
274             =head1 METHODS
275              
276             All the methods of L plus:
277              
278             =cut
279              
280              
281             =head2 makeRxValidator(rx,error)
282              
283             Create a regular expression base validator function. The supplied
284             regular expression gets anchored front and back automatically.
285              
286             =cut
287              
288             sub createRxValidator {
289 0     0 0 0 my $self = shift;
290 0         0 my $rx = shift;
291 0         0 my $error = shift;
292             return sub {
293 0     0   0 my $value = shift;
294 0 0       0 return undef if $value =~ /^${rx}$/;
295 0         0 return $error;
296 0         0 };
297             }
298              
299             =head2 filterHashKey(data,key)
300              
301             Walks a hash/array structure and removes all occurrences of the given
302             key.
303              
304             CODE references get turned into 'true' values and JSON true/false get
305             passed on.
306              
307             =cut
308              
309             sub filterHashKey {
310 0     0 1 0 my $self = shift;
311 0         0 my $data = shift;
312 0         0 my $filterKey = shift;
313 0         0 my $ref = ref $data;
314 0 0 0     0 if (not $ref
    0 0        
    0          
    0          
315             or $ref eq ref true
316             or $ref eq 'CallBackery::Translate'){
317 0         0 return $data;
318             }
319             elsif ($ref eq 'CODE'){
320 0         0 return true;
321             }
322             elsif ($ref eq 'ARRAY'){
323 0         0 return [ map { $self->filterHashKey($_,$filterKey) } @$data ];
  0         0  
324             }
325             elsif ($ref eq 'HASH'){
326             return {
327             map {
328 0         0 $_ ne $filterKey
329 0 0       0 ? ( $_ => $self->filterHashKey($data->{$_},$filterKey) )
330             : ();
331             } keys %$data
332             }
333             }
334 0         0 return undef;
335             }
336              
337             =head2 processData(arguments)
338              
339             Take the data from the plug-in screen and process them.
340              
341             =cut
342              
343             sub processData {
344 0     0 1 0 my $self = shift;
345 0         0 warn "Processing ".Dumper(\@_);
346             }
347              
348             =head2 getData(arguments)
349              
350             Receive current data for plug-in screen content.
351              
352             =cut
353              
354       0 1   sub getData {
355             }
356              
357             =head2 reConfigure
358              
359             Re-generate all configuration that does not require direct user
360             input. This function may be called from within action handlers to
361             apply newly acquired data to to the running system.
362              
363             =cut
364              
365       0 1   sub reConfigure {
366             }
367              
368             =head2 validateData(arguments)
369              
370             Validate user supplied data prior to acting on it.
371              
372             =cut
373              
374       0 1   sub validateData {
375             }
376              
377             =head2 mergeGrammar
378              
379             A very simpleminded grammar merger with no recursion. For identical
380             keys, the later instance wins.
381              
382             =cut
383              
384             sub mergeGrammar {
385 1     1 1 3 my $self = shift;
386 1         152 my $grammar = dclone shift;
387 1         4 my $newGrammar = shift;
388 1         6 for my $key (keys %$newGrammar){
389 4         9 my $existing = $grammar->{$key};
390 4   50     14 my $ref = ref $existing // 'NONE';
391 4 100       12 $ref eq 'ARRAY' && do {
392 1         3 push @$existing, @{$newGrammar->{$key}};
  1         4  
393 1         4 next;
394             };
395 3 50       10 $ref eq 'HASH' && do {
396 0         0 for my $subKey (keys %{$newGrammar->{$key}}) {
  0         0  
397 0         0 $existing->{$subKey} = $newGrammar->{$key}{$subKey};
398             };
399 0         0 next;
400             };
401 3         9 $grammar->{$key} = $newGrammar->{$key};
402             }
403 1         12 return $grammar;
404             }
405              
406             =head2 varCompiler
407              
408             Returns a compiler sub reference for use in configuration variables or
409             _text sections with perl syntax. The resulting sub will provide access
410             to a hash called $variableName.
411              
412             =cut
413              
414             sub varCompiler {
415 0     0 1 0 my $self = shift;
416             return sub {
417 0   0 0   0 my $code = $_[0] // '';
418             # check and modify content in place
419 0         0 my $perl = 'sub {'.$code.'}';
420 0         0 my $sub = eval $perl; ## no critic (ProhibitStringyEval)
421 0 0       0 if ($@){
422 0         0 return "Failed to compile $code: $@ ";
423             }
424 0         0 eval { $sub->({}) };
  0         0  
425 0 0       0 if ($@){
426 0         0 return "Failed to run $code: $@ ";
427             }
428             # MODIFY the calling argument
429 0         0 $_[0] = $sub;
430 0         0 return;
431 0         0 };
432             }
433              
434             =head2 massageConfig($cfg)
435              
436             Allow the plugin to 'massage' the config hash ... doing this requires
437             deep knowledge of the cfg structure ...
438              
439             =cut
440              
441             sub massageConfig {
442 0     0 1 0 my $self = shift;
443 0         0 my $cfg = shift;
444             }
445              
446             =head2 renderTemplate(template,destination)
447              
448             Render the given template and write the result into the given
449             file. These templates support the L language enhanced
450             by the command C which looks up values from the
451             config database. The convention is that each plugin writes data in
452             it's own namespace.
453              
454             If the destination already exists, the method compares the current
455             content with the new one. It will only update the file if the content
456             differs.
457              
458             The method returns 0 when there was no change and 1 when a new version
459             of the file was written.
460              
461             These additional commands are available to the templates.
462              
463             =over
464              
465             =item *
466              
467             slurp(file)
468              
469             =back
470              
471             =cut
472              
473             has cfgHash => sub {
474             my $self = shift;
475             return $self->app->config->cfgHash;
476             }, weak => 1;
477              
478             has template => sub {
479             my $self = shift;
480             my $mt = Mojo::Template->new();
481             $self->dbHandle;
482             my $dbLookup = sub { $self->getConfigValue(@_) // ''};
483            
484             # don't use L, use dbLookup instead
485             monkey_patch $mt->namespace,
486             L => $dbLookup;
487              
488             monkey_patch $mt->namespace,
489             dbLookup => $dbLookup;
490              
491             monkey_patch $mt->namespace,
492             app => sub { $self->app };
493              
494             monkey_patch $mt->namespace,
495             slurp => sub {
496             my $filename = shift;
497             return Mojo::File->new($filename)->slurp;
498             };
499             monkey_patch $mt->namespace,
500             cfgHash => sub { $self->cfgHash };
501              
502             monkey_patch $mt->namespace,
503             pluginCfg => sub { my $instance = shift;
504             my $cfg = $self->cfgHash->{PLUGIN}{prototype}{$instance}->config;
505             weaken $cfg;
506             return $cfg;
507             };
508             return $mt;
509             };
510              
511              
512             has homeDir => sub {
513             [getpwuid $>]->[7];
514             };
515              
516             sub renderTemplate{
517 0     0 1 0 my $self = shift;
518 0         0 my $template = shift;
519 0         0 my $destination = Mojo::File->new(shift);
520 0         0 $self->log->debug('['.$self->name.'] processing template '.$template);
521 0         0 my $newData = $self->template->render($self->app->home->rel_file('templates/system/'.$template)->slurp);
522 0 0       0 if (-r $destination){
523 0         0 my $oldData = Mojo::File->new($destination)->slurp;
524 0 0       0 if ($newData eq $oldData){
525 0         0 return 0
526             }
527             }
528 0         0 my $dir = $destination->dirname;
529 0 0       0 if (not -d $dir){
530 0         0 Mojo::File->new($dir)->make_path({mode => 755});
531             }
532              
533 0         0 $self->log->debug('['.$self->name."] writing $destination\n$newData");
534 0         0 eval {
535 0         0 local $SIG{__DIE__};
536 0         0 $destination->spew($newData);
537             };
538 0 0       0 if ($@){
539 0 0 0     0 if (blessed $@ and $@->isa('autodie::exception')){
540 0         0 $self->log->error('['.$self->name."] writing $template -> $destination: ".$@->errno);
541             }
542             else {
543 0         0 die $@;
544             }
545             }
546 0 0 0     0 if ($self->controller and $self->controller->can('runEventActions')){
547 0         0 $self->controller->runEventActions('changeConfig');
548             }
549 0         0 return 1;
550             }
551              
552             =head2 getConfigValue(key)
553              
554             Read a config value from the database.
555              
556             =cut
557              
558             sub getConfigValue {
559 0     0 1 0 my $self = shift;
560 0         0 my $key = shift;
561 0         0 my $value = $self->dbHandle->getConfigValue($key);
562 0 0       0 return undef if not defined $value;
563 0         0 my $ret = eval { decode_json($value) };
  0         0  
564             # warn "GET $key -> ".Dumper($ret);
565 0 0       0 if ($@){
566 0         0 die mkerror (3984,$@);
567             }
568 0         0 return $ret->[0];
569             }
570              
571             =head2 setConfigValue(key)
572              
573             Save a config value to the database.
574              
575             =cut
576              
577             sub setConfigValue {
578 0     0 1 0 my $self = shift;
579 0         0 my $key = shift;
580 0         0 my $value = shift;
581             # warn "SET $key -> ".Dumper([$value]);
582 0         0 $self->dbHandle->setConfigValue($key,encode_json([$value]));
583 0 0       0 if ($self->controller->can('runEventActions')){
584 0         0 $self->controller->runEventActions('changeConfig');
585             }
586 0         0 return $value;
587             }
588              
589             =head2 systemNoFd(args)
590              
591             A version of the system function that makes sure to NOT to inherit any
592             extra filehandles to the kids and sends the output of the call system
593             log file. I would suggest to use this in preference to the normal
594             system function. Especially when launching daemons since Mojo seems to
595             fiddle with $^F and will thus inherit open sockets to child processes.
596              
597             If the binary name starts with -, the output will be ignored ... this
598             can be necessary for programs starting daemons that do not close their
599             output. Otherwhise you will read the output of the daemon and NOT
600             terminate. We are also using kill 0 to check if the process is still
601             active.
602              
603             =cut
604              
605             sub systemNoFd {
606 0     0 1 0 my $self = shift;
607 0         0 my $binary = shift;
608 0         0 my $logoutput = 1;
609 0 0       0 if ($binary =~ s/^-//){
610 0         0 $logoutput = 0;
611             }
612 0         0 my $rdr;
613             my $wtr;
614              
615             # make sure there is no inheriting any sockets
616             # mojo should actually take care of this
617 0         0 for my $path (glob '/proc/self/fd/*'){
618 1     1   7560 no autodie;
  1         2  
  1         15  
619 0 0       0 my ($fd) = $path =~ m{/proc/self/fd/(\d+)} or next;
620 0 0       0 $fd > 3 or next;
621 0 0       0 my $link = readlink $path or next;
622 0 0       0 $link =~ /socket/ or next;
623 0 0       0 if (open my $fh, q{>&=}, int($fd)){
624             # $self->log->debug("Setting FIOCLEX on fd $fd ($link)");
625 0 0       0 if (defined &FIOCLEX){
    0          
626 0         0 ioctl $fh, FIOCLEX(),0;
627             }
628             elsif ($^O eq 'linux'){
629             # it seems we did not load the ioctl headers ...
630             # let's try this blindly since we are on linux after all
631 0         0 ioctl $fh, 21585, 0;
632             }
633             else {
634 0         0 die "investigate this (FD_CLOEXEC) since it should work but does not!";
635 0         0 fcntl($fh, F_SETFD, FD_CLOEXEC);
636             }
637             }
638             }
639 0         0 my $pid = eval {
640 0         0 open3($wtr, $rdr, undef,$binary,@_);
641             };
642 0         0 my $args = join " ",@_;
643 0 0       0 if ($@){
644 0         0 $self->log->warn("exec '$binary $args' failed: $!");
645             }
646             else {
647 0         0 $self->log->debug("running $binary($pid) $args");
648 0 0       0 if ($logoutput){
649 0         0 while (my $line = <$rdr>){
650 0         0 $line =~ s/[\r\n]//g;
651 0         0 $self->log->debug("$binary($pid) out: $line");
652 0         0 usleep 200; # give the process a chance to quit
653 0 0       0 last if not kill 0,$pid; # dead yet?
654             }
655             }
656 0         0 my $ret = waitpid( $pid, 0 );
657 0         0 $self->log->debug("running $binary($pid) done $ret");
658 0         0 return $ret;
659             }
660 0         0 return undef;
661             }
662              
663 4     4   3118 sub DESTROY ($self) {
  4         8  
  4         7  
664             # we are only interested in objects that get destroyed during
665             # global destruction as this is a potential problem
666 4   50     16 my $class = ref($self) // "child of ". __PACKAGE__;
667 4 50       16 if (${^GLOBAL_PHASE} ne 'DESTRUCT') {
668             # $self->log->debug($class." DESTROYed");
669 4         270 return;
670             }
671 0 0 0       if (blessed $self && ref $self->log){
672             $self->log->debug("late destruction of $class object during global destruction")
673 0 0         unless $self->{prototype};
674 0           return;
675             }
676             warn "extra late destruction of $class object during global destruction\n"
677 0 0         unless $self->{prototype};
678             }
679              
680             1;
681             __END__