1<?php
2/**
3 * Zend Framework
4 *
5 * LICENSE
6 *
7 * This source file is subject to the new BSD license that is bundled
8 * with this package in the file LICENSE.txt.
9 * It is also available through the world-wide-web at this URL:
10 * http://framework.zend.com/license/new-bsd
11 * If you did not receive a copy of the license and are unable to
12 * obtain it through the world-wide-web, please send an email
13 * to license@zend.com so we can send you a copy immediately.
14 *
15 * @category   Zend
16 * @package    Zend_XmlRpc
17 * @subpackage Server
18 * @copyright  Copyright (c) 2005-2011 Zend Technologies USA Inc. (http://www.zend.com)
19 * @license    http://framework.zend.com/license/new-bsd     New BSD License
20 * @version    $Id: Server.php 23775 2011-03-01 17:25:24Z ralph $
21 */
22
23/**
24 * Extends Zend_Server_Abstract
25 */
26require_once 'Zend/Server/Abstract.php';
27
28/**
29 * XMLRPC Request
30 */
31require_once 'Zend/XmlRpc/Request.php';
32
33/**
34 * XMLRPC Response
35 */
36require_once 'Zend/XmlRpc/Response.php';
37
38/**
39 * XMLRPC HTTP Response
40 */
41require_once 'Zend/XmlRpc/Response/Http.php';
42
43/**
44 * XMLRPC server fault class
45 */
46require_once 'Zend/XmlRpc/Server/Fault.php';
47
48/**
49 * XMLRPC server system methods class
50 */
51require_once 'Zend/XmlRpc/Server/System.php';
52
53/**
54 * Convert PHP to and from xmlrpc native types
55 */
56require_once 'Zend/XmlRpc/Value.php';
57
58/**
59 * Reflection API for function/method introspection
60 */
61require_once 'Zend/Server/Reflection.php';
62
63/**
64 * Zend_Server_Reflection_Function_Abstract
65 */
66require_once 'Zend/Server/Reflection/Function/Abstract.php';
67
68/**
69 * Specifically grab the Zend_Server_Reflection_Method for manually setting up
70 * system.* methods and handling callbacks in {@link loadFunctions()}.
71 */
72require_once 'Zend/Server/Reflection/Method.php';
73
74/**
75 * An XML-RPC server implementation
76 *
77 * Example:
78 * <code>
79 * require_once 'Zend/XmlRpc/Server.php';
80 * require_once 'Zend/XmlRpc/Server/Cache.php';
81 * require_once 'Zend/XmlRpc/Server/Fault.php';
82 * require_once 'My/Exception.php';
83 * require_once 'My/Fault/Observer.php';
84 *
85 * // Instantiate server
86 * $server = new Zend_XmlRpc_Server();
87 *
88 * // Allow some exceptions to report as fault responses:
89 * Zend_XmlRpc_Server_Fault::attachFaultException('My_Exception');
90 * Zend_XmlRpc_Server_Fault::attachObserver('My_Fault_Observer');
91 *
92 * // Get or build dispatch table:
93 * if (!Zend_XmlRpc_Server_Cache::get($filename, $server)) {
94 *     require_once 'Some/Service/Class.php';
95 *     require_once 'Another/Service/Class.php';
96 *
97 *     // Attach Some_Service_Class in 'some' namespace
98 *     $server->setClass('Some_Service_Class', 'some');
99 *
100 *     // Attach Another_Service_Class in 'another' namespace
101 *     $server->setClass('Another_Service_Class', 'another');
102 *
103 *     // Create dispatch table cache file
104 *     Zend_XmlRpc_Server_Cache::save($filename, $server);
105 * }
106 *
107 * $response = $server->handle();
108 * echo $response;
109 * </code>
110 *
111 * @category   Zend
112 * @package    Zend_XmlRpc
113 * @subpackage Server
114 * @copyright  Copyright (c) 2005-2011 Zend Technologies USA Inc. (http://www.zend.com)
115 * @license    http://framework.zend.com/license/new-bsd     New BSD License
116 */
117class Zend_XmlRpc_Server extends Zend_Server_Abstract
118{
119    /**
120     * Character encoding
121     * @var string
122     */
123    protected $_encoding = 'UTF-8';
124
125    /**
126     * Request processed
127     * @var null|Zend_XmlRpc_Request
128     */
129    protected $_request = null;
130
131    /**
132     * Class to use for responses; defaults to {@link Zend_XmlRpc_Response_Http}
133     * @var string
134     */
135    protected $_responseClass = 'Zend_XmlRpc_Response_Http';
136
137    /**
138     * Dispatch table of name => method pairs
139     * @var Zend_Server_Definition
140     */
141    protected $_table;
142
143    /**
144     * PHP types => XML-RPC types
145     * @var array
146     */
147    protected $_typeMap = array(
148        'i4'                         => 'i4',
149        'int'                        => 'int',
150        'integer'                    => 'int',
151        'Zend_Crypt_Math_BigInteger' => 'i8',
152        'i8'                         => 'i8',
153        'ex:i8'                      => 'i8',
154        'double'                     => 'double',
155        'float'                      => 'double',
156        'real'                       => 'double',
157        'boolean'                    => 'boolean',
158        'bool'                       => 'boolean',
159        'true'                       => 'boolean',
160        'false'                      => 'boolean',
161        'string'                     => 'string',
162        'str'                        => 'string',
163        'base64'                     => 'base64',
164        'dateTime.iso8601'           => 'dateTime.iso8601',
165        'date'                       => 'dateTime.iso8601',
166        'time'                       => 'dateTime.iso8601',
167        'Zend_Date'                  => 'dateTime.iso8601',
168        'DateTime'                   => 'dateTime.iso8601',
169        'array'                      => 'array',
170        'struct'                     => 'struct',
171        'null'                       => 'nil',
172        'nil'                        => 'nil',
173        'ex:nil'                     => 'nil',
174        'void'                       => 'void',
175        'mixed'                      => 'struct',
176    );
177
178    /**
179     * Send arguments to all methods or just constructor?
180     *
181     * @var bool
182     */
183    protected $_sendArgumentsToAllMethods = true;
184
185    /**
186     * Constructor
187     *
188     * Creates system.* methods.
189     *
190     * @return void
191     */
192    public function __construct()
193    {
194        $this->_table = new Zend_Server_Definition();
195        $this->_registerSystemMethods();
196    }
197
198    /**
199     * Proxy calls to system object
200     *
201     * @param  string $method
202     * @param  array $params
203     * @return mixed
204     * @throws Zend_XmlRpc_Server_Exception
205     */
206    public function __call($method, $params)
207    {
208        $system = $this->getSystem();
209        if (!method_exists($system, $method)) {
210            require_once 'Zend/XmlRpc/Server/Exception.php';
211            throw new Zend_XmlRpc_Server_Exception('Unknown instance method called on server: '.$method);
212        }
213        return call_user_func_array(array($system, $method), $params);
214    }
215
216    /**
217     * Attach a callback as an XMLRPC method
218     *
219     * Attaches a callback as an XMLRPC method, prefixing the XMLRPC method name
220     * with $namespace, if provided. Reflection is done on the callback's
221     * docblock to create the methodHelp for the XMLRPC method.
222     *
223     * Additional arguments to pass to the function at dispatch may be passed;
224     * any arguments following the namespace will be aggregated and passed at
225     * dispatch time.
226     *
227     * @param string|array $function Valid callback
228     * @param string $namespace Optional namespace prefix
229     * @return void
230     * @throws Zend_XmlRpc_Server_Exception
231     */
232    public function addFunction($function, $namespace = '')
233    {
234        if (!is_string($function) && !is_array($function)) {
235            require_once 'Zend/XmlRpc/Server/Exception.php';
236            throw new Zend_XmlRpc_Server_Exception('Unable to attach function; invalid', 611);
237        }
238
239        $argv = null;
240        if (2 < func_num_args()) {
241            $argv = func_get_args();
242            $argv = array_slice($argv, 2);
243        }
244
245        $function = (array) $function;
246        foreach ($function as $func) {
247            if (!is_string($func) || !function_exists($func)) {
248                require_once 'Zend/XmlRpc/Server/Exception.php';
249                throw new Zend_XmlRpc_Server_Exception('Unable to attach function; invalid', 611);
250            }
251            $reflection = Zend_Server_Reflection::reflectFunction($func, $argv, $namespace);
252            $this->_buildSignature($reflection);
253        }
254    }
255
256    /**
257     * Attach class methods as XMLRPC method handlers
258     *
259     * $class may be either a class name or an object. Reflection is done on the
260     * class or object to determine the available public methods, and each is
261     * attached to the server as an available method; if a $namespace has been
262     * provided, that namespace is used to prefix the XMLRPC method names.
263     *
264     * Any additional arguments beyond $namespace will be passed to a method at
265     * invocation.
266     *
267     * @param string|object $class
268     * @param string $namespace Optional
269     * @param mixed $argv Optional arguments to pass to methods
270     * @return void
271     * @throws Zend_XmlRpc_Server_Exception on invalid input
272     */
273    public function setClass($class, $namespace = '', $argv = null)
274    {
275        if (is_string($class) && !class_exists($class)) {
276            require_once 'Zend/XmlRpc/Server/Exception.php';
277            throw new Zend_XmlRpc_Server_Exception('Invalid method class', 610);
278        }
279
280        $argv = null;
281        if (2 < func_num_args()) {
282            $argv = func_get_args();
283            $argv = array_slice($argv, 2);
284        }
285
286        $dispatchable = Zend_Server_Reflection::reflectClass($class, $argv, $namespace);
287        foreach ($dispatchable->getMethods() as $reflection) {
288            $this->_buildSignature($reflection, $class);
289        }
290    }
291
292    /**
293     * Raise an xmlrpc server fault
294     *
295     * @param string|Exception $fault
296     * @param int $code
297     * @return Zend_XmlRpc_Server_Fault
298     */
299    public function fault($fault = null, $code = 404)
300    {
301        if (!$fault instanceof Exception) {
302            $fault = (string) $fault;
303            if (empty($fault)) {
304                $fault = 'Unknown Error';
305            }
306            require_once 'Zend/XmlRpc/Server/Exception.php';
307            $fault = new Zend_XmlRpc_Server_Exception($fault, $code);
308        }
309
310        return Zend_XmlRpc_Server_Fault::getInstance($fault);
311    }
312
313    /**
314     * Handle an xmlrpc call
315     *
316     * @param Zend_XmlRpc_Request $request Optional
317     * @return Zend_XmlRpc_Response|Zend_XmlRpc_Fault
318     */
319    public function handle($request = false)
320    {
321        // Get request
322        if ((!$request || !$request instanceof Zend_XmlRpc_Request)
323            && (null === ($request = $this->getRequest()))
324        ) {
325            require_once 'Zend/XmlRpc/Request/Http.php';
326            $request = new Zend_XmlRpc_Request_Http();
327            $request->setEncoding($this->getEncoding());
328        }
329
330        $this->setRequest($request);
331
332        if ($request->isFault()) {
333            $response = $request->getFault();
334        } else {
335            try {
336                $response = $this->_handle($request);
337            } catch (Exception $e) {
338                $response = $this->fault($e);
339            }
340        }
341
342        // Set output encoding
343        $response->setEncoding($this->getEncoding());
344
345        return $response;
346    }
347
348    /**
349     * Load methods as returned from {@link getFunctions}
350     *
351     * Typically, you will not use this method; it will be called using the
352     * results pulled from {@link Zend_XmlRpc_Server_Cache::get()}.
353     *
354     * @param  array|Zend_Server_Definition $definition
355     * @return void
356     * @throws Zend_XmlRpc_Server_Exception on invalid input
357     */
358    public function loadFunctions($definition)
359    {
360        if (!is_array($definition) && (!$definition instanceof Zend_Server_Definition)) {
361            if (is_object($definition)) {
362                $type = get_class($definition);
363            } else {
364                $type = gettype($definition);
365            }
366            require_once 'Zend/XmlRpc/Server/Exception.php';
367            throw new Zend_XmlRpc_Server_Exception('Unable to load server definition; must be an array or Zend_Server_Definition, received '.$type, 612);
368        }
369
370        $this->_table->clearMethods();
371        $this->_registerSystemMethods();
372
373        if ($definition instanceof Zend_Server_Definition) {
374            $definition = $definition->getMethods();
375        }
376
377        foreach ($definition as $key => $method) {
378            if ('system.' == substr($key, 0, 7)) {
379                continue;
380            }
381            $this->_table->addMethod($method, $key);
382        }
383    }
384
385    /**
386     * Set encoding
387     *
388     * @param string $encoding
389     * @return Zend_XmlRpc_Server
390     */
391    public function setEncoding($encoding)
392    {
393        $this->_encoding = $encoding;
394        Zend_XmlRpc_Value::setEncoding($encoding);
395        return $this;
396    }
397
398    /**
399     * Retrieve current encoding
400     *
401     * @return string
402     */
403    public function getEncoding()
404    {
405        return $this->_encoding;
406    }
407
408    /**
409     * Do nothing; persistence is handled via {@link Zend_XmlRpc_Server_Cache}
410     *
411     * @param  mixed $mode
412     * @return void
413     */
414    public function setPersistence($mode)
415    {
416    }
417
418    /**
419     * Set the request object
420     *
421     * @param string|Zend_XmlRpc_Request $request
422     * @return Zend_XmlRpc_Server
423     * @throws Zend_XmlRpc_Server_Exception on invalid request class or object
424     */
425    public function setRequest($request)
426    {
427        if (is_string($request) && class_exists($request)) {
428            $request = new $request();
429            if (!$request instanceof Zend_XmlRpc_Request) {
430                require_once 'Zend/XmlRpc/Server/Exception.php';
431                throw new Zend_XmlRpc_Server_Exception('Invalid request class');
432            }
433            $request->setEncoding($this->getEncoding());
434        } elseif (!$request instanceof Zend_XmlRpc_Request) {
435            require_once 'Zend/XmlRpc/Server/Exception.php';
436            throw new Zend_XmlRpc_Server_Exception('Invalid request object');
437        }
438
439        $this->_request = $request;
440        return $this;
441    }
442
443    /**
444     * Return currently registered request object
445     *
446     * @return null|Zend_XmlRpc_Request
447     */
448    public function getRequest()
449    {
450        return $this->_request;
451    }
452
453    /**
454     * Set the class to use for the response
455     *
456     * @param string $class
457     * @return boolean True if class was set, false if not
458     */
459    public function setResponseClass($class)
460    {
461        if (!class_exists($class) or
462            ($c = new ReflectionClass($class) and !$c->isSubclassOf('Zend_XmlRpc_Response'))) {
463
464            require_once 'Zend/XmlRpc/Server/Exception.php';
465            throw new Zend_XmlRpc_Server_Exception('Invalid response class');
466        }
467        $this->_responseClass = $class;
468        return true;
469    }
470
471    /**
472     * Retrieve current response class
473     *
474     * @return string
475     */
476    public function getResponseClass()
477    {
478        return $this->_responseClass;
479    }
480
481    /**
482     * Retrieve dispatch table
483     *
484     * @return Zend_Server_Definition
485     */
486    public function getDispatchTable()
487    {
488        return $this->_table;
489    }
490
491    /**
492     * Returns a list of registered methods
493     *
494     * Returns an array of dispatchables (Zend_Server_Reflection_Function,
495     * _Method, and _Class items).
496     *
497     * @return array
498     */
499    public function getFunctions()
500    {
501        return $this->_table->toArray();
502    }
503
504    /**
505     * Retrieve system object
506     *
507     * @return Zend_XmlRpc_Server_System
508     */
509    public function getSystem()
510    {
511        return $this->_system;
512    }
513
514    /**
515     * Send arguments to all methods?
516     *
517     * If setClass() is used to add classes to the server, this flag defined
518     * how to handle arguments. If set to true, all methods including constructor
519     * will receive the arguments. If set to false, only constructor will receive the
520     * arguments
521     * @param boolean $flag
522     */
523    public function sendArgumentsToAllMethods($flag = null)
524    {
525        if ($flag === null) {
526            return $this->_sendArgumentsToAllMethods;
527        }
528
529        $this->_sendArgumentsToAllMethods = (bool) $flag;
530        return $this;
531    }
532
533    /**
534     * Map PHP type to XML-RPC type
535     *
536     * @param  string $type
537     * @return string
538     */
539    protected function _fixType($type)
540    {
541        if (isset($this->_typeMap[$type])) {
542            return $this->_typeMap[$type];
543        }
544        return 'void';
545    }
546
547    /**
548     * Handle an xmlrpc call (actual work)
549     *
550     * @param Zend_XmlRpc_Request $request
551     * @return Zend_XmlRpc_Response
552     * @throws Zend_XmlRpcServer_Exception|Exception
553     * Zend_XmlRpcServer_Exceptions are thrown for internal errors; otherwise,
554     * any other exception may be thrown by the callback
555     */
556    protected function _handle(Zend_XmlRpc_Request $request)
557    {
558        $method = $request->getMethod();
559
560        // Check for valid method
561        if (!$this->_table->hasMethod($method)) {
562            require_once 'Zend/XmlRpc/Server/Exception.php';
563            throw new Zend_XmlRpc_Server_Exception('Method "'.$method.'" does not exist', 620);
564        }
565
566        $info     = $this->_table->getMethod($method);
567        $params   = $request->getParams();
568        $argv     = $info->getInvokeArguments();
569        if (0 < count($argv) and $this->sendArgumentsToAllMethods()) {
570            $params = array_merge($params, $argv);
571        }
572
573        // Check calling parameters against signatures
574        $matched    = false;
575        $sigCalled  = $request->getTypes();
576
577        $sigLength  = count($sigCalled);
578        $paramsLen  = count($params);
579        if ($sigLength < $paramsLen) {
580            for ($i = $sigLength; $i < $paramsLen; ++$i) {
581                $xmlRpcValue = Zend_XmlRpc_Value::getXmlRpcValue($params[$i]);
582                $sigCalled[] = $xmlRpcValue->getType();
583            }
584        }
585
586        $signatures = $info->getPrototypes();
587        foreach ($signatures as $signature) {
588            $sigParams = $signature->getParameters();
589            if ($sigCalled === $sigParams) {
590                $matched = true;
591                break;
592            }
593        }
594        if (!$matched) {
595            require_once 'Zend/XmlRpc/Server/Exception.php';
596            throw new Zend_XmlRpc_Server_Exception('Calling parameters do not match signature', 623);
597        }
598
599        $return        = $this->_dispatch($info, $params);
600        $responseClass = $this->getResponseClass();
601        return new $responseClass($return);
602    }
603
604    /**
605     * Register system methods with the server
606     *
607     * @return void
608     */
609    protected function _registerSystemMethods()
610    {
611        $system = new Zend_XmlRpc_Server_System($this);
612        $this->_system = $system;
613        $this->setClass($system, 'system');
614    }
615}
616