Passed
Push — main ( 66245a...80ccfb )
by Dimitri
12:45 queued 12s
created

ServerRequest::_processFiles()   B

Complexity

Conditions 8
Paths 11

Size

Total Lines 50
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

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

301
            $config['url'] = '/' . /** @scrutinizer ignore-type */ $config['url'];
Loading history...
302
        }
303
304
        if (strpos($config['url'], '?') !== false) {
305
            [$config['url'], $config['environment']['QUERY_STRING']] = explode('?', $config['url']);
306
307
            parse_str($config['environment']['QUERY_STRING'], $queryArgs);
308
            $config['query'] += $queryArgs;
309
        }
310
311
        $config['environment']['REQUEST_URI'] = $config['url'];
312
313
        return $config;
314
    }
315
316
    /**
317
     * Obtenez le type de contenu utilisé dans cette requête.
318
     */
319
    public function contentType(): ?string
320
    {
321
        $type = $this->getEnv('CONTENT_TYPE');
322
        if ($type) {
323
            return $type;
324
        }
325
326
        return $this->getEnv('HTTP_CONTENT_TYPE');
327
    }
328
329
    /**
330
     * Renvoie l'instance de l'objet Session pour cette requête
331
     */
332
    public function getSession(): Session
333
    {
334
        return $this->session;
335
    }
336
337
    /**
338
     * Obtenez l'adresse IP que le client utilise ou dit qu'il utilise.
339
     */
340
    public function clientIp(): string
341
    {
342
        if ($this->trustProxy && $this->getEnv('HTTP_X_FORWARDED_FOR')) {
343
            $addresses = array_map('trim', explode(',', (string) $this->getEnv('HTTP_X_FORWARDED_FOR')));
344
            $trusted   = (count($this->trustedProxies) > 0);
345
            $n         = count($addresses);
346
347
            if ($trusted) {
348
                $trusted = array_diff($addresses, $this->trustedProxies);
349
                $trusted = (count($trusted) === 1);
350
            }
351
352
            if ($trusted) {
353
                return $addresses[0];
354
            }
355
356
            return $addresses[$n - 1];
357
        }
358
359
        if ($this->trustProxy && $this->getEnv('HTTP_X_REAL_IP')) {
360
            $ipaddr = $this->getEnv('HTTP_X_REAL_IP');
361
        } elseif ($this->trustProxy && $this->getEnv('HTTP_CLIENT_IP')) {
362
            $ipaddr = $this->getEnv('HTTP_CLIENT_IP');
363
        } else {
364
            $ipaddr = $this->getEnv('REMOTE_ADDR');
365
        }
366
367
        return trim((string) $ipaddr);
368
    }
369
370
    /**
371
     * Enregistrer des proxys de confiance
372
     *
373
     * @param string[] $proxies ips liste des proxys de confiance
374
     */
375
    public function setTrustedProxies(array $proxies): void
376
    {
377
        $this->trustedProxies = $proxies;
378
        $this->trustProxy     = true;
379
    }
380
381
    /**
382
     * Obtenez les proxys de confiance
383
     */
384
    public function getTrustedProxies(): array
385
    {
386
        return $this->trustedProxies;
387
    }
388
389
    /**
390
     * Renvoie le référent qui a référé cette requête.
391
     *
392
     * @param bool $local Tentative de renvoi d'une adresse locale.
393
     *                    Les adresses locales ne contiennent pas de noms d'hôtes..
394
     */
395
    public function referer(bool $local = true): ?string
396
    {
397
        $ref = $this->getEnv('HTTP_REFERER');
398
399
        $base = /* Configure::read('App.fullBaseUrl') .  */ $this->webroot;
400
        if (! empty($ref) && ! empty($base)) {
401
            if ($local && strpos($ref, $base) === 0) {
402
                $ref = substr($ref, strlen($base));
403
                if ($ref === '' || strpos($ref, '//') === 0) {
404
                    $ref = '/';
405
                }
406
                if ($ref[0] !== '/') {
407
                    $ref = '/' . $ref;
408
                }
409
410
                return $ref;
411
            }
412
            if (! $local) {
413
                return $ref;
414
            }
415
        }
416
417
        return null;
418
    }
419
420
    /**
421
     * Gestionnaire de méthodes manquant, les poignées enveloppent les anciennes méthodes de type isAjax()
422
     *
423
     * @throws BadMethodCallException lorsqu'une méthode invalide est appelée.
424
     *
425
     * @return bool
426
     */
427
    public function __call(string $name, array $params)
428
    {
429
        if (strpos($name, 'is') === 0) {
430
            $type = strtolower(substr($name, 2));
431
432
            array_unshift($params, $type);
433
434
            return $this->is(...$params);
435
        }
436
437
        throw new BadMethodCallException(sprintf('Method "%s()" does not exist', $name));
438
    }
439
440
    /**
441
     * Vérifiez si une demande est d'un certain type.
442
     *
443
     * Utilise les règles de détection intégrées ainsi que des règles supplémentaires
444
     * défini avec {@link \BlitzPHP\Http\ServerRequest::addDetector()}. Tout détecteur peut être appelé
445
     * comme `is($type)` ou `is$Type()`.
446
     *
447
     * @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.
448
     *
449
     * @return bool Si la demande est du type que vous vérifiez.
450
     */
451
    public function is($type, ...$args): bool
452
    {
453
        if (is_array($type)) {
454
            foreach ($type as $_type) {
455
                if ($this->is($_type)) {
456
                    return true;
457
                }
458
            }
459
460
            return false;
461
        }
462
463
        $type = strtolower($type);
464
        if (! isset(static::$_detectors[$type])) {
465
            return false;
466
        }
467
        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...
468
            return $this->_is($type, $args);
469
        }
470
471
        return $this->_detectorCache[$type] = $this->_detectorCache[$type] ?? $this->_is($type, $args);
472
    }
473
474
    /**
475
     * Efface le cache du détecteur d'instance, utilisé par la fonction is()
476
     */
477
    public function clearDetectorCache(): void
478
    {
479
        $this->_detectorCache = [];
480
    }
481
482
    /**
483
     * Worker pour la fonction publique is()
484
     *
485
     * @param string $type Le type de requête que vous souhaitez vérifier.
486
     * @param array  $args Tableau d'arguments de détecteur personnalisés.
487
     *
488
     * @return bool Si la demande est du type que vous vérifiez.
489
     */
490
    protected function _is(string $type, array $args): bool
491
    {
492
        $detect = static::$_detectors[$type];
493
        if (is_callable($detect)) {
494
            array_unshift($args, $this);
495
496
            return $detect(...$args);
497
        }
498
        if (isset($detect['env']) && $this->_environmentDetector($detect)) {
0 ignored issues
show
Bug introduced by
It seems like $detect can also be of type callable; however, parameter $detect of BlitzPHP\Http\ServerRequ...:_environmentDetector() does only seem to accept array, 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

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

1897
            parse_str($data, /** @scrutinizer ignore-type */ $data);
Loading history...
1898
        }
1899
        if (ini_get('magic_quotes_gpc') === '1') {
1900
            $data = Helpers::stripslashesDeep($this->data);
0 ignored issues
show
Bug introduced by
The method stripslashesDeep() does not exist on BlitzPHP\Utilities\Helpers. ( Ignorable by Annotation )

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

1900
            /** @scrutinizer ignore-call */ 
1901
            $data = Helpers::stripslashesDeep($this->data);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
1901
        }
1902
1903
        if ($this->hasHeader('X-Http-Method-Override')) {
1904
            $data['_method'] = $this->getHeaderLine('X-Http-Method-Override');
1905
            $override        = true;
1906
        }
1907
        $this->_environment['ORIGINAL_REQUEST_METHOD'] = $method;
1908
1909
        if (isset($data['_method'])) {
1910
            $this->_environment['REQUEST_METHOD'] = $data['_method'];
1911
            unset($data['_method']);
1912
            $override = true;
1913
        }
1914
1915
        if ($override && ! in_array($this->_environment['REQUEST_METHOD'], ['PUT', 'POST', 'DELETE', 'PATCH'], true)) {
1916
            $data = [];
1917
        }
1918
1919
        return $data;
1920
    }
1921
1922
    /**
1923
     * Process the GET parameters and move things into the object.
1924
     *
1925
     * @param array  $query       The array to which the parsed keys/values are being added.
1926
     * @param string $queryString A query string from the URL if provided
1927
     *
1928
     * @return array An array containing the parsed query string as keys/values.
1929
     */
1930
    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

1930
    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...
1931
    {
1932
        if (ini_get('magic_quotes_gpc') === '1') {
1933
            $q = Helpers::stripslashesDeep($_GET);
1934
        } else {
1935
            $q = $_GET;
1936
        }
1937
        $query = array_merge($q, $query);
1938
1939
        $unsetUrl = '/' . str_replace(['.', ' '], '_', urldecode($this->url));
0 ignored issues
show
Bug Best Practice introduced by
The property url does not exist on BlitzPHP\Http\ServerRequest. Did you maybe forget to declare it?
Loading history...
1940
        unset($query[$unsetUrl], $query[$this->base . $unsetUrl]);
1941
1942
        if (strpos($this->url, '?') !== false) {
1943
            [, $querystr] = explode('?', $this->url);
1944
            parse_str($querystr, $queryArgs);
1945
            $query += $queryArgs;
1946
        }
1947
        if (isset($this->params['url'])) {
1948
            $query = array_merge($this->params['url'], $query);
1949
        }
1950
1951
        return $query;
1952
    }
1953
1954
    /**
1955
     * Process uploaded files and move things onto the post data.
1956
     *
1957
     * @param array $post  Post data to merge files onto.
1958
     * @param array $files Uploaded files to merge in.
1959
     *
1960
     * @return array merged post + file data.
1961
     */
1962
    protected function _processFiles(array $post, array $files): array
1963
    {
1964
        if (! is_array($files)) {
0 ignored issues
show
introduced by
The condition is_array($files) is always true.
Loading history...
1965
            return $post;
1966
        }
1967
1968
        $fileData = [];
1969
1970
        foreach ($files as $key => $value) {
1971
            if ($value instanceof UploadedFileInterface) {
1972
                $fileData[$key] = $value;
1973
1974
                continue;
1975
            }
1976
1977
            if (is_array($value) && isset($value['tmp_name'])) {
1978
                $fileData[$key] = $this->_createUploadedFile($value);
1979
1980
                continue;
1981
            }
1982
1983
            throw new InvalidArgumentException(sprintf(
1984
                'Invalid value in FILES "%s"',
1985
                json_encode($value)
1986
            ));
1987
        }
1988
1989
        $this->uploadedFiles = $fileData;
1990
1991
        // Make a flat map that can be inserted into $post for BC.
1992
        $fileMap = Arr::flatten($fileData);
1993
1994
        foreach ($fileMap as $key => $file) {
1995
            $error   = $file->getError();
1996
            $tmpName = '';
1997
1998
            if ($error === UPLOAD_ERR_OK) {
1999
                $tmpName = $file->getStream()->getMetadata('uri');
2000
            }
2001
2002
            $post = Arr::insert($post, $key, [
2003
                'tmp_name' => $tmpName,
2004
                'error'    => $error,
2005
                'name'     => $file->getClientFilename(),
2006
                'type'     => $file->getClientMediaType(),
2007
                'size'     => $file->getSize(),
2008
            ]);
2009
        }
2010
2011
        return $post;
2012
    }
2013
2014
    /**
2015
     * Create an UploadedFile instance from a $_FILES array.
2016
     *
2017
     * If the value represents an array of values, this method will
2018
     * recursively process the data.
2019
     *
2020
     * @param array $value $_FILES struct
2021
     *
2022
     * @return array|UploadedFileInterface
2023
     */
2024
    protected function _createUploadedFile(array $value)
2025
    {
2026
        if (is_array($value['tmp_name'])) {
2027
            return $this->_normalizeNestedFiles($value);
2028
        }
2029
2030
        return new UploadedFile(
2031
            $value['tmp_name'],
2032
            $value['size'],
2033
            $value['error'],
2034
            $value['name'],
2035
            $value['type']
2036
        );
2037
    }
2038
2039
    /**
2040
     * Normalize an array of file specifications.
2041
     *
2042
     * Loops through all nested files and returns a normalized array of
2043
     * UploadedFileInterface instances.
2044
     *
2045
     * @param array $files The file data to normalize & convert.
2046
     *
2047
     * @return array An array of UploadedFileInterface objects.
2048
     */
2049
    protected function _normalizeNestedFiles(array $files = []): array
2050
    {
2051
        $normalizedFiles = [];
2052
2053
        foreach (array_keys($files['tmp_name']) as $key) {
2054
            $spec = [
2055
                'tmp_name' => $files['tmp_name'][$key],
2056
                'size'     => $files['size'][$key],
2057
                'error'    => $files['error'][$key],
2058
                'name'     => $files['name'][$key],
2059
                'type'     => $files['type'][$key],
2060
            ];
2061
2062
            $normalizedFiles[$key] = $this->_createUploadedFile($spec);
2063
        }
2064
2065
        return $normalizedFiles;
2066
    }
2067
}
2068