]> sipb.mit.edu Git - ikiwiki.git/blob - ikiwiki
Getopt::Long is a huge, heavy perl module. So why use it?
[ikiwiki.git] / ikiwiki
1 #!/usr/bin/perl -T
2
3 $ENV{PATH}="/usr/local/bin:/usr/bin:/bin";
4
5 use lib '.'; # For use without installation, removed by Makefile.
6
7 package IkiWiki;
8 use warnings;
9 use strict;
10 use Memoize;
11 use File::Spec;
12 use HTML::Template;
13
14 use vars qw{%config %links %oldlinks %oldpagemtime %renderedfiles %pagesources};
15
16 # Holds global config settings, also used by some modules.
17 our %config=( #{{{
18         wiki_file_prune_regexp => qr{((^|/).svn/|\.\.|^\.|\/\.|\.html?$)},
19         wiki_link_regexp => qr/\[\[([^\s\]]+)\]\]/,
20         wiki_file_regexp => qr/(^[-A-Za-z0-9_.:\/+]+$)/,
21         verbose => 0,
22         wikiname => "wiki",
23         default_pageext => ".mdwn",
24         cgi => 0,
25         svn => 1,
26         url => '',
27         cgiurl => '',
28         historyurl => '',
29         diffurl => '',
30         anonok => 0,
31         rebuild => 0,
32         wrapper => undef,
33         wrappermode => undef,
34         srcdir => undef,
35         destdir => undef,
36         templatedir => "/usr/share/ikiwiki/templates",
37         setup => undef,
38         adminuser => undef,
39 ); #}}}
40
41 # option parsing #{{{
42 if (! exists $ENV{WRAPPED_OPTIONS}) {
43         eval q{use Getopt::Long};
44         GetOptions(
45                 "setup|s=s" => \$config{setup},
46                 "wikiname=s" => \$config{wikiname},
47                 "verbose|v!" => \$config{verbose},
48                 "rebuild!" => \$config{rebuild},
49                 "wrapper:s" => sub { $config{wrapper}=$_[1] ? $_[1] : "ikiwiki-wrap" },
50                 "wrappermode=i" => \$config{wrappermode},
51                 "svn!" => \$config{svn},
52                 "anonok!" => \$config{anonok},
53                 "cgi!" => \$config{cgi},
54                 "url=s" => \$config{url},
55                 "cgiurl=s" => \$config{cgiurl},
56                 "historyurl=s" => \$config{historyurl},
57                 "diffurl=s" => \$config{diffurl},
58                 "exclude=s@" => sub {
59                         $config{wiki_file_prune_regexp}=qr/$config{wiki_file_prune_regexp}|$_[1]/;
60                 },
61                 "adminuser=s@" => sub { push @{$config{adminuser}}, $_[1] },
62                 "templatedir=s" => sub { $config{templatedir}=possibly_foolish_untaint($_[1]) },
63         ) || usage();
64
65         if (! $config{setup}) {
66                 usage() unless @ARGV == 2;
67                 $config{srcdir} = possibly_foolish_untaint(shift);
68                 $config{destdir} = possibly_foolish_untaint(shift);
69                 checkoptions();
70         }
71 }
72 else {
73         # wrapper passes a full config structure in the environment
74         # variable
75         eval possibly_foolish_untaint($ENV{WRAPPED_OPTIONS});
76         checkoptions();
77 }
78 #}}}
79
80 sub checkoptions { #{{{
81         if ($config{cgi} && ! length $config{url}) {
82                 error("Must specify url to wiki with --url when using --cgi");
83         }
84         
85         $config{wikistatedir}="$config{srcdir}/.ikiwiki"
86                 unless exists $config{wikistatedir};
87         
88         if ($config{svn}) {
89                 require IkiWiki::RCS::SVN;
90                 $config{rcs}=1;
91         }
92         else {
93                 require IkiWiki::RCS::Stub;
94                 $config{rcs}=0;
95         }
96 } #}}}
97
98 sub usage { #{{{
99         die "usage: ikiwiki [options] source dest\n";
100 } #}}}
101
102 sub error { #{{{
103         if ($config{cgi}) {
104                 print "Content-type: text/html\n\n";
105                 print misctemplate("Error", "<p>Error: @_</p>");
106         }
107         die @_;
108 } #}}}
109
110 sub debug ($) { #{{{
111         return unless $config{verbose};
112         if (! $config{cgi}) {
113                 print "@_\n";
114         }
115         else {
116                 print STDERR "@_\n";
117         }
118 } #}}}
119
120 sub possibly_foolish_untaint { #{{{
121         my $tainted=shift;
122         my ($untainted)=$tainted=~/(.*)/;
123         return $untainted;
124 } #}}}
125
126 sub basename ($) { #{{{
127         my $file=shift;
128
129         $file=~s!.*/!!;
130         return $file;
131 } #}}}
132
133 sub dirname ($) { #{{{
134         my $file=shift;
135
136         $file=~s!/?[^/]+$!!;
137         return $file;
138 } #}}}
139
140 sub pagetype ($) { #{{{
141         my $page=shift;
142         
143         if ($page =~ /\.mdwn$/) {
144                 return ".mdwn";
145         }
146         else {
147                 return "unknown";
148         }
149 } #}}}
150
151 sub pagename ($) { #{{{
152         my $file=shift;
153
154         my $type=pagetype($file);
155         my $page=$file;
156         $page=~s/\Q$type\E*$// unless $type eq 'unknown';
157         return $page;
158 } #}}}
159
160 sub htmlpage ($) { #{{{
161         my $page=shift;
162
163         return $page.".html";
164 } #}}}
165
166 sub readfile ($) { #{{{
167         my $file=shift;
168
169         if (-l $file) {
170                 error("cannot read a symlink ($file)");
171         }
172         
173         local $/=undef;
174         open (IN, "$file") || error("failed to read $file: $!");
175         my $ret=<IN>;
176         close IN;
177         return $ret;
178 } #}}}
179
180 sub writefile ($$) { #{{{
181         my $file=shift;
182         my $content=shift;
183         
184         if (-l $file) {
185                 error("cannot write to a symlink ($file)");
186         }
187
188         my $dir=dirname($file);
189         if (! -d $dir) {
190                 my $d="";
191                 foreach my $s (split(m!/+!, $dir)) {
192                         $d.="$s/";
193                         if (! -d $d) {
194                                 mkdir($d) || error("failed to create directory $d: $!");
195                         }
196                 }
197         }
198         
199         open (OUT, ">$file") || error("failed to write $file: $!");
200         print OUT $content;
201         close OUT;
202 } #}}}
203
204 sub bestlink ($$) { #{{{
205         # Given a page and the text of a link on the page, determine which
206         # existing page that link best points to. Prefers pages under a
207         # subdirectory with the same name as the source page, failing that
208         # goes down the directory tree to the base looking for matching
209         # pages.
210         my $page=shift;
211         my $link=lc(shift);
212         
213         my $cwd=$page;
214         do {
215                 my $l=$cwd;
216                 $l.="/" if length $l;
217                 $l.=$link;
218
219                 if (exists $links{$l}) {
220                         #debug("for $page, \"$link\", use $l");
221                         return $l;
222                 }
223         } while $cwd=~s!/?[^/]+$!!;
224
225         #print STDERR "warning: page $page, broken link: $link\n";
226         return "";
227 } #}}}
228
229 sub isinlinableimage ($) { #{{{
230         my $file=shift;
231         
232         $file=~/\.(png|gif|jpg|jpeg)$/;
233 } #}}}
234
235 sub htmllink { #{{{
236         my $page=shift;
237         my $link=shift;
238         my $noimageinline=shift; # don't turn links into inline html images
239         my $forcesubpage=shift; # force a link to a subpage
240
241         my $bestlink;
242         if (! $forcesubpage) {
243                 $bestlink=bestlink($page, $link);
244         }
245         else {
246                 $bestlink="$page/".lc($link);
247         }
248
249         return $link if length $bestlink && $page eq $bestlink;
250         
251         # TODO BUG: %renderedfiles may not have it, if the linked to page
252         # was also added and isn't yet rendered! Note that this bug is
253         # masked by the bug mentioned below that makes all new files
254         # be rendered twice.
255         if (! grep { $_ eq $bestlink } values %renderedfiles) {
256                 $bestlink=htmlpage($bestlink);
257         }
258         if (! grep { $_ eq $bestlink } values %renderedfiles) {
259                 return "<a href=\"$config{cgiurl}?do=create&page=$link&from=$page\">?</a>$link"
260         }
261         
262         $bestlink=File::Spec->abs2rel($bestlink, dirname($page));
263         
264         if (! $noimageinline && isinlinableimage($bestlink)) {
265                 return "<img src=\"$bestlink\">";
266         }
267         return "<a href=\"$bestlink\">$link</a>";
268 } #}}}
269
270 sub indexlink () { #{{{
271         return "<a href=\"$config{url}\">$config{wikiname}</a>";
272 } #}}}
273
274 sub lockwiki () { #{{{
275         # Take an exclusive lock on the wiki to prevent multiple concurrent
276         # run issues. The lock will be dropped on program exit.
277         if (! -d $config{wikistatedir}) {
278                 mkdir($config{wikistatedir});
279         }
280         open(WIKILOCK, ">$config{wikistatedir}/lockfile") ||
281                 error ("cannot write to $config{wikistatedir}/lockfile: $!");
282         if (! flock(WIKILOCK, 2 | 4)) {
283                 debug("wiki seems to be locked, waiting for lock");
284                 my $wait=600; # arbitrary, but don't hang forever to 
285                               # prevent process pileup
286                 for (1..600) {
287                         return if flock(WIKILOCK, 2 | 4);
288                         sleep 1;
289                 }
290                 error("wiki is locked; waited $wait seconds without lock being freed (possible stuck process or stale lock?)");
291         }
292 } #}}}
293
294 sub unlockwiki () { #{{{
295         close WIKILOCK;
296 } #}}}
297
298 sub loadindex () { #{{{
299         open (IN, "$config{wikistatedir}/index") || return;
300         while (<IN>) {
301                 $_=possibly_foolish_untaint($_);
302                 chomp;
303                 my ($mtime, $file, $rendered, @links)=split(' ', $_);
304                 my $page=pagename($file);
305                 $pagesources{$page}=$file;
306                 $oldpagemtime{$page}=$mtime;
307                 $oldlinks{$page}=[@links];
308                 $links{$page}=[@links];
309                 $renderedfiles{$page}=$rendered;
310         }
311         close IN;
312 } #}}}
313
314 sub saveindex () { #{{{
315         if (! -d $config{wikistatedir}) {
316                 mkdir($config{wikistatedir});
317         }
318         open (OUT, ">$config{wikistatedir}/index") || 
319                 error("cannot write to $config{wikistatedir}/index: $!");
320         foreach my $page (keys %oldpagemtime) {
321                 print OUT "$oldpagemtime{$page} $pagesources{$page} $renderedfiles{$page} ".
322                         join(" ", @{$links{$page}})."\n"
323                                 if $oldpagemtime{$page};
324         }
325         close OUT;
326 } #}}}
327
328 sub misctemplate ($$) { #{{{
329         my $title=shift;
330         my $pagebody=shift;
331         
332         my $template=HTML::Template->new(
333                 filename => "$config{templatedir}/misc.tmpl"
334         );
335         $template->param(
336                 title => $title,
337                 indexlink => indexlink(),
338                 wikiname => $config{wikiname},
339                 pagebody => $pagebody,
340         );
341         return $template->output;
342 }#}}}
343
344 sub userinfo_get ($$) { #{{{
345         my $user=shift;
346         my $field=shift;
347
348         eval q{use Storable};
349         my $userdata=eval{ Storable::lock_retrieve("$config{wikistatedir}/userdb") };
350         if (! defined $userdata || ! ref $userdata || 
351             ! exists $userdata->{$user} || ! ref $userdata->{$user} ||
352             ! exists $userdata->{$user}->{$field}) {
353                 return "";
354         }
355         return $userdata->{$user}->{$field};
356 } #}}}
357
358 sub userinfo_set ($$$) { #{{{
359         my $user=shift;
360         my $field=shift;
361         my $value=shift;
362         
363         eval q{use Storable};
364         my $userdata=eval{ Storable::lock_retrieve("$config{wikistatedir}/userdb") };
365         if (! defined $userdata || ! ref $userdata || 
366             ! exists $userdata->{$user} || ! ref $userdata->{$user}) {
367                 return "";
368         }
369         
370         $userdata->{$user}->{$field}=$value;
371         my $oldmask=umask(077);
372         my $ret=Storable::lock_store($userdata, "$config{wikistatedir}/userdb");
373         umask($oldmask);
374         return $ret;
375 } #}}}
376
377 sub userinfo_setall ($$) { #{{{
378         my $user=shift;
379         my $info=shift;
380         
381         eval q{use Storable};
382         my $userdata=eval{ Storable::lock_retrieve("$config{wikistatedir}/userdb") };
383         if (! defined $userdata || ! ref $userdata) {
384                 $userdata={};
385         }
386         $userdata->{$user}=$info;
387         my $oldmask=umask(077);
388         my $ret=Storable::lock_store($userdata, "$config{wikistatedir}/userdb");
389         umask($oldmask);
390         return $ret;
391 } #}}}
392
393 sub is_admin ($) { #{{{
394         my $user_name=shift;
395
396         return grep { $_ eq $user_name } @{$config{adminuser}};
397 } #}}}
398
399 sub glob_match ($$) { #{{{
400         my $page=shift;
401         my $glob=shift;
402
403         # turn glob into safe regexp
404         $glob=quotemeta($glob);
405         $glob=~s/\\\*/.*/g;
406         $glob=~s/\\\?/./g;
407         $glob=~s!\\/!/!g;
408         
409         $page=~/^$glob$/i;
410 } #}}}
411
412 sub globlist_match ($$) { #{{{
413         my $page=shift;
414         my @globlist=split(" ", shift);
415
416         # check any negated globs first
417         foreach my $glob (@globlist) {
418                 return 0 if $glob=~/^!(.*)/ && glob_match($page, $1);
419         }
420
421         foreach my $glob (@globlist) {
422                 return 1 if glob_match($page, $glob);
423         }
424         
425         return 0;
426 } #}}}
427
428 # main {{{
429 memoize('pagename');
430 memoize('bestlink');
431 if ($config{setup}) {
432         require IkiWiki::Setup;
433         setup();
434 }
435 lockwiki();
436 if ($config{wrapper}) {
437         require IkiWiki::Wrapper;
438         gen_wrapper();
439         exit;
440 }
441 loadindex() unless $config{rebuild};
442 if ($config{cgi}) {
443         require IkiWiki::CGI;
444         cgi();
445 }
446 else {
447         require IkiWiki::Render;
448         rcs_update();
449         refresh();
450         saveindex();
451 }
452 #}}}