Skip to content

Commit fb11c36

Browse files
Merge branch '1.3-develop' into MC-34590
2 parents feb8164 + 70742ba commit fb11c36

File tree

7 files changed

+259
-49
lines changed

7 files changed

+259
-49
lines changed

app/code/Magento/PageBuilder/Model/Filter/Template.php

Lines changed: 98 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -7,47 +7,65 @@
77

88
namespace Magento\PageBuilder\Model\Filter;
99

10+
use DOMDocument;
11+
use DOMElement;
12+
use DOMException;
13+
use DOMNode;
14+
use DOMXPath;
15+
use Exception;
16+
use Magento\Framework\Exception\LocalizedException;
17+
use Magento\Framework\Math\Random;
18+
use Magento\Framework\Serialize\Serializer\Json;
19+
use Magento\Framework\View\ConfigInterface;
20+
use Magento\PageBuilder\Plugin\Filter\TemplatePlugin;
21+
use Psr\Log\LoggerInterface;
22+
1023
/**
1124
* Specific template filters for Page Builder content
1225
*/
1326
class Template
1427
{
1528
/**
16-
* @var \Magento\Framework\View\ConfigInterface
29+
* @var ConfigInterface
1730
*/
1831
private $viewConfig;
1932

2033
/**
21-
* @var \Psr\Log\LoggerInterface
34+
* @var LoggerInterface
2235
*/
2336
private $logger;
2437

2538
/**
26-
* @var \DOMDocument
39+
* @var DOMDocument
2740
*/
2841
private $domDocument;
2942

3043
/**
31-
* @var \Magento\Framework\Math\Random
44+
* @var Random
3245
*/
3346
private $mathRandom;
3447

3548
/**
36-
* @var \Magento\Framework\Serialize\Serializer\Json
49+
* @var Json
3750
*/
3851
private $json;
3952

4053
/**
41-
* @param \Psr\Log\LoggerInterface $logger
42-
* @param \Magento\Framework\View\ConfigInterface $viewConfig
43-
* @param \Magento\Framework\Math\Random $mathRandom
44-
* @param \Magento\Framework\Serialize\Serializer\Json $json
54+
* @var array
55+
*/
56+
private $scripts;
57+
58+
/**
59+
* @param LoggerInterface $logger
60+
* @param ConfigInterface $viewConfig
61+
* @param Random $mathRandom
62+
* @param Json $json
4563
*/
4664
public function __construct(
47-
\Psr\Log\LoggerInterface $logger,
48-
\Magento\Framework\View\ConfigInterface $viewConfig,
49-
\Magento\Framework\Math\Random $mathRandom,
50-
\Magento\Framework\Serialize\Serializer\Json $json
65+
LoggerInterface $logger,
66+
ConfigInterface $viewConfig,
67+
Random $mathRandom,
68+
Json $json
5169
) {
5270
$this->logger = $logger;
5371
$this->viewConfig = $viewConfig;
@@ -59,22 +77,22 @@ public function __construct(
5977
* After filter of template data apply transformations
6078
*
6179
* @param string $result
62-
*
6380
* @return string
6481
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
6582
*/
6683
public function filter(string $result) : string
6784
{
6885
$this->domDocument = false;
86+
$this->scripts = [];
6987

7088
// Validate if the filtered result requires background image processing
71-
if (preg_match(\Magento\PageBuilder\Plugin\Filter\TemplatePlugin::BACKGROUND_IMAGE_PATTERN, $result)) {
89+
if (preg_match(TemplatePlugin::BACKGROUND_IMAGE_PATTERN, $result)) {
7290
$document = $this->getDomDocument($result);
7391
$this->generateBackgroundImageStyles($document);
7492
}
7593

7694
// Process any HTML content types, they need to be decoded on the front-end
77-
if (preg_match(\Magento\PageBuilder\Plugin\Filter\TemplatePlugin::HTML_CONTENT_TYPE_PATTERN, $result)) {
95+
if (preg_match(TemplatePlugin::HTML_CONTENT_TYPE_PATTERN, $result)) {
7896
$document = $this->getDomDocument($result);
7997
$uniqueNodeNameToDecodedOuterHtmlMap = $this->generateDecodedHtmlPlaceholderMappingInDocument($document);
8098
}
@@ -112,6 +130,8 @@ function ($matches) {
112130

113131
$result = $docHtml;
114132
}
133+
134+
$result = $this->unmaskScriptTags($result);
115135
}
116136

117137
return $result;
@@ -122,9 +142,9 @@ function ($matches) {
122142
*
123143
* @param string $html
124144
*
125-
* @return \DOMDocument
145+
* @return DOMDocument
126146
*/
127-
private function getDomDocument(string $html) : \DOMDocument
147+
private function getDomDocument(string $html) : DOMDocument
128148
{
129149
if (!$this->domDocument) {
130150
$this->domDocument = $this->createDomDocument($html);
@@ -138,14 +158,16 @@ private function getDomDocument(string $html) : \DOMDocument
138158
*
139159
* @param string $html
140160
*
141-
* @return \DOMDocument
161+
* @return DOMDocument
142162
*/
143-
private function createDomDocument(string $html) : \DOMDocument
163+
private function createDomDocument(string $html) : DOMDocument
144164
{
145-
$domDocument = new \DOMDocument('1.0', 'UTF-8');
165+
$html = $this->maskScriptTags($html);
166+
167+
$domDocument = new DOMDocument('1.0', 'UTF-8');
146168
set_error_handler(
147169
function ($errorNumber, $errorString) {
148-
throw new \DOMException($errorString, $errorNumber);
170+
throw new DOMException($errorString, $errorNumber);
149171
}
150172
);
151173
$string = mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8');
@@ -155,7 +177,7 @@ function ($errorNumber, $errorString) {
155177
'<html><body>' . $string . '</body></html>'
156178
);
157179
libxml_clear_errors();
158-
} catch (\Exception $e) {
180+
} catch (Exception $e) {
159181
restore_error_handler();
160182
$this->logger->critical($e);
161183
}
@@ -167,16 +189,16 @@ function ($errorNumber, $errorString) {
167189
/**
168190
* Convert encoded HTML content types to placeholders and generate decoded outer html map for future replacement
169191
*
170-
* @param \DOMDocument $document
192+
* @param DOMDocument $document
171193
* @return array
172-
* @throws \Magento\Framework\Exception\LocalizedException
194+
* @throws LocalizedException
173195
*/
174-
private function generateDecodedHtmlPlaceholderMappingInDocument(\DOMDocument $document): array
196+
private function generateDecodedHtmlPlaceholderMappingInDocument(DOMDocument $document): array
175197
{
176-
$xpath = new \DOMXPath($document);
198+
$xpath = new DOMXPath($document);
177199

178200
// construct xpath query to fetch top-level ancestor html content type nodes
179-
/** @var $htmlContentTypeNodes \DOMNode[] */
201+
/** @var $htmlContentTypeNodes DOMNode[] */
180202
$htmlContentTypeNodes = $xpath->query(
181203
'//*[@data-content-type="html" and not(@data-decoded="true")]' .
182204
'[not(ancestor::*[@data-content-type="html"])]'
@@ -221,7 +243,7 @@ private function generateDecodedHtmlPlaceholderMappingInDocument(\DOMDocument $d
221243
// by the dom library
222244
$uniqueNodeName = $this->mathRandom->getRandomString(32, $this->mathRandom::CHARS_LOWERS);
223245

224-
$uniqueNode = new \DOMElement($uniqueNodeName);
246+
$uniqueNode = new DOMElement($uniqueNodeName);
225247
$htmlContentTypeNode->parentNode->replaceChild($uniqueNode, $htmlContentTypeNode);
226248

227249
$uniqueNodeNameToDecodedOuterHtmlMap[$uniqueNodeName] = $decodedOuterHtml;
@@ -233,14 +255,14 @@ private function generateDecodedHtmlPlaceholderMappingInDocument(\DOMDocument $d
233255
/**
234256
* Generate the CSS for any background images on the page
235257
*
236-
* @param \DOMDocument $document
258+
* @param DOMDocument $document
237259
*/
238-
private function generateBackgroundImageStyles(\DOMDocument $document) : void
260+
private function generateBackgroundImageStyles(DOMDocument $document) : void
239261
{
240-
$xpath = new \DOMXPath($document);
262+
$xpath = new DOMXPath($document);
241263
$nodes = $xpath->query('//*[@data-background-images]');
242264
foreach ($nodes as $node) {
243-
/* @var \DOMElement $node */
265+
/* @var DOMElement $node */
244266
$backgroundImages = $node->attributes->getNamedItem('data-background-images');
245267
if ($backgroundImages->nodeValue !== '') {
246268
$elementClass = uniqid('background-image-');
@@ -337,4 +359,47 @@ private function getMediaQuery(string $view) : ?string
337359
}
338360
return null;
339361
}
362+
363+
/**
364+
* Masks "x-magento-template" script tags in html content before loading it into DOM parser
365+
*
366+
* DOMDocument::loadHTML() will remove any closing tag inside script tag and will result in broken html template
367+
*
368+
* @param string $content
369+
* @return string
370+
* @see https://bugs.php.net/bug.php?id=52012
371+
*/
372+
private function maskScriptTags(string $content): string
373+
{
374+
$tag = 'script';
375+
$content = preg_replace_callback(
376+
sprintf('#<%1$s[^>]*type="text/x-magento-template\"[^>]*>.*?</%1$s>#is', $tag),
377+
function ($matches) {
378+
$key = $this->mathRandom->getRandomString(32, $this->mathRandom::CHARS_LOWERS);
379+
$this->scripts[$key] = $matches[0];
380+
return '<' . $key . '>' . '</' . $key . '>';
381+
},
382+
$content
383+
);
384+
return $content;
385+
}
386+
387+
/**
388+
* Replaces masked "x-magento-template" script tags with their corresponding content
389+
*
390+
* @param string $content
391+
* @return string
392+
* @see maskScriptTags()
393+
*/
394+
private function unmaskScriptTags(string $content): string
395+
{
396+
foreach ($this->scripts as $key => $script) {
397+
$content = str_replace(
398+
'<' . $key . '>' . '</' . $key . '>',
399+
$script,
400+
$content
401+
);
402+
}
403+
return $content;
404+
}
340405
}

app/code/Magento/PageBuilder/Test/Mftf/ActionGroup/ContentTypeProductsActionGroup.xml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -334,9 +334,9 @@
334334
<argument name="productGroupIndex" defaultValue="1" type="string"/>
335335
<argument name="navigationDotIndex" defaultValue="2" type="string"/>
336336
</arguments>
337-
<waitForElementVisible selector="{{page.carouselNavigationDot(productGroupIndex, navigationDotIndex)}}" stepKey="waitForNavigationDot"/>
338-
<click selector="{{page.carouselNavigationDot(productGroupIndex, navigationDotIndex)}}" stepKey="clickNavigationDot"/>
339-
<waitForPageLoad stepKey="waitForCarouselNavigation"/>
337+
<waitForElementVisible selector="{{page.carouselNavigationDotButton(productGroupIndex, navigationDotIndex)}}" stepKey="waitForNavigationDot"/>
338+
<click selector="{{page.carouselNavigationDotButton(productGroupIndex, navigationDotIndex)}}" stepKey="clickNavigationDot"/>
339+
<waitForElementVisible selector="{{page.carouselActiveProductNavigationDotByIndex(productGroupIndex, navigationDotIndex)}}" stepKey="waitForCarouselNavigation"/>
340340
<seeElement selector="{{page.carouselActiveProductNavigationDotByIndex(productGroupIndex, navigationDotIndex)}}" stepKey="seeClickedNavigationDotIsActive"/>
341341
</actionGroup>
342342
<actionGroup name="clickProductCarouselPreviousNavigationArrow">

app/code/Magento/PageBuilder/Test/Mftf/Section/PageBuilderProductsSection.xml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,8 @@
7272
<element name="showDots" type="button" parameterized="true" selector="(//div[contains(@class,'pagebuilder-products')]//div[contains(@data-appearance,'carousel')])[{{indexOfProductGroup}}][@data-show-dots='{{showDots}}']"/>
7373
<element name="carouselNavigationDotsArea" type="button" parameterized="true" selector="(//div[contains(@class,'pagebuilder-products')]//div[contains(@data-appearance,'carousel')])[{{indexOfProductGroup}}]//*[contains(@class,'slick-dots')]"/>
7474
<element name="carouselNavigationDots" type="button" parameterized="true" selector="(//div[contains(@class,'pagebuilder-products')]//div[contains(@data-appearance,'carousel')])[{{indexOfProductGroup}}]//*[contains(@class,'slick-dots')]//li"/>
75-
<element name="carouselNavigationDot" type="button" parameterized="true" selector="((//div[contains(@class,'pagebuilder-products')]//div[contains(@data-appearance,'carousel')])[{{indexOfProductGroup}}]//*[contains(@class,'slick-dots')]//li)[{{indexOfNavigationDot}}]"/>
75+
<element name="carouselNavigationDot" type="button" parameterized="true" selector="((//div[contains(@class,'pagebuilder-products')]//div[contains(@data-appearance,'carousel')])[{{indexOfProductGroup}}]//*[contains(@class,'slick-dots')]//li)[{{indexOfNavigationDot}}]" deprecated="New element was introduced. Please use 'ProductsCarouselOnStage.carouselNavigationDotButton'"/>
76+
<element name="carouselNavigationDotButton" type="button" parameterized="true" selector="((//div[contains(@class,'pagebuilder-products')]//div[contains(@data-appearance,'carousel')])[{{indexOfProductGroup}}]//*[contains(@class,'slick-dots')]//li)[{{indexOfNavigationDot}}]//button"/>
7677
<element name="carouselActiveProductNavigationDot" type="button" parameterized="true" selector="(//div[contains(@class,'pagebuilder-products')]//div[contains(@data-appearance,'carousel')])[{{indexOfProductGroup}}]//*[contains(@class,'slick-dots')]//*[contains(@class,'slick-active')]"/>
7778
<element name="carouselActiveProductNavigationDotByIndex" type="button" parameterized="true" selector="((//div[contains(@class,'pagebuilder-products')]//div[contains(@data-appearance,'carousel')])[{{indexOfProductGroup}}]//*[contains(@class,'slick-dots')]//li)[{{indexOfNavigationDot}}][contains(@class,'slick-active')]"/>
7879
<element name="carouselPreviousProductNavigationArrow" type="button" parameterized="true" selector="(//div[contains(@class,'pagebuilder-products')]//div[contains(@data-appearance,'carousel')])[{{indexOfProductGroup}}]//button[contains(@class,'slick-arrow') and contains(@class,'slick-prev')]"/>
@@ -148,7 +149,8 @@
148149
<element name="showDots" type="button" parameterized="true" selector="(//div[contains(@data-content-type,'products')])[{{indexOfProductGroup}}][@data-show-dots='{{showDots}}']"/>
149150
<element name="carouselNavigationDotsArea" type="button" parameterized="true" selector="(//div[contains(@data-content-type,'products')])[{{indexOfProductGroup}}]//*[contains(@class,'slick-dots')]"/>
150151
<element name="carouselNavigationDots" type="button" parameterized="true" selector="(//div[contains(@data-content-type,'products')])[{{indexOfProductGroup}}]//*[contains(@class,'slick-dots')]//button"/>
151-
<element name="carouselNavigationDot" type="button" parameterized="true" selector="((//div[contains(@data-content-type,'products')])[{{indexOfProductGroup}}]//*[contains(@class,'slick-dots')]//button)[{{indexOfNavigationDot}}]"/>
152+
<element name="carouselNavigationDot" type="button" parameterized="true" selector="((//div[contains(@data-content-type,'products')])[{{indexOfProductGroup}}]//*[contains(@class,'slick-dots')]//button)[{{indexOfNavigationDot}}]" deprecated="New element was introduced. Please use 'ProductsOnStorefront.carouselNavigationDotButton'"/>
153+
<element name="carouselNavigationDotButton" type="button" parameterized="true" selector="((//div[contains(@data-content-type,'products')])[{{indexOfProductGroup}}]//*[contains(@class,'slick-dots')]//button)[{{indexOfNavigationDot}}]"/>
152154
<element name="carouselActiveProductNavigationDot" type="button" parameterized="true" selector="(//div[contains(@data-content-type,'products')])[{{indexOfProductGroup}}]//*[contains(@class,'slick-dots')]//*[contains(@class,'slick-active')]"/>
153155
<element name="carouselActiveProductNavigationDotByIndex" type="button" parameterized="true" selector="((//div[contains(@data-content-type,'products')])[{{indexOfProductGroup}}]//*[contains(@class,'slick-dots')]//li)[{{indexOfNavigationDot}}][contains(@class,'slick-active')]"/>
154156
<element name="carouselPreviousProductNavigationArrow" type="button" parameterized="true" selector="(//div[contains(@data-content-type,'products')])[{{indexOfProductGroup}}]//button[contains(@class,'slick-arrow') and contains(@class,'slick-prev')]"/>

0 commit comments

Comments
 (0)