release 0.032
[catagits/Catalyst-Controller-WrapCGI.git] / lib / Catalyst / Controller / CGIBin.pm
CommitLineData
21a20b7e 1package Catalyst::Controller::CGIBin;
2
9cd47364 3use Moose;
5ae704bb 4use Moose::Util::TypeConstraints;
1ce18a56 5use mro 'c3';
9cd47364 6
7extends 'Catalyst::Controller::WrapCGI';
8
21a20b7e 9use File::Find::Rule ();
c264816e 10use File::Spec::Functions qw/splitdir abs2rel/;
11use IPC::Open3;
12use Symbol 'gensym';
13use List::MoreUtils 'any';
14use IO::File ();
e889d526 15use File::Temp 'tempfile';
d5ba2ab2 16use File::pushd;
d9280b8f 17use CGI::Compile;
5ae704bb 18
2340af9d 19use namespace::clean -except => 'meta';
21a20b7e 20
21a20b7e 21=head1 NAME
22
23Catalyst::Controller::CGIBin - Serve CGIs from root/cgi-bin
24
21a20b7e 25=cut
26
3149e3cb 27our $VERSION = '0.032';
21a20b7e 28
29=head1 SYNOPSIS
30
31In your controller:
32
33 package MyApp::Controller::Foo;
34
35 use parent qw/Catalyst::Controller::CGIBin/;
36
21a20b7e 37In your .conf:
38
39 <Controller::Foo>
9e040aeb 40 cgi_root_path cgi-bin
41 cgi_dir cgi-bin
42 cgi_chain_root /optional/private/path/to/Chained/root
43 cgi_file_pattern *.cgi
44 # or regex
45 cgi_file_pattern /\.pl\z/
21a20b7e 46 <CGI>
47 username_field username # used for REMOTE_USER env var
48 pass_env PERL5LIB
49 pass_env PATH
50 pass_env /^MYAPP_/
51 </CGI>
52 </Controller::Foo>
53
54=head1 DESCRIPTION
55
c264816e 56Dispatches to CGI files in root/cgi-bin for /cgi-bin/ paths.
57
58Unlike L<ModPerl::Registry> this module does _NOT_ stat and recompile the CGI
f410f043 59for every invocation. This may be supported in the future if there's interest.
21a20b7e 60
f410f043 61CGI paths are converted into action names using L</cgi_action>.
ee75330f 62
21a20b7e 63Inherits from L<Catalyst::Controller::WrapCGI>, see the documentation for that
f410f043 64module for other configuration information.
65
66=head1 CONFIG PARAMS
67
68=head2 cgi_root_path
69
184837e7 70The global URI path prefix for CGIs, defaults to C<cgi-bin>.
f410f043 71
1d40d9c3 72=head2 cgi_chain_root
73
74By default L<Path|Catalyst::DispatchType::Path> actions are created for CGIs,
75but if you specify this option, the actions will be created as
76L<Chained|Catalyst::DispatchType::Chained> end-points, chaining off the
77specified private path.
78
79If this option is used, the L</cgi_root_path> option is ignored. The root path
80will be determined by your chain.
81
82The L<PathPart|Catalyst::DispatchType::Chained/PathPart> of the action will be
83the path to the CGI file.
84
f410f043 85=head2 cgi_dir
86
87Path from which to read CGI files. Can be relative to C<$MYAPP_HOME/root> or
88absolute. Defaults to C<$MYAPP_HOME/root/cgi-bin>.
21a20b7e 89
9e040aeb 90=head2 cgi_file_pattern
91
92By default all files in L</cgi_dir> will be loaded as CGIs, however, with this
93option you can specify either a glob or a regex to match the names of files you
94want to be loaded.
95
96Can be an array of globs/regexes as well.
97
21a20b7e 98=cut
99
5ae704bb 100{ my $stringified = subtype as 'Str';
101 coerce $stringified,
102 from 'Object',
103 via { "$_" };
104
105 has cgi_root_path => (is => 'ro', coerce => 1, isa => $stringified, default => 'cgi-bin' );
106 has cgi_chain_root => (is => 'ro', isa => 'Str');
107 has cgi_dir => (is => 'ro', coerce => 1, isa => $stringified, default => 'cgi-bin');
108 has cgi_file_pattern => (is => 'rw', default => sub { ['*'] });
109
110}
9cd47364 111
21a20b7e 112sub register_actions {
ee75330f 113 my ($self, $app) = @_;
21a20b7e 114
d501f45e 115 my $cgi_bin;
116 if( File::Spec->file_name_is_absolute($self->cgi_dir) ) {
117 $cgi_bin = $self->cgi_dir;
118 } elsif( File::Spec->file_name_is_absolute( $app->config->{root} ) ) {
119 $cgi_bin = File::Spec->catdir( $app->config->{root}, $self->cgi_dir );
120 } else {
121 $cgi_bin = $app->path_to( $app->config->{root}, $self->cgi_dir);
122 }
21a20b7e 123
ee75330f 124 my $namespace = $self->action_namespace($app);
21a20b7e 125
126 my $class = ref $self || $self;
127
9e040aeb 128 my $patterns = $self->cgi_file_pattern;
129 $patterns = [ $patterns ] if not ref $patterns;
130 for my $pat (@$patterns) {
131 if ($pat =~ m{^/(.*)/\z}) {
132 $pat = qr/$1/;
133 }
134 }
135 $self->cgi_file_pattern($patterns);
136
137 for my $file (File::Find::Rule->file->name(@$patterns)->in($cgi_bin)) {
c264816e 138 my $cgi_path = abs2rel($file, $cgi_bin);
139
140 next if any { $_ eq '.svn' } splitdir $cgi_path;
f410f043 141 next if $cgi_path =~ /\.swp\z/;
c264816e 142
12d29ebf 143 my $path = join '/' => splitdir($cgi_path);
144 my $action_name = $self->cgi_action($path);
145 my $reverse = $namespace ? "$namespace/$action_name" : $action_name;
1d40d9c3 146
147 my $attrs = do {
148 if (my $chain_root = $self->cgi_chain_root) {
149 { Chained => [ $chain_root ], PathPart => [ $path ], Args => [] };
150 }
151 else {
152 { Path => [ $self->cgi_path($path) ] };
153 }
154 };
12d29ebf 155
21a20b7e 156 my ($cgi, $type);
21a20b7e 157
c264816e 158 if ($self->is_perl_cgi($file)) { # syntax check passed
21a20b7e 159 $type = 'Perl';
12d29ebf 160 $cgi = $self->wrap_perl_cgi($file, $action_name);
21a20b7e 161 } else {
21a20b7e 162 $type = 'Non-Perl';
12d29ebf 163 $cgi = $self->wrap_nonperl_cgi($file, $action_name);
21a20b7e 164 }
165
c264816e 166 $app->log->info("Registering root/cgi-bin/$cgi_path as a $type CGI.")
ee75330f 167 if $app->debug;
21a20b7e 168
c264816e 169 my $code = sub {
21a20b7e 170 my ($controller, $context) = @_;
171 $controller->cgi_to_response($context, $cgi)
172 };
173
174 my $action = $self->create_action(
175 name => $action_name,
176 code => $code,
177 reverse => $reverse,
178 namespace => $namespace,
179 class => $class,
180 attributes => $attrs
181 );
182
ee75330f 183 $app->dispatcher->register($app, $action);
21a20b7e 184 }
185
ee75330f 186 $self->next::method($app, @_);
2340af9d 187
f410f043 188# Tell Static::Simple to ignore cgi_dir
fa86df5d 189 if ($cgi_bin =~ /^\Q@{[ $app->path_to('root') ]}\E/) {
f410f043 190 my $rel = File::Spec->abs2rel($cgi_bin, $app->path_to('root'));
191
192 if (!any { $_ eq $rel }
193 @{ $app->config->{static}{ignore_dirs}||[] }) {
194 push @{ $app->config->{static}{ignore_dirs} }, $rel;
195 }
2340af9d 196 }
21a20b7e 197}
198
199=head1 METHODS
200
f410f043 201=head2 cgi_action
202
b9548267 203C<< $self->cgi_action($cgi) >>
21a20b7e 204
205Takes a path to a CGI from C<root/cgi-bin> such as C<foo/bar.cgi> and returns
d5ba2ab2 206the action name it is registered as.
fbaba9dd 207
21a20b7e 208=cut
209
210sub cgi_action {
211 my ($self, $cgi) = @_;
212
d5ba2ab2 213 my $action_name = 'CGI_' . $cgi;
214 $action_name =~ s/([^A-Za-z0-9_])/sprintf("_%2x", unpack("C", $1))/eg;
21a20b7e 215
d5ba2ab2 216 return $action_name;
21a20b7e 217}
218
f410f043 219=head2 cgi_path
220
b9548267 221C<< $self->cgi_path($cgi) >>
9cc2dd4c 222
223Takes a path to a CGI from C<root/cgi-bin> such as C<foo/bar.cgi> and returns
224the public path it should be registered under.
225
184837e7 226The default is to prefix with C<$cgi_root_path/>, using the C<cgi_root_path>
227config setting, above.
9cc2dd4c 228
229=cut
230
231sub cgi_path {
232 my ($self, $cgi) = @_;
9cd47364 233
234 my $root = $self->cgi_root_path;
235 $root =~ s{/*$}{};
236 return "$root/$cgi";
9cc2dd4c 237}
238
f410f043 239=head2 is_perl_cgi
240
b9548267 241C<< $self->is_perl_cgi($path) >>
c264816e 242
243Tries to figure out whether the CGI is Perl or not.
244
245If it's Perl, it will be inlined into a sub instead of being forked off, see
f410f043 246L</wrap_perl_cgi>.
c264816e 247
248=cut
249
250sub is_perl_cgi {
251 my ($self, $cgi) = @_;
252
fa86df5d 253 if ($^O eq 'MSWin32') {
254 # the fork code fails on Win32
255 eval { $self->wrap_perl_cgi($cgi, '__DUMMY__') };
256 my $success = $@ ? 0 : 1;
257 require Class::Unload;
258 Class::Unload->unload($self->cgi_package('__DUMMY__'));
259 return $success;
260 }
261
e889d526 262 my (undef, $tempfile) = tempfile;
c264816e 263
e889d526 264 my $pid = fork;
265 die "Cannot fork: $!" unless defined $pid;
c264816e 266
e889d526 267 if ($pid) {
268 waitpid $pid, 0;
269 my $errors = IO::File->new($tempfile)->getline;
270 unlink $tempfile;
271 return $errors ? 0 : 1;
272 }
c264816e 273
e889d526 274 # child
275 local *NULL;
c264816e 276 open NULL, '>', File::Spec->devnull;
e889d526 277 open STDOUT, '>&', \*NULL;
278 open STDERR, '>&', \*NULL;
279 close STDIN;
c264816e 280
efa4a434 281 eval { $self->wrap_perl_cgi($cgi, '__DUMMY__') };
282
e889d526 283 IO::File->new(">$tempfile")->print($@);
efa4a434 284
e889d526 285 exit;
c264816e 286}
287
f410f043 288=head2 wrap_perl_cgi
289
b9548267 290C<< $self->wrap_perl_cgi($path, $action_name) >>
c264816e 291
292Takes the path to a Perl CGI and returns a coderef suitable for passing to
d9280b8f 293cgi_to_response (from L<Catalyst::Controller::WrapCGI>) using L<CGI::Compile>.
c264816e 294
fbaba9dd 295C<$action_name> is the generated name for the action representing the CGI file
296from C<cgi_action>.
c264816e 297
298This is similar to how L<ModPerl::Registry> works, but will only work for
299well-written CGIs. Otherwise, you may have to override this method to do
300something more involved (see L<ModPerl::PerlRun>.)
301
f410f043 302Scripts with C<__DATA__> sections now work too, as well as scripts that call
303C<exit()>.
fbaba9dd 304
c264816e 305=cut
306
307sub wrap_perl_cgi {
12d29ebf 308 my ($self, $cgi, $action_name) = @_;
309
fa86df5d 310 return CGI::Compile->compile($cgi, $self->cgi_package($action_name));
311}
312
313=head2 cgi_package
314
315C<< $self->cgi_package($action_name) >>
316
317Returns the package name a Perl CGI is compiled into for a given
318C<$action_name>.
319
320=cut
321
322sub cgi_package {
323 my ($self, $action_name) = @_;
324
325 return "Catalyst::Controller::CGIBin::_CGIs_::$action_name";
c264816e 326}
327
f410f043 328=head2 wrap_nonperl_cgi
329
b9548267 330C<< $self->wrap_nonperl_cgi($path, $action_name) >>
c264816e 331
332Takes the path to a non-Perl CGI and returns a coderef for executing it.
333
12d29ebf 334C<$action_name> is the generated name for the action representing the CGI file.
335
d9280b8f 336By default returns something like:
c264816e 337
338 sub { system $path }
339
340=cut
341
342sub wrap_nonperl_cgi {
12d29ebf 343 my ($self, $cgi, $action_name) = @_;
c264816e 344
d9280b8f 345 return sub {
346 system $cgi;
347
348 if ($? == -1) {
349 die "failed to execute CGI '$cgi': $!";
350 }
351 elsif ($? & 127) {
352 die sprintf "CGI '$cgi' died with signal %d, %s coredump",
353 ($? & 127), ($? & 128) ? 'with' : 'without';
354 }
355 else {
356 my $exit_code = $? >> 8;
357
358 return 0 if $exit_code == 0;
359
360 die "CGI '$cgi' exited non-zero with: $exit_code";
361 }
362 };
c264816e 363}
364
f410f043 365__PACKAGE__->meta->make_immutable;
366
c264816e 367=head1 SEE ALSO
368
369L<Catalyst::Controller::WrapCGI>, L<CatalystX::GlobalContext>,
d5ba2ab2 370L<Catalyst::Controller>, L<CGI>, L<CGI::Compile>, L<Catalyst>
c264816e 371
21a20b7e 372=head1 BUGS
373
374Please report any bugs or feature requests to C<bug-catalyst-controller-wrapcgi at
375rt.cpan.org>, or through the web interface at
376L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Catalyst-Controller-WrapCGI>.
377I will be notified, and then you'll automatically be notified of progress on
378your bug as I make changes.
379
380=head1 SUPPORT
381
382More information at:
383
384=over 4
385
386=item * RT: CPAN's request tracker
387
388L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=Catalyst-Controller-WrapCGI>
389
390=item * AnnoCPAN: Annotated CPAN documentation
391
392L<http://annocpan.org/dist/Catalyst-Controller-WrapCGI>
393
394=item * CPAN Ratings
395
396L<http://cpanratings.perl.org/d/Catalyst-Controller-WrapCGI>
397
398=item * Search CPAN
399
400L<http://search.cpan.org/dist/Catalyst-Controller-WrapCGI>
401
402=back
403
efa4a434 404=head1 AUTHOR
405
406See L<Catalyst::Controller::WrapCGI/AUTHOR> and
407L<Catalyst::Controller::WrapCGI/CONTRIBUTORS>.
408
21a20b7e 409=head1 COPYRIGHT & LICENSE
410
efa4a434 411Copyright (c) 2008-2009 L<Catalyst::Controller::WrapCGI/AUTHOR> and
412L<Catalyst::Controller::WrapCGI/CONTRIBUTORS>.
21a20b7e 413
414This program is free software; you can redistribute it and/or modify it
415under the same terms as Perl itself.
416
417=cut
418
4191; # End of Catalyst::Controller::CGIBin
9e040aeb 420# vim:et sw=4 sts=4 tw=0: