diff --git a/public/lang/en/moodle.php b/public/lang/en/moodle.php index 35e7fc9bacd..e0b4312af01 100644 --- a/public/lang/en/moodle.php +++ b/public/lang/en/moodle.php @@ -2211,6 +2211,8 @@ $string['targetrole'] = 'Target role'; $string['teacheronly'] = 'for the {$a} only'; $string['teacherroles'] = '{$a} roles'; $string['teachers'] = 'Teachers'; +$string['telemetrytraceidfooterlink'] = 'Telemetry trace'; +$string['telemetrytraceidfooterlinkcopied'] = 'The telemetry trace ID has been copied to your clipboard. You can now paste it in the support form or anywhere else you need.'; $string['textcopiedtoclipboard'] = 'Text copied to clipboard'; $string['textediting'] = 'Text editor'; $string['textediting_help'] = 'If an HTML editor such as Atto or TinyMCE is selected, text input areas will have a toolbar with buttons for easily adding content. diff --git a/public/lib/amd/build/ajax.min.js b/public/lib/amd/build/ajax.min.js index 7eb39516561..af0cc2ac08a 100644 --- a/public/lib/amd/build/ajax.min.js +++ b/public/lib/amd/build/ajax.min.js @@ -8,6 +8,6 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since 2.9 */ -define("core/ajax",["jquery","core/config","core/log","core/url"],(function($,config,Log,URL){var unloading=!1,requestSuccess=function(responses){var request,response,nosessionupdate,exception=null,i=0;if(responses.error)for(;i2e3?(settings.type="POST",settings.data=ajaxRequestData):url=urlUseGet}return async?$.ajax(url,settings).done(requestSuccess).fail(requestFail):(settings.success=requestSuccess,settings.error=requestFail,$.ajax(url,settings)),promises}}})); +define("core/ajax",["jquery","core/config","core/log","core/url"],(function($,config,Log,URL){var unloading=!1,requestSuccess=function(responses){var request,response,nosessionupdate,exception=null,i=0;if(responses.error)for(;i2e3?(settings.type="POST",settings.data=ajaxRequestData):url=urlUseGet}return async?$.ajax(url,settings).done(requestSuccess).fail(requestFail):(settings.success=requestSuccess,settings.error=requestFail,$.ajax(url,settings)),promises}}})); //# sourceMappingURL=ajax.min.js.map \ No newline at end of file diff --git a/public/lib/amd/build/ajax.min.js.map b/public/lib/amd/build/ajax.min.js.map index 6bead2f8109..26e23e46446 100644 --- a/public/lib/amd/build/ajax.min.js.map +++ b/public/lib/amd/build/ajax.min.js.map @@ -1 +1 @@ -{"version":3,"file":"ajax.min.js","sources":["../src/ajax.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Standard Ajax wrapper for Moodle. It calls the central Ajax script,\n * which can call any existing webservice using the current session.\n * In addition, it can batch multiple requests and return multiple responses.\n *\n * @module core/ajax\n * @copyright 2015 Damyon Wiese \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n * @since 2.9\n */\ndefine(['jquery', 'core/config', 'core/log', 'core/url'], function($, config, Log, URL) {\n\n/**\n * A request to be performed.\n *\n * @typedef {object} request\n * @property {string} methodname The remote method to be called\n * @property {object} args The arguments to pass when fetching the remote content\n */\n\n // Keeps track of when the user leaves the page so we know not to show an error.\n var unloading = false;\n\n /**\n * Success handler. Called when the ajax call succeeds. Checks each response and\n * resolves or rejects the deferred from that request.\n *\n * @method requestSuccess\n * @private\n * @param {Object[]} responses Array of responses containing error, exception and data attributes.\n */\n var requestSuccess = function(responses) {\n // Call each of the success handlers.\n var requests = this,\n exception = null,\n i = 0,\n request,\n response,\n nosessionupdate;\n\n if (responses.error) {\n // There was an error with the request as a whole.\n // We need to reject each promise.\n // Unfortunately this may lead to duplicate dialogues, but each Promise must be rejected.\n for (; i < requests.length; i++) {\n request = requests[i];\n request.deferred.reject(responses);\n }\n\n return;\n }\n\n for (i = 0; i < requests.length; i++) {\n request = requests[i];\n\n response = responses[i];\n // We may not have responses for all the requests.\n if (typeof response !== \"undefined\") {\n if (response.error === false) {\n // Call the done handler if it was provided.\n request.deferred.resolve(response.data);\n } else {\n exception = response.exception;\n nosessionupdate = requests[i].nosessionupdate;\n break;\n }\n } else {\n // This is not an expected case.\n exception = new Error('missing response');\n break;\n }\n }\n // Something failed, reject the remaining promises.\n if (exception !== null) {\n // Redirect to the login page.\n if (exception.errorcode === \"servicerequireslogin\" && !nosessionupdate) {\n window.location = URL.relativeUrl(\"/login/index.php\");\n } else {\n requests.forEach(function(request) {\n request.deferred.reject(exception);\n });\n }\n }\n };\n\n /**\n * Fail handler. Called when the ajax call fails. Rejects all deferreds.\n *\n * @method requestFail\n * @private\n * @param {jqXHR} jqXHR The ajax object.\n * @param {string} textStatus The status string.\n * @param {Error|Object} exception The error thrown.\n */\n var requestFail = function(jqXHR, textStatus, exception) {\n // Reject all the promises.\n var requests = this;\n\n var i = 0;\n for (i = 0; i < requests.length; i++) {\n var request = requests[i];\n\n if (unloading) {\n // No need to trigger an error because we are already navigating.\n Log.error(\"Page unloaded.\");\n Log.error(exception);\n } else {\n request.deferred.reject(exception);\n }\n }\n };\n\n return /** @alias module:core/ajax */ {\n // Public variables and functions.\n /**\n * Make a series of ajax requests and return all the responses.\n *\n * @method call\n * @param {request[]} requests Array of requests with each containing methodname and args properties.\n * done and fail callbacks can be set for each element in the array, or the\n * can be attached to the promises returned by this function.\n * @param {Boolean} [async=true] If false this function will not return until the promises are resolved.\n * @param {Boolean} [loginrequired=true] When false this function calls an endpoint which does not use the\n * session.\n * Note: This may only be used with external functions which have been marked as\n * `'loginrequired' => false`\n * @param {Boolean} [nosessionupdate=false] If true, the timemodified for the session will not be updated.\n * @param {Number} [timeout] number of milliseconds to wait for a response. Defaults to no limit.\n * @param {Number} [cachekey] A cache key used to improve browser-side caching.\n * Typically the same `cachekey` is used for all function calls.\n * When the key changes, this causes the URL used to perform the fetch to change, which\n * prevents the existing browser cache from being used.\n * Note: This option is only availbale when `loginrequired` is `false`.\n * See {@link https://tracker.moodle.org/browser/MDL-65794} for more information.\n * @return {Promise[]} The Promises for each of the supplied requests.\n * The order of the Promise matches the order of requests exactly.\n *\n * @example A simple example that you might find in a repository module\n *\n * import {call as fetchMany} from 'core/ajax';\n *\n * export const fetchMessages = timeSince => fetchMany([{methodname: 'core_message_get_messages', args: {timeSince}}])[0];\n *\n * export const fetchNotifications = timeSince => fetchMany([{\n * methodname: 'core_message_get_notifications',\n * args: {\n * timeSince,\n * }\n * }])[0];\n *\n * export const fetchSomethingElse = (some, params, here) => fetchMany([{\n * methodname: 'core_get_something_else',\n * args: {\n * some,\n * params,\n * gohere: here,\n * },\n * }])[0];\n *\n * @example An example of fetching a string using the cachekey parameter\n * import {call as fetchMany} from 'core/ajax';\n * import * as Notification from 'core/notification';\n *\n * export const performAction = (some, args) => {\n * Promises.all(fetchMany([{methodname: 'core_get_string', args: {\n * stringid: 'do_not_copy',\n * component: 'core',\n * lang: 'en',\n * stringparams: [],\n * }}], true, false, false, undefined, M.cfg.langrev))\n * .then(([doNotCopyString]) => {\n * window.console.log(doNotCopyString);\n * })\n * .catch(Notification.exception);\n * };\n *\n */\n call: function(requests, async, loginrequired, nosessionupdate, timeout, cachekey) {\n $(window).bind('beforeunload', function() {\n unloading = true;\n });\n var ajaxRequestData = [],\n i,\n promises = [],\n methodInfo = [],\n requestInfo = '';\n\n var maxUrlLength = 2000;\n\n if (typeof loginrequired === \"undefined\") {\n loginrequired = true;\n }\n if (typeof async === \"undefined\") {\n async = true;\n }\n if (typeof timeout === 'undefined') {\n timeout = 0;\n }\n if (typeof cachekey === 'undefined') {\n cachekey = null;\n } else {\n cachekey = parseInt(cachekey);\n if (cachekey <= 0) {\n cachekey = null;\n } else if (!cachekey) {\n cachekey = null;\n }\n }\n\n if (typeof nosessionupdate === \"undefined\") {\n nosessionupdate = false;\n }\n for (i = 0; i < requests.length; i++) {\n var request = requests[i];\n ajaxRequestData.push({\n index: i,\n methodname: request.methodname,\n args: request.args\n });\n request.nosessionupdate = nosessionupdate;\n request.deferred = $.Deferred();\n promises.push(request.deferred.promise());\n // Allow setting done and fail handlers as arguments.\n // This is just a shortcut for the calling code.\n if (typeof request.done !== \"undefined\") {\n request.deferred.done(request.done);\n }\n if (typeof request.fail !== \"undefined\") {\n request.deferred.fail(request.fail);\n }\n request.index = i;\n methodInfo.push(request.methodname);\n }\n\n if (methodInfo.length <= 5) {\n requestInfo = methodInfo.sort().join();\n } else {\n requestInfo = methodInfo.length + '-method-calls';\n }\n\n ajaxRequestData = JSON.stringify(ajaxRequestData);\n var settings = {\n type: 'POST',\n context: requests,\n dataType: 'json',\n processData: false,\n async: async,\n contentType: \"application/json\",\n timeout: timeout\n };\n\n var script = 'service.php';\n var url = config.wwwroot + '/lib/ajax/';\n if (!loginrequired) {\n script = 'service-nologin.php';\n url += script + '?info=' + requestInfo;\n if (cachekey) {\n url += '&cachekey=' + cachekey;\n settings.type = 'GET';\n }\n } else {\n url += script + '?sesskey=' + config.sesskey + '&info=' + requestInfo;\n }\n\n if (nosessionupdate) {\n url += '&nosessionupdate=true';\n }\n\n if (settings.type === 'POST') {\n settings.data = ajaxRequestData;\n } else {\n var urlUseGet = url + '&args=' + encodeURIComponent(ajaxRequestData);\n\n if (urlUseGet.length > maxUrlLength) {\n settings.type = 'POST';\n settings.data = ajaxRequestData;\n } else {\n url = urlUseGet;\n }\n }\n\n // Jquery deprecated done and fail with async=false so we need to do this 2 ways.\n if (async) {\n $.ajax(url, settings)\n .done(requestSuccess)\n .fail(requestFail);\n } else {\n settings.success = requestSuccess;\n settings.error = requestFail;\n $.ajax(url, settings);\n }\n\n return promises;\n }\n };\n});\n"],"names":["define","$","config","Log","URL","unloading","requestSuccess","responses","request","response","nosessionupdate","exception","i","error","this","length","deferred","reject","Error","resolve","data","errorcode","forEach","window","location","relativeUrl","requestFail","jqXHR","textStatus","call","requests","async","loginrequired","timeout","cachekey","bind","ajaxRequestData","promises","methodInfo","requestInfo","parseInt","push","index","methodname","args","Deferred","promise","done","fail","sort","join","JSON","stringify","settings","type","context","dataType","processData","contentType","script","url","wwwroot","sesskey","urlUseGet","encodeURIComponent","ajax","success"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"ajax.min.js","sources":["../src/ajax.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Standard Ajax wrapper for Moodle. It calls the central Ajax script,\n * which can call any existing webservice using the current session.\n * In addition, it can batch multiple requests and return multiple responses.\n *\n * @module core/ajax\n * @copyright 2015 Damyon Wiese \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n * @since 2.9\n */\ndefine(['jquery', 'core/config', 'core/log', 'core/url'], function($, config, Log, URL) {\n\n/**\n * A request to be performed.\n *\n * @typedef {object} request\n * @property {string} methodname The remote method to be called\n * @property {object} args The arguments to pass when fetching the remote content\n */\n\n // Keeps track of when the user leaves the page so we know not to show an error.\n var unloading = false;\n\n /**\n * Success handler. Called when the ajax call succeeds. Checks each response and\n * resolves or rejects the deferred from that request.\n *\n * @method requestSuccess\n * @private\n * @param {Object[]} responses Array of responses containing error, exception and data attributes.\n */\n var requestSuccess = function(responses) {\n // Call each of the success handlers.\n var requests = this,\n exception = null,\n i = 0,\n request,\n response,\n nosessionupdate;\n\n if (responses.error) {\n // There was an error with the request as a whole.\n // We need to reject each promise.\n // Unfortunately this may lead to duplicate dialogues, but each Promise must be rejected.\n for (; i < requests.length; i++) {\n request = requests[i];\n request.deferred.reject(responses);\n }\n\n return;\n }\n\n for (i = 0; i < requests.length; i++) {\n request = requests[i];\n\n response = responses[i];\n // We may not have responses for all the requests.\n if (typeof response !== \"undefined\") {\n if (response.error === false) {\n // Call the done handler if it was provided.\n request.deferred.resolve(response.data);\n } else {\n exception = response.exception;\n nosessionupdate = requests[i].nosessionupdate;\n break;\n }\n } else {\n // This is not an expected case.\n exception = new Error('missing response');\n break;\n }\n }\n // Something failed, reject the remaining promises.\n if (exception !== null) {\n // Redirect to the login page.\n if (exception.errorcode === \"servicerequireslogin\" && !nosessionupdate) {\n window.location = URL.relativeUrl(\"/login/index.php\");\n } else {\n requests.forEach(function(request) {\n request.deferred.reject(exception);\n });\n }\n }\n };\n\n /**\n * Fail handler. Called when the ajax call fails. Rejects all deferreds.\n *\n * @method requestFail\n * @private\n * @param {jqXHR} jqXHR The ajax object.\n * @param {string} textStatus The status string.\n * @param {Error|Object} exception The error thrown.\n */\n var requestFail = function(jqXHR, textStatus, exception) {\n // Reject all the promises.\n var requests = this;\n\n var i = 0;\n for (i = 0; i < requests.length; i++) {\n var request = requests[i];\n\n if (unloading) {\n // No need to trigger an error because we are already navigating.\n Log.error(\"Page unloaded.\");\n Log.error(exception);\n } else {\n request.deferred.reject(exception);\n }\n }\n };\n\n return /** @alias module:core/ajax */ {\n // Public variables and functions.\n /**\n * Make a series of ajax requests and return all the responses.\n *\n * @method call\n * @param {request[]} requests Array of requests with each containing methodname and args properties.\n * done and fail callbacks can be set for each element in the array, or the\n * can be attached to the promises returned by this function.\n * @param {Boolean} [async=true] If false this function will not return until the promises are resolved.\n * @param {Boolean} [loginrequired=true] When false this function calls an endpoint which does not use the\n * session.\n * Note: This may only be used with external functions which have been marked as\n * `'loginrequired' => false`\n * @param {Boolean} [nosessionupdate=false] If true, the timemodified for the session will not be updated.\n * @param {Number} [timeout] number of milliseconds to wait for a response. Defaults to no limit.\n * @param {Number} [cachekey] A cache key used to improve browser-side caching.\n * Typically the same `cachekey` is used for all function calls.\n * When the key changes, this causes the URL used to perform the fetch to change, which\n * prevents the existing browser cache from being used.\n * Note: This option is only availbale when `loginrequired` is `false`.\n * See {@link https://tracker.moodle.org/browser/MDL-65794} for more information.\n * @return {Promise[]} The Promises for each of the supplied requests.\n * The order of the Promise matches the order of requests exactly.\n *\n * @example A simple example that you might find in a repository module\n *\n * import {call as fetchMany} from 'core/ajax';\n *\n * export const fetchMessages = timeSince => fetchMany([{methodname: 'core_message_get_messages', args: {timeSince}}])[0];\n *\n * export const fetchNotifications = timeSince => fetchMany([{\n * methodname: 'core_message_get_notifications',\n * args: {\n * timeSince,\n * }\n * }])[0];\n *\n * export const fetchSomethingElse = (some, params, here) => fetchMany([{\n * methodname: 'core_get_something_else',\n * args: {\n * some,\n * params,\n * gohere: here,\n * },\n * }])[0];\n *\n * @example An example of fetching a string using the cachekey parameter\n * import {call as fetchMany} from 'core/ajax';\n * import * as Notification from 'core/notification';\n *\n * export const performAction = (some, args) => {\n * Promises.all(fetchMany([{methodname: 'core_get_string', args: {\n * stringid: 'do_not_copy',\n * component: 'core',\n * lang: 'en',\n * stringparams: [],\n * }}], true, false, false, undefined, M.cfg.langrev))\n * .then(([doNotCopyString]) => {\n * window.console.log(doNotCopyString);\n * })\n * .catch(Notification.exception);\n * };\n *\n */\n call: function(requests, async, loginrequired, nosessionupdate, timeout, cachekey) {\n $(window).bind('beforeunload', function() {\n unloading = true;\n });\n var ajaxRequestData = [],\n i,\n promises = [],\n methodInfo = [],\n requestInfo = '';\n\n var maxUrlLength = 2000;\n\n if (typeof loginrequired === \"undefined\") {\n loginrequired = true;\n }\n if (typeof async === \"undefined\") {\n async = true;\n }\n if (typeof timeout === 'undefined') {\n timeout = 0;\n }\n if (typeof cachekey === 'undefined') {\n cachekey = null;\n } else {\n cachekey = parseInt(cachekey);\n if (cachekey <= 0) {\n cachekey = null;\n } else if (!cachekey) {\n cachekey = null;\n }\n }\n\n if (typeof nosessionupdate === \"undefined\") {\n nosessionupdate = false;\n }\n for (i = 0; i < requests.length; i++) {\n var request = requests[i];\n ajaxRequestData.push({\n index: i,\n methodname: request.methodname,\n args: request.args\n });\n request.nosessionupdate = nosessionupdate;\n request.deferred = $.Deferred();\n promises.push(request.deferred.promise());\n // Allow setting done and fail handlers as arguments.\n // This is just a shortcut for the calling code.\n if (typeof request.done !== \"undefined\") {\n request.deferred.done(request.done);\n }\n if (typeof request.fail !== \"undefined\") {\n request.deferred.fail(request.fail);\n }\n request.index = i;\n methodInfo.push(request.methodname);\n }\n\n if (methodInfo.length <= 5) {\n requestInfo = methodInfo.sort().join();\n } else {\n requestInfo = methodInfo.length + '-method-calls';\n }\n\n ajaxRequestData = JSON.stringify(ajaxRequestData);\n var settings = {\n type: 'POST',\n context: requests,\n dataType: 'json',\n processData: false,\n async: async,\n contentType: \"application/json\",\n timeout: timeout,\n headers: {\n pageparent: config.traceId || '',\n },\n };\n\n var script = 'service.php';\n var url = config.wwwroot + '/lib/ajax/';\n if (!loginrequired) {\n script = 'service-nologin.php';\n url += script + '?info=' + requestInfo;\n if (cachekey) {\n url += '&cachekey=' + cachekey;\n settings.type = 'GET';\n }\n } else {\n url += script + '?sesskey=' + config.sesskey + '&info=' + requestInfo;\n }\n\n if (nosessionupdate) {\n url += '&nosessionupdate=true';\n }\n\n if (settings.type === 'POST') {\n settings.data = ajaxRequestData;\n } else {\n var urlUseGet = url + '&args=' + encodeURIComponent(ajaxRequestData);\n\n if (urlUseGet.length > maxUrlLength) {\n settings.type = 'POST';\n settings.data = ajaxRequestData;\n } else {\n url = urlUseGet;\n }\n }\n\n // Jquery deprecated done and fail with async=false so we need to do this 2 ways.\n if (async) {\n $.ajax(url, settings)\n .done(requestSuccess)\n .fail(requestFail);\n } else {\n settings.success = requestSuccess;\n settings.error = requestFail;\n $.ajax(url, settings);\n }\n\n return promises;\n }\n };\n});\n"],"names":["define","$","config","Log","URL","unloading","requestSuccess","responses","request","response","nosessionupdate","exception","i","error","this","length","deferred","reject","Error","resolve","data","errorcode","forEach","window","location","relativeUrl","requestFail","jqXHR","textStatus","call","requests","async","loginrequired","timeout","cachekey","bind","ajaxRequestData","promises","methodInfo","requestInfo","parseInt","push","index","methodname","args","Deferred","promise","done","fail","sort","join","JSON","stringify","settings","type","context","dataType","processData","contentType","headers","pageparent","traceId","script","url","wwwroot","sesskey","urlUseGet","encodeURIComponent","ajax","success"],"mappings":"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"} \ No newline at end of file diff --git a/public/lib/amd/build/fetch.min.js b/public/lib/amd/build/fetch.min.js index d512928121a..32aa51e983e 100644 --- a/public/lib/amd/build/fetch.min.js +++ b/public/lib/amd/build/fetch.min.js @@ -1,3 +1,3 @@ -define("core/fetch",["exports","core/config","./pending"],(function(_exports,Cfg,_pending){var obj;function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _classStaticPrivateMethodGet(receiver,classConstructor,method){return function(receiver,classConstructor){if(receiver!==classConstructor)throw new TypeError("Private static access of wrong provenance")}(receiver,classConstructor),method}function _classPrivateFieldInitSpec(obj,privateMap,value){!function(obj,privateCollection){if(privateCollection.has(obj))throw new TypeError("Cannot initialize the same private elements twice on an object")}(obj,privateMap),privateMap.set(obj,value)}function _classPrivateFieldGet(receiver,privateMap){return function(receiver,descriptor){if(descriptor.get)return descriptor.get.call(receiver);return descriptor.value}(receiver,_classExtractFieldDescriptor(receiver,privateMap,"get"))}function _classPrivateFieldSet(receiver,privateMap,value){return function(receiver,descriptor,value){if(descriptor.set)descriptor.set.call(receiver,value);else{if(!descriptor.writable)throw new TypeError("attempted to set read only private field");descriptor.value=value}}(receiver,_classExtractFieldDescriptor(receiver,privateMap,"set"),value),value}function _classExtractFieldDescriptor(receiver,privateMap,action){if(!privateMap.has(receiver))throw new TypeError("attempted to "+action+" private field on non-instance");return privateMap.get(receiver)}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,Cfg=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(Cfg),_pending=(obj=_pending)&&obj.__esModule?obj:{default:obj};var _request=new WeakMap,_promise=new WeakMap,_resolve=new WeakMap,_reject=new WeakMap;class RequestWrapper{constructor(request){_classPrivateFieldInitSpec(this,_request,{writable:!0,value:null}),_classPrivateFieldInitSpec(this,_promise,{writable:!0,value:null}),_classPrivateFieldInitSpec(this,_resolve,{writable:!0,value:null}),_classPrivateFieldInitSpec(this,_reject,{writable:!0,value:null}),_classPrivateFieldSet(this,_request,request),_classPrivateFieldSet(this,_promise,new Promise(((resolve,reject)=>{_classPrivateFieldSet(this,_resolve,resolve),_classPrivateFieldSet(this,_reject,reject)})))}get request(){return _classPrivateFieldGet(this,_request)}get promise(){return _classPrivateFieldGet(this,_promise)}handleResponse(response){response.ok?_classPrivateFieldGet(this,_resolve).call(this,response):_classPrivateFieldGet(this,_reject).call(this,response.statusText)}}class Fetch{static async request(component,action){let{params:params={},body:body=null,method:method="GET"}=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};const pending=new _pending.default("Requesting ".concat(component,"/").concat(action," with ").concat(method)),requestWrapper=_classStaticPrivateMethodGet(Fetch,Fetch,_getRequest).call(Fetch,_classStaticPrivateMethodGet(Fetch,Fetch,_normaliseComponent).call(Fetch,component),action,{params:params,method:method,body:body}),result=await fetch(requestWrapper.request);return pending.resolve(),requestWrapper.handleResponse(result),requestWrapper.promise}static performGet(component,action){let{params:params={}}=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};return this.request(component,action,{params:params,method:"GET"})}static performHead(component,action){let{params:params={}}=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};return this.request(component,action,{params:params,method:"HEAD"})}static performPost(component,action){let{body:body}=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};return this.request(component,action,{body:body,method:"POST"})}static performPut(component,action){let{body:body}=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};return this.request(component,action,{body:body,method:"PUT"})}static performPatch(component,action){let{body:body}=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};return this.request(component,action,{body:body,method:"PATCH"})}static performDelete(component,action){let{params:params={},body:body=null}=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};return this.request(component,action,{body:body,params:params,method:"DELETE"})}}function _normaliseComponent(component){return component.replace(/^core_/,"")}function _getRequest(component,endpoint,_ref){let{params:params={},body:body=null,method:method="GET"}=_ref;const url=new URL("".concat(Cfg.apibase,"/rest/v2/").concat(component,"/").concat(endpoint)),options={method:method,headers:{Accept:"application/json","Content-Type":"application/json"}};return Object.entries(params).forEach((_ref2=>{let[key,value]=_ref2;url.searchParams.append(key,value)})),body&&(body instanceof FormData?options.body=body:options.body=body instanceof Object?JSON.stringify(body):body),new RequestWrapper(new Request(url,options))}return _exports.default=Fetch,_exports.default})); +define("core/fetch",["exports","core/config","./pending"],(function(_exports,Cfg,_pending){var obj;function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _classStaticPrivateMethodGet(receiver,classConstructor,method){return function(receiver,classConstructor){if(receiver!==classConstructor)throw new TypeError("Private static access of wrong provenance")}(receiver,classConstructor),method}function _classPrivateFieldInitSpec(obj,privateMap,value){!function(obj,privateCollection){if(privateCollection.has(obj))throw new TypeError("Cannot initialize the same private elements twice on an object")}(obj,privateMap),privateMap.set(obj,value)}function _classPrivateFieldGet(receiver,privateMap){return function(receiver,descriptor){if(descriptor.get)return descriptor.get.call(receiver);return descriptor.value}(receiver,_classExtractFieldDescriptor(receiver,privateMap,"get"))}function _classPrivateFieldSet(receiver,privateMap,value){return function(receiver,descriptor,value){if(descriptor.set)descriptor.set.call(receiver,value);else{if(!descriptor.writable)throw new TypeError("attempted to set read only private field");descriptor.value=value}}(receiver,_classExtractFieldDescriptor(receiver,privateMap,"set"),value),value}function _classExtractFieldDescriptor(receiver,privateMap,action){if(!privateMap.has(receiver))throw new TypeError("attempted to "+action+" private field on non-instance");return privateMap.get(receiver)}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,Cfg=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(Cfg),_pending=(obj=_pending)&&obj.__esModule?obj:{default:obj};var _request=new WeakMap,_promise=new WeakMap,_resolve=new WeakMap,_reject=new WeakMap;class RequestWrapper{constructor(request){_classPrivateFieldInitSpec(this,_request,{writable:!0,value:null}),_classPrivateFieldInitSpec(this,_promise,{writable:!0,value:null}),_classPrivateFieldInitSpec(this,_resolve,{writable:!0,value:null}),_classPrivateFieldInitSpec(this,_reject,{writable:!0,value:null}),_classPrivateFieldSet(this,_request,request),_classPrivateFieldSet(this,_promise,new Promise(((resolve,reject)=>{_classPrivateFieldSet(this,_resolve,resolve),_classPrivateFieldSet(this,_reject,reject)})))}get request(){return _classPrivateFieldGet(this,_request)}get promise(){return _classPrivateFieldGet(this,_promise)}handleResponse(response){response.ok?_classPrivateFieldGet(this,_resolve).call(this,response):_classPrivateFieldGet(this,_reject).call(this,response.statusText)}}class Fetch{static async request(component,action){let{params:params={},body:body=null,method:method="GET"}=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};const pending=new _pending.default("Requesting ".concat(component,"/").concat(action," with ").concat(method)),requestWrapper=_classStaticPrivateMethodGet(Fetch,Fetch,_getRequest).call(Fetch,_classStaticPrivateMethodGet(Fetch,Fetch,_normaliseComponent).call(Fetch,component),action,{params:params,method:method,body:body}),result=await fetch(requestWrapper.request);return pending.resolve(),requestWrapper.handleResponse(result),requestWrapper.promise}static performGet(component,action){let{params:params={}}=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};return this.request(component,action,{params:params,method:"GET"})}static performHead(component,action){let{params:params={}}=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};return this.request(component,action,{params:params,method:"HEAD"})}static performPost(component,action){let{body:body}=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};return this.request(component,action,{body:body,method:"POST"})}static performPut(component,action){let{body:body}=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};return this.request(component,action,{body:body,method:"PUT"})}static performPatch(component,action){let{body:body}=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};return this.request(component,action,{body:body,method:"PATCH"})}static performDelete(component,action){let{params:params={},body:body=null}=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};return this.request(component,action,{body:body,params:params,method:"DELETE"})}}function _normaliseComponent(component){return component.replace(/^core_/,"")}function _getRequest(component,endpoint,_ref){let{params:params={},body:body=null,method:method="GET"}=_ref;const url=new URL("".concat(Cfg.apibase,"/rest/v2/").concat(component,"/").concat(endpoint)),options={method:method,headers:{Accept:"application/json","Content-Type":"application/json",pageparent:Cfg.traceId||""}};return Object.entries(params).forEach((_ref2=>{let[key,value]=_ref2;url.searchParams.append(key,value)})),body&&(body instanceof FormData?options.body=body:options.body=body instanceof Object?JSON.stringify(body):body),new RequestWrapper(new Request(url,options))}return _exports.default=Fetch,_exports.default})); //# sourceMappingURL=fetch.min.js.map \ No newline at end of file diff --git a/public/lib/amd/build/fetch.min.js.map b/public/lib/amd/build/fetch.min.js.map index 3814206a2fa..029e6e262cc 100644 --- a/public/lib/amd/build/fetch.min.js.map +++ b/public/lib/amd/build/fetch.min.js.map @@ -1 +1 @@ -{"version":3,"file":"fetch.min.js","sources":["../src/fetch.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * The core/fetch module allows you to make web service requests to the Moodle API.\n *\n * @module core/fetch\n * @copyright Andrew Lyons \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n * @example Perform a single GET request\n * import Fetch from 'core/fetch';\n *\n * const result = Fetch.performGet('mod_example', 'animals', { params: { type: 'mammal' } });\n *\n * result.then((response) => {\n * // Do something with the Response object.\n * })\n * .catch((error) => {\n * // Handle the error\n * });\n */\n\nimport * as Cfg from 'core/config';\nimport PendingPromise from './pending';\n\n/**\n * A wrapper around the Request, including a Promise that is resolved when the request is complete.\n *\n * @class RequestWrapper\n * @private\n */\nclass RequestWrapper {\n /** @var {Request} */\n #request = null;\n\n /** @var {Promise} */\n #promise = null;\n\n /** @var {Function} */\n #resolve = null;\n\n /** @var {Function} */\n #reject = null;\n\n /**\n * Create a new RequestWrapper.\n *\n * @param {Request} request The request object that is wrapped\n */\n constructor(request) {\n this.#request = request;\n this.#promise = new Promise((resolve, reject) => {\n this.#resolve = resolve;\n this.#reject = reject;\n });\n }\n\n /**\n * Get the wrapped Request.\n *\n * @returns {Request}\n * @private\n */\n get request() {\n return this.#request;\n }\n\n /**\n * Get the Promise link to this request.\n *\n * @return {Promise}\n * @private\n */\n get promise() {\n return this.#promise;\n }\n\n /**\n * Handle the response from the request.\n *\n * @param {Response} response\n * @private\n */\n handleResponse(response) {\n if (response.ok) {\n this.#resolve(response);\n } else {\n this.#reject(response.statusText);\n }\n }\n}\n\n/**\n * A class to handle requests to the Moodle REST API.\n *\n * @class Fetch\n */\nexport default class Fetch {\n /**\n * Make a single request to the Moodle API.\n *\n * @param {string} component The frankenstyle component name\n * @param {string} action The component action to perform\n * @param {object} params\n * @param {object} [params.params = {}] The parameters to pass to the API\n * @param {string|Object|FormData} [params.body = null] The HTTP method to use\n * @param {string} [params.method = \"GET\"] The HTTP method to use\n * @returns {Promise} A promise that resolves to the Response object for the request\n */\n static async request(\n component,\n action,\n {\n params = {},\n body = null,\n method = 'GET',\n } = {},\n ) {\n const pending = new PendingPromise(`Requesting ${component}/${action} with ${method}`);\n const requestWrapper = Fetch.#getRequest(\n Fetch.#normaliseComponent(component),\n action,\n { params, method, body },\n );\n const result = await fetch(requestWrapper.request);\n\n pending.resolve();\n\n requestWrapper.handleResponse(result);\n\n return requestWrapper.promise;\n }\n\n /**\n * Make a request to the Moodle API.\n *\n * @param {string} component The frankenstyle component name\n * @param {string} action The component action to perform\n * @param {object} params\n * @param {object} [params.params = {}] The parameters to pass to the API\n * @returns {Promise} A promise that resolves to the Response object for the request\n */\n static performGet(\n component,\n action,\n {\n params = {},\n } = {},\n ) {\n return this.request(\n component,\n action,\n { params, method: 'GET' },\n );\n }\n\n /**\n * Make a request to the Moodle API.\n *\n * @param {string} component The frankenstyle component name\n * @param {string} action The component action to perform\n * @param {object} params\n * @param {object} [params.params = {}] The parameters to pass to the API\n * @returns {Promise} A promise that resolves to the Response object for the request\n */\n static performHead(\n component,\n action,\n {\n params = {},\n } = {},\n ) {\n return this.request(\n component,\n action,\n { params, method: 'HEAD' },\n );\n }\n\n /**\n * Make a request to the Moodle API.\n *\n * @param {string} component The frankenstyle component name\n * @param {string} action The component action to perform\n * @param {object} params\n * @param {string|Object|FormData} params.body The HTTP method to use\n * @returns {Promise} A promise that resolves to the Response object for the request\n */\n static performPost(\n component,\n action,\n {\n body,\n } = {},\n ) {\n return this.request(\n component,\n action,\n { body, method: 'POST' },\n );\n }\n\n /**\n * Make a request to the Moodle API.\n *\n * @param {string} component The frankenstyle component name\n * @param {string} action The component action to perform\n * @param {object} params\n * @param {string|Object|FormData} params.body The HTTP method to use\n * @returns {Promise} A promise that resolves to the Response object for the request\n */\n static performPut(\n component,\n action,\n {\n body,\n } = {},\n ) {\n return this.request(\n component,\n action,\n { body, method: 'PUT' },\n );\n }\n\n /**\n * Make a PATCH request to the Moodle API.\n *\n * @param {string} component The frankenstyle component name\n * @param {string} action The component action to perform\n * @param {object} params\n * @param {string|Object|FormData} params.body The HTTP method to use\n * @returns {Promise} A promise that resolves to the Response object for the request\n */\n static performPatch(\n component,\n action,\n {\n body,\n } = {},\n ) {\n return this.request(\n component,\n action,\n { body, method: 'PATCH' },\n );\n }\n\n /**\n * Make a request to the Moodle API.\n *\n * @param {string} component The frankenstyle component name\n * @param {string} action The component action to perform\n * @param {object} params\n * @param {object} [params.params = {}] The parameters to pass to the API\n * @param {string|Object|FormData} [params.body = null] The HTTP method to use\n * @returns {Promise} A promise that resolves to the Response object for the request\n */\n static performDelete(\n component,\n action,\n {\n params = {},\n body = null,\n } = {},\n ) {\n return this.request(\n component,\n action,\n {\n body,\n params,\n method: 'DELETE',\n },\n );\n }\n\n /**\n * Normalise the component name to remove the core_ prefix.\n *\n * @param {string} component\n * @returns {string}\n */\n static #normaliseComponent(component) {\n return component.replace(/^core_/, '');\n }\n\n /**\n * Get the Request for a given API request.\n *\n * @param {string} component The frankenstyle component name\n * @param {string} endpoint The endpoint within the componet to call\n * @param {object} params\n * @param {object} [params.params = {}] The parameters to pass to the API\n * @param {string|Object|FormData} [params.body = null] The HTTP method to use\n * @param {string} [params.method = \"GET\"] The HTTP method to use\n * @returns {RequestWrapper}\n */\n static #getRequest(\n component,\n endpoint,\n {\n params = {},\n body = null,\n method = 'GET',\n }\n ) {\n const url = new URL(`${Cfg.apibase}/rest/v2/${component}/${endpoint}`);\n const options = {\n method,\n headers: {\n 'Accept': 'application/json',\n 'Content-Type': 'application/json',\n },\n };\n\n Object.entries(params).forEach(([key, value]) => {\n url.searchParams.append(key, value);\n });\n\n if (body) {\n if (body instanceof FormData) {\n options.body = body;\n } else if (body instanceof Object) {\n options.body = JSON.stringify(body);\n } else {\n options.body = body;\n }\n }\n\n return new RequestWrapper(new Request(url, options));\n }\n}\n"],"names":["RequestWrapper","constructor","request","Promise","resolve","reject","this","promise","handleResponse","response","ok","statusText","Fetch","component","action","params","body","method","pending","PendingPromise","requestWrapper","result","fetch","replace","endpoint","url","URL","Cfg","apibase","options","headers","Object","entries","forEach","_ref2","key","value","searchParams","append","FormData","JSON","stringify","Request"],"mappings":"y/EA2CMA,eAkBFC,YAAYC,qEAhBD,mEAGA,mEAGA,kEAGD,2CAQUA,6CACA,IAAIC,SAAQ,CAACC,QAASC,8CAClBD,4CACDC,YAUnBH,2CACOI,eASPC,2CACOD,eASXE,eAAeC,UACPA,SAASC,kDACKD,wDAEDA,SAASE,mBAUbC,2BAabC,UACAC,YACAC,OACIA,OAAS,GADbC,KAEIA,KAAO,KAFXC,OAGIA,OAAS,8DACT,SAEEC,QAAU,IAAIC,sCAA6BN,sBAAaC,wBAAeG,SACvEG,4CAAiBR,MAtBVA,wBAsBUA,mCACnBA,MAvBSA,gCAuBTA,MAA0BC,WAC1BC,OACA,CAAEC,OAAAA,OAAQE,OAAAA,OAAQD,KAAAA,OAEhBK,aAAeC,MAAMF,eAAelB,gBAE1CgB,QAAQd,UAERgB,eAAeZ,eAAea,QAEvBD,eAAeb,0BAatBM,UACAC,YACAC,OACIA,OAAS,2DACT,UAEGT,KAAKJ,QACRW,UACAC,OACA,CAAEC,OAAAA,OAAQE,OAAQ,2BActBJ,UACAC,YACAC,OACIA,OAAS,2DACT,UAEGT,KAAKJ,QACRW,UACAC,OACA,CAAEC,OAAAA,OAAQE,OAAQ,4BActBJ,UACAC,YACAE,KACIA,6DACA,UAEGV,KAAKJ,QACRW,UACAC,OACA,CAAEE,KAAAA,KAAMC,OAAQ,2BAcpBJ,UACAC,YACAE,KACIA,6DACA,UAEGV,KAAKJ,QACRW,UACAC,OACA,CAAEE,KAAAA,KAAMC,OAAQ,4BAcpBJ,UACAC,YACAE,KACIA,6DACA,UAEGV,KAAKJ,QACRW,UACAC,OACA,CAAEE,KAAAA,KAAMC,OAAQ,+BAepBJ,UACAC,YACAC,OACIA,OAAS,GADbC,KAEIA,KAAO,6DACP,UAEGV,KAAKJ,QACRW,UACAC,OACA,CACIE,KAAAA,KACAD,OAAAA,OACAE,OAAQ,yCAWOJ,kBAChBA,UAAUU,QAAQ,SAAU,yBAenCV,UACAW,mBACAT,OACIA,OAAS,GADbC,KAEIA,KAAO,KAFXC,OAGIA,OAAS,kBAGPQ,IAAM,IAAIC,cAAOC,IAAIC,4BAAmBf,sBAAaW,WACrDK,QAAU,CACZZ,OAAAA,OACAa,QAAS,QACK,kCACM,4BAIxBC,OAAOC,QAAQjB,QAAQkB,SAAQC,YAAEC,IAAKC,aAClCX,IAAIY,aAAaC,OAAOH,IAAKC,UAG7BpB,OACIA,gBAAgBuB,SAChBV,QAAQb,KAAOA,KAEfa,QAAQb,KADDA,gBAAgBe,OACRS,KAAKC,UAAUzB,MAEfA,MAIhB,IAAIhB,eAAe,IAAI0C,QAAQjB,IAAKI"} \ No newline at end of file +{"version":3,"file":"fetch.min.js","sources":["../src/fetch.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * The core/fetch module allows you to make web service requests to the Moodle API.\n *\n * @module core/fetch\n * @copyright Andrew Lyons \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n * @example Perform a single GET request\n * import Fetch from 'core/fetch';\n *\n * const result = Fetch.performGet('mod_example', 'animals', { params: { type: 'mammal' } });\n *\n * result.then((response) => {\n * // Do something with the Response object.\n * })\n * .catch((error) => {\n * // Handle the error\n * });\n */\n\nimport * as Cfg from 'core/config';\nimport PendingPromise from './pending';\n\n/**\n * A wrapper around the Request, including a Promise that is resolved when the request is complete.\n *\n * @class RequestWrapper\n * @private\n */\nclass RequestWrapper {\n /** @var {Request} */\n #request = null;\n\n /** @var {Promise} */\n #promise = null;\n\n /** @var {Function} */\n #resolve = null;\n\n /** @var {Function} */\n #reject = null;\n\n /**\n * Create a new RequestWrapper.\n *\n * @param {Request} request The request object that is wrapped\n */\n constructor(request) {\n this.#request = request;\n this.#promise = new Promise((resolve, reject) => {\n this.#resolve = resolve;\n this.#reject = reject;\n });\n }\n\n /**\n * Get the wrapped Request.\n *\n * @returns {Request}\n * @private\n */\n get request() {\n return this.#request;\n }\n\n /**\n * Get the Promise link to this request.\n *\n * @return {Promise}\n * @private\n */\n get promise() {\n return this.#promise;\n }\n\n /**\n * Handle the response from the request.\n *\n * @param {Response} response\n * @private\n */\n handleResponse(response) {\n if (response.ok) {\n this.#resolve(response);\n } else {\n this.#reject(response.statusText);\n }\n }\n}\n\n/**\n * A class to handle requests to the Moodle REST API.\n *\n * @class Fetch\n */\nexport default class Fetch {\n /**\n * Make a single request to the Moodle API.\n *\n * @param {string} component The frankenstyle component name\n * @param {string} action The component action to perform\n * @param {object} params\n * @param {object} [params.params = {}] The parameters to pass to the API\n * @param {string|Object|FormData} [params.body = null] The HTTP method to use\n * @param {string} [params.method = \"GET\"] The HTTP method to use\n * @returns {Promise} A promise that resolves to the Response object for the request\n */\n static async request(\n component,\n action,\n {\n params = {},\n body = null,\n method = 'GET',\n } = {},\n ) {\n const pending = new PendingPromise(`Requesting ${component}/${action} with ${method}`);\n const requestWrapper = Fetch.#getRequest(\n Fetch.#normaliseComponent(component),\n action,\n { params, method, body },\n );\n const result = await fetch(requestWrapper.request);\n\n pending.resolve();\n\n requestWrapper.handleResponse(result);\n\n return requestWrapper.promise;\n }\n\n /**\n * Make a request to the Moodle API.\n *\n * @param {string} component The frankenstyle component name\n * @param {string} action The component action to perform\n * @param {object} params\n * @param {object} [params.params = {}] The parameters to pass to the API\n * @returns {Promise} A promise that resolves to the Response object for the request\n */\n static performGet(\n component,\n action,\n {\n params = {},\n } = {},\n ) {\n return this.request(\n component,\n action,\n { params, method: 'GET' },\n );\n }\n\n /**\n * Make a request to the Moodle API.\n *\n * @param {string} component The frankenstyle component name\n * @param {string} action The component action to perform\n * @param {object} params\n * @param {object} [params.params = {}] The parameters to pass to the API\n * @returns {Promise} A promise that resolves to the Response object for the request\n */\n static performHead(\n component,\n action,\n {\n params = {},\n } = {},\n ) {\n return this.request(\n component,\n action,\n { params, method: 'HEAD' },\n );\n }\n\n /**\n * Make a request to the Moodle API.\n *\n * @param {string} component The frankenstyle component name\n * @param {string} action The component action to perform\n * @param {object} params\n * @param {string|Object|FormData} params.body The HTTP method to use\n * @returns {Promise} A promise that resolves to the Response object for the request\n */\n static performPost(\n component,\n action,\n {\n body,\n } = {},\n ) {\n return this.request(\n component,\n action,\n { body, method: 'POST' },\n );\n }\n\n /**\n * Make a request to the Moodle API.\n *\n * @param {string} component The frankenstyle component name\n * @param {string} action The component action to perform\n * @param {object} params\n * @param {string|Object|FormData} params.body The HTTP method to use\n * @returns {Promise} A promise that resolves to the Response object for the request\n */\n static performPut(\n component,\n action,\n {\n body,\n } = {},\n ) {\n return this.request(\n component,\n action,\n { body, method: 'PUT' },\n );\n }\n\n /**\n * Make a PATCH request to the Moodle API.\n *\n * @param {string} component The frankenstyle component name\n * @param {string} action The component action to perform\n * @param {object} params\n * @param {string|Object|FormData} params.body The HTTP method to use\n * @returns {Promise} A promise that resolves to the Response object for the request\n */\n static performPatch(\n component,\n action,\n {\n body,\n } = {},\n ) {\n return this.request(\n component,\n action,\n { body, method: 'PATCH' },\n );\n }\n\n /**\n * Make a request to the Moodle API.\n *\n * @param {string} component The frankenstyle component name\n * @param {string} action The component action to perform\n * @param {object} params\n * @param {object} [params.params = {}] The parameters to pass to the API\n * @param {string|Object|FormData} [params.body = null] The HTTP method to use\n * @returns {Promise} A promise that resolves to the Response object for the request\n */\n static performDelete(\n component,\n action,\n {\n params = {},\n body = null,\n } = {},\n ) {\n return this.request(\n component,\n action,\n {\n body,\n params,\n method: 'DELETE',\n },\n );\n }\n\n /**\n * Normalise the component name to remove the core_ prefix.\n *\n * @param {string} component\n * @returns {string}\n */\n static #normaliseComponent(component) {\n return component.replace(/^core_/, '');\n }\n\n /**\n * Get the Request for a given API request.\n *\n * @param {string} component The frankenstyle component name\n * @param {string} endpoint The endpoint within the componet to call\n * @param {object} params\n * @param {object} [params.params = {}] The parameters to pass to the API\n * @param {string|Object|FormData} [params.body = null] The HTTP method to use\n * @param {string} [params.method = \"GET\"] The HTTP method to use\n * @returns {RequestWrapper}\n */\n static #getRequest(\n component,\n endpoint,\n {\n params = {},\n body = null,\n method = 'GET',\n }\n ) {\n const url = new URL(`${Cfg.apibase}/rest/v2/${component}/${endpoint}`);\n const options = {\n method,\n headers: {\n 'Accept': 'application/json',\n 'Content-Type': 'application/json',\n // Note: Do not use traceparent as this is used by the telemetry system to link requests from the\n // same request together.\n 'pageparent': Cfg.traceId || '',\n },\n };\n\n Object.entries(params).forEach(([key, value]) => {\n url.searchParams.append(key, value);\n });\n\n if (body) {\n if (body instanceof FormData) {\n options.body = body;\n } else if (body instanceof Object) {\n options.body = JSON.stringify(body);\n } else {\n options.body = body;\n }\n }\n\n return new RequestWrapper(new Request(url, options));\n }\n}\n"],"names":["RequestWrapper","constructor","request","Promise","resolve","reject","this","promise","handleResponse","response","ok","statusText","Fetch","component","action","params","body","method","pending","PendingPromise","requestWrapper","result","fetch","replace","endpoint","url","URL","Cfg","apibase","options","headers","traceId","Object","entries","forEach","_ref2","key","value","searchParams","append","FormData","JSON","stringify","Request"],"mappings":"y/EA2CMA,eAkBFC,YAAYC,qEAhBD,mEAGA,mEAGA,kEAGD,2CAQUA,6CACA,IAAIC,SAAQ,CAACC,QAASC,8CAClBD,4CACDC,YAUnBH,2CACOI,eASPC,2CACOD,eASXE,eAAeC,UACPA,SAASC,kDACKD,wDAEDA,SAASE,mBAUbC,2BAabC,UACAC,YACAC,OACIA,OAAS,GADbC,KAEIA,KAAO,KAFXC,OAGIA,OAAS,8DACT,SAEEC,QAAU,IAAIC,sCAA6BN,sBAAaC,wBAAeG,SACvEG,4CAAiBR,MAtBVA,wBAsBUA,mCACnBA,MAvBSA,gCAuBTA,MAA0BC,WAC1BC,OACA,CAAEC,OAAAA,OAAQE,OAAAA,OAAQD,KAAAA,OAEhBK,aAAeC,MAAMF,eAAelB,gBAE1CgB,QAAQd,UAERgB,eAAeZ,eAAea,QAEvBD,eAAeb,0BAatBM,UACAC,YACAC,OACIA,OAAS,2DACT,UAEGT,KAAKJ,QACRW,UACAC,OACA,CAAEC,OAAAA,OAAQE,OAAQ,2BActBJ,UACAC,YACAC,OACIA,OAAS,2DACT,UAEGT,KAAKJ,QACRW,UACAC,OACA,CAAEC,OAAAA,OAAQE,OAAQ,4BActBJ,UACAC,YACAE,KACIA,6DACA,UAEGV,KAAKJ,QACRW,UACAC,OACA,CAAEE,KAAAA,KAAMC,OAAQ,2BAcpBJ,UACAC,YACAE,KACIA,6DACA,UAEGV,KAAKJ,QACRW,UACAC,OACA,CAAEE,KAAAA,KAAMC,OAAQ,4BAcpBJ,UACAC,YACAE,KACIA,6DACA,UAEGV,KAAKJ,QACRW,UACAC,OACA,CAAEE,KAAAA,KAAMC,OAAQ,+BAepBJ,UACAC,YACAC,OACIA,OAAS,GADbC,KAEIA,KAAO,6DACP,UAEGV,KAAKJ,QACRW,UACAC,OACA,CACIE,KAAAA,KACAD,OAAAA,OACAE,OAAQ,yCAWOJ,kBAChBA,UAAUU,QAAQ,SAAU,yBAenCV,UACAW,mBACAT,OACIA,OAAS,GADbC,KAEIA,KAAO,KAFXC,OAGIA,OAAS,kBAGPQ,IAAM,IAAIC,cAAOC,IAAIC,4BAAmBf,sBAAaW,WACrDK,QAAU,CACZZ,OAAAA,OACAa,QAAS,QACK,kCACM,8BAGFH,IAAII,SAAW,YAIrCC,OAAOC,QAAQlB,QAAQmB,SAAQC,YAAEC,IAAKC,aAClCZ,IAAIa,aAAaC,OAAOH,IAAKC,UAG7BrB,OACIA,gBAAgBwB,SAChBX,QAAQb,KAAOA,KAEfa,QAAQb,KADDA,gBAAgBgB,OACRS,KAAKC,UAAU1B,MAEfA,MAIhB,IAAIhB,eAAe,IAAI2C,QAAQlB,IAAKI"} \ No newline at end of file diff --git a/public/lib/amd/src/ajax.js b/public/lib/amd/src/ajax.js index 9a32c1aea67..02c1d5ddc8b 100644 --- a/public/lib/amd/src/ajax.js +++ b/public/lib/amd/src/ajax.js @@ -261,7 +261,10 @@ define(['jquery', 'core/config', 'core/log', 'core/url'], function($, config, Lo processData: false, async: async, contentType: "application/json", - timeout: timeout + timeout: timeout, + headers: { + pageparent: config.traceId || '', + }, }; var script = 'service.php'; diff --git a/public/lib/amd/src/fetch.js b/public/lib/amd/src/fetch.js index 8b7c3be5c52..02145f4d24c 100644 --- a/public/lib/amd/src/fetch.js +++ b/public/lib/amd/src/fetch.js @@ -323,6 +323,9 @@ export default class Fetch { headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', + // Note: Do not use traceparent as this is used by the telemetry system to link requests from the + // same request together. + 'pageparent': Cfg.traceId || '', }, }; diff --git a/public/lib/classes/component.php b/public/lib/classes/component.php index b5a41b17c2e..142eb84925f 100644 --- a/public/lib/classes/component.php +++ b/public/lib/classes/component.php @@ -574,7 +574,7 @@ class component { // Always keep moodle_exception in place. $keyclasses = [ \core\exception\moodle_exception::class, - \core\router\middleware\api_validation_middleware::class, + \core\telemetry::class, ]; foreach ($keyclasses as $classname) { if (!array_key_exists($classname, $cache['classmap'])) { diff --git a/public/lib/classes/output/core_renderer.php b/public/lib/classes/output/core_renderer.php index f481946e502..43f8fea7c8e 100644 --- a/public/lib/classes/output/core_renderer.php +++ b/public/lib/classes/output/core_renderer.php @@ -4001,6 +4001,21 @@ EOD; return !empty($this->communication_link()); } + /** + * Returns the telemetry trace id for the current page, if available. + * + * This can be used to correlate logs and telemetry data with specific page views. + * It is typically presented in the footer or somewhere inconspicious so that user's experiencing difficulties + * may be asked for it. + * + * The value is also passed in the Response header if required. + * + * @return string|null + */ + public function telemetry_traceid(): ?string { + return \core\telemetry::get_page_id(); + } + /** * Returns the communication link, complete with html. * diff --git a/public/lib/classes/output/requirements/page_requirements_manager.php b/public/lib/classes/output/requirements/page_requirements_manager.php index 9b6f248281f..9607c4f503c 100644 --- a/public/lib/classes/output/requirements/page_requirements_manager.php +++ b/public/lib/classes/output/requirements/page_requirements_manager.php @@ -338,6 +338,7 @@ class page_requirements_manager { 'siteId' => (int) SITEID, 'userId' => (int) $USER->id, 'deprecationignorelist' => !empty($CFG->jsdeprecationignorelist) ? $CFG->jsdeprecationignorelist : [], + 'traceId' => \core\telemetry::get_trace_parent_id(), ]; if ($CFG->debugdeveloper) { $this->M_cfg['developerdebug'] = true; diff --git a/public/lib/classes/shutdown_manager.php b/public/lib/classes/shutdown_manager.php index 071700fc355..a64929b71ea 100644 --- a/public/lib/classes/shutdown_manager.php +++ b/public/lib/classes/shutdown_manager.php @@ -43,7 +43,7 @@ class shutdown_manager { */ public static function initialize(): void { if (self::$registered) { - debugging('Shutdown manager is already initialised!'); + self::log('Shutdown manager is already initialised!'); return; } self::$registered = true; @@ -76,6 +76,15 @@ class shutdown_manager { } } + /** + * Whether the shutdown manager initialized. + * + * @return bool + */ + public static function is_initialized(): bool { + return self::$registered; + } + /** * Signal handler for SIGINT, and SIGTERM. * @@ -169,12 +178,17 @@ class shutdown_manager { public static function shutdown_handler(): void { global $DB; - // In case we caught an out of memory shutdown we increase memory limit to unlimited, so we can gracefully shut down. - raise_memory_limit(MEMORY_UNLIMITED); + if (function_exists('raise_memory_limit')) { + // In case we caught an out of memory shutdown we increase memory limit to unlimited, + // so we can gracefully shut down. + raise_memory_limit(MEMORY_UNLIMITED); + } - // Always ensure we know who the user is in access logs even if they - // were logged in a weird way midway through the request. - set_access_log_user(); + if (function_exists('set_access_log_user')) { + // Always ensure we know who the user is in access logs even if they + // were logged in a weird way midway through the request. + set_access_log_user(); + } // Custom stuff first. foreach (self::$callbacks as $data) { @@ -189,7 +203,7 @@ class shutdown_manager { // Handle DB transactions, session need to be written afterwards // in order to maintain consistency in all session handlers. - if ($DB->is_transaction_started()) { + if ($DB && $DB->is_transaction_started()) { if (!defined('PHPUNIT_TEST') || !PHPUNIT_TEST) { // This should not happen, it usually indicates wrong catching of exceptions, // because all transactions should be finished manually or in default exception handler. @@ -242,7 +256,12 @@ class shutdown_manager { // phpcs:ignore moodle.PHP.ForbiddenFunctions.FoundWithAlternative error_log('Mem usage over ' . $apachereleasemem . ': marking Apache child for reaping.'); } - if (MDL_PERFTOLOG) { + + $logperformance = MDL_PERFTOLOG; + $logperformance = $logperformance || !empty($PERF->perfdebugdeferred); + $logperformance = $logperformance && function_exists('get_performance_info'); + + if ($logperformance) { $perf = get_performance_info(); // phpcs:ignore moodle.PHP.ForbiddenFunctions.FoundWithAlternative error_log("PERF: " . $perf['txt']); @@ -251,6 +270,7 @@ class shutdown_manager { $perf = get_performance_info(); echo $OUTPUT->select_element_for_replace('#perfdebugfooter', $perf['html']); } + if (MDL_PERFINC) { $inc = get_included_files(); $ts = 0; @@ -274,9 +294,11 @@ class shutdown_manager { } } - // Close the current streaming element if any. - if ($OUTPUT->has_started()) { - echo $OUTPUT->close_element_for_append(); + if ($OUTPUT) { + // Close the current streaming element if any. + if ($OUTPUT->has_started()) { + echo $OUTPUT->close_element_for_append(); + } } // Print any closing buffered tags. @@ -284,6 +306,22 @@ class shutdown_manager { echo $CFG->closingtags; } } + + /** + * Logging for the shutdown manager. + * + * @param string $value + */ + protected static function log(string $value): void { + if (function_exists('debugging')) { + // Use Moodle's debugging function if available. + debugging($value, DEBUG_DEVELOPER); + } else { + // Fallback to error_log if debugging is not available. + // This is useful for older PHP versions or when debugging is not set up. + error_log($value); // phpcs:ignore moodle.PHP.ForbiddenFunctions.FoundWithAlternative + } + } } // Alias this class to the old name. diff --git a/public/lib/classes/telemetry.php b/public/lib/classes/telemetry.php new file mode 100644 index 00000000000..25c1b1197fa --- /dev/null +++ b/public/lib/classes/telemetry.php @@ -0,0 +1,104 @@ +. + +namespace core; + +use OpenTelemetry\API\Globals; +use OpenTelemetry\API\Trace\LocalRootSpan; +use OpenTelemetry\API\Trace\Propagation\TraceContextPropagator; +use OpenTelemetry\API\Trace\Span; +use OpenTelemetry\Context\Context; + +/** + * OpenTelemetry Telemetry manager class for Moodle. + * + * This class acts as a central point for managing telemetry in Moodle, + * providing methods to initialize the telemetry system, get the current page ID, and record exceptions. + * + * @package core + * @copyright Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class telemetry { + /** + * Get the page ID for the current request. + * + * If OpenTelemetry is not configured, or there is no current root span, this will return null. + * + * @return string|null + */ + public static function get_page_id(): ?string { + if (!static::is_available()) { + return null; + } + + // Fetch the current root span, and return the trace ID as the page ID. + $rootspan = LocalRootSpan::current(); + $rootcontext = $rootspan->getContext(); + if ($rootcontext->isValid() === false) { + return null; + } + + return $rootcontext->getTraceId(); + } + + /** + * A helper method to get the trace id for the current request. + * + * @return string|null Null if OpenTelemetry is not configured + */ + public static function get_trace_parent_id(): ?string { + if (!static::is_available()) { + return null; + } + + $headers = []; + Globals::propagator()->inject($headers); + + return $headers[TraceContextPropagator::TRACEPARENT] ?? null; + } + + /** + * Whether Telemetry is available and configured. + * + * @return bool + */ + public static function is_available(): bool { + if (!class_exists(Globals::class)) { + return false; + } + + if (extension_loaded('opentelemetry') === false) { + return false; + } + + return true; + } + + /** + * Record an error, or exception to the current span. + * + * @param \Throwable $ex The exception to record. + */ + public static function record_throwable(\Throwable $ex): void { + if (!static::is_available()) { + return; + } + + $span = Span::fromContext(Context::getCurrent()); + $span->recordException($ex); + } +} diff --git a/public/lib/setup.php b/public/lib/setup.php index 17aaf0189df..122b511e57c 100644 --- a/public/lib/setup.php +++ b/public/lib/setup.php @@ -592,7 +592,12 @@ ini_set('include_path', $CFG->libdir . '/pear' . PATH_SEPARATOR . ini_get('inclu // Register our classloader. \core\component::register_autoloader(); -// Early profiling start, based exclusively on config.php $CFG settings +// Register our shutdown manager, do NOT use register_shutdown_function(). +if (\core\shutdown_manager::is_initialized() === false) { + \core\shutdown_manager::initialize(); +} + +// Early profiling start, based exclusively on config.php $CFG settings. if (!empty($CFG->earlyprofilingenabled) && !defined('ABORT_AFTER_CONFIG_CANCEL')) { require_once($CFG->libdir . '/xhprof/xhprof_moodle.php'); profiling_start(); @@ -778,10 +783,7 @@ if (!isset($CFG->debugdisplay)) { ini_set('display_errors', '1'); } -// Register our shutdown manager, do NOT use register_shutdown_function(). -\core\shutdown_manager::initialize(); - -// Verify upgrade is not running unless we are in a script that needs to execute in any case +// Verify upgrade is not running unless we are in a script that needs to execute in any case. if (!defined('NO_UPGRADE_CHECK') and isset($CFG->upgraderunning)) { if ($CFG->upgraderunning < time()) { unset_config('upgraderunning'); diff --git a/public/lib/setuplib.php b/public/lib/setuplib.php index 037fe6c1de4..61392516e82 100644 --- a/public/lib/setuplib.php +++ b/public/lib/setuplib.php @@ -136,7 +136,10 @@ function get_whoops(): ?\Whoops\Run { function default_exception_handler(Throwable $ex): void { global $CFG, $DB, $OUTPUT, $USER, $FULLME, $SESSION, $PAGE; - // detect active db transactions, rollback and log as error + // Record the throwable in OpenTelemetry if it's available. + \core\telemetry::record_throwable($ex); + + // Detect active db transactions, rollback and log as error. abort_all_db_transactions(); if (($ex instanceof required_capability_exception) && !CLI_SCRIPT && !AJAX_SCRIPT && !empty($CFG->autologinguests) && !empty($USER->autologinguest)) { diff --git a/public/theme/boost/templates/footer.mustache b/public/theme/boost/templates/footer.mustache index 9df0e02f3f1..e288520c975 100644 --- a/public/theme/boost/templates/footer.mustache +++ b/public/theme/boost/templates/footer.mustache @@ -73,7 +73,17 @@
- + {{# output.telemetry_traceid }} +
+ + {{{ output.telemetry_traceid }}} + + + {{#str}}telemetrytraceidfooterlink, core{{/str}} + +
+ {{/ output.telemetry_traceid }} {{{ output.standard_footer_html }}} {{{ output.standard_end_of_body_html }}} @@ -94,7 +104,10 @@ {{#js}} -require(['theme_boost/footer-popover'], function(FooterPopover) { +require([ + 'theme_boost/footer-popover', + 'core/copy_to_clipboard', +], function(FooterPopover) { FooterPopover.init(); }); {{/js}}