Implements Task 16 (final, P1-B): App\Db\Database::connect(Config): PDO factory and the 9 App\Repository\Pdo\Pdo*Repository classes backing the repository interfaces consumed by controllers since T1-T15, all-prepared statements, plus cloud/schema.sql (MySQL 8 DDL) and cloud/docs/cloud-deploy.md. Security hardening incorporated per accumulated task reviews: - PdoNonceRepository: nonces.nonce is PK; duplicate remember() throws instead of racing seen() (closes T7 TOCTOU). - PdoRateLimiterRepository::hit(): atomic INSERT ... ON DUPLICATE KEY UPDATE upsert on (rate_key, window_start), no read-modify-write race. - PdoSnapshotRepository::pathByUuid(): validates 32-hex uuid before building any filesystem path (closes T12 path-traversal risk). - PdoZoneRepository::save(): update path is ownership-scoped by (zone_id, device_id, camera_id); throws on mismatch instead of silently overwriting an unrelated zone (closes T13 cross-tenant overwrite risk). - Database::connect(): ERRMODE_EXCEPTION, EMULATE_PREPARES=false, FETCH_ASSOC. cloud/tests/PdoIntegrationTest.php round-trips Nonce/Event/Zone against a real sqlite :memory: DB, gated via markTestSkipped when pdo_sqlite is unavailable (this host has pdo_mysql only, no pdo_sqlite) -- suite stays green with 4 tests skipped. Full suite: 59 tests, 114 assertions, 4 skipped, 0 failures, 0 warnings/deprecations. php -l passes on all new files and on cloud/public/index.php (T15), which references all new FQCNs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ne4rU4FToTLHcBUMUvUnT1
32 lines
1 KiB
PHP
32 lines
1 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
namespace App\Repository\Pdo;
|
|
|
|
use App\Repository\RateLimiterRepository;
|
|
use PDO;
|
|
|
|
/**
|
|
* Atomic upsert via MySQL's INSERT ... ON DUPLICATE KEY UPDATE against the
|
|
* unique (rate_key, window_start) primary key (see schema.sql) -- no
|
|
* read-modify-write race between reading the current count and writing the
|
|
* incremented value back.
|
|
*/
|
|
final class PdoRateLimiterRepository implements RateLimiterRepository
|
|
{
|
|
public function __construct(private PDO $pdo) {}
|
|
|
|
public function hit(string $key, int $windowStart): int
|
|
{
|
|
$upsert = $this->pdo->prepare(
|
|
'INSERT INTO rate_limits (rate_key, window_start, count) VALUES (?, ?, 1)
|
|
ON DUPLICATE KEY UPDATE count = count + 1'
|
|
);
|
|
$upsert->execute([$key, $windowStart]);
|
|
|
|
$select = $this->pdo->prepare(
|
|
'SELECT count FROM rate_limits WHERE rate_key = ? AND window_start = ?'
|
|
);
|
|
$select->execute([$key, $windowStart]);
|
|
return (int)$select->fetchColumn();
|
|
}
|
|
}
|