9992f2c2699371eb39ddb86776bba6c18c7a3562
[ikiwiki.git] / IkiWiki / Plugin / filecheck.pm
1 #!/usr/bin/perl
2 package IkiWiki::Plugin::filecheck;
3
4 use warnings;
5 use strict;
6 use IkiWiki 3.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 import {
43         hook(type => "getsetup", id => "filecheck",  call => \&getsetup);
44 }
45
46 sub getsetup () {
47         return
48                 plugin => {
49                         safe => 1,
50                         rebuild => undef,
51                 },
52 }
53
54 sub parsesize ($) {
55         my $size=shift;
56
57         no warnings;
58         my $base=$size+0; # force to number
59         use warnings;
60         foreach my $unit (sort keys %units) {
61                 if ($size=~/[0-9\s]\Q$unit\E$/i) {
62                         return $base * $units{$unit};
63                 }
64         }
65         return $base;
66 }
67
68 # This is provided for other plugins that want to convert back the other way.
69 sub humansize ($) {
70         my $size=shift;
71
72         foreach my $unit (reverse sort { $units{$a} <=> $units{$b} || $b cmp $a } keys %units) {
73                 if ($size / $units{$unit} > 0.25) {
74                         return (int($size / $units{$unit} * 10)/10).$unit;
75                 }
76         }
77         return $size; # near zero, or negative
78 }
79
80 package IkiWiki::PageSpec;
81
82 sub match_maxsize ($$;@) {
83         my $page=shift;
84         my $maxsize=eval{IkiWiki::Plugin::filecheck::parsesize(shift)};
85         if ($@) {
86                 return IkiWiki::ErrorReason->new("unable to parse maxsize (or number too large)");
87         }
88
89         my %params=@_;
90         my $file=exists $params{file} ? $params{file} : IkiWiki::srcfile($IkiWiki::pagesources{$page});
91         if (! defined $file) {
92                 return IkiWiki::ErrorReason->new("file does not exist");
93         }
94
95         if (-s $file > $maxsize) {
96                 return IkiWiki::FailReason->new("file too large (".(-s $file)." >  $maxsize)");
97         }
98         else {
99                 return IkiWiki::SuccessReason->new("file not too large");
100         }
101 }
102
103 sub match_minsize ($$;@) {
104         my $page=shift;
105         my $minsize=eval{IkiWiki::Plugin::filecheck::parsesize(shift)};
106         if ($@) {
107                 return IkiWiki::ErrorReason->new("unable to parse minsize (or number too large)");
108         }
109
110         my %params=@_;
111         my $file=exists $params{file} ? $params{file} : IkiWiki::srcfile($IkiWiki::pagesources{$page});
112         if (! defined $file) {
113                 return IkiWiki::ErrorReason->new("file does not exist");
114         }
115
116         if (-s $file < $minsize) {
117                 return IkiWiki::FailReason->new("file too small");
118         }
119         else {
120                 return IkiWiki::SuccessReason->new("file not too small");
121         }
122 }
123
124 sub match_mimetype ($$;@) {
125         my $page=shift;
126         my $wanted=shift;
127
128         my %params=@_;
129         my $file=exists $params{file} ? $params{file} : IkiWiki::srcfile($IkiWiki::pagesources{$page});
130         if (! defined $file) {
131                 return IkiWiki::ErrorReason->new("file does not exist");
132         }
133
134         # Get the mime type.
135         #
136         # First, try File::Mimeinfo. This is fast, but doesn't recognise
137         # all files.
138         eval q{use File::MimeInfo::Magic};
139         my $mimeinfo_ok=! $@;
140         my $mimetype;
141         if ($mimeinfo_ok) {
142                 my $mimetype=File::MimeInfo::Magic::magic($file);
143         }
144
145         # Fall back to using file, which has a more complete
146         # magic database.
147         if (! defined $mimetype) {
148                 open(my $file_h, "-|", "file", "-bi", $file);
149                 $mimetype=<$file_h>;
150                 chomp $mimetype;
151                 close $file_h;
152         }
153         if (! defined $mimetype || $mimetype !~s /;.*//) {
154                 # Fall back to default value.
155                 $mimetype=File::MimeInfo::Magic::default($file)
156                         if $mimeinfo_ok;
157                 if (! defined $mimetype) {
158                         $mimetype="unknown";
159                 }
160         }
161
162         my $regexp=IkiWiki::glob2re($wanted);
163         if ($mimetype!~$regexp) {
164                 return IkiWiki::FailReason->new("file MIME type is $mimetype, not $wanted");
165         }
166         else {
167                 return IkiWiki::SuccessReason->new("file MIME type is $mimetype");
168         }
169 }
170
171 sub match_virusfree ($$;@) {
172         my $page=shift;
173         my $wanted=shift;
174
175         my %params=@_;
176         my $file=exists $params{file} ? $params{file} : IkiWiki::srcfile($IkiWiki::pagesources{$page});
177         if (! defined $file) {
178                 return IkiWiki::ErrorReason->new("file does not exist");
179         }
180
181         if (! exists $IkiWiki::config{virus_checker} ||
182             ! length $IkiWiki::config{virus_checker}) {
183                 return IkiWiki::ErrorReason->new("no virus_checker configured");
184         }
185
186         # The file needs to be fed into the virus checker on stdin,
187         # because the file is not world-readable, and if clamdscan is
188         # used, clamd would fail to read it.
189         eval q{use IPC::Open2};
190         error($@) if $@;
191         open (IN, "<", $file) || return IkiWiki::ErrorReason->new("failed to read file");
192         binmode(IN);
193         my $sigpipe=0;
194         $SIG{PIPE} = sub { $sigpipe=1 };
195         my $pid=open2(\*CHECKER_OUT, "<&IN", $IkiWiki::config{virus_checker}); 
196         my $reason=<CHECKER_OUT>;
197         chomp $reason;
198         1 while (<CHECKER_OUT>);
199         close(CHECKER_OUT);
200         waitpid $pid, 0;
201         $SIG{PIPE}="DEFAULT";
202         if ($sigpipe || $?) {
203                 if (! length $reason) {
204                         $reason="virus checker $IkiWiki::config{virus_checker}; failed with no output";
205                 }
206                 return IkiWiki::FailReason->new("file seems to contain a virus ($reason)");
207         }
208         else {
209                 return IkiWiki::SuccessReason->new("file seems virusfree ($reason)");
210         }
211 }
212
213 sub match_ispage ($$;@) {
214         my $filename=shift;
215
216         if (defined IkiWiki::pagetype($filename)) {
217                 return IkiWiki::SuccessReason->new("file is a wiki page");
218         }
219         else {
220                 return IkiWiki::FailReason->new("file is not a wiki page");
221         }
222 }