public function CspSettingsForm::buildForm in Content-Security-Policy 8
Form constructor.
Parameters
array $form: An associative array containing the structure of the form.
\Drupal\Core\Form\FormStateInterface $form_state: The current state of the form.
Return value
array The form structure.
Overrides ConfigFormBase::buildForm
File
- src/
Form/ CspSettingsForm.php, line 158
Class
- CspSettingsForm
- Form for editing Content Security Policy module settings.
Namespace
Drupal\csp\FormCode
public function buildForm(array $form, FormStateInterface $form_state) {
$reportingHandlerPluginDefinitions = $this->reportingHandlerPluginManager
->getDefinitions();
$config = $this
->config('csp.settings');
$autoDirectives = $this->libraryPolicyBuilder
->getSources();
$form['#attached']['library'][] = 'csp/admin';
$form['policies'] = [
'#type' => 'vertical_tabs',
'#title' => $this
->t('Policies'),
];
$directiveNames = $this
->getConfigurableDirectives();
$enforceOnlyDirectives = [
// @see https://w3c.github.io/webappsec-upgrade-insecure-requests/#delivery
'upgrade-insecure-requests',
// @see https://www.w3.org/TR/CSP/#directive-sandbox
'sandbox',
];
$policyTypes = [
'report-only' => $this
->t('Report Only'),
'enforce' => $this
->t('Enforced'),
];
foreach ($policyTypes as $policyTypeKey => $policyTypeName) {
$form[$policyTypeKey] = [
'#type' => 'details',
'#title' => $policyTypeName,
'#group' => 'policies',
'#tree' => TRUE,
];
if ($config
->get($policyTypeKey . '.enable')) {
$form['policies']['#default_tab'] = 'edit-' . $policyTypeKey;
}
$form[$policyTypeKey]['enable'] = [
'#type' => 'checkbox',
'#title' => $this
->t("Enable '@type'", [
'@type' => $policyTypeName,
]),
'#default_value' => $config
->get($policyTypeKey . '.enable') ?: FALSE,
];
$form[$policyTypeKey]['directives'] = [
'#type' => 'fieldset',
'#title' => $this
->t('Directives'),
'#description_display' => 'before',
'#tree' => TRUE,
];
foreach ($directiveNames as $directiveName) {
$directiveSchema = Csp::getDirectiveSchema($directiveName);
$form[$policyTypeKey]['directives'][$directiveName] = [
'#type' => 'container',
'#access' => $policyTypeKey == 'enforce' || !in_array($directiveName, $enforceOnlyDirectives),
];
$form[$policyTypeKey]['directives'][$directiveName]['enable'] = [
'#type' => 'checkbox',
'#title' => $directiveName,
];
if (!empty($autoDirectives[$directiveName])) {
$form[$policyTypeKey]['directives'][$directiveName]['enable']['#title'] .= ' <span class="csp-directive-auto">auto</span>';
}
if ($config
->get($policyTypeKey)) {
// Csp::DIRECTIVE_SCHEMA_OPTIONAL_TOKEN_LIST may be an empty array,
// so is_null() must be used instead of empty().
// Directives which cannot be empty should not be present in config.
// (e.g. boolean directives should only be present if TRUE).
$form[$policyTypeKey]['directives'][$directiveName]['enable']['#default_value'] = !is_null($config
->get($policyTypeKey . '.directives.' . $directiveName));
}
else {
// Directives to enable by default (with 'self').
if (in_array($directiveName, [
'script-src',
'script-src-attr',
'script-src-elem',
'style-src',
'style-src-attr',
'style-src-elem',
'frame-ancestors',
]) || isset($autoDirectives[$directiveName])) {
$form[$policyTypeKey]['directives'][$directiveName]['enable']['#default_value'] = TRUE;
}
}
$form[$policyTypeKey]['directives'][$directiveName]['options'] = [
'#type' => 'container',
'#states' => [
'visible' => [
':input[name="' . $policyTypeKey . '[directives][' . $directiveName . '][enable]"]' => [
'checked' => TRUE,
],
],
],
];
if (!in_array($directiveSchema, [
Csp::DIRECTIVE_SCHEMA_SOURCE_LIST,
Csp::DIRECTIVE_SCHEMA_ANCESTOR_SOURCE_LIST,
])) {
continue;
}
$sourceListBase = $config
->get($policyTypeKey . '.directives.' . $directiveName . '.base');
$form[$policyTypeKey]['directives'][$directiveName]['options']['base'] = [
'#type' => 'radios',
'#parents' => [
$policyTypeKey,
'directives',
$directiveName,
'base',
],
'#options' => [
'self' => "Self",
'none' => "None",
'any' => "Any",
'' => '<em>n/a</em>',
],
'#default_value' => $sourceListBase !== NULL ? $sourceListBase : 'self',
];
// Auto sources make a directive required, so remove the 'none' option.
if (isset($autoDirectives[$directiveName])) {
unset($form[$policyTypeKey]['directives'][$directiveName]['options']['base']['#options']['none']);
}
// Keywords are only applicable to serialized-source-list directives.
if ($directiveSchema == Csp::DIRECTIVE_SCHEMA_SOURCE_LIST) {
// States currently don't work on checkboxes elements, so need to be
// applied to a wrapper.
// @see https://www.drupal.org/project/drupal/issues/994360
$form[$policyTypeKey]['directives'][$directiveName]['options']['flags_wrapper'] = [
'#type' => 'container',
'#states' => [
'visible' => [
[
':input[name="' . $policyTypeKey . '[directives][' . $directiveName . '][base]"]' => [
'!value' => 'none',
],
],
],
],
];
$keywordOptions = self::getKeywordOptions($directiveName);
$keywordOptions = array_combine($keywordOptions, array_map(function ($keyword) {
return "<code>'" . $keyword . "'</code>";
}, $keywordOptions));
$form[$policyTypeKey]['directives'][$directiveName]['options']['flags_wrapper']['flags'] = [
'#type' => 'checkboxes',
'#parents' => [
$policyTypeKey,
'directives',
$directiveName,
'flags',
],
'#options' => $keywordOptions,
'#default_value' => $config
->get($policyTypeKey . '.directives.' . $directiveName . '.flags') ?: [],
];
}
if (!empty($autoDirectives[$directiveName])) {
$form[$policyTypeKey]['directives'][$directiveName]['options']['auto'] = [
'#type' => 'textarea',
'#parents' => [
$policyTypeKey,
'directives',
$directiveName,
'auto',
],
'#title' => 'Auto Sources',
'#value' => implode(' ', $autoDirectives[$directiveName]),
'#disabled' => TRUE,
];
}
$form[$policyTypeKey]['directives'][$directiveName]['options']['sources'] = [
'#type' => 'textarea',
'#parents' => [
$policyTypeKey,
'directives',
$directiveName,
'sources',
],
'#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') ?: []),
'#states' => [
'visible' => [
[
':input[name="' . $policyTypeKey . '[directives][' . $directiveName . '][base]"]' => [
'!value' => 'none',
],
],
],
],
];
}
$form[$policyTypeKey]['directives']['child-src']['options']['note'] = [
'#type' => 'markup',
'#markup' => '<em>' . $this
->t('Instead of child-src, nested browsing contexts and workers should use the frame-src and worker-src directives, respectively.') . '</em>',
'#weight' => -10,
];
if ($policyTypeKey === 'enforce') {
// block-all-mixed content is a no-op if upgrade-insecure-requests is
// enabled.
// @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/block-all-mixed-content
$form[$policyTypeKey]['directives']['block-all-mixed-content']['#states'] = [
'disabled' => [
[
':input[name="' . $policyTypeKey . '[directives][upgrade-insecure-requests][enable]"]' => [
'checked' => TRUE,
],
],
],
];
}
$form[$policyTypeKey]['directives']['plugin-types']['#states'] = [
'visible' => [
[
':input[name="' . $policyTypeKey . '[directives][object-src][base]"]' => [
'!value' => 'none',
],
// states.js has a bug which requires that the first OR group
// include all selectors used. 'enable' isn't really required for
// this condition, but is need for the later FALSE to work.
':input[name="' . $policyTypeKey . '[directives][object-src][enable]"]' => [
'checked' => TRUE,
],
],
'or',
[
':input[name="' . $policyTypeKey . '[directives][object-src][enable]"]' => [
'checked' => FALSE,
],
],
],
];
$form[$policyTypeKey]['directives']['plugin-types']['options']['mime-types'] = [
'#type' => 'textfield',
'#parents' => [
$policyTypeKey,
'directives',
'plugin-types',
'mime-types',
],
'#title' => $this
->t('MIME Types'),
'#default_value' => implode(' ', $config
->get($policyTypeKey . '.directives.plugin-types') ?: []),
'#description' => $this
->t('The <code>plugin-types</code> directive has been deprecated. <code>object-src</code> should be used to restrict embedded objects.'),
];
// 'sandbox' token values are defined by HTML specification for the iframe
// sandbox attribute.
// @see https://www.w3.org/TR/CSP/#directive-sandbox
// @see https://html.spec.whatwg.org/multipage/iframe-embed-object.html#attr-iframe-sandbox
$form[$policyTypeKey]['directives']['sandbox']['options']['keys'] = [
'#type' => 'checkboxes',
'#parents' => [
$policyTypeKey,
'directives',
'sandbox',
'keys',
],
'#options' => [
'allow-forms' => '<code>allow-forms</code>',
'allow-modals' => '<code>allow-modals</code>',
'allow-orientation-lock' => '<code>allow-orientation-lock</code>',
'allow-pointer-lock' => '<code>allow-pointer-lock</code>',
'allow-popups' => '<code>allow-popups</code>',
'allow-popups-to-escape-sandbox' => '<code>allow-popups-to-escape-sandbox</code>',
'allow-presentation' => '<code>allow-presentation</code>',
'allow-same-origin' => '<code>allow-same-origin</code>',
'allow-scripts' => '<code>allow-scripts</code>',
'allow-top-navigation' => '<code>allow-top-navigation</code>',
'allow-top-navigation-by-user-activation' => '<code>allow-top-navigation-by-user-activation</code>',
],
'#default_value' => $config
->get($policyTypeKey . '.directives.sandbox') ?: [],
];
$form[$policyTypeKey]['reporting'] = [
'#type' => 'fieldset',
'#title' => $this
->t('Reporting'),
'#tree' => TRUE,
];
$form[$policyTypeKey]['reporting']['handler'] = [
'#type' => 'radios',
'#title' => $this
->t('Handler'),
'#options' => [],
'#default_value' => $config
->get($policyTypeKey . '.reporting.plugin') ?: 'none',
];
foreach ($reportingHandlerPluginDefinitions as $reportingHandlerPluginDefinition) {
$reportingHandlerOptions = [
'type' => $policyTypeKey,
];
if ($config
->get($policyTypeKey . '.reporting.plugin') == $reportingHandlerPluginDefinition['id']) {
$reportingHandlerOptions += $config
->get($policyTypeKey . '.reporting.options') ?: [];
}
try {
$reportingHandlerPlugin = $this->reportingHandlerPluginManager
->createInstance($reportingHandlerPluginDefinition['id'], $reportingHandlerOptions);
} catch (PluginException $e) {
watchdog_exception('csp', $e);
continue;
}
$form[$policyTypeKey]['reporting']['handler']['#options'][$reportingHandlerPluginDefinition['id']] = $reportingHandlerPluginDefinition['label'];
$form[$policyTypeKey]['reporting'][$reportingHandlerPluginDefinition['id']] = $reportingHandlerPlugin
->getForm([
'#type' => 'item',
'#description' => $reportingHandlerPluginDefinition['description'],
'#states' => [
'visible' => [
':input[name="' . $policyTypeKey . '[reporting][handler]"]' => [
'value' => $reportingHandlerPluginDefinition['id'],
],
],
],
'#CspReportingHandlerPlugin' => $reportingHandlerPlugin,
]);
}
$form[$policyTypeKey]['clear'] = [
'#type' => 'submit',
'#value' => $this
->t('Reset @policyType policy to default values', [
'@policyType' => $policyTypeName,
]),
'#cspPolicyType' => $policyTypeKey,
'#button_type' => 'danger',
'#submit' => [
'::submitClearPolicy',
],
];
}
// Skip this check when building the form before validation/submission.
if (empty($form_state
->getUserInput())) {
$enabledPolicies = array_filter(array_keys($policyTypes), function ($policyTypeKey) use ($config) {
return $config
->get($policyTypeKey . '.enable');
});
if (empty($enabledPolicies)) {
$this
->messenger()
->addWarning($this
->t('No policies are currently enabled.'));
}
foreach ($policyTypes as $policyTypeKey => $policyTypeName) {
if (!$config
->get($policyTypeKey . '.enable')) {
continue;
}
foreach ($directiveNames as $directive) {
if ($directiveSources = $config
->get($policyTypeKey . '.directives.' . $directive . '.sources')) {
// '{hashAlgorithm}-{base64-value}'
$hashAlgoMatch = '(' . implode('|', Csp::HASH_ALGORITHMS) . ')-[\\w+/_-]+=*';
$hasHashSource = array_reduce($directiveSources, function ($return, $value) use ($hashAlgoMatch) {
return $return || preg_match("<^'" . $hashAlgoMatch . "'\$>", $value);
}, FALSE);
if ($hasHashSource) {
$this
->messenger()
->addWarning($this
->t('%policy %directive has a hash source configured, which may block functionality that relies on inline code.', [
'%policy' => $policyTypeName,
'%directive' => $directive,
]));
}
}
}
if (!is_null($config
->get($policyTypeKey . '.directives.plugin-types')) && !$config
->get($policyTypeKey . '.directives.object-src')) {
$this
->messenger()
->addWarning($this
->t('The <code>plugin-types</code> directive has been deprecated. <code>object-src</code> should be used to restrict embedded objects.'));
}
foreach ([
'script-src',
'style-src',
] as $directive) {
foreach ([
'-attr',
'-elem',
] as $subdirective) {
if ($config
->get($policyTypeKey . '.directives.' . $directive . $subdirective)) {
foreach (Csp::getDirectiveFallbackList($directive . $subdirective) as $fallbackDirective) {
if ($config
->get($policyTypeKey . '.directives.' . $fallbackDirective)) {
continue 2;
}
}
$this
->messenger()
->addWarning($this
->t('%policy %directive is enabled without a fallback directive for non-supporting browsers.', [
'%policy' => $policyTypeName,
'%directive' => $directive . $subdirective,
]));
}
}
}
}
}
return parent::buildForm($form, $form_state);
}