lib/amd/src/ajax.js

  1. // This file is part of Moodle - http://moodle.org/
  2. //
  3. // Moodle is free software: you can redistribute it and/or modify
  4. // it under the terms of the GNU General Public License as published by
  5. // the Free Software Foundation, either version 3 of the License, or
  6. // (at your option) any later version.
  7. //
  8. // Moodle is distributed in the hope that it will be useful,
  9. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. // GNU General Public License for more details.
  12. //
  13. // You should have received a copy of the GNU General Public License
  14. // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
  15. /**
  16. * Standard Ajax wrapper for Moodle. It calls the central Ajax script,
  17. * which can call any existing webservice using the current session.
  18. * In addition, it can batch multiple requests and return multiple responses.
  19. *
  20. * @module core/ajax
  21. * @copyright 2015 Damyon Wiese <damyon@moodle.com>
  22. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23. * @since 2.9
  24. */
  25. define(['jquery', 'core/config', 'core/log', 'core/url'], function($, config, Log, URL) {
  26. /**
  27. * A request to be performed.
  28. *
  29. * @typedef {object} request
  30. * @property {string} methodname The remote method to be called
  31. * @property {object} args The arguments to pass when fetching the remote content
  32. */
  33. // Keeps track of when the user leaves the page so we know not to show an error.
  34. var unloading = false;
  35. /**
  36. * Success handler. Called when the ajax call succeeds. Checks each response and
  37. * resolves or rejects the deferred from that request.
  38. *
  39. * @method requestSuccess
  40. * @private
  41. * @param {Object[]} responses Array of responses containing error, exception and data attributes.
  42. */
  43. var requestSuccess = function(responses) {
  44. // Call each of the success handlers.
  45. var requests = this,
  46. exception = null,
  47. i = 0,
  48. request,
  49. response,
  50. nosessionupdate;
  51. if (responses.error) {
  52. // There was an error with the request as a whole.
  53. // We need to reject each promise.
  54. // Unfortunately this may lead to duplicate dialogues, but each Promise must be rejected.
  55. for (; i < requests.length; i++) {
  56. request = requests[i];
  57. request.deferred.reject(responses);
  58. }
  59. return;
  60. }
  61. for (i = 0; i < requests.length; i++) {
  62. request = requests[i];
  63. response = responses[i];
  64. // We may not have responses for all the requests.
  65. if (typeof response !== "undefined") {
  66. if (response.error === false) {
  67. // Call the done handler if it was provided.
  68. request.deferred.resolve(response.data);
  69. } else {
  70. exception = response.exception;
  71. nosessionupdate = requests[i].nosessionupdate;
  72. break;
  73. }
  74. } else {
  75. // This is not an expected case.
  76. exception = new Error('missing response');
  77. break;
  78. }
  79. }
  80. // Something failed, reject the remaining promises.
  81. if (exception !== null) {
  82. // Redirect to the login page.
  83. if (exception.errorcode === "servicerequireslogin" && !nosessionupdate) {
  84. window.location = URL.relativeUrl("/login/index.php");
  85. } else {
  86. requests.forEach(function(request) {
  87. request.deferred.reject(exception);
  88. });
  89. }
  90. }
  91. };
  92. /**
  93. * Fail handler. Called when the ajax call fails. Rejects all deferreds.
  94. *
  95. * @method requestFail
  96. * @private
  97. * @param {jqXHR} jqXHR The ajax object.
  98. * @param {string} textStatus The status string.
  99. * @param {Error|Object} exception The error thrown.
  100. */
  101. var requestFail = function(jqXHR, textStatus, exception) {
  102. // Reject all the promises.
  103. var requests = this;
  104. var i = 0;
  105. for (i = 0; i < requests.length; i++) {
  106. var request = requests[i];
  107. if (unloading) {
  108. // No need to trigger an error because we are already navigating.
  109. Log.error("Page unloaded.");
  110. Log.error(exception);
  111. } else {
  112. request.deferred.reject(exception);
  113. }
  114. }
  115. };
  116. return /** @alias module:core/ajax */ {
  117. // Public variables and functions.
  118. /**
  119. * Make a series of ajax requests and return all the responses.
  120. *
  121. * @method call
  122. * @param {request[]} requests Array of requests with each containing methodname and args properties.
  123. * done and fail callbacks can be set for each element in the array, or the
  124. * can be attached to the promises returned by this function.
  125. * @param {Boolean} [async=true] If false this function will not return until the promises are resolved.
  126. * @param {Boolean} [loginrequired=true] When false this function calls an endpoint which does not use the
  127. * session.
  128. * Note: This may only be used with external functions which have been marked as
  129. * `'loginrequired' => false`
  130. * @param {Boolean} [nosessionupdate=false] If true, the timemodified for the session will not be updated.
  131. * @param {Number} [timeout] number of milliseconds to wait for a response. Defaults to no limit.
  132. * @param {Number} [cachekey] A cache key used to improve browser-side caching.
  133. * Typically the same `cachekey` is used for all function calls.
  134. * When the key changes, this causes the URL used to perform the fetch to change, which
  135. * prevents the existing browser cache from being used.
  136. * Note: This option is only availbale when `loginrequired` is `false`.
  137. * See {@link https://tracker.moodle.org/browser/MDL-65794} for more information.
  138. * @return {Promise[]} The Promises for each of the supplied requests.
  139. * The order of the Promise matches the order of requests exactly.
  140. *
  141. * @example <caption>A simple example that you might find in a repository module</caption>
  142. *
  143. * import {call as fetchMany} from 'core/ajax';
  144. *
  145. * export const fetchMessages = timeSince => fetchMany([{methodname: 'core_message_get_messages', args: {timeSince}}])[0];
  146. *
  147. * export const fetchNotifications = timeSince => fetchMany([{
  148. * methodname: 'core_message_get_notifications',
  149. * args: {
  150. * timeSince,
  151. * }
  152. * }])[0];
  153. *
  154. * export const fetchSomethingElse = (some, params, here) => fetchMany([{
  155. * methodname: 'core_get_something_else',
  156. * args: {
  157. * some,
  158. * params,
  159. * gohere: here,
  160. * },
  161. * }])[0];
  162. *
  163. * @example <caption>An example of fetching a string using the cachekey parameter</caption>
  164. * import {call as fetchMany} from 'core/ajax';
  165. * import * as Notification from 'core/notification';
  166. *
  167. * export const performAction = (some, args) => {
  168. * Promises.all(fetchMany([{methodname: 'core_get_string', args: {
  169. * stringid: 'do_not_copy',
  170. * component: 'core',
  171. * lang: 'en',
  172. * stringparams: [],
  173. * }}], true, false, false, undefined, M.cfg.langrev))
  174. * .then(([doNotCopyString]) => {
  175. * window.console.log(doNotCopyString);
  176. * })
  177. * .catch(Notification.exception);
  178. * };
  179. *
  180. */
  181. call: function(requests, async, loginrequired, nosessionupdate, timeout, cachekey) {
  182. $(window).bind('beforeunload', function() {
  183. unloading = true;
  184. });
  185. var ajaxRequestData = [],
  186. i,
  187. promises = [],
  188. methodInfo = [],
  189. requestInfo = '';
  190. var maxUrlLength = 2000;
  191. if (typeof loginrequired === "undefined") {
  192. loginrequired = true;
  193. }
  194. if (typeof async === "undefined") {
  195. async = true;
  196. }
  197. if (typeof timeout === 'undefined') {
  198. timeout = 0;
  199. }
  200. if (typeof cachekey === 'undefined') {
  201. cachekey = null;
  202. } else {
  203. cachekey = parseInt(cachekey);
  204. if (cachekey <= 0) {
  205. cachekey = null;
  206. } else if (!cachekey) {
  207. cachekey = null;
  208. }
  209. }
  210. if (typeof nosessionupdate === "undefined") {
  211. nosessionupdate = false;
  212. }
  213. for (i = 0; i < requests.length; i++) {
  214. var request = requests[i];
  215. ajaxRequestData.push({
  216. index: i,
  217. methodname: request.methodname,
  218. args: request.args
  219. });
  220. request.nosessionupdate = nosessionupdate;
  221. request.deferred = $.Deferred();
  222. promises.push(request.deferred.promise());
  223. // Allow setting done and fail handlers as arguments.
  224. // This is just a shortcut for the calling code.
  225. if (typeof request.done !== "undefined") {
  226. request.deferred.done(request.done);
  227. }
  228. if (typeof request.fail !== "undefined") {
  229. request.deferred.fail(request.fail);
  230. }
  231. request.index = i;
  232. methodInfo.push(request.methodname);
  233. }
  234. if (methodInfo.length <= 5) {
  235. requestInfo = methodInfo.sort().join();
  236. } else {
  237. requestInfo = methodInfo.length + '-method-calls';
  238. }
  239. ajaxRequestData = JSON.stringify(ajaxRequestData);
  240. var settings = {
  241. type: 'POST',
  242. context: requests,
  243. dataType: 'json',
  244. processData: false,
  245. async: async,
  246. contentType: "application/json",
  247. timeout: timeout
  248. };
  249. var script = 'service.php';
  250. var url = config.wwwroot + '/lib/ajax/';
  251. if (!loginrequired) {
  252. script = 'service-nologin.php';
  253. url += script + '?info=' + requestInfo;
  254. if (cachekey) {
  255. url += '&cachekey=' + cachekey;
  256. settings.type = 'GET';
  257. }
  258. } else {
  259. url += script + '?sesskey=' + config.sesskey + '&info=' + requestInfo;
  260. }
  261. if (nosessionupdate) {
  262. url += '&nosessionupdate=true';
  263. }
  264. if (settings.type === 'POST') {
  265. settings.data = ajaxRequestData;
  266. } else {
  267. var urlUseGet = url + '&args=' + encodeURIComponent(ajaxRequestData);
  268. if (urlUseGet.length > maxUrlLength) {
  269. settings.type = 'POST';
  270. settings.data = ajaxRequestData;
  271. } else {
  272. url = urlUseGet;
  273. }
  274. }
  275. // Jquery deprecated done and fail with async=false so we need to do this 2 ways.
  276. if (async) {
  277. $.ajax(url, settings)
  278. .done(requestSuccess)
  279. .fail(requestFail);
  280. } else {
  281. settings.success = requestSuccess;
  282. settings.error = requestFail;
  283. $.ajax(url, settings);
  284. }
  285. return promises;
  286. }
  287. };
  288. });