93 lines
3.6 KiB
PHP
93 lines
3.6 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Middleware;
|
|
|
|
use Closure;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Str;
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
|
|
final class EnsureSimulationRecoveryCookie
|
|
{
|
|
public const NAME = 'investmoz_simulation_recovery';
|
|
|
|
public const DAYS = 30;
|
|
|
|
public function handle(Request $request, Closure $next): Response
|
|
{
|
|
$token = $request->cookie(self::NAME);
|
|
$issued = ! is_string($token) || strlen($token) < 32 || strlen($token) > 256;
|
|
if (! is_string($token) || strlen($token) < 32 || strlen($token) > 256) {
|
|
$provided = $request->header('X-Simulation-Token');
|
|
$token = is_string($provided) && strlen($provided) >= 32 && strlen($provided) <= 256
|
|
? $provided
|
|
: Str::random(96);
|
|
}
|
|
$request->headers->set('X-Simulation-Token', $token);
|
|
|
|
$response = $next($request);
|
|
$forget = $request->isMethod('DELETE')
|
|
&& $request->path() === 'api/v1/simulation-drafts/current'
|
|
&& $response->getStatusCode() === 204;
|
|
$activity = $response->isSuccessful() && $this->isExplicitDraftActivity($request, $response);
|
|
$replay = $response->isSuccessful() && $this->isDraftActivityReplay($request, $response);
|
|
|
|
if (! $issued && ! $activity && ! $replay && ! $forget) {
|
|
return $response;
|
|
}
|
|
|
|
// Successful draft writes reset the server deadline to now + DAYS, so
|
|
// refresh the cookie by the same duration. Routine reads never refresh it.
|
|
$minutes = $forget ? 0 : self::DAYS * 24 * 60;
|
|
if ($replay) {
|
|
$payload = json_decode((string) $response->getContent(), true);
|
|
$expiry = is_array($payload) ? ($payload['expires_at'] ?? null) : null;
|
|
if (is_string($expiry)) {
|
|
try {
|
|
$secondsLeft = \Illuminate\Support\Carbon::parse($expiry)->getTimestamp() - now()->getTimestamp();
|
|
$minutes = max(1, (int) ceil($secondsLeft / 60));
|
|
} catch (\Throwable) {
|
|
// Preserve the standard cookie lifetime if the response has no valid deadline.
|
|
}
|
|
}
|
|
}
|
|
|
|
return $response->withCookie(cookie(
|
|
self::NAME,
|
|
$forget ? '' : $token,
|
|
$minutes,
|
|
'/api/v1',
|
|
null,
|
|
$request->isSecure(),
|
|
true,
|
|
false,
|
|
'lax',
|
|
));
|
|
}
|
|
|
|
private function isExplicitDraftActivity(Request $request, Response $response): bool
|
|
{
|
|
if ($request->isMethod('POST') && $request->path() === 'api/v1/simulation-drafts') {
|
|
return $response->getStatusCode() === 201;
|
|
}
|
|
|
|
return ($request->isMethod('PUT') && preg_match('#^api/v1/simulation-drafts/[0-9a-f-]{36}$#i', $request->path()) === 1)
|
|
|| ($request->isMethod('POST') && preg_match('#^api/v1/simulation-drafts/[0-9a-f-]{36}/(?:conversions|touch)$#i', $request->path()) === 1);
|
|
}
|
|
|
|
private function isDraftActivityReplay(Request $request, Response $response): bool
|
|
{
|
|
if (! $request->isMethod('POST') || $response->getStatusCode() !== 200) {
|
|
return false;
|
|
}
|
|
if ($request->path() !== 'api/v1/simulation-drafts'
|
|
&& preg_match('#^api/v1/simulation-drafts/[0-9a-f-]{36}/conversions$#i', $request->path()) !== 1) {
|
|
return false;
|
|
}
|
|
|
|
$payload = json_decode((string) $response->getContent(), true);
|
|
|
|
return is_array($payload) && ($payload['data']['replayed'] ?? $payload['replayed'] ?? false) === true;
|
|
}
|
|
}
|