summaryrefslogtreecommitdiff
path: root/IkiWiki.pm
blob: b5707195b9dcf7c3dd41241fa185cf547a48850b (plain)
  1. #!/usr/bin/perl
  2. package IkiWiki;
  3. use warnings;
  4. use strict;
  5. use Encode;
  6. use open qw{:utf8 :std};
  7. # Optimisation.
  8. use Memoize;
  9. memoize("abs2rel");
  10. use vars qw{%config %links %oldlinks %oldpagemtime %pagectime
  11. %renderedfiles %pagesources %depends %hooks %forcerebuild};
  12. sub defaultconfig () { #{{{
  13. wiki_file_prune_regexp => qr{((^|/).svn/|\.\.|^\.|\/\.|\.html?$|\.rss$)},
  14. wiki_link_regexp => qr/\[\[(?:([^\]\|]+)\|)?([^\s\]]+)\]\]/,
  15. wiki_processor_regexp => qr/\[\[(\w+)\s+([^\]]*)\]\]/,
  16. wiki_file_regexp => qr/(^[-[:alnum:]_.:\/+]+$)/,
  17. verbose => 0,
  18. wikiname => "wiki",
  19. default_pageext => "mdwn",
  20. cgi => 0,
  21. rcs => 'svn',
  22. notify => 0,
  23. url => '',
  24. cgiurl => '',
  25. historyurl => '',
  26. diffurl => '',
  27. anonok => 0,
  28. rss => 0,
  29. discussion => 1,
  30. rebuild => 0,
  31. refresh => 0,
  32. getctime => 0,
  33. w3mmode => 0,
  34. wrapper => undef,
  35. wrappermode => undef,
  36. svnrepo => undef,
  37. svnpath => "trunk",
  38. srcdir => undef,
  39. destdir => undef,
  40. pingurl => [],
  41. templatedir => "/usr/share/ikiwiki/templates",
  42. underlaydir => "/usr/share/ikiwiki/basewiki",
  43. setup => undef,
  44. adminuser => undef,
  45. adminemail => undef,
  46. plugin => [qw{mdwn inline htmlscrubber}],
  47. timeformat => '%c',
  48. locale => undef,
  49. } #}}}
  50. sub checkconfig () { #{{{
  51. # locale stuff; avoid LC_ALL since it overrides everything
  52. if (defined $ENV{LC_ALL}) {
  53. $ENV{LANG} = $ENV{LC_ALL};
  54. delete $ENV{LC_ALL};
  55. }
  56. if (defined $config{locale}) {
  57. eval q{use POSIX};
  58. $ENV{LANG} = $config{locale}
  59. if POSIX::setlocale(&POSIX::LC_TIME, $config{locale});
  60. }
  61. if ($config{w3mmode}) {
  62. eval q{use Cwd q{abs_path}};
  63. $config{srcdir}=possibly_foolish_untaint(abs_path($config{srcdir}));
  64. $config{destdir}=possibly_foolish_untaint(abs_path($config{destdir}));
  65. $config{cgiurl}="file:///\$LIB/ikiwiki-w3m.cgi/".$config{cgiurl}
  66. unless $config{cgiurl} =~ m!file:///!;
  67. $config{url}="file://".$config{destdir};
  68. }
  69. if ($config{cgi} && ! length $config{url}) {
  70. error("Must specify url to wiki with --url when using --cgi\n");
  71. }
  72. if ($config{rss} && ! length $config{url}) {
  73. error("Must specify url to wiki with --url when using --rss\n");
  74. }
  75. $config{wikistatedir}="$config{srcdir}/.ikiwiki"
  76. unless exists $config{wikistatedir};
  77. if ($config{rcs}) {
  78. eval qq{require IkiWiki::Rcs::$config{rcs}};
  79. if ($@) {
  80. error("Failed to load RCS module IkiWiki::Rcs::$config{rcs}: $@");
  81. }
  82. }
  83. else {
  84. require IkiWiki::Rcs::Stub;
  85. }
  86. run_hooks(checkconfig => sub { shift->() });
  87. } #}}}
  88. sub loadplugins () { #{{{
  89. foreach my $plugin (@{$config{plugin}}) {
  90. my $mod="IkiWiki::Plugin::".possibly_foolish_untaint($plugin);
  91. eval qq{use $mod};
  92. if ($@) {
  93. error("Failed to load plugin $mod: $@");
  94. }
  95. }
  96. } #}}}
  97. sub error ($) { #{{{
  98. if ($config{cgi}) {
  99. print "Content-type: text/html\n\n";
  100. print misctemplate("Error", "<p>Error: @_</p>");
  101. }
  102. die @_;
  103. } #}}}
  104. sub debug ($) { #{{{
  105. return unless $config{verbose};
  106. if (! $config{cgi}) {
  107. print "@_\n";
  108. }
  109. else {
  110. print STDERR "@_\n";
  111. }
  112. } #}}}
  113. sub possibly_foolish_untaint ($) { #{{{
  114. my $tainted=shift;
  115. my ($untainted)=$tainted=~/(.*)/;
  116. return $untainted;
  117. } #}}}
  118. sub basename ($) { #{{{
  119. my $file=shift;
  120. $file=~s!.*/+!!;
  121. return $file;
  122. } #}}}
  123. sub dirname ($) { #{{{
  124. my $file=shift;
  125. $file=~s!/*[^/]+$!!;
  126. return $file;
  127. } #}}}
  128. sub pagetype ($) { #{{{
  129. my $page=shift;
  130. if ($page =~ /\.([^.]+)$/) {
  131. return $1 if exists $hooks{htmlize}{$1};
  132. }
  133. return undef;
  134. } #}}}
  135. sub pagename ($) { #{{{
  136. my $file=shift;
  137. my $type=pagetype($file);
  138. my $page=$file;
  139. $page=~s/\Q.$type\E*$// if defined $type;
  140. return $page;
  141. } #}}}
  142. sub htmlpage ($) { #{{{
  143. my $page=shift;
  144. return $page.".html";
  145. } #}}}
  146. sub srcfile ($) { #{{{
  147. my $file=shift;
  148. return "$config{srcdir}/$file" if -e "$config{srcdir}/$file";
  149. return "$config{underlaydir}/$file" if -e "$config{underlaydir}/$file";
  150. error("internal error: $file cannot be found");
  151. } #}}}
  152. sub readfile ($;$) { #{{{
  153. my $file=shift;
  154. my $binary=shift;
  155. if (-l $file) {
  156. error("cannot read a symlink ($file)");
  157. }
  158. local $/=undef;
  159. open (IN, $file) || error("failed to read $file: $!");
  160. binmode(IN) if ($binary);
  161. my $ret=<IN>;
  162. close IN;
  163. return $ret;
  164. } #}}}
  165. sub writefile ($$$;$) { #{{{
  166. my $file=shift; # can include subdirs
  167. my $destdir=shift; # directory to put file in
  168. my $content=shift;
  169. my $binary=shift;
  170. my $test=$file;
  171. while (length $test) {
  172. if (-l "$destdir/$test") {
  173. error("cannot write to a symlink ($test)");
  174. }
  175. $test=dirname($test);
  176. }
  177. my $dir=dirname("$destdir/$file");
  178. if (! -d $dir) {
  179. my $d="";
  180. foreach my $s (split(m!/+!, $dir)) {
  181. $d.="$s/";
  182. if (! -d $d) {
  183. mkdir($d) || error("failed to create directory $d: $!");
  184. }
  185. }
  186. }
  187. open (OUT, ">$destdir/$file") || error("failed to write $destdir/$file: $!");
  188. binmode(OUT) if ($binary);
  189. print OUT $content;
  190. close OUT;
  191. } #}}}
  192. sub bestlink ($$) { #{{{
  193. # Given a page and the text of a link on the page, determine which
  194. # existing page that link best points to. Prefers pages under a
  195. # subdirectory with the same name as the source page, failing that
  196. # goes down the directory tree to the base looking for matching
  197. # pages.
  198. my $page=shift;
  199. my $link=lc(shift);
  200. my $cwd=$page;
  201. do {
  202. my $l=$cwd;
  203. $l.="/" if length $l;
  204. $l.=$link;
  205. if (exists $links{$l}) {
  206. #debug("for $page, \"$link\", use $l");
  207. return $l;
  208. }
  209. } while $cwd=~s!/?[^/]+$!!;
  210. #print STDERR "warning: page $page, broken link: $link\n";
  211. return "";
  212. } #}}}
  213. sub isinlinableimage ($) { #{{{
  214. my $file=shift;
  215. $file=~/\.(png|gif|jpg|jpeg)$/i;
  216. } #}}}
  217. sub pagetitle ($) { #{{{
  218. my $page=shift;
  219. $page=~s/__(\d+)__/&#$1;/g;
  220. $page=~y/_/ /;
  221. return $page;
  222. } #}}}
  223. sub titlepage ($) { #{{{
  224. my $title=shift;
  225. $title=~y/ /_/;
  226. $title=~s/([^-[:alnum:]_:+\/.])/"__".ord($1)."__"/eg;
  227. return $title;
  228. } #}}}
  229. sub cgiurl (@) { #{{{
  230. my %params=@_;
  231. return $config{cgiurl}."?".join("&amp;", map "$_=$params{$_}", keys %params);
  232. } #}}}
  233. sub styleurl (;$) { #{{{
  234. my $page=shift;
  235. return "$config{url}/style.css" if ! defined $page;
  236. $page=~s/[^\/]+$//;
  237. $page=~s/[^\/]+\//..\//g;
  238. return $page."style.css";
  239. } #}}}
  240. sub abs2rel ($$) {
  241. # Work around very innefficient behavior in File::Spec if abs2rel
  242. # is passed two relative paths. It's much faster if paths are
  243. # absolute!
  244. my $path="/".shift;
  245. my $base="/".shift;
  246. require File::Spec;
  247. my $ret=File::Spec->abs2rel($path, $base);
  248. $ret=~s/^// if defined $ret;
  249. return $ret;
  250. }
  251. sub htmllink ($$$;$$$) { #{{{
  252. my $lpage=shift; # the page doing the linking
  253. my $page=shift; # the page that will contain the link (different for inline)
  254. my $link=shift;
  255. my $noimageinline=shift; # don't turn links into inline html images
  256. my $forcesubpage=shift; # force a link to a subpage
  257. my $linktext=shift; # set to force the link text to something
  258. my $bestlink;
  259. if (! $forcesubpage) {
  260. $bestlink=bestlink($lpage, $link);
  261. }
  262. else {
  263. $bestlink="$lpage/".lc($link);
  264. }
  265. $linktext=pagetitle(basename($link)) unless defined $linktext;
  266. return $linktext if length $bestlink && $page eq $bestlink;
  267. # TODO BUG: %renderedfiles may not have it, if the linked to page
  268. # was also added and isn't yet rendered! Note that this bug is
  269. # masked by the bug that makes all new files be rendered twice.
  270. if (! grep { $_ eq $bestlink } values %renderedfiles) {
  271. $bestlink=htmlpage($bestlink);
  272. }
  273. if (! grep { $_ eq $bestlink } values %renderedfiles) {
  274. return "<span><a href=\"".
  275. cgiurl(do => "create", page => $link, from => $page).
  276. "\">?</a>$linktext</span>"
  277. }
  278. $bestlink=abs2rel($bestlink, dirname($page));
  279. if (! $noimageinline && isinlinableimage($bestlink)) {
  280. return "<img src=\"$bestlink\" alt=\"$linktext\" />";
  281. }
  282. return "<a href=\"$bestlink\">$linktext</a>";
  283. } #}}}
  284. sub indexlink () { #{{{
  285. return "<a href=\"$config{url}\">$config{wikiname}</a>";
  286. } #}}}
  287. sub lockwiki () { #{{{
  288. # Take an exclusive lock on the wiki to prevent multiple concurrent
  289. # run issues. The lock will be dropped on program exit.
  290. if (! -d $config{wikistatedir}) {
  291. mkdir($config{wikistatedir});
  292. }
  293. open(WIKILOCK, ">$config{wikistatedir}/lockfile") ||
  294. error ("cannot write to $config{wikistatedir}/lockfile: $!");
  295. if (! flock(WIKILOCK, 2 | 4)) {
  296. debug("wiki seems to be locked, waiting for lock");
  297. my $wait=600; # arbitrary, but don't hang forever to
  298. # prevent process pileup
  299. for (1..600) {
  300. return if flock(WIKILOCK, 2 | 4);
  301. sleep 1;
  302. }
  303. error("wiki is locked; waited $wait seconds without lock being freed (possible stuck process or stale lock?)");
  304. }
  305. } #}}}
  306. sub unlockwiki () { #{{{
  307. close WIKILOCK;
  308. } #}}}
  309. sub loadindex () { #{{{
  310. open (IN, "$config{wikistatedir}/index") || return;
  311. while (<IN>) {
  312. $_=possibly_foolish_untaint($_);
  313. chomp;
  314. my %items;
  315. $items{link}=[];
  316. foreach my $i (split(/ /, $_)) {
  317. my ($item, $val)=split(/=/, $i, 2);
  318. push @{$items{$item}}, $val;
  319. }
  320. next unless exists $items{src}; # skip bad lines for now
  321. my $page=pagename($items{src}[0]);
  322. if (! $config{rebuild}) {
  323. $pagesources{$page}=$items{src}[0];
  324. $oldpagemtime{$page}=$items{mtime}[0];
  325. $oldlinks{$page}=[@{$items{link}}];
  326. $links{$page}=[@{$items{link}}];
  327. $depends{$page}=join(" ", @{$items{depends}})
  328. if exists $items{depends};
  329. $renderedfiles{$page}=$items{dest}[0];
  330. }
  331. $pagectime{$page}=$items{ctime}[0];
  332. }
  333. close IN;
  334. } #}}}
  335. sub saveindex () { #{{{
  336. run_hooks(savestate => sub { shift->() });
  337. if (! -d $config{wikistatedir}) {
  338. mkdir($config{wikistatedir});
  339. }
  340. open (OUT, ">$config{wikistatedir}/index") ||
  341. error("cannot write to $config{wikistatedir}/index: $!");
  342. foreach my $page (keys %oldpagemtime) {
  343. next unless $oldpagemtime{$page};
  344. my $line="mtime=$oldpagemtime{$page} ".
  345. "ctime=$pagectime{$page} ".
  346. "src=$pagesources{$page} ".
  347. "dest=$renderedfiles{$page}";
  348. $line.=" link=$_" foreach @{$links{$page}};
  349. if (exists $depends{$page}) {
  350. $line.=" depends=$_" foreach split " ", $depends{$page};
  351. }
  352. print OUT $line."\n";
  353. }
  354. close OUT;
  355. } #}}}
  356. sub template_params (@) { #{{{
  357. my $filename=shift;
  358. require HTML::Template;
  359. return filter => sub {
  360. my $text_ref = shift;
  361. $$text_ref=&Encode::decode_utf8($$text_ref);
  362. },
  363. filename => "$config{templatedir}/$filename", @_;
  364. } #}}}
  365. sub template ($;@) { #{{{
  366. HTML::Template->new(template_params(@_));
  367. } #}}}
  368. sub misctemplate ($$) { #{{{
  369. my $title=shift;
  370. my $pagebody=shift;
  371. my $template=template("misc.tmpl");
  372. $template->param(
  373. title => $title,
  374. indexlink => indexlink(),
  375. wikiname => $config{wikiname},
  376. pagebody => $pagebody,
  377. styleurl => styleurl(),
  378. baseurl => "$config{url}/",
  379. );
  380. return $template->output;
  381. }#}}}
  382. sub glob_match ($$) { #{{{
  383. my $page=shift;
  384. my $glob=shift;
  385. if ($glob =~ /^link\((.+)\)$/) {
  386. my $rev = $links{$page} or return undef;
  387. foreach my $p (@$rev) {
  388. return 1 if lc $p eq $1;
  389. }
  390. return 0;
  391. } elsif ($glob =~ /^backlink\((.+)\)$/) {
  392. my $rev = $links{$1} or return undef;
  393. foreach my $p (@$rev) {
  394. return 1 if lc $p eq $page;
  395. }
  396. return 0;
  397. } else {
  398. # turn glob into safe regexp
  399. $glob=quotemeta($glob);
  400. $glob=~s/\\\*/.*/g;
  401. $glob=~s/\\\?/./g;
  402. $glob=~s!\\/!/!g;
  403. return $page=~/^$glob$/i;
  404. }
  405. } #}}}
  406. sub globlist_match ($$) { #{{{
  407. my $page=shift;
  408. my @globlist=split(" ", shift);
  409. # check any negated globs first
  410. foreach my $glob (@globlist) {
  411. return 0 if $glob=~/^!(.*)/ && glob_match($page, $1);
  412. }
  413. foreach my $glob (@globlist) {
  414. return 1 if glob_match($page, $glob);
  415. }
  416. return 0;
  417. } #}}}
  418. sub hook (@) { # {{{
  419. my %param=@_;
  420. if (! exists $param{type} || ! ref $param{call} || ! exists $param{id}) {
  421. error "hook requires type, call, and id parameters";
  422. }
  423. $hooks{$param{type}}{$param{id}}=\%param;
  424. } # }}}
  425. sub run_hooks ($$) { # {{{
  426. # Calls the given sub for each hook of the given type,
  427. # passing it the hook function to call.
  428. my $type=shift;
  429. my $sub=shift;
  430. if (exists $hooks{$type}) {
  431. foreach my $id (keys %{$hooks{$type}}) {
  432. $sub->($hooks{$type}{$id}{call});
  433. }
  434. }
  435. } #}}}
  436. 1