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