You are here

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\Form

Code

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);
}