TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
<?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\Html;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Core\Resource\Security\SvgSanitizer;
|
||||
use TYPO3\CMS\Core\SingletonInterface;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\PathUtility;
|
||||
use TYPO3\HtmlSanitizer\Behavior;
|
||||
use TYPO3\HtmlSanitizer\Behavior\NodeInterface;
|
||||
use TYPO3\HtmlSanitizer\Builder\CommonBuilder;
|
||||
use TYPO3\HtmlSanitizer\Context;
|
||||
use TYPO3\HtmlSanitizer\Sanitizer;
|
||||
use TYPO3\HtmlSanitizer\Visitor\CommonVisitor;
|
||||
|
||||
/**
|
||||
* Builder, creating a `Sanitizer` instance with "default"
|
||||
* behavior for tags, attributes and values.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class DefaultSanitizerBuilder extends CommonBuilder implements SingletonInterface
|
||||
{
|
||||
private Behavior $behavior;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
// + URL must be on local host, or is absolute URI path
|
||||
$isOnCurrentHostAttr = new Behavior\ClosureAttrValue(
|
||||
// @todo: This closure has a late dependency to $GLOBALS['TYPO3_REQUEST'] that should eventually
|
||||
// be made explicit, probably by handing over ServerRequestInterface to build().
|
||||
static function (string $value): bool {
|
||||
/** @var ServerRequestInterface|null $request */
|
||||
$request = $GLOBALS['TYPO3_REQUEST'] ?? null;
|
||||
if ($request === null) {
|
||||
throw new \RuntimeException('DefaultSanitizerBuilder requires an active PSR-7 request with normalizedParams attribute.', 1775675289);
|
||||
}
|
||||
return GeneralUtility::isValidUrl($value) && GeneralUtility::isOnCurrentHost($value, $request)
|
||||
|| PathUtility::isAbsolutePath($value) && GeneralUtility::isAllowedAbsPath($value);
|
||||
}
|
||||
);
|
||||
// + starting with `t3://`
|
||||
$isTypo3Uri = new Behavior\RegExpAttrValue('#^t3://#');
|
||||
|
||||
// extends common attributes for TYPO3-specific URIs
|
||||
$this->srcAttr->addValues($isOnCurrentHostAttr);
|
||||
$this->hrefAttr->addValues($isOnCurrentHostAttr, $isTypo3Uri);
|
||||
|
||||
// @todo `style` used in Introduction Package, inline CSS should be removed
|
||||
$this->globalAttrs[] = new Behavior\Attr('style');
|
||||
}
|
||||
|
||||
public function build(): Sanitizer
|
||||
{
|
||||
$behavior = $this->createBehavior();
|
||||
$visitor = GeneralUtility::makeInstance(CommonVisitor::class, $behavior);
|
||||
return GeneralUtility::makeInstance(Sanitizer::class, $behavior, $visitor);
|
||||
}
|
||||
|
||||
protected function createBehavior(): Behavior
|
||||
{
|
||||
if (!isset($this->behavior)) {
|
||||
$this->behavior = parent::createBehavior()
|
||||
->withName('default')
|
||||
->withNodes(new Behavior\NodeHandler(
|
||||
new Behavior\Tag('svg'),
|
||||
new Behavior\Handler\ClosureHandler(
|
||||
static function (NodeInterface $node, ?\DOMNode $domNode, Context $context): ?\DOMNode {
|
||||
if ($domNode === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$newNode = GeneralUtility::makeInstance(SvgSanitizer::class)
|
||||
->sanitizeNode($domNode);
|
||||
|
||||
// purge empty svg nodes
|
||||
if ($newNode->childNodes->length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$fragment = $domNode->ownerDocument->createDocumentFragment();
|
||||
$fragment->append($newNode);
|
||||
return $fragment;
|
||||
}
|
||||
)
|
||||
));
|
||||
}
|
||||
return $this->behavior;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?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\Html\Event;
|
||||
|
||||
/**
|
||||
* Event that is fired after RteHtmlParser modified the HTML input from RTE editor to the database
|
||||
* (for example transforming linebreaks)
|
||||
*/
|
||||
final class AfterTransformTextForPersistenceEvent
|
||||
{
|
||||
public function __construct(
|
||||
private string $htmlContent,
|
||||
private readonly string $initialHtmlContent,
|
||||
private readonly array $processingConfiguration
|
||||
) {}
|
||||
|
||||
public function getHtmlContent(): string
|
||||
{
|
||||
return $this->htmlContent;
|
||||
}
|
||||
|
||||
public function setHtmlContent(string $htmlContent): void
|
||||
{
|
||||
$this->htmlContent = $htmlContent;
|
||||
}
|
||||
|
||||
public function getInitialHtmlContent(): string
|
||||
{
|
||||
return $this->initialHtmlContent;
|
||||
}
|
||||
|
||||
public function getProcessingConfiguration(): array
|
||||
{
|
||||
return $this->processingConfiguration;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?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\Html\Event;
|
||||
|
||||
/**
|
||||
* Event that is fired after RteHtmlParser modified the HTML input from the database to the RTE editor
|
||||
* (for example transforming linebreaks)
|
||||
*/
|
||||
final class AfterTransformTextForRichTextEditorEvent
|
||||
{
|
||||
public function __construct(
|
||||
private string $htmlContent,
|
||||
private readonly string $initialHtmlContent,
|
||||
private readonly array $processingConfiguration
|
||||
) {}
|
||||
|
||||
public function getHtmlContent(): string
|
||||
{
|
||||
return $this->htmlContent;
|
||||
}
|
||||
|
||||
public function setHtmlContent(string $htmlContent): void
|
||||
{
|
||||
$this->htmlContent = $htmlContent;
|
||||
}
|
||||
|
||||
public function getInitialHtmlContent(): string
|
||||
{
|
||||
return $this->initialHtmlContent;
|
||||
}
|
||||
|
||||
public function getProcessingConfiguration(): array
|
||||
{
|
||||
return $this->processingConfiguration;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?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\Html\Event;
|
||||
|
||||
/**
|
||||
* Event that is fired before RteHtmlParser modified the HTML input from RTE editor to the database
|
||||
* (for example transforming linebreaks)
|
||||
*/
|
||||
final class BeforeTransformTextForPersistenceEvent
|
||||
{
|
||||
public function __construct(
|
||||
private string $htmlContent,
|
||||
private readonly string $initialHtmlContent,
|
||||
private readonly array $processingConfiguration
|
||||
) {}
|
||||
|
||||
public function getHtmlContent(): string
|
||||
{
|
||||
return $this->htmlContent;
|
||||
}
|
||||
|
||||
public function setHtmlContent(string $htmlContent): void
|
||||
{
|
||||
$this->htmlContent = $htmlContent;
|
||||
}
|
||||
|
||||
public function getInitialHtmlContent(): string
|
||||
{
|
||||
return $this->initialHtmlContent;
|
||||
}
|
||||
|
||||
public function getProcessingConfiguration(): array
|
||||
{
|
||||
return $this->processingConfiguration;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?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\Html\Event;
|
||||
|
||||
/**
|
||||
* Event that is fired before RteHtmlParser modified the HTML input from the database to the RTE editor
|
||||
* (for example transforming linebreaks)
|
||||
*/
|
||||
final class BeforeTransformTextForRichTextEditorEvent
|
||||
{
|
||||
public function __construct(
|
||||
private string $htmlContent,
|
||||
private readonly string $initialHtmlContent,
|
||||
private readonly array $processingConfiguration
|
||||
) {}
|
||||
|
||||
public function getHtmlContent(): string
|
||||
{
|
||||
return $this->htmlContent;
|
||||
}
|
||||
|
||||
public function setHtmlContent(string $htmlContent): void
|
||||
{
|
||||
$this->htmlContent = $htmlContent;
|
||||
}
|
||||
|
||||
public function getInitialHtmlContent(): string
|
||||
{
|
||||
return $this->initialHtmlContent;
|
||||
}
|
||||
|
||||
public function getProcessingConfiguration(): array
|
||||
{
|
||||
return $this->processingConfiguration;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?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\Html\Event;
|
||||
|
||||
use Psr\EventDispatcher\StoppableEventInterface;
|
||||
|
||||
/**
|
||||
* Event that is fired to validate if a link is valid or not.
|
||||
*/
|
||||
final class BrokenLinkAnalysisEvent implements StoppableEventInterface
|
||||
{
|
||||
private bool $isBroken = false;
|
||||
private bool $linkWasChecked = false;
|
||||
|
||||
/**
|
||||
* Message why a link was broken (used in e.g. RteHtmlParser as info)
|
||||
*/
|
||||
private string $reason = '';
|
||||
|
||||
public function __construct(private readonly string $linkType, private readonly array $linkData) {}
|
||||
|
||||
public function isPropagationStopped(): bool
|
||||
{
|
||||
// prevent other listeners from being called if link has been checked
|
||||
return $this->linkWasChecked;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the link type as string
|
||||
* @see LinkService types
|
||||
*/
|
||||
public function getLinkType(): string
|
||||
{
|
||||
return $this->linkType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns resolved LinkService data, depending on the type
|
||||
*/
|
||||
public function getLinkData(): array
|
||||
{
|
||||
return $this->linkData;
|
||||
}
|
||||
|
||||
public function markAsCheckedLink(): void
|
||||
{
|
||||
$this->linkWasChecked = true;
|
||||
}
|
||||
|
||||
public function markAsBrokenLink(string $reason = ''): void
|
||||
{
|
||||
$this->isBroken = true;
|
||||
$this->reason = $reason;
|
||||
}
|
||||
|
||||
public function isBrokenLink(): bool
|
||||
{
|
||||
return $this->isBroken;
|
||||
}
|
||||
|
||||
public function getReason(): string
|
||||
{
|
||||
return $this->reason;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
<?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\Html;
|
||||
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
readonly class HtmlCropper
|
||||
{
|
||||
protected const TAGS = 'a|abbr|address|area|article|aside|audio|b|bdi|bdo|blockquote|body|br|button|caption|cite|code|col|colgroup|data|datalist|dd|del|dfn|div|dl|dt|em|embed|fieldset|figcaption|figure|font|footer|form|h1|h2|h3|h4|h5|h6|header|hr|i|iframe|img|input|ins|kbd|keygen|label|legend|li|link|main|map|mark|meter|nav|object|ol|optgroup|option|output|p|param|pre|progress|q|rb|rp|rt|rtc|ruby|s|samp|section|select|small|source|span|strong|sub|sup|table|tbody|td|textarea|tfoot|th|thead|time|tr|track|u|ul|ut|var|video|wbr';
|
||||
|
||||
protected const TAGS_REG_EXP = '
|
||||
(
|
||||
(?:
|
||||
<!--.*?--> # a comment
|
||||
|
|
||||
<canvas[^>]*>.*?</canvas> # a canvas tag
|
||||
|
|
||||
<script[^>]*>.*?</script> # a script tag
|
||||
|
|
||||
<noscript[^>]*>.*?</noscript> # a noscript tag
|
||||
|
|
||||
<template[^>]*>.*?</template> # a template tag
|
||||
)
|
||||
|
|
||||
</?(?:%s)+ # opening tag (\'<tag\') or closing tag (\'</tag\')
|
||||
(?:
|
||||
(?:
|
||||
(?:
|
||||
\\s+\\w[\\w-]* # EITHER spaces, followed by attribute names
|
||||
(?:
|
||||
\\s*=?\\s* # equals
|
||||
(?>
|
||||
".*?" # attribute values in double-quotes
|
||||
|
|
||||
\'.*?\' # attribute values in single-quotes
|
||||
|
|
||||
[^\'">\\s]+ # plain attribute values
|
||||
)
|
||||
)?
|
||||
)
|
||||
| # OR a single dash (for TYPO3 link tag)
|
||||
(?:
|
||||
\\s+-
|
||||
)
|
||||
)+\\s*
|
||||
| # OR only spaces
|
||||
\\s*
|
||||
)
|
||||
/?> # closing the tag with \'>\' or \'/>\'
|
||||
)';
|
||||
|
||||
public function __construct(
|
||||
private LoggerInterface $logger,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Implements "cropHTML" which is a modified "substr" function allowing to limit a string length to a certain number
|
||||
* of chars (from either start or end of string) and having a pre/postfix applied if the string really was cropped.
|
||||
*
|
||||
* @param string $content The string to perform the operation on
|
||||
* @param int $numberOfChars Max number of chars of the string. Negative value means cropping from end of string.
|
||||
* @param string $replacementForEllipsis The pre/postfix string to apply if cropping occurs.
|
||||
* @param bool $cropToSpace If true then crop will be applied at nearest space.
|
||||
* @return string The processed input value.
|
||||
*/
|
||||
public function crop(string $content, int $numberOfChars, string $replacementForEllipsis, bool $cropToSpace): string
|
||||
{
|
||||
$cropFromRight = $numberOfChars < 0;
|
||||
|
||||
$sections = $this->splitContentIntoSections($content, $cropFromRight);
|
||||
|
||||
// Only crop text sections (chars of tag-blocks are not counted).
|
||||
$strLengthOfAllPrevTextSections = 0;
|
||||
|
||||
// This is the offset of the content item which was cropped.
|
||||
$croppedOffset = null;
|
||||
$amountOfSections = count($sections);
|
||||
|
||||
// For cropSectionToNextSpace we need a collection of all processed text sections
|
||||
$processedTextSectionsForCropping = [];
|
||||
|
||||
for ($offset = 0; $offset < $amountOfSections; $offset++) {
|
||||
if ($this->isTextSection($offset)) {
|
||||
$contentOfCurrentSection = $sections[$offset];
|
||||
$strLengthOfCurrentSection = mb_strlen(
|
||||
html_entity_decode($contentOfCurrentSection, ENT_COMPAT, 'UTF-8'),
|
||||
'utf-8'
|
||||
);
|
||||
|
||||
if ($strLengthOfAllPrevTextSections + $strLengthOfCurrentSection > abs($numberOfChars)) {
|
||||
$croppedOffset = $offset;
|
||||
$cropPosition = $this->getCropPosition(
|
||||
$contentOfCurrentSection,
|
||||
$numberOfChars,
|
||||
$strLengthOfAllPrevTextSections,
|
||||
$cropFromRight
|
||||
);
|
||||
|
||||
// Main cropping. Note the +1 and -1. These are there to be able to
|
||||
// check for space characters later on.
|
||||
$contentOfCurrentSection = !$cropFromRight
|
||||
? mb_substr($contentOfCurrentSection, 0, $cropPosition + 1)
|
||||
: mb_substr($contentOfCurrentSection, -$cropPosition - 1);
|
||||
|
||||
$contentOfCurrentSection = $this->cropSectionToNextSpace(
|
||||
$contentOfCurrentSection,
|
||||
$processedTextSectionsForCropping,
|
||||
$cropToSpace,
|
||||
$cropFromRight
|
||||
);
|
||||
|
||||
$sections[$offset] = $contentOfCurrentSection;
|
||||
break;
|
||||
}
|
||||
$strLengthOfAllPrevTextSections += $strLengthOfCurrentSection;
|
||||
if ($contentOfCurrentSection !== '') {
|
||||
$processedTextSectionsForCropping[] = $contentOfCurrentSection;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$sections = $this->closeCroppedTags($sections, $croppedOffset, $numberOfChars, $replacementForEllipsis);
|
||||
|
||||
// Reverse array once again if we are cropping from the end.
|
||||
if ($numberOfChars < 0) {
|
||||
$sections = array_reverse($sections);
|
||||
}
|
||||
|
||||
return implode('', $sections);
|
||||
}
|
||||
|
||||
/**
|
||||
* Split $content into an array(even items in the array are outside the tags, odd numbers are tag-blocks).
|
||||
*/
|
||||
protected function splitContentIntoSections(string $content, bool $cropFromRight): array
|
||||
{
|
||||
$splitPattern = sprintf(
|
||||
self::TAGS_REG_EXP,
|
||||
self::TAGS
|
||||
);
|
||||
|
||||
$sections = preg_split(
|
||||
'%' . $splitPattern . '%xs',
|
||||
$content,
|
||||
-1,
|
||||
PREG_SPLIT_DELIM_CAPTURE
|
||||
);
|
||||
if ($sections === false) {
|
||||
$this->logger->debug('Unable to split "{content}" into tags.', ['content' => $content]);
|
||||
$sections = [];
|
||||
}
|
||||
|
||||
// Reverse array if we are cropping from right.
|
||||
if ($cropFromRight) {
|
||||
$sections = array_reverse($sections);
|
||||
}
|
||||
|
||||
return $sections;
|
||||
}
|
||||
|
||||
protected function getCropPosition(
|
||||
string $contentOfCurrentSection,
|
||||
int $numberOfChars,
|
||||
int $strLengthOfAllPrevTextSections,
|
||||
bool $cropFromRight
|
||||
): int {
|
||||
$cropPosition = abs($numberOfChars) - $strLengthOfAllPrevTextSections;
|
||||
|
||||
// The snippet "&[^&\s;]{2,8};" in the RegEx below represents entities.
|
||||
$entityPattern = '/&[^&\\s;]{2,8};/';
|
||||
preg_match_all($entityPattern, $contentOfCurrentSection, $matches);
|
||||
$entityMatches = $matches[0];
|
||||
|
||||
// If we have found any html entities, these should be counted as 1 character.
|
||||
// Strategy is to replace all found entities with an arbitrary character ($)
|
||||
// and use this new string to count offsets.
|
||||
if ($entityMatches !== []) {
|
||||
$escapedContent = str_replace('$', ' ', $contentOfCurrentSection);
|
||||
$replacedContent = preg_replace($entityPattern, '$', $escapedContent, -1);
|
||||
$croppedContent = !$cropFromRight
|
||||
? mb_substr($replacedContent, 0, $cropPosition)
|
||||
: mb_substr($replacedContent, $numberOfChars, $cropPosition);
|
||||
|
||||
// In case of negative offsets, we need to reverse everything.
|
||||
// Because the string is cropped from behind, the entities
|
||||
// have to be replaced in reverse, too.
|
||||
if ($cropFromRight) {
|
||||
$croppedContent = strrev($croppedContent);
|
||||
$entityMatches = array_reverse($entityMatches);
|
||||
}
|
||||
|
||||
foreach ($entityMatches as $entity) {
|
||||
$croppedContent = preg_replace('/\$/', $entity, $croppedContent, 1);
|
||||
}
|
||||
|
||||
$cropPosition = mb_strlen($croppedContent);
|
||||
}
|
||||
|
||||
return $cropPosition;
|
||||
}
|
||||
|
||||
protected function closeCroppedTags(
|
||||
array $sections,
|
||||
?int $croppedOffset,
|
||||
int $numberOfChars,
|
||||
string $replacementForEllipsis
|
||||
): array {
|
||||
$closingTags = [];
|
||||
if ($croppedOffset !== null) {
|
||||
$openingTagRegEx = '#^<(\\w+)(?:\\s|>)#';
|
||||
$closingTagRegEx = '#^</(\\w+)(?:\\s|>)#';
|
||||
for ($offset = $croppedOffset - 1; $offset >= 0; $offset = $offset - 2) {
|
||||
if (str_ends_with($sections[$offset], '/>')) {
|
||||
// Ignore empty element tags (e.g. <br />).
|
||||
continue;
|
||||
}
|
||||
|
||||
preg_match($numberOfChars < 0 ? $closingTagRegEx : $openingTagRegEx, $sections[$offset], $matches);
|
||||
$tagName = $matches[1] ?? null;
|
||||
if ($tagName !== null) {
|
||||
// Seek for the closing (or opening) tag.
|
||||
$amountOfSections = count($sections);
|
||||
for ($seekingOffset = $offset + 2; $seekingOffset < $amountOfSections; $seekingOffset = $seekingOffset + 2) {
|
||||
preg_match($numberOfChars < 0 ? $openingTagRegEx : $closingTagRegEx, $sections[$seekingOffset], $matches);
|
||||
$seekingTagName = $matches[1] ?? null;
|
||||
if ($tagName === $seekingTagName) {
|
||||
// We found a matching tag.
|
||||
// Add closing tag only if it occurs after the cropped content item.
|
||||
if ($seekingOffset > $croppedOffset) {
|
||||
$closingTags[] = $sections[$seekingOffset];
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Drop the cropped items of the content array. The $closingTags will be added later on again.
|
||||
array_splice($sections, $croppedOffset + 1);
|
||||
}
|
||||
|
||||
return array_merge($sections, [
|
||||
$croppedOffset !== null ? trim($replacementForEllipsis) : '',
|
||||
], $closingTags);
|
||||
}
|
||||
|
||||
protected function cropSectionToNextSpace(
|
||||
string $contentOfCurrentSection,
|
||||
array $processedTextSectionsForCropping,
|
||||
bool $cropToSpace,
|
||||
bool $cropFromRight
|
||||
): string {
|
||||
// Crop to space means, we ensure to crop before (or after) a space.
|
||||
// If there are no spaces, this option has no effect.
|
||||
$cropToSpaceApplied = false;
|
||||
if ($cropToSpace) {
|
||||
$exploded = explode(' ', $contentOfCurrentSection);
|
||||
if (!$cropFromRight) {
|
||||
array_unshift(
|
||||
$exploded,
|
||||
...$processedTextSectionsForCropping
|
||||
);
|
||||
} else {
|
||||
array_push(
|
||||
$exploded,
|
||||
...$processedTextSectionsForCropping
|
||||
);
|
||||
}
|
||||
|
||||
if (count($exploded) > 1) {
|
||||
if (!$cropFromRight && $exploded[count($exploded) - 1] !== ' ') {
|
||||
array_pop($exploded);
|
||||
$cropToSpaceApplied = true;
|
||||
} elseif ($exploded[0] !== ' ') {
|
||||
array_shift($exploded);
|
||||
$cropToSpaceApplied = true;
|
||||
}
|
||||
}
|
||||
$exploded = array_diff($exploded, $processedTextSectionsForCropping);
|
||||
$contentOfCurrentSection = implode(' ', $exploded);
|
||||
}
|
||||
|
||||
// Only remove the extra character again, if crop2space did not apply anything.
|
||||
if (!$cropToSpaceApplied) {
|
||||
$contentOfCurrentSection = !$cropFromRight
|
||||
? mb_substr($contentOfCurrentSection, 0, -1)
|
||||
: mb_substr($contentOfCurrentSection, 1);
|
||||
}
|
||||
|
||||
return $contentOfCurrentSection;
|
||||
}
|
||||
|
||||
protected function isTextSection(int $offset): bool
|
||||
{
|
||||
return $offset % 2 === 0;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,104 @@
|
||||
<?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\Html;
|
||||
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\HtmlSanitizer\Behavior;
|
||||
use TYPO3\HtmlSanitizer\Behavior\Attr\UriAttrValueBuilder;
|
||||
use TYPO3\HtmlSanitizer\Builder\BuilderInterface;
|
||||
use TYPO3\HtmlSanitizer\Sanitizer;
|
||||
use TYPO3\HtmlSanitizer\Visitor\CommonVisitor;
|
||||
|
||||
/**
|
||||
* Builder, creating a `Sanitizer` instance for "i18n"
|
||||
* behavior for tags, attributes and values. Basically used
|
||||
* for language labels containing HTML.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
readonly class I18nSanitizerBuilder implements BuilderInterface
|
||||
{
|
||||
public function build(): Sanitizer
|
||||
{
|
||||
$globalAttrs = $this->createGlobalAttrs();
|
||||
$httpUriBuilder = GeneralUtility::makeInstance(UriAttrValueBuilder::class)
|
||||
->allowSchemes('http', 'https');
|
||||
|
||||
$behavior = GeneralUtility::makeInstance(Behavior::class)
|
||||
->withTags(
|
||||
(new Behavior\Tag('a', Behavior\Tag::ALLOW_CHILDREN))
|
||||
->addAttrs(...$globalAttrs)
|
||||
->addAttrs(...$this->createAttrs('rel', 'target'))
|
||||
->addAttrs(
|
||||
(new Behavior\Attr('href'))
|
||||
->withValues(...$httpUriBuilder->getValues()),
|
||||
),
|
||||
(new Behavior\Tag('b', Behavior\Tag::ALLOW_CHILDREN))
|
||||
->addAttrs(...$globalAttrs),
|
||||
(new Behavior\Tag('br'))
|
||||
->addAttrs(...$globalAttrs),
|
||||
(new Behavior\Tag('div', Behavior\Tag::ALLOW_CHILDREN))
|
||||
->addAttrs(...$globalAttrs),
|
||||
(new Behavior\Tag('em', Behavior\Tag::ALLOW_CHILDREN))
|
||||
->addAttrs(...$globalAttrs),
|
||||
(new Behavior\Tag('i', Behavior\Tag::ALLOW_CHILDREN))
|
||||
->addAttrs(...$globalAttrs),
|
||||
(new Behavior\Tag('li', Behavior\Tag::ALLOW_CHILDREN))
|
||||
->addAttrs(...$globalAttrs),
|
||||
(new Behavior\Tag('span', Behavior\Tag::ALLOW_CHILDREN))
|
||||
->addAttrs(...$globalAttrs),
|
||||
(new Behavior\Tag('strong', Behavior\Tag::ALLOW_CHILDREN))
|
||||
->addAttrs(...$globalAttrs),
|
||||
(new Behavior\Tag('ul', Behavior\Tag::ALLOW_CHILDREN))
|
||||
->addAttrs(...$globalAttrs)
|
||||
);
|
||||
|
||||
$visitor = GeneralUtility::makeInstance(CommonVisitor::class, $behavior);
|
||||
return GeneralUtility::makeInstance(Sanitizer::class, $behavior, $visitor);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Behavior\Attr[]
|
||||
*/
|
||||
protected function createGlobalAttrs(): array
|
||||
{
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes
|
||||
$attrs = $this->createAttrs(
|
||||
'class',
|
||||
'role',
|
||||
'tabindex',
|
||||
'title',
|
||||
);
|
||||
$attrs[] = new Behavior\Attr('aria-', Behavior\Attr::NAME_PREFIX);
|
||||
$attrs[] = new Behavior\Attr('data-', Behavior\Attr::NAME_PREFIX);
|
||||
return $attrs;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Behavior\Attr[]
|
||||
*/
|
||||
protected function createAttrs(string ...$names): array
|
||||
{
|
||||
return array_map(
|
||||
static function (string $name): Behavior\Attr {
|
||||
return new Behavior\Attr($name);
|
||||
},
|
||||
$names
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?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\Html;
|
||||
|
||||
use TYPO3\CMS\Core\Html\Visitor\UnwrapTagVisitor;
|
||||
use TYPO3\CMS\Core\SingletonInterface;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\HtmlSanitizer\Builder\CommonBuilder;
|
||||
use TYPO3\HtmlSanitizer\Sanitizer;
|
||||
use TYPO3\HtmlSanitizer\Visitor\CommonVisitor;
|
||||
|
||||
/**
|
||||
* Builder, creating a `Sanitizer` instance for previews in backend, skipping any links
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class PreviewSanitizerBuilder extends CommonBuilder implements SingletonInterface
|
||||
{
|
||||
public function build(): Sanitizer
|
||||
{
|
||||
$behavior = $this->createBehavior();
|
||||
$visitor = GeneralUtility::makeInstance(CommonVisitor::class, $behavior);
|
||||
$unwrapTagVisitor = GeneralUtility::makeInstance(UnwrapTagVisitor::class);
|
||||
return GeneralUtility::makeInstance(Sanitizer::class, $behavior, $visitor, $unwrapTagVisitor);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,873 @@
|
||||
<?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\Html;
|
||||
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||
use TYPO3\CMS\Core\Configuration\Features;
|
||||
use TYPO3\CMS\Core\Html\Event\AfterTransformTextForPersistenceEvent;
|
||||
use TYPO3\CMS\Core\Html\Event\AfterTransformTextForRichTextEditorEvent;
|
||||
use TYPO3\CMS\Core\Html\Event\BeforeTransformTextForPersistenceEvent;
|
||||
use TYPO3\CMS\Core\Html\Event\BeforeTransformTextForRichTextEditorEvent;
|
||||
use TYPO3\CMS\Core\Html\Event\BrokenLinkAnalysisEvent;
|
||||
use TYPO3\CMS\Core\LinkHandling\Exception\UnknownLinkHandlerException;
|
||||
use TYPO3\CMS\Core\LinkHandling\LinkService;
|
||||
use TYPO3\CMS\Core\Resource\Exception\InsufficientFolderAccessPermissionsException;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\HtmlSanitizer\Builder\BuilderInterface;
|
||||
|
||||
/**
|
||||
* Class for parsing HTML for the Rich Text Editor. (also called transformations)
|
||||
*
|
||||
* Concerning line breaks:
|
||||
* Regardless if LF (Unix-style) or CRLF (Windows) was put in, the HtmlParser works with LFs and migrates all
|
||||
* line breaks to LFs internally, however when all transformations are done, all LFs are transformed to CRLFs.
|
||||
* This means: RteHtmlParser always returns CRLFs to be maximum compatible with all formats.
|
||||
*/
|
||||
#[Autoconfigure(public: true)]
|
||||
class RteHtmlParser extends HtmlParser
|
||||
{
|
||||
/**
|
||||
* List of elements that are not wrapped into a "p" tag while doing the transformation.
|
||||
*/
|
||||
protected string $blockElementList = 'DIV,TABLE,BLOCKQUOTE,PRE,UL,OL,H1,H2,H3,H4,H5,H6,ADDRESS,DL,DD,HEADER,SECTION,FOOTER,NAV,ARTICLE,ASIDE,FIGURE,FIGCAPTION';
|
||||
|
||||
/**
|
||||
* List of all tags that are allowed by default
|
||||
*/
|
||||
protected string $defaultAllowedTagsList = 'b,i,u,a,img,br,div,center,pre,figure,figcaption,font,hr,sub,sup,p,strong,em,li,ul,ol,blockquote,strike,span,abbr,acronym,dfn,s,mark';
|
||||
|
||||
/**
|
||||
* Set to the TSconfig options coming from page TSconfig
|
||||
*/
|
||||
protected array $procOptions = [];
|
||||
|
||||
/**
|
||||
* Run-away brake for recursive calls.
|
||||
*/
|
||||
protected int $TS_transform_db_safecounter = 100;
|
||||
|
||||
/**
|
||||
* Data caching for processing function
|
||||
*/
|
||||
protected array $getKeepTags_cache = [];
|
||||
|
||||
/**
|
||||
* Storage of the allowed CSS class names in the RTE
|
||||
*/
|
||||
protected array $allowedClasses = [];
|
||||
|
||||
/**
|
||||
* A list of HTML attributes for <p> tags. Because <p> tags are wrapped currently in a special handling,
|
||||
* they have a special place for configuration via 'proc.keepPDIVattribs'
|
||||
*/
|
||||
protected array $allowedAttributesForParagraphTags = [
|
||||
'class',
|
||||
'align',
|
||||
'id',
|
||||
'title',
|
||||
'dir',
|
||||
'lang',
|
||||
'xml:lang',
|
||||
'itemscope',
|
||||
'itemtype',
|
||||
'itemprop',
|
||||
];
|
||||
|
||||
/**
|
||||
* Any tags that are allowed outside of <p> sections - usually similar to the block elements
|
||||
* plus some special tags like <hr> and <img> (if images are allowed).
|
||||
* Completely overrideable via 'proc.allowTagsOutside'
|
||||
*/
|
||||
protected array $allowedTagsOutsideOfParagraphs = [
|
||||
'address',
|
||||
'article',
|
||||
'aside',
|
||||
'blockquote',
|
||||
'div',
|
||||
'footer',
|
||||
'figure',
|
||||
'figcaption',
|
||||
'header',
|
||||
'hr',
|
||||
'nav',
|
||||
'section',
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
protected readonly EventDispatcherInterface $eventDispatcher,
|
||||
protected readonly LoggerInterface $logger,
|
||||
protected readonly LinkService $linkService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Sanitize and streamline given options (usually from RichTextConfiguration results "proc."
|
||||
* and set them to the respective properties.
|
||||
*/
|
||||
protected function setProcessingConfiguration(array $processingConfiguration): void
|
||||
{
|
||||
$this->procOptions = $processingConfiguration;
|
||||
$this->getKeepTags_cache = [];
|
||||
|
||||
if (isset($this->procOptions['allowedClasses.'])) {
|
||||
$this->allowedClasses = (array)$this->procOptions['allowedClasses.'];
|
||||
} else {
|
||||
$this->allowedClasses = GeneralUtility::trimExplode(',', $this->procOptions['allowedClasses'] ?? '', true);
|
||||
}
|
||||
|
||||
// Dynamic configuration of blockElementList
|
||||
if (!empty($this->procOptions['blockElementList'])) {
|
||||
if (!isset($this->procOptions['blockElementList.'])) {
|
||||
$blockElementList = GeneralUtility::trimExplode(',', $this->procOptions['blockElementList'], true);
|
||||
} else {
|
||||
$blockElementList = (array)$this->procOptions['blockElementList.'];
|
||||
}
|
||||
|
||||
$this->blockElementList = implode(',', $blockElementList);
|
||||
}
|
||||
|
||||
// Define which attributes are allowed on <p> tags
|
||||
if (isset($this->procOptions['allowAttributes.'])) {
|
||||
$this->allowedAttributesForParagraphTags = $this->procOptions['allowAttributes.'];
|
||||
}
|
||||
// Override tags which are allowed outside of <p> tags
|
||||
if (isset($this->procOptions['allowTagsOutside'])) {
|
||||
if (!isset($this->procOptions['allowTagsOutside.'])) {
|
||||
$this->allowedTagsOutsideOfParagraphs = GeneralUtility::trimExplode(',', strtolower($this->procOptions['allowTagsOutside']), true);
|
||||
} else {
|
||||
$this->allowedTagsOutsideOfParagraphs = (array)$this->procOptions['allowTagsOutside.'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main entry point for transforming RTE content in the database so the Rich Text Editor can deal with
|
||||
* e.g. links.
|
||||
*/
|
||||
public function transformTextForRichTextEditor(string $value, array $processingConfiguration): string
|
||||
{
|
||||
$initialValue = $value;
|
||||
$this->setProcessingConfiguration($processingConfiguration);
|
||||
$modes = $this->resolveAppliedTransformationModes('rte');
|
||||
|
||||
$beforeTransformTextForRichTextEditorEvent = new BeforeTransformTextForRichTextEditorEvent(
|
||||
$value,
|
||||
$initialValue,
|
||||
$processingConfiguration
|
||||
);
|
||||
$this->eventDispatcher->dispatch($beforeTransformTextForRichTextEditorEvent);
|
||||
$value = $beforeTransformTextForRichTextEditorEvent->getHtmlContent();
|
||||
|
||||
$value = $this->streamlineLineBreaksForProcessing($value);
|
||||
// If an entry HTML cleaner was configured, pass the content through the HTMLcleaner
|
||||
$value = $this->runHtmlParserIfConfigured($value, 'entryHTMLparser_rte');
|
||||
// Traverse modes
|
||||
foreach ($modes as $cmd) {
|
||||
switch ($cmd) {
|
||||
case 'detectbrokenlinks':
|
||||
$value = $this->markBrokenLinks($value);
|
||||
break;
|
||||
case 'css_transform':
|
||||
$value = $this->TS_transform_rte($value);
|
||||
break;
|
||||
default:
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
// If an exit HTML cleaner was configured, pass the content through the HTMLcleaner
|
||||
$value = $this->runHtmlParserIfConfigured($value, 'exitHTMLparser_rte');
|
||||
// Final clean up of linebreaks
|
||||
$value = $this->streamlineLineBreaksAfterProcessing($value);
|
||||
|
||||
$afterTransformTextForRichTextEditorEvent = new AfterTransformTextForRichTextEditorEvent(
|
||||
$value,
|
||||
$initialValue,
|
||||
$processingConfiguration
|
||||
);
|
||||
$this->eventDispatcher->dispatch($afterTransformTextForRichTextEditorEvent);
|
||||
return $afterTransformTextForRichTextEditorEvent->getHtmlContent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to process HTML content before it is stored in the database.
|
||||
*/
|
||||
public function transformTextForPersistence(string $value, array $processingConfiguration): string
|
||||
{
|
||||
$initialValue = $value;
|
||||
$this->setProcessingConfiguration($processingConfiguration);
|
||||
$modes = $this->resolveAppliedTransformationModes('db');
|
||||
|
||||
$beforeTransformTextForPersistenceEvent = new BeforeTransformTextForPersistenceEvent(
|
||||
$value,
|
||||
$initialValue,
|
||||
$processingConfiguration
|
||||
);
|
||||
$this->eventDispatcher->dispatch($beforeTransformTextForPersistenceEvent);
|
||||
$value = $beforeTransformTextForPersistenceEvent->getHtmlContent();
|
||||
|
||||
$value = $this->streamlineLineBreaksForProcessing($value);
|
||||
// If an entry HTML cleaner was configured, pass the content through the HTMLcleaner
|
||||
$value = $this->runHtmlParserIfConfigured($value, 'entryHTMLparser_db');
|
||||
// Traverse modes
|
||||
foreach ($modes as $cmd) {
|
||||
switch ($cmd) {
|
||||
case 'detectbrokenlinks':
|
||||
$value = $this->removeBrokenLinkMarkers($value);
|
||||
break;
|
||||
case 'ts_links':
|
||||
$value = $this->TS_links_db($value);
|
||||
break;
|
||||
case 'css_transform':
|
||||
// Transform empty paragraphs into spacing paragraphs
|
||||
$value = str_replace('<p></p>', '<p> </p>', $value);
|
||||
// Double any trailing spacing paragraph so that it does not get removed by divideIntoLines()
|
||||
$value = preg_replace('/<p> <\/p>$/', '<p> </p><p> </p>', $value) ?? $value;
|
||||
$value = $this->TS_transform_db($value);
|
||||
break;
|
||||
default:
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
// process markup with HTML Sanitizer
|
||||
$value = $this->htmlSanitize($value, $this->procOptions['HTMLparser_db.'] ?? []);
|
||||
// If an exit HTML cleaner was configured, pass the content through the HTMLcleaner
|
||||
$value = $this->runHtmlParserIfConfigured($value, 'exitHTMLparser_db');
|
||||
// Final clean up of linebreaks
|
||||
$value = $this->streamlineLineBreaksAfterProcessing($value);
|
||||
|
||||
$afterTransformTextForPersistenceEvent = new AfterTransformTextForPersistenceEvent(
|
||||
$value,
|
||||
$initialValue,
|
||||
$processingConfiguration
|
||||
);
|
||||
$this->eventDispatcher->dispatch($afterTransformTextForPersistenceEvent);
|
||||
return $afterTransformTextForPersistenceEvent->getHtmlContent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures what transformation modes should be executed, and that they are only executed once.
|
||||
*
|
||||
* @return array the resolved transformation modes
|
||||
*/
|
||||
protected function resolveAppliedTransformationModes(string $direction): array
|
||||
{
|
||||
// Setting modes / transformations to be called
|
||||
if ((string)($this->procOptions['overruleMode'] ?? '') !== '') {
|
||||
$modes = GeneralUtility::trimExplode(',', $this->procOptions['overruleMode']);
|
||||
} else {
|
||||
$modes = [$this->procOptions['mode']];
|
||||
}
|
||||
|
||||
$modeList = implode(',', $modes);
|
||||
|
||||
// Replace the shortcut "default" with all custom modes
|
||||
$modeList = str_replace('default', 'detectbrokenlinks,css_transform,ts_links', $modeList);
|
||||
|
||||
// Make list unique
|
||||
$modes = array_unique(GeneralUtility::trimExplode(',', $modeList, true));
|
||||
// Reverse order if direction is "rte"
|
||||
if ($direction === 'rte') {
|
||||
$modes = array_reverse($modes);
|
||||
}
|
||||
|
||||
return $modes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the HTML parser if it is configured
|
||||
* Getting additional HTML cleaner configuration. These are applied either before or after the main transformation
|
||||
* is done and thus totally independent processing options you can set up.
|
||||
*
|
||||
* This is only possible via TSconfig (procOptions) currently.
|
||||
*
|
||||
* @param string $configurationDirective used to look up in the procOptions if enabled, and then fetch the
|
||||
* @return string the processed content
|
||||
*/
|
||||
protected function runHtmlParserIfConfigured(string $content, string $configurationDirective): string
|
||||
{
|
||||
if (!empty($this->procOptions[$configurationDirective])) {
|
||||
[$keepTags, $keepNonMatchedTags, $hscMode, $additionalConfiguration] = $this->HTMLparserConfig($this->procOptions[$configurationDirective . '.']);
|
||||
$content = $this->HTMLcleaner($content, $keepTags, $keepNonMatchedTags, $hscMode, $additionalConfiguration);
|
||||
}
|
||||
return $content;
|
||||
}
|
||||
|
||||
/************************************
|
||||
*
|
||||
* Specific RTE TRANSFORMATION functions
|
||||
*
|
||||
*************************************/
|
||||
|
||||
/**
|
||||
* Transformation handler: 'ts_links' / direction: "db"
|
||||
* Processing anchor tags, and resolves them correctly again via the LinkService syntax
|
||||
*
|
||||
* Splits content into <a> tag blocks and processes each tag, and allows hooks to actually render
|
||||
* the result.
|
||||
*
|
||||
* @param string $value Content input
|
||||
* @return string Content output
|
||||
*/
|
||||
protected function TS_links_db(string $value): string
|
||||
{
|
||||
$blockSplit = $this->splitIntoBlock('A', $value);
|
||||
foreach ($blockSplit as $k => $v) {
|
||||
if ($k % 2) {
|
||||
[$tagAttributes] = $this->get_tag_attributes($this->getFirstTag($v), true);
|
||||
|
||||
// Anchors would not have a href attribute
|
||||
if (!isset($tagAttributes['href'])) {
|
||||
continue;
|
||||
}
|
||||
// Store the link as <a> tag as default by TYPO3, with the link service syntax
|
||||
try {
|
||||
$linkInformation = $this->linkService->resolve($tagAttributes['href']);
|
||||
$tagAttributes['href'] = $this->linkService->asString($linkInformation);
|
||||
} catch (UnknownLinkHandlerException $e) {
|
||||
$tagAttributes['href'] = $linkInformation['href'] ?? $tagAttributes['href'];
|
||||
}
|
||||
|
||||
$blockSplit[$k] = '<a ' . GeneralUtility::implodeAttributes($tagAttributes, true, true) . '>'
|
||||
. $this->TS_links_db($this->removeFirstAndLastTag($blockSplit[$k])) . '</a>';
|
||||
}
|
||||
}
|
||||
return implode('', $blockSplit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transformation handler: 'css_transform' / direction: "db"
|
||||
* Cleaning (->db) for standard content elements (ts)
|
||||
*
|
||||
* @param string $value Content input
|
||||
* @return string Content output
|
||||
* @see TS_transform_rte()
|
||||
*/
|
||||
protected function TS_transform_db(string $value): string
|
||||
{
|
||||
// Safety... so forever loops are avoided (they should not occur, but an error would potentially do this...)
|
||||
$this->TS_transform_db_safecounter--;
|
||||
if ($this->TS_transform_db_safecounter < 0) {
|
||||
return $value;
|
||||
}
|
||||
// Split the content from RTE by the occurrence of these blocks:
|
||||
$blockSplit = $this->splitIntoBlock($this->blockElementList, $value);
|
||||
|
||||
// Avoid superfluous linebreaks by transform_db after ending headListTag
|
||||
while (count($blockSplit) > 0 && trim(end($blockSplit)) === '') {
|
||||
array_pop($blockSplit);
|
||||
}
|
||||
|
||||
// Traverse the blocks
|
||||
foreach ($blockSplit as $k => $v) {
|
||||
if ($k % 2) {
|
||||
// Inside block:
|
||||
// Init:
|
||||
$tag = $this->getFirstTag($v);
|
||||
$tagName = strtolower($this->getFirstTagName($v));
|
||||
// Process based on the tag:
|
||||
switch ($tagName) {
|
||||
case 'blockquote':
|
||||
case 'dd':
|
||||
case 'div':
|
||||
case 'header':
|
||||
case 'section':
|
||||
case 'footer':
|
||||
case 'nav':
|
||||
case 'article':
|
||||
case 'aside':
|
||||
$blockSplit[$k] = $tag . $this->TS_transform_db($this->removeFirstAndLastTag($blockSplit[$k])) . '</' . $tagName . '>';
|
||||
break;
|
||||
case 'pre':
|
||||
break;
|
||||
default:
|
||||
// usually <hx> tags and <table> tags where no other block elements are within the tags
|
||||
// Eliminate true linebreaks inside block element tags
|
||||
$blockSplit[$k] = preg_replace('/[' . LF . ']+/', ' ', $blockSplit[$k]);
|
||||
}
|
||||
} else {
|
||||
// NON-block:
|
||||
if (trim($blockSplit[$k]) !== '') {
|
||||
$string = $blockSplit[$k];
|
||||
$string = preg_replace('#<([a-z]+)/>#', '<$1 />', $string);
|
||||
// Remove linebreaks preceding hr tags
|
||||
$string = preg_replace('/[' . LF . ']+<(hr)(\\s[^>\\/]*)?[[:space:]]*\\/?>/', '<$1$2/>', $string) ?? '';
|
||||
// Remove linebreaks following hr tags
|
||||
$string = preg_replace('/<(hr)(\\s[^>\\/]*)?[[:space:]]*\\/?>[' . LF . ']+/', '<$1$2/>', $string) ?? '';
|
||||
// Replace other linebreaks with space
|
||||
$string = preg_replace('/[' . LF . ']+/', ' ', $string);
|
||||
// process allowed/removed tags
|
||||
$string = $this->HTMLcleaner(
|
||||
(string)$string,
|
||||
$this->getKeepTags('db'),
|
||||
$this->procOptions['HTMLparser_db.']['keepNonMatchedTags'] ?? '',
|
||||
(int)($this->procOptions['HTMLparser_db.']['htmlSpecialChars'] ?? 0)
|
||||
);
|
||||
$blockSplit[$k] = (string)$this->divideIntoLines($string);
|
||||
} else {
|
||||
unset($blockSplit[$k]);
|
||||
}
|
||||
}
|
||||
}
|
||||
$this->TS_transform_db_safecounter++;
|
||||
return implode(LF, $blockSplit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transformation handler: css_transform / direction: "rte"
|
||||
* Set (->rte) for standard content elements (ts)
|
||||
*
|
||||
* @param string $value Content input
|
||||
* @return string Content output
|
||||
* @see TS_transform_db()
|
||||
*/
|
||||
protected function TS_transform_rte(string $value): string
|
||||
{
|
||||
// Split the content from database by the occurrence of the block elements
|
||||
$blockSplit = $this->splitIntoBlock($this->blockElementList, $value);
|
||||
// Traverse the blocks
|
||||
foreach ($blockSplit as $k => $v) {
|
||||
if ($k % 2) {
|
||||
// Inside one of the blocks:
|
||||
// Init:
|
||||
$tag = $this->getFirstTag($v);
|
||||
$tagName = strtolower($this->getFirstTagName($v));
|
||||
// Based on tagname, we do transformations:
|
||||
switch ($tagName) {
|
||||
case 'blockquote':
|
||||
case 'dd':
|
||||
case 'div':
|
||||
case 'header':
|
||||
case 'section':
|
||||
case 'footer':
|
||||
case 'nav':
|
||||
case 'article':
|
||||
case 'aside':
|
||||
$blockSplit[$k] = $tag . $this->TS_transform_rte($this->removeFirstAndLastTag($blockSplit[$k])) . '</' . $tagName . '>';
|
||||
break;
|
||||
}
|
||||
if (!isset($blockSplit[$k + 1])) {
|
||||
$blockSplit[$k + 1] = '';
|
||||
}
|
||||
$blockSplit[$k + 1] = preg_replace('/^[ ]*' . LF . '/', '', $blockSplit[$k + 1]);
|
||||
} else {
|
||||
// NON-block:
|
||||
$nextFTN = $this->getFirstTagName($blockSplit[$k + 1] ?? '');
|
||||
$onlyLineBreaks = (preg_match('/^[ ]*' . LF . '+[ ]*$/', $blockSplit[$k]) == 1);
|
||||
// If the line is followed by a block or is the last line:
|
||||
if (GeneralUtility::inList($this->blockElementList, $nextFTN) || !isset($blockSplit[$k + 1])) {
|
||||
// If the line contains more than just linebreaks, reduce the number of trailing linebreaks by 1
|
||||
if (!$onlyLineBreaks) {
|
||||
$blockSplit[$k] = preg_replace('/(' . LF . '*)' . LF . '[ ]*$/', '$1', $blockSplit[$k]);
|
||||
} else {
|
||||
// If the line contains only linebreaks, remove the leading linebreak
|
||||
$blockSplit[$k] = preg_replace('/^[ ]*' . LF . '/', '', $blockSplit[$k]);
|
||||
}
|
||||
}
|
||||
// If $blockSplit[$k] is blank then unset the line, unless the line only contained linebreaks
|
||||
if ((string)$blockSplit[$k] === '' && !$onlyLineBreaks) {
|
||||
unset($blockSplit[$k]);
|
||||
} else {
|
||||
$blockSplit[$k] = $this->setDivTags($blockSplit[$k]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return implode(LF, $blockSplit);
|
||||
}
|
||||
|
||||
/***************************************************************
|
||||
*
|
||||
* Generic RTE transformation, analysis and helper functions
|
||||
*
|
||||
**************************************************************/
|
||||
|
||||
/**
|
||||
* Function for cleaning content going into the database.
|
||||
* Content is cleaned eg. by removing unallowed HTML and ds-HSC content
|
||||
* It is basically calling HTMLcleaner from the parent class with some preset configuration specifically set up for cleaning content going from the RTE into the db
|
||||
*
|
||||
* @param string $content Content to clean up
|
||||
* @return string Clean content
|
||||
* @see getKeepTags()
|
||||
*/
|
||||
protected function HTMLcleaner_db(string $content): string
|
||||
{
|
||||
$keepTags = $this->getKeepTags('db');
|
||||
return $this->HTMLcleaner($content, $keepTags, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an array of configuration for the HTMLcleaner function based on whether content
|
||||
* go TO or FROM the Rich Text Editor ($direction)
|
||||
*
|
||||
* @param string $direction The direction of the content being processed by the output configuration; "db" (content going into the database FROM the rte) or "rte" (content going into the form)
|
||||
* @return array Configuration array
|
||||
* @see HTMLcleaner_db()
|
||||
*/
|
||||
protected function getKeepTags(string $direction): array
|
||||
{
|
||||
if (!isset($this->getKeepTags_cache[$direction]) || !is_array($this->getKeepTags_cache[$direction])) {
|
||||
// Setting up allowed tags:
|
||||
// Default is to get allowed/denied tags from internal array of processing options:
|
||||
// Construct default list of tags to keep:
|
||||
if (isset($this->procOptions['allowTags.']) && is_array($this->procOptions['allowTags.'])) {
|
||||
$keepTags = implode(',', $this->procOptions['allowTags.']);
|
||||
} else {
|
||||
$keepTags = $this->procOptions['allowTags'] ?? '';
|
||||
}
|
||||
$keepTags = array_flip(GeneralUtility::trimExplode(',', $this->defaultAllowedTagsList . ',' . strtolower($keepTags), true));
|
||||
// For tags to deny, remove them from $keepTags array:
|
||||
if (!isset($this->procOptions['denyTags.'])) {
|
||||
$denyTags = GeneralUtility::trimExplode(',', $this->procOptions['denyTags'] ?? '', true);
|
||||
} else {
|
||||
$denyTags = $this->procOptions['denyTags.'];
|
||||
}
|
||||
foreach ($denyTags as $dKe) {
|
||||
unset($keepTags[$dKe]);
|
||||
}
|
||||
// Based on the direction of content, set further options:
|
||||
switch ($direction) {
|
||||
case 'rte':
|
||||
// Transforming keepTags array so it can be understood by the HTMLcleaner function.
|
||||
// This basically converts the format of the array from TypoScript (having dots) to plain multi-dimensional array.
|
||||
[$keepTags] = $this->HTMLparserConfig($this->procOptions['HTMLparser_rte.'] ?? [], $keepTags);
|
||||
break;
|
||||
case 'db':
|
||||
// Setting up span tags if they are allowed:
|
||||
if (isset($keepTags['span'])) {
|
||||
$keepTags['span'] = [
|
||||
'allowedAttribs' => 'id,class,style,title,lang,xml:lang,dir,itemscope,itemtype,itemprop',
|
||||
'fixAttrib' => [
|
||||
'class' => [
|
||||
'removeIfFalse' => 1,
|
||||
],
|
||||
],
|
||||
'rmTagIfNoAttrib' => 1,
|
||||
];
|
||||
if (!empty($this->allowedClasses)) {
|
||||
$keepTags['span']['fixAttrib']['class']['list'] = $this->allowedClasses;
|
||||
}
|
||||
}
|
||||
// Setting further options, getting them from the processing options
|
||||
$TSc = $this->procOptions['HTMLparser_db.'] ?? [];
|
||||
if (empty($TSc['globalNesting'])) {
|
||||
$TSc['globalNesting'] = 'b,i,u,a,center,font,sub,sup,strong,em,strike,span';
|
||||
}
|
||||
if (empty($TSc['noAttrib'])) {
|
||||
$TSc['noAttrib'] = 'b,i,u,br,center,hr,sub,sup,strong,em,li,ul,ol,blockquote,strike';
|
||||
}
|
||||
// Transforming the array from TypoScript to regular array:
|
||||
[$keepTags] = $this->HTMLparserConfig($TSc, $keepTags);
|
||||
break;
|
||||
}
|
||||
// Caching (internally, in object memory) the result
|
||||
$this->getKeepTags_cache[$direction] = $keepTags;
|
||||
}
|
||||
// Return result:
|
||||
return $this->getKeepTags_cache[$direction];
|
||||
}
|
||||
|
||||
/**
|
||||
* This resolves the $value into parts based on <p>-sections. These are returned as lines separated by LF.
|
||||
* This point is to resolve the HTML-code returned from RTE into ordinary lines so it's 'human-readable'
|
||||
* The function ->setDivTags does the opposite.
|
||||
* This function processes content to go into the database.
|
||||
*
|
||||
* @param string $value Value to process.
|
||||
* @param int $count Recursion brake. Decremented on each recursion down to zero. Default is 5 (which equals the allowed nesting levels of p tags).
|
||||
* @param bool $returnArray If TRUE, an array with the lines is returned, otherwise a string of the processed input value.
|
||||
* @return string|array Processed input value.
|
||||
* @see setDivTags()
|
||||
*/
|
||||
protected function divideIntoLines(string $value, int $count = 5, bool $returnArray = false)
|
||||
{
|
||||
// Setting the third param will eliminate false end-tags. Maybe this is a good thing to do...?
|
||||
$paragraphBlocks = $this->splitIntoBlock('p', $value, true);
|
||||
// Returns plainly the content if there was no p sections in it
|
||||
if (count($paragraphBlocks) <= 1 || $count <= 0) {
|
||||
return $this->sanitizeLineBreaksForContentOnly($value);
|
||||
}
|
||||
|
||||
// Traverse the splitted sections
|
||||
foreach ($paragraphBlocks as $k => $v) {
|
||||
if ($k % 2) {
|
||||
// Inside a <p> section
|
||||
$v = $this->removeFirstAndLastTag($v);
|
||||
// Fetching 'sub-lines' - which will explode any further p nesting recursively
|
||||
$subLines = $this->divideIntoLines($v, $count - 1, true);
|
||||
// So, if there happened to be sub-nesting of p, this is written directly as the new content of THIS section. (This would be considered 'an error')
|
||||
if (is_array($subLines)) {
|
||||
$paragraphBlocks[$k] = implode(LF, $subLines);
|
||||
} else {
|
||||
//... but if NO subsection was found, we process it as a TRUE line without erroneous content:
|
||||
$paragraphBlocks[$k] = $this->processContentWithinParagraph($subLines, $paragraphBlocks[$k]);
|
||||
}
|
||||
// If it turns out the line is just blank (containing a possibly) then just make it pure blank.
|
||||
// But, prevent filtering of lines that are blank in sense above, but whose tags contain attributes.
|
||||
// Those attributes should have been filtered before; if they are still there they must be considered as possible content.
|
||||
if (trim(strip_tags($paragraphBlocks[$k])) === ' ' && !preg_match('/\\<(img)(\\s[^>]*)?\\/?>/si', $paragraphBlocks[$k]) && !preg_match('/\\<([^>]*)?( align| class| style| id| title| dir| lang| xml:lang)([^>]*)?>/si', trim($paragraphBlocks[$k]))) {
|
||||
$paragraphBlocks[$k] = '';
|
||||
}
|
||||
} else {
|
||||
// Outside a paragraph, if there is still something in there, just add a <p> tag
|
||||
// Remove positions which are outside <p> tags and without content
|
||||
$paragraphBlocks[$k] = trim(strip_tags($paragraphBlocks[$k], '<' . implode('><', $this->allowedTagsOutsideOfParagraphs) . '>'));
|
||||
$paragraphBlocks[$k] = $this->sanitizeLineBreaksForContentOnly($paragraphBlocks[$k]);
|
||||
if ((string)$paragraphBlocks[$k] === '') {
|
||||
unset($paragraphBlocks[$k]);
|
||||
} else {
|
||||
// add <p> tags around the content
|
||||
$paragraphBlocks[$k] = str_replace(strip_tags($paragraphBlocks[$k]), '<p>' . strip_tags($paragraphBlocks[$k]) . '</p>', $paragraphBlocks[$k]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return $returnArray ? $paragraphBlocks : implode(LF, $paragraphBlocks);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts all lines into <p></p>-sections (unless the line has a p - tag already)
|
||||
* For processing of content going FROM database TO RTE.
|
||||
*
|
||||
* @param string $value Value to convert
|
||||
* @return string Processed value.
|
||||
* @see divideIntoLines()
|
||||
*/
|
||||
protected function setDivTags(string $value): string
|
||||
{
|
||||
// First, setting configuration for the HTMLcleaner function. This will process each line between the <div>/<p> section on their way to the RTE
|
||||
$keepTags = $this->getKeepTags('rte');
|
||||
// Divide the content into lines
|
||||
$parts = explode(LF, $value);
|
||||
foreach ($parts as $k => $v) {
|
||||
// Processing of line content:
|
||||
// If the line is blank, set it to
|
||||
if (trim($parts[$k]) === '') {
|
||||
$parts[$k] = ' ';
|
||||
} else {
|
||||
// Clean the line content, keeping unknown tags (as they can be removed in the entryHTMLparser)
|
||||
$parts[$k] = $this->HTMLcleaner($parts[$k], $keepTags, 'protect');
|
||||
// convert double-encoded into regular however this could also be reversed via the exitHTMLparser
|
||||
// This was previously an option to disable called "dontConvAmpInNBSP_rte"
|
||||
$parts[$k] = str_replace('&nbsp;', ' ', $parts[$k]);
|
||||
}
|
||||
$partFirstTagName = strtolower($this->getFirstTagName($parts[$k]));
|
||||
// Wrapping the line in <p> tags if not already wrapped and does not contain an hr tag and is not allowed outside of paragraphs.
|
||||
if (!in_array($partFirstTagName, $this->allowedTagsOutsideOfParagraphs, true) && !preg_match('/<(hr)(\\s[^>\\/]*)?[[:space:]]*\\/?>/i', $partFirstTagName)) {
|
||||
$testStr = strtolower(trim($parts[$k]));
|
||||
if (!str_starts_with($testStr, '<div') || !str_ends_with($testStr, '</div>')) {
|
||||
if (!str_starts_with($testStr, '<p') || !str_ends_with($testStr, '</p>')) {
|
||||
// Only set p-tags if there is not already div or p tags:
|
||||
$parts[$k] = '<p>' . $parts[$k] . '</p>';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Implode result:
|
||||
return implode(LF, $parts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Used for transformation from RTE to DB
|
||||
*
|
||||
* Works on a single line within a <p> tag when storing into the database
|
||||
* This always adds <p> tags and validates the arguments,
|
||||
* additionally the content is cleaned up via the HTMLcleaner.
|
||||
*
|
||||
* @param string $content the content within the <p> tag
|
||||
* @param string $fullContentWithTag the whole <p> tag surrounded as well
|
||||
*
|
||||
* @return string the full <p> tag with cleaned content
|
||||
*/
|
||||
protected function processContentWithinParagraph(string $content, string $fullContentWithTag): string
|
||||
{
|
||||
// clean up the content
|
||||
$content = $this->HTMLcleaner_db($content);
|
||||
// Get the <p> tag, and validate the attributes
|
||||
$fTag = $this->getFirstTag($fullContentWithTag);
|
||||
// Check which attributes of the <p> tag to keep attributes
|
||||
if (!empty($this->allowedAttributesForParagraphTags)) {
|
||||
[$tagAttributes] = $this->get_tag_attributes($fTag);
|
||||
// Make sure the tag attributes only contain the ones that are defined to be allowed
|
||||
$tagAttributes = array_intersect_key($tagAttributes, array_flip($this->allowedAttributesForParagraphTags));
|
||||
|
||||
// Only allow classes that are whitelisted in $this->allowedClasses
|
||||
if (isset($tagAttributes['class']) && trim($tagAttributes['class']) !== '' && !empty($this->allowedClasses) && !in_array($tagAttributes['class'], $this->allowedClasses, true)) {
|
||||
$classes = GeneralUtility::trimExplode(' ', $tagAttributes['class'], true);
|
||||
$classes = array_intersect($classes, $this->allowedClasses);
|
||||
if (!empty($classes)) {
|
||||
$tagAttributes['class'] = implode(' ', $classes);
|
||||
} else {
|
||||
unset($tagAttributes['class']);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$tagAttributes = [];
|
||||
}
|
||||
// Remove any line break
|
||||
$content = str_replace(LF, '', $content);
|
||||
// Compile the surrounding <p> tag
|
||||
$content = '<' . rtrim('p ' . $this->compileTagAttribs($tagAttributes)) . '>' . $content . '</p>';
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap <hr> tags with LFs, and also remove double LFs, used when transforming from RTE to DB
|
||||
*
|
||||
* @return string the modified content
|
||||
*/
|
||||
protected function sanitizeLineBreaksForContentOnly(string $content): string
|
||||
{
|
||||
$content = preg_replace('/<(hr)(\\s[^>\\/]*)?[[:space:]]*\\/?>/i', LF . '<$1$2/>' . LF, $content) ?? $content;
|
||||
$content = str_replace(LF . LF, LF, $content);
|
||||
$content = preg_replace('/(^' . LF . ')|(' . LF . '$)/i', '', $content) ?? $content;
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called before any processing / transformation is made
|
||||
* Removing any CRs (char 13) and only deal with LFs (char 10) internally.
|
||||
* CR has a very disturbing effect, so just remove all CR and rely on LF
|
||||
*
|
||||
* Historical note: Previously it was possible to disable this functionality via disableUnifyLineBreaks.
|
||||
*
|
||||
* @param string $content the content to process
|
||||
* @return string the modified content
|
||||
*/
|
||||
protected function streamlineLineBreaksForProcessing(string $content): string
|
||||
{
|
||||
return str_replace(CR, '', $content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after any processing / transformation was made
|
||||
* just before the content is returned by the RTE parser all line breaks
|
||||
* get unified to be "CRLF"s again.
|
||||
*
|
||||
* Historical note: Previously it was possible to disable this functionality via disableUnifyLineBreaks.
|
||||
*
|
||||
* @param string $content the content to process
|
||||
* @return string the modified content
|
||||
*/
|
||||
protected function streamlineLineBreaksAfterProcessing(string $content): string
|
||||
{
|
||||
// Make sure no \r\n sequences has entered in the meantime
|
||||
$content = $this->streamlineLineBreaksForProcessing($content);
|
||||
// ... and then change all \n into \r\n
|
||||
return str_replace(LF, CRLF, $content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Content Transformation from DB to RTE
|
||||
* Checks all <a> tags which reference a t3://page and checks if the page is available
|
||||
* If not, some offensive styling is added.
|
||||
*
|
||||
* @return string the modified content
|
||||
*/
|
||||
protected function markBrokenLinks(string $content): string
|
||||
{
|
||||
$blocks = $this->splitIntoBlock('A', $content);
|
||||
foreach ($blocks as $position => $value) {
|
||||
if ($position % 2 === 0) {
|
||||
continue;
|
||||
}
|
||||
[$attributes] = $this->get_tag_attributes($this->getFirstTag($value), true);
|
||||
if (empty($attributes['href'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$hrefInformation = $this->linkService->resolve($attributes['href']);
|
||||
|
||||
$brokenLinkAnalysis = new BrokenLinkAnalysisEvent($hrefInformation['type'], $hrefInformation);
|
||||
$this->eventDispatcher->dispatch($brokenLinkAnalysis);
|
||||
if ($brokenLinkAnalysis->isBrokenLink()) {
|
||||
$attributes['data-rte-error'] = $brokenLinkAnalysis->getReason();
|
||||
}
|
||||
} catch (InsufficientFolderAccessPermissionsException $e) {
|
||||
// do nothing if user doesn't have access to the file/folder
|
||||
} catch (UnknownLinkHandlerException $e) {
|
||||
$attributes['data-rte-error'] = $e->getMessage();
|
||||
}
|
||||
|
||||
// Always rewrite the block to allow the nested calling even if a page is found
|
||||
$blocks[$position]
|
||||
= '<a ' . GeneralUtility::implodeAttributes($attributes, true, true) . '>'
|
||||
. $this->markBrokenLinks($this->removeFirstAndLastTag($blocks[$position]))
|
||||
. '</a>';
|
||||
}
|
||||
return implode('', $blocks);
|
||||
}
|
||||
|
||||
/**
|
||||
* Content Transformation from RTE to DB
|
||||
* Removes link information error attributes from <a> tags that are added to broken links
|
||||
*
|
||||
* @param string $content the content to process
|
||||
* @return string the modified content
|
||||
*/
|
||||
protected function removeBrokenLinkMarkers(string $content): string
|
||||
{
|
||||
$blocks = $this->splitIntoBlock('A', $content);
|
||||
foreach ($blocks as $position => $value) {
|
||||
if ($position % 2 === 0) {
|
||||
continue;
|
||||
}
|
||||
[$attributes] = $this->get_tag_attributes($this->getFirstTag($value), true);
|
||||
if (empty($attributes['href'])) {
|
||||
continue;
|
||||
}
|
||||
// Always remove the styling again (regardless of the page was found or not)
|
||||
// so the database does not contain ugly stuff
|
||||
unset($attributes['data-rte-error']);
|
||||
if (isset($attributes['style'])) {
|
||||
$attributes['style'] = trim(str_replace('background-color: yellow; border:2px red solid; color: black;', '', $attributes['style']));
|
||||
if (empty($attributes['style'])) {
|
||||
unset($attributes['style']);
|
||||
}
|
||||
}
|
||||
$blocks[$position]
|
||||
= '<a ' . GeneralUtility::implodeAttributes($attributes, true, true) . '>'
|
||||
. $this->removeBrokenLinkMarkers($this->removeFirstAndLastTag($blocks[$position]))
|
||||
. '</a>';
|
||||
}
|
||||
return implode('', $blocks);
|
||||
}
|
||||
|
||||
protected function htmlSanitize(string $content, array $configuration): string
|
||||
{
|
||||
$features = GeneralUtility::makeInstance(Features::class);
|
||||
// either `htmlSanitize = null` or `htmlSanitize = false`
|
||||
// or feature flag `security.backend.htmlSanitizeRte` is disabled
|
||||
if (array_key_exists('htmlSanitize', $configuration) && empty($configuration['htmlSanitize'])
|
||||
|| !$features->isFeatureEnabled('security.backend.htmlSanitizeRte')
|
||||
) {
|
||||
return $content;
|
||||
}
|
||||
|
||||
$build = $configuration['htmlSanitize.']['build'] ?? 'default';
|
||||
if (class_exists($build) && is_a($build, BuilderInterface::class, true)) {
|
||||
$builder = GeneralUtility::makeInstance($build);
|
||||
} else {
|
||||
$factory = GeneralUtility::makeInstance(SanitizerBuilderFactory::class);
|
||||
$builder = $factory->build($build);
|
||||
}
|
||||
$sanitizer = $builder->build();
|
||||
$initiator = GeneralUtility::makeInstance(SanitizerInitiator::class, static::class);
|
||||
return $sanitizer->sanitize($content, $initiator);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?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\Html;
|
||||
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\HtmlSanitizer\Builder\BuilderInterface;
|
||||
|
||||
/**
|
||||
* Factory for creating a (sanitizer) builder instance. Corresponding presets can
|
||||
* be declared in `$GLOBALS['TYPO3_CONF_VARS']['SYS']['htmlSanitizer']` like e.g.
|
||||
*
|
||||
* ```
|
||||
* $GLOBALS['TYPO3_CONF_VARS']['SYS']['htmlSanitizer'] = [
|
||||
* 'default' => \TYPO3\CMS\Core\Html\DefaultSanitizerBuilder::class,
|
||||
* 'custom' => \Vendor\Package\CustomBuilder::class,
|
||||
* ];
|
||||
* ```
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final readonly class SanitizerBuilderFactory
|
||||
{
|
||||
public function build(string $identifier): BuilderInterface
|
||||
{
|
||||
if (empty($GLOBALS['TYPO3_CONF_VARS']['SYS']['htmlSanitizer'][$identifier])) {
|
||||
throw new \LogicException(sprintf('Undefined `htmlSanitizer` identifier `%s`', $identifier), 1624876139);
|
||||
}
|
||||
$builder = GeneralUtility::makeInstance($GLOBALS['TYPO3_CONF_VARS']['SYS']['htmlSanitizer'][$identifier]);
|
||||
if (!$builder instanceof BuilderInterface) {
|
||||
throw new \LogicException(
|
||||
sprintf('Builder `%s` must implement interface `%s`', get_class($builder), BuilderInterface::class),
|
||||
1624876266
|
||||
);
|
||||
}
|
||||
return $builder;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?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\Html;
|
||||
|
||||
use TYPO3\HtmlSanitizer\InitiatorInterface;
|
||||
|
||||
/**
|
||||
* Initiator for HTML sanitization process, forwarded to sanitizer and used during logging.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class SanitizerInitiator implements InitiatorInterface
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $trace;
|
||||
|
||||
public function __construct(string $trace)
|
||||
{
|
||||
$this->trace = $trace;
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->trace;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?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\Html;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class SimpleNode
|
||||
{
|
||||
// similar to https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType
|
||||
public const TYPE_ELEMENT = 1;
|
||||
public const TYPE_TEXT = 3;
|
||||
public const TYPE_CDATA = 4;
|
||||
public const TYPE_COMMENT = 8;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $type;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $index;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $string;
|
||||
|
||||
public static function fromString(int $type, int $index, string $string): self
|
||||
{
|
||||
return new self($type, $index, $string);
|
||||
}
|
||||
|
||||
public function __construct(int $type, int $index, string $string)
|
||||
{
|
||||
$this->type = $type;
|
||||
$this->index = $index;
|
||||
$this->string = $string;
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->string;
|
||||
}
|
||||
|
||||
public function getType(): int
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
public function getIndex(): int
|
||||
{
|
||||
return $this->index;
|
||||
}
|
||||
|
||||
public function getElementName(): ?string
|
||||
{
|
||||
if ($this->getType() !== self::TYPE_ELEMENT) {
|
||||
return null;
|
||||
}
|
||||
if (!preg_match('#^<(?P<name>[a-z][a-z0-9-]*)\b#i', $this->string, $matches)) {
|
||||
return null;
|
||||
}
|
||||
return $matches['name'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
<?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\Html;
|
||||
|
||||
/**
|
||||
* Simple HTML node parser. The main focus is to determine "runaway nodes"
|
||||
* like `<span attribute="<runaway attribute="other">` and better nod boundaries.
|
||||
*
|
||||
* (Most of) the behavior is similar to Mozilla's behavior on handling those nodes.
|
||||
* (e.g. `div.innerHTML = 'x =<y>= z';` - but without creating closing node blocks)
|
||||
*
|
||||
* This parser does not resolve nested nodes - it just provides a flat node sequence.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class SimpleParser
|
||||
{
|
||||
protected ?string $attribute = null;
|
||||
|
||||
/**
|
||||
* @var SimpleNode[]
|
||||
*/
|
||||
protected array $nodes = [];
|
||||
protected int $currentType = SimpleNode::TYPE_TEXT;
|
||||
protected string $currentData = '';
|
||||
|
||||
public static function fromString(string $string): self
|
||||
{
|
||||
return new self($string);
|
||||
}
|
||||
|
||||
public function __construct(string $string)
|
||||
{
|
||||
$this->process($string);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int ...$types using `Node::TYPE_*`
|
||||
* @return SimpleNode[]
|
||||
*/
|
||||
public function getNodes(int ...$types): array
|
||||
{
|
||||
if (empty($types)) {
|
||||
return $this->nodes;
|
||||
}
|
||||
$nodes = array_filter(
|
||||
$this->nodes,
|
||||
static function (SimpleNode $node) use ($types): bool {
|
||||
return in_array(
|
||||
$node->getType(),
|
||||
$types,
|
||||
true
|
||||
);
|
||||
}
|
||||
);
|
||||
// reindex nodes
|
||||
return array_values($nodes);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int|null $type using `Node::TYPE_*`
|
||||
*/
|
||||
public function getFirstNode(?int $type = null): ?SimpleNode
|
||||
{
|
||||
foreach ($this->nodes as $node) {
|
||||
if ($type === null || $type === $node->getType()) {
|
||||
return $node;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int|null $type using `Node::TYPE_*`
|
||||
*/
|
||||
public function getLastNode(?int $type = null): ?SimpleNode
|
||||
{
|
||||
foreach (array_reverse($this->nodes) as $node) {
|
||||
if ($type === null || $type === $node->getType()) {
|
||||
return $node;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes token sequence and creates corresponding `Node` instances.
|
||||
*/
|
||||
protected function process(string $string): void
|
||||
{
|
||||
$skip = 0;
|
||||
$characters = str_split($string);
|
||||
foreach ($characters as $i => $character) {
|
||||
// skip tokens that already haven been processed
|
||||
if ($skip > 0 && $skip-- > 0) {
|
||||
continue;
|
||||
}
|
||||
// CDATA start
|
||||
if ($character === '<'
|
||||
&& $this->isType(SimpleNode::TYPE_TEXT) && substr($string, $i, 9) === '<![CDATA['
|
||||
) {
|
||||
$this->next(SimpleNode::TYPE_CDATA);
|
||||
$this->append('<![CDATA[');
|
||||
$skip = 8;
|
||||
// comment start
|
||||
} elseif ($character === '<'
|
||||
&& $this->isType(SimpleNode::TYPE_TEXT) && substr($string, $i, 4) === '<!--'
|
||||
) {
|
||||
$this->next(SimpleNode::TYPE_COMMENT);
|
||||
$this->append('<!--');
|
||||
$skip = 3;
|
||||
// element start
|
||||
} elseif ($character === '<'
|
||||
&& $this->isType(SimpleNode::TYPE_TEXT)
|
||||
&& preg_match('#^</?[a-z]#i', substr($string, $i, 3))
|
||||
) {
|
||||
$this->next(SimpleNode::TYPE_ELEMENT);
|
||||
$this->append($character);
|
||||
// CDATA end
|
||||
} elseif ($character === ']'
|
||||
&& $this->isType(SimpleNode::TYPE_CDATA) && substr($string, $i, 3) === ']]>'
|
||||
) {
|
||||
$this->append(']]>');
|
||||
$this->next(SimpleNode::TYPE_TEXT);
|
||||
$skip = 2;
|
||||
// comment end
|
||||
} elseif ($character === '-'
|
||||
&& $this->isType(SimpleNode::TYPE_COMMENT) && substr($string, $i, 3) === '-->'
|
||||
) {
|
||||
$this->append('-->');
|
||||
$this->next(SimpleNode::TYPE_TEXT);
|
||||
$skip = 2;
|
||||
// element end
|
||||
} elseif ($character === '>'
|
||||
&& $this->isType(SimpleNode::TYPE_ELEMENT) && !$this->inAttribute()
|
||||
) {
|
||||
$this->append($character);
|
||||
$this->next(SimpleNode::TYPE_TEXT);
|
||||
// element attribute start
|
||||
} elseif (($character === '"' || $character === "'")
|
||||
&& $this->isType(SimpleNode::TYPE_ELEMENT) && !$this->inAttribute()
|
||||
) {
|
||||
$this->attribute = $character;
|
||||
$this->append($character);
|
||||
// element attribute end
|
||||
} elseif (($character === '"' || $character === "'")
|
||||
&& $this->isType(SimpleNode::TYPE_ELEMENT) && $this->attribute === $character
|
||||
) {
|
||||
$this->append($character);
|
||||
$this->attribute = null;
|
||||
// anything else (put to current type)
|
||||
} else {
|
||||
$this->append($character);
|
||||
}
|
||||
}
|
||||
$this->finish();
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers creating "next" node instance, resets current state.
|
||||
*/
|
||||
protected function next(int $nextType): void
|
||||
{
|
||||
if ($this->currentData !== '') {
|
||||
$this->nodes[] = SimpleNode::fromString(
|
||||
$this->currentType,
|
||||
count($this->nodes),
|
||||
$this->currentData
|
||||
);
|
||||
}
|
||||
$this->currentType = $nextType;
|
||||
$this->currentData = '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Finishes missing text node instance - anything else (all of those are
|
||||
* tag-like "runaway" scenarios e.g. `<anything<!-- anything...` without being
|
||||
* closed correctly - those nodes are ignored on purpose!
|
||||
*/
|
||||
protected function finish(): void
|
||||
{
|
||||
if ($this->currentData === '') {
|
||||
return;
|
||||
}
|
||||
if ($this->isType(SimpleNode::TYPE_TEXT)) {
|
||||
$this->nodes[] = SimpleNode::fromString(
|
||||
$this->currentType,
|
||||
count($this->nodes),
|
||||
$this->currentData
|
||||
);
|
||||
}
|
||||
// either unfinished element or comment
|
||||
// (ignored on purpose)
|
||||
}
|
||||
|
||||
protected function append(string $string): void
|
||||
{
|
||||
$this->currentData .= $string;
|
||||
}
|
||||
|
||||
protected function isType(int $type): bool
|
||||
{
|
||||
return $this->currentType === $type;
|
||||
}
|
||||
|
||||
protected function inAttribute(): bool
|
||||
{
|
||||
return $this->attribute !== null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?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\Html\Srcset;
|
||||
|
||||
/**
|
||||
* Represents a source candidate for the HTML srcset attribute
|
||||
* using the "x" unit (relative pixel density, like "1x" or "2x")
|
||||
*/
|
||||
final class DensitySrcsetCandidate extends SrcsetCandidate
|
||||
{
|
||||
public function __construct(
|
||||
protected float $density,
|
||||
protected ?int $referenceWidth = null
|
||||
) {}
|
||||
|
||||
public function getDensity(): float
|
||||
{
|
||||
return $this->density;
|
||||
}
|
||||
|
||||
public function setDensity(float $density): static
|
||||
{
|
||||
$this->density = $density;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getReferenceWidth(): ?int
|
||||
{
|
||||
return $this->referenceWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
* The absolute $referenceWidth will be used as "1x" width.
|
||||
*/
|
||||
public function setReferenceWidth(int $referenceWidth): static
|
||||
{
|
||||
$this->referenceWidth = $referenceWidth;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDescriptor(): string
|
||||
{
|
||||
return $this->density . static::DENSITY_UNIT;
|
||||
}
|
||||
|
||||
public function getCalculatedWidth(): int
|
||||
{
|
||||
if ($this->referenceWidth === null) {
|
||||
throw new \InvalidArgumentException(sprintf(
|
||||
'Reference width needs to be specified if pixel density descriptors (e. g. 2x) are used in srcset: %s',
|
||||
$this->getDescriptor()
|
||||
), 1697743145);
|
||||
}
|
||||
return (int)($this->density * $this->referenceWidth);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
<?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\Html\Srcset;
|
||||
|
||||
/**
|
||||
* Generates a HTML srcset attribute for responsive images
|
||||
*/
|
||||
final class SrcsetAttribute
|
||||
{
|
||||
/** @var SrcsetCandidate[] */
|
||||
private array $candidates = [];
|
||||
private string $candidateType;
|
||||
|
||||
/**
|
||||
* @param string[] $descriptors Array of srcset width (like "300w, 500w") or density (like "1x, 2x")
|
||||
* descriptors.
|
||||
* @param int|null $referenceWidth Needs to be provided if $descriptors contains density descriptors
|
||||
* (like "2x") to be able to resolve the relative image dimensions.
|
||||
* The absolute $referenceWidth will be used as "1x" width.
|
||||
*/
|
||||
public static function createFromDescriptors(array $descriptors, ?int $referenceWidth = null): SrcsetAttribute
|
||||
{
|
||||
$generator = new SrcsetAttribute();
|
||||
foreach ($descriptors as $descriptor) {
|
||||
$generator->addCandidate(SrcsetCandidate::createFromDescriptor((string)$descriptor, $referenceWidth));
|
||||
}
|
||||
return $generator;
|
||||
}
|
||||
|
||||
public function addCandidate(SrcsetCandidate $candidate): static
|
||||
{
|
||||
if (!$this->isValidCandidate($candidate)) {
|
||||
throw new \InvalidArgumentException(sprintf(
|
||||
'Invalid mix of w and x descriptors in srcset: %s, ..., %s',
|
||||
$this->candidates[0]->generateSrcset(),
|
||||
$candidate->generateSrcset()
|
||||
), 1697745459);
|
||||
}
|
||||
|
||||
$this->candidates[] = $candidate;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return SrcsetCandidate[]
|
||||
*/
|
||||
public function getCandidates(): array
|
||||
{
|
||||
return $this->candidates;
|
||||
}
|
||||
|
||||
public function generateSrcset(): string
|
||||
{
|
||||
$uniqueSrcset = [];
|
||||
foreach ($this->candidates as $candidate) {
|
||||
$uniqueSrcset[$candidate->getDescriptor()] = $candidate->generateSrcset();
|
||||
}
|
||||
return implode(', ', $uniqueSrcset);
|
||||
}
|
||||
|
||||
public function __toString()
|
||||
{
|
||||
return $this->generateSrcset();
|
||||
}
|
||||
|
||||
private function isValidCandidate(SrcsetCandidate $candidate): bool
|
||||
{
|
||||
if (!isset($this->candidateType)) {
|
||||
$this->candidateType = ($candidate instanceof DensitySrcsetCandidate)
|
||||
? DensitySrcsetCandidate::class
|
||||
: WidthSrcsetCandidate::class;
|
||||
return true;
|
||||
}
|
||||
|
||||
return $candidate instanceof $this->candidateType;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
<?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\Html\Srcset;
|
||||
|
||||
/**
|
||||
* Represents a source candidate for the HTML srcset attribute
|
||||
*/
|
||||
abstract class SrcsetCandidate
|
||||
{
|
||||
public const WIDTH_UNIT = 'w';
|
||||
public const DENSITY_UNIT = 'x';
|
||||
|
||||
protected ?string $uri = null;
|
||||
|
||||
/**
|
||||
* @param string $descriptor srcset width (like "300w") or density (like "2x") descriptor.
|
||||
* @param int|null $referenceWidth Needs to be provided if $descriptor is a density descriptor
|
||||
* (like "2x") to be able to resolve the relative image dimensions.
|
||||
* The absolute $referenceWidth will be used as "1x" width.
|
||||
*/
|
||||
public static function createFromDescriptor(string $descriptor, ?int $referenceWidth = null): SrcsetCandidate
|
||||
{
|
||||
$mode = substr($descriptor, -1);
|
||||
$value = substr($descriptor, 0, -1);
|
||||
if (is_numeric($value)) {
|
||||
if ($mode === static::DENSITY_UNIT) {
|
||||
// '1.5x'
|
||||
return new DensitySrcsetCandidate((float)$value, $referenceWidth);
|
||||
}
|
||||
if ($mode === static::WIDTH_UNIT) {
|
||||
// '200w'
|
||||
return new WidthSrcsetCandidate((int)$value);
|
||||
}
|
||||
}
|
||||
throw new \InvalidArgumentException(
|
||||
'Invalid srcset descriptor provided, must be a numeric value that ends with "w" or "x": ' . $descriptor,
|
||||
1774527269,
|
||||
);
|
||||
}
|
||||
|
||||
abstract public function getDescriptor(): string;
|
||||
abstract public function getCalculatedWidth(): int;
|
||||
|
||||
public function setUri(string $uri): static
|
||||
{
|
||||
$this->uri = $uri;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getUri(): ?string
|
||||
{
|
||||
return $this->uri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that the provided URI can be used safely in a srcset attribute
|
||||
*/
|
||||
public function getSanitizedUri(): ?string
|
||||
{
|
||||
if ($this->uri === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return strtr($this->uri, [
|
||||
' ' => '%20',
|
||||
',' => '%2C',
|
||||
]);
|
||||
}
|
||||
|
||||
public function generateSrcset(): string
|
||||
{
|
||||
return $this->getSanitizedUri() . ' ' . $this->getDescriptor();
|
||||
}
|
||||
|
||||
public function __toString()
|
||||
{
|
||||
return $this->generateSrcset();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?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\Html\Srcset;
|
||||
|
||||
/**
|
||||
* Represents a source candidate for the HTML srcset attribute
|
||||
* using the "w" unit (absolute width in image pixels, like "200w")
|
||||
*/
|
||||
final class WidthSrcsetCandidate extends SrcsetCandidate
|
||||
{
|
||||
public function __construct(protected int $width) {}
|
||||
|
||||
public function setWidth(int $width): static
|
||||
{
|
||||
$this->width = $width;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCalculatedWidth(): int
|
||||
{
|
||||
return $this->width;
|
||||
}
|
||||
|
||||
public function getDescriptor(): string
|
||||
{
|
||||
return $this->width . static::WIDTH_UNIT;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?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\Html\Visitor;
|
||||
|
||||
use TYPO3\HtmlSanitizer\Context;
|
||||
use TYPO3\HtmlSanitizer\Visitor\VisitorInterface;
|
||||
|
||||
/**
|
||||
* Visitor to remove tags but keep its content
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class UnwrapTagVisitor implements VisitorInterface
|
||||
{
|
||||
private const array UNWRAP_TAGS = [
|
||||
'a',
|
||||
'h1',
|
||||
'h2',
|
||||
'h3',
|
||||
'h4',
|
||||
'h5',
|
||||
'h6',
|
||||
];
|
||||
|
||||
public function beforeTraverse(Context $context) {}
|
||||
|
||||
public function enterNode(\DOMNode $domNode): ?\DOMNode
|
||||
{
|
||||
if (
|
||||
!$domNode instanceof \DOMElement
|
||||
|| !in_array(strtolower($domNode->tagName), self::UNWRAP_TAGS, true)
|
||||
) {
|
||||
return $domNode;
|
||||
}
|
||||
|
||||
$parent = $domNode->parentNode;
|
||||
if ($parent === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Move all children before the current node
|
||||
while ($domNode->firstChild !== null) {
|
||||
$parent->insertBefore($domNode->firstChild, $domNode);
|
||||
}
|
||||
|
||||
// Remove the wrapping element
|
||||
return null;
|
||||
}
|
||||
|
||||
public function leaveNode(\DOMNode $domNode): \DOMNode
|
||||
{
|
||||
return $domNode;
|
||||
}
|
||||
|
||||
public function afterTraverse(Context $context) {}
|
||||
}
|
||||
Reference in New Issue
Block a user