18d9c2b68SGreg Roach<?php 28d9c2b68SGreg Roach 38d9c2b68SGreg Roach/** 48d9c2b68SGreg Roach * webtrees: online genealogy 58d9c2b68SGreg Roach * Copyright (C) 2021 webtrees development team 68d9c2b68SGreg Roach * This program is free software: you can redistribute it and/or modify 78d9c2b68SGreg Roach * it under the terms of the GNU General Public License as published by 88d9c2b68SGreg Roach * the Free Software Foundation, either version 3 of the License, or 98d9c2b68SGreg Roach * (at your option) any later version. 108d9c2b68SGreg Roach * This program is distributed in the hope that it will be useful, 118d9c2b68SGreg Roach * but WITHOUT ANY WARRANTY; without even the implied warranty of 128d9c2b68SGreg Roach * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 138d9c2b68SGreg Roach * GNU General Public License for more details. 148d9c2b68SGreg Roach * You should have received a copy of the GNU General Public License 158d9c2b68SGreg Roach * along with this program. If not, see <https://www.gnu.org/licenses/>. 168d9c2b68SGreg Roach */ 178d9c2b68SGreg Roach 188d9c2b68SGreg Roachdeclare(strict_types=1); 198d9c2b68SGreg Roach 208d9c2b68SGreg Roachnamespace Fisharebest\Webtrees; 218d9c2b68SGreg Roach 22b55cbc6bSGreg Roachuse Aura\Router\Route; 238d9c2b68SGreg Roachuse Closure; 24b55cbc6bSGreg Roachuse Fisharebest\Webtrees\Contracts\UserInterface; 2581b729d3SGreg Roachuse Fisharebest\Webtrees\Http\Exceptions\HttpBadRequestException; 268d9c2b68SGreg Roachuse LogicException; 278d9c2b68SGreg Roachuse Psr\Http\Message\ServerRequestInterface; 288d9c2b68SGreg Roach 298d9c2b68SGreg Roachuse function array_reduce; 308d9c2b68SGreg Roachuse function ctype_digit; 318d9c2b68SGreg Roachuse function is_array; 328d9c2b68SGreg Roachuse function is_int; 338d9c2b68SGreg Roachuse function is_string; 348d9c2b68SGreg Roachuse function parse_url; 3581b729d3SGreg Roachuse function preg_match; 368d9c2b68SGreg Roachuse function str_starts_with; 378d9c2b68SGreg Roach 388d9c2b68SGreg Roach/** 398d9c2b68SGreg Roach * Validate a parameter from an HTTP request 408d9c2b68SGreg Roach */ 418d9c2b68SGreg Roachclass Validator 428d9c2b68SGreg Roach{ 43*04b3bc82SGreg Roach /** @var array<int|string|Tree|UserInterface|array<int|string>> */ 448d9c2b68SGreg Roach private array $parameters; 458d9c2b68SGreg Roach 468d9c2b68SGreg Roach /** @var array<Closure> */ 478d9c2b68SGreg Roach private array $rules = []; 488d9c2b68SGreg Roach 498d9c2b68SGreg Roach /** 50*04b3bc82SGreg Roach * @param array<int|string|Tree|UserInterface|array<int|string>> $parameters 518d9c2b68SGreg Roach */ 528d9c2b68SGreg Roach public function __construct(array $parameters) 538d9c2b68SGreg Roach { 548d9c2b68SGreg Roach $this->parameters = $parameters; 558d9c2b68SGreg Roach } 568d9c2b68SGreg Roach 578d9c2b68SGreg Roach /** 588d9c2b68SGreg Roach * @param ServerRequestInterface $request 598d9c2b68SGreg Roach * 608d9c2b68SGreg Roach * @return self 618d9c2b68SGreg Roach */ 62b55cbc6bSGreg Roach public static function attributes(ServerRequestInterface $request): self 63b55cbc6bSGreg Roach { 64b55cbc6bSGreg Roach return new self($request->getAttributes()); 65b55cbc6bSGreg Roach } 66b55cbc6bSGreg Roach 67b55cbc6bSGreg Roach /** 68b55cbc6bSGreg Roach * @param ServerRequestInterface $request 69b55cbc6bSGreg Roach * 70b55cbc6bSGreg Roach * @return self 71b55cbc6bSGreg Roach */ 728d9c2b68SGreg Roach public static function parsedBody(ServerRequestInterface $request): self 738d9c2b68SGreg Roach { 748d9c2b68SGreg Roach return new self((array) $request->getParsedBody()); 758d9c2b68SGreg Roach } 768d9c2b68SGreg Roach 778d9c2b68SGreg Roach /** 788d9c2b68SGreg Roach * @param ServerRequestInterface $request 798d9c2b68SGreg Roach * 808d9c2b68SGreg Roach * @return self 818d9c2b68SGreg Roach */ 828d9c2b68SGreg Roach public static function queryParams(ServerRequestInterface $request): self 838d9c2b68SGreg Roach { 848d9c2b68SGreg Roach return new self($request->getQueryParams()); 858d9c2b68SGreg Roach } 868d9c2b68SGreg Roach 878d9c2b68SGreg Roach /** 88b55cbc6bSGreg Roach * @param ServerRequestInterface $request 89b55cbc6bSGreg Roach * 90b55cbc6bSGreg Roach * @return self 91b55cbc6bSGreg Roach */ 92b55cbc6bSGreg Roach public static function serverParams(ServerRequestInterface $request): self 93b55cbc6bSGreg Roach { 94b55cbc6bSGreg Roach return new self($request->getServerParams()); 95b55cbc6bSGreg Roach } 96b55cbc6bSGreg Roach 97b55cbc6bSGreg Roach /** 988d9c2b68SGreg Roach * @param int $minimum 998d9c2b68SGreg Roach * @param int $maximum 1008d9c2b68SGreg Roach * 1018d9c2b68SGreg Roach * @return self 1028d9c2b68SGreg Roach */ 1038d9c2b68SGreg Roach public function isBetween(int $minimum, int $maximum): self 1048d9c2b68SGreg Roach { 1052b1a9a98SGreg Roach $this->rules[] = static function (?int $value) use ($minimum, $maximum): ?int { 1062b1a9a98SGreg Roach if (is_int($value) && $value >= $minimum && $value <= $maximum) { 1078d9c2b68SGreg Roach return $value; 1088d9c2b68SGreg Roach } 1098d9c2b68SGreg Roach 11081b729d3SGreg Roach return null; 1118d9c2b68SGreg Roach }; 1128d9c2b68SGreg Roach 1138d9c2b68SGreg Roach return $this; 1148d9c2b68SGreg Roach } 1158d9c2b68SGreg Roach 11681b729d3SGreg Roach /** 1171c6adce8SGreg Roach * @param array<string> $values 1181c6adce8SGreg Roach * 1191c6adce8SGreg Roach * @return $this 1201c6adce8SGreg Roach */ 1211c6adce8SGreg Roach public function isInArray(array $values): self 1221c6adce8SGreg Roach { 1231c6adce8SGreg Roach $this->rules[] = static fn (?string $value): ?string => is_string($value) && in_array($value, $values, true) ? $value : null; 1241c6adce8SGreg Roach 1251c6adce8SGreg Roach return $this; 1261c6adce8SGreg Roach } 1271c6adce8SGreg Roach /** 12881b729d3SGreg Roach * @param string $base_url 12981b729d3SGreg Roach * 13081b729d3SGreg Roach * @return $this 13181b729d3SGreg Roach */ 13281b729d3SGreg Roach public function isLocalUrl(string $base_url): self 1338d9c2b68SGreg Roach { 1342b1a9a98SGreg Roach $this->rules[] = static function (?string $value) use ($base_url): ?string { 1358d9c2b68SGreg Roach if (is_string($value)) { 1368d9c2b68SGreg Roach $value_info = parse_url($value); 1378d9c2b68SGreg Roach $base_url_info = parse_url($base_url); 1388d9c2b68SGreg Roach 1398d9c2b68SGreg Roach if (!is_array($base_url_info)) { 1408d9c2b68SGreg Roach throw new LogicException(__METHOD__ . ' needs a valid URL'); 1418d9c2b68SGreg Roach } 1428d9c2b68SGreg Roach 1438d9c2b68SGreg Roach if (is_array($value_info)) { 1448d9c2b68SGreg Roach $scheme_ok = ($value_info['scheme'] ?? 'http') === ($base_url_info['scheme'] ?? 'http'); 1458d9c2b68SGreg Roach $host_ok = ($value_info['host'] ?? '') === ($base_url_info['host'] ?? ''); 1468d9c2b68SGreg Roach $port_ok = ($value_info['port'] ?? '') === ($base_url_info['port'] ?? ''); 1478d9c2b68SGreg Roach $user_ok = ($value_info['user'] ?? '') === ($base_url_info['user'] ?? ''); 1488d9c2b68SGreg Roach $path_ok = str_starts_with($value_info['path'] ?? '/', $base_url_info['path'] ?? '/'); 1498d9c2b68SGreg Roach 1508d9c2b68SGreg Roach if ($scheme_ok && $host_ok && $port_ok && $user_ok && $path_ok) { 1518d9c2b68SGreg Roach return $value; 1528d9c2b68SGreg Roach } 1538d9c2b68SGreg Roach } 1548d9c2b68SGreg Roach } 1558d9c2b68SGreg Roach 1562b1a9a98SGreg Roach return null; 15781b729d3SGreg Roach }; 15881b729d3SGreg Roach 15981b729d3SGreg Roach return $this; 16081b729d3SGreg Roach } 16181b729d3SGreg Roach 16281b729d3SGreg Roach /** 16381b729d3SGreg Roach * @return $this 16481b729d3SGreg Roach */ 165b55cbc6bSGreg Roach public function isTag(): self 166b55cbc6bSGreg Roach { 167b55cbc6bSGreg Roach $this->rules[] = static function (?string $value): ?string { 168b55cbc6bSGreg Roach if (is_string($value) && preg_match('/^' . Gedcom::REGEX_TAG . '$/', $value) === 1) { 169b55cbc6bSGreg Roach return $value; 170b55cbc6bSGreg Roach } 171b55cbc6bSGreg Roach 172b55cbc6bSGreg Roach return null; 173b55cbc6bSGreg Roach }; 174b55cbc6bSGreg Roach 175b55cbc6bSGreg Roach return $this; 176b55cbc6bSGreg Roach } 177b55cbc6bSGreg Roach 178b55cbc6bSGreg Roach /** 179b55cbc6bSGreg Roach * @return $this 180b55cbc6bSGreg Roach */ 18181b729d3SGreg Roach public function isXref(): self 18281b729d3SGreg Roach { 1832b1a9a98SGreg Roach $this->rules[] = static function (?string $value): ?string { 1842b1a9a98SGreg Roach if (is_string($value) && preg_match('/^' . Gedcom::REGEX_XREF . '$/', $value) === 1) { 18581b729d3SGreg Roach return $value; 18681b729d3SGreg Roach } 18781b729d3SGreg Roach 18881b729d3SGreg Roach return null; 1898d9c2b68SGreg Roach }; 1908d9c2b68SGreg Roach 1918d9c2b68SGreg Roach return $this; 1928d9c2b68SGreg Roach } 1938d9c2b68SGreg Roach 1948d9c2b68SGreg Roach /** 1958d9c2b68SGreg Roach * @param string $parameter 1968d9c2b68SGreg Roach * 197f6fdd746SJonathan Jaubart * @return array<string>|null 1988d9c2b68SGreg Roach */ 199b55cbc6bSGreg Roach public function optionalArray(string $parameter): ?array 2008d9c2b68SGreg Roach { 2018d9c2b68SGreg Roach $value = $this->parameters[$parameter] ?? null; 2028d9c2b68SGreg Roach 2038d9c2b68SGreg Roach if (!is_array($value)) { 20481b729d3SGreg Roach $value = null; 2058d9c2b68SGreg Roach } 2068d9c2b68SGreg Roach 2072b1a9a98SGreg Roach $callback = static fn (?array $value, Closure $rule): ?array => $rule($value); 2082b1a9a98SGreg Roach 2092b1a9a98SGreg Roach return array_reduce($this->rules, $callback, $value); 2108d9c2b68SGreg Roach } 2118d9c2b68SGreg Roach 2128d9c2b68SGreg Roach /** 2138d9c2b68SGreg Roach * @param string $parameter 2148d9c2b68SGreg Roach * 2158d9c2b68SGreg Roach * @return int|null 2168d9c2b68SGreg Roach */ 217b55cbc6bSGreg Roach public function optionalInteger(string $parameter): ?int 2188d9c2b68SGreg Roach { 2198d9c2b68SGreg Roach $value = $this->parameters[$parameter] ?? null; 2208d9c2b68SGreg Roach 2218d9c2b68SGreg Roach if (is_string($value) && ctype_digit($value)) { 2228d9c2b68SGreg Roach $value = (int) $value; 2238d9c2b68SGreg Roach } else { 2248d9c2b68SGreg Roach $value = null; 2258d9c2b68SGreg Roach } 2268d9c2b68SGreg Roach 2272b1a9a98SGreg Roach $callback = static fn (?int $value, Closure $rule): ?int => $rule($value); 2282b1a9a98SGreg Roach 2292b1a9a98SGreg Roach return array_reduce($this->rules, $callback, $value); 2308d9c2b68SGreg Roach } 2318d9c2b68SGreg Roach 2328d9c2b68SGreg Roach /** 2338d9c2b68SGreg Roach * @param string $parameter 2348d9c2b68SGreg Roach * 2358d9c2b68SGreg Roach * @return string|null 2368d9c2b68SGreg Roach */ 237b55cbc6bSGreg Roach public function optionalString(string $parameter): ?string 2388d9c2b68SGreg Roach { 2398d9c2b68SGreg Roach $value = $this->parameters[$parameter] ?? null; 2408d9c2b68SGreg Roach 2418d9c2b68SGreg Roach if (!is_string($value)) { 2428d9c2b68SGreg Roach $value = null; 2438d9c2b68SGreg Roach } 2448d9c2b68SGreg Roach 2452b1a9a98SGreg Roach $callback = static fn (?string $value, Closure $rule): ?string => $rule($value); 2462b1a9a98SGreg Roach 2472b1a9a98SGreg Roach return array_reduce($this->rules, $callback, $value); 2488d9c2b68SGreg Roach } 24981b729d3SGreg Roach 25081b729d3SGreg Roach /** 25181b729d3SGreg Roach * @param string $parameter 252b55cbc6bSGreg Roach * @param bool|null $default 253b55cbc6bSGreg Roach * 254b55cbc6bSGreg Roach * @return bool 255b55cbc6bSGreg Roach */ 256b55cbc6bSGreg Roach public function boolean(string $parameter, bool $default = null): bool 257b55cbc6bSGreg Roach { 258b55cbc6bSGreg Roach $value = $this->parameters[$parameter] ?? null; 259b55cbc6bSGreg Roach 260b55cbc6bSGreg Roach if (in_array($value, ['1', true], true)) { 261b55cbc6bSGreg Roach return true; 262b55cbc6bSGreg Roach } 263b55cbc6bSGreg Roach 264b55cbc6bSGreg Roach if (in_array($value, ['0', '', false], true)) { 265b55cbc6bSGreg Roach return false; 266b55cbc6bSGreg Roach } 267b55cbc6bSGreg Roach 268b55cbc6bSGreg Roach if ($default === null) { 269b55cbc6bSGreg Roach throw new HttpBadRequestException(I18N::translate('The parameter “%s” is missing.', $parameter)); 270b55cbc6bSGreg Roach } 271b55cbc6bSGreg Roach 272b55cbc6bSGreg Roach return $default; 273b55cbc6bSGreg Roach } 274b55cbc6bSGreg Roach 275b55cbc6bSGreg Roach /** 276b55cbc6bSGreg Roach * @param string $parameter 27781b729d3SGreg Roach * 27881b729d3SGreg Roach * @return array<string> 27981b729d3SGreg Roach */ 280b55cbc6bSGreg Roach public function array(string $parameter): array 28181b729d3SGreg Roach { 282b55cbc6bSGreg Roach $value = $this->parameters[$parameter] ?? null; 28381b729d3SGreg Roach 284*04b3bc82SGreg Roach if (!is_array($value) && $value !== null) { 285*04b3bc82SGreg Roach throw new HttpBadRequestException(I18N::translate('The parameter “%s” is missing.', $parameter)); 286b55cbc6bSGreg Roach } 287b55cbc6bSGreg Roach 288b55cbc6bSGreg Roach $callback = static fn (?array $value, Closure $rule): ?array => $rule($value); 289b55cbc6bSGreg Roach 290b55cbc6bSGreg Roach $value = array_reduce($this->rules, $callback, $value); 291b55cbc6bSGreg Roach $value ??= []; 292b55cbc6bSGreg Roach 293b55cbc6bSGreg Roach $check_utf8 = static function($v, $k) use ($parameter) { 294b55cbc6bSGreg Roach if (is_string($k) && !preg_match('//u', $k) || is_string($v) && !preg_match('//u', $v)) { 2952b1a9a98SGreg Roach throw new HttpBadRequestException(I18N::translate('The parameter “%s” is missing.', $parameter)); 29681b729d3SGreg Roach } 297b55cbc6bSGreg Roach }; 298b55cbc6bSGreg Roach 299b55cbc6bSGreg Roach array_walk_recursive($value, $check_utf8); 30081b729d3SGreg Roach 3012b1a9a98SGreg Roach return $value; 30281b729d3SGreg Roach } 30381b729d3SGreg Roach 30481b729d3SGreg Roach /** 30581b729d3SGreg Roach * @param string $parameter 306b55cbc6bSGreg Roach * @param int|null $default 30781b729d3SGreg Roach * 30881b729d3SGreg Roach * @return int 30981b729d3SGreg Roach */ 310b55cbc6bSGreg Roach public function integer(string $parameter, int $default = null): int 31181b729d3SGreg Roach { 312b55cbc6bSGreg Roach $value = $this->parameters[$parameter] ?? null; 313b55cbc6bSGreg Roach 314b55cbc6bSGreg Roach if (is_string($value) && ctype_digit($value)) { 315b55cbc6bSGreg Roach $value = (int) $value; 316*04b3bc82SGreg Roach } elseif (!is_int($value)) { 317b55cbc6bSGreg Roach $value = null; 318b55cbc6bSGreg Roach } 319b55cbc6bSGreg Roach 320b55cbc6bSGreg Roach $callback = static fn (?int $value, Closure $rule): ?int => $rule($value); 321b55cbc6bSGreg Roach 322b55cbc6bSGreg Roach $value = array_reduce($this->rules, $callback, $value); 323b55cbc6bSGreg Roach 324b55cbc6bSGreg Roach $value ??= $default; 32581b729d3SGreg Roach 3262b1a9a98SGreg Roach if ($value === null) { 3272b1a9a98SGreg Roach throw new HttpBadRequestException(I18N::translate('The parameter “%s” is missing.', $parameter)); 32881b729d3SGreg Roach } 32981b729d3SGreg Roach 3302b1a9a98SGreg Roach return $value; 33181b729d3SGreg Roach } 33281b729d3SGreg Roach 33381b729d3SGreg Roach /** 33481b729d3SGreg Roach * @param string $parameter 33581b729d3SGreg Roach * 336b55cbc6bSGreg Roach * @return Route 337b55cbc6bSGreg Roach */ 338b55cbc6bSGreg Roach public function route(string $parameter = 'route'): Route 339b55cbc6bSGreg Roach { 340b55cbc6bSGreg Roach $value = $this->parameters[$parameter] ?? null; 341b55cbc6bSGreg Roach 342b55cbc6bSGreg Roach if ($value instanceof Route) { 343b55cbc6bSGreg Roach return $value; 344b55cbc6bSGreg Roach } 345b55cbc6bSGreg Roach 346b55cbc6bSGreg Roach throw new HttpBadRequestException(I18N::translate('The parameter “%s” is missing.', $parameter)); 347b55cbc6bSGreg Roach } 348b55cbc6bSGreg Roach 349b55cbc6bSGreg Roach /** 350b55cbc6bSGreg Roach * @param string $parameter 351b55cbc6bSGreg Roach * @param string|null $default 352b55cbc6bSGreg Roach * 35381b729d3SGreg Roach * @return string 35481b729d3SGreg Roach */ 355b55cbc6bSGreg Roach public function string(string $parameter, string $default = null): string 35681b729d3SGreg Roach { 357b55cbc6bSGreg Roach $value = $this->parameters[$parameter] ?? null; 35881b729d3SGreg Roach 359b55cbc6bSGreg Roach if (!is_string($value)) { 360b55cbc6bSGreg Roach $value = null; 361b55cbc6bSGreg Roach } 362b55cbc6bSGreg Roach 363b55cbc6bSGreg Roach $callback = static fn (?string $value, Closure $rule): ?string => $rule($value); 364b55cbc6bSGreg Roach 365b55cbc6bSGreg Roach $value = array_reduce($this->rules, $callback, $value); 366b55cbc6bSGreg Roach $value ??= $default; 367b55cbc6bSGreg Roach 368b55cbc6bSGreg Roach if ($value === null || preg_match('//u', $value) !== 1) { 3692b1a9a98SGreg Roach throw new HttpBadRequestException(I18N::translate('The parameter “%s” is missing.', $parameter)); 37081b729d3SGreg Roach } 37181b729d3SGreg Roach 3722b1a9a98SGreg Roach return $value; 37381b729d3SGreg Roach } 374b55cbc6bSGreg Roach 375b55cbc6bSGreg Roach /** 376b55cbc6bSGreg Roach * @param string $parameter 377b55cbc6bSGreg Roach * 378b55cbc6bSGreg Roach * @return Tree 379b55cbc6bSGreg Roach */ 380b55cbc6bSGreg Roach public function tree(string $parameter = 'tree'): Tree 381b55cbc6bSGreg Roach { 382b55cbc6bSGreg Roach $value = $this->parameters[$parameter] ?? null; 383b55cbc6bSGreg Roach 384b55cbc6bSGreg Roach if ($value instanceof Tree) { 385b55cbc6bSGreg Roach return $value; 386b55cbc6bSGreg Roach } 387b55cbc6bSGreg Roach 388b55cbc6bSGreg Roach throw new HttpBadRequestException(I18N::translate('The parameter “%s” is missing.', $parameter)); 389b55cbc6bSGreg Roach } 390b55cbc6bSGreg Roach 391b55cbc6bSGreg Roach /** 392b55cbc6bSGreg Roach * @param string $parameter 393b55cbc6bSGreg Roach * 394b55cbc6bSGreg Roach * @return Tree|null 395b55cbc6bSGreg Roach */ 396b55cbc6bSGreg Roach public function treeOptional(string $parameter = 'tree'): ?Tree 397b55cbc6bSGreg Roach { 398b55cbc6bSGreg Roach $value = $this->parameters[$parameter] ?? null; 399b55cbc6bSGreg Roach 400b55cbc6bSGreg Roach if ($value === null || $value instanceof Tree) { 401b55cbc6bSGreg Roach return $value; 402b55cbc6bSGreg Roach } 403b55cbc6bSGreg Roach 404b55cbc6bSGreg Roach throw new HttpBadRequestException(I18N::translate('The parameter “%s” is missing.', $parameter)); 405b55cbc6bSGreg Roach } 406b55cbc6bSGreg Roach 407b55cbc6bSGreg Roach /** 408b55cbc6bSGreg Roach * @param string $parameter 409b55cbc6bSGreg Roach * 410b55cbc6bSGreg Roach * @return UserInterface 411b55cbc6bSGreg Roach */ 412b55cbc6bSGreg Roach public function user(string $parameter = 'user'): UserInterface 413b55cbc6bSGreg Roach { 414b55cbc6bSGreg Roach $value = $this->parameters[$parameter] ?? null; 415b55cbc6bSGreg Roach 416b55cbc6bSGreg Roach if ($value instanceof UserInterface) { 417b55cbc6bSGreg Roach return $value; 418b55cbc6bSGreg Roach } 419b55cbc6bSGreg Roach 420b55cbc6bSGreg Roach throw new HttpBadRequestException(I18N::translate('The parameter “%s” is missing.', $parameter)); 421b55cbc6bSGreg Roach } 4228d9c2b68SGreg Roach} 423