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   140778 use parent 'Pod::Simple::HTMLBatch';
  3         1122  
  3         19  
4 3     3   304869 use strict;
  3         6  
  3         127  
5 3     3   18 use vars qw( $VERSION @ISA );
  3         14  
  3         181  
6              
7 3     3   2338 use Pod::Webserver::Daemon;
  3         10  
  3         115  
8 3     3   1861 use Pod::Webserver::Response;
  3         8  
  3         86  
9              
10 3     3   19 use Pod::Simple::HTMLBatch;
  3         6  
  3         78  
11 3     3   3371 use Pod::Simple::TiedOutFH;
  3         5937  
  3         97  
12 3     3   23 use Pod::Simple;
  3         6  
  3         79  
13 3     3   2145 use IO::Socket;
  3         54820  
  3         19  
14 3     3   2350 use File::Spec;
  3         15  
  3         89  
15 3     3   18 use File::Spec::Unix ();
  3         8  
  3         556  
16              
17             our $VERSION = '3.09';
18              
19             # ------------------------------------------------
20              
21             BEGIN {
22 3 50 0 3   22 if(defined &DEBUG) { } # no-op
    50          
    0          
23 3         18009 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 34 my($self,$file,$type,$content) = @_;
38              
39 17 50 33     82 die "Missing filespec\n" unless defined $file and length $file;
40 17         34 $file = "/$file";
41 17         67 $file =~ s{/+}{/}s;
42 17 50 66     152 $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       34 $content = '' unless defined '';
53 17         63 $self->{'__daemon_fs'}{"\e$file"} = $type;
54 17         129 \( $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   17 sub _contents_filespec { return '/' } # overriding the superclass's
106              
107             # ------------------------------------------------
108              
109 14 50   14 0 2263 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 14 my $self = shift;
191              
192 1         3 my @opts;
193              
194 1 50       8 push @opts, LocalHost => $self->httpd_host if (defined $self->httpd_host);
195 1   50     17 push @opts, LocalPort => $self->httpd_port || 8020;
196              
197 1 50       22 if (defined $self->httpd_timeout)
198             {
199 1 50       13 if ($self->httpd_timeout > 0)
200             {
201 1         10 push @opts, Timeout => $self->httpd_timeout;
202             }
203             }
204             else
205             {
206 0         0 push @opts, Timeout => 5 * 3600; # Default to exit after 5 hours of idle time.
207             }
208              
209 1         39 $self->muse( "Starting daemon with options {@opts}" );
210 1 50       35 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 1597 my($self) = shift;
218              
219 1         228 DEBUG > -1 and print "I am process $$ = perl ", __PACKAGE__, " v$VERSION\n";
220              
221 1         4 $self->{'__daemon_fs'} = {}; # That's where we keep the bodies!!!!
222 1         9 $self->{'__expires_as_http_date'} = time2str(24*3600+time);
223 1         3 $self->{ '__start_as_http_date'} = time2str( time);
224              
225 1         11 $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         5 $self->add_to_fs( '/', 'text/html',
232             # We get this only when we start up in -q mode:
233             "* Perl Pod server *\n<p>Example URL: http://whatever/Getopt/Std\n\n"
234             );
235 1         11 $self->_spray_css( '/' );
236 1         41 $self->_spray_javascript( '/' );
237             DEBUG > 5 and print "In FS: ",
238 1         47 join(' ', map qq{"$_"}, sort grep !m/^\e/, keys %{ $self->{'__daemon_fs'} }),
239             "\n";
240              
241 1         5 $self->prep_lookup_table();
242              
243 1         8 return;
244              
245             } # End of prep_for_daemon.
246              
247             # ------------------------------------------------
248              
249             sub prep_lookup_table {
250 1     1 0 2 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     13 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         26 my $search = $Pod::Simple::HTMLBatch::SEARCH_CLASS->new;
266 1         40 my $dir_include = $self->dir_include;
267 1         6 if(DEBUG > -1) {
268 1 50       7 if ($#{$self->dir_include} >= 0) {
  1         4  
269 1         202 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         5 $self->{'httpd_has_noted_inc_already'} ++;
276             }
277 1 50       12 $m2p = $self->modnames2paths($dir_include ? $dir_include : undef);
278 1         3218396 $self->progress(0);
279              
280             # Filter out excluded folders
281 1         15 while ( my ($key, $value) = each %$m2p ) {
282 1069         9152 DEBUG > 1 and print "-e $value, ", (grep $value =~ /^\Q$_\E/, @{ $self->dir_exclude }), "\n";
283 1069 50       1162 delete $m2p->{$key} if grep $value =~ /^\Q$_\E/, @{ $self->dir_exclude };
  1069         2470  
284             }
285              
286 1 50 33     28 die "Missing path\n" unless $m2p and keys %$m2p;
287              
288 1         317 DEBUG > -1 and print " Done scanning \n";
289              
290 1         1191 foreach my $modname (sort keys %$m2p) {
291 1069         22086 my @namelets = split '::', $modname;
292 1069         3197 $self->note_for_contents_file( \@namelets, 'crunkIn', 'crunkOut' );
293             }
294 1         198 $self->write_contents_file('crunkBase');
295             }
296 1   50     48345 $self->{'__modname2path'} = $m2p || {};
297              
298 1         3 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   9 my($self, $modname, $filename, $resp) = @_;
328 1 50 33     85 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         10 my $modtime = (stat(_))[9]; # use my own modtime whynot!
334 1         11 $resp->content('');
335 1         5 my $contr = $resp->content_ref;
336              
337 1         16 $Pod::Simple::HTMLBatch::HTML_EXTENSION
338             = $Pod::Simple::HTML::HTML_EXTENSION = '';
339              
340 1         10 $resp->header('Last-Modified' => time2str($modtime) );
341              
342 1         2 my $retval;
343 1 50       28 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         138716 $self->muse( "$modname < $filename" );
355             } else {
356 0         0 $self->muse( "Ugh, couldn't convert $modname" );
357             }
358              
359 1         29 return $retval;
360              
361             } # End of _serve_pod.
362              
363             # ------------------------------------------------
364              
365             sub _serve_thing {
366 1     1   49 my($self, $conn, $req) = @_;
367 1 50       11 return $conn->send_error(405) unless $req->method eq 'GET'; # sanity
368              
369 1         10 my $path = $req->url;
370 1   50     17 $path .= substr( ($ENV{PATH} ||''), 0, 0); # to force-taint it.
371              
372 1         8 my $fs = $self->{'__daemon_fs'};
373 1         7 my $pods = $self->{'__modname2path'};
374 1         24 my $resp = Pod::Webserver::Response->new(200);
375 1   50     368 $resp->content_type( $fs->{"\e$path"} || 'text/html' );
376              
377 1         5 $path =~ s{:+}{/}g;
378 1         3 my $modname = $path;
379 1         16 $modname =~ s{/+}{::}g; $modname =~ s{^:+}{};
  1         6  
380 1         8 $modname =~ s{:+$}{}; $modname =~ s{:+$}{::}g;
  1         7  
381 1 50       27 if( $modname =~ m{^([a-zA-Z0-9_]+(?:::[a-zA-Z0-9_]+)*)$}s ) {
382 1         7 $modname = $1; # thus untainting
383             } else {
384 0         0 $modname = '';
385             }
386 1         2 DEBUG > 1 and print "Modname $modname ($path)\n";
387              
388 1 50       15 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       12 $resp ? $conn->send_response( $resp ) : $conn->send_error(404);
420              
421 1         9 return;
422              
423             } # End of _serve_thing.
424              
425             # ------------------------------------------------
426              
427             sub _wopen { # overriding the superclass's
428 15     15   7535 my($self, $outpath) = @_;
429              
430 15         48 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 9 my $self = shift;
438 1         10 $Pod::Simple::HTMLBatch::HTML_EXTENSION
439             = $Pod::Simple::HTML::HTML_EXTENSION = '';
440              
441 1         20 return $self->SUPER::write_contents_file(@_);
442              
443             } # End of write_contents_file.
444              
445             # ------------------------------------------------
446              
447 2     2 0 1917 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 764 my $time = shift;
459 9         59 my ($sec, $min, $hour, $mday, $mon, $year, $wday) = gmtime($time);
460 9         110 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__
485              
486             =head1 NAME
487              
488             Pod::Webserver -- Minimal web server for local Perl documentation
489              
490             =head1 SYNOPSIS
491              
492             % podwebserver
493             ...
494             You can now open your browser to http://localhost:8020/
495              
496             =head1 DESCRIPTION
497              
498             This module can be run as an application that works as a
499             minimal web server to serve local Perl documentation. It's like
500             L<perldoc> except it works through your browser.
501              
502             C<podwebserver -h> displays help:
503              
504             Pod::Webserver version 3.06
505             Running under perl version 5.018002 for linux
506             Usage:
507             podwebserver = Start podwebserver on localhost:8020. Search @INC
508             podwebserver -p 1234 = Start podwebserver on localhost:1234
509             podwebserver -p 1234 -H blorp = Start podwebserver on blorp:1234
510             podwebserver -t 3600 = Auto-exit in 1 hour. Default => 18000 (5 hours). 0 => No timeout
511             podwebserver -d /path/to/lib = Ignore @INC, and only search within /path/to/lib
512             podwebserver -e /path/to/skip = Exclude /path/to/skip files
513             podwebserver -q = Quick startup (but no Table of Contents)
514             podwebserver -v = Run with verbose output to STDOUT
515             podwebserver -h = See this message
516             podwebserver -V = Show version information
517              
518             Run 'perldoc Pod::Webserver' for more information.
519              
520             =head1 SECURITY (AND @INC)
521              
522             Pod::Webserver is not what you'd call a gaping security hole --
523             after all, all it does and could possibly do is serve HTML
524             versions of anything you could get by typing "perldoc
525             SomeModuleName". Pod::Webserver won't serve files at
526             arbitrary paths or anything.
527              
528             But do consider whether you're revealing anything by
529             basically showing off what versions of modules you've got
530             installed; and also consider whether you could be revealing
531             any proprietary or in-house module documentation.
532              
533             And also consider that this exposes the documentation
534             of modules (i.e., any Perl files that at all look like
535             modules) in your @INC dirs -- and your @INC probably
536             contains "."! If your current working directory could
537             contain modules I<whose Pod> you don't
538             want anyone to see, then you could do two things:
539             The cheap and easy way is to just chdir to an
540             uninteresting directory:
541              
542             mkdir ~/.empty; cd ~/.empty; podwebserver
543              
544             The more careful approach is to run podwebserver
545             under perl in -T (taint) mode (as explained in
546             L<perlsec>), and to explicitly specify what extra
547             directories you want in @INC, like so:
548              
549             perl -T -Isomepath -Imaybesomeotherpath -S podwebserver
550              
551             You can also use the -I trick (that's a capital "igh",
552             not a lowercase "ell") to add dirs to @INC even
553             if you're not using -T. For example:
554              
555             perl -I/that/thar/Module-Stuff-0.12/lib -S podwebserver
556              
557             An alternate approach is to use your shell's
558             environment-setting commands to alter PERL5LIB or
559             PERLLIB before starting podwebserver.
560              
561             These -T and -I switches are explained in L<perlrun>. But I'll note in
562             passing that you'll likely need to do this to get your PERLLIB
563             environment variable to be in @INC...
564              
565             perl -T -I$PERLLIB -S podwebserver
566              
567             (Or replacing that with PERL5LIB, if that's what you use.)
568              
569              
570             =head2 ON INDEXING '.' IN @INC
571              
572             Pod::Webserver uses the module Pod::Simple::Search to build the index
573             page you see at http://yourservername:8020/ (or whatever port you
574             choose instead of 8020). That module's indexer has one notable DWIM
575             feature: it reads over @INC, except that it skips the "." in @INC. But
576             you can work around this by expressing the current directory in some
577             other way than as just the single literal period -- either as some
578             more roundabout way, like so:
579              
580             perl -I./. -S podwebserver
581              
582             Or by just expressing the current directory absolutely:
583              
584             perl -I`pwd` -S podwebserver
585              
586             Note that even when "." isn't indexed, the Pod in files under it are
587             still accessible -- just as if you'd typed "perldoc whatever" and got
588             the Pod in F<./whatever.pl>
589              
590             =head1 SEE ALSO
591              
592             This module is implemented using many CPAN modules,
593             including: L<Pod::Simple::HTMLBatch> L<Pod::Simple::HTML>
594             L<Pod::Simple::Search> L<Pod::Simple>
595              
596             See also L<Pod::Perldoc> and L<http://search.cpan.org/>
597              
598             =head1 COPYRIGHT AND DISCLAIMERS
599              
600             Copyright (c) 2004-2006 Sean M. Burke. All rights reserved.
601              
602             This library is free software; you can redistribute it and/or modify it
603             under the same terms as Perl itself.
604              
605             This program is distributed in the hope that it will be useful, but
606             without any warranty; without even the implied warranty of
607             merchantability or fitness for a particular purpose.
608              
609             =head1 AUTHOR
610              
611             Original author: Sean M. Burke C<sburke@cpan.org>.
612              
613             Maintained by: Allison Randal C<allison@perl.org> and Ron Savage C<ron@savage.net.au>.
614              
615             =cut
616              
617