Skip to content

Commit 64e4d31

Browse files
committed
Add code to normalize document namespace-tags
1 parent 6d237b7 commit 64e4d31

13 files changed

+171
-9
lines changed

src/XML/AbstractElement.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,24 @@ public static function getLocalName(): string
222222
}
223223

224224

225+
/**
226+
* Whether the element may be normalized.
227+
*
228+
* @return bool
229+
*/
230+
public static function getNormalization(): bool
231+
{
232+
if (defined('static::NORMALIZATION')) {
233+
$normalization = static::NORMALIZATION;
234+
} else {
235+
$normalization = true;
236+
}
237+
238+
Assert::boolean($normalization, RuntimeException::class);
239+
return $normalization;
240+
}
241+
242+
225243
/**
226244
* Test if an object, at the state it's in, would produce an empty XML-element
227245
*

src/XML/Chunk.php

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,13 @@ final class Chunk implements SerializableElementInterface
2323
use SerializableElementTrait;
2424

2525

26+
/**
27+
* Whether the element may be normalized
28+
*
29+
* @var bool $normalization
30+
*/
31+
protected bool $normalization = true;
32+
2633
/**
2734
* The localName of the element.
2835
*
@@ -59,6 +66,28 @@ public function __construct(
5966
}
6067

6168

69+
/**
70+
* Collect the value of the normalization-property
71+
*
72+
* @return bool
73+
*/
74+
public function getNormalization(): bool
75+
{
76+
return $this->normalization;
77+
}
78+
79+
80+
/**
81+
* Set the value of the normalization-property
82+
*
83+
* @param bool $normalization
84+
*/
85+
public function setNormalization(bool $normalization): void
86+
{
87+
$this->normalization = $normalization;
88+
}
89+
90+
6291
/**
6392
* Collect the value of the localName-property
6493
*
@@ -254,7 +283,6 @@ public function toXML(?DOMElement $parent = null): DOMElement
254283
}
255284

256285
$parent->appendChild($doc->importNode($this->getXML(), true));
257-
258286
return $doc->documentElement;
259287
}
260288
}

src/XML/DOMDocumentFactory.php

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,20 @@
55
namespace SimpleSAML\XML;
66

77
use DOMDocument;
8+
use DOMElement;
89
use SimpleSAML\XML\Assert\Assert;
910
use SimpleSAML\XML\Exception\IOException;
1011
use SimpleSAML\XML\Exception\RuntimeException;
1112
use SimpleSAML\XML\Exception\UnparseableXMLException;
13+
use SimpleSAML\XPath\XPath;
1214

1315
use function file_get_contents;
1416
use function func_num_args;
1517
use function libxml_clear_errors;
1618
use function libxml_set_external_entity_loader;
1719
use function libxml_use_internal_errors;
1820
use function sprintf;
21+
use function strpos;
1922

2023
/**
2124
* @package simplesamlphp/xml-common
@@ -115,4 +118,83 @@ public static function create(string $version = '1.0', string $encoding = 'UTF-8
115118
{
116119
return new DOMDocument($version, $encoding);
117120
}
121+
122+
123+
/**
124+
* @param \DOMDocument $doc
125+
* @return \DOMDocument
126+
*/
127+
public static function normalizeDocument(DOMDocument $doc): DOMDocument
128+
{
129+
// Get the root element
130+
$root = $doc->documentElement;
131+
132+
// Collect all xmlns attributes from the document
133+
$xpath = XPath::getXPath($doc);
134+
$xmlnsAttributes = [];
135+
136+
// Register all namespaces to ensure XPath can handle them
137+
foreach ($xpath->query('//namespace::*') as $node) {
138+
$name = $node->nodeName === 'xmlns' ? 'xmlns' : $node->nodeName;
139+
if ($name !== 'xmlns:xml') {
140+
$xmlnsAttributes[$name] = $node->nodeValue;
141+
}
142+
}
143+
144+
// If no xmlns attributes found, return early with debug info
145+
if (empty($xmlnsAttributes)) {
146+
return $root->ownerDocument;
147+
}
148+
149+
// Remove xmlns attributes from all elements
150+
$nodes = $xpath->query('//*[namespace::*]');
151+
foreach ($nodes as $node) {
152+
if ($node instanceof DOMElement) {
153+
$attributesToRemove = [];
154+
foreach ($node->attributes as $attr) {
155+
if (strpos($attr->nodeName, 'xmlns') === 0 || $attr->nodeName === 'xmlns') {
156+
$attributesToRemove[] = $attr->nodeName;
157+
}
158+
}
159+
foreach ($attributesToRemove as $attrName) {
160+
$node->removeAttribute($attrName);
161+
}
162+
}
163+
}
164+
165+
// Add all collected xmlns attributes to the root element
166+
foreach ($xmlnsAttributes as $name => $value) {
167+
$root->setAttribute($name, $value);
168+
}
169+
170+
// Return the normalized XML
171+
return static::fromString($root->ownerDocument->saveXML());
172+
}
173+
174+
175+
/**
176+
* @param \DOMElement $elt
177+
* @param string $prefix
178+
* @return string|null
179+
*/
180+
public static function lookupNamespaceURI(DOMElement $elt, string $prefix): ?string
181+
{
182+
// Get the root element
183+
$root = $elt->ownerDocument->documentElement;
184+
185+
// Collect all xmlns attributes from the document
186+
$xpath = XPath::getXPath($elt->ownerDocument);
187+
188+
// Register all namespaces to ensure XPath can handle them
189+
$xmlnsAttributes = [];
190+
foreach ($xpath->query('//namespace::*') as $node) {
191+
$xmlnsAttributes[$node->localName] = $node->nodeValue;
192+
}
193+
194+
if (array_key_exists($prefix, $xmlnsAttributes)) {
195+
return $xmlnsAttributes[$prefix];
196+
}
197+
198+
return null;
199+
}
118200
}

src/XML/SerializableElementTrait.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@ public function __toString(): string
4141
$doc = DOMDocumentFactory::fromString($xmlString);
4242
$doc->formatOutput = $this->formatOutput;
4343

44+
if (static::getNormalization() === true) {
45+
$normalized = DOMDocumentFactory::normalizeDocument($doc);
46+
return $normalized->saveXML($normalized->firstChild);
47+
}
48+
4449
return $doc->saveXML($doc->firstChild);
4550
}
4651

src/XML/TypedTextContentTrait.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,6 @@ public function toXML(?DOMElement $parent = null): DOMElement
104104
}
105105

106106
$e->textContent = strval($this->getContent());
107-
108107
return $e;
109108
}
110109

src/XMLSchema/Type/QNameValue.php

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

77
use DOMElement;
88
use SimpleSAML\XML\Assert\Assert;
9+
use SimpleSAML\XML\DOMDocumentFactory;
910
use SimpleSAML\XMLSchema\Exception\SchemaViolationException;
1011
use SimpleSAML\XMLSchema\Type\Interface\AbstractAnySimpleType;
1112

@@ -175,7 +176,7 @@ public static function fromDocument(
175176
}
176177

177178
// Will return the default namespace (if any) when prefix is NULL
178-
$namespaceURI = $element->lookupNamespaceUri($namespacePrefix);
179+
$namespaceURI = DOMDocumentFactory::lookupNamespaceUri($element, $namespacePrefix);
179180

180181
return new static('{' . $namespaceURI . '}' . ($namespacePrefix ? $namespacePrefix . ':' : '') . $localName);
181182
}

tests/Helper/Element.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use DOMElement;
88
use SimpleSAML\Assert\Assert;
99
use SimpleSAML\XML\AbstractElement;
10+
use SimpleSAML\XML\DOMDocumentFactory;
1011
use SimpleSAML\XMLSchema\Exception\InvalidDOMElementException;
1112
use SimpleSAML\XMLSchema\Type\BooleanValue;
1213
use SimpleSAML\XMLSchema\Type\IntegerValue;
@@ -133,6 +134,7 @@ public function toXML(?DOMElement $parent = null): DOMElement
133134
$e->setAttribute('otherText', strval($this->getOtherString()));
134135
}
135136

136-
return $e;
137+
// @phpstan-ignore argument.type, return.type
138+
return DOMDocumentFactory::normalizeDocument($e->ownerDocument)->documentElement;
137139
}
138140
}

tests/Helper/ExtendableAttributesElement.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use DOMElement;
88
use SimpleSAML\Assert\Assert;
99
use SimpleSAML\XML\AbstractElement;
10+
use SimpleSAML\XML\DOMDocumentFactory;
1011
use SimpleSAML\XML\ExtendableAttributesTrait;
1112
use SimpleSAML\XML\SchemaValidatableElementInterface;
1213
use SimpleSAML\XML\SchemaValidatableElementTrait;
@@ -85,6 +86,7 @@ public function toXML(?DOMElement $parent = null): DOMElement
8586
$attr->toXML($e);
8687
}
8788

88-
return $e;
89+
// @phpstan-ignore argument.type, return.type
90+
return DOMDocumentFactory::normalizeDocument($e->ownerDocument)->documentElement;
8991
}
9092
}

tests/Helper/ExtendableElement.php

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

77
use DOMElement;
88
use SimpleSAML\XML\AbstractElement;
9+
use SimpleSAML\XML\DOMDocumentFactory;
910
use SimpleSAML\XML\ExtendableElementTrait;
1011
use SimpleSAML\XML\SchemaValidatableElementInterface;
1112
use SimpleSAML\XML\SchemaValidatableElementTrait;
@@ -76,14 +77,15 @@ public static function fromXML(DOMElement $xml): static
7677
*/
7778
public function toXML(?DOMElement $parent = null): DOMElement
7879
{
79-
$e = $this->instantiateParentElement();
80+
$e = $this->instantiateParentElement($parent);
8081

8182
foreach ($this->getElements() as $elt) {
8283
if (!$elt->isEmptyElement()) {
8384
$elt->toXML($e);
8485
}
8586
}
8687

87-
return $e;
88+
// @phpstan-ignore argument.type, return.type
89+
return DOMDocumentFactory::normalizeDocument($e->ownerDocument)->documentElement;
8890
}
8991
}

tests/XML/DOMDocumentFactoryTest.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,4 +114,17 @@ public function testEmptyStringIsNotValid(): void
114114
);
115115
DOMDocumentFactory::fromString('');
116116
}
117+
118+
119+
public function testNormalizeDocument(): void
120+
{
121+
$normalized = DOMDocumentFactory::fromFile('tests/resources/xml/domdocument_normalized.xml');
122+
$notNormalized = DOMDocumentFactory::fromFile('tests/resources/xml/domdocument_not_normalized.xml');
123+
$normalizedDoc = DOMDocumentFactory::normalizeDocument($notNormalized);
124+
125+
$this->assertEquals(
126+
$normalized->saveXML($normalized),
127+
$normalizedDoc->saveXML($normalizedDoc),
128+
);
129+
}
117130
}

0 commit comments

Comments
 (0)