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 228d9c2b68SGreg Roachuse Closure; 2381b729d3SGreg Roachuse Fisharebest\Webtrees\Http\Exceptions\HttpBadRequestException; 248d9c2b68SGreg Roachuse LogicException; 258d9c2b68SGreg Roachuse Psr\Http\Message\ServerRequestInterface; 268d9c2b68SGreg Roach 278d9c2b68SGreg Roachuse function array_reduce; 288d9c2b68SGreg Roachuse function ctype_digit; 2981b729d3SGreg Roachuse function gettype; 308d9c2b68SGreg Roachuse function is_array; 318d9c2b68SGreg Roachuse function is_int; 328d9c2b68SGreg Roachuse function is_string; 338d9c2b68SGreg Roachuse function parse_url; 3481b729d3SGreg Roachuse function preg_match; 358d9c2b68SGreg Roachuse function str_starts_with; 368d9c2b68SGreg Roach 378d9c2b68SGreg Roach/** 388d9c2b68SGreg Roach * Validate a parameter from an HTTP request 398d9c2b68SGreg Roach */ 408d9c2b68SGreg Roachclass Validator 418d9c2b68SGreg Roach{ 428d9c2b68SGreg Roach /** @var array<string|array> */ 438d9c2b68SGreg Roach private array $parameters; 448d9c2b68SGreg Roach 458d9c2b68SGreg Roach /** @var array<Closure> */ 468d9c2b68SGreg Roach private array $rules = []; 478d9c2b68SGreg Roach 488d9c2b68SGreg Roach /** 498d9c2b68SGreg Roach * @param array<string|array> $parameters 508d9c2b68SGreg Roach */ 518d9c2b68SGreg Roach public function __construct(array $parameters) 528d9c2b68SGreg Roach { 538d9c2b68SGreg Roach $this->parameters = $parameters; 548d9c2b68SGreg Roach } 558d9c2b68SGreg Roach 568d9c2b68SGreg Roach /** 578d9c2b68SGreg Roach * @param ServerRequestInterface $request 588d9c2b68SGreg Roach * 598d9c2b68SGreg Roach * @return self 608d9c2b68SGreg Roach */ 618d9c2b68SGreg Roach public static function parsedBody(ServerRequestInterface $request): self 628d9c2b68SGreg Roach { 638d9c2b68SGreg Roach return new self((array) $request->getParsedBody()); 648d9c2b68SGreg Roach } 658d9c2b68SGreg Roach 668d9c2b68SGreg Roach /** 678d9c2b68SGreg Roach * @param ServerRequestInterface $request 688d9c2b68SGreg Roach * 698d9c2b68SGreg Roach * @return self 708d9c2b68SGreg Roach */ 718d9c2b68SGreg Roach public static function queryParams(ServerRequestInterface $request): self 728d9c2b68SGreg Roach { 738d9c2b68SGreg Roach return new self($request->getQueryParams()); 748d9c2b68SGreg Roach } 758d9c2b68SGreg Roach 768d9c2b68SGreg Roach /** 778d9c2b68SGreg Roach * @param int $minimum 788d9c2b68SGreg Roach * @param int $maximum 798d9c2b68SGreg Roach * 808d9c2b68SGreg Roach * @return self 818d9c2b68SGreg Roach */ 828d9c2b68SGreg Roach public function isBetween(int $minimum, int $maximum): self 838d9c2b68SGreg Roach { 848d9c2b68SGreg Roach $this->rules[] = static function ($value) use ($minimum, $maximum): ?int { 858d9c2b68SGreg Roach if (is_int($value)) { 8681b729d3SGreg Roach if ($value >= $minimum && $value <= $maximum) { 878d9c2b68SGreg Roach return $value; 888d9c2b68SGreg Roach } 898d9c2b68SGreg Roach 9081b729d3SGreg Roach return null; 9181b729d3SGreg Roach } 9281b729d3SGreg Roach 9381b729d3SGreg Roach if ($value === null) { 9481b729d3SGreg Roach return null; 9581b729d3SGreg Roach } 9681b729d3SGreg Roach 9781b729d3SGreg Roach throw new LogicException(__METHOD__ . ' does not accept ' . gettype($value)); 988d9c2b68SGreg Roach }; 998d9c2b68SGreg Roach 1008d9c2b68SGreg Roach return $this; 1018d9c2b68SGreg Roach } 1028d9c2b68SGreg Roach 10381b729d3SGreg Roach /** 10481b729d3SGreg Roach * @param string $base_url 10581b729d3SGreg Roach * 10681b729d3SGreg Roach * @return $this 10781b729d3SGreg Roach */ 10881b729d3SGreg Roach public function isLocalUrl(string $base_url): self 1098d9c2b68SGreg Roach { 1108d9c2b68SGreg Roach $this->rules[] = static function ($value) use ($base_url): ?string { 1117729532eSGreg Roach if ($value === null) { 1127729532eSGreg Roach return null; 1137729532eSGreg Roach } 1147729532eSGreg Roach 1158d9c2b68SGreg Roach if (is_string($value)) { 1168d9c2b68SGreg Roach $value_info = parse_url($value); 1178d9c2b68SGreg Roach $base_url_info = parse_url($base_url); 1188d9c2b68SGreg Roach 1198d9c2b68SGreg Roach if (!is_array($base_url_info)) { 1208d9c2b68SGreg Roach throw new LogicException(__METHOD__ . ' needs a valid URL'); 1218d9c2b68SGreg Roach } 1228d9c2b68SGreg Roach 1238d9c2b68SGreg Roach if (is_array($value_info)) { 1248d9c2b68SGreg Roach $scheme_ok = ($value_info['scheme'] ?? 'http') === ($base_url_info['scheme'] ?? 'http'); 1258d9c2b68SGreg Roach $host_ok = ($value_info['host'] ?? '') === ($base_url_info['host'] ?? ''); 1268d9c2b68SGreg Roach $port_ok = ($value_info['port'] ?? '') === ($base_url_info['port'] ?? ''); 1278d9c2b68SGreg Roach $user_ok = ($value_info['user'] ?? '') === ($base_url_info['user'] ?? ''); 1288d9c2b68SGreg Roach $path_ok = str_starts_with($value_info['path'] ?? '/', $base_url_info['path'] ?? '/'); 1298d9c2b68SGreg Roach 1308d9c2b68SGreg Roach if ($scheme_ok && $host_ok && $port_ok && $user_ok && $path_ok) { 1318d9c2b68SGreg Roach return $value; 1328d9c2b68SGreg Roach } 1338d9c2b68SGreg Roach } 1348d9c2b68SGreg Roach 1358d9c2b68SGreg Roach return null; 1368d9c2b68SGreg Roach } 1378d9c2b68SGreg Roach 13881b729d3SGreg Roach throw new LogicException(__METHOD__ . ' does not accept ' . gettype($value)); 13981b729d3SGreg Roach }; 14081b729d3SGreg Roach 14181b729d3SGreg Roach return $this; 14281b729d3SGreg Roach } 14381b729d3SGreg Roach 14481b729d3SGreg Roach /** 14581b729d3SGreg Roach * @return $this 14681b729d3SGreg Roach */ 14781b729d3SGreg Roach public function isXref(): self 14881b729d3SGreg Roach { 14981b729d3SGreg Roach $this->rules[] = static function ($value) { 15081b729d3SGreg Roach if (is_string($value)) { 151*f6fdd746SJonathan Jaubart if (preg_match('/^' . Gedcom::REGEX_XREF . '$/', $value) === 1) { 15281b729d3SGreg Roach return $value; 15381b729d3SGreg Roach } 15481b729d3SGreg Roach 15581b729d3SGreg Roach return null; 15681b729d3SGreg Roach } 15781b729d3SGreg Roach 15881b729d3SGreg Roach if (is_array($value)) { 15981b729d3SGreg Roach foreach ($value as $item) { 160*f6fdd746SJonathan Jaubart if (preg_match('/^' . Gedcom::REGEX_XREF . '$/', $item) !== 1) { 16181b729d3SGreg Roach return null; 16281b729d3SGreg Roach } 16381b729d3SGreg Roach } 16481b729d3SGreg Roach 16581b729d3SGreg Roach return $value; 16681b729d3SGreg Roach } 16781b729d3SGreg Roach 16881b729d3SGreg Roach if ($value === null) { 16981b729d3SGreg Roach return null; 17081b729d3SGreg Roach } 17181b729d3SGreg Roach 17281b729d3SGreg Roach throw new LogicException(__METHOD__ . ' does not accept ' . gettype($value)); 1738d9c2b68SGreg Roach }; 1748d9c2b68SGreg Roach 1758d9c2b68SGreg Roach return $this; 1768d9c2b68SGreg Roach } 1778d9c2b68SGreg Roach 1788d9c2b68SGreg Roach /** 1798d9c2b68SGreg Roach * @param string $parameter 1808d9c2b68SGreg Roach * 181*f6fdd746SJonathan Jaubart * @return array<string>|null 1828d9c2b68SGreg Roach */ 183*f6fdd746SJonathan Jaubart public function array(string $parameter): ?array 1848d9c2b68SGreg Roach { 1858d9c2b68SGreg Roach $value = $this->parameters[$parameter] ?? null; 1868d9c2b68SGreg Roach 1878d9c2b68SGreg Roach if (!is_array($value)) { 18881b729d3SGreg Roach $value = null; 1898d9c2b68SGreg Roach } 1908d9c2b68SGreg Roach 191fcbce9d8SGreg Roach return array_reduce($this->rules, static fn ($value, $rule) => $rule($value), $value); 1928d9c2b68SGreg Roach } 1938d9c2b68SGreg Roach 1948d9c2b68SGreg Roach /** 1958d9c2b68SGreg Roach * @param string $parameter 1968d9c2b68SGreg Roach * 1978d9c2b68SGreg Roach * @return int|null 1988d9c2b68SGreg Roach */ 1998d9c2b68SGreg Roach public function integer(string $parameter): ?int 2008d9c2b68SGreg Roach { 2018d9c2b68SGreg Roach $value = $this->parameters[$parameter] ?? null; 2028d9c2b68SGreg Roach 2038d9c2b68SGreg Roach if (is_string($value) && ctype_digit($value)) { 2048d9c2b68SGreg Roach $value = (int) $value; 2058d9c2b68SGreg Roach } else { 2068d9c2b68SGreg Roach $value = null; 2078d9c2b68SGreg Roach } 2088d9c2b68SGreg Roach 209fcbce9d8SGreg Roach return array_reduce($this->rules, static fn ($value, $rule) => $rule($value), $value); 2108d9c2b68SGreg Roach } 2118d9c2b68SGreg Roach 2128d9c2b68SGreg Roach /** 2138d9c2b68SGreg Roach * @param string $parameter 2148d9c2b68SGreg Roach * 2158d9c2b68SGreg Roach * @return string|null 2168d9c2b68SGreg Roach */ 2178d9c2b68SGreg Roach public function string(string $parameter): ?string 2188d9c2b68SGreg Roach { 2198d9c2b68SGreg Roach $value = $this->parameters[$parameter] ?? null; 2208d9c2b68SGreg Roach 2218d9c2b68SGreg Roach if (!is_string($value)) { 2228d9c2b68SGreg Roach $value = null; 2238d9c2b68SGreg Roach } 2248d9c2b68SGreg Roach 2258d9c2b68SGreg Roach return array_reduce($this->rules, static fn ($value, $rule) => $rule($value), $value); 2268d9c2b68SGreg Roach } 22781b729d3SGreg Roach 22881b729d3SGreg Roach /** 22981b729d3SGreg Roach * @param string $parameter 23081b729d3SGreg Roach * 23181b729d3SGreg Roach * @return array<string> 23281b729d3SGreg Roach */ 23381b729d3SGreg Roach public function requiredArray(string $parameter): array 23481b729d3SGreg Roach { 23581b729d3SGreg Roach $value = $this->array($parameter); 23681b729d3SGreg Roach 23781b729d3SGreg Roach if (is_array($value)) { 23881b729d3SGreg Roach return $value; 23981b729d3SGreg Roach } 24081b729d3SGreg Roach 24181b729d3SGreg Roach throw new HttpBadRequestException(I18N::translate('The parameter “%s” is missing.', $parameter)); 24281b729d3SGreg Roach } 24381b729d3SGreg Roach 24481b729d3SGreg Roach /** 24581b729d3SGreg Roach * @param string $parameter 24681b729d3SGreg Roach * 24781b729d3SGreg Roach * @return int 24881b729d3SGreg Roach */ 24981b729d3SGreg Roach public function requiredInteger(string $parameter): int 25081b729d3SGreg Roach { 25181b729d3SGreg Roach $value = $this->integer($parameter); 25281b729d3SGreg Roach 25381b729d3SGreg Roach if (is_int($value)) { 25481b729d3SGreg Roach return $value; 25581b729d3SGreg Roach } 25681b729d3SGreg Roach 25781b729d3SGreg Roach throw new HttpBadRequestException(I18N::translate('The parameter “%s” is missing.', $parameter)); 25881b729d3SGreg Roach } 25981b729d3SGreg Roach 26081b729d3SGreg Roach /** 26181b729d3SGreg Roach * @param string $parameter 26281b729d3SGreg Roach * 26381b729d3SGreg Roach * @return string 26481b729d3SGreg Roach */ 26581b729d3SGreg Roach public function requiredString(string $parameter): string 26681b729d3SGreg Roach { 26781b729d3SGreg Roach $value = $this->string($parameter); 26881b729d3SGreg Roach 26981b729d3SGreg Roach if (is_string($value)) { 27081b729d3SGreg Roach return $value; 27181b729d3SGreg Roach } 27281b729d3SGreg Roach 27381b729d3SGreg Roach throw new HttpBadRequestException(I18N::translate('The parameter “%s” is missing.', $parameter)); 27481b729d3SGreg Roach } 2758d9c2b68SGreg Roach} 276