File Coverage

blib/lib/WWW/Scripter/Plugin/Ajax.pm
Criterion Covered Total %
statement 247 258 95.7
branch 102 116 87.9
condition 56 85 65.8
subroutine 46 47 97.8
pod 0 2 0.0
total 451 508 88.7


line stmt bran cond sub pod time code
1             package WWW::Scripter::Plugin::Ajax;
2              
3 3     3   336754 use 5.008005; # utf8'encode that stringifies
  3         11  
  3         166  
4              
5 3     3   21 use HTML::DOM::Interface ':all';
  3         7  
  3         729  
6 3     3   34 use Scalar::Util 'weaken';
  3         9  
  3         181  
7              
8 3     3   15 use warnings; no warnings 'utf8';
  3     3   9  
  3         140  
  3         17  
  3         7  
  3         1209  
9              
10             our $VERSION = '0.09';
11              
12             sub init {
13 3     3 0 39 my($pack,$mech) = (shift,shift);
14 3         7 my $js_plugin = $mech->use_plugin(JavaScript => @{$_[0]});
  3         32  
15 3         543 @{$_[0]} = ();
  3         10  
16            
17             $js_plugin->bind_classes({
18             __PACKAGE__.'::XMLHttpRequest' => 'XMLHttpRequest',
19             XMLHttpRequest => {
20             _constructor => sub {
21 29     29   3861429 (__PACKAGE__."::XMLHttpRequest")->new(
22             $mech, @_)
23             },
24             # ~~~ I need to verify these return types.
25 3         95 abort => METHOD | VOID,
26             getAllResponseHeaders => METHOD | STR,
27             getResponseHeader => METHOD | STR,
28             open => METHOD | VOID,
29             send => METHOD | VOID,
30             setRequestHeader => METHOD | VOID,
31              
32             onreadystatechange => OBJ,
33             readyState => NUM | READONLY,
34             responseText => STR | READONLY,
35             responseXML => OBJ | READONLY,
36             status => NUM | READONLY,
37             statusText => STR | READONLY,
38              
39             addEventListener => METHOD | VOID,
40             removeEventListener => METHOD | VOID,
41             dispatchEvent => METHOD | BOOL,
42              
43             _constants => [
44             map __PACKAGE__."::XMLHttpRequest::$_",qw[
45             UNSENT OPENED HEADERS_RECEIVED
46             LOADING DONE
47             ]],
48             },
49             });
50              
51 3     3   19 weaken $mech; no warnings 'parenthesis';
  3         12  
  3         387  
  3         34  
52 3         16 return bless \my $foo, $pack;
53             # That $foo thing is used below to store one tiny bit of info:
54             # whether bind_class has been called yet. (I’ll have to change the
55             # structure if we need to store anything else.)
56             }
57              
58             sub options {
59 0     0 0 0 ${+shift}->plugin('JavaScript')->options(@_);
  0         0  
60             }
61              
62             package WWW::Scripter::Plugin::Ajax::XMLHttpRequest;
63              
64 3     3   16 use Encode 2.09 qw 'decode encode find_encoding';
  3         68  
  3         229  
65 3     3   65 use Scalar::Util 1.09 qw 'weaken blessed refaddr';
  3         67  
  3         178  
66 3     3   18 use HTML::DOM::Event;
  3         6  
  3         152  
67 3         196 use HTML::DOM::Exception qw 'SYNTAX_ERR NOT_SUPPORTED_ERR
68 3     3   16 INVALID_STATE_ERR';
  3         6  
69 3     3   20 no HTTP'Cookies 5.833 (); # non-clobbering add_cookie_header
  3         68  
  3         63  
70 3     3   19 use HTTP::Headers;
  3         5  
  3         91  
71 3     3   23 use HTTP::Headers::Util 'split_header_words';
  3         4  
  3         206  
72 3     3   18 use HTTP'Message 5.827 (); # content_charset
  3         59  
  3         55  
73 3     3   16 use HTTP::Request;
  3         5  
  3         88  
74 3     3   17 no LWP::Protocol();
  3         4  
  3         56  
75 3     3   21 use URI 1;
  3         58  
  3         64  
76 3     3   16 use URI::Escape;
  3         6  
  3         318  
77              
78 3         9 use constant 1.03 do { my $x; +{
  3         5  
79 3         588 map(+($_=>$x++), qw[ UNSENT OPENED HEADERS_RECEIVED LOADING DONE]),
80             SECURITY_ERR => 18,
81             NETWORK_ERR => 19,
82             ABORT_ERR => 20,
83 3     3   23 }};
  3         58  
84              
85             # There are six different states that the object can be in:
86             # UNSENT - actually means uninitialised
87             # OPENED - i.e., initialised
88             # SENT - what it says
89             # HEADERS_RECEIVED - what it says
90             # LOADING - body is downloading
91             # DONE - zackly what it says
92             # Five of them are represented by the constants above, and
93             # are returned by the readyState method. The opened and
94             # sent states are conflated and represented by the OPENED con-
95             # stant in the badly-designed (if designed at all) public API. The
96             # SENT constant is used only internally, which is why it is one of the
97             # lexical constants below. We need to make this distinction, since cer-
98             # tain methods are supposed to die in the SENT state, but not the
99             # OPENED state. Furthermore, we *do* trigger orsc when the state
100             # changes to SENT.
101              
102             # The lc lexical constants are field indices.
103              
104             use constant::lexical {
105 3         71 SENT => 1.5,
106             mech => 0,
107             clone => 1,
108             method => 2,
109             url => 3,
110             async => 4,
111             name => 5,
112             pw => 6,
113             orsc => 7,
114             state => 8,
115             res => 9,
116             headers => 10,
117             tree => 11,
118             xml => 12, # boolean
119 3     3   4691 };
  3         33577  
120              
121             sub new {
122 29     29   245 my $self = bless [], shift;
123 29         110 $self->[mech] = shift;
124 29         141 weaken $self->[mech];
125 29         102 $self->[state] = 0;
126 29         178 $self;
127             }
128              
129              
130             # Instance Methods
131              
132             my $http_token = '[^]\0-\x1f\x7f()<>\@,;:\\\"/[?={} \t]+';
133             my $http_field_val = '[^\0-\ch\ck\cl\cn-\x1f]*';
134              
135             sub open{
136 96     96   736329 my ($self) = shift;
137 96         474 @$self[method,url,async] = @_;
138 96 100       4853 @_ < 3 and $self->[async] = 1; # default
139 96         353 shift,shift,shift;
140              
141 96         313 for($self->[method]) {
142 96 100       10851 /^$http_token\z/o
143             or die new HTML::DOM::Exception SYNTAX_ERR,
144             "Invalid HTTP method: $self->[method]";
145 95 100       43105 /^(?:connect|trac[ek])\z/i
146             and die new HTML::DOM::Exception SECURITY_ERR,
147             "Use of the $_ method is forbidden";
148 92         997 s/^(?:delete|head|options|(?:ge|p(?:os|u))t)\z/uc/ie;
  91         1920  
149             }
150              
151             # Work around a perl bug. See the comments in _dg_url below.
152 92         400 _dg_url($$self[url]);
153 92         652 _dg_url(my $base = $self->[mech]->base);
154              
155 92         418 $self->[url] = my $url = new_abs URI $self->[url],
156             $base;
157             # Unfortunately, sometimes new_abs turns on utf8-ness again.
158 92 50       23221 if(utf8'is_utf8("$self->[url]")) {
159             # If a newer version of URI changes its internals, this
160             # will die. Hopefully the bug will be gone by then and we
161             # can just ignore the error. (If not, tests will fail, so
162             # I’ll update it.)
163 0         0 local* @;
164 0         0 eval { utf8'downgrade ${$$self[url]} }
  0         0  
  0         0  
165             }
166              
167 92         2851 _check_url($url, $self->[mech]->uri);
168 86         2179 $url->fragment(undef); # ~~~ Shouldn’t WWW::Scripter be doing this
169              
170 86 100 100     2395 if(@_){ # name arg
    100          
171 8 100       62 if( defined($self->[name] = shift) ) {
172 7 100       33 $$self[name] =~ /:/
173             and die new HTML'DOM'Exception
174             SYNTAX_ERR,
175             (
176             "Names cannot contain colons ($$self[name])",
177             delete $$self[name]
178             )[0];
179 6 100 33     145 if(@_) {
    50          
180 5         18 $self->[pw] = shift;
181             }
182             elsif($url->can('userinfo')
183             and defined(my $ui = $url->userinfo)) {
184 1 50       31 $ui =~ /:(.*)/s and
185             $self->[pw] = uri_unescape($1)
186             }
187             }
188             }
189             elsif($url->can('userinfo') and defined(my$ ui = $url->userinfo)) {
190 3         84 ($self->[name],my $pw) = map uri_unescape($_),
191             split(":", $ui, 2);
192 3 100       65 $self->[pw] = $pw if defined $pw; # avoid clobbering it
193             # when we shouldn’t
194             }
195 85   100     2880 defined and utf8::encode $_ for @$self[name,pw];
196              
197 85         1752 delete @$self[res,headers];
198 85         149 $self->[state]=1;
199 85         360 $self->_trigger_orsc;
200 85         1005 return;
201             }
202              
203             sub _dg_url { # downgrade URL in place
204             # Work around a bug in perl. -e ignores the UTF-8 flag and uses the
205             # internal byte representation of the string, which causes file
206             # requests to fail.
207 184 50 0 184   86436 utf8'is_utf8($_[0])
208             and
209             $_[0] =~ /[^\0-\xff]/
210             && ($_[0] = uri_escape_utf8 $_[0], '^\x00-\x7f'),
211             utf8'downgrade $_[0];
212              
213             }
214              
215             sub _check_url { # checks protocol and same-originness
216 108     108   3188 my ($new,$current,$error_code) = @_;
217 108 100 50     493 length LWP'Protocol'implementor $new->scheme
218             or die new HTML::DOM::Exception
219             $error_code||NOT_SUPPORTED_ERR,
220 1         406 "Protocol scheme '${\$new->scheme}' is not supported";
221              
222 107         266586 my $host1 = eval{$current->host};
  107         626  
223 107         4190 my $host2 = eval{$new->host};
  107         653  
224 107 100 100     3314 !defined $host1 || !defined $host2 || $host1 ne $host2
      66        
      100        
225             and die new HTML'DOM'Exception $error_code||SECURITY_ERR,
226             "Permission denied ($new: wrong host)";
227 101 50 0     469 $current->scheme ne $new->scheme
228             and die new HTML'DOM'Exception $error_code||SECURITY_ERR,
229             "Permission denied ($new: wrong scheme)";
230 3     3   5321 no warnings 'uninitialized';
  3         11  
  3         3585  
231 101 100 50     2132 eval{$current->port}ne eval{$new->port}
  101         525  
  101         3129  
232             and die new HTML'DOM'Exception $error_code||SECURITY_ERR,
233             "Permission denied ($new: wrong port)";
234             }
235              
236             sub send{
237 87 100   87   48121 die new HTML::DOM::Exception INVALID_STATE_ERR,
238             "send can only be called once between calls to open"
239             unless $_[0][state] == OPENED;
240              
241 82         170 my ($self, $data) = @_;
242 82   100     1306 my $clone = $self->[clone] ||=
243             bless $self->[mech]->clone, 'LWP::UserAgent';
244             # ~~~ This doesn’t allow for Scripter subclasses that
245             # cache, etc. What’s the best way to circumvent
246             # Scripter’s DOM parsing, Mech’s global credentials,
247             # etc.?
248             # $clone->stack_depth(1);
249             # $clone->plugin('DOM')->scripts_enabled(0);
250 82 100       18462 my $headers = new HTTP::Headers @{$self->[headers]||[]};
  82         1454  
251 82 100 66     1673 defined $self->[name] || defined $self->[pw] and
252             $headers->authorization_basic($self->[name], $self->[pw]);
253 82         2477 my $request = new HTTP::Request $self->[method], $self->[url],
254             $headers;
255 82 100 100     15081 if($self->[method] !~ /^(?:get|head)\z/i && defined $data) {
256 9         17 my $default_mime = 'text/plain';
257 9         21 my $default_charset = 'utf-8';
258 9 50       48 if(defined blessed $data) {
259 9 100       126 if($data->isa('HTML::DOM')) {
    100          
260 1         3 $default_mime = 'text/html';
261 1         5 $default_charset = $data->charset;
262 1         15 $data = $data->innerHTML;
263             }
264             elsif($data->can('createElement')) { # quack!
265 1         3 $default_mime = 'application/xml';
266              
267             # XML documents override the HTTP charset
268 1         8 content_type $request
269             scalar content_type $request; # erase
270             # charset
271             # XML::LibXML:
272 1 50       51 if($data->can('serialize')) {
273 0         0 $default_charset
274             = $data->actualEncoding;
275 0         0 $data
276             = decode
277             $charset, $data->serialize;
278             }
279             else {
280             # Assume XML::DOM::Lite.
281             # ~~~ If this is not the case, what
282             # do we do???
283 1   33     39 our $xml_dom_lite_cereal ||= (
284             require XML'DOM'Lite,
285             new XML'DOM'Lite'Serializer::
286             );
287 1         19 $data
288             = join
289             "",
290             map
291             $xml_dom_lite_cereal
292             ->serializeToString($_),
293 1         14 @{ childNodes $data };
294             }
295             }
296             }
297              
298 9         2100 my $charset = $request->content_type_charset;
299 9 100       539 if(!defined $charset) {
    50          
300 7         51 content $request encode $default_charset, $data;
301 7   66     2073 content_type $request
302             (content_type $request || $default_mime)
303             . qq ';charset=$default_charset'
304             }
305             elsif(my $enc = find_encoding $charset) {
306 2         58 content $request $enc->encode("$data");
307             }
308             else {
309 0         0 content_type $request
310             content_type $request
311             . ";charset=$default_charset";
312 0         0 content $request encode $default_charset, $data;
313             }
314 9         404 $request->header('Content-Length' => length($request->content())) ;
315             }
316              
317 82         710 my $async = $self->[async];
318              
319 82 100       757 if($async) {
320 27         198 $self->[state] = SENT;
321 27         75 $self->_trigger_orsc;
322 27 100       109 $self->[state] or return;
323              
324 26         54 $self->[state] = HEADERS_RECEIVED; # ~~~ This is in the wrong place
325 26         59 $self->_trigger_orsc; # When I fix this, I need to
326 26 100       129 $self->[state] or return; # make sure the redirect_ok
327             # hook is not present during
328             # orsc, since it would affect
329             # other requests
330             }
331              
332             # Insert hook to check redirections
333 80         914 my $error; my $redirected; my $res;
  0         0  
334             {
335 3     3   20 no warnings 'redefine';
  3         7  
  3         2090  
  80         196  
336             local *LWP'UserAgent'redirect_ok = sub {
337 16     16   45942 local $@;
338 16         31 eval{
339 16         59 _check_url(
340             $_[1]->url,
341             $self->[mech]->uri,
342             NETWORK_ERR,
343             )
344             };
345 16         405 ++$redirected;
346 16         131 not $error=$@
347 80         1543 };
348              
349             # send request
350 80         593 $res = $self->[res] = $clone->request($request);
351             }
352              
353             # check for bad redirects
354 80 100 66     237314 if(
      66        
      66        
      66        
      66        
      66        
      66        
355             $error
356             or
357             # (check $redirected first to avoid a method call)
358             $redirected and $res->code =~ /^3/ and
359             (
360             $async || (
361             $error = new HTML'DOM'Exception NETWORK_ERR,
362             "Infinite redirect"
363             ),
364             1
365             )
366             or
367             defined($error = $res->header('client-warning'))
368             and (
369             $async || (
370             $error = new HTML'DOM'Exception NETWORK_ERR, $error
371             ),
372             1
373             )
374             ) {
375 6         383 $self->[state] = 4;
376 6         15 delete $self->[res];
377 6 100       88 $async ? ($self->_trigger_orsc, return) : die $error;
378             }
379              
380 74 100       8244 if($async) {
381 22         109 $self->[state] = LOADING;
382 22         78 $self->_trigger_orsc;
383 22 100       130 $self->[state] or return;
384             }
385              
386 73   100     2050 $self->[xml] = ($res->content_type||'') =~
387             /(?:^(?:application|text)\/xml|\+xml)\z/ || undef;
388             # This needs to be undef, rather than false, for responseXML to
389             # work correctly.
390              
391 73         2898 $self->[state] = 4; # complete
392 73         218 $self->_trigger_orsc;
393 73         149 delete $self->[tree] ;
394              
395             _:
396 73         1198 }
397              
398             sub abort { # ~~~ This must needs slay the other process once we have
399             # unfeigned asynchrony.
400 7     7   60508 delete +(my $self = shift)->[res];
401 7 100 100     76 $self->[state] > 1 && $$self[state] < 4 and
402             $self->[state] = 4,
403             $self->_trigger_orsc;
404 7         20 $self->[state] = 0;
405             return
406 7         36 }
407              
408             sub getAllResponseHeaders {
409 3     3   19 no warnings 'uninitialized';
  3         7  
  3         463  
410 11 100   11   5926 $_[0][state] <= OPENED
411             and die new HTML'DOM'Exception INVALID_STATE_ERR,
412             "getAllResponseHeaders only works after calls to send()";
413 9   100     81 (shift->[res]||return '')->headers->as_string("\r\n");
414             }
415              
416             sub getResponseHeader {
417 3     3   17 no warnings 'uninitialized';
  3         6  
  3         4445  
418 5 100   5   22992 $_[0][state] <= OPENED
419             and die new HTML'DOM'Exception INVALID_STATE_ERR,
420             "getResponseHeader only works after calls to send()";
421 4   100     32 (shift->[res]||return)->header(shift)
422             }
423              
424             sub setRequestHeader {
425 32 100   32   24769 die new HTML::DOM::Exception INVALID_STATE_ERR,
426             "setRequestHeader can only be called between open and send"
427             unless $_[0][state] == OPENED;
428 27 100       172 $_[1] =~ /^$http_token\z/o
429             or die new HTML::DOM::Exception SYNTAX_ERR,
430             "Invalid HTTP header name: $_[1]";
431 26 100       1419 defined $_[2] or return;
432 25 100       235 $_[2] =~ /^$http_field_val\z/o
433             or die new HTML::DOM::Exception SYNTAX_ERR,
434             "Invalid HTTP header value: $_[2]";
435              
436             # This regexp does not include all those in the 4th of Sep.
437             # Editor’s Draft of the spec. Anyway the spec only says ‘SHOULD’,
438             # so we are still compliant in this regard. I have very specific
439             # reasons for letting these through:
440             # Accept-Charset There is no reason the user agent should have
441             # to support charsets requested by a script. The
442             # script itself can decode the charset (once I’ve
443             # implemented overrideMimeType or responseData).
444             # Authorization If the user agent does not support an authenti-
445             # cation method, this should not prevent a script
446             # from using it.
447             # Cookie(2) Fake cookies are known enough to be documented
448             # in some books on Ajax/JS; e.g., the Rhino.
449             # User-Agent Some server-side scripts might want to distin-
450             # guish between actual user requests and script-
451             # based requests. After all, the scripts will be
452             # originating from the same server, so it’s not a
453             # matter of security.
454 24 100       656 return if $_[1] =~ /^(?:
455             (?:
456             accept-encoding
457             |
458             con(?:nection|tent-(?:length|transfer-encoding))
459             |
460             (?:dat|keep-aliv|upgrad)e
461             |
462             (?:expec|hos)t
463             |
464             referer
465             |
466             t(?:e|ra(?:iler|nsfer-encoding))
467             |
468             via
469             |
470             )\z
471             |
472             (?:proxy|sec)-
473             )/xi;
474              
475 8   100     123 push@{shift->[headers] ||= []}, ''.shift, ''.shift;
  8         71  
476             # We have to stringify to avoid making LWP hiccough.
477             }
478              
479              
480             # Attributes
481              
482             sub onreadystatechange {
483 11     11   17967 my $old = $_[0]->[orsc]{attr};
484 11 100       88 defined $_[1]
    50          
485             ? $_[0]->[orsc]{attr} = $_[1]
486             : delete $_[0]->[orsc]{attr}
487             if @_ > 1;
488 11         28 $old;
489             }
490              
491             sub readyState {
492 125     125   145138 int shift->[state];
493             }
494              
495             sub responseText { # string response from the server
496 78   100 78   212679 my $content = (my $res = $_[0]->[res]||return '')->content;
497 76         1669 my $cs = content_charset $res;
498 76 100       20822 my $ret = decode defined $cs ? $cs : utf8 => $content;
499 76         15241 $ret =~ s/\x{fffd}+/\x{fffd}/g;
500 76         556 $ret;
501             }
502              
503             sub responseXML { # XML::DOM::Lite object
504 17     17   21240 my $self = shift;
505 17 100       80 $$self[state] == 4 or return;
506 13 100 66     94 $$self[tree] || $$self[xml] && do {
507 5         2827 require WWW::Scripter::Plugin::Ajax::_xml_stuff;
508 5         45 $$self[mech]->plugin('JavaScript')->bind_classes(
509             \%WWW::Scripter::Plugin::Ajax::_xml_interf
510 5 100       17 ) unless ${$$self[mech]->plugin('Ajax')}++;
511 5         18302 $self->[tree] =
512             XML::DOM::Lite::Parser->parse($$self[res]->content);
513             # ~~~ xdlp returns an empty document when there is a parse
514             # error. Could I detect that and return nothing? Or can
515             # a valid XML document be empty?
516             }
517             }
518              
519             sub status { # HTTP status code
520 8 100   8   23324 die "The HTTP status code is not available yet"
521             if $_[0][state] < 3;
522 6         41 shift->[res]->code
523             }
524              
525             sub statusText { # HTTP status massage
526 5 100   5   55488 die "The HTTP status message is not available yet"
527             if $_[0][state] < 3;
528 3         28 shift->[res]->message
529             }
530              
531              
532             # EventTarget Methods
533              
534             sub _trigger_orsc {
535 239     239   2039 (my $event = (my $self = shift)->[mech]->document
536             ->createEvent
537             )->initEvent('readystatechange'); # 2nd and 3rg args false
538 239         21816 $self->dispatchEvent($event);
539 239         2827 return;
540             }
541              
542             sub addEventListener {
543 5     5   5210 my ($self,$name,$listener, $capture) = @_;
544 5 100       16 return if $capture;
545 4 100       111 return unless $name =~ /^readystatechange\z/i;
546 3         121 $$self[orsc]{refaddr $listener} = $listener;
547 3         15 return;
548             }
549              
550             sub removeEventListener {
551 2     2   2049 my ($self,$name,$listener, $capture) = @_;
552 2 50       6 return if $capture;
553 2 50       80 return unless $name =~ /^readystatechange\z/i;
554 2 50       94 exists $$self[orsc] &&
555             delete $$self[orsc]{refaddr $listener};
556 2         9 return;
557             }
558              
559             # ~~~ What about a ‘this’ value?
560             sub dispatchEvent { # This is where all the work is.
561 240     240   27193 my ($target, $event) = @_;
562 240         774 my $name = $event->type;
563 240 50       1757 return unless $name =~ /^readystatechange\z/i;
564              
565 240         1868 my $eh = $target->[mech]->document->error_handler;
566              
567 240         30370 $event->_set_target($target);
568 240         1742 $event->_set_eventPhase(HTML::DOM::Event::AT_TARGET);
569 240         1553 $event->_set_currentTarget($target);
570 240 100       1680 {eval {
  240         1684  
571 66 50 33     1589 defined blessed $_ && $_->can('handleEvent') ?
572             $_->handleEvent($event) : &$_($event);
573 66         70810 1
574 240   0     313 } or $eh and &$eh() for values %{$target->[orsc]||last};}
      33        
575 240         1035 return !cancelled $event;
576             }
577              
578             !+()
579              
580             __END__