From 3db52c962a7752a804225f3e795f2a4d9bf337a9 Mon Sep 17 00:00:00 2001 From: Montek Date: Thu, 5 Feb 2026 12:59:07 -0500 Subject: [PATCH 1/5] feat: add lorisFetch wrapper --- htdocs/js/loris-scripts.js | 92 +++++++++++++++++++++++++++----------- jslib/lorisFetch.js | 81 +++++++++++++++++++++++++++++++++ package-lock.json | 15 +++++++ 3 files changed, 161 insertions(+), 27 deletions(-) create mode 100644 jslib/lorisFetch.js diff --git a/htdocs/js/loris-scripts.js b/htdocs/js/loris-scripts.js index d07ae39f48..f49b6e74f9 100644 --- a/htdocs/js/loris-scripts.js +++ b/htdocs/js/loris-scripts.js @@ -1,5 +1,70 @@ /* eslint new-cap: ["error", {capIsNewExceptions: ["DynamicTable", "FileUpload"]}]*/ +/** + * Display the login modal when a request returns a 401 response. + */ +function handleUnauthorized() { + if (!window.$ || !window.loris) { + return; + } + + if (!$('#login-modal').length) { + return; + } + + if ($('#login-modal').hasClass('in')) { + $('#login-modal-error').show(); + return; + } + + $('#login-modal').modal('show'); + $('#modal-login') + .off('click.lorisFetch') + .on('click.lorisFetch', function(e) { + e.preventDefault(); + let data = { + username: $('#modal-username').val(), + password: $('#modal-password').val(), + login: 'Login', + }; + + window.lorisFetch(window.loris.BaseURL, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', + }, + body: new URLSearchParams(data), + }) + .then((response) => { + if (!response.ok) { + throw new Error('request_failed'); + } + $('#login-modal-error').hide(); + $('#login-modal').modal('hide'); + }) + .catch(() => { + $('#login-modal-error').show(); + }); + }); +} + +if (!window.lorisFetch) { + window.lorisFetch = function(input, init) { + const options = Object.assign( + { + credentials: 'same-origin', + }, + init || {} + ); + return fetch(input, options).then((response) => { + if (response.status === 401) { + handleUnauthorized(); + } + return response; + }); + }; +} + $(document).ready(function() { $('#menu-toggle').click(function(e) { e.preventDefault(); @@ -8,30 +73,3 @@ $(document).ready(function() { $('.dynamictable').DynamicTable(); $('.fileUpload').FileUpload(); }); - -$(document).ajaxError(function(event, jqxhr, settings, thrownError) { - if (jqxhr.status === 401) { - if ($('#login-modal').hasClass('in')) { - $('#login-modal-error').show(); - } else { - $('#login-modal').modal('show'); - $('#modal-login').click(function(e) { - e.preventDefault(); - let data = { - username: $('#modal-username').val(), - password: $('#modal-password').val(), - login: 'Login', - }; - $.ajax({ - type: 'post', - url: loris.BaseURL, - data: data, - success: function() { - $('#login-modal-error').hide(); - $('#login-modal').modal('hide'); - }, - }); - }); - } - } -}); diff --git a/jslib/lorisFetch.js b/jslib/lorisFetch.js new file mode 100644 index 0000000000..1aba03792c --- /dev/null +++ b/jslib/lorisFetch.js @@ -0,0 +1,81 @@ +/** + * Display the login modal when a request returns a 401 response. + */ +function handleUnauthorized() { + if (typeof window === 'undefined') { + return; + } + if (!window.$ || !window.loris) { + return; + } + + const $ = window.$; + if (!$('#login-modal').length) { + return; + } + + if ($('#login-modal').hasClass('in')) { + $('#login-modal-error').show(); + return; + } + + $('#login-modal').modal('show'); + $('#modal-login') + .off('click.lorisFetch') + .on('click.lorisFetch', function(e) { + e.preventDefault(); + let data = { + username: $('#modal-username').val(), + password: $('#modal-password').val(), + login: 'Login', + }; + + fetch(window.loris.BaseURL, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', + }, + body: new URLSearchParams(data), + credentials: 'same-origin', + }) + .then((response) => { + if (!response.ok) { + throw new Error('request_failed'); + } + $('#login-modal-error').hide(); + $('#login-modal').modal('hide'); + }) + .catch(() => { + $('#login-modal-error').show(); + }); + }); +} + +/** + * Wrapper around fetch that keeps credentials and handles 401s. + * + * @param {*} input + * @param {object=} init + * @return {Promise} + */ +function lorisFetch(input, init) { + const options = Object.assign( + { + credentials: 'same-origin', + }, + init || {} + ); + + return fetch(input, options).then((response) => { + if (response.status === 401) { + handleUnauthorized(); + } + return response; + }); +} + +if (typeof window !== 'undefined') { + window.lorisFetch = lorisFetch; +} + +export default lorisFetch; diff --git a/package-lock.json b/package-lock.json index b8b2159e62..6cf935d04e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6128,6 +6128,21 @@ "dev": true, "license": "ISC" }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.1", "license": "MIT" From 469d8cdfbc2ec78b97f283f7dd2ef20b40107036 Mon Sep 17 00:00:00 2001 From: Montek Date: Thu, 5 Feb 2026 12:59:47 -0500 Subject: [PATCH 2/5] chore: drop package-lock change --- package-lock.json | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6cf935d04e..b8b2159e62 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6128,21 +6128,6 @@ "dev": true, "license": "ISC" }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.1", "license": "MIT" From c468aa88bd6f91b0d2938c4504a3990dadb6f7cd Mon Sep 17 00:00:00 2001 From: Montek Date: Thu, 5 Feb 2026 13:02:36 -0500 Subject: [PATCH 3/5] feat(configuration): replace $.ajax with lorisFetch --- modules/configuration/js/cohort.js | 83 +++++++++++-------- .../configuration/jsx/configuration_helper.js | 55 +++++++----- 2 files changed, 81 insertions(+), 57 deletions(-) diff --git a/modules/configuration/js/cohort.js b/modules/configuration/js/cohort.js index aa7070bf74..fc0a8ff803 100644 --- a/modules/configuration/js/cohort.js +++ b/modules/configuration/js/cohort.js @@ -1,5 +1,6 @@ $(document).ready(function() { "use strict"; + var lorisFetch = window.lorisFetch || fetch; $('div').tooltip(); $(".savecohort").click(function(e) { var form = $(e.currentTarget).closest('form'); @@ -11,48 +12,58 @@ $(document).ready(function() { var recruitmentTarget = $(form.find(".cohortRecruitmentTarget")).val(); e.preventDefault(); - $.ajax( - { - "type" : "post", - "url" : loris.BaseURL + "/configuration/ajax/updateCohort.php", - "data" : { + lorisFetch(loris.BaseURL + "/configuration/ajax/updateCohort.php", { + method: "post", + headers: { + "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", + }, + body: new URLSearchParams({ "cohortID" : cohortID, "title" : title, "useEDC" : useEDC, "WindowDifference" : windowDifference, "RecruitmentTarget" : recruitmentTarget, - }, - "dataType": "json", - "success" : function(data) { - $(form.find(".saveStatus")) - .text(data.ok) - .css({ 'color': 'green'}) - .fadeIn(500) - .delay(1000); - if (cohortID === 'new') { - setTimeout(function(){ - location.reload(); - }, 1000); - } else { - var projectDiv = document.getElementById(`#cohort${cohortID}`); - var Name = projectDiv.innerText; - projectDiv.innerText = title; - var projectHeader = document.getElementById(`cohort${cohortID}`); - projectHeader.children[0].innerText = title + projectHeader.children[0].innerText.substring( - Name.length - ); - } - }, - "error" : function(data) { - $(form.find(".saveStatus")) - .text(data.responseJSON.error) - .css({ 'color': 'red'}) - .fadeIn(500) - .delay(1000); + }), + credentials: "same-origin", + }) + .then(async function(response) { + var text = await response.text(); + var data = null; + try { + data = JSON.parse(text); + } catch (err) { + data = null; } - } - - ); + if (!response.ok) { + throw {error: (data && data.error) ? data.error : text}; + } + $(form.find(".saveStatus")) + .text(data && data.ok ? data.ok : "Saved") + .css({ 'color': 'green'}) + .fadeIn(500) + .delay(1000); + if (cohortID === 'new') { + setTimeout(function(){ + location.reload(); + }, 1000); + } else { + var projectDiv = document.getElementById(`#cohort${cohortID}`); + var Name = projectDiv.innerText; + projectDiv.innerText = title; + var projectHeader = document.getElementById(`cohort${cohortID}`); + projectHeader.children[0].innerText = title + projectHeader.children[0].innerText.substring( + Name.length + ); + } + }) + .catch(function(data) { + var message = data && data.error ? data.error : 'Request failed.'; + $(form.find(".saveStatus")) + .text(message) + .css({ 'color': 'red'}) + .fadeIn(500) + .delay(1000); + }); }); diff --git a/modules/configuration/jsx/configuration_helper.js b/modules/configuration/jsx/configuration_helper.js index 010d406bea..144523badc 100644 --- a/modules/configuration/jsx/configuration_helper.js +++ b/modules/configuration/jsx/configuration_helper.js @@ -1,4 +1,5 @@ import swal from 'sweetalert2'; +import lorisFetch from 'jslib/lorisFetch'; $(function() { 'use strict'; @@ -59,11 +60,17 @@ $(function() { let id = $(this).attr('name'); let button = this; - $.ajax({ - type: 'post', - url: loris.BaseURL + '/configuration/ajax/process.php', - data: {remove: id}, - success: function() { + lorisFetch(loris.BaseURL + '/configuration/ajax/process.php', { + method: 'post', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', + }, + body: new URLSearchParams({remove: id}), + }) + .then((response) => { + if (!response.ok) { + throw new Error('request_failed'); + } if ($(button) .parent().parent().parent().children() .length > 1 @@ -83,12 +90,10 @@ $(function() { .addClass('remove-new') .removeClass('btn-remove'); } - }, - error: function(xhr, desc, err) { - console.error(xhr); - console.error('Details: ' + desc + '\nError:' + err); - }, - }); + }) + .catch((err) => { + console.error(err); + }); } }); }); @@ -102,23 +107,31 @@ $(function() { // Clear previous feedback $('.submit-area > label').remove(); - $.ajax({ - type: 'post', - url: loris.BaseURL + '/configuration/ajax/process.php', - data: form, - success: function() { + lorisFetch(loris.BaseURL + '/configuration/ajax/process.php', { + method: 'post', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', + }, + body: form, + }) + .then(async (response) => { + if (!response.ok) { + let text = await response.text(); + let error = new Error('request_failed'); + error.lorisMessage = text; + throw error; + } let html = ''; $(html) .hide() .appendTo('.submit-area') .fadeIn(500).delay(1000).fadeOut(500); location.reload(); - }, - error: function(xhr, desc, err) { - let html = ''; + }) + .catch((error) => { + let html = ''; $(html).hide().appendTo('.submit-area').fadeIn(500).delay(1000); - }, - }); + }); }); // On form reset, to delete the elements added with the "Add field" button that were not submitted. From eaddbb1cd9c574898dc88d17d1a9291a1b3040ce Mon Sep 17 00:00:00 2001 From: Montek Date: Tue, 10 Feb 2026 11:40:03 -0500 Subject: [PATCH 4/5] refactor(configuration): use module Client for ajax/process requests --- .../configuration/jsx/ConfigurationClient.js | 45 +++++++++++++++++ .../configuration/jsx/configuration_helper.js | 48 ++++++++----------- 2 files changed, 65 insertions(+), 28 deletions(-) create mode 100644 modules/configuration/jsx/ConfigurationClient.js diff --git a/modules/configuration/jsx/ConfigurationClient.js b/modules/configuration/jsx/ConfigurationClient.js new file mode 100644 index 0000000000..88fdbcef18 --- /dev/null +++ b/modules/configuration/jsx/ConfigurationClient.js @@ -0,0 +1,45 @@ +import {Client, Errors} from 'jslib'; + +/** + * Configuration module HTTP client. + */ +class ConfigurationClient extends Client { + /** + * @constructor + */ + constructor() { + super('/configuration/ajax'); + } + + /** + * Submit URL-encoded form data and return response text. + * + * @param {string} subEndpoint + * @param {string|URLSearchParams} body + * @return {Promise} + */ + async postForm(subEndpoint, body) { + const url = new URL(subEndpoint, this.baseURL); + const request = new Request(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', + }, + body: body, + }); + try { + const response = await fetch(request); + if (!response.ok) { + throw new Errors.ApiResponse(request, response); + } + return response.text(); + } catch (error) { + if (error instanceof Errors.Http) { + throw error; + } + throw new Errors.ApiNetwork(request); + } + } +} + +export default ConfigurationClient; diff --git a/modules/configuration/jsx/configuration_helper.js b/modules/configuration/jsx/configuration_helper.js index 144523badc..8e3413fc52 100644 --- a/modules/configuration/jsx/configuration_helper.js +++ b/modules/configuration/jsx/configuration_helper.js @@ -1,5 +1,7 @@ import swal from 'sweetalert2'; -import lorisFetch from 'jslib/lorisFetch'; +import ConfigurationClient from './ConfigurationClient'; + +const configurationClient = new ConfigurationClient(); $(function() { 'use strict'; @@ -60,17 +62,11 @@ $(function() { let id = $(this).attr('name'); let button = this; - lorisFetch(loris.BaseURL + '/configuration/ajax/process.php', { - method: 'post', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', - }, - body: new URLSearchParams({remove: id}), - }) - .then((response) => { - if (!response.ok) { - throw new Error('request_failed'); - } + configurationClient.postForm( + 'process.php', + new URLSearchParams({remove: id}) + ) + .then(() => { if ($(button) .parent().parent().parent().children() .length > 1 @@ -107,20 +103,8 @@ $(function() { // Clear previous feedback $('.submit-area > label').remove(); - lorisFetch(loris.BaseURL + '/configuration/ajax/process.php', { - method: 'post', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', - }, - body: form, - }) - .then(async (response) => { - if (!response.ok) { - let text = await response.text(); - let error = new Error('request_failed'); - error.lorisMessage = text; - throw error; - } + configurationClient.postForm('process.php', form) + .then(() => { let html = ''; $(html) .hide() @@ -128,8 +112,16 @@ $(function() { .fadeIn(500).delay(1000).fadeOut(500); location.reload(); }) - .catch((error) => { - let html = ''; + .catch(async (error) => { + let errorMessage = ''; + if (error && error.response) { + try { + errorMessage = await error.response.text(); + } catch (responseError) { + errorMessage = ''; + } + } + let html = ''; $(html).hide().appendTo('.submit-area').fadeIn(500).delay(1000); }); }); From 21ecacec407d74e003aa490896a69690bc4a7f5a Mon Sep 17 00:00:00 2001 From: Montek Date: Thu, 12 Feb 2026 09:50:41 -0500 Subject: [PATCH 5/5] refactor(configuration): align Client usage with #9999 architecture --- .../configuration/jsx/ConfigurationClient.js | 45 ----------------- .../configuration/jsx/configuration_helper.js | 48 +++++++++++-------- 2 files changed, 28 insertions(+), 65 deletions(-) delete mode 100644 modules/configuration/jsx/ConfigurationClient.js diff --git a/modules/configuration/jsx/ConfigurationClient.js b/modules/configuration/jsx/ConfigurationClient.js deleted file mode 100644 index 88fdbcef18..0000000000 --- a/modules/configuration/jsx/ConfigurationClient.js +++ /dev/null @@ -1,45 +0,0 @@ -import {Client, Errors} from 'jslib'; - -/** - * Configuration module HTTP client. - */ -class ConfigurationClient extends Client { - /** - * @constructor - */ - constructor() { - super('/configuration/ajax'); - } - - /** - * Submit URL-encoded form data and return response text. - * - * @param {string} subEndpoint - * @param {string|URLSearchParams} body - * @return {Promise} - */ - async postForm(subEndpoint, body) { - const url = new URL(subEndpoint, this.baseURL); - const request = new Request(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', - }, - body: body, - }); - try { - const response = await fetch(request); - if (!response.ok) { - throw new Errors.ApiResponse(request, response); - } - return response.text(); - } catch (error) { - if (error instanceof Errors.Http) { - throw error; - } - throw new Errors.ApiNetwork(request); - } - } -} - -export default ConfigurationClient; diff --git a/modules/configuration/jsx/configuration_helper.js b/modules/configuration/jsx/configuration_helper.js index 8e3413fc52..144523badc 100644 --- a/modules/configuration/jsx/configuration_helper.js +++ b/modules/configuration/jsx/configuration_helper.js @@ -1,7 +1,5 @@ import swal from 'sweetalert2'; -import ConfigurationClient from './ConfigurationClient'; - -const configurationClient = new ConfigurationClient(); +import lorisFetch from 'jslib/lorisFetch'; $(function() { 'use strict'; @@ -62,11 +60,17 @@ $(function() { let id = $(this).attr('name'); let button = this; - configurationClient.postForm( - 'process.php', - new URLSearchParams({remove: id}) - ) - .then(() => { + lorisFetch(loris.BaseURL + '/configuration/ajax/process.php', { + method: 'post', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', + }, + body: new URLSearchParams({remove: id}), + }) + .then((response) => { + if (!response.ok) { + throw new Error('request_failed'); + } if ($(button) .parent().parent().parent().children() .length > 1 @@ -103,8 +107,20 @@ $(function() { // Clear previous feedback $('.submit-area > label').remove(); - configurationClient.postForm('process.php', form) - .then(() => { + lorisFetch(loris.BaseURL + '/configuration/ajax/process.php', { + method: 'post', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', + }, + body: form, + }) + .then(async (response) => { + if (!response.ok) { + let text = await response.text(); + let error = new Error('request_failed'); + error.lorisMessage = text; + throw error; + } let html = ''; $(html) .hide() @@ -112,16 +128,8 @@ $(function() { .fadeIn(500).delay(1000).fadeOut(500); location.reload(); }) - .catch(async (error) => { - let errorMessage = ''; - if (error && error.response) { - try { - errorMessage = await error.response.text(); - } catch (responseError) { - errorMessage = ''; - } - } - let html = ''; + .catch((error) => { + let html = ''; $(html).hide().appendTo('.submit-area').fadeIn(500).delay(1000); }); });