File Coverage

blib/lib/WWW/MLite.pm
Criterion Covered Total %
statement 39 221 17.6
branch 0 102 0.0
condition 0 92 0.0
subroutine 13 29 44.8
pod 14 14 100.0
total 66 458 14.4


line stmt bran cond sub pod time code
1             package WWW::MLite; # $Id: MLite.pm 44 2019-05-31 10:06:54Z minus $
2 1     1   68939 use strict;
  1         10  
  1         31  
3 1     1   605 use utf8;
  1         14  
  1         6  
4              
5             =encoding utf-8
6              
7             =head1 NAME
8              
9             WWW::MLite - Lite Web Application Framework
10              
11             =head1 VERSION
12              
13             Version 2.00
14              
15             =head1 SYNOPSIS
16              
17             package MyApp;
18              
19             use base qw/WWW::MLite/;
20              
21             use HTTP::Status qw/:constants/;
22             use Data::Dumper;
23              
24             __PACKAGE__->register_method( # GET /myapp
25             name => "getIndex",
26             description => "Index page",
27             method => "GET",
28             path => "/myapp",
29             deep => 0,
30             attrs => {
31             foo => 'blah-blah-blah',
32             bar => 'on',
33             deserialize => 0,
34             serialize => 1,
35             },
36             requires => undef,
37             returns => undef,
38             code => sub {
39             my $self = shift;
40             my @params = @_;
41              
42             $self->data(Dumper({
43             params => [@params],
44             name => $self->name,
45             description => $self->info("description"),
46             attrs => $self->info("attrs"),
47             path => $self->info("path"),
48             method => $self->info("method"),
49             requires => $self->info("requires"),
50             returns => $self->info("returns"),
51             }));
52              
53             return HTTP_OK; # HTTP RC
54             });
55              
56             1;
57              
58             package main;
59              
60             use FindBin qw/$Bin/;
61             use lib "$Bin/../lib";
62              
63             use CGI;
64             use File::Spec;
65              
66             my $q = new CGI;
67             my $server = MyApp->new(
68             project => "MyApp",
69             ident => "myapp",
70             root => File::Spec->catdir($Bin, "conf"),
71             #confopts => {... Config::General options ...},
72             configfile => File::Spec->catfile($Bin, "conf", "myapp.conf"),
73             log => "on",
74             logfd => fileno(STDERR),
75             #logfile => '/path/to/log/file.log',
76             nph => 0, # NPH (no-parsed-header)
77             );
78             print $server->call($q->request_method, $q->request_uri, $q) or die($server->error);
79              
80             =head1 DESCRIPTION
81              
82             Lite Web Application Framework
83              
84             This module allows you to quickly and easily write a REST servers
85              
86             =head2 new
87              
88             my $server = MyApp->new(
89             project => "MyApp",
90             ident => "myapp",
91             root => File::Spec->catdir($Bin, "conf"),
92             #confopts => {... Config::General options ...},
93             configfile => File::Spec->catfile($Bin, "conf", "myapp.conf"),
94             log => "on",
95             logfd => fileno(STDERR),
96             #logfile => '/path/to/log/file.log',
97             nph => 0, # NPH (no-parsed-header)
98             );
99              
100             Returns CTK object as WWW::MLite server
101              
102             =over 4
103              
104             =item confopts
105              
106             Optional value. L options
107              
108             =item configfile
109              
110             File of configuration
111              
112             Default: /etc/myapp/myapp.conf
113              
114             =item log
115              
116             General switch for logging enable/disable
117              
118             Default: off
119              
120             Also see configuration for logging manage
121              
122             =item logfd
123              
124             File descriptor or fileno
125              
126             Default: none (use syslog)
127              
128             See L
129              
130             =item logfile
131              
132             Log file path. Not recommended!
133              
134             =item nph
135              
136             Enable or disable NPH mode (no-parsed-header)
137              
138             Default: 0
139              
140             See L
141              
142             This option for the response subroutine only!
143              
144             =item root
145              
146             Root directory for project. This is NOT document root directory!
147              
148             Default: /etc/myapp
149              
150             =back
151              
152             See also L and L
153              
154             =head1 METHODS
155              
156             List of available methods
157              
158             =head2 call
159              
160             See L
161              
162             =head2 call_method
163              
164             $server->call_method( $ENV{REQUEST_URI}, $ENV{REQUEST_METHOD}, ... );
165              
166             Runs the callback function from current method with additional parameters
167              
168             Note: any number of parameters can be specified,
169             all of them will be receive in the callback function and in your overridden the response subroutine
170              
171             Returns: response content
172              
173             =head2 check_http_method
174              
175             $server->check_http_method("GET"); # returns 1
176             $server->check_http_method("OPTIONS"); # returns 0
177              
178             Checks the availability of the HTTP method by its name and returns the status
179              
180             =head2 code
181              
182             my $code = $server->code;
183             my $code = $server->code( 500 );
184              
185             Gets/Sets response HTTP code
186              
187             Default: 200 (HTTP_OK)
188              
189             See L
190              
191             =head2 cleanup
192              
193             $server->cleanup;
194              
195             Cleans the all working data and resets it to default values
196              
197             =head2 data
198              
199             my $data = $server->data;
200             $server->data({
201             param1 => "new value",
202             });
203              
204             Gets/Sets working data structure or HTTP content
205              
206             Default: undef
207              
208             See L
209              
210             =head2 head
211              
212             my $head = $server->head;
213             $server->head({
214             "Content-Type" => "text/plain",
215             });
216              
217             Gets/Sets HTTP headers
218              
219             Default: "text/plain"
220              
221             See L
222              
223             =head2 info
224              
225             my $info = $server->info;
226             my $description => $server->info("description");
227             my $attrs = $server->info("attrs");
228             my $path = $server->info("path");
229             my $method = $server>info("method");
230             my $requires = $server->info("requires");
231             my $returns = $server->info("returns");
232              
233             Returns the info structure or info-data of current method
234              
235             =head2 lookup_method
236              
237             my $method = $server->lookup_method($ENV{REQUEST_URI}, $ENV{REQUEST_METHOD});
238              
239             Returns $method structure from hash of registered methods; or undef if method is not registered
240              
241             =head2 message
242              
243             my $message = $server->message;
244             my $message = $server->message( "Internal Server Error" );
245              
246             Gets/Sets response HTTP message
247              
248             Default: "OK"
249              
250             See L
251              
252             =head2 name
253              
254             my $name = $server->name;
255              
256             Returns name of current method. Default: default
257              
258             =head2 register_method
259              
260             use base qw/WWW::MLite/;
261              
262             use HTTP::Status qw/:constants/;
263             use Data::Dumper;
264              
265             __PACKAGE__->register_method( # GET /myapp
266             name => "getIndex",
267             description => "Index page",
268             method => "GET",
269             path => "/myapp",
270             deep => 0,
271             attrs => {
272             foo => 'blah-blah-blah',
273             bar => 'on',
274             deserialize => 0,
275             serialize => 1,
276             },
277             requires => [
278             qw/ user1 user2 userX /
279             ],
280             returns => {
281             type => 'any',
282             },
283             code => sub {
284             my $self = shift;
285             my @params = @_;
286              
287             # ... your method's code here ...
288              
289             return HTTP_OK; # HTTP RC
290             });
291              
292             Registers new method and returns operation status
293              
294             B This is non class method!
295              
296             =over 4
297              
298             =item attrs
299              
300             Sets attributes of the method as hashref
301              
302             Default: {}
303              
304             In the method's code or response method, you can get the attribute values using the $self->info("attrs") method
305              
306             =item code
307              
308             Sets callback function
309              
310             Default: sub { return HTTP::Status::HTTP_OK }
311              
312             This callback function MUST return HTTP status code
313              
314             See L
315              
316             =item deep, depth
317              
318             Enables deeply scanning of path for method lookup. If this param is set to true then the
319             mechanism of the deeply lookuping will be enabled. For example:
320              
321             For registered path /foo with enabled deep lookuping will be matched any another
322             incoming path that begins from /foo prefix: /foo, /foo/bar, /foo/bar/baz and etc.
323              
324             Default: 0
325              
326             =item description
327              
328             Sets the description of method
329              
330             Default: none
331              
332             =item name
333              
334             Sets the name of method
335              
336             Default: default
337              
338             =item method
339              
340             Sets the HTTP method for trapping. Supported: GET, POST, PUT, DELETE.
341              
342             Default: GET
343              
344             =item path
345              
346             Sets the URL's path for trapping
347              
348             Default: /
349              
350             =item requires
351              
352             Array-ref structure that contains list of groups/users or any data for authorization
353              
354             Default: []
355              
356             =item returns
357              
358             Hash-ref structure that contains schema
359              
360             Default: {}
361              
362             See L, L, L
363              
364             =back
365              
366             =head2 response
367              
368             The method for response prepare.
369              
370             You can override this method in Your class.
371              
372             But note! This method MUST returns serialized or plain content for output
373              
374             For examle:
375              
376             sub response {
377             my $self = shift;
378             my $rc = $self->code; # RC HTTP code (from yuor methods)
379             my $head = $self->head; # HTTP Headers (hashref)
380             my $data = $self->data; # The working data
381             my $msg = $self->message || HTTP::Status::status_message($rc) || "Unknown code";
382              
383             my @res = (sprintf("Status: %s %s", $rc, $msg));
384             push @res, sprintf("Content-Type: %s", "text/plain; charset=utf-8");
385             push @res, "", $data // "";
386             return join("\015\012", @res);
387             }
388              
389             =head2 again
390              
391             Internal use only!
392              
393             See L
394              
395             =head1 EXAMPLES
396              
397             See all examples on METACPAN website L
398              
399             =head1 HISTORY
400              
401             See C file
402              
403             =head1 TO DO
404              
405             See C file
406              
407             =head1 BUGS
408              
409             * none noted
410              
411             Please report any bugs to https://rt.cpan.org/.
412              
413             =head1 SEE ALSO
414              
415             L, L
416              
417             =head1 AUTHOR
418              
419             Serż Minus (Sergey Lepenkov) L Eabalama@cpan.orgE
420              
421             =head1 COPYRIGHT
422              
423             Copyright (C) 1998-2019 D&D Corporation. All Rights Reserved
424              
425             =head1 LICENSE
426              
427             This program is free software; you can redistribute it and/or
428             modify it under the same terms as Perl itself.
429              
430             See C file and L
431              
432             =cut
433              
434 1     1   113 use vars qw/ $VERSION /;
  1         2  
  1         83  
435             $VERSION = '2.00';
436              
437 1     1   6 use base qw/ CTK /;
  1         2  
  1         530  
438             $CTK::PLUGIN_ALIAS_MAP{log} = "WWW::MLite::Log";
439              
440 1     1   176610 use Storable qw/dclone/; # for dclone
  1         5196  
  1         66  
441 1     1   514 use HTTP::Status qw//;
  1         4488  
  1         31  
442 1     1   491 use HTTP::Message;
  1         14991  
  1         39  
443 1     1   8 use HTTP::Headers;
  1         2  
  1         22  
444 1     1   455 use HTTP::Response;
  1         2365  
  1         30  
445 1     1   477 use HTTP::Date;
  1         1667  
  1         70  
446 1     1   418 use CTK::ConfGenUtil;
  1         1000  
  1         135  
447 1     1   453 use CTK::TFVals qw/ :ALL /;
  1         1995  
  1         302  
448              
449             use constant {
450 1         2454 APP_PLUGINS => [qw/
451             config log
452             /],
453             METHODS => {
454             GET => 1,
455             POST => 1,
456             PUT => 1,
457             DELETE => 1,
458             PATCH => 1,
459             },
460             EOL => "\015\012",
461             KEY_MASK => "%s#%s", # METHOD, PATH
462             REG_KEY_MASK => "%s#%s#%d", # CLASS, SERVER_NAME, SERVER_PORT
463             DEFAULT_METHOD => "GET",
464             DEFAULT_NAME => "default",
465             DEFAULT_PATH => "/", # Root
466             DEFAULT_SERVER_NAME => "localhost",
467             DEFAULT_SERVER_PORT => 80,
468             DEFAULT_CONTENT_TYPE=> "text/plain",
469 1     1   9 };
  1         2  
470              
471             my %method_registry;
472              
473             sub again {
474 0     0 1   my $self = shift;
475 0           my $args = $self->origin;
476 0           my $status = $self->load_plugins(@{(APP_PLUGINS)});
  0            
477 0 0         $self->{status} = 0 unless $status;
478 0           my $config = $self->configobj;
479              
480             # Autoloading logger (data from config)
481 0   0       my $log_on = $config->get("logenable") || $config->get("logenabled") || 0;
482 0 0 0       if ($self->logmode && $log_on) {
483 0   0       my $logopts = $args->{logopts} || {};
484 0 0         my $logfile = defined($args->{logfile}) ? $self->logfile : $config->get("logfile"); # From args or config
485 0 0         $logopts->{facility} = $args->{logfacility} if defined($args->{logfacility}); # From args only!
486 0 0         if ($args->{logfd}) {
487 0           $logopts->{fd} = $args->{logfd};
488             } else {
489 0 0 0       $logopts->{file} = $logfile if defined($logfile) && length($logfile);
490             }
491             $logopts->{ident} = defined($args->{ident})
492             ? $args->{ident}
493 0 0 0       : ($config->get("logident") // $self->project); # From args or config
494             $logopts->{level} = defined($args->{loglevel})
495             ? $args->{loglevel}
496 0 0         : ($config->get("loglevel")); # From args or config
497 0 0         $self->logger_init(%$logopts) or do {
498 0           $self->error("Can't initialize logger");
499 0           $self->{status} = 0;
500             };
501             }
502              
503             # Set methods
504             my $registry_key = sprintf(REG_KEY_MASK,
505             ref($self),
506             $ENV{SERVER_NAME} || DEFAULT_SERVER_NAME,
507 0   0       $ENV{SERVER_PORT} || DEFAULT_SERVER_PORT,
      0        
508             );
509             $self->{methods} = exists($method_registry{$registry_key}) ? $method_registry{$registry_key} : {},
510              
511             # Set name, info, code, head, data
512 0 0         $self->{name} = undef; # Method name
513 0           $self->{info} = undef; # Method info (without code)
514 0           $self->{code} = undef; # Response code (RC)
515 0           $self->{message} = undef; # Response message
516 0           $self->{head} = undef; # Response headers
517 0           $self->{data} = undef; # Response data
518 0           $self->{request_method} = undef; # Request method
519 0           $self->{request_uri} = undef; # Request uri
520              
521 0           return $self;
522             }
523              
524             sub register_method {
525 0     0 1   my $class = shift; # Caller's class
526 0 0         croak("Can't use reference in class name context") if ref($class);
527 0           my %info = @_;
528             my $registry_key = sprintf(REG_KEY_MASK,
529             $class,
530             $ENV{SERVER_NAME} || DEFAULT_SERVER_NAME,
531 0   0       $ENV{SERVER_PORT} || DEFAULT_SERVER_PORT,
      0        
532             );
533 0 0         $method_registry{$registry_key} = {} unless exists($method_registry{$registry_key});
534 0           my $methods = $method_registry{$registry_key};
535              
536             # Method & Path
537 0   0       my $meth = $info{method} || DEFAULT_METHOD;
538 0 0         $meth = DEFAULT_METHOD unless grep {$_ eq $meth} keys %{(METHODS())};
  0            
  0            
539 0   0       my $path = $info{path} // "";
540 0           $path =~ s/\/+$//;
541 0 0         $path = DEFAULT_PATH unless length($path);
542              
543             # Meta
544 0   0       my $name = $info{name} || DEFAULT_NAME;
545 0   0 0     my $code = $info{code} || sub {return HTTP::Status::HTTP_OK};
  0            
546 0 0 0       my $attrs = $info{attrs} && is_hash($info{attrs}) ? $info{attrs} : {};
547 0 0 0       my $returns = $info{returns} && is_hash($info{returns}) ? $info{returns} : {};
548 0   0       my $description = $info{description} || "";
549 0   0       my $deep = $info{deep} || $info{depth} || 0;
550 0   0       my $requires = array($info{requires} || []);
551              
552             # Key
553 0           my $key = sprintf(KEY_MASK, $meth, $path);
554 0 0         if ($methods->{$key}) {
555 0   0       my $tname = $methods->{$key}{name} || DEFAULT_NAME;
556 0 0         return 0 if $tname ne $name;
557             }
558              
559 0           $methods->{$key} = {
560             method => $meth,
561             path => $path,
562             name => $name,
563             code => $code,
564             deep => $deep,
565             requires=> $requires,
566             attrs => $attrs,
567             returns => $returns,
568             description => $description,
569             };
570 0           return 1;
571             }
572             sub check_http_method {
573 0     0 1   my $self = shift;
574 0           my $meth = shift;
575 0 0         return 0 unless $meth;
576 0 0         return 1 if $meth eq 'HEAD';
577 0           my $meths = METHODS;
578 0 0         return $meths->{$meth} ? 1 : 0;
579             }
580              
581             sub name {
582 0     0 1   my $self = shift;
583 0   0       return $self->{name} || DEFAULT_NAME;
584             }
585             sub info {
586 0     0 1   my $self = shift;
587 0           my $name = shift;
588 0   0       my $meta = dclone($self->{info} || {name => $self->name});
589 0 0         return $meta unless defined($name);
590 0 0         return undef unless defined $meta->{$name};
591 0           return $meta->{$name};
592             }
593             sub code {
594 0     0 1   my $self = shift;
595 0           my $value = shift;
596 0 0         return fv2zero($self->{code}) unless defined($value);
597 0   0       $self->{code} = $value || HTTP::Status::HTTP_OK;
598 0           return $self->{code};
599             }
600             sub message {
601 0     0 1   my $self = shift;
602 0           my $value = shift;
603 0 0         return $self->{message} unless defined($value);
604 0   0       $self->{message} = $value || HTTP::Status::status_message(HTTP::Status::HTTP_OK);
605 0           return $self->{message};
606             }
607             sub head {
608 0     0 1   my $self = shift;
609 0           my $struct = shift;
610 0 0         return $self->{head} unless defined($struct);
611 0           $self->{head} = $struct;
612 0           return $struct;
613             }
614             sub data {
615 0     0 1   my $self = shift;
616 0           my $struct = shift;
617 0 0         return $self->{data} unless defined($struct);
618 0           $self->{data} = $struct;
619 0           return $struct;
620             }
621              
622             sub lookup_method {
623 0     0 1   my $self = shift;
624 0           my ($imeth, $ipath) = @_;
625              
626             # Method
627 0   0       my $meth = uc($imeth || DEFAULT_METHOD);
628 0 0         $meth = "GET" if $meth eq 'HEAD';
629 0 0         unless ($self->check_http_method($meth)) {
630 0           $self->error(sprintf("The HTTP %s method not allowed", $meth));
631 0           return undef;
632             }
633              
634             # Path
635 0   0       my $path = $ipath || DEFAULT_PATH;
636 0           $path =~ s/[?\#](.*)$//;
637 0           $path =~ s/\/+$//;
638 0 0         $path = DEFAULT_PATH unless length($path);
639              
640             # Get method
641 0           my $name;
642 0           my $key = sprintf(KEY_MASK, $meth, $path);
643 0           my $methods = $self->{methods};
644             # ...by key
645             return $methods->{$key} if $methods->{$key}
646             && $methods->{$key}{name}
647 0 0 0       && $methods->{$key}{code};
      0        
648             # ...by path
649 0           foreach my $p (_scan_backward($path)) {
650 0           my $ikey = sprintf(KEY_MASK, $meth, $p);
651             return $methods->{$ikey} if $methods->{$ikey}
652             && $methods->{$ikey}{deep}
653             && $methods->{$ikey}{name}
654 0 0 0       && $methods->{$key}{code};
      0        
      0        
655             }
656 0           $self->error(sprintf("Method not found (%s %s)", $meth, $path));
657 0           return undef;
658             }
659             sub call_method {
660 0     0 1   my $self = shift;
661 0           my $meth = shift;
662 0           my $path = shift;
663 0           my @params = @_;
664 0           $self->cleanup;
665 0           $self->{request_method} = $meth;
666 0           $self->{request_uri} = $path;
667 0 0         my $method = $self->lookup_method($meth, $path) or return;
668 0 0         unless(ref($method) eq 'HASH') {
669 0           $self->error("Incorrect method structure");
670 0           return;
671             }
672              
673             # Get info
674 0           my %info;
675             my $func;
676 0           foreach my $k (keys %$method) {
677 0 0         next unless defined $k;
678 0 0         if ($k eq 'code') {
    0          
679 0           $func = $method->{code};
680 0           next;
681             } elsif ($k eq 'name') {
682 0           $self->{name} = $method->{name};
683             }
684 0           $info{$k} = $method->{$k};
685             }
686 0           $self->{info} = dclone(\%info);
687              
688             # Call method
689 0           my $rc;
690 0 0         if(ref($func) eq 'CODE') {
691 0           $rc = &$func($self, @params);
692             } else {
693 0           $self->message(sprintf("The code of method %s not found!", $self->name));
694 0           $rc = HTTP::Status::HTTP_NOT_IMPLEMENTED;
695             }
696 0           $self->{code} = $rc;
697              
698             # Call response
699 0 0         unless (HTTP::Status::status_message($rc)) {
700 0           $self->message(sprintf("Method %s returns incorrect HTTP status code!", $self->name));
701 0           $self->{code} = HTTP::Status::HTTP_INTERNAL_SERVER_ERROR;
702             }
703 0           return $self->response(@params);
704             }
705 0     0 1   sub call { goto &call_method }
706              
707             sub cleanup {
708 0     0 1   my $self = shift;
709 0           $self->error(""); # Flush error
710 0           $self->{name} = undef; # Method name
711 0           $self->{info} = undef; # Method info (without code)
712 0           $self->{code} = undef; # Response code (RC)
713 0           $self->{message} = undef; # Response message
714 0           $self->{head} = undef; # Response headers
715 0           $self->{data} = undef; # Response data
716 0           $self->{request_method} = undef; # Request method
717 0           $self->{request_uri} = undef; # Request uri
718 0           return 1;
719             }
720              
721             sub response {
722 0     0 1   my $self = shift;
723 0           my $rc = $self->code;
724 0           my $head = $self->head;
725 0           my $data = $self->data;
726 0   0       my $msg = $self->message || HTTP::Status::status_message($rc) || "Unknown code";
727              
728             # Content
729 0           my $dct = DEFAULT_CONTENT_TYPE;
730 0   0       my $content = $data // "";
731 0 0         $content = "" if $rc =~ /^(1\d\d|[23]04)$/; # make sure content we have no content
732 0 0         if (utf8::is_utf8($content)) {
733 0           utf8::encode($content);
734 0           $dct .= "; charset=utf-8";
735             }
736 0           my $cl = length($content);
737 0 0         $cl += length("\n") if $self->origin->{nph}; # Hack for HTTP::Message::as_string (eol char)
738              
739             # Headers
740 0           my $h = HTTP::Headers->new(Status => sprintf("%s %s", $rc, $msg));
741 0 0         if (is_void($head)) { # No data!
    0          
742 0           $h->header('Server' => sprintf("%s/%s", __PACKAGE__, $VERSION));
743 0           $h->header('Connection' => 'close');
744 0           $h->header('Date' => HTTP::Date::time2str(time()));
745 0           $h->header('Content-Type' => $dct);
746             } elsif (is_hash($head)) { # Data!
747 0           $h->header(%$head);
748             }
749 0 0 0       $h->header('Content-Length' => $cl) if $cl && !$h->header('Content-Length');
750              
751             # Response
752 0 0 0       my $ishead = $self->{request_method} && $self->{request_method} eq 'HEAD' ? 1 : 0;
753 0 0 0       my $r = HTTP::Response->new($rc, $msg, $h, ($cl && !$ishead ? $content : ""));
754              
755             # Done!
756             return $self->origin->{nph}
757             ? $r->as_string
758 0 0 0       : join(EOL, $r->{'_headers'}->as_string(EOL), ($cl && !$ishead ? $content : ""));
    0          
759             }
760              
761             sub _scan_backward { # Returns for /foo/bar/baz array: /foo/bar/baz, /foo/bar, /foo, /
762 0   0 0     my $p = shift // '';
763 0 0 0       my @out = ($p) if length($p) && $p ne '/';
764 0           while ($p =~ s/\/[^\/]+$//) {
765 0 0         push @out, $p if length($p)
766             }
767 0           push @out, '/';
768 0           return @out;
769             }
770              
771             1;
772              
773             __END__