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);
}