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