From 25a8e2fad8d34421e331a1873cf574ddb4fe72ca Mon Sep 17 00:00:00 2001 From: "Nicholas K. Dionysopoulos" Date: Thu, 9 Jun 2022 10:59:37 +0300 Subject: [PATCH 1/2] Enqueue requests made from QuickIcon plugins Fix for gh-38001 --- .../js/extensionupdatecheck.es6.js | 6 ++ .../js/jupdatecheck.es6.js | 57 ++++++++++-------- .../js/overridecheck.es6.js | 6 ++ .../js/privacycheck.es6.js | 6 ++ build/media_source/system/js/core.es6.js | 59 ++++++++++++++++++- 5 files changed, 108 insertions(+), 26 deletions(-) diff --git a/build/media_source/plg_quickicon_extensionupdate/js/extensionupdatecheck.es6.js b/build/media_source/plg_quickicon_extensionupdate/js/extensionupdatecheck.es6.js index 5ace8d26e992e..a2b02a009933c 100644 --- a/build/media_source/plg_quickicon_extensionupdate/js/extensionupdatecheck.es6.js +++ b/build/media_source/plg_quickicon_extensionupdate/js/extensionupdatecheck.es6.js @@ -25,11 +25,17 @@ } }; + /** + * DO NOT use fetch() for QuickIcon requests. They must be queued. + * + * @see https://github.com/joomla/joomla-cms/issues/38001 + */ Joomla.request({ url: options.ajaxUrl, method: 'GET', data: '', perform: true, + queued: true, onSuccess: (response) => { const updateInfoList = JSON.parse(response); diff --git a/build/media_source/plg_quickicon_joomlaupdate/js/jupdatecheck.es6.js b/build/media_source/plg_quickicon_joomlaupdate/js/jupdatecheck.es6.js index 4167927bdd400..8b1d552e9afa7 100644 --- a/build/media_source/plg_quickicon_joomlaupdate/js/jupdatecheck.es6.js +++ b/build/media_source/plg_quickicon_joomlaupdate/js/jupdatecheck.es6.js @@ -21,36 +21,43 @@ if (Joomla && Joomla.getOptions('js-extensions-update')) { const fetchUpdate = () => { const options = Joomla.getOptions('js-joomla-update'); - fetch(options.ajaxUrl, { method: 'GET' }) - .then((response) => { - response.json().then((updateInfoList) => { - if (Array.isArray(updateInfoList)) { - if (updateInfoList.length === 0) { - // No updates - update('success', Joomla.Text._('PLG_QUICKICON_JOOMLAUPDATE_UPTODATE')); - } else { - const updateInfo = updateInfoList.shift(); + /** + * DO NOT use fetch() for QuickIcon requests. They must be queued. + * + * @see https://github.com/joomla/joomla-cms/issues/38001 + */ + Joomla.request({ + url: options.ajaxUrl, + method: 'GET', + data: '', + perform: true, + queued: true, + onSuccess: (response) => { + const updateInfoList = JSON.parse(response); - if (updateInfo.version !== options.version) { - update('danger', Joomla.Text._('PLG_QUICKICON_JOOMLAUPDATE_UPDATEFOUND').replace('%s', ` \u200E ${updateInfo.version}`)); - } else { - update('success', Joomla.Text._('PLG_QUICKICON_JOOMLAUPDATE_UPTODATE')); - } - } + if (Array.isArray(updateInfoList)) { + if (updateInfoList.length === 0) { + // No updates + update('success', Joomla.Text._('PLG_QUICKICON_JOOMLAUPDATE_UPTODATE')); } else { - // An error occurred - update('danger', Joomla.Text._('PLG_QUICKICON_JOOMLAUPDATE_ERROR')); + const updateInfo = updateInfoList.shift(); + + if (updateInfo.version !== options.version) { + update('danger', Joomla.Text._('PLG_QUICKICON_JOOMLAUPDATE_UPDATEFOUND').replace('%s', ` \u200E ${updateInfo.version}`)); + } else { + update('success', Joomla.Text._('PLG_QUICKICON_JOOMLAUPDATE_UPTODATE')); + } } - }) - .catch(() => { - // An error occurred - update('danger', Joomla.Text._('PLG_QUICKICON_JOOMLAUPDATE_ERROR')); - }); - }) - .catch(() => { + } else { + // An error occurred + update('danger', Joomla.Text._('PLG_QUICKICON_JOOMLAUPDATE_ERROR')); + } + }, + onError: () => { // An error occurred update('danger', Joomla.Text._('PLG_QUICKICON_JOOMLAUPDATE_ERROR')); - }); + }, + }); }; // Give some times to the layout and other scripts to settle their stuff diff --git a/build/media_source/plg_quickicon_overridecheck/js/overridecheck.es6.js b/build/media_source/plg_quickicon_overridecheck/js/overridecheck.es6.js index 059fec48f4671..92327c82fd66c 100644 --- a/build/media_source/plg_quickicon_overridecheck/js/overridecheck.es6.js +++ b/build/media_source/plg_quickicon_overridecheck/js/overridecheck.es6.js @@ -28,11 +28,17 @@ } }; + /** + * DO NOT use fetch() for QuickIcon requests. They must be queued. + * + * @see https://github.com/joomla/joomla-cms/issues/38001 + */ Joomla.request({ url: options.ajaxUrl, method: 'GET', data: '', perform: true, + queued: true, onSuccess: (response) => { const updateInfoList = JSON.parse(response); diff --git a/build/media_source/plg_quickicon_privacycheck/js/privacycheck.es6.js b/build/media_source/plg_quickicon_privacycheck/js/privacycheck.es6.js index 3b5fd16bf9c12..ed6fe3058d7fe 100644 --- a/build/media_source/plg_quickicon_privacycheck/js/privacycheck.es6.js +++ b/build/media_source/plg_quickicon_privacycheck/js/privacycheck.es6.js @@ -14,11 +14,17 @@ const quickicon = document.getElementById('plg_quickicon_privacycheck'); const link = quickicon.querySelector('span.j-links-link'); + /** + * DO NOT use fetch() for QuickIcon requests. They must be queued. + * + * @see https://github.com/joomla/joomla-cms/issues/38001 + */ Joomla.request({ url: ajaxUrl, method: 'GET', data: '', perform: true, + queued: true, onSuccess: (response) => { try { const request = JSON.parse(response); diff --git a/build/media_source/system/js/core.es6.js b/build/media_source/system/js/core.es6.js index e0f2aae4670b1..d0dd7bc06e8cf 100644 --- a/build/media_source/system/js/core.es6.js +++ b/build/media_source/system/js/core.es6.js @@ -565,6 +565,27 @@ window.Joomla.Modal = window.Joomla.Modal || { }); }; + /** + * Joomla Request queue. + * + * A FIFO queue of requests to execute serially. Used to prevent simultaneous execution of + * multiple requests against the server which could trigger its Denial of Service protection. + * + * @type {Array} + * + * @since __DEPLOY_VERSION__ + */ + const requestQueue = []; + + /** + * Flag to indicate whether Joomla is performing a queued Request. + * + * @type {boolean} + * + * @since __DEPLOY_VERSION__ + */ + let performingQueuedRequest = false; + /** * Method to perform AJAX request * @@ -576,6 +597,8 @@ window.Joomla.Modal = window.Joomla.Modal || { * https://developer.mozilla.org/docs/Web/API/XMLHttpRequest/send * perform: true, Perform the request immediately * or return XMLHttpRequest instance and perform it later + * queued: false, Put the request in a FIFO queue; prevents simultaneous execution of + * multiple requests to avoid triggering the server's Denial of Service protection. * headers: null, Object of custom headers, eg {'X-Foo': 'Bar', 'X-Bar': 'Foo'} * * onBefore: (xhr) => {} // Callback on before the request @@ -598,13 +621,32 @@ window.Joomla.Modal = window.Joomla.Modal || { * @see https://developer.mozilla.org/docs/Web/API/XMLHttpRequest */ Joomla.request = (options) => { + /** + * Processes queued Request objects. + * + * @since __DEPLOY_VERSION__ + */ + const processQueuedRequests = () => { + if (performingQueuedRequest || requestQueue.length === 0) { + return; + } + + performingQueuedRequest = true; + + const nextRequest = requestQueue.shift(); + + nextRequest.xhr.send(nextRequest.data); + }; + let xhr; + // Prepare the options const newOptions = Joomla.extend({ url: '', method: 'GET', data: null, perform: true, + queued: false, }, options); // Set up XMLHttpRequest instance @@ -648,6 +690,12 @@ window.Joomla.Modal = window.Joomla.Modal || { return; } + // The request is finished; step through any more queued requests + if (newOptions.queued) { + performingQueuedRequest = false; + processQueuedRequests(); + } + // Request finished and response is ready if (xhr.status === 200) { if (newOptions.onSuccess) { @@ -663,7 +711,7 @@ window.Joomla.Modal = window.Joomla.Modal || { }; // Do request - if (newOptions.perform) { + if (newOptions.perform && !newOptions.queued) { if (newOptions.onBefore && newOptions.onBefore.call(window, xhr) === false) { // Request interrupted return xhr; @@ -671,6 +719,15 @@ window.Joomla.Modal = window.Joomla.Modal || { xhr.send(newOptions.data); } + + // Enqueue request and try to process the queue + if (newOptions.queued) { + requestQueue.push({ + xhr, + data: newOptions.data, + }); + processQueuedRequests(); + } } catch (error) { // eslint-disable-next-line no-unused-expressions,no-console window.console ? console.log(error) : null; From 294a760ec52ae4ac2e058dd7fb4f8c24dfc29526 Mon Sep 17 00:00:00 2001 From: "Nicholas K. Dionysopoulos" Date: Thu, 9 Jun 2022 11:26:53 +0300 Subject: [PATCH 2/2] Also enqueue requests made be AJAX badges AJAX badges appear in custom dashboards rendered by com_cpanel, e.g. the System Dashboard. --- build/media_source/com_cpanel/js/admin-system-loader.es6.js | 1 + 1 file changed, 1 insertion(+) diff --git a/build/media_source/com_cpanel/js/admin-system-loader.es6.js b/build/media_source/com_cpanel/js/admin-system-loader.es6.js index 27bea3975c2c1..891f94d0c8e65 100644 --- a/build/media_source/com_cpanel/js/admin-system-loader.es6.js +++ b/build/media_source/com_cpanel/js/admin-system-loader.es6.js @@ -20,6 +20,7 @@ Joomla.request({ url: badgeurl, method: 'POST', + queued: true, onSuccess: (resp) => { let response; try {