aboutsummaryrefslogtreecommitdiff
path: root/IkiWiki/Plugin/external.pm
diff options
context:
space:
mode:
Diffstat (limited to 'IkiWiki/Plugin/external.pm')
-rw-r--r--IkiWiki/Plugin/external.pm169
1 files changed, 169 insertions, 0 deletions
diff --git a/IkiWiki/Plugin/external.pm b/IkiWiki/Plugin/external.pm
new file mode 100644
index 000000000..e3504a30d
--- /dev/null
+++ b/IkiWiki/Plugin/external.pm
@@ -0,0 +1,169 @@
+#!/usr/bin/perl
+# Support for external plugins written in other languages.
+# Communication via XML RPC 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 RPC::XML;
+use RPC::XML::Parser;
+use IPC::Open2;
+use IO::Handle;
+
+my %plugins;
+
+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);
+
+ # open2 doesn't respect "use open ':utf8'"
+ binmode($plugin_read, ':utf8');
+ binmode($plugin_write, ':utf8');
+
+ $plugins{$plugin}={in => $plugin_read, out => $plugin_write, pid => $pid,
+ accum => ""};
+
+ rpc_call($plugins{$plugin}, "import");
+} #}}}
+
+sub rpc_write ($$) { #{{{
+ my $fh=shift;
+ my $string=shift;
+
+ $fh->print($string."\n");
+ $fh->flush;
+} #}}}
+
+sub rpc_call ($$;@) { #{{{
+ my $plugin=shift;
+ my $command=shift;
+
+ # send the command
+ my $req=RPC::XML::request->new($command, @_);
+ rpc_write($plugin->{out}, $req->as_string);
+
+ # process incoming rpc until a result is available
+ while ($_ = $plugin->{in}->getline) {
+ $plugin->{accum}.=$_;
+ while ($plugin->{accum} =~ /^\s*(<\?xml\s.*?<\/(?:methodCall|methodResponse)>)\n(.*)/s) {
+ $plugin->{accum}=$2;
+ my $r = RPC::XML::Parser->new->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')) {
+ return @{$value->value};
+ }
+ elsif ($value->isa('RPC::XML::struct')) {
+ return %{$value->value};
+ }
+ elsif ($value->isa('RPC::XML::fault')) {
+ die $value->string;
+ }
+ else {
+ return $value->value;
+ }
+ }
+
+ my $name=$r->name;
+ my @args=map { $_->value } @{$r->args};
+
+ # When dispatching a function, first look in
+ # IkiWiki::RPC::XML. This allows overriding
+ # IkiWiki functions with RPC friendly versions.
+ my $ret;
+ if (exists $IkiWiki::RPC::XML::{$name}) {
+ $ret=$IkiWiki::RPC::XML::{$name}($plugin, @args);
+ }
+ elsif (exists $IkiWiki::{$name}) {
+ $ret=$IkiWiki::{$name}(@args);
+ }
+ else {
+ error("XML RPC call error, unknown function: $name");
+ }
+
+ my $string=eval { RPC::XML::response->new($ret)->as_string };
+ if ($@ && ref $ret) {
+ # One common reason for serialisation to
+ # fail is a complex return type that cannot
+ # be represented as an XML RPC response.
+ # Handle this case by just returning 1.
+ $string=eval { RPC::XML::response->new(1)->as_string };
+ }
+ if ($@) {
+ error("XML response serialisation failed: $@");
+ }
+ rpc_write($plugin->{out}, $string);
+ }
+ }
+
+ return undef;
+} #}}}
+
+package IkiWiki::RPC::XML;
+
+sub getvar ($$$) { #{{{
+ my $plugin=shift;
+ my $varname="IkiWiki::".shift;
+ my $key=shift;
+
+ no strict 'refs';
+ my $ret=$varname->{$key};
+ use strict 'refs';
+ return $ret;
+} #}}}
+
+sub setvar ($$$;@) { #{{{
+ my $plugin=shift;
+ my $varname="IkiWiki::".shift;
+ my $key=shift;
+
+ no strict 'refs';
+ my $ret=$varname->{$key}=@_;
+ use strict 'refs';
+ return $ret;
+} #}}}
+
+sub inject ($@) { #{{{
+ # Bind a given perl function name to a particular RPC request.
+ my $plugin=shift;
+ my %params=@_;
+
+ if (! exists $params{name} || ! exists $params{call}) {
+ die "inject needs name and call parameters";
+ }
+ my $sub = sub {
+ IkiWiki::Plugin::external::rpc_call($plugin, $params{call}, @_)
+ };
+ eval qq{*$params{name}=\$sub};
+ return 1;
+} #}}}
+
+sub hook ($@) { #{{{
+ # the call parameter is a function name to call, since XML RPC
+ # cannot pass a function reference
+ my $plugin=shift;
+ my %params=@_;
+
+ my $callback=$params{call};
+ delete $params{call};
+
+ IkiWiki::hook(%params, call => sub {
+ IkiWiki::Plugin::external::rpc_call($plugin, $callback, @_)
+ });
+} #}}}
+
+sub pagespec_match ($@) { #{{{
+ # convert pagespec_match's return object into a XML RPC boolean
+ my $plugin=shift;
+
+ return RPC::XML::boolean->new(0 + IkiWiki::pagespec_march(@_));
+} #}}}
+
+1