media: PM-Medien privat ausliefern + Vorschau-Signatur (M4, M5, Fix#4/#5)
- Neue PM-Bilder/Anhänge liegen auf der privaten local-Disk; Auslieferung über kontrollierte Media-Routen mit gemeinsamer Zugriffslogik (AuthorizesPressReleaseMediaAccess). Öffentliche Medien werden aus den geladenen Modellen ermittelt (weniger Queries) und länger gecacht. - Anonyme Token-Vorschau lädt Bilder über temporär signierte URLs (Fix#5). - API-Resource baut Variant-URLs über die Media-Route statt /storage (Fix#4). Storage-Schicht prüft zusätzlich den serverseitigen MIME-Type (M5). - press-releases:privatize-media migriert Bestandsmedien. Deployment: einmalig `php artisan press-releases:privatize-media` ausführen. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
0b35c3b04d
commit
000ae93e3b
21 changed files with 588 additions and 70 deletions
|
|
@ -0,0 +1,57 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Web\Concerns;
|
||||
|
||||
use App\Models\PressRelease;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
|
||||
/**
|
||||
* Gemeinsame Zugriffslogik der geschützten Presse-Medienrouten (Bilder und
|
||||
* Anhänge): Öffentlich sichtbare PMs sind frei abrufbar; sonst braucht es eine
|
||||
* gültige Signatur (anonyme Token-Vorschau) oder eine angemeldete Session mit
|
||||
* `view`-Recht auf die PM.
|
||||
*/
|
||||
trait AuthorizesPressReleaseMediaAccess
|
||||
{
|
||||
protected function authorizePressReleaseMediaAccess(Request $request, PressRelease $pressRelease, bool $isPublic): void
|
||||
{
|
||||
if ($isPublic) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Signierte URL (Vorschau-Link) autorisiert den Zugriff auf die Medien
|
||||
// einer noch nicht öffentlichen PM, ohne eine Session zu erfordern.
|
||||
if ($request->hasValidSignature()) {
|
||||
return;
|
||||
}
|
||||
|
||||
abort_unless(auth()->check(), 404);
|
||||
Gate::authorize('view', $pressRelease);
|
||||
}
|
||||
|
||||
/**
|
||||
* Öffentliche Sichtbarkeit aus den bereits geladenen Modellen ermitteln
|
||||
* (PM inkl. `company` werden vom Controller eager-geladen), statt eine
|
||||
* separate EXISTS-Abfrage je Medien-Request zu fahren.
|
||||
*/
|
||||
protected function pressReleaseMediaIsPublic(PressRelease $pressRelease): bool
|
||||
{
|
||||
$company = $pressRelease->company;
|
||||
|
||||
return $pressRelease->isVisibleInArchive()
|
||||
&& $company !== null
|
||||
&& (bool) $company->is_active;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache-Control für die Auslieferung. Öffentliche Medien werden lange
|
||||
* gecacht (Browser/CDN), damit die PHP-Route auf den hochfrequenten
|
||||
* Archiv-/Detailseiten nicht bei jedem Bild erneut getroffen wird; nicht
|
||||
* öffentliche (Vorschau/geschützt) werden nie gecacht.
|
||||
*/
|
||||
protected function mediaCacheControl(bool $isPublic): string
|
||||
{
|
||||
return $isPublic ? 'public, max-age=86400' : 'private, no-store';
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Web;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Web\Concerns\AuthorizesPressReleaseMediaAccess;
|
||||
use App\Models\PressRelease;
|
||||
use App\Models\PressReleaseAttachment;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
class PressReleaseAttachmentMediaController extends Controller
|
||||
{
|
||||
use AuthorizesPressReleaseMediaAccess;
|
||||
|
||||
public function __invoke(Request $request, int $attachment): StreamedResponse
|
||||
{
|
||||
$attachment = PressReleaseAttachment::query()
|
||||
->with(['pressRelease' => fn ($query) => $query
|
||||
->withoutGlobalScopes()
|
||||
->with(['company' => fn ($company) => $company->withoutGlobalScopes()])])
|
||||
->findOrFail($attachment);
|
||||
$pressRelease = $attachment->pressRelease;
|
||||
|
||||
abort_unless($pressRelease instanceof PressRelease, 404);
|
||||
$isPublic = $this->pressReleaseMediaIsPublic($pressRelease);
|
||||
$this->authorizePressReleaseMediaAccess($request, $pressRelease, $isPublic);
|
||||
|
||||
return Storage::disk($attachment->disk)->download(
|
||||
$attachment->path,
|
||||
$attachment->original_name,
|
||||
[
|
||||
'Cache-Control' => $this->mediaCacheControl($isPublic),
|
||||
'X-Content-Type-Options' => 'nosniff',
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Web;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Web\Concerns\AuthorizesPressReleaseMediaAccess;
|
||||
use App\Models\PressRelease;
|
||||
use App\Models\PressReleaseImage;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
class PressReleaseImageMediaController extends Controller
|
||||
{
|
||||
use AuthorizesPressReleaseMediaAccess;
|
||||
|
||||
public function __invoke(Request $request, int $image, ?string $variant = null): StreamedResponse
|
||||
{
|
||||
$image = PressReleaseImage::query()
|
||||
->with(['pressRelease' => fn ($query) => $query
|
||||
->withoutGlobalScopes()
|
||||
->with(['company' => fn ($company) => $company->withoutGlobalScopes()])])
|
||||
->findOrFail($image);
|
||||
$pressRelease = $image->pressRelease;
|
||||
|
||||
abort_unless($pressRelease instanceof PressRelease, 404);
|
||||
$isPublic = $this->pressReleaseMediaIsPublic($pressRelease);
|
||||
$this->authorizePressReleaseMediaAccess($request, $pressRelease, $isPublic);
|
||||
|
||||
$path = $image->pathForVariant($variant);
|
||||
|
||||
abort_unless(is_string($path) && filled($path), 404);
|
||||
|
||||
return Storage::disk($image->disk)->response($path, headers: [
|
||||
'Cache-Control' => $this->mediaCacheControl($isPublic),
|
||||
'X-Content-Type-Options' => 'nosniff',
|
||||
]);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue