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 gettype; 30use function is_array; 31use function is_int; 32use function is_string; 33use function parse_url; 34use function preg_match; 35use function str_starts_with; 36 37/** 38 * Validate a parameter from an HTTP request 39 */ 40class Validator 41{ 42 /** @var array<string|array> */ 43 private array $parameters; 44 45 /** @var array<Closure> */ 46 private array $rules = []; 47 48 /** 49 * @param array<string|array> $parameters 50 */ 51 public function __construct(array $parameters) 52 { 53 $this->parameters = $parameters; 54 } 55 56 /** 57 * @param ServerRequestInterface $request 58 * 59 * @return self 60 */ 61 public static function parsedBody(ServerRequestInterface $request): self 62 { 63 return new self((array) $request->getParsedBody()); 64 } 65 66 /** 67 * @param ServerRequestInterface $request 68 * 69 * @return self 70 */ 71 public static function queryParams(ServerRequestInterface $request): self 72 { 73 return new self($request->getQueryParams()); 74 } 75 76 /** 77 * @param int $minimum 78 * @param int $maximum 79 * 80 * @return self 81 */ 82 public function isBetween(int $minimum, int $maximum): self 83 { 84 $this->rules[] = static function ($value) use ($minimum, $maximum): ?int { 85 if (is_int($value)) { 86 if ($value >= $minimum && $value <= $maximum) { 87 return $value; 88 } 89 90 return null; 91 } 92 93 if ($value === null) { 94 return null; 95 } 96 97 throw new LogicException(__METHOD__ . ' does not accept ' . gettype($value)); 98 }; 99 100 return $this; 101 } 102 103 /** 104 * @param string $base_url 105 * 106 * @return $this 107 */ 108 public function isLocalUrl(string $base_url): self 109 { 110 $this->rules[] = static function ($value) use ($base_url): ?string { 111 if ($value === null) { 112 return null; 113 } 114 115 if (is_string($value)) { 116 $value_info = parse_url($value); 117 $base_url_info = parse_url($base_url); 118 119 if (!is_array($base_url_info)) { 120 throw new LogicException(__METHOD__ . ' needs a valid URL'); 121 } 122 123 if (is_array($value_info)) { 124 $scheme_ok = ($value_info['scheme'] ?? 'http') === ($base_url_info['scheme'] ?? 'http'); 125 $host_ok = ($value_info['host'] ?? '') === ($base_url_info['host'] ?? ''); 126 $port_ok = ($value_info['port'] ?? '') === ($base_url_info['port'] ?? ''); 127 $user_ok = ($value_info['user'] ?? '') === ($base_url_info['user'] ?? ''); 128 $path_ok = str_starts_with($value_info['path'] ?? '/', $base_url_info['path'] ?? '/'); 129 130 if ($scheme_ok && $host_ok && $port_ok && $user_ok && $path_ok) { 131 return $value; 132 } 133 } 134 135 return null; 136 } 137 138 if ($value === null) { 139 return null; 140 } 141 142 throw new LogicException(__METHOD__ . ' does not accept ' . gettype($value)); 143 }; 144 145 return $this; 146 } 147 148 /** 149 * @return $this 150 */ 151 public function isXref(): self 152 { 153 $this->rules[] = static function ($value) { 154 if (is_string($value)) { 155 if (preg_match('/^' . Gedcom::REGEX_XREF . '$/', $value)) { 156 return $value; 157 } 158 159 return null; 160 } 161 162 if (is_array($value)) { 163 foreach ($value as $item) { 164 if (!preg_match('/^' . Gedcom::REGEX_XREF . '$/', $item)) { 165 return null; 166 } 167 } 168 169 return $value; 170 } 171 172 if ($value === null) { 173 return null; 174 } 175 176 throw new LogicException(__METHOD__ . ' does not accept ' . gettype($value)); 177 }; 178 179 return $this; 180 } 181 182 /** 183 * @param string $parameter 184 * 185 * @return array<string> 186 */ 187 public function array(string $parameter): array 188 { 189 $value = $this->parameters[$parameter] ?? null; 190 191 if (!is_array($value)) { 192 $value = null; 193 } 194 195 return array_reduce($this->rules, static fn ($value, $rule) => $rule($value), $value); 196 } 197 198 /** 199 * @param string $parameter 200 * 201 * @return int|null 202 */ 203 public function integer(string $parameter): ?int 204 { 205 $value = $this->parameters[$parameter] ?? null; 206 207 if (is_string($value) && ctype_digit($value)) { 208 $value = (int) $value; 209 } else { 210 $value = null; 211 } 212 213 return array_reduce($this->rules, static fn ($value, $rule) => $rule($value), $value); 214 } 215 216 /** 217 * @param string $parameter 218 * 219 * @return string|null 220 */ 221 public function string(string $parameter): ?string 222 { 223 $value = $this->parameters[$parameter] ?? null; 224 225 if (!is_string($value)) { 226 $value = null; 227 } 228 229 return array_reduce($this->rules, static fn ($value, $rule) => $rule($value), $value); 230 } 231 232 /** 233 * @param string $parameter 234 * 235 * @return array<string> 236 */ 237 public function requiredArray(string $parameter): array 238 { 239 $value = $this->array($parameter); 240 241 if (is_array($value)) { 242 return $value; 243 } 244 245 throw new HttpBadRequestException(I18N::translate('The parameter “%s” is missing.', $parameter)); 246 } 247 248 /** 249 * @param string $parameter 250 * 251 * @return int 252 */ 253 public function requiredInteger(string $parameter): int 254 { 255 $value = $this->integer($parameter); 256 257 if (is_int($value)) { 258 return $value; 259 } 260 261 throw new HttpBadRequestException(I18N::translate('The parameter “%s” is missing.', $parameter)); 262 } 263 264 /** 265 * @param string $parameter 266 * 267 * @return string 268 */ 269 public function requiredString(string $parameter): string 270 { 271 $value = $this->string($parameter); 272 273 if (is_string($value)) { 274 return $value; 275 } 276 277 throw new HttpBadRequestException(I18N::translate('The parameter “%s” is missing.', $parameter)); 278 } 279} 280