Skip to content
Snippets Groups Projects
Commit e5838c39 authored by Luke Leber's avatar Luke Leber
Browse files

Issue #3333973 by Luke.Leber: Add a HTML-purifier-less visual layout plugin

parent c68cd11e
Branches
Tags 2.2.0
Loading
......@@ -8,6 +8,7 @@
"source": "https://git.drupalcode.org/project/diff_plus"
},
"require":{
"caxy/php-htmldiff": "^0.1.14",
"drupal/diff": "^1.0",
"ext-dom": "*"
}
......
......@@ -14,6 +14,13 @@ services:
tags:
- { name: theme_negotiator, priority: 0 }
theme.negotiator.diff_plus_visual_inline_html5:
class: Drupal\diff_plus\Theme\VisualInlineHtml5ThemeNegotiator
arguments:
- '@theme_handler'
tags:
- { name: theme_negotiator, priority: 0 }
diff_plus.controller_alter:
class: Drupal\diff_plus\EventSubscriber\DiffControllerAlterSubscriber
arguments:
......
<?php
namespace Drupal\diff_plus\Plugin\diff\Layout;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\Xss;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Render\Markup;
use Drupal\diff\Controller\PluginRevisionController;
use Drupal\diff\Plugin\diff\Layout\VisualInlineDiffLayout;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides Layout Builder diff layout.
*
* @DiffLayoutBuilder(
* id = "visual_inline_html5",
* label = @Translation("Visual Inline (HTML5)"),
* description = @Translation("HTML-5 compatible visual layout, displays revision comparison using the entity type view mode."),
* )
*/
class VisualInlineHtml5DiffLayout extends VisualInlineDiffLayout {
/**
* An array of "safe" HTML tags to pass to the XSS filter.
*/
protected const HTML5_TAGS = [
'a', 'abbr', 'address', 'area', 'article', 'aside', 'audio', 'b', 'base',
'bdi', 'bdo', 'blockquote', 'body', 'br', 'button', 'canvas', 'caption',
'cite', 'code', 'col', 'colgroup', 'data', 'datalist', 'dd', 'del',
'details', 'dfn', 'dialog', 'div', 'dl', 'dt', 'em', 'embed', 'fieldset',
'figcaption', 'figure', 'footer', 'form', 'head', 'header', 'hgroup',
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'html', 'i', 'iframe', 'img',
'input', 'ins', 'kbd', 'keygen', 'label', 'legend', 'li', 'link', 'main',
'map', 'mark', 'menu', 'menuitem', 'meta', 'meter', 'nav', 'noscript',
'object', 'ol', 'optgroup', 'option', 'output', 'p', 'param', 'picture',
'pre', 'progress', 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'section',
'select', 'small', 'source', 'span', 'strong', 'style', 'sub', 'summary',
'sup', 'svg', 'table', 'tbody', 'td', 'template', 'textarea', 'tfoot',
'th', 'thead', 'time', 'title', 'tr', 'track', 'u', 'ul', 'var', 'video',
'wbr',
];
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
$instance = parent::create($container, $configuration, $plugin_id, $plugin_definition);
$instance->htmlDiff->getConfig()->setPurifierEnabled(FALSE);
return $instance;
}
/**
* Preprocesses the provided markup to provide a normalization layer.
*
* @param string $markup
* The markup to normalize.
*
* @return string
* The normalized markup.
*/
protected function preprocessMarkup($markup) {
// The diff library has issues with comments, so strip them.
$dom = Html::load($markup);
$xpath = new \DOMXPath($dom);
foreach ($xpath->query('//comment()') as $element) {
$element->parentNode->removeChild($element);
}
return Html::serialize($dom);
}
/**
* {@inheritdoc}
*/
public function build(ContentEntityInterface $left_revision, ContentEntityInterface $right_revision, ContentEntityInterface $entity) {
$build = parent::build($left_revision, $right_revision, $entity);
// Fix the view mode selection links.
foreach ($build['controls']['view_mode']['filter']['#links'] as $view_mode => &$link) {
$link['url'] = PluginRevisionController::diffRoute(
$entity,
$left_revision->getRevisionId(),
$right_revision->getRevisionId(),
'visual_inline_html5',
['view_mode' => $view_mode ?: $this->requestStack->getCurrentRequest()->query->get('view_mode', 'default')]
);
}
// Swap out the stock visual diff with a new one.
$this->htmlDiff->setOldHtml(
$this->preprocessMarkup(
$this->htmlDiff->getOldHtml()
)
);
$this->htmlDiff->setNewHtml(
$this->preprocessMarkup(
$this->htmlDiff->getNewHtml()
)
);
$this->htmlDiff->build();
// @todo Ask the security team about whether this is 100% safe.
$build['diff']['#markup'] = Markup::create(
Xss::filter(
$this->htmlDiff->getDifference(),
static::HTML5_TAGS
),
);
return $build;
}
}
<?php
namespace Drupal\diff_plus\Theme;
use Drupal\Core\Extension\ThemeHandlerInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Theme\ThemeNegotiatorInterface;
/**
* Ensures that the default theme is used for raw HTML diffs.
*/
class VisualInlineHtml5ThemeNegotiator implements ThemeNegotiatorInterface {
/**
* The theme handler service.
*
* @var \Drupal\Core\Extension\ThemeHandlerInterface
*/
protected $themeHandler;
/**
* Creates a theme negotiator instance.
*
* @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler
* The theme handler service.
*/
public function __construct(ThemeHandlerInterface $theme_handler) {
$this->themeHandler = $theme_handler;
}
/**
* {@inheritdoc}
*/
public function applies(RouteMatchInterface $route_match) {
return $route_match->getRouteName() === 'diff.revisions_diff' && $route_match->getParameter('filter') === 'visual_inline_html5';
}
/**
* {@inheritdoc}
*/
public function determineActiveTheme(RouteMatchInterface $route_match) {
return $this->themeHandler->getDefault();
}
}
......@@ -64,7 +64,7 @@ class DiffPlusRawHtmlLayoutPluginTest extends WebDriverTestBase {
// Create a node and a pair of revisions.
$node = $this->drupalCreateNode([
'title' => t('Hello, world!'),
'title' => 'Hello, world!',
'type' => 'article',
'body' => [
'value' => '<p>This is a test!</p>',
......
<?php
namespace Drupal\Tests\diff_plus\FunctionalJavascript;
use Drupal\filter\Entity\FilterFormat;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
use Drupal\Tests\node\Traits\NodeCreationTrait;
/**
* Contains test cases for the raw html diff layout.
*
* @group diff_plus
*/
class DiffPlusVisualInlineHtml5DiffLayoutTest extends WebDriverTestBase {
use NodeCreationTrait;
use ContentTypeCreationTrait;
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected static $modules = [
'node',
'diff',
'diff_plus',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Enable the layout plugin.
$diff_settings = $this->config('diff.settings')->get();
$diff_settings['general_settings']['layout_plugins']['visual_inline_html5'] = [
'enabled' => TRUE,
'weight' => 8,
];
$this
->config('diff.settings')
->setData($diff_settings)
->save();
// Create a filter format for the body field.
$filtered_html_format = FilterFormat::create([
'format' => 'unrestricted_html',
'name' => 'Unrestricted HTML',
]);
$filtered_html_format->save();
// Create a content type.
$this->drupalCreateContentType([
'type' => 'article',
'name' => 'Article',
]);
// Create a node and a pair of revisions.
$node = $this->drupalCreateNode([
'title' => 'Hello, world!',
'type' => 'article',
'body' => [
'value' => '<p>This is a test!</p>',
'format' => 'unrestricted_html',
],
]);
$node->set('body', [
'value' => '<p>This is a test!</p><p>This is a test too!</p>',
'format' => 'unrestricted_html',
]);
$node->setNewRevision();
$node->save();
}
/**
* Test case for the Raw HTML diff layout.
*/
public function testRawHtmlLayoutPlugin() {
$this->drupalLogin($this->drupalCreateUser([], NULL, TRUE));
$this->drupalGet('/node/1/revisions/view/1/2/visual_inline_html5');
$page = $this->getSession()->getPage();
$insertion = $page->find('css', '.diffins');
static::assertSame('This is a test too!', $insertion->getText());
}
}
<?php
namespace Drupal\Tests\diff_plus\Unit;
use Drupal\Core\Extension\ThemeHandlerInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\diff_plus\Theme\VisualInlineHtml5ThemeNegotiator;
use Drupal\Tests\UnitTestCase;
/**
* Contains test cases for the visual inline (html5) theme negotiator.
*
* @group diff_plus
*/
class VisualInlineHtml5ThemeNegotiatorTest extends UnitTestCase {
/**
* The subject under test.
*
* @var \Drupal\diff_plus\Theme\VisualInlineHtml5ThemeNegotiator
*/
protected $instance;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$theme_handler = $this->createMock(ThemeHandlerInterface::class);
$theme_handler->method('getDefault')->willReturn('olivero');
$this->instance = new VisualInlineHtml5ThemeNegotiator($theme_handler);
}
/**
* Test case for the ::applies method with a non-diff route.
*/
public function testAppliesNonDiffRoute() {
$route_match = $this->createMock(RouteMatchInterface::class);
$route_match->method('getRouteName')->willReturn('entity.node.canonical');
static::assertFalse($this->instance->applies($route_match));
}
/**
* Test case for the ::applies method with a non-visual-inline-html filter.
*/
public function testAppliesDiffRouteDifferentFilter() {
$route_match = $this->createMock(RouteMatchInterface::class);
$route_match->method('getRouteName')->willReturn('diff.revisions_diff');
$route_match->method('getParameter')->willReturn('visual');
static::assertFalse($this->instance->applies($route_match));
}
/**
* Test case for the ::applies method with a visual inline (html5) filter.
*/
public function testAppliesDiffRouteVisualInlineHtml5Filter() {
$route_match = $this->createMock(RouteMatchInterface::class);
$route_match->method('getRouteName')->willReturn('diff.revisions_diff');
$route_match->method('getParameter')->willReturn('visual_inline_html5');
static::assertTrue($this->instance->applies($route_match));
}
/**
* Test case for the ::determineActiveTheme method.
*/
public function testDetermineActiveTheme() {
$route_match = $this->createMock(RouteMatchInterface::class);
static::assertEquals('olivero', $this->instance->determineActiveTheme($route_match));
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment