File Coverage

blib/lib/CGI/Application/Dispatch/PSGI.pm
Criterion Covered Total %
statement 145 173 83.8
branch 83 118 70.3
condition 42 58 72.4
subroutine 13 13 100.0
pod 4 5 80.0
total 287 367 78.2


line stmt bran cond sub pod time code
1             package CGI::Application::Dispatch::PSGI;
2 1     1   115878 use strict;
  1         3  
  1         40  
3 1     1   19 use warnings;
  1         5  
  1         40  
4 1     1   6 use Carp 'carp';
  1         2  
  1         68  
5 1     1   1127 use HTTP::Exception;
  1         26807  
  1         7  
6              
7             our $VERSION = '3.12';
8             our $DEBUG = 0;
9              
10             =pod
11              
12             =head1 NAME
13              
14             CGI::Application::Dispatch::PSGI - Dispatch requests to
15             CGI::Application based objects using PSGI
16              
17             =head1 SYNOPSIS
18              
19             =head2 Out of Box
20              
21             Under mod_perl:
22              
23             # change "Apache1" to "Apache2" as needed.
24              
25            
26             SetHandler perl-script
27             PerlHandler Plack::Handler::Apache1
28             PerlSetVar psgi_app /path/to/app.psgi
29            
30              
31            
32             use Plack::Handler::Apache1;
33             Plack::Handler::Apache1->preload("/path/to/app.psgi");
34            
35              
36             Under CGI:
37              
38             This would be the instance script for your application, such
39             as /cgi-bin/dispatch.cgi:
40              
41             ### in your dispatch.psgi:
42             # ( in a persistent environment, use FindBin::Real instead. )
43             use FindBin 'Bin';
44             use lib "$Bin/../perllib';
45             use Your::Application::Dispatch;
46             Your::Application::Dispatch->as_psgi;
47              
48             ### In Your::Application::Dispatch;
49             package Your::Application::Dispatch;
50             use base 'CGI::Application::Dispatch::PSGI';
51              
52              
53             =head2 With a dispatch table
54              
55             package MyApp::Dispatch;
56             use base 'CGI::Application::Dispatch::PSGI';
57              
58             sub dispatch_args {
59             return {
60             prefix => 'MyApp',
61             table => [
62             '' => { app => 'Welcome', rm => 'start' },
63             ':app/:rm' => { },
64             'admin/:app/:rm' => { prefix => 'MyApp::Admin' },
65             ],
66             };
67             }
68              
69             The C<< .psgi >> file is constructed as above.
70              
71             =head2 With a custom query object
72              
73             If you want to supply your own PSGI object, something like this in
74             your .psgi file will work:
75              
76             sub {
77             my $env = shift;
78             my $app = CGI::Application::Dispatch::PSGI->as_psgi(
79             table => [
80             '/:rm' => { app => 'TestApp' }
81             ],
82             args_to_new => {
83             QUERY => CGI::PSGI->new($env)
84             }
85             );
86             return $app->($env);
87             }
88              
89              
90             =head1 DESCRIPTION
91              
92             This module provides a way to look at the path (as returned by C<<
93             $env->{PATH_INFO} >>) of the incoming request, parse off the desired
94             module and its run mode, create an instance of that module and run it.
95              
96             It will translate a URI like this (in a persistent environment)
97              
98             /app/module_name/run_mode
99              
100             or this (vanilla CGI)
101              
102             /app/index.cgi/module_name/run_mode
103              
104             into something that will be functionally similar to this
105              
106             my $app = Module::Name->new(..);
107             $app->mode_param(sub {'run_mode'}); #this will set the run mode
108              
109             =head1 METHODS
110              
111             =cut
112              
113             sub as_psgi {
114 11     11 1 1988 my ($self, %args) = @_;
115              
116             # merge dispatch_args() and %args with %args taking precendence
117 11         60 my $dispatch_args = $self->dispatch_args(\%args);
118 11         70 for my $arg (keys %$dispatch_args) {
119              
120             # args_to_new should be merged
121 43 100       95 if($arg eq 'args_to_new') {
122 11   100     60 $args{args_to_new} ||= {};
123              
124             # merge the PARAMS hash
125 11 100       37 if($dispatch_args->{args_to_new}->{PARAMS}) {
126              
127             # merge the hashes
128 2         8 $args{args_to_new}->{PARAMS} = {
129 2 100       18 %{$dispatch_args->{args_to_new}->{PARAMS}},
130 2         4 %{$args{args_to_new}->{PARAMS} || {}},
131             };
132             }
133              
134             # combine any TMPL_PATHs
135 11 50       41 if($dispatch_args->{args_to_new}->{TMPL_PATH}) {
136              
137             # make sure the orginial is an array ref
138 0 0       0 if($args{args_to_new}->{TMPL_PATH}) {
139 0 0       0 if(!ref $args{args_to_new}->{TMPL_PATH}) {
140 0         0 $args{args_to_new}->{TMPL_PATH} = [$args{args_to_new}->{TMPL_PATH}];
141             }
142             } else {
143 0         0 $args{args_to_new}->{TMPL_PATH} = [];
144             }
145              
146             # now add the rest to the end
147 0 0       0 if(ref $dispatch_args->{args_to_new}->{TMPL_PATH}) {
148 0         0 push(
149 0         0 @{$args{args_to_new}->{TMPL_PATH}},
150 0         0 @{$dispatch_args->{args_to_new}->{TMPL_PATH}},
151             );
152             } else {
153 0         0 push(
154 0         0 @{$args{args_to_new}->{TMPL_PATH}},
155             $dispatch_args->{args_to_new}->{TMPL_PATH},
156             );
157             }
158             }
159              
160             # now merge the args_to_new hashes
161 11         20 $args{args_to_new} = {%{$dispatch_args->{args_to_new}}, %{$args{args_to_new}},};
  11         30  
  11         51  
162             } else {
163              
164             # anything else should override
165 32 100       122 $args{$arg} = $dispatch_args->{$arg} unless exists $args{$arg};
166             }
167             }
168              
169 11 50       40 $DEBUG = $args{debug} ? 1 : 0;
170              
171             # check for extra args (for backwards compatibility)
172 11         42 for (keys %args) {
173             next
174 44 50 100     410 if( $_ eq 'prefix'
      66        
      66        
      66        
      100        
      66        
      33        
175             or $_ eq 'default'
176             or $_ eq 'debug'
177             or $_ eq 'rm'
178             or $_ eq 'args_to_new'
179             or $_ eq 'table'
180             or $_ eq 'auto_rest'
181             or $_ eq 'auto_rest_lc');
182 0         0 die "Passing extra args ('$_') to as_psgi() is not supported! Did you mean to use 'args_to_new' ?";
183 0         0 $args{args_to_new}->{$_} = delete $args{$_};
184             }
185              
186             return sub {
187 28     28   84241 my $env = shift;
188              
189             # get the PATH_INFO
190 28         66 my $path_info = $env->{PATH_INFO};
191              
192             # use the 'default' if we need to
193 28 100 100     194 $path_info = $args{default} || '' if(!$path_info || $path_info eq '/');
      66        
194              
195             # make sure they all start and end with a '/', to correspond
196             # with the RE we'll make
197 28 100       80 $path_info = "/$path_info" unless(index($path_info, '/') == 0);
198 28 100       106 $path_info = "$path_info/" unless(substr($path_info, -1) eq '/');
199              
200 28         34 my ($module, $rm, $local_prefix, $local_args_to_new);
201             # take args from path
202 0         0 my $named_args;
203 28         41 eval {
204 28 100       214 $named_args = $self->_parse_path($path_info, $args{table},$env)
205             or HTTP::Exception->throw(404, status_message => 'Resource not found');
206             };
207 28 100       1648 if (my $e = HTTP::Exception->caught) {
208 2         40 return $self->http_error($e);
209             }
210              
211 26 50       726 if($DEBUG) {
212 0         0 require Data::Dumper;
213 0         0 warn "[Dispatch] Named args from match: " . Data::Dumper::Dumper($named_args) . "\n";
214             }
215              
216 26 50 33     199 if(exists($named_args->{PARAMS}) || exists($named_args->{TMPL_PATH})) {
217 0         0 carp "PARAMS and TMPL_PATH are not allowed here. Did you mean to use args_to_new?";
218 0         0 HTTP::Exception->throw(500, status_message => 'PARAMS and TMPL_PATH not allowed');
219             }
220              
221             # eval and catch any exceptions that might be thrown
222 26         31 my ($output, @final_dispatch_args);
223 0         0 my $psgi_app;
224 26         37 eval {
225 26         116 ($module, $local_prefix, $rm, $local_args_to_new) =
226 26         37 delete @{$named_args}{qw(app prefix rm args_to_new)};
227              
228             # If another name for dispatch_url_remainder has been set move
229             # the value to the requested name
230 26 100       221 if($$named_args{'*'}) {
231 1         5 $$named_args{$$named_args{'*'}} = $$named_args{'dispatch_url_remainder'};
232 1         4 delete $$named_args{'*'};
233 1         2 delete $$named_args{'dispatch_url_remainder'};
234             }
235              
236 26 50       61 $module or HTTP::Exception->throw(500, status_message => 'App not defined');
237 26         93 $module = $self->translate_module_name($module);
238              
239 26   100     117 $local_prefix ||= $args{prefix};
240 26 100       83 $module = $local_prefix . '::' . $module if($local_prefix);
241              
242 26   66     111 $local_args_to_new ||= $args{args_to_new};
243              
244             # add the rest of the named_args to PARAMS
245 26         66 @{$local_args_to_new->{PARAMS}}{keys %$named_args} = values %$named_args;
  26         73  
246              
247 26 100       80 my $auto_rest =
248             defined $named_args->{auto_rest} ? $named_args->{auto_rest} : $args{auto_rest};
249 26 100 66     88 if($auto_rest && defined $rm && length $rm) {
      100        
250 3 100       14 my $method_lc =
251             defined $named_args->{auto_rest_lc}
252             ? $named_args->{auto_rest_lc}
253             : $args{auto_rest_lc};
254 3         7 my $http_method = $env->{REQUEST_METHOD};
255 3 100       9 $http_method = lc $http_method if $method_lc;
256 3         9 $rm .= "_$http_method";
257             }
258             # load and run the module
259 26         68 @final_dispatch_args = ($module, $rm, $local_args_to_new);
260 26         92 $self->require_module($module);
261 24         94 $psgi_app = $self->_run_app($module, $rm, $local_args_to_new,$env);
262             };
263 26 100       2197 if (my $e = HTTP::Exception->caught) {
    50          
264 3         62 return $self->http_error($e);
265             }
266             elsif ($e = Exception::Class->caught) {
267 0 0       0 ref $e ? $e->rethrow : die $e;
268             }
269 23         665 return $psgi_app;
270             }
271 11         178 }
272              
273             =head2 as_psgi(%args)
274              
275             This is the primary method used during dispatch.
276              
277             #!/usr/bin/perl
278             use strict;
279             use CGI::Application::Dispatch::PSGI;
280              
281             CGI::Application::Dispatch::PSGI->as_psgi(
282             prefix => 'MyApp',
283             default => 'module_name',
284             );
285              
286             This method accepts the following name value pairs:
287              
288             =over
289              
290             =item default
291              
292             Specify a value to use for the path if one is not available.
293             This could be the case if the default page is selected (eg: "/" ).
294              
295             =item prefix
296              
297             This option will set the string that will be prepended to the name of
298             the application module before it is loaded and created. So to use our
299             previous example request of
300              
301             /app/index.cgi/module_name/run_mode
302              
303             This would by default load and create a module named
304             'Module::Name'. But let's say that you have all of your application
305             specific modules under the 'My' namespace. If you set this option to
306             'My' then it would instead load the 'My::Module::Name' application
307             module instead.
308              
309             =item args_to_new
310              
311             This is a hash of arguments that are passed into the C
312             constructor of the application.
313              
314             =item table
315              
316             In most cases, simply using Dispatch with the C and C
317             is enough to simplify your application and your URLs, but there are
318             many cases where you want more power. Enter the dispatch table. Since
319             this table can be slightly complicated, a whole section exists on its
320             use. Please see the L section.
321              
322             =item debug
323              
324             Set to a true value to send debugging output for this module to
325             STDERR. Off by default.
326              
327             =item auto_rest
328              
329             This tells Dispatch that you are using REST by default and that you
330             care about which HTTP method is being used. Dispatch will append the
331             HTTP method name (upper case by default) to the run mode that is
332             determined after finding the appropriate dispatch rule. So a GET
333             request that translates into C<< MyApp::Module->foo >> will become
334             C<< MyApp::Module->foo_GET >>.
335              
336             This can be overridden on a per-rule basis in a custom dispatch table.
337              
338             =item auto_rest_lc
339              
340             In combinaion with L this tells Dispatch that you prefer
341             lower cased HTTP method names. So instead of C and
342             C you'll have C and C.
343              
344             =back
345              
346             =cut
347              
348             sub http_error {
349 5     5 0 7 my ($self, $e) = @_;
350              
351 5 50       46 warn '[Dispatch] ERROR'
352             . ($ENV{REQUEST_URI} ? " for request '$ENV{REQUEST_URI}': " : ': ')
353             . $e->error . "\n";
354              
355 5 50       74 my $errno = $e->isa('HTTP::Exception::Base') ? $e->code : 500;
356 5 50       38 my $output = $e->isa('HTTP::Exception::Base') ? $e->status_message : "Internal Server Error";
357              
358             # The custom status message was most useful for logging. Return
359             # generic messages to the user.
360 5 100       43 $output = 'Not Found' if ($e->code == 404);
361 5 50       22 $output = 'Internal Server Error' if ($e->code == 500);
362              
363              
364 5         51 return [ $errno, [], [ $output ] ];
365             }
366              
367             # protected method - designed to be used by sub classes, not by end users
368             sub _parse_path {
369 28     28   61 my ($self, $path, $table, $env) = @_;
370              
371             # get the module name from the table
372 28 50       77 return unless defined($path);
373              
374 28 50       96 unless(ref($table) eq 'ARRAY') {
375 0         0 warn "[Dispatch] Invalid or no dispatch table!\n";
376 0         0 return;
377             }
378              
379             # look at each rule and stop when we get a match
380 28         102 for(my $i = 0 ; $i < scalar(@$table) ; $i += 2) {
381              
382 77         153 my $rule = $table->[$i];
383              
384             # are we trying to dispatch based on HTTP_METHOD?
385 77         264 my $http_method_regex = qr/\[([^\]]+)\]$/;
386 77 100       350 if($rule =~ /$http_method_regex/) {
387 7         18 my $http_method = $1;
388              
389             # go ahead to the next rule
390 7 100       37 next unless lc($1) eq lc($env->{REQUEST_METHOD});
391              
392             # remove the method portion from the rule
393 4         27 $rule =~ s/$http_method_regex//;
394             }
395              
396             # make sure they start and end with a '/' to match how
397             # PATH_INFO is formatted
398 74 50       245 $rule = "/$rule" unless(index($rule, '/') == 0);
399 74 50       222 $rule = "$rule/" if(substr($rule, -1) ne '/');
400              
401 74         131 my @names = ();
402              
403             # translate the rule into a regular expression, but remember
404             # where the named args are
405             # '/:foo' will become '/([^\/]*)'
406             # and
407             # '/:bar?' will become '/?([^\/]*)?'
408             # and then remember which position it matches
409              
410 74         378 $rule =~ s{
411             (^|/) # beginning or a /
412             (:([^/\?]+)(\?)?) # stuff in between
413             }{
414 100         240 push(@names, $3);
415 100 100       497 $1 . ($4 ? '?([^/]*)?' : '([^/]*)')
416             }gxe;
417              
418             # '/*/' will become '/(.*)/$' the end / is added to the end of
419             # both $rule and $path elsewhere
420 74 100       231 if($rule =~ m{/\*/$}) {
421 9         39 $rule =~ s{/\*/$}{/(.*)/\$};
422 9         18 push(@names, 'dispatch_url_remainder');
423             }
424              
425             warn
426 74 50       161 "[Dispatch] Trying to match '${path}' against rule '$table->[$i]' using regex '${rule}'\n"
427             if $DEBUG;
428              
429             # if we found a match, then run with it
430 74 100       1988 if(my @values = ($path =~ m#^$rule$#)) {
431              
432 26 50       61 warn "[Dispatch] Matched!\n" if $DEBUG;
433              
434 26         63 my %named_args = %{$table->[++$i]};
  26         144  
435 26 100       123 @named_args{@names} = @values if @names;
436              
437 26         259 return \%named_args;
438             }
439             }
440              
441 2         18 return;
442             }
443              
444             sub _run_app {
445 24     24   49 my ($self, $module, $rm, $args,$env) = @_;
446              
447 24 50       66 if($DEBUG) {
448 0         0 require Data::Dumper;
449 0         0 warn "[Dispatch] Final args to pass to new(): " . Data::Dumper::Dumper($args) . "\n";
450             }
451              
452 24 100       56 if($rm) {
453              
454             # check runmode name
455 21         100 ($rm) = ($rm =~ /^([a-zA-Z_][\w']+)$/);
456 21 50       60 HTTP::Exception->throw(400, status_message => "Invalid characters in runmode name") unless $rm;
457              
458             }
459              
460             # now create and run then application object
461 24 50       50 warn "[Dispatch] creating instance of $module\n" if($DEBUG);
462              
463 24         33 my $psgi;
464 24         29 eval {
465 24         31 my $app = do {
466 24 100 66     166 if (ref($args) eq 'HASH' and not defined $args->{QUERY}) {
    50          
467 9         1615 require CGI::PSGI;
468 9         19151 $args->{QUERY} = CGI::PSGI->new($env);
469 9         9231 $module->new($args);
470             }
471             elsif (ref($args) eq 'HASH') {
472 15         96 $module->new($args);
473             }
474             else {
475 0         0 $module->new();
476             }
477             };
478 24 100   21   6784 $app->mode_param(sub { return $rm }) if($rm);
  21         2417  
479 24         325 $psgi = $app->run_as_psgi;
480             };
481              
482             # App threw an HTTP::Exception? Cool. Bubble it up.
483 24         10497 my $e;
484 24 100       114 if ($e = HTTP::Exception->caught) {
485 1         27 $e->rethrow;
486             }
487             else {
488 23         455 $e = Exception::Class->caught();
489              
490             # catch invalid run-mode stuff
491 23 50 33     340 if (not ref $e and $e =~ /No such run mode/) {
    50 33        
492 0         0 HTTP::Exception->throw(404, status_message => "RM '$rm' not found");
493             }
494             # otherwise, it's an internal server error.
495             elsif (defined $e and length $e) {
496 0         0 HTTP::Exception->throw(500, status_message => "Unknown error: $e");
497             #return $psgi;
498             }
499             else {
500             # no exception
501 23         83 return $psgi;
502             }
503             }
504             }
505              
506             =head2 dispatch_args()
507              
508             Returns a hashref of args that will be passed to L(). It
509             will return the following structure by default.
510              
511             {
512             prefix => '',
513             args_to_new => {},
514             table => [
515             ':app' => {},
516             ':app/:rm' => {},
517             ],
518             }
519              
520             This is the perfect place to override when creating a subclass to
521             provide a richer dispatch L.
522              
523             When called, it receives 1 argument, which is a reference to the hash
524             of args passed into L.
525              
526             =cut
527              
528             sub dispatch_args {
529 10     10 1 25 my ($self, $args) = @_;
530             return {
531 10   100     183 default => ($args->{default} || ''),
      100        
      100        
532             prefix => ($args->{prefix} || ''),
533             args_to_new => ($args->{args_to_new} || {}),
534             table => [
535             ':app' => {},
536             ':app/:rm' => {},
537             ],
538             };
539             }
540              
541             =head2 translate_module_name($input)
542              
543             This method is used to control how the module name is translated from
544             the matching section of the path (see L<"Path Parsing">. The main
545             reason that this method exists is so that it can be overridden if it
546             doesn't do exactly what you want.
547              
548             The following transformations are performed on the input:
549              
550             =over
551              
552             =item The text is split on '_'s (underscores)
553             and each word has its first letter capitalized. The words are then joined
554             back together and each instance of an underscore is replaced by '::'.
555              
556              
557             =item The text is split on '-'s (hyphens)
558             and each word has its first letter capitalized. The words are then joined
559             back together and each instance of a hyphen removed.
560              
561             =back
562              
563             Here are some examples to make it even clearer:
564              
565             module_name => Module::Name
566             module-name => ModuleName
567             admin_top-scores => Admin::TopScores
568              
569             =cut
570              
571             sub translate_module_name {
572 25     25 1 46 my ($self, $input) = @_;
573              
574 25         85 $input = join('::', map { ucfirst($_) } split(/_/, $input));
  47         156  
575 25         82 $input = join('', map { ucfirst($_) } split(/-/, $input));
  25         65  
576              
577 25         72 return $input;
578             }
579              
580             =head2 require_module($module_name)
581              
582             This class method is used internally to take a module name (supplied
583             by L) and require it in a secure fashion. It is
584             provided as a public class method so that if you override other
585             functionality of this module, you can still safely require user
586             specified modules. If there are any problems requiring the named
587             module, then we will C.
588              
589             CGI::Application::Dispatch::PSGI->require_module('MyApp::Module::Name');
590              
591             =cut
592              
593             sub require_module {
594 26     26 1 49 my ($self, $module) = @_;
595              
596 26 50       62 $module or HTTP::Exception->throw(404, status_message => "Can't define module name");
597              
598             #untaint the module name
599 26         123 ($module) = ($module =~ /^([A-Za-z][A-Za-z0-9_\-\:\']+)$/);
600              
601 26 50       63 unless($module) {
602 0         0 HTTP::Exception->throw(400, status_message => "Invalid characters in module name");
603             }
604              
605 26 50       61 warn "[Dispatch] loading module $module\n" if($DEBUG);
606 26         1770 eval "require $module";
607 26 100       434 return unless $@;
608              
609 2         4 my $module_path = $module;
610 2         7 $module_path =~ s/::/\//g;
611              
612 2 50       32 if($@ =~ /Can't locate $module_path.pm/) {
613 2         23 HTTP::Exception->throw(404, status_message => "Can't find module $module");
614             }
615             else {
616 0           HTTP::Exception->throw(500, status_message => "Unable to load module '$module': $@");
617             }
618             }
619              
620             1;
621              
622             __END__