1<?php 2 3/** 4 * webtrees: online genealogy 5 * Copyright (C) 2021 webtrees development team 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License as published by 8 * the Free Software Foundation, either version 3 of the License, or 9 * (at your option) any later version. 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * You should have received a copy of the GNU General Public License 15 * along with this program. If not, see <https://www.gnu.org/licenses/>. 16 */ 17 18declare(strict_types=1); 19 20namespace Fisharebest\Webtrees; 21 22use Closure; 23use Fisharebest\Webtrees\Http\Exceptions\HttpBadRequestException; 24use LogicException; 25use Psr\Http\Message\ServerRequestInterface; 26 27use function array_reduce; 28use function ctype_digit; 29use function is_array; 30use function is_int; 31use function is_string; 32use function parse_url; 33use function preg_match; 34use function str_starts_with; 35 36/** 37 * Validate a parameter from an HTTP request 38 */ 39class Validator 40{ 41 /** @var array<string|array<string>> */ 42 private array $parameters; 43 44 /** @var array<Closure> */ 45 private array $rules = []; 46 47 /** 48 * @param array<string|array<string>> $parameters 49 */ 50 public function __construct(array $parameters) 51 { 52 $this->parameters = $parameters; 53 } 54 55 /** 56 * @param ServerRequestInterface $request 57 * 58 * @return self 59 */ 60 public static function parsedBody(ServerRequestInterface $request): self 61 { 62 return new self((array) $request->getParsedBody()); 63 } 64 65 /** 66 * @param ServerRequestInterface $request 67 * 68 * @return self 69 */ 70 public static function queryParams(ServerRequestInterface $request): self 71 { 72 return new self($request->getQueryParams()); 73 } 74 75 /** 76 * @param int $minimum 77 * @param int $maximum 78 * 79 * @return self 80 */ 81 public function isBetween(int $minimum, int $maximum): self 82 { 83 $this->rules[] = static function (?int $value) use ($minimum, $maximum): ?int { 84 if (is_int($value) && $value >= $minimum && $value <= $maximum) { 85 return $value; 86 } 87 88 return null; 89 }; 90 91 return $this; 92 } 93 94 /** 95 * @param array<string> $values 96 * 97 * @return $this 98 */ 99 public function isInArray(array $values): self 100 { 101 $this->rules[] = static fn (?string $value): ?string => is_string($value) && in_array($value, $values, true) ? $value : null; 102 103 return $this; 104 } 105 /** 106 * @param string $base_url 107 * 108 * @return $this 109 */ 110 public function isLocalUrl(string $base_url): self 111 { 112 $this->rules[] = static function (?string $value) use ($base_url): ?string { 113 if (is_string($value)) { 114 $value_info = parse_url($value); 115 $base_url_info = parse_url($base_url); 116 117 if (!is_array($base_url_info)) { 118 throw new LogicException(__METHOD__ . ' needs a valid URL'); 119 } 120 121 if (is_array($value_info)) { 122 $scheme_ok = ($value_info['scheme'] ?? 'http') === ($base_url_info['scheme'] ?? 'http'); 123 $host_ok = ($value_info['host'] ?? '') === ($base_url_info['host'] ?? ''); 124 $port_ok = ($value_info['port'] ?? '') === ($base_url_info['port'] ?? ''); 125 $user_ok = ($value_info['user'] ?? '') === ($base_url_info['user'] ?? ''); 126 $path_ok = str_starts_with($value_info['path'] ?? '/', $base_url_info['path'] ?? '/'); 127 128 if ($scheme_ok && $host_ok && $port_ok && $user_ok && $path_ok) { 129 return $value; 130 } 131 } 132 } 133 134 return null; 135 }; 136 137 return $this; 138 } 139 140 /** 141 * @return $this 142 */ 143 public function isXref(): self 144 { 145 $this->rules[] = static function (?string $value): ?string { 146 if (is_string($value) && preg_match('/^' . Gedcom::REGEX_XREF . '$/', $value) === 1) { 147 return $value; 148 } 149 150 return null; 151 }; 152 153 return $this; 154 } 155 156 /** 157 * @param string $parameter 158 * 159 * @return array<string>|null 160 */ 161 public function array(string $parameter): ?array 162 { 163 $value = $this->parameters[$parameter] ?? null; 164 165 if (!is_array($value)) { 166 $value = null; 167 } 168 169 $callback = static fn (?array $value, Closure $rule): ?array => $rule($value); 170 171 return array_reduce($this->rules, $callback, $value); 172 } 173 174 /** 175 * @param string $parameter 176 * 177 * @return int|null 178 */ 179 public function integer(string $parameter): ?int 180 { 181 $value = $this->parameters[$parameter] ?? null; 182 183 if (is_string($value) && ctype_digit($value)) { 184 $value = (int) $value; 185 } else { 186 $value = null; 187 } 188 189 $callback = static fn (?int $value, Closure $rule): ?int => $rule($value); 190 191 return array_reduce($this->rules, $callback, $value); 192 } 193 194 /** 195 * @param string $parameter 196 * 197 * @return string|null 198 */ 199 public function string(string $parameter): ?string 200 { 201 $value = $this->parameters[$parameter] ?? null; 202 203 if (!is_string($value)) { 204 $value = null; 205 } 206 207 $callback = static fn (?string $value, Closure $rule): ?string => $rule($value); 208 209 return array_reduce($this->rules, $callback, $value); 210 } 211 212 /** 213 * @param string $parameter 214 * 215 * @return array<string> 216 */ 217 public function requiredArray(string $parameter): array 218 { 219 $value = $this->array($parameter); 220 221 if ($value === null) { 222 throw new HttpBadRequestException(I18N::translate('The parameter “%s” is missing.', $parameter)); 223 } 224 225 return $value; 226 } 227 228 /** 229 * @param string $parameter 230 * 231 * @return int 232 */ 233 public function requiredInteger(string $parameter): int 234 { 235 $value = $this->integer($parameter); 236 237 if ($value === null) { 238 throw new HttpBadRequestException(I18N::translate('The parameter “%s” is missing.', $parameter)); 239 } 240 241 return $value; 242 } 243 244 /** 245 * @param string $parameter 246 * 247 * @return string 248 */ 249 public function requiredString(string $parameter): string 250 { 251 $value = $this->string($parameter); 252 253 if ($value === null) { 254 throw new HttpBadRequestException(I18N::translate('The parameter “%s” is missing.', $parameter)); 255 } 256 257 return $value; 258 } 259} 260