Merge branch 'MDL-64642-master' of https://github.com/albertgasset/moodle

This commit is contained in:
Sara Arjona 2019-03-26 19:36:21 +01:00
commit ca06f046af
6 changed files with 275 additions and 2 deletions

View File

@ -26,6 +26,7 @@ namespace tool_mobile;
defined('MOODLE_INTERNAL') || die();
require_once("$CFG->libdir/externallib.php");
require_once("$CFG->dirroot/webservice/lib.php");
use external_api;
use external_files;
@ -460,4 +461,119 @@ class external extends external_api {
)
);
}
/**
* Returns description of method parameters
*
* @return external_function_parameters
* @since Moodle 3.7
*/
public static function call_external_functions_parameters() {
return new external_function_parameters([
'requests' => new external_multiple_structure(
new external_single_structure([
'function' => new external_value(PARAM_ALPHANUMEXT, 'Function name'),
'arguments' => new external_value(PARAM_RAW, 'JSON-encoded object with named arguments', VALUE_DEFAULT, '{}'),
'settingraw' => new external_value(PARAM_BOOL, 'Return raw text', VALUE_DEFAULT, false),
'settingfilter' => new external_value(PARAM_BOOL, 'Filter text', VALUE_DEFAULT, false),
'settingfileurl' => new external_value(PARAM_BOOL, 'Rewrite plugin file URLs', VALUE_DEFAULT, true),
'settinglang' => new external_value(PARAM_LANG, 'Session language', VALUE_DEFAULT, ''),
])
)
]);
}
/**
* Call multiple external functions and return all responses.
*
* @param array $requests List of requests.
* @return array Responses.
* @since Moodle 3.7
*/
public static function call_external_functions($requests) {
global $SESSION;
$params = self::validate_parameters(self::call_external_functions_parameters(), ['requests' => $requests]);
// We need to check if the functions being called are included in the service of the current token.
// This function only works when using mobile services via REST (this is intended).
$webservicemanager = new \webservice;
$token = $webservicemanager->get_user_ws_token(required_param('wstoken', PARAM_ALPHANUM));
$settings = \external_settings::get_instance();
$defaultlang = current_language();
$responses = [];
foreach ($params['requests'] as $request) {
// Some external functions modify _GET or $_POST data, we need to restore the original data after each call.
$originalget = fullclone($_GET);
$originalpost = fullclone($_POST);
// Set external settings and language.
$settings->set_raw($request['settingraw']);
$settings->set_filter($request['settingfilter']);
$settings->set_fileurl($request['settingfileurl']);
$settings->set_lang($request['settinglang']);
$SESSION->lang = $request['settinglang'] ?: $defaultlang;
// Parse arguments to an array, validation is done in external_api::call_external_function.
$args = @json_decode($request['arguments'], true);
if (!is_array($args)) {
$args = [];
}
if ($webservicemanager->service_function_exists($request['function'], $token->externalserviceid)) {
$response = external_api::call_external_function($request['function'], $args, false);
} else {
// Function not included in the service, return an access exception.
$response = [
'error' => true,
'exception' => [
'errorcode' => 'accessexception',
'module' => 'webservice'
]
];
if (debugging('', DEBUG_DEVELOPER)) {
$response['exception']['debuginfo'] = 'Access to the function is not allowed.';
}
}
if (isset($response['data'])) {
$response['data'] = json_encode($response['data']);
}
if (isset($response['exception'])) {
$response['exception'] = json_encode($response['exception']);
}
$responses[] = $response;
// Restore original $_GET and $_POST.
$_GET = $originalget;
$_POST = $originalpost;
if ($response['error']) {
// Do not process the remaining requests.
break;
}
}
return ['responses' => $responses];
}
/**
* Returns description of method result value
*
* @return external_single_structure
* @since Moodle 3.7
*/
public static function call_external_functions_returns() {
return new external_function_parameters([
'responses' => new external_multiple_structure(
new external_single_structure([
'error' => new external_value(PARAM_BOOL, 'Whether an exception was thrown.'),
'data' => new external_value(PARAM_RAW, 'JSON-encoded response data', VALUE_OPTIONAL),
'exception' => new external_value(PARAM_RAW, 'JSON-encoed exception info', VALUE_OPTIONAL),
])
)
]);
}
}

View File

@ -61,6 +61,7 @@ $functions = array(
'type' => 'write',
'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
),
'tool_mobile_get_content' => array(
'classname' => 'tool_mobile\external',
'methodname' => 'get_content',
@ -68,5 +69,13 @@ $functions = array(
'type' => 'read',
'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
),
'tool_mobile_call_external_functions' => array(
'classname' => 'tool_mobile\external',
'methodname' => 'call_external_functions',
'description' => 'Call multiple external functions and return all responses.',
'type' => 'write',
'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
),
);

View File

@ -30,6 +30,7 @@ global $CFG;
require_once($CFG->dirroot . '/webservice/tests/helpers.php');
require_once($CFG->dirroot . '/admin/tool/mobile/tests/fixtures/output/mobile.php');
require_once($CFG->dirroot . '/webservice/lib.php');
use tool_mobile\external;
use tool_mobile\api;
@ -358,4 +359,147 @@ class tool_mobile_external_testcase extends externallib_advanced_testcase {
$this->expectException('moodle_exception');
$result = external::get_content('tool_blahblahblah', 'test_view');
}
public function test_call_external_functions() {
global $SESSION;
$this->resetAfterTest(true);
$category = self::getDataGenerator()->create_category(array('name' => 'Category 1'));
$course = self::getDataGenerator()->create_course([
'category' => $category->id,
'shortname' => 'c1',
'summary' => '<span lang="en" class="multilang">Course summary</span>'
. '<span lang="eo" class="multilang">Kurso resumo</span>'
. '@@PLUGINFILE@@/filename.txt'
. '<!-- Comment stripped when formatting text -->',
'summaryformat' => FORMAT_MOODLE
]);
$user1 = self::getDataGenerator()->create_user(['username' => 'user1', 'lastaccess' => time()]);
$user2 = self::getDataGenerator()->create_user(['username' => 'user2', 'lastaccess' => time()]);
self::setUser($user1);
// Setup WS token.
$webservicemanager = new \webservice;
$service = $webservicemanager->get_external_service_by_shortname(MOODLE_OFFICIAL_MOBILE_SERVICE);
$token = external_generate_token_for_current_user($service);
$_POST['wstoken'] = $token->token;
// Workaround for external_api::call_external_function requiring sesskey.
$_POST['sesskey'] = sesskey();
// Call some functions.
$requests = [
[
'function' => 'core_course_get_courses_by_field',
'arguments' => json_encode(['field' => 'id', 'value' => $course->id])
],
[
'function' => 'core_user_get_users_by_field',
'arguments' => json_encode(['field' => 'id', 'values' => [$user1->id]])
],
[
'function' => 'core_user_get_user_preferences',
'arguments' => json_encode(['name' => 'some_setting', 'userid' => $user2->id])
],
[
'function' => 'core_course_get_courses_by_field',
'arguments' => json_encode(['field' => 'shortname', 'value' => $course->shortname])
],
];
$result = external::call_external_functions($requests);
// We need to execute the return values cleaning process to simulate the web service server.
$result = external_api::clean_returnvalue(external::call_external_functions_returns(), $result);
// Only 3 responses, the 4th request is not executed because the 3rd throws an exception.
$this->assertCount(3, $result['responses']);
$this->assertFalse($result['responses'][0]['error']);
$coursedata = external_api::clean_returnvalue(
core_course_external::get_courses_by_field_returns(),
core_course_external::get_courses_by_field('id', $course->id));
$this->assertEquals(json_encode($coursedata), $result['responses'][0]['data']);
$this->assertFalse($result['responses'][1]['error']);
$userdata = external_api::clean_returnvalue(
core_user_external::get_users_by_field_returns(),
core_user_external::get_users_by_field('id', [$user1->id]));
$this->assertEquals(json_encode($userdata), $result['responses'][1]['data']);
$this->assertTrue($result['responses'][2]['error']);
$exception = json_decode($result['responses'][2]['exception'], true);
$this->assertEquals('nopermissions', $exception['errorcode']);
// Call a function not included in the external service.
$_POST['wstoken'] = $token->token;
$functions = $webservicemanager->get_not_associated_external_functions($service->id);
$requests = [['function' => current($functions)->name]];
$result = external::call_external_functions($requests);
$this->assertTrue($result['responses'][0]['error']);
$exception = json_decode($result['responses'][0]['exception'], true);
$this->assertEquals('accessexception', $exception['errorcode']);
$this->assertEquals('webservice', $exception['module']);
// Call a function with different external settings.
filter_set_global_state('multilang', TEXTFILTER_ON);
$_POST['wstoken'] = $token->token;
$SESSION->lang = 'eo'; // Change default language, so we can test changing it to "en".
$requests = [
[
'function' => 'core_course_get_courses_by_field',
'arguments' => json_encode(['field' => 'id', 'value' => $course->id]),
],
[
'function' => 'core_course_get_courses_by_field',
'arguments' => json_encode(['field' => 'id', 'value' => $course->id]),
'settingraw' => '1'
],
[
'function' => 'core_course_get_courses_by_field',
'arguments' => json_encode(['field' => 'id', 'value' => $course->id]),
'settingraw' => '1',
'settingfileurl' => '0'
],
[
'function' => 'core_course_get_courses_by_field',
'arguments' => json_encode(['field' => 'id', 'value' => $course->id]),
'settingfilter' => '1',
'settinglang' => 'en'
],
];
$result = external::call_external_functions($requests);
$this->assertCount(4, $result['responses']);
$context = \context_course::instance($course->id);
$pluginfile = 'webservice/pluginfile.php';
$this->assertFalse($result['responses'][0]['error']);
$data = json_decode($result['responses'][0]['data']);
$expected = file_rewrite_pluginfile_urls($course->summary, $pluginfile, $context->id, 'course', 'summary', null);
$expected = format_text($expected, $course->summaryformat, ['para' => false, 'filter' => false]);
$this->assertEquals($expected, $data->courses[0]->summary);
$this->assertFalse($result['responses'][1]['error']);
$data = json_decode($result['responses'][1]['data']);
$expected = file_rewrite_pluginfile_urls($course->summary, $pluginfile, $context->id, 'course', 'summary', null);
$this->assertEquals($expected, $data->courses[0]->summary);
$this->assertFalse($result['responses'][2]['error']);
$data = json_decode($result['responses'][2]['data']);
$this->assertEquals($course->summary, $data->courses[0]->summary);
$this->assertFalse($result['responses'][3]['error']);
$data = json_decode($result['responses'][3]['data']);
$expected = file_rewrite_pluginfile_urls($course->summary, $pluginfile, $context->id, 'course', 'summary', null);
$SESSION->lang = 'en'; // We expect filtered text in english.
$expected = format_text($expected, $course->summaryformat, ['para' => false, 'filter' => true]);
$this->assertEquals($expected, $data->courses[0]->summary);
}
}

View File

@ -1,6 +1,10 @@
This files describes changes in tool_mobile code.
Information provided here is intended especially for developers.
=== 3.7 ===
* New external function tool_mobile::tool_mobile_call_external_function allows calling multiple external functions and returns all responses.
=== 3.5 ===
* External function tool_mobile::tool_mobile_get_plugins_supporting_mobile now returns additional plugins information required by

View File

@ -23,7 +23,7 @@
*/
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2019021100; // The current plugin version (Date: YYYYMMDDXX).
$plugin->version = 2019021101; // The current plugin version (Date: YYYYMMDDXX).
$plugin->requires = 2018112800; // Requires this Moodle version.
$plugin->component = 'tool_mobile'; // Full name of the plugin (used for diagnostics).
$plugin->dependencies = array(

View File

@ -206,7 +206,7 @@ class external_api {
}
// Do not allow access to write or delete webservices as a public user.
if ($externalfunctioninfo->loginrequired) {
if ($externalfunctioninfo->loginrequired && !WS_SERVER) {
if (defined('NO_MOODLE_COOKIES') && NO_MOODLE_COOKIES && !PHPUNIT_TEST) {
throw new moodle_exception('servicerequireslogin', 'webservice');
}