From 5c1577a38297f60a9c779ca94294607562311634 Mon Sep 17 00:00:00 2001 From: TheWitness Date: Thu, 15 Oct 2026 13:39:18 -0400 Subject: [PATCH] Force plugins.php action icons over POST and enlarge Tabler icons (#8394) The install, enable, remote enable, load and dialog-confirmed actions on plugins.php are guarded by csrf_require_post() on the server. The anchors were still activated by the generic GET anchor handler, so clicking them returned a 405 on the develop branch. Submit these actions over POST: - The dialog Continue button and the Check Latest control now post. - A capture-phase document click listener intercepts the install, enable, remote enable and load icons before the bubble-phase generic handler, so they post, keep working after Ajax table refreshes, and bind only once. Tabler (i.ti) icons rendered smaller than the FontAwesome glyphs used on the 1.2.x branch, so give them an explicit 16px rule matching the fa layout in each bundled theme, and cover the new rule with develop-theme-correctness targeted tests. --- include/themes/cacti/main.css | 7 ++++ include/themes/carrot/main.css | 7 ++++ include/themes/dark/main.css | 7 ++++ include/themes/deepness/main.css | 7 ++++ include/themes/hollyberry/main.css | 7 ++++ include/themes/modern/main.css | 7 ++++ include/themes/paper-plane/main.css | 7 ++++ include/themes/paw/main.css | 7 ++++ include/themes/raspberry/main.css | 7 ++++ include/themes/sunrise/main.css | 7 ++++ plugins.php | 36 +++++++++++++++++-- .../tests/develop-theme-correctness.spec.ts | 10 ++++++ tests/js/csrf_post_actions.test.js | 13 +++++++ 13 files changed, 127 insertions(+), 2 deletions(-) diff --git a/include/themes/cacti/main.css b/include/themes/cacti/main.css index e3f54af3b..07697295a 100644 --- a/include/themes/cacti/main.css +++ b/include/themes/cacti/main.css @@ -144,6 +144,13 @@ a:hover { color: #333333; } +i.ti { + font-size: 16px; + padding: 0px 3px; + text-align: left; + vertical-align: middle; +} + i.fa, i.fas, i.fab, i.far, i.fal { font-size: 15px; padding: 0px 3px; diff --git a/include/themes/carrot/main.css b/include/themes/carrot/main.css index ec63dd3eb..3f4db86a0 100644 --- a/include/themes/carrot/main.css +++ b/include/themes/carrot/main.css @@ -144,6 +144,13 @@ a:hover { color: #333333; } +i.ti { + font-size: 16px; + padding: 0px 3px; + text-align: left; + vertical-align: middle; +} + i.fa, i.fas, i.fab, i.far, i.fal { font-size: 15px; padding: 0px 3px; diff --git a/include/themes/dark/main.css b/include/themes/dark/main.css index bfb4645ba..9f7178fc1 100644 --- a/include/themes/dark/main.css +++ b/include/themes/dark/main.css @@ -92,6 +92,13 @@ img { vertical-align: middle; } +i.ti { + font-size: 16px; + padding: 0px 3px; + text-align: left; + vertical-align: middle; +} + i.fa, i.fas, i.fab, i.far, i.fal { font-size: 15px; padding: 0px 3px; diff --git a/include/themes/deepness/main.css b/include/themes/deepness/main.css index 92f9122ad..2ba518fc0 100644 --- a/include/themes/deepness/main.css +++ b/include/themes/deepness/main.css @@ -113,6 +113,13 @@ img { vertical-align: middle; } +i.ti { + font-size: 16px; + padding: 0px 3px; + text-align: left; + vertical-align: middle; +} + i.fa, i.fas, i.fab, i.far, i.fal { padding: 0px 3px; font-size: 15px; diff --git a/include/themes/hollyberry/main.css b/include/themes/hollyberry/main.css index 1b90d4e36..6ccef3f2a 100644 --- a/include/themes/hollyberry/main.css +++ b/include/themes/hollyberry/main.css @@ -144,6 +144,13 @@ a:hover { color: #333333; } +i.ti { + font-size: 16px; + padding: 0px 3px; + text-align: left; + vertical-align: middle; +} + i.fa, i.fas, i.fab, i.far, i.fal { font-size: 15px; padding: 0px 3px; diff --git a/include/themes/modern/main.css b/include/themes/modern/main.css index 24a57c174..5e6b84eb5 100644 --- a/include/themes/modern/main.css +++ b/include/themes/modern/main.css @@ -97,6 +97,13 @@ img { vertical-align: middle; } +i.ti { + font-size: 16px; + padding: 0px 3px; + text-align: left; + vertical-align: middle; +} + i.fa, i.fas, i.fab, i.far, i.fal { font-size: 15px; padding: 0px 3px; diff --git a/include/themes/paper-plane/main.css b/include/themes/paper-plane/main.css index c18d27586..eb7f14348 100644 --- a/include/themes/paper-plane/main.css +++ b/include/themes/paper-plane/main.css @@ -72,6 +72,13 @@ img { vertical-align: middle; } +i.ti { + font-size: 16px; + padding: 0px 3px; + text-align: left; + vertical-align: middle; +} + i.fa, i.fas, i.fab, i.far, i.fal { font-size: 15px; padding: 0px 3px; diff --git a/include/themes/paw/main.css b/include/themes/paw/main.css index b4d2dac4c..9e8ef9ede 100644 --- a/include/themes/paw/main.css +++ b/include/themes/paw/main.css @@ -74,6 +74,13 @@ img { vertical-align: middle; } +i.ti { + font-size: 16px; + padding: 0px 3px; + text-align: left; + vertical-align: middle; +} + i.fa, i.fas, i.fab, i.far, i.fal { font-size: 15px; padding: 0px 3px; diff --git a/include/themes/raspberry/main.css b/include/themes/raspberry/main.css index 6234865af..daeaa9fa7 100644 --- a/include/themes/raspberry/main.css +++ b/include/themes/raspberry/main.css @@ -144,6 +144,13 @@ a:hover { color: #333333; } +i.ti { + font-size: 16px; + padding: 0px 3px; + text-align: left; + vertical-align: middle; +} + i.fa, i.fas, i.fab, i.far, i.fal { font-size: 15px; padding: 0px 3px; diff --git a/include/themes/sunrise/main.css b/include/themes/sunrise/main.css index 27f9e02f5..dfac0e25f 100644 --- a/include/themes/sunrise/main.css +++ b/include/themes/sunrise/main.css @@ -87,6 +87,13 @@ img { vertical-align: middle; } +i.ti { + font-size: 16px; + padding: 0px 3px; + text-align: left; + vertical-align: middle; +} + i.fa, i.fas, i.fab, i.far, i.fal { font-size: 15px; padding: 0px 3px; diff --git a/plugins.php b/plugins.php index decd35ed9..930cc950f 100644 --- a/plugins.php +++ b/plugins.php @@ -803,7 +803,10 @@ function update_show_current() : void { post['__csrf_magic'] = csrfMagicToken; postUrl({url: url, noState: true}, post); } else { - loadUrl({url: url}); + // Navigate the whole page so the jQuery UI dialog is cleared; an + // Ajax postUrl() would leave #pidialog detached under body and + // break the next confirmation. + submitPageUsingPost(url); } } } @@ -872,9 +875,38 @@ function update_show_current() : void { $('#latest').click(function() { strURL = 'plugins.php?action=latest'; - loadUrl({url:strURL}); + submitPageUsingPost(strURL); }); + // The install, enable, remote enable and load actions are guarded by + // csrf_require_post() on the server, so they must be submitted over POST. + // A capture-phase listener on the document intercepts these clicks before + // the bubble-phase generic anchor handler (which would issue a GET and be + // rejected), works for rows added by later Ajax table refreshes, and is + // bound only once for the life of the page. + if (!window.cactiPluginPostActionsBound) { + window.cactiPluginPostActionsBound = true; + + document.addEventListener('click', function(event) { + var anchor = event.target.closest('a.piinstall, a.pienable, a.piload'); + + if (anchor === null) { + return; + } + + var url = anchor.getAttribute('href'); + + if (url === null || url === '' || url === '#') { + return; + } + + event.preventDefault(); + event.stopImmediatePropagation(); + + postUrl({ url: url }, { __csrf_magic: csrfMagicToken }); + }, true); + } + $('#form_plugins').submit(function(event) { event.preventDefault(); applyFilter(); diff --git a/tests/e2e/tests/develop-theme-correctness.spec.ts b/tests/e2e/tests/develop-theme-correctness.spec.ts index d30343646..faa36a464 100644 --- a/tests/e2e/tests/develop-theme-correctness.spec.ts +++ b/tests/e2e/tests/develop-theme-correctness.spec.ts @@ -64,6 +64,16 @@ for (const [file, html, selector, pseudo, property, expected] of [ ['include/themes/midwinter/css/media/compact-landscape.css', '', '.navBreadCrumb div:first-child', '::after', 'fontFamily', /^"Font Awesome 5 Free", sans-serif$/], ['include/themes/midwinter/css/media/compact-landscape.css', '', 'a', '', 'textDecorationLine', /^none$/], ['include/themes/midwinter/css/media/compact-landscape.css', '', '.jstree-ocl', '::before', 'fontFamily', /^tabler-icons, sans-serif$/], + ['include/themes/cacti/main.css', '', 'i.ti', '', 'fontSize', /^16px$/], + ['include/themes/carrot/main.css', '', 'i.ti', '', 'fontSize', /^16px$/], + ['include/themes/dark/main.css', '', 'i.ti', '', 'fontSize', /^16px$/], + ['include/themes/deepness/main.css', '', 'i.ti', '', 'fontSize', /^16px$/], + ['include/themes/hollyberry/main.css', '', 'i.ti', '', 'fontSize', /^16px$/], + ['include/themes/modern/main.css', '', 'i.ti', '', 'fontSize', /^16px$/], + ['include/themes/paper-plane/main.css', '', 'i.ti', '', 'fontSize', /^16px$/], + ['include/themes/paw/main.css', '', 'i.ti', '', 'fontSize', /^16px$/], + ['include/themes/raspberry/main.css', '', 'i.ti', '', 'fontSize', /^16px$/], + ['include/themes/sunrise/main.css', '', 'i.ti', '', 'fontSize', /^16px$/], ] as [string, string, string, string, string, RegExp][]) { test(`${file} keeps repaired ${selector}${pseudo} ${property}`, async ({ page }) => { await page.setContent(`${html}`); diff --git a/tests/js/csrf_post_actions.test.js b/tests/js/csrf_post_actions.test.js index 82ce31ad3..a26fcb86a 100644 --- a/tests/js/csrf_post_actions.test.js +++ b/tests/js/csrf_post_actions.test.js @@ -97,3 +97,16 @@ test('the state-action handler survives element-level unbind calls', () => { assert.match(source, /\$\(document\)\.rebind\('click\.cactiPostAction', 'a\.cactiPostAction'/); assert.doesNotMatch(source, /\$\('a\.cactiPostAction'\)\.off\('click\.cactiPostAction'/); }); + +test('plugins.php confirmation Continue navigates the page so repeated confirmations keep opening', () => { + const pageSource = fs.readFileSync(path.join(__dirname, '..', '..', 'plugins.php'), 'utf8'); + const dialog = extractFunction(pageSource, 'displayDialog'); + + // Without an inline #dialogForm the Continue button must navigate the whole + // page via submitPageUsingPost(). An Ajax loadUrl()/postUrl() here leaves the + // jQuery UI dialog detached under , so the second confirmation's + // remove().append() builds into a detached node and never opens. + assert.match(dialog, /else\s*\{[^}]*submitPageUsingPost\(url\);/); + assert.doesNotMatch(dialog, /else\s*\{\s*(?:loadUrl|postUrl)\(/); +}); +