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/FlattenWsdlImports.php b/src/Xml/Configurator/FlattenWsdlImports.php index 6646cc7..767b20e 100644 --- a/src/Xml/Configurator/FlattenWsdlImports.php +++ b/src/Xml/Configurator/FlattenWsdlImports.php @@ -16,6 +16,7 @@ use VeeWee\Xml\Exception\RuntimeException; use function VeeWee\Xml\Dom\Locator\document_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; use function VeeWee\Xml\Dom\Manipulator\Node\replace_by_external_nodes; @@ -82,6 +83,7 @@ private function importWsdlImportElement(DOMElement $import): void private function importWsdlPart(DOMElement $importElement, Document $importedDocument): void { $definitions = $importedDocument->map(document_element()); + copy_named_xmlns_attributes($importElement->ownerDocument->documentElement, $definitions); replace_by_external_nodes( $importElement, 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..dab0375 --- /dev/null +++ b/src/Xml/Visitor/ReprefixTypeQname.php @@ -0,0 +1,40 @@ + $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(); + } +} 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..95a4748 --- /dev/null +++ b/src/Xml/Xmlns/RegisterNonConflictingXmlnsNamespaces.php @@ -0,0 +1,129 @@ + + */ +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 $existingLinkedNamespaces + * + * @return Option + */ + 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 */ + return none(); + } + + /** @var Option */ + return some([$xmlns->prefix => $existingPrefix]); + } + + /** + * @return Option + * + * @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 */ + return none(); + } + + $this->copyXmlnsDeclaration($existingSchema, $xmlns->namespaceURI, $uniquePrefix); + + /** @var Option */ + return some([$xmlns->prefix => $uniquePrefix]); + } + + /** + * @throws RuntimeException + */ + private function copyXmlnsDeclaration(DOMElement $existingSchema, string $namespaceUri, string $prefix): void + { + xmlns_attribute($prefix, $namespaceUri)($existingSchema); + } +} 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 @@ + FIXTURE_DIR.'/flattening/import-multi-xsd.wsdl', 'expected' => Document::fromXmlFile(FIXTURE_DIR.'/flattening/result/import-multi-xsd-result.wsdl', comparable()), ]; + yield 'import-namespaces' => [ + 'wsdl' => FIXTURE_DIR.'/flattening/import-namespaces.wsdl', + 'expected' => Document::fromXmlFile(FIXTURE_DIR.'/flattening/result/import-namespaces.wsdl', comparable()), + ]; } } diff --git a/tests/Unit/Xml/Configurator/FlattenXsdImportsTest.php b/tests/Unit/Xml/Configurator/FlattenXsdImportsTest.php index fe06559..f6dff00 100644 --- a/tests/Unit/Xml/Configurator/FlattenXsdImportsTest.php +++ b/tests/Unit/Xml/Configurator/FlattenXsdImportsTest.php @@ -76,5 +76,10 @@ public function provideTestCases() 'expected' => Document::fromXmlFile(FIXTURE_DIR.'/flattening/result/rearranged-imports.wsdl'), comparable(), ]; + yield 'import-xmlns-issue' => [ + 'wsdl' => FIXTURE_DIR.'/flattening/conflicting-imports.wsdl', + 'expected' => Document::fromXmlFile(FIXTURE_DIR.'/flattening/result/conflicting-imports.wsdl'), + canonicalize(), + ]; } } diff --git a/tests/Unit/Xml/Visitor/ReprefixTypeQnameTest.php b/tests/Unit/Xml/Visitor/ReprefixTypeQnameTest.php new file mode 100644 index 0000000..0a41ee4 --- /dev/null +++ b/tests/Unit/Xml/Visitor/ReprefixTypeQnameTest.php @@ -0,0 +1,73 @@ +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' => [ + '', + '', + ]; + yield 'other-attribute' => [ + '', + '', + ]; + yield 'no-qualified' => [ + '', + '', + ]; + yield 'simple' => [ + '', + '', + ]; + yield 'element' => [ + '', + '', + ]; + yield 'attribute' => [ + '', + '', + ]; + yield 'nested-schema' => [ + << + + + + + EOXML, + << + + + + + EOXML, + ]; + yield 'dont-chain-reprefixes' => [ + '', + '', + ]; + } +} diff --git a/tests/Unit/Xml/Xmlns/RegisterNonConflictingXmlnsNamespacesTest.php b/tests/Unit/Xml/Xmlns/RegisterNonConflictingXmlnsNamespacesTest.php new file mode 100644 index 0000000..93a7a4a --- /dev/null +++ b/tests/Unit/Xml/Xmlns/RegisterNonConflictingXmlnsNamespacesTest.php @@ -0,0 +1,72 @@ +locateDocumentElement(), + $importedSchema->locateDocumentElement() + ); + + static::assertSame($expectedExistingSchemaXml, $existingSchema->stringifyDocumentElement()); + static::assertSame($expectedImportedSchemaXml, $importedSchema->stringifyDocumentElement()); + } + + public static function provideCases(): iterable + { + yield 'no-conflict' => [ + 'existingSchemaXml' => '', + 'importedSchemaXml' => '', + 'expectedExistingSchemaXml' => '', + 'expectedImportedSchemaXml' => '', + ]; + yield 'conflict' => [ + 'existingSchemaXml' => '', + 'importedSchemaXml' => '', + 'expectedExistingSchemaXml' => '', + 'expectedImportedSchemaXml' => '', + ]; + yield 'conflict-with-existing-alternative' => [ + 'existingSchemaXml' => '', + 'importedSchemaXml' => '', + 'expectedExistingSchemaXml' => '', + 'expectedImportedSchemaXml' => '', + ]; + yield 'multiple-conflicts' => [ + 'existingSchemaXml' => '', + 'importedSchemaXml' => << + + + + + EOXML, + 'expectedExistingSchemaXml' => '', + 'expectedImportedSchemaXml' => << + + + + + EOXML, + ]; + } +} diff --git a/tests/fixtures/flattening/conflicting-imports.wsdl b/tests/fixtures/flattening/conflicting-imports.wsdl new file mode 100644 index 0000000..f3793e6 --- /dev/null +++ b/tests/fixtures/flattening/conflicting-imports.wsdl @@ -0,0 +1,16 @@ + + + + + + + + + + diff --git a/tests/fixtures/flattening/import-namespaces.wsdl b/tests/fixtures/flattening/import-namespaces.wsdl new file mode 100644 index 0000000..ea75982 --- /dev/null +++ b/tests/fixtures/flattening/import-namespaces.wsdl @@ -0,0 +1,7 @@ + + + + diff --git a/tests/fixtures/flattening/result/conflicting-imports.wsdl b/tests/fixtures/flattening/result/conflicting-imports.wsdl new file mode 100644 index 0000000..afaa05a --- /dev/null +++ b/tests/fixtures/flattening/result/conflicting-imports.wsdl @@ -0,0 +1,20 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/fixtures/flattening/result/import-namespaces.wsdl b/tests/fixtures/flattening/result/import-namespaces.wsdl new file mode 100644 index 0000000..3a6f2fe --- /dev/null +++ b/tests/fixtures/flattening/result/import-namespaces.wsdl @@ -0,0 +1,9 @@ + + + + diff --git a/tests/fixtures/flattening/wsdl/import-namespaces.wsdl b/tests/fixtures/flattening/wsdl/import-namespaces.wsdl new file mode 100644 index 0000000..2fac0dc --- /dev/null +++ b/tests/fixtures/flattening/wsdl/import-namespaces.wsdl @@ -0,0 +1,8 @@ + + + diff --git a/tests/fixtures/flattening/xsd/conflicting-import.xsd b/tests/fixtures/flattening/xsd/conflicting-import.xsd new file mode 100644 index 0000000..61e1279 --- /dev/null +++ b/tests/fixtures/flattening/xsd/conflicting-import.xsd @@ -0,0 +1,9 @@ + + + + +