filecheck: Fall back to testing for binary or plain text files if no mime type is...
[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 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 # This is provided for other plugins that want to convert back the other way.
57 sub humansize ($) { #{{{
58         my $size=shift;
59
60         foreach my $unit (reverse sort { $units{$a} <=> $units{$b} || $b cmp $a } keys %units) {
61                 if ($size / $units{$unit} > 0.25) {
62                         return (int($size / $units{$unit} * 10)/10).$unit;
63                 }
64         }
65         return $size; # near zero, or negative
66 } #}}}
67
68 package IkiWiki::PageSpec;
69
70 sub match_maxsize ($$;@) { #{{{
71         my $page=shift;
72         my $maxsize=eval{IkiWiki::Plugin::filecheck::parsesize(shift)};
73         if ($@) {
74                 return IkiWiki::FailReason->new("unable to parse maxsize (or number too large)");
75         }
76
77         my %params=@_;
78         my $file=exists $params{file} ? $params{file} : $IkiWiki::pagesources{$page};
79         if (! defined $file) {
80                 return IkiWiki::FailReason->new("no file specified");
81         }
82
83         if (-s $file > $maxsize) {
84                 return IkiWiki::FailReason->new("file too large (".(-s $file)." >  $maxsize)");
85         }
86         else {
87                 return IkiWiki::SuccessReason->new("file not too large");
88         }
89 } #}}}
90
91 sub match_minsize ($$;@) { #{{{
92         my $page=shift;
93         my $minsize=eval{IkiWiki::Plugin::filecheck::parsesize(shift)};
94         if ($@) {
95                 return IkiWiki::FailReason->new("unable to parse minsize (or number too large)");
96         }
97
98         my %params=@_;
99         my $file=exists $params{file} ? $params{file} : $IkiWiki::pagesources{$page};
100         if (! defined $file) {
101                 return IkiWiki::FailReason->new("no file specified");
102         }
103
104         if (-s $file < $minsize) {
105                 return IkiWiki::FailReason->new("file too small");
106         }
107         else {
108                 return IkiWiki::SuccessReason->new("file not too small");
109         }
110 } #}}}
111
112 sub match_mimetype ($$;@) { #{{{
113         my $page=shift;
114         my $wanted=shift;
115
116         my %params=@_;
117         my $file=exists $params{file} ? $params{file} : $IkiWiki::pagesources{$page};
118         if (! defined $file) {
119                 return IkiWiki::FailReason->new("no file specified");
120         }
121
122         # Use ::magic to get the mime type, the idea is to only trust
123         # data obtained by examining the actual file contents.
124         eval q{use File::MimeInfo::Magic};
125         if ($@) {
126                 return IkiWiki::FailReason->new("failed to load File::MimeInfo::Magic ($@); cannot check MIME type");
127         }
128         my $mimetype=File::MimeInfo::Magic::magic($file);
129         if (! defined $mimetype) {
130                 $mimetype=File::MimeInfo::Magic::default($file);
131                 if (! defined $mimetype) {
132                         $mimetype="unknown";
133                 }
134         }
135
136         my $regexp=IkiWiki::glob2re($wanted);
137         if ($mimetype!~/^$regexp$/i) {
138                 return IkiWiki::FailReason->new("file MIME type is $mimetype, not $wanted");
139         }
140         else {
141                 return IkiWiki::SuccessReason->new("file MIME type is $mimetype");
142         }
143 } #}}}
144
145 sub match_virusfree ($$;@) { #{{{
146         my $page=shift;
147         my $wanted=shift;
148
149         my %params=@_;
150         my $file=exists $params{file} ? $params{file} : $IkiWiki::pagesources{$page};
151         if (! defined $file) {
152                 return IkiWiki::FailReason->new("no file specified");
153         }
154
155         if (! exists $IkiWiki::config{virus_checker} ||
156             ! length $IkiWiki::config{virus_checker}) {
157                 return IkiWiki::FailReason->new("no virus_checker configured");
158         }
159
160         # The file needs to be fed into the virus checker on stdin,
161         # because the file is not world-readable, and if clamdscan is
162         # used, clamd would fail to read it.
163         eval q{use IPC::Open2};
164         error($@) if $@;
165         open (IN, "<", $file) || return IkiWiki::FailReason->new("failed to read file");
166         binmode(IN);
167         my $sigpipe=0;
168         $SIG{PIPE} = sub { $sigpipe=1 };
169         my $pid=open2(\*CHECKER_OUT, "<&IN", $IkiWiki::config{virus_checker}); 
170         my $reason=<CHECKER_OUT>;
171         chomp $reason;
172         1 while (<CHECKER_OUT>);
173         close(CHECKER_OUT);
174         waitpid $pid, 0;
175         $SIG{PIPE}="DEFAULT";
176         if ($sigpipe || $?) {
177                 if (! length $reason) {
178                         $reason="virus checker $IkiWiki::config{virus_checker}; failed with no output";
179                 }
180                 return IkiWiki::FailReason->new("file seems to contain a virus ($reason)");
181         }
182         else {
183                 return IkiWiki::SuccessReason->new("file seems virusfree ($reason)");
184         }
185 } #}}}
186
187 sub match_ispage ($$;@) { #{{{
188         my $filename=shift;
189
190         if (defined IkiWiki::pagetype($filename)) {
191                 return IkiWiki::SuccessReason->new("file is a wiki page");
192         }
193         else {
194                 return IkiWiki::FailReason->new("file is not a wiki page");
195         }
196 } #}}}