Skip to content

Commit 0aaddc1

Browse files
committed
Merge branch 'feature/simplify-schema-validation'
2 parents ea339c4 + e67ade3 commit 0aaddc1

13 files changed

+203
-112
lines changed

src/DOMDocumentFactory.php

Lines changed: 1 addition & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,14 @@
88
use SimpleSAML\Assert\Assert;
99
use SimpleSAML\XML\Exception\IOException;
1010
use SimpleSAML\XML\Exception\RuntimeException;
11-
use SimpleSAML\XML\Exception\SchemaViolationException;
1211
use SimpleSAML\XML\Exception\UnparseableXMLException;
1312

14-
use function array_unique;
15-
use function file_exists;
1613
use function file_get_contents;
1714
use function func_num_args;
18-
use function implode;
1915
use function libxml_clear_errors;
20-
use function libxml_get_errors;
2116
use function libxml_set_external_entity_loader;
2217
use function libxml_use_internal_errors;
2318
use function sprintf;
24-
use function trim;
2519

2620
/**
2721
* @package simplesamlphp/xml-common
@@ -37,14 +31,12 @@ final class DOMDocumentFactory
3731

3832
/**
3933
* @param string $xml
40-
* @param string|null $schemaFile
4134
* @param non-negative-int $options
4235
*
4336
* @return \DOMDocument
4437
*/
4538
public static function fromString(
4639
string $xml,
47-
?string $schemaFile = null,
4840
int $options = self::DEFAULT_OPTIONS,
4941
): DOMDocument {
5042
libxml_set_external_entity_loader(null);
@@ -64,11 +56,6 @@ public static function fromString(
6456
$options |= LIBXML_NO_XXE;
6557
}
6658

67-
// Perform optional schema validation
68-
if (!empty($schemaFile)) {
69-
self::schemaValidation($xml, $schemaFile, $options);
70-
}
71-
7259
$domDocument = self::create();
7360
$loaded = $domDocument->loadXML($xml, $options);
7461

@@ -97,7 +84,6 @@ public static function fromString(
9784

9885
/**
9986
* @param string $file
100-
* @param string|null $schemaFile
10187
* @param non-negative-int $options
10288
*
10389
* @return \DOMDocument
@@ -117,9 +103,7 @@ public static function fromFile(
117103
}
118104

119105
Assert::notWhitespaceOnly($xml, sprintf('File "%s" does not have content', $file), RuntimeException::class);
120-
return (func_num_args() < 3)
121-
? static::fromString($xml, $schemaFile)
122-
: static::fromString($xml, $schemaFile, $options);
106+
return (func_num_args() < 2) ? static::fromString($xml) : static::fromString($xml, $options);
123107
}
124108

125109

@@ -132,39 +116,4 @@ public static function create(string $version = '1.0', string $encoding = 'UTF-8
132116
{
133117
return new DOMDocument($version, $encoding);
134118
}
135-
136-
137-
/**
138-
* Validate an XML-string against a given schema.
139-
*
140-
* @param string $xml
141-
* @param string $schemaFile
142-
* @param int $options
143-
*
144-
* @throws \SimpleSAML\XML\Exception\SchemaViolationException when validation fails.
145-
*/
146-
public static function schemaValidation(
147-
string $xml,
148-
string $schemaFile,
149-
int $options = self::DEFAULT_OPTIONS,
150-
): void {
151-
if (!file_exists($schemaFile)) {
152-
throw new IOException('File not found.');
153-
}
154-
155-
$document = DOMDocumentFactory::fromString($xml);
156-
$result = $document->schemaValidate($schemaFile);
157-
158-
if ($result === false) {
159-
$msgs = [];
160-
foreach (libxml_get_errors() as $err) {
161-
$msgs[] = trim($err->message) . ' on line ' . $err->line;
162-
}
163-
164-
throw new SchemaViolationException(sprintf(
165-
"XML schema validation errors:\n - %s",
166-
implode("\n - ", array_unique($msgs)),
167-
));
168-
}
169-
}
170119
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace SimpleSAML\XML;
6+
7+
use DOMDocument;
8+
9+
/**
10+
* interface class to be implemented by all the classes that can be validated against a schema
11+
*
12+
* @package simplesamlphp/xml-common
13+
*/
14+
interface SchemaValidatableElementInterface extends ElementInterface
15+
{
16+
/**
17+
* Validate the given DOMDocument against the schema set for this element
18+
*
19+
* @return \DOMDocument
20+
* @throws \SimpleSAML\XML\Exception\SchemaViolationException
21+
*/
22+
public static function schemaValidate(DOMDocument $document): DOMDocument;
23+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace SimpleSAML\XML;
6+
7+
use DOMDocument;
8+
use SimpleSAML\Assert\Assert;
9+
use SimpleSAML\XML\Exception\IOException;
10+
use SimpleSAML\XML\Exception\SchemaViolationException;
11+
12+
use function array_unique;
13+
use function defined;
14+
use function file_exists;
15+
use function implode;
16+
use function libxml_get_errors;
17+
use function restore_error_handler;
18+
use function set_error_handler;
19+
use function sprintf;
20+
use function trim;
21+
22+
/**
23+
* trait class to be used by all the classes that implement the SchemaValidatableElementInterface
24+
*
25+
* @package simplesamlphp/xml-common
26+
*/
27+
trait SchemaValidatableElementTrait
28+
{
29+
/**
30+
* Validate the given DOMDocument against the schema set for this element
31+
*
32+
* @return \DOMDocument
33+
* @throws \SimpleSAML\XML\Exception\SchemaViolationException
34+
*/
35+
public static function schemaValidate(DOMDocument $document): DOMDocument
36+
{
37+
$schemaFile = self::getSchemaFile();
38+
39+
// Dirty trick to catch the warnings emitted by XML-DOMs schemaValidate
40+
// This will turn the warning into an exception
41+
set_error_handler(static function (int $errno, string $errstr): never {
42+
throw new SchemaViolationException($errstr, $errno);
43+
}, E_WARNING);
44+
45+
try {
46+
$result = $document->schemaValidate($schemaFile);
47+
} finally {
48+
// Restore the error handler, whether we throw an exception or not
49+
restore_error_handler();
50+
}
51+
52+
$msgs = [];
53+
if ($result === false) {
54+
foreach (libxml_get_errors() as $err) {
55+
$msgs[] = trim($err->message) . ' on line ' . $err->line;
56+
}
57+
58+
throw new SchemaViolationException(sprintf(
59+
"XML schema validation errors:\n - %s",
60+
implode("\n - ", array_unique($msgs)),
61+
));
62+
}
63+
64+
return $document;
65+
}
66+
67+
68+
/**
69+
* Get the schema file that can validate this element.
70+
* The path must be relative to the project's base directory.
71+
*
72+
* @return string
73+
*/
74+
public static function getSchemaFile(): string
75+
{
76+
if (defined('static::SCHEMA')) {
77+
$schemaFile = static::SCHEMA;
78+
}
79+
80+
Assert::true(file_exists($schemaFile), sprintf("File not found: %s", $schemaFile), IOException::class);
81+
return $schemaFile;
82+
}
83+
}

src/TestUtils/SchemaValidationTestTrait.php

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66

77
use DOMDocument;
88
use PHPUnit\Framework\Attributes\Depends;
9-
use SimpleSAML\XML\DOMDocumentFactory;
109

1110
use function class_exists;
1211

@@ -20,9 +19,6 @@ trait SchemaValidationTestTrait
2019
/** @var class-string */
2120
protected static string $testedClass;
2221

23-
/** @var string */
24-
protected static string $schemaFile;
25-
2622
/** @var \DOMDocument */
2723
protected static DOMDocument $xmlRepresentation;
2824

@@ -38,26 +34,21 @@ public function testSchemaValidation(): void
3834
'Unable to run ' . self::class . '::testSchemaValidation(). Please set ' . self::class
3935
. ':$testedClass to a class-string representing the XML-class being tested',
4036
);
41-
} elseif (empty(self::$schemaFile)) {
42-
$this->markTestSkipped(
43-
'Unable to run ' . self::class . '::testSchemaValidation(). Please set ' . self::class
44-
. ':$schema to point to a schema file',
45-
);
4637
} elseif (empty(self::$xmlRepresentation)) {
4738
$this->markTestSkipped(
4839
'Unable to run ' . self::class . '::testSchemaValidation(). Please set ' . self::class
4940
. ':$xmlRepresentation to a DOMDocument representing the XML-class being tested',
5041
);
5142
} else {
5243
// Validate before serialization
53-
DOMDocumentFactory::schemaValidation(self::$xmlRepresentation->saveXML(), self::$schemaFile);
44+
self::$testedClass::schemaValidate(self::$xmlRepresentation);
5445

5546
// Perform serialization
5647
$class = self::$testedClass::fromXML(self::$xmlRepresentation->documentElement);
5748
$serializedClass = $class->toXML();
5849

5950
// Validate after serialization
60-
DOMDocumentFactory::schemaValidation($serializedClass->ownerDocument->saveXML(), self::$schemaFile);
51+
self::$testedClass::schemaValidate($serializedClass->ownerDocument);
6152

6253
// If we got this far and no exceptions were thrown, consider this test passed!
6354
$this->addToAssertionCount(1);

tests/Utils/BooleanElement.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,29 @@
66

77
use SimpleSAML\XML\AbstractElement;
88
use SimpleSAML\XML\BooleanElementTrait;
9+
use SimpleSAML\XML\SchemaValidatableElementInterface;
10+
use SimpleSAML\XML\SchemaValidatableElementTrait;
911

1012
/**
1113
* Empty shell class for testing BooleanElement.
1214
*
1315
* @package simplesaml/xml-common
16+
*
17+
* Note: this class is not final for testing purposes.
1418
*/
15-
final class BooleanElement extends AbstractElement
19+
class BooleanElement extends AbstractElement implements SchemaValidatableElementInterface
1620
{
1721
use BooleanElementTrait;
22+
use SchemaValidatableElementTrait;
1823

1924
/** @var string */
2025
public const NS = 'urn:x-simplesamlphp:namespace';
2126

2227
/** @var string */
2328
public const NS_PREFIX = 'ssp';
2429

30+
public const SCHEMA = '/file/does/not/exist.xsd';
31+
2532

2633
/**
2734
* @param string $content

tests/Utils/ExtendableAttributesElement.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,19 @@
99
use SimpleSAML\XML\AbstractElement;
1010
use SimpleSAML\XML\Exception\InvalidDOMElementException;
1111
use SimpleSAML\XML\ExtendableAttributesTrait;
12+
use SimpleSAML\XML\SchemaValidatableElementInterface;
13+
use SimpleSAML\XML\SchemaValidatableElementTrait;
1214
use SimpleSAML\XML\XsNamespace as NS;
1315

1416
/**
1517
* Empty shell class for testing ExtendableAttributesTrait.
1618
*
1719
* @package simplesaml/xml-security
1820
*/
19-
class ExtendableAttributesElement extends AbstractElement
21+
class ExtendableAttributesElement extends AbstractElement implements SchemaValidatableElementInterface
2022
{
2123
use ExtendableAttributesTrait;
24+
use SchemaValidatableElementTrait;
2225

2326
/** @var string */
2427
public const NS = 'urn:x-simplesamlphp:namespace';
@@ -29,6 +32,9 @@ class ExtendableAttributesElement extends AbstractElement
2932
/** @var string */
3033
public const LOCALNAME = 'ExtendableAttributesElement';
3134

35+
/** @var string */
36+
public const SCHEMA = 'tests/resources/schemas/simplesamlphp.xsd';
37+
3238
/** @var string|\SimpleSAML\XML\XsNamespace */
3339
final public const XS_ANY_ATTR_NAMESPACE = NS::ANY;
3440

tests/Utils/ExtendableElement.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
use DOMElement;
88
use SimpleSAML\XML\AbstractElement;
99
use SimpleSAML\XML\ExtendableElementTrait;
10+
use SimpleSAML\XML\SchemaValidatableElementInterface;
11+
use SimpleSAML\XML\SchemaValidatableElementTrait;
1012
use SimpleSAML\XML\SerializableElementTrait;
1113
use SimpleSAML\XML\XsNamespace as NS;
1214

@@ -15,9 +17,10 @@
1517
*
1618
* @package simplesaml/xml-security
1719
*/
18-
class ExtendableElement extends AbstractElement
20+
class ExtendableElement extends AbstractElement implements SchemaValidatableElementInterface
1921
{
2022
use ExtendableElementTrait;
23+
use SchemaValidatableElementTrait;
2124
use SerializableElementTrait;
2225

2326
/** @var string */
@@ -29,6 +32,9 @@ class ExtendableElement extends AbstractElement
2932
/** @var string */
3033
public const LOCALNAME = 'ExtendableElement';
3134

35+
/** @var string */
36+
public const SCHEMA = 'tests/resources/schemas/simplesamlphp.xsd';
37+
3238
/** @var \SimpleSAML\XML\XsNamespace|array<int, \SimpleSAML\XML\XsNamespace> */
3339
final public const XS_ANY_ELT_NAMESPACE = NS::ANY;
3440

tests/Utils/StringElement.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,18 @@
55
namespace SimpleSAML\Test\XML;
66

77
use SimpleSAML\XML\AbstractElement;
8+
use SimpleSAML\XML\SchemaValidatableElementInterface;
9+
use SimpleSAML\XML\SchemaValidatableElementTrait;
810
use SimpleSAML\XML\StringElementTrait;
911

1012
/**
1113
* Empty shell class for testing String elements.
1214
*
1315
* @package simplesaml/xml-common
1416
*/
15-
final class StringElement extends AbstractElement
17+
final class StringElement extends AbstractElement implements SchemaValidatableElementInterface
1618
{
19+
use SchemaValidatableElementTrait;
1720
use StringElementTrait;
1821

1922
/** @var string */
@@ -22,6 +25,9 @@ final class StringElement extends AbstractElement
2225
/** @var string */
2326
public const NS_PREFIX = 'ssp';
2427

28+
/** @var string */
29+
public const SCHEMA = 'tests/resources/schemas/simplesamlphp.xsd';
30+
2531

2632
/**
2733
* @param string $content

0 commit comments

Comments
 (0)