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{ 4304b3bc82SGreg 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 /** 5004b3bc82SGreg 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 { 123c3bff7b4SGreg Roach $this->rules[] = static fn (?string $value): ?string => $value !== null && in_array($value, $values, true) ? $value : null; 1241c6adce8SGreg Roach 1251c6adce8SGreg Roach return $this; 1261c6adce8SGreg Roach } 127c3bff7b4SGreg Roach 128c3bff7b4SGreg Roach /** 129*158900c2SGreg Roach * @param array<string> $values 130*158900c2SGreg Roach * 131*158900c2SGreg Roach * @return $this 132*158900c2SGreg Roach */ 133*158900c2SGreg Roach public function isInArrayKeys(array $values): self 134*158900c2SGreg Roach { 135*158900c2SGreg Roach return $this->isInArray(array_keys($values)); 136*158900c2SGreg Roach } 137*158900c2SGreg Roach 138*158900c2SGreg Roach /** 139c3bff7b4SGreg Roach * @return $this 140c3bff7b4SGreg Roach */ 141c3bff7b4SGreg Roach public function isNotEmpty(): self 142c3bff7b4SGreg Roach { 143c3bff7b4SGreg Roach $this->rules[] = static fn (?string $value): ?string => $value !== null && $value !== '' ? $value : null; 144c3bff7b4SGreg Roach 145c3bff7b4SGreg Roach return $this; 146c3bff7b4SGreg Roach } 147c3bff7b4SGreg Roach 1481c6adce8SGreg Roach /** 14981b729d3SGreg Roach * @param string $base_url 15081b729d3SGreg Roach * 15181b729d3SGreg Roach * @return $this 15281b729d3SGreg Roach */ 15381b729d3SGreg Roach public function isLocalUrl(string $base_url): self 1548d9c2b68SGreg Roach { 1552b1a9a98SGreg Roach $this->rules[] = static function (?string $value) use ($base_url): ?string { 156c3bff7b4SGreg Roach if ($value !== null) { 1578d9c2b68SGreg Roach $value_info = parse_url($value); 1588d9c2b68SGreg Roach $base_url_info = parse_url($base_url); 1598d9c2b68SGreg Roach 1608d9c2b68SGreg Roach if (!is_array($base_url_info)) { 1618d9c2b68SGreg Roach throw new LogicException(__METHOD__ . ' needs a valid URL'); 1628d9c2b68SGreg Roach } 1638d9c2b68SGreg Roach 1648d9c2b68SGreg Roach if (is_array($value_info)) { 1658d9c2b68SGreg Roach $scheme_ok = ($value_info['scheme'] ?? 'http') === ($base_url_info['scheme'] ?? 'http'); 1668d9c2b68SGreg Roach $host_ok = ($value_info['host'] ?? '') === ($base_url_info['host'] ?? ''); 1678d9c2b68SGreg Roach $port_ok = ($value_info['port'] ?? '') === ($base_url_info['port'] ?? ''); 1688d9c2b68SGreg Roach $user_ok = ($value_info['user'] ?? '') === ($base_url_info['user'] ?? ''); 1698d9c2b68SGreg Roach $path_ok = str_starts_with($value_info['path'] ?? '/', $base_url_info['path'] ?? '/'); 1708d9c2b68SGreg Roach 1718d9c2b68SGreg Roach if ($scheme_ok && $host_ok && $port_ok && $user_ok && $path_ok) { 1728d9c2b68SGreg Roach return $value; 1738d9c2b68SGreg Roach } 1748d9c2b68SGreg Roach } 1758d9c2b68SGreg Roach } 1768d9c2b68SGreg Roach 1772b1a9a98SGreg Roach return null; 17881b729d3SGreg Roach }; 17981b729d3SGreg Roach 18081b729d3SGreg Roach return $this; 18181b729d3SGreg Roach } 18281b729d3SGreg Roach 18381b729d3SGreg Roach /** 18481b729d3SGreg Roach * @return $this 18581b729d3SGreg Roach */ 186b55cbc6bSGreg Roach public function isTag(): self 187b55cbc6bSGreg Roach { 188b55cbc6bSGreg Roach $this->rules[] = static function (?string $value): ?string { 189c3bff7b4SGreg Roach if ($value !== null && preg_match('/^' . Gedcom::REGEX_TAG . '$/', $value) === 1) { 190b55cbc6bSGreg Roach return $value; 191b55cbc6bSGreg Roach } 192b55cbc6bSGreg Roach 193b55cbc6bSGreg Roach return null; 194b55cbc6bSGreg Roach }; 195b55cbc6bSGreg Roach 196b55cbc6bSGreg Roach return $this; 197b55cbc6bSGreg Roach } 198b55cbc6bSGreg Roach 199b55cbc6bSGreg Roach /** 200b55cbc6bSGreg Roach * @return $this 201b55cbc6bSGreg Roach */ 20281b729d3SGreg Roach public function isXref(): self 20381b729d3SGreg Roach { 2042b1a9a98SGreg Roach $this->rules[] = static function (?string $value): ?string { 205c3bff7b4SGreg Roach if ($value !== null && preg_match('/^' . Gedcom::REGEX_XREF . '$/', $value) === 1) { 20681b729d3SGreg Roach return $value; 20781b729d3SGreg Roach } 20881b729d3SGreg Roach 20981b729d3SGreg Roach return null; 2108d9c2b68SGreg Roach }; 2118d9c2b68SGreg Roach 2128d9c2b68SGreg Roach return $this; 2138d9c2b68SGreg Roach } 2148d9c2b68SGreg Roach 2158d9c2b68SGreg Roach /** 2168d9c2b68SGreg Roach * @param string $parameter 217b55cbc6bSGreg Roach * @param bool|null $default 218b55cbc6bSGreg Roach * 219b55cbc6bSGreg Roach * @return bool 220b55cbc6bSGreg Roach */ 221b55cbc6bSGreg Roach public function boolean(string $parameter, bool $default = null): bool 222b55cbc6bSGreg Roach { 223b55cbc6bSGreg Roach $value = $this->parameters[$parameter] ?? null; 224b55cbc6bSGreg Roach 225b55cbc6bSGreg Roach if (in_array($value, ['1', true], true)) { 226b55cbc6bSGreg Roach return true; 227b55cbc6bSGreg Roach } 228b55cbc6bSGreg Roach 229b55cbc6bSGreg Roach if (in_array($value, ['0', '', false], true)) { 230b55cbc6bSGreg Roach return false; 231b55cbc6bSGreg Roach } 232b55cbc6bSGreg Roach 233b55cbc6bSGreg Roach if ($default === null) { 234b55cbc6bSGreg Roach throw new HttpBadRequestException(I18N::translate('The parameter “%s” is missing.', $parameter)); 235b55cbc6bSGreg Roach } 236b55cbc6bSGreg Roach 237b55cbc6bSGreg Roach return $default; 238b55cbc6bSGreg Roach } 239b55cbc6bSGreg Roach 240b55cbc6bSGreg Roach /** 241b55cbc6bSGreg Roach * @param string $parameter 24281b729d3SGreg Roach * 24381b729d3SGreg Roach * @return array<string> 24481b729d3SGreg Roach */ 245b55cbc6bSGreg Roach public function array(string $parameter): array 24681b729d3SGreg Roach { 247b55cbc6bSGreg Roach $value = $this->parameters[$parameter] ?? null; 24881b729d3SGreg Roach 24904b3bc82SGreg Roach if (!is_array($value) && $value !== null) { 25004b3bc82SGreg Roach throw new HttpBadRequestException(I18N::translate('The parameter “%s” is missing.', $parameter)); 251b55cbc6bSGreg Roach } 252b55cbc6bSGreg Roach 253b55cbc6bSGreg Roach $callback = static fn (?array $value, Closure $rule): ?array => $rule($value); 254b55cbc6bSGreg Roach 255b55cbc6bSGreg Roach $value = array_reduce($this->rules, $callback, $value); 256b55cbc6bSGreg Roach $value ??= []; 257b55cbc6bSGreg Roach 258b55cbc6bSGreg Roach $check_utf8 = static function ($v, $k) use ($parameter) { 259b55cbc6bSGreg Roach if (is_string($k) && !preg_match('//u', $k) || is_string($v) && !preg_match('//u', $v)) { 2602b1a9a98SGreg Roach throw new HttpBadRequestException(I18N::translate('The parameter “%s” is missing.', $parameter)); 26181b729d3SGreg Roach } 262b55cbc6bSGreg Roach }; 263b55cbc6bSGreg Roach 264b55cbc6bSGreg Roach array_walk_recursive($value, $check_utf8); 26581b729d3SGreg Roach 2662b1a9a98SGreg Roach return $value; 26781b729d3SGreg Roach } 26881b729d3SGreg Roach 26981b729d3SGreg Roach /** 27081b729d3SGreg Roach * @param string $parameter 271b55cbc6bSGreg Roach * @param int|null $default 27281b729d3SGreg Roach * 27381b729d3SGreg Roach * @return int 27481b729d3SGreg Roach */ 275b55cbc6bSGreg Roach public function integer(string $parameter, int $default = null): int 27681b729d3SGreg Roach { 277b55cbc6bSGreg Roach $value = $this->parameters[$parameter] ?? null; 278b55cbc6bSGreg Roach 279b55cbc6bSGreg Roach if (is_string($value) && ctype_digit($value)) { 280b55cbc6bSGreg Roach $value = (int) $value; 28104b3bc82SGreg Roach } elseif (!is_int($value)) { 282b55cbc6bSGreg Roach $value = null; 283b55cbc6bSGreg Roach } 284b55cbc6bSGreg Roach 285b55cbc6bSGreg Roach $callback = static fn (?int $value, Closure $rule): ?int => $rule($value); 286b55cbc6bSGreg Roach 287b55cbc6bSGreg Roach $value = array_reduce($this->rules, $callback, $value); 288b55cbc6bSGreg Roach 289b55cbc6bSGreg Roach $value ??= $default; 29081b729d3SGreg Roach 2912b1a9a98SGreg Roach if ($value === null) { 2922b1a9a98SGreg Roach throw new HttpBadRequestException(I18N::translate('The parameter “%s” is missing.', $parameter)); 29381b729d3SGreg Roach } 29481b729d3SGreg Roach 2952b1a9a98SGreg Roach return $value; 29681b729d3SGreg Roach } 29781b729d3SGreg Roach 29881b729d3SGreg Roach /** 29981b729d3SGreg Roach * @param string $parameter 30081b729d3SGreg Roach * 301b55cbc6bSGreg Roach * @return Route 302b55cbc6bSGreg Roach */ 303b55cbc6bSGreg Roach public function route(string $parameter = 'route'): Route 304b55cbc6bSGreg Roach { 305b55cbc6bSGreg Roach $value = $this->parameters[$parameter] ?? null; 306b55cbc6bSGreg Roach 307b55cbc6bSGreg Roach if ($value instanceof Route) { 308b55cbc6bSGreg Roach return $value; 309b55cbc6bSGreg Roach } 310b55cbc6bSGreg Roach 311b55cbc6bSGreg Roach throw new HttpBadRequestException(I18N::translate('The parameter “%s” is missing.', $parameter)); 312b55cbc6bSGreg Roach } 313b55cbc6bSGreg Roach 314b55cbc6bSGreg Roach /** 315b55cbc6bSGreg Roach * @param string $parameter 316b55cbc6bSGreg Roach * @param string|null $default 317b55cbc6bSGreg Roach * 31881b729d3SGreg Roach * @return string 31981b729d3SGreg Roach */ 320b55cbc6bSGreg Roach public function string(string $parameter, string $default = null): string 32181b729d3SGreg Roach { 322b55cbc6bSGreg Roach $value = $this->parameters[$parameter] ?? null; 32381b729d3SGreg Roach 324b55cbc6bSGreg Roach if (!is_string($value)) { 325b55cbc6bSGreg Roach $value = null; 326b55cbc6bSGreg Roach } 327b55cbc6bSGreg Roach 328b55cbc6bSGreg Roach $callback = static fn (?string $value, Closure $rule): ?string => $rule($value); 329b55cbc6bSGreg Roach 330b55cbc6bSGreg Roach $value = array_reduce($this->rules, $callback, $value); 331b55cbc6bSGreg Roach $value ??= $default; 332b55cbc6bSGreg Roach 333b55cbc6bSGreg Roach if ($value === null || preg_match('//u', $value) !== 1) { 3342b1a9a98SGreg Roach throw new HttpBadRequestException(I18N::translate('The parameter “%s” is missing.', $parameter)); 33581b729d3SGreg Roach } 33681b729d3SGreg Roach 3372b1a9a98SGreg Roach return $value; 33881b729d3SGreg Roach } 339b55cbc6bSGreg Roach 340b55cbc6bSGreg Roach /** 341b55cbc6bSGreg Roach * @param string $parameter 342b55cbc6bSGreg Roach * 343b55cbc6bSGreg Roach * @return Tree 344b55cbc6bSGreg Roach */ 345b55cbc6bSGreg Roach public function tree(string $parameter = 'tree'): Tree 346b55cbc6bSGreg Roach { 347b55cbc6bSGreg Roach $value = $this->parameters[$parameter] ?? null; 348b55cbc6bSGreg Roach 349b55cbc6bSGreg Roach if ($value instanceof Tree) { 350b55cbc6bSGreg Roach return $value; 351b55cbc6bSGreg Roach } 352b55cbc6bSGreg Roach 353b55cbc6bSGreg Roach throw new HttpBadRequestException(I18N::translate('The parameter “%s” is missing.', $parameter)); 354b55cbc6bSGreg Roach } 355b55cbc6bSGreg Roach 356b55cbc6bSGreg Roach /** 357b55cbc6bSGreg Roach * @param string $parameter 358b55cbc6bSGreg Roach * 359b55cbc6bSGreg Roach * @return Tree|null 360b55cbc6bSGreg Roach */ 361b55cbc6bSGreg Roach public function treeOptional(string $parameter = 'tree'): ?Tree 362b55cbc6bSGreg Roach { 363b55cbc6bSGreg Roach $value = $this->parameters[$parameter] ?? null; 364b55cbc6bSGreg Roach 365b55cbc6bSGreg Roach if ($value === null || $value instanceof Tree) { 366b55cbc6bSGreg Roach return $value; 367b55cbc6bSGreg Roach } 368b55cbc6bSGreg Roach 369b55cbc6bSGreg Roach throw new HttpBadRequestException(I18N::translate('The parameter “%s” is missing.', $parameter)); 370b55cbc6bSGreg Roach } 371b55cbc6bSGreg Roach 372b55cbc6bSGreg Roach /** 373b55cbc6bSGreg Roach * @param string $parameter 374b55cbc6bSGreg Roach * 375b55cbc6bSGreg Roach * @return UserInterface 376b55cbc6bSGreg Roach */ 377b55cbc6bSGreg Roach public function user(string $parameter = 'user'): UserInterface 378b55cbc6bSGreg Roach { 379b55cbc6bSGreg Roach $value = $this->parameters[$parameter] ?? null; 380b55cbc6bSGreg Roach 381b55cbc6bSGreg Roach if ($value instanceof UserInterface) { 382b55cbc6bSGreg Roach return $value; 383b55cbc6bSGreg Roach } 384b55cbc6bSGreg Roach 385b55cbc6bSGreg Roach throw new HttpBadRequestException(I18N::translate('The parameter “%s” is missing.', $parameter)); 386b55cbc6bSGreg Roach } 3878d9c2b68SGreg Roach} 388