version 0.31
[catagits/Catalyst-Plugin-Static-Simple.git] / lib / Catalyst / Plugin / Static / Simple.pm
CommitLineData
d6d29b9b 1package Catalyst::Plugin::Static::Simple;
2
d925e93e 3use Moose::Role;
d6d29b9b 4use File::stat;
bdf5afa1 5use File::Spec ();
6use IO::File ();
7use MIME::Types ();
151b8e0f 8use MooseX::Types::Moose qw/ArrayRef Str/;
ee1e7faf 9use Catalyst::Utils;
aa5935f1 10use namespace::autoclean;
d6d29b9b 11
9e2ff944 12our $VERSION = '0.31';
d6d29b9b 13
d925e93e 14has _static_file => ( is => 'rw' );
151b8e0f 15has _static_debug_message => ( is => 'rw', isa => ArrayRef[Str] );
d6d29b9b 16
d925e93e 17before prepare_action => sub {
d6d29b9b 18 my $c = shift;
d6d29b9b 19 my $path = $c->req->path;
ee1e7faf 20 my $config = $c->config->{'Plugin::Static::Simple'};
b108737b 21
792411e6 22 $path =~ s/%([0-9A-Fa-f]{2})/chr(hex($1))/eg;
b1d96e3e 23
d6d29b9b 24 # is the URI in a static-defined path?
bdf5afa1 25 foreach my $dir ( @{ $config->{dirs} } ) {
9d557523 26 my $dir_re = quotemeta $dir;
b108737b 27
0495a293 28 # strip trailing slashes, they'll be added in our regex
29 $dir_re =~ s{/$}{};
b108737b 30
a15abf71 31 my $re;
32
33 if ( $dir =~ m{^qr/}xms ) {
34 $re = eval $dir;
35
36 if ($@) {
37 $c->error( "Error compiling static dir regex '$dir': $@" );
38 }
b1d96e3e 39 }
a15abf71 40 else {
41 $re = qr{^${dir_re}/};
42 }
43
d6d29b9b 44 if ( $path =~ $re ) {
c8ee8fd2 45 if ( $c->_locate_static_file( $path, 1 ) ) {
b06be085 46 $c->_debug_msg( 'from static directory' )
bdf5afa1 47 if $config->{debug};
d6d29b9b 48 } else {
49 $c->_debug_msg( "404: file not found: $path" )
bdf5afa1 50 if $config->{debug};
d6d29b9b 51 $c->res->status( 404 );
0495a293 52 $c->res->content_type( 'text/html' );
d6d29b9b 53 }
54 }
55 }
b108737b 56
d6d29b9b 57 # Does the path have an extension?
b1d96e3e 58 if ( $path =~ /.*\.(\S{1,})$/xms ) {
d6d29b9b 59 # and does it exist?
792411e6 60 $c->_locate_static_file( $path );
d6d29b9b 61 }
d925e93e 62};
b108737b 63
7c97dd21 64around dispatch => sub {
65 my $orig = shift;
b1d96e3e 66 my $c = shift;
b108737b 67
2268e329 68 return if ( $c->res->status != 200 );
b108737b 69
b1d96e3e 70 if ( $c->_static_file ) {
ee1e7faf 71 if ( $c->config->{'Plugin::Static::Simple'}->{no_logs} && $c->log->can('abort') ) {
a28d35e9 72 $c->log->abort( 1 );
73 }
b1d96e3e 74 return $c->_serve_static;
75 }
76 else {
7c97dd21 77 return $c->$orig(@_);
b1d96e3e 78 }
d925e93e 79};
b1d96e3e 80
d925e93e 81before finalize => sub {
d6d29b9b 82 my $c = shift;
b108737b 83
d6d29b9b 84 # display all log messages
ee1e7faf 85 if ( $c->config->{'Plugin::Static::Simple'}->{debug} && scalar @{$c->_debug_msg} ) {
be327929 86 $c->log->debug( 'Static::Simple: ' . join q{ }, @{$c->_debug_msg} );
d6d29b9b 87 }
d925e93e 88};
d6d29b9b 89
aa5935f1 90before setup_finalize => sub {
d6d29b9b 91 my $c = shift;
b108737b 92
ee1e7faf 93 $c->log->warn("Deprecated 'static' config key used, please use the key 'Plugin::Static::Simple' instead")
94 if exists $c->config->{static};
95 my $config
96 = $c->config->{'Plugin::Static::Simple'}
97 = $c->config->{'static'}
98 = Catalyst::Utils::merge_hashes(
99 $c->config->{'Plugin::Static::Simple'} || {},
100 $c->config->{static} || {}
101 );
b108737b 102
bdf5afa1 103 $config->{dirs} ||= [];
104 $config->{include_path} ||= [ $c->config->{root} ];
105 $config->{mime_types} ||= {};
106 $config->{ignore_extensions} ||= [ qw/tmpl tt tt2 html xhtml/ ];
107 $config->{ignore_dirs} ||= [];
108 $config->{debug} ||= $c->debug;
109 $config->{no_logs} = 1 unless defined $config->{no_logs};
6a009cf0 110 $config->{no_logs} = 0 if $config->{logging};
b108737b 111
d6d29b9b 112 # load up a MIME::Types object, only loading types with
113 # at least 1 file extension
bdf5afa1 114 $config->{mime_types_obj} = MIME::Types->new( only_complete => 1 );
aa5935f1 115};
d6d29b9b 116
117# Search through all included directories for the static file
118# Based on Template Toolkit INCLUDE_PATH code
119sub _locate_static_file {
c8ee8fd2 120 my ( $c, $path, $in_static_dir ) = @_;
b108737b 121
bdf5afa1 122 $path = File::Spec->catdir(
b108737b 123 File::Spec->no_upwards( File::Spec->splitdir( $path ) )
bdf5afa1 124 );
b108737b 125
ee1e7faf 126 my $config = $c->config->{'Plugin::Static::Simple'};
bdf5afa1 127 my @ipaths = @{ $config->{include_path} };
d6d29b9b 128 my $dpaths;
129 my $count = 64; # maximum number of directories to search
b108737b 130
8cc672a2 131 DIR_CHECK:
d6d29b9b 132 while ( @ipaths && --$count) {
8cc672a2 133 my $dir = shift @ipaths || next DIR_CHECK;
b108737b 134
d6d29b9b 135 if ( ref $dir eq 'CODE' ) {
136 eval { $dpaths = &$dir( $c ) };
137 if ($@) {
b06be085 138 $c->log->error( 'Static::Simple: include_path error: ' . $@ );
d6d29b9b 139 } else {
b06be085 140 unshift @ipaths, @$dpaths;
8cc672a2 141 next DIR_CHECK;
d6d29b9b 142 }
143 } else {
48791b66 144 $dir =~ s/(\/|\\)$//xms;
d6d29b9b 145 if ( -d $dir && -f $dir . '/' . $path ) {
b108737b 146
c8ee8fd2 147 # Don't ignore any files in static dirs defined with 'dirs'
148 unless ( $in_static_dir ) {
149 # do we need to ignore the file?
150 for my $ignore ( @{ $config->{ignore_dirs} } ) {
151 $ignore =~ s{(/|\\)$}{};
152 if ( $path =~ /^$ignore(\/|\\)/ ) {
153 $c->_debug_msg( "Ignoring directory `$ignore`" )
154 if $config->{debug};
155 next DIR_CHECK;
156 }
8cc672a2 157 }
b108737b 158
c8ee8fd2 159 # do we need to ignore based on extension?
160 for my $ignore_ext ( @{ $config->{ignore_extensions} } ) {
161 if ( $path =~ /.*\.${ignore_ext}$/ixms ) {
162 $c->_debug_msg( "Ignoring extension `$ignore_ext`" )
163 if $config->{debug};
164 next DIR_CHECK;
165 }
bdf5afa1 166 }
8cc672a2 167 }
b108737b 168
8cc672a2 169 $c->_debug_msg( 'Serving ' . $dir . '/' . $path )
bdf5afa1 170 if $config->{debug};
d6d29b9b 171 return $c->_static_file( $dir . '/' . $path );
172 }
173 }
174 }
b108737b 175
2268e329 176 return;
d6d29b9b 177}
178
d6d29b9b 179sub _serve_static {
180 my $c = shift;
ee1e7faf 181 my $config = $c->config->{'Plugin::Static::Simple'};
b108737b 182
ab02ca0d 183 my $full_path = shift || $c->_static_file;
792411e6 184 my $type = $c->_ext_to_type( $full_path );
185 my $stat = stat $full_path;
d6d29b9b 186
d6d29b9b 187 $c->res->headers->content_type( $type );
188 $c->res->headers->content_length( $stat->size );
189 $c->res->headers->last_modified( $stat->mtime );
41cac5ef 190 # Tell Firefox & friends its OK to cache, even over SSL:
191 $c->res->headers->header('Cache-control' => 'public');
192 # Optionally, set a fixed expiry time:
193 if ($config->{expires}) {
194 $c->res->headers->expires(time() + $config->{expires});
195 }
2cb3d585 196
b108737b 197 my $fh = IO::File->new( $full_path, 'r' );
198 if ( defined $fh ) {
199 binmode $fh;
200 $c->res->body( $fh );
2cb3d585 201 }
202 else {
b108737b 203 Catalyst::Exception->throw(
204 message => "Unable to open $full_path for reading" );
2cb3d585 205 }
b108737b 206
b1d96e3e 207 return 1;
208}
209
ab02ca0d 210sub serve_static_file {
211 my ( $c, $full_path ) = @_;
212
ee1e7faf 213 my $config = $c->config->{'Plugin::Static::Simple'};
b108737b 214
ab02ca0d 215 if ( -e $full_path ) {
216 $c->_debug_msg( "Serving static file: $full_path" )
217 if $config->{debug};
218 }
219 else {
220 $c->_debug_msg( "404: file not found: $full_path" )
221 if $config->{debug};
222 $c->res->status( 404 );
0495a293 223 $c->res->content_type( 'text/html' );
ab02ca0d 224 return;
225 }
226
227 $c->_serve_static( $full_path );
228}
229
b1d96e3e 230# looks up the correct MIME type for the current file extension
231sub _ext_to_type {
792411e6 232 my ( $c, $full_path ) = @_;
b108737b 233
ee1e7faf 234 my $config = $c->config->{'Plugin::Static::Simple'};
b108737b 235
792411e6 236 if ( $full_path =~ /.*\.(\S{1,})$/xms ) {
b1d96e3e 237 my $ext = $1;
b108737b 238 my $type = $config->{mime_types}{$ext}
bdf5afa1 239 || $config->{mime_types_obj}->mimeTypeOf( $ext );
2268e329 240 if ( $type ) {
bdf5afa1 241 $c->_debug_msg( "as $type" ) if $config->{debug};
5224ce15 242 return ( ref $type ) ? $type->type : $type;
b1d96e3e 243 }
244 else {
245 $c->_debug_msg( "as text/plain (unknown extension $ext)" )
bdf5afa1 246 if $config->{debug};
b1d96e3e 247 return 'text/plain';
248 }
249 }
250 else {
251 $c->_debug_msg( 'as text/plain (no extension)' )
bdf5afa1 252 if $config->{debug};
b1d96e3e 253 return 'text/plain';
254 }
d6d29b9b 255}
256
257sub _debug_msg {
258 my ( $c, $msg ) = @_;
b108737b 259
2268e329 260 if ( !defined $c->_static_debug_message ) {
261 $c->_static_debug_message( [] );
b1d96e3e 262 }
b108737b 263
b1d96e3e 264 if ( $msg ) {
2268e329 265 push @{ $c->_static_debug_message }, $msg;
b1d96e3e 266 }
b108737b 267
2268e329 268 return $c->_static_debug_message;
d6d29b9b 269}
b1d96e3e 270
2711;
272__END__
273
274=head1 NAME
275
276Catalyst::Plugin::Static::Simple - Make serving static pages painless.
277
278=head1 SYNOPSIS
279
b6fdf01d 280 package MyApp;
281 use Catalyst qw/ Static::Simple /;
282 MyApp->setup;
b648683e 283 # that's it; static content is automatically served by Catalyst
284 # from the application's root directory, though you can configure
285 # things or bypass Catalyst entirely in a production environment
286 #
287 # one caveat: the files must be served from an absolute path
6e89d83c 288 # (i.e. /images/foo.png)
b1d96e3e 289
290=head1 DESCRIPTION
291
bc5b1283 292The Static::Simple plugin is designed to make serving static content in
293your application during development quick and easy, without requiring a
294single line of code from you.
b1d96e3e 295
bc5b1283 296This plugin detects static files by looking at the file extension in the
297URL (such as B<.css> or B<.png> or B<.js>). The plugin uses the
298lightweight L<MIME::Types> module to map file extensions to
299IANA-registered MIME types, and will serve your static files with the
300correct MIME type directly to the browser, without being processed
301through Catalyst.
b1d96e3e 302
303Note that actions mapped to paths using periods (.) will still operate
304properly.
305
200e206c 306If the plugin can not find the file, the request is dispatched to your
b108737b 307application instead. This means you are responsible for generating a
200e206c 308C<404> error if your applicaton can not process the request:
309
310 # handled by static::simple, not dispatched to your application
311 /images/exists.png
aa5935f1 312
200e206c 313 # static::simple will not find the file and let your application
314 # handle the request. You are responsible for generating a file
315 # or returning a 404 error
316 /images/does_not_exist.png
317
bc5b1283 318Though Static::Simple is designed to work out-of-the-box, you can tweak
319the operation by adding various configuration options. In a production
320environment, you will probably want to use your webserver to deliver
321static content; for an example see L<USING WITH APACHE>, below.
322
41cac5ef 323=head1 DEFAULT BEHAVIOUR
bc5b1283 324
325By default, Static::Simple will deliver all files having extensions
326(that is, bits of text following a period (C<.>)), I<except> files
327having the extensions C<tmpl>, C<tt>, C<tt2>, C<html>, and
328C<xhtml>. These files, and all files without extensions, will be
329processed through Catalyst. If L<MIME::Types> doesn't recognize an
330extension, it will be served as C<text/plain>.
331
332To restate: files having the extensions C<tmpl>, C<tt>, C<tt2>, C<html>,
333and C<xhtml> I<will not> be served statically by default, they will be
334processed by Catalyst. Thus if you want to use C<.html> files from
335within a Catalyst app as static files, you need to change the
336configuration of Static::Simple. Note also that files having any other
337extension I<will> be served statically, so if you're using any other
338extension for template files, you should also change the configuration.
339
340Logging of static files is turned off by default.
b1d96e3e 341
342=head1 ADVANCED CONFIGURATION
343
bc5b1283 344Configuration is completely optional and is specified within
fa25e422 345C<MyApp-E<gt>config-E<gt>{Plugin::Static::Simple}>. If you use any of these options,
86880b0d 346this module will probably feel less "simple" to you!
b1d96e3e 347
bc5b1283 348=head2 Enabling request logging
2de14076 349
bc5b1283 350Since Catalyst 5.50, logging of static requests is turned off by
351default; static requests tend to clutter the log output and rarely
352reveal anything useful. However, if you want to enable logging of static
353requests, you can do so by setting
fa25e422 354C<MyApp-E<gt>config-E<gt>{Plugin::Static::Simple}-E<gt>{logging}> to 1.
2de14076 355
2268e329 356=head2 Forcing directories into static mode
b1d96e3e 357
bc5b1283 358Define a list of top-level directories beneath your 'root' directory
359that should always be served in static mode. Regular expressions may be
360specified using C<qr//>.
b1d96e3e 361
a5d909f1 362 MyApp->config(
fa25e422 363 'Plugin::Static::Simple' => {
a5d909f1 364 dirs => [
365 'static',
366 qr/^(images|css)/,
367 ],
368 }
369 );
b1d96e3e 370
fa43d6b5 371=head2 Including additional directories
b1d96e3e 372
373You may specify a list of directories in which to search for your static
bc5b1283 374files. The directories will be searched in order and will return the
375first file found. Note that your root directory is B<not> automatically
376added to the search path when you specify an C<include_path>. You should
377use C<MyApp-E<gt>config-E<gt>{root}> to add it.
b1d96e3e 378
a5d909f1 379 MyApp->config(
fa25e422 380 'Plugin::Static::Simple' => {
a5d909f1 381 include_path => [
382 '/path/to/overlay',
383 \&incpath_generator,
384 MyApp->config->{root},
385 ],
386 },
387 );
b108737b 388
bc5b1283 389With the above setting, a request for the file C</images/logo.jpg> will search
b1d96e3e 390for the following files, returning the first one found:
391
392 /path/to/overlay/images/logo.jpg
393 /dynamic/path/images/logo.jpg
394 /your/app/home/root/images/logo.jpg
b108737b 395
b1d96e3e 396The include path can contain a subroutine reference to dynamically return a
bc5b1283 397list of available directories. This method will receive the C<$c> object as a
b1d96e3e 398parameter and should return a reference to a list of directories. Errors can
bc5b1283 399be reported using C<die()>. This method will be called every time a file is
b1d96e3e 400requested that appears to be a static file (i.e. it has an extension).
401
402For example:
403
404 sub incpath_generator {
405 my $c = shift;
a5d909f1 406
b1d96e3e 407 if ( $c->session->{customer_dir} ) {
408 return [ $c->session->{customer_dir} ];
409 } else {
410 die "No customer dir defined.";
411 }
412 }
b108737b 413
8cc672a2 414=head2 Ignoring certain types of files
415
bc5b1283 416There are some file types you may not wish to serve as static files.
417Most important in this category are your raw template files. By
418default, files with the extensions C<tmpl>, C<tt>, C<tt2>, C<html>, and
419C<xhtml> will be ignored by Static::Simple in the interest of security.
420If you wish to define your own extensions to ignore, use the
421C<ignore_extensions> option:
8cc672a2 422
a5d909f1 423 MyApp->config(
fa25e422 424 'Plugin::Static::Simple' => {
a5d909f1 425 ignore_extensions => [ qw/html asp php/ ],
426 },
427 );
b108737b 428
8cc672a2 429=head2 Ignoring entire directories
430
bc5b1283 431To prevent an entire directory from being served statically, you can use
432the C<ignore_dirs> option. This option contains a list of relative
433directory paths to ignore. If using C<include_path>, the path will be
434checked against every included path.
8cc672a2 435
a5d909f1 436 MyApp->config(
fa25e422 437 'Plugin::Static::Simple' => {
a5d909f1 438 ignore_dirs => [ qw/tmpl css/ ],
439 },
440 );
b108737b 441
bc5b1283 442For example, if combined with the above C<include_path> setting, this
443C<ignore_dirs> value will ignore the following directories if they exist:
8cc672a2 444
445 /path/to/overlay/tmpl
446 /path/to/overlay/css
447 /dynamic/path/tmpl
448 /dynamic/path/css
449 /your/app/home/root/tmpl
b108737b 450 /your/app/home/root/css
b1d96e3e 451
2268e329 452=head2 Custom MIME types
b1d96e3e 453
bc5b1283 454To override or add to the default MIME types set by the L<MIME::Types>
455module, you may enter your own extension to MIME type mapping.
b1d96e3e 456
a5d909f1 457 MyApp->config(
fa25e422 458 'Plugin::Static::Simple' => {
a5d909f1 459 mime_types => {
460 jpg => 'image/jpg',
461 png => 'image/png',
462 },
463 },
464 );
2268e329 465
41cac5ef 466=head2 Controlling caching with Expires header
467
468The files served by Static::Simple will have a Last-Modified header set,
469which allows some browsers to cache them for a while. However if you want
470to explicitly set an Expires header, such as to allow proxies to cache your
471static content, then you can do so by setting the "expires" config option.
472
473The value indicates the number of seconds after access time to allow caching.
474So a value of zero really means "don't cache at all", and any higher values
475will keep the file around for that long.
476
477 MyApp->config(
fa25e422 478 'Plugin::Static::Simple' => {
41cac5ef 479 expires => 3600, # Caching allowed for one hour.
480 },
481 );
482
d38d0ed6 483=head2 Compatibility with other plugins
b1d96e3e 484
d38d0ed6 485Since version 0.12, Static::Simple plays nice with other plugins. It no
bc5b1283 486longer short-circuits the C<prepare_action> stage as it was causing too
487many compatibility issues with other plugins.
b1d96e3e 488
2268e329 489=head2 Debugging information
b1d96e3e 490
491Enable additional debugging information printed in the Catalyst log. This
492is automatically enabled when running Catalyst in -Debug mode.
493
a5d909f1 494 MyApp->config(
fa25e422 495 'Plugin::Static::Simple' => {
a5d909f1 496 debug => 1,
497 },
498 );
b108737b 499
2cb3d585 500=head1 USING WITH APACHE
501
6e89d83c 502While Static::Simple will work just fine serving files through Catalyst
503in mod_perl, for increased performance you may wish to have Apache
504handle the serving of your static files directly. To do this, simply use
505a dedicated directory for your static files and configure an Apache
506Location block for that directory This approach is recommended for
507production installations.
2cb3d585 508
6e89d83c 509 <Location /myapp/static>
2cb3d585 510 SetHandler default-handler
511 </Location>
b1d96e3e 512
bc5b1283 513Using this approach Apache will bypass any handling of these directories
514through Catalyst. You can leave Static::Simple as part of your
515application, and it will continue to function on a development server,
516or using Catalyst's built-in server.
517
6e89d83c 518In practice, your Catalyst application is probably (i.e. should be)
519structured in the recommended way (i.e., that generated by bootstrapping
520the application with the C<catalyst.pl> script, with a main directory
521under which is a C<lib/> directory for module files and a C<root/>
522directory for templates and static files). Thus, unless you break up
523this structure when deploying your app by moving the static files to a
524different location in your filesystem, you will need to use an Alias
525directive in Apache to point to the right place. You will then need to
526add a Directory block to give permission for Apache to serve these
527files. The final configuration will look something like this:
528
529 Alias /myapp/static /filesystem/path/to/MyApp/root/static
530 <Directory /filesystem/path/to/MyApp/root/static>
531 allow from all
532 </Directory>
533 <Location /myapp/static>
534 SetHandler default-handler
535 </Location>
536
071c0042 537If you are running in a VirtualHost, you can just set the DocumentRoot
b108737b 538location to the location of your root directory; see
071c0042 539L<Catalyst::Engine::Apache2::MP20>.
540
ab02ca0d 541=head1 PUBLIC METHODS
542
543=head2 serve_static_file $file_path
544
545Will serve the file located in $file_path statically. This is useful when
546you need to autogenerate them if they don't exist, or they are stored in a model.
547
548 package MyApp::Controller::User;
549
550 sub curr_user_thumb : PathPart("my_thumbnail.png") {
551 my ( $self, $c ) = @_;
552 my $file_path = $c->user->picture_thumbnail_path;
553 $c->serve_static_file($file_path);
554 }
555
033a7581 556=head1 INTERNAL EXTENDED METHODS
557
558Static::Simple extends the following steps in the Catalyst process.
559
b108737b 560=head2 prepare_action
033a7581 561
bc5b1283 562C<prepare_action> is used to first check if the request path is a static
563file. If so, we skip all other C<prepare_action> steps to improve
564performance.
033a7581 565
566=head2 dispatch
567
bc5b1283 568C<dispatch> takes the file found during C<prepare_action> and writes it
569to the output.
033a7581 570
571=head2 finalize
572
bc5b1283 573C<finalize> serves up final header information and displays any log
574messages.
033a7581 575
576=head2 setup
577
bc5b1283 578C<setup> initializes all default values.
033a7581 579
d6d29b9b 580=head1 SEE ALSO
581
b108737b 582L<Catalyst>, L<Catalyst::Plugin::Static>,
b1d96e3e 583L<http://www.iana.org/assignments/media-types/>
d6d29b9b 584
585=head1 AUTHOR
586
b1d96e3e 587Andy Grundman, <andy@hybridized.org>
d6d29b9b 588
fa43d6b5 589=head1 CONTRIBUTORS
590
591Marcus Ramberg, <mramberg@cpan.org>
ab02ca0d 592
bc5b1283 593Jesse Sheidlower, <jester@panix.com>
fa43d6b5 594
ab02ca0d 595Guillermo Roditi, <groditi@cpan.org>
596
9936ddfa 597Florian Ragwitz, <rafl@debian.org>
598
599Tomas Doran, <bobtfish@bobtfish.net>
600
601Justin Wheeler (dnm)
b108737b 602
7c97dd21 603Matt S Trout, <mst@shadowcat.co.uk>
604
41cac5ef 605Toby Corkindale, <tjc@wintrmute.net>
606
d6d29b9b 607=head1 THANKS
608
609The authors of Catalyst::Plugin::Static:
610
611 Sebastian Riedel
612 Christian Hansen
613 Marcus Ramberg
614
615For the include_path code from Template Toolkit:
616
617 Andy Wardley
618
619=head1 COPYRIGHT
620
41cac5ef 621Copyright (c) 2005 - 2011
1cc75f96 622the Catalyst::Plugin::Static::Simple L</AUTHOR> and L</CONTRIBUTORS>
623as listed above.
624
625=head1 LICENSE
626
d6d29b9b 627This program is free software, you can redistribute it and/or modify it under
628the same terms as Perl itself.
629
630=cut