123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453 |
- <?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
- */
- class DomMerger implements DomMergerInterface
- {
- /**
- * Format of items in errors array to be used by default. Available placeholders - fields of \LibXMLError.
- */
- 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 = [];
- /**
- * Context XPath
- *
- * @var array
- */
- protected $contextXPath = [];
- /**
- * Is merge simple XML Element
- *
- * @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)
- */
- 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;
- }
- }
- }
- /**
- * 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);
- }
- }
|