harden(update): drop inline spinner style (R4), server-side return-path sanitise

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
feat/v1-foundation
boban 2026-06-20 20:22:40 +02:00
parent d2f70900ff
commit df5d9e2112
2 changed files with 10 additions and 16 deletions

View File

@ -32,14 +32,13 @@
{{-- Spinner + heading --}}
<div class="flex flex-col items-center gap-5 text-center">
<div id="js-spinner" class="relative h-14 w-14" aria-hidden="true">
<svg class="h-14 w-14 -rotate-90" viewBox="0 0 56 56" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg class="h-14 w-14 -rotate-90 animate-spin origin-center" viewBox="0 0 56 56" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="28" cy="28" r="24" stroke-width="3" class="stroke-line"/>
<circle id="js-spinner-arc" cx="28" cy="28" r="24" stroke-width="3"
stroke-linecap="round"
stroke-dasharray="150.796"
stroke-dashoffset="37.699"
class="stroke-accent origin-center"
style="animation: clusev-spin 1.4s linear infinite; transform-origin: 50% 50%;" />
class="stroke-accent origin-center" />
</svg>
</div>
@ -100,11 +99,6 @@
</div>
</div>
{{-- Inline CSS for the spinner animation (no Alpine/external deps) --}}
<style>
@keyframes clusev-spin { to { transform: rotate(360deg); } }
</style>
{{-- Inline JS no external dependencies. Reads ?return= for the redirect target.
Polls /up every 2500 ms; requires 2 consecutive 200s before redirecting (flap guard).
Times out after 10 minutes and shows a manual-reload prompt. --}}
@ -113,13 +107,8 @@
'use strict';
// ── Return URL ─────────────────────────────────────────────────────────
// Sanitise to a same-origin relative path: must start with a single /,
// must NOT start with //, http, or contain backslashes.
var raw = new URLSearchParams(window.location.search).get('return') || '';
var returnUrl = '/';
if (raw && /^\/[^\/\\]/.test(raw) && !/^\/\//g.test(raw) && !/^https?:/i.test(raw)) {
returnUrl = raw;
}
// Server-validated path from the route handler (defence-in-depth guard).
var returnUrl = {{ \Illuminate\Support\Js::from($returnPath) }};
// ── Phase timing heuristic (purely visual) ─────────────────────────────
// Approx durations (seconds) per phase before advancing the highlight.

View File

@ -110,6 +110,11 @@ Route::middleware('auth')->group(function () {
// Plain Blade view — deliberately NOT a Livewire component so it survives the
// container teardown that follows an update request (R1/R2 exception, by design).
Route::get('/update-progress', fn () => view('update-progress'))->name('update.progress');
Route::get('/update-progress', function (Request $request) {
$parsed = parse_url((string) $request->query('return', ''), PHP_URL_PATH);
$return = (is_string($parsed) && str_starts_with($parsed, '/') && ! str_starts_with($parsed, '//')) ? $parsed : '/';
return view('update-progress', ['returnPath' => $return]);
})->name('update.progress');
});
});