Passed
Pull Request — main (#4226)
by David
06:04
created

Validator::optionalInteger()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 7
nc 2
nop 1
dl 0
loc 13
rs 10
c 0
b 0
f 0
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
18
declare(strict_types=1);
19
20
namespace Fisharebest\Webtrees;
21
22
use Aura\Router\Route;
23
use Closure;
24
use Fisharebest\Webtrees\Contracts\UserInterface;
25
use Fisharebest\Webtrees\Http\Exceptions\HttpBadRequestException;
26
use LogicException;
27
use Psr\Http\Message\ServerRequestInterface;
28
29
use function array_reduce;
30
use function ctype_digit;
31
use function is_array;
32
use function is_int;
33
use function is_string;
34
use function parse_url;
35
use function preg_match;
36
use function str_starts_with;
37
38
/**
39
 * Validate a parameter from an HTTP request
40
 */
41
class 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 => is_string($value) && in_array($value, $values, true) ? $value : null;
124
125
        return $this;
126
    }
127
    /**
128
     * @param string $base_url
129
     *
130
     * @return $this
131
     */
132
    public function isLocalUrl(string $base_url): self
133
    {
134
        $this->rules[] = static function (?string $value) use ($base_url): ?string {
135
            if (is_string($value)) {
136
                $value_info    = parse_url($value);
137
                $base_url_info = parse_url($base_url);
138
139
                if (!is_array($base_url_info)) {
1 ignored issue
show
introduced by
The condition is_array($base_url_info) is always true.
Loading history...
140
                    throw new LogicException(__METHOD__ . ' needs a valid URL');
141
                }
142
143
                if (is_array($value_info)) {
1 ignored issue
show
introduced by
The condition is_array($value_info) is always true.
Loading history...
144
                    $scheme_ok = ($value_info['scheme'] ?? 'http') === ($base_url_info['scheme'] ?? 'http');
145
                    $host_ok   = ($value_info['host'] ?? '') === ($base_url_info['host'] ?? '');
146
                    $port_ok   = ($value_info['port'] ?? '') === ($base_url_info['port'] ?? '');
147
                    $user_ok   = ($value_info['user'] ?? '') === ($base_url_info['user'] ?? '');
148
                    $path_ok   = str_starts_with($value_info['path'] ?? '/', $base_url_info['path'] ?? '/');
149
150
                    if ($scheme_ok && $host_ok && $port_ok && $user_ok && $path_ok) {
151
                        return $value;
152
                    }
153
                }
154
            }
155
156
            return null;
157
        };
158
159
        return $this;
160
    }
161
162
    /**
163
     * @return $this
164
     */
165
    public function isTag(): self
166
    {
167
        $this->rules[] = static function (?string $value): ?string {
168
            if (is_string($value) && preg_match('/^' . Gedcom::REGEX_TAG . '$/', $value) === 1) {
169
                return $value;
170
            }
171
172
            return null;
173
        };
174
175
        return $this;
176
    }
177
178
    /**
179
     * @return $this
180
     */
181
    public function isXref(): self
182
    {
183
        $this->rules[] = static function (?string $value): ?string {
184
            if (is_string($value) && preg_match('/^' . Gedcom::REGEX_XREF . '$/', $value) === 1) {
185
                return $value;
186
            }
187
188
            return null;
189
        };
190
191
        return $this;
192
    }
193
194
    /**
195
     * @param string    $parameter
196
     * @param bool|null $default
197
     *
198
     * @return bool
199
     */
200
    public function boolean(string $parameter, bool $default = null): bool
201
    {
202
        $value = $this->parameters[$parameter] ?? null;
203
204
        if (in_array($value, ['1', true], true)) {
205
            return true;
206
        }
207
208
        if (in_array($value, ['0', '', false], true)) {
209
            return false;
210
        }
211
212
        if ($default === null) {
213
            throw new HttpBadRequestException(I18N::translate('The parameter “%s” is missing.', $parameter));
214
        }
215
216
        return $default;
217
    }
218
219
    /**
220
     * @param string $parameter
221
     *
222
     * @return array<string>
223
     */
224
    public function array(string $parameter): array
225
    {
226
        $value = $this->parameters[$parameter] ?? null;
227
228
        if (!is_array($value) && $value !== null) {
229
            throw new HttpBadRequestException(I18N::translate('The parameter “%s” is missing.', $parameter));
230
        }
231
232
        $callback = static fn (?array $value, Closure $rule): ?array => $rule($value);
233
234
        $value = array_reduce($this->rules, $callback, $value);
235
        $value ??= [];
236
237
        $check_utf8 = static function ($v, $k) use ($parameter) {
238
            if (is_string($k) && !preg_match('//u', $k) || is_string($v) && !preg_match('//u', $v)) {
1 ignored issue
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: (is_string($k) && ! preg...! preg_match('//u', $v), Probably Intended Meaning: is_string($k) && (! preg... preg_match('//u', $v))
Loading history...
239
                throw new HttpBadRequestException(I18N::translate('The parameter “%s” is missing.', $parameter));
240
            }
241
        };
242
243
        array_walk_recursive($value, $check_utf8);
244
245
        return $value;
246
    }
247
248
    /**
249
     * @param string   $parameter
250
     * @param int|null $default
251
     *
252
     * @return int
253
     */
254
    public function integer(string $parameter, int $default = null): int
255
    {
256
        $value = $this->parameters[$parameter] ?? null;
257
258
        if (is_string($value) && ctype_digit($value)) {
259
            $value = (int) $value;
260
        } elseif (!is_int($value)) {
261
            $value = null;
262
        }
263
264
        $callback = static fn (?int $value, Closure $rule): ?int => $rule($value);
265
266
        $value = array_reduce($this->rules, $callback, $value);
267
268
        $value ??= $default;
269
270
        if ($value === null) {
271
            throw new HttpBadRequestException(I18N::translate('The parameter “%s” is missing.', $parameter));
272
        }
273
274
        return $value;
275
    }
276
277
    /**
278
     * @param string $parameter
279
     *
280
     * @return Route
281
     */
282
    public function route(string $parameter = 'route'): Route
283
    {
284
        $value = $this->parameters[$parameter] ?? null;
285
286
        if ($value instanceof Route) {
287
            return $value;
288
        }
289
290
        throw new HttpBadRequestException(I18N::translate('The parameter “%s” is missing.', $parameter));
291
    }
292
293
    /**
294
     * @param string      $parameter
295
     * @param string|null $default
296
     *
297
     * @return string
298
     */
299
    public function string(string $parameter, string $default = null): string
300
    {
301
        $value = $this->parameters[$parameter] ?? null;
302
303
        if (!is_string($value)) {
304
            $value = null;
305
        }
306
307
        $callback = static fn (?string $value, Closure $rule): ?string => $rule($value);
308
309
        $value =  array_reduce($this->rules, $callback, $value);
310
        $value ??= $default;
311
312
        if ($value === null || preg_match('//u', $value) !== 1) {
313
            throw new HttpBadRequestException(I18N::translate('The parameter “%s” is missing.', $parameter));
314
        }
315
316
        return $value;
317
    }
318
319
    /**
320
     * @param string $parameter
321
     *
322
     * @return Tree
323
     */
324
    public function tree(string $parameter = 'tree'): Tree
325
    {
326
        $value = $this->parameters[$parameter] ?? null;
327
328
        if ($value instanceof Tree) {
329
            return $value;
330
        }
331
332
        throw new HttpBadRequestException(I18N::translate('The parameter “%s” is missing.', $parameter));
333
    }
334
335
    /**
336
     * @param string $parameter
337
     *
338
     * @return Tree|null
339
     */
340
    public function treeOptional(string $parameter = 'tree'): ?Tree
341
    {
342
        $value = $this->parameters[$parameter] ?? null;
343
344
        if ($value === null || $value instanceof Tree) {
345
            return $value;
346
        }
347
348
        throw new HttpBadRequestException(I18N::translate('The parameter “%s” is missing.', $parameter));
349
    }
350
351
    /**
352
     * @param string $parameter
353
     *
354
     * @return UserInterface
355
     */
356
    public function user(string $parameter = 'user'): UserInterface
357
    {
358
        $value = $this->parameters[$parameter] ?? null;
359
360
        if ($value instanceof UserInterface) {
361
            return $value;
362
        }
363
364
        throw new HttpBadRequestException(I18N::translate('The parameter “%s” is missing.', $parameter));
365
    }
366
}
367