Skip to content

Commit

Permalink
Resolve conflicting XMLNS imports
Browse files Browse the repository at this point in the history
  • Loading branch information
veewee committed Sep 27, 2024
1 parent dff99df commit 50261a2
Show file tree
Hide file tree
Showing 4 changed files with 178 additions and 26 deletions.
30 changes: 4 additions & 26 deletions src/Xml/Configurator/FlattenXsdImports.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
use Soap\Wsdl\Loader\Context\FlatteningContext;
use Soap\Wsdl\Uri\IncludePathBuilder;
use Soap\Wsdl\Xml\Exception\FlattenException;
use Soap\Wsdl\Xml\Xmlns\FixRemovedDefaultXmlnsDeclarationsDuringImport;
use Soap\Wsdl\Xml\Xmlns\RegisterNonConflictingXmlnsNamespaces;
use Soap\Xml\Xpath\WsdlPreset;
use VeeWee\Xml\Dom\Configurator\Configurator;
use VeeWee\Xml\Dom\Document;
Expand All @@ -21,7 +23,6 @@
use function Psl\Vec\reverse;
use function VeeWee\Xml\Dom\Assert\assert_element;
use function VeeWee\Xml\Dom\Locator\Node\children;
use function VeeWee\Xml\Dom\Manipulator\Element\copy_named_xmlns_attributes;
use function VeeWee\Xml\Dom\Manipulator\Node\append_external_node;
use function VeeWee\Xml\Dom\Manipulator\Node\remove;

Expand Down Expand Up @@ -187,42 +188,19 @@ private function registerSchemaInTypes(DOMElement $schema): void
// If no schema exists yet: Add the newly loaded schema as a completely new schema in the WSDL types.
if (!$existingSchema) {
$imported = assert_element(append_external_node($types, $schema));
$this->fixRemovedDefaultXmlnsDeclarationsDuringImport($imported, $schema);
(new FixRemovedDefaultXmlnsDeclarationsDuringImport())($imported, $schema);
return;
}

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

/**
* @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.
*/
private function fixRemovedDefaultXmlnsDeclarationsDuringImport(DOMElement $target, DOMElement $source): void
{
if (!$source->getAttribute('xmlns') || $target->hasAttribute('xmlns')) {
return;
}

$target->setAttribute('xmlns', $source->getAttribute('xmlns'));
}

/**
* Makes sure to rearrange the import statements on top of the flattened XSD schema.
* This makes the flattened XSD spec compliant:
Expand Down
37 changes: 37 additions & 0 deletions src/Xml/Visitor/ReprefixTypeQname.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

namespace Soap\Wsdl\Xml\Visitor;

use VeeWee\Xml\Dom\Traverser\Action;
use VeeWee\Xml\Dom\Traverser\Visitor;
use function VeeWee\Xml\Dom\Predicate\is_attribute;

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();
}

$value = $node->nodeValue;
if (!str_contains($value, ':')) {

Check failure on line 24 in src/Xml/Visitor/ReprefixTypeQname.php

View workflow job for this annotation

GitHub Actions / PHP 8.1 @ ubuntu-latest

PossiblyNullArgument

src/Xml/Visitor/ReprefixTypeQname.php:24:27: PossiblyNullArgument: Argument 1 of str_contains cannot be null, possibly null value provided (see https://psalm.dev/078)

Check failure on line 24 in src/Xml/Visitor/ReprefixTypeQname.php

View workflow job for this annotation

GitHub Actions / PHP 8.2 @ ubuntu-latest

PossiblyNullArgument

src/Xml/Visitor/ReprefixTypeQname.php:24:27: PossiblyNullArgument: Argument 1 of str_contains cannot be null, possibly null value provided (see https://psalm.dev/078)

Check failure on line 24 in src/Xml/Visitor/ReprefixTypeQname.php

View workflow job for this annotation

GitHub Actions / PHP 8.3 @ ubuntu-latest

PossiblyNullArgument

src/Xml/Visitor/ReprefixTypeQname.php:24:27: PossiblyNullArgument: Argument 1 of str_contains cannot be null, possibly null value provided (see https://psalm.dev/078)
return new Action\Noop();
}

[$currentPrefix, $currentTypeName] = explode(':', $value, 2);

Check failure on line 28 in src/Xml/Visitor/ReprefixTypeQname.php

View workflow job for this annotation

GitHub Actions / PHP 8.1 @ ubuntu-latest

PossiblyUndefinedArrayOffset

src/Xml/Visitor/ReprefixTypeQname.php:28:26: PossiblyUndefinedArrayOffset: Possibly undefined array key (see https://psalm.dev/167)

Check failure on line 28 in src/Xml/Visitor/ReprefixTypeQname.php

View workflow job for this annotation

GitHub Actions / PHP 8.1 @ ubuntu-latest

PossiblyNullArgument

src/Xml/Visitor/ReprefixTypeQname.php:28:59: PossiblyNullArgument: Argument 2 of explode cannot be null, possibly null value provided (see https://psalm.dev/078)

Check failure on line 28 in src/Xml/Visitor/ReprefixTypeQname.php

View workflow job for this annotation

GitHub Actions / PHP 8.2 @ ubuntu-latest

PossiblyUndefinedArrayOffset

src/Xml/Visitor/ReprefixTypeQname.php:28:26: PossiblyUndefinedArrayOffset: Possibly undefined array key (see https://psalm.dev/167)

Check failure on line 28 in src/Xml/Visitor/ReprefixTypeQname.php

View workflow job for this annotation

GitHub Actions / PHP 8.2 @ ubuntu-latest

PossiblyNullArgument

src/Xml/Visitor/ReprefixTypeQname.php:28:59: PossiblyNullArgument: Argument 2 of explode cannot be null, possibly null value provided (see https://psalm.dev/078)

Check failure on line 28 in src/Xml/Visitor/ReprefixTypeQname.php

View workflow job for this annotation

GitHub Actions / PHP 8.3 @ ubuntu-latest

PossiblyUndefinedArrayOffset

src/Xml/Visitor/ReprefixTypeQname.php:28:26: PossiblyUndefinedArrayOffset: Possibly undefined array key (see https://psalm.dev/167)

Check failure on line 28 in src/Xml/Visitor/ReprefixTypeQname.php

View workflow job for this annotation

GitHub Actions / PHP 8.3 @ ubuntu-latest

PossiblyNullArgument

src/Xml/Visitor/ReprefixTypeQname.php:28:59: PossiblyNullArgument: Argument 2 of explode cannot be null, possibly null value provided (see https://psalm.dev/078)
if ($currentPrefix !== $this->originalPrefix) {
return new Action\Noop();
}

$node->nodeValue = $this->newPrefix . ':'.$currentTypeName;

return new Action\Noop();
}
}
28 changes: 28 additions & 0 deletions src/Xml/Xmlns/FixRemovedDefaultXmlnsDeclarationsDuringImport.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

namespace Soap\Wsdl\Xml\Xmlns;

/**
* @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'));
}
}
109 changes: 109 additions & 0 deletions src/Xml/Xmlns/RegisterNonConflictingXmlnsNamespaces.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<?php

namespace Soap\Wsdl\Xml\Xmlns;

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
{
public function __invoke(\DOMElement $existingSchema, \DOMElement $newSchema): void
{
$existingLinkedNamespaces = linked_namespaces($existingSchema);

Check failure on line 26 in src/Xml/Xmlns/RegisterNonConflictingXmlnsNamespaces.php

View workflow job for this annotation

GitHub Actions / PHP 8.1 @ ubuntu-latest

MissingThrowsDocblock

src/Xml/Xmlns/RegisterNonConflictingXmlnsNamespaces.php:26:37: MissingThrowsDocblock: VeeWee\Xml\Exception\RuntimeException is thrown but not caught - please either catch or add a @throws annotation (see https://psalm.dev/169)

Check failure on line 26 in src/Xml/Xmlns/RegisterNonConflictingXmlnsNamespaces.php

View workflow job for this annotation

GitHub Actions / PHP 8.2 @ ubuntu-latest

MissingThrowsDocblock

src/Xml/Xmlns/RegisterNonConflictingXmlnsNamespaces.php:26:37: MissingThrowsDocblock: VeeWee\Xml\Exception\RuntimeException is thrown but not caught - please either catch or add a @throws annotation (see https://psalm.dev/169)

Check failure on line 26 in src/Xml/Xmlns/RegisterNonConflictingXmlnsNamespaces.php

View workflow job for this annotation

GitHub Actions / PHP 8.3 @ ubuntu-latest

MissingThrowsDocblock

src/Xml/Xmlns/RegisterNonConflictingXmlnsNamespaces.php:26:37: MissingThrowsDocblock: VeeWee\Xml\Exception\RuntimeException is thrown but not caught - please either catch or add a @throws annotation (see https://psalm.dev/169)
linked_namespaces($newSchema)->forEach(function (\DOMNameSpaceNode $xmlns) use ($existingSchema, $newSchema, $existingLinkedNamespaces) {

Check failure on line 27 in src/Xml/Xmlns/RegisterNonConflictingXmlnsNamespaces.php

View workflow job for this annotation

GitHub Actions / PHP 8.1 @ ubuntu-latest

MissingThrowsDocblock

src/Xml/Xmlns/RegisterNonConflictingXmlnsNamespaces.php:27:9: MissingThrowsDocblock: VeeWee\Xml\Exception\RuntimeException is thrown but not caught - please either catch or add a @throws annotation (see https://psalm.dev/169)

Check failure on line 27 in src/Xml/Xmlns/RegisterNonConflictingXmlnsNamespaces.php

View workflow job for this annotation

GitHub Actions / PHP 8.2 @ ubuntu-latest

MissingThrowsDocblock

src/Xml/Xmlns/RegisterNonConflictingXmlnsNamespaces.php:27:9: MissingThrowsDocblock: VeeWee\Xml\Exception\RuntimeException is thrown but not caught - please either catch or add a @throws annotation (see https://psalm.dev/169)

Check failure on line 27 in src/Xml/Xmlns/RegisterNonConflictingXmlnsNamespaces.php

View workflow job for this annotation

GitHub Actions / PHP 8.3 @ ubuntu-latest

MissingThrowsDocblock

src/Xml/Xmlns/RegisterNonConflictingXmlnsNamespaces.php:27:9: MissingThrowsDocblock: VeeWee\Xml\Exception\RuntimeException is thrown but not caught - please either catch or add a @throws annotation (see https://psalm.dev/169)
// 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);

Check failure on line 46 in src/Xml/Xmlns/RegisterNonConflictingXmlnsNamespaces.php

View workflow job for this annotation

GitHub Actions / PHP 8.1 @ ubuntu-latest

PossiblyNullArgument

src/Xml/Xmlns/RegisterNonConflictingXmlnsNamespaces.php:46:45: PossiblyNullArgument: Argument 2 of VeeWee\Xml\Dom\Builder\xmlns_attribute cannot be null, possibly null value provided (see https://psalm.dev/078)

Check failure on line 46 in src/Xml/Xmlns/RegisterNonConflictingXmlnsNamespaces.php

View workflow job for this annotation

GitHub Actions / PHP 8.2 @ ubuntu-latest

PossiblyNullArgument

src/Xml/Xmlns/RegisterNonConflictingXmlnsNamespaces.php:46:45: PossiblyNullArgument: Argument 2 of VeeWee\Xml\Dom\Builder\xmlns_attribute cannot be null, possibly null value provided (see https://psalm.dev/078)

Check failure on line 46 in src/Xml/Xmlns/RegisterNonConflictingXmlnsNamespaces.php

View workflow job for this annotation

GitHub Actions / PHP 8.3 @ ubuntu-latest

PossiblyNullArgument

src/Xml/Xmlns/RegisterNonConflictingXmlnsNamespaces.php:46:45: PossiblyNullArgument: Argument 2 of VeeWee\Xml\Dom\Builder\xmlns_attribute cannot be null, possibly null value provided (see https://psalm.dev/078)
});

(new FixRemovedDefaultXmlnsDeclarationsDuringImport())($existingSchema, $newSchema);
}

/**
* @param NodeList<\DOMNameSpaceNode> $existingLinkedNamespaces

Check failure on line 53 in src/Xml/Xmlns/RegisterNonConflictingXmlnsNamespaces.php

View workflow job for this annotation

GitHub Actions / PHP 8.1 @ ubuntu-latest

InvalidTemplateParam

src/Xml/Xmlns/RegisterNonConflictingXmlnsNamespaces.php:53:15: InvalidTemplateParam: Extended template param T of VeeWee\Xml\Dom\Collection\NodeList<DOMNameSpaceNode> expects type DOMNode, type DOMNameSpaceNode given (see https://psalm.dev/183)

Check failure on line 53 in src/Xml/Xmlns/RegisterNonConflictingXmlnsNamespaces.php

View workflow job for this annotation

GitHub Actions / PHP 8.2 @ ubuntu-latest

InvalidTemplateParam

src/Xml/Xmlns/RegisterNonConflictingXmlnsNamespaces.php:53:15: InvalidTemplateParam: Extended template param T of VeeWee\Xml\Dom\Collection\NodeList<DOMNameSpaceNode> expects type DOMNode, type DOMNameSpaceNode given (see https://psalm.dev/183)

Check failure on line 53 in src/Xml/Xmlns/RegisterNonConflictingXmlnsNamespaces.php

View workflow job for this annotation

GitHub Actions / PHP 8.3 @ ubuntu-latest

InvalidTemplateParam

src/Xml/Xmlns/RegisterNonConflictingXmlnsNamespaces.php:53:15: InvalidTemplateParam: Extended template param T of VeeWee\Xml\Dom\Collection\NodeList<DOMNameSpaceNode> expects type DOMNode, type DOMNameSpaceNode given (see https://psalm.dev/183)
*/
private function tryUsingExistingPrefix(
NodeList $existingLinkedNamespaces,
\DOMElement $newSchema,
\DOMNameSpaceNode $xmlns
): bool {
$existingPrefix = $existingLinkedNamespaces->filter(

Check failure on line 60 in src/Xml/Xmlns/RegisterNonConflictingXmlnsNamespaces.php

View workflow job for this annotation

GitHub Actions / PHP 8.1 @ ubuntu-latest

InvalidTemplateParam

src/Xml/Xmlns/RegisterNonConflictingXmlnsNamespaces.php:60:27: InvalidTemplateParam: Extended template param T of VeeWee\Xml\Dom\Collection\NodeList<DOMNameSpaceNode> expects type DOMNode, type DOMNameSpaceNode given (see https://psalm.dev/183)

Check failure on line 60 in src/Xml/Xmlns/RegisterNonConflictingXmlnsNamespaces.php

View workflow job for this annotation

GitHub Actions / PHP 8.2 @ ubuntu-latest

InvalidTemplateParam

src/Xml/Xmlns/RegisterNonConflictingXmlnsNamespaces.php:60:27: InvalidTemplateParam: Extended template param T of VeeWee\Xml\Dom\Collection\NodeList<DOMNameSpaceNode> expects type DOMNode, type DOMNameSpaceNode given (see https://psalm.dev/183)

Check failure on line 60 in src/Xml/Xmlns/RegisterNonConflictingXmlnsNamespaces.php

View workflow job for this annotation

GitHub Actions / PHP 8.3 @ ubuntu-latest

InvalidTemplateParam

src/Xml/Xmlns/RegisterNonConflictingXmlnsNamespaces.php:60:27: InvalidTemplateParam: Extended template param T of VeeWee\Xml\Dom\Collection\NodeList<DOMNameSpaceNode> expects type DOMNode, type DOMNameSpaceNode given (see https://psalm.dev/183)
static fn (\DOMNameSpaceNode $node) => $node->namespaceURI === $xmlns->namespaceURI
)->first()?->prefix;

if (!$existingPrefix) {

Check failure on line 64 in src/Xml/Xmlns/RegisterNonConflictingXmlnsNamespaces.php

View workflow job for this annotation

GitHub Actions / PHP 8.1 @ ubuntu-latest

RiskyTruthyFalsyComparison

src/Xml/Xmlns/RegisterNonConflictingXmlnsNamespaces.php:64:13: RiskyTruthyFalsyComparison: Operand of type null|string contains type string, which can be falsy and truthy. This can cause possibly unexpected behavior. Use strict comparison instead. (see https://psalm.dev/356)

Check failure on line 64 in src/Xml/Xmlns/RegisterNonConflictingXmlnsNamespaces.php

View workflow job for this annotation

GitHub Actions / PHP 8.2 @ ubuntu-latest

RiskyTruthyFalsyComparison

src/Xml/Xmlns/RegisterNonConflictingXmlnsNamespaces.php:64:13: RiskyTruthyFalsyComparison: Operand of type null|string contains type string, which can be falsy and truthy. This can cause possibly unexpected behavior. Use strict comparison instead. (see https://psalm.dev/356)

Check failure on line 64 in src/Xml/Xmlns/RegisterNonConflictingXmlnsNamespaces.php

View workflow job for this annotation

GitHub Actions / PHP 8.3 @ ubuntu-latest

RiskyTruthyFalsyComparison

src/Xml/Xmlns/RegisterNonConflictingXmlnsNamespaces.php:64:13: RiskyTruthyFalsyComparison: Operand of type null|string contains type string, which can be falsy and truthy. This can cause possibly unexpected behavior. Use strict comparison instead. (see https://psalm.dev/356)
return false;
}

$this->reprefixTypeDeclarations($newSchema, $xmlns->prefix, $existingPrefix);

return true;
}

private function tryUsingUniquePrefixHash(\DOMElement $existingSchema, \DOMElement $newSchema, \DOMNameSpaceNode $xmlns): bool
{
$uniquePrefix = 'ns' . substr(md5($xmlns->namespaceURI), 0, 8);

Check failure on line 75 in src/Xml/Xmlns/RegisterNonConflictingXmlnsNamespaces.php

View workflow job for this annotation

GitHub Actions / PHP 8.1 @ ubuntu-latest

PossiblyNullArgument

src/Xml/Xmlns/RegisterNonConflictingXmlnsNamespaces.php:75:43: PossiblyNullArgument: Argument 1 of md5 cannot be null, possibly null value provided (see https://psalm.dev/078)

Check failure on line 75 in src/Xml/Xmlns/RegisterNonConflictingXmlnsNamespaces.php

View workflow job for this annotation

GitHub Actions / PHP 8.2 @ ubuntu-latest

PossiblyNullArgument

src/Xml/Xmlns/RegisterNonConflictingXmlnsNamespaces.php:75:43: PossiblyNullArgument: Argument 1 of md5 cannot be null, possibly null value provided (see https://psalm.dev/078)

Check failure on line 75 in src/Xml/Xmlns/RegisterNonConflictingXmlnsNamespaces.php

View workflow job for this annotation

GitHub Actions / PHP 8.3 @ ubuntu-latest

PossiblyNullArgument

src/Xml/Xmlns/RegisterNonConflictingXmlnsNamespaces.php:75:43: PossiblyNullArgument: Argument 1 of md5 cannot be null, possibly null value provided (see https://psalm.dev/078)
if ($existingSchema->hasAttribute('xmlns:'.$uniquePrefix)) {



var_dump($xmlns->namespaceURI);
var_dump($existingSchema->ownerDocument->lookupPrefix($xmlns->namespaceURI));
var_dump($existingSchema->ownerDocument->lookupNamespaceURI($uniquePrefix));

echo $existingSchema->ownerDocument->saveXML();

die($uniquePrefix . 'already exists');

return false;
}

$this->copyXmlnsDeclaration($existingSchema, $xmlns->namespaceURI, $uniquePrefix);
$this->rePrefixTypeDeclarations($newSchema, $xmlns->prefix, $uniquePrefix);

return true;
}

private function copyXmlnsDeclaration(\DOMElement $existingSchema, string $namespaceUri, string $prefix): void
{
xmlns_attribute($prefix, $namespaceUri)($existingSchema);
}

private function rePrefixTypeDeclarations(\DOMElement $newSchema, string $originalPrefix, string $newPrefix): void
{
Document::fromUnsafeDocument($newSchema->ownerDocument)->traverse(new ReprefixTypeQname(
$originalPrefix,
$newPrefix
));
}
}

0 comments on commit 50261a2

Please sign in to comment.