xref: /webtrees/app/Validator.php (revision f6fdd7466ca895d4b685167619a017925064cccb)
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
228d9c2b68SGreg Roachuse Closure;
2381b729d3SGreg Roachuse Fisharebest\Webtrees\Http\Exceptions\HttpBadRequestException;
248d9c2b68SGreg Roachuse LogicException;
258d9c2b68SGreg Roachuse Psr\Http\Message\ServerRequestInterface;
268d9c2b68SGreg Roach
278d9c2b68SGreg Roachuse function array_reduce;
288d9c2b68SGreg Roachuse function ctype_digit;
2981b729d3SGreg Roachuse function gettype;
308d9c2b68SGreg Roachuse function is_array;
318d9c2b68SGreg Roachuse function is_int;
328d9c2b68SGreg Roachuse function is_string;
338d9c2b68SGreg Roachuse function parse_url;
3481b729d3SGreg Roachuse function preg_match;
358d9c2b68SGreg Roachuse function str_starts_with;
368d9c2b68SGreg Roach
378d9c2b68SGreg Roach/**
388d9c2b68SGreg Roach * Validate a parameter from an HTTP request
398d9c2b68SGreg Roach */
408d9c2b68SGreg Roachclass Validator
418d9c2b68SGreg Roach{
428d9c2b68SGreg Roach    /** @var array<string|array> */
438d9c2b68SGreg Roach    private array $parameters;
448d9c2b68SGreg Roach
458d9c2b68SGreg Roach    /** @var array<Closure> */
468d9c2b68SGreg Roach    private array $rules = [];
478d9c2b68SGreg Roach
488d9c2b68SGreg Roach    /**
498d9c2b68SGreg Roach     * @param array<string|array> $parameters
508d9c2b68SGreg Roach     */
518d9c2b68SGreg Roach    public function __construct(array $parameters)
528d9c2b68SGreg Roach    {
538d9c2b68SGreg Roach        $this->parameters = $parameters;
548d9c2b68SGreg Roach    }
558d9c2b68SGreg Roach
568d9c2b68SGreg Roach    /**
578d9c2b68SGreg Roach     * @param ServerRequestInterface $request
588d9c2b68SGreg Roach     *
598d9c2b68SGreg Roach     * @return self
608d9c2b68SGreg Roach     */
618d9c2b68SGreg Roach    public static function parsedBody(ServerRequestInterface $request): self
628d9c2b68SGreg Roach    {
638d9c2b68SGreg Roach        return new self((array) $request->getParsedBody());
648d9c2b68SGreg Roach    }
658d9c2b68SGreg Roach
668d9c2b68SGreg Roach    /**
678d9c2b68SGreg Roach     * @param ServerRequestInterface $request
688d9c2b68SGreg Roach     *
698d9c2b68SGreg Roach     * @return self
708d9c2b68SGreg Roach     */
718d9c2b68SGreg Roach    public static function queryParams(ServerRequestInterface $request): self
728d9c2b68SGreg Roach    {
738d9c2b68SGreg Roach        return new self($request->getQueryParams());
748d9c2b68SGreg Roach    }
758d9c2b68SGreg Roach
768d9c2b68SGreg Roach    /**
778d9c2b68SGreg Roach     * @param int $minimum
788d9c2b68SGreg Roach     * @param int $maximum
798d9c2b68SGreg Roach     *
808d9c2b68SGreg Roach     * @return self
818d9c2b68SGreg Roach     */
828d9c2b68SGreg Roach    public function isBetween(int $minimum, int $maximum): self
838d9c2b68SGreg Roach    {
848d9c2b68SGreg Roach        $this->rules[] = static function ($value) use ($minimum, $maximum): ?int {
858d9c2b68SGreg Roach            if (is_int($value)) {
8681b729d3SGreg Roach                if ($value >= $minimum && $value <= $maximum) {
878d9c2b68SGreg Roach                    return $value;
888d9c2b68SGreg Roach                }
898d9c2b68SGreg Roach
9081b729d3SGreg Roach                return null;
9181b729d3SGreg Roach            }
9281b729d3SGreg Roach
9381b729d3SGreg Roach            if ($value === null) {
9481b729d3SGreg Roach                return null;
9581b729d3SGreg Roach            }
9681b729d3SGreg Roach
9781b729d3SGreg Roach            throw new LogicException(__METHOD__ . ' does not accept ' . gettype($value));
988d9c2b68SGreg Roach        };
998d9c2b68SGreg Roach
1008d9c2b68SGreg Roach        return $this;
1018d9c2b68SGreg Roach    }
1028d9c2b68SGreg Roach
10381b729d3SGreg Roach    /**
10481b729d3SGreg Roach     * @param string $base_url
10581b729d3SGreg Roach     *
10681b729d3SGreg Roach     * @return $this
10781b729d3SGreg Roach     */
10881b729d3SGreg Roach    public function isLocalUrl(string $base_url): self
1098d9c2b68SGreg Roach    {
1108d9c2b68SGreg Roach        $this->rules[] = static function ($value) use ($base_url): ?string {
1117729532eSGreg Roach            if ($value === null) {
1127729532eSGreg Roach                return null;
1137729532eSGreg Roach            }
1147729532eSGreg Roach
1158d9c2b68SGreg Roach            if (is_string($value)) {
1168d9c2b68SGreg Roach                $value_info    = parse_url($value);
1178d9c2b68SGreg Roach                $base_url_info = parse_url($base_url);
1188d9c2b68SGreg Roach
1198d9c2b68SGreg Roach                if (!is_array($base_url_info)) {
1208d9c2b68SGreg Roach                    throw new LogicException(__METHOD__ . ' needs a valid URL');
1218d9c2b68SGreg Roach                }
1228d9c2b68SGreg Roach
1238d9c2b68SGreg Roach                if (is_array($value_info)) {
1248d9c2b68SGreg Roach                    $scheme_ok = ($value_info['scheme'] ?? 'http') === ($base_url_info['scheme'] ?? 'http');
1258d9c2b68SGreg Roach                    $host_ok   = ($value_info['host'] ?? '') === ($base_url_info['host'] ?? '');
1268d9c2b68SGreg Roach                    $port_ok   = ($value_info['port'] ?? '') === ($base_url_info['port'] ?? '');
1278d9c2b68SGreg Roach                    $user_ok   = ($value_info['user'] ?? '') === ($base_url_info['user'] ?? '');
1288d9c2b68SGreg Roach                    $path_ok   = str_starts_with($value_info['path'] ?? '/', $base_url_info['path'] ?? '/');
1298d9c2b68SGreg Roach
1308d9c2b68SGreg Roach                    if ($scheme_ok && $host_ok && $port_ok && $user_ok && $path_ok) {
1318d9c2b68SGreg Roach                        return $value;
1328d9c2b68SGreg Roach                    }
1338d9c2b68SGreg Roach                }
1348d9c2b68SGreg Roach
1358d9c2b68SGreg Roach                return null;
1368d9c2b68SGreg Roach            }
1378d9c2b68SGreg Roach
13881b729d3SGreg Roach            throw new LogicException(__METHOD__ . ' does not accept ' . gettype($value));
13981b729d3SGreg Roach        };
14081b729d3SGreg Roach
14181b729d3SGreg Roach        return $this;
14281b729d3SGreg Roach    }
14381b729d3SGreg Roach
14481b729d3SGreg Roach    /**
14581b729d3SGreg Roach     * @return $this
14681b729d3SGreg Roach     */
14781b729d3SGreg Roach    public function isXref(): self
14881b729d3SGreg Roach    {
14981b729d3SGreg Roach        $this->rules[] = static function ($value) {
15081b729d3SGreg Roach            if (is_string($value)) {
151*f6fdd746SJonathan Jaubart                if (preg_match('/^' . Gedcom::REGEX_XREF . '$/', $value) === 1) {
15281b729d3SGreg Roach                    return $value;
15381b729d3SGreg Roach                }
15481b729d3SGreg Roach
15581b729d3SGreg Roach                return null;
15681b729d3SGreg Roach            }
15781b729d3SGreg Roach
15881b729d3SGreg Roach            if (is_array($value)) {
15981b729d3SGreg Roach                foreach ($value as $item) {
160*f6fdd746SJonathan Jaubart                    if (preg_match('/^' . Gedcom::REGEX_XREF . '$/', $item) !== 1) {
16181b729d3SGreg Roach                        return null;
16281b729d3SGreg Roach                    }
16381b729d3SGreg Roach                }
16481b729d3SGreg Roach
16581b729d3SGreg Roach                return $value;
16681b729d3SGreg Roach            }
16781b729d3SGreg Roach
16881b729d3SGreg Roach            if ($value === null) {
16981b729d3SGreg Roach                return null;
17081b729d3SGreg Roach            }
17181b729d3SGreg Roach
17281b729d3SGreg Roach            throw new LogicException(__METHOD__ . ' does not accept ' . gettype($value));
1738d9c2b68SGreg Roach        };
1748d9c2b68SGreg Roach
1758d9c2b68SGreg Roach        return $this;
1768d9c2b68SGreg Roach    }
1778d9c2b68SGreg Roach
1788d9c2b68SGreg Roach    /**
1798d9c2b68SGreg Roach     * @param string $parameter
1808d9c2b68SGreg Roach     *
181*f6fdd746SJonathan Jaubart     * @return array<string>|null
1828d9c2b68SGreg Roach     */
183*f6fdd746SJonathan Jaubart    public function array(string $parameter): ?array
1848d9c2b68SGreg Roach    {
1858d9c2b68SGreg Roach        $value = $this->parameters[$parameter] ?? null;
1868d9c2b68SGreg Roach
1878d9c2b68SGreg Roach        if (!is_array($value)) {
18881b729d3SGreg Roach            $value = null;
1898d9c2b68SGreg Roach        }
1908d9c2b68SGreg Roach
191fcbce9d8SGreg Roach        return array_reduce($this->rules, static fn ($value, $rule) => $rule($value), $value);
1928d9c2b68SGreg Roach    }
1938d9c2b68SGreg Roach
1948d9c2b68SGreg Roach    /**
1958d9c2b68SGreg Roach     * @param string $parameter
1968d9c2b68SGreg Roach     *
1978d9c2b68SGreg Roach     * @return int|null
1988d9c2b68SGreg Roach     */
1998d9c2b68SGreg Roach    public function integer(string $parameter): ?int
2008d9c2b68SGreg Roach    {
2018d9c2b68SGreg Roach        $value = $this->parameters[$parameter] ?? null;
2028d9c2b68SGreg Roach
2038d9c2b68SGreg Roach        if (is_string($value) && ctype_digit($value)) {
2048d9c2b68SGreg Roach            $value = (int) $value;
2058d9c2b68SGreg Roach        } else {
2068d9c2b68SGreg Roach            $value = null;
2078d9c2b68SGreg Roach        }
2088d9c2b68SGreg Roach
209fcbce9d8SGreg Roach        return array_reduce($this->rules, static fn ($value, $rule) => $rule($value), $value);
2108d9c2b68SGreg Roach    }
2118d9c2b68SGreg Roach
2128d9c2b68SGreg Roach    /**
2138d9c2b68SGreg Roach     * @param string $parameter
2148d9c2b68SGreg Roach     *
2158d9c2b68SGreg Roach     * @return string|null
2168d9c2b68SGreg Roach     */
2178d9c2b68SGreg Roach    public function string(string $parameter): ?string
2188d9c2b68SGreg Roach    {
2198d9c2b68SGreg Roach        $value = $this->parameters[$parameter] ?? null;
2208d9c2b68SGreg Roach
2218d9c2b68SGreg Roach        if (!is_string($value)) {
2228d9c2b68SGreg Roach            $value = null;
2238d9c2b68SGreg Roach        }
2248d9c2b68SGreg Roach
2258d9c2b68SGreg Roach        return array_reduce($this->rules, static fn ($value, $rule) => $rule($value), $value);
2268d9c2b68SGreg Roach    }
22781b729d3SGreg Roach
22881b729d3SGreg Roach    /**
22981b729d3SGreg Roach     * @param string $parameter
23081b729d3SGreg Roach     *
23181b729d3SGreg Roach     * @return array<string>
23281b729d3SGreg Roach     */
23381b729d3SGreg Roach    public function requiredArray(string $parameter): array
23481b729d3SGreg Roach    {
23581b729d3SGreg Roach        $value = $this->array($parameter);
23681b729d3SGreg Roach
23781b729d3SGreg Roach        if (is_array($value)) {
23881b729d3SGreg Roach            return $value;
23981b729d3SGreg Roach        }
24081b729d3SGreg Roach
24181b729d3SGreg Roach        throw new HttpBadRequestException(I18N::translate('The parameter “%s” is missing.', $parameter));
24281b729d3SGreg Roach    }
24381b729d3SGreg Roach
24481b729d3SGreg Roach    /**
24581b729d3SGreg Roach     * @param string $parameter
24681b729d3SGreg Roach     *
24781b729d3SGreg Roach     * @return int
24881b729d3SGreg Roach     */
24981b729d3SGreg Roach    public function requiredInteger(string $parameter): int
25081b729d3SGreg Roach    {
25181b729d3SGreg Roach        $value = $this->integer($parameter);
25281b729d3SGreg Roach
25381b729d3SGreg Roach        if (is_int($value)) {
25481b729d3SGreg Roach            return $value;
25581b729d3SGreg Roach        }
25681b729d3SGreg Roach
25781b729d3SGreg Roach        throw new HttpBadRequestException(I18N::translate('The parameter “%s” is missing.', $parameter));
25881b729d3SGreg Roach    }
25981b729d3SGreg Roach
26081b729d3SGreg Roach    /**
26181b729d3SGreg Roach     * @param string $parameter
26281b729d3SGreg Roach     *
26381b729d3SGreg Roach     * @return string
26481b729d3SGreg Roach     */
26581b729d3SGreg Roach    public function requiredString(string $parameter): string
26681b729d3SGreg Roach    {
26781b729d3SGreg Roach        $value = $this->string($parameter);
26881b729d3SGreg Roach
26981b729d3SGreg Roach        if (is_string($value)) {
27081b729d3SGreg Roach            return $value;
27181b729d3SGreg Roach        }
27281b729d3SGreg Roach
27381b729d3SGreg Roach        throw new HttpBadRequestException(I18N::translate('The parameter “%s” is missing.', $parameter));
27481b729d3SGreg Roach    }
2758d9c2b68SGreg Roach}
276