Skip to content

Commit 8975e4f

Browse files
authored
Merge pull request #23 from veewee/resolve-conflicting-xmlns
Resolve conflicting XMLNS imports
2 parents dff99df + c0fc5a5 commit 8975e4f

17 files changed

+439
-26
lines changed

psalm.xml

+3
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,7 @@
2727
<plugins>
2828
<pluginClass class="Psalm\SymfonyPsalmPlugin\Plugin"/>
2929
</plugins>
30+
<stubs>
31+
<file name="stubs/dom.phpstub" />
32+
</stubs>
3033
</psalm>

src/Xml/Configurator/FlattenWsdlImports.php

+2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use VeeWee\Xml\Exception\RuntimeException;
1717
use function VeeWee\Xml\Dom\Locator\document_element;
1818
use function VeeWee\Xml\Dom\Locator\Node\children;
19+
use function VeeWee\Xml\Dom\Manipulator\Element\copy_named_xmlns_attributes;
1920
use function VeeWee\Xml\Dom\Manipulator\Node\append_external_node;
2021
use function VeeWee\Xml\Dom\Manipulator\Node\remove;
2122
use function VeeWee\Xml\Dom\Manipulator\Node\replace_by_external_nodes;
@@ -82,6 +83,7 @@ private function importWsdlImportElement(DOMElement $import): void
8283
private function importWsdlPart(DOMElement $importElement, Document $importedDocument): void
8384
{
8485
$definitions = $importedDocument->map(document_element());
86+
copy_named_xmlns_attributes($importElement->ownerDocument->documentElement, $definitions);
8587

8688
replace_by_external_nodes(
8789
$importElement,

src/Xml/Configurator/FlattenXsdImports.php

+5-26
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
use Soap\Wsdl\Loader\Context\FlatteningContext;
1313
use Soap\Wsdl\Uri\IncludePathBuilder;
1414
use Soap\Wsdl\Xml\Exception\FlattenException;
15+
use Soap\Wsdl\Xml\Xmlns\FixRemovedDefaultXmlnsDeclarationsDuringImport;
16+
use Soap\Wsdl\Xml\Xmlns\RegisterNonConflictingXmlnsNamespaces;
1517
use Soap\Xml\Xpath\WsdlPreset;
1618
use VeeWee\Xml\Dom\Configurator\Configurator;
1719
use VeeWee\Xml\Dom\Document;
@@ -21,7 +23,6 @@
2123
use function Psl\Vec\reverse;
2224
use function VeeWee\Xml\Dom\Assert\assert_element;
2325
use function VeeWee\Xml\Dom\Locator\Node\children;
24-
use function VeeWee\Xml\Dom\Manipulator\Element\copy_named_xmlns_attributes;
2526
use function VeeWee\Xml\Dom\Manipulator\Node\append_external_node;
2627
use function VeeWee\Xml\Dom\Manipulator\Node\remove;
2728

@@ -170,6 +171,7 @@ private function loadSchema(string $location): ?DOMElement
170171
* This function registers the newly provided schema in the WSDL types section.
171172
* It groups all imports by targetNamespace.
172173
*
174+
* @throws \RuntimeException
173175
* @throws RuntimeException
174176
* @throws AssertException
175177
*/
@@ -187,42 +189,19 @@ private function registerSchemaInTypes(DOMElement $schema): void
187189
// If no schema exists yet: Add the newly loaded schema as a completely new schema in the WSDL types.
188190
if (!$existingSchema) {
189191
$imported = assert_element(append_external_node($types, $schema));
190-
$this->fixRemovedDefaultXmlnsDeclarationsDuringImport($imported, $schema);
192+
(new FixRemovedDefaultXmlnsDeclarationsDuringImport())($imported, $schema);
191193
return;
192194
}
193195

194196
// When an existing schema exists, all xmlns attributes need to be copied.
195197
// This is to make sure that possible QNames (strings) get resolved in XSD.
196198
// Finally - all children of the newly loaded schema can be appended to the existing schema.
197-
copy_named_xmlns_attributes($existingSchema, $schema);
198-
$this->fixRemovedDefaultXmlnsDeclarationsDuringImport($existingSchema, $schema);
199+
(new RegisterNonConflictingXmlnsNamespaces())($existingSchema, $schema);
199200
children($schema)->forEach(
200201
static fn (DOMNode $node) => append_external_node($existingSchema, $node)
201202
);
202203
}
203204

204-
/**
205-
* @see https://gist.github.com/veewee/32c3aa94adcf878700a9d5baa4b2a2de
206-
*
207-
* PHP does an optimization of namespaces during `importNode()`.
208-
* In some cases, this causes the root xmlns to be removed from the imported node which could lead to xsd qname errors.
209-
*
210-
* This function tries to re-add the root xmlns if it's available on the source but not on the target.
211-
*
212-
* It will most likely be solved in PHP 8.4's new spec compliant DOM\XMLDocument implementation.
213-
* @see https://github.com/php/php-src/pull/13031
214-
*
215-
* For now, this will do the trick.
216-
*/
217-
private function fixRemovedDefaultXmlnsDeclarationsDuringImport(DOMElement $target, DOMElement $source): void
218-
{
219-
if (!$source->getAttribute('xmlns') || $target->hasAttribute('xmlns')) {
220-
return;
221-
}
222-
223-
$target->setAttribute('xmlns', $source->getAttribute('xmlns'));
224-
}
225-
226205
/**
227206
* Makes sure to rearrange the import statements on top of the flattened XSD schema.
228207
* This makes the flattened XSD spec compliant:

src/Xml/Visitor/ReprefixTypeQname.php

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Soap\Wsdl\Xml\Visitor;
4+
5+
use DOMNode;
6+
use VeeWee\Xml\Dom\Traverser\Action;
7+
use VeeWee\Xml\Dom\Traverser\Visitor;
8+
use function VeeWee\Xml\Dom\Predicate\is_attribute;
9+
10+
final class ReprefixTypeQname extends Visitor\AbstractVisitor
11+
{
12+
/**
13+
* @param array<string, string> $prefixMap - "From" key - "To" value prefix map
14+
*/
15+
public function __construct(
16+
private readonly array $prefixMap
17+
) {
18+
}
19+
20+
public function onNodeEnter(DOMNode $node): Action
21+
{
22+
if (!is_attribute($node) || $node->localName !== 'type') {
23+
return new Action\Noop();
24+
}
25+
26+
$parts = explode(':', $node->nodeValue ?? '', 2);
27+
if (count($parts) !== 2) {
28+
return new Action\Noop();
29+
}
30+
31+
[$currentPrefix, $currentTypeName] = $parts;
32+
if (!array_key_exists($currentPrefix, $this->prefixMap)) {
33+
return new Action\Noop();
34+
}
35+
36+
$node->nodeValue = $this->prefixMap[$currentPrefix].':'.$currentTypeName;
37+
38+
return new Action\Noop();
39+
}
40+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Soap\Wsdl\Xml\Xmlns;
4+
5+
use DOMElement;
6+
7+
/**
8+
* @see https://gist.github.com/veewee/32c3aa94adcf878700a9d5baa4b2a2de
9+
*
10+
* PHP does an optimization of namespaces during `importNode()`.
11+
* In some cases, this causes the root xmlns to be removed from the imported node which could lead to xsd qname errors.
12+
*
13+
* This function tries to re-add the root xmlns if it's available on the source but not on the target.
14+
*
15+
* It will most likely be solved in PHP 8.4's new spec compliant DOM\XMLDocument implementation.
16+
* @see https://github.com/php/php-src/pull/13031
17+
*
18+
* For now, this will do the trick.
19+
*/
20+
final class FixRemovedDefaultXmlnsDeclarationsDuringImport
21+
{
22+
public function __invoke(DOMElement $target, DOMElement $source): void
23+
{
24+
if (!$source->getAttribute('xmlns') || $target->hasAttribute('xmlns')) {
25+
return;
26+
}
27+
28+
$target->setAttribute('xmlns', $source->getAttribute('xmlns'));
29+
}
30+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Soap\Wsdl\Xml\Xmlns;
4+
5+
use DOMElement;
6+
use DOMNameSpaceNode;
7+
use Psl\Option\Option;
8+
use RuntimeException;
9+
use Soap\Wsdl\Xml\Visitor\ReprefixTypeQname;
10+
use VeeWee\Xml\Dom\Collection\NodeList;
11+
use VeeWee\Xml\Dom\Document;
12+
use function Psl\Dict\merge;
13+
use function Psl\Option\none;
14+
use function Psl\Option\some;
15+
use function VeeWee\Xml\Dom\Builder\xmlns_attribute;
16+
use function VeeWee\Xml\Dom\Locator\Xmlns\linked_namespaces;
17+
18+
/**
19+
* Cross-import schemas can contain namespace conflicts.
20+
*
21+
* For example: import1 requires import2:
22+
*
23+
* - Import 1 specifies xmlns:ns1="urn:1"
24+
* - Import 2 specifies xmlns:ns1="urn:2".
25+
*
26+
* This method will detect conflicting namespaces and resolve them.
27+
* Namespaces will be renamed to a unique name and the "type" argument with QName's will be re-prefixed.
28+
*
29+
* @psalm-type RePrefixMap=array<string, string>
30+
*/
31+
final class RegisterNonConflictingXmlnsNamespaces
32+
{
33+
/**
34+
* @throws RuntimeException
35+
*/
36+
public function __invoke(DOMElement $existingSchema, DOMElement $newSchema): void
37+
{
38+
$existingLinkedNamespaces = linked_namespaces($existingSchema);
39+
40+
$rePrefixMap = linked_namespaces($newSchema)->reduce(
41+
/**
42+
* @param RePrefixMap $rePrefixMap
43+
* @return RePrefixMap
44+
*/
45+
function (array $rePrefixMap, DOMNameSpaceNode $xmlns) use ($existingSchema, $existingLinkedNamespaces): array {
46+
// Skip non-named xmlns attributes:
47+
if (!$xmlns->prefix) {
48+
return $rePrefixMap;
49+
}
50+
51+
// Check for duplicates:
52+
if ($existingSchema->hasAttribute($xmlns->nodeName) && $existingSchema->getAttribute($xmlns->nodeName) !== $xmlns->prefix) {
53+
return merge(
54+
$rePrefixMap,
55+
// Can be improved with orElse when we are using PSL V3.
56+
$this->tryUsingExistingPrefix($existingLinkedNamespaces, $xmlns)
57+
->unwrapOrElse(
58+
fn () => $this->tryUsingUniquePrefixHash($existingSchema, $xmlns)
59+
->unwrapOrElse(
60+
static fn () => throw new RuntimeException('Could not resolve conflicting namespace declarations whilst flattening your WSDL file.')
61+
)
62+
)
63+
);
64+
}
65+
66+
xmlns_attribute($xmlns->prefix, $xmlns->namespaceURI)($existingSchema);
67+
68+
return $rePrefixMap;
69+
},
70+
[]
71+
);
72+
73+
if (count($rePrefixMap)) {
74+
Document::fromUnsafeDocument($newSchema->ownerDocument)->traverse(new ReprefixTypeQname($rePrefixMap));
75+
}
76+
(new FixRemovedDefaultXmlnsDeclarationsDuringImport())($existingSchema, $newSchema);
77+
}
78+
79+
/**
80+
* @param NodeList<DOMNameSpaceNode> $existingLinkedNamespaces
81+
*
82+
* @return Option<RePrefixMap>
83+
*/
84+
private function tryUsingExistingPrefix(
85+
NodeList $existingLinkedNamespaces,
86+
DOMNameSpaceNode $xmlns
87+
): Option {
88+
$existingPrefix = $existingLinkedNamespaces->filter(
89+
static fn (DOMNameSpaceNode $node) => $node->namespaceURI === $xmlns->namespaceURI
90+
)->first()?->prefix;
91+
92+
if ($existingPrefix === null) {
93+
/** @var Option<RePrefixMap> */
94+
return none();
95+
}
96+
97+
/** @var Option<RePrefixMap> */
98+
return some([$xmlns->prefix => $existingPrefix]);
99+
}
100+
101+
/**
102+
* @return Option<RePrefixMap>
103+
*
104+
* @throws RuntimeException
105+
*/
106+
private function tryUsingUniquePrefixHash(
107+
DOMElement $existingSchema,
108+
DOMNameSpaceNode $xmlns
109+
): Option {
110+
$uniquePrefix = 'ns' . substr(md5($xmlns->namespaceURI), 0, 8);
111+
if ($existingSchema->hasAttribute('xmlns:'.$uniquePrefix)) {
112+
/** @var Option<RePrefixMap> */
113+
return none();
114+
}
115+
116+
$this->copyXmlnsDeclaration($existingSchema, $xmlns->namespaceURI, $uniquePrefix);
117+
118+
/** @var Option<RePrefixMap> */
119+
return some([$xmlns->prefix => $uniquePrefix]);
120+
}
121+
122+
/**
123+
* @throws RuntimeException
124+
*/
125+
private function copyXmlnsDeclaration(DOMElement $existingSchema, string $namespaceUri, string $prefix): void
126+
{
127+
xmlns_attribute($prefix, $namespaceUri)($existingSchema);
128+
}
129+
}

stubs/dom.phpstub

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php
2+
3+
class DOMNameSpaceNode extends DOMNode {
4+
public string $namespaceURI;
5+
public string $nodeName;
6+
public string $prefix;
7+
}

tests/Unit/Xml/Configurator/FlattenWsdlImportsTest.php

+4
Original file line numberDiff line numberDiff line change
@@ -54,5 +54,9 @@ public function provideTestCases()
5454
'wsdl' => FIXTURE_DIR.'/flattening/import-multi-xsd.wsdl',
5555
'expected' => Document::fromXmlFile(FIXTURE_DIR.'/flattening/result/import-multi-xsd-result.wsdl', comparable()),
5656
];
57+
yield 'import-namespaces' => [
58+
'wsdl' => FIXTURE_DIR.'/flattening/import-namespaces.wsdl',
59+
'expected' => Document::fromXmlFile(FIXTURE_DIR.'/flattening/result/import-namespaces.wsdl', comparable()),
60+
];
5761
}
5862
}

tests/Unit/Xml/Configurator/FlattenXsdImportsTest.php

+5
Original file line numberDiff line numberDiff line change
@@ -76,5 +76,10 @@ public function provideTestCases()
7676
'expected' => Document::fromXmlFile(FIXTURE_DIR.'/flattening/result/rearranged-imports.wsdl'),
7777
comparable(),
7878
];
79+
yield 'import-xmlns-issue' => [
80+
'wsdl' => FIXTURE_DIR.'/flattening/conflicting-imports.wsdl',
81+
'expected' => Document::fromXmlFile(FIXTURE_DIR.'/flattening/result/conflicting-imports.wsdl'),
82+
canonicalize(),
83+
];
7984
}
8085
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace SoapTest\Wsdl\Unit\Xml\Visitor;
4+
5+
use PHPUnit\Framework\TestCase;
6+
use Soap\Wsdl\Xml\Visitor\ReprefixTypeQname;
7+
use VeeWee\Xml\Dom\Document;
8+
9+
final class ReprefixTypeQnameTest extends TestCase
10+
{
11+
/**
12+
*
13+
* @dataProvider provideCases
14+
*/
15+
public function test_it_can_reprefix_qname_types(string $input, string $expected): void
16+
{
17+
$doc = Document::fromXmlString($input);
18+
$doc->traverse(new ReprefixTypeQname([
19+
'tns' => 'new',
20+
'new' => 'previous', // To make sure prefix replacements don't get chained
21+
]));
22+
23+
static::assertXmlStringEqualsXmlString($expected, $doc->toXmlString());
24+
}
25+
26+
public static function provideCases(): iterable
27+
{
28+
yield 'no-attr' => [
29+
'<element />',
30+
'<element />',
31+
];
32+
yield 'other-attribute' => [
33+
'<element other="xsd:Type" />',
34+
'<element other="xsd:Type" />',
35+
];
36+
yield 'no-qualified' => [
37+
'<element type="Type" />',
38+
'<element type="Type" />',
39+
];
40+
yield 'simple' => [
41+
'<node type="tns:Type" />',
42+
'<node type="new:Type" />',
43+
];
44+
yield 'element' => [
45+
'<element type="tns:Type" />',
46+
'<element type="new:Type" />',
47+
];
48+
yield 'attribute' => [
49+
'<attribute type="tns:Type" />',
50+
'<attribute type="new:Type" />',
51+
];
52+
yield 'nested-schema' => [
53+
<<<EOXML
54+
<complexType name="Store">
55+
<sequence>
56+
<element minOccurs="1" maxOccurs="1" name="phone" type="tns:string"/>
57+
</sequence>
58+
</complexType>
59+
EOXML,
60+
<<<EOXML
61+
<complexType name="Store">
62+
<sequence>
63+
<element minOccurs="1" maxOccurs="1" name="phone" type="new:string"/>
64+
</sequence>
65+
</complexType>
66+
EOXML,
67+
];
68+
yield 'dont-chain-reprefixes' => [
69+
'<schema><element type="tns:Type" /><element type="new:Type" /></schema>',
70+
'<schema><element type="new:Type" /><element type="previous:Type" /></schema>',
71+
];
72+
}
73+
}

0 commit comments

Comments
 (0)