move_detect/cloud/src/Http/Request.php
유창욱 c66b034921 fix(cloud): logout order, url normalization, length caps, package hygiene (P4-9/10/11, P5-6)
- P4-9 [L] logout() rotates the session id before wiping (was regenerate-after-destroy → warning).
- P4-10 [L] EventValidator caps device_id/camera_id (64) + zone_id (32) → 400 not a MySQL
  'Data too long' 500 the edge retries forever.
- P4-11 [L] Request::pathFor collapses leading slashes so '//api/events' isn't misrouted to '/events'.
- P5-6 [L] cafe24 build prunes dev caches (.ruff_cache/.impeccable/__pycache__/…) from the package.

python + phpunit (86) green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Xmn8KZWqiQUguEMRtn6rW
2026-07-17 09:48:35 +09:00

67 lines
3.1 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Http;
final class Request
{
private array $headers;
public function __construct(
private string $method, private string $path, array $headers,
private string $body, private array $query = [], private array $post = []
) {
$this->headers = [];
foreach ($headers as $k => $v) $this->headers[strtolower($k)] = $v;
}
public function method(): string { return $this->method; }
public function path(): string { return $this->path; }
public function header(string $name): ?string { return $this->headers[strtolower($name)] ?? null; }
public function body(): string { return $this->body; }
public function json(): array { $d = json_decode($this->body, true); return is_array($d) ? $d : []; }
public function query(string $k): ?string { return isset($this->query[$k]) ? (string)$this->query[$k] : null; }
public function post(string $k): ?string { return isset($this->post[$k]) ? (string)$this->post[$k] : null; }
/**
* The URL prefix the app is mounted under, derived from SCRIPT_NAME:
* '/index.php' => '' (web root), '/move_detect/index.php' => '/move_detect'.
*
* SCRIPT_NAME is set by the web server, never by the caller. Deriving the mount point
* from client-controlled input (a query param, a header) would let anyone relocate the
* app and route around the prefix.
*/
public static function baseFrom(string $scriptName): string
{
if ($scriptName === '' || $scriptName[0] !== '/') return '';
$dir = str_replace('\\', '/', dirname($scriptName));
return $dir === '/' || $dir === '.' ? '' : rtrim($dir, '/');
}
/**
* The route path for a REQUEST_URI: query string dropped, mount prefix removed.
* Routes are declared as '/login', '/api/events', ... so without this every route 404s
* whenever the app is mounted anywhere but the web root.
*/
public static function pathFor(string $requestUri, string $base): string
{
// Collapse leading slashes FIRST: parse_url('//api/events') reads 'api' as a
// scheme-relative host and returns '/events', silently dropping the first segment.
$requestUri = '/' . ltrim($requestUri, '/');
$path = parse_url($requestUri, PHP_URL_PATH) ?: '/';
if ($base !== '' && ($path === $base || str_starts_with($path, $base . '/'))) {
// Match $base exactly or as a whole segment. A bare str_starts_with($path, $base)
// would also strip '/move_detected/x' and route it as 'ed/x'.
$path = substr($path, strlen($base));
}
return $path === '' ? '/' : $path;
}
public static function fromGlobals(): self
{
$headers = function_exists('getallheaders') ? (getallheaders() ?: []) : [];
$base = self::baseFrom((string)($_SERVER['SCRIPT_NAME'] ?? ''));
return new self(
$_SERVER['REQUEST_METHOD'] ?? 'GET',
self::pathFor((string)($_SERVER['REQUEST_URI'] ?? '/'), $base),
$headers, file_get_contents('php://input') ?: '', $_GET, $_POST
);
}
}