a1997e19 |
1 | #!/usr/bin/perl -T |
710a8b2d |
2 | $ENV{PATH}="/usr/local/bin:/usr/bin:/bin"; |
a1997e19 |
3 | |
6c8cf5dd |
4 | package IkiWiki; |
a1997e19 |
5 | use warnings; |
6 | use strict; |
a1997e19 |
7 | use File::Spec; |
94eab28a |
8 | use HTML::Template; |
0943c375 |
9 | use lib '.'; # For use without installation, removed by Makefile. |
a1997e19 |
10 | |
72e8ace2 |
11 | use vars qw{%config %links %oldlinks %oldpagemtime %pagectime |
7b4600df |
12 | %renderedfiles %pagesources %inlinepages}; |
09902169 |
13 | |
800dfe66 |
14 | sub usage () { #{{{ |
15 | die "usage: ikiwiki [options] source dest\n"; |
16 | } #}}} |
17 | |
0943c375 |
18 | sub getconfig () { #{{{ |
19 | if (! exists $ENV{WRAPPED_OPTIONS}) { |
d59734ca |
20 | %config=( |
975ae094 |
21 | wiki_file_prune_regexp => qr{((^|/).svn/|\.\.|^\.|\/\.|\.html?$|\.rss$)}, |
d9f1b121 |
22 | wiki_link_regexp => qr/\[\[(?:([^\s\]\|]+)\|)?([^\s\]]+)\]\]/, |
7b4600df |
23 | wiki_processor_regexp => qr/\[\[(\w+)\s+([^\]]+)\]\]/, |
a0321594 |
24 | wiki_file_regexp => qr/(^[-[:alnum:]_.:\/+]+$)/, |
d59734ca |
25 | verbose => 0, |
26 | wikiname => "wiki", |
27 | default_pageext => ".mdwn", |
28 | cgi => 0, |
29 | svn => 1, |
13e3bf86 |
30 | notify => 0, |
d59734ca |
31 | url => '', |
32 | cgiurl => '', |
33 | historyurl => '', |
34 | diffurl => '', |
35 | anonok => 0, |
49524c42 |
36 | rss => 0, |
d7aecf6d |
37 | sanitize => 1, |
d59734ca |
38 | rebuild => 0, |
ffc0be87 |
39 | refresh => 0, |
90923561 |
40 | getctime => 0, |
0d9539d5 |
41 | hyperestraier => 0, |
d59734ca |
42 | wrapper => undef, |
43 | wrappermode => undef, |
13e3bf86 |
44 | svnrepo => undef, |
45 | svnpath => "trunk", |
d59734ca |
46 | srcdir => undef, |
47 | destdir => undef, |
48 | templatedir => "/usr/share/ikiwiki/templates", |
975ae094 |
49 | underlaydir => "/usr/share/ikiwiki/basewiki", |
d59734ca |
50 | setup => undef, |
51 | adminuser => undef, |
13e3bf86 |
52 | adminemail => undef, |
d59734ca |
53 | ); |
54 | |
0943c375 |
55 | eval q{use Getopt::Long}; |
56 | GetOptions( |
57 | "setup|s=s" => \$config{setup}, |
58 | "wikiname=s" => \$config{wikiname}, |
59 | "verbose|v!" => \$config{verbose}, |
60 | "rebuild!" => \$config{rebuild}, |
ffc0be87 |
61 | "refresh!" => \$config{refresh}, |
90923561 |
62 | "getctime" => \$config{getctime}, |
0943c375 |
63 | "wrappermode=i" => \$config{wrappermode}, |
64 | "svn!" => \$config{svn}, |
65 | "anonok!" => \$config{anonok}, |
0d9539d5 |
66 | "hyperestraier" => \$config{hyperestraier}, |
ca366fc9 |
67 | "rss!" => \$config{rss}, |
0943c375 |
68 | "cgi!" => \$config{cgi}, |
13e3bf86 |
69 | "notify!" => \$config{notify}, |
d7aecf6d |
70 | "sanitize!" => \$config{sanitize}, |
0943c375 |
71 | "url=s" => \$config{url}, |
72 | "cgiurl=s" => \$config{cgiurl}, |
73 | "historyurl=s" => \$config{historyurl}, |
74 | "diffurl=s" => \$config{diffurl}, |
13e3bf86 |
75 | "svnrepo" => \$config{svnrepo}, |
76 | "svnpath" => \$config{svnpath}, |
77 | "adminemail=s" => \$config{adminemail}, |
0943c375 |
78 | "exclude=s@" => sub { |
79 | $config{wiki_file_prune_regexp}=qr/$config{wiki_file_prune_regexp}|$_[1]/; |
80 | }, |
81 | "adminuser=s@" => sub { |
82 | push @{$config{adminuser}}, $_[1] |
83 | }, |
84 | "templatedir=s" => sub { |
85 | $config{templatedir}=possibly_foolish_untaint($_[1]) |
86 | }, |
975ae094 |
87 | "underlaydir=s" => sub { |
88 | $config{underlaydir}=possibly_foolish_untaint($_[1]) |
89 | }, |
0943c375 |
90 | "wrapper:s" => sub { |
91 | $config{wrapper}=$_[1] ? $_[1] : "ikiwiki-wrap" |
92 | }, |
93 | ) || usage(); |
94 | |
95 | if (! $config{setup}) { |
96 | usage() unless @ARGV == 2; |
d6a4e17e |
97 | $config{srcdir} = possibly_foolish_untaint(shift @ARGV); |
98 | $config{destdir} = possibly_foolish_untaint(shift @ARGV); |
0943c375 |
99 | checkconfig(); |
100 | } |
101 | } |
102 | else { |
103 | # wrapper passes a full config structure in the environment |
104 | # variable |
105 | eval possibly_foolish_untaint($ENV{WRAPPED_OPTIONS}); |
106 | checkconfig(); |
107 | } |
108 | } #}}} |
109 | |
28055bbc |
110 | sub checkconfig () { #{{{ |
4796acda |
111 | if ($config{cgi} && ! length $config{url}) { |
ca366fc9 |
112 | error("Must specify url to wiki with --url when using --cgi\n"); |
113 | } |
114 | if ($config{rss} && ! length $config{url}) { |
115 | error("Must specify url to wiki with --url when using --rss\n"); |
4796acda |
116 | } |
0d9539d5 |
117 | if ($config{hyperestraier} && ! length $config{url}) { |
118 | error("Must specify --url when using --hyperestraier\n"); |
119 | } |
6c8cf5dd |
120 | |
e4d9da55 |
121 | $config{wikistatedir}="$config{srcdir}/.ikiwiki" |
122 | unless exists $config{wikistatedir}; |
6c8cf5dd |
123 | |
124 | if ($config{svn}) { |
a223f454 |
125 | require IkiWiki::Rcs::SVN; |
6c8cf5dd |
126 | $config{rcs}=1; |
127 | } |
128 | else { |
a223f454 |
129 | require IkiWiki::Rcs::Stub; |
6c8cf5dd |
130 | $config{rcs}=0; |
131 | } |
e4d9da55 |
132 | } #}}} |
a1997e19 |
133 | |
28055bbc |
134 | sub error ($) { #{{{ |
09902169 |
135 | if ($config{cgi}) { |
942d5896 |
136 | print "Content-type: text/html\n\n"; |
798c48a1 |
137 | print misctemplate("Error", "<p>Error: @_</p>"); |
942d5896 |
138 | } |
2885278e |
139 | die @_; |
de0f43fa |
140 | } #}}} |
a1997e19 |
141 | |
28055bbc |
142 | sub possibly_foolish_untaint ($) { #{{{ |
143 | my $tainted=shift; |
144 | my ($untainted)=$tainted=~/(.*)/; |
145 | return $untainted; |
146 | } #}}} |
147 | |
de0f43fa |
148 | sub debug ($) { #{{{ |
09902169 |
149 | return unless $config{verbose}; |
150 | if (! $config{cgi}) { |
151 | print "@_\n"; |
798c48a1 |
152 | } |
153 | else { |
09902169 |
154 | print STDERR "@_\n"; |
798c48a1 |
155 | } |
de0f43fa |
156 | } #}}} |
a1997e19 |
157 | |
de0f43fa |
158 | sub basename ($) { #{{{ |
a1997e19 |
159 | my $file=shift; |
160 | |
3b90dc85 |
161 | $file=~s!.*/+!!; |
a1997e19 |
162 | return $file; |
de0f43fa |
163 | } #}}} |
a1997e19 |
164 | |
de0f43fa |
165 | sub dirname ($) { #{{{ |
a1997e19 |
166 | my $file=shift; |
167 | |
3b90dc85 |
168 | $file=~s!/*[^/]+$!!; |
a1997e19 |
169 | return $file; |
de0f43fa |
170 | } #}}} |
a1997e19 |
171 | |
de0f43fa |
172 | sub pagetype ($) { #{{{ |
a1997e19 |
173 | my $page=shift; |
174 | |
175 | if ($page =~ /\.mdwn$/) { |
176 | return ".mdwn"; |
177 | } |
178 | else { |
179 | return "unknown"; |
180 | } |
de0f43fa |
181 | } #}}} |
a1997e19 |
182 | |
de0f43fa |
183 | sub pagename ($) { #{{{ |
a1997e19 |
184 | my $file=shift; |
185 | |
186 | my $type=pagetype($file); |
187 | my $page=$file; |
188 | $page=~s/\Q$type\E*$// unless $type eq 'unknown'; |
189 | return $page; |
de0f43fa |
190 | } #}}} |
a1997e19 |
191 | |
de0f43fa |
192 | sub htmlpage ($) { #{{{ |
a1997e19 |
193 | my $page=shift; |
194 | |
195 | return $page.".html"; |
de0f43fa |
196 | } #}}} |
a1997e19 |
197 | |
975ae094 |
198 | sub srcfile ($) { #{{{ |
199 | my $file=shift; |
200 | |
201 | return "$config{srcdir}/$file" if -e "$config{srcdir}/$file"; |
202 | return "$config{underlaydir}/$file" if -e "$config{underlaydir}/$file"; |
203 | error("internal error: $file cannot be found"); |
204 | } #}}} |
205 | |
f50bd57b |
206 | sub readfile ($;$) { #{{{ |
942d5896 |
207 | my $file=shift; |
f50bd57b |
208 | my $binary=shift; |
a1997e19 |
209 | |
62f1f973 |
210 | if (-l $file) { |
211 | error("cannot read a symlink ($file)"); |
212 | } |
213 | |
a1997e19 |
214 | local $/=undef; |
f50bd57b |
215 | open (IN, $file) || error("failed to read $file: $!"); |
216 | binmode(IN) if $binary; |
942d5896 |
217 | my $ret=<IN>; |
218 | close IN; |
a1997e19 |
219 | return $ret; |
de0f43fa |
220 | } #}}} |
a1997e19 |
221 | |
f50bd57b |
222 | sub writefile ($$$;$) { #{{{ |
efe91335 |
223 | my $file=shift; # can include subdirs |
224 | my $destdir=shift; # directory to put file in |
a1997e19 |
225 | my $content=shift; |
f50bd57b |
226 | my $binary=shift; |
62f1f973 |
227 | |
efe91335 |
228 | my $test=$file; |
229 | while (length $test) { |
230 | if (-l "$destdir/$test") { |
231 | error("cannot write to a symlink ($test)"); |
232 | } |
233 | $test=dirname($test); |
62f1f973 |
234 | } |
a1997e19 |
235 | |
efe91335 |
236 | my $dir=dirname("$destdir/$file"); |
a1997e19 |
237 | if (! -d $dir) { |
238 | my $d=""; |
239 | foreach my $s (split(m!/+!, $dir)) { |
240 | $d.="$s/"; |
241 | if (! -d $d) { |
242 | mkdir($d) || error("failed to create directory $d: $!"); |
243 | } |
244 | } |
245 | } |
246 | |
efe91335 |
247 | open (OUT, ">$destdir/$file") || error("failed to write $destdir/$file: $!"); |
f50bd57b |
248 | binmode(OUT) if $binary; |
942d5896 |
249 | print OUT $content; |
250 | close OUT; |
de0f43fa |
251 | } #}}} |
a1997e19 |
252 | |
de0f43fa |
253 | sub bestlink ($$) { #{{{ |
710a8b2d |
254 | # Given a page and the text of a link on the page, determine which |
255 | # existing page that link best points to. Prefers pages under a |
256 | # subdirectory with the same name as the source page, failing that |
257 | # goes down the directory tree to the base looking for matching |
258 | # pages. |
a1997e19 |
259 | my $page=shift; |
260 | my $link=lc(shift); |
261 | |
262 | my $cwd=$page; |
263 | do { |
264 | my $l=$cwd; |
265 | $l.="/" if length $l; |
266 | $l.=$link; |
267 | |
268 | if (exists $links{$l}) { |
269 | #debug("for $page, \"$link\", use $l"); |
270 | return $l; |
271 | } |
272 | } while $cwd=~s!/?[^/]+$!!; |
273 | |
d3d8890e |
274 | #print STDERR "warning: page $page, broken link: $link\n"; |
a1997e19 |
275 | return ""; |
de0f43fa |
276 | } #}}} |
a1997e19 |
277 | |
de0f43fa |
278 | sub isinlinableimage ($) { #{{{ |
a1997e19 |
279 | my $file=shift; |
280 | |
c72af182 |
281 | $file=~/\.(png|gif|jpg|jpeg)$/i; |
de0f43fa |
282 | } #}}} |
a1997e19 |
283 | |
a223f454 |
284 | sub pagetitle ($) { #{{{ |
285 | my $page=shift; |
286 | $page=~s/__(\d+)__/&#$1;/g; |
287 | $page=~y/_/ /; |
288 | return $page; |
289 | } #}}} |
290 | |
0a95ac21 |
291 | sub titlepage ($) { #{{{ |
292 | my $title=shift; |
293 | $title=~y/ /_/; |
a0321594 |
294 | $title=~s/([^-[:alnum:]_:+\/.])/"__".ord($1)."__"/eg; |
0a95ac21 |
295 | return $title; |
296 | } #}}} |
297 | |
e41dd1e2 |
298 | sub cgiurl (@) { #{{{ |
299 | my %params=@_; |
300 | |
301 | return $config{cgiurl}."?".join("&", map "$_=$params{$_}", keys %params); |
302 | } #}}} |
303 | |
5591d621 |
304 | sub styleurl (;$) { #{{{ |
305 | my $page=shift; |
306 | |
307 | return "$config{url}/style.css" if ! defined $page; |
308 | |
309 | $page=~s/[^\/]+$//; |
310 | $page=~s/[^\/]+\//..\//g; |
311 | return $page."style.css"; |
312 | } #}}} |
313 | |
d9f1b121 |
314 | sub htmllink ($$;$$$) { #{{{ |
a1997e19 |
315 | my $page=shift; |
316 | my $link=shift; |
2d4bf757 |
317 | my $noimageinline=shift; # don't turn links into inline html images |
7f8efe45 |
318 | my $forcesubpage=shift; # force a link to a subpage |
d9f1b121 |
319 | my $linktext=shift; # set to force the link text to something |
a1997e19 |
320 | |
7f8efe45 |
321 | my $bestlink; |
322 | if (! $forcesubpage) { |
323 | $bestlink=bestlink($page, $link); |
324 | } |
325 | else { |
326 | $bestlink="$page/".lc($link); |
327 | } |
a1997e19 |
328 | |
d9f1b121 |
329 | $linktext=pagetitle(basename($link)) unless defined $linktext; |
a223f454 |
330 | |
331 | return $linktext if length $bestlink && $page eq $bestlink; |
a1997e19 |
332 | |
aa0afc56 |
333 | # TODO BUG: %renderedfiles may not have it, if the linked to page |
d3d8890e |
334 | # was also added and isn't yet rendered! Note that this bug is |
335 | # masked by the bug mentioned below that makes all new files |
336 | # be rendered twice. |
a1997e19 |
337 | if (! grep { $_ eq $bestlink } values %renderedfiles) { |
338 | $bestlink=htmlpage($bestlink); |
339 | } |
340 | if (! grep { $_ eq $bestlink } values %renderedfiles) { |
e41dd1e2 |
341 | return "<span><a href=\"". |
342 | cgiurl(do => "create", page => $link, from =>$page). |
343 | "\">?</a>$linktext</span>" |
a1997e19 |
344 | } |
345 | |
346 | $bestlink=File::Spec->abs2rel($bestlink, dirname($page)); |
347 | |
2d4bf757 |
348 | if (! $noimageinline && isinlinableimage($bestlink)) { |
5591d621 |
349 | return "<img src=\"$bestlink\" alt=\"$linktext\" />"; |
a1997e19 |
350 | } |
a223f454 |
351 | return "<a href=\"$bestlink\">$linktext</a>"; |
de0f43fa |
352 | } #}}} |
a1997e19 |
353 | |
1311d67f |
354 | sub indexlink () { #{{{ |
09902169 |
355 | return "<a href=\"$config{url}\">$config{wikiname}</a>"; |
1311d67f |
356 | } #}}} |
2d4bf757 |
357 | |
d6239525 |
358 | sub lockwiki () { #{{{ |
359 | # Take an exclusive lock on the wiki to prevent multiple concurrent |
360 | # run issues. The lock will be dropped on program exit. |
e4d9da55 |
361 | if (! -d $config{wikistatedir}) { |
362 | mkdir($config{wikistatedir}); |
d6239525 |
363 | } |
e4d9da55 |
364 | open(WIKILOCK, ">$config{wikistatedir}/lockfile") || |
365 | error ("cannot write to $config{wikistatedir}/lockfile: $!"); |
d6239525 |
366 | if (! flock(WIKILOCK, 2 | 4)) { |
367 | debug("wiki seems to be locked, waiting for lock"); |
368 | my $wait=600; # arbitrary, but don't hang forever to |
369 | # prevent process pileup |
370 | for (1..600) { |
371 | return if flock(WIKILOCK, 2 | 4); |
372 | sleep 1; |
373 | } |
374 | error("wiki is locked; waited $wait seconds without lock being freed (possible stuck process or stale lock?)"); |
375 | } |
376 | } #}}} |
377 | |
2971c429 |
378 | sub unlockwiki () { #{{{ |
7fca2e01 |
379 | close WIKILOCK; |
380 | } #}}} |
381 | |
de0f43fa |
382 | sub loadindex () { #{{{ |
e4d9da55 |
383 | open (IN, "$config{wikistatedir}/index") || return; |
a1997e19 |
384 | while (<IN>) { |
eeb4e694 |
385 | $_=possibly_foolish_untaint($_); |
a1997e19 |
386 | chomp; |
ca0be85c |
387 | my %items; |
388 | $items{link}=[]; |
389 | foreach my $i (split(/ /, $_)) { |
390 | my ($item, $val)=split(/=/, $i, 2); |
391 | push @{$items{$item}}, $val; |
392 | } |
393 | |
4a4c8680 |
394 | next unless exists $items{src}; # skip bad lines for now |
395 | |
ca0be85c |
396 | my $page=pagename($items{src}[0]); |
4f93b8e2 |
397 | if (! $config{rebuild}) { |
398 | $pagesources{$page}=$items{src}[0]; |
399 | $oldpagemtime{$page}=$items{mtime}[0]; |
400 | $oldlinks{$page}=[@{$items{link}}]; |
401 | $links{$page}=[@{$items{link}}]; |
7b4600df |
402 | $inlinepages{$page}=join(" ", @{$items{inlinepage}}) |
403 | if exists $items{inlinepage}; |
4f93b8e2 |
404 | $renderedfiles{$page}=$items{dest}[0]; |
405 | } |
72e8ace2 |
406 | $pagectime{$page}=$items{ctime}[0]; |
a1997e19 |
407 | } |
408 | close IN; |
de0f43fa |
409 | } #}}} |
a1997e19 |
410 | |
de0f43fa |
411 | sub saveindex () { #{{{ |
e4d9da55 |
412 | if (! -d $config{wikistatedir}) { |
413 | mkdir($config{wikistatedir}); |
1311d67f |
414 | } |
e4d9da55 |
415 | open (OUT, ">$config{wikistatedir}/index") || |
416 | error("cannot write to $config{wikistatedir}/index: $!"); |
a1997e19 |
417 | foreach my $page (keys %oldpagemtime) { |
7b4600df |
418 | next unless $oldpagemtime{$page}; |
ca0be85c |
419 | my $line="mtime=$oldpagemtime{$page} ". |
72e8ace2 |
420 | "ctime=$pagectime{$page} ". |
ca0be85c |
421 | "src=$pagesources{$page} ". |
422 | "dest=$renderedfiles{$page}"; |
7b4600df |
423 | $line.=" link=$_" foreach @{$links{$page}}; |
424 | if (exists $inlinepages{$page}) { |
425 | $line.=" inlinepage=$_" foreach split " ", $inlinepages{$page}; |
ca0be85c |
426 | } |
427 | print OUT $line."\n"; |
a1997e19 |
428 | } |
429 | close OUT; |
de0f43fa |
430 | } #}}} |
a1997e19 |
431 | |
798c48a1 |
432 | sub misctemplate ($$) { #{{{ |
433 | my $title=shift; |
434 | my $pagebody=shift; |
435 | |
436 | my $template=HTML::Template->new( |
09902169 |
437 | filename => "$config{templatedir}/misc.tmpl" |
8859b2fe |
438 | ); |
798c48a1 |
439 | $template->param( |
440 | title => $title, |
441 | indexlink => indexlink(), |
09902169 |
442 | wikiname => $config{wikiname}, |
798c48a1 |
443 | pagebody => $pagebody, |
5591d621 |
444 | styleurl => styleurl(), |
c4e0e8c3 |
445 | baseurl => "$config{url}/", |
798c48a1 |
446 | ); |
447 | return $template->output; |
448 | }#}}} |
9ab1c273 |
449 | |
325d5c79 |
450 | sub glob_match ($$) { #{{{ |
451 | my $page=shift; |
452 | my $glob=shift; |
453 | |
454 | # turn glob into safe regexp |
455 | $glob=quotemeta($glob); |
456 | $glob=~s/\\\*/.*/g; |
457 | $glob=~s/\\\?/./g; |
458 | $glob=~s!\\/!/!g; |
459 | |
460 | $page=~/^$glob$/i; |
461 | } #}}} |
462 | |
463 | sub globlist_match ($$) { #{{{ |
464 | my $page=shift; |
465 | my @globlist=split(" ", shift); |
466 | |
467 | # check any negated globs first |
468 | foreach my $glob (@globlist) { |
469 | return 0 if $glob=~/^!(.*)/ && glob_match($page, $1); |
470 | } |
471 | |
472 | foreach my $glob (@globlist) { |
473 | return 1 if glob_match($page, $glob); |
474 | } |
475 | |
476 | return 0; |
477 | } #}}} |
478 | |
384327bf |
479 | sub main () { #{{{ |
0943c375 |
480 | getconfig(); |
481 | |
e910e674 |
482 | if ($config{cgi}) { |
483 | lockwiki(); |
484 | loadindex(); |
485 | require IkiWiki::CGI; |
486 | cgi(); |
487 | } |
488 | elsif ($config{setup}) { |
384327bf |
489 | require IkiWiki::Setup; |
490 | setup(); |
491 | } |
5230f4e5 |
492 | elsif ($config{wrapper}) { |
493 | lockwiki(); |
384327bf |
494 | require IkiWiki::Wrapper; |
495 | gen_wrapper(); |
384327bf |
496 | } |
384327bf |
497 | else { |
5230f4e5 |
498 | lockwiki(); |
4f93b8e2 |
499 | loadindex(); |
384327bf |
500 | require IkiWiki::Render; |
501 | rcs_update(); |
13e3bf86 |
502 | rcs_notify() if $config{notify}; |
90923561 |
503 | rcs_getctime() if $config{getctime}; |
384327bf |
504 | refresh(); |
505 | saveindex(); |
506 | } |
507 | } #}}} |
508 | |
509 | main; |