1<?php 2 3/** 4 * webtrees: online genealogy 5 * Copyright (C) 2022 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 Aura\Router\Route; 23use Closure; 24use Fisharebest\Webtrees\Contracts\UserInterface; 25use Fisharebest\Webtrees\Http\Exceptions\HttpBadRequestException; 26use Psr\Http\Message\ServerRequestInterface; 27 28use function array_reduce; 29use function ctype_digit; 30use function in_array; 31use function is_array; 32use function is_int; 33use function is_string; 34use function parse_url; 35use function preg_match; 36use function str_starts_with; 37 38/** 39 * Validate a parameter from an HTTP request 40 */ 41class Validator 42{ 43 /** @var array<int|string|Tree|UserInterface|array<int|string>> */ 44 private array $parameters; 45 46 private ServerRequestInterface $request; 47 48 /** @var array<Closure> */ 49 private array $rules = []; 50 51 /** 52 * @param array<int|string|Tree|UserInterface|array<int|string>> $parameters 53 * @param ServerRequestInterface $request 54 */ 55 public function __construct(array $parameters, ServerRequestInterface $request) 56 { 57 $this->parameters = $parameters; 58 $this->request = $request; 59 } 60 61 /** 62 * @param ServerRequestInterface $request 63 * 64 * @return self 65 */ 66 public static function attributes(ServerRequestInterface $request): self 67 { 68 return new self($request->getAttributes(), $request); 69 } 70 71 /** 72 * @param ServerRequestInterface $request 73 * 74 * @return self 75 */ 76 public static function parsedBody(ServerRequestInterface $request): self 77 { 78 return new self((array) $request->getParsedBody(), $request); 79 } 80 81 /** 82 * @param ServerRequestInterface $request 83 * 84 * @return self 85 */ 86 public static function queryParams(ServerRequestInterface $request): self 87 { 88 return new self($request->getQueryParams(), $request); 89 } 90 91 /** 92 * @param ServerRequestInterface $request 93 * 94 * @return self 95 */ 96 public static function serverParams(ServerRequestInterface $request): self 97 { 98 return new self($request->getServerParams(), $request); 99 } 100 101 /** 102 * @param int $minimum 103 * @param int $maximum 104 * 105 * @return self 106 */ 107 public function isBetween(int $minimum, int $maximum): self 108 { 109 $this->rules[] = static function (?int $value) use ($minimum, $maximum): ?int { 110 if (is_int($value) && $value >= $minimum && $value <= $maximum) { 111 return $value; 112 } 113 114 return null; 115 }; 116 117 return $this; 118 } 119 120 /** 121 * @param array<string> $values 122 * 123 * @return self 124 */ 125 public function isInArray(array $values): self 126 { 127 $this->rules[] = static fn (/*int|string|null*/ $value)/*: int|string|null*/ => $value !== null && in_array($value, $values, true) ? $value : null; 128 129 return $this; 130 } 131 132 /** 133 * @param array<string> $values 134 * 135 * @return self 136 */ 137 public function isInArrayKeys(array $values): self 138 { 139 return $this->isInArray(array_keys($values)); 140 } 141 142 /** 143 * @return self 144 */ 145 public function isNotEmpty(): self 146 { 147 $this->rules[] = static fn (?string $value): ?string => $value !== null && $value !== '' ? $value : null; 148 149 return $this; 150 } 151 152 /** 153 * @return self 154 */ 155 public function isLocalUrl(): self 156 { 157 $base_url = $this->request->getAttribute('base_url', ''); 158 159 $this->rules[] = static function (?string $value) use ($base_url): ?string { 160 if ($value !== null) { 161 $value_info = parse_url($value); 162 $base_url_info = parse_url($base_url); 163 164 if (is_array($value_info) && is_array($base_url_info)) { 165 $scheme_ok = ($value_info['scheme'] ?? 'http') === ($base_url_info['scheme'] ?? 'http'); 166 $host_ok = ($value_info['host'] ?? '') === ($base_url_info['host'] ?? ''); 167 $port_ok = ($value_info['port'] ?? '') === ($base_url_info['port'] ?? ''); 168 $user_ok = ($value_info['user'] ?? '') === ($base_url_info['user'] ?? ''); 169 $path_ok = str_starts_with($value_info['path'] ?? '/', $base_url_info['path'] ?? '/'); 170 171 if ($scheme_ok && $host_ok && $port_ok && $user_ok && $path_ok) { 172 return $value; 173 } 174 } 175 } 176 177 return null; 178 }; 179 180 return $this; 181 } 182 183 /** 184 * @return self 185 */ 186 public function isTag(): self 187 { 188 $this->rules[] = static function (?string $value): ?string { 189 if ($value !== null && preg_match('/^' . Gedcom::REGEX_TAG . '$/', $value) === 1) { 190 return $value; 191 } 192 193 return null; 194 }; 195 196 return $this; 197 } 198 199 /** 200 * @return self 201 */ 202 public function isXref(): self 203 { 204 $this->rules[] = static function (?string $value): ?string { 205 if ($value !== null && preg_match('/^' . Gedcom::REGEX_XREF . '$/', $value) === 1) { 206 return $value; 207 } 208 209 return null; 210 }; 211 212 return $this; 213 } 214 215 /** 216 * @param string $parameter 217 * @param bool|null $default 218 * 219 * @return bool 220 */ 221 public function boolean(string $parameter, bool $default = null): bool 222 { 223 $value = $this->parameters[$parameter] ?? null; 224 225 if (in_array($value, ['1', 'on', true], true)) { 226 return true; 227 } 228 229 if (in_array($value, ['0', '', false], true)) { 230 return false; 231 } 232 233 if ($default === null) { 234 throw new HttpBadRequestException(I18N::translate('The parameter “%s” is missing.', $parameter)); 235 } 236 237 return $default; 238 } 239 240 /** 241 * @param string $parameter 242 * 243 * @return array<string> 244 */ 245 public function array(string $parameter): array 246 { 247 $value = $this->parameters[$parameter] ?? null; 248 249 if (!is_array($value) && $value !== null) { 250 throw new HttpBadRequestException(I18N::translate('The parameter “%s” is missing.', $parameter)); 251 } 252 253 $callback = static fn (?array $value, Closure $rule): ?array => $rule($value); 254 255 $value = array_reduce($this->rules, $callback, $value); 256 $value ??= []; 257 258 $check_utf8 = static function ($v, $k) use ($parameter) { 259 if (is_string($k) && !preg_match('//u', $k) || is_string($v) && !preg_match('//u', $v)) { 260 throw new HttpBadRequestException(I18N::translate('The parameter “%s” is missing.', $parameter)); 261 } 262 }; 263 264 array_walk_recursive($value, $check_utf8); 265 266 return $value; 267 } 268 269 /** 270 * @param string $parameter 271 * @param int|null $default 272 * 273 * @return int 274 */ 275 public function integer(string $parameter, int $default = null): int 276 { 277 $value = $this->parameters[$parameter] ?? null; 278 279 if (is_string($value) && ctype_digit($value)) { 280 $value = (int) $value; 281 } elseif (!is_int($value)) { 282 $value = null; 283 } 284 285 $callback = static fn (?int $value, Closure $rule): ?int => $rule($value); 286 287 $value = array_reduce($this->rules, $callback, $value); 288 289 $value ??= $default; 290 291 if ($value === null) { 292 throw new HttpBadRequestException(I18N::translate('The parameter “%s” is missing.', $parameter)); 293 } 294 295 return $value; 296 } 297 298 /** 299 * @param string $parameter 300 * 301 * @return Route 302 */ 303 public function route(string $parameter = 'route'): Route 304 { 305 $value = $this->parameters[$parameter] ?? null; 306 307 if ($value instanceof Route) { 308 return $value; 309 } 310 311 throw new HttpBadRequestException(I18N::translate('The parameter “%s” is missing.', $parameter)); 312 } 313 314 /** 315 * @param string $parameter 316 * @param string|null $default 317 * 318 * @return string 319 */ 320 public function string(string $parameter, string $default = null): string 321 { 322 $value = $this->parameters[$parameter] ?? null; 323 324 if (!is_string($value)) { 325 $value = null; 326 } 327 328 $callback = static fn (?string $value, Closure $rule): ?string => $rule($value); 329 330 $value = array_reduce($this->rules, $callback, $value); 331 $value ??= $default; 332 333 if ($value === null || preg_match('//u', $value) !== 1) { 334 throw new HttpBadRequestException(I18N::translate('The parameter “%s” is missing.', $parameter)); 335 } 336 337 return $value; 338 } 339 340 /** 341 * @param string $parameter 342 * 343 * @return Tree 344 */ 345 public function tree(string $parameter = 'tree'): Tree 346 { 347 $value = $this->parameters[$parameter] ?? null; 348 349 if ($value instanceof Tree) { 350 return $value; 351 } 352 353 throw new HttpBadRequestException(I18N::translate('The parameter “%s” is missing.', $parameter)); 354 } 355 356 /** 357 * @param string $parameter 358 * 359 * @return Tree|null 360 */ 361 public function treeOptional(string $parameter = 'tree'): ?Tree 362 { 363 $value = $this->parameters[$parameter] ?? null; 364 365 if ($value === null || $value instanceof Tree) { 366 return $value; 367 } 368 369 throw new HttpBadRequestException(I18N::translate('The parameter “%s” is missing.', $parameter)); 370 } 371 372 /** 373 * @param string $parameter 374 * 375 * @return UserInterface 376 */ 377 public function user(string $parameter = 'user'): UserInterface 378 { 379 $value = $this->parameters[$parameter] ?? null; 380 381 if ($value instanceof UserInterface) { 382 return $value; 383 } 384 385 throw new HttpBadRequestException(I18N::translate('The parameter “%s” is missing.', $parameter)); 386 } 387} 388