63f07c9448b6d15ca6614dae297fc5704604ce57
[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               map {
173                 my $header = $_;
174                 map { $header => $_ } $upl->headers->header($header)
175               } $upl->headers->header_field_names
176             ]
177           } keys %uploads
178         ];
179       $body_content = $post->content;
180       $req->content_type($post->header('Content-Type'));
181     } elsif (%$body_params) {
182       my $encoder = URI->new;
183       $encoder->query_form(%$body_params);
184       $body_content = $encoder->query;
185       $req->content_type('application/x-www-form-urlencoded');
186     }
187   }
188
189   my $filtered_env = $self->_filtered_env(\%ENV);
190
191   $req->content($body_content);
192   $req->content_length(length($body_content));
193
194   my $username_field = $self->{CGI}{username_field} || 'username';
195
196   my $username = (($c->can('user_exists') && $c->user_exists)
197                ? eval { $c->user->obj->$username_field }
198                 : '');
199
200   my $path_info = '/'.join '/' => map uri_escape_utf8($_), @{ $c->req->args };
201
202   my $env = HTTP::Request::AsCGI->new(
203               $req,
204               ($username ? (REMOTE_USER => $username) : ()),
205               %$filtered_env,
206               PATH_INFO => $path_info,
207               FILEPATH_INFO => '/'.$c->action.$path_info, # eww
208               SCRIPT_NAME => $c->uri_for($c->action)->path
209             );
210
211   {
212     local *STDIN = $REAL_STDIN;   # restore the real ones so the filenos
213     local *STDOUT = $REAL_STDOUT; # are 0 and 1 for the env setup
214
215     my $old = select($REAL_STDOUT); # in case somebody just calls 'print'
216
217     my $saved_error;
218
219     $env->setup;
220     eval { $call->() };
221     $saved_error = $@;
222     $env->restore;
223
224     select($old);
225
226     Catalyst::Exception->throw(
227         message => "CGI invocation failed: $saved_error"
228     ) if $saved_error;
229   }
230
231   return $env->response;
232 }
233
234 sub _filtered_env {
235   my ($self, $env) = @_;
236   my @ok;
237
238   my $pass_env = $self->{CGI}{pass_env};
239   $pass_env = []            if not defined $pass_env;
240   $pass_env = [ $pass_env ] unless ref $pass_env;
241
242   my $kill_env = $self->{CGI}{kill_env};
243   $kill_env = [ 'MOD_PERL', 'CONTENT_TYPE' ] unless defined $kill_env;
244   $kill_env = [ $kill_env ]  unless ref $kill_env;
245
246   if (@$pass_env) {
247     for (@$pass_env) {
248       if (m!^/(.*)/\z!) {
249         my $re = qr/$1/;
250         push @ok, grep /$re/, keys %$env;
251       } else {
252         push @ok, $_;
253       }
254     }
255   } else {
256     @ok = keys %$env;
257   }
258
259   for my $k (@$kill_env) {
260     if ($k =~ m!^/(.*)/\z!) {
261       my $re = qr/$1/;
262       @ok = grep { ! /$re/ } @ok;
263     } else {
264       @ok = grep { $_ ne $k } @ok;
265     }
266   }
267   return { map {; $_ => $env->{$_} } @ok };
268 }
269
270 __PACKAGE__->meta->make_immutable;
271
272 =head1 ACKNOWLEDGEMENTS
273
274 Original development sponsored by L<http://www.altinity.com/>
275
276 =head1 SEE ALSO
277
278 L<Catalyst::Controller::CGIBin>, L<CatalystX::GlobalContext>,
279 L<Catalyst::Controller>, L<CGI>, L<Catalyst>
280
281 =head1 AUTHORS
282
283 Originally written by:
284
285 Matt S. Trout, C<< <mst at shadowcat.co.uk> >>
286
287 Contributors:
288
289 Rafael Kitover C<< <rkitover at cpan.org> >>
290
291 Hans Dieter Pearcey C<< <hdp at cpan.org> >>
292
293 =head1 BUGS
294
295 Please report any bugs or feature requests to C<bug-catalyst-controller-wrapcgi
296 at rt.cpan.org>, or through the web interface at
297 L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Catalyst-Controller-WrapCGI>.
298 I will be notified, and then you'll automatically be notified of progress on
299 your bug as I make changes.
300
301 =head1 SUPPORT
302
303 More information at:
304
305 =over 4
306
307 =item * RT: CPAN's request tracker
308
309 L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=Catalyst-Controller-WrapCGI>
310
311 =item * AnnoCPAN: Annotated CPAN documentation
312
313 L<http://annocpan.org/dist/Catalyst-Controller-WrapCGI>
314
315 =item * CPAN Ratings
316
317 L<http://cpanratings.perl.org/d/Catalyst-Controller-WrapCGI>
318
319 =item * Search CPAN
320
321 L<http://search.cpan.org/dist/Catalyst-Controller-WrapCGI>
322
323 =back
324
325 =head1 COPYRIGHT & LICENSE
326
327 Copyright (c) 2008 Matt S. Trout
328
329 This program is free software; you can redistribute it and/or modify it
330 under the same terms as Perl itself.
331
332 =cut
333
334 1; # End of Catalyst::Controller::WrapCGI
335
336 # vim: expandtab shiftwidth=2 ts=2 tw=80: