Skip to content

Commit 84042a4

Browse files
committed
Use DOMDocument for schema validation. Remove insignificant whitespace when asserting xml strings during testing.
revert:Insignificant Whitespace removal
1 parent 71d3e0d commit 84042a4

2 files changed

Lines changed: 135 additions & 17 deletions

File tree

composer.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,6 @@
7272
"scripts": {
7373
"pre-commit": [
7474
"vendor/bin/phpcs -p",
75-
"vendor/bin/composer-require-checker check --config-file=tools/composer-require-checker.json composer.json",
7675
"vendor/bin/phpstan analyze -c phpstan.neon",
7776
"vendor/bin/phpstan analyze -c phpstan-dev.neon",
7877
"vendor/bin/composer-unused --excludePackage=simplesamlphp/composer-xmlprovider-installer",

src/XML/SchemaValidatableElementTrait.php

Lines changed: 135 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010
use SimpleSAML\XML\Exception\RuntimeException;
1111
use SimpleSAML\XMLSchema\Exception\SchemaViolationException;
1212

13+
use function array_filter;
1314
use function array_unique;
15+
use function array_values;
1416
use function defined;
1517
use function file_exists;
1618
use function implode;
@@ -28,42 +30,159 @@
2830
trait SchemaValidatableElementTrait
2931
{
3032
/**
31-
* Validate the given DOMDocument against the schema set for this element
33+
* Validate the given Dom\Document against the schema set for this element
3234
*
3335
* @param \Dom\Document $document
3436
* @param string|null $schemaFile
3537
*
3638
* @throws \SimpleSAML\XML\Exception\IOException
3739
* @throws \SimpleSAML\XMLSchema\Exception\SchemaViolationException
40+
* @return \Dom\Document
3841
*/
3942
public static function schemaValidate(Dom\Document $document, ?string $schemaFile = null): Dom\Document
4043
{
41-
$internalErrors = libxml_use_internal_errors(true);
42-
libxml_clear_errors();
44+
$previousUseInternalErrors = libxml_use_internal_errors(true);
4345

44-
if ($schemaFile === null) {
45-
$schemaFile = self::getSchemaFile();
46-
}
46+
try {
47+
libxml_clear_errors();
48+
49+
$schemaFile ??= self::getSchemaFile();
50+
51+
if (!$document instanceof Dom\XMLDocument) {
52+
throw new \LogicException('Schema validation requires an instance of Dom\\XMLDocument.');
53+
}
54+
55+
$root = $document->documentElement;
56+
if ($root === null) {
57+
throw new SchemaViolationException('The document has no document element.');
58+
}
59+
60+
/**
61+
* Dom\Document serialization:
62+
* - We need the exact serialized root bytes for the legacy validator.
63+
* - PHPStan may not know about Dom\Document::saveXml() depending on stubs/runtime.
64+
*
65+
* @phpstan-ignore-next-line
66+
*/
67+
$xmlRoot = $document->saveXml($root);
68+
if ($xmlRoot === false || trim($xmlRoot) === '') {
69+
throw new SchemaViolationException('Could not serialize XML root for schema validation.');
70+
}
4771

48-
// Must suppress the warnings here in order to throw them as an error below.
49-
$result = @$document->schemaValidate($schemaFile);
72+
// 1) Legacy validation (authoritative for now)
73+
$legacyResult = self::schemaValidateWithLegacyDom($xmlRoot, $schemaFile);
5074

51-
if ($result === false) {
52-
$msgs = [];
53-
foreach (libxml_get_errors() as $err) {
54-
$msgs[] = trim($err->message) . ' on line ' . $err->line;
75+
if ($legacyResult['ok'] === false) {
76+
throw new SchemaViolationException(sprintf(
77+
"XML schema validation errors:\n - %s",
78+
implode("\n - ", $legacyResult['errors'] ?: ['no libxml errors reported']),
79+
));
5580
}
5681

82+
// 2) New validation (temporarily disabled)
83+
//
84+
// NOTE: Dom\Document::schemaValidate() can produce false negatives in some cases.
85+
// See: https://github.com/php/php-src/issues/22219
86+
//
87+
// $domResult = self::schemaValidateWithDomDocument($document, $schemaFile);
88+
// if ($domResult['ok'] === false) {
89+
// throw new SchemaViolationException(sprintf(
90+
// "XML schema validation errors:\n - %s",
91+
// implode("\n - ", $domResult['errors'] ?: ['no libxml errors reported']),
92+
// ));
93+
// }
94+
95+
return $document;
96+
} finally {
97+
libxml_clear_errors();
98+
libxml_use_internal_errors($previousUseInternalErrors);
99+
}
100+
}
101+
102+
103+
/**
104+
* Validate a Dom\Document instance using a provided XML schema file.
105+
*
106+
* @param \Dom\Document $document The document to validate.
107+
* @param string $schemaFile The XML schema file to validate against.
108+
*
109+
* @return array{
110+
* ok: bool,
111+
* errors: list<string>
112+
* }
113+
*/
114+
private static function schemaValidateWithDomDocument(Dom\Document $document, string $schemaFile): array
115+
{
116+
libxml_clear_errors();
117+
118+
// Must suppress warnings here in order to throw them as an error below.
119+
$ok = @$document->schemaValidate($schemaFile);
120+
121+
return [
122+
'ok' => $ok,
123+
'errors' => $ok ? [] : self::schemaCollectLibxmlErrors('no libxml errors reported'),
124+
];
125+
}
126+
127+
128+
/**
129+
* Validate a serialized XML root element using legacy DOMDocument techniques.
130+
*
131+
* @param string $xmlRoot The serialized root XML element to validate.
132+
* @param string $schemaFile The XML schema file to validate against.
133+
*
134+
* @return array{
135+
* ok: bool,
136+
* errors: list<string>
137+
* }
138+
*/
139+
private static function schemaValidateWithLegacyDom(string $xmlRoot, string $schemaFile): array
140+
{
141+
$legacy = new \DOMDocument('1.0', 'UTF-8');
142+
$legacy->preserveWhiteSpace = true;
143+
$legacy->formatOutput = false;
144+
145+
libxml_clear_errors();
146+
$loaded = $legacy->loadXML($xmlRoot, LIBXML_NONET);
147+
148+
if ($loaded === false) {
149+
$parseErrors = self::schemaCollectLibxmlErrors('Unknown XML parse error.');
150+
57151
throw new SchemaViolationException(sprintf(
58-
"XML schema validation errors:\n - %s",
59-
implode("\n - ", array_unique($msgs)),
152+
"Could not parse serialized XML for schema validation.\n - %s",
153+
implode("\n - ", $parseErrors),
60154
));
61155
}
62156

63-
libxml_use_internal_errors($internalErrors);
64157
libxml_clear_errors();
158+
$ok = $legacy->schemaValidate($schemaFile);
159+
160+
return [
161+
'ok' => $ok,
162+
'errors' => $ok ? [] : self::schemaCollectLibxmlErrors('no libxml errors reported'),
163+
];
164+
}
165+
166+
167+
/**
168+
* Collect and format errors reported by libxml during XML processing.
169+
*
170+
* @param string $fallback Fallback message if no errors are reported.
171+
*
172+
* @return list<string> A list of error messages.
173+
*/
174+
private static function schemaCollectLibxmlErrors(string $fallback): array
175+
{
176+
$msgs = [];
177+
foreach (libxml_get_errors() as $err) {
178+
$message = trim($err->message);
179+
$line = $err->line;
180+
$msgs[] = $line > 0 ? sprintf('%s on line %d', $message, $line) : $message;
181+
}
182+
183+
$msgs = array_values(array_unique(array_filter($msgs)));
65184

66-
return $document;
185+
return $msgs === [] ? [$fallback] : $msgs;
67186
}
68187

69188

0 commit comments

Comments
 (0)