Skip to content

Commit

Permalink
fix: add target and rel attributes to external links to open in new t…
Browse files Browse the repository at this point in the history
…ab for links inside fields using the markdown_editor (ckeditor 5) format

Refs: RW-812
  • Loading branch information
orakili committed Sep 27, 2023
1 parent f1b683d commit 745e8cb
Show file tree
Hide file tree
Showing 2 changed files with 226 additions and 5 deletions.
89 changes: 84 additions & 5 deletions config/filter.format.markdown_editor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ langcode: en
status: true
dependencies:
module:
- editor
- media
- reliefweb_fields
- reliefweb_guidelines
- reliefweb_utility
name: 'Markdown with Editor'
format: markdown_editor
Expand All @@ -13,7 +16,7 @@ filters:
id: filter_html
provider: filter
status: true
weight: -10
weight: -49
settings:
allowed_html: '<br> <p> <h2> <h3> <h4> <h5> <h6> <strong> <em> <a href> <ul> <ol> <li>'
filter_html_help: false
Expand All @@ -22,24 +25,100 @@ filters:
id: filter_htmlcorrector
provider: filter
status: true
weight: 10
weight: -47
settings: { }
filter_markdown:
id: filter_markdown
provider: reliefweb_utility
status: true
weight: -20
weight: -50
settings: { }
reliefweb_token_filter:
id: reliefweb_token_filter
provider: reliefweb_utility
status: false
weight: 0
weight: -37
settings:
replace_empty: '0'
reliefweb_formatted_text:
id: reliefweb_formatted_text
provider: reliefweb_fields
status: true
weight: 0
weight: -48
settings: { }
reliefweb_external_link_filter:
id: reliefweb_external_link_filter
provider: reliefweb_utility
status: true
weight: -46
settings: { }
editor_file_reference:
id: editor_file_reference
provider: editor
status: false
weight: -44
settings: { }
filter_html_escape:
id: filter_html_escape
provider: filter
status: false
weight: -45
settings: { }
filter_url:
id: filter_url
provider: filter
status: false
weight: -40
settings:
filter_url_length: 72
filter_html_image_secure:
id: filter_html_image_secure
provider: filter
status: false
weight: -36
settings: { }
filter_image_lazy_load:
id: filter_image_lazy_load
provider: filter
status: false
weight: -35
settings: { }
filter_caption:
id: filter_caption
provider: filter
status: false
weight: -41
settings: { }
filter_autop:
id: filter_autop
provider: filter
status: false
weight: -42
settings: { }
filter_align:
id: filter_align
provider: filter
status: false
weight: -43
settings: { }
media_embed:
id: media_embed
provider: media
status: false
weight: -34
settings:
default_view_mode: default
allowed_view_modes: { }
allowed_media_types: { }
filter_guideline_link:
id: filter_guideline_link
provider: reliefweb_guidelines
status: false
weight: -39
settings: { }
filter_iframe:
id: filter_iframe
provider: reliefweb_utility
status: false
weight: -38
settings: { }
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
<?php

namespace Drupal\reliefweb_utility\Plugin\Filter;

use Drupal\Component\Render\MarkupInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\filter\FilterProcessResult;
use Drupal\filter\Plugin\FilterBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\RequestStack;

/**
* Provides a filter to add attributes to external links to open in a new tab.
*
* @Filter(
* id = "reliefweb_external_link_filter",
* title = @Translation("Open external Links in new tab."),
* description = @Translation("Add target and rel attributes to external links so they open in a new tab."),
* type = Drupal\filter\Plugin\FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE,
* )
*/
class ExternalLinkFilter extends FilterBase implements ContainerFactoryPluginInterface {

/**
* Current request.
*
* @var \Symfony\Component\HttpFoundation\RequestStack
*/
protected $requestStack;

/**
* Internal host pattern.
*
* @var string
*/
protected $internalHostPattern;

/**
* Constructs a markdown filter plugin.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
* The request stack.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, RequestStack $request_stack) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->requestStack = $request_stack;
}

/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('request_stack')
);
}

/**
* {@inheritdoc}
*/
public function process($text, $langcode) {
if (is_string($text) || $text instanceof MarkupInterface) {
// Adding this meta tag is necessary to tell \DOMDocument we are dealing
// with UTF-8 encoded html.
$flags = LIBXML_NONET | LIBXML_NOBLANKS | LIBXML_NOERROR | LIBXML_NOWARNING;
$meta = '<meta http-equiv="Content-Type" content="text/html; charset=utf-8">';
$prefix = '<!DOCTYPE html><html><head>' . $meta . '</head><body>';
$suffix = '</body></html>';
$dom = new \DOMDocument();
$dom->loadHTML($prefix . $text . $suffix, $flags);

// Try to get the body.
$links = $dom->getElementsByTagName('a');
foreach ($links as $link) {
$this->handleLink($link);
}

$html = $dom->saveHTML();

// Search for the body tag and return its content.
$start = mb_strpos($html, '<body>');
$end = mb_strrpos($html, '</body>');
if ($start !== FALSE && $end !== FALSE) {
$start += 6;
$text = trim(mb_substr($html, $start, $end - $start));
}
}
return new FilterProcessResult($text);
}

/**
* Check if a URL is an ReliefWeb URL.
*
* @param string $url
* URL to check.
*
* @return bool
* TRUE if the URL is internal.
*/
protected function isInternalUrl($url) {
if (empty($url)) {
return TRUE;
}

if (!isset($this->internalHostPattern)) {
$internal_hosts = [
preg_quote($this->requestStack->getCurrentRequest()->getHost()),
preg_quote('reliefweb.int'),
];

$this->internalHostPattern = '#^https?://(' . implode('|', $internal_hosts) . ')(/|$)#';
}

return preg_match('#^https?://#', $url) !== 1 ||
preg_match($this->internalHostPattern, $url) === 1;
}

/**
* Add the target and rel attributes to external links to open in a new tab.
*
* @param \DOMNode $node
* Link node.
*/
protected function handleLink(\DOMNode $node) {
$url = $node->getAttribute('href');

if (!$this->isInternalUrl($url)) {
$node->setAttribute('target', '_blank');
$node->setAttribute('rel', 'noreferrer noopener');
}
}

}

0 comments on commit 745e8cb

Please sign in to comment.