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