File Coverage

blib/lib/POE/Component/Server/Bayeux/Request.pm
Criterion Covered Total %
statement 30 146 20.5
branch 0 36 0.0
condition 0 12 0.0
subroutine 10 24 41.6
pod 14 14 100.0
total 54 232 23.2


line stmt bran cond sub pod time code
1             package POE::Component::Server::Bayeux::Request;
2              
3             =head1 NAME
4              
5             POE::Component::Server::Bayeux::Request - A single Bayeux request
6              
7             =head1 DESCRIPTION
8              
9             Objects in this class represent a single Bayeux request made to a Bayeux server. Requests are instantiated with an HTTP::Request and HTTP::Response object. This class is responsible for parsing the request content into a JSON object, creating one or more L objects that represent the possible message types of the Bayeux protocol, and handling each one in turn.
10              
11             =cut
12              
13 3     3   2632 use strict;
  3         6  
  3         116  
14 3     3   35 use warnings;
  3         7  
  3         101  
15 3     3   3313 use HTTP::Status; # for RC_OK
  3         12489  
  3         1045  
16 3     3   4109 use HTTP::Request::Common;
  3         167367  
  3         319  
17 3     3   2615 use CGI::Simple::Util qw(unescape);
  3         8421  
  3         204  
18 3     3   2826 use JSON::Any qw(XS);
  3         51052  
  3         22  
19 3     3   23047 use Data::UUID;
  3         6  
  3         222  
20 3     3   16 use Params::Validate;
  3         6  
  3         138  
21              
22 3     3   16 use POE qw(Component::Server::Bayeux::Message::Factory);
  3         6  
  3         28  
23              
24 3     3   195 use base qw(Class::Accessor);
  3         7  
  3         5872  
25             __PACKAGE__->mk_accessors(qw(
26             id
27             is_complete
28             is_error
29             ip
30             http_request
31             http_response
32             json_response
33             messages
34             responses
35             heap
36             delay
37             post_handle
38             json_comment_filtered
39             ));
40              
41             ## Class Globals ###
42              
43             my $json = JSON::Any->new();
44             my $uuid = Data::UUID->new();
45              
46             ## Class Methods ###
47              
48             =head1 CLASS METHODS
49              
50             =head2 new ()
51              
52             =over 4
53              
54             Requires 'request' (L), 'response' (L), and 'server_heap'.
55             Returns init()'ed class object.
56              
57             =back
58              
59             =cut
60              
61             sub new {
62 0     0 1   my $class = shift;
63              
64 0           my %args = validate(@_, {
65             request => 1,
66             response => 1,
67             server_heap => 1,
68             ip => 0,
69             });
70              
71 0           my %self = (
72             http_request => $args{request},
73             http_response => $args{response},
74             heap => $args{server_heap},
75             messages => [],
76             responses => [],
77             id => $uuid->create_str,
78             ip => $args{ip},
79             );
80              
81 0           my $self = bless \%self, $class;
82 0           $self->init();
83 0           return $self;
84             }
85              
86             ## Object Methods, Public ###
87              
88             =head1 OBJECT METHODS
89              
90             =head2 handle ()
91              
92             =over 4
93              
94             Call after creating the request. Calls the pre_handle(), handle()
95             methods on each message, possibly completing the request.
96              
97             =back
98              
99             =cut
100              
101             sub handle {
102 0     0 1   my ($self) = @_;
103              
104 0           my $heap = $self->heap;
105              
106             # Some messages (/meta/connect, for example) need to be handled in a specific
107             # order. Allow each message to affect the queueing.
108 0           foreach my $message (@{ $self->messages }) {
  0            
109 0           $message->pre_handle();
110             }
111              
112             # Starting at the beginning of the message array, process each message in
113             # turn. Messages will interact with the Request $self object, adding responses
114             # and in some cases affecting other messages still in the stack.
115              
116 0           while (my $message = shift @{ $self->messages }) {
  0            
117 0           $message->handle();
118             }
119              
120 0 0         if ($self->delay) {
121 0           $poe_kernel->post($heap->{manager}, 'delay_request', $self->id, $self->delay);
122 0           $self->delay(0);
123 0           $self->is_complete(0);
124 0           $self->http_response->streaming(1);
125             }
126             else {
127 0           $self->complete();
128             }
129             }
130              
131             =head2 complete ()
132              
133             =over 4
134              
135             Completes the request, calling the post_handle() method on the messages
136             that need it.
137              
138             =back
139              
140             =cut
141              
142             sub complete {
143 0     0 1   my ($self) = @_;
144              
145 0           $self->form_response( @{ $self->responses } );
  0            
146 0           $self->is_complete(1);
147 0 0         if ($self->http_response->streaming) {
148 0           $self->http_response->send( $self->http_response );
149 0           $self->http_response->close();
150             }
151              
152             # Ensure no KeepAlive
153 0           $self->http_request->header(Connection => 'close');
154              
155 0 0         if ($self->post_handle) {
156 0           while (my $message = shift @{ $self->post_handle }) {
  0            
157 0           $message->post_handle();
158             }
159             }
160             }
161              
162             ## Object Methods, Private ###
163              
164             =head1 PRIVATE METHODS
165              
166             =over 4
167              
168             These methods are mainly called by messages during their handle() phase.
169              
170             =back
171              
172             =head2 client ($id)
173              
174             =over 4
175              
176             Returns a L object with the given id.
177              
178             =back
179              
180             =cut
181              
182             sub client {
183 0     0 1   my ($self, $id) = @_;
184              
185 0           return POE::Component::Server::Bayeux::Client->new(
186             request => $self,
187             id => $id,
188             server_heap => $self->heap,
189             );
190             }
191              
192             =head2 add_response ($response)
193              
194             =over 4
195              
196             Adds a message response onto the stack of responses.
197              
198             =back
199              
200             =cut
201              
202             sub add_response {
203 0     0 1   my ($self, $response) = @_;
204              
205 0           push @{ $self->responses }, $response;
  0            
206             }
207              
208             =head2 clear_stack ()
209              
210             =over 4
211              
212             Clears all messages and responses.
213              
214             =back
215              
216             =cut
217              
218             sub clear_stack {
219 0     0 1   my ($self) = @_;
220              
221 0           $self->messages([]);
222 0           $self->responses([]);
223             }
224              
225             =head2 add_post_handle ($message)
226              
227             =over 4
228              
229             Adds a message to be handled in the post_handle() code.
230              
231             =back
232              
233             =cut
234              
235             sub add_post_handle {
236 0     0 1   my ($self, $message) = @_;
237              
238 0           push @{ $self->{post_handle} }, $message;
  0            
239             }
240              
241             =head2 init ()
242              
243             =over 4
244              
245             Parses the L object, extracting the JSON
246             payload, creating a stack of L messages.
247              
248             =back
249              
250             =cut
251              
252             sub init {
253 0     0 1   my ($self) = @_;
254              
255 0           my $request = $self->{http_request};
256 0           my $response = $self->{http_response};
257              
258             ## Extract the JSON payload
259              
260 0           my $params;
261 0           my $json_requests = [];
262              
263 0           my $payload;
264              
265             # Parse the content type string
266              
267 0           my $content_type = $request->content_type;
268              
269             # Support 'text/json; charset=UTF-8'
270 0           my %content_type_opts;
271 0           my @content_type_parts = split /\s*;\s*/, $content_type;
272 0 0         if (int @content_type_parts > 1) {
273 0           $content_type = shift @content_type_parts;
274 0           foreach my $part (@content_type_parts) {
275 0           my ($key, $value) = split /=/, $part;
276             # May or may not be key/value pairs, and are case-sensitive
277 0           $content_type_opts{$key} = $value;
278             }
279             }
280              
281 0 0         if ($content_type eq 'application/x-www-form-urlencoded') {
    0          
282             # POST or GET
283 0 0         if (my $content = $request->content) {
    0          
284 0           $params = $content;
285             }
286             elsif ($request->uri =~ m!\?message=!) {
287 0           ($params) = $request->uri =~ m/\?(.*)/;
288             }
289              
290 0 0         if (! $params) {
291 0           return $self->error("No content found in HTTP request (content type '$content_type')");
292             }
293              
294             # Decode the urlencoded key-value pairs
295 0           my %content;
296 0           foreach my $pair (split /&/, $params) {
297 0           my ($key, $value) = split /=/, $pair, 2;
298 0 0 0       next unless $key && $value;
299 0           $content{ unescape($key) } = unescape($value);
300             }
301              
302 0 0         if (! $content{message}) {
303 0           return $self->error("No 'message' key pair found in content");
304             }
305 0           $payload = $content{message};
306             }
307             elsif ($content_type eq 'text/json') {
308 0           $payload = $request->content;
309             }
310             else {
311 0           return $self->error("Unsupported connection content-type '$content_type'");
312             }
313              
314             # Decode the payload
315 0           eval {
316 0           $json_requests = $json->decode($payload);
317             };
318 0 0         if ($@) {
319 0           return $self->error("Failed to decode JSON payload: $@" );
320             }
321 0 0 0       if (! $json_requests || ! ref $json_requests) {
322 0           return $self->error("Invalid JSON payload; must be array or object");
323             }
324 0 0         if (ref $json_requests eq 'HASH') {
325 0           $json_requests = [ $json_requests ];
326             }
327              
328 0           $self->logger->debug("New remote request from ".$self->ip.", id ".$self->id.":", $json_requests);
329              
330 0           foreach my $message (@$json_requests) {
331 0           my $bayeux_message;
332 0           eval {
333 0           $bayeux_message = POE::Component::Server::Bayeux::Message::Factory->create(
334             request => $self,
335             data => $message,
336             );
337             };
338 0 0 0       if ($@ || ! $bayeux_message
      0        
339             || $bayeux_message->isa('POE::Component::Server::Bayeux::Message::Invalid')
340             ) {
341 0 0         $self->error("Invalid message found" . ($@ ? " ($@)" : ''));
342 0           $self->logger->debug("Remote request was invalid");
343 0           last;
344             }
345             else {
346 0           push @{ $self->{messages} }, $bayeux_message;
  0            
347             }
348             }
349             }
350              
351             =head2 error ($message)
352              
353             =over 4
354              
355             Convienence method to throw an error, returning to the client.
356              
357             =back
358              
359             =cut
360              
361             sub error {
362 0     0 1   my ($self, $error) = @_;
363              
364 0 0         return if $self->is_error;
365              
366 0           $self->form_response(
367             {
368             error => $error,
369             successful => JSON::XS::false,
370             }
371             );
372              
373 0           $self->is_error(1);
374 0           $self->is_complete(1);
375             }
376              
377             =head2 form_response (@messages)
378              
379             =over 4
380              
381             Encodes the messages into the payload of the response
382              
383             =back
384              
385             =cut
386              
387             sub form_response {
388 0     0 1   my ($self, @message) = @_;
389              
390 0           my $response = $self->http_response;
391 0           $self->json_response( \@message );
392 0           my $encoded = $json->encode( \@message );
393              
394 0           my $type = 'text/json';
395 0 0         if ($self->json_comment_filtered) {
396 0           $encoded = "/*$encoded*/";
397 0           $type = 'text/json-comment-filtered';
398             }
399              
400 0           $response->header( 'Content-Type' => "$type; charset=utf-8" );
401 0           $response->code(RC_OK);
402 0           $response->content( $encoded );
403             }
404              
405             =head2 logger ()
406              
407             =over 4
408              
409             Returns the server's logger.
410              
411             =back
412              
413             =cut
414              
415             sub logger {
416 0     0 1   my ($self) = @_;
417              
418 0           return $self->heap->{logger};
419             }
420              
421             ## POE passthru methods
422              
423             =head2 subscribe ($client_id, $channel)
424              
425             =over 4
426              
427             Passthru to the POE server's subscribe state
428              
429             =back
430              
431             =cut
432              
433             sub subscribe {
434 0     0 1   my ($self, $client_id, $channel) = @_;
435 0           $poe_kernel->post($self->heap->{manager}, 'subscribe', {
436             client_id => $client_id,
437             channel => $channel,
438             });
439             }
440              
441             =head2 unsubscribe ($client_id, $channel)
442              
443             =over 4
444              
445             Passthru to the POE server's unsubscribe state
446              
447             =back
448              
449             =cut
450              
451             sub unsubscribe {
452 0     0 1   my ($self, $client_id, $channel) = @_;
453 0           $poe_kernel->post($self->heap->{manager}, 'unsubscribe', {
454             client_id => $client_id,
455             channel => $channel,
456             });
457             }
458              
459             =head2 publish ($client_id, $channel, $data)
460              
461             =over 4
462              
463             Passthru to the POE server's publish state
464              
465             =back
466              
467             =cut
468              
469             sub publish {
470 0     0 1   my ($self, $client_id, $channel, $data) = @_;
471 0           $poe_kernel->post($self->heap->{manager}, 'publish', {
472             client_id => $client_id,
473             channel => $channel,
474             data => $data,
475             });
476             }
477              
478             =head1 COPYRIGHT
479              
480             Copyright (c) 2008 Eric Waters and XMission LLC (http://www.xmission.com/).
481             All rights reserved. This program is free software; you can redistribute it
482             and/or modify it under the same terms as Perl itself.
483              
484             The full text of the license can be found in the LICENSE file included with
485             this module.
486              
487             =head1 AUTHOR
488              
489             Eric Waters
490              
491             =cut
492              
493             1;