= self::MIN_PASSWORD_LENGTH; } /** The name as stored, or null when the pair does not match an account. */ public function verify(string $username, string $password): ?string { $user = $this->find($username); if ($user === null || !password_verify($password, $user['password'])) { return null; } return $user['username']; } public function exists(string $username): bool { return $this->find($username) !== null; } /** @return list */ public function names(): array { return array_map(static fn(array $u): string => $u['username'], $this->all()); } /** Adds the account or replaces the password of one that is already there. */ public function put(string $username, string $password): void { if (!self::isValidName($username)) { throw new InvalidArgumentException("Invalid user name: {$username}"); } $users = array_values( array_filter( $this->all(), static fn(array $u): bool => strcasecmp($u['username'], $username) !== 0, ), ); $users[] = ['username' => $username, 'password' => password_hash($password, PASSWORD_DEFAULT)]; usort($users, static fn(array $a, array $b): int => strcmp($a['username'], $b['username'])); $this->write($users); } /** @return array{username:string,password:string}|null */ private function find(string $username): ?array { foreach ($this->all() as $user) { if (strcasecmp($user['username'], $username) === 0) { return $user; } } return null; } /** @return list */ private function all(): array { if (!is_file($this->file)) { return []; } $raw = file_get_contents($this->file); $data = $raw === false ? null : json_decode($raw, true); if (!is_array($data) || !is_array($data['users'] ?? null)) { return []; } $users = []; foreach ($data['users'] as $user) { if (is_array($user) && is_string($user['username'] ?? null) && is_string($user['password'] ?? null)) { $users[] = ['username' => $user['username'], 'password' => $user['password']]; } } return $users; } /** @param list $users */ private function write(array $users): void { $dir = dirname($this->file); if (!is_dir($dir) && !mkdir($dir, 0775, true) && !is_dir($dir)) { throw new RuntimeException("Cannot create data directory {$dir}"); } $json = json_encode(['users' => $users], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); if ($json === false) { throw new RuntimeException('Cannot encode users'); } $tmp = $this->file . '.' . bin2hex(random_bytes(4)) . '.tmp'; if (file_put_contents($tmp, $json, LOCK_EX) === false || !rename($tmp, $this->file)) { @unlink($tmp); throw new RuntimeException('Cannot write users file'); } @chmod($this->file, 0640); } }