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