Commit 27940f08 authored by Benni Mack's avatar Benni Mack
Browse files

[TASK] Deprecate cObj->getMailTo

The method cObj->getMailTo() is only used in EmailLinkBuilder
and its functionality is now moved into this class,
as the scope belongs to this class.

Resolves: #96500
Releases: main
Change-Id: Id051a5889997be7a2115a2a68e223797b5a6431c
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/72944


Tested-by: core-ci's avatarcore-ci <typo3@b13.com>
Tested-by: Oliver Bartsch's avatarOliver Bartsch <bo@cedev.de>
Tested-by: Stefan Bürk's avatarStefan Bürk <stefan@buerk.tech>
Tested-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
Reviewed-by: Oliver Bartsch's avatarOliver Bartsch <bo@cedev.de>
Reviewed-by: Stefan Bürk's avatarStefan Bürk <stefan@buerk.tech>
Reviewed-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
parent 6e25062a
.. include:: ../../Includes.txt
======================================================
Deprecation: #96500 - ContentObjectRenderer->getMailTo
======================================================
See :issue:`96500`
Description
===========
Since :issue:`96483`, the :html:`<f:link.email/>` ViewHelper is directly
using `TypoLink` for the email link generation. As a result, the
:php:`ContentObjectRenderer->getMailTo()` method was only used in
:php:`EmailLinkBuilder`, the central place for building email links
with `TypoLink`.
To stick to the separation of concerns principle, the corresponding
functionality has been moved to :php:`EmailLinkBuilder->processEmailLink()`
and the :php:`ContentObjectRenderer->getMailTo()` method has been
marked as deprecated.
Impact
======
Calling :php:`ContentObjectRenderer->getMailTo()` will trigger a
PHP :php:`E_USER_DEPRECATED` error. The extension scanner will
find usages as weak match.
Affected Installations
======================
All installations directly calling :php:`ContentObjectRenderer->getMailTo()`
in custom extension code.
Migration
=========
All occurences in extension code have to be replaced by
:php:`EmailLinkBuilder->processEmailLink()`.
.. index:: Frontend, PHP-API, FullyScanned, ext:frontend
...@@ -55,7 +55,6 @@ use TYPO3\CMS\Core\Resource\FileReference; ...@@ -55,7 +55,6 @@ use TYPO3\CMS\Core\Resource\FileReference;
use TYPO3\CMS\Core\Resource\Folder; use TYPO3\CMS\Core\Resource\Folder;
use TYPO3\CMS\Core\Resource\ProcessedFile; use TYPO3\CMS\Core\Resource\ProcessedFile;
use TYPO3\CMS\Core\Resource\ResourceFactory; use TYPO3\CMS\Core\Resource\ResourceFactory;
use TYPO3\CMS\Core\Service\DependencyOrderingService;
use TYPO3\CMS\Core\Service\FlexFormService; use TYPO3\CMS\Core\Service\FlexFormService;
use TYPO3\CMS\Core\Site\SiteFinder; use TYPO3\CMS\Core\Site\SiteFinder;
use TYPO3\CMS\Core\TimeTracker\TimeTracker; use TYPO3\CMS\Core\TimeTracker\TimeTracker;
...@@ -75,12 +74,12 @@ use TYPO3\CMS\Frontend\ContentObject\Exception\ContentRenderingException; ...@@ -75,12 +74,12 @@ use TYPO3\CMS\Frontend\ContentObject\Exception\ContentRenderingException;
use TYPO3\CMS\Frontend\ContentObject\Exception\ExceptionHandlerInterface; use TYPO3\CMS\Frontend\ContentObject\Exception\ExceptionHandlerInterface;
use TYPO3\CMS\Frontend\ContentObject\Exception\ProductionExceptionHandler; use TYPO3\CMS\Frontend\ContentObject\Exception\ProductionExceptionHandler;
use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController; use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
use TYPO3\CMS\Frontend\Http\UrlProcessorInterface;
use TYPO3\CMS\Frontend\Imaging\GifBuilder; use TYPO3\CMS\Frontend\Imaging\GifBuilder;
use TYPO3\CMS\Frontend\Page\PageLayoutResolver; use TYPO3\CMS\Frontend\Page\PageLayoutResolver;
use TYPO3\CMS\Frontend\Resource\FilePathSanitizer; use TYPO3\CMS\Frontend\Resource\FilePathSanitizer;
use TYPO3\CMS\Frontend\Service\TypoLinkCodecService; use TYPO3\CMS\Frontend\Service\TypoLinkCodecService;
use TYPO3\CMS\Frontend\Typolink\AbstractTypolinkBuilder; use TYPO3\CMS\Frontend\Typolink\AbstractTypolinkBuilder;
use TYPO3\CMS\Frontend\Typolink\EmailLinkBuilder;
use TYPO3\CMS\Frontend\Typolink\LinkResult; use TYPO3\CMS\Frontend\Typolink\LinkResult;
use TYPO3\CMS\Frontend\Typolink\LinkResultInterface; use TYPO3\CMS\Frontend\Typolink\LinkResultInterface;
use TYPO3\CMS\Frontend\Typolink\UnableToLinkException; use TYPO3\CMS\Frontend\Typolink\UnableToLinkException;
...@@ -4944,152 +4943,16 @@ class ContentObjectRenderer implements LoggerAwareInterface ...@@ -4944,152 +4943,16 @@ class ContentObjectRenderer implements LoggerAwareInterface
} }
/** /**
* Loops over all configured URL modifier hooks (if available) and returns the generated URL or NULL if no URL was generated.
*
* @param string $context The context in which the method is called (e.g. typoLink).
* @param string $url The URL that should be processed.
* @param array $typolinkConfiguration The current link configuration array.
* @return string|null Returns NULL if URL was not processed or the processed URL as a string.
* @throws \RuntimeException if a hook was registered but did not fulfill the correct parameters.
*/
protected function processUrl($context, $url, $typolinkConfiguration = [])
{
$urlProcessors = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['urlProcessing']['urlProcessors'] ?? [];
if (empty($urlProcessors)) {
return $url;
}
foreach ($urlProcessors as $identifier => $configuration) {
if (empty($configuration) || !is_array($configuration)) {
throw new \RuntimeException('Missing configuration for URI processor "' . $identifier . '".', 1442050529);
}
if (!is_string($configuration['processor']) || empty($configuration['processor']) || !class_exists($configuration['processor']) || !is_subclass_of($configuration['processor'], UrlProcessorInterface::class)) {
throw new \RuntimeException('The URI processor "' . $identifier . '" defines an invalid provider. Ensure the class exists and implements the "' . UrlProcessorInterface::class . '".', 1442050579);
}
}
$orderedProcessors = GeneralUtility::makeInstance(DependencyOrderingService::class)->orderByDependencies($urlProcessors);
$keepProcessing = true;
foreach ($orderedProcessors as $configuration) {
/** @var UrlProcessorInterface $urlProcessor */
$urlProcessor = GeneralUtility::makeInstance($configuration['processor']);
$url = $urlProcessor->process($context, $url, $typolinkConfiguration, $this, $keepProcessing);
if (!$keepProcessing) {
break;
}
}
return $url;
}
/**
* Creates a href attibute for given $mailAddress.
* The function uses spamProtectEmailAddresses for encoding the mailto statement.
* If spamProtectEmailAddresses is disabled, it'll just return a string like "mailto:user@example.tld".
*
* Returns an array with three items (numeric index)
* #0: $mailToUrl (string), ready to be inserted into the href attribute of the <a> tag
* #1: $linktxt (string), content between starting and ending `<a>` tag
* #2: $attributes (array<string, string>), additional attributes for `<a>` tag
*
* @param string $mailAddress Email address * @param string $mailAddress Email address
* @param string $linktxt Link text, default will be the email address. * @param string $linktxt Link text, default will be the email address.
* @return array{0: string, 1: string, 2: array<string, string>} A numerical array with three items * @return array{0: string, 1: string, 2: array<string, string>} A numerical array with three items
* @deprecated will be removed in TYPO3 v13.0. Use EmailLinkBuilder->processEmailLink() instead.
*/ */
public function getMailTo($mailAddress, $linktxt) public function getMailTo($mailAddress, $linktxt)
{ {
$mailAddress = (string)$mailAddress; trigger_error('ContentObjectRenderer->getMailTo() will be removed in TYPO3 v13.0, Use EmailLinkBuilder->processEmailLink() instead.', E_USER_DEPRECATED);
if ((string)$linktxt === '') { $linkBuilder = GeneralUtility::makeInstance(EmailLinkBuilder::class, $this, $this->getTypoScriptFrontendController());
$linktxt = htmlspecialchars($mailAddress); return $linkBuilder->processEmailLink((string)$mailAddress, (string)$linktxt);
}
$originalMailToUrl = 'mailto:' . $mailAddress;
$mailToUrl = $this->processUrl(UrlProcessorInterface::CONTEXT_MAIL, $originalMailToUrl);
$attributes = [];
// no processing happened, therefore, the default processing kicks in
if ($mailToUrl === $originalMailToUrl) {
$tsfe = $this->getTypoScriptFrontendController();
if ($tsfe instanceof TypoScriptFrontendController && $tsfe->spamProtectEmailAddresses) {
$mailToUrl = $this->encryptEmail($mailToUrl, (int)$tsfe->spamProtectEmailAddresses);
$attributes = [
'data-mailto-token' => $mailToUrl,
'data-mailto-vector' => (int)$tsfe->spamProtectEmailAddresses,
];
$mailToUrl = '#';
$atLabel = '(at)';
if (($atLabelFromConfig = trim($tsfe->config['config']['spamProtectEmailAddresses_atSubst'] ?? '')) !== '') {
$atLabel = $atLabelFromConfig;
}
$spamProtectedMailAddress = str_replace('@', $atLabel, htmlspecialchars($mailAddress));
if ($tsfe->config['config']['spamProtectEmailAddresses_lastDotSubst'] ?? false) {
$lastDotLabel = trim($tsfe->config['config']['spamProtectEmailAddresses_lastDotSubst']);
$lastDotLabel = $lastDotLabel ?: '(dot)';
$spamProtectedMailAddress = preg_replace('/\\.([^\\.]+)$/', $lastDotLabel . '$1', $spamProtectedMailAddress);
if ($spamProtectedMailAddress === null) {
$this->logger->debug('Error replacing the last dot in email address "{email}"', ['email' => $spamProtectedMailAddress]);
$spamProtectedMailAddress = '';
}
}
$linktxt = str_ireplace($mailAddress, $spamProtectedMailAddress, $linktxt);
$this->addDefaultFrontendJavaScript();
}
}
return [$mailToUrl, $linktxt, $attributes];
}
/**
* Encryption of email addresses for <A>-tags See the spam protection setup in TS 'config.'
*
* @param string $string Input string to en/decode: "mailto:some@example.com
* @param int $type a number between -10 and 10, taken from config.spamProtectEmailAddresses
* @return string encoded version of $string
*/
protected function encryptEmail(string $string, int $type): string
{
$out = '';
// like str_rot13() but with a variable offset and a wider character range
$len = strlen($string);
$offset = $type;
for ($i = 0; $i < $len; $i++) {
$charValue = ord($string[$i]);
// 0-9 . , - + / :
if ($charValue >= 43 && $charValue <= 58) {
$out .= $this->encryptCharcode($charValue, 43, 58, $offset);
} elseif ($charValue >= 64 && $charValue <= 90) {
// A-Z @
$out .= $this->encryptCharcode($charValue, 64, 90, $offset);
} elseif ($charValue >= 97 && $charValue <= 122) {
// a-z
$out .= $this->encryptCharcode($charValue, 97, 122, $offset);
} else {
$out .= $string[$i];
}
}
return $out;
}
/**
* Encryption (or decryption) of a single character.
* Within the given range the character is shifted with the supplied offset.
*
* @param int $n Ordinal of input character
* @param int $start Start of range
* @param int $end End of range
* @param int $offset Offset
* @return string encoded/decoded version of character
*/
protected function encryptCharcode($n, $start, $end, $offset)
{
$n = $n + $offset;
if ($offset > 0 && $n > $end) {
$n = $start + ($n - $end - 1);
} elseif ($offset < 0 && $n < $start) {
$n = $end - ($start - $n - 1);
}
return chr($n);
} }
/** /**
......
...@@ -33,7 +33,6 @@ interface UrlProcessorInterface ...@@ -33,7 +33,6 @@ interface UrlProcessorInterface
/** /**
* Generates the JumpURL for the given parameters. * Generates the JumpURL for the given parameters.
* *
* @see \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer::processUrl()
* @param string $context The context in which the URL is generated (e.g. "typolink" or one of the constants above). * @param string $context The context in which the URL is generated (e.g. "typolink" or one of the constants above).
* @param string $url The URL that should be processed. * @param string $url The URL that should be processed.
* @param array $configuration The link configuration. * @param array $configuration The link configuration.
......
...@@ -17,20 +17,142 @@ declare(strict_types=1); ...@@ -17,20 +17,142 @@ declare(strict_types=1);
namespace TYPO3\CMS\Frontend\Typolink; namespace TYPO3\CMS\Frontend\Typolink;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use TYPO3\CMS\Core\LinkHandling\LinkService; use TYPO3\CMS\Core\LinkHandling\LinkService;
use TYPO3\CMS\Core\Page\DefaultJavaScriptAssetTrait;
use TYPO3\CMS\Frontend\Http\UrlProcessorInterface;
/** /**
* Builds a TypoLink to an email address * Builds a TypoLink to an email address, also takes care of additional functionality for the time being
* such as the infamous config.spamProtectedEmailAddresses option.
*/ */
class EmailLinkBuilder extends AbstractTypolinkBuilder class EmailLinkBuilder extends AbstractTypolinkBuilder implements LoggerAwareInterface
{ {
use DefaultJavaScriptAssetTrait;
use LoggerAwareTrait;
public function build(array &$linkDetails, string $linkText, string $target, array $conf): LinkResultInterface public function build(array &$linkDetails, string $linkText, string $target, array $conf): LinkResultInterface
{ {
[$url, $linkText, $attributes] = $this->contentObjectRenderer->getMailTo($linkDetails['email'], $linkText); [$url, $linkText, $attributes] = $this->processEmailLink($linkDetails['email'], $linkText);
return (new LinkResult(LinkService::TYPE_EMAIL, $url)) return (new LinkResult(LinkService::TYPE_EMAIL, $url))
->withTarget($target) ->withTarget($target)
->withLinkConfiguration($conf) ->withLinkConfiguration($conf)
->withLinkText($linkText) ->withLinkText($linkText)
->withAttributes($attributes); ->withAttributes($attributes);
} }
/**
* Creates a href attibute for given $mailAddress.
* The function uses spamProtectEmailAddresses for encoding the mailto statement.
* If spamProtectEmailAddresses is disabled, it'll just return a string like "mailto:user@example.tld".
*
* Returns an array with three items (numeric index)
* #0: $mailToUrl (string), ready to be inserted into the href attribute of the <a> tag
* #1: $linkText (string), content between starting and ending `<a>` tag
* #2: $attributes (array<string, string>), additional attributes for `<a>` tag
*
* @param string $mailAddress Email address
* @param string $linkText Link text, default will be the email address.
* @return array{0: string, 1: string, 2: array<string, string>} A numerical array with three items
*/
public function processEmailLink(string $mailAddress, string $linkText): array
{
$linkText = $linkText ?: htmlspecialchars($mailAddress);
$originalMailToUrl = 'mailto:' . $mailAddress;
$mailToUrl = $this->processUrl(UrlProcessorInterface::CONTEXT_MAIL, $originalMailToUrl);
$attributes = [];
// no processing happened, therefore, the default processing kicks in
if ($mailToUrl === $originalMailToUrl) {
$tsfe = $this->getTypoScriptFrontendController();
if ($tsfe->spamProtectEmailAddresses) {
$mailToUrl = $this->encryptEmail($mailToUrl, $tsfe->spamProtectEmailAddresses);
if ($tsfe->spamProtectEmailAddresses !== 'ascii') {
$attributes = [
'data-mailto-token' => $mailToUrl,
'data-mailto-vector' => (int)$tsfe->spamProtectEmailAddresses,
];
$mailToUrl = '#';
$this->addDefaultFrontendJavaScript();
}
$atLabel = '(at)';
if (($atLabelFromConfig = trim($tsfe->config['config']['spamProtectEmailAddresses_atSubst'] ?? '')) !== '') {
$atLabel = $atLabelFromConfig;
}
$spamProtectedMailAddress = str_replace('@', $atLabel, htmlspecialchars($mailAddress));
if ($tsfe->config['config']['spamProtectEmailAddresses_lastDotSubst'] ?? false) {
$lastDotLabel = trim($tsfe->config['config']['spamProtectEmailAddresses_lastDotSubst']);
$lastDotLabel = $lastDotLabel ?: '(dot)';
$spamProtectedMailAddress = preg_replace('/\\.([^\\.]+)$/', $lastDotLabel . '$1', $spamProtectedMailAddress);
if ($spamProtectedMailAddress === null) {
$this->logger->debug('Error replacing the last dot in email address "{email}"', ['email' => $spamProtectedMailAddress]);
$spamProtectedMailAddress = '';
}
}
$linkText = str_ireplace($mailAddress, $spamProtectedMailAddress, $linkText);
}
}
return [$mailToUrl, $linkText, $attributes];
}
/**
* Encryption of email addresses for <A>-tags See the spam protection setup in TS 'config.'
*
* @param string $string Input string to en/decode: "mailto:some@example.com
* @param mixed $type - either "ascii" or a number between -10 and 10, taken from config.spamProtectEmailAddresses
* @return string encoded version of $string
*/
protected function encryptEmail(string $string, $type): string
{
$out = '';
// obfuscates using the decimal HTML entity references for each character
if ($type === 'ascii') {
foreach (preg_split('//u', $string, -1, PREG_SPLIT_NO_EMPTY) as $char) {
$out .= '&#' . mb_ord($char) . ';';
}
} else {
// like str_rot13() but with a variable offset and a wider character range
$len = strlen($string);
$offset = (int)$type;
for ($i = 0; $i < $len; $i++) {
$charValue = ord($string[$i]);
// 0-9 . , - + / :
if ($charValue >= 43 && $charValue <= 58) {
$out .= $this->encryptCharcode($charValue, 43, 58, $offset);
} elseif ($charValue >= 64 && $charValue <= 90) {
// A-Z @
$out .= $this->encryptCharcode($charValue, 64, 90, $offset);
} elseif ($charValue >= 97 && $charValue <= 122) {
// a-z
$out .= $this->encryptCharcode($charValue, 97, 122, $offset);
} else {
$out .= $string[$i];
}
}
}
return $out;
}
/**
* Encryption (or decryption) of a single character.
* Within the given range the character is shifted with the supplied offset.
*
* @param int $n Ordinal of input character
* @param int $start Start of range
* @param int $end End of range
* @param int $offset Offset
* @return string encoded/decoded version of character
*/
protected function encryptCharcode(int $n, int $start, int $end, int $offset): string
{
$n = $n + $offset;
if ($offset > 0 && $n > $end) {
$n = $start + ($n - $end - 1);
} elseif ($offset < 0 && $n < $start) {
$n = $end - ($start - $n - 1);
}
return chr($n);
}
} }
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend\Tests\Unit\Typolink;
use TYPO3\CMS\Frontend\Typolink\EmailLinkBuilder;
use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
class EmailLinkBuilderTest extends UnitTestCase
{
public function emailSpamProtectionWithTypeAsciiDataProvider(): array
{
return [
'Simple email address' => [
'test@email.tld',
'&#116;&#101;&#115;&#116;&#64;&#101;&#109;&#97;&#105;&#108;&#46;&#116;&#108;&#100;',
],
'Simple email address with unicode characters' => [
'matthäus@email.tld',
'&#109;&#97;&#116;&#116;&#104;&#228;&#117;&#115;&#64;&#101;&#109;&#97;&#105;&#108;&#46;&#116;&#108;&#100;',
],
'Susceptible email address' => [
'"><script>alert(\'emailSpamProtection\')</script>',
'&#34;&#62;&#60;&#115;&#99;&#114;&#105;&#112;&#116;&#62;&#97;&#108;&#101;&#114;&#116;&#40;&#39;&#101;&#109;&#97;&#105;&#108;&#83;&#112;&#97;&#109;&#80;&#114;&#111;&#116;&#101;&#99;&#116;&#105;&#111;&#110;&#39;&#41;&#60;&#47;&#115;&#99;&#114;&#105;&#112;&#116;&#62;',
],
'Susceptible email address with unicode characters' => [
'"><script>alert(\'ȅmǡilSpamProtȅction\')</script>',
'&#34;&#62;&#60;&#115;&#99;&#114;&#105;&#112;&#116;&#62;&#97;&#108;&#101;&#114;&#116;&#40;&#39;&#517;&#109;&#481;&#105;&#108;&#83;&#112;&#97;&#109;&#80;&#114;&#111;&#116;&#517;&#99;&#116;&#105;&#111;&#110;&#39;&#41;&#60;&#47;&#115;&#99;&#114;&#105;&#112;&#116;&#62;',
],
];
}
/**
* Check if email spam protection processes all UTF-8 characters properly
*
* @test
* @dataProvider emailSpamProtectionWithTypeAsciiDataProvider
*/
public function mailSpamProtectionWithTypeAscii(string $content, string $expected): void
{
$subject = $this->getAccessibleMock(EmailLinkBuilder::class, ['dummy'], [], '', false);
self::assertSame(
$expected,
$subject->_call('encryptEmail', $content, 'ascii')
);
}
}
...@@ -5091,4 +5091,11 @@ return [ ...@@ -5091,4 +5091,11 @@ return [
'Breaking-96351-UnusedTemplateService-updateRootlineDataMethodRemoved.rst', 'Breaking-96351-UnusedTemplateService-updateRootlineDataMethodRemoved.rst',
], ],
], ],
'TTYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer->getMailTo' => [
'numberOfMandatoryArguments' => 2,
'maximumNumberOfArguments' => 2,
'restFiles' => [
'Deprecation-96500-ContentObjectRenderer-getMailTo.rst',
],
],
]; ];
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment