From 43d0fa1e78765ab1272f2f06d4a86a0c667ac538 Mon Sep 17 00:00:00 2001 From: Wolfgang Klinger <wolfgang@wazum.com> Date: Sun, 25 Nov 2018 11:32:17 +0100 Subject: [PATCH] [BUGFIX] Fix parseFunc handling of nested tags Resolves: #39261 Releases: master Change-Id: I981e9af652635db661126c1a4e0ccf3841417d54 Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/58946 Tested-by: TYPO3com <noreply@typo3.com> Tested-by: Georg Ringer <georg.ringer@gmail.com> Tested-by: Benni Mack <benni@typo3.org> Reviewed-by: Susanne Moog <look@susi.dev> Reviewed-by: Georg Ringer <georg.ringer@gmail.com> Reviewed-by: Benni Mack <benni@typo3.org> --- .../ContentObject/ContentObjectRenderer.php | 78 ++++- .../ContentObjectRendererTest.php | 321 ++++++++++++++++++ 2 files changed, 385 insertions(+), 14 deletions(-) diff --git a/typo3/sysext/frontend/Classes/ContentObject/ContentObjectRenderer.php b/typo3/sysext/frontend/Classes/ContentObject/ContentObjectRenderer.php index 476ac5436b78..8d0781cb1409 100644 --- a/typo3/sysext/frontend/Classes/ContentObject/ContentObjectRenderer.php +++ b/typo3/sysext/frontend/Classes/ContentObject/ContentObjectRenderer.php @@ -3790,7 +3790,7 @@ class ContentObjectRenderer implements LoggerAwareInterface // Pointer to the total string position $pointer = 0; // Loaded with the current typo-tag if any. - $currentTag = ''; + $currentTag = null; $stripNL = 0; $contentAccum = []; $contentAccumP = 0; @@ -3799,7 +3799,7 @@ class ContentObjectRenderer implements LoggerAwareInterface $totalLen = strlen($theValue); do { if (!$inside) { - if (!is_array($currentTag)) { + if ($currentTag === null) { // These operations should only be performed on code outside the typotags... // data: this checks that we enter tags ONLY if the first char in the tag is alphanumeric OR '/' $len_p = 0; @@ -3812,17 +3812,12 @@ class ContentObjectRenderer implements LoggerAwareInterface } while ($c > 0 && $endChar && ($endChar < 97 || $endChar > 122) && $endChar != 47); $len = $len_p - 1; } else { - // If we're inside a currentTag, just take it to the end of that tag! - $tempContent = strtolower(substr($theValue, $pointer)); - $len = strpos($tempContent, '</' . $currentTag[0]); - if (is_string($len) && !$len) { - $len = strlen($tempContent); - } + $len = $this->getContentLengthOfCurrentTag($theValue, $pointer, $currentTag[0]); } // $data is the content until the next <tag-start or end is detected. // In case of a currentTag set, this would mean all data between the start- and end-tags $data = substr($theValue, $pointer, $len); - if ($data != '') { + if ($data !== false) { if ($stripNL) { // If the previous tag was set to strip NewLines in the beginning of the next data-chunk. $data = preg_replace('/^[ ]*' . CR . '?' . LF . '/', '', $data); @@ -3832,7 +3827,7 @@ class ContentObjectRenderer implements LoggerAwareInterface // Constants $tsfe = $this->getTypoScriptFrontendController(); $tmpConstants = $tsfe->tmpl->setup['constants.'] ?? null; - if ($conf['constants'] && is_array($tmpConstants)) { + if (!empty($conf['constants']) && is_array($tmpConstants)) { foreach ($tmpConstants as $key => $val) { if (is_string($val)) { $data = str_replace('###' . $key . '###', $val, $data); @@ -3893,6 +3888,16 @@ class ContentObjectRenderer implements LoggerAwareInterface $data = $newstring; } } + // Search for tags to process in current data and + // call this method recursively if found + if (strpos($data, '<') !== false) { + foreach ($conf['tags.'] as $tag => $tagConfig) { + if (strpos($data, '<' . $tag) !== false) { + $data = $this->_parseFunc($data, $conf); + break; + } + } + } $contentAccum[$contentAccumP] = isset($contentAccum[$contentAccumP]) ? $contentAccum[$contentAccumP] . $data : $data; @@ -3909,6 +3914,7 @@ class ContentObjectRenderer implements LoggerAwareInterface } $tag = explode(' ', trim($tagContent), 2); $tag[0] = strtolower($tag[0]); + // end tag like </li> if ($tag[0][0] === '/') { $tag[0] = substr($tag[0], 1); $tag['out'] = 1; @@ -3936,15 +3942,15 @@ class ContentObjectRenderer implements LoggerAwareInterface // This flag indicates, that this TypoTag section should NOT be included in the nonTypoTag content. $breakOut = (bool)($theConf['breakoutTypoTagContent'] ?? false); $this->parameters = []; - if ($currentTag[1]) { + if (isset($currentTag[1])) { $params = GeneralUtility::get_tag_attributes($currentTag[1]); if (is_array($params)) { foreach ($params as $option => $val) { $this->parameters[strtolower($option)] = $val; } } + $this->parameters['allParams'] = trim($currentTag[1]); } - $this->parameters['allParams'] = trim($currentTag[1]); // Removes NL in the beginning and end of the tag-content AND at the end of the currentTagBuffer. // $stripNL depends on the configuration of the current tag if ($stripNL) { @@ -3967,7 +3973,7 @@ class ContentObjectRenderer implements LoggerAwareInterface unset($contentAccum[$contentAccumP - 1]); $contentAccumP -= 2; } - $currentTag = ''; + $currentTag = null; $treated = true; } // other tags @@ -3977,7 +3983,8 @@ class ContentObjectRenderer implements LoggerAwareInterface } else { // If a tag was not a typo tag, then it is just added to the content $stripNL = false; - if (GeneralUtility::inList($allowTags, $tag[0]) || $denyTags !== '*' && !GeneralUtility::inList($denyTags, $tag[0])) { + if (GeneralUtility::inList($allowTags, $tag[0]) || + ($denyTags !== '*' && !GeneralUtility::inList($denyTags, $tag[0]))) { $contentAccum[$contentAccumP] = isset($contentAccum[$contentAccumP]) ? $contentAccum[$contentAccumP] . $data : $data; @@ -7151,4 +7158,47 @@ class ContentObjectRenderer implements LoggerAwareInterface // Otherwise just return the link text return $linkText; } + + /** + * Get content length of the current tag that could also contain nested tag contents + * + * @param string $theValue + * @param int $pointer + * @param string $currentTag + * @return int + */ + protected function getContentLengthOfCurrentTag(string $theValue, int $pointer, string $currentTag): int + { + $tempContent = strtolower(substr($theValue, $pointer)); + $startTag = '<' . $currentTag; + $endTag = '</' . $currentTag . '>'; + $offsetCount = 0; + + // Take care for nested tags + do { + $nextMatchingEndTagPosition = strpos($tempContent, $endTag); + $nextSameTypeTagPosition = strpos($tempContent, $startTag); + + // filter out nested tag contents to help getting the correct closing tag + if ($nextSameTypeTagPosition !== false && $nextSameTypeTagPosition < $nextMatchingEndTagPosition) { + $lastOpeningTagStartPosition = strrpos(substr($tempContent, 0, $nextMatchingEndTagPosition), $startTag); + $closingTagEndPosition = $nextMatchingEndTagPosition + strlen($endTag); + $offsetCount += $closingTagEndPosition - $lastOpeningTagStartPosition; + + // replace content from latest tag start to latest tag end + $tempContent = substr($tempContent, 0, $lastOpeningTagStartPosition) . substr($tempContent, $closingTagEndPosition); + } + } while ( + ($nextMatchingEndTagPosition !== false && $nextSameTypeTagPosition !== false) && + $nextSameTypeTagPosition < $nextMatchingEndTagPosition + ); + + // if no closing tag is found we use length of the whole content + $endingOffset = strlen($tempContent); + if ($nextMatchingEndTagPosition !== false) { + $endingOffset = $nextMatchingEndTagPosition + $offsetCount; + } + + return $endingOffset; + } } diff --git a/typo3/sysext/frontend/Tests/Unit/ContentObject/ContentObjectRendererTest.php b/typo3/sysext/frontend/Tests/Unit/ContentObject/ContentObjectRendererTest.php index 941d4b3f27ec..a586aa611cc1 100644 --- a/typo3/sysext/frontend/Tests/Unit/ContentObject/ContentObjectRendererTest.php +++ b/typo3/sysext/frontend/Tests/Unit/ContentObject/ContentObjectRendererTest.php @@ -2718,6 +2718,315 @@ class ContentObjectRendererTest extends UnitTestCase ]; } + /** + * Data provider for the parseFuncParsesNestedTagsProperly test + * + * @return \Generator multi-dimensional array with test data + * @see parseFuncParsesNestedTagsProperly + */ + public function _parseFuncParsesNestedTagsProperlyDataProvider(): \Generator + { + yield 'list with empty and filled li' => [ + '<ul> + <li></li> + <li>second</li> +</ul>', + [ + 'parseFunc' => '', + 'parseFunc.' => [ + 'tags.' => [ + 'li' => 'TEXT', + 'li.' => [ + 'wrap' => '<li>LI:|</li>', + 'current' => '1' + ] + ] + ] + ], + '<ul> + <li>LI:</li> + <li>LI:second</li> +</ul>', + ]; + yield 'list with filled li wrapped by a div containing text' => [ + '<div>text<ul><li></li><li>second</li></ul></div>', + [ + 'parseFunc' => '', + 'parseFunc.' => [ + 'tags.' => [ + 'li' => 'TEXT', + 'li.' => [ + 'wrap' => '<li>LI:|</li>', + 'current' => '1' + ] + ] + ] + ], + '<div>text<ul><li>LI:</li><li>LI:second</li></ul></div>', + ]; + yield 'link list with empty li modification' => [ + '<ul> + <li> + <ul> + <li></li> + </ul> + </li> +</ul>', + [ + 'parseFunc' => '', + 'parseFunc.' => [ + 'tags.' => [ + 'li' => 'TEXT', + 'li.' => [ + 'wrap' => '<li>LI:|</li>', + 'current' => '1' + ] + ] + ] + ], + '<ul> + <li>LI: + <ul> + <li>LI:</li> + </ul> + </li> +</ul>', + ]; + + yield 'link list with li modifications' => [ + '<ul> + <li>first</li> + <li>second + <ul> + <li>first sub</li> + <li>second sub</li> + </ul> + </li> +</ul>', + [ + 'parseFunc' => '', + 'parseFunc.' => [ + 'tags.' => [ + 'li' => 'TEXT', + 'li.' => [ + 'wrap' => '<li>LI:|</li>', + 'current' => '1' + ] + ] + ] + ], + '<ul> + <li>LI:first</li> + <li>LI:second + <ul> + <li>LI:first sub</li> + <li>LI:second sub</li> + </ul> + </li> +</ul>' + ]; + yield 'link list with li modifications and no text' => [ + '<ul> + <li>first</li> + <li> + <ul> + <li>first sub</li> + <li>second sub</li> + </ul> + </li> +</ul>', + [ + 'parseFunc' => '', + 'parseFunc.' => [ + 'tags.' => [ + 'li' => 'TEXT', + 'li.' => [ + 'wrap' => '<li>LI:|</li>', + 'current' => '1' + ] + ] + ] + ], + '<ul> + <li>LI:first</li> + <li>LI: + <ul> + <li>LI:first sub</li> + <li>LI:second sub</li> + </ul> + </li> +</ul>', + ]; + yield 'link list with li modifications on third level' => [ + '<ul> + <li>first</li> + <li>second + <ul> + <li>first sub + <ul> + <li>first sub sub</li> + <li>second sub sub</li> + </ul> + </li> + <li>second sub</li> + </ul> + </li> +</ul>', + [ + 'parseFunc' => '', + 'parseFunc.' => [ + 'tags.' => [ + 'li' => 'TEXT', + 'li.' => [ + 'wrap' => '<li>LI:|</li>', + 'current' => '1' + ] + ] + ] + ], + '<ul> + <li>LI:first</li> + <li>LI:second + <ul> + <li>LI:first sub + <ul> + <li>LI:first sub sub</li> + <li>LI:second sub sub</li> + </ul> + </li> + <li>LI:second sub</li> + </ul> + </li> +</ul>', + ]; + yield 'link list with li modifications on third level no text' => [ + '<ul> + <li>first</li> + <li> + <ul> + <li> + <ul> + <li>first sub sub</li> + <li>first sub sub</li> + </ul> + </li> + <li>second sub</li> + </ul> + </li> +</ul>', + [ + 'parseFunc' => '', + 'parseFunc.' => [ + 'tags.' => [ + 'li' => 'TEXT', + 'li.' => [ + 'wrap' => '<li>LI:|</li>', + 'current' => '1' + ] + ] + ] + ], + '<ul> + <li>LI:first</li> + <li>LI: + <ul> + <li>LI: + <ul> + <li>LI:first sub sub</li> + <li>LI:first sub sub</li> + </ul> + </li> + <li>LI:second sub</li> + </ul> + </li> +</ul>', + ]; + yield 'link list with ul and li modifications' => [ + '<ul> + <li>first</li> + <li>second + <ul> + <li>first sub</li> + <li>second sub</li> + </ul> + </li> +</ul>', + [ + 'parseFunc' => '', + 'parseFunc.' => [ + 'tags.' => [ + 'ul' => 'TEXT', + 'ul.' => [ + 'wrap' => '<ul><li>intro</li>|<li>outro</li></ul>', + 'current' => '1' + ], + 'li' => 'TEXT', + 'li.' => [ + 'wrap' => '<li>LI:|</li>', + 'current' => '1' + ] + ] + ] + ], + '<ul><li>intro</li> + <li>LI:first</li> + <li>LI:second + <ul><li>intro</li> + <li>LI:first sub</li> + <li>LI:second sub</li> + <li>outro</li></ul> + </li> +<li>outro</li></ul>', + ]; + + yield 'link list with li containing p tag and sub list' => [ + '<ul> + <li>first</li> + <li> + <ul> + <li> + <p> + <ul> + <li>first sub sub</li> + <li>first sub sub</li> + </ul> + </p> + </li> + <li>second sub</li> + </ul> + </li> +</ul>', + [ + 'parseFunc' => '', + 'parseFunc.' => [ + 'tags.' => [ + 'li' => 'TEXT', + 'li.' => [ + 'wrap' => '<li>LI:|</li>', + 'current' => '1' + ] + ] + ] + ], + '<ul> + <li>LI:first</li> + <li>LI: + <ul> + <li>LI: + <p> + <ul> + <li>LI:first sub sub</li> + <li>LI:first sub sub</li> + </ul> + </p> + </li> + <li>LI:second sub</li> + </ul> + </li> +</ul>', + ]; + } + /** * @test * @dataProvider _parseFuncReturnsCorrectHtmlDataProvider @@ -2730,6 +3039,18 @@ class ContentObjectRendererTest extends UnitTestCase self::assertEquals($expectedResult, $this->subject->stdWrap_parseFunc($value, $configuration)); } + /** + * @test + * @dataProvider _parseFuncParsesNestedTagsProperlyDataProvider + * @param string $value + * @param array $configuration + * @param string $expectedResult + */ + public function parseFuncParsesNestedTagsProperly(string $value, array $configuration, string $expectedResult): void + { + self::assertEquals($expectedResult, $this->subject->stdWrap_parseFunc($value, $configuration)); + } + /** * @return array */ -- GitLab