fix: Imagick in der Betriebsüberwachung, Scanner-Anfragen an Livewire als 400, abgebrochene Alpine-Übergänge nicht mehr melden

Auf Live fehlte die PHP-Erweiterung imagick: Herkunftsprüfung und
KI-Vorab-Check der Bilder scheiterten still und ließen jedes Bild durch.
AdminSystemHealth zeigt jetzt die Zeile „PHP-Erweiterung Imagick“ (rot bei
eingeschaltetem Vorab-Check, sonst gelb).

GlitchTip server-pressekonto, Issues 38 und 39: gebaute Livewire-Anfragen
setzen gesperrte Properties (portal/language im Meldungs-Feed und in der
Suche) oder schicken Uploads an Komponenten ohne Upload. Livewire weist sie
korrekt ab; statt 500 bzw. 419 (das als „Seite abgelaufen“ gemeldet wurde)
antworten sie jetzt mit 400 und werden nicht mehr gemeldet.

Browser (Issue 18): Alpine bricht x-transition-Übergänge mit
{isFromCancelledTransition: true} ab — keine Fehlermeldung mehr.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-10-08 13:54:14 +00:00
parent 6a86fa488e
commit 87ad433bca
5 changed files with 90 additions and 0 deletions

View file

@ -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<string, mixed> $queue
*/

View file

@ -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();

View file

@ -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));
});
}

View file

@ -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');
});

View file

@ -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',