WrapCGI - file uploads support
[catagits/Catalyst-Controller-WrapCGI.git] / lib / Catalyst / Controller / WrapCGI.pm
1 package Catalyst::Controller::WrapCGI;
2
3 use Moose;
4 use mro 'c3';
5
6 extends 'Catalyst::Controller';
7
8 use HTTP::Request::AsCGI ();
9 use HTTP::Request ();
10 use URI ();
11 use Catalyst::Exception ();
12 use URI::Escape;
13 use HTTP::Request::Common;
14
15 use namespace::clean -except => 'meta';
16
17 =head1 NAME
18
19 Catalyst::Controller::WrapCGI - Run CGIs in Catalyst
20
21 =head1 VERSION
22
23 Version 0.0033
24
25 =cut
26
27 our $VERSION = '0.0033';
28
29 =head1 SYNOPSIS
30
31     package MyApp::Controller::Foo;
32
33     use parent qw/Catalyst::Controller::WrapCGI/;
34     use CGI ();
35
36     sub hello : Path('cgi-bin/hello.cgi') {
37         my ($self, $c) = @_;
38
39         $self->cgi_to_response($c, sub {
40             my $q = CGI->new;
41             print $q->header, $q->start_html('Hello'),
42                 $q->h1('Catalyst Rocks!'),
43                 $q->end_html;
44         });
45     }
46
47 In your .conf, configure which environment variables to pass:
48
49     <Controller::Foo>
50         <CGI>
51             username_field username # used for REMOTE_USER env var
52             pass_env PERL5LIB
53             pass_env PATH
54             pass_env /^MYAPP_/
55             kill_env MOD_PERL
56         </CGI>
57     </Controller::Foo>
58
59 =head1 DESCRIPTION
60
61 Allows you to run Perl code in a CGI environment derived from your L<Catalyst>
62 context.
63
64 B<*WARNING*>: do not export L<CGI> functions into a Controller, it will break
65 with L<Catalyst> 5.8 onward.
66
67 If you just want to run CGIs from files, see L<Catalyst::Controller::CGIBin>.
68
69 =head1 CONFIGURATION
70
71 C<< $your_controller->{CGI}{pass_env} >> should be an array of environment variables
72 or regular expressions to pass through to your CGIs. Entries surrounded by C</>
73 characters are considered regular expressions.
74
75 C<< $your_controller->{CGI}{kill_env} >> should be an array of environment
76 variables or regular expressions to remove from the environment before passing
77 it to your CGIs.  Entries surrounded by C</> characters are considered regular
78 expressions.
79
80 Default is to pass the whole of C<%ENV>, except for C<MOD_PERL> and
81 C<CONTENT_TYPE> (that is, the default C<kill_env> is C<[ qw(MOD_PERL
82 CONTENT_TYPE) ]>.
83
84 C<< $your_controller->{CGI}{username_field} >> should be the field for your user's name, which will be
85 read from C<< $c->user->obj >>. Defaults to 'username'.
86
87 See L</SYNOPSIS> for an example.
88
89 =cut
90
91 # Hack-around because Catalyst::Engine::HTTP goes and changes
92 # them to be the remote socket, and FCGI.pm does even dumber things.
93
94 open my $REAL_STDIN, "<&=".fileno(*STDIN);
95 open my $REAL_STDOUT, ">>&=".fileno(*STDOUT);
96
97 =head1 METHODS
98
99 =head2 cgi_to_response
100
101 C<<$self->cgi_to_response($c, $coderef)>>
102
103 Does the magic of running $coderef in a CGI environment, and populating the
104 appropriate parts of your Catalyst context with the results.
105
106 Calls L</wrap_cgi>.
107
108 =cut
109
110 sub cgi_to_response {
111   my ($self, $c, $script) = @_;
112
113   my $res = $self->wrap_cgi($c, $script);
114
115   # if the CGI doesn't set the response code but sets location they were
116   # probably trying to redirect so set 302 for them
117
118   my $location = $res->headers->header('Location');
119
120   if (defined $location && length $location && $res->code == 200) {
121     $c->res->status(302);
122   } else { 
123     $c->res->status($res->code);
124   }
125   $c->res->body($res->content);
126   $c->res->headers($res->headers);
127 }
128
129 =head2 wrap_cgi
130
131 C<<$self->wrap_cgi($c, $coderef)>>
132
133 Runs $coderef in a CGI environment using L<HTTP::Request::AsCGI>, returns an
134 L<HTTP::Response>.
135
136 The CGI environment is set up based on $c.
137
138 The environment variables to pass on are taken from the configuration for your
139 Controller, see L</SYNOPSIS> for an example. If you don't supply a list of
140 environment variables to pass, the whole of %ENV is used.
141
142 Used by L</cgi_to_response>, which is probably what you want to use as well.
143
144 =cut
145
146 sub wrap_cgi {
147   my ($self, $c, $call) = @_;
148   my $req = HTTP::Request->new(
149     map { $c->req->$_ } qw/method uri headers/
150   );
151   my $body = $c->req->body;
152   my $body_content = '';
153
154   $req->content_type($c->req->content_type); # set this now so we can override
155
156   if ($body) { # Slurp from body filehandle
157     local $/; $body_content = <$body>;
158   } else {
159     my $body_params = $c->req->body_parameters;
160
161     if (my %uploads = %{ $c->req->uploads }) {
162       my $post = POST 'http://localhost/',
163         Content_Type => 'form-data',
164         Content => [
165           %$body_params,
166           map {
167             my $upl = $uploads{$_};
168             $_ => [
169               undef,
170               $upl->filename,
171               Content => $upl->slurp,
172               'Content-Type' => $upl->type || 'application/octet-stream',
173               map (
174                 $_ => $upl->headers->header($_)
175               ), grep !/^Content-(?:Type|Disposition)$/,
176                             $upl->headers->header_field_names
177             ]
178           } keys %uploads
179         ];
180       $body_content = $post->content;
181       $req->content_type($post->header('Content-Type'));
182     } elsif (%$body_params) {
183       my $encoder = URI->new;
184       $encoder->query_form(%$body_params);
185       $body_content = $encoder->query;
186       $req->content_type('application/x-www-form-urlencoded');
187     }
188   }
189
190   my $filtered_env = $self->_filtered_env(\%ENV);
191
192   $req->content($body_content);
193   $req->content_length(length($body_content));
194
195   my $username_field = $self->{CGI}{username_field} || 'username';
196
197   my $username = (($c->can('user_exists') && $c->user_exists)
198                ? eval { $c->user->obj->$username_field }
199                 : '');
200
201   my $path_info = '/'.join '/' => map uri_escape_utf8($_), @{ $c->req->args };
202
203   my $env = HTTP::Request::AsCGI->new(
204               $req,
205               ($username ? (REMOTE_USER => $username) : ()),
206               %$filtered_env,
207               PATH_INFO => $path_info,
208               FILEPATH_INFO => '/'.$c->action.$path_info, # eww
209               SCRIPT_NAME => $c->uri_for($c->action)->path
210             );
211
212   {
213     local *STDIN = $REAL_STDIN;   # restore the real ones so the filenos
214     local *STDOUT = $REAL_STDOUT; # are 0 and 1 for the env setup
215
216     my $old = select($REAL_STDOUT); # in case somebody just calls 'print'
217
218     my $saved_error;
219
220     $env->setup;
221     eval { $call->() };
222     $saved_error = $@;
223     $env->restore;
224
225     select($old);
226
227     Catalyst::Exception->throw(
228         message => "CGI invocation failed: $saved_error"
229     ) if $saved_error;
230   }
231
232   return $env->response;
233 }
234
235 sub _filtered_env {
236   my ($self, $env) = @_;
237   my @ok;
238
239   my $pass_env = $self->{CGI}{pass_env};
240   $pass_env = []            if not defined $pass_env;
241   $pass_env = [ $pass_env ] unless ref $pass_env;
242
243   my $kill_env = $self->{CGI}{kill_env};
244   $kill_env = [ 'MOD_PERL', 'CONTENT_TYPE' ] unless defined $kill_env;
245   $kill_env = [ $kill_env ]  unless ref $kill_env;
246
247   if (@$pass_env) {
248     for (@$pass_env) {
249       if (m!^/(.*)/\z!) {
250         my $re = qr/$1/;
251         push @ok, grep /$re/, keys %$env;
252       } else {
253         push @ok, $_;
254       }
255     }
256   } else {
257     @ok = keys %$env;
258   }
259
260   for my $k (@$kill_env) {
261     if ($k =~ m!^/(.*)/\z!) {
262       my $re = qr/$1/;
263       @ok = grep { ! /$re/ } @ok;
264     } else {
265       @ok = grep { $_ ne $k } @ok;
266     }
267   }
268   return { map {; $_ => $env->{$_} } @ok };
269 }
270
271 __PACKAGE__->meta->make_immutable;
272
273 =head1 ACKNOWLEDGEMENTS
274
275 Original development sponsored by L<http://www.altinity.com/>
276
277 =head1 SEE ALSO
278
279 L<Catalyst::Controller::CGIBin>, L<CatalystX::GlobalContext>,
280 L<Catalyst::Controller>, L<CGI>, L<Catalyst>
281
282 =head1 AUTHORS
283
284 Originally written by:
285
286 Matt S. Trout, C<< <mst at shadowcat.co.uk> >>
287
288 Contributors:
289
290 Rafael Kitover C<< <rkitover at cpan.org> >>
291
292 Hans Dieter Pearcey C<< <hdp at cpan.org> >>
293
294 =head1 BUGS
295
296 Please report any bugs or feature requests to C<bug-catalyst-controller-wrapcgi
297 at rt.cpan.org>, or through the web interface at
298 L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Catalyst-Controller-WrapCGI>.
299 I will be notified, and then you'll automatically be notified of progress on
300 your bug as I make changes.
301
302 =head1 SUPPORT
303
304 More information at:
305
306 =over 4
307
308 =item * RT: CPAN's request tracker
309
310 L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=Catalyst-Controller-WrapCGI>
311
312 =item * AnnoCPAN: Annotated CPAN documentation
313
314 L<http://annocpan.org/dist/Catalyst-Controller-WrapCGI>
315
316 =item * CPAN Ratings
317
318 L<http://cpanratings.perl.org/d/Catalyst-Controller-WrapCGI>
319
320 =item * Search CPAN
321
322 L<http://search.cpan.org/dist/Catalyst-Controller-WrapCGI>
323
324 =back
325
326 =head1 COPYRIGHT & LICENSE
327
328 Copyright (c) 2008 Matt S. Trout
329
330 This program is free software; you can redistribute it and/or modify it
331 under the same terms as Perl itself.
332
333 =cut
334
335 1; # End of Catalyst::Controller::WrapCGI
336
337 # vim: expandtab shiftwidth=2 ts=2 tw=80: