Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • project/obfuscate
  • issue/obfuscate-3280543
  • issue/obfuscate-3369574
  • issue/obfuscate-3433660
  • issue/obfuscate-3471561
  • issue/obfuscate-3366415
  • issue/obfuscate-3506101
7 results
Show changes
Commits on Source (3)
// Propaganistas provides the Rot13 in the global namespace.
// So it has been adapted here an wrapped for Drupal.
// The inline <script> has been replaced as well.
(function ($, Drupal, once) {
(function (Drupal, once) {
'use strict';
var dummyElement = document.createElement('div');
function transformRot13(rot13Email) {
let result = rot13Email.replace(/[A-Za-z]/g, function (c) {
return String.fromCharCode(c.charCodeAt(0) + (c.toUpperCase() <= 'M' ? 13 : -13));
......@@ -16,19 +18,57 @@
function init(element) {
// Remove the css fallback
$(element).find('.js-disabled').remove();
var toRemove = element.querySelector('.js-disabled');
if (toRemove) {
toRemove.remove();
}
// Transform back the rot 13
var rot13Element = $(element).find('.js-enabled');
rot13Element.show();
var transformedEmail = transformRot13(rot13Element.text());
rot13Element.html('<a href="mailto:'+transformedEmail+'">'+transformedEmail+'</a>');
var rot13Element = element.querySelector('.js-enabled');
if (!rot13Element) {
return;
}
var email = transformRot13(rot13Element.textContent);
var parts = {
innerhtml: email,
type: 'plain',
pre: '',
post: '',
query: '',
q: '',
};
for (var attrib in rot13Element.dataset) {
if (attrib == 'innerhtml') {
parts[attrib] = transformRot13(rot13Element.dataset[attrib]);
}
else {
parts[attrib] = rot13Element.dataset[attrib];
}
}
if (parts.type == 'mailto') {
if (parts.query) {
parts.q = '?';
}
// If the innerHTML is HTML, it will have been encoded entities and the
// actual HTML will get displayed. Make it "real" HTML.
dummyElement.innerHTML = parts.innerhtml;
element.outerHTML = `<a ${parts.pre} href="mailto:${email}${parts.q}${parts.query}" ${parts.post}>${dummyElement.textContent}</a>`;
}
else {
element.outerHTML = email;
}
element.style.display = 'block';
}
Drupal.behaviors.obfuscateRot13 = {
attach: function (context) {
let elements = jQuery(once('.boshfpngr-e13', context)).each(function () {
init(this);
var elements = once('boshfpngr-e13', '.boshfpngr-e13', context);
elements.forEach(function (e) {
init(e);
});
}
};
})(jQuery, Drupal, once);
})(Drupal, once);
name: Obfuscate
type: module
description: Obfuscates email addresses with a Field Formatter of Email field, a text Filter and a Twig extension.
core_version_requirement: ^9 || ^10
core_version_requirement: ^10.1 || ^11
package: Input filters
configure: obfuscate.obfuscate_config_form
dependencies:
......
......@@ -7,4 +7,3 @@ rot13:
js/rot13.js: { }
dependencies:
- core/once
- core/jquery
<?php
namespace Drupal\obfuscate;
use Drupal\Component\Utility\Html;
use Drupal\filter\FilterProcessResult;
/**
* Trait for extracting email addresses and mailto links.
*
* Trait ObfuscateExtractEmailAndLinksTrait
*/
trait ObfuscateExtractEmailAndLinksTrait {
// These will help us deal with inline images, which if very large
// break the preg_match and preg_replace.
static $PATTERN_IMG_INLINE = '/data\:(?:.+?)base64(?:.+?)["|\']/';
static $PATTERN_IMG_PLACEHOLDER = '__obfuscate_img_placeholder__';
/**
* Safeguard pattern to operate replacements.
*/
static $SAFEGUARD = '!%%$$';
/**
* Stores the original element to restore.
*
* @var array
*/
private $elementsQueue = [];
/**
* Replace inline images with a placeholder.
*
* @param string $text
* The text to be processed.
*
* @return array
* The text with images replaced and the images.
*/
public function replaceImagesWithPlaceholders(string $text) : array {
// HTML image tags need to be handled separately, as they may contain base64
// encoded images slowing down the email regex function.
// Therefore, remove all image contents and add them back later.
// See https://drupal.org/node/1243042 for details.
$images = [[]];
preg_match_all(self::$PATTERN_IMG_INLINE, $text, $images);
$text = preg_replace(self::$PATTERN_IMG_INLINE, self::$PATTERN_IMG_PLACEHOLDER, $text);
return [ $text, $images];
}
/**
* Restore images that where replaced by placeholders.
*
* @param string $text
* The placeholdered text.
* @param array $images
* The images.
*
* @return string $text
* The text with placeholders replaced.
*/
public function restorePlaceholderedImages(string $text, array $images) : string {
// Revert back to the original image contents.
foreach ($images[0] as $image) {
$text = preg_replace('/' . self::$PATTERN_IMG_PLACEHOLDER . '/', $image, $text, 1);
}
return $text;
}
/**
* Returns main pattern.
*
* Set up a regex constant to split an email address into name and domain
* parts. The following pattern is not perfect (who is?), but is intended to
* intercept things which look like email addresses. It is not intended to
* determine if an address is valid. It will not intercept addresses with
* quoted local parts.
*
* @return string
* Main pattern.
*/
private function getPatternMain() {
return "([-\.\~\'\!\#\$\%\&\+\/\*\=\?\^\_\`\{\|\}\w\+^@]+)"
// @
. '@'
// Group 2.
. '((?:'
// One or more letters or dashes followed by a dot.
. '[-\w]+\.'
// The whole thing one or more times.
. ')+'
// With between 2 and 63 letters at the end (NB new TLDs)
. '[A-Z]{2,63})';
}
/**
* Returns pattern email bare.
*
* Top and tail the email regexp it so that it is case insensitive and
* ignores whitespace.
*
* @return string
* Bare email pattern.
*/
private function getPatternEmailBare() {
return '!' . $this->getPatternMain() . '!ix';
}
/**
* Returns pattern email with options.
*
* Options such as subject or body
* e.g. <a href="mailto:email@example.com?subject=Hi there!&body=Dear Sir">
*
* @return string
* Email with options pattern.
*/
private function getPatternEmailWithOptions() {
return '!' . $this->getPatternMain() . '\[(.*?)\]!ix';
}
/**
* Returns patterns mailto.
*
* Next set up a regex for mailto: URLs.
* - see http://www.faqs.org/rfcs/rfc2368.html
* This captures the whole mailto: URL into the second group,
* the name into the third group and the domain into
* the fourth. The tag contents go into the fifth.
*
* @return string
* Mailto pattern.
*/
private function getPatternMailto() {
// Opening <a and spaces.
return '!<a\s+'
// Any attributes.
. "((?:\w+\s*=\s*)(?:\w+|\"[^\"]*\"|'[^']*'))*?"
// whitespace.
. '\s*'
// The href attribute.
. "href\s*=\s*(['\"])(mailto:"
// The email address.
. $this->getPatternMain()
// An optional ? followed.
. "(?:\?[A-Za-z0-9_= %\.\-\~\_\&;\!\*\(\)\\'#&]*)?)"
// By a query string. NB
// we allow spaces here,
// even though strictly
// they should be URL
// encoded
// the relevant quote.
. '\\2'
// Character
// any more attributes.
. "((?:\s+\w+\s*=\s*)(?:\w+|\"[^\"]*\"|'[^']*'))*?"
// End of the first tag.
. '>'
// Tag contents. NB this.
. '(.*?)'
// Will not work properly
// if there is a nested
// <a>, but this is not
// valid xhtml anyway.
// closing tag.
. '</a>!ixs';
}
/**
* Safeguards ROT 13 obfuscated emails.
*
* Applies a safeguard based on an index to preserve already
* obfuscated emails from further alteration.
*
* @param string $text
* The text that may contain ROT 13 obfuscated emails.
*
* @return \DOMDocument
* The ROT 13 safeguarded DOM.
*/
private function rot13Safeguard($text) {
// Reset.
$this->elementsQueue = [];
$dom = Html::load($text);
$xPath = new \DOMXPath($dom);
$index = 0;
/** @var \DOMElement $domElement */
foreach ($xPath->query('//span[contains(@class,"boshfpngr-e13")]') as $domElement) {
$this->elementsQueue[] = $domElement;
$safeguardElement = $dom->createElement('span', 'tmp');
$safeguardElement->setAttribute('id', $this->getUniqueSafeguard($index));
$domElement->parentNode->replaceChild($safeguardElement, $domElement);
$index++;
}
$dom->saveHTML($dom->documentElement);
return $dom;
}
/**
* Restores the ROT 13 safeguarded values.
*
* @param string $text
* The ROT 13 safeguarded text.
*
* @return \DOMDocument
* The restored DOM that may contain ROT 13 obfuscated emails.
*/
private function restoreRot13Safeguard($text) {
$dom = Html::load($text);
$xPath = new \DOMXPath($dom);
/** @var \DOMElement $domElement */
foreach ($this->elementsQueue as $index => $domElement) {
$safeguardElements = $xPath->query("//span[@id='" . $this->getUniqueSafeguard($index) . "']");
/** @var \DOMElement $safeguardElement */
$safeguardElement = $safeguardElements[0];
if ($safeguardElement instanceof \DOMElement && $domElement instanceof \DOMElement) {
$node = $dom->importNode($domElement, TRUE);
$dom->documentElement->appendChild($node);
$safeguardElement->parentNode->replaceChild($node, $safeguardElement);
}
}
$dom->saveHTML($dom->documentElement);
return $dom;
}
/**
* Returns an unique safeguard to identify the element to replace back.
*
* @param int $index
* Index of the element to replace.
*
* @return string
* Safeguard with index.
*/
private function getUniqueSafeguard($index) {
return self::$SAFEGUARD . $index . strrev(self::$SAFEGUARD);
}
/**
* Callback function for preg_replace_callback on getPatternEmailBare.
*
* @param array $matches
* An array containing parts of an email address.
*
* @return string
* The span with which to replace the email address.
*/
public function callbackBareEmailAddresses(array $matches) {
return $this->output($matches[1] . '@' . $matches[2],'', []);
}
/**
* Callback function for preg_replace_callback on getPatternMailto.
*
* Replace an email addresses which has been found with the appropriate
* <span> tags.
*
* @param array $matches
* An array containing parts of an email address or mailto: URL.
*
* @return string
* The span with which to replace the email address.
*/
public function callbackMailto(array $matches) {
$extra = [ 'type' => 'mailto' ];
// Take the mailto: URL in $matches[3] and split the query string
// into its component parts, putting them in $headers as
// [0]=>"header=contents" etc. We cannot use parse_str because
// the query string might contain dots.
// Single quote can be encoded as &#039; which breaks parse_url
// Replace it back to a single quote which is perfectly valid.
$matches[3] = str_replace("&#039;", '\'', $matches[3]);
$query = parse_url($matches[3], PHP_URL_QUERY);
if ($query) {
$extra['query'] = str_replace('&amp;', '&', $query);
}
// Take all <a> attributes except the href and put them into an extra attrib.
// Before href.
if (!empty($matches[1])) {
$matches[1] = trim($matches[1]);
$extra['pre'] = $matches[1];
}
// After href.
if (!empty($matches[6])) {
$matches[6] = trim($matches[6]);
$extra['post'] = $matches[6];
}
return $this->output($matches[4] . '@' . $matches[5], $matches[7], $extra);
}
/**
* Callback function for preg_replace_callback on getPatternEmailWithOptions.
*
* @param array $matches
* An array containing parts of an email address.
*
* @return string
* The span with which to replace the email address.
*/
public function callbackEmailAddressesWithOptions(array $matches) {
$vars = [];
if (!empty($matches[3])) {
$options = explode('|', $matches[3]);
if (!empty($options[0])) {
$custom_form_url = trim($options[0]);
if (!empty($custom_form_url)) {
$vars['custom_form_url'] = $custom_form_url;
}
}
if (!empty($options[1])) {
$custom_displaytext = trim($options[1]);
if (!empty($custom_displaytext)) {
$vars['custom_displaytext'] = $custom_displaytext;
}
}
}
return $this->output($matches[1], $matches[2], '', '', $vars);
}
/**
* A helper function for the callbacks.
*
* Obfuscates the email address with the method chosen from the
* system wide configuration.
*
* @param string $email
* The email address.
* @param string $contents
* The contents of any <a> tag.
* @param array $extra
* Extra attributes to be added to the output wrapper as data.
*
* @return string
* The obfuscated email address as a link.
*/
private function output($email, $contents, array $extra) {
/** @var \Drupal\obfuscate\ObfuscateMail $obfuscateMail */
$obfuscateMail = \Drupal::service('obfuscate_mail');
/** @var \Drupal\Core\Render\RendererInterface $renderer */
$renderer = \Drupal::service('renderer');
$output = $obfuscateMail->getObfuscatedLink($email, $contents, $extra);
// @todo implement spamspan coverage of contents and headers.
return $renderer->render($output);
}
/**
* {@inheritdoc}
*/
public function process($text, $langcode = '') {
[$text, $images] = $this->replaceImagesWithPlaceholders($text);
// Now we can convert all mailto URLs.
$config = \Drupal::config('obfuscate.settings');
$method = $config->get('obfuscate')['method'];
if ($method == 'html_entity') {
$text = preg_replace_callback($this->getPatternMailto(), [$this, 'callbackMailto'], $text);
}
else {
// Apply then the email obfuscation.
$text = preg_replace_callback($this->getPatternMailto(), [$this, 'callbackMailto'], $text);
$dom = $this->rot13Safeguard($text);
$text = Html::serialize($dom);
$text = preg_replace_callback($this->getPatternEmailBare(), [$this, 'callbackBareEmailAddresses'], $text);
// Set then back the safeguarded obfuscated emails.
$newDom = $this->restoreRot13Safeguard($text);
$text = Html::serialize($newDom);
}
$text = $this->restorePlaceholderedImages($text, $images);
// @TODO FilterProcessResult should only really be used for the Filter
// patch. It doesn't seem to cause any issues at the moment but...
$result = new FilterProcessResult($text);
// Libraries are not attached via the template in this case.
if ($method == 'rot_13') {
$result->setAttachments([
'library' => [
'obfuscate/rot13',
],
]);
}
return $result;
}
}
\ No newline at end of file
......@@ -37,15 +37,15 @@ class ObfuscateMail implements ObfuscateMailInterface {
/**
* {@inheritdoc}
*/
public function getObfuscatedLink($email, array $params = [], $text = '') {
return $this->obfuscateMailMethod->getObfuscatedLink($email, $params, $text);
public function getObfuscatedLink($email, $text = '', $extra = []) {
return $this->obfuscateMailMethod->getObfuscatedLink($email, $text, $extra);
}
/**
* {@inheritdoc}
*/
public function obfuscateEmail($email) {
return $this->obfuscateMailMethod->obfuscateEmail($email);
public function obfuscateEmail($email, $text = '') {
return $this->obfuscateMailMethod->obfuscateEmail($email, $text);
}
}
......@@ -14,13 +14,10 @@ class ObfuscateMailHtmlEntity implements ObfuscateMailInterface {
/**
* {@inheritdoc}
*/
public function getObfuscatedLink($email, array $params = [], $text = '') {
if (!is_array($params)) {
$params = [];
}
public function getObfuscatedLink($email, $text = '', $extra = []) {
// Tell search engines to ignore obfuscated uri.
if (!isset($params['rel'])) {
if (!isset($extra['rel'])) {
$params['rel'] = 'nofollow';
}
......@@ -55,7 +52,7 @@ class ObfuscateMailHtmlEntity implements ObfuscateMailInterface {
// @todo use twig template to allow override
$link = '<a href="' . $obfuscatedEmailUrl . '"';
foreach ($params as $param => $value) {
foreach ($extra as $param => $value) {
$link .= ' ' . $param . '="' . htmlspecialchars($value) . '"';
}
$link .= '>' . $innerHtml . '</a>';
......
......@@ -14,21 +14,16 @@ namespace Drupal\obfuscate;
*/
class ObfuscateMailROT13 implements ObfuscateMailInterface {
/**
* ROT 13 class name of 'obfuscate-r13'.
*
* This is not a dependency of ROT 13, it is merely a way
* to remove hints for spammers.
*/
const OBFUSCATE_ROT_13_CSS_CLASS = 'boshfpngr-e13';
// Safeguard string.
const SAFEGUARD = '$%$!!$%$';
/**
* {@inheritdoc}
*/
public function getObfuscatedLink($email, array $params = [], $text = '') {
public function getObfuscatedLink($email, $text = '', $extra = []) {
$build = [
'#theme' => 'email_rot13_link',
'#link' => $this->obfuscateEmail($email),
'#link' => $this->obfuscateEmail($email, $text, $extra),
];
return $build;
}
......@@ -36,56 +31,34 @@ class ObfuscateMailROT13 implements ObfuscateMailInterface {
/**
* {@inheritdoc}
*/
public function obfuscateEmail($string) {
public function obfuscateEmail($email, $text = '', $extra = []) {
// Propaganistas vendor provides this method as a global function.
// So it is copied here instead of using Composer.
// The inline <script> and <noscript> and inline styles
// for css fallback have been replaced as well.
// Casting $string to a string allows passing of objects
// implementing the __toString() magic method.
$string = (string) $string;
// Safeguard string.
$safeguard = '$%$!!$%$';
// Define patterns for extracting emails.
// The vendor selection pattern has been simplified because
// most of the work has already been done and at this stage the string
// that is being passed is already an email address.
$patterns = [
// Plain emails.
'|[_a-z0-9-]+(?:\.[_a-z0-9-]+)*@[a-z0-9-]+(?:\.[a-z0-9-]+)*(?:\.[a-z]{2,3})|i',
];
foreach ($patterns as $pattern) {
$string = preg_replace_callback($pattern, function ($parts) use ($safeguard) {
$email = (string) $email;
// Clean up element parts.
$parts = array_map('trim', $parts);
if ($text) {
$extra['innerHTML'] = $text;
}
// ROT13 implementation for JS-enabled browsers.
$js = '<span class="js-enabled">' . str_rot13($parts[0]) . '</span>';
array_walk($extra, function(&$value, $key) {
$escaped = htmlspecialchars($value, ENT_QUOTES);
if ($key == 'innerHTML') {
$escaped = str_rot13($escaped);
}
$value = "data-{$key}='{$escaped}'";
});
$data = implode(' ', $extra);
// Reversed direction implementation for non-JS browsers.
if (stripos($parts[0], '<a') === 0) {
// Mailto tag; if link content equals the email,
// just display the email, otherwise display a formatted string.
$nojs = ($parts[1] == $parts[3]) ? $parts[1] : (' > ' . $parts[1] . ' < ' . $parts[3]);
}
else {
// Plain email; display the plain email.
$nojs = $parts[0];
}
$nojs = '<span class="js-disabled">' . strrev($nojs) . '</span>';
$email = trim($email);
// Safeguard the obfuscation so it won't get picked up
// by the next iteration.
return str_replace('@', $safeguard, $js . $nojs);
}, $string);
}
$js = "<span class='js-enabled' {$data}>" . str_rot13($email) . '</span>';
$nojs = '<span class="js-disabled">' . strrev($email) . '</span>';
// Revert all safeguards.
return str_replace($safeguard, '@', $string);
return $js . $nojs;
}
}
......@@ -7,6 +7,7 @@ use Drupal\Core\Field\FieldItemInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\FormatterBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\obfuscate\ObfuscateExtractEmailAndLinksTrait;
use Drupal\obfuscate\ObfuscateMailFactory;
/**
......@@ -21,6 +22,7 @@ use Drupal\obfuscate\ObfuscateMailFactory;
* )
*/
class ObfuscateFieldFormatter extends FormatterBase {
use ObfuscateExtractEmailAndLinksTrait;
/**
* {@inheritdoc}
......@@ -94,6 +96,8 @@ class ObfuscateFieldFormatter extends FormatterBase {
$linkLabel = $token->replace($linkLabel, [$entity->getEntityTypeId() => $entity]);
}
foreach ($items as $delta => $item) {
// @TODO Use process here. Need to consider the linkLabel.
//$elements[$delta] = $this->process($item->value);
$elements[$delta] = $obfuscateMail->getObfuscatedLink($item->value, [], $linkLabel);
}
return $elements;
......
......@@ -5,6 +5,7 @@ namespace Drupal\obfuscate\Plugin\Filter;
use Drupal\filter\FilterProcessResult;
use Drupal\filter\Plugin\FilterBase;
use Drupal\Component\Utility\Html;
use Drupal\obfuscate\ObfuscateExtractEmailAndLinksTrait;
/**
* Provides a filter to obfuscate email addresses.
......@@ -20,122 +21,7 @@ use Drupal\Component\Utility\Html;
* )
*/
class ObfuscateMail extends FilterBase {
// These will help us deal with inline images, which if very large
// break the preg_match and preg_replace.
const PATTERN_IMG_INLINE = '/data\:(?:.+?)base64(?:.+?)["|\']/';
const PATTERN_IMG_PLACEHOLDER = '__obfuscate_img_placeholder__';
/**
* Safeguard pattern to operate replacements.
*/
const SAFEGUARD = '!%%$$';
/**
* Stores the original element to restore.
*
* @var array
*/
private $elementsQueue = [];
/**
* Returns main pattern.
*
* Set up a regex constant to split an email address into name and domain
* parts. The following pattern is not perfect (who is?), but is intended to
* intercept things which look like email addresses. It is not intended to
* determine if an address is valid. It will not intercept addresses with
* quoted local parts.
*
* @return string
* Main pattern.
*/
private function getPatternMain() {
return "([-\.\~\'\!\#\$\%\&\+\/\*\=\?\^\_\`\{\|\}\w\+^@]+)"
// @
. '@'
// Group 2.
. '((?:'
// One or more letters or dashes followed by a dot.
. '[-\w]+\.'
// The whole thing one or more times.
. ')+'
// With between 2 and 63 letters at the end (NB new TLDs)
. '[A-Z]{2,63})';
}
/**
* Returns pattern email bare.
*
* Top and tail the email regexp it so that it is case insensitive and
* ignores whitespace.
*
* @return string
* Bare email pattern.
*/
private function getPatternEmailBare() {
return '!' . $this->getPatternMain() . '!ix';
}
/**
* Returns pattern email with options.
*
* Options such as subject or body
* e.g. <a href="mailto:email@example.com?subject=Hi there!&body=Dear Sir">
*
* @return string
* Email with options pattern.
*/
private function getPatternEmailWithOptions() {
return '!' . $this->getPatternMain() . '\[(.*?)\]!ix';
}
/**
* Returns patterns mailto.
*
* Next set up a regex for mailto: URLs.
* - see http://www.faqs.org/rfcs/rfc2368.html
* This captures the whole mailto: URL into the second group,
* the name into the third group and the domain into
* the fourth. The tag contents go into the fifth.
*
* @return string
* Mailto pattern.
*/
private function getPatternMailto() {
// Opening <a and spaces.
return '!<a\s+'
// Any attributes.
. "((?:\w+\s*=\s*)(?:\w+|\"[^\"]*\"|'[^']*'))*?"
// whitespace.
. '\s*'
// The href attribute.
. "href\s*=\s*(['\"])(mailto:"
// The email address.
. $this->getPatternMain()
// An optional ? followed.
. "(?:\?[A-Za-z0-9_= %\.\-\~\_\&;\!\*\(\)\\'#&]*)?)"
// By a query string. NB
// we allow spaces here,
// even though strictly
// they should be URL
// encoded
// the relevant quote.
. '\\2'
// Character
// any more attributes.
. "((?:\s+\w+\s*=\s*)(?:\w+|\"[^\"]*\"|'[^']*'))*?"
// End of the first tag.
. '>'
// Tag contents. NB this.
. '(.*?)'
// Will not work properly
// if there is a nested
// <a>, but this is not
// valid xhtml anyway.
// closing tag.
. '</a>!ix';
}
use ObfuscateExtractEmailAndLinksTrait;
/**
* {@inheritdoc}
......@@ -144,249 +30,4 @@ class ObfuscateMail extends FilterBase {
return $this->t('Each email address will be obfuscated with the system wide configuration.');
}
/**
* {@inheritdoc}
*/
public function process($text, $langcode) {
// HTML image tags need to be handled separately, as they may contain base64
// encoded images slowing down the email regex function.
// Therefore, remove all image contents and add them back later.
// See https://drupal.org/node/1243042 for details.
$images = [[]];
preg_match_all(self::PATTERN_IMG_INLINE, $text, $images);
$text = preg_replace(self::PATTERN_IMG_INLINE, self::PATTERN_IMG_PLACEHOLDER, $text);
// Now we can convert all mailto URLs.
$text = preg_replace_callback($this->getPatternMailto(), [$this, 'callbackMailto'], $text);
// All bare email addresses with optional formatting information.
// @todo implement
// $text = preg_replace_callback($this->getPatternEmailWithOptions(),
// [$this, 'callbackEmailAddressesWithOptions'], $text);
// And finally, all bare email addresses.
// @todo use polymorphism to handle this exception/remove coupling
// that should be only in the scope of ROT 13
// A match could already have been applied with the result
// of rot13 for the mailto callback, so in this case we are double
// obfuscating a rot13 email (which cannot happen in
// the case of html_entity method).
// This could probably be simplified by a
// negative lookahead / lookbehind regex.
$dom = $this->rot13Safeguard($text);
$text = Html::serialize($dom);
// Apply then the bare email obfuscation.
$text = preg_replace_callback($this->getPatternEmailBare(), [$this, 'callbackBareEmailAddresses'], $text);
// Set then back the safeguarded obfuscated emails.
$newDom = $this->restoreRot13Safeguard($text);
$text = Html::serialize($newDom);
// Revert back to the original image contents.
foreach ($images[0] as $image) {
$text = preg_replace('/' . self::PATTERN_IMG_PLACEHOLDER . '/', $image, $text, 1);
}
$result = new FilterProcessResult($text);
// Libraries are not attached via the template in this case.
$result->setAttachments([
'library' => [
'obfuscate/rot13',
],
]);
return $result;
}
/**
* Safeguards ROT 13 obfuscated emails.
*
* Applies a safeguard based on an index to preserve already
* obfuscated emails from further alteration.
*
* @param string $text
* The text that may contain ROT 13 obfuscated emails.
*
* @return \DOMDocument
* The ROT 13 safeguarded DOM.
*/
private function rot13Safeguard($text) {
$dom = Html::load($text);
$xPath = new \DOMXPath($dom);
$index = 0;
/** @var \DOMElement $domElement */
foreach ($xPath->query('//span[contains(@class,"boshfpngr-e13")]') as $domElement) {
$this->elementsQueue[] = $domElement;
$safeguardElement = $dom->createElement('span', 'tmp');
$safeguardElement->setAttribute('id', $this->getUniqueSafeguard($index));
$domElement->parentNode->replaceChild($safeguardElement, $domElement);
$index++;
}
$dom->saveHTML($dom->documentElement);
return $dom;
}
/**
* Restores the ROT 13 safeguarded values.
*
* @param string $text
* The ROT 13 safeguarded text.
*
* @return \DOMDocument
* The restored DOM that may contain ROT 13 obfuscated emails.
*/
private function restoreRot13Safeguard($text) {
$dom = Html::load($text);
$xPath = new \DOMXPath($dom);
/** @var \DOMElement $domElement */
foreach ($this->elementsQueue as $index => $domElement) {
$safeguardElements = $xPath->query("//span[@id='" . $this->getUniqueSafeguard($index) . "']");
/** @var \DOMElement $safeguardElement */
$safeguardElement = $safeguardElements[0];
if ($safeguardElement instanceof \DOMElement && $domElement instanceof \DOMElement) {
$node = $dom->importNode($domElement, TRUE);
$dom->documentElement->appendChild($node);
$safeguardElement->parentNode->replaceChild($node, $safeguardElement);
}
}
$dom->saveHTML($dom->documentElement);
return $dom;
}
/**
* Returns an unique safeguard to identify the element to replace back.
*
* @param int $index
* Index of the element to replace.
*
* @return string
* Safeguard with index.
*/
private function getUniqueSafeguard($index) {
return self::SAFEGUARD . $index . strrev(self::SAFEGUARD);
}
/**
* Callback function for preg_replace_callback on getPatternEmailBare.
*
* @param array $matches
* An array containing parts of an email address.
*
* @return string
* The span with which to replace the email address.
*/
public function callbackBareEmailAddresses(array $matches) {
return $this->output($matches[1], $matches[2]);
}
/**
* Callback function for preg_replace_callback on getPatternMailto.
*
* Replace an email addresses which has been found with the appropriate
* <span> tags.
*
* @param array $matches
* An array containing parts of an email address or mailto: URL.
*
* @return string
* The span with which to replace the email address.
*/
public function callbackMailto(array $matches) {
$headers = [];
// Take the mailto: URL in $matches[3] and split the query string
// into its component parts, putting them in $headers as
// [0]=>"header=contents" etc. We cannot use parse_str because
// the query string might contain dots.
// Single quote can be encoded as &#039; which breaks parse_url
// Replace it back to a single quote which is perfectly valid.
$matches[3] = str_replace("&#039;", '\'', $matches[3]);
$query = parse_url($matches[3], PHP_URL_QUERY);
if ($query) {
$query = str_replace('&amp;', '&', $query);
$headers = preg_split('/[&;]/', $query);
// If no matches, $headers[0] will be set to '' so $headers must be reset.
if ($headers[0] == '') {
$headers = [];
}
}
// Take all <a> attributes except the href and put them into custom $vars.
$vars = $attributes = [];
// Before href.
if (!empty($matches[1])) {
$matches[1] = trim($matches[1]);
$attributes[] = $matches[1];
}
// After href.
if (!empty($matches[6])) {
$matches[6] = trim($matches[6]);
$attributes[] = $matches[6];
}
if (count($attributes)) {
$vars['extra_attributes'] = implode(' ', $attributes);
}
return $this->output($matches[4], $matches[5], $matches[7], $headers, $vars);
}
/**
* Callback function for preg_replace_callback on getPatternEmailWithOptions.
*
* @param array $matches
* An array containing parts of an email address.
*
* @return string
* The span with which to replace the email address.
*/
public function callbackEmailAddressesWithOptions(array $matches) {
$vars = [];
if (!empty($matches[3])) {
$options = explode('|', $matches[3]);
if (!empty($options[0])) {
$custom_form_url = trim($options[0]);
if (!empty($custom_form_url)) {
$vars['custom_form_url'] = $custom_form_url;
}
}
if (!empty($options[1])) {
$custom_displaytext = trim($options[1]);
if (!empty($custom_displaytext)) {
$vars['custom_displaytext'] = $custom_displaytext;
}
}
}
return $this->output($matches[1], $matches[2], '', '', $vars);
}
/**
* A helper function for the callbacks.
*
* Obfuscates the email address with the method chosen from the
* system wide configuration.
*
* @param string $name
* The user name.
* @param string $domain
* The email domain.
* @param string $contents
* The contents of any <a> tag.
* @param array $headers
* The email headers extracted from a mailto: URL @todo implement.
* @param array $vars
* Optional parameters @todo implement.
*
* @return string
* The obfuscated email address as a link.
*/
private function output($name, $domain, $contents = '', array $headers = [], array $vars = []) {
/** @var \Drupal\obfuscate\ObfuscateMail $obfuscateMail */
$obfuscateMail = \Drupal::service('obfuscate_mail');
/** @var \Drupal\Core\Render\RendererInterface $renderer */
$renderer = \Drupal::service('renderer');
$output = $obfuscateMail->getObfuscatedLink($name . '@' . $domain);
// @todo implement spamspan coverage of contents and headers.
return $renderer->render($output);
}
}
......@@ -10,6 +10,7 @@ use Twig\TwigFilter;
* Twig extension that wraps obfuscation methods.
*/
class TwigExtension extends AbstractExtension {
use ObfuscateExtractEmailAndLinksTrait;
/**
* @var \Drupal\obfuscate\ObfuscateMail
......@@ -47,7 +48,7 @@ class TwigExtension extends AbstractExtension {
*/
public function getFunctions() {
return [
new TwigFilter('obfuscate', [$this, 'obfuscate']),
new TwigFilter('obfuscate', $this->obfuscate(...)),
];
}
......@@ -56,22 +57,25 @@ class TwigExtension extends AbstractExtension {
*/
public function getFilters() {
$filters = [
new TwigFilter('obfuscateMail', [$this, 'obfuscateMail']),
new TwigFilter('obfuscateMail', $this->obfuscateMail(...)),
];
return $filters;
}
/**
* Replaces an email string by an obfuscated link.
* Replaces email substrings by obfuscated links.
*
* @param string $mail
* A plain email address.
* The method name is historical - we used to assume the parameter is an
* email address with no context.
*
* @param string $text
* Some text being rendered that might contain one or more email addresses.
*
* @return array
* The email obfuscated as a link.
* The text, with any email addresses obfuscated.
*/
public function obfuscateMail($mail) {
return $this->obfuscateMail->getObfuscatedLink($mail);
public function obfuscateMail(string $text) {
return $this->process($text);
}
/**
......
......@@ -15,12 +15,17 @@ use Drupal\field\Entity\FieldStorageConfig;
*/
class ObfuscateTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Modules to enable.
*
* @var array
*/
public static $modules = [
protected static $modules = [
'field',
'node',
'filter',
......