Skip to content

Commit 6806aec

Browse files
author
Oleksii Korshenko
authored
Merge pull request #216 from magento-tango/MAGETWO-52625
[Tango] MAGETWO-52625: Fail fast for layout XML errors
2 parents 644e60d + 88118f8 commit 6806aec

File tree

9 files changed

+323
-88
lines changed

9 files changed

+323
-88
lines changed

dev/tests/integration/testsuite/Magento/Framework/View/Layout/MergeTest.php

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
*/
66
namespace Magento\Framework\View\Layout;
77

8+
use Magento\Framework\App\State;
89
use Magento\Framework\Phrase;
910

1011
/**
@@ -396,17 +397,23 @@ public function testLoadWithInvalidArgumentThrowsException()
396397

397398
/**
398399
* Test loading invalid layout
400+
*
401+
* @expectedException \Exception
402+
* @expectedExceptionMessage Layout is invalid.
399403
*/
400404
public function testLoadWithInvalidLayout()
401405
{
402406
$this->_model->addPageHandles(['default']);
403407

404-
$this->_appState->expects($this->any())->method('getMode')->will($this->returnValue('developer'));
408+
$this->_appState->expects($this->once())->method('getMode')->willReturn(State::MODE_DEVELOPER);
405409

406-
$this->_layoutValidator->expects($this->any())->method('getMessages')
407-
->will($this->returnValue(['testMessage1', 'testMessage2']));
410+
$this->_layoutValidator->expects($this->any())
411+
->method('getMessages')
412+
->willReturn(['testMessage1', 'testMessage2']);
408413

409-
$this->_layoutValidator->expects($this->any())->method('isValid')->will($this->returnValue(false));
414+
$this->_layoutValidator->expects($this->any())
415+
->method('isValid')
416+
->willThrowException(new \Exception('Layout is invalid.'));
410417

411418
$suffix = md5(implode('|', $this->_model->getHandles()));
412419
$cacheId = "LAYOUT_{$this->_theme->getArea()}_STORE{$this->scope->getId()}_{$this->_theme->getId()}{$suffix}";
@@ -420,4 +427,15 @@ public function testLoadWithInvalidLayout()
420427

421428
$this->_model->load();
422429
}
430+
431+
/**
432+
* @expectedException \Magento\Framework\Config\Dom\ValidationException
433+
* @expectedExceptionMessageRegExp /_mergeFiles\/layout\/file_wrong\.xml\' is not valid/
434+
*/
435+
public function testLayoutUpdateFileIsNotValid()
436+
{
437+
$this->_appState->expects($this->once())->method('getMode')->willReturn(State::MODE_DEVELOPER);
438+
439+
$this->_model->addPageHandles(['default']);
440+
}
423441
}

lib/internal/Magento/Framework/Config/Dom.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
namespace Magento\Framework\Config;
1313

1414
use Magento\Framework\Config\Dom\UrnResolver;
15+
use Magento\Framework\Config\Dom\ValidationSchemaException;
16+
use Magento\Framework\Phrase;
1517

1618
/**
1719
* Class Dom
@@ -313,8 +315,10 @@ public static function validateDomDocument(
313315
$errors = self::getXmlErrors($errorFormat);
314316
}
315317
} catch (\Exception $exception) {
318+
$errors = self::getXmlErrors($errorFormat);
316319
libxml_use_internal_errors(false);
317-
throw $exception;
320+
array_unshift($errors, new Phrase('Processed schema file: %1', [$schema]));
321+
throw new ValidationSchemaException(new Phrase(implode("\n", $errors)));
318322
}
319323
libxml_set_external_entity_loader(null);
320324
libxml_use_internal_errors(false);
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
/**
3+
* Copyright © 2016 Magento. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
7+
/**
8+
* Exception that should be thrown by DOM model when incoming xsd is not valid.
9+
*/
10+
namespace Magento\Framework\Config\Dom;
11+
12+
use Magento\Framework\Exception\LocalizedException;
13+
14+
class ValidationSchemaException extends LocalizedException
15+
{
16+
}

lib/internal/Magento/Framework/Config/Test/Unit/DomTest.php

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,14 @@
88
class DomTest extends \PHPUnit_Framework_TestCase
99
{
1010
/**
11-
* @var \Magento\Framework\Config\ValidationStateInterface
11+
* @var \Magento\Framework\Config\ValidationStateInterface|\PHPUnit_Framework_MockObject_MockObject
1212
*/
1313
protected $validationStateMock;
1414

1515
protected function setUp()
1616
{
17-
$this->validationStateMock = $this->getMock(
18-
\Magento\Framework\Config\ValidationStateInterface::class,
19-
[],
20-
[],
21-
'',
22-
false
17+
$this->validationStateMock = $this->getMockForAbstractClass(
18+
\Magento\Framework\Config\ValidationStateInterface::class
2319
);
2420
$this->validationStateMock->method('isValidationRequired')
2521
->willReturn(true);
@@ -176,7 +172,29 @@ public function testValidateUnknownError()
176172
$domMock->expects($this->once())
177173
->method('schemaValidate')
178174
->with($schemaFile)
179-
->will($this->returnValue(false));
180-
$this->assertEquals(['Unknown validation error'], $dom->validateDomDocument($domMock, $schemaFile));
175+
->willReturn(false);
176+
$this->assertEquals(
177+
["Element 'unknown_node': This element is not expected. Expected is ( node ).\nLine: 1\n"],
178+
$dom->validateDomDocument($domMock, $schemaFile)
179+
);
180+
}
181+
182+
/**
183+
* @expectedException \Magento\Framework\Config\Dom\ValidationSchemaException
184+
*/
185+
public function testValidateDomDocumentThrowsException()
186+
{
187+
if (!function_exists('libxml_set_external_entity_loader')) {
188+
$this->markTestSkipped('Skipped on HHVM. Will be fixed in MAGETWO-45033');
189+
}
190+
$xml = '<root><node id="id1"/><node id="id2"/></root>';
191+
$schemaFile = __DIR__ . '/_files/sample.xsd';
192+
$dom = new \Magento\Framework\Config\Dom($xml, $this->validationStateMock);
193+
$domMock = $this->getMock(\DOMDocument::class, ['schemaValidate'], []);
194+
$domMock->expects($this->once())
195+
->method('schemaValidate')
196+
->with($schemaFile)
197+
->willThrowException(new \Exception());
198+
$dom->validateDomDocument($domMock, $schemaFile);
181199
}
182200
}

lib/internal/Magento/Framework/View/Layout/etc/layout_merged.xsd

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
-->
88
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
99
<xs:include schemaLocation="urn:magento:framework:View/Layout/etc/elements.xsd"/>
10+
<xs:include schemaLocation="urn:magento:framework:View/Layout/etc/head.xsd"/>
11+
<xs:include schemaLocation="urn:magento:framework:View/Layout/etc/body.xsd"/>
1012

1113
<xs:element name="layout">
1214
<xs:annotation>
@@ -20,29 +22,31 @@
2022
</xs:sequence>
2123
</xs:complexType>
2224
<xs:key name="handleName">
23-
<xs:selector xpath="handle"></xs:selector>
24-
<xs:field xpath="@id"></xs:field>
25+
<xs:selector xpath="handle"/>
26+
<xs:field xpath="@id"/>
2527
</xs:key>
2628
</xs:element>
2729

28-
<xs:element name="handle" type="handleType">
29-
<xs:unique name="blockKey">
30-
<xs:selector xpath=".//block"/>
31-
<xs:field xpath="@name"/>
32-
</xs:unique>
33-
<xs:unique name="containerKey">
34-
<xs:selector xpath=".//container"/>
35-
<xs:field xpath="@name"/>
36-
</xs:unique>
37-
<xs:keyref name="blockReference" refer="blockKey">
38-
<xs:selector xpath=".//referenceBlock"/>
39-
<xs:field xpath="@name"/>
40-
</xs:keyref>
41-
<xs:keyref name="containerReference" refer="containerKey">
42-
<xs:selector xpath=".//referenceContainer"/>
43-
<xs:field xpath="@name"/>
44-
</xs:keyref>
45-
</xs:element>
30+
<xs:element name="handle" type="handleType" />
31+
32+
<xs:complexType name="layoutType">
33+
<xs:annotation>
34+
<xs:documentation>
35+
Layout Type definition
36+
</xs:documentation>
37+
</xs:annotation>
38+
<xs:choice minOccurs="0" maxOccurs="unbounded">
39+
<xs:element ref="referenceContainer" minOccurs="0" maxOccurs="unbounded"/>
40+
<xs:element ref="container" minOccurs="0" maxOccurs="unbounded"/>
41+
<xs:element ref="update" minOccurs="0" maxOccurs="unbounded"/>
42+
<xs:element ref="remove" minOccurs="0" maxOccurs="unbounded"/>
43+
<xs:element ref="move" minOccurs="0" maxOccurs="unbounded"/>
44+
<xs:element ref="block" minOccurs="0" maxOccurs="unbounded"/>
45+
<xs:element ref="referenceBlock" minOccurs="0" maxOccurs="unbounded"/>
46+
<xs:element name="body" type="bodyType" minOccurs="0" maxOccurs="unbounded"/>
47+
<xs:element name="head" type="headType" minOccurs="0" maxOccurs="unbounded"/>
48+
</xs:choice>
49+
</xs:complexType>
4650

4751
<xs:complexType name="handleType">
4852
<xs:annotation>

lib/internal/Magento/Framework/View/Model/Layout/Merge.php

Lines changed: 47 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@
55
*/
66
namespace Magento\Framework\View\Model\Layout;
77

8+
use Magento\Framework\App\State;
89
use Magento\Framework\Filesystem\DriverPool;
910
use Magento\Framework\Filesystem\File\ReadFactory;
1011
use Magento\Framework\View\Model\Layout\Update\Validator;
12+
use Magento\Framework\Config\Dom\ValidationException;
1113

1214
/**
1315
* Layout merge model
@@ -201,7 +203,9 @@ public function __construct(
201203
*/
202204
public function addUpdate($update)
203205
{
204-
$this->updates[] = $update;
206+
if (!in_array($update, $this->updates)) {
207+
$this->updates[] = $update;
208+
}
205209
return $this;
206210
}
207211

@@ -447,20 +451,24 @@ public function load($handles = [])
447451
* @param string $cacheId
448452
* @param string $layout
449453
* @return $this
454+
* @throws \Exception
450455
*/
451456
protected function _validateMergedLayout($cacheId, $layout)
452457
{
453458
$layoutStr = '<handle id="handle">' . $layout . '</handle>';
454459

455-
if ($this->appState->getMode() === \Magento\Framework\App\State::MODE_DEVELOPER) {
456-
if (!$this->layoutValidator->isValid($layoutStr, Validator::LAYOUT_SCHEMA_MERGED, false)) {
457-
$messages = $this->layoutValidator->getMessages();
458-
//Add first message to exception
459-
$message = reset($messages);
460-
$this->logger->info(
461-
'Cache file with merged layout: ' . $cacheId
462-
. ' and handles ' . implode(', ', (array)$this->getHandles()) . ': ' . $message
463-
);
460+
try {
461+
$this->layoutValidator->isValid($layoutStr, Validator::LAYOUT_SCHEMA_MERGED, false);
462+
} catch (\Exception $e) {
463+
$messages = $this->layoutValidator->getMessages();
464+
//Add first message to exception
465+
$message = reset($messages);
466+
$this->logger->info(
467+
'Cache file with merged layout: ' . $cacheId
468+
. ' and handles ' . implode(', ', (array)$this->getHandles()) . ': ' . $message
469+
);
470+
if ($this->appState->getMode() === \Magento\Framework\App\State::MODE_DEVELOPER) {
471+
throw $e;
464472
}
465473
}
466474

@@ -693,7 +701,19 @@ protected function _loadFileLayoutUpdatesXml()
693701
/** @var $fileXml \Magento\Framework\View\Layout\Element */
694702
$fileXml = $this->_loadXmlString($fileStr);
695703
if (!$fileXml instanceof \Magento\Framework\View\Layout\Element) {
696-
$this->_logXmlErrors($file->getFilename(), libxml_get_errors());
704+
$xmlErrors = $this->getXmlErrors(libxml_get_errors());
705+
$this->_logXmlErrors($file->getFilename(), $xmlErrors);
706+
if ($this->appState->getMode() === State::MODE_DEVELOPER) {
707+
throw new ValidationException(
708+
new \Magento\Framework\Phrase(
709+
"Theme layout update file '%1' is not valid.\n%2",
710+
[
711+
$file->getFilename(),
712+
implode("\n", $xmlErrors)
713+
]
714+
)
715+
);
716+
}
697717
libxml_clear_errors();
698718
continue;
699719
}
@@ -721,21 +741,31 @@ protected function _loadFileLayoutUpdatesXml()
721741
* Log xml errors to system log
722742
*
723743
* @param string $fileName
724-
* @param array $libXmlErrors
744+
* @param array $xmlErrors
725745
* @return void
726746
*/
727-
protected function _logXmlErrors($fileName, $libXmlErrors)
747+
protected function _logXmlErrors($fileName, $xmlErrors)
748+
{
749+
$this->logger->info(
750+
sprintf("Theme layout update file '%s' is not valid.\n%s", $fileName, implode("\n", $xmlErrors))
751+
);
752+
}
753+
754+
/**
755+
* Get formatted xml errors
756+
*
757+
* @param array $libXmlErrors
758+
* @return array
759+
*/
760+
private function getXmlErrors($libXmlErrors)
728761
{
729762
$errors = [];
730763
if (count($libXmlErrors)) {
731764
foreach ($libXmlErrors as $error) {
732765
$errors[] = "{$error->message} Line: {$error->line}";
733766
}
734-
735-
$this->logger->info(
736-
sprintf("Theme layout update file '%s' is not valid.\n%s", $fileName, implode("\n", $errors))
737-
);
738767
}
768+
return $errors;
739769
}
740770

741771
/**

lib/internal/Magento/Framework/View/Model/Layout/Update/Validator.php

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
namespace Magento\Framework\View\Model\Layout\Update;
77

88
use Magento\Framework\Config\Dom\UrnResolver;
9+
use Magento\Framework\Config\Dom\ValidationSchemaException;
910

1011
/**
1112
* Validator for custom layout update
@@ -16,6 +17,8 @@ class Validator extends \Zend_Validate_Abstract
1617
{
1718
const XML_INVALID = 'invalidXml';
1819

20+
const XSD_INVALID = 'invalidXsd';
21+
1922
const HELPER_ARGUMENT_TYPE = 'helperArgumentType';
2023

2124
const UPDATER_MODEL = 'updaterModel';
@@ -93,6 +96,9 @@ protected function _initMessageTemplates()
9396
self::XML_INVALID => (string)new \Magento\Framework\Phrase(
9497
'Please correct the XML data and try again. %value%'
9598
),
99+
self::XSD_INVALID => (string)new \Magento\Framework\Phrase(
100+
'Please correct the XSD data and try again. %value%'
101+
),
96102
];
97103
}
98104
return $this;
@@ -101,14 +107,15 @@ protected function _initMessageTemplates()
101107
/**
102108
* Returns true if and only if $value meets the validation requirements
103109
*
104-
* If $value fails validation, then this method returns false, and
110+
* If $value fails validation, then this method throws exception, and
105111
* getMessages() will return an array of messages that explain why the
106112
* validation failed.
107113
*
108114
* @param string $value
109115
* @param string $schema
110116
* @param bool $isSecurityCheck
111117
* @return bool
118+
* @throws \Exception
112119
*/
113120
public function isValid($value, $schema = self::LAYOUT_SCHEMA_PAGE_HANDLE, $isSecurityCheck = true)
114121
{
@@ -132,10 +139,13 @@ public function isValid($value, $schema = self::LAYOUT_SCHEMA_PAGE_HANDLE, $isSe
132139
}
133140
} catch (\Magento\Framework\Config\Dom\ValidationException $e) {
134141
$this->_error(self::XML_INVALID, $e->getMessage());
135-
return false;
142+
throw $e;
143+
} catch (ValidationSchemaException $e) {
144+
$this->_error(self::XSD_INVALID, $e->getMessage());
145+
throw $e;
136146
} catch (\Exception $e) {
137147
$this->_error(self::XML_INVALID);
138-
return false;
148+
throw $e;
139149
}
140150
return true;
141151
}

0 commit comments

Comments
 (0)