Skip to content
Snippets Groups Projects
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
VisualInlineHtml5DiffLayout.php 6.43 KiB
<?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',
  ];

  /**
   * The config factory service.
   *
   * @var \Drupal\Core\Config\ConfigFactoryInterface
   */
  protected $configFactory;

  /**
   * The current user.
   *
   * @var \Drupal\Core\Session\AccountProxyInterface
   */
  protected $currentUser;

  /**
   * The user data service.
   *
   * @var \Drupal\user\UserDataInterface
   */
  protected $userData;

  /**
   * The account switcher service.
   *
   * @var \Drupal\Core\Session\AccountSwitcherInterface
   */
  protected $accountSwitcher;

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

    $instance->configFactory = $container->get('config.factory');
    $instance->currentUser = $container->get('current_user');
    $instance->userData = $container->get('user.data');
    $instance->accountSwitcher = $container->get('account_switcher');

    return $instance;
  }

  /**
   * Gets the settings to render the diff with.
   *
   * @return array
   *   The settings to render the diff with.
   */
  protected function getDiffSettings() {
    $settings = $this->configFactory->get('diff_plus.settings')->get();
    if ($this->currentUser->hasPermission('personalize diff plus settings')) {
      $settings = array_replace_recursive(
        $settings,
        $this->userData->get('diff_plus', $this->currentUser->id(), 'settings') ?? []
      );
    }
    return $settings;
  }

  /**
   * Preprocesses the provided markup to provide a normalization layer.
   *
   * @param string $markup
   *   The markup to normalize.
   * @param array $settings
   *   The diff layout settings.
   *
   * @return string
   *   The normalized markup.
   */
  protected function preprocessMarkup($markup, array $settings) {

    // 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);
    }

    // Remove all script tags before Xss::filter can mess things up.
    $script_tags = $dom->getElementsByTagName('script');
    while ($script_tags->count()) {
      $script_tag = $script_tags->item(0);
      $script_tag->parentNode->removeChild($script_tag);
    }

    // Work around a bug in Xss::filter.
    if ($settings['visual_html5_preserve_inline_styles']) {
      $elements_with_inline_styles = $xpath->query('//*[@style]');
      foreach ($elements_with_inline_styles as $element_with_inline_styles) {
        $element_with_inline_styles->setAttribute(
          'data-diff-plus-style',
          $element_with_inline_styles->getAttribute('style')
        );
      }
    }

    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')]
      );
    }

    $settings = $this->getDiffSettings();

    // Swap out the stock visual diff with a new one.
    $this->htmlDiff->setOldHtml(
      $this->preprocessMarkup(
        $this->htmlDiff->getOldHtml(),
        $settings
      )
    );

    $this->htmlDiff->setNewHtml(
      $this->preprocessMarkup(
        $this->htmlDiff->getNewHtml(),
        $settings
      )
    );

    $this->htmlDiff->build();

    $markup = Xss::filter($this->htmlDiff->getDifference(), static::HTML5_TAGS);

    if ($settings['visual_html5_preserve_inline_styles']) {
      $dom = Html::load($markup);
      $xpath = new \DOMXPath($dom);
      $elements_with_inline_styles = $xpath->query('//*[@data-diff-plus-style]');
      foreach ($elements_with_inline_styles as $element_with_inline_styles) {
        $element_with_inline_styles->setAttribute(
          'style',
          $element_with_inline_styles->getAttribute('data-diff-plus-style')
        );
      }
      $markup = Html::serialize($dom);
    }

    // @todo Ask the security team about whether this is 100% safe.
    $build['diff']['#markup'] = Markup::create($markup);
    return $build;
  }

}