+ return $content;
+}
+
+sub linkify ($$$) {
+ my $page=shift;
+ my $destpage=shift;
+ my $content=shift;
+
+ run_hooks(linkify => sub {
+ $content=shift->(
+ page => $page,
+ destpage => $destpage,
+ content => $content,
+ );
+ });
+
+ return $content;
+}
+
+our %preprocessing;
+our $preprocess_preview=0;
+sub preprocess ($$$;$$) {
+ my $page=shift; # the page the data comes from
+ my $destpage=shift; # the page the data will appear in (different for inline)
+ my $content=shift;
+ my $scan=shift;
+ my $preview=shift;
+
+ # Using local because it needs to be set within any nested calls
+ # of this function.
+ local $preprocess_preview=$preview if defined $preview;
+
+ my $handle=sub {
+ my $escape=shift;
+ my $prefix=shift;
+ my $command=shift;
+ my $params=shift;
+ $params="" if ! defined $params;
+
+ if (length $escape) {
+ return "[[$prefix$command $params]]";
+ }
+ elsif (exists $hooks{preprocess}{$command}) {
+ return "" if $scan && ! $hooks{preprocess}{$command}{scan};
+ # Note: preserve order of params, some plugins may
+ # consider it significant.
+ my @params;
+ while ($params =~ m{
+ (?:([-.\w]+)=)? # 1: named parameter key?
+ (?:
+ """(.*?)""" # 2: triple-quoted value
+ |
+ "([^"]*?)" # 3: single-quoted value
+ |
+ '''(.*?)''' # 4: triple-single-quote
+ |
+ <<([a-zA-Z]+)\n # 5: heredoc start
+ (.*?)\n\5 # 6: heredoc value
+ |
+ (\S+) # 7: unquoted value
+ )
+ (?:\s+|$) # delimiter to next param
+ }msgx) {
+ my $key=$1;
+ my $val;
+ if (defined $2) {
+ $val=$2;
+ $val=~s/\r\n/\n/mg;
+ $val=~s/^\n+//g;
+ $val=~s/\n+$//g;
+ }
+ elsif (defined $3) {
+ $val=$3;
+ }
+ elsif (defined $4) {
+ $val=$4;
+ }
+ elsif (defined $7) {
+ $val=$7;
+ }
+ elsif (defined $6) {
+ $val=$6;
+ }
+
+ if (defined $key) {
+ push @params, $key, $val;
+ }
+ else {
+ push @params, $val, '';
+ }
+ }
+ if ($preprocessing{$page}++ > 8) {
+ # Avoid loops of preprocessed pages preprocessing
+ # other pages that preprocess them, etc.
+ return "[[!$command <span class=\"error\">".
+ sprintf(gettext("preprocessing loop detected on %s at depth %i"),
+ $page, $preprocessing{$page}).
+ "</span>]]";
+ }
+ my $ret;
+ if (! $scan) {
+ $ret=eval {
+ $hooks{preprocess}{$command}{call}->(
+ @params,
+ page => $page,
+ destpage => $destpage,
+ preview => $preprocess_preview,
+ );
+ };
+ if ($@) {
+ my $error=$@;
+ chomp $error;
+ eval q{use HTML::Entities};
+ # Also encode most ASCII punctuation
+ # as entities so that error messages
+ # are not interpreted as Markdown etc.
+ $error = encode_entities($error, '^-A-Za-z0-9+_,./:;= '."'");
+ $ret="[[!$command <span class=\"error\">".
+ gettext("Error").": $error"."</span>]]";
+ }
+ }
+ else {
+ # use void context during scan pass
+ eval {
+ $hooks{preprocess}{$command}{call}->(
+ @params,
+ page => $page,
+ destpage => $destpage,
+ preview => $preprocess_preview,
+ );
+ };
+ $ret="";
+ }
+ $preprocessing{$page}--;
+ return $ret;
+ }
+ else {
+ return "[[$prefix$command $params]]";
+ }
+ };
+
+ my $regex;
+ if ($config{prefix_directives}) {
+ $regex = qr{
+ (\\?) # 1: escape?
+ \[\[(!) # directive open; 2: prefix
+ ([-\w]+) # 3: command
+ ( # 4: the parameters..
+ \s+ # Must have space if parameters present
+ (?:
+ (?:[-.\w]+=)? # named parameter key?
+ (?:
+ """.*?""" # triple-quoted value
+ |
+ "[^"]*?" # single-quoted value
+ |
+ '''.*?''' # triple-single-quote
+ |
+ <<([a-zA-Z]+)\n # 5: heredoc start
+ (?:.*?)\n\5 # heredoc value
+ |
+ [^"\s\]]+ # unquoted value
+ )
+ \s* # whitespace or end
+ # of directive
+ )
+ *)? # 0 or more parameters
+ \]\] # directive closed
+ }sx;
+ }
+ else {
+ $regex = qr{
+ (\\?) # 1: escape?
+ \[\[(!?) # directive open; 2: optional prefix
+ ([-\w]+) # 3: command
+ \s+
+ ( # 4: the parameters..
+ (?:
+ (?:[-.\w]+=)? # named parameter key?
+ (?:
+ """.*?""" # triple-quoted value
+ |
+ "[^"]*?" # single-quoted value
+ |
+ '''.*?''' # triple-single-quote
+ |
+ <<([a-zA-Z]+)\n # 5: heredoc start
+ (?:.*?)\n\5 # heredoc value
+ |
+ [^"\s\]]+ # unquoted value
+ )
+ \s* # whitespace or end
+ # of directive
+ )
+ *) # 0 or more parameters
+ \]\] # directive closed
+ }sx;
+ }
+
+ $content =~ s{$regex}{$handle->($1, $2, $3, $4)}eg;
+ return $content;
+}
+
+sub filter ($$$) {
+ my $page=shift;
+ my $destpage=shift;
+ my $content=shift;
+
+ run_hooks(filter => sub {
+ $content=shift->(page => $page, destpage => $destpage,
+ content => $content);
+ });
+
+ return $content;
+}
+
+sub check_canedit ($$$;$) {
+ my $page=shift;
+ my $q=shift;
+ my $session=shift;
+ my $nonfatal=shift;
+
+ my $canedit;
+ run_hooks(canedit => sub {
+ return if defined $canedit;
+ my $ret=shift->($page, $q, $session);
+ if (defined $ret) {
+ if ($ret eq "") {
+ $canedit=1;
+ }
+ elsif (ref $ret eq 'CODE') {
+ $ret->() unless $nonfatal;
+ $canedit=0;
+ }
+ elsif (defined $ret) {
+ error($ret) unless $nonfatal;
+ $canedit=0;
+ }
+ }
+ });
+ return defined $canedit ? $canedit : 1;
+}
+
+sub check_content (@) {
+ my %params=@_;
+
+ return 1 if ! exists $hooks{checkcontent}; # optimisation
+
+ if (exists $pagesources{$params{page}}) {
+ my @diff;
+ my %old=map { $_ => 1 }
+ split("\n", readfile(srcfile($pagesources{$params{page}})));
+ foreach my $line (split("\n", $params{content})) {
+ push @diff, $line if ! exists $old{$line};
+ }
+ $params{diff}=join("\n", @diff);
+ }
+
+ my $ok;
+ run_hooks(checkcontent => sub {
+ return if defined $ok;
+ my $ret=shift->(%params);
+ if (defined $ret) {
+ if ($ret eq "") {
+ $ok=1;
+ }
+ elsif (ref $ret eq 'CODE') {
+ $ret->() unless $params{nonfatal};
+ $ok=0;
+ }
+ elsif (defined $ret) {
+ error($ret) unless $params{nonfatal};
+ $ok=0;
+ }
+ }
+
+ });
+ return defined $ok ? $ok : 1;
+}
+
+sub check_canchange (@) {
+ my %params = @_;
+ my $cgi = $params{cgi};
+ my $session = $params{session};
+ my @changes = @{$params{changes}};
+
+ my %newfiles;
+ foreach my $change (@changes) {
+ # This untaint is safe because we check file_pruned and
+ # wiki_file_regexp.
+ my ($file)=$change->{file}=~/$config{wiki_file_regexp}/;
+ $file=possibly_foolish_untaint($file);
+ if (! defined $file || ! length $file ||
+ file_pruned($file)) {
+ error(sprintf(gettext("bad file name %s"), $file));
+ }
+
+ my $type=pagetype($file);
+ my $page=pagename($file) if defined $type;
+
+ if ($change->{action} eq 'add') {
+ $newfiles{$file}=1;
+ }
+
+ if ($change->{action} eq 'change' ||
+ $change->{action} eq 'add') {
+ if (defined $page) {
+ check_canedit($page, $cgi, $session);
+ next;
+ }
+ else {
+ if (IkiWiki::Plugin::attachment->can("check_canattach")) {
+ IkiWiki::Plugin::attachment::check_canattach($session, $file, $change->{path});
+ check_canedit($file, $cgi, $session);
+ next;
+ }
+ }
+ }
+ elsif ($change->{action} eq 'remove') {
+ # check_canremove tests to see if the file is present
+ # on disk. This will fail when a single commit adds a
+ # file and then removes it again. Avoid the problem
+ # by not testing the removal in such pairs of changes.
+ # (The add is still tested, just to make sure that
+ # no data is added to the repo that a web edit
+ # could not add.)
+ next if $newfiles{$file};
+
+ if (IkiWiki::Plugin::remove->can("check_canremove")) {
+ IkiWiki::Plugin::remove::check_canremove(defined $page ? $page : $file, $cgi, $session);
+ check_canedit(defined $page ? $page : $file, $cgi, $session);
+ next;
+ }
+ }
+ else {
+ error "unknown action ".$change->{action};
+ }
+
+ error sprintf(gettext("you are not allowed to change %s"), $file);
+ }
+}
+
+
+my $wikilock;
+
+sub lockwiki () {
+ # Take an exclusive lock on the wiki to prevent multiple concurrent
+ # run issues. The lock will be dropped on program exit.
+ if (! -d $config{wikistatedir}) {
+ mkdir($config{wikistatedir});
+ }
+ open($wikilock, '>', "$config{wikistatedir}/lockfile") ||
+ error ("cannot write to $config{wikistatedir}/lockfile: $!");
+ if (! flock($wikilock, LOCK_EX | LOCK_NB)) {
+ debug("failed to get lock; waiting...");
+ if (! flock($wikilock, LOCK_EX)) {
+ error("failed to get lock");
+ }
+ }
+ return 1;
+}
+
+sub unlockwiki () {
+ POSIX::close($ENV{IKIWIKI_CGILOCK_FD}) if exists $ENV{IKIWIKI_CGILOCK_FD};
+ return close($wikilock) if $wikilock;
+ return;
+}
+
+my $commitlock;
+
+sub commit_hook_enabled () {
+ open($commitlock, '+>', "$config{wikistatedir}/commitlock") ||
+ error("cannot write to $config{wikistatedir}/commitlock: $!");
+ if (! flock($commitlock, 1 | 4)) { # LOCK_SH | LOCK_NB to test
+ close($commitlock) || error("failed closing commitlock: $!");
+ return 0;
+ }
+ close($commitlock) || error("failed closing commitlock: $!");
+ return 1;
+}
+
+sub disable_commit_hook () {
+ open($commitlock, '>', "$config{wikistatedir}/commitlock") ||
+ error("cannot write to $config{wikistatedir}/commitlock: $!");
+ if (! flock($commitlock, 2)) { # LOCK_EX
+ error("failed to get commit lock");
+ }
+ return 1;
+}
+
+sub enable_commit_hook () {
+ return close($commitlock) if $commitlock;
+ return;
+}
+
+sub loadindex () {
+ %oldrenderedfiles=%pagectime=();
+ my $rebuild=$config{rebuild};
+ if (! $rebuild) {
+ %pagesources=%pagemtime=%oldlinks=%links=%depends=
+ %destsources=%renderedfiles=%pagecase=%pagestate=
+ %depends_simple=%typedlinks=%oldtypedlinks=();
+ }
+ my $in;
+ if (! open ($in, "<", "$config{wikistatedir}/indexdb")) {
+ if (-e "$config{wikistatedir}/index") {
+ system("ikiwiki-transition", "indexdb", $config{srcdir});
+ open ($in, "<", "$config{wikistatedir}/indexdb") || return;
+ }
+ else {
+ # gettime on first build
+ $config{gettime}=1 unless defined $config{gettime};
+ return;
+ }
+ }
+
+ my $index=Storable::fd_retrieve($in);
+ if (! defined $index) {
+ return 0;
+ }
+
+ my $pages;
+ if (exists $index->{version} && ! ref $index->{version}) {
+ $pages=$index->{page};
+ %wikistate=%{$index->{state}};
+ # Handle plugins that got disabled by loading a new setup.
+ if (exists $config{setupfile}) {
+ require IkiWiki::Setup;
+ IkiWiki::Setup::disabled_plugins(
+ grep { ! $loaded_plugins{$_} } keys %wikistate);
+ }
+ }
+ else {
+ $pages=$index;
+ %wikistate=();
+ }
+
+ foreach my $src (keys %$pages) {
+ my $d=$pages->{$src};
+ my $page;
+ if (exists $d->{page} && ! $rebuild) {
+ $page=$d->{page};
+ }
+ else {
+ $page=pagename($src);
+ }
+ $pagectime{$page}=$d->{ctime};
+ $pagesources{$page}=$src;
+ if (! $rebuild) {
+ $pagemtime{$page}=$d->{mtime};
+ $renderedfiles{$page}=$d->{dest};
+ if (exists $d->{links} && ref $d->{links}) {
+ $links{$page}=$d->{links};
+ $oldlinks{$page}=[@{$d->{links}}];
+ }
+ if (ref $d->{depends_simple} eq 'ARRAY') {
+ # old format
+ $depends_simple{$page}={
+ map { $_ => 1 } @{$d->{depends_simple}}
+ };
+ }
+ elsif (exists $d->{depends_simple}) {
+ $depends_simple{$page}=$d->{depends_simple};
+ }
+ if (exists $d->{dependslist}) {
+ # old format
+ $depends{$page}={
+ map { $_ => $DEPEND_CONTENT }
+ @{$d->{dependslist}}
+ };
+ }
+ elsif (exists $d->{depends} && ! ref $d->{depends}) {
+ # old format
+ $depends{$page}={$d->{depends} => $DEPEND_CONTENT };
+ }
+ elsif (exists $d->{depends}) {
+ $depends{$page}=$d->{depends};
+ }
+ if (exists $d->{state}) {
+ $pagestate{$page}=$d->{state};
+ }
+ if (exists $d->{typedlinks}) {
+ $typedlinks{$page}=$d->{typedlinks};
+
+ while (my ($type, $links) = each %{$typedlinks{$page}}) {
+ next unless %$links;
+ $oldtypedlinks{$page}{$type} = {%$links};
+ }
+ }
+ }
+ $oldrenderedfiles{$page}=[@{$d->{dest}}];
+ }
+ foreach my $page (keys %pagesources) {
+ $pagecase{lc $page}=$page;
+ }
+ foreach my $page (keys %renderedfiles) {
+ $destsources{$_}=$page foreach @{$renderedfiles{$page}};
+ }
+ $lastrev=$index->{lastrev};
+ @underlayfiles=@{$index->{underlayfiles}} if ref $index->{underlayfiles};
+ return close($in);
+}
+
+sub saveindex () {
+ run_hooks(savestate => sub { shift->() });
+
+ my @plugins=keys %loaded_plugins;
+
+ if (! -d $config{wikistatedir}) {
+ mkdir($config{wikistatedir});
+ }
+ my $newfile="$config{wikistatedir}/indexdb.new";
+ my $cleanup = sub { unlink($newfile) };
+ open (my $out, '>', $newfile) || error("cannot write to $newfile: $!", $cleanup);
+
+ my %index;
+ foreach my $page (keys %pagemtime) {
+ next unless $pagemtime{$page};
+ my $src=$pagesources{$page};
+
+ $index{page}{$src}={
+ page => $page,
+ ctime => $pagectime{$page},
+ mtime => $pagemtime{$page},
+ dest => $renderedfiles{$page},
+ links => $links{$page},
+ };
+
+ if (exists $depends{$page}) {
+ $index{page}{$src}{depends} = $depends{$page};
+ }
+
+ if (exists $depends_simple{$page}) {
+ $index{page}{$src}{depends_simple} = $depends_simple{$page};
+ }
+
+ if (exists $typedlinks{$page} && %{$typedlinks{$page}}) {
+ $index{page}{$src}{typedlinks} = $typedlinks{$page};
+ }
+
+ if (exists $pagestate{$page}) {
+ $index{page}{$src}{state}=$pagestate{$page};
+ }
+ }
+
+ $index{state}={};
+ foreach my $id (@plugins) {
+ $index{state}{$id}={}; # used to detect disabled plugins
+ foreach my $key (keys %{$wikistate{$id}}) {
+ $index{state}{$id}{$key}=$wikistate{$id}{$key};
+ }
+ }
+
+ $index{lastrev}=$lastrev;
+ $index{underlayfiles}=\@underlayfiles;
+
+ $index{version}="3";
+ my $ret=Storable::nstore_fd(\%index, $out);
+ return if ! defined $ret || ! $ret;
+ close $out || error("failed saving to $newfile: $!", $cleanup);
+ rename($newfile, "$config{wikistatedir}/indexdb") ||
+ error("failed renaming $newfile to $config{wikistatedir}/indexdb", $cleanup);
+
+ return 1;
+}
+
+sub template_file ($) {
+ my $name=shift;
+
+ my $tpage=($name =~ s/^\///) ? $name : "templates/$name";
+ my $template;
+ if ($name !~ /\.tmpl$/ && exists $pagesources{$tpage}) {
+ $template=srcfile($pagesources{$tpage}, 1);
+ $name.=".tmpl";
+ }
+ else {
+ $template=srcfile($tpage, 1);
+ }
+
+ if (defined $template) {
+ return $template, $tpage, 1 if wantarray;
+ return $template;
+ }
+ else {
+ $name=~s:/::; # avoid path traversal
+ foreach my $dir ($config{templatedir},
+ "$installdir/share/ikiwiki/templates") {
+ if (-e "$dir/$name") {
+ $template="$dir/$name";
+ last;
+ }
+ }
+ if (defined $template) {
+ return $template, $tpage if wantarray;
+ return $template;
+ }
+ }
+
+ return;
+}
+
+sub template_depends ($$;@) {
+ my $name=shift;
+ my $page=shift;
+
+ my ($filename, $tpage, $untrusted)=template_file($name);
+ if (! defined $filename) {
+ error(sprintf(gettext("template %s not found"), $name))
+ }
+
+ if (defined $page && defined $tpage) {
+ add_depends($page, $tpage);
+ }
+
+ my @opts=(
+ filter => sub {
+ my $text_ref = shift;
+ ${$text_ref} = decode_utf8(${$text_ref});
+ run_hooks(readtemplate => sub {
+ ${$text_ref} = shift->(
+ id => $name,
+ page => $tpage,
+ content => ${$text_ref},
+ untrusted => $untrusted,
+ );
+ });
+ },
+ loop_context_vars => 1,
+ die_on_bad_params => 0,
+ parent_global_vars => 1,
+ filename => $filename,
+ @_,
+ ($untrusted ? (no_includes => 1) : ()),
+ );
+ return @opts if wantarray;
+
+ require HTML::Template;
+ return HTML::Template->new(@opts);
+}
+
+sub template ($;@) {
+ template_depends(shift, undef, @_);
+}
+
+sub templateactions ($$) {
+ my $template=shift;
+ my $page=shift;
+
+ my $have_actions=0;
+ my @actions;
+ run_hooks(pageactions => sub {
+ push @actions, map { { action => $_ } }
+ grep { defined } shift->(page => $page);
+ });
+ $template->param(actions => \@actions);
+
+ if ($config{cgiurl} && exists $hooks{auth}) {
+ $template->param(prefsurl => cgiurl(do => "prefs"));
+ $have_actions=1;
+ }
+
+ if ($have_actions || @actions) {
+ $template->param(have_actions => 1);
+ }
+}
+
+sub hook (@) {
+ my %param=@_;
+
+ if (! exists $param{type} || ! ref $param{call} || ! exists $param{id}) {
+ error 'hook requires type, call, and id parameters';
+ }
+
+ return if $param{no_override} && exists $hooks{$param{type}}{$param{id}};
+
+ $hooks{$param{type}}{$param{id}}=\%param;
+ return 1;
+}
+
+sub run_hooks ($$) {
+ # Calls the given sub for each hook of the given type,
+ # passing it the hook function to call.
+ my $type=shift;
+ my $sub=shift;
+
+ if (exists $hooks{$type}) {
+ my (@first, @middle, @last);
+ foreach my $id (keys %{$hooks{$type}}) {
+ if ($hooks{$type}{$id}{first}) {
+ push @first, $id;
+ }
+ elsif ($hooks{$type}{$id}{last}) {
+ push @last, $id;
+ }
+ else {
+ push @middle, $id;
+ }
+ }
+ foreach my $id (@first, @middle, @last) {
+ $sub->($hooks{$type}{$id}{call});
+ }
+ }
+
+ return 1;
+}
+
+sub rcs_update () {
+ $hooks{rcs}{rcs_update}{call}->(@_);
+}
+
+sub rcs_prepedit ($) {
+ $hooks{rcs}{rcs_prepedit}{call}->(@_);
+}
+
+sub rcs_commit (@) {
+ $hooks{rcs}{rcs_commit}{call}->(@_);
+}
+
+sub rcs_commit_staged (@) {
+ $hooks{rcs}{rcs_commit_staged}{call}->(@_);
+}
+
+sub rcs_add ($) {
+ $hooks{rcs}{rcs_add}{call}->(@_);
+}
+
+sub rcs_remove ($) {
+ $hooks{rcs}{rcs_remove}{call}->(@_);
+}
+
+sub rcs_rename ($$) {
+ $hooks{rcs}{rcs_rename}{call}->(@_);
+}
+
+sub rcs_recentchanges ($) {
+ $hooks{rcs}{rcs_recentchanges}{call}->(@_);
+}
+
+sub rcs_diff ($;$) {
+ $hooks{rcs}{rcs_diff}{call}->(@_);
+}
+
+sub rcs_getctime ($) {
+ $hooks{rcs}{rcs_getctime}{call}->(@_);
+}
+
+sub rcs_getmtime ($) {
+ $hooks{rcs}{rcs_getmtime}{call}->(@_);
+}
+
+sub rcs_receive () {
+ $hooks{rcs}{rcs_receive}{call}->();
+}
+
+sub add_depends ($$;$) {
+ my $page=shift;
+ my $pagespec=shift;
+ my $deptype=shift || $DEPEND_CONTENT;
+
+ # Is the pagespec a simple page name?
+ if ($pagespec =~ /$config{wiki_file_regexp}/ &&
+ $pagespec !~ /[\s*?()!]/) {
+ $depends_simple{$page}{lc $pagespec} |= $deptype;
+ return 1;
+ }
+
+ # Add explicit dependencies for influences.
+ my $sub=pagespec_translate($pagespec);
+ return unless defined $sub;
+ foreach my $p (keys %pagesources) {
+ my $r=$sub->($p, location => $page);
+ my $i=$r->influences;
+ my $static=$r->influences_static;
+ foreach my $k (keys %$i) {
+ next unless $r || $static || $k eq $page;
+ $depends_simple{$page}{lc $k} |= $i->{$k};
+ }
+ last if $static;
+ }
+
+ $depends{$page}{$pagespec} |= $deptype;
+ return 1;
+}
+
+sub deptype (@) {
+ my $deptype=0;
+ foreach my $type (@_) {
+ if ($type eq 'presence') {
+ $deptype |= $DEPEND_PRESENCE;
+ }
+ elsif ($type eq 'links') {
+ $deptype |= $DEPEND_LINKS;
+ }
+ elsif ($type eq 'content') {
+ $deptype |= $DEPEND_CONTENT;
+ }
+ }
+ return $deptype;
+}
+
+my $file_prune_regexp;
+sub file_pruned ($) {
+ my $file=shift;
+
+ if (defined $config{include} && length $config{include}) {
+ return 0 if $file =~ m/$config{include}/;
+ }