Magento 2 Documentation  2.3
Documentation for Magento 2 CMS v2.3 (December 2018)
Dom.php
Go to the documentation of this file.
1 <?php
10 namespace Magento\Framework\Config;
11 
15 
23 class Dom
24 {
28  const ROOT_NAMESPACE_PREFIX = 'x';
29 
33  const ERROR_FORMAT_DEFAULT = "%message%\nLine: %line%\n";
34 
38  private $validationState;
39 
45  protected $dom;
46 
50  protected $nodeMergingConfig;
51 
57  protected $typeAttributeName;
58 
64  protected $schema;
65 
71  protected $errorFormat;
72 
78  protected $rootNamespace;
79 
83  private static $urnResolver;
84 
89  private static $resolvedSchemaPaths = [];
90 
104  public function __construct(
105  $xml,
106  \Magento\Framework\Config\ValidationStateInterface $validationState,
107  array $idAttributes = [],
108  $typeAttributeName = null,
109  $schemaFile = null,
110  $errorFormat = self::ERROR_FORMAT_DEFAULT
111  ) {
112  $this->validationState = $validationState;
113  $this->schema = $schemaFile;
114  $this->nodeMergingConfig = new Dom\NodeMergingConfig(new Dom\NodePathMatcher(), $idAttributes);
115  $this->typeAttributeName = $typeAttributeName;
116  $this->errorFormat = $errorFormat;
117  $this->dom = $this->_initDom($xml);
118  $this->rootNamespace = $this->dom->lookupNamespaceUri($this->dom->namespaceURI);
119  }
120 
128  private static function getXmlErrors($errorFormat)
129  {
130  $errors = [];
131  $validationErrors = libxml_get_errors();
132  if (count($validationErrors)) {
133  foreach ($validationErrors as $error) {
134  $errors[] = self::_renderErrorMessage($error, $errorFormat);
135  }
136  } else {
137  $errors[] = 'Unknown validation error';
138  }
139  return $errors;
140  }
141 
148  public function merge($xml)
149  {
150  $dom = $this->_initDom($xml);
151  $this->_mergeNode($dom->documentElement, '');
152  }
153 
167  protected function _mergeNode(\DOMElement $node, $parentPath)
168  {
169  $path = $this->_getNodePathByParent($node, $parentPath);
170 
171  $matchedNode = $this->_getMatchedNode($path);
172 
173  /* Update matched node attributes and value */
174  if ($matchedNode) {
175  //different node type
176  if ($this->typeAttributeName &&
177  $node->hasAttribute($this->typeAttributeName) &&
178  $matchedNode->hasAttribute($this->typeAttributeName) &&
179  $node->getAttribute($this->typeAttributeName) !== $matchedNode->getAttribute($this->typeAttributeName)
180  ) {
181  $parentMatchedNode = $this->_getMatchedNode($parentPath);
182  $newNode = $this->dom->importNode($node, true);
183  $parentMatchedNode->replaceChild($newNode, $matchedNode);
184  return;
185  }
186 
187  $this->_mergeAttributes($matchedNode, $node);
188  if (!$node->hasChildNodes()) {
189  return;
190  }
191  /* override node value */
192  if ($this->_isTextNode($node)) {
193  /* skip the case when the matched node has children, otherwise they get overridden */
194  if (!$matchedNode->hasChildNodes() || $this->_isTextNode($matchedNode)) {
195  $matchedNode->nodeValue = $node->childNodes->item(0)->nodeValue;
196  }
197  } else {
198  /* recursive merge for all child nodes */
199  foreach ($node->childNodes as $childNode) {
200  if ($childNode instanceof \DOMElement) {
201  $this->_mergeNode($childNode, $path);
202  }
203  }
204  }
205  } else {
206  /* Add node as is to the document under the same parent element */
207  $parentMatchedNode = $this->_getMatchedNode($parentPath);
208  $newNode = $this->dom->importNode($node, true);
209  $parentMatchedNode->appendChild($newNode);
210  }
211  }
212 
219  protected function _isTextNode($node)
220  {
221  return $node->childNodes->length == 1 && $node->childNodes->item(0) instanceof \DOMText;
222  }
223 
231  protected function _mergeAttributes($baseNode, $mergeNode)
232  {
233  foreach ($mergeNode->attributes as $attribute) {
234  $baseNode->setAttribute($this->_getAttributeName($attribute), $attribute->value);
235  }
236  }
237 
245  protected function _getNodePathByParent(\DOMElement $node, $parentPath)
246  {
247  $prefix = $this->rootNamespace === null ? '' : self::ROOT_NAMESPACE_PREFIX . ':';
248  $path = $parentPath . '/' . $prefix . $node->tagName;
249  $idAttribute = $this->nodeMergingConfig->getIdAttribute($path);
250  if (is_array($idAttribute)) {
251  $constraints = [];
252  foreach ($idAttribute as $attribute) {
253  $value = $node->getAttribute($attribute);
254  $constraints[] = "@{$attribute}='{$value}'";
255  }
256  $path .= '[' . implode(' and ', $constraints) . ']';
257  } elseif ($idAttribute && ($value = $node->getAttribute($idAttribute))) {
258  $path .= "[@{$idAttribute}='{$value}']";
259  }
260  return $path;
261  }
262 
271  protected function _getMatchedNode($nodePath)
272  {
273  $xPath = new \DOMXPath($this->dom);
274  if ($this->rootNamespace) {
275  $xPath->registerNamespace(self::ROOT_NAMESPACE_PREFIX, $this->rootNamespace);
276  }
277  $matchedNodes = $xPath->query($nodePath);
278  $node = null;
279  if ($matchedNodes->length > 1) {
280  throw new \Magento\Framework\Exception\LocalizedException(
281  new \Magento\Framework\Phrase(
282  "More than one node matching the query: %1, Xml is: %2",
283  [$nodePath, $this->dom->saveXML()]
284  )
285  );
286  } elseif ($matchedNodes->length == 1) {
287  $node = $matchedNodes->item(0);
288  }
289  return $node;
290  }
291 
301  public static function validateDomDocument(
302  \DOMDocument $dom,
303  $schema,
304  $errorFormat = self::ERROR_FORMAT_DEFAULT
305  ) {
306  if (!function_exists('libxml_set_external_entity_loader')) {
307  return [];
308  }
309 
310  if (!self::$urnResolver) {
311  self::$urnResolver = new UrnResolver();
312  }
313  if (!isset(self::$resolvedSchemaPaths[$schema])) {
314  self::$resolvedSchemaPaths[$schema] = self::$urnResolver->getRealPath($schema);
315  }
316  $schema = self::$resolvedSchemaPaths[$schema];
317 
318  libxml_use_internal_errors(true);
319  libxml_set_external_entity_loader([self::$urnResolver, 'registerEntityLoader']);
320  $errors = [];
321  try {
322  $result = $dom->schemaValidate($schema);
323  if (!$result) {
324  $errors = self::getXmlErrors($errorFormat);
325  }
326  } catch (\Exception $exception) {
327  $errors = self::getXmlErrors($errorFormat);
328  libxml_use_internal_errors(false);
329  array_unshift($errors, new Phrase('Processed schema file: %1', [$schema]));
330  throw new ValidationSchemaException(new Phrase(implode("\n", $errors)));
331  }
332  libxml_set_external_entity_loader(null);
333  libxml_use_internal_errors(false);
334  return $errors;
335  }
336 
345  private static function _renderErrorMessage(\LibXMLError $errorInfo, $format)
346  {
347  $result = $format;
348  foreach ($errorInfo as $field => $value) {
349  $placeholder = '%' . $field . '%';
350  $value = trim((string)$value);
351  $result = str_replace($placeholder, $value, $result);
352  }
353  if (strpos($result, '%') !== false) {
354  if (preg_match_all('/%.+%/', $result, $matches)) {
355  $unsupported = [];
356  foreach ($matches[0] as $placeholder) {
357  if (strpos($result, $placeholder) !== false) {
358  $unsupported[] = $placeholder;
359  }
360  }
361  if (!empty($unsupported)) {
362  throw new \InvalidArgumentException(
363  "Error format '{$format}' contains unsupported placeholders: " . implode(', ', $unsupported)
364  );
365  }
366  }
367  }
368  return $result;
369  }
370 
376  public function getDom()
377  {
378  return $this->dom;
379  }
380 
388  protected function _initDom($xml)
389  {
390  $dom = new \DOMDocument();
391  $useErrors = libxml_use_internal_errors(true);
392  $res = $dom->loadXML($xml);
393  if (!$res) {
394  $errors = self::getXmlErrors($this->errorFormat);
395  libxml_use_internal_errors($useErrors);
396  throw new \Magento\Framework\Config\Dom\ValidationException(implode("\n", $errors));
397  }
398  libxml_use_internal_errors($useErrors);
399  if ($this->validationState->isValidationRequired() && $this->schema) {
400  $errors = $this->validateDomDocument($dom, $this->schema, $this->errorFormat);
401  if (count($errors)) {
402  throw new \Magento\Framework\Config\Dom\ValidationException(implode("\n", $errors));
403  }
404  }
405  return $dom;
406  }
407 
415  public function validate($schemaFileName, &$errors = [])
416  {
417  if ($this->validationState->isValidationRequired()) {
418  $errors = $this->validateDomDocument($this->dom, $schemaFileName, $this->errorFormat);
419  return !count($errors);
420  }
421  return true;
422  }
423 
430  public function setSchemaFile($schemaFile)
431  {
432  $this->schema = $schemaFile;
433  return $this;
434  }
435 
442  private function _getAttributeName($attribute)
443  {
444  if ($attribute->prefix !== null && !empty($attribute->prefix)) {
445  $attributeName = $attribute->prefix . ':' . $attribute->name;
446  } else {
447  $attributeName = $attribute->name;
448  }
449  return $attributeName;
450  }
451 }
validate($schemaFileName, &$errors=[])
Definition: Dom.php:415
_getNodePathByParent(\DOMElement $node, $parentPath)
Definition: Dom.php:245
elseif(isset( $params[ 'redirect_parent']))
Definition: iframe.phtml:17
static validateDomDocument(\DOMDocument $dom, $schema, $errorFormat=self::ERROR_FORMAT_DEFAULT)
Definition: Dom.php:301
__construct( $xml, \Magento\Framework\Config\ValidationStateInterface $validationState, array $idAttributes=[], $typeAttributeName=null, $schemaFile=null, $errorFormat=self::ERROR_FORMAT_DEFAULT)
Definition: Dom.php:104
$prefix
Definition: name.phtml:25
$value
Definition: gender.phtml:16
$format
Definition: list.phtml:12
_mergeNode(\DOMElement $node, $parentPath)
Definition: Dom.php:167
_mergeAttributes($baseNode, $mergeNode)
Definition: Dom.php:231
_getMatchedNode($nodePath)
Definition: Dom.php:271
$errors
Definition: overview.phtml:9
setSchemaFile($schemaFile)
Definition: Dom.php:430