From 9de8b26b1ce213c36076a5da41504b8c42330457 Mon Sep 17 00:00:00 2001 From: Toon Verwerft Date: Fri, 27 Sep 2024 07:42:43 +0200 Subject: [PATCH] Resolve conflicting XMLNS imports --- psalm.xml | 3 + src/Xml/Configurator/FlattenXsdImports.php | 31 +---- src/Xml/Visitor/ReprefixTypeQname.php | 38 ++++++ ...edDefaultXmlnsDeclarationsDuringImport.php | 30 +++++ .../RegisterNonConflictingXmlnsNamespaces.php | 115 ++++++++++++++++++ stubs/dom.phpstub | 7 ++ 6 files changed, 198 insertions(+), 26 deletions(-) create mode 100644 src/Xml/Visitor/ReprefixTypeQname.php create mode 100644 src/Xml/Xmlns/FixRemovedDefaultXmlnsDeclarationsDuringImport.php create mode 100644 src/Xml/Xmlns/RegisterNonConflictingXmlnsNamespaces.php create mode 100644 stubs/dom.phpstub diff --git a/psalm.xml b/psalm.xml index c362257..0dd8970 100644 --- a/psalm.xml +++ b/psalm.xml @@ -27,4 +27,7 @@ + + + diff --git a/src/Xml/Configurator/FlattenXsdImports.php b/src/Xml/Configurator/FlattenXsdImports.php index 05d5c56..4f0a484 100644 --- a/src/Xml/Configurator/FlattenXsdImports.php +++ b/src/Xml/Configurator/FlattenXsdImports.php @@ -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; @@ -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; @@ -170,6 +171,7 @@ private function loadSchema(string $location): ?DOMElement * This function registers the newly provided schema in the WSDL types section. * It groups all imports by targetNamespace. * + * @throws \RuntimeException * @throws RuntimeException * @throws AssertException */ @@ -187,42 +189,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: diff --git a/src/Xml/Visitor/ReprefixTypeQname.php b/src/Xml/Visitor/ReprefixTypeQname.php new file mode 100644 index 0000000..652b9a3 --- /dev/null +++ b/src/Xml/Visitor/ReprefixTypeQname.php @@ -0,0 +1,38 @@ +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(); + } +} diff --git a/src/Xml/Xmlns/FixRemovedDefaultXmlnsDeclarationsDuringImport.php b/src/Xml/Xmlns/FixRemovedDefaultXmlnsDeclarationsDuringImport.php new file mode 100644 index 0000000..3d1a244 --- /dev/null +++ b/src/Xml/Xmlns/FixRemovedDefaultXmlnsDeclarationsDuringImport.php @@ -0,0 +1,30 @@ +getAttribute('xmlns') || $target->hasAttribute('xmlns')) { + return; + } + + $target->setAttribute('xmlns', $source->getAttribute('xmlns')); + } +} diff --git a/src/Xml/Xmlns/RegisterNonConflictingXmlnsNamespaces.php b/src/Xml/Xmlns/RegisterNonConflictingXmlnsNamespaces.php new file mode 100644 index 0000000..7eb963c --- /dev/null +++ b/src/Xml/Xmlns/RegisterNonConflictingXmlnsNamespaces.php @@ -0,0 +1,115 @@ +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 $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 + )); + } +} diff --git a/stubs/dom.phpstub b/stubs/dom.phpstub new file mode 100644 index 0000000..73162e9 --- /dev/null +++ b/stubs/dom.phpstub @@ -0,0 +1,7 @@ +