diff --git a/_build/test/Tests/Model/modParserTest.php b/_build/test/Tests/Model/modParserTest.php
index 545264f8258..eee4359aef9 100644
--- a/_build/test/Tests/Model/modParserTest.php
+++ b/_build/test/Tests/Model/modParserTest.php
@@ -11,7 +11,9 @@
*/
namespace MODX\Revolution\Tests\Model;
-
+use MODX\Revolution\modElementPropertySet;
+use MODX\Revolution\modPropertySet;
+use MODX\Revolution\modSnippet;
use MODX\Revolution\modX;
use MODX\Revolution\MODxTestCase;
use MODX\Revolution\MODxTestHarness;
@@ -764,23 +766,188 @@ public function providerProcessElementTags() {
],
// tags directly within brackets
// @todo this test fails
-// [
-// [
-// 'processed' => 1,
-// 'content' => ''
-// ],
-// '[[+tag1:notempty=``]]',
-// [
-// 'parentTag' => '',
-// 'processUncacheable' => true,
-// 'removeUnprocessed' => false,
-// 'prefix' => '[[',
-// 'suffix' => ']]',
-// 'tokens' => [],
-// 'depth' => 1
-// ]
-// ],
+ // [
+ // [
+ // 'processed' => 1,
+ // 'content' => ''
+ // ],
+ // '[[+tag1:notempty=``]]',
+ // [
+ // 'parentTag' => '',
+ // 'processUncacheable' => true,
+ // 'removeUnprocessed' => false,
+ // 'prefix' => '[[',
+ // 'suffix' => ']]',
+ // 'tokens' => [],
+ // 'depth' => 1
+ // ]
+ // ],
+
+ // #16318 parsing tags with @ in the value causes it to break the tag
+ [
+ [
+ 'processed' => 1,
+ 'content' => "aaa
+[[nonExistentSnippet? &x=`bbb@ccc`]]
+ddd
+eee"
+ ],
+ "[[+empty_content:empty=`aaa
+[[nonExistentSnippet? &x=`bbb@ccc`]]
+ddd
+`]]eee",
+ [
+ 'parentTag' => '',
+ 'processUncacheable' => true,
+ 'removeUnprocessed' => false,
+ 'prefix' => '[[',
+ 'suffix' => ']]',
+ 'tokens' => [],
+ 'depth' => 0
+ ]
+ ]
+ ];
+ }
+
+ /**
+ * @dataProvider providerPropertySetCall
+ * @param $content
+ * @param $expected
+ * @param $propertySet
+ * @param $params
+ * @return void
+ */
+ public function testPropertySetCall($content, $expected, $propertySet, $params)
+ {
+ /** @var modPropertySet $set */
+ $set = $this->modx->newObject(modPropertySet::class);
+ $set->set('name', 'propset_' . bin2hex(random_bytes(4)));
+ $set->setProperties($propertySet);
+ self::assertTrue($set->save());
+
+ /** @var modSnippet $set */
+ $snippet = $this->modx->newObject(modSnippet::class);
+ $snippet->set('name', 'snippet_' . bin2hex(random_bytes(4)));
+ $snippet->set('content', 'save());
+
+ $join = $this->modx->newObject(modElementPropertySet::class);
+ $join->fromArray([
+ 'element' => $snippet->get('id'),
+ 'element_class' => $snippet->_class,
+ 'property_set' => $set->get('id'),
+ ], '', true);
+ $join->save();
+ self::assertTrue($join->save());
+
+ $content = str_replace('propSetName', $set->get('name'), $content);
+ $content = str_replace('snippetName', $snippet->get('name'), $content);
+
+ $c = $content;
+
+ $this->modx->parser->processElementTags(
+ $params['parentTag'],
+ $content,
+ $params['processUncacheable'],
+ $params['removeUnprocessed'],
+ $params['prefix'],
+ $params['suffix'],
+ $params['tokens'],
+ $params['depth']
+ );
+
+ $set->remove();
+ $snippet->remove();
+ $join->remove();
+
+ $this->assertEquals($expected, $content, "Did not get expected results from parsing {$c}.");
+ }
+ public function providerPropertySetCall()
+ {
+ // In this test, snippetName and propSetName are replaced with a random string
+ // for each run
+ return [
+ [
+ '[[snippetName? &prop=`123`]]',
+ '123',
+ [],
+ [
+ 'parentTag' => '',
+ 'processUncacheable' => true,
+ 'removeUnprocessed' => false,
+ 'prefix' => '[[',
+ 'suffix' => ']]',
+ 'tokens' => [],
+ 'depth' => 10
+ ]
+ ],
+ [
+ '[[snippetName@propSetName]]',
+ '123',
+ [
+ 'prop' => '123',
+ ],
+ [
+ 'parentTag' => '',
+ 'processUncacheable' => true,
+ 'removeUnprocessed' => false,
+ 'prefix' => '[[',
+ 'suffix' => ']]',
+ 'tokens' => [],
+ 'depth' => 10
+ ]
+ ],
+ [
+ '[[snippetName@propSetName? &otherProp=`foo`]]',
+ '789',
+ [
+ 'prop' => '789',
+ ],
+ [
+ 'parentTag' => '',
+ 'processUncacheable' => true,
+ 'removeUnprocessed' => false,
+ 'prefix' => '[[',
+ 'suffix' => ']]',
+ 'tokens' => [],
+ 'depth' => 10
+ ]
+ ],
+ [
+ '[[snippetName@propSetName? &prop=`123`]]',
+ '123',
+ [
+ 'prop' => '456', // needs to be ignored because &prop is specified as override
+ ],
+ [
+ 'parentTag' => '',
+ 'processUncacheable' => true,
+ 'removeUnprocessed' => false,
+ 'prefix' => '[[',
+ 'suffix' => ']]',
+ 'tokens' => [],
+ 'depth' => 10
+ ]
+ ],
+ [
+ 'This is a [[snippetName@propSetName:default=`default value`]]',
+ 'This is a default value',
+ [
+ 'otherProp' => 'not this one', // props other than the test 'prop' should have no effect on output
+ ],
+ [
+ 'parentTag' => '',
+ 'processUncacheable' => true,
+ 'removeUnprocessed' => false,
+ 'prefix' => '[[',
+ 'suffix' => ']]',
+ 'tokens' => [],
+ 'depth' => 10
+ ]
+ ],
];
+
}
/**
diff --git a/core/src/Revolution/modElement.php b/core/src/Revolution/modElement.php
index 55d9a3b62c6..ef1f0f992ba 100644
--- a/core/src/Revolution/modElement.php
+++ b/core/src/Revolution/modElement.php
@@ -726,19 +726,23 @@ public function getPropertySet($setName = null)
{
$propertySet = null;
$name = $this->get('name');
- if (strpos($name, '@') !== false) {
- $psName = '';
- $split = xPDO:: escSplit('@', $name);
- if ($split && isset($split[1])) {
- $name = $split[0];
- $psName = $split[1];
- $filters = xPDO:: escSplit(':', $setName);
- if ($filters && isset($filters[1]) && !empty($filters[1])) {
- $psName = $filters[0];
- $name .= ':' . $filters[1];
- }
- $this->set('name', $name);
- }
+
+ $startFiltersIndex = strpos($name, ':');
+
+ if ($startFiltersIndex !== false) {
+ $tagStart = mb_substr($name, 0, $startFiltersIndex);
+ $tagEnd = mb_substr($name, $startFiltersIndex);
+ } else {
+ $tagStart = $name;
+ $tagEnd = '';
+ }
+
+ if (strpos($tagStart, '@') !== false) {
+ $split = xPDO:: escSplit('@', $tagStart);
+ $psName = $split[1];
+
+ $this->set('name', $split[0] . $tagEnd);
+
if (!empty($psName)) {
$psObj = $this->xpdo->getObjectGraph(modPropertySet::class, '{"Elements":{}}', [
'Elements.element' => $this->id,
diff --git a/core/src/Revolution/modTag.php b/core/src/Revolution/modTag.php
index 6120511f557..a397c5ea72e 100644
--- a/core/src/Revolution/modTag.php
+++ b/core/src/Revolution/modTag.php
@@ -511,19 +511,23 @@ public function getPropertySet($setName = null)
{
$propertySet = null;
$name = $this->get('name');
- if (strpos($name, '@') !== false) {
- $psName = '';
- $split = xPDO:: escSplit('@', $name);
- if ($split && isset($split[1])) {
- $name = $split[0];
- $psName = $split[1];
- $filters = xPDO:: escSplit(':', $setName);
- if ($filters && isset($filters[1]) && !empty($filters[1])) {
- $psName = $filters[0];
- $name .= ':' . $filters[1];
- }
- $this->set('name', $name);
- }
+
+ $startFiltersIndex = strpos($name, ':');
+
+ if ($startFiltersIndex !== false) {
+ $tagStart = mb_substr($name, 0, $startFiltersIndex);
+ $tagEnd = mb_substr($name, $startFiltersIndex);
+ } else {
+ $tagStart = $name;
+ $tagEnd = '';
+ }
+
+ if (strpos($tagStart, '@') !== false) {
+ $split = xPDO:: escSplit('@', $tagStart);
+ $psName = $split[1];
+
+ $this->set('name', $split[0] . $tagEnd);
+
if (!empty($psName)) {
$psObj = $this->modx->getObject(modPropertySet::class, ['name' => $psName]);
if ($psObj) {