55namespace SimpleSAML \XML ;
66
77use DOMDocument ;
8+ use Exception ;
9+ use LibXMLError ;
810use SimpleSAML \Assert \Assert ;
911use SimpleSAML \XML \Exception \IOException ;
1012use SimpleSAML \XML \Exception \RuntimeException ;
13+ use SimpleSAML \XML \Exception \SchemaViolationException ;
1114use SimpleSAML \XML \Exception \UnparseableXMLException ;
15+ use XMLReader ;
1216
17+ use function array_unique ;
1318use function file_get_contents ;
1419use function func_num_args ;
20+ use function implode ;
1521use function libxml_clear_errors ;
1622use function libxml_get_last_error ;
1723use function libxml_set_external_entity_loader ;
1824use function libxml_use_internal_errors ;
1925use function sprintf ;
26+ use function trim ;
2027
2128/**
2229 * @package simplesamlphp/xml-common
@@ -32,12 +39,14 @@ final class DOMDocumentFactory
3239
3340 /**
3441 * @param string $xml
42+ * @param string|null $schemaFile
3543 * @param non-negative-int $options
3644 *
3745 * @return \DOMDocument
3846 */
3947 public static function fromString (
4048 string $ xml ,
49+ ?string $ schemaFile = null ,
4150 int $ options = self ::DEFAULT_OPTIONS ,
4251 ): DOMDocument {
4352 libxml_set_external_entity_loader (null );
@@ -57,6 +66,11 @@ public static function fromString(
5766 $ options |= LIBXML_NO_XXE ;
5867 }
5968
69+ // Perform optional schema validation
70+ if (!empty ($ schemaFile )) {
71+ self ::schemaValidation ($ xml , $ schemaFile , $ options );
72+ }
73+
6074 $ domDocument = self ::create ();
6175 $ loaded = $ domDocument ->loadXML ($ xml , $ options );
6276
@@ -85,12 +99,16 @@ public static function fromString(
8599
86100 /**
87101 * @param string $file
102+ * @param string|null $schemaFile
88103 * @param non-negative-int $options
89104 *
90105 * @return \DOMDocument
91106 */
92- public static function fromFile (string $ file , int $ options = self ::DEFAULT_OPTIONS ): DOMDocument
93- {
107+ public static function fromFile (
108+ string $ file ,
109+ ?string $ schemaFile = null ,
110+ int $ options = self ::DEFAULT_OPTIONS ,
111+ ): DOMDocument {
94112 error_clear_last ();
95113 $ xml = @file_get_contents ($ file );
96114 if ($ xml === false ) {
@@ -101,7 +119,9 @@ public static function fromFile(string $file, int $options = self::DEFAULT_OPTIO
101119 }
102120
103121 Assert::notWhitespaceOnly ($ xml , sprintf ('File "%s" does not have content ' , $ file ), RuntimeException::class);
104- return (func_num_args () === 1 ) ? static ::fromString ($ xml ) : static ::fromString ($ xml , $ options );
122+ return (func_num_args () < 3 )
123+ ? static ::fromString ($ xml , $ schemaFile )
124+ : static ::fromString ($ xml , $ schemaFile , $ options );
105125 }
106126
107127
@@ -114,4 +134,49 @@ public static function create(string $version = '1.0', string $encoding = 'UTF-8
114134 {
115135 return new DOMDocument ($ version , $ encoding );
116136 }
137+
138+
139+ /**
140+ * Validate an XML-string against a given schema.
141+ *
142+ * @param string $xml
143+ * @param string $schemaFile
144+ * @param int $options
145+ *
146+ * @throws \SimpleSAML\XML\Exception\SchemaViolationException when validation fails.
147+ */
148+ public static function schemaValidation (
149+ string $ xml ,
150+ string $ schemaFile ,
151+ int $ options = self ::DEFAULT_OPTIONS ,
152+ ): void {
153+ $ xmlReader = XMLReader::XML ($ xml , null , $ options );
154+ Assert::notFalse ($ xmlReader , SchemaViolationException::class);
155+
156+ libxml_use_internal_errors (true );
157+
158+ try {
159+ $ xmlReader ->setSchema ($ schemaFile );
160+ } catch (Exception ) {
161+ $ err = libxml_get_last_error ();
162+ throw new SchemaViolationException (trim ($ err ->message ) . ' on line ' . $ err ->line );
163+ }
164+
165+ $ msgs = [];
166+ while ($ xmlReader ->read ()) {
167+ if (!$ xmlReader ->isValid ()) {
168+ $ err = libxml_get_last_error ();
169+ if ($ err instanceof LibXMLError) {
170+ $ msgs [] = trim ($ err ->message ) . ' on line ' . $ err ->line ;
171+ }
172+ }
173+ }
174+
175+ if ($ msgs ) {
176+ throw new SchemaViolationException (sprintf (
177+ "XML schema validation errors: \n - %s " ,
178+ implode ("\n - " , array_unique ($ msgs )),
179+ ));
180+ }
181+ }
117182}
0 commit comments