You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
818 lines
22 KiB
818 lines
22 KiB
<?php |
|
|
|
/** |
|
* This file is part of CodeIgniter 4 framework. |
|
* |
|
* (c) CodeIgniter Foundation <admin@codeigniter.com> |
|
* |
|
* For the full copyright and license information, please view |
|
* the LICENSE file that was distributed with this source code. |
|
*/ |
|
|
|
namespace CodeIgniter\HTTP; |
|
|
|
use CodeIgniter\Cookie\Cookie; |
|
use CodeIgniter\Cookie\CookieStore; |
|
use CodeIgniter\Cookie\Exceptions\CookieException; |
|
use CodeIgniter\HTTP\Exceptions\HTTPException; |
|
use CodeIgniter\I18n\Time; |
|
use CodeIgniter\Pager\PagerInterface; |
|
use CodeIgniter\Security\Exceptions\SecurityException; |
|
use Config\Cookie as CookieConfig; |
|
use Config\Services; |
|
use DateTime; |
|
use DateTimeZone; |
|
use InvalidArgumentException; |
|
|
|
/** |
|
* Response Trait |
|
* |
|
* Additional methods to make a PSR-7 Response class |
|
* compliant with the framework's own ResponseInterface. |
|
* |
|
* @see https://github.com/php-fig/http-message/blob/master/src/ResponseInterface.php |
|
*/ |
|
trait ResponseTrait |
|
{ |
|
/** |
|
* Whether Content Security Policy is being enforced. |
|
* |
|
* @var bool |
|
* |
|
* @deprecated Use $this->CSP->enabled() instead. |
|
*/ |
|
protected $CSPEnabled = false; |
|
|
|
/** |
|
* Content security policy handler |
|
* |
|
* @var ContentSecurityPolicy |
|
* |
|
* @deprecated Will be protected. Use `getCSP()` instead. |
|
*/ |
|
public $CSP; |
|
|
|
/** |
|
* CookieStore instance. |
|
* |
|
* @var CookieStore |
|
*/ |
|
protected $cookieStore; |
|
|
|
/** |
|
* Set a cookie name prefix if you need to avoid collisions |
|
* |
|
* @var string |
|
* |
|
* @deprecated Use the dedicated Cookie class instead. |
|
*/ |
|
protected $cookiePrefix = ''; |
|
|
|
/** |
|
* Set to .your-domain.com for site-wide cookies |
|
* |
|
* @var string |
|
* |
|
* @deprecated Use the dedicated Cookie class instead. |
|
*/ |
|
protected $cookieDomain = ''; |
|
|
|
/** |
|
* Typically will be a forward slash |
|
* |
|
* @var string |
|
* |
|
* @deprecated Use the dedicated Cookie class instead. |
|
*/ |
|
protected $cookiePath = '/'; |
|
|
|
/** |
|
* Cookie will only be set if a secure HTTPS connection exists. |
|
* |
|
* @var bool |
|
* |
|
* @deprecated Use the dedicated Cookie class instead. |
|
*/ |
|
protected $cookieSecure = false; |
|
|
|
/** |
|
* Cookie will only be accessible via HTTP(S) (no javascript) |
|
* |
|
* @var bool |
|
* |
|
* @deprecated Use the dedicated Cookie class instead. |
|
*/ |
|
protected $cookieHTTPOnly = false; |
|
|
|
/** |
|
* Cookie SameSite setting |
|
* |
|
* @var string |
|
* |
|
* @deprecated Use the dedicated Cookie class instead. |
|
*/ |
|
protected $cookieSameSite = Cookie::SAMESITE_LAX; |
|
|
|
/** |
|
* Stores all cookies that were set in the response. |
|
* |
|
* @var array |
|
* |
|
* @deprecated Use the dedicated Cookie class instead. |
|
*/ |
|
protected $cookies = []; |
|
|
|
/** |
|
* Type of format the body is in. |
|
* Valid: html, json, xml |
|
* |
|
* @var string |
|
*/ |
|
protected $bodyFormat = 'html'; |
|
|
|
/** |
|
* Return an instance with the specified status code and, optionally, reason phrase. |
|
* |
|
* If no reason phrase is specified, will default recommended reason phrase for |
|
* the response's status code. |
|
* |
|
* @see http://tools.ietf.org/html/rfc7231#section-6 |
|
* @see http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml |
|
* |
|
* @param int $code The 3-digit integer result code to set. |
|
* @param string $reason The reason phrase to use with the |
|
* provided status code; if none is provided, will |
|
* default to the IANA name. |
|
* |
|
* @return $this |
|
* |
|
* @throws HTTPException For invalid status code arguments. |
|
*/ |
|
public function setStatusCode(int $code, string $reason = '') |
|
{ |
|
// Valid range? |
|
if ($code < 100 || $code > 599) { |
|
throw HTTPException::forInvalidStatusCode($code); |
|
} |
|
|
|
// Unknown and no message? |
|
if (! array_key_exists($code, static::$statusCodes) && ($reason === '')) { |
|
throw HTTPException::forUnkownStatusCode($code); |
|
} |
|
|
|
$this->statusCode = $code; |
|
|
|
$this->reason = ($reason !== '') ? $reason : static::$statusCodes[$code]; |
|
|
|
return $this; |
|
} |
|
|
|
// -------------------------------------------------------------------- |
|
// Convenience Methods |
|
// -------------------------------------------------------------------- |
|
|
|
/** |
|
* Sets the date header |
|
* |
|
* @return $this |
|
*/ |
|
public function setDate(DateTime $date) |
|
{ |
|
$date->setTimezone(new DateTimeZone('UTC')); |
|
|
|
$this->setHeader('Date', $date->format('D, d M Y H:i:s') . ' GMT'); |
|
|
|
return $this; |
|
} |
|
|
|
/** |
|
* Set the Link Header |
|
* |
|
* @see http://tools.ietf.org/html/rfc5988 |
|
* |
|
* @return $this |
|
* |
|
* @todo Recommend moving to Pager |
|
*/ |
|
public function setLink(PagerInterface $pager) |
|
{ |
|
$links = ''; |
|
|
|
if ($previous = $pager->getPreviousPageURI()) { |
|
$links .= '<' . $pager->getPageURI($pager->getFirstPage()) . '>; rel="first",'; |
|
$links .= '<' . $previous . '>; rel="prev"'; |
|
} |
|
|
|
if (($next = $pager->getNextPageURI()) && $previous) { |
|
$links .= ','; |
|
} |
|
|
|
if ($next) { |
|
$links .= '<' . $next . '>; rel="next",'; |
|
$links .= '<' . $pager->getPageURI($pager->getLastPage()) . '>; rel="last"'; |
|
} |
|
|
|
$this->setHeader('Link', $links); |
|
|
|
return $this; |
|
} |
|
|
|
/** |
|
* Sets the Content Type header for this response with the mime type |
|
* and, optionally, the charset. |
|
* |
|
* @return $this |
|
*/ |
|
public function setContentType(string $mime, string $charset = 'UTF-8') |
|
{ |
|
// add charset attribute if not already there and provided as parm |
|
if ((strpos($mime, 'charset=') < 1) && ($charset !== '')) { |
|
$mime .= '; charset=' . $charset; |
|
} |
|
|
|
$this->removeHeader('Content-Type'); // replace existing content type |
|
$this->setHeader('Content-Type', $mime); |
|
|
|
return $this; |
|
} |
|
|
|
/** |
|
* Converts the $body into JSON and sets the Content Type header. |
|
* |
|
* @param array|object|string $body |
|
* |
|
* @return $this |
|
*/ |
|
public function setJSON($body, bool $unencoded = false) |
|
{ |
|
$this->body = $this->formatBody($body, 'json' . ($unencoded ? '-unencoded' : '')); |
|
|
|
return $this; |
|
} |
|
|
|
/** |
|
* Returns the current body, converted to JSON is it isn't already. |
|
* |
|
* @return string|null |
|
* |
|
* @throws InvalidArgumentException If the body property is not array. |
|
*/ |
|
public function getJSON() |
|
{ |
|
$body = $this->body; |
|
|
|
if ($this->bodyFormat !== 'json') { |
|
$body = Services::format()->getFormatter('application/json')->format($body); |
|
} |
|
|
|
return $body ?: null; |
|
} |
|
|
|
/** |
|
* Converts $body into XML, and sets the correct Content-Type. |
|
* |
|
* @param array|string $body |
|
* |
|
* @return $this |
|
*/ |
|
public function setXML($body) |
|
{ |
|
$this->body = $this->formatBody($body, 'xml'); |
|
|
|
return $this; |
|
} |
|
|
|
/** |
|
* Retrieves the current body into XML and returns it. |
|
* |
|
* @return bool|string|null |
|
* |
|
* @throws InvalidArgumentException If the body property is not array. |
|
*/ |
|
public function getXML() |
|
{ |
|
$body = $this->body; |
|
|
|
if ($this->bodyFormat !== 'xml') { |
|
$body = Services::format()->getFormatter('application/xml')->format($body); |
|
} |
|
|
|
return $body; |
|
} |
|
|
|
/** |
|
* Handles conversion of the data into the appropriate format, |
|
* and sets the correct Content-Type header for our response. |
|
* |
|
* @param array|object|string $body |
|
* @param string $format Valid: json, xml |
|
* |
|
* @return false|string |
|
* |
|
* @throws InvalidArgumentException If the body property is not string or array. |
|
*/ |
|
protected function formatBody($body, string $format) |
|
{ |
|
$this->bodyFormat = ($format === 'json-unencoded' ? 'json' : $format); |
|
$mime = "application/{$this->bodyFormat}"; |
|
$this->setContentType($mime); |
|
|
|
// Nothing much to do for a string... |
|
if (! is_string($body) || $format === 'json-unencoded') { |
|
$body = Services::format()->getFormatter($mime)->format($body); |
|
} |
|
|
|
return $body; |
|
} |
|
|
|
// -------------------------------------------------------------------- |
|
// Cache Control Methods |
|
// |
|
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9 |
|
// -------------------------------------------------------------------- |
|
|
|
/** |
|
* Sets the appropriate headers to ensure this response |
|
* is not cached by the browsers. |
|
* |
|
* @return $this |
|
* |
|
* @todo Recommend researching these directives, might need: 'private', 'no-transform', 'no-store', 'must-revalidate' |
|
* |
|
* @see DownloadResponse::noCache() |
|
*/ |
|
public function noCache() |
|
{ |
|
$this->removeHeader('Cache-Control'); |
|
$this->setHeader('Cache-Control', ['no-store', 'max-age=0', 'no-cache']); |
|
|
|
return $this; |
|
} |
|
|
|
/** |
|
* A shortcut method that allows the developer to set all of the |
|
* cache-control headers in one method call. |
|
* |
|
* The options array is used to provide the cache-control directives |
|
* for the header. It might look something like: |
|
* |
|
* $options = [ |
|
* 'max-age' => 300, |
|
* 's-maxage' => 900 |
|
* 'etag' => 'abcde', |
|
* ]; |
|
* |
|
* Typical options are: |
|
* - etag |
|
* - last-modified |
|
* - max-age |
|
* - s-maxage |
|
* - private |
|
* - public |
|
* - must-revalidate |
|
* - proxy-revalidate |
|
* - no-transform |
|
* |
|
* @return $this |
|
*/ |
|
public function setCache(array $options = []) |
|
{ |
|
if ($options === []) { |
|
return $this; |
|
} |
|
|
|
$this->removeHeader('Cache-Control'); |
|
$this->removeHeader('ETag'); |
|
|
|
// ETag |
|
if (isset($options['etag'])) { |
|
$this->setHeader('ETag', $options['etag']); |
|
unset($options['etag']); |
|
} |
|
|
|
// Last Modified |
|
if (isset($options['last-modified'])) { |
|
$this->setLastModified($options['last-modified']); |
|
|
|
unset($options['last-modified']); |
|
} |
|
|
|
$this->setHeader('Cache-Control', $options); |
|
|
|
return $this; |
|
} |
|
|
|
/** |
|
* Sets the Last-Modified date header. |
|
* |
|
* $date can be either a string representation of the date or, |
|
* preferably, an instance of DateTime. |
|
* |
|
* @param DateTime|string $date |
|
* |
|
* @return $this |
|
*/ |
|
public function setLastModified($date) |
|
{ |
|
if ($date instanceof DateTime) { |
|
$date->setTimezone(new DateTimeZone('UTC')); |
|
$this->setHeader('Last-Modified', $date->format('D, d M Y H:i:s') . ' GMT'); |
|
} elseif (is_string($date)) { |
|
$this->setHeader('Last-Modified', $date); |
|
} |
|
|
|
return $this; |
|
} |
|
|
|
// -------------------------------------------------------------------- |
|
// Output Methods |
|
// -------------------------------------------------------------------- |
|
|
|
/** |
|
* Sends the output to the browser. |
|
* |
|
* @return $this |
|
*/ |
|
public function send() |
|
{ |
|
// If we're enforcing a Content Security Policy, |
|
// we need to give it a chance to build out it's headers. |
|
if ($this->CSP->enabled()) { |
|
$this->CSP->finalize($this); |
|
} else { |
|
$this->body = str_replace(['{csp-style-nonce}', '{csp-script-nonce}'], '', $this->body ?? ''); |
|
} |
|
|
|
$this->sendHeaders(); |
|
$this->sendCookies(); |
|
$this->sendBody(); |
|
|
|
return $this; |
|
} |
|
|
|
/** |
|
* Sends the headers of this HTTP response to the browser. |
|
* |
|
* @return $this |
|
*/ |
|
public function sendHeaders() |
|
{ |
|
// Have the headers already been sent? |
|
if ($this->pretend || headers_sent()) { |
|
return $this; |
|
} |
|
|
|
// Per spec, MUST be sent with each request, if possible. |
|
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html |
|
if (! isset($this->headers['Date']) && PHP_SAPI !== 'cli-server') { |
|
$this->setDate(DateTime::createFromFormat('U', (string) Time::now()->getTimestamp())); |
|
} |
|
|
|
// HTTP Status |
|
header(sprintf('HTTP/%s %s %s', $this->getProtocolVersion(), $this->getStatusCode(), $this->getReasonPhrase()), true, $this->getStatusCode()); |
|
|
|
// Send all of our headers |
|
foreach (array_keys($this->headers()) as $name) { |
|
header($name . ': ' . $this->getHeaderLine($name), false, $this->getStatusCode()); |
|
} |
|
|
|
return $this; |
|
} |
|
|
|
/** |
|
* Sends the Body of the message to the browser. |
|
* |
|
* @return $this |
|
*/ |
|
public function sendBody() |
|
{ |
|
echo $this->body; |
|
|
|
return $this; |
|
} |
|
|
|
/** |
|
* Perform a redirect to a new URL, in two flavors: header or location. |
|
* |
|
* @param string $uri The URI to redirect to |
|
* @param int|null $code The type of redirection, defaults to 302 |
|
* |
|
* @return $this |
|
* |
|
* @throws HTTPException For invalid status code. |
|
*/ |
|
public function redirect(string $uri, string $method = 'auto', ?int $code = null) |
|
{ |
|
// IIS environment likely? Use 'refresh' for better compatibility |
|
if ( |
|
$method === 'auto' |
|
&& isset($_SERVER['SERVER_SOFTWARE']) |
|
&& strpos($_SERVER['SERVER_SOFTWARE'], 'Microsoft-IIS') !== false |
|
) { |
|
$method = 'refresh'; |
|
} elseif ($method !== 'refresh' && $code === null) { |
|
// override status code for HTTP/1.1 & higher |
|
if ( |
|
isset($_SERVER['SERVER_PROTOCOL'], $_SERVER['REQUEST_METHOD']) |
|
&& $this->getProtocolVersion() >= 1.1 |
|
) { |
|
if ($_SERVER['REQUEST_METHOD'] === 'GET') { |
|
$code = 302; |
|
} elseif (in_array($_SERVER['REQUEST_METHOD'], ['POST', 'PUT', 'DELETE'], true)) { |
|
// reference: https://en.wikipedia.org/wiki/Post/Redirect/Get |
|
$code = 303; |
|
} else { |
|
$code = 307; |
|
} |
|
} |
|
} |
|
|
|
if ($code === null) { |
|
$code = 302; |
|
} |
|
|
|
switch ($method) { |
|
case 'refresh': |
|
$this->setHeader('Refresh', '0;url=' . $uri); |
|
break; |
|
|
|
default: |
|
$this->setHeader('Location', $uri); |
|
break; |
|
} |
|
|
|
$this->setStatusCode($code); |
|
|
|
return $this; |
|
} |
|
|
|
/** |
|
* Set a cookie |
|
* |
|
* Accepts an arbitrary number of binds (up to 7) or an associative |
|
* array in the first parameter containing all the values. |
|
* |
|
* @param array|Cookie|string $name Cookie name / array containing binds / Cookie object |
|
* @param string $value Cookie value |
|
* @param string $expire Cookie expiration time in seconds |
|
* @param string $domain Cookie domain (e.g.: '.yourdomain.com') |
|
* @param string $path Cookie path (default: '/') |
|
* @param string $prefix Cookie name prefix ('': the default prefix) |
|
* @param bool|null $secure Whether to only transfer cookies via SSL |
|
* @param bool|null $httponly Whether only make the cookie accessible via HTTP (no javascript) |
|
* @param string|null $samesite |
|
* |
|
* @return $this |
|
*/ |
|
public function setCookie( |
|
$name, |
|
$value = '', |
|
$expire = '', |
|
$domain = '', |
|
$path = '/', |
|
$prefix = '', |
|
$secure = null, |
|
$httponly = null, |
|
$samesite = null |
|
) { |
|
if ($name instanceof Cookie) { |
|
$this->cookieStore = $this->cookieStore->put($name); |
|
|
|
return $this; |
|
} |
|
|
|
/** @var CookieConfig|null $cookieConfig */ |
|
$cookieConfig = config(CookieConfig::class); |
|
|
|
if ($cookieConfig instanceof CookieConfig) { |
|
$secure ??= $cookieConfig->secure; |
|
$httponly ??= $cookieConfig->httponly; |
|
$samesite ??= $cookieConfig->samesite; |
|
} |
|
|
|
if (is_array($name)) { |
|
// always leave 'name' in last place, as the loop will break otherwise, due to ${$item} |
|
foreach (['samesite', 'value', 'expire', 'domain', 'path', 'prefix', 'secure', 'httponly', 'name'] as $item) { |
|
if (isset($name[$item])) { |
|
${$item} = $name[$item]; |
|
} |
|
} |
|
} |
|
|
|
if (is_numeric($expire)) { |
|
$expire = $expire > 0 ? Time::now()->getTimestamp() + $expire : 0; |
|
} |
|
|
|
$cookie = new Cookie($name, $value, [ |
|
'expires' => $expire ?: 0, |
|
'domain' => $domain, |
|
'path' => $path, |
|
'prefix' => $prefix, |
|
'secure' => $secure, |
|
'httponly' => $httponly, |
|
'samesite' => $samesite ?? '', |
|
]); |
|
|
|
$this->cookieStore = $this->cookieStore->put($cookie); |
|
|
|
return $this; |
|
} |
|
|
|
/** |
|
* Returns the `CookieStore` instance. |
|
* |
|
* @return CookieStore |
|
*/ |
|
public function getCookieStore() |
|
{ |
|
return $this->cookieStore; |
|
} |
|
|
|
/** |
|
* Checks to see if the Response has a specified cookie or not. |
|
*/ |
|
public function hasCookie(string $name, ?string $value = null, string $prefix = ''): bool |
|
{ |
|
$prefix = $prefix ?: Cookie::setDefaults()['prefix']; // to retain BC |
|
|
|
return $this->cookieStore->has($name, $prefix, $value); |
|
} |
|
|
|
/** |
|
* Returns the cookie |
|
* |
|
* @param string $prefix Cookie prefix. |
|
* '': the default prefix |
|
* |
|
* @return array<string, Cookie>|Cookie|null |
|
*/ |
|
public function getCookie(?string $name = null, string $prefix = '') |
|
{ |
|
if ((string) $name === '') { |
|
return $this->cookieStore->display(); |
|
} |
|
|
|
try { |
|
$prefix = $prefix ?: Cookie::setDefaults()['prefix']; // to retain BC |
|
|
|
return $this->cookieStore->get($name, $prefix); |
|
} catch (CookieException $e) { |
|
log_message('error', (string) $e); |
|
|
|
return null; |
|
} |
|
} |
|
|
|
/** |
|
* Sets a cookie to be deleted when the response is sent. |
|
* |
|
* @return $this |
|
*/ |
|
public function deleteCookie(string $name = '', string $domain = '', string $path = '/', string $prefix = '') |
|
{ |
|
if ($name === '') { |
|
return $this; |
|
} |
|
|
|
$prefix = $prefix ?: Cookie::setDefaults()['prefix']; // to retain BC |
|
|
|
$prefixed = $prefix . $name; |
|
$store = $this->cookieStore; |
|
$found = false; |
|
|
|
/** @var Cookie $cookie */ |
|
foreach ($store as $cookie) { |
|
if ($cookie->getPrefixedName() === $prefixed) { |
|
if ($domain !== $cookie->getDomain()) { |
|
continue; |
|
} |
|
|
|
if ($path !== $cookie->getPath()) { |
|
continue; |
|
} |
|
|
|
$cookie = $cookie->withValue('')->withExpired(); |
|
$found = true; |
|
|
|
$this->cookieStore = $store->put($cookie); |
|
break; |
|
} |
|
} |
|
|
|
if (! $found) { |
|
$this->setCookie($name, '', '', $domain, $path, $prefix); |
|
} |
|
|
|
return $this; |
|
} |
|
|
|
/** |
|
* Returns all cookies currently set. |
|
* |
|
* @return array<string, Cookie> |
|
*/ |
|
public function getCookies() |
|
{ |
|
return $this->cookieStore->display(); |
|
} |
|
|
|
/** |
|
* Actually sets the cookies. |
|
* |
|
* @return void |
|
*/ |
|
protected function sendCookies() |
|
{ |
|
if ($this->pretend) { |
|
return; |
|
} |
|
|
|
$this->dispatchCookies(); |
|
} |
|
|
|
private function dispatchCookies(): void |
|
{ |
|
/** @var IncomingRequest $request */ |
|
$request = Services::request(); |
|
|
|
foreach ($this->cookieStore->display() as $cookie) { |
|
if ($cookie->isSecure() && ! $request->isSecure()) { |
|
throw SecurityException::forDisallowedAction(); |
|
} |
|
|
|
$name = $cookie->getPrefixedName(); |
|
$value = $cookie->getValue(); |
|
$options = $cookie->getOptions(); |
|
|
|
if ($cookie->isRaw()) { |
|
$this->doSetRawCookie($name, $value, $options); |
|
} else { |
|
$this->doSetCookie($name, $value, $options); |
|
} |
|
} |
|
|
|
$this->cookieStore->clear(); |
|
} |
|
|
|
/** |
|
* Extracted call to `setrawcookie()` in order to run unit tests on it. |
|
* |
|
* @codeCoverageIgnore |
|
*/ |
|
private function doSetRawCookie(string $name, string $value, array $options): void |
|
{ |
|
setrawcookie($name, $value, $options); |
|
} |
|
|
|
/** |
|
* Extracted call to `setcookie()` in order to run unit tests on it. |
|
* |
|
* @codeCoverageIgnore |
|
*/ |
|
private function doSetCookie(string $name, string $value, array $options): void |
|
{ |
|
setcookie($name, $value, $options); |
|
} |
|
|
|
/** |
|
* Force a download. |
|
* |
|
* Generates the headers that force a download to happen. And |
|
* sends the file to the browser. |
|
* |
|
* @param string $filename The name you want the downloaded file to be named |
|
* or the path to the file to send |
|
* @param string|null $data The data to be downloaded. Set null if the $filename is the file path |
|
* @param bool $setMime Whether to try and send the actual MIME type |
|
* |
|
* @return DownloadResponse|null |
|
*/ |
|
public function download(string $filename = '', $data = '', bool $setMime = false) |
|
{ |
|
if ($filename === '' || $data === '') { |
|
return null; |
|
} |
|
|
|
$filepath = ''; |
|
if ($data === null) { |
|
$filepath = $filename; |
|
$filename = explode('/', str_replace(DIRECTORY_SEPARATOR, '/', $filename)); |
|
$filename = end($filename); |
|
} |
|
|
|
$response = new DownloadResponse($filename, $setMime); |
|
|
|
if ($filepath !== '') { |
|
$response->setFilePath($filepath); |
|
} elseif ($data !== null) { |
|
$response->setBinary($data); |
|
} |
|
|
|
return $response; |
|
} |
|
|
|
public function getCSP(): ContentSecurityPolicy |
|
{ |
|
return $this->CSP; |
|
} |
|
}
|
|
|