Files
crm-ms1/v4_ci4/system/HTTP/ContentSecurityPolicy.php
2026-05-27 11:44:10 -04:00

1056 lines
29 KiB
PHP

<?php
declare(strict_types=1);
/**
* 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\Exceptions\InvalidArgumentException;
use Config\App;
use Config\ContentSecurityPolicy as ContentSecurityPolicyConfig;
/**
* Provides tools for working with the Content-Security-Policy header
* to help defeat XSS attacks.
*
* @see http://www.w3.org/TR/CSP/
* @see http://www.html5rocks.com/en/tutorials/security/content-security-policy/
* @see http://content-security-policy.com/
* @see https://www.owasp.org/index.php/Content_Security_Policy
* @see \CodeIgniter\HTTP\ContentSecurityPolicyTest
*/
class ContentSecurityPolicy
{
private const DIRECTIVES_ALLOWING_SOURCE_LISTS = [
'base-uri' => 'baseURI',
'child-src' => 'childSrc',
'connect-src' => 'connectSrc',
'default-src' => 'defaultSrc',
'font-src' => 'fontSrc',
'form-action' => 'formAction',
'frame-ancestors' => 'frameAncestors',
'frame-src' => 'frameSrc',
'img-src' => 'imageSrc',
'media-src' => 'mediaSrc',
'object-src' => 'objectSrc',
'plugin-types' => 'pluginTypes',
'script-src' => 'scriptSrc',
'style-src' => 'styleSrc',
'sandbox' => 'sandbox',
'manifest-src' => 'manifestSrc',
'script-src-elem' => 'scriptSrcElem',
'script-src-attr' => 'scriptSrcAttr',
'style-src-elem' => 'styleSrcElem',
'style-src-attr' => 'styleSrcAttr',
'worker-src' => 'workerSrc',
];
/**
* Map of CSP directives to this class's properties.
*
* @var array<string, string>
*/
protected array $directives = [
...self::DIRECTIVES_ALLOWING_SOURCE_LISTS,
'report-uri' => 'reportURI',
'report-to' => 'reportTo',
];
/**
* The `base-uri` directive restricts the URLs that can be used to specify the document base URL.
*
* @var array<string, bool>|string|null
*/
protected $baseURI = [];
/**
* The `child-src` directive governs the creation of nested browsing contexts as well
* as Worker execution contexts.
*
* @var array<string, bool>|string
*/
protected $childSrc = [];
/**
* The `connect-src` directive restricts which URLs the protected resource can load using script interfaces.
*
* @var array<string, bool>|string
*/
protected $connectSrc = [];
/**
* The `default-src` directive sets a default source list for a number of directives.
*
* @var array<string, bool>|string|null
*/
protected $defaultSrc = [];
/**
* The `font-src` directive restricts from where the protected resource can load fonts.
*
* @var array<string, bool>|string
*/
protected $fontSrc = [];
/**
* The `form-action` directive restricts which URLs can be used as the action of HTML form elements.
*
* @var array<string, bool>|string
*/
protected $formAction = [];
/**
* The `frame-ancestors` directive indicates whether the user agent should allow embedding
* the resource using a `frame`, `iframe`, `object`, `embed` or `applet` element,
* or equivalent functionality in non-HTML resources.
*
* @var array<string, bool>|string
*/
protected $frameAncestors = [];
/**
* The `frame-src` directive restricts the URLs which may be loaded into child navigables.
*
* @var array<string, bool>|string
*/
protected $frameSrc = [];
/**
* The `img-src` directive restricts from where the protected resource can load images.
*
* @var array<string, bool>|string
*/
protected $imageSrc = [];
/**
* The `media-src` directive restricts from where the protected resource can load video,
* audio, and associated text tracks.
*
* @var array<string, bool>|string
*/
protected $mediaSrc = [];
/**
* The `object-src` directive restricts from where the protected resource can load plugins.
*
* @var array<string, bool>|string
*/
protected $objectSrc = [];
/**
* The `plugin-types` directive restricts the set of plugins that can be invoked by the
* protected resource by limiting the types of resources that can be embedded.
*
* @var array<string, bool>|string
*/
protected $pluginTypes = [];
/**
* The `script-src` directive restricts which scripts the protected resource can execute.
*
* @var array<string, bool>|string
*/
protected $scriptSrc = [];
/**
* The `style-src` directive restricts which styles the user may applies to the protected resource.
*
* @var array<string, bool>|string
*/
protected $styleSrc = [];
/**
* The `sandbox` directive specifies an HTML sandbox policy that the user agent applies to the protected resource.
*
* @var array<string, bool>|string
*/
protected $sandbox = [];
/**
* The `report-uri` directive specifies a URL to which the user agent sends reports about policy violation.
*
* @var string|null
*/
protected $reportURI;
/**
* The `report-to` directive specifies a named group in a Reporting API
* endpoint to which the user agent sends reports about policy violation.
*/
protected ?string $reportTo = null;
// --------------------------------------------------------------
// CSP Level 3 Directives
// --------------------------------------------------------------
/**
* The `manifest-src` directive restricts the URLs from which application manifests may be loaded.
*
* @var array<string, bool>|string
*/
protected $manifestSrc = [];
/**
* The `script-src-elem` directive applies to all script requests and script blocks.
*
* @var array<string, bool>|string
*/
protected array|string $scriptSrcElem = [];
/**
* The `script-src-attr` directive applies to event handlers and, if present,
* it will override the `script-src` directive for relevant checks.
*
* @var array<string, bool>|string
*/
protected array|string $scriptSrcAttr = [];
/**
* The `style-src-elem` directive governs the behaviour of styles except
* for styles defined in inline attributes.
*
* @var array<string, bool>|string
*/
protected array|string $styleSrcElem = [];
/**
* The `style-src-attr` directive governs the behaviour of style attributes.
*
* @var array<string, bool>|string
*/
protected array|string $styleSrcAttr = [];
/**
* The `worker-src` directive restricts the URLs which may be loaded as a `Worker`,
* `SharedWorker`, or `ServiceWorker`.
*
* @var array<string, bool>|string
*/
protected array|string $workerSrc = [];
/**
* Instructs user agents to rewrite URL schemes by changing HTTP to HTTPS.
*
* @var bool
*/
protected $upgradeInsecureRequests = false;
/**
* Set to `true` to make all directives report-only instead of enforced.
*
* @var bool
*/
protected $reportOnly = false;
/**
* Set of valid keyword-sources.
*
* @see https://www.w3.org/TR/CSP3/#source-expression
*
* @var list<string>
*/
protected $validSources = [
// CSP2 keywords
'self',
'none',
'unsafe-inline',
'unsafe-eval',
// CSP3 keywords
'strict-dynamic',
'unsafe-hashes',
'report-sample',
'unsafe-allow-redirects',
'wasm-unsafe-eval',
'trusted-types-eval',
'report-sha256',
'report-sha384',
'report-sha512',
];
/**
* Set of nonces generated.
*
* @var list<string>
*
* @deprecated 4.7.0 Never used.
*/
protected $nonces = [];
/**
* Nonce for style tags.
*
* @var string|null
*/
protected $styleNonce;
/**
* Nonce for script tags.
*
* @var string|null
*/
protected $scriptNonce;
/**
* Nonce placeholder for style tags.
*
* @var string
*/
protected $styleNonceTag = '{csp-style-nonce}';
/**
* Nonce placeholder for script tags.
*
* @var string
*/
protected $scriptNonceTag = '{csp-script-nonce}';
/**
* Replace nonce tags automatically?
*
* @var bool
*/
protected $autoNonce = true;
/**
* An array of header info since we have to build
* ourselves before passing to a Response object.
*
* @var array<string, string>
*/
protected $tempHeaders = [];
/**
* An array of header info to build that should only be reported.
*
* @var array<string, string>
*/
protected $reportOnlyHeaders = [];
/**
* Whether Content Security Policy is being enforced.
*
* @var bool
*/
protected $CSPEnabled = false;
/**
* Map of reporting endpoints to their URLs.
*
* @var array<string, string>
*/
private array $reportingEndpoints = [];
/**
* Stores our default values from the Config file.
*/
public function __construct(ContentSecurityPolicyConfig $config)
{
$this->CSPEnabled = config(App::class)->CSPEnabled;
foreach (get_object_vars($config) as $setting => $value) {
if (! property_exists($this, $setting)) {
continue;
}
if (
in_array($setting, self::DIRECTIVES_ALLOWING_SOURCE_LISTS, true)
&& is_array($value)
&& array_is_list($value)
) {
// Config sets these directives as `list<string>|string`
// but we need them as `array<string, bool>` internally.
$this->{$setting} = array_combine($value, array_fill(0, count($value), $this->reportOnly));
continue;
}
$this->{$setting} = $value;
}
if (! is_array($this->styleSrc)) {
$this->styleSrc = [$this->styleSrc => $this->reportOnly];
}
if (! is_array($this->scriptSrc)) {
$this->scriptSrc = [$this->scriptSrc => $this->reportOnly];
}
}
/**
* Whether Content Security Policy is being enforced.
*/
public function enabled(): bool
{
return $this->CSPEnabled;
}
/**
* Get the nonce for the style tag.
*/
public function getStyleNonce(): string
{
if ($this->styleNonce === null) {
$this->styleNonce = base64_encode(random_bytes(12));
$this->addStyleSrc('nonce-' . $this->styleNonce);
}
return $this->styleNonce;
}
/**
* Get the nonce for the script tag.
*/
public function getScriptNonce(): string
{
if ($this->scriptNonce === null) {
$this->scriptNonce = base64_encode(random_bytes(12));
$this->addScriptSrc('nonce-' . $this->scriptNonce);
}
return $this->scriptNonce;
}
/**
* Compiles and sets the appropriate headers in the request.
*
* Should be called just prior to sending the response to the user agent.
*
* @return void
*/
public function finalize(ResponseInterface $response)
{
if ($this->autoNonce) {
$this->generateNonces($response);
}
$this->buildHeaders($response);
}
/**
* If TRUE, nothing will be restricted. Instead all violations will
* be reported to the reportURI for monitoring. This is useful when
* you are just starting to implement the policy, and will help
* determine what errors need to be addressed before you turn on
* all filtering.
*
* @return $this
*/
public function reportOnly(bool $value = true)
{
$this->reportOnly = $value;
return $this;
}
/**
* Adds a new value to the `base-uri` directive.
*
* `base-uri` restricts the URLs that can appear in a page's <base> element.
*
* @see http://www.w3.org/TR/CSP/#directive-base-uri
*
* @param list<string>|string $uri
*
* @return $this
*/
public function addBaseURI($uri, ?bool $explicitReporting = null)
{
$this->addOption($uri, 'baseURI', $explicitReporting ?? $this->reportOnly);
return $this;
}
/**
* Adds a new value to the `child-src` directive.
*
* `child-src` lists the URLs for workers and embedded frame contents.
* For example: child-src https://youtube.com would enable embedding
* videos from YouTube but not from other origins.
*
* @see http://www.w3.org/TR/CSP/#directive-child-src
*
* @param list<string>|string $uri
*
* @return $this
*/
public function addChildSrc($uri, ?bool $explicitReporting = null)
{
$this->addOption($uri, 'childSrc', $explicitReporting ?? $this->reportOnly);
return $this;
}
/**
* Adds a new value to the `connect-src` directive.
*
* `connect-src` limits the origins to which you can connect
* (via XHR, WebSockets, and EventSource).
*
* @see http://www.w3.org/TR/CSP/#directive-connect-src
*
* @param list<string>|string $uri
*
* @return $this
*/
public function addConnectSrc($uri, ?bool $explicitReporting = null)
{
$this->addOption($uri, 'connectSrc', $explicitReporting ?? $this->reportOnly);
return $this;
}
/**
* Adds a new value to the `default-src` directive.
*
* `default-src` is the URI that is used for many of the settings when
* no other source has been set.
*
* @see http://www.w3.org/TR/CSP/#directive-default-src
*
* @param list<string>|string $uri
*
* @return $this
*/
public function setDefaultSrc($uri, ?bool $explicitReporting = null)
{
$this->defaultSrc = [(string) $uri => $explicitReporting ?? $this->reportOnly];
return $this;
}
/**
* Adds a new value to the `font-src` directive.
*
* `font-src` specifies the origins that can serve web fonts.
*
* @see http://www.w3.org/TR/CSP/#directive-font-src
*
* @param list<string>|string $uri
*
* @return $this
*/
public function addFontSrc($uri, ?bool $explicitReporting = null)
{
$this->addOption($uri, 'fontSrc', $explicitReporting ?? $this->reportOnly);
return $this;
}
/**
* Adds a new value to the `form-action` directive.
*
* @see http://www.w3.org/TR/CSP/#directive-form-action
*
* @param list<string>|string $uri
*
* @return $this
*/
public function addFormAction($uri, ?bool $explicitReporting = null)
{
$this->addOption($uri, 'formAction', $explicitReporting ?? $this->reportOnly);
return $this;
}
/**
* Adds a new value to the `frame-ancestors` directive.
*
* @see http://www.w3.org/TR/CSP/#directive-frame-ancestors
*
* @param list<string>|string $uri
*
* @return $this
*/
public function addFrameAncestor($uri, ?bool $explicitReporting = null)
{
$this->addOption($uri, 'frameAncestors', $explicitReporting ?? $this->reportOnly);
return $this;
}
/**
* Adds a new value to the `frame-src` directive.
*
* @see http://www.w3.org/TR/CSP/#directive-frame-src
*
* @param list<string>|string $uri
*
* @return $this
*/
public function addFrameSrc($uri, ?bool $explicitReporting = null)
{
$this->addOption($uri, 'frameSrc', $explicitReporting ?? $this->reportOnly);
return $this;
}
/**
* Adds a new value to the `img-src` directive.
*
* @see http://www.w3.org/TR/CSP/#directive-img-src
*
* @param list<string>|string $uri
*
* @return $this
*/
public function addImageSrc($uri, ?bool $explicitReporting = null)
{
$this->addOption($uri, 'imageSrc', $explicitReporting ?? $this->reportOnly);
return $this;
}
/**
* Adds a new value to the `media-src` directive.
*
* @see http://www.w3.org/TR/CSP/#directive-media-src
*
* @param list<string>|string $uri
*
* @return $this
*/
public function addMediaSrc($uri, ?bool $explicitReporting = null)
{
$this->addOption($uri, 'mediaSrc', $explicitReporting ?? $this->reportOnly);
return $this;
}
/**
* Adds a new value to the `manifest-src` directive.
*
* @see https://www.w3.org/TR/CSP/#directive-manifest-src
*
* @param list<string>|string $uri
*
* @return $this
*/
public function addManifestSrc($uri, ?bool $explicitReporting = null)
{
$this->addOption($uri, 'manifestSrc', $explicitReporting ?? $this->reportOnly);
return $this;
}
/**
* Adds a new value to the `object-src` directive.
*
* @see http://www.w3.org/TR/CSP/#directive-object-src
*
* @param list<string>|string $uri
*
* @return $this
*/
public function addObjectSrc($uri, ?bool $explicitReporting = null)
{
$this->addOption($uri, 'objectSrc', $explicitReporting ?? $this->reportOnly);
return $this;
}
/**
* Adds a new value to the `plugin-types` directive.
*
* @see http://www.w3.org/TR/CSP/#directive-plugin-types
*
* @param list<string>|string $mime
*
* @return $this
*/
public function addPluginType($mime, ?bool $explicitReporting = null)
{
$this->addOption($mime, 'pluginTypes', $explicitReporting ?? $this->reportOnly);
return $this;
}
/**
* Adds a new value to the `sandbox` directive.
*
* @see http://www.w3.org/TR/CSP/#directive-sandbox
*
* @param list<string>|string $flags
*
* @return $this
*/
public function addSandbox($flags, ?bool $explicitReporting = null)
{
$this->addOption($flags, 'sandbox', $explicitReporting ?? $this->reportOnly);
return $this;
}
/**
* Adds a new value to the `script-src` directive.
*
* @see http://www.w3.org/TR/CSP/#directive-script-src
*
* @param list<string>|string $uri
*
* @return $this
*/
public function addScriptSrc($uri, ?bool $explicitReporting = null)
{
$this->addOption($uri, 'scriptSrc', $explicitReporting ?? $this->reportOnly);
return $this;
}
/**
* Adds a new value to the `script-src-elem` directive.
*
* @see https://www.w3.org/TR/CSP/#directive-script-src-elem
*
* @param list<string>|string $uri
*/
public function addScriptSrcElem(array|string $uri, ?bool $explicitReporting = null): static
{
$this->addOption($uri, 'scriptSrcElem', $explicitReporting ?? $this->reportOnly);
return $this;
}
/**
* Adds a new value to the `script-src-attr` directive.
*
* @see https://www.w3.org/TR/CSP/#directive-script-src-attr
*
* @param list<string>|string $uri
*/
public function addScriptSrcAttr(array|string $uri, ?bool $explicitReporting = null): static
{
$this->addOption($uri, 'scriptSrcAttr', $explicitReporting ?? $this->reportOnly);
return $this;
}
/**
* Adds a new value to the `style-src` directive.
*
* @see http://www.w3.org/TR/CSP/#directive-style-src
*
* @param list<string>|string $uri
*
* @return $this
*/
public function addStyleSrc($uri, ?bool $explicitReporting = null)
{
$this->addOption($uri, 'styleSrc', $explicitReporting ?? $this->reportOnly);
return $this;
}
/**
* Adds a new value to the `style-src-elem` directive.
*
* @see https://www.w3.org/TR/CSP/#directive-style-src-elem
*
* @param list<string>|string $uri
*/
public function addStyleSrcElem(array|string $uri, ?bool $explicitReporting = null): static
{
$this->addOption($uri, 'styleSrcElem', $explicitReporting ?? $this->reportOnly);
return $this;
}
/**
* Adds a new value to the `style-src-attr` directive.
*
* @see https://www.w3.org/TR/CSP/#directive-style-src-attr
*
* @param list<string>|string $uri
*/
public function addStyleSrcAttr(array|string $uri, ?bool $explicitReporting = null): static
{
$this->addOption($uri, 'styleSrcAttr', $explicitReporting ?? $this->reportOnly);
return $this;
}
/**
* Adds a new value to the `worker-src` directive.
*
* @see https://www.w3.org/TR/CSP/#directive-worker-src
*
* @param list<string>|string $uri
*/
public function addWorkerSrc($uri, ?bool $explicitReporting = null): static
{
$this->addOption($uri, 'workerSrc', $explicitReporting ?? $this->reportOnly);
return $this;
}
/**
* Sets whether the user agents should rewrite URL schemes, changing HTTP to HTTPS.
*
* @return $this
*/
public function upgradeInsecureRequests(bool $value = true)
{
$this->upgradeInsecureRequests = $value;
return $this;
}
/**
* Specifies a URL where a browser will send reports when a content
* security policy is violated.
*
* @see http://www.w3.org/TR/CSP/#directive-report-uri
*
* @param string $uri URL to send reports. Set `''` if you want to remove
* this directive at runtime.
*
* @return $this
*/
public function setReportURI(string $uri)
{
$this->reportURI = $uri;
return $this;
}
/**
* Specifies a named group in a Reporting API endpoint to which the user
* agent sends reports about policy violation.
*
* @see https://www.w3.org/TR/CSP/#directive-report-to
*
* @param string $endpoint The name of the reporting endpoint. Set `''` if you
* want to remove this directive at runtime.
*/
public function setReportToEndpoint(string $endpoint): static
{
if ($endpoint === '') {
$this->reportURI = null;
$this->reportTo = null;
return $this;
}
if (! array_key_exists($endpoint, $this->reportingEndpoints)) {
throw new InvalidArgumentException(sprintf('The reporting endpoint "%s" has not been defined.', $endpoint));
}
$this->reportURI = $this->reportingEndpoints[$endpoint]; // for BC with browsers that do not support `report-to`
$this->reportTo = $endpoint;
return $this;
}
/**
* Adds reporting endpoints to the `Reporting-Endpoints` header.
*
* @param array<string, string> $endpoint
*/
public function addReportingEndpoints(array $endpoint): static
{
foreach ($endpoint as $name => $url) {
$this->reportingEndpoints[$name] = $url;
}
return $this;
}
/**
* DRY method to add an string or array to a class property.
*
* @param list<string>|string $options
*
* @return void
*/
protected function addOption($options, string $target, ?bool $explicitReporting = null)
{
// Ensure we have an array to work with...
if (is_string($this->{$target})) {
$this->{$target} = [$this->{$target} => $this->reportOnly];
}
$options = is_array($options) ? $options : [$options];
foreach ($options as $option) {
$this->{$target}[$option] = $explicitReporting ?? $this->reportOnly;
}
}
/**
* Scans the body of the request message and replaces any nonce
* placeholders with actual nonces, that we'll then add to our
* headers.
*
* @return void
*/
protected function generateNonces(ResponseInterface $response)
{
$body = (string) $response->getBody();
if ($body === '') {
return;
}
// Replace style and script placeholders with nonces
$pattern = sprintf('/(%s|%s)/', preg_quote($this->styleNonceTag, '/'), preg_quote($this->scriptNonceTag, '/'));
$body = preg_replace_callback($pattern, function ($match): string {
$nonce = $match[0] === $this->styleNonceTag ? $this->getStyleNonce() : $this->getScriptNonce();
return "nonce=\"{$nonce}\"";
}, $body);
$response->setBody($body);
}
/**
* Based on the current state of the elements, will add the appropriate
* Content-Security-Policy and Content-Security-Policy-Report-Only headers
* with their values to the response object.
*
* @return void
*/
protected function buildHeaders(ResponseInterface $response)
{
$response->setHeader('Content-Security-Policy', []);
$response->setHeader('Content-Security-Policy-Report-Only', []);
$response->setHeader('Reporting-Endpoints', []);
if (in_array($this->baseURI, ['', null, []], true)) {
$this->baseURI = 'self';
}
if (in_array($this->defaultSrc, ['', null, []], true)) {
$this->defaultSrc = 'self';
}
foreach ($this->directives as $name => $property) {
if ($name === 'report-uri' && (string) $this->reportURI === '') {
continue;
}
if ($name === 'report-to' && (string) $this->reportTo === '') {
continue;
}
if ($this->{$property} !== null) {
$this->addToHeader($name, $this->{$property});
}
}
// Compile our own header strings here since if we just
// append it to the response, it will be joined with
// commas, not semi-colons as we need.
if ($this->reportingEndpoints !== []) {
$endpoints = [];
foreach ($this->reportingEndpoints as $name => $url) {
$endpoints[] = trim("{$name}=\"{$url}\"");
}
$response->appendHeader('Reporting-Endpoints', implode(', ', $endpoints));
$this->reportingEndpoints = [];
}
if ($this->tempHeaders !== []) {
$header = [];
foreach ($this->tempHeaders as $name => $value) {
$header[] = trim("{$name} {$value}");
}
if ($this->upgradeInsecureRequests) {
$header[] = 'upgrade-insecure-requests';
}
$response->appendHeader('Content-Security-Policy', implode('; ', $header));
$this->tempHeaders = [];
}
if ($this->reportOnlyHeaders !== []) {
$header = [];
foreach ($this->reportOnlyHeaders as $name => $value) {
$header[] = trim("{$name} {$value}");
}
$response->appendHeader('Content-Security-Policy-Report-Only', implode('; ', $header));
$this->reportOnlyHeaders = [];
}
}
/**
* Adds a directive and its options to the appropriate header. The $values
* array might have options that are geared toward either the regular or the
* reportOnly header, since it's viable to have both simultaneously.
*
* @param array<string, bool>|string $values
*
* @return void
*/
protected function addToHeader(string $name, $values = null)
{
if (is_string($values)) {
$values = [$values => $this->reportOnly];
}
$sources = [];
$reportSources = [];
foreach ($values as $value => $reportOnly) {
if (
in_array($value, $this->validSources, true)
|| str_starts_with($value, 'nonce-')
|| str_starts_with($value, 'sha256-')
|| str_starts_with($value, 'sha384-')
|| str_starts_with($value, 'sha512-')
) {
$value = "'{$value}'";
}
if ($reportOnly) {
$reportSources[] = $value;
} else {
$sources[] = $value;
}
}
if ($sources !== []) {
$this->tempHeaders[$name] = implode(' ', $sources);
}
if ($reportSources !== []) {
$this->reportOnlyHeaders[$name] = implode(' ', $reportSources);
}
}
public function clearDirective(string $directive): void
{
if (! array_key_exists($directive, $this->directives)) {
return;
}
if ($directive === 'report-uri') {
$this->reportURI = null;
return;
}
if ($directive === 'report-to') {
$this->reportURI = null;
$this->reportTo = null;
return;
}
$this->{$this->directives[$directive]} = [];
}
}