summaryrefslogtreecommitdiff
path: root/IkiWiki/Plugin/external.pm
blob: ec91c79db2149a1b58a63f34aedc4b83b7a76a4a (plain)
  1. #!/usr/bin/perl
  2. # Support for external plugins written in other languages.
  3. # Communication via XML RPC to a pipe.
  4. # See externaldemo for an example of a plugin that uses this.
  5. package IkiWiki::Plugin::external;
  6. use warnings;
  7. use strict;
  8. use IkiWiki 3.00;
  9. use RPC::XML;
  10. use IPC::Open2;
  11. use IO::Handle;
  12. my %plugins;
  13. sub import {
  14. my $self=shift;
  15. my $plugin=shift;
  16. return unless defined $plugin;
  17. my ($plugin_read, $plugin_write);
  18. my $pid = open2($plugin_read, $plugin_write,
  19. IkiWiki::possibly_foolish_untaint($plugin));
  20. # open2 doesn't respect "use open ':utf8'"
  21. binmode($plugin_read, ':utf8');
  22. binmode($plugin_write, ':utf8');
  23. $plugins{$plugin}={in => $plugin_read, out => $plugin_write, pid => $pid,
  24. accum => ""};
  25. $RPC::XML::ENCODING="utf-8";
  26. rpc_call($plugins{$plugin}, "import");
  27. }
  28. sub rpc_write ($$) {
  29. my $fh=shift;
  30. my $string=shift;
  31. $fh->print($string."\n");
  32. $fh->flush;
  33. }
  34. sub rpc_call ($$;@) {
  35. my $plugin=shift;
  36. my $command=shift;
  37. # send the command
  38. my $req=RPC::XML::request->new($command, @_);
  39. rpc_write($plugin->{out}, $req->as_string);
  40. # process incoming rpc until a result is available
  41. while ($_ = $plugin->{in}->getline) {
  42. $plugin->{accum}.=$_;
  43. while ($plugin->{accum} =~ /^\s*(<\?xml\s.*?<\/(?:methodCall|methodResponse)>)\n(.*)/s) {
  44. $plugin->{accum}=$2;
  45. my $parser;
  46. eval q{
  47. use RPC::XML::ParserFactory;
  48. $parser = RPC::XML::ParserFactory->new;
  49. };
  50. if ($@) {
  51. # old interface
  52. eval q{
  53. use RPC::XML::Parser;
  54. $parser = RPC::XML::Parser->new;
  55. };
  56. }
  57. my $r=$parser->parse($1);
  58. error("XML RPC parser failure: $r") unless ref $r;
  59. if ($r->isa('RPC::XML::response')) {
  60. my $value=$r->value;
  61. if ($r->is_fault($value)) {
  62. # throw the error as best we can
  63. print STDERR $value->string."\n";
  64. return "";
  65. }
  66. elsif ($value->isa('RPC::XML::array')) {
  67. return @{$value->value};
  68. }
  69. elsif ($value->isa('RPC::XML::struct')) {
  70. my %hash=%{$value->value};
  71. # XML-RPC v1 does not allow for
  72. # nil/null/None/undef values to be
  73. # transmitted. The <nil/> extension
  74. # is the right fix, but for
  75. # back-compat, let external plugins send
  76. # a hash with one key "null" pointing
  77. # to an empty string.
  78. if (exists $hash{null} &&
  79. $hash{null} eq "" &&
  80. int(keys(%hash)) == 1) {
  81. return undef;
  82. }
  83. return %hash;
  84. }
  85. else {
  86. return $value->value;
  87. }
  88. }
  89. my $name=$r->name;
  90. my @args=map { $_->value } @{$r->args};
  91. # When dispatching a function, first look in
  92. # IkiWiki::RPC::XML. This allows overriding
  93. # IkiWiki functions with RPC friendly versions.
  94. my $ret;
  95. if (exists $IkiWiki::RPC::XML::{$name}) {
  96. $ret=$IkiWiki::RPC::XML::{$name}($plugin, @args);
  97. }
  98. elsif (exists $IkiWiki::{$name}) {
  99. $ret=$IkiWiki::{$name}(@args);
  100. }
  101. else {
  102. error("XML RPC call error, unknown function: $name");
  103. }
  104. # XML-RPC v1 does not allow for nil/null/None/undef
  105. # values to be transmitted, so until XML::RPC::Parser
  106. # honours v2 (<nil/>), send a hash with one key "null"
  107. # pointing to an empty string.
  108. if (! defined $ret) {
  109. $ret={"null" => ""};
  110. }
  111. my $string=eval { RPC::XML::response->new($ret)->as_string };
  112. if ($@ && ref $ret) {
  113. # One common reason for serialisation to
  114. # fail is a complex return type that cannot
  115. # be represented as an XML RPC response.
  116. # Handle this case by just returning 1.
  117. $string=eval { RPC::XML::response->new(1)->as_string };
  118. }
  119. if ($@) {
  120. error("XML response serialisation failed: $@");
  121. }
  122. rpc_write($plugin->{out}, $string);
  123. }
  124. }
  125. return undef;
  126. }
  127. package IkiWiki::RPC::XML;
  128. use Memoize;
  129. sub getvar ($$$) {
  130. my $plugin=shift;
  131. my $varname="IkiWiki::".shift;
  132. my $key=shift;
  133. no strict 'refs';
  134. my $ret=$varname->{$key};
  135. use strict 'refs';
  136. return $ret;
  137. }
  138. sub setvar ($$$;@) {
  139. my $plugin=shift;
  140. my $varname="IkiWiki::".shift;
  141. my $key=shift;
  142. my $value=shift;
  143. no strict 'refs';
  144. my $ret=$varname->{$key}=$value;
  145. use strict 'refs';
  146. return $ret;
  147. }
  148. sub getstate ($$$$) {
  149. my $plugin=shift;
  150. my $page=shift;
  151. my $id=shift;
  152. my $key=shift;
  153. return $IkiWiki::pagestate{$page}{$id}{$key};
  154. }
  155. sub setstate ($$$$;@) {
  156. my $plugin=shift;
  157. my $page=shift;
  158. my $id=shift;
  159. my $key=shift;
  160. my $value=shift;
  161. return $IkiWiki::pagestate{$page}{$id}{$key}=$value;
  162. }
  163. sub getargv ($) {
  164. my $plugin=shift;
  165. return \@ARGV;
  166. }
  167. sub setargv ($@) {
  168. my $plugin=shift;
  169. my $array=shift;
  170. @ARGV=@$array;
  171. }
  172. sub inject ($@) {
  173. # Bind a given perl function name to a particular RPC request.
  174. my $plugin=shift;
  175. my %params=@_;
  176. if (! exists $params{name} || ! exists $params{call}) {
  177. die "inject needs name and call parameters";
  178. }
  179. my $sub = sub {
  180. IkiWiki::Plugin::external::rpc_call($plugin, $params{call}, @_)
  181. };
  182. $sub=memoize($sub) if $params{memoize};
  183. # This will add it to the symbol table even if not present.
  184. no warnings;
  185. eval qq{*$params{name}=\$sub};
  186. use warnings;
  187. # This will ensure that everywhere it was exported to sees
  188. # the injected version.
  189. IkiWiki::inject(name => $params{name}, call => $sub);
  190. return 1;
  191. }
  192. sub hook ($@) {
  193. # the call parameter is a function name to call, since XML RPC
  194. # cannot pass a function reference
  195. my $plugin=shift;
  196. my %params=@_;
  197. my $callback=$params{call};
  198. delete $params{call};
  199. IkiWiki::hook(%params, call => sub {
  200. IkiWiki::Plugin::external::rpc_call($plugin, $callback, @_);
  201. });
  202. }
  203. sub pagespec_match ($@) {
  204. # convert return object into a XML RPC boolean
  205. my $plugin=shift;
  206. my $page=shift;
  207. my $spec=shift;
  208. return RPC::XML::boolean->new(0 + IkiWiki::pagespec_match(
  209. $page, $spec, @_));
  210. }
  211. 1