StyleSensibleElementConstraintValidator.php
Same filename in other branches
Namespace
Drupal\ckeditor5\Plugin\Validation\ConstraintFile
-
core/
modules/ ckeditor5/ src/ Plugin/ Validation/ Constraint/ StyleSensibleElementConstraintValidator.php
View source
<?php
declare (strict_types=1);
namespace Drupal\ckeditor5\Plugin\Validation\Constraint;
// cspell:ignore enableable
use Drupal\ckeditor5\HTMLRestrictions;
use Drupal\ckeditor5\Plugin\CKEditor5Plugin\Style;
use Drupal\ckeditor5\Plugin\CKEditor5PluginDefinition;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
/**
* Styles can only be specified for HTML5 tags and extra classes.
*
* @internal
*/
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) : void {
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();
}
}
Classes
Title | Deprecated | Summary |
---|---|---|
StyleSensibleElementConstraintValidator | Styles can only be specified for HTML5 tags and extra classes. |
Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.