xref: /webtrees/app/Validator.php (revision 158900c2b212361851fcf0964e99084458ce1b3a)
18d9c2b68SGreg Roach<?php
28d9c2b68SGreg Roach
38d9c2b68SGreg Roach/**
48d9c2b68SGreg Roach * webtrees: online genealogy
58d9c2b68SGreg Roach * Copyright (C) 2021 webtrees development team
68d9c2b68SGreg Roach * This program is free software: you can redistribute it and/or modify
78d9c2b68SGreg Roach * it under the terms of the GNU General Public License as published by
88d9c2b68SGreg Roach * the Free Software Foundation, either version 3 of the License, or
98d9c2b68SGreg Roach * (at your option) any later version.
108d9c2b68SGreg Roach * This program is distributed in the hope that it will be useful,
118d9c2b68SGreg Roach * but WITHOUT ANY WARRANTY; without even the implied warranty of
128d9c2b68SGreg Roach * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
138d9c2b68SGreg Roach * GNU General Public License for more details.
148d9c2b68SGreg Roach * You should have received a copy of the GNU General Public License
158d9c2b68SGreg Roach * along with this program. If not, see <https://www.gnu.org/licenses/>.
168d9c2b68SGreg Roach */
178d9c2b68SGreg Roach
188d9c2b68SGreg Roachdeclare(strict_types=1);
198d9c2b68SGreg Roach
208d9c2b68SGreg Roachnamespace Fisharebest\Webtrees;
218d9c2b68SGreg Roach
22b55cbc6bSGreg Roachuse Aura\Router\Route;
238d9c2b68SGreg Roachuse Closure;
24b55cbc6bSGreg Roachuse Fisharebest\Webtrees\Contracts\UserInterface;
2581b729d3SGreg Roachuse Fisharebest\Webtrees\Http\Exceptions\HttpBadRequestException;
268d9c2b68SGreg Roachuse LogicException;
278d9c2b68SGreg Roachuse Psr\Http\Message\ServerRequestInterface;
288d9c2b68SGreg Roach
298d9c2b68SGreg Roachuse function array_reduce;
308d9c2b68SGreg Roachuse function ctype_digit;
318d9c2b68SGreg Roachuse function is_array;
328d9c2b68SGreg Roachuse function is_int;
338d9c2b68SGreg Roachuse function is_string;
348d9c2b68SGreg Roachuse function parse_url;
3581b729d3SGreg Roachuse function preg_match;
368d9c2b68SGreg Roachuse function str_starts_with;
378d9c2b68SGreg Roach
388d9c2b68SGreg Roach/**
398d9c2b68SGreg Roach * Validate a parameter from an HTTP request
408d9c2b68SGreg Roach */
418d9c2b68SGreg Roachclass Validator
428d9c2b68SGreg Roach{
4304b3bc82SGreg Roach    /** @var array<int|string|Tree|UserInterface|array<int|string>> */
448d9c2b68SGreg Roach    private array $parameters;
458d9c2b68SGreg Roach
468d9c2b68SGreg Roach    /** @var array<Closure> */
478d9c2b68SGreg Roach    private array $rules = [];
488d9c2b68SGreg Roach
498d9c2b68SGreg Roach    /**
5004b3bc82SGreg Roach     * @param array<int|string|Tree|UserInterface|array<int|string>> $parameters
518d9c2b68SGreg Roach     */
528d9c2b68SGreg Roach    public function __construct(array $parameters)
538d9c2b68SGreg Roach    {
548d9c2b68SGreg Roach        $this->parameters = $parameters;
558d9c2b68SGreg Roach    }
568d9c2b68SGreg Roach
578d9c2b68SGreg Roach    /**
588d9c2b68SGreg Roach     * @param ServerRequestInterface $request
598d9c2b68SGreg Roach     *
608d9c2b68SGreg Roach     * @return self
618d9c2b68SGreg Roach     */
62b55cbc6bSGreg Roach    public static function attributes(ServerRequestInterface $request): self
63b55cbc6bSGreg Roach    {
64b55cbc6bSGreg Roach        return new self($request->getAttributes());
65b55cbc6bSGreg Roach    }
66b55cbc6bSGreg Roach
67b55cbc6bSGreg Roach    /**
68b55cbc6bSGreg Roach     * @param ServerRequestInterface $request
69b55cbc6bSGreg Roach     *
70b55cbc6bSGreg Roach     * @return self
71b55cbc6bSGreg Roach     */
728d9c2b68SGreg Roach    public static function parsedBody(ServerRequestInterface $request): self
738d9c2b68SGreg Roach    {
748d9c2b68SGreg Roach        return new self((array) $request->getParsedBody());
758d9c2b68SGreg Roach    }
768d9c2b68SGreg Roach
778d9c2b68SGreg Roach    /**
788d9c2b68SGreg Roach     * @param ServerRequestInterface $request
798d9c2b68SGreg Roach     *
808d9c2b68SGreg Roach     * @return self
818d9c2b68SGreg Roach     */
828d9c2b68SGreg Roach    public static function queryParams(ServerRequestInterface $request): self
838d9c2b68SGreg Roach    {
848d9c2b68SGreg Roach        return new self($request->getQueryParams());
858d9c2b68SGreg Roach    }
868d9c2b68SGreg Roach
878d9c2b68SGreg Roach    /**
88b55cbc6bSGreg Roach     * @param ServerRequestInterface $request
89b55cbc6bSGreg Roach     *
90b55cbc6bSGreg Roach     * @return self
91b55cbc6bSGreg Roach     */
92b55cbc6bSGreg Roach    public static function serverParams(ServerRequestInterface $request): self
93b55cbc6bSGreg Roach    {
94b55cbc6bSGreg Roach        return new self($request->getServerParams());
95b55cbc6bSGreg Roach    }
96b55cbc6bSGreg Roach
97b55cbc6bSGreg Roach    /**
988d9c2b68SGreg Roach     * @param int $minimum
998d9c2b68SGreg Roach     * @param int $maximum
1008d9c2b68SGreg Roach     *
1018d9c2b68SGreg Roach     * @return self
1028d9c2b68SGreg Roach     */
1038d9c2b68SGreg Roach    public function isBetween(int $minimum, int $maximum): self
1048d9c2b68SGreg Roach    {
1052b1a9a98SGreg Roach        $this->rules[] = static function (?int $value) use ($minimum, $maximum): ?int {
1062b1a9a98SGreg Roach            if (is_int($value) && $value >= $minimum && $value <= $maximum) {
1078d9c2b68SGreg Roach                return $value;
1088d9c2b68SGreg Roach            }
1098d9c2b68SGreg Roach
11081b729d3SGreg Roach            return null;
1118d9c2b68SGreg Roach        };
1128d9c2b68SGreg Roach
1138d9c2b68SGreg Roach        return $this;
1148d9c2b68SGreg Roach    }
1158d9c2b68SGreg Roach
11681b729d3SGreg Roach    /**
1171c6adce8SGreg Roach     * @param array<string> $values
1181c6adce8SGreg Roach     *
1191c6adce8SGreg Roach     * @return $this
1201c6adce8SGreg Roach     */
1211c6adce8SGreg Roach    public function isInArray(array $values): self
1221c6adce8SGreg Roach    {
123c3bff7b4SGreg Roach        $this->rules[] = static fn (?string $value): ?string => $value !== null && in_array($value, $values, true) ? $value : null;
1241c6adce8SGreg Roach
1251c6adce8SGreg Roach        return $this;
1261c6adce8SGreg Roach    }
127c3bff7b4SGreg Roach
128c3bff7b4SGreg Roach    /**
129*158900c2SGreg Roach     * @param array<string> $values
130*158900c2SGreg Roach     *
131*158900c2SGreg Roach     * @return $this
132*158900c2SGreg Roach     */
133*158900c2SGreg Roach    public function isInArrayKeys(array $values): self
134*158900c2SGreg Roach    {
135*158900c2SGreg Roach        return $this->isInArray(array_keys($values));
136*158900c2SGreg Roach    }
137*158900c2SGreg Roach
138*158900c2SGreg Roach    /**
139c3bff7b4SGreg Roach     * @return $this
140c3bff7b4SGreg Roach     */
141c3bff7b4SGreg Roach    public function isNotEmpty(): self
142c3bff7b4SGreg Roach    {
143c3bff7b4SGreg Roach        $this->rules[] = static fn (?string $value): ?string => $value !== null && $value !== '' ? $value : null;
144c3bff7b4SGreg Roach
145c3bff7b4SGreg Roach        return $this;
146c3bff7b4SGreg Roach    }
147c3bff7b4SGreg Roach
1481c6adce8SGreg Roach    /**
14981b729d3SGreg Roach     * @param string $base_url
15081b729d3SGreg Roach     *
15181b729d3SGreg Roach     * @return $this
15281b729d3SGreg Roach     */
15381b729d3SGreg Roach    public function isLocalUrl(string $base_url): self
1548d9c2b68SGreg Roach    {
1552b1a9a98SGreg Roach        $this->rules[] = static function (?string $value) use ($base_url): ?string {
156c3bff7b4SGreg Roach            if ($value !== null) {
1578d9c2b68SGreg Roach                $value_info    = parse_url($value);
1588d9c2b68SGreg Roach                $base_url_info = parse_url($base_url);
1598d9c2b68SGreg Roach
1608d9c2b68SGreg Roach                if (!is_array($base_url_info)) {
1618d9c2b68SGreg Roach                    throw new LogicException(__METHOD__ . ' needs a valid URL');
1628d9c2b68SGreg Roach                }
1638d9c2b68SGreg Roach
1648d9c2b68SGreg Roach                if (is_array($value_info)) {
1658d9c2b68SGreg Roach                    $scheme_ok = ($value_info['scheme'] ?? 'http') === ($base_url_info['scheme'] ?? 'http');
1668d9c2b68SGreg Roach                    $host_ok   = ($value_info['host'] ?? '') === ($base_url_info['host'] ?? '');
1678d9c2b68SGreg Roach                    $port_ok   = ($value_info['port'] ?? '') === ($base_url_info['port'] ?? '');
1688d9c2b68SGreg Roach                    $user_ok   = ($value_info['user'] ?? '') === ($base_url_info['user'] ?? '');
1698d9c2b68SGreg Roach                    $path_ok   = str_starts_with($value_info['path'] ?? '/', $base_url_info['path'] ?? '/');
1708d9c2b68SGreg Roach
1718d9c2b68SGreg Roach                    if ($scheme_ok && $host_ok && $port_ok && $user_ok && $path_ok) {
1728d9c2b68SGreg Roach                        return $value;
1738d9c2b68SGreg Roach                    }
1748d9c2b68SGreg Roach                }
1758d9c2b68SGreg Roach            }
1768d9c2b68SGreg Roach
1772b1a9a98SGreg Roach            return null;
17881b729d3SGreg Roach        };
17981b729d3SGreg Roach
18081b729d3SGreg Roach        return $this;
18181b729d3SGreg Roach    }
18281b729d3SGreg Roach
18381b729d3SGreg Roach    /**
18481b729d3SGreg Roach     * @return $this
18581b729d3SGreg Roach     */
186b55cbc6bSGreg Roach    public function isTag(): self
187b55cbc6bSGreg Roach    {
188b55cbc6bSGreg Roach        $this->rules[] = static function (?string $value): ?string {
189c3bff7b4SGreg Roach            if ($value !== null && preg_match('/^' . Gedcom::REGEX_TAG . '$/', $value) === 1) {
190b55cbc6bSGreg Roach                return $value;
191b55cbc6bSGreg Roach            }
192b55cbc6bSGreg Roach
193b55cbc6bSGreg Roach            return null;
194b55cbc6bSGreg Roach        };
195b55cbc6bSGreg Roach
196b55cbc6bSGreg Roach        return $this;
197b55cbc6bSGreg Roach    }
198b55cbc6bSGreg Roach
199b55cbc6bSGreg Roach    /**
200b55cbc6bSGreg Roach     * @return $this
201b55cbc6bSGreg Roach     */
20281b729d3SGreg Roach    public function isXref(): self
20381b729d3SGreg Roach    {
2042b1a9a98SGreg Roach        $this->rules[] = static function (?string $value): ?string {
205c3bff7b4SGreg Roach            if ($value !== null && preg_match('/^' . Gedcom::REGEX_XREF . '$/', $value) === 1) {
20681b729d3SGreg Roach                return $value;
20781b729d3SGreg Roach            }
20881b729d3SGreg Roach
20981b729d3SGreg Roach            return null;
2108d9c2b68SGreg Roach        };
2118d9c2b68SGreg Roach
2128d9c2b68SGreg Roach        return $this;
2138d9c2b68SGreg Roach    }
2148d9c2b68SGreg Roach
2158d9c2b68SGreg Roach    /**
2168d9c2b68SGreg Roach     * @param string    $parameter
217b55cbc6bSGreg Roach     * @param bool|null $default
218b55cbc6bSGreg Roach     *
219b55cbc6bSGreg Roach     * @return bool
220b55cbc6bSGreg Roach     */
221b55cbc6bSGreg Roach    public function boolean(string $parameter, bool $default = null): bool
222b55cbc6bSGreg Roach    {
223b55cbc6bSGreg Roach        $value = $this->parameters[$parameter] ?? null;
224b55cbc6bSGreg Roach
225b55cbc6bSGreg Roach        if (in_array($value, ['1', true], true)) {
226b55cbc6bSGreg Roach            return true;
227b55cbc6bSGreg Roach        }
228b55cbc6bSGreg Roach
229b55cbc6bSGreg Roach        if (in_array($value, ['0', '', false], true)) {
230b55cbc6bSGreg Roach            return false;
231b55cbc6bSGreg Roach        }
232b55cbc6bSGreg Roach
233b55cbc6bSGreg Roach        if ($default === null) {
234b55cbc6bSGreg Roach            throw new HttpBadRequestException(I18N::translate('The parameter “%s” is missing.', $parameter));
235b55cbc6bSGreg Roach        }
236b55cbc6bSGreg Roach
237b55cbc6bSGreg Roach        return $default;
238b55cbc6bSGreg Roach    }
239b55cbc6bSGreg Roach
240b55cbc6bSGreg Roach    /**
241b55cbc6bSGreg Roach     * @param string $parameter
24281b729d3SGreg Roach     *
24381b729d3SGreg Roach     * @return array<string>
24481b729d3SGreg Roach     */
245b55cbc6bSGreg Roach    public function array(string $parameter): array
24681b729d3SGreg Roach    {
247b55cbc6bSGreg Roach        $value = $this->parameters[$parameter] ?? null;
24881b729d3SGreg Roach
24904b3bc82SGreg Roach        if (!is_array($value) && $value !== null) {
25004b3bc82SGreg Roach            throw new HttpBadRequestException(I18N::translate('The parameter “%s” is missing.', $parameter));
251b55cbc6bSGreg Roach        }
252b55cbc6bSGreg Roach
253b55cbc6bSGreg Roach        $callback = static fn (?array $value, Closure $rule): ?array => $rule($value);
254b55cbc6bSGreg Roach
255b55cbc6bSGreg Roach        $value = array_reduce($this->rules, $callback, $value);
256b55cbc6bSGreg Roach        $value ??= [];
257b55cbc6bSGreg Roach
258b55cbc6bSGreg Roach        $check_utf8 = static function ($v, $k) use ($parameter) {
259b55cbc6bSGreg Roach            if (is_string($k) && !preg_match('//u', $k) || is_string($v) && !preg_match('//u', $v)) {
2602b1a9a98SGreg Roach                throw new HttpBadRequestException(I18N::translate('The parameter “%s” is missing.', $parameter));
26181b729d3SGreg Roach            }
262b55cbc6bSGreg Roach        };
263b55cbc6bSGreg Roach
264b55cbc6bSGreg Roach        array_walk_recursive($value, $check_utf8);
26581b729d3SGreg Roach
2662b1a9a98SGreg Roach        return $value;
26781b729d3SGreg Roach    }
26881b729d3SGreg Roach
26981b729d3SGreg Roach    /**
27081b729d3SGreg Roach     * @param string   $parameter
271b55cbc6bSGreg Roach     * @param int|null $default
27281b729d3SGreg Roach     *
27381b729d3SGreg Roach     * @return int
27481b729d3SGreg Roach     */
275b55cbc6bSGreg Roach    public function integer(string $parameter, int $default = null): int
27681b729d3SGreg Roach    {
277b55cbc6bSGreg Roach        $value = $this->parameters[$parameter] ?? null;
278b55cbc6bSGreg Roach
279b55cbc6bSGreg Roach        if (is_string($value) && ctype_digit($value)) {
280b55cbc6bSGreg Roach            $value = (int) $value;
28104b3bc82SGreg Roach        } elseif (!is_int($value)) {
282b55cbc6bSGreg Roach            $value = null;
283b55cbc6bSGreg Roach        }
284b55cbc6bSGreg Roach
285b55cbc6bSGreg Roach        $callback = static fn (?int $value, Closure $rule): ?int => $rule($value);
286b55cbc6bSGreg Roach
287b55cbc6bSGreg Roach        $value = array_reduce($this->rules, $callback, $value);
288b55cbc6bSGreg Roach
289b55cbc6bSGreg Roach        $value ??= $default;
29081b729d3SGreg Roach
2912b1a9a98SGreg Roach        if ($value === null) {
2922b1a9a98SGreg Roach            throw new HttpBadRequestException(I18N::translate('The parameter “%s” is missing.', $parameter));
29381b729d3SGreg Roach        }
29481b729d3SGreg Roach
2952b1a9a98SGreg Roach        return $value;
29681b729d3SGreg Roach    }
29781b729d3SGreg Roach
29881b729d3SGreg Roach    /**
29981b729d3SGreg Roach     * @param string $parameter
30081b729d3SGreg Roach     *
301b55cbc6bSGreg Roach     * @return Route
302b55cbc6bSGreg Roach     */
303b55cbc6bSGreg Roach    public function route(string $parameter = 'route'): Route
304b55cbc6bSGreg Roach    {
305b55cbc6bSGreg Roach        $value = $this->parameters[$parameter] ?? null;
306b55cbc6bSGreg Roach
307b55cbc6bSGreg Roach        if ($value instanceof Route) {
308b55cbc6bSGreg Roach            return $value;
309b55cbc6bSGreg Roach        }
310b55cbc6bSGreg Roach
311b55cbc6bSGreg Roach        throw new HttpBadRequestException(I18N::translate('The parameter “%s” is missing.', $parameter));
312b55cbc6bSGreg Roach    }
313b55cbc6bSGreg Roach
314b55cbc6bSGreg Roach    /**
315b55cbc6bSGreg Roach     * @param string      $parameter
316b55cbc6bSGreg Roach     * @param string|null $default
317b55cbc6bSGreg Roach     *
31881b729d3SGreg Roach     * @return string
31981b729d3SGreg Roach     */
320b55cbc6bSGreg Roach    public function string(string $parameter, string $default = null): string
32181b729d3SGreg Roach    {
322b55cbc6bSGreg Roach        $value = $this->parameters[$parameter] ?? null;
32381b729d3SGreg Roach
324b55cbc6bSGreg Roach        if (!is_string($value)) {
325b55cbc6bSGreg Roach            $value = null;
326b55cbc6bSGreg Roach        }
327b55cbc6bSGreg Roach
328b55cbc6bSGreg Roach        $callback = static fn (?string $value, Closure $rule): ?string => $rule($value);
329b55cbc6bSGreg Roach
330b55cbc6bSGreg Roach        $value =  array_reduce($this->rules, $callback, $value);
331b55cbc6bSGreg Roach        $value ??= $default;
332b55cbc6bSGreg Roach
333b55cbc6bSGreg Roach        if ($value === null || preg_match('//u', $value) !== 1) {
3342b1a9a98SGreg Roach            throw new HttpBadRequestException(I18N::translate('The parameter “%s” is missing.', $parameter));
33581b729d3SGreg Roach        }
33681b729d3SGreg Roach
3372b1a9a98SGreg Roach        return $value;
33881b729d3SGreg Roach    }
339b55cbc6bSGreg Roach
340b55cbc6bSGreg Roach    /**
341b55cbc6bSGreg Roach     * @param string $parameter
342b55cbc6bSGreg Roach     *
343b55cbc6bSGreg Roach     * @return Tree
344b55cbc6bSGreg Roach     */
345b55cbc6bSGreg Roach    public function tree(string $parameter = 'tree'): Tree
346b55cbc6bSGreg Roach    {
347b55cbc6bSGreg Roach        $value = $this->parameters[$parameter] ?? null;
348b55cbc6bSGreg Roach
349b55cbc6bSGreg Roach        if ($value instanceof Tree) {
350b55cbc6bSGreg Roach            return $value;
351b55cbc6bSGreg Roach        }
352b55cbc6bSGreg Roach
353b55cbc6bSGreg Roach        throw new HttpBadRequestException(I18N::translate('The parameter “%s” is missing.', $parameter));
354b55cbc6bSGreg Roach    }
355b55cbc6bSGreg Roach
356b55cbc6bSGreg Roach    /**
357b55cbc6bSGreg Roach     * @param string $parameter
358b55cbc6bSGreg Roach     *
359b55cbc6bSGreg Roach     * @return Tree|null
360b55cbc6bSGreg Roach     */
361b55cbc6bSGreg Roach    public function treeOptional(string $parameter = 'tree'): ?Tree
362b55cbc6bSGreg Roach    {
363b55cbc6bSGreg Roach        $value = $this->parameters[$parameter] ?? null;
364b55cbc6bSGreg Roach
365b55cbc6bSGreg Roach        if ($value === null || $value instanceof Tree) {
366b55cbc6bSGreg Roach            return $value;
367b55cbc6bSGreg Roach        }
368b55cbc6bSGreg Roach
369b55cbc6bSGreg Roach        throw new HttpBadRequestException(I18N::translate('The parameter “%s” is missing.', $parameter));
370b55cbc6bSGreg Roach    }
371b55cbc6bSGreg Roach
372b55cbc6bSGreg Roach    /**
373b55cbc6bSGreg Roach     * @param string $parameter
374b55cbc6bSGreg Roach     *
375b55cbc6bSGreg Roach     * @return UserInterface
376b55cbc6bSGreg Roach     */
377b55cbc6bSGreg Roach    public function user(string $parameter = 'user'): UserInterface
378b55cbc6bSGreg Roach    {
379b55cbc6bSGreg Roach        $value = $this->parameters[$parameter] ?? null;
380b55cbc6bSGreg Roach
381b55cbc6bSGreg Roach        if ($value instanceof UserInterface) {
382b55cbc6bSGreg Roach            return $value;
383b55cbc6bSGreg Roach        }
384b55cbc6bSGreg Roach
385b55cbc6bSGreg Roach        throw new HttpBadRequestException(I18N::translate('The parameter “%s” is missing.', $parameter));
386b55cbc6bSGreg Roach    }
3878d9c2b68SGreg Roach}
388