File Coverage

lib/PSGI/Hector.pm
Criterion Covered Total %
statement 112 149 75.1
branch 19 40 47.5
condition 4 12 33.3
subroutine 27 32 84.3
pod 11 12 91.6
total 173 245 70.6


line stmt bran cond sub pod time code
1             #main framework object
2             package PSGI::Hector;
3              
4             =pod
5              
6             =head1 NAME
7              
8             PSGI::Hector - Very simple PSGI web framework
9              
10             =head1 SYNOPSIS
11              
12             my $app = App->init({
13             'responsePlugin' => 'Some::Class',
14             });
15             ###########################
16             ###########################
17             package App;
18             use PSGI::Hector;
19             use parent qw(PSGI::Hector);
20             ###########################
21             sub handleDefault{
22             #add code here for landing page
23             }
24              
25             =head1 DESCRIPTION
26              
27             All action subs are passed a L object as the only parameter, from this you should be able to reach
28             everything you need.
29              
30             =head1 METHODS
31              
32             =cut
33              
34 5     5   169747 use strict;
  5         31  
  5         113  
35 5     5   19 use warnings;
  5         7  
  5         106  
36 5     5   47 use File::Basename;
  5         9  
  5         453  
37 5     5   1908 use Class::Load qw(is_class_loaded load_class);
  5         81930  
  5         259  
38 5     5   1452 use parent qw(PSGI::Hector::Base PSGI::Hector::Utils);
  5         857  
  5         22  
39 5     5   1538 use PSGI::Hector::Response;
  5         11  
  5         116  
40 5     5   1551 use PSGI::Hector::Session; #for session management
  5         53  
  5         173  
41 5     5   1803 use PSGI::Hector::Request;
  5         15  
  5         142  
42 5     5   1578 use PSGI::Hector::Log;
  5         11  
  5         3915  
43             our $VERSION = "2.0";
44             #########################################################
45              
46             =head2 init(\%options)
47              
48             my $options = {
49             'responsePlugin' => 'Some::Class',
50             'checkReferer' => 0,
51             'sessionClass' => 'Some::Class',
52             'requestClass' => 'Some::Class',
53             'sefUrls' => 0,
54             'debug' => 1
55             };
56             my $h = PSGI::Hector->init($options);
57            
58             Factory class method, used in creating the application from the F file. The return value
59             from this method can be used where ever $app would be used.
60              
61             This hash reference passed to the method contains any general options for the framework.
62              
63             =cut
64              
65             #########################################################
66             sub init{
67 1     1 1 666 my($class, $options) = @_;
68             return sub {
69 0     0   0 my $env = shift;
70 0         0 my $h = $class->new($options, $env);
71 0         0 $h->run(); #do this thing!
72 1         5 };
73             }
74             #########################################################
75              
76             =head2 new(\%options, \%env)
77              
78             my $options = {};
79             my $h = PSGI::Hector->new($options, $env);
80              
81             Constructor, requires a hash references of options to be passed and the PSGI environment.
82              
83             Usually this method is invoked from the init() class method.
84              
85             =cut
86              
87             #########################################################
88             sub new{
89 8     8 1 4191 my($class, $options, $env) = @_;
90 8 50       24 die("No response plugin option provided") unless $options->{'responsePlugin'}; #this option is mandatory
91              
92 8         38 my $self = $class->SUPER::new();
93 8         36 $self->{'_env'} = $env;
94 8         12 $self->{'_options'} = $options;
95 8         22 my $requestClass = $self->__getFullClassName("Request");
96 8 50       325 if($self->getOption('requestClass')){
97 0         0 $requestClass = $self->getOption('requestClass');
98             }
99 8 100       15 if(!defined($self->getOption('debug'))){ #turn off debugging by default
100 2         4 $self->_setOption("debug", 0);
101             }
102 8         40 $self->{'_request'} = $requestClass->new($env);
103 8         74 $self->{'_response'} = PSGI::Hector::Response->new($self, $self->getOption('responsePlugin')); #this could need access to a request object
104 8         46 $self->{'_log'} = PSGI::Hector::Log->new({
105             'debug' => $self->getOption('debug')
106             });
107 8         34 $self->_init(); #perform initial setup
108 8         16 $self;
109             }
110             #########################################################
111              
112             =pod
113              
114             =head2 getResponse()
115              
116             my $response = $h->getResponse();
117              
118             Returns the current instance of the response plugin object, previously defined in the constructor options.
119             See L for more details.
120              
121             =cut
122              
123             ###########################################################
124             sub getResponse{
125 8     8 1 917 shift->{'_response'};
126             }
127             #########################################################
128              
129             =pod
130              
131             =head2 getSession()
132              
133             my $session = $h->getSession();
134              
135             Returns the current instance of the L object.
136              
137             =cut
138              
139             ###########################################################
140             sub getSession{
141 1     1 1 642 my $self = shift;
142 1 50       3 if(!$self->{'_session'}){
143 1         3 my $sessionClass = $self->__getFullClassName("Session");
144 1 50       56 if($self->getOption('sessionClass')){
145 0         0 $sessionClass = $self->getOption('sessionClass');
146             }
147 1         10 $self->{'_session'} = $sessionClass->new($self);
148             }
149 1         2 $self->{'_session'};
150             }
151             #########################################################
152              
153             =pod
154              
155             =head2 getRequest()
156              
157             my $request = $h->getRequest();
158              
159             Returns the current instance of the L object.
160              
161             =cut
162              
163             ###########################################################
164             sub getRequest{
165 7     7 1 358 my $request = shift->{'_request'};
166 7 50       53 $request or die("No request object found");
167             }
168             #########################################################
169              
170             =pod
171              
172             =head2 getLog()
173              
174             my $logger = $h->getLog();
175              
176             returns an current instance of the L object.
177              
178             =cut
179              
180             #########################################################
181             sub getLog{
182 5     5 1 397 shift->{'_log'};
183             }
184             #########################################################
185              
186             =pod
187              
188             =head2 getAction()
189              
190             my $action = $h->getAction();
191              
192             Returns the curent action that the web application is performing. This is the current value of the "action"
193             request form field or query string item.
194              
195             If search engine friendly URLs are turned on the action will be determined from the last part of the script URL.
196              
197             =cut
198              
199             ###########################################################
200             sub getAction{
201 1     1 1 2 my $self = shift;
202 1         1 my $action = "default";
203 1         2 my $request = $self->getRequest();
204 1         3 my $params = $request->getParameters();
205 1 50       147 if(defined($params->{'action'})){ #get action from query string or post string
206 0         0 $action = $params->{'action'};
207             }
208             else{ #do we have search engine friendly urls
209 1         3 my $sefAction = $self->_getSefAction();
210 1 50       3 if($sefAction){
211 0         0 $action = $sefAction;
212             }
213             }
214 1         4 $action;
215             }
216             #########################################################
217              
218             =pod
219              
220             =head2 getUrlForAction($action, $queryString)
221              
222             my $url = $h->getUrlForAction("someAction", "a=b&c=d");
223              
224             Returns the URL for the application with the given action and query string.
225              
226             =cut
227              
228             #########################################################
229             sub getUrlForAction{
230 2     2 1 576 my($self, $action, $query) = @_;
231 2         4 my $url = "/" . $action;
232 2 50       5 if($query){ #add query string
233 2         4 $url .= "?" . $query;
234             }
235 2         13 $url;
236             }
237             #########################################################
238              
239             =pod
240              
241             =head2 getFullUrlForAction($action, $queryString)
242              
243             my $url = $h->getFullUrlForAction("someAction", "a=b&c=d");
244              
245             Returns the Full URL for the application with the given action and query string and hostname.
246              
247             =cut
248              
249             #########################################################
250             sub getFullUrlForAction{
251 1     1 1 2 my($self, $action, $query) = @_;
252 1         2 $self->getSiteUrl() . $self->getUrlForAction($action, $query);
253             }
254             #########################################################
255              
256             =pod
257              
258             =head2 run()
259              
260             $h->run();
261              
262             This methood is required for the web application to deal with the current request.
263             It should be called after any setup is done.
264              
265             If the response object decides that the response has not been modified then this
266             method will not run any action functions.
267              
268             The action sub run will be determined by first checking the actions hash if previously
269             given to the object then by checking if a method prefixed with "handle" exists in the
270             current class.
271              
272             =cut
273              
274             ###########################################################
275             sub run{ #run the code for the given action
276 0     0 1 0 my $self = shift;
277 0         0 my $response = $self->getResponse();
278 0 0       0 if($response->code() != 304){ #need to do something
279 0         0 $self->getLog()->log("Need to run action sub", 'debug');
280 0         0 my $action = $self->getAction();
281 0         0 $self->getLog()->log("Using action: '$action'", 'debug');
282 0         0 my $subName = "handle" . ucfirst($action); #add prefix for security
283 0         0 my $class = ref($self);
284 0 0       0 if($class->can($subName)){ #default action sub exists
285 0         0 eval{
286 0         0 $self->$subName();
287             };
288 0 0       0 if($@){ #problem with sub
289 0         0 $response->setError($@);
290             }
291             }
292             else{ #no code to execute
293 0         0 $response->code(404);
294 0         0 $response->message('Not Found');
295 0         0 $response->setError("No action found for: $action");
296             }
297             }
298 0         0 $response->display(); #display the output to the browser
299             }
300             ##########################################################
301              
302             =pod
303              
304             =head2 getOption("key")
305              
306             my $value = $h->getOption("debug");
307              
308             Returns the value of the configuration option given.
309              
310             =cut
311              
312             ##########################################################
313             sub getOption{
314 43     43 1 665 my($self, $key) = @_;
315 43         43 my $value = undef;
316 43 100       90 if(defined($self->{'_options'}->{$key})){ #this config option has been set
317 24         32 $value = $self->{'_options'}->{$key};
318             }
319 43         123 $value;
320             }
321             ##########################################################
322             sub getEnv{
323 18     18 0 30 shift->{'_env'};
324             }
325             ###########################################################
326             # Private methods
327             #########################################################
328             sub __getFullClassName{
329 13     13   307 my($self, $name) = @_;
330 13         21 my $class = ref($self);
331 13 50       33 $class->__checkClass($class, $name) or die('Could not find full class name');
332             }
333             #########################################################
334             sub __checkClass{
335 29     29   48 my($class, $current, $name) = @_;
336 29         48 my $try = $current . "::" . $name;
337 29 100       67 return $try if is_class_loaded($try);
338 16         559 eval{
339 16         27 load_class($try);
340             };
341 16 50       9110 if($@){
342 5     5   31 no strict 'refs';
  5         7  
  5         2249  
343 16         18 my @parents = @{$current . "::ISA"}; #get base classes
  16         53  
344 16         24 my @results = map { $class->__checkClass($_, $name) } @parents;
  16         32  
345 16         237 my @found = grep { defined($_) } @results;
  16         26  
346 16         50 shift(@found)
347             }
348             }
349             #########################################################
350             sub __getActionDigest{
351 0     0   0 my $self = shift;
352 0         0 my $sha1 = Digest::SHA1->new();
353 0         0 $sha1->add($self->getAction());
354 0         0 $sha1->hexdigest();
355             }
356             ###########################################################
357             sub _getSefAction{
358 1     1   1 my $self = shift;
359 1         2 my $action = undef;
360 1         2 my $env = $self->getEnv();
361 1 50 33     4 if(defined($env->{'PATH_INFO'}) && $env->{'PATH_INFO'} =~ m/\/(.+)$/){ #get the action from the last part of the url
362 0         0 $action = $1;
363             }
364 1         1 $action;
365             }
366             ###########################################################
367             sub _init{ #things to do when this object is created
368 8     8   10 my $self = shift;
369 8 50 33     14 if(!defined($self->getOption('checkReferer')) || $self->getOption('checkReferer')){ #check the referer by default
370 8         19 $self->_checkReferer(); #check this first
371             }
372 8         10 1;
373             }
374             ###########################################################
375             sub _checkReferer{ #simple referer check for very basic security
376 8     8   9 my $self = shift;
377 8         10 my $result = 0;
378 8         17 my $env = $self->getEnv();
379 8         15 my $host = $env->{'HTTP_HOST'};
380 8 50 33     184 if($host && $env->{'HTTP_REFERER'} && $env->{'HTTP_REFERER'} =~ m/^(http|https):\/\/$host/){ #simple check here
      33        
381 8         15 $result = 1;
382             }
383             else{
384 0         0 my $response = $self->getResponse();
385 0         0 $response->setError("Details where not sent from the correct web page");
386             }
387 8         23 $result;
388             }
389             ##########################################################
390             sub _getActions{
391 0     0   0 shift->{'_actions'};
392             }
393             ###########################################################
394             sub _setOption{
395 2     2   4 my($self, $key, $value) = @_;
396 2         5 $self->{'_options'}->{$key} = $value;
397 2         3 1;
398             }
399             ##########################################################
400             sub _getScriptName{ #returns the basename of the running script
401 0     0     my $self = shift;
402 0           my $env = $self->getEnv();
403 0           my $scriptName = $env->{'REQUEST_URI'};
404 0 0         die("Cant find scriptname, are you running a CGI") unless($scriptName);
405 0           return basename($scriptName);
406             }
407             ###########################################################
408              
409             =pod
410              
411             =head1 CONFIGURATION SUMMARY
412              
413             The following list gives a summary of each Hector
414             configuration options.
415              
416             =head3 responsePlugin
417              
418             A scalar string consisting of the response class to use.
419              
420             See L for details on how to create your own response class, or
421             a list of response classes provided in this package.
422              
423             =head3 checkReferer
424              
425             Flag to indicate if referer checking should be performed. When enabled an
426             error will raised when the referer is not present or does not contain the server's
427             hostname.
428              
429             This option is enabled by default.
430              
431             =head3 sessionClass
432              
433             A scalar string consisting of the session class to use. Useful if you want to change the way
434             session are stored.
435              
436             Defaults to ref($self)::Session
437              
438             =head3 requestClass
439              
440             A scalar string consisting of the request class to use. Useful if you want to change the way
441             requests are handled.
442              
443             Defaults to ref($self)::Request
444              
445             =head3 sefUrls
446              
447             A boolean value indicating if search engine friendly URLs are to be used.
448              
449             =head3 debug
450              
451             A boolean value indicating if debug mode is enabled. This can then be used in output views or code to print extra debug.
452              
453             =head1 Notes
454              
455             To change the session prefix characters use the following code at the top of your script:
456              
457             $PSGI::Hector::Session::prefix = "ABC";
458            
459             To change the session file save path use the following code at the top of your script:
460              
461             $PSGI::Hector::Session::path = "/var/tmp";
462            
463             =head2 Reverse proxies
464              
465             To run your application behind a reverse proxy using apache, please use the following setup:
466              
467            
468             RequestHeader set X-Forwarded-Script-Name /psgi
469             RequestHeader set X-Traversal-Path /
470             ProxyPass http://localhost:8080/
471             ProxyPassReverse http://localhost:8080/
472            
473              
474             =head1 Author
475              
476             MacGyveR
477              
478             Development questions, bug reports, and patches are welcome to the above address
479              
480             =head1 See Also
481              
482             =head1 Copyright
483              
484             Copyright (c) 2017 MacGyveR. All rights reserved.
485              
486             This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
487              
488             =cut
489              
490             ###########################################################
491             return 1;