Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
196 changes: 142 additions & 54 deletions _build/test/Tests/Model/modParserTest.php
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<?php

/*
* This file is part of the MODX Revolution package.
*
Expand All @@ -11,6 +12,7 @@
*/
namespace MODX\Revolution\Tests\Model;

use MODX\Revolution\modChunk;
use MODX\Revolution\modElementPropertySet;
use MODX\Revolution\modPropertySet;
use MODX\Revolution\modSnippet;
Expand Down Expand Up @@ -427,43 +429,10 @@ public function providerProcessElementTags() {
]
],
[
// This test makes sure that spacing around the `:` doesn't matter and spacing in
// the output is kept
// Spacing around `:` is ignored; nested else tags finish in one pass once
// merge prefers the longer collected tag over `[[+is2]]`.
[
'processed' => 1,
'content' => "
[[+is2
:is=`2`
:then=`2`
:else=`more`
]]
"
],
"[[+is2
:is=`1`
:then=`[[+is2]]`
:else=`
[[+is2
:is=`2`
:then=`[[+is2]]`
:else=`more`
]]
`
]]",
[
'parentTag' => '',
'processUncacheable' => true,
'removeUnprocessed' => false,
'prefix' => '[[',
'suffix' => ']]',
'tokens' => [],
'depth' => 0
]
],
[
// Same as previous, but now parsing 2-depth to get the final result
[
'processed' => 2,
'content' => "
2
"
Expand All @@ -479,22 +448,6 @@ public function providerProcessElementTags() {
]]
`
]]",
[
'parentTag' => '',
'processUncacheable' => true,
'removeUnprocessed' => false,
'prefix' => '[[',
'suffix' => ']]',
'tokens' => [],
'depth' => 2
]
],
[
[
'processed' => 1,
'content' => "[[+is2:is=`2`:then=`2`:else=`more`]]"
],
"[[+is2:is=`1`:then=`[[+is2]]`:else=`[[+is2:is=`2`:then=`[[+is2]]`:else=`more`]]`]]",
[
'parentTag' => '',
'processUncacheable' => true,
Expand All @@ -507,7 +460,7 @@ public function providerProcessElementTags() {
],
[
[
'processed' => 2,
'processed' => 1,
'content' => "2"
],
"[[+is2:is=`1`:then=`[[+is2]]`:else=`[[+is2:is=`2`:then=`[[+is2]]`:else=`more`]]`]]",
Expand All @@ -518,12 +471,12 @@ public function providerProcessElementTags() {
'prefix' => '[[',
'suffix' => ']]',
'tokens' => [],
'depth' => 2
'depth' => 0
]
],
[
[
'processed' => 2,
'processed' => 1,
'content' => "more"
],
"[[+is3:is=`1`:then=`[[+is3]]`:else=`[[+is3:is=`2`:then=`[[+is3]]`:else=`more`]]`]]",
Expand Down Expand Up @@ -1264,6 +1217,141 @@ public function providerTestRealname() {
];
}

/**
* Shorter tags that appear inside longer collected tags must not clobber them on merge.
* This is the #13043 failure mode: `[[*id]]` replaced inside `[[$chunk-[[*id]]?…]]`.
*/
public function testMergeTagOutputOverlappingNestedTags()
{
$longTag = '[[$chunk-[[*id]]? &x=`1`]]';
$content = '$chunk-[[*id]]? &nestedcontent=`' . $longTag . '`';
$tagMap = [
'[[*id]]' => '1',
$longTag => '<div>inner</div>',
];

$this->modx->parser->mergeTagOutput($tagMap, $content);

$this->assertSame(
'$chunk-1? &nestedcontent=`<div>inner</div>`',
$content
);
}

/**
* Nested chunk calls whose names contain another tag, e.g. `[[$chunk-[[+id]]]]`.
*
* @dataProvider providerNestedChunkNameContainsTag
* @param int $levels
*/
public function testNestedChunkNameContainsTag($levels)
{
$suffix = bin2hex(random_bytes(3));
$chunkName = 'n13043-' . $suffix;
$chunk = $this->modx->newObject(modChunk::class);
$chunk->set('name', $chunkName);
$chunk->set(
'snippet',
'<div class="wrapper">'
. '<span>Level: [[+level:default=`not set`]]</span>'
. '[[+nestedcontent]]</div>'
);
$chunk->setCacheable(false);
$this->assertTrue($chunk->save());

$this->modx->setPlaceholder('n13043id', $suffix);

$inner = 'inner-13043';
for ($level = $levels; $level >= 1; $level--) {
$inner = '[[$n13043-[[+n13043id]]? &level=`' . $level . '` &nestedcontent=`' . $inner . '`]]';
}
$content = $inner;

try {
$this->modx->parser->processElementTags('', $content, true, false, '[[', ']]', [], 10);
$this->assertSame($levels, substr_count($content, '<div class="wrapper">'));
for ($level = 1; $level <= $levels; $level++) {
$this->assertStringContainsString('Level: ' . $level, $content);
}
$this->assertStringContainsString('inner-13043', $content);
$this->assertStringNotContainsString('[[$', $content);
} finally {
$chunk->remove();
$this->modx->unsetPlaceholder('n13043id');
}
}

/**
* A cacheable element's output may contain uncacheable tags, deferred to a later
* iteration when parser_recurse_uncacheable is enabled (the default). Merge order
* must not rewrite those emitted tags with values captured before the element executed.
*/
public function testMergedOutputKeepsDeferredUncacheableTags()
{
$name = 'sg16991xx' . bin2hex(random_bytes(4));
$snippet = $this->modx->newObject(modSnippet::class);
$snippet->set('name', $name);
$snippet->set(
'snippet',
'$modx->setPlaceholder(\'greeting16991\', \'AFTER\'); return \'tpl says [[!+greeting16991]]\';'
);
$this->assertTrue($snippet->save());
$this->modx->setPlaceholder('greeting16991', 'BEFORE');
/* cacheable snippet call; same signature as the uncached pass in modResource::process() */
$content = "Header shows [[!+greeting16991]] ... [[{$name}]]";
try {
$this->modx->parser->processElementTags('', $content, true, false, '[[', ']]', [], 10);
$this->assertStringContainsString('Header shows BEFORE', $content);
$this->assertStringContainsString('tpl says AFTER', $content);
} finally {
$snippet->remove();
$this->modx->unsetPlaceholder('greeting16991');
}
}

/**
* An uncacheable snippet called standalone and also emitted by a cacheable chunk
* must execute once per occurrence, not have the standalone output duplicated.
*/
public function testMergedOutputKeepsDeferredUncacheableSnippetExecutions()
{
$suffix = bin2hex(random_bytes(4));
$snipName = 'uid16991' . $suffix;
$chunkName = 'wrap16991xx' . $suffix;
$snippet = $this->modx->newObject(modSnippet::class);
$snippet->set('name', $snipName);
$snippet->set(
'snippet',
'$n = (int) $modx->getPlaceholder(\'uidcount16991\') + 1;'
. '$modx->setPlaceholder(\'uidcount16991\', $n);'
. 'return \'UID\' . $n;'
);
$this->assertTrue($snippet->save());
$chunk = $this->modx->newObject(modChunk::class);
$chunk->set('name', $chunkName);
$chunk->set('snippet', "<div>[[!{$snipName}]]</div>");
$this->assertTrue($chunk->save());
$content = "[[!{$snipName}]] ... [[\${$chunkName}]]";
try {
$this->modx->parser->processElementTags('', $content, true, false, '[[', ']]', [], 10);
$this->assertStringContainsString('UID1', $content);
$this->assertStringContainsString('UID2', $content);
$this->assertSame(2, (int) $this->modx->getPlaceholder('uidcount16991'));
} finally {
$snippet->remove();
$chunk->remove();
$this->modx->unsetPlaceholder('uidcount16991');
}
}

public function providerNestedChunkNameContainsTag()
{
return [
'two levels' => [2],
'three levels' => [3],
];
}

public function testDefaultNonExistingTvValue() {
$output = "[[*foo:default=`bar`]]";
$this->modx->parser->processElementTags('', $output, true, false, '[[', ']]', [], 10);
Expand Down
2 changes: 1 addition & 1 deletion core/src/Revolution/modParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@
*/
public function mergeTagOutput(array $tagMap, & $content) {
if (!empty ($content) && is_array($tagMap) && !empty ($tagMap)) {
$content= str_replace(array_keys($tagMap), array_values($tagMap), $content);
$content= strtr($content, $tagMap);

Check failure on line 253 in core/src/Revolution/modParser.php

View workflow job for this annotation

GitHub Actions / phpcs

Expected at least 1 space before "="; 0 found
}
}

Expand Down
Loading