diff --git a/app/Services/Admin/AdminSystemHealth.php b/app/Services/Admin/AdminSystemHealth.php index 15b9cab5..9c01b05e 100644 --- a/app/Services/Admin/AdminSystemHealth.php +++ b/app/Services/Admin/AdminSystemHealth.php @@ -238,6 +238,19 @@ class AdminSystemHealth : 'SESSION_SECURE_COOKIE ist aus'.($isLocal ? ' (lokal ok).' : ' — in https-Umgebungen aktivieren.'), ]; + // Imagick trägt Herkunftsprüfung und KI-Vorab-Check der Bilder. Fehlt + // die Erweiterung, scheitern beide still (Bilder werden durchgelassen) + // — so geschehen auf Live bis 08.10.2026. + $precheckEnabled = (bool) config('images.precheck.enabled'); + $checks[] = [ + 'key' => 'imagick', + 'label' => 'PHP-Erweiterung Imagick', + 'status' => $this->imagickAvailable() ? self::StatusOk : ($precheckEnabled ? self::StatusCritical : self::StatusWarn), + 'detail' => $this->imagickAvailable() + ? 'Imagick ist geladen (Herkunftsprüfung und KI-Vorab-Check der Bilder).' + : 'Imagick fehlt — Herkunftsprüfung'.($precheckEnabled ? ' und KI-Vorab-Check' : '').' der Bilder laufen ins Leere. Auf dem Server php8.4-imagick installieren und PHP-FPM neu laden.', + ]; + // Herkunftsprüfung der Bild-Uploads (Bildkennzeichnung E.7): Fehler // der letzten sieben Tage zählen; Zustand D im Formular blockiert // nichts, muss aber sichtbar bleiben. @@ -341,6 +354,11 @@ class AdminSystemHealth return $checks; } + protected function imagickAvailable(): bool + { + return class_exists(\Imagick::class); + } + /** * @param array $queue */ diff --git a/bootstrap/app.php b/bootstrap/app.php index 0ff845d2..b97c4877 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -21,8 +21,11 @@ use Illuminate\Http\Request; use Illuminate\Support\Facades\Route; use Laravel\Sanctum\Http\Middleware\CheckAbilities; use Laravel\Sanctum\Http\Middleware\CheckForAnyAbility; +use Livewire\Features\SupportFileUploads\MissingFileUploadsTraitException; +use Livewire\Features\SupportLockedProperties\CannotUpdateLockedPropertyException; use Sentry\Laravel\Integration; use Symfony\Component\Console\Exception\CommandNotFoundException; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; return Application::configure(basePath: dirname(__DIR__)) ->withRouting( @@ -177,4 +180,18 @@ return Application::configure(basePath: dirname(__DIR__)) // Vertippte artisan-Befehle sind kein Anwendungsfehler (GlitchTip // server-pressekonto, Issue 9: „config:cachels“ auf Live). $exceptions->dontReport(CommandNotFoundException::class); + + // Manipulierte Livewire-Anfragen (Scanner, die gesperrte Properties + // setzen oder Uploads an Komponenten ohne Upload schicken) als 400 + // beantworten statt als 500 bzw. 419 — Livewire weist sie korrekt ab, + // sie sind kein Anwendungsfehler und kein abgelaufenes Formular + // (GlitchTip server-pressekonto, Issues 38 und 39, 08.10.2026). + $exceptions->map( + CannotUpdateLockedPropertyException::class, + fn (CannotUpdateLockedPropertyException $e): BadRequestHttpException => new BadRequestHttpException('Ungültige Livewire-Anfrage.', $e), + ); + $exceptions->map( + MissingFileUploadsTraitException::class, + fn (MissingFileUploadsTraitException $e): BadRequestHttpException => new BadRequestHttpException('Ungültige Livewire-Anfrage.', $e), + ); })->create(); diff --git a/resources/js/error-reporting.js b/resources/js/error-reporting.js index d2b474e6..65c6cddb 100644 --- a/resources/js/error-reporting.js +++ b/resources/js/error-reporting.js @@ -123,6 +123,12 @@ if (config?.dsn) { }); window.addEventListener('unhandledrejection', (event) => { + // Alpine bricht laufende x-transition-Übergänge mit diesem Objekt ab + // (GlitchTip browser-pressekonto, Issue 18) — kein Fehler. + if (event.reason?.isFromCancelledTransition === true) { + return; + } + capture(rejectionToError(event.reason)); }); } diff --git a/tests/Feature/Admin/SystemHealthTest.php b/tests/Feature/Admin/SystemHealthTest.php index da9be2e0..0898fd5f 100644 --- a/tests/Feature/Admin/SystemHealthTest.php +++ b/tests/Feature/Admin/SystemHealthTest.php @@ -124,3 +124,28 @@ test('snapshot flags pending media privatization', function () { expect($checks['media_privatization']['status'])->toBe('warn') ->and($checks['media_privatization']['detail'])->toContain('1'); }); + +test('snapshot flags a missing imagick extension', function (bool $precheckEnabled, string $status) { + config()->set('images.precheck.enabled', $precheckEnabled); + app()->bind(AdminSystemHealth::class, fn () => new class extends AdminSystemHealth + { + protected function imagickAvailable(): bool + { + return false; + } + }); + + $checks = collect(app(AdminSystemHealth::class)->snapshot()['checks'])->keyBy('key'); + + expect($checks['imagick']['status'])->toBe($status) + ->and($checks['imagick']['detail'])->toContain('php8.4-imagick'); +})->with([ + 'precheck off' => [false, 'warn'], + 'precheck on' => [true, 'critical'], +]); + +test('snapshot reports imagick as ok when it is loaded', function () { + $checks = collect(app(AdminSystemHealth::class)->snapshot()['checks'])->keyBy('key'); + + expect($checks['imagick']['status'])->toBe(class_exists(Imagick::class) ? 'ok' : 'warn'); +}); diff --git a/tests/Feature/Monitoring/ErrorReportingTest.php b/tests/Feature/Monitoring/ErrorReportingTest.php index 9b2b8c1a..d02e22ab 100644 --- a/tests/Feature/Monitoring/ErrorReportingTest.php +++ b/tests/Feature/Monitoring/ErrorReportingTest.php @@ -2,7 +2,10 @@ use App\Models\User; use App\Support\Monitoring\ErrorReportScrubber; +use Illuminate\Contracts\Debug\ExceptionHandler; use Illuminate\Support\Facades\Route; +use Livewire\Features\SupportFileUploads\MissingFileUploadsTraitException; +use Livewire\Features\SupportLockedProperties\CannotUpdateLockedPropertyException; use Sentry\Event; use Sentry\ExceptionDataBag; use Sentry\SentrySdk; @@ -44,6 +47,27 @@ test('a mistyped artisan command is not reported', function () { expect($glitchTip->events)->toBe([]); }); +test('tampered livewire requests are answered with 400 and not reported', function (Closure $makeException) { + $glitchTip = FakeGlitchTip::install(); + $exception = $makeException(); + + report($exception); + + $response = app(ExceptionHandler::class)->render(request(), $exception); + + expect($glitchTip->events)->toBe([]) + ->and($response->getStatusCode())->toBe(400); +})->with([ + 'locked property' => fn () => new CannotUpdateLockedPropertyException('portal'), + 'upload without trait' => fn () => new MissingFileUploadsTraitException(new class + { + public function getName(): string + { + return 'web.press-release-feed'; + } + }), +]); + test('request data loses tokens, signatures and bodies', function () { $scrubbed = ErrorReportScrubber::scrubRequest([ 'url' => 'https://pressekonto.test/newsroom/claim/5?signature=abc123&expires=1700000000&src=mail_publish',