xref: /webtrees/app/Validator.php (revision a8b39ba2d349525974d59077419f7a8ea8802aaa)
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