diff --git a/lang/en/webservice.php b/lang/en/webservice.php index b05912728ff..83e10564e58 100644 --- a/lang/en/webservice.php +++ b/lang/en/webservice.php @@ -40,6 +40,7 @@ $string['apiexplorer'] = 'API explorer'; $string['apiexplorernotavalaible'] = 'API explorer not available yet.'; $string['arguments'] = 'Arguments'; $string['authmethod'] = 'Authentication method'; +$string['callablefromajax'] = 'Callable from AJAX'; $string['cannotcreatetoken'] = 'No permission to create web service token for the service {$a}.'; $string['cannotgetcoursecontents'] = 'Cannot get course contents'; $string['configwebserviceplugins'] = 'For security reasons, only protocols that are in use should be enabled.'; diff --git a/lib/ajax/service.php b/lib/ajax/service.php new file mode 100644 index 00000000000..7a72e889a9a --- /dev/null +++ b/lib/ajax/service.php @@ -0,0 +1,89 @@ +. + +/** + * This file is used to call any registered externallib function in Moodle. + * + * It will process more than one request and return more than one response if required. + * It is recommended to add webservice functions and re-use this script instead of + * writing any new custom ajax scripts. + * + * @since Moodle 2.9 + * @package core + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +define('AJAX_SCRIPT', true); + +require_once(dirname(__FILE__) . '/../../config.php'); +require_once($CFG->libdir . '/externallib.php'); + +require_login(null, true, null, true, true); + +$rawjson = file_get_contents('php://input'); + +$requests = json_decode($rawjson, true); +if ($requests === null) { + $lasterror = json_last_error_msg(); + throw new coding_exception('Invalid json in request: ' . $lasterror); +} +$responses = array(); + + +foreach ($requests as $request) { + $response = array(); + $methodname = clean_param($request['methodname'], PARAM_ALPHANUMEXT); + $index = clean_param($request['index'], PARAM_INT); + $args = $request['args']; + + try { + $externalfunctioninfo = external_function_info($methodname); + + if (!$externalfunctioninfo->allowed_from_ajax) { + throw new moodle_exception('servicenotavailable', 'webservice'); + } + + // Validate params, this also sorts the params properly, we need the correct order in the next part. + $callable = array($externalfunctioninfo->classname, 'validate_parameters'); + $params = call_user_func($callable, + $externalfunctioninfo->parameters_desc, + $args); + + // Execute - gulp! + $callable = array($externalfunctioninfo->classname, $externalfunctioninfo->methodname); + $result = call_user_func_array($callable, + array_values($params)); + + $response['error'] = false; + $response['data'] = $result; + $responses[$index] = $response; + } catch (Exception $e) { + $jsonexception = get_exception_info($e); + unset($jsonexception->a); + if (!debugging('', DEBUG_DEVELOPER)) { + unset($jsonexception->debuginfo); + unset($jsonexception->backtrace); + } + $response['error'] = true; + $response['exception'] = $jsonexception; + $responses[$index] = $response; + // Do not process the remaining requests. + break; + } +} + +echo json_encode($responses); diff --git a/lib/amd/build/ajax.min.js b/lib/amd/build/ajax.min.js new file mode 100644 index 00000000000..bd2e6236024 --- /dev/null +++ b/lib/amd/build/ajax.min.js @@ -0,0 +1 @@ +define(["jquery","core/config"],function(a,b){var c=function(a){var b,c,d=this,e=null,f=0;for(f=0;f. + +/** + * Standard Ajax wrapper for Moodle. It calls the central Ajax script, + * which can call any existing webservice using the current session. + * In addition, it can batch multiple requests and return multiple responses. + * + * @module core/ajax + * @package core + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define(['jquery', 'core/config'], function($, config) { + + /** + * Success handler. Called when the ajax call succeeds. Checks each response and + * resolves or rejects the deferred from that request. + * + * @param {Object[]} responses Array of responses containing error, exception and data attributes. + */ + var requestSuccess = function(responses) { + // Call each of the success handlers. + var requests = this; + var exception = null; + var i = 0; + var request; + var response; + + for (i = 0; i < requests.length; i++) { + request = requests[i]; + + response = responses[i]; + // We may not have responses for all the requests. + if (typeof response !== "undefined") { + if (response.error === false) { + // Call the done handler if it was provided. + request.deferred.resolve(response.data); + } else { + exception = response.exception; + break; + } + } else { + // This is not an expected case. + exception = new Error('missing response'); + break; + } + } + // Something failed, reject the remaining promises. + if (exception !== null) { + for (; i < requests.length; i++) { + request = requests[i]; + request.deferred.reject(exception); + } + } + }; + + /** + * Fail handler. Called when the ajax call fails. Rejects all deferreds. + * + * @param {jqXHR} jqXHR The ajax object. + * @param {string} textStatus The status string. + */ + var requestFail = function(jqXHR, textStatus) { + // Reject all the promises. + var requests = this; + + var i = 0; + for (i = 0; i < requests.length; i++) { + var request = requests[i]; + + if (typeof request.fail != "undefined") { + request.deferred.reject(textStatus); + } + } + }; + + return /** @alias module:core/ajax */ { + // Public variables and functions. + /** + * Make a series of ajax requests and return all the responses. + * @param {Object[]} Array of requests with each containing methodname and args properties. + * done and fail callbacks can be set for each element in the array, or the + * can be attached to the promises returned by this function. + * @return {Promise{}} Array of promises that will be resolved when the ajax call returns. + */ + call: function(requests) { + var ajaxRequestData = [], + i, + promises = []; + for (i = 0; i < requests.length; i++) { + var request = requests[i]; + ajaxRequestData.push({ + index: i, + methodname: request.methodname, + args: request.args + }); + request.deferred = $.Deferred(); + promises.push(request.deferred.promise()); + // Allow setting done and fail handlers as arguments. + // This is just a shortcut for the calling code. + if (typeof request.done !== "undefined") { + request.deferred.done(request.done); + } + if (typeof request.fail !== "undefined") { + request.deferred.fail(request.fail); + } + request.index = i; + } + + ajaxRequestData = JSON.stringify(ajaxRequestData); + var settings = { + type: 'POST', + data: ajaxRequestData, + context: requests, + dataType: 'json', + processData: false + }; + + $.ajax(config.wwwroot + '/lib/ajax/service.php', settings) + .done(requestSuccess) + .fail(requestFail); + + return promises; + } + }; +}); diff --git a/lib/amd/src/config.js b/lib/amd/src/config.js new file mode 100644 index 00000000000..848567f8f11 --- /dev/null +++ b/lib/amd/src/config.js @@ -0,0 +1,28 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Expose the M.cfg global variable. + * + * @module core/config + * @package core + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define(function() { + + // This module exposes only the raw data from M.cfg; + return /** @alias module:core/config */ M.cfg; +}); diff --git a/lib/external/externallib.php b/lib/external/externallib.php index a0526c53379..1909c2f3fc7 100644 --- a/lib/external/externallib.php +++ b/lib/external/externallib.php @@ -77,6 +77,16 @@ class core_external extends external_api { return $strparams; } + /** + * Can this function be called directly from ajax? + * + * @return boolean + * @since Moodle 2.9 + */ + public static function get_string_is_allowed_from_ajax() { + return true; + } + /** * Returns description of get_string parameters * @@ -153,6 +163,17 @@ class core_external extends external_api { ); } + /** + * Can this function be called directly from ajax? + * + * @return boolean + * @since Moodle 2.9 + */ + public static function get_strings_is_allowed_from_ajax() { + return true; + } + + /** * Return multiple call to core get_string() * @@ -216,6 +237,16 @@ class core_external extends external_api { ); } + /** + * Can this function be called directly from ajax? + * + * @return boolean + * @since Moodle 2.9 + */ + public static function get_component_strings_is_allowed_from_ajax() { + return true; + } + /** * Return all lang strings of a component - call to core get_component_strings(). * diff --git a/lib/externallib.php b/lib/externallib.php index a9ed4bf4805..d6669321bc4 100644 --- a/lib/externallib.php +++ b/lib/externallib.php @@ -56,6 +56,7 @@ function external_function_info($function, $strictness=MUST_EXIST) { } } + $function->ajax_method = $function->methodname.'_is_allowed_from_ajax'; $function->parameters_method = $function->methodname.'_parameters'; $function->returns_method = $function->methodname.'_returns'; $function->deprecated_method = $function->methodname.'_is_deprecated'; @@ -75,6 +76,12 @@ function external_function_info($function, $strictness=MUST_EXIST) { $function->deprecated = true; } } + $function->allowed_from_ajax = false; + if (method_exists($function->classname, $function->ajax_method)) { + if (call_user_func(array($function->classname, $function->ajax_method)) === true) { + $function->allowed_from_ajax = true; + } + } // fetch the parameters description $function->parameters_desc = call_user_func(array($function->classname, $function->parameters_method)); diff --git a/webservice/renderer.php b/webservice/renderer.php index 624109cbffa..c04825a033d 100644 --- a/webservice/renderer.php +++ b/webservice/renderer.php @@ -811,6 +811,14 @@ EOF; $documentationhtml .= html_writer::end_tag('span'); } $documentationhtml .= $br . $br; + + // Ajax info. + $documentationhtml .= html_writer::start_tag('span', array('style' => 'color:#EA33A6')); + $documentationhtml .= get_string('callablefromajax', 'webservice') . $br; + $documentationhtml .= html_writer::end_tag('span'); + $documentationhtml .= $description->allowed_from_ajax ? get_string('yes') : get_string('no'); + $documentationhtml .= $br . $br; + if (empty($printableformat)) { $documentationhtml .= print_collapsible_region_end(true); }