From 0af5593122aafd543b72bcb6d40c73a382e46d8c Mon Sep 17 00:00:00 2001 From: Jun Pataleta Date: Fri, 20 Nov 2015 14:54:24 -0600 Subject: [PATCH] MDL-52209 webservice_xmlrpc: Remove Zend from webservice_xmlrpc --- webservice/xmlrpc/lib.php | 72 +++--- webservice/xmlrpc/locallib.php | 244 +++++++++++++----- .../xmlrpc/tests/fixtures/array_response.xml | 22 ++ .../xmlrpc/tests/fixtures/fault_response.xml | 21 ++ .../xmlrpc/tests/fixtures/value_response.xml | 10 + webservice/xmlrpc/tests/lib_test.php | 135 ++++++++++ 6 files changed, 409 insertions(+), 95 deletions(-) create mode 100644 webservice/xmlrpc/tests/fixtures/array_response.xml create mode 100644 webservice/xmlrpc/tests/fixtures/fault_response.xml create mode 100644 webservice/xmlrpc/tests/fixtures/value_response.xml create mode 100644 webservice/xmlrpc/tests/lib_test.php diff --git a/webservice/xmlrpc/lib.php b/webservice/xmlrpc/lib.php index 579735bc080..6cb9cf718b0 100644 --- a/webservice/xmlrpc/lib.php +++ b/webservice/xmlrpc/lib.php @@ -23,8 +23,6 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -require_once 'Zend/XmlRpc/Client.php'; - /** * Moodle XML-RPC client * @@ -34,10 +32,13 @@ require_once 'Zend/XmlRpc/Client.php'; * @copyright 2010 Jerome Mouneyrac * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class webservice_xmlrpc_client extends Zend_XmlRpc_Client { +class webservice_xmlrpc_client { - /** @var string server url e.g. https://yyyyy.com/server.php */ - private $serverurl; + /** @var moodle_url The XML-RPC server url. */ + protected $serverurl; + + /** @var string The token for the XML-RPC call. */ + protected $token; /** * Constructor @@ -46,22 +47,8 @@ class webservice_xmlrpc_client extends Zend_XmlRpc_Client { * @param string $token the token used to do the web service call */ public function __construct($serverurl, $token) { - global $CFG; - $this->serverurl = $serverurl; - $serverurl = $serverurl . '?wstoken=' . $token; - parent::__construct($serverurl); - if (!empty($CFG->proxyhost) && !is_proxybypass($serverurl)) { - $config = array( - 'adapter' => 'Zend_Http_Client_Adapter_Proxy', - 'proxy_host' => $CFG->proxyhost, - 'proxy_user' => !empty($CFG->proxyuser) ? $CFG->proxyuser : null, - 'proxy_pass' => !empty($CFG->proxypassword) ? $CFG->proxypassword : null - ); - if (!empty($CFG->proxyport)) { - $config['proxy_port'] = $CFG->proxyport; - } - $this->getHttpClient()->setConfig($config); - } + $this->serverurl = new moodle_url($serverurl); + $this->token = $token; } /** @@ -70,26 +57,47 @@ class webservice_xmlrpc_client extends Zend_XmlRpc_Client { * @param string $token the token used to do the web service call */ public function set_token($token) { - $this->_serverAddress = $this->serverurl . '?wstoken=' . $token; + $this->token = $token; } /** * Execute client WS request with token authentication * * @param string $functionname the function name - * @param array $params the parameters of the function - * @return mixed + * @param array $params An associative array containing the the parameters of the function being called. + * @return mixed The decoded XML RPC response. + * @throws moodle_exception */ - public function call($functionname, $params=array()) { - global $DB, $CFG; + public function call($functionname, $params = array()) { + if ($this->token) { + $this->serverurl->param('wstoken', $this->token); + } - //zend expects 0 based array with numeric indexes - $params = array_values($params); + // Set output options. + $outputoptions = array( + 'encoding' => 'utf-8' + ); - //traditional Zend soap client call (integrating the token into the URL) - $result = parent::call($functionname, $params); + // Encode the request. + $request = xmlrpc_encode_request($functionname, $params, $outputoptions); + + // Set the headers. + $headers = array( + 'Content-Length' => strlen($request), + 'Content-Type' => 'text/xml; charset=utf-8', + 'Host' => $this->serverurl->get_host(), + 'User-Agent' => 'Moodle XML-RPC Client/1.0', + ); + + // Get the response. + $response = download_file_content($this->serverurl, $headers, $request); + + // Decode the response. + $result = xmlrpc_decode($response); + if (is_array($result) && xmlrpc_is_fault($result)) { + throw new moodle_exception($result['faultString']); + } return $result; } - -} \ No newline at end of file +} diff --git a/webservice/xmlrpc/locallib.php b/webservice/xmlrpc/locallib.php index c99a21ab2f7..c97ed7d398b 100644 --- a/webservice/xmlrpc/locallib.php +++ b/webservice/xmlrpc/locallib.php @@ -24,53 +24,6 @@ */ require_once("$CFG->dirroot/webservice/lib.php"); -require_once 'Zend/XmlRpc/Server.php'; - -/** - * The Zend XMLRPC server but with a fault that return debuginfo - * - * @package webservice_xmlrpc - * @copyright 2011 Jerome Mouneyrac - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @since Moodle 2.2 - */ -class moodle_zend_xmlrpc_server extends Zend_XmlRpc_Server { - - /** - * Raise an xmlrpc server fault - * - * Moodle note: the difference with the Zend server is that we throw a plain PHP Exception - * with the debuginfo integrated to the exception message when DEBUG >= NORMAL - * - * @param string|Exception $fault - * @param int $code - * @return Zend_XmlRpc_Server_Fault - */ - public function fault($fault = null, $code = 404) - { - // Intercept any exceptions with debug info and transform it in Moodle exception. - if ($fault instanceof Exception) { - // Code php exception must be a long - // we obtain a hash of the errorcode, and then to get an integer hash. - $code = base_convert(md5($fault->errorcode), 16, 10); - // Code php exception being a long, it has a maximum number of digits. - // we strip the $code to 8 digits, and hope for no error code collisions. - // Collisions should be pretty rare, and if needed the client can retrieve - // the accurate errorcode from the last | in the exception message. - $code = substr($code, 0, 8); - // Add the debuginfo to the exception message if debuginfo must be returned. - if (debugging() and isset($fault->debuginfo)) { - $fault = new Exception($fault->getMessage() . ' | DEBUG INFO: ' . $fault->debuginfo - . ' | ERRORCODE: ' . $fault->errorcode, $code); - } else { - $fault = new Exception($fault->getMessage() - . ' | ERRORCODE: ' . $fault->errorcode, $code); - } - } - - return parent::fault($fault, $code); - } -} /** * XML-RPC service server implementation. @@ -80,7 +33,10 @@ class moodle_zend_xmlrpc_server extends Zend_XmlRpc_Server { * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 2.0 */ -class webservice_xmlrpc_server extends webservice_zend_server { +class webservice_xmlrpc_server extends webservice_base_server { + + /** @var string $response The XML-RPC response string. */ + private $response; /** * Contructor @@ -88,25 +44,186 @@ class webservice_xmlrpc_server extends webservice_zend_server { * @param string $authmethod authentication method of the web service (WEBSERVICE_AUTHMETHOD_PERMANENT_TOKEN, ...) */ public function __construct($authmethod) { - require_once 'Zend/XmlRpc/Server.php'; - parent::__construct($authmethod, 'moodle_zend_xmlrpc_server'); + parent::__construct($authmethod); $this->wsname = 'xmlrpc'; } /** - * Set up zend service class + * This method parses the request input, it needs to get: + * 1/ user authentication - username+password or token + * 2/ function name + * 3/ function parameters */ - protected function init_zend_server() { - parent::init_zend_server(); - // this exception indicates request failed - Zend_XmlRpc_Server_Fault::attachFaultException('moodle_exception'); - //when DEBUG >= NORMAL then the thrown exceptions are "casted" into a plain PHP Exception class - //in order to display the $debuginfo (see moodle_zend_xmlrpc_server class - MDL-29435) - if (debugging()) { - Zend_XmlRpc_Server_Fault::attachFaultException('Exception'); + protected function parse_request() { + // Retrieve and clean the POST/GET parameters from the parameters specific to the server. + parent::set_web_service_call_settings(); + + // Get GET and POST parameters. + $methodvariables = array_merge($_GET, $_POST); + + if ($this->authmethod == WEBSERVICE_AUTHMETHOD_USERNAME) { + $this->username = isset($methodvariables['wsusername']) ? $methodvariables['wsusername'] : null; + unset($methodvariables['wsusername']); + + $this->password = isset($methodvariables['wspassword']) ? $methodvariables['wspassword'] : null; + unset($methodvariables['wspassword']); + } else { + $this->token = isset($methodvariables['wstoken']) ? $methodvariables['wstoken'] : null; + unset($methodvariables['wstoken']); + } + + // Get the XML-RPC request data. + $rawpostdata = file_get_contents("php://input"); + $methodname = null; + + // Decode the request to get the decoded parameters and the name of the method to be called. + $decodedparams = xmlrpc_decode_request($rawpostdata, $methodname); + + // Add the decoded parameters to the methodvariables array. + if (is_array($decodedparams)) { + foreach ($decodedparams as $param) { + // Check if decoded param is an associative array. + if (is_array($param) && array_keys($param) !== range(0, count($param) - 1)) { + $methodvariables = array_merge($methodvariables, $param); + } else { + $methodvariables[] = $param; + } + } + } + + $this->functionname = $methodname; + $this->parameters = $methodvariables; + } + + /** + * Prepares the response. + */ + protected function prepare_response() { + try { + if (!empty($this->function->returns_desc)) { + $validatedvalues = external_api::clean_returnvalue($this->function->returns_desc, $this->returns); + $encodingoptions = array( + "encoding" => "utf-8" + ); + // We can now convert the response to the requested XML-RPC format. + $this->response = xmlrpc_encode_request(null, $validatedvalues, $encodingoptions); + } + } catch (invalid_response_exception $ex) { + $this->response = $this->generate_error($ex); } } + /** + * Send the result of function call to the WS client. + */ + protected function send_response() { + $this->prepare_response(); + $this->send_headers(); + echo $this->response; + } + + /** + * Send the error information to the WS client. + * + * @param Exception $ex + */ + protected function send_error($ex = null) { + $this->send_headers(); + echo $this->generate_error($ex); + } + + /** + * Sends the headers for the XML-RPC response. + */ + protected function send_headers() { + // Standard headers. + header('HTTP/1.1 200 OK'); + header('Connection: close'); + header('Content-Length: ' . strlen($this->response)); + header('Content-Type: text/xml; charset=utf-8'); + header('Date: ' . gmdate('D, d M Y H:i:s', 0) . ' GMT'); + header('Server: Moodle XML-RPC Server/1.0'); + // Other headers. + header('Cache-Control: private, must-revalidate, pre-check=0, post-check=0, max-age=0'); + header('Expires: '. gmdate('D, d M Y H:i:s', 0) .' GMT'); + header('Pragma: no-cache'); + header('Accept-Ranges: none'); + // Allow cross-origin requests only for Web Services. + // This allow to receive requests done by Web Workers or webapps in different domains. + header('Access-Control-Allow-Origin: *'); + } + + /** + * Generate the XML-RPC fault response. + * + * @param Exception $ex The exception. + * @param int $faultcode The faultCode to be included in the fault response + * @return string The XML-RPC fault response xml containing the faultCode and faultString. + */ + protected function generate_error(Exception $ex, $faultcode = 404) { + $error = $ex->getMessage(); + + if (!empty($ex->errorcode)) { + // The faultCode must be an int, so we obtain a hash of the errorcode then get an integer value of the hash. + $faultcode = base_convert(md5($ex->errorcode), 16, 10); + + // We strip the $code to 8 digits (and hope for no error code collisions). + // Collisions should be pretty rare, and if needed the client can retrieve + // the accurate errorcode from the last | in the exception message. + $faultcode = substr($faultcode, 0, 8); + + // Add the debuginfo to the exception message if debuginfo must be returned. + if (debugging() and isset($ex->debuginfo)) { + $error .= ' | DEBUG INFO: ' . $ex->debuginfo . ' | ERRORCODE: ' . $ex->errorcode; + } else { + $error .= ' | ERRORCODE: ' . $ex->errorcode; + } + } + + $fault = array( + 'faultCode' => (int) $faultcode, + 'faultString' => $error + ); + + $encodingoptions = array( + "encoding" => "utf-8" + ); + + return xmlrpc_encode_request(null, $fault, $encodingoptions); + } +} + +/** + * The Zend XMLRPC server but with a fault that return debuginfo. + * + * MDL-52209: Since Zend is being removed from Moodle, this class will be deprecated and eventually removed. + * Please use webservice_xmlrpc_server instead. + * + * @package webservice_xmlrpc + * @copyright 2011 Jerome Mouneyrac + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since Moodle 2.2 + * @deprecated since 3.1, see {@link webservice_xmlrpc_server}. + */ +class moodle_zend_xmlrpc_server extends webservice_xmlrpc_server { + + /** + * Raise an xmlrpc server fault. + * + * Moodle note: the difference with the Zend server is that we throw a plain PHP Exception + * with the debuginfo integrated to the exception message when DEBUG >= NORMAL. + * + * @param string|Exception $fault + * @param int $code + * @return Zend_XmlRpc_Server_Fault + * @deprecated since 3.1, see {@link webservice_xmlrpc_server::generate_error()}. + */ + public function fault($fault = null, $code = 404) { + debugging('moodle_zend_xmlrpc_server::fault() is deprecated, please use ' . + 'webservice_xmlrpc_server::generate_error() instead.', DEBUG_DEVELOPER); + + return $this->generate_error($fault, $code); + } } /** @@ -126,11 +243,12 @@ class webservice_xmlrpc_test_client implements webservice_test_client_interface * @return mixed */ public function simpletest($serverurl, $function, $params) { - //zend expects 0 based array with numeric indexes - $params = array_values($params); + global $CFG; - require_once 'Zend/XmlRpc/Client.php'; - $client = new Zend_XmlRpc_Client($serverurl); + $url = new moodle_url($serverurl); + $token = $url->get_param('wstoken'); + require_once($CFG->dirroot . '/webservice/xmlrpc/lib.php'); + $client = new webservice_xmlrpc_client($serverurl, $token); return $client->call($function, $params); } } diff --git a/webservice/xmlrpc/tests/fixtures/array_response.xml b/webservice/xmlrpc/tests/fixtures/array_response.xml new file mode 100644 index 00000000000..fb44121e61e --- /dev/null +++ b/webservice/xmlrpc/tests/fixtures/array_response.xml @@ -0,0 +1,22 @@ + + + + + + + + + 1 + + + Test string + + + 3.1416 + + + + + + + diff --git a/webservice/xmlrpc/tests/fixtures/fault_response.xml b/webservice/xmlrpc/tests/fixtures/fault_response.xml new file mode 100644 index 00000000000..ff90ff15814 --- /dev/null +++ b/webservice/xmlrpc/tests/fixtures/fault_response.xml @@ -0,0 +1,21 @@ + + + + + + + faultCode + + 23604497 + + + + faultString + + Can not find data record in database table external_functions. | DEBUG INFO: SELECT * FROM {external_functions} WHERE name = ? [array ( 0 => 'core_course_get_course', )] | ERRORCODE: invalidrecord + + + + + + diff --git a/webservice/xmlrpc/tests/fixtures/value_response.xml b/webservice/xmlrpc/tests/fixtures/value_response.xml new file mode 100644 index 00000000000..daa29c62b3b --- /dev/null +++ b/webservice/xmlrpc/tests/fixtures/value_response.xml @@ -0,0 +1,10 @@ + + + + + + 1 + + + + diff --git a/webservice/xmlrpc/tests/lib_test.php b/webservice/xmlrpc/tests/lib_test.php new file mode 100644 index 00000000000..e3ce56a7ee5 --- /dev/null +++ b/webservice/xmlrpc/tests/lib_test.php @@ -0,0 +1,135 @@ +. + +/** + * Unit tests for the XML-RPC web service. + * + * @package webservice_xmlrpc + * @category test + * @copyright 2015 Jun Pataleta + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot . '/webservice/xmlrpc/lib.php'); + +/** + * Unit tests for the XML-RPC web service. + * + * @package webservice_xmlrpc + * @category test + * @copyright 2015 Jun Pataleta + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class webservice_xmlrpc_test extends advanced_testcase { + + /** + * Setup. + */ + public function setUp() { + $this->resetAfterTest(); + } + + /** + * Test for array response. + */ + public function test_client_with_array_response() { + global $CFG; + + $client = new webservice_xmlrpc_client_mock('/webservice/xmlrpc/server.php', 'anytoken'); + $mockresponse = file_get_contents($CFG->dirroot . '/webservice/xmlrpc/tests/fixtures/array_response.xml'); + $client->set_mock_response($mockresponse); + $result = $client->call('testfunction'); + $this->assertEquals(xmlrpc_decode($mockresponse), $result); + } + + /** + * Test for value response. + */ + public function test_client_with_value_response() { + global $CFG; + + $client = new webservice_xmlrpc_client_mock('/webservice/xmlrpc/server.php', 'anytoken'); + $mockresponse = file_get_contents($CFG->dirroot . '/webservice/xmlrpc/tests/fixtures/value_response.xml'); + $client->set_mock_response($mockresponse); + $result = $client->call('testfunction'); + $this->assertEquals(xmlrpc_decode($mockresponse), $result); + } + + /** + * Test for fault response. + */ + public function test_client_with_fault_response() { + global $CFG; + + $client = new webservice_xmlrpc_client_mock('/webservice/xmlrpc/server.php', 'anytoken'); + $mockresponse = file_get_contents($CFG->dirroot . '/webservice/xmlrpc/tests/fixtures/fault_response.xml'); + $client->set_mock_response($mockresponse); + $this->setExpectedException('moodle_exception'); + $client->call('testfunction'); + } +} + +/** + * Class webservice_xmlrpc_client_mock. + * + * Mock class that returns the processed XML-RPC response. + * + * @package webservice_xmlrpc + * @category test + * @copyright 2015 Jun Pataleta + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class webservice_xmlrpc_client_mock extends webservice_xmlrpc_client { + + /** @var string The mock XML-RPC response string. */ + private $mockresponse; + + /** + * XML-RPC mock response setter. + * + * @param string $mockresponse + */ + public function set_mock_response($mockresponse) { + $this->mockresponse = $mockresponse; + } + + /** + * Since the call method uses download_file_content and it is hard to make an actual call to a web service, + * we'll just have to simulate the receipt of the response from the server using the mock response so we + * can test the processing result of this method. + * + * @param string $functionname the function name + * @param array $params the parameters of the function + * @return mixed The decoded XML RPC response. + * @throws moodle_exception + */ + public function call($functionname, $params = array()) { + // Get the response. + $response = $this->mockresponse; + + // This is the part of the code in webservice_xmlrpc_client::call() what we would like to test. + // Decode the response. + $result = xmlrpc_decode($response); + if (is_array($result) && xmlrpc_is_fault($result)) { + throw new moodle_exception($result['faultString']); + } + + return $result; + } +}