Bug #92301
closedElements getting wrapped with p tags, even if allowed outside paragraph
100%
Description
Let's assume we have a CKEditor plugin/widget that generates the following code
<div>
<time datetime="2001-05-15T22:00">Tuesday, 15. May 2001, 10:00 Uhr</time>
</div>
After saving, this code gets correctly inserted into the database. But after that,
Typo3 loads the following code inside the RTE, even if the time tag is allowed outside of a paragraph via tsconfig
<div>
<p><time datetime="2001-05-15T22:00">Tuesday, 15. May 2001, 10:00 Uhr</time></p>
</div>
After saving the record again, the code is also wrong in the database.
I managed to fix this issue after hours of php debugging.
The problem is located in the RteHtmlParser(typo3/sysext/core/Classes/Html/RteHtmlParser.php).
The allowedTagsOutsideOfParagraphs property of the RteHtmlParser class, is not getting queried before the call of the setDivTags method inside the TS_transform_rte method.
I created a Typo3 fork, where I fixed the problem.
Edit:
I created a better patch.
For anyone who stumbles across this issue, I managed to create a workaround.
I've just created a custom transformation class that inherits from the RteHtmlParser class.
<?php
namespace Vendor\Sitepackage\Service;
use Psr\EventDispatcher\EventDispatcherInterface;
use TYPO3\CMS\Core\Html\RteHtmlParser;
class RteTransformation extends RteHtmlParser
{
/**
* NOTE: must be public as it is accessed by \TYPO3\CMS\Core\Html\RteHtmlParser without API
*
* @var \TYPO3\CMS\Core\Html\RteHtmlParser
*/
public $pObj;
/**
* NOTE: must be public as it is accessed by \TYPO3\CMS\Core\Html\RteHtmlParser without API
*
* @var string
*/
public $transformationKey = 'tx_sitepackage_transformation';
/**
* @var array
*/
protected $configuration;
public function __construct(EventDispatcherInterface $eventDispatcher = null)
{
$this->eventDispatcher = $eventDispatcher;
}
protected function loadConfiguration()
{
$this->configuration = $this->pObj->procOptions['usertrans.'][$this->transformationKey . '.'];
$this->setProcessingConfiguration($this->pObj->procOptions);
}
/**
* Transforms RTE content prior to database storage
*
* @param string $value RTE HTML to clean for database storage
* @return string
*/
public function transform_db($value)
{
$this->loadConfiguration();
// 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);
return $this->TS_transform_db($value);
}
/**
* Transforms database content for RTE display
*
* @param string $value Database content to transform into RTE-ready HTML
* @return string
*/
public function transform_rte($value)
{
$this->loadConfiguration();
return $this->TS_transform_rte($value);
}
/**
* OVERRIDES DEFAULT METHOD IN RteHtmlParser.php!!!
* 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($value)
{
// 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]);
}
$partFTN = strtolower($this->getFirstTagName($parts[$k] ?? ''));
// Filter out the hr
$allowedTagsOutsideOfParagraphs = array_filter($this->allowedTagsOutsideOfParagraphs, function ($tag) {
return $tag != 'hr';
});
// 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 (!preg_match('/<(hr)(\\s[^>\\/]*)?[[:space:]]*\\/?>/i', $parts[$k]) && !in_array($partFTN, $allowedTagsOutsideOfParagraphs)) {
$testStr = strtolower(trim($parts[$k]));
if (strpos($testStr, '<div') !== 0 || substr($testStr, -6) !== '</div>') {
if (strpos($testStr, '<p') !== 0 || substr($testStr, -4) !== '</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);
}
}
Notice that I have overrode the default setDivTags method with my changes.
Don't forget to add this line to your ext_localconf.php
/***************
* Register Custom Transformation
*/
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_parsehtml_proc.php']['transformation']['tx_sitepackage_transformation'] = Vendor\Sitepackage\Service\RteTransformation::class;
This is how you can use the custom transformation in the RTE.tsconfig
config.tt_content.bodytext.proc.overruleMode = detectbrokenlinks,tx_sitepackage_transformation,ts_links
Have a look at https://docs.typo3.org/m/typo3/reference-coreapi/master/en-us/ApiOverview/Rte/Transformations/CustomApi.html if you need more informations on how to use custom transformations.
Here are a few more infomations on how to use custom transformations https://docs.typo3.org/m/typo3/reference-coreapi/7.6/en-us/_sources/Rte/Transformations/CustomApi/Index.rst.txt?refid=transformations-custom&line=25