File Coverage

blib/lib/AnyEvent/HTTPD/ExtDirect.pm
Criterion Covered Total %
statement 175 192 91.1
branch 35 48 72.9
condition 10 26 38.4
subroutine 29 31 93.5
pod 3 6 50.0
total 252 303 83.1


line stmt bran cond sub pod time code
1             package AnyEvent::HTTPD::ExtDirect;
2              
3 4     4   679557 use parent 'AnyEvent::HTTPD';
  4         7  
  4         25  
4              
5 4     4   117484 use common::sense;
  4         9  
  4         27  
6              
7 4     4   150 use Carp;
  4         9  
  4         200  
8 4     4   2024 use IO::File;
  4         6848  
  4         501  
9 4     4   2969 use File::Temp;
  4         30642  
  4         595  
10 4     4   30 use File::Basename;
  4         6  
  4         263  
11              
12 4     4   23 use AnyEvent::HTTPD::Request;
  4         3  
  4         78  
13              
14 4     4   17 use RPC::ExtDirect::Util;
  4         5  
  4         150  
15 4     4   18 use RPC::ExtDirect::Util::Accessor;
  4         4  
  4         87  
16 4     4   39 use RPC::ExtDirect::Config;
  4         4  
  4         71  
17 4     4   15 use RPC::ExtDirect::API;
  4         5  
  4         29  
18 4     4   79 use RPC::ExtDirect;
  4         6  
  4         23  
19              
20             #
21             # This module is not compatible with RPC::ExtDirect < 3.0
22             #
23              
24             croak __PACKAGE__." requires RPC::ExtDirect 3.0+"
25             if $RPC::ExtDirect::VERSION lt '3.0';
26              
27             ### PACKAGE GLOBAL VARIABLE ###
28             #
29             # Version of the module
30             #
31              
32             our $VERSION = '3.20';
33              
34             ### PUBLIC CLASS METHOD (CONSTRUCTOR) ###
35             #
36             # Instantiate a new AnyEvent::HTTPD::ExtDirect object
37             #
38              
39             sub new {
40 35     35 1 172328 my $class = shift;
41            
42 35 50 33     312 my %arg = @_ == 1 && 'HASH' eq ref $_[0] ? %{ $_[0] }
  0         0  
43             : @_
44             ;
45            
46 35   33     393 my $api = delete $arg{api} || RPC::ExtDirect->get_api();
47 35   33     288 my $config = delete $arg{config} || $api->config;
48            
49 35         348 $config->add_accessors(
50             overwrite => 1,
51             complex => [{
52             accessor => 'router_class_anyevent',
53             fallback => 'router_class',
54             }, {
55             accessor => 'eventprovider_class_anyevent',
56             fallback => 'eventprovider_class',
57             }],
58             );
59            
60 35         15758 for my $var ( qw/ router_class eventprovider_class / ) {
61 70         115 my $method = "${var}_anyevent";
62            
63 70 50       201 $config->$method( delete $arg{$var} ) if exists $arg{$var};
64             }
65            
66             # AnyEvent::HTTPD wants only IP addresses
67 35 50       183 $arg{host} = '127.0.0.1' if $arg{host} =~ /localhost/io;
68              
69 35         211 my $self = $class->SUPER::new(%arg);
70            
71 35         14176 $self->config($config);
72 35         889 $self->api($api);
73              
74 35         288 return $self;
75             }
76              
77             ### PUBLIC INSTANCE METHOD ###
78             #
79             # Run the server
80             #
81              
82             sub run {
83 0     0 1 0 my ($self) = @_;
84            
85 0         0 my $config = $self->config;
86              
87 0         0 $self->set_callbacks(
88             api_path => $config->api_path,
89             router_path => $config->router_path,
90             poll_path => $config->poll_path,
91             );
92              
93 0         0 $self->SUPER::run();
94             }
95              
96             ### PUBLIC INSTANCE METHOD ###
97             #
98             # Handle Ext.Direct API calls
99             #
100              
101             sub handle_api {
102 3     3 0 183315 my ($self, $req) = @_;
103              
104             # Get the API JavaScript chunk
105 3         5 my $js = eval {
106 3         68 $self->api->get_remoting_api( config => $self->config )
107             };
108              
109             # If JS API call failed, return error
110 3 50       14652 return $self->_error_response if $@;
111              
112             # Content length should be in octets
113 4     4   3856 my $content_length = do { use bytes; my $len = length $js };
  4         30  
  4         14  
  3         5  
  3         6  
114              
115 3         22 $req->respond([
116             200,
117             'OK',
118             {
119             'Content-Type' => 'application/javascript',
120             'Content-Length' => $content_length,
121             },
122             $js
123             ]);
124              
125 3         1288 $self->stop_request;
126             }
127              
128             ### PUBLIC INSTANCE METHOD ###
129             #
130             # Handle Ext.Direct method requests
131             #
132              
133             sub handle_router {
134 27     27 0 2460453 my ($self, $req) = @_;
135            
136 27 50       127 if ( $req->method ne 'POST' ) {
137 0         0 $req->respond( $self->_error_response );
138 0         0 $self->stop_request;
139            
140 0         0 return;
141             }
142            
143 27         939 my $config = $self->config;
144 27         799 my $api = $self->api;
145              
146             # Naked AnyEvent::HTTPD::Request object doesn't provide several
147             # utility methods we'll need down below, and we will need it as
148             # an environment object, too
149 27         338 my $env = bless $req, __PACKAGE__.'::Env';
150              
151             # We're trying to distinguish between a raw POST and a form call
152 27         126 my $router_input = $self->_extract_post_data($env);
153              
154             # If the extraction fails, undef is returned by the method above
155 27 50       74 if ( !defined $router_input ) {
156 0         0 $req->respond( $self->_error_response );
157 0         0 $self->stop_request;
158              
159 0         0 return;
160             }
161            
162 27         638 my $router_class = $config->router_class_anyevent;
163            
164 27         3223 eval "require $router_class";
165            
166 27         2218 my $router = $router_class->new(
167             config => $config,
168             api => $api,
169             );
170              
171             # No need for eval here, Router won't throw exceptions
172 27         406 my $result = $router->route($router_input, $env);
173              
174             # Router result is Plack-compatible arrayref; there's not much
175             # difference in what AnyEvent::HTTPD expects so we just convert it
176             # in place
177 27         211 $req->respond([
178             200,
179             'OK',
180 27         57319 +{ @{ $result->[1] } },
181             $result->[2]->[0],
182             ]);
183            
184 27         12224 $self->stop_request;
185             }
186              
187             ### PUBLIC INSTANCE METHOD ###
188             #
189             # Polls Event handlers for events, returning serialized stream
190             #
191              
192             sub handle_events {
193 5     5 0 387718 my ($self, $req) = @_;
194            
195             # Only GET and POST methods are supported for polling
196 5         22 my $method = $req->method;
197            
198 5 50 33     47 if ( $method ne 'GET' && $method ne 'POST' ) {
199 0         0 $req->respond( $self->_error_response );
200 0         0 $self->stop_request;
201            
202 0         0 return;
203             }
204            
205 5         179 my $config = $self->config;
206 5         153 my $api = $self->api;
207            
208 5         46 my $env = bless $req, __PACKAGE__.'::Env';
209            
210 5         109 my $provider_class = $config->eventprovider_class_anyevent;
211            
212 5         609 eval "require $provider_class";
213            
214 5         2254 my $provider = $provider_class->new(
215             config => $config,
216             api => $api,
217             );
218            
219             # Polling for Events is safe from exceptions
220 5         73 my $http_body = $provider->poll($env);
221            
222             my $content_length
223 4     4   1508 = do { no warnings 'void'; use bytes; length $http_body };
  4     4   5  
  4         145  
  4         14  
  4         4  
  4         12  
  5         16844  
  5         12  
224            
225 5         47 $req->respond([
226             200,
227             'OK',
228             {
229             'Content-Type' => 'application/json; charset=utf-8',
230             'Content-Length' => $content_length,
231             },
232             $http_body,
233             ]);
234            
235 5         2544 $self->stop_request;
236             }
237              
238             ### PUBLIC INSTANCE METHOD ###
239             #
240             # Register the callbacks for Ext.Direct handlers.
241             # This effectively "primes" the server but does not make it
242             # enter a blocking wait.
243             #
244              
245             sub set_callbacks {
246 35     35 1 2473 my ($self, %arg) = @_;
247              
248 35         598 my $config = $self->config;
249            
250 35   33     238 my $api_path = $arg{api_path} || $config->api_path;
251 35   33     122 my $router_path = $arg{router_path} || $config->router_path;
252 35   33     101 my $poll_path = $arg{poll_path} || $config->poll_path;
253            
254 35         287 $self->reg_cb(
255             $api_path => $self->can('handle_api'),
256             $router_path => $self->can('handle_router'),
257             $poll_path => $self->can('handle_events'),
258             );
259             }
260              
261             ### PUBLIC INSTANCE METHODS ###
262             #
263             # Read-write accessors.
264             #
265              
266             RPC::ExtDirect::Util::Accessor::mk_accessors(
267             simple => [qw/ api config /],
268             );
269              
270             ############## PRIVATE METHODS BELOW ##############
271              
272             ### PRIVATE INSTANCE METHOD ###
273             #
274             # Deals with intricacies of POST-fu and returns something suitable to
275             # feed to Router (string or hashref, really). Or undef if something
276             # goes too wrong to recover.
277             #
278             # This code was mostly copied from the Plack gateway and adapted
279             # for AnyEvent::HTTPD.
280             #
281              
282             sub _extract_post_data {
283 27     27   50 my ($self, $req) = @_;
284              
285             # The smartest way to tell if a form was submitted that *I* know of
286             # is to look for 'extAction' and 'extMethod' keywords in form params.
287 27   66     98 my $is_form = $req->param('extAction') && $req->param('extMethod');
288              
289             # If form is not involved, it's easy: just return raw POST (or undef)
290 27 100       68 if ( !$is_form ) {
291 21         87 my $postdata = $req->content;
292 21 50       129 return $postdata ne '' ? $postdata
293             : undef
294             ;
295             };
296              
297             # If any files are attached, extUpload field will be set to 'true'
298 6         12 my $has_uploads = $req->param('extUpload') eq 'true';
299              
300             # Outgoing hash
301 6         25 my %keyword;
302              
303             # Pluck all parameters from the Request object
304 6         34 for my $param ( $req->params ) {
305 54         94 my @values = $req->param($param);
306 54 100       140 $keyword{ $param } = @values == 0 ? undef
    50          
307             : @values == 1 ? $values[0]
308             : [ @values ]
309             ;
310             };
311              
312             # Find all file uploads
313 6 100       20 for ( $has_uploads ? ($has_uploads) : () ) {
314             # The list of fields that contain file uploads
315 3         10 my @upload_fields = $req->upload_fields;
316            
317 3 100       16 last unless @upload_fields;
318            
319 2         3 my @uploaded_files;
320            
321 2         4 for my $field_name ( @upload_fields ) {
322 2         7 my $uploads = $req->raw_param($field_name);
323              
324             # We need files as a formatted list
325 2         3 my @field_uploads = map { $self->_format_upload($_) } @$uploads;
  4         10  
326 2         4 push @uploaded_files, @field_uploads;
327              
328             # Now remove the field that contained files
329 2         6 delete @keyword{ $field_name };
330             }
331              
332 2 50       8 $keyword{ '_uploads' } = \@uploaded_files if @uploaded_files;
333             };
334              
335             # Metadata is JSON encoded; decode_metadata lives by side effects!
336 6 100       19 if ( exists $keyword{metadata} ) {
337 3         16 RPC::ExtDirect::Util::decode_metadata($self, \%keyword);
338             }
339              
340             # Remove extType because it's meaningless later on
341 6         110 delete $keyword{ extType };
342              
343             # Fix TID so that it comes as number (JavaScript is picky)
344 6 50       24 $keyword{ extTID } += 0 if exists $keyword{ extTID };
345              
346 6         13 return \%keyword;
347             }
348              
349             ### PRIVATE INSTANCE METHOD ###
350             #
351             # Take the file content and metadata and format it in a way
352             # that RPC::ExtDirect handlers expect
353             #
354              
355             sub _format_upload {
356 4     4   5 my ($self, $upload) = @_;
357            
358 4     4   2153 my $content_length = do { use bytes; length $upload->[0] };
  4         5  
  4         14  
  4         4  
  4         8  
359            
360 4         16 my ($fh, $fname) = File::Temp::tempfile;
361            
362 4         1161 binmode $fh;
363 4         142 syswrite $fh, $upload->[0], $content_length;
364            
365 4         12 sysseek $fh, 0, 0;
366            
367             # We don't need the file content anymore, so try to release
368             # the memory it takes
369 4         7 $upload->[0] = undef;
370            
371 4         5 my $filename = $upload->[2];
372 4         162 my $basename = File::Basename::basename($filename);
373 4         6 my $type = $upload->[1];
374 4         19 my $handle = IO::File->new_from_fd($fh->fileno, '<');
375              
376             return {
377 4         220 filename => $filename,
378             basename => $basename,
379             type => $type,
380             size => $content_length,
381             path => $fname,
382             handle => $handle,
383             };
384             }
385              
386             ### PRIVATE INSTANCE METHOD ###
387             #
388             # Return an error response formatted to AnyEvent::HTTPD likes
389             #
390              
391             sub _error_response {
392 0     0   0 [ 500, 'Internal Server Error', { 'Content-Type' => 'text/html' }, '' ]
393             }
394              
395             package
396             AnyEvent::HTTPD::ExtDirect::Env;
397              
398 4     4   833 use parent 'AnyEvent::HTTPD::Request';
  4         6  
  4         59  
399              
400             #
401             # AnyEvent::HTTPD::Request stores the form parameters in a peculiar format:
402             # $self->{parm} is a hashref of arrayrefs; each arrayref contain one
403             # or more values, again in arrayrefs with fixed number of items:
404             # [ content, content-type, file-name ]
405             #
406             # For anything but file uploads, the last 2 elements are undef; for the
407             # file uploads they're the file MIME type and name, respectively.
408             #
409             # A dump might look like this:
410             #
411             # $self->{parm}:
412             # 0 HASH
413             # 'formFieldName' => ARRAY
414             # 0 ARRAY =>
415             # 0 'form field value'
416             # 1 undef
417             # 2 undef
418             # 'fieldWithMultipleValues' => ARRAY # SURMISED!
419             # 0 ARRAY =>
420             # 0 'form field value 0'
421             # 1 undef
422             # 2 undef
423             # 1 ARRAY =>
424             # 0 'form field value 1'
425             # 1 undef
426             # 2 undef
427             # 'fileUploads' => ARRAY
428             # 0 ARRAY =>
429             # 0 'first file content (all of it!)'
430             # 1 'first file MIME type'
431             # 2 'first file name'
432             # 1 ARRAY =>
433             # 0 'second file content'
434             # 1 'second file MIME type'
435             # 2 'second file name'
436             #
437             # There is no method that returns multiple values for a non-file field,
438             # and no method that returns file upload parameters, so we have to
439             # roll our own
440             #
441              
442             sub param {
443 95     95   3145 my ($self, $key) = @_;
444            
445 95 100       228 return $self->params unless defined $key;
446            
447             # [] is to avoid autovivification biting my ass ;)
448 94 100       98 my @values = map { $_->[0] } @{ $self->{parm}->{$key} || [] };
  71         107  
  94         374  
449            
450 94 100       280 return wantarray ? @values : shift @values;
451             }
452              
453             # Go over the fields and return the list of names for the fields
454             # that contain file uploads
455             sub upload_fields {
456 3     3   3 my ($self) = @_;
457            
458 3         5 my @upload_fields;
459            
460 3         4 my $params = $self->{parm};
461            
462             FIELD:
463 3         10 for my $field_name ( keys %$params ) {
464 29         21 my $values = $params->{ $field_name };
465            
466 29         22 for my $value ( @$values ) {
467            
468             # We surmise that for a file upload, at least MIME type
469             # should be defined (name is optional)
470 29 100       53 if ( defined $value->[1] ) {
471 2         3 push @upload_fields, $field_name;
472 2         8 next FIELD;
473             }
474             }
475             }
476            
477 3         6 return @upload_fields;
478             }
479              
480             sub raw_param {
481 2     2   3 my ($self, $key) = @_;
482            
483 2         6 return $self->{parm}->{$key};
484             }
485              
486             sub cookie {
487 2     2   3244 my ($self, $key) = @_;
488              
489 2         5 my %cookies;
490              
491 2 50       8 if ( $self->{_cookies} ) {
492 0         0 %cookies = %{ $self->{_cookies} };
  0         0  
493             }
494             else {
495 2         9 my $headers = $self->headers;
496 2 50       15 my $cookie_hdr = $headers ? $headers->{cookie} : '';
497 2         11 %cookies = map { split /=/, $_ } split /;\s+/, $cookie_hdr;
  2         11  
498 2         6 $self->{_cookies} = \%cookies;
499             }
500              
501 2 100       17 return $key ? $cookies{ $key } : keys %cookies;
502             }
503              
504             sub http {
505 2     2   6983 my ($self, $key) = @_;
506              
507 2   50     13 my $headers = $self->headers || {};
508              
509 2 100       25 return $key ? $headers->{ lc $key } : keys %$headers;
510             }
511              
512             1;
513