class StyleSensibleElementConstraintValidator

Same name in other branches
  1. 9 core/modules/ckeditor5/src/Plugin/Validation/Constraint/StyleSensibleElementConstraintValidator.php \Drupal\ckeditor5\Plugin\Validation\Constraint\StyleSensibleElementConstraintValidator
  2. 11.x core/modules/ckeditor5/src/Plugin/Validation/Constraint/StyleSensibleElementConstraintValidator.php \Drupal\ckeditor5\Plugin\Validation\Constraint\StyleSensibleElementConstraintValidator

Styles can only be specified for HTML5 tags and extra classes.

@internal

Hierarchy

Expanded class hierarchy of StyleSensibleElementConstraintValidator

File

core/modules/ckeditor5/src/Plugin/Validation/Constraint/StyleSensibleElementConstraintValidator.php, line 23

Namespace

Drupal\ckeditor5\Plugin\Validation\Constraint
View source
class StyleSensibleElementConstraintValidator extends ConstraintValidator implements ContainerInjectionInterface {
    use PrecedingConstraintAwareValidatorTrait;
    use PluginManagerDependentValidatorTrait;
    use TextEditorObjectDependentValidatorTrait;
    
    /**
     * Tags whose plugins are known to not yet integrate with the Style plugin.
     *
     * To prevent the user from configuring the Style plugin and reasonably
     * expecting it to work correctly for tags of plugins that are known to
     * yet integrate with the Style plugin, generate a validation error for these.
     */
    protected const KNOWN_UNSUPPORTED_TAGS = [
        // @see https://www.drupal.org/project/drupal/issues/3117172
'<drupal-media>',
        // @see https://github.com/ckeditor/ckeditor5/issues/13778
'<img>',
        // @see https://github.com/ckeditor/ckeditor5/blob/39ad30090ead9dd2d54c3ac53d7f446ade9fd8ce/packages/ckeditor5-html-support/src/schemadefinitions.ts#L12-L50
'<keygen>',
        '<applet>',
        '<basefont>',
        '<isindex>',
        '<hr>',
        '<br>',
        '<area>',
        '<command>',
        '<map>',
        '<wbr>',
        '<colgroup>',
        '<col>',
        '<datalist>',
        '<track>',
        '<source>',
        '<option>',
        '<param>',
        '<optgroup>',
        '<link>',
        '<noscript>',
    ];
    
    /**
     * {@inheritdoc}
     *
     * @throws \Symfony\Component\Validator\Exception\UnexpectedTypeException
     *   Thrown when the given constraint is not supported by this validator.
     */
    public function validate($element, Constraint $constraint) {
        if (!$constraint instanceof StyleSensibleElementConstraint) {
            throw new UnexpectedTypeException($constraint, StyleSensibleElementConstraint::class);
        }
        // The preceding constraints (in this case: CKEditor5Element) must be valid.
        if ($this->hasViolationsForPrecedingConstraints($constraint)) {
            return;
        }
        $text_editor = $this->createTextEditorObjectFromContext();
        // The single tag for which a style is specified, which we are checking now.
        $style_element = HTMLRestrictions::fromString($element);
        assert(count($style_element->getAllowedElements()) === 1);
        [
            $tag,
            $classes,
        ] = Style::getTagAndClasses($style_element);
        // Ensure the tag is in the range supported by the Style plugin.
        $superset = HTMLRestrictions::fromString('<$any-html5-element class>');
        $supported_range = $superset->merge($style_element->extractPlainTagsSubset());
        if (!$style_element->diff($supported_range)
            ->allowsNothing()) {
            $this->context
                ->buildViolation($constraint->nonHtml5TagMessage)
                ->setParameter('@tag', sprintf("<%s>", $tag))
                ->addViolation();
            return;
        }
        // Get the list of tags enabled by every plugin other than Style.
        $other_enabled_plugins = $this->getOtherEnabledPlugins($text_editor, 'ckeditor5_style');
        $enableable_disabled_plugins = $this->getEnableableDisabledPlugins($text_editor);
        $other_enabled_plugin_elements = new HTMLRestrictions($this->pluginManager
            ->getProvidedElements(array_keys($other_enabled_plugins), $text_editor, FALSE));
        $disabled_plugin_elements = new HTMLRestrictions($this->pluginManager
            ->getProvidedElements(array_keys($enableable_disabled_plugins), $text_editor, FALSE));
        // Next, validate that the classes specified for this style are not
        // supported by an enabled plugin.
        if (self::intersectionWithClasses($style_element, $other_enabled_plugin_elements)) {
            $this->context
                ->buildViolation($constraint->conflictingEnabledPluginMessage)
                ->setParameter('@tag', sprintf("<%s>", $tag))
                ->setParameter('@classes', implode(", ", $classes))
                ->setParameter('%plugin', $this->findStyleConflictingPluginLabel($style_element))
                ->addViolation();
        }
        elseif (self::intersectionWithClasses($style_element, $disabled_plugin_elements)) {
            $this->context
                ->buildViolation($constraint->conflictingDisabledPluginMessage)
                ->setParameter('@tag', sprintf("<%s>", $tag))
                ->setParameter('@classes', implode(", ", $classes))
                ->setParameter('%plugin', $this->findStyleConflictingPluginLabel($style_element))
                ->addViolation();
        }
        // Finally, while the configuration is technically valid if this point was
        // reached, there are some known compatibility issues. Inform the user that
        // for that reason, this configuration must be considered invalid.
        $unsupported = $style_element->intersect(HTMLRestrictions::fromString(implode(' ', static::KNOWN_UNSUPPORTED_TAGS)));
        if (!$unsupported->allowsNothing()) {
            $this->context
                ->buildViolation($constraint->unsupportedTagMessage)
                ->setParameter('@tag', sprintf("<%s>", $tag))
                ->addViolation();
        }
    }
    
    /**
     * Checks if there is an intersection on allowed 'class' attribute values.
     *
     * @param \Drupal\ckeditor5\HTMLRestrictions $a
     *   One set of HTML restrictions.
     * @param \Drupal\ckeditor5\HTMLRestrictions $b
     *   Another set of HTML restrictions.
     *
     * @return bool
     *   Whether there is an intersection.
     */
    private static function intersectionWithClasses(HTMLRestrictions $a, HTMLRestrictions $b) : bool {
        // Compute the intersection, but first resolve wildcards, by merging
        // tags of the other operand. Because only tags are merged, this cannot
        // introduce a 'class' attribute intersection.
        // For example: a plugin may support `<$text-container class="foo">`. On its
        // own that would not trigger an intersection, but when resolved into
        // concrete tags it could.
        $tags_from_a = array_diff(array_keys($a->getConcreteSubset()
            ->getAllowedElements()), [
            '*',
        ]);
        $tags_from_b = array_diff(array_keys($b->getConcreteSubset()
            ->getAllowedElements()), [
            '*',
        ]);
        $a = $a->merge(new HTMLRestrictions(array_fill_keys($tags_from_b, FALSE)));
        $b = $b->merge(new HTMLRestrictions(array_fill_keys($tags_from_a, FALSE)));
        // When a plugin allows all classes on a tag, we assume there is no
        // problem with having the style plugin adding classes to that element.
        // When allowing all classes we don't expect a specific user experience
        // so adding a class through a plugin or the style plugin is the same.
        $b_without_class_wildcard = $b->getAllowedElements();
        foreach ($b_without_class_wildcard as $allowedElement => $config) {
            // When all classes are allowed, remove the configuration so that
            // the intersect below does not include classes.
            if (!empty($config['class']) && $config['class'] === TRUE) {
                unset($b_without_class_wildcard[$allowedElement]['class']);
            }
            // HTMLRestrictions does not accept a tag with an empty array, make sure
            // to remove them here.
            if (empty($b_without_class_wildcard[$allowedElement])) {
                unset($b_without_class_wildcard[$allowedElement]);
            }
        }
        $intersection = $a->intersect(new HTMLRestrictions($b_without_class_wildcard));
        // Leverage the "GHS configuration" representation to easily find whether
        // there is an intersection for classes. Other implementations are possible.
        $intersection_as_ghs_config = $intersection->toGeneralHtmlSupportConfig();
        $ghs_config_classes = array_column($intersection_as_ghs_config, 'classes');
        return !empty($ghs_config_classes);
    }
    
    /**
     * Finds the plugin with elements that conflict with the style element.
     *
     * @param \Drupal\ckeditor5\HTMLRestrictions $needle
     *   A style definition element: a single tag, plus the 'class' attribute,
     *   plus >=1 allowed 'class' attribute values.
     *
     * @return \Drupal\Core\StringTranslation\TranslatableMarkup
     *   The label of the plugin that is conflicting with this style.
     *
     * @throws \OutOfBoundsException
     *   When a $needle is provided which does not exist among the other plugins.
     */
    private function findStyleConflictingPluginLabel(HTMLRestrictions $needle) : TranslatableMarkup {
        foreach ($this->pluginManager
            ->getDefinitions() as $id => $definition) {
            // We're looking to find the other plugin, not this one.
            if ($id === 'ckeditor5_style') {
                continue;
            }
            assert($definition instanceof CKEditor5PluginDefinition);
            if (!$definition->hasElements()) {
                continue;
            }
            $haystack = HTMLRestrictions::fromString(implode($definition->getElements()));
            if ($id === 'ckeditor5_sourceEditing') {
                // The Source Editing plugin's allowed elements are based on stored
                // config. This differs from all other plugins, which establish allowed
                // elements as part of their definition. Because of this, the $haystack
                // is calculated differently for Source Editing.
                $text_editor = $this->createTextEditorObjectFromContext();
                $editor_plugins = $text_editor->getSettings()['plugins'];
                if (!empty($editor_plugins['ckeditor5_sourceEditing'])) {
                    $source_tags = $editor_plugins['ckeditor5_sourceEditing']['allowed_tags'];
                    $haystack = HTMLRestrictions::fromString(implode($source_tags));
                }
            }
            if (self::intersectionWithClasses($needle, $haystack)) {
                return $definition->label();
            }
        }
        throw new \OutOfBoundsException();
    }

}

Members

Title Sort descending Modifiers Object type Summary Overrides
PluginManagerDependentValidatorTrait::$pluginManager protected property The CKEditor 5 plugin manager.
PluginManagerDependentValidatorTrait::create public static function 1
PluginManagerDependentValidatorTrait::getEnableableDisabledPlugins private function Gets all disabled CKEditor 5 plugin definitions the user can enable.
PluginManagerDependentValidatorTrait::getOtherEnabledPlugins private function Gets all other enabled CKEditor 5 plugin definitions.
PluginManagerDependentValidatorTrait::__construct public function Constructs a CKEditor5ConstraintValidatorTrait object. 1
PrecedingConstraintAwareValidatorTrait::getPrecedingConstraints private function Gets the constraints preceding the given constraint in the current context.
PrecedingConstraintAwareValidatorTrait::hasViolationsForPrecedingConstraints protected function Checks whether any preceding constraints have been violated.
StyleSensibleElementConstraintValidator::findStyleConflictingPluginLabel private function Finds the plugin with elements that conflict with the style element.
StyleSensibleElementConstraintValidator::intersectionWithClasses private static function Checks if there is an intersection on allowed &#039;class&#039; attribute values.
StyleSensibleElementConstraintValidator::KNOWN_UNSUPPORTED_TAGS protected constant Tags whose plugins are known to not yet integrate with the Style plugin.
StyleSensibleElementConstraintValidator::validate public function
TextEditorObjectDependentValidatorTrait::createTextEditorObjectFromContext private function Creates a text editor object from the execution context.

Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.