| Current Path : /home/rtorresani/www/vendor/magento/framework/View/Element/UiComponent/Config/ |
| Current File : //home/rtorresani/www/vendor/magento/framework/View/Element/UiComponent/Config/DomMerger.php |
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
namespace Magento\Framework\View\Element\UiComponent\Config;
use Magento\Framework\Config\Dom;
use Magento\Framework\Config\ValidationStateInterface;
class DomMerger implements DomMergerInterface
{
/**
* Format of items in errors array to be used by default. Available placeholders - fields of \LibXMLError.
*/
public const ERROR_FORMAT_DEFAULT = "Message: %message%\nLine: %line%\n";
/**
* @var \Magento\Framework\Config\ValidationStateInterface
*/
private $validationState;
/**
* Location schema file
*
* @var string
*/
protected $schemaFilePath;
/**
* Result DOM document
*
* @var \DOMDocument
*/
protected $domDocument;
/**
* Id attribute list
*
* @var array
*/
protected $idAttributes = [];
/**
* @var array
*/
protected $contextXPath = [];
/**
* @var bool
*/
protected $isMergeSimpleXMLElement;
/**
* @var string
*/
private $schema;
/**
* Build DOM with initial XML contents and specifying identifier attributes for merging
*
* Format of $schema: Absolute schema file path or URN
* Format of $idAttributes: array('name', 'id')
* Format of $contextXPath: array('/config/ui')
* The path to ID attribute name should not include any attribute notations or modifiers -- only node names
*
* @param ValidationStateInterface $validationState
* @param string $schema
* @param bool $isMergeSimpleXMLElement
* @param array $contextXPath
* @param array $idAttributes
*/
public function __construct(
ValidationStateInterface $validationState,
$schema,
$isMergeSimpleXMLElement = false,
array $contextXPath = [],
array $idAttributes = []
) {
$this->validationState = $validationState;
$this->schema = $schema;
$this->isMergeSimpleXMLElement = $isMergeSimpleXMLElement;
$this->contextXPath = $contextXPath;
$this->idAttributes = $idAttributes;
}
/**
* Is id attribute
*
* @param string $attributeName
* @return bool
*/
protected function isIdAttribute($attributeName)
{
return in_array($attributeName, $this->idAttributes);
}
/**
* Is merge context
*
* @param string $xPath
* @return bool
*/
protected function isMergeContext($xPath)
{
foreach ($this->contextXPath as $context) {
if (strpos($xPath, $context) === 0) {
return true;
}
}
return false;
}
/**
* Is context XPath
*
* @param array $xPath
* @return bool
*/
protected function isContextXPath(array $xPath)
{
return count(array_intersect($xPath, $this->contextXPath)) === count($xPath);
}
/**
* Merges attributes of the merge node to the base node
*
* @param \DOMElement $baseNode
* @param \DOMNode $mergeNode
* @return void
*/
protected function mergeAttributes(\DOMElement $baseNode, \DOMNode $mergeNode)
{
foreach ($mergeNode->attributes as $name => $attribute) {
$baseNode->setAttribute($name, $attribute->value);
}
}
/**
* Create XPath
*
* @param \DOMNode $node
* @return string
*/
protected function createXPath(\DOMNode $node)
{
$parentXPath = '';
$currentXPath = $node->getNodePath() ?? '';
if ($node->parentNode !== null && !$node->isSameNode($node->parentNode)) {
$parentXPath = $this->createXPath($node->parentNode);
$pathParts = explode('/', $currentXPath);
$currentXPath = '/' . end($pathParts);
}
$attributesXPath = '';
if ($node->hasAttributes()) {
$attributes = [];
foreach ($node->attributes as $name => $attribute) {
if ($this->isIdAttribute($name)) {
$attributes[] = sprintf('@%s="%s"', $name, $attribute->value);
break;
}
}
if (!empty($attributes)) {
if (substr($currentXPath, -1) === ']') {
$currentXPath = substr($currentXPath, 0, strrpos($currentXPath, '['));
}
$attributesXPath = '[' . implode(' and ', $attributes) . ']';
}
}
return '/' . trim($parentXPath . $currentXPath . $attributesXPath, '/');
}
/**
* Merge nested xml nodes
*
* @param \DOMXPath $rootDomXPath
* @param \DOMNodeList $insertedNodes
* @param \DOMNode $contextNode
* @return void
*
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
* @SuppressWarnings(PHPMD.NPathComplexity)
* phpcs:disable Generic.Metrics.NestingLevel
*/
protected function nestedMerge(\DOMXPath $rootDomXPath, \DOMNodeList $insertedNodes, \DOMNode $contextNode)
{
for ($i = 0, $iLength = $insertedNodes->length; $i < $iLength; ++$i) {
$insertedItem = $insertedNodes->item($i);
switch ($insertedItem->nodeType) {
case XML_TEXT_NODE:
case XML_COMMENT_NODE:
case XML_CDATA_SECTION_NODE:
if (trim($insertedItem->textContent) !== '') {
$this->insertBefore($contextNode, $insertedItem);
}
break;
default:
$insertedXPath = $this->createXPath($insertedItem);
$rootMatchList = $rootDomXPath->query($insertedXPath, $contextNode);
$jLength = $rootMatchList->length;
if ($jLength > 0) {
for ($j = 0; $j < $jLength; ++$j) {
$rootItem = $rootMatchList->item($j);
$rootItemXPath = $this->createXPath($rootItem);
if ($this->isMergeContext($insertedXPath)) {
if ($this->isTextNode($insertedItem) && $this->isTextNode($rootItem)) {
$rootItem->nodeValue = $insertedItem->nodeValue;
} elseif (!$this->isContextXPath([$rootItemXPath, $insertedXPath])
&& !$this->hasIdAttribute($rootItem)
&& !$this->hasIdAttribute($insertedItem)
) {
if ($this->isMergeSimpleXMLElement) {
$this->nestedMerge($rootDomXPath, $insertedItem->childNodes, $rootItem);
$this->mergeAttributes($rootItem, $insertedItem);
} else {
$this->appendChild($contextNode, $insertedItem);
}
} else {
$this->nestedMerge($rootDomXPath, $insertedItem->childNodes, $rootItem);
$this->mergeAttributes($rootItem, $insertedItem);
}
} else {
$this->appendChild($contextNode, $insertedItem);
}
}
} else {
$this->appendChild($contextNode, $insertedItem);
}
break;
}
}
}
// phpcs:enable Generic.Metrics.NestingLevel
/**
* Append child node
*
* @param \DOMNode $parentNode
* @param \DOMNode $childNode
* @return void
*/
protected function appendChild(\DOMNode $parentNode, \DOMNode $childNode)
{
$importNode = $this->getDom()->importNode($childNode, true);
$parentNode->appendChild($importNode);
}
/**
* Insert before
*
* @param \DOMNode $parentNode
* @param \DOMNode $childNode
* @return void
*/
protected function insertBefore(\DOMNode $parentNode, \DOMNode $childNode)
{
$importNode = $this->getDom()->importNode($childNode, true);
$parentNode->insertBefore($importNode);
}
/**
* Check if the node content is text
*
* @param \DOMNode $node
* @return bool
*/
protected function isTextNode(\DOMNode $node)
{
return $node->childNodes->length == 1 && $node->childNodes->item(0) instanceof \DOMText;
}
/**
* Has ID attribute
*
* @param \DOMNode $node
* @return bool
* @SuppressWarnings(PHPMD.UnusedLocalVariable)
*/
protected function hasIdAttribute(\DOMNode $node)
{
if (!$node->hasAttributes()) {
return false;
}
foreach ($node->attributes as $name => $attribute) {
if (in_array($name, $this->idAttributes)) {
return true;
}
}
return false;
}
/**
* Recursive merging of the \DOMElement into the original document
*
* Algorithm:
* 1. Find the same node in original document
* 2. Extend and override original document node attributes and scalar value if found
* 3. Append new node if original document doesn't have the same node
*
* @param \DOMElement $node
* @throws \Magento\Framework\Exception\LocalizedException
* @return void
*/
public function mergeNode(\DOMElement $node)
{
$parentDoom = $this->getDom();
$this->nestedMerge(new \DOMXPath($parentDoom), $node->childNodes, $parentDoom->documentElement);
}
/**
* Create DOM document based on $xml parameter
*
* @param string $xml
* @return \DOMDocument
* @throws \Magento\Framework\Exception\LocalizedException
*/
public function createDomDocument($xml)
{
$domDocument = new \DOMDocument();
$domDocument->loadXML($xml);
if ($this->validationState->isValidationRequired() && $this->schema) {
$errors = $this->validateDomDocument($domDocument);
if (count($errors)) {
throw new \Magento\Framework\Exception\LocalizedException(
new \Magento\Framework\Phrase(implode("\n", $errors))
);
}
}
return $domDocument;
}
/**
* Validate dom document
*
* @param \DOMDocument $domDocument
* @param string|null $schema
* @return array of errors
* @throws \Exception
*/
protected function validateDomDocument(\DOMDocument $domDocument, $schema = null)
{
$schema = $schema !== null ? $schema : $this->schema;
libxml_use_internal_errors(true);
try {
$errors = \Magento\Framework\Config\Dom::validateDomDocument($domDocument, $schema);
} catch (\Exception $exception) {
libxml_use_internal_errors(false);
throw $exception;
}
libxml_use_internal_errors(false);
return $errors;
}
/**
* Render error message string by replacing placeholders '%field%' with properties of \LibXMLError
*
* @param \LibXMLError $errorInfo
* @return string
* @throws \Magento\Framework\Exception\LocalizedException
*/
protected function renderErrorMessage(\LibXMLError $errorInfo)
{
$result = static::ERROR_FORMAT_DEFAULT;
foreach ($errorInfo as $field => $value) {
$result = str_replace('%' . $field . '%', trim((string)$value), $result);
}
if (strpos($result, '%') !== false) {
throw new \Magento\Framework\Exception\LocalizedException(
new \Magento\Framework\Phrase(
'Error format "' . static::ERROR_FORMAT_DEFAULT . '" contains unsupported placeholders.'
)
);
}
return $result;
}
/**
* Merge string $xml into DOM document
*
* @param string $xml
* @return void
*/
public function merge($xml)
{
if (!isset($this->domDocument)) {
$this->domDocument = $this->createDomDocument($xml);
} else {
$this->mergeNode($this->createDomDocument($xml)->documentElement);
}
}
/**
* Get DOM document
*
* @return \DOMDocument
* @throws \Magento\Framework\Exception\LocalizedException
*/
public function getDom()
{
if (!isset($this->domDocument)) {
throw new \Magento\Framework\Exception\LocalizedException(
new \Magento\Framework\Phrase('Object DOMDocument should be created.')
);
}
return $this->domDocument;
}
/**
* Set DOM document
*
* @param \DOMDocument $domDocument
* @return void
*/
public function setDom(\DOMDocument $domDocument)
{
$this->domDocument = $domDocument;
}
/**
* Unset DOM document
*
* @return void
*/
public function unsetDom()
{
unset($this->domDocument);
}
/**
* Validate self contents towards to specified schema
*
* @param string|null $schemaFilePath
* @return array
*/
public function validate($schemaFilePath = null)
{
if (!$this->validationState->isValidationRequired()) {
return [];
}
return $this->validateDomDocument($this->getDom(), $schemaFilePath);
}
}