Fix update confirmation dialog not blocking form submission

The previous implementation used inline onsubmit handlers with return
confirmVersionChange(...), which could fail silently if any JavaScript
error occurred on the page, causing the form to submit without confirmation.

Fixes:
- Use event.preventDefault() FIRST to ensure form never submits by default
- Use DOMContentLoaded event listeners instead of inline handlers
- Properly escape translation strings using json_encode filter
- Wrap in IIFE with 'use strict' for better error handling
- Use data-attributes to identify forms and pass isDowngrade state

Fix DOMContentLoaded race condition in update form handlers

The event listener was not attaching if DOMContentLoaded had already
fired by the time the script executed. Now checks document.readyState
and attaches handlers immediately if DOM is already ready.

Added console.log statements to help debug form handler attachment.

Use Stimulus controller for update confirmation dialogs

The inline script was blocked by Content Security Policy (CSP).
Stimulus controllers are bundled with webpack and properly allowed by CSP.

- Create update_confirm_controller.js Stimulus controller
- Remove inline script from template
- Pass translation strings via data-* attributes
This commit is contained in:
Sebastian Almberg 2026-01-30 22:18:21 +01:00
parent 97e3b0aa09
commit 0bfbbc961d
2 changed files with 94 additions and 35 deletions

View file

@ -133,7 +133,13 @@
</div>
{% if status.update_available and status.can_auto_update and validation.valid %}
<form action="{{ path('admin_update_manager_start') }}" method="post" onsubmit="return confirmVersionChange('{{ status.latest_tag }}', false);">
<form action="{{ path('admin_update_manager_start') }}" method="post"
data-controller="update-confirm"
data-update-confirm-is-downgrade-value="false"
data-update-confirm-target-version-value="{{ status.latest_tag }}"
data-update-confirm-confirm-update-value="{{ 'update_manager.confirm_update'|trans }}"
data-update-confirm-confirm-downgrade-value="{{ 'update_manager.confirm_downgrade'|trans }}"
data-update-confirm-downgrade-warning-value="{{ 'update_manager.downgrade_removes_update_manager'|trans }}">
<input type="hidden" name="_token" value="{{ csrf_token('update_manager_start') }}">
<input type="hidden" name="version" value="{{ status.latest_tag }}">
@ -237,7 +243,12 @@
</a>
{% if release.version != status.current_version and status.can_auto_update and validation.valid %}
<form action="{{ path('admin_update_manager_start') }}" method="post" class="d-inline"
onsubmit="return confirmVersionChange('{{ release.tag }}', {{ release.version < status.current_version ? 'true' : 'false' }});">
data-controller="update-confirm"
data-update-confirm-is-downgrade-value="{{ release.version < status.current_version ? 'true' : 'false' }}"
data-update-confirm-target-version-value="{{ release.tag }}"
data-update-confirm-confirm-update-value="{{ 'update_manager.confirm_update'|trans }}"
data-update-confirm-confirm-downgrade-value="{{ 'update_manager.confirm_downgrade'|trans }}"
data-update-confirm-downgrade-warning-value="{{ 'update_manager.downgrade_removes_update_manager'|trans }}">
<input type="hidden" name="_token" value="{{ csrf_token('update_manager_start') }}">
<input type="hidden" name="version" value="{{ release.tag }}">
<input type="hidden" name="backup" value="1">
@ -371,37 +382,4 @@
</div>
{% endif %}
</div>
<script>
// Version where Update Manager was introduced
const UPDATE_MANAGER_MIN_VERSION = '2.6.0';
function confirmVersionChange(targetVersion, isDowngrade) {
const targetClean = targetVersion.replace(/^v/, '');
if (isDowngrade) {
// Check if downgrading to a version without Update Manager
if (compareVersions(targetClean, UPDATE_MANAGER_MIN_VERSION) < 0) {
return confirm(
'{% trans %}update_manager.confirm_downgrade{% endtrans %}\n\n' +
'⚠️ {% trans %}update_manager.downgrade_removes_update_manager{% endtrans %}'
);
}
return confirm('{% trans %}update_manager.confirm_downgrade{% endtrans %}');
}
return confirm('{% trans %}update_manager.confirm_update{% endtrans %}');
}
function compareVersions(v1, v2) {
const parts1 = v1.split('.').map(Number);
const parts2 = v2.split('.').map(Number);
for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
const p1 = parts1[i] || 0;
const p2 = parts2[i] || 0;
if (p1 < p2) return -1;
if (p1 > p2) return 1;
}
return 0;
}
</script>
{% endblock %}