Passed
Push — validation ( 1c78f5...5db15e )
by Greg
08:11
created

Validator::serverParams()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
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<string|Tree|UserInterface|array<string>> */
44
    private array $parameters;
45
46
    /** @var array<Closure> */
47
    private array $rules = [];
48
49
    /**
50
     * @param array<string|array<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
     *
197
     * @return array<string>|null
198
     */
199
    public function optionalArray(string $parameter): ?array
200
    {
201
        $value = $this->parameters[$parameter] ?? null;
202
203
        if (!is_array($value)) {
204
            $value = null;
205
        }
206
207
        $callback = static fn (?array $value, Closure $rule): ?array => $rule($value);
208
209
        return array_reduce($this->rules, $callback, $value);
210
    }
211
212
    /**
213
     * @param string $parameter
214
     *
215
     * @return int|null
216
     */
217
    public function optionalInteger(string $parameter): ?int
218
    {
219
        $value = $this->parameters[$parameter] ?? null;
220
221
        if (is_string($value) && ctype_digit($value)) {
222
            $value = (int) $value;
223
        } else {
224
            $value = null;
225
        }
226
227
        $callback = static fn (?int $value, Closure $rule): ?int => $rule($value);
228
229
        return array_reduce($this->rules, $callback, $value);
230
    }
231
232
    /**
233
     * @param string $parameter
234
     *
235
     * @return string|null
236
     */
237
    public function optionalString(string $parameter): ?string
238
    {
239
        $value = $this->parameters[$parameter] ?? null;
240
241
        if (!is_string($value)) {
242
            $value = null;
243
        }
244
245
        $callback = static fn (?string $value, Closure $rule): ?string => $rule($value);
246
247
        return array_reduce($this->rules, $callback, $value);
248
    }
249
250
    /**
251
     * @param string    $parameter
252
     * @param bool|null $default
253
     *
254
     * @return bool
255
     */
256
    public function boolean(string $parameter, bool $default = null): bool
257
    {
258
        $value = $this->parameters[$parameter] ?? null;
259
260
        if (in_array($value, ['1', true], true)) {
261
            return true;
262
        }
263
264
        if (in_array($value, ['0', '', false], true)) {
265
            return false;
266
        }
267
268
        if ($default === null) {
269
            throw new HttpBadRequestException(I18N::translate('The parameter “%s” is missing.', $parameter));
270
        }
271
272
        return $default;
273
    }
274
275
    /**
276
     * @param string                 $parameter
277
     * @param array<int|string>|null $default
278
     *
279
     * @return array<string>
280
     */
281
    public function array(string $parameter, array $default = null): array
282
    {
283
        $value = $this->parameters[$parameter] ?? null;
284
285
        if (!is_array($value)) {
286
            $value = null;
287
        }
288
289
        $callback = static fn (?array $value, Closure $rule): ?array => $rule($value);
290
291
        $value = array_reduce($this->rules, $callback, $value);
292
293
        $value ??= $default;
294
295
        if ($value === null) {
296
            throw new HttpBadRequestException(I18N::translate('The parameter “%s” is missing.', $parameter));
297
        }
298
299
        return $value;
300
    }
301
302
    /**
303
     * @param string   $parameter
304
     * @param int|null $default
305
     *
306
     * @return int
307
     */
308
    public function integer(string $parameter, int $default = null): int
309
    {
310
        $value = $this->parameters[$parameter] ?? null;
311
312
        if (is_string($value) && ctype_digit($value)) {
313
            $value = (int) $value;
314
        } else {
315
            $value = null;
316
        }
317
318
        $callback = static fn (?int $value, Closure $rule): ?int => $rule($value);
319
320
        $value = array_reduce($this->rules, $callback, $value);
321
322
        $value ??= $default;
323
324
        if ($value === null) {
325
            throw new HttpBadRequestException(I18N::translate('The parameter “%s” is missing.', $parameter));
326
        }
327
328
        return $value;
329
    }
330
331
    /**
332
     * @param string $parameter
333
     *
334
     * @return Route
335
     */
336
    public function route(string $parameter = 'route'): Route
337
    {
338
        $value = $this->parameters[$parameter] ?? null;
339
340
        if ($value instanceof Route) {
341
            return $value;
342
        }
343
344
        throw new HttpBadRequestException(I18N::translate('The parameter “%s” is missing.', $parameter));
345
    }
346
347
    /**
348
     * @param string      $parameter
349
     * @param string|null $default
350
     *
351
     * @return string
352
     */
353
    public function string(string $parameter, string $default = null): string
354
    {
355
        $value = $this->parameters[$parameter] ?? null;
356
357
        if (!is_string($value)) {
358
            $value = null;
359
        }
360
361
        $callback = static fn (?string $value, Closure $rule): ?string => $rule($value);
362
363
        $value =  array_reduce($this->rules, $callback, $value);
364
        $value ??= $default;
365
366
        if ($value === null) {
367
            throw new HttpBadRequestException(I18N::translate('The parameter “%s” is missing.', $parameter));
368
        }
369
370
        return $value;
371
    }
372
373
    /**
374
     * @param string $parameter
375
     *
376
     * @return Tree
377
     */
378
    public function tree(string $parameter = 'tree'): Tree
379
    {
380
        $value = $this->parameters[$parameter] ?? null;
381
382
        if ($value instanceof Tree) {
383
            return $value;
384
        }
385
386
        throw new HttpBadRequestException(I18N::translate('The parameter “%s” is missing.', $parameter));
387
    }
388
389
    /**
390
     * @param string $parameter
391
     *
392
     * @return Tree|null
393
     */
394
    public function treeOptional(string $parameter = 'tree'): ?Tree
395
    {
396
        $value = $this->parameters[$parameter] ?? null;
397
398
        if ($value === null || $value instanceof Tree) {
399
            return $value;
400
        }
401
402
        throw new HttpBadRequestException(I18N::translate('The parameter “%s” is missing.', $parameter));
403
    }
404
405
    /**
406
     * @param string $parameter
407
     *
408
     * @return UserInterface
409
     */
410
    public function user(string $parameter = 'user'): UserInterface
411
    {
412
        $value = $this->parameters[$parameter] ?? null;
413
414
        if ($value instanceof UserInterface) {
415
            return $value;
416
        }
417
418
        throw new HttpBadRequestException(I18N::translate('The parameter “%s” is missing.', $parameter));
419
    }
420
}
421