File Coverage

blib/lib/WWW/LinkRot.pm
Criterion Covered Total %
statement 33 157 21.0
branch 0 58 0.0
condition 0 6 0.0
subroutine 11 15 73.3
pod 4 4 100.0
total 48 240 20.0


line stmt bran cond sub pod time code
1             package WWW::LinkRot;
2 1     1   706 use warnings;
  1         2  
  1         33  
3 1     1   5 use strict;
  1         2  
  1         19  
4 1     1   4 use Carp;
  1         1  
  1         52  
5 1     1   4 use utf8;
  1         2  
  1         4  
6             require Exporter;
7             our @ISA = qw(Exporter);
8             our @EXPORT_OK = qw/
9             check_links
10             get_links
11             html_report
12             replace
13             /;
14             our %EXPORT_TAGS = (
15             all => \@EXPORT_OK,
16             );
17 1     1   836 use LWP::UserAgent;
  1         44727  
  1         35  
18 1     1   629 use HTML::Make;
  1         32274  
  1         45  
19 1     1   547 use HTML::Make::Page 'make_page';
  1         1248  
  1         99  
20 1     1   503 use File::Slurper qw!read_text write_text!;
  1         2266  
  1         62  
21 1     1   466 use JSON::Create 'write_json';
  1         1287  
  1         61  
22 1     1   8 use JSON::Parse 'read_json';
  1         2  
  1         56  
23 1     1   508 use Convert::Moji 'make_regex';
  1         2003  
  1         1481  
24              
25             our $VERSION = '0.02';
26              
27             sub get_links
28             {
29 0     0 1   my ($files) = @_;
30 0           my %links;
31 0           for my $file (@$files) {
32 0 0         if (! -f $file) {
33 0           carp "Can't find file '$file'";
34 0           next;
35             }
36 0           my $text = read_text ($file);
37             # Remove comments so that commented-out links don't appear in
38             # the results.
39 0           $text =~ s///gsm;
40 0           while ($text =~ /href=["'](.*?)["']/g) {
41 0           my $link = $1;
42 0           push @{$links{$link}}, $file;
  0            
43             }
44             }
45 0           return \%links;
46             }
47              
48             sub check_links
49             {
50 0     0 1   my ($links, %options) = @_;
51 0 0 0       if (! $links || ref $links ne 'HASH') {
52 0           carp "Usage: check_links (\%links, %options)";
53             }
54 0           my $out = $options{out};
55 0           my $verbose = $options{verbose};
56 0           my $nook = $options{nook};
57 0           my $tempfile = "$out-temp.json";
58 0           my %skip;
59 0           my $ua = LWP::UserAgent->new (
60             agent => __PACKAGE__,
61             );
62 0           $ua->max_redirect (0);
63             # Time out after five seconds (dead sites etc.)
64 0           $ua->timeout (5);
65              
66 0 0         if (-f $out) {
67 0           my $old = read_json ($out);
68 0           for my $link (@$old) {
69 0 0         if ($link->{status} =~ /200/) {
70 0           $skip{$link->{link}} = $link;
71             }
72             }
73             }
74 0           my $count = 0;
75 0           my @checks;
76 0           for my $link (sort keys %$links) {
77 0 0         if ($nook) {
78 0 0         if ($skip{$link}) {
79 0 0         if ($verbose) {
80 0           print "$link was OK last time, skipping\n";
81             }
82             # Keep a copy of this link in the output.
83 0           push @checks, $skip{$link};
84 0           next;
85             }
86             }
87             my %r = (
88             link => $link,
89 0           files => $links->{$link},
90             );
91 0 0         if ($verbose) {
92 0           print "Getting $link...\n";
93             }
94 0           my $res = $ua->get ($link);
95 0           $r{status} = $res->status_line ();
96 0 0         if ($r{status} =~ m!^30[12]!) {
97 0           $r{location} = $res->header ('location');
98             }
99 0           push @checks, \%r;
100 0           $count++;
101 0 0         if ($count % 5 == 0) {
102 0           write_json ($tempfile, \@checks, indent => 1, sort => 1);
103             }
104             }
105 0 0         unlink ($tempfile) or carp "Error unlinking $tempfile: $!";
106 0           write_json ($out, \@checks, indent => 1, sort => 1);
107             }
108              
109             sub html_report
110             {
111 0     0 1   my (%options) = @_;
112 0           my $links = read_json ($options{in});
113 0           my $title = $options{title};
114 0 0         if (! $title) {
115 0           $title = 'WWW::LinkRot link report';
116             }
117 0           my $style = <
118             .error {
119             background: gold;
120             }
121              
122             .moved {
123             background: pink;
124             }
125             EOF
126 0           my ($html, $body) = make_page (
127             title => $title,
128             style => $style,
129             );
130 0           $body->push ('h1', text => $title);
131 0           my $table = $body->push ('table');
132 0           for my $xlink (@$links) {
133 0           my $status = $xlink->{status};
134 0           my $class = 'OK';
135 0 0         if ($status =~ /30.*/) {
    0          
136 0           $class = 'moved';
137             }
138             elsif ($status =~ /^[45].*/) {
139 0           $class = 'error';
140             }
141 0           my $row = $table->push ('tr', class => $class,);
142 0           my $link = $row->push ('td');
143 0           my $text = $xlink->{link};
144 0 0         if (length ($text) > 100) {
145 0           $text = substr ($text, 0, 100);
146             }
147 0           my $h = $xlink->{link};
148 0           $link->push (
149             'a',
150             attr => {
151             target => '_blank',
152             href => $h,
153             },
154             text => $text,
155             );
156 0           my $archive = "https://web.archive.org/web/*/$h";
157 0           $link->push (
158             'a',
159             attr => {
160             href => $archive,
161             target => '_blank',
162             },
163             text => '[archive]',
164             );
165 0           my $statcell = $row->push ('td', text => $xlink->{status});
166 0 0         if ($class eq 'moved') {
167 0           my $loc = $xlink->{location};
168 0 0         if ($loc) {
169 0           my $hs = $h;
170 0           $hs =~ s!http!https!;
171 0 0         if ($hs eq $loc) {
172 0           $statcell->add_text (' (HTTPS)');
173             }
174             else {
175 0           $statcell->push ('a', href => $loc, text => $loc);
176             }
177             }
178             }
179 0 0         if ($options{nofiles}) {
180 0           next;
181             }
182 0           my $files = $row->push ('td');
183 0           my $filelist = $xlink->{files};
184 0 0         if ($filelist) {
185 0           my $nfiles = scalar (@$filelist);
186 0           my $maxfiles = $nfiles;
187 0 0         if ($nfiles > 5) {
188 0           $maxfiles = 5;
189             }
190 0           my $filen = 0;
191 0           for my $file (@$filelist) {
192 0           $filen++;
193 0 0         if ($filen > $maxfiles) {
194 0           last;
195             }
196 0 0         if ($options{strip}) {
197 0           $file =~ s!$options{strip}!!;
198             }
199 0           my $href;
200 0 0         if ($options{url}) {
201 0           $href = "$options{url}/$file";
202             }
203             else {
204 0           $href = $file;
205             }
206 0           $files->push (
207             'a',
208             attr => {target => '_blank', href => $href},
209             text => $file
210             );
211             }
212             }
213             }
214 0           write_text ($options{out}, $html->text ());
215             }
216              
217             sub replace
218             {
219 0     0 1   my ($links, $files, %options) = @_;
220 0           my $verbose = $options{verbose};
221 0           my @moved;
222 0           for my $l (keys %$links) {
223 0           my $link = $links->{$l};
224 0 0 0       if ($link->{status} =~ m!^30! && $link->{location}) {
225 0           push @moved, $l;
226 0 0         if ($verbose) {
227 0           print "Link '$l' to be edited.\n";
228             }
229             }
230             }
231 0           my $re = make_regex (@moved);
232 0           for my $file (@$files) {
233 0           my $text = read_text ($file);
234 0 0         if ($text =~ s!($re)!$links->{$1}{location}!g) {
235 0 0         if ($verbose) {
236 0           print "Some links in '$file' changed.\n";
237             }
238 0           write_text ($file, $text);
239             }
240             else {
241 0 0         if ($verbose) {
242 0           print "'$file' is unchanged, not writing.\n";
243             }
244             }
245             }
246             }
247              
248             1;