...
 
Commits (4)
......@@ -578,6 +578,11 @@ class RteHtmlParser extends HtmlParser implements LoggerAwareInterface
foreach ($blockSplit as $k => $v) {
if ($k % 2) {
list($tagAttributes) = $this->get_tag_attributes($this->getFirstTag($v), true);
// Anchors would not have an href attribute
if (!isset($tagAttributes['href'])) {
continue;
}
$linkService = GeneralUtility::makeInstance(LinkService::class);
$linkInformation = $linkService->resolve($tagAttributes['href'] ?? '');
......
......@@ -1281,7 +1281,7 @@ class GeneralUtility
if ($val !== '=') {
if ($valuemode) {
if ($name) {
$attributes[$name] = $val;
$attributes[$name] = htmlspecialchars_decode($val);
$name = '';
}
} else {
......
......@@ -625,4 +625,82 @@ class RteHtmlParserTest extends UnitTestCase
$thisConfig = ['proc.' => $this->procOptions];
$this->assertEquals($expectedResult, $subject->RTE_transform($subject->RTE_transform($content, [], 'db', $thisConfig), [], 'rte', $thisConfig));
}
/**
* Data provider for anchorCorrectlyTransformedOnWayToDatabase
*/
public static function anchorCorrectlyTransformedOnWayToDatabaseProvider()
{
return [
[
'<p><a name="some_anchor"></a></p>' . CRLF . '<h3>Some headline here</h3>',
'<p><a name="some_anchor"></a></p>' . CRLF . '<h3>Some headline here</h3>'
],
[
'<p><a id="some_anchor"></a></p>' . CRLF . '<h3>Some headline here</h3>',
'<p><a id="some_anchor"></a></p>' . CRLF . '<h3>Some headline here</h3>'
],
[
'<p><a name="some_anchor" id="some_anchor"></a></p>' . CRLF . '<h3>Some headline here</h3>',
'<p><a name="some_anchor" id="some_anchor"></a></p>' . CRLF . '<h3>Some headline here</h3>'
],
[
'<p><a id="some_anchor">Some text inside the anchor</a></p>',
'<p><a id="some_anchor">Some text inside the anchor</a></p>'
]
];
}
/**
* @test
* @dataProvider anchorCorrectlyTransformedOnWayToDatabaseProvider
* @param $content
* @param $expectedResult
*/
public function anchorCorrectlyTransformedOnWayToDatabase($content, $expectedResult)
{
$eventDispatcher = $this->createMock(EventDispatcherInterface::class);
$subject = new RteHtmlParser($eventDispatcher);
$thisConfig = ['proc.' => $this->procOptions];
self::assertEquals($expectedResult, $subject->RTE_transform($content, [], 'db', $thisConfig));
}
/**
* Data provider for anchorCorrectlyTransformedOnWayToDatabaseAndBackToRTE
*/
public static function anchorCorrectlyTransformedOnWayToDatabaseAndBackToRTEProvider()
{
return [
[
'<p><a name="some_anchor"></a></p>' . CRLF . '<h3>Some headline here</h3>',
'<p><a name="some_anchor"></a></p>' . CRLF . '<h3>Some headline here</h3>'
],
[
'<p><a id="some_anchor"></a></p>' . CRLF . '<h3>Some headline here</h3>',
'<p><a id="some_anchor"></a></p>' . CRLF . '<h3>Some headline here</h3>'
],
[
'<p><a name="some_anchor" id="some_anchor"></a></p>' . CRLF . '<h3>Some headline here</h3>',
'<p><a name="some_anchor" id="some_anchor"></a></p>' . CRLF . '<h3>Some headline here</h3>'
],
[
'<p><a id="some_anchor">Some text inside the anchor</a></p>',
'<p><a id="some_anchor">Some text inside the anchor</a></p>'
]
];
}
/**
* @test
* @dataProvider anchorCorrectlyTransformedOnWayToDatabaseAndBackToRTEProvider
* @param $content
* @param $expectedResult
*/
public function anchorCorrectlyTransformedOnWayToDatabaseAndBackToRTE($content, $expectedResult)
{
$eventDispatcher = $this->createMock(EventDispatcherInterface::class);
$subject = new RteHtmlParser($eventDispatcher);
$thisConfig = ['proc.' => $this->procOptions];
self::assertEquals($expectedResult, $subject->RTE_transform($subject->RTE_transform($content, [], 'db', $thisConfig), [], 'rte', $thisConfig));
}
}
......@@ -102,7 +102,7 @@ class EmailViewHelperTest extends ViewHelperBaseTestcase
'Plain email with spam protection' => [
'some@email.tld',
1,
'<a href="javascript:linkTo_UnCryptMailto(\'nbjmup+tpnfAfnbjm\/ume\');">some(at)email.tld</a>',
'<a href="javascript:linkTo_UnCryptMailto(%27nbjmup%2BtpnfAfnbjm%5C%2Fume%27);">some(at)email.tld</a>',
],
'Plain email with ascii spam protection' => [
'some@email.tld',
......@@ -117,7 +117,7 @@ class EmailViewHelperTest extends ViewHelperBaseTestcase
'Susceptible email with spam protection' => [
'"><script>alert(\'email\')</script>',
1,
'<a href="javascript:linkTo_UnCryptMailto(\'nbjmup+\u0022\u003E\u003Ctdsjqu\u003Ebmfsu(\u0027fnbjm\u0027)\u003C0tdsjqu\u003E\');">&quot;&gt;&lt;script&gt;alert(\'email\')&lt;/script&gt;</a>',
'<a href="javascript:linkTo_UnCryptMailto(%27nbjmup%2B%5Cu0022%5Cu003E%5Cu003Ctdsjqu%5Cu003Ebmfsu%28%5Cu0027fnbjm%5Cu0027%29%5Cu003C0tdsjqu%5Cu003E%27);">&quot;&gt;&lt;script&gt;alert(\'email\')&lt;/script&gt;</a>',
],
'Susceptible email with ascii spam protection' => [
'"><script>alert(\'email\')</script>',
......
......@@ -5407,7 +5407,7 @@ class ContentObjectRenderer implements LoggerAwareInterface
$title = $resolvedLinkParameters['title'];
if (!$linkParameter) {
return $linkText;
return $this->resolveAnchorLink($linkText, $conf);
}
// Detecting kind of link and resolve all necessary parameters
......@@ -5749,7 +5749,8 @@ class ContentObjectRenderer implements LoggerAwareInterface
if ($tsfe->spamProtectEmailAddresses) {
$mailToUrl = $this->encryptEmail($mailToUrl, $tsfe->spamProtectEmailAddresses);
if ($tsfe->spamProtectEmailAddresses !== 'ascii') {
$mailToUrl = 'javascript:linkTo_UnCryptMailto(' . GeneralUtility::quoteJSvalue($mailToUrl) . ');';
$encodedForJsAndHref = rawurlencode(GeneralUtility::quoteJSvalue($mailToUrl));
$mailToUrl = 'javascript:linkTo_UnCryptMailto(' . $encodedForJsAndHref . ');';
}
$atLabel = trim($tsfe->config['config']['spamProtectEmailAddresses_atSubst']) ?: '(at)';
$spamProtectedMailAddress = str_replace('@', $atLabel, htmlspecialchars($mailAddress));
......@@ -7520,4 +7521,25 @@ class ContentObjectRenderer implements LoggerAwareInterface
{
return $this->typoScriptFrontendController ?: $GLOBALS['TSFE'];
}
/**
* Support anchors without href value
* Changes ContentObjectRenderer::typolink to render a tag without href,
* if id or name attribute is present.
*
* @param string $linkText
* @param array $conf Typolink configuration decoded as array
* @return string Full a-Tag or just the linktext if id or name are not set.
*/
protected function resolveAnchorLink(string $linkText, array $conf): string
{
$anchorTag = '<a ' . $this->getATagParams($conf) . '>';
$aTagParams = GeneralUtility::get_tag_attributes($anchorTag);
// If it looks like a anchor tag, render it anyway
if (isset($aTagParams['id']) || isset($aTagParams['name'])) {
return $anchorTag . $linkText . '</a>';
}
// Otherwise just return the link text
return $linkText;
}
}
......@@ -31,6 +31,10 @@ use TYPO3\CMS\Frontend\Page\PageAccessFailureReasons;
/**
* Resolves redirects of site if base is not /
* Can be replaced or extended by extensions if GeoIP-based or user-agent based language redirects need to happen.
*
* Please note that the redirect usually does not contain the Query Parameters, as special query parameters
* like "id", "L" and "cHash" could then result in an error loop.
* One special case (adding a "/") is keeping the query parameters though.
*/
class SiteBaseRedirectResolver implements MiddlewareInterface
{
......
......@@ -27,7 +27,7 @@ class ExternalUrlLinkBuilder extends AbstractTypolinkBuilder
public function build(array &$linkDetails, string $linkText, string $target, array $conf): array
{
return [
$this->processUrl(UrlProcessorInterface::CONTEXT_EXTERNAL, htmlspecialchars_decode($linkDetails['url']), $conf),
$this->processUrl(UrlProcessorInterface::CONTEXT_EXTERNAL, $linkDetails['url'], $conf),
$this->parseFallbackLinkTextIfLinkTextIsEmpty($linkText, $linkDetails['url']),
$target ?: $this->resolveTargetAttribute($conf, 'extTarget', true, $this->getTypoScriptFrontendController()->extTarget)
];
......
......@@ -437,6 +437,37 @@ class ContentObjectRendererTest extends \TYPO3\TestingFramework\Core\Functional\
$this->assertEquals($expectedResult, $subject->typoLink($linkText, $configuration));
}
/**
* @test
*/
public function typolinkReturnsCorrectLinkForEmails()
{
$expected = '<a href="mailto:test@example.com">Send me an email</a>';
$subject = new ContentObjectRenderer();
$result = $subject->typoLink('Send me an email', ['parameter' => 'mailto:test@example.com']);
self::assertEquals($expected, $result);
$result = $subject->typoLink('Send me an email', ['parameter' => 'test@example.com']);
self::assertEquals($expected, $result);
}
/**
* @test
*/
public function typolinkReturnsCorrectLinkForSpamEncryptedEmails()
{
$tsfe = $this->getMockBuilder(TypoScriptFrontendController::class)->disableOriginalConstructor()->getMock();
$subject = new ContentObjectRenderer($tsfe);
$tsfe->spamProtectEmailAddresses = 1;
$result = $subject->typoLink('Send me an email', ['parameter' => 'mailto:test@example.com']);
self::assertEquals('<a href="javascript:linkTo_UnCryptMailto(%27nbjmup%2BuftuAfybnqmf%5C%2Fdpn%27);">Send me an email</a>', $result);
$tsfe->spamProtectEmailAddresses = 'ascii';
$result = $subject->typoLink('Send me an email', ['parameter' => 'mailto:test@example.com']);
self::assertEquals('<a href="&#109;&#97;&#105;&#108;&#116;&#111;&#58;&#116;&#101;&#115;&#116;&#64;&#101;&#120;&#97;&#109;&#112;&#108;&#101;&#46;&#99;&#111;&#109;">Send me an email</a>', $result);
}
/**
* @test
*/
......
......@@ -2920,7 +2920,7 @@ class ContentObjectRendererTest extends UnitTestCase
],
'some.body@test.typo3.org',
'mailto:some.body@test.typo3.org',
'<a href="javascript:linkTo_UnCryptMailto(\'nbjmup+tpnf\/cpezAuftu\/uzqp4\/psh\');">some.body(at)test.typo3.org</a>',
'<a href="javascript:linkTo_UnCryptMailto(%27nbjmup%2Btpnf%5C%2FcpezAuftu%5C%2Fuzqp4%5C%2Fpsh%27);">some.body(at)test.typo3.org</a>',
],
'mono-alphabetic substitution offset +1 with at substitution' => [
[
......@@ -2930,7 +2930,7 @@ class ContentObjectRendererTest extends UnitTestCase
],
'some.body@test.typo3.org',
'mailto:some.body@test.typo3.org',
'<a href="javascript:linkTo_UnCryptMailto(\'nbjmup+tpnf\/cpezAuftu\/uzqp4\/psh\');">some.body@test.typo3.org</a>',
'<a href="javascript:linkTo_UnCryptMailto(%27nbjmup%2Btpnf%5C%2FcpezAuftu%5C%2Fuzqp4%5C%2Fpsh%27);">some.body@test.typo3.org</a>',
],
'mono-alphabetic substitution offset +1 with at and dot substitution' => [
[
......@@ -2940,7 +2940,7 @@ class ContentObjectRendererTest extends UnitTestCase
],
'some.body@test.typo3.org',
'mailto:some.body@test.typo3.org',
'<a href="javascript:linkTo_UnCryptMailto(\'nbjmup+tpnf\/cpezAuftu\/uzqp4\/psh\');">some.body(at)test.typo3(dot)org</a>',
'<a href="javascript:linkTo_UnCryptMailto(%27nbjmup%2Btpnf%5C%2FcpezAuftu%5C%2Fuzqp4%5C%2Fpsh%27);">some.body(at)test.typo3(dot)org</a>',
],
'mono-alphabetic substitution offset -1 with at and dot substitution' => [
[
......@@ -2950,7 +2950,17 @@ class ContentObjectRendererTest extends UnitTestCase
],
'some.body@test.typo3.org',
'mailto:some.body@test.typo3.org',
'<a href="javascript:linkTo_UnCryptMailto(\'lzhksn9rnld-ancxZsdrs-sxon2-nqf\');">some.body(at)test.typo3(dot)org</a>',
'<a href="javascript:linkTo_UnCryptMailto(%27lzhksn9rnld-ancxZsdrs-sxon2-nqf%27);">some.body(at)test.typo3(dot)org</a>',
],
'mono-alphabetic substitution offset 2 with at and dot substitution and encoded subject' => [
[
'spamProtectEmailAddresses' => '2',
'spamProtectEmailAddresses_atSubst' => '(at)',
'spamProtectEmailAddresses_lastDotSubst' => '(dot)',
],
'some.body@test.typo3.org',
'mailto:some.body@test.typo3.org?subject=foo%20bar',
'<a href="javascript:linkTo_UnCryptMailto(%27ocknvq%2Cuqog0dqfaBvguv0varq50qti%3Fuwdlgev%3Dhqq%2542dct%27);">some.body@test.typo3.org</a>',
],
'entity substitution with at and dot substitution' => [
[
......
......@@ -109,6 +109,13 @@ class SiteBaseRedirectResolverTest extends UnitTestCase
null,
''
],
'redirect to first language adding the slash' => [
'https://twenty.one/en',
'https://twenty.one/en/',
$site1,
null,
''
],
'redirect to second language removing a slash' => [
'https://twenty.one/fr/',
'https://twenty.one/fr',
......@@ -123,6 +130,20 @@ class SiteBaseRedirectResolverTest extends UnitTestCase
null,
''
],
'redirect to first language and remove nested arguments' => [
'https://twenty.one/?foo[bar]=foobar&bar=foo',
'https://twenty.one/en/',
$site1,
null,
''
],
'redirect to second language removing a slash but keeping the nested arguments' => [
'https://twenty.one/fr/?foo[bar]=foobar&bar=foo',
'https://twenty.one/fr?foo%5Bbar%5D=foobar&bar=foo',
$site1,
$site1->getLanguageById(1),
'/'
],
];
}
......@@ -211,4 +232,71 @@ class SiteBaseRedirectResolverTest extends UnitTestCase
$response = $subject->process($request, $this->siteFoundRequestHandler);
$this->assertEquals($expectedStatusCode, $response->getStatusCode());
}
/**
* @return array
*/
public function doNotRedirectOnBaseWithoutQueryDataProvider(): array
{
$site1 = new Site('outside-site', 13, [
'base' => 'https://twenty.one/',
'languages' => [
0 => [
'languageId' => 0,
'locale' => 'en_US.UTF-8',
'base' => '/en/'
],
1 => [
'languageId' => 1,
'locale' => 'fr_CA.UTF-8',
'base' => '/fr'
]
]
]);
return [
'no redirect for base' => [
'https://twenty.one/en/',
$site1,
$site1->getLanguageById(0),
''
],
'no redirect for base when ID is given' => [
'https://twenty.one/index.php?id=2',
$site1,
$site1->getLanguageById(0),
''
],
'no redirect for base and nested arguments' => [
'https://twenty.one/en/?foo[bar]=foobar&bar=foo',
$site1,
$site1->getLanguageById(0),
''
],
];
}
/**
* @param string $incomingUrl
* @param Site $site
* @param SiteLanguage|null $language
* @param string $tail
* @dataProvider doNotRedirectOnBaseWithoutQueryDataProvider
* @test
*/
public function doNotRedirectOnBaseWithoutQuery(
string $incomingUrl,
Site $site,
?SiteLanguage $language,
string $tail
): void {
$routeResult = new SiteRouteResult(new Uri($incomingUrl), $site, $language, $tail);
$request = new ServerRequest($incomingUrl, 'GET');
$request = $request->withAttribute('site', $site);
$request = $request->withAttribute('language', $language);
$request = $request->withAttribute('routing', $routeResult);
$subject = new SiteBaseRedirectResolver();
$response = $subject->process($request, $this->siteFoundRequestHandler);
self::assertEquals(200, $response->getStatusCode());
}
}
......@@ -283,7 +283,7 @@ class PageLinkHandler extends AbstractLinkHandler implements LinkHandlerInterfac
public function modifyLinkAttributes(array $fieldDefinitions)
{
$configuration = $this->linkBrowser->getConfiguration();
if (!empty($configuration['pageIdSelector.']['enabled'])) {
if (!empty($configuration['pageIdSelector']['enabled'])) {
$this->linkAttributes[] = 'pageIdSelector';
$fieldDefinitions['pageIdSelector'] = '
<form class="form-horizontal"><div class="form-group form-group-sm">
......