File Coverage

blib/lib/Metabrik/Client/Www.pm
Criterion Covered Total %
statement 9 343 2.6
branch 0 182 0.0
condition 0 57 0.0
subroutine 3 37 8.1
pod 1 31 3.2
total 13 650 2.0


line stmt bran cond sub pod time code
1             #
2             # $Id$
3             #
4             # client::www Brik
5             #
6             package Metabrik::Client::Www;
7 9     9   73 use strict;
  9         18  
  9         257  
8 9     9   49 use warnings;
  9         17  
  9         255  
9              
10 9     9   43 use base qw(Metabrik);
  9         20  
  9         29583  
11              
12             sub brik_properties {
13             return {
14 0     0 1   revision => '$Revision$',
15             tags => [ qw(unstable browser http javascript screenshot) ],
16             author => 'GomoR ',
17             license => 'http://opensource.org/licenses/BSD-3-Clause',
18             attributes => {
19             uri => [ qw(uri) ],
20             username => [ qw(username) ],
21             password => [ qw(password) ],
22             ignore_content => [ qw(0|1) ],
23             user_agent => [ qw(user_agent) ],
24             ssl_verify => [ qw(0|1) ],
25             datadir => [ qw(datadir) ],
26             timeout => [ qw(0|1) ],
27             rtimeout => [ qw(timeout) ],
28             add_headers => [ qw(http_headers_hash) ],
29             do_javascript => [ qw(0|1) ],
30             do_redirects => [ qw(0|1) ],
31             src_ip => [ qw(ip_address) ],
32             max_redirects => [ qw(count) ],
33             client => [ qw(object) ],
34             _last => [ qw(object|INTERNAL) ],
35             _last_code => [ qw(code|INTERNAL) ],
36             },
37             attributes_default => {
38             ssl_verify => 0,
39             ignore_content => 0,
40             timeout => 0,
41             rtimeout => 10,
42             add_headers => {},
43             do_javascript => 0,
44             do_redirects => 1,
45             max_redirects => 10,
46             },
47             commands => {
48             install => [ ], # Inherited
49             create_user_agent => [ ],
50             reset_user_agent => [ ],
51             get => [ qw(uri|OPTIONAL username|OPTIONAL password|OPTIONAL) ],
52             cat => [ qw(uri|OPTIONAL username|OPTIONAL password|OPTIONAL) ],
53             post => [ qw(content_hash uri|OPTIONAL username|OPTIONAL password|OPTIONAL) ],
54             patch => [ qw(content_hash uri|OPTIONAL username|OPTIONAL password|OPTIONAL) ],
55             put => [ qw(content_hash uri|OPTIONAL username|OPTIONAL password|OPTIONAL) ],
56             head => [ qw(uri|OPTIONAL username|OPTIONAL password|OPTIONAL) ],
57             delete => [ qw(uri|OPTIONAL username|OPTIONAL password|OPTIONAL) ],
58             options => [ qw(uri|OPTIONAL username|OPTIONAL password|OPTIONAL) ],
59             code => [ ],
60             content => [ ],
61             get_content => [ qw(uri|OPTIONAL username|OPTIONAL password|OPTIONAL) ],
62             post_content => [ qw(content_hash uri|OPTIONAL username|OPTIONAL password|OPTIONAL) ],
63             save_content => [ qw(output) ],
64             headers => [ ],
65             get_response_headers => [ ],
66             delete_request_header => [ qw(header) ],
67             get_response_header => [ qw(header) ],
68             set_request_header => [ qw(header value|value_list) ],
69             forms => [ ],
70             links => [ ],
71             trace_redirect => [ qw(uri|OPTIONAL) ],
72             screenshot => [ qw(uri output) ],
73             eval_javascript => [ qw(js) ],
74             info => [ qw(uri|OPTIONAL) ],
75             mirror => [ qw(url|$url_list output|OPTIONAL datadir|OPTIONAL) ],
76             parse => [ qw(html) ],
77             get_last => [ ],
78             get_last_code => [ ],
79             },
80             require_modules => {
81             'IO::Socket::SSL' => [ ],
82             'Progress::Any::Output' => [ ],
83             'Progress::Any::Output::TermProgressBarColor' => [ ],
84             'Data::Dumper' => [ ],
85             'HTML::TreeBuilder' => [ ],
86             'LWP::UserAgent' => [ ],
87             'LWP::UserAgent::ProgressAny' => [ ],
88             'HTTP::Request' => [ ],
89             'HTTP::Request::Common' => [ ],
90             'WWW::Mechanize' => [ ],
91             'Mozilla::CA' => [ ],
92             'HTML::Form' => [ ],
93             'Metabrik::File::Write' => [ ],
94             'Metabrik::System::File' => [ ],
95             'Metabrik::Network::Address' => [ ],
96             },
97             optional_modules => {
98             'WWW::Mechanize::PhantomJS' => [ ],
99             },
100             optional_binaries => {
101             phantomjs => [ ],
102             },
103             };
104             }
105              
106             sub create_user_agent {
107 0     0 0   my $self = shift;
108 0           my ($uri, $username, $password) = @_;
109              
110 0           $self->log->debug("create_user_agent: creating agent");
111              
112 0   0       $uri ||= $self->uri;
113              
114             # Use IO::Socket::SSL which supports timeouts among other things.
115 0           $ENV{PERL_NET_HTTPS_SSL_SOCKET_CLASS} = 'IO::Socket::SSL';
116              
117 0 0         my $ssl_verify = $self->ssl_verify
118             ? IO::Socket::SSL::SSL_VERIFY_PEER()
119             : IO::Socket::SSL::SSL_VERIFY_NONE();
120              
121 0           my %args = (
122             stack_depth => 0, # Default is infinite, and will eat-up whole memory.
123             # 0 means completely turn off the feature.
124             autocheck => 0, # Do not throw on error by checking HTTP code. Let us do it.
125             timeout => $self->rtimeout,
126             ssl_opts => {
127             verify_hostname => $self->ssl_verify,
128             SSL_verify_mode => $ssl_verify,
129             SSL_ca_file => Mozilla::CA::SSL_ca_file(),
130             # SNI support - defaults to PeerHost
131             # SSL_hostname => 'hostname',
132             },
133             );
134              
135 0           my $mechanize = 'WWW::Mechanize';
136 0 0         if ($self->do_javascript) {
137 0 0 0       if ($self->brik_has_module('WWW::Mechanize::PhantomJS')
138             && $self->brik_has_binary('phantomjs')) {
139 0           $mechanize = 'WWW::Mechanize::PhantomJS';
140             }
141             else {
142 0           return $self->log->error("create_user_agent: module [WWW::Mechanize::PhantomJS] not found, cannot do_javascript");
143             }
144             }
145 0 0 0       if ((! $self->do_redirects) && $mechanize eq 'WWW::Mechanize::PhantomJS') {
    0          
146 0           $self->log->warning("create_user_agent: module [WWW::Mechanize::PhantomJS] does ".
147             "not support do_redirects, won't use it.");
148             }
149             elsif ($self->do_redirects) {
150 0           $args{max_redirect} = $self->max_redirects;
151             }
152             else { # Follow redirects not wanted
153 0           $args{max_redirect} = 0;
154             }
155              
156 0           my $src_ip = $self->src_ip;
157 0 0         if (defined($src_ip)) {
158 0 0         my $na = Metabrik::Network::Address->new_from_brik_init($self) or return;
159 0 0         if (! $na->is_ip($src_ip)) {
160 0           return $self->log->error("create_user_agent: src_ip [$src_ip] is invalid");
161             }
162 0           $args{local_address} = $src_ip;
163             }
164              
165 0           my $mech = $mechanize->new(%args);
166 0 0         if (! defined($mech)) {
167 0           return $self->log->error("create_user_agent: unable to create WWW::Mechanize object");
168             }
169              
170 0 0         if ($self->user_agent) {
171 0           $mech->agent($self->user_agent);
172             }
173             else {
174             # Some WWW::Mechanize::* modules can't do that
175 0 0         if ($mech->can('agent_alias')) {
176 0           $mech->agent_alias('Linux Mozilla');
177             }
178             }
179              
180 0 0         $username = defined($username) ? $username : $self->username;
181 0 0         $password = defined($password) ? $password : $self->password;
182 0 0 0       if (defined($username) && defined($password)) {
183 0           $self->log->debug("create_user_agent: using Basic authentication");
184 0           $mech->cookie_jar({});
185 0           $mech->credentials($username, $password);
186             }
187              
188 0 0         if ($self->log->level > 2) {
189 0     0     $mech->add_handler("request_send", sub { shift->dump; return });
  0            
  0            
190 0     0     $mech->add_handler("response_done", sub { shift->dump; return });
  0            
  0            
191             }
192              
193 0           return $mech;
194             }
195              
196             sub reset_user_agent {
197 0     0 0   my $self = shift;
198              
199 0           $self->client(undef);
200              
201 0           return 1;
202             }
203              
204             sub _method {
205 0     0     my $self = shift;
206 0           my ($uri, $username, $password, $method, $data) = @_;
207              
208 0   0       $uri ||= $self->uri;
209 0 0         $self->brik_help_run_undef_arg($method, $uri) or return;
210              
211 0           $self->timeout(0);
212              
213 0 0         $username = defined($username) ? $username : $self->username;
214 0 0         $password = defined($password) ? $password : $self->password;
215 0           my $client = $self->client;
216 0 0         if (! defined($self->client)) {
217 0 0         $client = $self->create_user_agent($uri, $username, $password) or return;
218 0           $self->client($client);
219             }
220              
221 0           my $add_headers = $self->add_headers;
222 0 0         if (defined($add_headers)) {
223 0           for my $k (keys %$add_headers) {
224 0           my $v = $add_headers->{$k};
225 0 0         if (ref($v) eq 'ARRAY') {
226 0           my $this = join('; ', @$v);
227 0           $client->add_header($k => $this);
228             }
229             else {
230 0           $client->add_header($k => $v);
231             }
232             }
233             }
234              
235 0           $self->log->verbose("$method: $uri");
236              
237 0           my $response;
238 0           eval {
239 0 0 0       if ($method ne 'get' && ref($client) eq 'WWW::Mechanize::PhantomJS') {
240 0           return $self->log->error("$method: method not supported by WWW::Mechanize::PhantomJS");
241             }
242 0 0 0       if ($method eq 'post' || $method eq 'put') {
    0 0        
    0          
243 0           $response = $client->$method($uri, Content => $data);
244             }
245             elsif ($method eq 'patch') {
246             # https://stackoverflow.com/questions/23910962/how-to-send-a-http-patch-request-with-lwpuseragent
247 0           my $req = HTTP::Request::Common::PATCH($uri, [ %$data ]);
248 0           $response = $client->request($req);
249             }
250             elsif ($method eq 'options' || $method eq 'patch') {
251 0           my $req = HTTP::Request->new($method, $uri, $add_headers);
252 0           $response = $client->request($req);
253             }
254             else {
255 0           $response = $client->$method($uri);
256             }
257             };
258 0 0         if ($@) {
259 0           chomp($@);
260 0 0         if ($@ =~ /read timeout/i) {
261 0           $self->timeout(1);
262             }
263 0           return $self->log->error("$method: unable to use method [$method] to uri [$uri]: $@");
264             }
265              
266 0           $self->_last($response);
267              
268 0           my %r = ();
269 0           $r{code} = $response->code;
270 0 0         if (! $self->ignore_content) {
271 0 0         if ($self->do_javascript) {
272             # decoded_content method is available in WWW::Mechanize::PhantomJS
273             # but is available in HTTP::Request response otherwise.
274 0           $r{content} = $client->decoded_content;
275             }
276             else {
277 0           $r{content} = $response->decoded_content;
278             }
279             }
280              
281             # Error messages seen from IO::Socket::SSL module.
282 0 0         if ($r{content} =~ /^Can't connect to .+Connection timed out at /is) {
    0          
    0          
283 0           $self->timeout(1);
284 0           return $self->log->error("$method: $uri: connection timed out");
285             }
286             elsif ($r{content} =~ /^Can't connect to .+?\n\n(.+?) at /is) {
287 0           return $self->log->error("$method: $uri: ".lcfirst($1));
288             }
289             elsif ($r{content} =~ /^Connect failed: connect: Interrupted system call/i) {
290 0           return $self->log->error("$method: $uri: connection interrupted by syscall");
291             }
292              
293 0           my $headers = $response->headers;
294 0           $r{headers} = { map { $_ => $headers->{$_} } keys %$headers };
  0            
295 0           delete $r{headers}->{'::std_case'};
296              
297 0           return \%r;
298             }
299              
300             sub get {
301 0     0 0   my $self = shift;
302 0           my ($uri, $username, $password) = @_;
303              
304 0           return $self->_method($uri, $username, $password, 'get');
305             }
306              
307             sub cat {
308 0     0 0   my $self = shift;
309 0           my ($uri, $username, $password) = @_;
310              
311 0 0         $self->_method($uri, $username, $password, 'get') or return;
312 0           return $self->content;
313             }
314              
315             sub post {
316 0     0 0   my $self = shift;
317 0           my ($href, $uri, $username, $password) = @_;
318              
319 0 0         $self->brik_help_run_undef_arg('post', $href) or return;
320              
321 0           return $self->_method($uri, $username, $password, 'post', $href);
322             }
323              
324             sub put {
325 0     0 0   my $self = shift;
326 0           my ($href, $uri, $username, $password) = @_;
327              
328 0 0         $self->brik_help_run_undef_arg('put', $href) or return;
329              
330 0           return $self->_method($uri, $username, $password, 'put', $href);
331             }
332              
333             sub patch {
334 0     0 0   my $self = shift;
335 0           my ($href, $uri, $username, $password) = @_;
336              
337 0 0         $self->brik_help_run_undef_arg('patch', $href) or return;
338              
339 0           return $self->_method($uri, $username, $password, 'patch', $href);
340             }
341              
342             sub delete {
343 0     0 0   my $self = shift;
344 0           my ($uri, $username, $password) = @_;
345              
346 0           return $self->_method($uri, $username, $password, 'delete');
347             }
348              
349             sub options {
350 0     0 0   my $self = shift;
351 0           my ($uri, $username, $password) = @_;
352              
353 0           return $self->_method($uri, $username, $password, 'options');
354             }
355              
356             sub head {
357 0     0 0   my $self = shift;
358 0           my ($uri, $username, $password) = @_;
359              
360 0           return $self->_method($uri, $username, $password, 'head');
361             }
362              
363             sub code {
364 0     0 0   my $self = shift;
365              
366 0           my $last = $self->_last;
367 0 0         if (! defined($last)) {
368 0           return $self->log->error("code: you have to execute a request first");
369             }
370              
371 0           return $last->code;
372             }
373              
374             sub content {
375 0     0 0   my $self = shift;
376              
377 0           my $last = $self->_last;
378 0 0         if (! defined($last)) {
379 0           return $self->log->error("content: you have to execute a request first");
380             }
381              
382 0 0         if ($self->do_javascript) {
383             # decoded_content method is available in WWW::Mechanize::PhantomJS
384             # but is available in HTTP::Request response otherwise.
385 0           my $client = $self->client;
386 0           return $client->decoded_content;
387             }
388              
389 0           return $last->decoded_content;
390             }
391              
392             sub get_content {
393 0     0 0   my $self = shift;
394 0           my @args = @_;
395              
396 0 0         $self->get(@args) or return;
397 0           return $self->content;
398             }
399              
400             sub post_content {
401 0     0 0   my $self = shift;
402 0           my @args = @_;
403              
404 0 0         $self->post(@args) or return;
405 0           return $self->content;
406             }
407              
408             sub save_content {
409 0     0 0   my $self = shift;
410 0           my ($output) = @_;
411              
412 0           my $last = $self->_last;
413 0 0         if (! defined($last)) {
414 0           return $self->log->error("save_content: you have to execute a request first");
415             }
416              
417 0           eval {
418 0           $self->client->save_content($output);
419             };
420 0 0         if ($@) {
421 0           chomp($@);
422 0           return $self->log->error("save_content: unable to save content: $@");
423             }
424              
425 0           return 1;
426             }
427              
428             sub headers {
429 0     0 0   my $self = shift;
430              
431 0           my $last = $self->_last;
432 0 0         if (! defined($last)) {
433 0           return $self->log->error("headers: you have to execute a request first");
434             }
435              
436 0           return $last->headers;
437             }
438              
439             #
440             # Alias for headers Command
441             #
442             sub get_response_headers {
443 0     0 0   my $self = shift;
444              
445 0           return $self->headers;
446             }
447              
448             #
449             # Remove one header for next request.
450             #
451             sub delete_request_header {
452 0     0 0   my $self = shift;
453 0           my ($header) = @_;
454              
455 0 0         $self->brik_help_run_undef_arg('delete_header', $header) or return;
456              
457 0           my $headers = $self->add_headers;
458 0   0       my $value = $headers->{$header} || 'undef';
459 0           delete $headers->{$header};
460              
461 0           return $value;
462             }
463              
464             #
465             # Return one header from last response.
466             #
467             sub get_response_header {
468 0     0 0   my $self = shift;
469 0           my ($header) = @_;
470              
471 0 0         $self->brik_help_run_undef_arg('get_header', $header) or return;
472              
473 0 0         my $headers = $self->headers or return;
474 0 0         if (exists($headers->{$header})) {
475 0           return $headers->{$header};
476             }
477              
478 0           $self->log->verbose("get_header: header [$header] not found");
479              
480 0           return 0;
481             }
482              
483             #
484             # Set header for next request.
485             #
486             sub set_request_header {
487 0     0 0   my $self = shift;
488 0           my ($header, $value) = @_;
489              
490 0 0         $self->brik_help_run_undef_arg('set_request_header', $header) or return;
491 0 0         $self->brik_help_run_undef_arg('set_request_header', $value) or return;
492              
493 0           my $headers = $self->add_headers;
494 0           $headers->{$header} = $value;
495              
496 0           return $value;
497             }
498              
499             sub links {
500 0     0 0   my $self = shift;
501              
502 0           my $last = $self->_last;
503 0 0         if (! defined($last)) {
504 0           return $self->log->error("links: you have to execute a request first");
505             }
506              
507 0           my @links = ();
508 0           for my $l ($self->client->links) {
509 0           push @links, $l->url;
510 0           $self->log->verbose("links: found link [".$l->url."]");
511             }
512              
513 0           return \@links;
514             }
515              
516             sub forms {
517 0     0 0   my $self = shift;
518              
519 0           my $last = $self->_last;
520 0 0         if (! defined($last)) {
521 0           return $self->log->error("forms: you have to execute a request first");
522             }
523              
524 0           my $client = $self->client;
525              
526 0 0         if ($self->log->level > 2) {
527 0           print Data::Dumper::Dumper($last->headers)."\n";
528             }
529              
530             # We use our own "manual" way to get access to content:
531             # WWW::Mechanize::PhantomJS is clearly broken, and we have to support
532             # WWW::Mechanize also. At some point, we should write a good WWW::Mechanize::PhantomJS
533             # module.
534             #my @forms = $client->forms;
535 0 0         my $content = $self->content or return;
536 0           my @forms = HTML::Form->parse($content, $client->base);
537              
538 0           my @result = ();
539 0           for my $form (@forms) {
540 0   0       my $name = $form->{attr}{name} || 'undef';
541 0           my $action = $form->{action};
542 0   0       my $method = $form->{method} || 'undef';
543              
544 0           my $h = {
545             action => $action->as_string,
546             method => $method,
547             };
548              
549 0           for my $input (@{$form->{inputs}}) {
  0            
550 0   0       my $type = $input->{type} || '';
551 0   0       my $name = $input->{name} || '';
552 0   0       my $value = $input->{value} || '';
553 0 0         if ($type ne 'submit') {
554 0           $h->{input}{$name} = $value;
555             }
556             }
557              
558 0           push @result, $h;
559             }
560              
561 0           return \@result;
562             }
563              
564             sub trace_redirect {
565 0     0 0   my $self = shift;
566 0           my ($uri, $username, $password) = @_;
567              
568 0   0       $uri ||= $self->uri;
569 0 0         $self->brik_help_run_undef_arg('trace_redirect', $uri) or return;
570              
571 0           my $prev = $self->do_redirects;
572 0           $self->do_redirects(0);
573              
574 0           my @results = ();
575              
576 0           my $location = $uri;
577             # Max 20 redirects
578 0           for (1..20) {
579 0           $self->log->verbose("trace_redirect: $location");
580              
581 0           my $response;
582 0           eval {
583 0           $response = $self->get($location);
584             };
585 0 0         if ($@) {
586 0           chomp($@);
587 0           return $self->log->error("trace_redirect: unable to get uri [$uri]: $@");
588             }
589              
590 0           my $this = {
591             uri => $location,
592             code => $self->code,
593             };
594 0           push @results, $this;
595              
596 0 0 0       if ($this->{code} != 302 && $this->{code} != 301) {
597 0           last;
598             }
599              
600 0           $location = $this->{location} = $self->headers->{location};
601             }
602              
603 0           $self->do_redirects($prev);
604              
605 0           return \@results;
606             }
607              
608             sub screenshot {
609 0     0 0   my $self = shift;
610 0           my ($uri, $output) = @_;
611              
612 0 0         $self->brik_help_run_undef_arg('screenshot', $uri) or return;
613 0 0         $self->brik_help_run_undef_arg('screenshot', $output) or return;
614              
615 0 0 0       if ($self->brik_has_module('WWW::Mechanize::PhantomJS')
616             && $self->brik_has_binary('phantomjs')) {
617 0 0         my $mech = WWW::Mechanize::PhantomJS->new
618             or return $self->log->error("screenshot: PhantomJS failed");
619              
620 0 0         my $get = $mech->get($uri)
621             or return $self->log->error("screenshot: get uri [$uri] failed");
622              
623 0 0         my $data = $mech->content_as_png
624             or return $self->log->error("screenshot: content_as_png failed");
625              
626 0 0         my $write = Metabrik::File::Write->new_from_brik_init($self) or return;
627 0           $write->encoding('ascii');
628 0           $write->overwrite(1);
629 0           $write->append(0);
630              
631 0 0         $write->open($output) or return $self->log->error("screenshot: open failed");
632 0 0         $write->write($data) or return $self->log->error("screenshot: write failed");
633 0           $write->close;
634              
635 0           return $output;
636             }
637              
638 0           return $self->log->error("screenshot: optional module [WWW::Mechanize::PhantomJS] and optional binary [phantomjs] are not available");
639             }
640              
641             sub eval_javascript {
642 0     0 0   my $self = shift;
643 0           my ($js) = @_;
644              
645 0 0         $self->brik_help_run_undef_arg('eval_javascript', $js) or return;
646              
647             # Perl module Wight may also be an option.
648              
649 0 0 0       if ($self->brik_has_module('WWW::Mechanize::PhantomJS')
650             && $self->brik_has_binary('phantomjs')) {
651 0 0         my $mech = WWW::Mechanize::PhantomJS->new(launch_arg => ['ghostdriver/src/main.js'])
652             or return $self->log->error("eval_javascript: PhantomJS failed");
653              
654 0           return $mech->eval_in_page($js);
655             }
656              
657 0           return $self->log->error("eval_javascript: optional module [WWW::Mechanize::PhantomJS] ".
658             "and optional binary [phantomjs] are not available");
659             }
660              
661             sub info {
662 0     0 0   my $self = shift;
663 0           my ($uri) = @_;
664              
665 0   0       $uri ||= $self->uri;
666 0 0         $self->brik_help_run_undef_arg('info', $uri) or return;
667              
668 0 0         my $r = $self->get($uri) or return;
669 0           my $headers = $r->{headers};
670              
671             # Taken from apps.json from Wappalyzer
672 0           my @headers = qw(
673             IBM-Web2-Location
674             X-Drupal-Cache
675             X-Powered-By
676             X-Drectory-Script
677             Set-Cookie
678             X-Powered-CMS
679             X-KoobooCMS-Version
680             X-ATG-Version
681             User-Agent
682             X-Varnish
683             X-Compressed-By
684             X-Firefox-Spdy
685             X-ServedBy
686             MicrosoftSharePointTeamServices
687             Set-Cookie
688             Generator
689             X-CDN
690             Server
691             X-Tumblr-User
692             X-XRDS-Location
693             X-Content-Encoded-By
694             X-Ghost-Cache-Status
695             X-Umbraco-Version
696             X-Rack-Cache
697             Liferay-Portal
698             X-Flow-Powered
699             X-Swiftlet-Cache
700             X-Lift-Version
701             X-Spip-Cache
702             X-Wix-Dispatcher-Cache-Hit
703             COMMERCE-SERVER-SOFTWARE
704             X-AMP-Version
705             X-Powered-By-Plesk
706             X-Akamai-Transformed
707             X-Confluence-Request-Time
708             X-Mod-Pagespeed
709             Composed-By
710             Via
711             );
712              
713 0 0         if ($self->log->level > 2) {
714 0           print Data::Dumper::Dumper($headers)."\n";
715             }
716              
717 0           my %info = ();
718 0           for my $hdr (@headers) {
719 0           my $this = $headers->header(lc($hdr));
720 0 0         $info{$hdr} = $this if defined($this);
721             }
722              
723 0           my $title = $r->{title};
724 0 0         if (defined($title)) {
725 0           print "Title: $title\n";
726             }
727              
728 0           for my $k (sort { $a cmp $b } keys %info) {
  0            
729 0           print "$k: ".$info{$k}."\n";
730             }
731              
732 0           return 1;
733             }
734              
735             sub mirror {
736 0     0 0   my $self = shift;
737 0           my ($url, $output, $datadir) = @_;
738              
739 0   0       $datadir ||= $self->datadir;
740 0 0         $self->brik_help_run_undef_arg('mirror', $url) or return;
741 0 0         my $ref = $self->brik_help_run_invalid_arg('mirror', $url, 'SCALAR', 'ARRAY') or return;
742              
743 0           my @files = ();
744 0 0         if ($ref eq 'ARRAY') {
745 0 0         $self->brik_help_run_empty_array_arg('mirror', $url) or return;
746              
747 0           for my $this (@$url) {
748 0 0         my $file = $self->mirror($this, $output) or next;
749 0           push @files, @$file;
750             }
751             }
752             else {
753 0 0 0       if ($url !~ /^https?:\/\// && $url !~ /^ftp:\/\//) {
754 0           return $self->log->error("mirror: invalid URL [$url]");
755             }
756              
757 0 0         my $sf = Metabrik::System::File->new_from_brik_init($self) or return;
758 0 0         if (! defined($output)) {
759 0 0         my $filename = $sf->basefile($url) or return;
760 0           $output = $datadir.'/'.$filename;
761             }
762             else { # $output is defined
763 0 0         if (! $sf->is_absolute($output)) { # We want default datadir for output file
764 0           $output = $datadir.'/'.$output;
765             }
766             }
767              
768 0           $self->log->debug("mirror: url[$url] output[$output]");
769              
770 0 0         my $mech = $self->create_user_agent or return;
771 0           LWP::UserAgent::ProgressAny::__add_handlers($mech);
772 0           Progress::Any::Output->set("TermProgressBarColor");
773              
774 0           my $rc;
775 0           eval {
776 0           $rc = $mech->mirror($url, $output);
777             };
778 0 0         if ($@) {
779 0           chomp($@);
780 0           return $self->log->error("mirror: mirroring URL [$url] to local file [$output] failed: $@");
781             }
782 0           my $code = $rc->code;
783 0           $self->_last_code($code);
784 0 0         if ($code == 200) {
    0          
785 0           push @files, $output;
786 0           $self->log->verbose("mirror: downloading URL [$url] to local file [$output] done");
787             }
788             elsif ($code == 304) { # Not modified
789 0           $self->log->verbose("mirror: file [$output] not modified since last check");
790             }
791             else {
792 0           return $self->log->error("mirror: error while mirroring URL [$url] with code: [$code]");
793             }
794             }
795              
796 0           return \@files;
797             }
798              
799             sub parse {
800 0     0 0   my $self = shift;
801 0           my ($html) = @_;
802              
803 0 0         $self->brik_help_run_undef_arg('parse', $html) or return;
804              
805 0           return HTML::TreeBuilder->new_from_content($html);
806             }
807              
808             sub get_last {
809 0     0 0   my $self = shift;
810              
811 0           return $self->_last;
812             }
813              
814             sub get_last_code {
815 0     0 0   my $self = shift;
816              
817 0           return $self->_last_code;
818             }
819              
820             1;
821              
822             __END__