You are here

class CspSettingsForm in Content-Security-Policy 8

Form for editing Content Security Policy module settings.

Hierarchy

Expanded class hierarchy of CspSettingsForm

1 file declares its use of CspSettingsForm
CspSettingsFormTest.php in tests/src/Unit/Form/CspSettingsFormTest.php
1 string reference to 'CspSettingsForm'
csp.routing.yml in ./csp.routing.yml
csp.routing.yml

File

src/Form/CspSettingsForm.php, line 18

Namespace

Drupal\csp\Form
View source
class CspSettingsForm extends ConfigFormBase {

  /**
   * The Library Policy Builder service.
   *
   * @var \Drupal\csp\LibraryPolicyBuilder
   */
  private $libraryPolicyBuilder;

  /**
   * The Reporting Handler Plugin Manager service.
   *
   * @var \Drupal\csp\ReportingHandlerPluginManager
   */
  private $reportingHandlerPluginManager;

  /**
   * A map of keywords and the directives which they are valid for.
   *
   * @var array
   */
  private static $keywordDirectiveMap = [
    // A violation’s sample will be populated with the first 40 characters of an
    // inline script, event handler, or style that caused an violation.
    // Violations which stem from an external file will not include a sample in
    // the violation report.
    // @see https://www.w3.org/TR/CSP3/#framework-violation
    'report-sample' => [
      'default-src',
      'script-src',
      'script-src-attr',
      'script-src-elem',
      'style-src',
      'style-src-attr',
      'style-src-elem',
    ],
    'strict-dynamic' => [
      'default-src',
      'script-src',
    ],
    'unsafe-allow-redirects' => [
      'navigate-to',
    ],
    // Since "unsafe-eval" acts as a global page flag, script-src-attr and
    // script-src-elem are not used when performing this check, instead
    // script-src (or it’s fallback directive) is always used.
    // @see https://www.w3.org/TR/CSP3/#directive-script-src
    'unsafe-eval' => [
      'default-src',
      'script-src',
      'style-src',
    ],
    // Unsafe-hashes only applies to inline attributes.
    'unsafe-hashes' => [
      'default-src',
      'script-src',
      'script-src-attr',
      'style-src',
      'style-src-attr',
    ],
    'unsafe-inline' => [
      'default-src',
      'script-src',
      'script-src-attr',
      'script-src-elem',
      'style-src',
      'style-src-attr',
      'style-src-elem',
    ],
  ];

  /**
   * {@inheritdoc}
   */
  public function getFormId() {
    return 'csp_settings';
  }

  /**
   * {@inheritdoc}
   */
  protected function getEditableConfigNames() {
    return [
      'csp.settings',
    ];
  }

  /**
   * Constructs a \Drupal\csp\Form\CspSettingsForm object.
   *
   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
   *   The factory for configuration objects.
   * @param \Drupal\csp\LibraryPolicyBuilder $libraryPolicyBuilder
   *   The Library Policy Builder service.
   * @param \Drupal\csp\ReportingHandlerPluginManager $reportingHandlerPluginManager
   *   The Reporting Handler Plugin Manger service.
   * @param \Drupal\Core\Messenger\MessengerInterface|null $messenger
   *   The Messenger service.
   */
  public function __construct(ConfigFactoryInterface $config_factory, LibraryPolicyBuilder $libraryPolicyBuilder, ReportingHandlerPluginManager $reportingHandlerPluginManager, MessengerInterface $messenger) {
    parent::__construct($config_factory);
    $this->libraryPolicyBuilder = $libraryPolicyBuilder;
    $this->reportingHandlerPluginManager = $reportingHandlerPluginManager;
    $this
      ->setMessenger($messenger);
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static($container
      ->get('config.factory'), $container
      ->get('csp.library_policy_builder'), $container
      ->get('plugin.manager.csp_reporting_handler'), $container
      ->get('messenger'));
  }

  /**
   * Get the directives that should be configurable.
   *
   * @return array
   *   An array of directive names.
   */
  private function getConfigurableDirectives() {

    // Exclude some directives
    // - Reporting directives are handled by plugins.
    // - 'referrer' was deprecated prior to CSP Level 1 and not supported in
    //   most browsers.
    // - 'require-sri-for' was never publicly implemented, and dropped from the
    //   SRI spec.
    $directives = array_diff(Csp::getDirectiveNames(), [
      'report-uri',
      'report-to',
      'referrer',
      'require-sri-for',
    ]);
    return $directives;
  }

  /**
   * Get the valid keyword options for a directive.
   *
   * @param string $directive
   *   The directive to get keywords for.
   *
   * @return array
   *   An array of keywords.
   */
  private function getKeywordOptions($directive) {
    $allKeywords = [
      'unsafe-inline',
      'unsafe-eval',
      'unsafe-hashes',
      'unsafe-allow-redirects',
      'strict-dynamic',
      'report-sample',
    ];
    return array_filter($allKeywords, function ($keyword) use ($directive) {
      return !array_key_exists($keyword, self::$keywordDirectiveMap) || in_array($directive, self::$keywordDirectiveMap[$keyword]);
    });
  }

  /**
   * {@inheritdoc}
   */
  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);
  }

  /**
   * {@inheritdoc}
   */
  public function validateForm(array &$form, FormStateInterface $form_state) {
    foreach ([
      'report-only',
      'enforce',
    ] as $policyTypeKey) {
      $directiveNames = $this
        ->getConfigurableDirectives();
      foreach ($directiveNames as $directiveName) {
        if ($directiveSources = $form_state
          ->getValue([
          $policyTypeKey,
          'directives',
          $directiveName,
          'sources',
        ])) {
          $sourcesArray = preg_split('/,?\\s+/', $directiveSources);
          $hasNonceSource = array_reduce($sourcesArray, function ($return, $value) {
            return $return || preg_match("<^'nonce->", $value);
          }, FALSE);
          if ($hasNonceSource) {
            $form_state
              ->setError($form[$policyTypeKey]['directives'][$directiveName]['options']['sources'], $this
              ->t('<a href=":docUrl">Nonces must be a unique value for each request</a>, so cannot be set in configuration.', [
              ':docUrl' => 'https://www.w3.org/TR/CSP3/#security-considerations',
            ]));
          }

          // '{hashAlgorithm}-{base64-value}'
          $hashAlgoMatch = '(' . implode('|', Csp::HASH_ALGORITHMS) . ')-[\\w+/_-]+=*';
          $hasInvalidSource = array_reduce($sourcesArray, function ($return, $value) use ($hashAlgoMatch) {
            return $return || !(preg_match('<^([a-z]+:)?$>', $value) || static::isValidHost($value) || preg_match("<^'(" . $hashAlgoMatch . ")'\$>", $value));
          }, FALSE);
          if ($hasInvalidSource) {
            $form_state
              ->setError($form[$policyTypeKey]['directives'][$directiveName]['options']['sources'], $this
              ->t('Invalid domain or protocol provided.'));
          }
        }
      }

      // Don't validate if not enabled; value will be skipped on save.
      if ($form_state
        ->getValue([
        $policyTypeKey,
        'directives',
        'plugin-types',
        'enable',
      ])) {
        $invalidTypes = array_reduce(preg_split('/,?\\s+/', $form_state
          ->getValue([
          $policyTypeKey,
          'directives',
          'plugin-types',
          'mime-types',
        ], '')), function ($return, $value) {
          return $return || !preg_match('<^([\\w-]+/[\\w-]+)?$>', $value);
        }, FALSE);
        if ($invalidTypes) {
          $form_state
            ->setError($form[$policyTypeKey]['directives']['plugin-types']['options']['mime-types'], $this
            ->t('Invalid MIME-Type provided.'));
        }
      }
      if ($reportingHandlerPluginId = $form_state
        ->getValue([
        $policyTypeKey,
        'reporting',
        'handler',
      ])) {
        $form[$policyTypeKey]['reporting'][$reportingHandlerPluginId]['#CspReportingHandlerPlugin']
          ->validateForm($form[$policyTypeKey]['reporting'][$reportingHandlerPluginId], $form_state);
      }
      else {
        $form_state
          ->setError($form[$policyTypeKey]['reporting']['handler'], $this
          ->t('Reporting Handler is required for enabled policies.'));
      }
    }
    parent::validateForm($form, $form_state);
  }

  /**
   * Verifies the syntax of the given URL.
   *
   * Similar to UrlHelper::isValid(), except:
   * - protocol is optional; can only be http/https, or ws/wss.
   * - domains must have at least a top-level and secondary domain.
   * - an initial subdomain wildcard is allowed
   * - wildcard is allowed as port value
   * - query is not allowed.
   *
   * @param string $url
   *   The URL to verify.
   *
   * @return bool
   *   TRUE if the URL is in a valid format, FALSE otherwise.
   */
  protected static function isValidHost($url) {
    return (bool) preg_match("\n        /^                                                      # Start at the beginning of the text\n        (?:[a-z][a-z0-9\\-.+]+:\\/\\/)?                             # Scheme (optional)\n        (?:\n          (?:                                                   # A domain name or a IPv4 address\n            (?:\\*\\.)?                                           # Wildcard prefix (optional)\n            (?:(?:[a-z0-9\\-\\.]|%[0-9a-f]{2})+\\.)+\n            (?:[a-z0-9\\-\\.]|%[0-9a-f]{2})+\n          )\n          |(?:\\[(?:[0-9a-f]{0,4}:)*(?:[0-9a-f]{0,4})\\])         # or a well formed IPv6 address\n          |localhost\n        )\n        (?::(?:[0-9]+|\\*))?                                     # Server port number or wildcard (optional)\n        (?:[\\/|\\?]\n          (?:[\\w#!:\\.\\+=&@\$'~*,;\\/\\(\\)\\[\\]\\-]|%[0-9a-f]{2})     # The path (optional)\n        *)?\n      \$/xi", $url);
  }

  /**
   * Submit handler for clear policy buttons.
   *
   * @param array $form
   *   The form structure.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state.
   */
  public function submitClearPolicy(array &$form, FormStateInterface $form_state) {
    $submitElement = $form_state
      ->getTriggeringElement();
    $this
      ->config('csp.settings')
      ->clear($submitElement['#cspPolicyType'])
      ->save();
  }

  /**
   * {@inheritdoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {
    $config = $this
      ->config('csp.settings');
    $directiveNames = $this
      ->getConfigurableDirectives();
    foreach ([
      'report-only',
      'enforce',
    ] as $policyTypeKey) {
      $config
        ->clear($policyTypeKey);
      $policyFormData = $form_state
        ->getValue($policyTypeKey);
      $config
        ->set($policyTypeKey . '.enable', !empty($policyFormData['enable']));
      foreach ($directiveNames as $directiveName) {
        if (empty($policyFormData['directives'][$directiveName])) {
          continue;
        }
        $directiveFormData = $policyFormData['directives'][$directiveName];
        $directiveOptions = [];
        if (empty($directiveFormData['enable'])) {
          continue;
        }
        $directiveSchema = Csp::getDirectiveSchema($directiveName);
        if ($directiveSchema === Csp::DIRECTIVE_SCHEMA_BOOLEAN) {
          $directiveOptions = TRUE;
        }
        elseif ($directiveSchema === Csp::DIRECTIVE_SCHEMA_MEDIA_TYPE_LIST) {

          // If "object-src: none" all plugins will be blocked even if type is
          // allowed.  The form field is hidden and skips validation, so make
          // sure value is not saved.
          if ($directiveName == 'plugin-types' && $policyFormData['directives']['object-src']['enable'] && $policyFormData['directives']['object-src']['base'] == 'none') {
            continue;
          }
          $directiveOptions = array_filter(preg_split('/,?\\s+/', $directiveFormData['mime-types']));
        }
        elseif (in_array($directiveSchema, [
          Csp::DIRECTIVE_SCHEMA_TOKEN_LIST,
          Csp::DIRECTIVE_SCHEMA_OPTIONAL_TOKEN_LIST,
        ])) {
          $directiveOptions = array_keys(array_filter($directiveFormData['keys']));
        }
        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']);
              }
            }
          }
          $directiveOptions['base'] = $directiveFormData['base'];
        }
        if (!empty($directiveOptions) || in_array($directiveSchema, [
          Csp::DIRECTIVE_SCHEMA_OPTIONAL_TOKEN_LIST,
          Csp::DIRECTIVE_SCHEMA_MEDIA_TYPE_LIST,
        ])) {
          $config
            ->set($policyTypeKey . '.directives.' . $directiveName, $directiveOptions);
        }
      }
      $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);
      }
    }
    $config
      ->save();
    parent::submitForm($form, $form_state);
  }

}

Members

Namesort descending Modifiers Type Description Overrides
ConfigFormBaseTrait::config protected function Retrieves a configuration object.
CspSettingsForm::$keywordDirectiveMap private static property A map of keywords and the directives which they are valid for.
CspSettingsForm::$libraryPolicyBuilder private property The Library Policy Builder service.
CspSettingsForm::$reportingHandlerPluginManager private property The Reporting Handler Plugin Manager service.
CspSettingsForm::buildForm public function Form constructor. Overrides ConfigFormBase::buildForm
CspSettingsForm::create public static function Instantiates a new instance of this class. Overrides ConfigFormBase::create
CspSettingsForm::getConfigurableDirectives private function Get the directives that should be configurable.
CspSettingsForm::getEditableConfigNames protected function Gets the configuration names that will be editable. Overrides ConfigFormBaseTrait::getEditableConfigNames
CspSettingsForm::getFormId public function Returns a unique string identifying the form. Overrides FormInterface::getFormId
CspSettingsForm::getKeywordOptions private function Get the valid keyword options for a directive.
CspSettingsForm::isValidHost protected static function Verifies the syntax of the given URL. 1
CspSettingsForm::submitClearPolicy public function Submit handler for clear policy buttons.
CspSettingsForm::submitForm public function Form submission handler. Overrides ConfigFormBase::submitForm
CspSettingsForm::validateForm public function Form validation handler. Overrides FormBase::validateForm
CspSettingsForm::__construct public function Constructs a \Drupal\csp\Form\CspSettingsForm object. Overrides ConfigFormBase::__construct
DependencySerializationTrait::$_entityStorages protected property An array of entity type IDs keyed by the property name of their storages.
DependencySerializationTrait::$_serviceIds protected property An array of service IDs keyed by property name used for serialization.
DependencySerializationTrait::__sleep public function 1
DependencySerializationTrait::__wakeup public function 2
FormBase::$configFactory protected property The config factory. 1
FormBase::$requestStack protected property The request stack. 1
FormBase::$routeMatch protected property The route match.
FormBase::configFactory protected function Gets the config factory for this form. 1
FormBase::container private function Returns the service container.
FormBase::currentUser protected function Gets the current user.
FormBase::getRequest protected function Gets the request object.
FormBase::getRouteMatch protected function Gets the route match.
FormBase::logger protected function Gets the logger for a specific channel.
FormBase::redirect protected function Returns a redirect response object for the specified route. Overrides UrlGeneratorTrait::redirect
FormBase::resetConfigFactory public function Resets the configuration factory.
FormBase::setConfigFactory public function Sets the config factory for this form.
FormBase::setRequestStack public function Sets the request stack object to use.
LinkGeneratorTrait::$linkGenerator protected property The link generator. 1
LinkGeneratorTrait::getLinkGenerator Deprecated protected function Returns the link generator.
LinkGeneratorTrait::l Deprecated protected function Renders a link to a route given a route name and its parameters.
LinkGeneratorTrait::setLinkGenerator Deprecated public function Sets the link generator service.
LoggerChannelTrait::$loggerFactory protected property The logger channel factory service.
LoggerChannelTrait::getLogger protected function Gets the logger for a specific channel.
LoggerChannelTrait::setLoggerFactory public function Injects the logger channel factory.
MessengerTrait::$messenger protected property The messenger. 29
MessengerTrait::messenger public function Gets the messenger. 29
MessengerTrait::setMessenger public function Sets the messenger.
RedirectDestinationTrait::$redirectDestination protected property The redirect destination service. 1
RedirectDestinationTrait::getDestinationArray protected function Prepares a 'destination' URL query parameter for use with \Drupal\Core\Url.
RedirectDestinationTrait::getRedirectDestination protected function Returns the redirect destination service.
RedirectDestinationTrait::setRedirectDestination public function Sets the redirect destination service.
StringTranslationTrait::$stringTranslation protected property The string translation service. 1
StringTranslationTrait::formatPlural protected function Formats a string containing a count of items.
StringTranslationTrait::getNumberOfPlurals protected function Returns the number of plurals supported by a given language.
StringTranslationTrait::getStringTranslation protected function Gets the string translation service.
StringTranslationTrait::setStringTranslation public function Sets the string translation service to use. 2
StringTranslationTrait::t protected function Translates a string to the current language or to a given language.
UrlGeneratorTrait::$urlGenerator protected property The url generator.
UrlGeneratorTrait::getUrlGenerator Deprecated protected function Returns the URL generator service.
UrlGeneratorTrait::setUrlGenerator Deprecated public function Sets the URL generator service.
UrlGeneratorTrait::url Deprecated protected function Generates a URL or path for a specific route based on the given parameters.