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