Skip to content
Snippets Groups Projects
Commit 051cc45a authored by Geoff Appleby's avatar Geoff Appleby
Browse files

Issue #3472720 Use config_target in settings form

parent dd900909
No related branches found
No related tags found
1 merge request!49Issue #3472720 Use form config_target
Pipeline #324586 passed
......@@ -200,7 +200,9 @@ csp_directive.trusted-types:
sequence:
type: string
constraints:
Regex: <^[a-z0-9#=_/@.%-]+$>i
Regex:
pattern: <^[a-z0-9#=_/@.%-]+$>i
message: '%value is not a valid trusted types policy name'
csp_directive.require-trusted-types-for:
type: sequence
label: require-trusted-types-for
......@@ -224,9 +226,16 @@ csp_reporting_handler.report-uri-com:
subdomain:
type: string
label: 'Subdomain'
constraints:
NotBlank: []
Regex:
# Custom domains must be 4-30 characters, but generated domains are 32.
pattern: <^([a-z\d]{4,30}|[a-z\d]{32})$>i
message: Subdomain must be 4-30 alphanumeric characters.
wizard:
type: boolean
label: 'Wizard'
requiredKey: false
csp_reporting_handler.uri:
type: mapping
label: 'URI'
......
......@@ -6,12 +6,13 @@ use Drupal\Component\Plugin\Exception\PluginException;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Config\TypedConfigManagerInterface;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\ConfigTarget;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\ToConfig;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\StringTranslation\PluralTranslatableMarkup;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\csp\Csp;
use Drupal\csp\LibraryPolicyBuilder;
use Drupal\csp\Plugin\Validation\Constraint\SourceConstraint;
use Drupal\csp\ReportingHandlerPluginManager;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Validator\ConstraintViolationInterface;
......@@ -154,7 +155,7 @@ class CspSettingsForm extends ConfigFormBase {
* @return string[]
* An array of keywords.
*/
private function getKeywordOptions($directive): array {
private function getKeywordOptions(string $directive): array {
return array_keys(array_filter(
self::$keywordDirectiveMap,
function ($directives) use ($directive) {
......@@ -202,7 +203,10 @@ class CspSettingsForm extends ConfigFormBase {
$form[$policyTypeKey]['enable'] = [
'#type' => 'checkbox',
'#title' => $this->t("Enable '@type'", ['@type' => $policyTypeName]),
'#default_value' => $config->get($policyTypeKey . '.enable') ?: FALSE,
'#config_target' => new ConfigTarget(
'csp.settings',
$policyTypeKey . '.enable',
),
];
$form[$policyTypeKey]['directives'] = [
......@@ -218,6 +222,21 @@ class CspSettingsForm extends ConfigFormBase {
$form[$policyTypeKey]['directives'][$directiveName] = [
'#type' => 'container',
'#access' => $policyTypeKey == 'enforce' || !in_array($directiveName, $enforceOnlyDirectives),
'#config_target' => new ConfigTarget(
'csp.settings',
$policyTypeKey . '.directives.' . $directiveName,
toConfig: match($directiveSchema) {
Csp::DIRECTIVE_SCHEMA_BOOLEAN => self::booleanToConfig(...),
Csp::DIRECTIVE_SCHEMA_TOKEN_LIST,
Csp::DIRECTIVE_SCHEMA_OPTIONAL_TOKEN_LIST => self::tokenListToConfig(...),
Csp::DIRECTIVE_SCHEMA_ALLOW_BLOCK => self::allowBlockToConfig(...),
Csp::DIRECTIVE_SCHEMA_SOURCE_LIST,
Csp::DIRECTIVE_SCHEMA_ANCESTOR_SOURCE_LIST => self::sourceListToConfig(...),
Csp::DIRECTIVE_SCHEMA_TRUSTED_TYPES => self::trustedTypesToConfig(...),
Csp::DIRECTIVE_SCHEMA_TRUSTED_TYPES_SINK_GROUPS => self::sinkGroupsToConfig(...),
default => self::directiveToConfig(...),
},
),
];
$form[$policyTypeKey]['directives'][$directiveName]['enable'] = [
......@@ -322,6 +341,11 @@ class CspSettingsForm extends ConfigFormBase {
'#title' => $this->t('Additional Sources'),
'#description' => $this->t('Additional domains or protocols to allow for this directive, separated by a space.'),
'#default_value' => implode(' ', $config->get($policyTypeKey . '.directives.' . $directiveName . '.sources') ?: []),
'#config_target' => new ConfigTarget(
'csp.settings',
$policyTypeKey . '.directives.' . $directiveName . '.sources',
toConfig: fn() => ToConfig::NoOp,
),
'#states' => [
'visible' => [
[':input[name="' . $policyTypeKey . '[directives][' . $directiveName . '][base]"]' => ['!value' => 'none']],
......@@ -409,6 +433,11 @@ class CspSettingsForm extends ConfigFormBase {
'#parents' => [$policyTypeKey, 'directives', 'trusted-types', 'policy-names'],
'#title' => $this->t('Policy Names'),
'#default_value' => implode(' ', $config->get($policyTypeKey . '.directives.trusted-types.policy-names') ?? []),
'#config_target' => new ConfigTarget(
'csp.settings',
$policyTypeKey . '.directives.trusted-types.policy-names',
toConfig: fn() => ToConfig::NoOp,
),
'#states' => [
'!visible' => [
[
......@@ -439,6 +468,11 @@ class CspSettingsForm extends ConfigFormBase {
'#type' => 'fieldset',
'#title' => $this->t('Reporting'),
'#tree' => TRUE,
'#config_target' => new ConfigTarget(
'csp.settings',
$policyTypeKey . '.reporting',
toConfig: self::reportingToConfig(...),
),
];
$form[$policyTypeKey]['reporting']['handler'] = [
'#type' => 'radios',
......@@ -518,6 +552,12 @@ class CspSettingsForm extends ConfigFormBase {
}
foreach ($directiveNames as $directive) {
if (
!str_starts_with($directive, 'script-src')
&& !str_starts_with($directive, 'style-src')
) {
continue;
}
if (($directiveSources = $config->get($policyTypeKey . '.directives.' . $directive . '.sources'))) {
// '{hashAlgorithm}-{base64-value}'
......@@ -569,60 +609,7 @@ class CspSettingsForm extends ConfigFormBase {
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state): void {
/** @var \Symfony\Component\Validator\Validator\RecursiveValidator $sourceValidator */
$sourceValidator = \Drupal::service('validation.basic_recursive_validator_factory')->createValidator();
foreach (['report-only', 'enforce'] as $policyTypeKey) {
$directiveNames = $this->getConfigurableDirectives();
foreach ($directiveNames as $directiveName) {
if (($directiveSources = $form_state->getValue([$policyTypeKey, 'directives', $directiveName, 'sources']))) {
$validationContext = $sourceValidator->startContext();
$sourcesArray = preg_split('/,?\s+/', $directiveSources);
foreach ($sourcesArray as $source) {
$validationContext->validate($source, new SourceConstraint());
}
if ($validationContext->getViolations()->count()) {
$form_state->setError(
$form[$policyTypeKey]['directives'][$directiveName]['options']['sources'],
new PluralTranslatableMarkup(
$validationContext->getViolations()->count(),
'Invalid source value for %directive: %value',
'Invalid source values for %directive: %value',
[
'%directive' => $directiveName,
'%value' => implode(', ', array_map(
function (ConstraintViolationInterface $violation): string {
return $violation->getInvalidValue();
},
iterator_to_array($validationContext->getViolations())
)),
]
)
);
}
}
}
// Check that trusted types policy names are valid.
// See https://w3c.github.io/trusted-types/dist/spec/#trusted-types-csp-directive.
if (
$form_state->getValue([$policyTypeKey, 'directives', 'trusted-types', 'enable'])
&& $form_state->getValue([$policyTypeKey, 'directives', 'trusted-types', 'base']) === ''
&& ($trusted_type_policy_names = trim($form_state->getValue([$policyTypeKey, 'directives', 'trusted-types', 'policy-names'])))
) {
$trusted_type_policy_names_array = preg_split('/,?\s+/', $trusted_type_policy_names);
$invalid_policy_names = array_filter($trusted_type_policy_names_array, function ($expression) {
return !preg_match('<^[a-z0-9#=_/@.%-]+$>i', $expression);
});
if (!empty($invalid_policy_names)) {
$form_state->setError(
$form[$policyTypeKey]['directives']['trusted-types']['options']['policy-names'],
$this->t('Invalid Trusted Types policy names: "%types".', ['%types' => implode(' ', $invalid_policy_names)])
);
}
}
if (($reportingHandlerPluginId = $form_state->getValue([$policyTypeKey, 'reporting', 'handler']))) {
if ($this->reportingHandlerPluginManager->hasDefinition($reportingHandlerPluginId)) {
$this->reportingHandlerPluginManager->createInstance($reportingHandlerPluginId, ['type' => $policyTypeKey])
......@@ -641,6 +628,62 @@ class CspSettingsForm extends ConfigFormBase {
parent::validateForm($form, $form_state);
}
/**
* {@inheritdoc}
*
* @param string $form_element_name
* The form element for which to format multiple violation messages.
* @param array<ConstraintViolationInterface> $violations
* The list of constraint violations that apply to this form element.
*/
protected function formatMultipleViolationsMessage(string $form_element_name, array $violations): TranslatableMarkup {
/** @var array{("report-only"|"enforce"), string, string, string} $elementKeys */
$elementKeys = explode('][', $form_element_name);
$messageArgs = fn () => [
'%policy' => match($elementKeys[0]) {
'report-only' => $this->t('Report Only'),
'enforce' => $this->t('Enforced'),
},
'%directive' => $elementKeys[2],
'%value' => implode(', ', array_map(
function (ConstraintViolationInterface $violation): string {
return $violation->getInvalidValue();
},
$violations
)),
];
if (str_ends_with($form_element_name, 'sources')) {
return $this->formatPlural(
count($violations),
'Invalid %policy %directive source: %value',
'Invalid %policy %directive sources: %value',
$messageArgs()
);
}
elseif (str_ends_with($form_element_name, 'trusted-types][policy-names')) {
return $this->formatPlural(
count($violations),
'Invalid %policy Trusted Types Policy Name: %value',
'Invalid %policy Trusted Types Policy Names: %value',
$messageArgs()
);
}
// Drupal ^11.0.6 returns MarkupInterface|\Stringable, which need conversion
// on prior versions to respect TranslatableMarkup type hint.
$parent = parent::formatMultipleViolationsMessage($form_element_name, $violations);
// @phpstan-ignore-next-line
if ($parent instanceof TranslatableMarkup) {
return $parent;
}
else {
// phpcs:ignore Drupal.Semantics.FunctionT.NotLiteralString
return $this->t((string) $parent);
}
}
/**
* Submit handler for clear policy buttons.
*
......@@ -658,97 +701,151 @@ class CspSettingsForm extends ConfigFormBase {
}
/**
* {@inheritdoc}
* Convert form state to config array for directive options.
*
* @param array<string, mixed> $value
* The submitted form values.
*
* @return array<string, mixed>|\Drupal\Core\Form\ToConfig
* Directive config array, or a ToConfig enum value.
*/
public function submitForm(array &$form, FormStateInterface $form_state): void {
$config = $this->config('csp.settings');
private static function directiveToConfig(array $value): array|ToConfig {
if (!$value['enable']) {
return ToConfig::DeleteKey;
}
unset($value['enable']);
$directiveNames = $this->getConfigurableDirectives();
foreach (['report-only', 'enforce'] as $policyTypeKey) {
$config->clear($policyTypeKey);
return $value;
}
$policyFormData = $form_state->getValue($policyTypeKey);
/**
* Convert form state to config value for a boolean directive.
*
* @param array<string, mixed> $value
* The submitted form values.
*
* @return bool|\Drupal\Core\Form\ToConfig
* Directive config value, or a ToConfig enum value.
*/
private static function booleanToConfig(array $value): bool|ToConfig {
return !empty($value['enable']) ? TRUE : ToConfig::DeleteKey;
}
$config->set($policyTypeKey . '.enable', !empty($policyFormData['enable']));
/**
* Convert form state to config value for a token list directive.
*
* @param array<string, mixed> $value
* The submitted form values.
*
* @return array<string>|\Drupal\Core\Form\ToConfig
* Directive config array, or a ToConfig enum value.
*/
private static function tokenListToConfig(array $value): array|ToConfig {
if (!$value['enable']) {
return ToConfig::DeleteKey;
}
unset($value['enable']);
foreach ($directiveNames as $directiveName) {
if (empty($policyFormData['directives'][$directiveName])) {
continue;
}
return array_keys(array_filter($value['keys']));
}
$directiveFormData = $policyFormData['directives'][$directiveName];
$directiveOptions = [];
/**
* Convert form state to config value for an allow/block directive.
*
* @param array<string, mixed> $value
* The submitted form values.
*
* @return string|\Drupal\Core\Form\ToConfig
* Directive config array, or a ToConfig enum value.
*/
private static function allowBlockToConfig(array $value): string|ToConfig {
return !empty($value['enable']) ? $value['value'] : ToConfig::DeleteKey;
}
if (empty($directiveFormData['enable'])) {
continue;
}
/**
* Convert form state to config array for source list directive.
*
* @param array<string, mixed> $value
* The submitted form values.
*
* @return array<string, mixed>|\Drupal\Core\Form\ToConfig
* Directive config array, or a ToConfig enum value.
*/
private static function sourceListToConfig(array $value): array|ToConfig {
if (!$value['enable']) {
return ToConfig::DeleteKey;
}
unset($value['enable']);
$directiveSchema = Csp::getDirectiveSchema($directiveName);
if ($value['base'] === 'none') {
return ['base' => 'none'];
}
if ($directiveSchema === Csp::DIRECTIVE_SCHEMA_BOOLEAN) {
$directiveOptions = TRUE;
}
elseif (in_array($directiveSchema, [
Csp::DIRECTIVE_SCHEMA_TOKEN_LIST,
Csp::DIRECTIVE_SCHEMA_OPTIONAL_TOKEN_LIST,
])) {
$directiveOptions = array_keys(array_filter($directiveFormData['keys']));
}
elseif ($directiveSchema === Csp::DIRECTIVE_SCHEMA_ALLOW_BLOCK) {
$directiveOptions = $directiveFormData['value'];
}
elseif (in_array($directiveSchema, [
Csp::DIRECTIVE_SCHEMA_SOURCE_LIST,
Csp::DIRECTIVE_SCHEMA_ANCESTOR_SOURCE_LIST,
])) {
if ($directiveFormData['base'] !== 'none') {
if (!empty($directiveFormData['sources'])) {
$directiveOptions['sources'] = array_filter(preg_split('/,?\s+/', $directiveFormData['sources']));
}
if ($directiveSchema == Csp::DIRECTIVE_SCHEMA_SOURCE_LIST) {
$directiveFormData['flags'] = array_filter($directiveFormData['flags']);
if (!empty($directiveFormData['flags'])) {
$directiveOptions['flags'] = array_keys($directiveFormData['flags']);
}
}
}
$value['sources'] = array_filter(preg_split('/,?\s+/', $value['sources']));
$directiveOptions['base'] = $directiveFormData['base'];
}
elseif ($directiveSchema == Csp::DIRECTIVE_SCHEMA_TRUSTED_TYPES) {
$directiveOptions['base'] = $directiveFormData['base'];
$directiveOptions['allow-duplicates'] = $directiveFormData['base'] !== 'none' && $directiveFormData['allow-duplicates'];
$directiveOptions['policy-names'] = $directiveOptions['base'] === '' ?
array_filter(preg_split('/,?\s+/', $directiveFormData['policy-names'])) :
[];
}
elseif ($directiveSchema == Csp::DIRECTIVE_SCHEMA_TRUSTED_TYPES_SINK_GROUPS) {
// Script is currently the only valid value, so set it when directive
// is enabled.
// See https://w3c.github.io/trusted-types/dist/spec/#require-trusted-types-for-csp-directive.
$directiveOptions = ['script'];
}
if (array_key_exists('flags', $value)) {
$value['flags'] = array_keys(array_filter($value['flags']));
}
if (
!empty($directiveOptions)
||
$directiveSchema === Csp::DIRECTIVE_SCHEMA_OPTIONAL_TOKEN_LIST
) {
$config->set($policyTypeKey . '.directives.' . $directiveName, $directiveOptions);
}
}
return array_filter($value);
}
$reportHandlerPluginId = $form_state->getValue([$policyTypeKey, 'reporting', 'handler']);
$config->set($policyTypeKey . '.reporting', ['plugin' => $reportHandlerPluginId]);
$reportHandlerOptions = $form_state->getValue([$policyTypeKey, 'reporting', $reportHandlerPluginId]);
if ($reportHandlerOptions) {
$config->set($policyTypeKey . '.reporting.options', $reportHandlerOptions);
}
/**
* Convert form state to config array for trusted types directive.
*
* @param array<string, mixed> $value
* The submitted form values.
*
* @return array<string, mixed>|\Drupal\Core\Form\ToConfig
* Directive config array, or a ToConfig enum value.
*/
private static function trustedTypesToConfig(array $value): array|ToConfig {
if (!$value['enable']) {
return ToConfig::DeleteKey;
}
unset($value['enable']);
$value['allow-duplicates'] = $value['base'] !== 'none' && $value['allow-duplicates'];
$value['policy-names'] = $value['base'] === '' ?
array_filter(preg_split('/,?\s+/', $value['policy-names'])) :
[];
return $value;
}
/**
* Convert form state to config array for trusted types sink groups.
*
* @param array<string, mixed> $value
* The submitted form values.
*
* @return array<string>|\Drupal\Core\Form\ToConfig
* Directive config array, or a ToConfig enum value.
*/
private static function sinkGroupsToConfig(array $value): array|ToConfig {
// Script is currently the only valid value, so set it when enabled.
// See https://w3c.github.io/trusted-types/dist/spec/#require-trusted-types-for-csp-directive.
return !empty($value['enable']) ? ['script'] : ToConfig::DeleteKey;
}
$config->save();
/**
* Convert form state to config array for reporting options.
*
* @param array<string, mixed> $value
* The submitted form values.
*
* @return array<string, mixed>
* Reporting config array.
*/
private static function reportingToConfig(array $value): array {
$return = [
'plugin' => $value['handler'],
];
if (!empty($value[$value['handler']])) {
$return['options'] = $value[$value['handler']];
}
parent::submitForm($form, $form_state);
return $return;
}
}
......@@ -2,8 +2,6 @@
namespace Drupal\csp\Plugin\CspReportingHandler;
use Drupal\Core\Form\FormStateInterface;
use Drupal\csp\Csp;
use Drupal\csp\Plugin\ReportingHandlerBase;
/**
......@@ -17,25 +15,4 @@ use Drupal\csp\Plugin\ReportingHandlerBase;
*/
class None extends ReportingHandlerBase {
/**
* {@inheritdoc}
*/
public function getForm(array $form): array {
return $form;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state): void {
}
/**
* {@inheritdoc}
*/
public function alterPolicy(Csp $policy): void {
}
}
......@@ -2,7 +2,8 @@
namespace Drupal\csp\Plugin\CspReportingHandler;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\ConfigTarget;
use Drupal\Core\Form\ToConfig;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\csp\Csp;
use Drupal\csp\Plugin\ReportingHandlerBase;
......@@ -34,6 +35,11 @@ class ReportUri extends ReportingHandlerBase {
':url' => 'https://report-uri.com/account/setup/',
]),
'#default_value' => $this->configuration['subdomain'] ?? '',
'#config_target' => new ConfigTarget(
'csp.settings',
$this->configuration['type'] . '.reporting.options.subdomain',
toConfig: fn() => ToConfig::NoOp,
),
'#states' => [
'required' => [
':input[name="' . $this->configuration['type'] . '[enable]"]' => ['checked' => TRUE],
......@@ -58,17 +64,6 @@ class ReportUri extends ReportingHandlerBase {
return $form;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state): void {
$subdomain = $form_state->getValue($form['subdomain']['#parents']);
// Custom domains must be 4-30 characters, but generated domains are 32.
if (!preg_match('/^[a-z\d]{4,32}$/i', $subdomain)) {
$form_state->setError($form['subdomain'], 'Must be 4-30 alphanumeric characters.');
}
}
/**
* {@inheritdoc}
*/
......
......@@ -23,11 +23,13 @@ class SourceConstraintValidator extends ConstraintValidator {
if (!$constraint->allowNonce && self::isValidNonce($value)) {
$this->context->buildViolation("Nonce sources are not valid.")
->setInvalidValue($value)
->setCause('nonce')
->addViolation();
}
elseif (!$constraint->allowHash && self::isValidHash($value)) {
$this->context->buildViolation("Hash sources are not valid.")
->setInvalidValue($value)
->setCause('hash')
->addViolation();
}
elseif (
......@@ -36,8 +38,8 @@ class SourceConstraintValidator extends ConstraintValidator {
&& !($constraint->allowHash && self::isValidHash($value))
&& !($constraint->allowNonce && self::isValidNonce($value))
) {
$this->context->buildViolation('"@value" is not a valid source')
->setParameter('@value', $value)
$this->context->buildViolation('"%value" is not a valid source')
->setParameter('%value', $value)
->setInvalidValue($value)
->addViolation();
}
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment