-
-
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.
- Loading branch information
Showing
12 changed files
with
386 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
<?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 | ||
{ | ||
public function __construct( | ||
private readonly string $originalPrefix, | ||
private readonly string $newPrefix, | ||
) { | ||
} | ||
|
||
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 ($currentPrefix !== $this->originalPrefix) { | ||
return new Action\Noop(); | ||
} | ||
|
||
$node->nodeValue = $this->newPrefix . ':'.$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')); | ||
} | ||
} |
115 changes: 115 additions & 0 deletions
115
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,115 @@ | ||
<?php declare(strict_types=1); | ||
|
||
namespace Soap\Wsdl\Xml\Xmlns; | ||
|
||
use DOMElement; | ||
use DOMNameSpaceNode; | ||
use RuntimeException; | ||
use Soap\Wsdl\Xml\Visitor\ReprefixTypeQname; | ||
use VeeWee\Xml\Dom\Collection\NodeList; | ||
use VeeWee\Xml\Dom\Document; | ||
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. | ||
*/ | ||
final class RegisterNonConflictingXmlnsNamespaces | ||
{ | ||
/** | ||
* @throws RuntimeException | ||
*/ | ||
public function __invoke(DOMElement $existingSchema, DOMElement $newSchema): void | ||
{ | ||
$existingLinkedNamespaces = linked_namespaces($existingSchema); | ||
linked_namespaces($newSchema)->forEach(function (DOMNameSpaceNode $xmlns) use ($existingSchema, $newSchema, $existingLinkedNamespaces) { | ||
// Skip non-named xmlns attributes: | ||
if (!$xmlns->prefix) { | ||
return; | ||
} | ||
|
||
// Check for duplicates: | ||
if ($existingSchema->hasAttribute($xmlns->nodeName) && $existingSchema->getAttribute($xmlns->nodeName) !== $xmlns->prefix) { | ||
if ($this->tryUsingExistingPrefix($existingLinkedNamespaces, $newSchema, $xmlns)) { | ||
return; | ||
} | ||
|
||
if ($this->tryUsingUniquePrefixHash($existingSchema, $newSchema, $xmlns)) { | ||
return; | ||
} | ||
|
||
throw new RuntimeException('Could not resolve conflicting namespace declarations whilst flattening your WSDL file.'); | ||
} | ||
|
||
xmlns_attribute($xmlns->prefix, $xmlns->namespaceURI)($existingSchema); | ||
}); | ||
|
||
(new FixRemovedDefaultXmlnsDeclarationsDuringImport())($existingSchema, $newSchema); | ||
} | ||
|
||
/** | ||
* @param NodeList<DOMNameSpaceNode> $existingLinkedNamespaces | ||
* | ||
* @throws RuntimeException | ||
*/ | ||
private function tryUsingExistingPrefix( | ||
NodeList $existingLinkedNamespaces, | ||
DOMElement $newSchema, | ||
DOMNameSpaceNode $xmlns | ||
): bool { | ||
$existingPrefix = $existingLinkedNamespaces->filter( | ||
static fn (DOMNameSpaceNode $node) => $node->namespaceURI === $xmlns->namespaceURI | ||
)->first()?->prefix; | ||
|
||
if ($existingPrefix === null) { | ||
return false; | ||
} | ||
|
||
$this->reprefixTypeDeclarations($newSchema, $xmlns->prefix, $existingPrefix); | ||
|
||
return true; | ||
} | ||
|
||
/** | ||
* @throws RuntimeException | ||
*/ | ||
private function tryUsingUniquePrefixHash(DOMElement $existingSchema, DOMElement $newSchema, DOMNameSpaceNode $xmlns): bool | ||
{ | ||
$uniquePrefix = 'ns' . substr(md5($xmlns->namespaceURI), 0, 8); | ||
if ($existingSchema->hasAttribute('xmlns:'.$uniquePrefix)) { | ||
return false; | ||
} | ||
|
||
$this->copyXmlnsDeclaration($existingSchema, $xmlns->namespaceURI, $uniquePrefix); | ||
$this->rePrefixTypeDeclarations($newSchema, $xmlns->prefix, $uniquePrefix); | ||
|
||
return true; | ||
} | ||
|
||
/** | ||
* @throws RuntimeException | ||
*/ | ||
private function copyXmlnsDeclaration(DOMElement $existingSchema, string $namespaceUri, string $prefix): void | ||
{ | ||
xmlns_attribute($prefix, $namespaceUri)($existingSchema); | ||
} | ||
|
||
/** | ||
* @throws RuntimeException | ||
*/ | ||
private function rePrefixTypeDeclarations(DOMElement $newSchema, string $originalPrefix, string $newPrefix): void | ||
{ | ||
Document::fromUnsafeDocument($newSchema->ownerDocument)->traverse(new ReprefixTypeQname( | ||
$originalPrefix, | ||
$newPrefix | ||
)); | ||
} | ||
} |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
<?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')); | ||
|
||
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 'nexted-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, | ||
]; | ||
} | ||
} |
Oops, something went wrong.