-
-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #23 from veewee/resolve-conflicting-xmlns
Resolve conflicting XMLNS imports
- Loading branch information
Showing
17 changed files
with
439 additions
and
26 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
<?php declare(strict_types=1); | ||
|
||
namespace Soap\Wsdl\Xml\Visitor; | ||
|
||
use DOMNode; | ||
use VeeWee\Xml\Dom\Traverser\Action; | ||
use VeeWee\Xml\Dom\Traverser\Visitor; | ||
use function VeeWee\Xml\Dom\Predicate\is_attribute; | ||
|
||
final class ReprefixTypeQname extends Visitor\AbstractVisitor | ||
{ | ||
/** | ||
* @param array<string, string> $prefixMap - "From" key - "To" value prefix map | ||
*/ | ||
public function __construct( | ||
private readonly array $prefixMap | ||
) { | ||
} | ||
|
||
public function onNodeEnter(DOMNode $node): Action | ||
{ | ||
if (!is_attribute($node) || $node->localName !== 'type') { | ||
return new Action\Noop(); | ||
} | ||
|
||
$parts = explode(':', $node->nodeValue ?? '', 2); | ||
if (count($parts) !== 2) { | ||
return new Action\Noop(); | ||
} | ||
|
||
[$currentPrefix, $currentTypeName] = $parts; | ||
if (!array_key_exists($currentPrefix, $this->prefixMap)) { | ||
return new Action\Noop(); | ||
} | ||
|
||
$node->nodeValue = $this->prefixMap[$currentPrefix].':'.$currentTypeName; | ||
|
||
return new Action\Noop(); | ||
} | ||
} |
30 changes: 30 additions & 0 deletions
30
src/Xml/Xmlns/FixRemovedDefaultXmlnsDeclarationsDuringImport.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
<?php declare(strict_types=1); | ||
|
||
namespace Soap\Wsdl\Xml\Xmlns; | ||
|
||
use DOMElement; | ||
|
||
/** | ||
* @see https://gist.github.com/veewee/32c3aa94adcf878700a9d5baa4b2a2de | ||
* | ||
* PHP does an optimization of namespaces during `importNode()`. | ||
* In some cases, this causes the root xmlns to be removed from the imported node which could lead to xsd qname errors. | ||
* | ||
* This function tries to re-add the root xmlns if it's available on the source but not on the target. | ||
* | ||
* It will most likely be solved in PHP 8.4's new spec compliant DOM\XMLDocument implementation. | ||
* @see https://github.com/php/php-src/pull/13031 | ||
* | ||
* For now, this will do the trick. | ||
*/ | ||
final class FixRemovedDefaultXmlnsDeclarationsDuringImport | ||
{ | ||
public function __invoke(DOMElement $target, DOMElement $source): void | ||
{ | ||
if (!$source->getAttribute('xmlns') || $target->hasAttribute('xmlns')) { | ||
return; | ||
} | ||
|
||
$target->setAttribute('xmlns', $source->getAttribute('xmlns')); | ||
} | ||
} |
129 changes: 129 additions & 0 deletions
129
src/Xml/Xmlns/RegisterNonConflictingXmlnsNamespaces.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
<?php declare(strict_types=1); | ||
|
||
namespace Soap\Wsdl\Xml\Xmlns; | ||
|
||
use DOMElement; | ||
use DOMNameSpaceNode; | ||
use Psl\Option\Option; | ||
use RuntimeException; | ||
use Soap\Wsdl\Xml\Visitor\ReprefixTypeQname; | ||
use VeeWee\Xml\Dom\Collection\NodeList; | ||
use VeeWee\Xml\Dom\Document; | ||
use function Psl\Dict\merge; | ||
use function Psl\Option\none; | ||
use function Psl\Option\some; | ||
use function VeeWee\Xml\Dom\Builder\xmlns_attribute; | ||
use function VeeWee\Xml\Dom\Locator\Xmlns\linked_namespaces; | ||
|
||
/** | ||
* Cross-import schemas can contain namespace conflicts. | ||
* | ||
* For example: import1 requires import2: | ||
* | ||
* - Import 1 specifies xmlns:ns1="urn:1" | ||
* - Import 2 specifies xmlns:ns1="urn:2". | ||
* | ||
* This method will detect conflicting namespaces and resolve them. | ||
* Namespaces will be renamed to a unique name and the "type" argument with QName's will be re-prefixed. | ||
* | ||
* @psalm-type RePrefixMap=array<string, string> | ||
*/ | ||
final class RegisterNonConflictingXmlnsNamespaces | ||
{ | ||
/** | ||
* @throws RuntimeException | ||
*/ | ||
public function __invoke(DOMElement $existingSchema, DOMElement $newSchema): void | ||
{ | ||
$existingLinkedNamespaces = linked_namespaces($existingSchema); | ||
|
||
$rePrefixMap = linked_namespaces($newSchema)->reduce( | ||
/** | ||
* @param RePrefixMap $rePrefixMap | ||
* @return RePrefixMap | ||
*/ | ||
function (array $rePrefixMap, DOMNameSpaceNode $xmlns) use ($existingSchema, $existingLinkedNamespaces): array { | ||
// Skip non-named xmlns attributes: | ||
if (!$xmlns->prefix) { | ||
return $rePrefixMap; | ||
} | ||
|
||
// Check for duplicates: | ||
if ($existingSchema->hasAttribute($xmlns->nodeName) && $existingSchema->getAttribute($xmlns->nodeName) !== $xmlns->prefix) { | ||
return merge( | ||
$rePrefixMap, | ||
// Can be improved with orElse when we are using PSL V3. | ||
$this->tryUsingExistingPrefix($existingLinkedNamespaces, $xmlns) | ||
->unwrapOrElse( | ||
fn () => $this->tryUsingUniquePrefixHash($existingSchema, $xmlns) | ||
->unwrapOrElse( | ||
static fn () => throw new RuntimeException('Could not resolve conflicting namespace declarations whilst flattening your WSDL file.') | ||
) | ||
) | ||
); | ||
} | ||
|
||
xmlns_attribute($xmlns->prefix, $xmlns->namespaceURI)($existingSchema); | ||
|
||
return $rePrefixMap; | ||
}, | ||
[] | ||
); | ||
|
||
if (count($rePrefixMap)) { | ||
Document::fromUnsafeDocument($newSchema->ownerDocument)->traverse(new ReprefixTypeQname($rePrefixMap)); | ||
} | ||
(new FixRemovedDefaultXmlnsDeclarationsDuringImport())($existingSchema, $newSchema); | ||
} | ||
|
||
/** | ||
* @param NodeList<DOMNameSpaceNode> $existingLinkedNamespaces | ||
* | ||
* @return Option<RePrefixMap> | ||
*/ | ||
private function tryUsingExistingPrefix( | ||
NodeList $existingLinkedNamespaces, | ||
DOMNameSpaceNode $xmlns | ||
): Option { | ||
$existingPrefix = $existingLinkedNamespaces->filter( | ||
static fn (DOMNameSpaceNode $node) => $node->namespaceURI === $xmlns->namespaceURI | ||
)->first()?->prefix; | ||
|
||
if ($existingPrefix === null) { | ||
/** @var Option<RePrefixMap> */ | ||
return none(); | ||
} | ||
|
||
/** @var Option<RePrefixMap> */ | ||
return some([$xmlns->prefix => $existingPrefix]); | ||
} | ||
|
||
/** | ||
* @return Option<RePrefixMap> | ||
* | ||
* @throws RuntimeException | ||
*/ | ||
private function tryUsingUniquePrefixHash( | ||
DOMElement $existingSchema, | ||
DOMNameSpaceNode $xmlns | ||
): Option { | ||
$uniquePrefix = 'ns' . substr(md5($xmlns->namespaceURI), 0, 8); | ||
if ($existingSchema->hasAttribute('xmlns:'.$uniquePrefix)) { | ||
/** @var Option<RePrefixMap> */ | ||
return none(); | ||
} | ||
|
||
$this->copyXmlnsDeclaration($existingSchema, $xmlns->namespaceURI, $uniquePrefix); | ||
|
||
/** @var Option<RePrefixMap> */ | ||
return some([$xmlns->prefix => $uniquePrefix]); | ||
} | ||
|
||
/** | ||
* @throws RuntimeException | ||
*/ | ||
private function copyXmlnsDeclaration(DOMElement $existingSchema, string $namespaceUri, string $prefix): void | ||
{ | ||
xmlns_attribute($prefix, $namespaceUri)($existingSchema); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
<?php | ||
|
||
class DOMNameSpaceNode extends DOMNode { | ||
public string $namespaceURI; | ||
public string $nodeName; | ||
public string $prefix; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
<?php declare(strict_types=1); | ||
|
||
namespace SoapTest\Wsdl\Unit\Xml\Visitor; | ||
|
||
use PHPUnit\Framework\TestCase; | ||
use Soap\Wsdl\Xml\Visitor\ReprefixTypeQname; | ||
use VeeWee\Xml\Dom\Document; | ||
|
||
final class ReprefixTypeQnameTest extends TestCase | ||
{ | ||
/** | ||
* | ||
* @dataProvider provideCases | ||
*/ | ||
public function test_it_can_reprefix_qname_types(string $input, string $expected): void | ||
{ | ||
$doc = Document::fromXmlString($input); | ||
$doc->traverse(new ReprefixTypeQname([ | ||
'tns' => 'new', | ||
'new' => 'previous', // To make sure prefix replacements don't get chained | ||
])); | ||
|
||
static::assertXmlStringEqualsXmlString($expected, $doc->toXmlString()); | ||
} | ||
|
||
public static function provideCases(): iterable | ||
{ | ||
yield 'no-attr' => [ | ||
'<element />', | ||
'<element />', | ||
]; | ||
yield 'other-attribute' => [ | ||
'<element other="xsd:Type" />', | ||
'<element other="xsd:Type" />', | ||
]; | ||
yield 'no-qualified' => [ | ||
'<element type="Type" />', | ||
'<element type="Type" />', | ||
]; | ||
yield 'simple' => [ | ||
'<node type="tns:Type" />', | ||
'<node type="new:Type" />', | ||
]; | ||
yield 'element' => [ | ||
'<element type="tns:Type" />', | ||
'<element type="new:Type" />', | ||
]; | ||
yield 'attribute' => [ | ||
'<attribute type="tns:Type" />', | ||
'<attribute type="new:Type" />', | ||
]; | ||
yield 'nested-schema' => [ | ||
<<<EOXML | ||
<complexType name="Store"> | ||
<sequence> | ||
<element minOccurs="1" maxOccurs="1" name="phone" type="tns:string"/> | ||
</sequence> | ||
</complexType> | ||
EOXML, | ||
<<<EOXML | ||
<complexType name="Store"> | ||
<sequence> | ||
<element minOccurs="1" maxOccurs="1" name="phone" type="new:string"/> | ||
</sequence> | ||
</complexType> | ||
EOXML, | ||
]; | ||
yield 'dont-chain-reprefixes' => [ | ||
'<schema><element type="tns:Type" /><element type="new:Type" /></schema>', | ||
'<schema><element type="new:Type" /><element type="previous:Type" /></schema>', | ||
]; | ||
} | ||
} |
Oops, something went wrong.