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