-
Notifications
You must be signed in to change notification settings - Fork 26
/
ripcord_server.php
417 lines (401 loc) · 14 KB
/
ripcord_server.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
<?php
/**
* Ripcord is an easy to use XML-RPC library for PHP.
* @package Ripcord
* @author Auke van Slooten <auke@muze.nl>
* @copyright Copyright (C) 2010, Muze <www.muze.nl>
* @license http://opensource.org/licenses/gpl-3.0.html GNU Public License
* @version Ripcord 0.9 - PHP 5
*/
/**
* Includes the static ripcord factory class and exceptions
*/
require_once(dirname(__FILE__).'/ripcord.php');
/**
* This class implements the Ripcord server. It is an OO wrapper around PHP's XML-RPC methods, with some added features.
* You can create an XML-RPC (or Simple RPC or a simple SOAP 1.1) server by defining a class with public methods and passing
* an object (or array of objects) of this class to the constructor of Ripcord_Server. Then simply call the run() method.
*
* A basic example:
* <code>
* <?php
* $myObject = new MyClass();
* $server = ripcord::server( $myObject );
* $server->run();
* ?>
* </code>
*
* An example with namespaces in the method names and a static class as rpc service.
* <code>
* <?php
* $myObject = new MyClass();
* $server = ripcord::server(
* array(
* 'namespace1' => $myObject,
* 'namespace2' => 'myOtherClass'
* )
* );
* $server->run();
* ?>
* </code>
*
* You don't need to instantiate a class to use it with Ripcord, in the above example 'myOtherClass' is the
* name of a PHP class to use. In addition you may also specify functions or methods directly, in any format
* that matches PHP's is_callable() criteria.
* @package Ripcord
*/
/*
TODO:
- create seperate interface for encoding / decoding requests
- create xmlrpc-epi class using xmlrpc_encode/decode for xml-rpc, simple-rpc and for now soap
- add json-rpc class (http://json-rpc.org/wiki/specification)
- pass list of protocol parsers/generators in the constructor of the server
- protocol must know how to handle the system.* methods
*/
class Ripcord_Server
{
/**
* Contains a reference to the Ripcord documentor object.
* @see Ripcord_Documentor
*/
private $documentor = null;
/**
* Contains a reference to the XML-RPC server created with xmlrpc_server_create.
*/
private $xmlrpc = null;
/**
* Contains a list of methods set for this server. Excludes the system.* methods automatically
* created by PHP's xmlrpc_server_create.
*/
private $methods = array();
/**
* Contains an array with outputOptions, used when calling methods on the xmlrpc server created with
* xmlrpc_server_create. These options can be overridden through the $options parameter of the
* Ripcord_Server constructor.
* @see Ripcord_Server::setOutputOption()
*/
private $outputOptions = array(
"output_type" => "xml",
"verbosity" => "pretty",
"escaping" => array("markup"),
"version" => "auto",
"encoding" => "utf-8"
);
/**
* Creates a new instance of the Ripcord server.
* @param mixed $services. Optional. An object or array of objects. The public methods in these objects will be exposed
* through the RPC server. If the services array has non-numeric keys, the key for each object will define its namespace.
* @param array $options. Optional. Allows you to override the default server settings. Accepted key names are:
* - 'documentor': allows you to specify an alternative HTML documentor class, or if set to false, no HTML documentor.
* - 'name' : The name of the server, used by the default HTML documentor.
* - 'css' : An url of a css file to link to in the HTML documentation.
* - 'wsdl' : The wsdl 1.0 description of this service (only usefull if you run the 'soap 1.1' version, or the 'auto' version
* - 'wsdl2' : The wsdl 2.0 description of this service
* In addition you can set any of the outputOptions for the xmlrpc server.
* @see Ripcord_Server::setOutputOption()
* @throws Ripcord_InvalidArgumentException (ripcord::unknownServiceType) when passed an incorrect service
* @throws Ripcord_ConfigurationException (ripcord::xmlrpcNotInstalled) when the xmlrpc extension in not available.
*/
function __construct($services = null, $options = null, $documentor = null)
{
if ( !function_exists( 'xmlrpc_server_create' ) )
{
throw new Ripcord_ConfigurationException('PHP XMLRPC library is not installed',
ripcord::xmlrpcNotInstalled );
}
libxml_disable_entity_loader(); // prevents XXE attacks
$this->xmlrpc = xmlrpc_server_create();
if (isset($services))
{
if (is_array($services))
{
foreach ($services as $serviceName => $service)
{
$this->addService($service, $serviceName);
}
} else {
$this->addService($services);
}
}
if ( isset($documentor) && is_object($documentor) ) {
$this->documentor = $documentor;
xmlrpc_server_register_introspection_callback( $this->xmlrpc,
array( $this->documentor, 'getIntrospectionXML') );
}
if ( isset($options) )
{
$this->outputOptions = array_merge($this->outputOptions, $options);
}
}
/**
* Allows you to add a service to the server after construction.
* @param object $service The object or class whose public methods must be added to the rpc server. May also be a function or method.
* @param string $serviceName Optional. The namespace for the methods.
* @throws Ripcord_InvalidArgumentException (ripcord::unknownServiceType) when passed an incorrect service
*/
public function addService($service, $serviceName = 0)
{
if ( is_object( $service ) )
{
$reflection = new ReflectionObject( $service );
}
else if ( is_string( $service ) && class_exists( $service ) )
{
$reflection = new ReflectionClass( $service );
}
else if ( is_callable( $service ) ) // method passed directly
{
$this->addMethod( $serviceName, $service );
return;
}
else
{
throw new Ripcord_InvalidArgumentException( 'Unknown service type ' . $serviceName,
ripcord::unknownServiceType );
}
if ( $serviceName && !is_numeric( $serviceName ) )
{
$serviceName .= '.';
}
else
{
$serviceName = '';
}
$methods = $reflection->getMethods();
if ( is_array( $methods ) )
{
foreach( $methods as $method )
{
if ( substr( $method->name, 0, 1 ) != '_'
&& !$method->isPrivate() && !$method->isProtected())
{
$rpcMethodName = $serviceName . $method->name;
$this->addMethod(
$rpcMethodName,
array( $service, $method->name )
);
}
}
}
}
/**
* Allows you to add a single method to the server after construction.
* @param string $name The name of the method as exposed through the rpc server
* @param callback $method The name of the method to call, or an array with classname or object and method name.
*/
public function addMethod($name, $method)
{
$this->methods[$name] = array(
'name' => $name,
'call' => $method
);
xmlrpc_server_register_method( $this->xmlrpc, $name, array( $this, 'call' ) );
}
/**
* Runs the rpc server. Automatically handles an incoming request.
*/
public function run()
{
if ($this->documentor) {
$this->documentor->setMethodData( $this->methods );
}
$request_xml = file_get_contents( 'php://input' );
if ( !$request_xml )
{
if ( ( $query = $_SERVER['QUERY_STRING'] )
&& isset($this->wsdl[$query]) && $this->wsdl[$query] )
{
header('Content-type: text/xml');
header('Access-Control-Allow-Origin: *');
$wsdl = $this->wsdl[$query];
header('Content-Length: '.strlen($wsdl) );
echo $wsdl;
}
else if ( $this->documentor )
{
header('Content-type: text/html; charset=' . $this->outputOptions['encoding']);
$this->documentor->handle( $this, $this->methods );
}
else
{
// FIXME: add check for json-rpc protocol, if set and none of the xml protocols are set, use that
header('Content-type: text/xml');
header('Access-Control-Allow-Origin: *');
$result = xmlrpc_encode_request(
null,
ripcord::fault( -1, 'No request xml found.' ),
$this->outputOptions
);
header('Content-Length: '.strlen( $result ) );
echo $result;
}
}
else
{
// FIXME: add check for the protocol of the request, could be json-rpc, then check if it is supported.
header('Content-type: text/xml');
header('Access-Control-Allow-Origin: *');
$result = $this->handle( $request_xml );
header('Content-Length: '.strlen($result) );
echo $result;
}
}
/**
* This method wraps around xmlrpc_decode_request, since it is borken in many ways. This wraps
* around all the ugliness needed to make it not dump core and not print expat warnings.
*/
private function parseRequest( $request_xml ) {
$xml = @simplexml_load_string($request_xml);
if (!$xml && !$xml->getNamespaces()) {
// FIXME: check for protocol json-rpc
//simplexml in combination with namespaces (soap) lets $xml evaluate to false
return xmlrpc_encode_request(
null,
ripcord::fault( -3, 'Invalid Method Call - Ripcord Server accepts only XML-RPC, SimpleRPC or SOAP 1.1 calls'),
$this->outputOptions
);
} else {
// prevent segmentation fault on incorrect xmlrpc request (without methodName)
$methodCall = $xml->xpath('//methodCall');
if ($methodCall) { //xml-rpc
$methodName = $xml->xpath('//methodName');
if (!$methodName) {
return xmlrpc_encode_request(
null,
ripcord::fault( -3, 'Invalid Method Call - No methodName given'),
$this->outputOptions
);
}
}
}
$method = null;
ob_start(); // xmlrpc_decode echo expat errors if the xml is not valid, can't stop it.
$params = xmlrpc_decode_request($request_xml, $method);
ob_end_clean(); // clean up any xml errors
return array( 'methodName' => $method, 'params' => $params );
}
/**
* This method implements the system.multiCall method without dumping core. The built-in method from the
* xmlrpc library dumps core when you have registered any php methods, fixed in php 5.3.2
*/
private function multiCall( $params = null ) {
if ( $params && is_array( $params ) )
{
$result = array();
$params = $params[0];
foreach ( $params as $param ) {
$method = $param['methodName'];
$args = $param['params'];
try {
// XML-RPC specification says that non-fault results must be in a single item array
$result[] = array( $this->call($method, $args) );
} catch( Exception $e) {
$result[] = ripcord::fault( $e->getCode(), $e->getMessage() );
}
}
$result = xmlrpc_encode_request( null, $result, $this->outputOptions );
} else {
$result = xmlrpc_encode_request(
null,
ripcord::fault( -2, 'Illegal or no params set for system.multiCall'),
$this->outputOptions
);
}
return $result;
}
/**
* Handles the given request xml
* @param string $request_xml The incoming request.
* @return string
*/
public function handle($request_xml)
{
$result = $this->parseRequest( $request_xml );
if (!$result || ripcord::isFault( $result ) )
{
return $result;
}
else
{
$method = $result['methodName'];
$params = $result['params'];
}
if ( $method == 'system.multiCall' || $method == 'system.multicall' ) {
// php's xml-rpc server (xmlrpc-epi) crashes on multicall, so handle it ourselves... fixed in php 5.3.2
$result = $this->multiCall( $params );
} else {
try {
$result = xmlrpc_server_call_method(
$this->xmlrpc, $request_xml, null, $this->outputOptions
);
} catch( Exception $e) {
$result = xmlrpc_encode_request(
null,
ripcord::fault( $e->getCode(), $e->getMessage() ),
$this->outputOptions
);
}
}
return $result;
}
/**
* Calls a method by its rpc name.
* @param string $method The rpc name of the method
* @param array $args The arguments to this method
* @return mixed
* @throws Ripcord_InvalidArgumentException (ripcord::cannotRecurse) when passed a recursive multiCall
* @throws Ripcord_BadMethodCallException (ripcord::methodNotFound) when the requested method isn't available.
*/
public function call( $method, $args = null )
{
if ( isset( $this->methods[$method] ) )
{
$call = $this->methods[$method]['call'];
return call_user_func_array( $call, $args);
} else {
if ( substr( $method, 0, 7 ) == 'system.' )
{
if ( $method == 'system.multiCall' ) {
throw new Ripcord_InvalidArgumentException(
'Cannot recurse system.multiCall', ripcord::cannotRecurse );
}
// system methods are handled internally by the xmlrpc server, so we've got to create a makebelieve request,
// there is no other way because of a badly designed API
$req = xmlrpc_encode_request( $method, $args, $this->outputOptions );
$result = xmlrpc_server_call_method( $this->xmlrpc, $req, null,
$this->outputOptions);
return xmlrpc_decode( $result );
} else {
throw new Ripcord_BadMethodCallException( 'Method '.$method.' not found.',
ripcord::methodNotFound );
}
}
}
/**
* Allows you to set specific output options of the server after construction.
* @param string $option The name of the option
* @param mixed $value The value of the option
* The options are:
* - output_type: Return data as either php native data or xml encoded. Can be either 'php' or 'xml'. 'xml' is the default.
* - verbosity: Determines the compactness of generated xml. Can be either 'no_white_space', 'newlines_only' or 'pretty'.
* 'pretty' is the default.
* - escaping: Determines how/whether to escape certain characters. 1 or more values are allowed. If multiple, they need
* to be specified as a sub-array. Options are: 'cdata', 'non-ascii', 'non-print' and 'markup'. Default is 'non-ascii',
* 'non-print' and 'markup'.
* - version: Version of the xml vocabulary to use. Currently, three are supported: 'xmlrpc', 'soap 1.1' and 'simple'. The
* keyword 'auto' is also recognized and tells the server to respond in whichever version the request cam in. 'auto' is
* the default.
* - encoding: The character encoding that the data is in. Can be any supported character encoding. Default is 'utf-8'.
*/
public function setOutputOption($option, $value)
{
if ( isset($this->outputOptions[$option]) )
{
$this->outputOptions[$option] = $value;
return true;
} else {
return false;
}
}
}
?>