ci4

Форк
0
/
ResponseTrait.php 
751 строка · 20.8 Кб
1
<?php
2

3
declare(strict_types=1);
4

5
/**
6
 * This file is part of CodeIgniter 4 framework.
7
 *
8
 * (c) CodeIgniter Foundation <admin@codeigniter.com>
9
 *
10
 * For the full copyright and license information, please view
11
 * the LICENSE file that was distributed with this source code.
12
 */
13

14
namespace CodeIgniter\HTTP;
15

16
use CodeIgniter\Cookie\Cookie;
17
use CodeIgniter\Cookie\CookieStore;
18
use CodeIgniter\Cookie\Exceptions\CookieException;
19
use CodeIgniter\HTTP\Exceptions\HTTPException;
20
use CodeIgniter\I18n\Time;
21
use CodeIgniter\Pager\PagerInterface;
22
use CodeIgniter\Security\Exceptions\SecurityException;
23
use Config\Cookie as CookieConfig;
24
use DateTime;
25
use DateTimeZone;
26
use InvalidArgumentException;
27

28
/**
29
 * Response Trait
30
 *
31
 * Additional methods to make a PSR-7 Response class
32
 * compliant with the framework's own ResponseInterface.
33
 *
34
 * @see https://github.com/php-fig/http-message/blob/master/src/ResponseInterface.php
35
 */
36
trait ResponseTrait
37
{
38
    /**
39
     * Content security policy handler
40
     *
41
     * @var ContentSecurityPolicy
42
     */
43
    protected $CSP;
44

45
    /**
46
     * CookieStore instance.
47
     *
48
     * @var CookieStore
49
     */
50
    protected $cookieStore;
51

52
    /**
53
     * Type of format the body is in.
54
     * Valid: html, json, xml
55
     *
56
     * @var string
57
     */
58
    protected $bodyFormat = 'html';
59

60
    /**
61
     * Return an instance with the specified status code and, optionally, reason phrase.
62
     *
63
     * If no reason phrase is specified, will default recommended reason phrase for
64
     * the response's status code.
65
     *
66
     * @see http://tools.ietf.org/html/rfc7231#section-6
67
     * @see http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
68
     *
69
     * @param int    $code   The 3-digit integer result code to set.
70
     * @param string $reason The reason phrase to use with the
71
     *                       provided status code; if none is provided, will
72
     *                       default to the IANA name.
73
     *
74
     * @return $this
75
     *
76
     * @throws HTTPException For invalid status code arguments.
77
     */
78
    public function setStatusCode(int $code, string $reason = '')
79
    {
80
        // Valid range?
81
        if ($code < 100 || $code > 599) {
82
            throw HTTPException::forInvalidStatusCode($code);
83
        }
84

85
        // Unknown and no message?
86
        if (! array_key_exists($code, static::$statusCodes) && ($reason === '')) {
87
            throw HTTPException::forUnkownStatusCode($code);
88
        }
89

90
        $this->statusCode = $code;
91

92
        $this->reason = ($reason !== '') ? $reason : static::$statusCodes[$code];
93

94
        return $this;
95
    }
96

97
    // --------------------------------------------------------------------
98
    // Convenience Methods
99
    // --------------------------------------------------------------------
100

101
    /**
102
     * Sets the date header
103
     *
104
     * @return $this
105
     */
106
    public function setDate(DateTime $date)
107
    {
108
        $date->setTimezone(new DateTimeZone('UTC'));
109

110
        $this->setHeader('Date', $date->format('D, d M Y H:i:s') . ' GMT');
111

112
        return $this;
113
    }
114

115
    /**
116
     * Set the Link Header
117
     *
118
     * @see http://tools.ietf.org/html/rfc5988
119
     *
120
     * @return $this
121
     *
122
     * @todo Recommend moving to Pager
123
     */
124
    public function setLink(PagerInterface $pager)
125
    {
126
        $links = '';
127

128
        if ($previous = $pager->getPreviousPageURI()) {
129
            $links .= '<' . $pager->getPageURI($pager->getFirstPage()) . '>; rel="first",';
130
            $links .= '<' . $previous . '>; rel="prev"';
131
        }
132

133
        if (($next = $pager->getNextPageURI()) && $previous) {
134
            $links .= ',';
135
        }
136

137
        if ($next) {
138
            $links .= '<' . $next . '>; rel="next",';
139
            $links .= '<' . $pager->getPageURI($pager->getLastPage()) . '>; rel="last"';
140
        }
141

142
        $this->setHeader('Link', $links);
143

144
        return $this;
145
    }
146

147
    /**
148
     * Sets the Content Type header for this response with the mime type
149
     * and, optionally, the charset.
150
     *
151
     * @return $this
152
     */
153
    public function setContentType(string $mime, string $charset = 'UTF-8')
154
    {
155
        // add charset attribute if not already there and provided as parm
156
        if ((strpos($mime, 'charset=') < 1) && ($charset !== '')) {
157
            $mime .= '; charset=' . $charset;
158
        }
159

160
        $this->removeHeader('Content-Type'); // replace existing content type
161
        $this->setHeader('Content-Type', $mime);
162

163
        return $this;
164
    }
165

166
    /**
167
     * Converts the $body into JSON and sets the Content Type header.
168
     *
169
     * @param array|object|string $body
170
     *
171
     * @return $this
172
     */
173
    public function setJSON($body, bool $unencoded = false)
174
    {
175
        $this->body = $this->formatBody($body, 'json' . ($unencoded ? '-unencoded' : ''));
176

177
        return $this;
178
    }
179

180
    /**
181
     * Returns the current body, converted to JSON is it isn't already.
182
     *
183
     * @return string|null
184
     *
185
     * @throws InvalidArgumentException If the body property is not array.
186
     */
187
    public function getJSON()
188
    {
189
        $body = $this->body;
190

191
        if ($this->bodyFormat !== 'json') {
192
            $body = service('format')->getFormatter('application/json')->format($body);
193
        }
194

195
        return $body ?: null;
196
    }
197

198
    /**
199
     * Converts $body into XML, and sets the correct Content-Type.
200
     *
201
     * @param array|string $body
202
     *
203
     * @return $this
204
     */
205
    public function setXML($body)
206
    {
207
        $this->body = $this->formatBody($body, 'xml');
208

209
        return $this;
210
    }
211

212
    /**
213
     * Retrieves the current body into XML and returns it.
214
     *
215
     * @return bool|string|null
216
     *
217
     * @throws InvalidArgumentException If the body property is not array.
218
     */
219
    public function getXML()
220
    {
221
        $body = $this->body;
222

223
        if ($this->bodyFormat !== 'xml') {
224
            $body = service('format')->getFormatter('application/xml')->format($body);
225
        }
226

227
        return $body;
228
    }
229

230
    /**
231
     * Handles conversion of the data into the appropriate format,
232
     * and sets the correct Content-Type header for our response.
233
     *
234
     * @param array|object|string $body
235
     * @param string              $format Valid: json, xml
236
     *
237
     * @return false|string
238
     *
239
     * @throws InvalidArgumentException If the body property is not string or array.
240
     */
241
    protected function formatBody($body, string $format)
242
    {
243
        $this->bodyFormat = ($format === 'json-unencoded' ? 'json' : $format);
244
        $mime             = "application/{$this->bodyFormat}";
245
        $this->setContentType($mime);
246

247
        // Nothing much to do for a string...
248
        if (! is_string($body) || $format === 'json-unencoded') {
249
            $body = service('format')->getFormatter($mime)->format($body);
250
        }
251

252
        return $body;
253
    }
254

255
    // --------------------------------------------------------------------
256
    // Cache Control Methods
257
    //
258
    // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9
259
    // --------------------------------------------------------------------
260

261
    /**
262
     * Sets the appropriate headers to ensure this response
263
     * is not cached by the browsers.
264
     *
265
     * @return $this
266
     *
267
     * @todo Recommend researching these directives, might need: 'private', 'no-transform', 'no-store', 'must-revalidate'
268
     *
269
     * @see DownloadResponse::noCache()
270
     */
271
    public function noCache()
272
    {
273
        $this->removeHeader('Cache-Control');
274
        $this->setHeader('Cache-Control', ['no-store', 'max-age=0', 'no-cache']);
275

276
        return $this;
277
    }
278

279
    /**
280
     * A shortcut method that allows the developer to set all of the
281
     * cache-control headers in one method call.
282
     *
283
     * The options array is used to provide the cache-control directives
284
     * for the header. It might look something like:
285
     *
286
     *      $options = [
287
     *          'max-age'  => 300,
288
     *          's-maxage' => 900
289
     *          'etag'     => 'abcde',
290
     *      ];
291
     *
292
     * Typical options are:
293
     *  - etag
294
     *  - last-modified
295
     *  - max-age
296
     *  - s-maxage
297
     *  - private
298
     *  - public
299
     *  - must-revalidate
300
     *  - proxy-revalidate
301
     *  - no-transform
302
     *
303
     * @return $this
304
     */
305
    public function setCache(array $options = [])
306
    {
307
        if ($options === []) {
308
            return $this;
309
        }
310

311
        $this->removeHeader('Cache-Control');
312
        $this->removeHeader('ETag');
313

314
        // ETag
315
        if (isset($options['etag'])) {
316
            $this->setHeader('ETag', $options['etag']);
317
            unset($options['etag']);
318
        }
319

320
        // Last Modified
321
        if (isset($options['last-modified'])) {
322
            $this->setLastModified($options['last-modified']);
323

324
            unset($options['last-modified']);
325
        }
326

327
        $this->setHeader('Cache-Control', $options);
328

329
        return $this;
330
    }
331

332
    /**
333
     * Sets the Last-Modified date header.
334
     *
335
     * $date can be either a string representation of the date or,
336
     * preferably, an instance of DateTime.
337
     *
338
     * @param DateTime|string $date
339
     *
340
     * @return $this
341
     */
342
    public function setLastModified($date)
343
    {
344
        if ($date instanceof DateTime) {
345
            $date->setTimezone(new DateTimeZone('UTC'));
346
            $this->setHeader('Last-Modified', $date->format('D, d M Y H:i:s') . ' GMT');
347
        } elseif (is_string($date)) {
348
            $this->setHeader('Last-Modified', $date);
349
        }
350

351
        return $this;
352
    }
353

354
    // --------------------------------------------------------------------
355
    // Output Methods
356
    // --------------------------------------------------------------------
357

358
    /**
359
     * Sends the output to the browser.
360
     *
361
     * @return $this
362
     */
363
    public function send()
364
    {
365
        // If we're enforcing a Content Security Policy,
366
        // we need to give it a chance to build out it's headers.
367
        if ($this->CSP->enabled()) {
368
            $this->CSP->finalize($this);
369
        } else {
370
            $this->body = str_replace(['{csp-style-nonce}', '{csp-script-nonce}'], '', $this->body ?? '');
371
        }
372

373
        $this->sendHeaders();
374
        $this->sendCookies();
375
        $this->sendBody();
376

377
        return $this;
378
    }
379

380
    /**
381
     * Sends the headers of this HTTP response to the browser.
382
     *
383
     * @return $this
384
     */
385
    public function sendHeaders()
386
    {
387
        // Have the headers already been sent?
388
        if ($this->pretend || headers_sent()) {
389
            return $this;
390
        }
391

392
        // Per spec, MUST be sent with each request, if possible.
393
        // http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html
394
        if (! isset($this->headers['Date']) && PHP_SAPI !== 'cli-server') {
395
            $this->setDate(DateTime::createFromFormat('U', (string) Time::now()->getTimestamp()));
396
        }
397

398
        // HTTP Status
399
        header(sprintf('HTTP/%s %s %s', $this->getProtocolVersion(), $this->getStatusCode(), $this->getReasonPhrase()), true, $this->getStatusCode());
400

401
        // Send all of our headers
402
        foreach ($this->headers() as $name => $value) {
403
            if ($value instanceof Header) {
404
                header(
405
                    $name . ': ' . $value->getValueLine(),
406
                    false,
407
                    $this->getStatusCode()
408
                );
409
            } else {
410
                foreach ($value as $header) {
411
                    header(
412
                        $name . ': ' . $header->getValueLine(),
413
                        false,
414
                        $this->getStatusCode()
415
                    );
416
                }
417
            }
418
        }
419

420
        return $this;
421
    }
422

423
    /**
424
     * Sends the Body of the message to the browser.
425
     *
426
     * @return $this
427
     */
428
    public function sendBody()
429
    {
430
        echo $this->body;
431

432
        return $this;
433
    }
434

435
    /**
436
     * Perform a redirect to a new URL, in two flavors: header or location.
437
     *
438
     * @param string   $uri  The URI to redirect to
439
     * @param int|null $code The type of redirection, defaults to 302
440
     *
441
     * @return $this
442
     *
443
     * @throws HTTPException For invalid status code.
444
     */
445
    public function redirect(string $uri, string $method = 'auto', ?int $code = null)
446
    {
447
        // IIS environment likely? Use 'refresh' for better compatibility
448
        if (
449
            $method === 'auto'
450
            && isset($_SERVER['SERVER_SOFTWARE'])
451
            && str_contains($_SERVER['SERVER_SOFTWARE'], 'Microsoft-IIS')
452
        ) {
453
            $method = 'refresh';
454
        } elseif ($method !== 'refresh' && $code === null) {
455
            // override status code for HTTP/1.1 & higher
456
            if (
457
                isset($_SERVER['SERVER_PROTOCOL'], $_SERVER['REQUEST_METHOD'])
458
                && $this->getProtocolVersion() >= 1.1
459
            ) {
460
                if ($_SERVER['REQUEST_METHOD'] === Method::GET) {
461
                    $code = 302;
462
                } elseif (in_array($_SERVER['REQUEST_METHOD'], [Method::POST, Method::PUT, Method::DELETE], true)) {
463
                    // reference: https://en.wikipedia.org/wiki/Post/Redirect/Get
464
                    $code = 303;
465
                } else {
466
                    $code = 307;
467
                }
468
            }
469
        }
470

471
        if ($code === null) {
472
            $code = 302;
473
        }
474

475
        match ($method) {
476
            'refresh' => $this->setHeader('Refresh', '0;url=' . $uri),
477
            default   => $this->setHeader('Location', $uri),
478
        };
479

480
        $this->setStatusCode($code);
481

482
        return $this;
483
    }
484

485
    /**
486
     * Set a cookie
487
     *
488
     * Accepts an arbitrary number of binds (up to 7) or an associative
489
     * array in the first parameter containing all the values.
490
     *
491
     * @param array|Cookie|string $name     Cookie name / array containing binds / Cookie object
492
     * @param string              $value    Cookie value
493
     * @param int                 $expire   Cookie expiration time in seconds
494
     * @param string              $domain   Cookie domain (e.g.: '.yourdomain.com')
495
     * @param string              $path     Cookie path (default: '/')
496
     * @param string              $prefix   Cookie name prefix ('': the default prefix)
497
     * @param bool|null           $secure   Whether to only transfer cookies via SSL
498
     * @param bool|null           $httponly Whether only make the cookie accessible via HTTP (no javascript)
499
     * @param string|null         $samesite
500
     *
501
     * @return $this
502
     */
503
    public function setCookie(
504
        $name,
505
        $value = '',
506
        $expire = 0,
507
        $domain = '',
508
        $path = '/',
509
        $prefix = '',
510
        $secure = null,
511
        $httponly = null,
512
        $samesite = null
513
    ) {
514
        if ($name instanceof Cookie) {
515
            $this->cookieStore = $this->cookieStore->put($name);
516

517
            return $this;
518
        }
519

520
        $cookieConfig = config(CookieConfig::class);
521

522
        $secure ??= $cookieConfig->secure;
523
        $httponly ??= $cookieConfig->httponly;
524
        $samesite ??= $cookieConfig->samesite;
525

526
        if (is_array($name)) {
527
            // always leave 'name' in last place, as the loop will break otherwise, due to ${$item}
528
            foreach (['samesite', 'value', 'expire', 'domain', 'path', 'prefix', 'secure', 'httponly', 'name'] as $item) {
529
                if (isset($name[$item])) {
530
                    ${$item} = $name[$item];
531
                }
532
            }
533
        }
534

535
        if (is_numeric($expire)) {
536
            $expire = $expire > 0 ? Time::now()->getTimestamp() + $expire : 0;
537
        }
538

539
        $cookie = new Cookie($name, $value, [
540
            'expires'  => $expire ?: 0,
541
            'domain'   => $domain,
542
            'path'     => $path,
543
            'prefix'   => $prefix,
544
            'secure'   => $secure,
545
            'httponly' => $httponly,
546
            'samesite' => $samesite ?? '',
547
        ]);
548

549
        $this->cookieStore = $this->cookieStore->put($cookie);
550

551
        return $this;
552
    }
553

554
    /**
555
     * Returns the `CookieStore` instance.
556
     *
557
     * @return CookieStore
558
     */
559
    public function getCookieStore()
560
    {
561
        return $this->cookieStore;
562
    }
563

564
    /**
565
     * Checks to see if the Response has a specified cookie or not.
566
     */
567
    public function hasCookie(string $name, ?string $value = null, string $prefix = ''): bool
568
    {
569
        $prefix = $prefix ?: Cookie::setDefaults()['prefix']; // to retain BC
570

571
        return $this->cookieStore->has($name, $prefix, $value);
572
    }
573

574
    /**
575
     * Returns the cookie
576
     *
577
     * @param string $prefix Cookie prefix.
578
     *                       '': the default prefix
579
     *
580
     * @return array<string, Cookie>|Cookie|null
581
     */
582
    public function getCookie(?string $name = null, string $prefix = '')
583
    {
584
        if ((string) $name === '') {
585
            return $this->cookieStore->display();
586
        }
587

588
        try {
589
            $prefix = $prefix ?: Cookie::setDefaults()['prefix']; // to retain BC
590

591
            return $this->cookieStore->get($name, $prefix);
592
        } catch (CookieException $e) {
593
            log_message('error', (string) $e);
594

595
            return null;
596
        }
597
    }
598

599
    /**
600
     * Sets a cookie to be deleted when the response is sent.
601
     *
602
     * @return $this
603
     */
604
    public function deleteCookie(string $name = '', string $domain = '', string $path = '/', string $prefix = '')
605
    {
606
        if ($name === '') {
607
            return $this;
608
        }
609

610
        $prefix = $prefix ?: Cookie::setDefaults()['prefix']; // to retain BC
611

612
        $prefixed = $prefix . $name;
613
        $store    = $this->cookieStore;
614
        $found    = false;
615

616
        /** @var Cookie $cookie */
617
        foreach ($store as $cookie) {
618
            if ($cookie->getPrefixedName() === $prefixed) {
619
                if ($domain !== $cookie->getDomain()) {
620
                    continue;
621
                }
622

623
                if ($path !== $cookie->getPath()) {
624
                    continue;
625
                }
626

627
                $cookie = $cookie->withValue('')->withExpired();
628
                $found  = true;
629

630
                $this->cookieStore = $store->put($cookie);
631
                break;
632
            }
633
        }
634

635
        if (! $found) {
636
            $this->setCookie($name, '', 0, $domain, $path, $prefix);
637
        }
638

639
        return $this;
640
    }
641

642
    /**
643
     * Returns all cookies currently set.
644
     *
645
     * @return array<string, Cookie>
646
     */
647
    public function getCookies()
648
    {
649
        return $this->cookieStore->display();
650
    }
651

652
    /**
653
     * Actually sets the cookies.
654
     *
655
     * @return void
656
     */
657
    protected function sendCookies()
658
    {
659
        if ($this->pretend) {
660
            return;
661
        }
662

663
        $this->dispatchCookies();
664
    }
665

666
    private function dispatchCookies(): void
667
    {
668
        /** @var IncomingRequest $request */
669
        $request = service('request');
670

671
        foreach ($this->cookieStore->display() as $cookie) {
672
            if ($cookie->isSecure() && ! $request->isSecure()) {
673
                throw SecurityException::forInsecureCookie();
674
            }
675

676
            $name    = $cookie->getPrefixedName();
677
            $value   = $cookie->getValue();
678
            $options = $cookie->getOptions();
679

680
            if ($cookie->isRaw()) {
681
                $this->doSetRawCookie($name, $value, $options);
682
            } else {
683
                $this->doSetCookie($name, $value, $options);
684
            }
685
        }
686

687
        $this->cookieStore->clear();
688
    }
689

690
    /**
691
     * Extracted call to `setrawcookie()` in order to run unit tests on it.
692
     *
693
     * @codeCoverageIgnore
694
     */
695
    private function doSetRawCookie(string $name, string $value, array $options): void
696
    {
697
        setrawcookie($name, $value, $options);
698
    }
699

700
    /**
701
     * Extracted call to `setcookie()` in order to run unit tests on it.
702
     *
703
     * @codeCoverageIgnore
704
     */
705
    private function doSetCookie(string $name, string $value, array $options): void
706
    {
707
        setcookie($name, $value, $options);
708
    }
709

710
    /**
711
     * Force a download.
712
     *
713
     * Generates the headers that force a download to happen. And
714
     * sends the file to the browser.
715
     *
716
     * @param string      $filename The name you want the downloaded file to be named
717
     *                              or the path to the file to send
718
     * @param string|null $data     The data to be downloaded. Set null if the $filename is the file path
719
     * @param bool        $setMime  Whether to try and send the actual MIME type
720
     *
721
     * @return DownloadResponse|null
722
     */
723
    public function download(string $filename = '', $data = '', bool $setMime = false)
724
    {
725
        if ($filename === '' || $data === '') {
726
            return null;
727
        }
728

729
        $filepath = '';
730
        if ($data === null) {
731
            $filepath = $filename;
732
            $filename = explode('/', str_replace(DIRECTORY_SEPARATOR, '/', $filename));
733
            $filename = end($filename);
734
        }
735

736
        $response = new DownloadResponse($filename, $setMime);
737

738
        if ($filepath !== '') {
739
            $response->setFilePath($filepath);
740
        } elseif ($data !== null) {
741
            $response->setBinary($data);
742
        }
743

744
        return $response;
745
    }
746

747
    public function getCSP(): ContentSecurityPolicy
748
    {
749
        return $this->CSP;
750
    }
751
}
752

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.