X-Git-Url: http://git.vanrenterghem.biz/git.ikiwiki.info.git/blobdiff_plain/5b78246d11948e93f54ae32dd800e9adaf55a546..339f15d6f4a2d22014e2d38b7459de8a28c5fa8f:/IkiWiki/Plugin/external.pm diff --git a/IkiWiki/Plugin/external.pm b/IkiWiki/Plugin/external.pm index 9c31a70eb..a4cc1dd3c 100644 --- a/IkiWiki/Plugin/external.pm +++ b/IkiWiki/Plugin/external.pm @@ -1,26 +1,26 @@ #!/usr/bin/perl # Support for external plugins written in other languages. -# Communication via XML RPC a pipe. +# Communication via XML RPC to a pipe. # See externaldemo for an example of a plugin that uses this. package IkiWiki::Plugin::external; use warnings; use strict; -use IkiWiki 2.00; +use IkiWiki 3.00; use RPC::XML; -use RPC::XML::Parser; use IPC::Open2; use IO::Handle; my %plugins; -sub import { #{{{ +sub import { my $self=shift; my $plugin=shift; return unless defined $plugin; my ($plugin_read, $plugin_write); - my $pid = open2($plugin_read, $plugin_write, $plugin); + my $pid = open2($plugin_read, $plugin_write, + IkiWiki::possibly_foolish_untaint($plugin)); # open2 doesn't respect "use open ':utf8'" binmode($plugin_read, ':utf8'); @@ -29,18 +29,21 @@ sub import { #{{{ $plugins{$plugin}={in => $plugin_read, out => $plugin_write, pid => $pid, accum => ""}; + $RPC::XML::ENCODING="utf-8"; + $RPC::XML::FORCE_STRING_ENCODING="true"; + rpc_call($plugins{$plugin}, "import"); -} #}}} +} -sub rpc_write ($$) { #{{{ +sub rpc_write ($$) { my $fh=shift; my $string=shift; $fh->print($string."\n"); $fh->flush; -} #}}} +} -sub rpc_call ($$;@) { #{{{ +sub rpc_call ($$;@) { my $plugin=shift; my $command=shift; @@ -53,18 +56,47 @@ sub rpc_call ($$;@) { #{{{ $plugin->{accum}.=$_; while ($plugin->{accum} =~ /^\s*(<\?xml\s.*?<\/(?:methodCall|methodResponse)>)\n(.*)/s) { $plugin->{accum}=$2; - my $r = RPC::XML::Parser->new->parse($1); + my $parser; + eval q{ + use RPC::XML::ParserFactory; + $parser = RPC::XML::ParserFactory->new; + }; + if ($@) { + # old interface + eval q{ + use RPC::XML::Parser; + $parser = RPC::XML::Parser->new; + }; + } + my $r=$parser->parse($1); error("XML RPC parser failure: $r") unless ref $r; if ($r->isa('RPC::XML::response')) { my $value=$r->value; - if ($value->isa('RPC::XML::array')) { + if ($r->is_fault($value)) { + # throw the error as best we can + print STDERR $value->string."\n"; + return ""; + } + elsif ($value->isa('RPC::XML::array')) { return @{$value->value}; } elsif ($value->isa('RPC::XML::struct')) { - return %{$value->value}; - } - elsif ($value->isa('RPC::XML::fault')) { - die $value->string; + my %hash=%{$value->value}; + + # XML-RPC v1 does not allow for + # nil/null/None/undef values to be + # transmitted. The extension + # is the right fix, but for + # back-compat, let external plugins send + # a hash with one key "null" pointing + # to an empty string. + if (exists $hash{null} && + $hash{null} eq "" && + int(keys(%hash)) == 1) { + return undef; + } + + return %hash; } else { return $value->value; @@ -88,6 +120,14 @@ sub rpc_call ($$;@) { #{{{ error("XML RPC call error, unknown function: $name"); } + # XML-RPC v1 does not allow for nil/null/None/undef + # values to be transmitted, so until XML::RPC::Parser + # honours v2 (), send a hash with one key "null" + # pointing to an empty string. + if (! defined $ret) { + $ret={"null" => ""}; + } + my $string=eval { RPC::XML::response->new($ret)->as_string }; if ($@ && ref $ret) { # One common reason for serialisation to @@ -104,12 +144,12 @@ sub rpc_call ($$;@) { #{{{ } return undef; -} #}}} +} package IkiWiki::RPC::XML; use Memoize; -sub getvar ($$$) { #{{{ +sub getvar ($$$) { my $plugin=shift; my $varname="IkiWiki::".shift; my $key=shift; @@ -118,20 +158,53 @@ sub getvar ($$$) { #{{{ my $ret=$varname->{$key}; use strict 'refs'; return $ret; -} #}}} +} -sub setvar ($$$;@) { #{{{ +sub setvar ($$$;@) { my $plugin=shift; my $varname="IkiWiki::".shift; my $key=shift; + my $value=shift; no strict 'refs'; - my $ret=$varname->{$key}=@_; + my $ret=$varname->{$key}=$value; use strict 'refs'; return $ret; -} #}}} +} + +sub getstate ($$$$) { + my $plugin=shift; + my $page=shift; + my $id=shift; + my $key=shift; + + return $IkiWiki::pagestate{$page}{$id}{$key}; +} -sub inject ($@) { #{{{ +sub setstate ($$$$;@) { + my $plugin=shift; + my $page=shift; + my $id=shift; + my $key=shift; + my $value=shift; + + return $IkiWiki::pagestate{$page}{$id}{$key}=$value; +} + +sub getargv ($) { + my $plugin=shift; + + return \@ARGV; +} + +sub setargv ($@) { + my $plugin=shift; + my $array=shift; + + @ARGV=@$array; +} + +sub inject ($@) { # Bind a given perl function name to a particular RPC request. my $plugin=shift; my %params=@_; @@ -142,12 +215,20 @@ sub inject ($@) { #{{{ my $sub = sub { IkiWiki::Plugin::external::rpc_call($plugin, $params{call}, @_) }; + $sub=memoize($sub) if $params{memoize}; + + # This will add it to the symbol table even if not present. + no warnings; eval qq{*$params{name}=\$sub}; - memoize($params{name}) if $params{memoize}; + use warnings; + + # This will ensure that everywhere it was exported to sees + # the injected version. + IkiWiki::inject(name => $params{name}, call => $sub); return 1; -} #}}} +} -sub hook ($@) { #{{{ +sub hook ($@) { # the call parameter is a function name to call, since XML RPC # cannot pass a function reference my $plugin=shift; @@ -157,15 +238,18 @@ sub hook ($@) { #{{{ delete $params{call}; IkiWiki::hook(%params, call => sub { - IkiWiki::Plugin::external::rpc_call($plugin, $callback, @_) + IkiWiki::Plugin::external::rpc_call($plugin, $callback, @_); }); -} #}}} +} -sub pagespec_match ($@) { #{{{ - # convert pagespec_match's return object into a XML RPC boolean +sub pagespec_match ($@) { + # convert return object into a XML RPC boolean my $plugin=shift; + my $page=shift; + my $spec=shift; - return RPC::XML::boolean->new(0 + IkiWiki::pagespec_march(@_)); -} #}}} + return RPC::XML::boolean->new(0 + IkiWiki::pagespec_match( + $page, $spec, @_)); +} 1