Passed
Push — main ( cd5116...99c066 )
by Dimitri
12:52
created

ServerRequest::withAddedHeader()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
cc 2
eloc 8
c 0
b 0
f 0
nc 2
nop 2
dl 0
loc 12
ccs 0
cts 7
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 7
        ];
215
216 7
        $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 7
            $config['session'] = Services::session(false);
228
        }
229
230
        if (empty($config['environment']['REQUEST_METHOD'])) {
231 7
            $config['environment']['REQUEST_METHOD'] = $_SERVER['REQUEST_METHOD'] ?? 'GET';
232
        }
233
234 7
        $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 7
                $config = $this->processUrlOption($config);
244
            }
245 7
            $uri = Psr7ServerRequest::getUriFromGlobals();
246
        }
247
248
        if (in_array($uri->getHost(), ['localhost', '127.0.0.1'], true)) {
249 7
            $uri = $uri->withHost(parse_url(config('app.base_url'), PHP_URL_HOST));
0 ignored issues
show
Bug introduced by
It seems like config('app.base_url') can also be of type BlitzPHP\Config\Config; however, parameter $url of parse_url() 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

249
            $uri = $uri->withHost(parse_url(/** @scrutinizer ignore-type */ config('app.base_url'), PHP_URL_HOST));
Loading history...
250
        }
251
252 7
        $this->_environment = $config['environment'];
253
254 7
        $this->uri     = $uri;
255 7
        $this->base    = $config['base'];
256 7
        $this->webroot = $config['webroot'];
257
258
        if (isset($config['input'])) {
259 7
            $stream = new Stream(\GuzzleHttp\Psr7\Utils::tryFopen('php://memory', 'rw'));
260
            $stream->write($config['input']);
261
            $stream->rewind();
262
        } else {
263 7
            $stream = new Stream(\GuzzleHttp\Psr7\Utils::tryFopen('php://input', 'r'));
264
        }
265 7
        $this->stream = $stream;
266
267 7
        $config['post'] = $this->_processPost($config['post']);
268 7
        $this->data     = $this->_processFiles($config['post'], $config['files']);
269 7
        $this->query    = $config['query'];
270 7
        $this->params   = $config['params'];
271 7
        $this->session  = $config['session'];
272
    }
273
274
    /**
275
     * Définissez les variables d'environnement en fonction de l'option `url` pour faciliter la génération d'instance UriInterface.
276
     *
277
     * L'option `query` est également mise à jour en fonction de la chaîne de requête de l'URL.
278
     */
279
    protected function processUrlOption(array $config): array
280
    {
281
        if ($config['url'][0] !== '/') {
282
            $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

282
            $config['url'] = '/' . /** @scrutinizer ignore-type */ $config['url'];
Loading history...
283
        }
284
285
        if (str_contains($config['url'], '?')) {
286
            [$config['url'], $config['environment']['QUERY_STRING']] = explode('?', $config['url']);
287
288
            parse_str($config['environment']['QUERY_STRING'], $queryArgs);
289
            $config['query'] += $queryArgs;
290
        }
291
292
        $config['environment']['REQUEST_URI'] = $config['url'];
293
294
        return $config;
295
    }
296
297
    /**
298
     * Obtenez le type de contenu utilisé dans cette requête.
299
     */
300
    public function contentType(): ?string
301
    {
302
        return $this->getEnv('CONTENT_TYPE') ?: $this->getEnv('HTTP_CONTENT_TYPE');
303
    }
304
305
    /**
306
     * Renvoie l'instance de l'objet Session pour cette requête
307
     */
308
    public function session(): Store
309
    {
310 10
        return $this->session;
311
    }
312
313
    /**
314
     * Obtenez l'adresse IP que le client utilise ou dit qu'il utilise.
315
     */
316
    public function clientIp(): string
317
    {
318
        if ($this->trustProxy && $this->getEnv('HTTP_X_FORWARDED_FOR')) {
319
            $addresses = array_map('trim', explode(',', (string) $this->getEnv('HTTP_X_FORWARDED_FOR')));
320
            $trusted   = (count($this->trustedProxies) > 0);
321
            $n         = count($addresses);
322
323
            if ($trusted) {
324
                $trusted = array_diff($addresses, $this->trustedProxies);
325
                $trusted = (count($trusted) === 1);
326
            }
327
328
            if ($trusted) {
329
                return $addresses[0];
330
            }
331
332
            return $addresses[$n - 1];
333
        }
334
335
        if ($this->trustProxy && $this->getEnv('HTTP_X_REAL_IP')) {
336
            $ipaddr = $this->getEnv('HTTP_X_REAL_IP');
337
        } elseif ($this->trustProxy && $this->getEnv('HTTP_CLIENT_IP')) {
338
            $ipaddr = $this->getEnv('HTTP_CLIENT_IP');
339
        } else {
340
            $ipaddr = $this->getEnv('REMOTE_ADDR');
341
        }
342
343
        return trim((string) $ipaddr);
344
    }
345
346
    /**
347
     * Enregistrer des proxys de confiance
348
     *
349
     * @param string[] $proxies ips liste des proxys de confiance
350
     */
351
    public function setTrustedProxies(array $proxies): void
352
    {
353
        $this->trustedProxies = $proxies;
354
        $this->trustProxy     = true;
355
        $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

355
        $this->uri            = $this->uri->withScheme(/** @scrutinizer ignore-type */ $this->scheme());
Loading history...
356
    }
357
358
    /**
359
     * Obtenez les proxys de confiance
360
     */
361
    public function getTrustedProxies(): array
362
    {
363
        return $this->trustedProxies;
364
    }
365
366
    /**
367
     * Renvoie le référent qui a référé cette requête.
368
     *
369
     * @param bool $local Tentative de renvoi d'une adresse locale.
370
     *                    Les adresses locales ne contiennent pas de noms d'hôtes..
371
     */
372
    public function referer(bool $local = true): ?string
373
    {
374
        $ref = $this->getEnv('HTTP_REFERER');
375
376
        $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

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

1812
        if (! in_array($locale, /** @scrutinizer ignore-type */ $validLocales, true)) {
Loading history...
1813
            $locale = config('app.language');
1814
        }
1815
1816
        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

1816
        Services::translator()->setLocale(/** @scrutinizer ignore-type */ $locale);
Loading history...
1817
1818
        return $this->withAttribute('locale', $locale);
1819
    }
1820
1821
    /**
1822
     * Obtient les paramètres régionaux actuels, avec un retour à la valeur par défaut
1823
     * locale si aucune n'est définie.
1824
     */
1825
    public function getLocale(): string
1826
    {
1827 6
        $locale = $this->getAttribute('locale');
1828
        if (empty($locale)) {
1829 6
            $locale = $this->getAttribute('lang');
1830
        }
1831
1832 6
        return $locale ?? Services::translator()->getLocale();
1833
    }
1834
1835
    /**
1836
     * Read data from `php://input`. Useful when interacting with XML or JSON
1837
     * request body content.
1838
     *
1839
     * Getting input with a decoding function:
1840
     *
1841
     * ```
1842
     * $this->request->input('json_decode');
1843
     * ```
1844
     *
1845
     * Getting input using a decoding function, and additional params:
1846
     *
1847
     * ```
1848
     * $this->request->input('Xml::build', ['return' => 'DOMDocument']);
1849
     * ```
1850
     *
1851
     * Any additional parameters are applied to the callback in the order they are given.
1852
     *
1853
     * @param string|null $callback A decoding callback that will convert the string data to another
1854
     *                              representation. Leave empty to access the raw input data. You can also
1855
     *                              supply additional parameters for the decoding callback using var args, see above.
1856
     * @param array       ...$args  The additional arguments
1857
     *
1858
     * @return string The decoded/processed request data.
1859
     */
1860
    public function input($callback = null, ...$args): string
1861
    {
1862
        $this->stream->rewind();
1863
        $input = $this->stream->getContents();
1864
        if ($callback) {
1865
            array_unshift($args, $input);
1866
1867
            return $callback(...$args);
1868
        }
1869
1870
        return $input;
1871
    }
1872
1873
    /**
1874
     * Sets the REQUEST_METHOD environment variable based on the simulated _method
1875
     * HTTP override value. The 'ORIGINAL_REQUEST_METHOD' is also preserved, if you
1876
     * want the read the non-simulated HTTP method the client used.
1877
     *
1878
     * @param array $data Array of post data.
1879
     *
1880
     * @return array
1881
     */
1882
    protected function _processPost(array $data)
1883
    {
1884 7
        $method   = $this->getEnv('REQUEST_METHOD');
1885 7
        $override = false;
1886
1887
        if ($_POST) {
1888 2
            $data = $_POST;
1889
        } elseif (
1890
            in_array($method, ['PUT', 'DELETE', 'PATCH'], true)
1891
            && str_starts_with($this->contentType() ?? '', 'application/x-www-form-urlencoded')
1892
        ) {
1893
            $data = $this->input();
1894
            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

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

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