Skip to content

Commit

Permalink
Merge branch 'feature/simplify-schema-validation'
Browse files Browse the repository at this point in the history
  • Loading branch information
tvdijen committed Dec 16, 2024
2 parents ea339c4 + e67ade3 commit 0aaddc1
Show file tree
Hide file tree
Showing 13 changed files with 203 additions and 112 deletions.
53 changes: 1 addition & 52 deletions src/DOMDocumentFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,14 @@
use SimpleSAML\Assert\Assert;
use SimpleSAML\XML\Exception\IOException;
use SimpleSAML\XML\Exception\RuntimeException;
use SimpleSAML\XML\Exception\SchemaViolationException;
use SimpleSAML\XML\Exception\UnparseableXMLException;

use function array_unique;
use function file_exists;
use function file_get_contents;
use function func_num_args;
use function implode;
use function libxml_clear_errors;
use function libxml_get_errors;
use function libxml_set_external_entity_loader;
use function libxml_use_internal_errors;
use function sprintf;
use function trim;

/**
* @package simplesamlphp/xml-common
Expand All @@ -37,14 +31,12 @@ final class DOMDocumentFactory

/**
* @param string $xml
* @param string|null $schemaFile
* @param non-negative-int $options
*
* @return \DOMDocument
*/
public static function fromString(
string $xml,
?string $schemaFile = null,
int $options = self::DEFAULT_OPTIONS,
): DOMDocument {
libxml_set_external_entity_loader(null);
Expand All @@ -64,11 +56,6 @@ public static function fromString(
$options |= LIBXML_NO_XXE;
}

// Perform optional schema validation
if (!empty($schemaFile)) {
self::schemaValidation($xml, $schemaFile, $options);
}

$domDocument = self::create();
$loaded = $domDocument->loadXML($xml, $options);

Expand Down Expand Up @@ -97,7 +84,6 @@ public static function fromString(

/**
* @param string $file
* @param string|null $schemaFile
* @param non-negative-int $options
*
* @return \DOMDocument
Expand All @@ -117,9 +103,7 @@ public static function fromFile(
}

Assert::notWhitespaceOnly($xml, sprintf('File "%s" does not have content', $file), RuntimeException::class);
return (func_num_args() < 3)
? static::fromString($xml, $schemaFile)
: static::fromString($xml, $schemaFile, $options);
return (func_num_args() < 2) ? static::fromString($xml) : static::fromString($xml, $options);
}


Expand All @@ -132,39 +116,4 @@ public static function create(string $version = '1.0', string $encoding = 'UTF-8
{
return new DOMDocument($version, $encoding);
}


/**
* Validate an XML-string against a given schema.
*
* @param string $xml
* @param string $schemaFile
* @param int $options
*
* @throws \SimpleSAML\XML\Exception\SchemaViolationException when validation fails.
*/
public static function schemaValidation(
string $xml,
string $schemaFile,
int $options = self::DEFAULT_OPTIONS,
): void {
if (!file_exists($schemaFile)) {
throw new IOException('File not found.');
}

$document = DOMDocumentFactory::fromString($xml);
$result = $document->schemaValidate($schemaFile);

if ($result === false) {
$msgs = [];
foreach (libxml_get_errors() as $err) {
$msgs[] = trim($err->message) . ' on line ' . $err->line;
}

throw new SchemaViolationException(sprintf(
"XML schema validation errors:\n - %s",
implode("\n - ", array_unique($msgs)),
));
}
}
}
23 changes: 23 additions & 0 deletions src/SchemaValidatableElementInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

namespace SimpleSAML\XML;

use DOMDocument;

/**
* interface class to be implemented by all the classes that can be validated against a schema
*
* @package simplesamlphp/xml-common
*/
interface SchemaValidatableElementInterface extends ElementInterface
{
/**
* Validate the given DOMDocument against the schema set for this element
*
* @return \DOMDocument
* @throws \SimpleSAML\XML\Exception\SchemaViolationException
*/
public static function schemaValidate(DOMDocument $document): DOMDocument;
}
83 changes: 83 additions & 0 deletions src/SchemaValidatableElementTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<?php

declare(strict_types=1);

namespace SimpleSAML\XML;

use DOMDocument;
use SimpleSAML\Assert\Assert;
use SimpleSAML\XML\Exception\IOException;
use SimpleSAML\XML\Exception\SchemaViolationException;

use function array_unique;
use function defined;
use function file_exists;
use function implode;
use function libxml_get_errors;
use function restore_error_handler;
use function set_error_handler;
use function sprintf;
use function trim;

/**
* trait class to be used by all the classes that implement the SchemaValidatableElementInterface
*
* @package simplesamlphp/xml-common
*/
trait SchemaValidatableElementTrait
{
/**
* Validate the given DOMDocument against the schema set for this element
*
* @return \DOMDocument
* @throws \SimpleSAML\XML\Exception\SchemaViolationException
*/
public static function schemaValidate(DOMDocument $document): DOMDocument
{
$schemaFile = self::getSchemaFile();

// Dirty trick to catch the warnings emitted by XML-DOMs schemaValidate
// This will turn the warning into an exception
set_error_handler(static function (int $errno, string $errstr): never {
throw new SchemaViolationException($errstr, $errno);
}, E_WARNING);

try {
$result = $document->schemaValidate($schemaFile);
} finally {
// Restore the error handler, whether we throw an exception or not
restore_error_handler();
}

$msgs = [];
if ($result === false) {
foreach (libxml_get_errors() as $err) {
$msgs[] = trim($err->message) . ' on line ' . $err->line;
}

throw new SchemaViolationException(sprintf(
"XML schema validation errors:\n - %s",
implode("\n - ", array_unique($msgs)),
));
}

return $document;
}


/**
* Get the schema file that can validate this element.
* The path must be relative to the project's base directory.
*
* @return string
*/
public static function getSchemaFile(): string
{
if (defined('static::SCHEMA')) {
$schemaFile = static::SCHEMA;
}

Assert::true(file_exists($schemaFile), sprintf("File not found: %s", $schemaFile), IOException::class);
return $schemaFile;
}
}
13 changes: 2 additions & 11 deletions src/TestUtils/SchemaValidationTestTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

use DOMDocument;
use PHPUnit\Framework\Attributes\Depends;
use SimpleSAML\XML\DOMDocumentFactory;

use function class_exists;

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

/** @var string */
protected static string $schemaFile;

/** @var \DOMDocument */
protected static DOMDocument $xmlRepresentation;

Expand All @@ -38,26 +34,21 @@ public function testSchemaValidation(): void
'Unable to run ' . self::class . '::testSchemaValidation(). Please set ' . self::class
. ':$testedClass to a class-string representing the XML-class being tested',
);
} elseif (empty(self::$schemaFile)) {
$this->markTestSkipped(
'Unable to run ' . self::class . '::testSchemaValidation(). Please set ' . self::class
. ':$schema to point to a schema file',
);
} elseif (empty(self::$xmlRepresentation)) {
$this->markTestSkipped(
'Unable to run ' . self::class . '::testSchemaValidation(). Please set ' . self::class
. ':$xmlRepresentation to a DOMDocument representing the XML-class being tested',
);
} else {
// Validate before serialization
DOMDocumentFactory::schemaValidation(self::$xmlRepresentation->saveXML(), self::$schemaFile);
self::$testedClass::schemaValidate(self::$xmlRepresentation);

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

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

// If we got this far and no exceptions were thrown, consider this test passed!
$this->addToAssertionCount(1);
Expand Down
9 changes: 8 additions & 1 deletion tests/Utils/BooleanElement.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,29 @@

use SimpleSAML\XML\AbstractElement;
use SimpleSAML\XML\BooleanElementTrait;
use SimpleSAML\XML\SchemaValidatableElementInterface;
use SimpleSAML\XML\SchemaValidatableElementTrait;

/**
* Empty shell class for testing BooleanElement.
*
* @package simplesaml/xml-common
*
* Note: this class is not final for testing purposes.
*/
final class BooleanElement extends AbstractElement
class BooleanElement extends AbstractElement implements SchemaValidatableElementInterface
{
use BooleanElementTrait;
use SchemaValidatableElementTrait;

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

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

public const SCHEMA = '/file/does/not/exist.xsd';


/**
* @param string $content
Expand Down
8 changes: 7 additions & 1 deletion tests/Utils/ExtendableAttributesElement.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,19 @@
use SimpleSAML\XML\AbstractElement;
use SimpleSAML\XML\Exception\InvalidDOMElementException;
use SimpleSAML\XML\ExtendableAttributesTrait;
use SimpleSAML\XML\SchemaValidatableElementInterface;
use SimpleSAML\XML\SchemaValidatableElementTrait;
use SimpleSAML\XML\XsNamespace as NS;

/**
* Empty shell class for testing ExtendableAttributesTrait.
*
* @package simplesaml/xml-security
*/
class ExtendableAttributesElement extends AbstractElement
class ExtendableAttributesElement extends AbstractElement implements SchemaValidatableElementInterface
{
use ExtendableAttributesTrait;
use SchemaValidatableElementTrait;

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

/** @var string */
public const SCHEMA = 'tests/resources/schemas/simplesamlphp.xsd';

/** @var string|\SimpleSAML\XML\XsNamespace */
final public const XS_ANY_ATTR_NAMESPACE = NS::ANY;

Expand Down
8 changes: 7 additions & 1 deletion tests/Utils/ExtendableElement.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
use DOMElement;
use SimpleSAML\XML\AbstractElement;
use SimpleSAML\XML\ExtendableElementTrait;
use SimpleSAML\XML\SchemaValidatableElementInterface;
use SimpleSAML\XML\SchemaValidatableElementTrait;
use SimpleSAML\XML\SerializableElementTrait;
use SimpleSAML\XML\XsNamespace as NS;

Expand All @@ -15,9 +17,10 @@
*
* @package simplesaml/xml-security
*/
class ExtendableElement extends AbstractElement
class ExtendableElement extends AbstractElement implements SchemaValidatableElementInterface
{
use ExtendableElementTrait;
use SchemaValidatableElementTrait;
use SerializableElementTrait;

/** @var string */
Expand All @@ -29,6 +32,9 @@ class ExtendableElement extends AbstractElement
/** @var string */
public const LOCALNAME = 'ExtendableElement';

/** @var string */
public const SCHEMA = 'tests/resources/schemas/simplesamlphp.xsd';

/** @var \SimpleSAML\XML\XsNamespace|array<int, \SimpleSAML\XML\XsNamespace> */
final public const XS_ANY_ELT_NAMESPACE = NS::ANY;

Expand Down
8 changes: 7 additions & 1 deletion tests/Utils/StringElement.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,18 @@
namespace SimpleSAML\Test\XML;

use SimpleSAML\XML\AbstractElement;
use SimpleSAML\XML\SchemaValidatableElementInterface;
use SimpleSAML\XML\SchemaValidatableElementTrait;
use SimpleSAML\XML\StringElementTrait;

/**
* Empty shell class for testing String elements.
*
* @package simplesaml/xml-common
*/
final class StringElement extends AbstractElement
final class StringElement extends AbstractElement implements SchemaValidatableElementInterface
{
use SchemaValidatableElementTrait;
use StringElementTrait;

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

/** @var string */
public const SCHEMA = 'tests/resources/schemas/simplesamlphp.xsd';


/**
* @param string $content
Expand Down
Loading

0 comments on commit 0aaddc1

Please sign in to comment.