File Coverage

blib/lib/Pod/Webserver.pm
Criterion Covered Total %
statement 133 212 62.7
branch 33 110 30.0
condition 11 49 22.4
subroutine 25 31 80.6
pod 0 11 0.0
total 202 413 48.9


line stmt bran cond sub pod time code
1             package Pod::Webserver;
2              
3 3     3   91318 use parent 'Pod::Simple::HTMLBatch';
  3         1020  
  3         15  
4 3     3   168796 use strict;
  3         7  
  3         160  
5 3     3   19 use vars qw( $VERSION @ISA );
  3         11  
  3         218  
6              
7 3     3   1554 use Pod::Webserver::Daemon;
  3         7  
  3         89  
8 3     3   1100 use Pod::Webserver::Response;
  3         5  
  3         69  
9              
10 3     3   12 use Pod::Simple::HTMLBatch;
  3         5  
  3         50  
11 3     3   1569 use Pod::Simple::TiedOutFH;
  3         3912  
  3         109  
12 3     3   23 use Pod::Simple;
  3         6  
  3         86  
13 3     3   1757 use IO::Socket;
  3         40520  
  3         18  
14 3     3   2808 use File::Spec;
  3         20  
  3         74  
15 3     3   14 use File::Spec::Unix ();
  3         4  
  3         513  
16              
17             our $VERSION = '3.10';
18              
19             # ------------------------------------------------
20              
21             BEGIN {
22 3 50 0 3   22 if(defined &DEBUG) { } # no-op
    50          
    0          
23 3         9171 elsif( defined &Pod::Simple::DEBUG ) { *DEBUG = \&Pod::Simple::DEBUG }
24             elsif( ($ENV{'PODWEBSERVERDEBUG'} || '') =~ m/^(\d+)$/ )
25 0         0 { my $x = $1; *DEBUG = sub(){$x} }
  0         0  
  0         0  
26 0         0 else { *DEBUG = sub () {0}; }
27              
28             } # End of BEGIN.
29              
30             # ------------------------------------------------
31              
32             #sub Pod::Simple::HTMLBatch::DEBUG () {5}
33              
34             # ------------------------------------------------
35              
36             sub add_to_fs { # add an item to my virtual in-memory filesystem
37 17     17 0 23 my($self,$file,$type,$content) = @_;
38              
39 17 50 33     71 die "Missing filespec\n" unless defined $file and length $file;
40 17         23 $file = "/$file";
41 17         62 $file =~ s{/+}{/}s;
42 17 50 66     144 $type ||=
    50          
    50          
    50          
    50          
    50          
    100          
43             $file eq '/' ? 'text/html' # special case
44             : $file =~ m/\.dat?/ ? 'application/octet-stream'
45             : $file =~ m/\.html?/ ? 'text/html'
46             : $file =~ m/\.txt/ ? 'text/plain'
47             : $file =~ m/\.gif/ ? 'image/gif'
48             : $file =~ m/\.jpe?g/ ? 'image/jpeg'
49             : $file =~ m/\.png/ ? 'image/png'
50             : 'text/plain'
51             ;
52 17 50       31 $content = '' unless defined '';
53 17         43 $self->{'__daemon_fs'}{"\e$file"} = $type;
54 17         124 \( $self->{'__daemon_fs'}{$file} = $content );
55              
56             } # End of add_to_fs.
57              
58             # ------------------------------------------------
59              
60             sub _arg_h {
61 0   0 0   0 my $class = ref($_[0]) || $_[0];
62 0         0 $_[0]->_arg_V;
63 0         0 print join "\n",
64             "Usage:",
65             " podwebserver = Start podwebserver on localhost:8020. Search \@INC",
66             " podwebserver -p 1234 = Start podwebserver on localhost:1234",
67             " podwebserver -p 1234 -H blorp = Start podwebserver on blorp:1234",
68             " podwebserver -t 3600 = Auto-exit in 1 hour. Default => 18000 (5 hours). 0 => No timeout",
69             " podwebserver -d /path/to/lib = Ignore \@INC, and only search within /path/to/lib",
70             " podwebserver -e /path/to/skip = Exclude /path/to/skip files",
71             " podwebserver -q = Quick startup (but no Table of Contents)",
72             " podwebserver -v = Run with verbose output to STDOUT",
73             " podwebserver -h = See this message",
74             " podwebserver -V = Show version information",
75             "\nRun 'perldoc $class' for more information.",
76             "";
77 0         0 return;
78              
79             } # End of _arg_h.
80              
81             # ------------------------------------------------
82              
83             sub _arg_V {
84 0   0 0   0 my $class = ref($_[0]) || $_[0];
85             #
86             # Anything else particularly useful to report here?
87             #
88 0 0       0 print '', __PACKAGE__, " version $VERSION",
89             # and report if we're running a subclass:
90             (__PACKAGE__ eq $class) ? () : (" ($class)"),
91             "\n",
92             ;
93 0         0 print " Running under perl version $] for $^O",
94             (chr(65) eq 'A') ? "\n" : " in a non-ASCII world\n";
95 0 0 0     0 print " Win32::BuildNumber ", &Win32::BuildNumber(), "\n"
96             if defined(&Win32::BuildNumber) and defined &Win32::BuildNumber();
97 0 0       0 print " MacPerl verison $MacPerl::Version\n"
98             if defined $MacPerl::Version;
99 0         0 return;
100              
101             } # End of _arg_V.
102              
103             # ------------------------------------------------
104              
105 1     1   15 sub _contents_filespec { return '/' } # overriding the superclass's
106              
107             # ------------------------------------------------
108              
109 14 50   14 0 1895 sub filespecsys { $_[0]{'_filespecsys'} || 'File::Spec::Unix' }
110              
111             # ------------------------------------------------
112              
113             sub _get_options {
114 0     0   0 my($self) = shift;
115 0         0 $self->verbose(0);
116 0 0       0 return unless @ARGV;
117 0         0 require Getopt::Std;
118 0         0 my %o;
119              
120 0 0       0 Getopt::Std::getopts( "d:e:H:hp:qt:Vv" => \%o ) || die "Failed to parse options\n";
121              
122             # The 2 switches that shortcut the run:
123 0 0 0     0 $o{'h'} and exit( $self->_arg_h || 0);
124 0 0 0     0 $o{'V'} and exit( $self->_arg_V || 0);
125              
126 0 0 0     0 $self->_arg_h, exit(0) if ($o{p} and ($o{p} !~ /^\d+$/) );
127 0 0 0     0 $self->_arg_h, exit(0) if ($o{t} and ($o{t} !~ /^\d+$/) );
128              
129 0 0       0 $self->dir_exclude( [ map File::Spec->canonpath($_), split(/:|;/, $o{'e'}) ] ) if ($o{'e'});
130 0 0       0 $self->dir_include( [ map File::Spec->canonpath($_), split(/:|;/, $o{'d'}) ] ) if ($o{'d'});
131              
132 0 0       0 $self->httpd_host( $o{'H'} ) if $o{'H'};
133 0 0       0 $self->httpd_port( $o{'p'} ) if $o{'p'};
134 0 0       0 $self->httpd_timeout( $o{'t'} ) if $o{'t'};
135              
136 0 0       0 $self->skip_indexing(1) if $o{'q'};
137 0 0       0 $self->verbose(4) if $o{'v'};
138              
139 0         0 return;
140              
141             } # End of _get_options.
142              
143             # ------------------------------------------------
144              
145             # Run me as: perl -MPod::HTTP -e Pod::Webserver::httpd
146             # or (assuming you have it installed), just run "podwebserver"
147              
148             sub httpd {
149 0 0   0 0 0 my $self = @_ ? shift(@_) : __PACKAGE__;
150 0 0       0 $self = $self->new unless ref $self;
151 0         0 $self->{'_batch_start_time'} = time();
152 0         0 $self->_get_options;
153 0         0 $self->_init_options;
154              
155 0         0 $self->contents_file('/');
156 0         0 $self->prep_for_daemon;
157              
158 0   0     0 my $daemon = $self->new_daemon || return;
159 0         0 my $url = $daemon->url;
160 0 0       0 $url =~ s{//default\b}{//localhost} if $^O =~ m/Win32/; # lame hack
161              
162 0         0 DEBUG > -1 and print "You can now open your browser to $url\n";
163              
164 0         0 return $self->run_daemon($daemon);
165              
166             } # End of httpd.
167              
168             # ------------------------------------------------
169              
170             sub _init_options
171             {
172 0     0   0 my($self) = shift;
173              
174 0         0 $self->dir_exclude([]);
175 0         0 $self->dir_include([@INC]);
176              
177             } # End of _init_options.
178              
179             # ------------------------------------------------
180              
181 1     1 0 3 sub makepath { return } # overriding the superclass's
182              
183             # ------------------------------------------------
184              
185             #sub muse { return 1 }
186              
187             # ------------------------------------------------
188              
189             sub new_daemon {
190 1     1 0 9 my $self = shift;
191              
192 1         2 my @opts;
193              
194 1 50       5 push @opts, LocalHost => $self->httpd_host if (defined $self->httpd_host);
195 1   50     13 push @opts, LocalPort => $self->httpd_port || 8020;
196              
197 1 50       10 if (defined $self->httpd_timeout)
198             {
199 1 50       8 if ($self->httpd_timeout > 0)
200             {
201 1         8 push @opts, Timeout => $self->httpd_timeout;
202             }
203             }
204             else
205             {
206 0         0 push @opts, Timeout => 24 * 3600; # Default to exit after 24 hours of idle time.
207             }
208              
209 1         37 $self->muse( "Starting daemon with options {@opts}" );
210 1 50       22 Pod::Webserver::Daemon->new(@opts) || die "Can't start a daemon: $!\n";
211              
212             } # End of _new_daemon.
213              
214             # ------------------------------------------------
215              
216             sub prep_for_daemon {
217 1     1 0 1113 my($self) = shift;
218              
219 1         21 DEBUG > -1 and print "I am process $$ = perl ", __PACKAGE__, " v$VERSION\n";
220              
221 1         3 $self->{'__daemon_fs'} = {}; # That's where we keep the bodies!!!!
222 1         7 $self->{'__expires_as_http_date'} = time2str(24*3600+time);
223 1         3 $self->{ '__start_as_http_date'} = time2str( time);
224              
225 1         8 $self->add_to_fs( 'robots.txt', 'text/plain', join "\cm\cj",
226             "User-agent: *",
227             "Disallow: /",
228             "", "", "# I am " . __PACKAGE__ . " v$VERSION", "", "",
229             );
230              
231 1         3 $self->add_to_fs( '/', 'text/html',
232             # We get this only when we start up in -q mode:
233             "* Perl Pod server *\n

Example URL: http://whatever/Getopt/Std\n\n"

234             );
235 1         8 $self->_spray_css( '/' );
236 1         61 $self->_spray_javascript( '/' );
237             DEBUG > 5 and print "In FS: ",
238 1         37 join(' ', map qq{"$_"}, sort grep !m/^\e/, keys %{ $self->{'__daemon_fs'} }),
239             "\n";
240              
241 1         6 $self->prep_lookup_table();
242              
243 1         4 return;
244              
245             } # End of prep_for_daemon.
246              
247             # ------------------------------------------------
248              
249             sub prep_lookup_table {
250 1     1 0 1 my $self = shift;
251              
252 1         2 my $m2p;
253              
254 1 50       4 if( $self->skip_indexing ) {
255 0         0 $self->muse("Skipping \@INC indexing.");
256             } else {
257              
258 1 50 33     12 if($self->progress) {
    50          
259 0         0 DEBUG and print "Using existing progress object\n";
260             } elsif( DEBUG or ($self->verbose() >= 1 and $self->verbose() <= 5) ) {
261 0         0 require Pod::Simple::Progress;
262 0         0 $self->progress( Pod::Simple::Progress->new(4) );
263             }
264              
265 1         22 my $search = $Pod::Simple::HTMLBatch::SEARCH_CLASS->new;
266 1         35 my $dir_include = $self->dir_include;
267 1         5 if(DEBUG > -1) {
268 1 50       1 if ($#{$self->dir_include} >= 0) {
  1         3  
269 1         49 print " Indexing all of @$dir_include -- this might take a minute.\n";
270             }
271             else {
272 0         0 print " Indexing all of \@INC -- this might take a minute.\n";
273 0         0 DEBUG > 1 and print "\@INC = [ @INC ]\n";
274             }
275 1         4 $self->{'httpd_has_noted_inc_already'} ++;
276             }
277 1 50       14 $m2p = $self->modnames2paths($dir_include ? $dir_include : undef);
278 1         1295987 $self->progress(0);
279              
280             # Filter out excluded folders
281 1         14 while ( my ($key, $value) = each %$m2p ) {
282 1070         6050 DEBUG > 1 and print "-e $value, ", (grep $value =~ /^\Q$_\E/, @{ $self->dir_exclude }), "\n";
283 1070 50       670 delete $m2p->{$key} if grep $value =~ /^\Q$_\E/, @{ $self->dir_exclude };
  1070         1582  
284             }
285              
286 1 50 33     26 die "Missing path\n" unless $m2p and keys %$m2p;
287              
288 1         72 DEBUG > -1 and print " Done scanning \n";
289              
290 1         960 foreach my $modname (sort keys %$m2p) {
291 1070         17857 my @namelets = split '::', $modname;
292 1070         2208 $self->note_for_contents_file( \@namelets, 'crunkIn', 'crunkOut' );
293             }
294 1         241 $self->write_contents_file('crunkBase');
295             }
296 1   50     33944 $self->{'__modname2path'} = $m2p || {};
297              
298 1         4 return;
299              
300             } # End of prep_lookup_table.
301              
302             # ------------------------------------------------
303              
304             sub run_daemon {
305 0     0 0 0 my($self, $daemon) = @_;
306              
307 0         0 while( my $conn = $daemon->accept ) {
308 0 0       0 if( my $req = $conn->get_request ) {
309             #^^ That used to be a while(... instead of an if( ..., but the
310             # keepalive wasn't working so great, so let's just leave it for now.
311             # It's not like our server here is streaming GIFs or anything.
312              
313 0         0 DEBUG and print "Answering connection at ", localtime()."\n";
314 0         0 $self->_serve_thing($conn, $req);
315             }
316 0         0 $conn->close;
317 0         0 undef($conn);
318             }
319 0         0 $self->muse("HTTP Server terminated");
320 0         0 return;
321              
322             } # End of run_daemon.
323              
324             # ------------------------------------------------
325              
326             sub _serve_pod {
327 1     1   5 my($self, $modname, $filename, $resp) = @_;
328 1 50 33     71 unless( -e $filename and -r _ and -s _ ) { # sanity
      33        
329 0         0 $self->muse( "But filename $filename is no good!" );
330 0         0 return;
331             }
332              
333 1         7 my $modtime = (stat(_))[9]; # use my own modtime whynot!
334 1         14 $resp->content('');
335 1         6 my $contr = $resp->content_ref;
336              
337 1         9 $Pod::Simple::HTMLBatch::HTML_EXTENSION
338             = $Pod::Simple::HTML::HTML_EXTENSION = '';
339              
340 1         6 $resp->header('Last-Modified' => time2str($modtime) );
341              
342 1         2 my $retval;
343 1 50       24 if(
344             # This is totally gross and hacky. So unless your name rhymes
345             # with "Pawn Lurk", you have to cover your eyes right now.
346             $retval =
347             $self->_do_one_batch_conversion(
348             $modname,
349             { $modname => $filename },
350             '/',
351             Pod::Simple::TiedOutFH->handle_on($contr),
352             )
353             ) {
354 1         155794 $self->muse( "$modname < $filename" );
355             } else {
356 0         0 $self->muse( "Ugh, couldn't convert $modname" );
357             }
358              
359 1         28 return $retval;
360              
361             } # End of _serve_pod.
362              
363             # ------------------------------------------------
364              
365             sub _serve_thing {
366 1     1   35 my($self, $conn, $req) = @_;
367 1 50       8 return $conn->send_error(405) unless $req->method eq 'GET'; # sanity
368              
369 1         8 my $path = $req->url;
370 1   50     13 $path .= substr( ($ENV{PATH} ||''), 0, 0); # to force-taint it.
371              
372 1         5 my $fs = $self->{'__daemon_fs'};
373 1         5 my $pods = $self->{'__modname2path'};
374 1         16 my $resp = Pod::Webserver::Response->new(200);
375 1   50     30 $resp->content_type( $fs->{"\e$path"} || 'text/html' );
376              
377 1         4 $path =~ s{:+}{/}g;
378 1         3 my $modname = $path;
379 1         11 $modname =~ s{/+}{::}g; $modname =~ s{^:+}{};
  1         4  
380 1         5 $modname =~ s{:+$}{}; $modname =~ s{:+$}{::}g;
  1         6  
381 1 50       20 if( $modname =~ m{^([a-zA-Z0-9_]+(?:::[a-zA-Z0-9_]+)*)$}s ) {
382 1         5 $modname = $1; # thus untainting
383             } else {
384 0         0 $modname = '';
385             }
386 1         3 DEBUG > 1 and print "Modname $modname ($path)\n";
387              
388 1 50       12 if( $fs->{$path} ) { # Is it in our mini-filesystem?
    50          
    50          
389 0         0 $resp->content( $fs->{$path} );
390 0         0 $resp->header( 'Last-Modified' => $self->{ '__start_as_http_date'} );
391 0         0 $resp->header( 'Expires' => $self->{'__expires_as_http_date'} );
392 0         0 $self->muse("Serving pre-cooked $path");
393             } elsif( $modname eq '' ) {
394 0         0 $resp = '';
395              
396             # After here, it's only untainted module names
397             } elsif( $pods->{$modname} ) { # Is it known pod?
398             #$self->muse("I know $modname as ", $pods->{$modname});
399 1 50       9 $self->_serve_pod( $modname, $pods->{$modname}, $resp ) or $resp = '';
400              
401             } else {
402             # If it's not known, look for it.
403             # This is necessary for indexless mode, and also useful just in case
404             # the user has just installed a new module (after the index was generated)
405 0         0 my $fspath = $Pod::Simple::HTMLBatch::SEARCH_CLASS->new->find($modname);
406              
407 0 0       0 if( defined($fspath) ) {
408             #$self->muse("Found $modname as $fspath");
409 0         0 $self->_serve_pod( $modname, $fspath, $resp );
410             } else {
411 0         0 $resp = '';
412 0         0 $self->muse("Can't find $modname in \@INC");
413 0 0       0 unless( $self->{'httpd_has_noted_inc_already'} ++ ) {
414 0         0 $self->muse(" \@INC = [ @INC ]");
415             }
416             }
417             }
418              
419 1 50       14 $resp ? $conn->send_response( $resp ) : $conn->send_error(404);
420              
421 1         13 return;
422              
423             } # End of _serve_thing.
424              
425             # ------------------------------------------------
426              
427             sub _wopen { # overriding the superclass's
428 15     15   7377 my($self, $outpath) = @_;
429              
430 15         31 return Pod::Simple::TiedOutFH->handle_on( $self->add_to_fs($outpath) );
431              
432             } # End of _wopen.
433              
434             # ------------------------------------------------
435              
436             sub write_contents_file {
437 1     1 0 7 my $self = shift;
438 1         8 $Pod::Simple::HTMLBatch::HTML_EXTENSION
439             = $Pod::Simple::HTML::HTML_EXTENSION = '';
440              
441 1         22 return $self->SUPER::write_contents_file(@_);
442              
443             } # End of write_contents_file.
444              
445             # ------------------------------------------------
446              
447 2     2 0 1539 sub url_up_to_contents { return '/' } # overriding the superclass's
448              
449             # ------------------------------------------------
450              
451             # Inlined from HTTP::Date to avoid a dependency
452              
453             {
454             my @DoW = qw(Sun Mon Tue Wed Thu Fri Sat);
455             my @MoY = qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec);
456              
457             sub time2str (;$) {
458 9     9 0 566 my $time = shift;
459 9         151 my ($sec, $min, $hour, $mday, $mon, $year, $wday) = gmtime($time);
460 9         128 sprintf("%s, %02d %s %04d %02d:%02d:%02d GMT",
461             $DoW[$wday],
462             $mday, $MoY[$mon], $year+1900,
463             $hour, $min, $sec);
464             }
465             }
466              
467             # ------------------------------------------------
468              
469             __PACKAGE__->Pod::Simple::_accessorize(
470             'dir_include',
471             'dir_exclude',
472             'httpd_port',
473             'httpd_host',
474             'httpd_timeout',
475             'skip_indexing',
476             );
477              
478             httpd() unless caller;
479              
480             # ------------------------------------------------
481              
482             1;
483              
484             __END__