move recentchanges link enabling into a pagetemplate hook
[ikiwiki] / IkiWiki / Plugin / external.pm
1 #!/usr/bin/perl
2 # Support for external plugins written in other languages.
3 # Communication via XML RPC a pipe.
4 # See externaldemo for an example of a plugin that uses this.
5 package IkiWiki::Plugin::external;
6
7 use warnings;
8 use strict;
9 use IkiWiki 2.00;
10 use RPC::XML;
11 use RPC::XML::Parser;
12 use IPC::Open2;
13 use IO::Handle;
14
15 my %plugins;
16
17 sub import { #{{{
18         my $self=shift;
19         my $plugin=shift;
20         return unless defined $plugin;
21
22         my ($plugin_read, $plugin_write);
23         my $pid = open2($plugin_read, $plugin_write,
24                 IkiWiki::possibly_foolish_untaint($plugin));
25
26         # open2 doesn't respect "use open ':utf8'"
27         binmode($plugin_read, ':utf8');
28         binmode($plugin_write, ':utf8');
29
30         $plugins{$plugin}={in => $plugin_read, out => $plugin_write, pid => $pid,
31                 accum => ""};
32
33         rpc_call($plugins{$plugin}, "import");
34 } #}}}
35
36 sub rpc_write ($$) { #{{{
37         my $fh=shift;
38         my $string=shift;
39
40         $fh->print($string."\n");
41         $fh->flush;
42 } #}}}
43
44 sub rpc_call ($$;@) { #{{{
45         my $plugin=shift;
46         my $command=shift;
47
48         # send the command
49         my $req=RPC::XML::request->new($command, @_);
50         rpc_write($plugin->{out}, $req->as_string);
51
52         # process incoming rpc until a result is available
53         while ($_ = $plugin->{in}->getline) {
54                 $plugin->{accum}.=$_;
55                 while ($plugin->{accum} =~ /^\s*(<\?xml\s.*?<\/(?:methodCall|methodResponse)>)\n(.*)/s) {
56                         $plugin->{accum}=$2;
57                         my $r = RPC::XML::Parser->new->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 ($value->isa('RPC::XML::array')) {
62                                         return @{$value->value};
63                                 }
64                                 elsif ($value->isa('RPC::XML::struct')) {
65                                         return %{$value->value};
66                                 }
67                                 elsif ($value->isa('RPC::XML::fault')) {
68                                         die $value->string;
69                                 }
70                                 else {
71                                         return $value->value;
72                                 }
73                         }
74
75                         my $name=$r->name;
76                         my @args=map { $_->value } @{$r->args};
77
78                         # When dispatching a function, first look in 
79                         # IkiWiki::RPC::XML. This allows overriding
80                         # IkiWiki functions with RPC friendly versions.
81                         my $ret;
82                         if (exists $IkiWiki::RPC::XML::{$name}) {
83                                 $ret=$IkiWiki::RPC::XML::{$name}($plugin, @args);
84                         }
85                         elsif (exists $IkiWiki::{$name}) {
86                                 $ret=$IkiWiki::{$name}(@args);
87                         }
88                         else {
89                                 error("XML RPC call error, unknown function: $name");
90                         }
91
92                         my $string=eval { RPC::XML::response->new($ret)->as_string };
93                         if ($@ && ref $ret) {
94                                 # One common reason for serialisation to
95                                 # fail is a complex return type that cannot
96                                 # be represented as an XML RPC response.
97                                 # Handle this case by just returning 1.
98                                 $string=eval { RPC::XML::response->new(1)->as_string };
99                         }
100                         if ($@) {
101                                 error("XML response serialisation failed: $@");
102                         }
103                         rpc_write($plugin->{out}, $string);
104                 }
105         }
106
107         return undef;
108 } #}}}
109
110 package IkiWiki::RPC::XML;
111 use Memoize;
112
113 sub getvar ($$$) { #{{{
114         my $plugin=shift;
115         my $varname="IkiWiki::".shift;
116         my $key=shift;
117
118         no strict 'refs';
119         my $ret=$varname->{$key};
120         use strict 'refs';
121         return $ret;
122 } #}}}
123
124 sub setvar ($$$;@) { #{{{
125         my $plugin=shift;
126         my $varname="IkiWiki::".shift;
127         my $key=shift;
128
129         no strict 'refs';
130         my $ret=$varname->{$key}=@_;
131         use strict 'refs';
132         return $ret;
133 } #}}}
134
135 sub getstate ($$$$) { #{{{
136         my $plugin=shift;
137         my $page=shift;
138         my $id=shift;
139         my $key=shift;
140
141         return $IkiWiki::pagestate{$page}{$id}{$key};
142 } #}}}
143
144 sub setstate ($$$$;@) { #{{{
145         my $plugin=shift;
146         my $page=shift;
147         my $id=shift;
148         my $key=shift;
149
150         return $IkiWiki::pagestate{$page}{$id}{$key}=@_;
151 } #}}}
152
153 sub inject ($@) { #{{{
154         # Bind a given perl function name to a particular RPC request.
155         my $plugin=shift;
156         my %params=@_;
157
158         if (! exists $params{name} || ! exists $params{call}) {
159                 die "inject needs name and call parameters";
160         }
161         my $sub = sub {
162                 IkiWiki::Plugin::external::rpc_call($plugin, $params{call}, @_)
163         };
164         eval qq{*$params{name}=\$sub};
165         memoize($params{name}) if $params{memoize};
166         return 1;
167 } #}}}
168
169 sub hook ($@) { #{{{
170         # the call parameter is a function name to call, since XML RPC
171         # cannot pass a function reference
172         my $plugin=shift;
173         my %params=@_;
174
175         my $callback=$params{call};
176         delete $params{call};
177
178         IkiWiki::hook(%params, call => sub {
179                 IkiWiki::Plugin::external::rpc_call($plugin, $callback, @_)
180         });
181 } #}}}
182
183 sub pagespec_match ($@) { #{{{
184         # convert pagespec_match's return object into a XML RPC boolean
185         my $plugin=shift;
186
187         return RPC::XML::boolean->new(0 + IkiWiki::pagespec_march(@_));
188 } #}}}
189
190 1