From 3078a3b7a18059bbff30c787502fdd49357909fe Mon Sep 17 00:00:00 2001 From: Cameron Ball Date: Thu, 9 Jun 2016 23:55:00 +0800 Subject: [PATCH] MDL-54868 webservice_xmlrpc: Don't escape non-ascii characters --- webservice/xmlrpc/locallib.php | 25 ++- webservice/xmlrpc/tests/locallib_test.php | 162 ++++++++++++++++++ .../xmlrpc/tests/xmlrpc_server_test.php | 114 ++++++++++++ 3 files changed, 295 insertions(+), 6 deletions(-) create mode 100644 webservice/xmlrpc/tests/locallib_test.php create mode 100644 webservice/xmlrpc/tests/xmlrpc_server_test.php diff --git a/webservice/xmlrpc/locallib.php b/webservice/xmlrpc/locallib.php index 87cd695caa7..e03728fbb74 100644 --- a/webservice/xmlrpc/locallib.php +++ b/webservice/xmlrpc/locallib.php @@ -73,11 +73,11 @@ class webservice_xmlrpc_server extends webservice_base_server { } // Get the XML-RPC request data. - $rawpostdata = file_get_contents("php://input"); + $rawpostdata = $this->fetch_input_content(); $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); + $decodedparams = xmlrpc_decode_request($rawpostdata, $methodname, 'UTF-8'); $methodinfo = external_api::external_function_info($methodname); $methodparams = array_keys($methodinfo->parameters_desc->keys); @@ -94,6 +94,15 @@ class webservice_xmlrpc_server extends webservice_base_server { $this->parameters = $methodvariables; } + /** + * Fetch content from the client. + * + * @return string + */ + protected function fetch_input_content() { + return file_get_contents('php://input'); + } + /** * Prepares the response. */ @@ -102,8 +111,10 @@ class webservice_xmlrpc_server extends webservice_base_server { if (!empty($this->function->returns_desc)) { $validatedvalues = external_api::clean_returnvalue($this->function->returns_desc, $this->returns); $encodingoptions = array( - "encoding" => "utf-8", - "verbosity" => "no_white_space" + "encoding" => "UTF-8", + "verbosity" => "no_white_space", + // See MDL-54868. + "escaping" => ["markup"] ); // We can now convert the response to the requested XML-RPC format. $this->response = xmlrpc_encode_request(null, $validatedvalues, $encodingoptions); @@ -186,8 +197,10 @@ class webservice_xmlrpc_server extends webservice_base_server { ); $encodingoptions = array( - "encoding" => "utf-8", - "verbosity" => "no_white_space" + "encoding" => "UTF-8", + "verbosity" => "no_white_space", + // See MDL-54868. + "escaping" => ["markup"] ); return xmlrpc_encode_request(null, $fault, $encodingoptions); diff --git a/webservice/xmlrpc/tests/locallib_test.php b/webservice/xmlrpc/tests/locallib_test.php new file mode 100644 index 00000000000..ad99cbed8eb --- /dev/null +++ b/webservice/xmlrpc/tests/locallib_test.php @@ -0,0 +1,162 @@ +. + +/** + * Unit tests for the XML-RPC web service server. + * + * @package webservice_xmlrpc + * @category test + * @copyright 2016 Cameron Ball + * @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/locallib.php'); + +/** + * Unit tests for the XML-RPC web service server. + * + * @package webservice_xmlrpc + * @category test + * @copyright 2016 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class webservice_xmlrpc_locallib_testcase extends advanced_testcase { + + /** + * Setup. + */ + public function setUp() { + if (!function_exists('xmlrpc_decode')) { + $this->markTestSkipped('XMLRPC is not installed.'); + } + } + + /** + * Test that the response generated is correct + * + * There is a bug in PHP that causes the xml_rpc library to + * incorrectly escape multibyte characters. See https://bugs.php.net/bug.php?id=41650 + * + * @dataProvider prepare_response_provider + * @param string $returnsdesc Webservice function return description + * @param string $returns Webservice function description + * @param string $expected The expected XML-RPC response + */ + public function test_prepare_response($returnsdesc, $returns, $expected) { + $server = $this->getMockBuilder('webservice_xmlrpc_server') + ->disableOriginalConstructor() + ->setMethods(null) + ->getMock(); + + $rc = new \ReflectionClass('webservice_xmlrpc_server'); + $rcm = $rc->getMethod('prepare_response'); + $rcm->setAccessible(true); + + $func = $rc->getProperty('function'); + $func->setAccessible(true); + $func->setValue($server, (object) ['returns_desc' => new external_value(PARAM_RAW, $returnsdesc, VALUE_OPTIONAL)]); + + $ret = $rc->getProperty('returns'); + $ret->setAccessible(true); + $ret->setValue($server, $returns); + + $rcm->invokeArgs($server, []); + $response = $rc->getProperty('response'); + $response->setAccessible(true); + + $this->assertEquals($expected, $response->getValue($server)); + } + + /** + * Test that the response generated is correct + * + * There is a bug in PHP that causes the xml_rpc library to + * incorrectly escape multibyte characters. See https://bugs.php.net/bug.php?id=41650 + * + * @dataProvider generate_error_provider + * @param Exception $exception An exception to be provided to generate_error + * @param string $code An error code to be provided to generate_error + * @param string $expected The expected XML-RPC response + */ + public function test_generate_error($exception, $code, $expected) { + $server = $this->getMockBuilder('webservice_xmlrpc_server') + ->disableOriginalConstructor() + ->setMethods(null) + ->getMock(); + + $rc = new \ReflectionClass('webservice_xmlrpc_server'); + $rcm = $rc->getMethod('generate_error'); + $rcm->setAccessible(true); + + if ($code === null) { + $result = $rcm->invokeArgs($server, [$exception]); + } else { + $result = $rcm->invokeArgs($server, [$exception, $code]); + } + $this->assertEquals($expected, $result); + } + + /** + * Data provider for the prepare_response testcase + * + * @return array of testcases + */ + public function prepare_response_provider() { + return [ + 'Description written with Latin script' => [ + 'Ennyn Durin, Aran Moria: pedo mellon a minno', + 'Mellon!', + 'Mellon!' + . '' + ], + 'Description with non-Latin glyphs' => [ + 'What biscuits do you have?', + // V Unicode 9! V. + '😂🤵😂 𝒪𝓃𝓁𝓎 𝓉𝒽𝑒 𝒻𝒾𝓃𝑒𝓈𝓉 𝐼𝓉𝒶𝓁𝒾𝒶𝓃 𝒷𝒾𝓈𝒸𝓊𝒾𝓉𝓈 😂🤵😂', + '' + . '😂🤵😂 𝒪𝓃𝓁𝓎 𝓉𝒽𝑒 𝒻𝒾𝓃𝑒𝓈𝓉 𝐼𝓉𝒶𝓁𝒾𝒶𝓃 𝒷𝒾𝓈𝒸𝓊𝒾𝓉𝓈 😂🤵😂' + ] + ]; + } + + /** + * Data provider for the generate_error testcase + * + * @return array of testcases + */ + public function generate_error_provider() { + return [ + 'Standard exception with default faultcode' => [ + new \Exception(), + null, + 'faultCode404faultString' + ], + 'Standard exception with default faultcode and exception content' => [ + new \Exception('PC LOAD LETTER'), + null, + 'faultCode404faultStringPC LOAD LETTER' + ], + 'Standard exception with really messed up non-Latin glyphs' => [ + new \Exception('P̫̬̳̫̓͊̇r̨͎̜ͧa͚̬̙̺͎̙ͬẏ͎̲̦̲e̶̞͎͙̻͐̉r͙̙ͮ̓̈ͧ̔̃ ̠ͨ́ͭ̎̎̇̿n̗̥̞͗o̼̖͛̂̒̿ͮ͘t̷̞͎̘̘̝̥̲͂̌ͭ ͕̹͚̪͖̖̊̆́̒ͫ̓̀fͤͦͭͥ͊ͩo̼̱̻̹͒̿͒u̡͕̞͕̜̠͕ͥͭ̈̄̈́͐ń̘̼̇͜d̸̰̻͎͉̱̰̥̿͒'), + null, + 'faultCode404faultStringP̫̬̳̫̓͊̇r̨͎̜ͧa͚̬̙̺͎̙ͬẏ͎̲̦̲e̶̞͎͙̻͐̉r͙̙ͮ̓̈ͧ̔̃ ̠ͨ́ͭ̎̎̇̿n̗̥̞͗o̼̖͛̂̒̿ͮ͘t̷̞͎̘̘̝̥̲͂̌ͭ ͕̹͚̪͖̖̊̆́̒ͫ̓̀fͤͦͭͥ͊ͩo̼̱̻̹͒̿͒u̡͕̞͕̜̠͕ͥͭ̈̄̈́͐ń̘̼̇͜d̸̰̻͎͉̱̰̥̿͒' + ] + ]; + } +} diff --git a/webservice/xmlrpc/tests/xmlrpc_server_test.php b/webservice/xmlrpc/tests/xmlrpc_server_test.php new file mode 100644 index 00000000000..a4f6feaa740 --- /dev/null +++ b/webservice/xmlrpc/tests/xmlrpc_server_test.php @@ -0,0 +1,114 @@ +. + +/** + * Unit tests for the XML-RPC web service server. + * + * @package webservice_xmlrpc + * @category test + * @copyright 2016 Andrew Nicols + * @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/locallib.php'); + +/** + * Unit tests for the XML-RPC web service server. + * + * @package webservice_xmlrpc + * @category test + * @copyright 2016 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class xmlrpc_server_test extends advanced_testcase { + + /** + * Setup. + */ + public function setUp() { + if (!function_exists('xmlrpc_decode')) { + $this->markTestSkipped('XMLRPC is not installed.'); + } + } + + /** + * Test parameter parsing. + * + * @dataProvider parse_request_provider + * @param string $input The XML-RPC request + * @param string $expectfunction The expected value for the function after decoding the request + * @param array $expectparams The expected value for the params after decoding the request + */ + public function test_parse_request($input, $expectfunction, $expectparams) { + $server = $this->getMockBuilder('\webservice_xmlrpc_server') + ->setMethods(['fetch_input_content']) + ->disableOriginalConstructor() + ->getMock(); + + $server->method('fetch_input_content') + ->willReturn($input); + + $rc = new \ReflectionClass('\webservice_xmlrpc_server'); + $rcm = $rc->getMethod('parse_request'); + $rcm->setAccessible(true); + $rcm->invoke($server); + + $rcp = $rc->getProperty('functionname'); + $rcp->setAccessible(true); + $this->assertEquals($expectfunction, $rcp->getValue($server)); + + $rcp = $rc->getProperty('parameters'); + $rcp->setAccessible(true); + $this->assertEquals($expectparams, $rcp->getValue($server)); + } + + /** + * Data provider for testing parse_request. + * + * @return array + */ + public function parse_request_provider() { + $xml = ''; + + // This valid webservice call has one required param ('component'), and one optional param ('lang'). + $validmethod = 'core_get_component_strings'; + $requiredparams = 'moodle'; + $allparams = 'moodleen' + . ''; + $requiredparamsnonlatin = 'ᛞᛁᛞᛃᛟᚢᚲᚾᛟᚹᛈᚺᛈᛋᚢᛈᛈᛟᚱᛏᛋᚢᛏᚠ8ᚡᚨᚱᛁᚨᛒᛚᛖᚾᚨᛗᛖᛋ'; + + return [ + 'Valid method, required params only' => [ + "{$xml}{$validmethod}{$requiredparams}", + 'core_get_component_strings', + ['component' => 'moodle'], + ], + 'Valid method, all params' => [ + "{$xml}{$validmethod}{$allparams}", + 'core_get_component_strings', + ['component' => 'moodle', 'lang' => 'en'], + ], + 'Valid method required params only (non Latin)' => [ + "{$xml}{$validmethod}{$requiredparamsnonlatin}", + 'core_get_component_strings', + ['component' => 'ᛞᛁᛞᛃᛟᚢᚲᚾᛟᚹᛈᚺᛈᛋᚢᛈᛈᛟᚱᛏᛋᚢᛏᚠ8ᚡᚨᚱᛁᚨᛒᛚᛖᚾᚨᛗᛖᛋ'], + ], + ]; + } +}