Fix download modal: use per-backup modals for CSP/Turbo compatibility

- Replace shared modal + inline JS with per-backup modals that have
  filename pre-set in hidden fields (no JavaScript needed)
- Add data-turbo="false" to download forms for native browser handling
- Add data-bs-dismiss="modal" to submit button to auto-close modal
- Add hidden username field for Chrome accessibility best practice
- Fix test: GET on POST-only route returns 404 not 405
This commit is contained in:
Sebastian Almberg 2026-03-06 08:41:43 +01:00
parent dd8698840d
commit 877e3005bc
2 changed files with 46 additions and 53 deletions

View file

@ -418,10 +418,9 @@
<div class="btn-group btn-group-sm">
{% if not backup_download_disabled and is_granted('@system.manage_updates') %}
<button type="button"
class="btn btn-outline-secondary btn-download-backup"
data-filename="{{ backup.file }}"
class="btn btn-outline-secondary"
data-bs-toggle="modal"
data-bs-target="#downloadBackupModal"
data-bs-target="#downloadBackupModal-{{ loop.index }}"
title="{% trans %}update_manager.backup.download{% endtrans %}">
<i class="fas fa-download"></i>
</button>
@ -457,6 +456,48 @@
</form>
{% endif %}
</div>
{% if not backup_download_disabled and is_granted('@system.manage_updates') %}
{# Per-backup download modal - no inline JS needed, CSP compatible with Turbo #}
<div class="modal fade" id="downloadBackupModal-{{ loop.index }}" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<form action="{{ path('admin_update_manager_backup_download') }}" method="post" data-turbo="false">
<div class="modal-header">
<h5 class="modal-title">
<i class="fas fa-download me-2"></i>{% trans %}update_manager.backup.download{% endtrans %}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="alert alert-warning">
<i class="fas fa-exclamation-triangle me-1"></i>
{% trans %}update_manager.backup.download.security_warning{% endtrans %}
</div>
<p class="text-muted small mb-3">{{ backup.file }}</p>
<input type="hidden" name="_token" value="{{ csrf_token('update_manager_download') }}">
<input type="hidden" name="filename" value="{{ backup.file }}">
<input type="hidden" name="username" autocomplete="username">
<div class="mb-3">
<label for="downloadPassword-{{ loop.index }}" class="form-label">
{% trans %}update_manager.backup.download.password_label{% endtrans %}
</label>
<input type="password" class="form-control" id="downloadPassword-{{ loop.index }}"
name="password" required autocomplete="current-password">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
{% trans %}cancel{% endtrans %}
</button>
<button type="submit" class="btn btn-primary" data-bs-dismiss="modal">
<i class="fas fa-download me-1"></i>{% trans %}update_manager.backup.download{% endtrans %}
</button>
</div>
</form>
</div>
</div>
</div>
{% endif %}
</td>
</tr>
{% else %}
@ -477,52 +518,4 @@
</div>
</div>
{# Password confirmation modal for backup download #}
{% if not backup_download_disabled and is_granted('@system.manage_updates') %}
<div class="modal fade" id="downloadBackupModal" tabindex="-1" aria-labelledby="downloadBackupModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<form action="{{ path('admin_update_manager_backup_download') }}" method="post" id="downloadBackupForm">
<div class="modal-header">
<h5 class="modal-title" id="downloadBackupModalLabel">
<i class="fas fa-download me-2"></i>{% trans %}update_manager.backup.download{% endtrans %}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="alert alert-warning">
<i class="fas fa-exclamation-triangle me-1"></i>
{% trans %}update_manager.backup.download.security_warning{% endtrans %}
</div>
<input type="hidden" name="_token" value="{{ csrf_token('update_manager_download') }}">
<input type="hidden" name="filename" id="downloadBackupFilename" value="">
<div class="mb-3">
<label for="downloadBackupPassword" class="form-label">
{% trans %}update_manager.backup.download.password_label{% endtrans %}
</label>
<input type="password" class="form-control" id="downloadBackupPassword"
name="password" required autocomplete="current-password">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
{% trans %}cancel{% endtrans %}
</button>
<button type="submit" class="btn btn-primary">
<i class="fas fa-download me-1"></i>{% trans %}update_manager.backup.download{% endtrans %}
</button>
</div>
</form>
</div>
</div>
</div>
<script nonce="{{ csp_nonce('script') }}">
document.getElementById('downloadBackupModal').addEventListener('show.bs.modal', function (event) {
var button = event.relatedTarget;
var filename = button.getAttribute('data-filename');
document.getElementById('downloadBackupFilename').value = filename;
document.getElementById('downloadBackupPassword').value = '';
});
</script>
{% endif %}
{% endblock %}

View file

@ -251,10 +251,10 @@ final class UpdateManagerControllerTest extends WebTestCase
$client = static::createClient();
$this->loginAsAdmin($client);
// GET should return 405 Method Not Allowed
// GET returns 404 since no GET route exists for this path
$client->request('GET', '/en/system/update-manager/backup/download');
$this->assertResponseStatusCodeSame(405);
$this->assertResponseStatusCodeSame(404);
}
public function testDownloadBackupRequiresAuth(): void