SourceEditingRedundantTagsConstraintValidator.php

Same filename and directory in other branches
  1. 10 core/modules/ckeditor5/src/Plugin/Validation/Constraint/SourceEditingRedundantTagsConstraintValidator.php
  2. 11.x core/modules/ckeditor5/src/Plugin/Validation/Constraint/SourceEditingRedundantTagsConstraintValidator.php

Namespace

Drupal\ckeditor5\Plugin\Validation\Constraint

File

core/modules/ckeditor5/src/Plugin/Validation/Constraint/SourceEditingRedundantTagsConstraintValidator.php

View source
<?php

declare (strict_types=1);
namespace Drupal\ckeditor5\Plugin\Validation\Constraint;

// cspell:ignore enableable
use Drupal\ckeditor5\HTMLRestrictions;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;

/**
 * Ensures tags already available via plugin are not be added to Source Editing.
 *
 * @internal
 */
class SourceEditingRedundantTagsConstraintValidator extends ConstraintValidator implements ContainerInjectionInterface {
    use PluginManagerDependentValidatorTrait;
    use StringTranslationTrait;
    use TextEditorObjectDependentValidatorTrait;
    
    /**
     * {@inheritdoc}
     *
     * @throws \Symfony\Component\Validator\Exception\UnexpectedTypeException
     *   Thrown when the given constraint is not supported by this validator.
     */
    public function validate($value, Constraint $constraint) {
        if (!$constraint instanceof SourceEditingRedundantTagsConstraint) {
            throw new UnexpectedTypeException($constraint, __NAMESPACE__ . '\\SourceEditingRedundantTagsConstraint');
        }
        if (empty($value)) {
            return;
        }
        $text_editor = $this->createTextEditorObjectFromContext();
        $other_enabled_plugins = $this->getOtherEnabledPlugins($text_editor, 'ckeditor5_sourceEditing');
        $enableable_disabled_plugins = $this->getEnableableDisabledPlugins($text_editor);
        // An array of tags enabled by every plugin other than Source Editing.
        $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));
        $enabled_plugin_plain_tags = new HTMLRestrictions($this->pluginManager
            ->getProvidedElements(array_keys($other_enabled_plugins), $text_editor, FALSE, TRUE));
        $disabled_plugin_plain_tags = new HTMLRestrictions($this->pluginManager
            ->getProvidedElements(array_keys($enableable_disabled_plugins), $text_editor, FALSE, TRUE));
        // The single element for which source editing is enabled, which we are
        // checking now.
        $source_enabled_element = HTMLRestrictions::fromString($value);
        // Test for empty allowed elements with resolved wildcards since, for the
        // purposes of this validator, HTML restrictions containing only wildcards
        // should be considered empty.
        // @todo Remove this early return in
        //   https://www.drupal.org/project/drupal/issues/2820364. It is only
        //   necessary because CKEditor5ElementConstraintValidator does not run
        //   before this, which means that this validator cannot assume it receives
        //   valid values.
        if (count($source_enabled_element->getAllowedElements()) !== 1) {
            return;
        }
        $enabled_plugin_overlap = $enabled_plugin_elements->intersect($source_enabled_element);
        $disabled_plugin_overlap = $disabled_plugin_elements->merge($enabled_plugin_elements)
            ->intersect($source_enabled_element)
            ->diff($enabled_plugin_overlap);
        foreach ([
            $enabled_plugin_overlap,
            $disabled_plugin_overlap,
        ] as $overlap) {
            $checking_enabled = $overlap === $enabled_plugin_overlap;
            if (!$overlap->allowsNothing()) {
                $plugins_to_check_against = $checking_enabled ? $other_enabled_plugins : $enableable_disabled_plugins;
                $plain_tags_to_check_against = $checking_enabled ? $enabled_plugin_plain_tags : $disabled_plugin_plain_tags;
                $tags_plugin_report = $this->pluginsSupplyingTagsMessage($overlap, $plugins_to_check_against, $enabled_plugin_elements);
                $message = $checking_enabled ? $constraint->enabledPluginsMessage : $constraint->availablePluginsMessage;
                // Determine which element type is relevant for the violation message.
                assert(count($overlap->getAllowedElements(FALSE)) === 1);
                $overlap_tag = array_keys($overlap->getAllowedElements(FALSE))[0];
                $is_attr_overlap = self::tagHasAttributeRestrictions($overlap, $overlap_tag);
                // If the entirety (so not just the tag but also the attributes, and not
                // just some of the attribute values, but all of them) of the HTML
                // elements being configured to be edited via the Source Editing plugin
                // is supported by a CKEditor 5 plugin, complain. But if some attribute
                // or some attribute value is still not yet supported, do not generate a
                // violation message.
                // If there is overlap, but some attribute/attribute value is still not
                // supported, exit this iteration without generating a violation
                // message. Essentially: when assessing a particular value
                // (for example `<foo bar baz>`), only CKEditor 5 plugins providing an
                // exact match (`<foo bar baz>`) or a superset (`<foo bar baz qux>`) can
                // trigger a violation, not subsets (`<foo bar>`).
                if ($is_attr_overlap && !$source_enabled_element->diff($overlap)
                    ->allowsNothing()) {
                    continue;
                }
                // If there is overlap, but the plain tag is not supported in the
                // overlap, exit this iteration without generating a violation message.
                // Essentially when assessing a particular value (for example `<span>`),
                // CKEditor 5 plugins supporting only the creation of attributes on this
                // tag (`<span lang>`) and not supporting the creation of this plain tag
                // (`<span>` explicitly listed in their elements) can trigger a
                // violation.
                if (!$is_attr_overlap) {
                    $value_is_plain_tag_only = !self::tagHasAttributeRestrictions($source_enabled_element, $overlap_tag);
                    // When the configured value is a plain tag (`<tag>`): do not generate
                    // a violation message if this tag cannot be created by any CKEditor 5
                    // plugin.
                    if ($value_is_plain_tag_only && $overlap->intersect($plain_tags_to_check_against)
                        ->allowsNothing()) {
                        continue;
                    }
                    // When the configured value is not a plain tag (so the value has the
                    // shape `<tag attr>`, not `<tag>`): do not generate a violation
                    // message if the tag can already be created by another CKEditor 5
                    // plugin: this is just adding the ability to set more attributes.
                    // Note: this does not check whether the plain tag can indeed be
                    // created, validating that is out of scope for this validator.
                    // @see \Drupal\ckeditor5\Plugin\Validation\Constraint\FundamentalCompatibilityConstraintValidator::checkAllHtmlTagsAreCreatable()
                    if (!$value_is_plain_tag_only) {
                        continue;
                    }
                }
                // If we reach this, it means the entirety (so not just the tag but also
                // the attributes, and not just some of the attribute values, but all of
                // them) of the HTML elements being configured to be edited via the
                // Source Editing plugin's 'allowed_tags' configuration is supported by
                // a CKEditor 5 plugin. This earns a violation.
                $this->context
                    ->buildViolation($message)
                    ->setParameter('@element_type', $is_attr_overlap ? $this->t('attribute') : $this->t('tag'))
                    ->setParameter('%overlapping_tags', $tags_plugin_report)
                    ->addViolation();
            }
        }
    }
    
    /**
     * Inspects whether the given tag has attribute restrictions.
     *
     * @param \Drupal\ckeditor5\HTMLRestrictions $r
     *   A set of HTML restrictions to inspect.
     * @param string $tag_name
     *   The tag to check for attribute restrictions in $r.
     *
     * @return bool
     *   TRUE if the given tag has attribute restrictions, FALSE otherwise.
     */
    private static function tagHasAttributeRestrictions(HTMLRestrictions $r, string $tag_name) : bool {
        $all_elements = $r->getAllowedElements(FALSE);
        assert(isset($all_elements[$tag_name]));
        return is_array($r->getAllowedElements(FALSE)[$tag_name]);
    }
    
    /**
     * Creates a message listing plugins and the overlapping tags they provide.
     *
     * @param \Drupal\ckeditor5\HTMLRestrictions $overlap
     *   An array of overlapping tags.
     * @param \Drupal\ckeditor5\Plugin\CKEditor5PluginDefinition[] $plugin_definitions
     *   An array of plugin definitions where overlap was found.
     * @param \Drupal\ckeditor5\HTMLRestrictions $enabled_plugin_restrictions
     *   The set of HTML restrictions for all already enabled CKEditor 5 plugins.
     *
     * @return string
     *   A list of plugins that provide the overlapping tags.
     */
    private function pluginsSupplyingTagsMessage(HTMLRestrictions $overlap, array $plugin_definitions, HTMLRestrictions $enabled_plugin_restrictions) : string {
        $message_array = [];
        $message_string = '';
        foreach ($plugin_definitions as $definition) {
            if ($definition->hasElements()) {
                $plugin_capabilities = HTMLRestrictions::fromString(implode(' ', $definition->getElements()));
                // If this plugin supports wildcards, resolve them.
                if (!$plugin_capabilities->getWildcardSubset()
                    ->allowsNothing()) {
                    $plugin_capabilities = $plugin_capabilities->merge($enabled_plugin_restrictions)
                        ->diff($enabled_plugin_restrictions);
                }
                // Skip plugins that provide a subset, only mention the plugin that
                // actually provides the overlap.
                // For example: avoid listing the image alignment/captioning plugins
                // when matching `<img src>`; only lists the main image plugin.
                if (!$overlap->diff($plugin_capabilities)
                    ->allowsNothing()) {
                    continue;
                }
                foreach ($plugin_capabilities->intersect($overlap)
                    ->toCKEditor5ElementsArray() as $element) {
                    $message_array[(string) $definition->label()][] = $element;
                }
            }
        }
        foreach ($message_array as $plugin_label => $tag_list) {
            $tags_string = implode(', ', $tag_list);
            $message_string .= "{$plugin_label} ({$tags_string}), ";
        }
        return trim($message_string, ' ,');
    }

}

Classes

Title Deprecated Summary
SourceEditingRedundantTagsConstraintValidator Ensures tags already available via plugin are not be added to Source Editing.

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