filecheck: New plugin factoring out the PageSpec additions that were originally part...
[ikiwiki] / IkiWiki / Plugin / filecheck.pm
1 #!/usr/bin/perl
2 package IkiWiki::Plugin::filecheck;
3
4 use warnings;
5 use strict;
6 use IkiWiki 2.00;
7
8 my %units=( #{{{        # size in bytes
9         B               => 1,
10         byte            => 1,
11         KB              => 2 ** 10,
12         kilobyte        => 2 ** 10,
13         K               => 2 ** 10,
14         KB              => 2 ** 10,
15         kilobyte        => 2 ** 10,
16         M               => 2 ** 20,
17         MB              => 2 ** 20,
18         megabyte        => 2 ** 20,
19         G               => 2 ** 30,
20         GB              => 2 ** 30,
21         gigabyte        => 2 ** 30,
22         T               => 2 ** 40,
23         TB              => 2 ** 40,
24         terabyte        => 2 ** 40,
25         P               => 2 ** 50,
26         PB              => 2 ** 50,
27         petabyte        => 2 ** 50,
28         E               => 2 ** 60,
29         EB              => 2 ** 60,
30         exabyte         => 2 ** 60,
31         Z               => 2 ** 70,
32         ZB              => 2 ** 70,
33         zettabyte       => 2 ** 70,
34         Y               => 2 ** 80,
35         YB              => 2 ** 80,
36         yottabyte       => 2 ** 80,
37         # ikiwiki, if you find you need larger data quantities, either modify
38         # yourself to add them, or travel back in time to 2008 and kill me.
39         #   -- Joey
40 ); #}}}
41
42 sub parsesize ($) { #{{{
43         my $size=shift;
44
45         no warnings;
46         my $base=$size+0; # force to number
47         use warnings;
48         foreach my $unit (sort keys %units) {
49                 if ($size=~/[0-9\s]\Q$unit\E$/i) {
50                         return $base * $units{$unit};
51                 }
52         }
53         return $base;
54 } #}}}
55
56 sub humansize ($) { #{{{
57         my $size=shift;
58
59         foreach my $unit (reverse sort { $units{$a} <=> $units{$b} || $b cmp $a } keys %units) {
60                 if ($size / $units{$unit} > 0.25) {
61                         return (int($size / $units{$unit} * 10)/10).$unit;
62                 }
63         }
64         return $size; # near zero, or negative
65 } #}}}
66
67 package IkiWiki::PageSpec;
68
69 sub match_maxsize ($$;@) { #{{{
70         my $page=shift;
71         my $maxsize=eval{IkiWiki::Plugin::attachment::parsesize(shift)};
72         if ($@) {
73                 return IkiWiki::FailReason->new("unable to parse maxsize (or number too large)");
74         }
75
76         my %params=@_;
77         my $file=exists $params{file} ? $params{file} : $IkiWiki::pagesources{$page};
78         if (! defined $file) {
79                 return IkiWiki::FailReason->new("no file specified");
80         }
81
82         if (-s $file > $maxsize) {
83                 return IkiWiki::FailReason->new("file too large (".(-s $file)." >  $maxsize)");
84         }
85         else {
86                 return IkiWiki::SuccessReason->new("file not too large");
87         }
88 } #}}}
89
90 sub match_minsize ($$;@) { #{{{
91         my $page=shift;
92         my $minsize=eval{IkiWiki::Plugin::attachment::parsesize(shift)};
93         if ($@) {
94                 return IkiWiki::FailReason->new("unable to parse minsize (or number too large)");
95         }
96
97         my %params=@_;
98         my $file=exists $params{file} ? $params{file} : $IkiWiki::pagesources{$page};
99         if (! defined $file) {
100                 return IkiWiki::FailReason->new("no file specified");
101         }
102
103         if (-s $file < $minsize) {
104                 return IkiWiki::FailReason->new("file too small");
105         }
106         else {
107                 return IkiWiki::SuccessReason->new("file not too small");
108         }
109 } #}}}
110
111 sub match_mimetype ($$;@) { #{{{
112         my $page=shift;
113         my $wanted=shift;
114
115         my %params=@_;
116         my $file=exists $params{file} ? $params{file} : $IkiWiki::pagesources{$page};
117         if (! defined $file) {
118                 return IkiWiki::FailReason->new("no file specified");
119         }
120
121         # Use ::magic to get the mime type, the idea is to only trust
122         # data obtained by examining the actual file contents.
123         eval q{use File::MimeInfo::Magic};
124         if ($@) {
125                 return IkiWiki::FailReason->new("failed to load File::MimeInfo::Magic ($@); cannot check MIME type");
126         }
127         my $mimetype=File::MimeInfo::Magic::magic($file);
128         if (! defined $mimetype) {
129                 $mimetype="unknown";
130         }
131
132         my $regexp=IkiWiki::glob2re($wanted);
133         if ($mimetype!~/^$regexp$/i) {
134                 return IkiWiki::FailReason->new("file MIME type is $mimetype, not $wanted");
135         }
136         else {
137                 return IkiWiki::SuccessReason->new("file MIME type is $mimetype");
138         }
139 } #}}}
140
141 sub match_virusfree ($$;@) { #{{{
142         my $page=shift;
143         my $wanted=shift;
144
145         my %params=@_;
146         my $file=exists $params{file} ? $params{file} : $IkiWiki::pagesources{$page};
147         if (! defined $file) {
148                 return IkiWiki::FailReason->new("no file specified");
149         }
150
151         if (! exists $IkiWiki::config{virus_checker} ||
152             ! length $IkiWiki::config{virus_checker}) {
153                 return IkiWiki::FailReason->new("no virus_checker configured");
154         }
155
156         # The file needs to be fed into the virus checker on stdin,
157         # because the file is not world-readable, and if clamdscan is
158         # used, clamd would fail to read it.
159         eval q{use IPC::Open2};
160         error($@) if $@;
161         open (IN, "<", $file) || return IkiWiki::FailReason->new("failed to read file");
162         binmode(IN);
163         my $sigpipe=0;
164         $SIG{PIPE} = sub { $sigpipe=1 };
165         my $pid=open2(\*CHECKER_OUT, "<&IN", $IkiWiki::config{virus_checker}); 
166         my $reason=<CHECKER_OUT>;
167         chomp $reason;
168         1 while (<CHECKER_OUT>);
169         close(CHECKER_OUT);
170         waitpid $pid, 0;
171         $SIG{PIPE}="DEFAULT";
172         if ($sigpipe || $?) {
173                 if (! length $reason) {
174                         $reason="virus checker $IkiWiki::config{virus_checker}; failed with no output";
175                 }
176                 return IkiWiki::FailReason->new("file seems to contain a virus ($reason)");
177         }
178         else {
179                 return IkiWiki::SuccessReason->new("file seems virusfree ($reason)");
180         }
181 } #}}}
182
183 sub match_ispage ($$;@) { #{{{
184         my $filename=shift;
185
186         if (defined IkiWiki::pagetype($filename)) {
187                 return IkiWiki::SuccessReason->new("file is a wiki page");
188         }
189         else {
190                 return IkiWiki::FailReason->new("file is not a wiki page");
191         }
192 } #}}}