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<int|string|Tree|UserInterface|array<int|string>> */ 44 private array $parameters; 45 46 /** @var array<Closure> */ 47 private array $rules = []; 48 49 /** 50 * @param array<int|string|Tree|UserInterface|array<int|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 => $value !== null && in_array($value, $values, true) ? $value : null; 124 125 return $this; 126 } 127 128 /** 129 * @return $this 130 */ 131 public function isNotEmpty(): self 132 { 133 $this->rules[] = static fn (?string $value): ?string => $value !== null && $value !== '' ? $value : null; 134 135 return $this; 136 } 137 138 /** 139 * @param string $base_url 140 * 141 * @return $this 142 */ 143 public function isLocalUrl(string $base_url): self 144 { 145 $this->rules[] = static function (?string $value) use ($base_url): ?string { 146 if ($value !== null) { 147 $value_info = parse_url($value); 148 $base_url_info = parse_url($base_url); 149 150 if (!is_array($base_url_info)) { 151 throw new LogicException(__METHOD__ . ' needs a valid URL'); 152 } 153 154 if (is_array($value_info)) { 155 $scheme_ok = ($value_info['scheme'] ?? 'http') === ($base_url_info['scheme'] ?? 'http'); 156 $host_ok = ($value_info['host'] ?? '') === ($base_url_info['host'] ?? ''); 157 $port_ok = ($value_info['port'] ?? '') === ($base_url_info['port'] ?? ''); 158 $user_ok = ($value_info['user'] ?? '') === ($base_url_info['user'] ?? ''); 159 $path_ok = str_starts_with($value_info['path'] ?? '/', $base_url_info['path'] ?? '/'); 160 161 if ($scheme_ok && $host_ok && $port_ok && $user_ok && $path_ok) { 162 return $value; 163 } 164 } 165 } 166 167 return null; 168 }; 169 170 return $this; 171 } 172 173 /** 174 * @return $this 175 */ 176 public function isTag(): self 177 { 178 $this->rules[] = static function (?string $value): ?string { 179 if ($value !== null && preg_match('/^' . Gedcom::REGEX_TAG . '$/', $value) === 1) { 180 return $value; 181 } 182 183 return null; 184 }; 185 186 return $this; 187 } 188 189 /** 190 * @return $this 191 */ 192 public function isXref(): self 193 { 194 $this->rules[] = static function (?string $value): ?string { 195 if ($value !== null && preg_match('/^' . Gedcom::REGEX_XREF . '$/', $value) === 1) { 196 return $value; 197 } 198 199 return null; 200 }; 201 202 return $this; 203 } 204 205 /** 206 * @param string $parameter 207 * @param bool|null $default 208 * 209 * @return bool 210 */ 211 public function boolean(string $parameter, bool $default = null): bool 212 { 213 $value = $this->parameters[$parameter] ?? null; 214 215 if (in_array($value, ['1', true], true)) { 216 return true; 217 } 218 219 if (in_array($value, ['0', '', false], true)) { 220 return false; 221 } 222 223 if ($default === null) { 224 throw new HttpBadRequestException(I18N::translate('The parameter “%s” is missing.', $parameter)); 225 } 226 227 return $default; 228 } 229 230 /** 231 * @param string $parameter 232 * 233 * @return array<string> 234 */ 235 public function array(string $parameter): array 236 { 237 $value = $this->parameters[$parameter] ?? null; 238 239 if (!is_array($value) && $value !== null) { 240 throw new HttpBadRequestException(I18N::translate('The parameter “%s” is missing.', $parameter)); 241 } 242 243 $callback = static fn (?array $value, Closure $rule): ?array => $rule($value); 244 245 $value = array_reduce($this->rules, $callback, $value); 246 $value ??= []; 247 248 $check_utf8 = static function ($v, $k) use ($parameter) { 249 if (is_string($k) && !preg_match('//u', $k) || is_string($v) && !preg_match('//u', $v)) { 250 throw new HttpBadRequestException(I18N::translate('The parameter “%s” is missing.', $parameter)); 251 } 252 }; 253 254 array_walk_recursive($value, $check_utf8); 255 256 return $value; 257 } 258 259 /** 260 * @param string $parameter 261 * @param int|null $default 262 * 263 * @return int 264 */ 265 public function integer(string $parameter, int $default = null): int 266 { 267 $value = $this->parameters[$parameter] ?? null; 268 269 if (is_string($value) && ctype_digit($value)) { 270 $value = (int) $value; 271 } elseif (!is_int($value)) { 272 $value = null; 273 } 274 275 $callback = static fn (?int $value, Closure $rule): ?int => $rule($value); 276 277 $value = array_reduce($this->rules, $callback, $value); 278 279 $value ??= $default; 280 281 if ($value === null) { 282 throw new HttpBadRequestException(I18N::translate('The parameter “%s” is missing.', $parameter)); 283 } 284 285 return $value; 286 } 287 288 /** 289 * @param string $parameter 290 * 291 * @return Route 292 */ 293 public function route(string $parameter = 'route'): Route 294 { 295 $value = $this->parameters[$parameter] ?? null; 296 297 if ($value instanceof Route) { 298 return $value; 299 } 300 301 throw new HttpBadRequestException(I18N::translate('The parameter “%s” is missing.', $parameter)); 302 } 303 304 /** 305 * @param string $parameter 306 * @param string|null $default 307 * 308 * @return string 309 */ 310 public function string(string $parameter, string $default = null): string 311 { 312 $value = $this->parameters[$parameter] ?? null; 313 314 if (!is_string($value)) { 315 $value = null; 316 } 317 318 $callback = static fn (?string $value, Closure $rule): ?string => $rule($value); 319 320 $value = array_reduce($this->rules, $callback, $value); 321 $value ??= $default; 322 323 if ($value === null || preg_match('//u', $value) !== 1) { 324 throw new HttpBadRequestException(I18N::translate('The parameter “%s” is missing.', $parameter)); 325 } 326 327 return $value; 328 } 329 330 /** 331 * @param string $parameter 332 * 333 * @return Tree 334 */ 335 public function tree(string $parameter = 'tree'): Tree 336 { 337 $value = $this->parameters[$parameter] ?? null; 338 339 if ($value instanceof Tree) { 340 return $value; 341 } 342 343 throw new HttpBadRequestException(I18N::translate('The parameter “%s” is missing.', $parameter)); 344 } 345 346 /** 347 * @param string $parameter 348 * 349 * @return Tree|null 350 */ 351 public function treeOptional(string $parameter = 'tree'): ?Tree 352 { 353 $value = $this->parameters[$parameter] ?? null; 354 355 if ($value === null || $value instanceof Tree) { 356 return $value; 357 } 358 359 throw new HttpBadRequestException(I18N::translate('The parameter “%s” is missing.', $parameter)); 360 } 361 362 /** 363 * @param string $parameter 364 * 365 * @return UserInterface 366 */ 367 public function user(string $parameter = 'user'): UserInterface 368 { 369 $value = $this->parameters[$parameter] ?? null; 370 371 if ($value instanceof UserInterface) { 372 return $value; 373 } 374 375 throw new HttpBadRequestException(I18N::translate('The parameter “%s” is missing.', $parameter)); 376 } 377} 378