- P3-2 [H] snapshot retention (7 days): snapshot_retention_days config;
SnapshotRepository::purgeOlderThan (interface + Pdo + double, files-then-rows,
bounded batch); opportunistic purge in the ingest path (Cafe24 has no cron).
- P4-1 [H] index.php sets display_errors=0 + a global exception handler → generic
{"error":"internal"} + error_log (no DSN/db_user leak); ships www/move_detect/.user.ini.
- P4-2 [H] ConfigController + HeartbeatController take an optional rate limiter +
clientIp; pre-auth per-IP anti-flood → 429 (were unthrottled).
- P4-3 [M] PdoNonceRepository::remember + PdoRateLimiterRepository::hit opportunistically
reap expired rows (Cafe24 has no cron; tables grew unbounded).
- P4-4 [M] Router::dispatch rawurldecodes captured params (Korean/space camera_id could
save zones but never load them).
Tests: python green; phpunit 84 green. See docs/delivery-audit.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Xmn8KZWqiQUguEMRtn6rW
40 lines
1.6 KiB
PHP
40 lines
1.6 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
namespace App\Repository\Pdo;
|
|
|
|
use App\Repository\NonceRepository;
|
|
use PDO;
|
|
|
|
/**
|
|
* `nonces.nonce` is the PRIMARY KEY (see schema.sql). IngestController already
|
|
* calls seen() before remember(), so under normal operation remember() never
|
|
* collides. Under a replay race (two requests with the same nonce arriving
|
|
* concurrently), the PK uniqueness constraint is the real guard: the losing
|
|
* remember() throws a PDOException on the duplicate key instead of silently
|
|
* overwriting -- closing the seen()/remember() TOCTOU flagged in the Task 7
|
|
* review. We deliberately do not catch/swallow that exception here.
|
|
*/
|
|
final class PdoNonceRepository implements NonceRepository
|
|
{
|
|
public function __construct(private PDO $pdo) {}
|
|
|
|
public function seen(string $nonce): bool
|
|
{
|
|
$st = $this->pdo->prepare('SELECT 1 FROM nonces WHERE nonce = ? LIMIT 1');
|
|
$st->execute([$nonce]);
|
|
return (bool)$st->fetchColumn();
|
|
}
|
|
|
|
public function remember(string $nonce, int $expiresAt): void
|
|
{
|
|
$st = $this->pdo->prepare('INSERT INTO nonces (nonce, expires_at) VALUES (?, ?)');
|
|
$st->execute([$nonce, $expiresAt]);
|
|
// Cron-less reap: Cafe24 has no scheduler, so expired nonces accumulate forever until
|
|
// the table bloats and inserts slow/fail. Opportunistically prune a bounded batch of
|
|
// already-expired rows (safe: seen() only cares about unexpired ones).
|
|
if (mt_rand(1, 100) === 1) {
|
|
$del = $this->pdo->prepare('DELETE FROM nonces WHERE expires_at < ? LIMIT 5000');
|
|
$del->execute([time()]);
|
|
}
|
|
}
|
|
}
|