attributes->set('preview', $this->isPreview($request)); return $next($request); } protected function isPreview(Request $request): bool { return $this->hasSharedSecret($request) || $this->hasSignedLink($request); } /** * The shared secret: one value, held by the frontend, that unlocks * everything unpublished. */ protected function hasSharedSecret(Request $request): bool { $secret = (string) config('site.preview_token'); // An unset secret must never mean "everyone gets preview access". if (blank($secret)) { return false; } $presented = $request->header('X-Preview-Token') ?? $request->query('preview_token'); return is_string($presented) && hash_equals($secret, $presented); } /** * A link generated for one record. * * Signed and short-lived, and it says which record it is for — so one * forwarded to the wrong person leaks a single draft rather than every * unpublished thing on the site. */ protected function hasSignedLink(Request $request): bool { $token = $request->header('X-Preview-Link') ?? $request->query('preview'); if (! is_string($token) || blank($token)) { return false; } $payload = PreviewLink::open($token); if ($payload === null) { return false; } // Handed to the controllers so a link for one draft cannot be used to // read another. $request->attributes->set('preview_scope', $payload); return true; } }