File Coverage

blib/lib/Plack/Middleware/ExtDirect.pm
Criterion Covered Total %
statement 139 141 98.5
branch 32 48 66.6
condition 5 12 41.6
subroutine 28 29 96.5
pod 2 2 100.0
total 206 232 88.7


line stmt bran cond sub pod time code
1             package Plack::Middleware::ExtDirect;
2              
3 4     4   432505 use parent 'Plack::Middleware';
  4         8  
  4         26  
4              
5 4     4   227 use strict;
  4         4  
  4         87  
6 4     4   15 use warnings;
  4         7  
  4         113  
7 4     4   16 no warnings 'uninitialized'; ## no critic
  4         5  
  4         109  
8              
9 4     4   15 use Carp;
  4         6  
  4         249  
10 4     4   1855 use IO::File;
  4         6412  
  4         460  
11              
12 4     4   1864 use Plack::Request;
  4         107958  
  4         134  
13 4     4   30 use Plack::Util;
  4         5  
  4         100  
14              
15 4     4   16 use RPC::ExtDirect::Util::Accessor;
  4         5  
  4         86  
16 4     4   34 use RPC::ExtDirect::Config;
  4         5  
  4         72  
17 4     4   12 use RPC::ExtDirect::API;
  4         5  
  4         29  
18 4     4   84 use RPC::ExtDirect;
  4         7  
  4         24  
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.01';
33              
34             ### PUBLIC INSTANCE METHOD (CONSTRUCTOR) ###
35             #
36             # Instantiates a new Plack::Middleware::ExtDirect object
37             #
38              
39             sub new {
40 20     20 1 78497 my $class = shift;
41            
42 20 50 33     175 my %params = @_ == 1 && 'HASH' eq ref($_[0]) ? %{ $_[0] } : @_;
  20         103  
43            
44 20   33     179 my $api = delete $params{api} || RPC::ExtDirect->get_api();
45 20   33     735 my $config = delete $params{config} || $api->config;
46            
47             # These two are not method calls, they need to do their stuff *before*
48             # we have found $self
49 20         203 _decorate_config($config);
50 20         8608 _process_params($api, $config, \%params);
51            
52 20         115 my $self = $class->SUPER::new(%params);
53            
54 20         621 $self->config($config);
55 20         950 $self->api($api);
56            
57 20         160 return $self;
58             }
59              
60             ### PUBLIC INSTANCE METHOD ###
61             #
62             # Dispatch calls to Ext.Direct handlers
63             #
64              
65             sub call {
66 20     20 1 65352 my ($self, $env) = @_;
67            
68 20         532 my $config = $self->config;
69              
70             # Run the relevant handler. Router calls are the most frequent
71             # so we test for them first
72 20         149 for ( $env->{PATH_INFO} ) {
73 20 100       419 return $self->_handle_router($env) if $_ =~ $config->router_path;
74 8 100       289 return $self->_handle_events($env) if $_ =~ $config->poll_path;
75 3 50       110 return $self->_handle_api($env) if $_ =~ $config->api_path;
76             };
77              
78             # Not our URI, fall through
79 0         0 return $self->app->($env);
80             }
81              
82             ### PUBLIC INSTANCE METHODS ###
83             #
84             # Read-write accessors
85             #
86              
87             RPC::ExtDirect::Util::Accessor->mk_accessors(
88             simple => [qw/ api config /],
89             );
90              
91             ############## PRIVATE METHODS BELOW ##############
92              
93             ### PRIVATE PACKAGE SUBROUTINE ###
94             #
95             # Decorate a Config object with __PACKAGE__-specific accessors
96             #
97              
98             sub _decorate_config {
99 20     20   29 my ($config) = @_;
100            
101 20         222 $config->add_accessors(
102             overwrite => 1,
103             complex => [{
104             accessor => 'router_class_plack',
105             fallback => 'router_class',
106             }, {
107             accessor => 'eventprovider_class_plack',
108             fallback => 'eventprovider_class',
109             }],
110             );
111             }
112              
113             ### PRIVATE PACKAGE SUBROUTINE ###
114             #
115             # Process parameters directly passed to the constructor
116             # and set the Config/API options accordingly
117             #
118              
119             sub _process_params {
120 20     20   43 my ($api, $config, $params) = @_;
121            
122             # We used to accept these parameters directly in the constructor;
123             # this behavior is not recommended now but it doesn't make much sense
124             # to deprecate it either
125 20         71 my @compat_params = qw/
126             api_path router_path poll_path namespace remoting_var polling_var
127             auto_connect debug no_polling
128             /;
129            
130 20         38 for my $var ( @compat_params ) {
131 180 100       3044 $config->$var( delete $params->{$var} ) if exists $params->{$var};
132             }
133            
134 20 50       82 $config->router_class_plack( delete $params->{router} )
135             if exists $params->{router};
136            
137 20 50       52 $config->eventprovider_class_plack( delete $params->{event_provider} )
138             if exists $params->{event_provider};
139            
140 20         82 for my $type ( $api->HOOK_TYPES ) {
141 60 50       166 my $code = delete $params->{ $type } if exists $params->{ $type };
142            
143 60 50       128 $api->add_hook( type => $type, code => $code ) if defined $code;
144             }
145             }
146              
147             ### PRIVATE INSTANCE METHOD ###
148             #
149             # Handles Ext.Direct API calls
150             #
151              
152             sub _handle_api {
153 3     3   45 my ($self, $env) = @_;
154              
155             # Get the API JavaScript chunk
156 3         6 my $js = eval {
157 3         71 $self->api->get_remoting_api( config => $self->config )
158             };
159              
160             # If JS API call failed, return error
161 3 50       10538 return $self->_error_response if $@;
162              
163             # We need content length, in octets
164 4     4   1732 my $content_length = do { use bytes; my $len = length $js };
  4         6  
  4         20  
  3         7  
  3         8  
165              
166             return [
167 3         42 200,
168             [
169             'Content-Type' => 'application/javascript',
170             'Content-Length' => $content_length,
171             ],
172             [ $js ],
173             ];
174             }
175              
176             ### PRIVATE INSTANCE METHOD ###
177             #
178             # Dispatches Ext.Direct method requests
179             #
180              
181             sub _handle_router {
182 12     12   130 my ($self, $env) = @_;
183            
184             # Throw an error if any method but POST is used
185 12 50       33 return $self->_error_response
186             unless $env->{REQUEST_METHOD} eq 'POST';
187            
188 12         206 my $config = $self->config;
189 12         240 my $api = $self->api;
190              
191             # Now we need a Request object
192 12         107 my $req = Plack::Request->new($env);
193              
194             # Try to distinguish between raw POST and form call
195 12         111 my $router_input = $self->_extract_post_data($req);
196              
197             # When extraction fails, undef is returned by method above
198 12 50       29 return $self->_error_response unless defined $router_input;
199              
200             # Rebless request as our environment object for compatibility
201 12         39 bless $req, __PACKAGE__.'::Env';
202            
203 12         414 my $router_class = $config->router_class_plack;
204            
205 12         925 eval "require $router_class";
206            
207 12         2285 my $router = $router_class->new(
208             config => $config,
209             api => $api,
210             );
211            
212             # Routing requests is safe (Router won't croak under torture)
213 12         141 my $result = $router->route($router_input, $req);
214              
215 12         23242 return $result;
216             }
217              
218             ### PRIVATE INSTANCE METHOD ###
219             #
220             # Polls Event handlers for events, returning serialized stream
221             #
222              
223             sub _handle_events {
224 5     5   52 my ($self, $env) = @_;
225            
226             # Only GET and POST methods are supported for polling
227 5 50       31 return $self->_error_response
228             if $env->{REQUEST_METHOD} !~ / \A (GET|POST) \z /xms;
229              
230 5         40 my $req = Plack::Middleware::ExtDirect::Env->new($env);
231            
232 5         127 my $config = $self->config;
233 5         96 my $api = $self->api;
234            
235 5         152 my $provider_class = $config->eventprovider_class_plack;
236            
237 5         440 eval "require $provider_class";
238            
239 5         1766 my $provider = $provider_class->new(
240             config => $config,
241             api => $api,
242             );
243              
244             # Polling for Events is safe
245 5         57 my $http_body = $provider->poll($req);
246              
247             # We need content length, in octets
248             my $content_length
249 4     4   1469 = do { no warnings 'void'; use bytes; length $http_body };
  4     4   7  
  4         155  
  4         24  
  4         11  
  4         15  
  5         12561  
  5         9  
250              
251             return [
252 5         62 200,
253             [
254             'Content-Type' => 'application/json; charset=utf-8',
255             'Content-Length' => $content_length,
256             ],
257             [ $http_body ],
258             ];
259             }
260              
261             ### PRIVATE INSTANCE METHOD ###
262             #
263             # Deals with intricacies of POST-fu and returns something suitable to
264             # feed to Router (string or hashref, really). Or undef if something
265             # goes too wrong to recover.
266             #
267              
268             sub _extract_post_data {
269 12     12   14 my ($self, $req) = @_;
270              
271             # The smartest way to tell if a form was submitted that *I* know of
272             # is to look for 'extAction' and 'extMethod' keywords in form params.
273 12   66     37 my $is_form = $req->param('extAction') && $req->param('extMethod');
274              
275             # If form is not involved, it's easy: just return raw POST (or undef)
276 12 100       34060 if ( !$is_form ) {
277 9         34 my $postdata = $req->content;
278 9 50       294 return $postdata ne '' ? $postdata
279             : undef
280             ;
281             };
282              
283             # If any files are attached, extUpload field will be set to 'true'
284 3         8 my $has_uploads = $req->param('extUpload') eq 'true';
285              
286             # Outgoing hash
287 3         24 my %keyword;
288              
289             # Pluck all parameters from Plack::Request
290 3         7 for my $param ( $req->param ) {
291 25         60 my @values = $req->param($param);
292 25 50       402 $keyword{ $param } = @values == 0 ? undef
    50          
293             : @values == 1 ? $values[0]
294             : [ @values ]
295             ;
296             };
297              
298             # Find all file uploads
299 3 100       11 if ( $has_uploads ) {
300 2         6 my $uploads = $req->uploads; # Hash::MultiValue
301              
302             # We need files as plain list (keys %$uploads is by design)
303             my @field_uploads
304 2         17 = map { $self->_format_uploads( $uploads->get_all($_) ) }
  2         5  
305             keys %$uploads;
306              
307             # Now remove fields that contained files
308 2         9 delete @keyword{ $uploads->keys };
309              
310 2 50       29 $keyword{ '_uploads' } = \@field_uploads if @field_uploads;
311             };
312              
313             # Remove extType because it's meaningless later on
314 3         4 delete $keyword{ extType };
315              
316             # Fix TID so that it comes as a number (JavaScript is picky)
317 3 50       12 $keyword{ extTID } += 0 if exists $keyword{ extTID };
318              
319 3         11 return \%keyword;
320             }
321              
322             ### PRIVATE INSTANCE METHOD ###
323             #
324             # Takes info from Plack::Request::Upload and formats it as needed
325             #
326              
327             sub _format_uploads {
328 2     2   17 my ($self, @uploads) = @_;
329              
330 4         231 my @result = map {
331 2         2 {
332             filename => $_->filename,
333             basename => $_->basename,
334             type => $_->content_type,
335             size => $_->size,
336             path => $_->path,
337             handle => IO::File->new($_->path, 'r'),
338             }
339             }
340             @uploads;
341              
342 2         254 return @result;
343             }
344              
345             ### PRIVATE INSTANCE METHOD ###
346             #
347             # Returns error response in Plack format
348             #
349              
350 0     0   0 sub _error_response { [ 500, [ 'Content-Type' => 'text/html' ], [] ] }
351              
352             # Small utility class
353             package
354             Plack::Middleware::ExtDirect::Env;
355              
356 4     4   1608 use parent 'Plack::Request';
  4         4  
  4         29  
357              
358             sub http {
359 2     2   6386 my ($self, $name) = @_;
360              
361 2         11 my $hdr = $self->headers;
362              
363 2 100       321 return $name ? $hdr->header($name)
364             : $hdr->header_field_names
365             ;
366             }
367              
368             sub param {
369 2     2   2143 my ($self, $name) = @_;
370              
371 2 50       20 return $name eq 'POSTDATA' ? $self->content
    100          
372             : $name eq '' ? ( $self->SUPER::param(), 'POSTDATA' )
373             : $self->SUPER::param($name)
374             ;
375             }
376              
377             sub cookie {
378 2     2   2285 my ($self, $name) = @_;
379              
380 1         8 return $name ? $self->cookies()->{ $name }
381 2 100       7 : keys %{ $self->cookies() }
382             ;
383             }
384              
385             1;