From bf25a81dc5dc3da2bd2374d2e0705ade2ac39e1d Mon Sep 17 00:00:00 2001 From: Oliver Hader <oliver@typo3.org> Date: Wed, 8 Sep 2021 23:46:56 +0200 Subject: [PATCH] [FEATURE] Introduce <f:transform.html> view-helper Introduces `<f:transform.html>` view-helper, providing capabilities to resolves system internal links, like `t3://`. Example: <f:transform.html selector="a.href,div.data-uri"> <a href="t3://page?uid=1" class="page">visit</a> <div data-uri="t3://page?uid=1" class="page trigger">visit</div> </f:transform.html> ... will be resolved and transformed to the following markup ... <a href="https://typo3.localhost/" class="page">visit</a> <div data-uri="https://typo3.localhost/" class="page trigger"> visit</div> Following Composer dependency is made explicit: composer req masterminds/html5:'^2.7' ext-dom:'*' Resolves: #95176 Releases: master Change-Id: Ib0101fbe120343dc404f0816da6d38946df0d931 Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/70977 Tested-by: core-ci <typo3@b13.com> Tested-by: Benni Mack <benni@typo3.org> Tested-by: Lina Wolf <112@linawolf.de> Tested-by: Oliver Hader <oliver.hader@typo3.org> Reviewed-by: Benni Mack <benni@typo3.org> Reviewed-by: Lina Wolf <112@linawolf.de> Reviewed-by: Oliver Hader <oliver.hader@typo3.org> --- composer.json | 2 + composer.lock | 3 +- .../DependencyInjection/CommonFactory.php | 33 +++ typo3/sysext/core/Configuration/Services.yaml | 3 + ...176-IntroduceFtransformhtmlView-helper.rst | 67 ++++++ typo3/sysext/core/composer.json | 2 + .../ViewHelpers/Transform/HtmlViewHelper.php | 116 ++++++++++ .../Transform/HtmlViewHelperTest.php | 206 ++++++++++++++++++ .../frontend/Classes/Html/HtmlWorker.php | 180 +++++++++++++++ .../Classes/Typolink/LinkResultFactory.php | 62 ++++++ .../frontend/Configuration/Services.yaml | 6 + 11 files changed, 679 insertions(+), 1 deletion(-) create mode 100644 typo3/sysext/core/Classes/DependencyInjection/CommonFactory.php create mode 100644 typo3/sysext/core/Documentation/Changelog/master/Feature-95176-IntroduceFtransformhtmlView-helper.rst create mode 100644 typo3/sysext/fluid/Classes/ViewHelpers/Transform/HtmlViewHelper.php create mode 100644 typo3/sysext/fluid/Tests/Functional/ViewHelpers/Transform/HtmlViewHelperTest.php create mode 100644 typo3/sysext/frontend/Classes/Html/HtmlWorker.php create mode 100644 typo3/sysext/frontend/Classes/Typolink/LinkResultFactory.php diff --git a/composer.json b/composer.json index d49a26ea1b37..45d786729bc3 100644 --- a/composer.json +++ b/composer.json @@ -33,6 +33,7 @@ "require": { "php": "^7.4 || ^8.0", "ext-PDO": "*", + "ext-dom": "*", "ext-json": "*", "ext-libxml": "*", "ext-pcre": "*", @@ -52,6 +53,7 @@ "guzzlehttp/guzzle": "^7.3.0", "guzzlehttp/promises": "^1.4.0", "guzzlehttp/psr7": "^1.7.0 || ^2.0", + "masterminds/html5": "^2.7", "nikic/php-parser": "^4.10.4", "phpdocumentor/reflection-docblock": "^5.2", "phpdocumentor/type-resolver": "^1.4", diff --git a/composer.lock b/composer.lock index dfe49e158eb1..edd1f5dd5f0c 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "1b0a189cecb7784c25b2a2e33e2de299", + "content-hash": "5f6335f542e4863fbf3cf8b727155030", "packages": [ { "name": "bacon/bacon-qr-code", @@ -8270,6 +8270,7 @@ "platform": { "php": "^7.4 || ^8.0", "ext-pdo": "*", + "ext-dom": "*", "ext-json": "*", "ext-libxml": "*", "ext-pcre": "*", diff --git a/typo3/sysext/core/Classes/DependencyInjection/CommonFactory.php b/typo3/sysext/core/Classes/DependencyInjection/CommonFactory.php new file mode 100644 index 000000000000..2f30ae8f06d4 --- /dev/null +++ b/typo3/sysext/core/Classes/DependencyInjection/CommonFactory.php @@ -0,0 +1,33 @@ +<?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\Core\DependencyInjection; + +use Masterminds\HTML5; + +/** + * @internal + */ +class CommonFactory +{ + public static function createHtml5Parser(): HTML5 + { + return new HTML5([ + 'disable_html_ns' => true, + ]); + } +} diff --git a/typo3/sysext/core/Configuration/Services.yaml b/typo3/sysext/core/Configuration/Services.yaml index c3861e7725ae..8ded353560a1 100644 --- a/typo3/sysext/core/Configuration/Services.yaml +++ b/typo3/sysext/core/Configuration/Services.yaml @@ -335,3 +335,6 @@ services: # External dependencies GuzzleHttp\Client: factory: ['TYPO3\CMS\Core\Http\Client\GuzzleClientFactory', 'getClient'] + Masterminds\HTML5: + public: true + factory: ['TYPO3\CMS\Core\DependencyInjection\CommonFactory', 'createHtml5Parser'] diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-95176-IntroduceFtransformhtmlView-helper.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-95176-IntroduceFtransformhtmlView-helper.rst new file mode 100644 index 000000000000..e5d82b427d83 --- /dev/null +++ b/typo3/sysext/core/Documentation/Changelog/master/Feature-95176-IntroduceFtransformhtmlView-helper.rst @@ -0,0 +1,67 @@ +.. include:: ../../Includes.txt + +========================================================== +Feature: #95176 - Introduce <f:transform.html> view helper +========================================================== + +See :issue:`95176` + +Description +=========== + +Using Fluid view helper :html:`<f:format.html>` provides capabilities to +resolve `t3://` URIs, which is used in backend contexts as well. Internally +:html:`<f:format.html>` relies on an existing frontend context, with +corresponding TypoScript configuration in :ts:`lib.parseFunc` being given. + +In order to separate concerns better, a new :html:`<f:transform.html>` +view helper has been introduced + +* to be used in frontend and backend context without relying on TypoScript, +* to avoid mixing parsing, sanitization and transformation concerns in + previously used :php:`ContentObjectRenderer::parseFunc` method of the + frontend rendering process. + +Impact +====== + +Individual TYPO3 link handlers (like `t3://` URIs) can be resolved and +substituted without relying on TypoScript configuration and without mixing +concerns in :php:`ContentObjectRenderer::parseFunc` by using Fluid view helper +:html:`<f:transform.html>`. + +Syntax +------ + +:html:`<f:transform.html selector="[ node.attr, node.attr ]" onFailure="[ behavior ]">` + +* `selector`: (optional) comma separated list of node attributes to be considered, + e.g. `subjects="a.href,a.data-uri,img.src"` (default `a.href`) +* `onFailure` (optional) corresponding behavior, in case transformation failed, e.g. + URI was invalid or could not be resolved properly (default `removeEnclosure`). + Based on example :html:`<a href="t3://INVALID">value</a>`. corresponding results + of each behavior would be like this: + + + `removeEnclosure`: :html:`value` (removed enclosing tag) + + `removeTag`: :html:`` (removed tag, incl. child nodes) + + `removeAttr`: :html:`<a>value</a>` (removed attribute) + + `null`: :html:`<a href="t3://INVALID">value</a>` (unmodified, as given) + +Example +------- + +.. code-block:: html + + <f:transform.html selector="a.href,div.data-uri"> + <a href="t3://page?uid=1" class="page">visit</a> + <div data-uri="t3://page?uid=1" class="page trigger">visit</div> + </f:transform.html> + +... will be resolved and transformed to the following markup ... + +.. code-block:: html + + <a href="https://typo3.localhost/" class="page">visit</a> + <div data-uri="https://typo3.localhost/" class="page trigger">visit</div> + +.. index:: Backend, Fluid, Frontend, ext:fluid diff --git a/typo3/sysext/core/composer.json b/typo3/sysext/core/composer.json index 30184367c653..06e7b26de53f 100644 --- a/typo3/sysext/core/composer.json +++ b/typo3/sysext/core/composer.json @@ -21,6 +21,7 @@ "require": { "php": "^7.4 || ^8.0", "ext-PDO": "*", + "ext-dom": "*", "ext-json": "*", "ext-libxml": "*", "ext-pcre": "*", @@ -39,6 +40,7 @@ "enshrined/svg-sanitize": "^0.14.1", "guzzlehttp/guzzle": "^7.3.0", "guzzlehttp/psr7": "^1.7.0 || ^2.0", + "masterminds/html5": "^2.7", "nikic/php-parser": "^4.10.4", "psr/container": "^1.1 || ^2.0", "psr/event-dispatcher": "^1.0", diff --git a/typo3/sysext/fluid/Classes/ViewHelpers/Transform/HtmlViewHelper.php b/typo3/sysext/fluid/Classes/ViewHelpers/Transform/HtmlViewHelper.php new file mode 100644 index 000000000000..b7ade44773d1 --- /dev/null +++ b/typo3/sysext/fluid/Classes/ViewHelpers/Transform/HtmlViewHelper.php @@ -0,0 +1,116 @@ +<?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\Fluid\ViewHelpers\Transform; + +use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Frontend\Html\HtmlWorker; +use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface; +use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper; +use TYPO3Fluid\Fluid\Core\ViewHelper\Exception as ViewHelperException; +use TYPO3Fluid\Fluid\Core\ViewHelper\Traits\CompileWithRenderStatic; + +/** + * Transforms HTML and substitutes internal link scheme aspects. + * + * Examples + * ======== + * + * Default parameters + * ------------------ + * + * :: + * + * <f:transform.html selector="a.href" onFailure="removeEnclosure"> + * <a href="t3://page?uid=1" class="home">Home</a> + * </f:transform.html> + * + * Output:: + * + * <a href="https://example.com/home" class="home">Home</a> + * + * Inline notation + * --------------- + * + * :: + * + * {content -> f:transform.html(selector:'a.href', onFailure:'removeEnclosure')} + */ +class HtmlViewHelper extends AbstractViewHelper +{ + use CompileWithRenderStatic; + + protected const MAP_ON_FAILURE = [ + '' => 0, + 'null' => 0, + 'removeTag' => HtmlWorker::REMOVE_TAG_ON_FAILURE, + 'removeAttr' => HtmlWorker::REMOVE_ATTR_ON_FAILURE, + 'removeEnclosure' => HtmlWorker::REMOVE_ENCLOSURE_ON_FAILURE, + ]; + + /** + * @var bool + */ + protected $escapeChildren = false; + + /** + * @var bool + */ + protected $escapeOutput = false; + + /** + * @throws ViewHelperException + */ + public function initializeArguments() + { + $this->registerArgument( + 'selector', + 'string', + 'comma separated list of node attributes to be considered', + false, + 'a.href' + ); + $this->registerArgument( + 'onFailure', + 'string', + 'behavior on failure, either `removeTag`, `removeAttr`, `removeEnclosure` or `null`', + false, + 'removeEnclosure' + ); + } + + /** + * @param array{selector: string} $arguments + * @param \Closure $renderChildrenClosure + * @param RenderingContextInterface $renderingContext + * + * @return string transformed markup + */ + public static function renderStatic(array $arguments, \Closure $renderChildrenClosure, RenderingContextInterface $renderingContext) + { + $content = $renderChildrenClosure(); + $worker = GeneralUtility::makeInstance(HtmlWorker::class); + + $selector = $arguments['selector']; + $onFailure = $arguments['onFailure']; + $onFailureFlags = self::MAP_ON_FAILURE[$onFailure] ?? HtmlWorker::REMOVE_ENCLOSURE_ON_FAILURE; + + return (string)$worker + ->parse($content) + ->transformUri($selector, $onFailureFlags); + } +} diff --git a/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Transform/HtmlViewHelperTest.php b/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Transform/HtmlViewHelperTest.php new file mode 100644 index 000000000000..ddd5f91bebee --- /dev/null +++ b/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Transform/HtmlViewHelperTest.php @@ -0,0 +1,206 @@ +<?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\Fluid\Tests\Functional\ViewHelpers\Transform; + +use TYPO3\CMS\Core\Core\SystemEnvironmentBuilder; +use TYPO3\CMS\Core\Http\ServerRequest; +use TYPO3\CMS\Core\Tests\Functional\SiteHandling\SiteBasedTestTrait; +use TYPO3\CMS\Fluid\View\StandaloneView; +use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase; + +class HtmlViewHelperTest extends FunctionalTestCase +{ + use SiteBasedTestTrait; + + protected const LANGUAGE_PRESETS = [ + 'EN' => ['id' => 0, 'title' => 'English', 'locale' => 'en_US.UTF8', 'iso' => 'en', 'hrefLang' => 'en-US', 'direction' => ''] + ]; + + protected $backupGlobals = true; + + protected function setUp(): void + { + parent::setUp(); + $this->importDataSet('EXT:core/Tests/Functional/Fixtures/pages.xml'); + $this->writeSiteConfiguration( + 'typo3-localhost', + $this->buildSiteConfiguration(1, 'https://typo3.localhost/'), + [$this->buildDefaultLanguageConfiguration('EN', '/')] + ); + + $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest('https://typo3.localhost/', 'GET')) + ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE); + } + + public static function isTransformedDataProvider(): array + { + return [ + 'any HTML tag' => [ + '<p>value a</p><p>value b</p>', + '<p>value a</p><p>value b</p>', + ], + 'unknown HTML tag' => [ + '<unknown>value</unknown>', + '<unknown>value</unknown>', + ], + 'empty' => [ + '<a href>visit</a>', + '<a href>visit</a>', + ], + 'invalid' => [ + '<a href="#">visit</a>', + '<a href="#">visit</a>', + ], + 'tel anchor' => [ + '<a href="tel:+123456789" class="phone voice">call</a>', + '<a href="tel:+123456789" class="phone voice">call</a>' + ], + 'mailto anchor' => [ + '<a href="mailto:test@typo3.localhost?subject=Test" class="mailto">send mail</a>', + '<a href="mailto:test@typo3.localhost?subject=Test" class="mailto">send mail</a>', + ], + 'https anchor' => [ + '<a href="https://typo3.localhost/path/visit.html" class="page">visit</a>', + '<a href="https://typo3.localhost/path/visit.html" class="page">visit</a>', + ], + 'absolute anchor' => [ + '<a href="/path/visit.html" class="page">visit</a>', + '<a href="/path/visit.html" class="page">visit</a>', + ], + 'relative anchor' => [ + '<a href="path/visit.html" class="page">visit</a>', + '<a href="path/visit.html" class="page">visit</a>', + ], + 't3-page anchor' => [ + '<a href="t3://page?uid=1" class="page">visit</a>', + '<a href="https://typo3.localhost/" class="page">visit</a>', + ], + 't3-page without uid anchor' => [ + '<a href="t3://page">visit</a>', + '<a href="https://typo3.localhost/">visit</a>', + ], + ]; + } + + /** + * @param string $payload + * @param string $expectation + * @test + * @dataProvider isTransformedDataProvider + */ + public function isTransformed(string $payload, string $expectation): void + { + $view = new StandaloneView(); + $view->setTemplateSource(sprintf('<f:transform.html>%s</f:transform.html>', $payload)); + self::assertSame($expectation, $view->render()); + } + + public static function isTransformedWithSelectorDataProvider(): array + { + return [ + 'a.href' => [ + 'a.href', + '<a href="t3://page?uid=1" class="page">visit</a>', + '<a href="https://typo3.localhost/" class="page">visit</a>' + ], + '.href' => [ + '.href', + '<a href="t3://page?uid=1" class="page">visit</a>', + '<a href="https://typo3.localhost/" class="page">visit</a>' + ], + 'div.data-uri' => [ + 'div.data-uri', + '<div data-uri="t3://page?uid=1" class="page">visit</div>', + '<div data-uri="https://typo3.localhost/" class="page">visit</div>' + ], + 'a.href,div.data-uri' => [ + 'a.href,div.data-uri', + '<a href="t3://page?uid=1">visit</a><div data-uri="t3://page?uid=1">visit</div>', + '<a href="https://typo3.localhost/">visit</a><div data-uri="https://typo3.localhost/">visit</div>', + ], + ]; + } + + /** + * @param string $selector + * @param string $payload + * @param string $expectation + * @test + * @dataProvider isTransformedWithSelectorDataProvider + */ + public function isTransformedWithSelector(string $selector, string $payload, string $expectation): void + { + $view = new StandaloneView(); + $view->setTemplateSource(sprintf('<f:transform.html selector="%s">%s</f:transform.html>', $selector, $payload)); + self::assertSame($expectation, $view->render()); + } + + public static function isTransformedWithOnFailureDataProvider(): array + { + return [ + 't3-page invalid uid anchor (default)' => [ + null, + '<a href="t3://page?uid=9876">visit</a>', + 'visit', + ], + 't3-page invalid uid anchor ("removeEnclosure")' => [ + 'removeEnclosure', + '<a href="t3://page?uid=9876">visit</a>', + 'visit', + ], + 't3-page invalid uid anchor ("removeTag")' => [ + 'removeTag', + '<a href="t3://page?uid=9876">visit</a>', + '', + ], + 't3-page invalid uid anchor ("removeAttr")' => [ + 'removeAttr', + '<a href="t3://page?uid=9876">visit</a>', + '<a>visit</a>', + ], + 't3-page invalid uid anchor ("null")' => [ + 'null', + '<a href="t3://page?uid=9876">visit</a>', + '<a href="t3://page?uid=9876">visit</a>', + ], + 't3-page invalid uid anchor ("")' => [ + '', + '<a href="t3://page?uid=9876">visit</a>', + '<a href="t3://page?uid=9876">visit</a>', + ], + ]; + } + + /** + * @param string|null $onFailure + * @param string $payload + * @param string $expectation + * @test + * @dataProvider isTransformedWithOnFailureDataProvider + */ + public function isTransformedWithOnFailure(?string $onFailure, string $payload, string $expectation): void + { + $view = new StandaloneView(); + $view->setTemplateSource(sprintf( + '<f:transform.html %s>%s</f:transform.html>', + $onFailure !== null ? 'onFailure="' . $onFailure . '"' : '', + $payload + )); + self::assertSame($expectation, $view->render()); + } +} diff --git a/typo3/sysext/frontend/Classes/Html/HtmlWorker.php b/typo3/sysext/frontend/Classes/Html/HtmlWorker.php new file mode 100644 index 000000000000..e16182bdd196 --- /dev/null +++ b/typo3/sysext/frontend/Classes/Html/HtmlWorker.php @@ -0,0 +1,180 @@ +<?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\Html; + +use DOMDocument; +use DOMDocumentFragment; +use DOMElement; +use DOMNode; +use DOMXPath; +use Exception; +use Masterminds\HTML5; +use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Frontend\Typolink\LinkResultFactory; + +/** + * @internal API still might change + */ +class HtmlWorker +{ + /** + * Removes corresponding tag in case there's a failure + * e.g. `<a href="t3://!!INVALID!!">value</a>` --> `` + */ + public const REMOVE_TAG_ON_FAILURE = 1; + + /** + * Removes corresponding attribute in case there's a failure + * e.g. `<a href="t3://!!INVALID!!">value</a>` --> `<a>value</a>` + */ + public const REMOVE_ATTR_ON_FAILURE = 2; + + /** + * Removes corresponding enclosure in case there's a failure + * e.g. `<a href="t3://!!INVALID!!">value</a>` --> `value` + */ + public const REMOVE_ENCLOSURE_ON_FAILURE = 4; + + protected LinkResultFactory $linkResultFactory; + protected HTML5 $parser; + + protected ?DOMNode $mount = null; + protected ?DOMDocument $document = null; + + public function __construct(LinkResultFactory $linkResultFactory, HTML5 $parser) + { + $this->linkResultFactory = $linkResultFactory; + $this->parser = $parser; + } + + public function __toString(): string + { + if (!$this->mount instanceof DOMNode || !$this->document instanceof DOMDocument) { + return ''; + } + return $this->parser->saveHTML($this->mount->childNodes); + } + + public function parse(string $html): self + { + // use document fragment to separate markup from default structure (html, body, ...) + $fragment = $this->parser->parseFragment($html); + // mount fragment to make it accessible in current document + $this->mount = $this->mountFragment($fragment); + $this->document = $this->mount->ownerDocument; + return $this; + } + + public function transformUri(string $selector, int $flags = 0): self + { + if (!$this->mount instanceof DOMNode || !$this->document instanceof DOMDocument) { + return $this; + } + $subjects = $this->parseSelector($selector); + // use xpath to traverse potential candidates having "links" + $xpath = new DOMXPath($this->document); + foreach ($subjects as $subject) { + $attrName = $subject['attr']; + $expression = sprintf('//%s[@%s]', $subject['node'], $attrName); + /** @var DOMElement $element */ + foreach ($xpath->query($expression, $this->mount) as $element) { + $elementAttrValue = $element->getAttribute($attrName); + $scheme = parse_url($elementAttrValue, PHP_URL_SCHEME); + // skip values not having a URI-scheme + if (empty($scheme)) { + continue; + } + try { + $linkResult = $this->linkResultFactory->createFromUriString($elementAttrValue); + } catch (Exception $exception) { + $this->onTransformUriFailure($element, $subject, $flags); + continue; + } + $linkResultAttrValues = array_filter($linkResult->getAttributes()); + // usually link results contain `href` attr value, which needs to be assigned + // to a different value in case selector (e.g. `img.src` instead f `a.href`) + if (isset($linkResultAttrValues['href']) && $attrName !== 'href') { + $element->setAttribute($attrName, $linkResultAttrValues['href']); + unset($linkResultAttrValues['href']); + } + foreach ($linkResultAttrValues as $name => $value) { + $element->setAttribute($name, $value); + } + } + } + return $this; + } + + /** + * @param DOMElement $element current element encountered failure + * @param array{node: string, attr: string} $subject node-attr combination + * @param int $flags + */ + protected function onTransformUriFailure(DOMElement $element, array $subject, int $flags): void + { + if (($flags & self::REMOVE_TAG_ON_FAILURE) === self::REMOVE_TAG_ON_FAILURE) { + $element->parentNode->removeChild($element); + } elseif (($flags & self::REMOVE_ATTR_ON_FAILURE) === self::REMOVE_ATTR_ON_FAILURE) { + $attrName = $subject['attr']; + $element->removeAttribute($attrName); + } elseif (($flags & self::REMOVE_ENCLOSURE_ON_FAILURE) === self::REMOVE_ENCLOSURE_ON_FAILURE) { + // moves children out of element's enclosure, then removes (empty) element + // eg `<ELEMENT><a><b><c></ELEMENT><NEXT>` + // 1) `<ELEMENT><b><c></ELEMENT><a><NEXT>` + // 2) `<ELEMENT><c></ELEMENT><a><b><NEXT>` + // 3) `<ELEMENT></ELEMENT><a><b><c><NEXT>` + // rm `<a><b><c><NEXT>` + foreach ($element->childNodes as $child) { + $element->parentNode->insertBefore($child, $element->nextSibling); + } + $element->parentNode->removeChild($element); + } + } + + /** + * @param string $selector + * @return array{node: string, attr: string}[] + */ + protected function parseSelector(string $selector): array + { + $items = GeneralUtility::trimExplode(',', $selector, true); + $items = array_map( + function (string $item): ?array { + $parts = explode('.', $item); + if (count($parts) !== 2) { + return null; + } + return [ + 'node' => $parts[0] ?: '*', + 'attr' => $parts[1], + ]; + }, + $items + ); + return array_filter($items); + } + + protected function mountFragment(DOMDocumentFragment $fragment): DOMNode + { + $document = $fragment->ownerDocument; + $mount = $document->createElement('div'); + $document->appendChild($mount); + $mount->appendChild($fragment); + return $mount; + } +} diff --git a/typo3/sysext/frontend/Classes/Typolink/LinkResultFactory.php b/typo3/sysext/frontend/Classes/Typolink/LinkResultFactory.php new file mode 100644 index 000000000000..65a5b286e20a --- /dev/null +++ b/typo3/sysext/frontend/Classes/Typolink/LinkResultFactory.php @@ -0,0 +1,62 @@ +<?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\Typolink; + +use TYPO3\CMS\Core\LinkHandling\LinkService; +use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer; + +/** + * Factory for LinkResult instances + */ +class LinkResultFactory +{ + protected LinkService $linkService; + + public function __construct(LinkService $linkService) + { + $this->linkService = $linkService; + } + + public function createFromUriString(string $uri): LinkResultInterface + { + $linkDetails = $this->linkService->resolve($uri); + $linkDetails['typoLinkParameter'] = $uri; + $linkType = $linkDetails['type'] ?? ''; + + $linkBuilder = $this->resolveLinkBuilder($linkType); + if ($linkBuilder !== null) { + return $linkBuilder->build($linkDetails, '', '', []); + } + return GeneralUtility::makeInstance( + LinkResult::class, + $linkDetails['type'] ?? '', + $linkDetails['url'] ?? $uri + ); + } + + protected function resolveLinkBuilder(string $linkType): ?AbstractTypolinkBuilder + { + $className = (string)($GLOBALS['TYPO3_CONF_VARS']['FE']['typolinkBuilder'][$linkType] ?? ''); + if (!is_a($className, AbstractTypolinkBuilder::class, true)) { + return null; + } + $contentObjectRenderer = GeneralUtility::makeInstance(ContentObjectRenderer::class); + return GeneralUtility::makeInstance($className, $contentObjectRenderer); + } +} diff --git a/typo3/sysext/frontend/Configuration/Services.yaml b/typo3/sysext/frontend/Configuration/Services.yaml index dc0765ac62ab..be4f789c40cf 100644 --- a/typo3/sysext/frontend/Configuration/Services.yaml +++ b/typo3/sysext/frontend/Configuration/Services.yaml @@ -36,3 +36,9 @@ services: - name: event.listener identifier: 'typo3-frontend/overlay' method: 'languageAndWorkspaceOverlay' + + TYPO3\CMS\Frontend\Html\HtmlWorker: + public: true + + TYPO3\CMS\Frontend\Typolink\LinkResultFactory: + public: true -- GitLab