Passed
Push — main ( 5ed6ff...c7b99b )
by Dimitri
28:04
created

ServerRequest::domain()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
cc 2
eloc 6
c 0
b 0
f 0
nc 2
nop 1
dl 0
loc 11
ccs 0
cts 5
cp 0
crap 6
rs 10
1
<?php
2
3
/**
4
 * This file is part of Blitz PHP framework.
5
 *
6
 * (c) 2022 Dimitri Sitchet Tomkeu <[email protected]>
7
 *
8
 * For the full copyright and license information, please view
9
 * the LICENSE file that was distributed with this source code.
10
 */
11
12
namespace BlitzPHP\Http;
13
14
use BadMethodCallException;
15
use BlitzPHP\Container\Services;
16
use BlitzPHP\Exceptions\FrameworkException;
17
use BlitzPHP\Exceptions\HttpException;
18
use BlitzPHP\Filesystem\Files\UploadedFile;
19
use BlitzPHP\Session\Cookie\CookieCollection;
20
use BlitzPHP\Session\Store;
21
use BlitzPHP\Utilities\Helpers;
22
use BlitzPHP\Utilities\Iterable\Arr;
23
use Closure;
24
use GuzzleHttp\Psr7\ServerRequest as Psr7ServerRequest;
25
use GuzzleHttp\Psr7\Stream;
26
use InvalidArgumentException;
27
use Psr\Http\Message\ServerRequestInterface;
28
use Psr\Http\Message\StreamInterface;
29
use Psr\Http\Message\UploadedFileInterface;
30
use Psr\Http\Message\UriInterface;
31
32
/**
33
 * Une classe qui aide à envelopper les informations de la requête et les détails d'une seule requête.
34
 * Fournit des méthodes couramment utilisées pour effectuer une introspection sur les en-têtes et le corps de la requête.
35
 */
36
class ServerRequest implements ServerRequestInterface
37
{
38
    /**
39
     * Tableau de paramètres analysés à partir de l'URL.
40
     */
41
    protected array $params = [
42
        'plugin'     => null,
43
        'controller' => null,
44
        'action'     => null,
45
        '_ext'       => null,
46
        'pass'       => [],
47
    ];
48
49
    /**
50
     * Tableau de données POST. Contiendra des données de formulaire ainsi que des fichiers téléchargés.
51
     * Dans les requêtes PUT/PATCH/DELETE, cette propriété contiendra les données encodées du formulaire.
52
     */
53
    protected null|array|object $data = [];
54
55
    /**
56
     * Tableau d'arguments de chaîne de requête
57
     */
58
    protected array $query = [];
59
60
    /**
61
     * Tableau de données de cookie.
62
     *
63
     * @var array<string, mixed>
64
     */
65
    protected array $cookies = [];
66
67
    /**
68
     * Tableau de données d'environnement.
69
     *
70
     * @var array<string, mixed>
71
     */
72
    protected array $_environment = [];
73
74
    /**
75
     * Chemin de l'URL de base.
76
     */
77
    protected string $base;
78
79
    /**
80
     * segment de chemin webroot pour la demande.
81
     */
82
    protected string $webroot = '/';
83
84
    /**
85
     * S'il faut faire confiance aux en-têtes HTTP_X définis par la plupart des équilibreurs de charge.
86
     * Défini sur vrai uniquement si votre application s'exécute derrière des équilibreurs de charge/proxies que vous contrôlez.
87
     */
88
    public bool $trustProxy = false;
89
90
    /**
91
     * Liste des proxys de confiance
92
     *
93
     * @var string[]
94
     */
95
    protected array $trustedProxies = [];
96
97
    /**
98
     * Les détecteurs intégrés utilisés avec `is()` peuvent être modifiés avec `addDetector()`.
99
     *
100
     * Il existe plusieurs façons de spécifier un détecteur, voir `addDetector()` pour
101
     * les différents formats et façons de définir des détecteurs.
102
     *
103
     * @var array<array|Closure>
104
     */
105
    protected static array $_detectors = [
106
        'get'     => ['env' => 'REQUEST_METHOD', 'value' => 'GET'],
107
        'post'    => ['env' => 'REQUEST_METHOD', 'value' => 'POST'],
108
        'put'     => ['env' => 'REQUEST_METHOD', 'value' => 'PUT'],
109
        'patch'   => ['env' => 'REQUEST_METHOD', 'value' => 'PATCH'],
110
        'delete'  => ['env' => 'REQUEST_METHOD', 'value' => 'DELETE'],
111
        'head'    => ['env' => 'REQUEST_METHOD', 'value' => 'HEAD'],
112
        'options' => ['env' => 'REQUEST_METHOD', 'value' => 'OPTIONS'],
113
        'https'   => ['env' => 'HTTPS', 'options' => [1, 'on']],
114
        'ssl'     => ['env' => 'HTTPS', 'options' => [1, 'on']],
115
        'ajax'    => ['env' => 'HTTP_X_REQUESTED_WITH', 'value' => 'XMLHttpRequest'],
116
        'json'    => ['accept' => ['application/json'], 'param' => '_ext', 'value' => 'json'],
117
        'xml'     => ['accept' => ['application/xml', 'text/xml'], 'param' => '_ext', 'value' => 'xml'],
118
    ];
119
120
    /**
121
     * Cache d'instance pour les résultats des appels is(something)
122
     *
123
     * @var array<string, bool>
124
     */
125
    protected array $_detectorCache = [];
126
127
    /**
128
     * Flux du corps de la requête. Contient php://input sauf si l'option constructeur `input` est utilisée.
129
     */
130
    protected StreamInterface $stream;
131
132
    /**
133
     * instance Uri
134
     */
135
    protected UriInterface $uri;
136
137
    /**
138
     * Instance d'un objet Session relative à cette requête
139
     */
140
    protected Store $session;
141
142
    /**
143
     * Stockez les attributs supplémentaires attachés à la requête.
144
     *
145
     * @var array<string, mixed>
146
     */
147
    protected array $attributes = [];
148
149
    /**
150
     * Une liste de propriétés émulées par les méthodes d'attribut PSR7.
151
     *
152
     * @var array<string>
153
     */
154
    protected array $emulatedAttributes = ['session', 'flash', 'webroot', 'base', 'params', 'here'];
155
156
    /**
157
     * Tableau de fichiers.
158
     *
159
     * @var UploadedFile[]
160
     */
161
    protected array $uploadedFiles = [];
162
163
    /**
164
     * La version du protocole HTTP utilisée.
165
     */
166
    protected ?string $protocol = null;
167
168
    /**
169
     * La cible de la requête si elle est remplacée
170
     */
171
    protected ?string $requestTarget = null;
172
173
    /**
174
     * Negotiator
175
     */
176
    protected ?Negotiator $negotiator = null;
177
178
    /**
179
     * Créer un nouvel objet de requête.
180
     *
181
     * Vous pouvez fournir les données sous forme de tableau ou de chaîne. Si tu utilises
182
     * une chaîne, vous ne pouvez fournir que l'URL de la demande. L'utilisation d'un tableau
183
     * vous permettent de fournir les clés suivantes :
184
     *
185
     * - `post` Données POST ou données de chaîne sans requête
186
     * - `query` Données supplémentaires de la chaîne de requête.
187
     * - `files` Fichiers téléchargés dans une structure normalisée, avec chaque feuille une instance de UploadedFileInterface.
188
     * - `cookies` Cookies pour cette demande.
189
     * - `environment` $_SERVER et $_ENV données.
190
     * - `url` L'URL sans le chemin de base de la requête.
191
     * - `uri` L'objet PSR7 UriInterface. Si nul, un sera créé à partir de `url` ou `environment`.
192
     * - `base` L'URL de base de la requête.
193
     * - `webroot` Le répertoire webroot pour la requête.
194
     * - `input` Les données qui proviendraient de php://input ceci est utile pour simuler
195
     * requêtes avec mise, patch ou suppression de données.
196
     * - `session` Une instance d'un objet Session
197
     *
198
     * @param array<string, mixed> $config Un tableau de données de requête avec lequel créer une requête.
199
     */
200
    public function __construct(array $config = [])
201
    {
202
        $config += [
203
            'params'      => $this->params,
204
            'query'       => $_GET,
205
            'post'        => $_POST,
206
            'files'       => $_FILES,
207
            'cookies'     => $_COOKIE,
208
            'environment' => [],
209
            'url'         => '',
210
            'uri'         => null,
211
            'base'        => '',
212
            'webroot'     => '',
213
            'input'       => null,
214
        ];
215
216
        $this->_setConfig($config);
217
    }
218
219
    /**
220
     * Traitez les données de configuration/paramètres dans les propriétés.
221
     *
222
     * @param array<string, mixed> $config
223
     */
224
    protected function _setConfig(array $config): void
225
    {
226
        if (empty($config['session'])) {
227
            $config['session'] = Services::session(false);
228
        }
229
230
        if (empty($config['environment']['REQUEST_METHOD'])) {
231
            $config['environment']['REQUEST_METHOD'] = $_SERVER['REQUEST_METHOD'] ?? 'GET';
232
        }
233
234
        $this->cookies = $config['cookies'];
235
236
        if (isset($config['uri'])) {
237
            if (! $config['uri'] instanceof UriInterface) {
238
                throw new FrameworkException('The `uri` key must be an instance of ' . UriInterface::class);
239
            }
240
            $uri = $config['uri'];
241
        } else {
242
            if ($config['url'] !== '') {
243
                $config = $this->processUrlOption($config);
244
            }
245
            $uri = Psr7ServerRequest::getUriFromGlobals();
246
        }
247
248
        $this->_environment = $config['environment'];
249
250
        $this->uri     = $uri;
251
        $this->base    = $config['base'];
252
        $this->webroot = $config['webroot'];
253
254
        if (isset($config['input'])) {
255
            $stream = new Stream(\GuzzleHttp\Psr7\Utils::tryFopen('php://memory', 'rw'));
256
            $stream->write($config['input']);
257
            $stream->rewind();
258
        } else {
259
            $stream = new Stream(\GuzzleHttp\Psr7\Utils::tryFopen('php://input', 'r'));
260
        }
261
        $this->stream = $stream;
262
263
        $config['post'] = $this->_processPost($config['post']);
264
        $this->data     = $this->_processFiles($config['post'], $config['files']);
265
        $this->query    = $config['query'];
266
        $this->params   = $config['params'];
267
        $this->session  = $config['session'];
268
    }
269
270
    /**
271
     * Définissez les variables d'environnement en fonction de l'option `url` pour faciliter la génération d'instance UriInterface.
272
     *
273
     * L'option `query` est également mise à jour en fonction de la chaîne de requête de l'URL.
274
     */
275
    protected function processUrlOption(array $config): array
276
    {
277
        if ($config['url'][0] !== '/') {
278
            $config['url'] = '/' . $config['url'];
0 ignored issues
show
Bug introduced by
Are you sure $config['url'] of type array can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

278
            $config['url'] = '/' . /** @scrutinizer ignore-type */ $config['url'];
Loading history...
279
        }
280
281
        if (str_contains($config['url'], '?')) {
282
            [$config['url'], $config['environment']['QUERY_STRING']] = explode('?', $config['url']);
283
284
            parse_str($config['environment']['QUERY_STRING'], $queryArgs);
285
            $config['query'] += $queryArgs;
286
        }
287
288
        $config['environment']['REQUEST_URI'] = $config['url'];
289
290
        return $config;
291
    }
292
293
    /**
294
     * Obtenez le type de contenu utilisé dans cette requête.
295
     */
296
    public function contentType(): ?string
297
    {
298
        return $this->getEnv('CONTENT_TYPE') ?: $this->getEnv('HTTP_CONTENT_TYPE');
299
    }
300
301
    /**
302
     * Renvoie l'instance de l'objet Session pour cette requête
303
     */
304
    public function session(): Store
305
    {
306
        return $this->session;
307
    }
308
309
    /**
310
     * Obtenez l'adresse IP que le client utilise ou dit qu'il utilise.
311
     */
312
    public function clientIp(): string
313
    {
314
        if ($this->trustProxy && $this->getEnv('HTTP_X_FORWARDED_FOR')) {
315
            $addresses = array_map('trim', explode(',', (string) $this->getEnv('HTTP_X_FORWARDED_FOR')));
316
            $trusted   = (count($this->trustedProxies) > 0);
317
            $n         = count($addresses);
318
319
            if ($trusted) {
320
                $trusted = array_diff($addresses, $this->trustedProxies);
321
                $trusted = (count($trusted) === 1);
322
            }
323
324
            if ($trusted) {
325
                return $addresses[0];
326
            }
327
328
            return $addresses[$n - 1];
329
        }
330
331
        if ($this->trustProxy && $this->getEnv('HTTP_X_REAL_IP')) {
332
            $ipaddr = $this->getEnv('HTTP_X_REAL_IP');
333
        } elseif ($this->trustProxy && $this->getEnv('HTTP_CLIENT_IP')) {
334
            $ipaddr = $this->getEnv('HTTP_CLIENT_IP');
335
        } else {
336
            $ipaddr = $this->getEnv('REMOTE_ADDR');
337
        }
338
339
        return trim((string) $ipaddr);
340
    }
341
342
    /**
343
     * Enregistrer des proxys de confiance
344
     *
345
     * @param string[] $proxies ips liste des proxys de confiance
346
     */
347
    public function setTrustedProxies(array $proxies): void
348
    {
349
        $this->trustedProxies = $proxies;
350
        $this->trustProxy     = true;
351
        $this->uri            = $this->uri->withScheme($this->scheme());
0 ignored issues
show
Bug introduced by
It seems like $this->scheme() can also be of type null; however, parameter $scheme of Psr\Http\Message\UriInterface::withScheme() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

351
        $this->uri            = $this->uri->withScheme(/** @scrutinizer ignore-type */ $this->scheme());
Loading history...
352
    }
353
354
    /**
355
     * Obtenez les proxys de confiance
356
     */
357
    public function getTrustedProxies(): array
358
    {
359
        return $this->trustedProxies;
360
    }
361
362
    /**
363
     * Renvoie le référent qui a référé cette requête.
364
     *
365
     * @param bool $local Tentative de renvoi d'une adresse locale.
366
     *                    Les adresses locales ne contiennent pas de noms d'hôtes..
367
     */
368
    public function referer(bool $local = true): ?string
369
    {
370
        $ref = $this->getEnv('HTTP_REFERER');
371
372
        $base = config('app.base_url') . $this->webroot;
0 ignored issues
show
Bug introduced by
Are you sure config('app.base_url') of type BlitzPHP\Config\Config|null can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

372
        $base = /** @scrutinizer ignore-type */ config('app.base_url') . $this->webroot;
Loading history...
373
        if (empty($base) || empty($ref)) {
374
            return null;
375
        }
376
377
        if ($local && str_starts_with($ref, $base)) {
378
            $ref = substr($ref, strlen($base));
379
            if ($ref === '' || str_starts_with($ref, '//')) {
380
                $ref = '/';
381
            }
382
            if ($ref[0] !== '/') {
383
                $ref = '/' . $ref;
384
            }
385
386
            return $ref;
387
        }
388
389
        if ($local) {
390
            return null;
391
        }
392
393
        return $ref;
394
    }
395
396
    /**
397
     * Gestionnaire de méthodes manquant, les poignées enveloppent les anciennes méthodes de type isAjax()
398
     *
399
     * @return bool
400
     *
401
     * @throws BadMethodCallException lorsqu'une méthode invalide est appelée.
402
     */
403
    public function __call(string $name, array $params)
404
    {
405
        if (str_starts_with($name, 'is')) {
406
            $type = strtolower(substr($name, 2));
407
408
            array_unshift($params, $type);
409
410
            return $this->is(...$params);
411
        }
412
413
        throw new BadMethodCallException(sprintf('La méthode "%s()" n\'existe pas', $name));
414
    }
415
416
    /**
417
     * Vérifiez si une demande est d'un certain type.
418
     *
419
     * Utilise les règles de détection intégrées ainsi que des règles supplémentaires
420
     * défini avec {@link \BlitzPHP\Http\ServerRequest::addDetector()}. Tout détecteur peut être appelé
421
     * comme `is($type)` ou `is$Type()`.
422
     *
423
     * @param string|string[] $type Le type de requête que vous souhaitez vérifier. S'il s'agit d'un tableau, cette méthode renverra true si la requête correspond à n'importe quel type.
424
     *
425
     * @return bool Si la demande est du type que vous vérifiez.
426
     */
427
    public function is($type, ...$args): bool
428
    {
429
        if (is_array($type)) {
430
            foreach ($type as $_type) {
431
                if ($this->is($_type)) {
432
                    return true;
433
                }
434
            }
435
436
            return false;
437
        }
438
439
        $type = strtolower($type);
440
        if (! isset(static::$_detectors[$type])) {
441
            return false;
442
        }
443
        if ($args) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $args of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
444
            return $this->_is($type, $args);
445
        }
446
447
        return $this->_detectorCache[$type] ??= $this->_is($type, $args);
448
    }
449
450
    /**
451
     * Efface le cache du détecteur d'instance, utilisé par la fonction is()
452
     */
453
    public function clearDetectorCache(): void
454
    {
455
        $this->_detectorCache = [];
456
    }
457
458
    /**
459
     * Worker pour la fonction publique is()
460
     *
461
     * @param string $type Le type de requête que vous souhaitez vérifier.
462
     * @param array  $args Tableau d'arguments de détecteur personnalisés.
463
     *
464
     * @return bool Si la demande est du type que vous vérifiez.
465
     */
466
    protected function _is(string $type, array $args): bool
467
    {
468
        $detect = static::$_detectors[$type];
469
        if ($detect instanceof Closure) {
470
            array_unshift($args, $this);
471
472
            return $detect(...$args);
473
        }
474
        if (isset($detect['env']) && $this->_environmentDetector($detect)) {
475
            return true;
476
        }
477
        if (isset($detect['header']) && $this->_headerDetector($detect)) {
478
            return true;
479
        }
480
        if (isset($detect['accept']) && $this->_acceptHeaderDetector($detect)) {
481
            return true;
482
        }
483
484
        return (bool) (isset($detect['param']) && $this->_paramDetector($detect));
485
    }
486
487
    /**
488
     * Détecte si un en-tête d'acceptation spécifique est présent.
489
     *
490
     * @param array $detect Tableau d'options du détecteur.
491
     *
492
     * @return bool Si la demande est du type que vous vérifiez.
493
     */
494
    protected function _acceptHeaderDetector(array $detect): bool
495
    {
496
        $acceptHeaders = explode(',', (string) $this->getEnv('HTTP_ACCEPT'));
497
498
        foreach ($detect['accept'] as $header) {
499
            if (in_array($header, $acceptHeaders, true)) {
500
                return true;
501
            }
502
        }
503
504
        return false;
505
    }
506
507
    /**
508
     * Détecte si un en-tête spécifique est présent.
509
     *
510
     * @param array $detect Tableau d'options du détecteur.
511
     *
512
     * @return bool Si la demande est du type que vous vérifiez.
513
     */
514
    protected function _headerDetector(array $detect): bool
515
    {
516
        foreach ($detect['header'] as $header => $value) {
517
            $header = $this->getEnv('http_' . $header);
518
            if ($header !== null) {
519
                if ($value instanceof Closure) {
520
                    return $value($header);
521
                }
522
523
                return $header === $value;
524
            }
525
        }
526
527
        return false;
528
    }
529
530
    /**
531
     * Détecte si un paramètre de requête spécifique est présent.
532
     *
533
     * @param array $detect Tableau d'options du détecteur.
534
     *
535
     * @return bool Si la demande est du type que vous vérifiez.
536
     */
537
    protected function _paramDetector(array $detect): bool
538
    {
539
        $key = $detect['param'];
540
        if (isset($detect['value'])) {
541
            $value = $detect['value'];
542
543
            return isset($this->params[$key]) ? $this->params[$key] === $value : false;
544
        }
545
        if (isset($detect['options'])) {
546
            return isset($this->params[$key]) ? in_array($this->params[$key], $detect['options'], true) : false;
547
        }
548
549
        return false;
550
    }
551
552
    /**
553
     * Détecte si une variable d'environnement spécifique est présente.
554
     *
555
     * @param array $detect Tableau d'options du détecteur.
556
     *
557
     * @return bool Si la demande est du type que vous vérifiez.
558
     */
559
    protected function _environmentDetector(array $detect): bool
560
    {
561
        if (isset($detect['env'])) {
562
            if (isset($detect['value'])) {
563
                return $this->getEnv($detect['env']) === $detect['value'];
564
            }
565
            if (isset($detect['pattern'])) {
566
                return (bool) preg_match($detect['pattern'], (string) $this->getEnv($detect['env']));
567
            }
568
            if (isset($detect['options'])) {
569
                $pattern = '/' . implode('|', $detect['options']) . '/i';
570
571
                return (bool) preg_match($pattern, (string) $this->getEnv($detect['env']));
572
            }
573
        }
574
575
        return false;
576
    }
577
578
    /**
579
     * Vérifier qu'une requête correspond à tous les types donnés.
580
     *
581
     * Vous permet de tester plusieurs types et d'unir les résultats.
582
     * Voir Request::is() pour savoir comment ajouter des types supplémentaires et le
583
     * types intégrés.
584
     *
585
     * @param string[] $types Les types à vérifier.
586
     *
587
     * @see \BlitzPHP\Http\ServerRequest::is()
588
     */
589
    public function isAll(array $types): bool
590
    {
591
        foreach ($types as $type) {
592
            if (! $this->is($type)) {
593
                return false;
594
            }
595
        }
596
597
        return true;
598
    }
599
600
    /**
601
     * Ajouter un nouveau détecteur à la liste des détecteurs qu'une requête peut utiliser.
602
     * Il existe plusieurs types de détecteurs différents qui peuvent être réglés.
603
     *
604
     * ### Comparaison des rappels
605
     *
606
     * Les détecteurs de rappel vous permettent de fournir un callable pour gérer le chèque.
607
     * Le rappel recevra l'objet de requête comme seul paramètre.
608
     *
609
     * ```
610
     * addDetector('custom', function ($request) { //Renvoyer un booléen });
611
     * ```
612
     *
613
     * ### Comparaison des valeurs d'environnement
614
     *
615
     * Une comparaison de valeur d'environnement, compare une valeur extraite de `env()` à une valeur connue
616
     * la valeur d'environnement est l'égalité vérifiée par rapport à la valeur fournie.
617
     *
618
     * ```
619
     * addDetector('post', ['env' => 'REQUEST_METHOD', 'value' => 'POST']);
620
     * ```
621
     *
622
     * ### Comparaison des paramètres de demande
623
     *
624
     * Permet des détecteurs personnalisés sur les paramètres de demande.
625
     *
626
     * ```
627
     * addDetector('admin', ['param' => 'prefix', 'value' => 'admin']);
628
     * ```
629
     *
630
     * ### Accepter la comparaison
631
     *
632
     * Permet au détecteur de comparer avec la valeur d'en-tête Accepter.
633
     *
634
     * ```
635
     * addDetector('csv', ['accept' => 'text/csv']);
636
     * ```
637
     *
638
     * ### Comparaison d'en-tête
639
     *
640
     * Permet de comparer un ou plusieurs en-têtes.
641
     *
642
     * ```
643
     * addDetector('fancy', ['header' => ['X-Fancy' => 1]);
644
     * ```
645
     *
646
     * Les types `param`, `env` et de comparaison permettent ce qui suit
647
     * options de comparaison de valeur :
648
     *
649
     * ### Comparaison des valeurs de modèle
650
     *
651
     * La comparaison de valeurs de modèles vous permet de comparer une valeur extraite de `env()` à une expression régulière.
652
     *
653
     * ```
654
     * addDetector('iphone', ['env' => 'HTTP_USER_AGENT', 'pattern' => '/iPhone/i']);
655
     * ```
656
     *
657
     * ### Comparaison basée sur les options
658
     *
659
     * Les comparaisons basées sur des options utilisent une liste d'options pour créer une expression régulière. Appels ultérieurs
660
     * ajouter un détecteur d'options déjà défini fusionnera les options.
661
     *
662
     * ```
663
     * addDetector('mobile', ['env' => 'HTTP_USER_AGENT', 'options' => ['Fennec']]);
664
     * ```
665
     *
666
     * Vous pouvez également comparer plusieurs valeurs
667
     * en utilisant la touche `options`. Ceci est utile lorsque vous souhaitez vérifier
668
     * si une valeur de requête se trouve dans une liste d'options.
669
     *
670
     * `addDetector('extension', ['param' => '_ext', 'options' => ['pdf', 'csv']]`
671
     *
672
     * @param array|callable $detector Un callback ou tableau d'options pour la définition du détecteur.
673
     */
674
    public static function addDetector(string $name, $detector): void
675
    {
676
        $name = strtolower($name);
677
        if ($detector instanceof Closure) {
678
            static::$_detectors[$name] = $detector;
679
680
            return;
681
        }
682
        if (isset(static::$_detectors[$name], $detector['options'])) {
683
            /** @var array $data */
684
            $data     = static::$_detectors[$name];
685
            $detector = Arr::merge($data, $detector);
686
        }
687
        static::$_detectors[$name] = $detector;
688
    }
689
690
    /**
691
     * Normaliser un nom d'en-tête dans la version SERVER.
692
     */
693
    protected function normalizeHeaderName(string $name): string
694
    {
695 2
        $name = str_replace('-', '_', strtoupper($name));
696
        if (! in_array($name, ['CONTENT_LENGTH', 'CONTENT_TYPE'], true)) {
697 2
            $name = 'HTTP_' . $name;
698
        }
699
700 2
        return $name;
701
    }
702
703
    /**
704
     * Obtenez tous les en-têtes de la requête.
705
     *
706
     * Renvoie un tableau associatif où les noms d'en-tête sont
707
     * les clés et les valeurs sont une liste de valeurs d'en-tête.
708
     *
709
     * Bien que les noms d'en-tête ne soient pas sensibles à la casse, getHeaders() normalisera
710
     * les en-têtes.
711
     *
712
     * @return array<string[]> Un tableau associatif d'en-têtes et leurs valeurs.
713
     *
714
     * @see http://www.php-fig.org/psr/psr-7/ Cette méthode fait partie de l'interface de requête du serveur PSR-7.
715
     */
716
    public function getHeaders(): array
717
    {
718
        $headers = [];
719
720
        foreach ($this->_environment as $key => $value) {
721
            $name = null;
722
            if (str_starts_with($key, 'HTTP_')) {
723
                $name = substr($key, 5);
724
            }
725
            if (str_starts_with($key, 'CONTENT_')) {
726
                $name = $key;
727
            }
728
            if ($name !== null) {
729
                $name           = str_replace('_', ' ', strtolower($name));
730
                $name           = str_replace(' ', '-', ucwords($name));
731
                $headers[$name] = (array) $value;
732
            }
733
        }
734
735
        return $headers;
736
    }
737
738
    /**
739
     * Vérifiez si un en-tête est défini dans la requête.
740
     *
741
     * @param string $name L'en-tête que vous souhaitez obtenir (insensible à la casse)
742
     *
743
     * @see http://www.php-fig.org/psr/psr-7/ Cette méthode fait partie de l'interface de requête du serveur PSR-7.
744
     */
745
    public function hasHeader(string $name): bool
746
    {
747
        if (isset($this->_environment[$this->normalizeHeaderName($name)])) {
748
            return true;
749
        }
750
751
        return [] !== $this->getHeader($name);
752
    }
753
754
    /**
755
     * Obtenez un seul en-tête de la requête.
756
     *
757
     * Renvoie la valeur de l'en-tête sous forme de tableau. Si l'en-tête
758
     * n'est pas présent, un tableau vide sera retourné.
759
     *
760
     * @param string $name L'en-tête que vous souhaitez obtenir (insensible à la casse)
761
     *
762
     * @return string[] Un tableau associatif d'en-têtes et leurs valeurs.
763
     *                  Si l'en-tête n'existe pas, un tableau vide sera retourné.
764
     *
765
     * @see http://www.php-fig.org/psr/psr-7/ Cette méthode fait partie de l'interface de requête du serveur PSR-7.
766
     */
767
    public function getHeader(string $name): array
768
    {
769 2
        $name = $this->normalizeHeaderName($name);
770
        if (isset($this->_environment[$name])) {
771 2
            return (array) $this->_environment[$name];
772
        }
773
774 2
        return (array) $this->getEnv($name);
775
    }
776
777
    /**
778
     * Obtenez un seul en-tête sous forme de chaîne à partir de la requête.
779
     *
780
     * @param string $name L'en-tête que vous souhaitez obtenir (insensible à la casse)
781
     *
782
     * @return string Les valeurs d'en-tête sont réduites à une chaîne séparée par des virgules.
783
     *
784
     * @see http://www.php-fig.org/psr/psr-7/ Cette méthode fait partie de l'interface de requête du serveur PSR-7.
785
     */
786
    public function getHeaderLine(string $name): string
787
    {
788 2
        $value = $this->getHeader($name);
789
790 2
        return implode(', ', $value);
791
    }
792
793
    /**
794
     * Obtenez une demande modifiée avec l'en-tête fourni.
795
     *
796
     * @param array|string $value
797
     *
798
     * @see http://www.php-fig.org/psr/psr-7/ Cette méthode fait partie de l'interface de requête du serveur PSR-7.
799
     */
800
    public function withHeader(string $name, $value): static
801
    {
802
        $new                      = clone $this;
803
        $name                     = $this->normalizeHeaderName($name);
804
        $new->_environment[$name] = $value;
805
806
        return $new;
807
    }
808
809
    /**
810
     * Obtenez une demande modifiée avec l'en-tête fourni.
811
     *
812
     * Les valeurs d'en-tête existantes seront conservées. La valeur fournie
813
     * sera ajouté aux valeurs existantes.
814
     *
815
     * @param array|string $value
816
     *
817
     * @see http://www.php-fig.org/psr/psr-7/ Cette méthode fait partie de l'interface de requête du serveur PSR-7.
818
     */
819
    public function withAddedHeader(string $name, $value): static
820
    {
821
        $new      = clone $this;
822
        $name     = $this->normalizeHeaderName($name);
823
        $existing = [];
824
        if (isset($new->_environment[$name])) {
825
            $existing = (array) $new->_environment[$name];
826
        }
827
        $existing                 = array_merge($existing, (array) $value);
828
        $new->_environment[$name] = $existing;
829
830
        return $new;
831
    }
832
833
    /**
834
     * Obtenez une demande modifiée sans en-tête fourni.
835
     *
836
     * @see http://www.php-fig.org/psr/psr-7/ Cette méthode fait partie de l'interface de requête du serveur PSR-7.
837
     */
838
    public function withoutHeader(string $name): static
839
    {
840
        $new  = clone $this;
841
        $name = $this->normalizeHeaderName($name);
842
        unset($new->_environment[$name]);
843
844
        return $new;
845
    }
846
847
    /**
848
     * Obtenez la méthode HTTP utilisée pour cette requête.
849
     * Il existe plusieurs manières de spécifier une méthode.
850
     *
851
     * - Si votre client le prend en charge, vous pouvez utiliser des méthodes HTTP natives.
852
     * - Vous pouvez définir l'en-tête HTTP-X-Method-Override.
853
     * - Vous pouvez soumettre une entrée avec le nom `_method`
854
     *
855
     * Chacune de ces 3 approches peut être utilisée pour définir la méthode HTTP utilisée
856
     * par BlitzPHP en interne, et affectera le résultat de cette méthode.
857
     *
858
     * @see http://www.php-fig.org/psr/psr-7/ Cette méthode fait partie de l'interface de requête du serveur PSR-7.
859
     */
860
    public function getMethod(): string
861
    {
862 8
        return (string) $this->getEnv('REQUEST_METHOD');
863
    }
864
865
    /**
866
     * Mettez à jour la méthode de requête et obtenez une nouvelle instance.
867
     *
868
     * @see http://www.php-fig.org/psr/psr-7/ Cette méthode fait partie de l'interface de requête du serveur PSR-7.
869
     */
870
    public function withMethod(string $method): static
871
    {
872 18
        $new = clone $this;
873
874
        if (! preg_match('/^[!#$%&\'*+.^_`\|~0-9a-z-]+$/i', $method)) {
875
            throw new InvalidArgumentException(sprintf(
876
                'Méthode HTTP non prise en charge "%s" fournie',
877
                $method
878 18
            ));
879
        }
880 18
        $new->_environment['REQUEST_METHOD'] = $method;
881
882 18
        return $new;
883
    }
884
885
    /**
886
     * Obtenez tous les paramètres de l'environnement du serveur.
887
     *
888
     * Lire toutes les données 'environnement' ou 'serveur' qui ont été
889
     * utilisé pour créer cette requête.
890
     *
891
     * @see http://www.php-fig.org/psr/psr-7/ Cette méthode fait partie de l'interface de requête du serveur PSR-7.
892
     */
893
    public function getServerParams(): array
894
    {
895
        return $this->_environment;
896
    }
897
898
    /**
899
     * Obtenez tous les paramètres de requête conformément aux spécifications PSR-7. Pour lire des valeurs de requête spécifiques
900
     * utilisez la méthode alternative getQuery().
901
     *
902
     * @see http://www.php-fig.org/psr/psr-7/ Cette méthode fait partie de l'interface de requête du serveur PSR-7.
903
     */
904
    public function getQueryParams(): array
905
    {
906
        return $this->query;
907
    }
908
909
    /**
910
     * Mettez à jour les données de la chaîne de requête et obtenez une nouvelle instance.
911
     *
912
     * @param array $query Les données de la chaîne de requête à utiliser
913
     *
914
     * @see http://www.php-fig.org/psr/psr-7/ Cette méthode fait partie de l'interface de requête du serveur PSR-7.
915
     */
916
    public function withQueryParams(array $query): static
917
    {
918
        $new        = clone $this;
919
        $new->query = $query;
920
921
        return $new;
922
    }
923
924
    /**
925
     * Obtenez l'hôte sur lequel la demande a été traitée.
926
     */
927
    public function host(): ?string
928
    {
929
        if ($this->trustProxy && $this->getEnv('HTTP_X_FORWARDED_HOST')) {
930
            return $this->getEnv('HTTP_X_FORWARDED_HOST');
931
        }
932
933
        return $this->getEnv('HTTP_HOST');
934
    }
935
936
    /**
937
     * Obtenez le port sur lequel la demande a été traitée.
938
     */
939
    public function port(): ?string
940
    {
941
        if ($this->trustProxy && $this->getEnv('HTTP_X_FORWARDED_PORT')) {
942
            return $this->getEnv('HTTP_X_FORWARDED_PORT');
943
        }
944
945
        return $this->getEnv('SERVER_PORT');
946
    }
947
948
    /**
949
     * Obtenez le schéma d'URL actuel utilisé pour la demande.
950
     *
951
     * par exemple. 'http' ou 'https'
952
     */
953
    public function scheme(): ?string
954
    {
955
        if ($this->trustProxy && $this->getEnv('HTTP_X_FORWARDED_PROTO')) {
956
            return $this->getEnv('HTTP_X_FORWARDED_PROTO');
957
        }
958
959
        return $this->getEnv('HTTPS') ? 'https' : 'http';
960
    }
961
962
    /**
963
     * Obtenez le nom de domaine et incluez les segments $tldLength du tld.
964
     *
965
     * @param int $tldLength Nombre de segments que contient votre tld. Par exemple : `example.com` contient 1 tld.
966
     *                       Alors que `example.co.uk` contient 2.
967
     *
968
     * @return string Nom de domaine sans sous-domaines.
969
     */
970
    public function domain(int $tldLength = 1): string
971
    {
972
        $host = $this->host();
973
        if (empty($host)) {
974
            return '';
975
        }
976
977
        $segments = explode('.', $host);
978
        $domain   = array_slice($segments, -1 * ($tldLength + 1));
979
980
        return implode('.', $domain);
981
    }
982
983
    /**
984
     * Obtenez les sous-domaines d'un hôte.
985
     *
986
     * @param int $tldLength Nombre de segments que contient votre tld. Par exemple : `example.com` contient 1 tld.
987
     *                       Alors que `example.co.uk` contient 2.
988
     *
989
     * @return string[] Un tableau de sous-domaines.
990
     */
991
    public function subdomains(int $tldLength = 1): array
992
    {
993
        $host = $this->host();
994
        if (empty($host)) {
995
            return [];
996
        }
997
998
        $segments = explode('.', $host);
999
1000
        return array_slice($segments, 0, -1 * ($tldLength + 1));
1001
    }
1002
1003
    /**
1004
     * Découvrez quels types de contenu le client accepte ou vérifiez s'il accepte un
1005
     * type particulier de contenu.
1006
     *
1007
     * #### Obtenir tous les types :
1008
     *
1009
     * ```
1010
     * $this->request->accepts();
1011
     * ```
1012
     *
1013
     * #### Vérifier un seul type :
1014
     *
1015
     * ```
1016
     * $this->request->accepts('application/json');
1017
     * ```
1018
     *
1019
     * Cette méthode ordonnera les types de contenu renvoyés par les valeurs de préférence indiquées
1020
     * par le client.
1021
     *
1022
     * @param string|null $type Le type de contenu à vérifier. Laissez null pour obtenir tous les types qu'un client accepte.
1023
     *
1024
     * @return bool|string[] Soit un tableau de tous les types acceptés par le client, soit un booléen s'il accepte le type fourni.
1025
     */
1026
    public function accepts(?string $type = null)
1027
    {
1028
        $raw    = $this->parseAccept();
1029
        $accept = [];
1030
1031
        foreach ($raw as $types) {
1032
            $accept = array_merge($accept, $types);
1033
        }
1034
        if ($type === null) {
1035
            return $accept;
1036
        }
1037
1038
        return in_array($type, $accept, true);
1039
    }
1040
1041
    /**
1042
     * Analyser l'en-tête HTTP_ACCEPT et renvoyer un tableau trié avec les types de contenu
1043
     * comme clés et valeurs pref comme valeurs.
1044
     *
1045
     * Généralement, vous souhaitez utiliser {@link \BlitzPHP\Http\ServerRequest::accepts()} pour obtenir une liste simple
1046
     * des types de contenu acceptés.
1047
     *
1048
     * @return array Un tableau de `prefValue => [contenu/types]`
1049
     */
1050
    public function parseAccept(): array
1051
    {
1052
        return $this->_parseAcceptWithQualifier($this->getHeaderLine('Accept'));
1053
    }
1054
1055
    /**
1056
     * Obtenez les langues acceptées par le client ou vérifiez si une langue spécifique est acceptée.
1057
     *
1058
     * Obtenez la liste des langues acceptées :
1059
     *
1060
     * ``` \BlitzPHP\Http\ServerRequest::acceptLanguage(); ```
1061
     *
1062
     * Vérifiez si une langue spécifique est acceptée :
1063
     *
1064
     * ``` \BlitzPHP\Http\ServerRequest::acceptLanguage('es-es'); ```
1065
     *
1066
     * @return array|bool Si un $language est fourni, un booléen. Sinon, le tableau des langues acceptées.
1067
     */
1068
    public function acceptLanguage(?string $language = null)
1069
    {
1070
        $raw    = $this->_parseAcceptWithQualifier($this->getHeaderLine('Accept-Language'));
1071
        $accept = [];
1072
1073
        foreach ($raw as $languages) {
1074
            foreach ($languages as &$lang) {
1075
                if (strpos($lang, '_')) {
1076
                    $lang = str_replace('_', '-', $lang);
1077
                }
1078
                $lang = strtolower($lang);
1079
            }
1080
            $accept = array_merge($accept, $languages);
1081
        }
1082
        if ($language === null) {
1083
            return $accept;
1084
        }
1085
1086
        return in_array(strtolower($language), $accept, true);
1087
    }
1088
1089
    /**
1090
     * Analysez les en-têtes Accept* avec les options de qualificateur.
1091
     *
1092
     * Seuls les qualificatifs seront extraits, toutes les autres extensions acceptées seront
1093
     * jetés car ils ne sont pas fréquemment utilisés.
1094
     */
1095
    protected function _parseAcceptWithQualifier(string $header): array
1096
    {
1097
        $accept  = [];
1098
        $headers = explode(',', $header);
1099
1100
        foreach (array_filter($headers) as $value) {
1101
            $prefValue = '1.0';
1102
            $value     = trim($value);
1103
1104
            $semiPos = strpos($value, ';');
1105
            if ($semiPos !== false) {
1106
                $params = explode(';', $value);
1107
                $value  = trim($params[0]);
1108
1109
                foreach ($params as $param) {
1110
                    $qPos = strpos($param, 'q=');
1111
                    if ($qPos !== false) {
1112
                        $prefValue = substr($param, $qPos + 2);
1113
                    }
1114
                }
1115
            }
1116
1117
            if (! isset($accept[$prefValue])) {
1118
                $accept[$prefValue] = [];
1119
            }
1120
            if ($prefValue) {
1121
                $accept[$prefValue][] = $value;
1122
            }
1123
        }
1124
        krsort($accept);
1125
1126
        return $accept;
1127
    }
1128
1129
    /**
1130
     * Lire une valeur de requête spécifique ou un chemin en pointillés.
1131
     *
1132
     * Les développeurs sont encouragés à utiliser getQueryParams() s'ils ont besoin de tout le tableau de requête,
1133
     * car il est compatible PSR-7, et cette méthode ne l'est pas. En utilisant Hash::get(), vous pouvez également obtenir des paramètres uniques.
1134
     *
1135
     * ### Alternative PSR-7
1136
     *
1137
     * ```
1138
     * $value = Arr::get($request->getQueryParams(), 'Post.id');
1139
     * ```
1140
     *
1141
     * @param string|null $name    Le nom ou le chemin en pointillé vers le paramètre de requête ou null pour tout lire.
1142
     * @param mixed       $default La valeur par défaut si le paramètre nommé n'est pas défini et que $name n'est pas nul.
1143
     *
1144
     * @return array|string|null Requête de données.
1145
     *
1146
     * @see ServerRequest::getQueryParams()
1147
     */
1148
    public function getQuery(?string $name = null, $default = null)
1149
    {
1150
        if ($name === null) {
1151
            return $this->query;
1152
        }
1153
1154
        return Arr::get($this->query, $name, $default);
1155
    }
1156
1157
    /**
1158
     * Fournit un accesseur sécurisé pour les données de requête. Permet
1159
     * vous permet d'utiliser des chemins compatibles Arr::get().
1160
     *
1161
     * ### Lecture des valeurs.
1162
     *
1163
     * ```
1164
     * // récupère toutes les données
1165
     * $request->getData();
1166
     *
1167
     * // Lire un champ spécifique.
1168
     * $request->getData('Post.title');
1169
     *
1170
     * // Avec une valeur par défaut.
1171
     * $request->getData('Post.not there', 'default value');
1172
     * ```
1173
     *
1174
     * Lors de la lecture des valeurs, vous obtiendrez `null` pour les clés/valeurs qui n'existent pas.
1175
     *
1176
     * Les développeurs sont encouragés à utiliser getParsedBody() s'ils ont besoin de tout le tableau de données,
1177
     * car il est compatible PSR-7, et cette méthode ne l'est pas. En utilisant Hash::get(), vous pouvez également obtenir des paramètres uniques.
1178
     *
1179
     * ### Alternative PSR-7
1180
     *
1181
     * ```
1182
     * $value = Arr::get($request->getParsedBody(), 'Post.id');
1183
     * ```
1184
     *
1185
     * @param string|null $name    Nom séparé par un point de la valeur à lire. Ou null pour lire toutes les données.
1186
     * @param mixed       $default Les données par défaut.
1187
     *
1188
     * @return mixed La valeur en cours de lecture.
1189
     */
1190
    public function getData(?string $name = null, $default = null)
1191
    {
1192
        if ($name === null) {
1193
            return $this->data;
1194
        }
1195
        if (! is_array($this->data)) {
1196
            return $default;
1197
        }
1198
1199
        return Arr::get($this->data, $name, $default);
1200
    }
1201
1202
    /**
1203
     * Lire les données de cookie à partir des données de cookie de la demande.
1204
     *
1205
     * @param string            $key     La clé ou le chemin en pointillés que vous voulez lire.
1206
     * @param array|string|null $default La valeur par défaut si le cookie n'est pas défini.
1207
     *
1208
     * @return array|string|null Soit la valeur du cookie, soit null si la valeur n'existe pas.
1209
     */
1210
    public function getCookie(string $key, $default = null)
1211
    {
1212
        return Arr::get($this->cookies, $key, $default);
1213
    }
1214
1215
    /**
1216
     * Obtenir une collection de cookies basée sur les cookies de la requête
1217
     *
1218
     * La CookieCollection vous permet d'interagir avec les cookies de demande en utilisant
1219
     * Objets `\BlitzPHP\Http\Cookie\Cookie` et peut faire des cookies de demande de conversion
1220
     * dans les cookies de réponse plus facile.
1221
     *
1222
     * Cette méthode créera une nouvelle collection de cookies à chaque appel.
1223
     * Il s'agit d'une optimisation qui permet d'allouer moins d'objets jusqu'à
1224
     * plus la CookieCollection est nécessaire. En général, vous devriez préférer
1225
     * `getCookie()` et `getCookieParams()` sur cette méthode. Utilisation d'une collection de cookies
1226
     * est idéal si vos cookies contiennent des données complexes encodées en JSON.
1227
     */
1228
    public function getCookieCollection(): CookieCollection
1229
    {
1230
        return CookieCollection::createFromServerRequest($this);
1231
    }
1232
1233
    /**
1234
     * Remplacez les cookies de la requête par ceux contenus dans
1235
     * la CookieCollection fournie.
1236
     */
1237
    public function withCookieCollection(CookieCollection $cookies): static
1238
    {
1239
        $new    = clone $this;
1240
        $values = [];
1241
1242
        foreach ($cookies as $cookie) {
1243
            $values[$cookie->getName()] = $cookie->getValue();
1244
        }
1245
        $new->cookies = $values;
1246
1247
        return $new;
1248
    }
1249
1250
    /**
1251
     * Obtenez toutes les données de cookie de la requête.
1252
     *
1253
     * @return array Un tableau de données de cookie.
1254
     */
1255
    public function getCookieParams(): array
1256
    {
1257
        return $this->cookies;
1258
    }
1259
1260
    /**
1261
     * Remplacez les cookies et obtenez une nouvelle instance de requête.
1262
     *
1263
     * @param array $cookies Les nouvelles données de cookie à utiliser.
1264
     */
1265
    public function withCookieParams(array $cookies): static
1266
    {
1267
        $new          = clone $this;
1268
        $new->cookies = $cookies;
1269
1270
        return $new;
1271
    }
1272
1273
    /**
1274
     * Obtenez les données de corps de requête analysées.
1275
     *
1276
     * Si la requête Content-Type est soit application/x-www-form-urlencoded
1277
     * ou multipart/form-data, et la méthode de requête est POST, ce sera le
1278
     * publier des données. Pour les autres types de contenu, il peut s'agir de la requête désérialisée
1279
     * corps.
1280
     *
1281
     * @return array|object|null Les paramètres de corps désérialisés, le cas échéant.
1282
     *                           Il s'agira généralement d'un tableau.
1283
     */
1284
    public function getParsedBody()
1285
    {
1286
        return $this->data;
1287
    }
1288
1289
    /**
1290
     * Mettez à jour le corps analysé et obtenez une nouvelle instance.
1291
     *
1292
     * @param array|object|null $data Les données de corps désérialisées. Cette volonté
1293
     *                                être généralement dans un tableau ou un objet.
1294
     */
1295
    public function withParsedBody($data): static
1296
    {
1297
        $new       = clone $this;
1298
        $new->data = $data;
1299
1300
        return $new;
1301
    }
1302
1303
    /**
1304
     * Récupère la version du protocole HTTP sous forme de chaîne.
1305
     *
1306
     * @return string Version du protocole HTTP.
1307
     */
1308
    public function getProtocolVersion(): string
1309
    {
1310
        if ($this->protocol) {
1311
            return $this->protocol;
1312
        }
1313
1314
        // Remplissez paresseusement ces données car elles ne sont généralement pas utilisées.
1315
        preg_match('/^HTTP\/([\d.]+)$/', (string) $this->getEnv('SERVER_PROTOCOL'), $match);
1316
        $protocol = '1.1';
1317
        if (isset($match[1])) {
1318
            $protocol = $match[1];
1319
        }
1320
        $this->protocol = $protocol;
1321
1322
        return $this->protocol;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->protocol could return the type null which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
1323
    }
1324
1325
    /**
1326
     * Renvoie une instance avec la version de protocole HTTP spécifiée.
1327
     *
1328
     * La chaîne de version DOIT contenir uniquement le numéro de version HTTP (par exemple,
1329
     * "1.1", "1.0").
1330
     *
1331
     * @param string $version Version du protocole HTTP
1332
     */
1333
    public function withProtocolVersion(string $version): static
1334
    {
1335
        if (! preg_match('/^(1\.[01]|2(\.[0])?)$/', $version)) {
1336
            throw new InvalidArgumentException(sprintf('Version de protocole `%s` non prise en charge fournie.', $version));
1337
        }
1338
        $new           = clone $this;
1339
        $new->protocol = $version;
1340
1341
        return $new;
1342
    }
1343
1344
    /**
1345
     * Obtenez une valeur à partir des données d'environnement de la demande.
1346
     * Se replier sur env() si la clé n'est pas définie dans la propriété $environment.
1347
     *
1348
     * @param string      $key     La clé à partir de laquelle vous voulez lire.
1349
     * @param string|null $default Valeur par défaut lors de la tentative de récupération d'un environnement
1350
     *                             valeur de la variable qui n'existe pas.
1351
     *
1352
     * @return string|null Soit la valeur de l'environnement, soit null si la valeur n'existe pas.
1353
     */
1354
    public function getEnv(string $key, ?string $default = null): ?string
1355
    {
1356 10
        $key = strtoupper($key);
1357
        if (! array_key_exists($key, $this->_environment) || null === $this->_environment[$key]) {
1358 2
            $this->_environment[$key] = env($key);
1359
        }
1360
1361 10
        return $this->_environment[$key] !== null ? (string) $this->_environment[$key] : $default;
1362
    }
1363
1364
    /**
1365
     * Mettez à jour la demande avec un nouvel élément de données d'environnement.
1366
     *
1367
     * Renvoie un objet de requête mis à jour. Cette méthode retourne
1368
     * un *nouvel* objet de requête et ne mute pas la requête sur place.
1369
     */
1370
    public function withEnv(string $key, string $value): static
1371
    {
1372
        $new                     = clone $this;
1373
        $new->_environment[$key] = $value;
1374
        $new->clearDetectorCache();
1375
1376
        return $new;
1377
    }
1378
1379
    /**
1380
     * Autoriser uniquement certaines méthodes de requête HTTP, si la méthode de requête ne correspond pas
1381
     * une erreur 405 s'affichera et l'en-tête de réponse "Autoriser" requis sera défini.
1382
     *
1383
     * Exemple:
1384
     *
1385
     * $this->request->allowMethod('post');
1386
     * ou alors
1387
     * $this->request->allowMethod(['post', 'delete']);
1388
     *
1389
     * Si la requête est GET, l'en-tête de réponse "Autoriser : POST, SUPPRIMER" sera défini
1390
     * et une erreur 405 sera renvoyée.
1391
     *
1392
     * @param string|string[] $methods Méthodes de requête HTTP autorisées.
1393
     *
1394
     * @throws HttpException
1395
     */
1396
    public function allowMethod($methods): bool
1397
    {
1398
        $methods = (array) $methods;
1399
1400
        foreach ($methods as $method) {
1401
            if ($this->is($method)) {
1402
                return true;
1403
            }
1404
        }
1405
        $allowed = strtoupper(implode(', ', $methods));
1406
1407
        throw HttpException::methodNotAllowed($allowed);
1408
    }
1409
1410
    /**
1411
     * Mettez à jour la demande avec un nouvel élément de données de demande.
1412
     *
1413
     * Renvoie un objet de requête mis à jour. Cette méthode retourne
1414
     * un *nouvel* objet de requête et ne mute pas la requête sur place.
1415
     *
1416
     * Utilisez `withParsedBody()` si vous devez remplacer toutes les données de la requête.
1417
     *
1418
     * @param string $name Le chemin séparé par des points où insérer $value.
1419
     */
1420
    public function withData(string $name, mixed $value): static
1421
    {
1422
        $copy = clone $this;
1423
1424
        if (is_array($copy->data)) {
1425
            $copy->data = Arr::insert($copy->data, $name, $value);
1426
        }
1427
1428
        return $copy;
1429
    }
1430
1431
    /**
1432
     * Mettre à jour la demande en supprimant un élément de données.
1433
     *
1434
     * Renvoie un objet de requête mis à jour. Cette méthode retourne
1435
     * un *nouvel* objet de requête et ne mute pas la requête sur place.
1436
     *
1437
     * @param string $name Le chemin séparé par des points à supprimer.
1438
     */
1439
    public function withoutData(string $name): static
1440
    {
1441
        $copy = clone $this;
1442
1443
        if (is_array($copy->data)) {
1444
            $copy->data = Arr::remove($copy->data, $name);
1445
        }
1446
1447
        return $copy;
1448
    }
1449
1450
    /**
1451
     * Mettre à jour la requête avec un nouveau paramètre de routage
1452
     *
1453
     * Renvoie un objet de requête mis à jour. Cette méthode retourne
1454
     * un *nouvel* objet de requête et ne mute pas la requête sur place.
1455
     *
1456
     * @param string $name Le chemin séparé par des points où insérer $value.
1457
     */
1458
    public function withParam(string $name, mixed $value): static
1459
    {
1460
        $copy         = clone $this;
1461
        $copy->params = Arr::insert($copy->params, $name, $value);
1462
1463
        return $copy;
1464
    }
1465
1466
    /**
1467
     * Accédez en toute sécurité aux valeurs dans $this->params.
1468
     */
1469
    public function getParam(string $name, mixed $default = null)
1470
    {
1471
        return Arr::get($this->params, $name, $default);
1472
    }
1473
1474
    /**
1475
     * Renvoie une instance avec l'attribut de requête spécifié.
1476
     *
1477
     * @param string $name  Le nom de l'attribut.
1478
     * @param mixed  $value La valeur de l'attribut.
1479
     */
1480
    public function withAttribute(string $name, mixed $value): static
1481
    {
1482
        $new = clone $this;
1483
        if (in_array($name, $this->emulatedAttributes, true)) {
1484
            $new->{$name} = $value;
1485
        } else {
1486
            $new->attributes[$name] = $value;
1487
        }
1488
1489
        return $new;
1490
    }
1491
1492
    /**
1493
     * Renvoie une instance sans l'attribut de requête spécifié.
1494
     *
1495
     * @param string $name Le nom de l'attribut.
1496
     *
1497
     * @throws InvalidArgumentException
1498
     */
1499
    public function withoutAttribute(string $name): static
1500
    {
1501
        $new = clone $this;
1502
        if (in_array($name, $this->emulatedAttributes, true)) {
1503
            throw new InvalidArgumentException(
1504
                "Vous ne pouvez pas supprimer '{$name}'. C'est un attribut BlitzPHP obligatoire."
1505
            );
1506
        }
1507
        unset($new->attributes[$name]);
1508
1509
        return $new;
1510
    }
1511
1512
    /**
1513
     * Tentatives d'obtenir de vieilles données d'entrée qui a été flashé à la session avec redirect_with_input().
1514
     * Il vérifie d'abord les données dans les anciennes données POST, puis les anciennes données GET et enfin vérifier les tableaux de points
1515
     *
1516
     * @return array|string|null
1517
     */
1518
    public function getOldInput(string $key)
1519
    {
1520
        return $this->session()->getOldInput($key);
1521
    }
1522
1523
    /**
1524
     * Lire un attribut de la requête ou obtenir la valeur par défaut
1525
     *
1526
     * @param string $name    Le nom de l'attribut.
1527
     * @param mixed  $default La valeur par défaut si l'attribut n'a pas été défini.
1528
     */
1529
    public function getAttribute(string $name, mixed $default = null): mixed
1530
    {
1531
        if (in_array($name, $this->emulatedAttributes, true)) {
1532
            if ($name === 'here') {
1533
                return $this->base . $this->uri->getPath();
1534
            }
1535
1536
            return $this->{$name};
1537
        }
1538
        if (array_key_exists($name, $this->attributes)) {
1539 4
            return $this->attributes[$name];
1540
        }
1541
1542 4
        return $default;
1543
    }
1544
1545
    /**
1546
     * Obtenez tous les attributs de la requête.
1547
     *
1548
     * Cela inclura les attributs params, webroot, base et here fournis par BlitzPHP.
1549
     */
1550
    public function getAttributes(): array
1551
    {
1552
        $emulated = [
1553
            'params'  => $this->params,
1554
            'webroot' => $this->webroot,
1555
            'base'    => $this->base,
1556
            'here'    => $this->base . $this->uri->getPath(),
1557
        ];
1558
1559
        return $this->attributes + $emulated;
1560
    }
1561
1562
    /**
1563
     * Obtenez le fichier téléchargé à partir d'un chemin en pointillés.
1564
     *
1565
     * @param string $path Le chemin séparé par des points vers le fichier que vous voulez.
1566
     *
1567
     * @return UploadedFileInterface|UploadedFileInterface[]|null
1568
     */
1569
    public function getUploadedFile(string $path)
1570
    {
1571
        $file = Arr::get($this->uploadedFiles, $path);
1572
        if (is_array($file)) {
1573
            foreach ($file as $f) {
1574
                if (! ($f instanceof UploadedFile)) {
1575
                    return null;
1576
                }
1577
            }
1578
1579
            return $file;
1580
        }
1581
1582
        if (! ($file instanceof UploadedFileInterface)) {
1583
            return null;
1584
        }
1585
1586
        return $file;
1587
    }
1588
1589
    /**
1590
     * Obtenez le tableau des fichiers téléchargés à partir de la requête.
1591
     */
1592
    public function getUploadedFiles(): array
1593
    {
1594
        return $this->uploadedFiles;
1595
    }
1596
1597
    /**
1598
     * Mettez à jour la demande en remplaçant les fichiers et en créant une nouvelle instance.
1599
     *
1600
     * @param array $uploadedFiles Un tableau d'objets de fichiers téléchargés.
1601
     *
1602
     * @throws InvalidArgumentException lorsque $files contient un objet invalide.
1603
     */
1604
    public function withUploadedFiles(array $uploadedFiles): static
1605
    {
1606
        $this->validateUploadedFiles($uploadedFiles, '');
1607
        $new                = clone $this;
1608
        $new->uploadedFiles = $uploadedFiles;
1609
1610
        return $new;
1611
    }
1612
1613
    /**
1614
     * Validez de manière récursive les données de fichier téléchargées.
1615
     *
1616
     * @param array  $uploadedFiles Le nouveau tableau de fichiers à valider.
1617
     * @param string $path          Le chemin jusqu'ici.
1618
     *
1619
     * @throws InvalidArgumentException Si des éléments feuilles ne sont pas des fichiers valides.
1620
     */
1621
    protected function validateUploadedFiles(array $uploadedFiles, string $path): void
1622
    {
1623
        foreach ($uploadedFiles as $key => $file) {
1624
            if (is_array($file)) {
1625
                $this->validateUploadedFiles($file, $key . '.');
1626
1627
                continue;
1628
            }
1629
1630
            if (! $file instanceof UploadedFileInterface) {
1631
                throw new InvalidArgumentException("Fichier invalide à '{$path}{$key}'");
1632
            }
1633
        }
1634
    }
1635
1636
    /**
1637
     * Obtient le corps du message.
1638
     */
1639
    public function getBody(): StreamInterface
1640
    {
1641
        return $this->stream;
1642
    }
1643
1644
    /**
1645
     * Renvoie une instance avec le corps de message spécifié.
1646
     */
1647
    public function withBody(StreamInterface $body): static
1648
    {
1649
        $new         = clone $this;
1650
        $new->stream = $body;
1651
1652
        return $new;
1653
    }
1654
1655
    /**
1656
     * Récupère l'instance d'URI.
1657
     */
1658
    public function getUri(): UriInterface
1659
    {
1660
        return $this->uri;
1661
    }
1662
1663
    /**
1664
     * Renvoie une instance avec l'uri spécifié
1665
     *
1666
     * *Attention* Remplacer l'Uri ne mettra pas à jour la `base`, `webroot`,
1667
     * et les attributs `url`.
1668
     *
1669
     * @param bool $preserveHost Indique si l'hôte doit être conservé.
1670
     */
1671
    public function withUri(UriInterface $uri, bool $preserveHost = false): static
1672
    {
1673
        $new      = clone $this;
1674
        $new->uri = $uri;
1675
1676
        if ($preserveHost && $this->hasHeader('Host')) {
1677
            return $new;
1678
        }
1679
1680
        $host = $uri->getHost();
1681
        if (! $host) {
1682
            return $new;
1683
        }
1684
        $port = $uri->getPort();
1685
        if ($port) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $port of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
1686
            $host .= ':' . $port;
1687
        }
1688
        $new->_environment['HTTP_HOST'] = $host;
1689
1690
        return $new;
1691
    }
1692
1693
    /**
1694
     * Créez une nouvelle instance avec une cible de demande spécifique.
1695
     *
1696
     * Vous pouvez utiliser cette méthode pour écraser la cible de la demande qui est
1697
     * déduit de l'Uri de la requête. Cela vous permet également de modifier la demande
1698
     * la forme de la cible en une forme absolue, une forme d'autorité ou une forme d'astérisque
1699
     *
1700
     * @see https://tools.ietf.org/html/rfc7230#section-2.7 (pour les différentes formes de demande-cible autorisées dans les messages de demande)
1701
     *
1702
     * @param string $requestTarget La cible de la requête.
1703
     *
1704
     * @psalm-suppress MoreSpecificImplementedParamType
1705
     */
1706
    public function withRequestTarget(string $requestTarget): static
1707
    {
1708
        $new                = clone $this;
1709
        $new->requestTarget = $requestTarget;
1710
1711
        return $new;
1712
    }
1713
1714
    /**
1715
     * Récupère la cible de la requête.
1716
     *
1717
     * Récupère la cible de la demande du message soit telle qu'elle a été demandée,
1718
     * ou comme défini avec `withRequestTarget()`. Par défaut, cela renverra le
1719
     * chemin relatif de l'application sans répertoire de base et la chaîne de requête
1720
     * défini dans l'environnement SERVER.
1721
     */
1722
    public function getRequestTarget(): string
1723
    {
1724
        if ($this->requestTarget !== null) {
1725
            return $this->requestTarget;
1726
        }
1727
1728
        $target = $this->uri->getPath();
1729
        if ($this->uri->getQuery()) {
1730
            $target .= '?' . $this->uri->getQuery();
1731
        }
1732
1733
        if (empty($target)) {
1734
            $target = '/';
1735
        }
1736
1737
        return $target;
1738
    }
1739
1740
    /**
1741
     * Récupère le chemin de la requête en cours.
1742
     */
1743
    public function getPath(): string
1744
    {
1745
        if ($this->requestTarget === null) {
1746
            return $this->uri->getPath();
1747
        }
1748
1749
        [$path] = explode('?', $this->requestTarget);
1750
1751
        return $path;
1752
    }
1753
1754
    /**
1755
     * Fournit un moyen pratique de travailler avec la classe Negotiate
1756
     * pour la négociation de contenu.
1757
     */
1758
    public function negotiate(string $type, array $supported, bool $strictMatch = false): string
1759
    {
1760
        if (null === $this->negotiator) {
1761
            $this->negotiator = Services::negotiator($this, true);
1762
        }
1763
1764
        switch (strtolower($type)) {
1765
            case 'media':
1766
                return $this->negotiator->media($supported, $strictMatch);
1767
1768
            case 'charset':
1769
                return $this->negotiator->charset($supported);
1770
1771
            case 'encoding':
1772
                return $this->negotiator->encoding($supported);
1773
1774
            case 'language':
1775
                return $this->negotiator->language($supported);
1776
        }
1777
1778
        throw new HttpException($type . ' is not a valid negotiation type. Must be one of: media, charset, encoding, language.');
1779
    }
1780
1781
    /**
1782
     * Définit la chaîne locale pour cette requête.
1783
     */
1784
    public function withLocale(string $locale): static
1785
    {
1786
        $validLocales = config('app.supported_locales');
1787
        // S'il ne s'agit pas d'un paramètre régional valide, définissez-le
1788
        // aux paramètres régionaux par défaut du site.
1789
        if (! in_array($locale, $validLocales, true)) {
0 ignored issues
show
Bug introduced by
$validLocales of type BlitzPHP\Config\Config|null is incompatible with the type array expected by parameter $haystack of in_array(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1789
        if (! in_array($locale, /** @scrutinizer ignore-type */ $validLocales, true)) {
Loading history...
1790
            $locale = config('app.language');
1791
        }
1792
1793
        Services::translator()->setLocale($locale);
0 ignored issues
show
Bug introduced by
It seems like $locale can also be of type BlitzPHP\Config\Config; however, parameter $locale of BlitzPHP\Translator\Translate::setLocale() does only seem to accept null|string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1793
        Services::translator()->setLocale(/** @scrutinizer ignore-type */ $locale);
Loading history...
1794
1795
        return $this->withAttribute('locale', $locale);
1796
    }
1797
1798
    /**
1799
     * Obtient les paramètres régionaux actuels, avec un retour à la valeur par défaut
1800
     * locale si aucune n'est définie.
1801
     */
1802
    public function getLocale(): string
1803
    {
1804 4
        $locale = $this->getAttribute('locale');
1805
        if (empty($locale)) {
1806 4
            $locale = $this->getAttribute('lang');
1807
        }
1808
1809 4
        return $locale ?? Services::translator()->getLocale();
1810
    }
1811
1812
    /**
1813
     * Read data from `php://input`. Useful when interacting with XML or JSON
1814
     * request body content.
1815
     *
1816
     * Getting input with a decoding function:
1817
     *
1818
     * ```
1819
     * $this->request->input('json_decode');
1820
     * ```
1821
     *
1822
     * Getting input using a decoding function, and additional params:
1823
     *
1824
     * ```
1825
     * $this->request->input('Xml::build', ['return' => 'DOMDocument']);
1826
     * ```
1827
     *
1828
     * Any additional parameters are applied to the callback in the order they are given.
1829
     *
1830
     * @param string|null $callback A decoding callback that will convert the string data to another
1831
     *                              representation. Leave empty to access the raw input data. You can also
1832
     *                              supply additional parameters for the decoding callback using var args, see above.
1833
     * @param array       ...$args  The additional arguments
1834
     *
1835
     * @return string The decoded/processed request data.
1836
     */
1837
    public function input($callback = null, ...$args): string
1838
    {
1839
        $this->stream->rewind();
1840
        $input = $this->stream->getContents();
1841
        if ($callback) {
1842
            array_unshift($args, $input);
1843
1844
            return $callback(...$args);
1845
        }
1846
1847
        return $input;
1848
    }
1849
1850
    /**
1851
     * Sets the REQUEST_METHOD environment variable based on the simulated _method
1852
     * HTTP override value. The 'ORIGINAL_REQUEST_METHOD' is also preserved, if you
1853
     * want the read the non-simulated HTTP method the client used.
1854
     *
1855
     * @param array $data Array of post data.
1856
     *
1857
     * @return array
1858
     */
1859
    protected function _processPost(array $data)
1860
    {
1861
        $method   = $this->getEnv('REQUEST_METHOD');
1862
        $override = false;
1863
1864
        if ($_POST) {
1865
            $data = $_POST;
1866
        } elseif (
1867
            in_array($method, ['PUT', 'DELETE', 'PATCH'], true)
1868
            && str_starts_with($this->contentType() ?? '', 'application/x-www-form-urlencoded')
1869
        ) {
1870
            $data = $this->input();
1871
            parse_str($data, $data);
0 ignored issues
show
Bug introduced by
$data of type string is incompatible with the type array expected by parameter $result of parse_str(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1871
            parse_str($data, /** @scrutinizer ignore-type */ $data);
Loading history...
1872
        }
1873
        if (ini_get('magic_quotes_gpc') === '1') {
1874
            $data = Helpers::stripslashesDeep((array) $this->data);
1875
        }
1876
1877
        if ($this->hasHeader('X-Http-Method-Override')) {
1878
            $data['_method'] = $this->getHeaderLine('X-Http-Method-Override');
1879
            $override        = true;
1880
        }
1881
        $this->_environment['ORIGINAL_REQUEST_METHOD'] = $method;
1882
1883
        if (isset($data['_method'])) {
1884
            $this->_environment['REQUEST_METHOD'] = $data['_method'];
1885
            unset($data['_method']);
1886
            $override = true;
1887
        }
1888
1889
        if ($override && ! in_array($this->_environment['REQUEST_METHOD'], ['PUT', 'POST', 'DELETE', 'PATCH'], true)) {
1890
            $data = [];
1891
        }
1892
1893
        return $data;
1894
    }
1895
1896
    /**
1897
     * Process the GET parameters and move things into the object.
1898
     *
1899
     * @param array  $query       The array to which the parsed keys/values are being added.
1900
     * @param string $queryString A query string from the URL if provided
1901
     *
1902
     * @return array An array containing the parsed query string as keys/values.
1903
     */
1904
    protected function _processGet($query, $queryString = '')
0 ignored issues
show
Unused Code introduced by
The parameter $queryString is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

1904
    protected function _processGet($query, /** @scrutinizer ignore-unused */ $queryString = '')

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1905
    {
1906
        if (ini_get('magic_quotes_gpc') === '1') {
1907
            $q = Helpers::stripslashesDeep($_GET);
1908
        } else {
1909
            $q = $_GET;
1910
        }
1911
        $query = array_merge($q, $query);
1912
        $url   = (string) $this->uri;
1913
1914
        $unsetUrl = '/' . str_replace(['.', ' '], '_', urldecode($url));
1915
        unset($query[$unsetUrl], $query[$this->base . $unsetUrl]);
1916
1917
        if (str_contains($url, '?')) {
1918
            [, $querystr] = explode('?', $url);
1919
            parse_str($querystr, $queryArgs);
1920
            $query += $queryArgs;
1921
        }
1922
        if (isset($this->params['url'])) {
1923
            $query = array_merge($this->params['url'], $query);
1924
        }
1925
1926
        return $query;
1927
    }
1928
1929
    /**
1930
     * Process uploaded files and move things onto the post data.
1931
     *
1932
     * @param array $post  Post data to merge files onto.
1933
     * @param array $files Uploaded files to merge in.
1934
     *
1935
     * @return array merged post + file data.
1936
     */
1937
    protected function _processFiles(array $post, array $files): array
1938
    {
1939
        if (! is_array($files)) {
0 ignored issues
show
introduced by
The condition is_array($files) is always true.
Loading history...
1940
            return $post;
1941
        }
1942
1943
        $fileData = [];
1944
1945
        foreach ($files as $key => $value) {
1946
            if ($value instanceof UploadedFileInterface) {
1947
                $fileData[$key] = $value;
1948
1949
                continue;
1950
            }
1951
1952
            if (is_array($value) && isset($value['tmp_name'])) {
1953
                $fileData[$key] = $this->_createUploadedFile($value);
1954
1955
                continue;
1956
            }
1957
1958
            throw new InvalidArgumentException(sprintf(
1959
                'Invalid value in FILES "%s"',
1960
                json_encode($value)
1961
            ));
1962
        }
1963
1964
        $this->uploadedFiles = $fileData;
1965
1966
        // Make a flat map that can be inserted into $post for BC.
1967
        $fileMap = Arr::flatten($fileData);
1968
1969
        foreach ($fileMap as $key => $file) {
1970
            $error   = $file->getError();
1971
            $tmpName = '';
1972
1973
            if ($error === UPLOAD_ERR_OK) {
1974
                $tmpName = $file->getStream()->getMetadata('uri');
1975
            }
1976
1977
            $post = Arr::insert($post, $key, [
1978
                'tmp_name' => $tmpName,
1979
                'error'    => $error,
1980
                'name'     => $file->getClientFilename(),
1981
                'type'     => $file->getClientMediaType(),
1982
                'size'     => $file->getSize(),
1983
            ]);
1984
        }
1985
1986
        return $post;
1987
    }
1988
1989
    /**
1990
     * Create an UploadedFile instance from a $_FILES array.
1991
     *
1992
     * If the value represents an array of values, this method will
1993
     * recursively process the data.
1994
     *
1995
     * @param array $value $_FILES struct
1996
     *
1997
     * @return UploadedFile|UploadedFile[]
1998
     */
1999
    protected function _createUploadedFile(array $value)
2000
    {
2001
        if (is_array($value['tmp_name'])) {
2002
            return $this->_normalizeNestedFiles($value);
2003
        }
2004
2005
        return new UploadedFile(
2006
            $value['tmp_name'],
2007
            $value['size'],
2008
            $value['error'],
2009
            $value['name'],
2010
            $value['type']
2011
        );
2012
    }
2013
2014
    /**
2015
     * Normalize an array of file specifications.
2016
     *
2017
     * Loops through all nested files and returns a normalized array of
2018
     * UploadedFileInterface instances.
2019
     *
2020
     * @param array $files The file data to normalize & convert.
2021
     *
2022
     * @return UploadedFile[]
2023
     */
2024
    protected function _normalizeNestedFiles(array $files = []): array
2025
    {
2026
        $normalizedFiles = [];
2027
2028
        foreach (array_keys($files['tmp_name']) as $key) {
2029
            $spec = [
2030
                'tmp_name' => $files['tmp_name'][$key],
2031
                'size'     => $files['size'][$key],
2032
                'error'    => $files['error'][$key],
2033
                'name'     => $files['name'][$key],
2034
                'type'     => $files['type'][$key],
2035
            ];
2036
2037
            $normalizedFiles[$key] = $this->_createUploadedFile($spec);
2038
        }
2039
2040
        return $normalizedFiles;
2041
    }
2042
}
2043