diff --git a/src/BeSimple/SoapClient/WsdlDownloader.php b/src/BeSimple/SoapClient/WsdlDownloader.php index e3a5d0b..1c1d413 100644 --- a/src/BeSimple/SoapClient/WsdlDownloader.php +++ b/src/BeSimple/SoapClient/WsdlDownloader.php @@ -13,6 +13,8 @@ namespace BeSimple\SoapClient; use BeSimple\SoapClient\Curl\Curl; +use BeSimple\SoapClient\Xml\RemoteFileResolver; +use BeSimple\SoapClient\Xml\XmlFileDomDocumentProcessor; use BeSimple\SoapCommon\Cache; use BeSimple\SoapCommon\Helper; use DOMDocument; @@ -30,6 +32,11 @@ use Exception; */ class WsdlDownloader { + public static function instantiateDownloader() + { + return new self(); + } + /** * @param Curl $curl * @param string $wsdlPath WSDL file URL/path @@ -39,10 +46,10 @@ class WsdlDownloader */ public function getWsdlPath(Curl $curl, $wsdlPath, $wsdCacheType, $resolveRemoteIncludes = true) { - $isRemoteFile = $this->isRemoteFile($wsdlPath); + $isRemoteFile = RemoteFileResolver::instantiateResolver()->isRemoteFile($wsdlPath); $isCacheEnabled = $wsdCacheType === Cache::TYPE_NONE ? false : Cache::isEnabled(); if ($isCacheEnabled === true) { - $cacheFilePath = Cache::getDirectory().DIRECTORY_SEPARATOR.'wsdl_'.md5($wsdlPath).'.cache'; + $cacheFilePath = Cache::getDirectory() . DIRECTORY_SEPARATOR . 'wsdl_' . md5($wsdlPath) . '.cache'; $isCacheExisting = file_exists($cacheFilePath); if ($isCacheExisting) { $fileModificationTime = filemtime($cacheFilePath); @@ -54,46 +61,17 @@ class WsdlDownloader $isCacheExisting = $isCacheValid = false; } if ($isCacheExisting === false || $isCacheValid === false) { - $this->writeCacheFile($curl, $wsdCacheType, $wsdlPath, $cacheFilePath, $resolveRemoteIncludes, $isRemoteFile); + XmlFileDomDocumentProcessor::writeCacheFile($curl, $wsdCacheType, $wsdlPath, $cacheFilePath, $resolveRemoteIncludes, $isRemoteFile); } return $this->getLocalWsdlPath($cacheFilePath); - } else { - - if ($isRemoteFile === true) { - return $wsdlPath; - } - - return $this->getLocalWsdlPath($wsdlPath); } - } - - private function writeCacheFile(Curl $curl, $cacheType, $wsdlPath, $cacheFilePath, $resolveRemoteIncludes, $isRemoteFile) - { if ($isRemoteFile === true) { - $curlResponse = $curl->executeCurlWithCachedSession($wsdlPath); - if ($curlResponse->curlStatusSuccess()) { - if (mb_strlen($curlResponse->getResponseBody()) === 0) { - throw new Exception('Could not write WSDL cache file: empty curl response from: '.$wsdlPath); - } - if ($resolveRemoteIncludes === true) { - $document = $this->getXmlFileDomDocument($curl, $cacheType, $curlResponse->getResponseBody(), $wsdlPath); - $this->saveXmlDomDocument($document, $cacheFilePath); - } else { - file_put_contents($cacheFilePath, $curlResponse->getResponseBody()); - } - } else { - throw new Exception('Could not write WSDL cache file: Download failed with message: '.$curlResponse->getCurlErrorMessage()); - } - } else { - if (file_exists($wsdlPath)) { - $document = $this->getXmlFileDomDocument($curl, $cacheType, file_get_contents($wsdlPath)); - $this->saveXmlDomDocument($document, $cacheFilePath); - } else { - throw new Exception('Could write WSDL cache file: local file does not exist: '.$wsdlPath); - } + return $wsdlPath; } + + return $this->getLocalWsdlPath($wsdlPath); } private function getLocalWsdlPath($wsdlPath) @@ -104,164 +82,6 @@ class WsdlDownloader } - throw new Exception('Could not download WSDL: local file does not exist: '.$wsdlPath); - } - - /** - * @param string $wsdlPath File URL/path - * @return boolean - */ - private function isRemoteFile($wsdlPath) - { - $parsedUrlOrFalse = @parse_url($wsdlPath); - if ($parsedUrlOrFalse !== false) { - if (isset($parsedUrlOrFalse['scheme']) && substr($parsedUrlOrFalse['scheme'], 0, 4) === 'http') { - - return true; - } - - return false; - } - - throw new Exception('Could not determine wsdlPath is remote: '.$wsdlPath); - } - - /** - * Resolves remote WSDL/XSD includes within the WSDL files. - * - * @param Curl $curl - * @param int $cacheType - * @param string $xmlFileSource XML file contents - * @param boolean $parentFilePath Parent file name - * @return DOMDocument - */ - private function getXmlFileDomDocument(Curl $curl, $cacheType, $xmlFileSource, $parentFilePath = null) - { - $document = new DOMDocument('1.0', 'utf-8'); - if ($document->loadXML($xmlFileSource) === false) { - throw new Exception('Could not save downloaded WSDL cache: '.$xmlFileSource); - } - - $xpath = new DOMXPath($document); - $this->updateXmlDocument($curl, $cacheType, $xpath, Helper::PFX_WSDL, Helper::NS_WSDL, 'location', $parentFilePath); - $this->updateXmlDocument($curl, $cacheType, $xpath, Helper::PFX_XML_SCHEMA, Helper::NS_XML_SCHEMA, 'schemaLocation', $parentFilePath); - - return $document; - } - - private function saveXmlDomDocument(DOMDocument $document, $cacheFilePath) - { - try { - $xmlContents = $document->saveXML(); - if ($xmlContents === '') { - throw new Exception('Could not write WSDL cache file: DOMDocument returned empty XML file'); - } - file_put_contents($cacheFilePath, $xmlContents); - } catch (Exception $e) { - unlink($cacheFilePath); - throw new Exception('Could not write WSDL cache file: save method returned error: ' . $e->getMessage()); - } - } - - private function updateXmlDocument( - Curl $curl, - $cacheType, - DOMXPath $xpath, - $schemaPrefix, - $schemaUrl, - $locationAttributeName, - $parentFilePath = null - ) { - $xpath->registerNamespace($schemaPrefix, $schemaUrl); - $nodes = $xpath->query('.//'.$schemaPrefix.':include | .//'.$schemaPrefix.':import'); - if ($nodes->length > 0) { - foreach ($nodes as $node) { - /** @var DOMElement $node */ - $locationPath = $node->getAttribute($locationAttributeName); - if ($locationPath !== '') { - if ($this->isRemoteFile($locationPath)) { - $node->setAttribute( - $locationAttributeName, - $this->getWsdlPath( - $curl, - $locationPath, - $cacheType, - true - ) - ); - } elseif ($parentFilePath !== null) { - $node->setAttribute( - $locationAttributeName, - $this->getWsdlPath( - $curl, - $this->resolveRelativePathInUrl($parentFilePath, $locationPath), - $cacheType, - true - ) - ); - } - } - } - } - } - - /** - * Resolves the relative path to base into an absolute. - * - * @param string $base Base path - * @param string $relative Relative path - * - * @return string - */ - private function resolveRelativePathInUrl($base, $relative) - { - $urlParts = parse_url($base); - - // combine base path with relative path - if (isset($urlParts['path']) && mb_strlen($relative) > 0 && '/' === $relative{0}) { - // $relative is absolute path from domain (starts with /) - $path = $relative; - } elseif (isset($urlParts['path']) && strrpos($urlParts['path'], '/') === (strlen($urlParts['path']) )) { - // base path is directory - $path = $urlParts['path'].$relative; - } elseif (isset($urlParts['path'])) { - // strip filename from base path - $path = substr($urlParts['path'], 0, strrpos($urlParts['path'], '/')).'/'.$relative; - } else { - // no base path - $path = '/'.$relative; - } - - // foo/./bar ==> foo/bar - // remove double slashes - $path = preg_replace(array('#/\./#', '#/+#'), '/', $path); - - // split path by '/' - $parts = explode('/', $path); - - // resolve /../ - foreach ($parts as $key => $part) { - if ($part === '..') { - $keyToDelete = $key - 1; - while ($keyToDelete > 0) { - if (isset($parts[$keyToDelete])) { - unset($parts[$keyToDelete]); - - break; - } - - $keyToDelete--; - } - - unset($parts[$key]); - } - } - - $hostname = $urlParts['scheme'].'://'.$urlParts['host']; - if (isset($urlParts['port'])) { - $hostname .= ':'.$urlParts['port']; - } - - return $hostname.implode('/', $parts); + throw new Exception('Could not download WSDL: local file does not exist: ' . $wsdlPath); } } diff --git a/src/BeSimple/SoapClient/Xml/Path/RelativePathResolver.php b/src/BeSimple/SoapClient/Xml/Path/RelativePathResolver.php new file mode 100644 index 0000000..2417478 --- /dev/null +++ b/src/BeSimple/SoapClient/Xml/Path/RelativePathResolver.php @@ -0,0 +1,75 @@ + 0 && $isRelativePathAbsolute) { + // $relative is absolute path from domain (starts with /) + $path = $relative; + } elseif (isset($urlParts['path']) && strrpos($urlParts['path'], '/') === (strlen($urlParts['path']) )) { + // base path is directory + $path = $urlParts['path'].$relative; + } elseif (isset($urlParts['path'])) { + // strip filename from base path + $path = substr($urlParts['path'], 0, strrpos($urlParts['path'], '/')).'/'.$relative; + } else { + // no base path + $path = '/'.$relative; + } + + // foo/./bar ==> foo/bar + // remove double slashes + $path = preg_replace(array('#/\./#', '#/+#'), '/', $path); + + // split path by '/' + $parts = explode('/', $path); + + // resolve /../ + foreach ($parts as $key => $part) { + if ($part === '..') { + $keyToDelete = $key - 1; + while ($keyToDelete > 0) { + if (isset($parts[$keyToDelete])) { + unset($parts[$keyToDelete]); + + break; + } + + $keyToDelete--; + } + + unset($parts[$key]); + } + } + + $hostname = $urlParts['scheme'].'://'.$urlParts['host']; + if (isset($urlParts['port'])) { + $hostname .= ':'.$urlParts['port']; + } + $implodedParts = implode('/', $parts); + if (substr($implodedParts, 0, 1) !== '/') { + $implodedParts = '/'.$implodedParts; + } + + return $hostname.$implodedParts; + } +} diff --git a/src/BeSimple/SoapClient/Xml/RemoteFileResolver.php b/src/BeSimple/SoapClient/Xml/RemoteFileResolver.php new file mode 100644 index 0000000..0f6d0e7 --- /dev/null +++ b/src/BeSimple/SoapClient/Xml/RemoteFileResolver.php @@ -0,0 +1,32 @@ +registerNamespace($schemaPrefix, $schemaUrl); + $nodes = $xpath->query('.//'.$schemaPrefix.':include | .//'.$schemaPrefix.':import'); + if ($nodes->length > 0) { + foreach ($nodes as $node) { + /** @var DOMElement $node */ + $locationPath = $node->getAttribute($locationAttributeName); + if ($locationPath !== '') { + if (RemoteFileResolver::instantiateResolver()->isRemoteFile($locationPath)) { + $node->setAttribute( + $locationAttributeName, + WsdlDownloader::instantiateDownloader()->getWsdlPath( + $curl, + $locationPath, + $cacheType, + true + ) + ); + } elseif ($parentFilePath !== null) { + $node->setAttribute( + $locationAttributeName, + WsdlDownloader::instantiateDownloader()->getWsdlPath( + $curl, + self::resolveRelativePathInUrl($parentFilePath, $locationPath), + $cacheType, + true + ) + ); + } + } + } + } + } + + /** + * Resolves the relative path to base into an absolute. + * + * @param string $base Base path + * @param string $relative Relative path + * + * @return string + */ + private static function resolveRelativePathInUrl($base, $relative) + { + $urlParts = parse_url($base); + $isRelativePathAbsolute = 0 === strpos($relative, '/') || 0 === strpos($relative, '..'); + + // combine base path with relative path + if (isset($urlParts['path']) && mb_strlen($relative) > 0 && $isRelativePathAbsolute) { + // $relative is absolute path from domain (starts with /) + $path = $relative; + } elseif (isset($urlParts['path']) && strrpos($urlParts['path'], '/') === (strlen($urlParts['path']) )) { + // base path is directory + $path = $urlParts['path'].$relative; + } elseif (isset($urlParts['path'])) { + // strip filename from base path + $path = substr($urlParts['path'], 0, strrpos($urlParts['path'], '/')).'/'.$relative; + } else { + // no base path + $path = '/'.$relative; + } + + // foo/./bar ==> foo/bar + // remove double slashes + $path = preg_replace(array('#/\./#', '#/+#'), '/', $path); + + // split path by '/' + $parts = explode('/', $path); + + // resolve /../ + foreach ($parts as $key => $part) { + if ($part === '..') { + $keyToDelete = $key - 1; + while ($keyToDelete > 0) { + if (isset($parts[$keyToDelete])) { + unset($parts[$keyToDelete]); + + break; + } + + $keyToDelete--; + } + + unset($parts[$key]); + } + } + + $hostname = $urlParts['scheme'].'://'.$urlParts['host']; + if (isset($urlParts['port'])) { + $hostname .= ':'.$urlParts['port']; + } + if (substr($hostname, -1) !== '/') { + $hostname .= '/'; + } + + return $hostname.implode('/', $parts); + } +} diff --git a/src/BeSimple/SoapClient/Xml/XmlFileDomDocumentProcessor.php b/src/BeSimple/SoapClient/Xml/XmlFileDomDocumentProcessor.php new file mode 100644 index 0000000..1e8f079 --- /dev/null +++ b/src/BeSimple/SoapClient/Xml/XmlFileDomDocumentProcessor.php @@ -0,0 +1,77 @@ +executeCurlWithCachedSession($wsdlPath); + if ($curlResponse->curlStatusSuccess()) { + if (mb_strlen($curlResponse->getResponseBody()) === 0) { + throw new Exception('Could not write WSDL cache file: empty curl response from: '.$wsdlPath); + } + if ($resolveRemoteIncludes === true) { + $document = self::getXmlFileDomDocument($curl, $cacheType, $curlResponse->getResponseBody(), $wsdlPath); + self::saveXmlDomDocument($document, $cacheFilePath); + } else { + file_put_contents($cacheFilePath, $curlResponse->getResponseBody()); + } + } else { + throw new Exception('Could not write WSDL cache file: Download failed with message: '.$curlResponse->getCurlErrorMessage()); + } + } else { + if (file_exists($wsdlPath)) { + $document = self::getXmlFileDomDocument($curl, $cacheType, file_get_contents($wsdlPath)); + self::saveXmlDomDocument($document, $cacheFilePath); + } else { + throw new Exception('Could write WSDL cache file: local file does not exist: '.$wsdlPath); + } + } + } + + /** + * Resolves remote WSDL/XSD includes within the WSDL files. + * + * @param Curl $curl + * @param int $cacheType + * @param string $xmlFileSource XML file contents + * @param boolean $parentFilePath Parent file name + * @return DOMDocument + */ + private static function getXmlFileDomDocument(Curl $curl, $cacheType, $xmlFileSource, $parentFilePath = null) + { + $document = new DOMDocument('1.0', 'utf-8'); + if ($document->loadXML($xmlFileSource) === false) { + throw new Exception('Could not save downloaded WSDL cache: '.$xmlFileSource); + } + + $xpath = new DOMXPath($document); + $xmlDomDocumentImportReplacer = XmlDomDocumentImportReplacer::instantiateReplacer(); + $xmlDomDocumentImportReplacer->updateXmlDocument($curl, $cacheType, $xpath, Helper::PFX_WSDL, Helper::NS_WSDL, 'location', $parentFilePath); + $xmlDomDocumentImportReplacer->updateXmlDocument($curl, $cacheType, $xpath, Helper::PFX_XML_SCHEMA, Helper::NS_XML_SCHEMA, 'schemaLocation', $parentFilePath); + + return $document; + } + + private static function saveXmlDomDocument(DOMDocument $document, $cacheFilePath) + { + try { + $xmlContents = $document->saveXML(); + if ($xmlContents === '') { + throw new Exception('Could not write WSDL cache file: DOMDocument returned empty XML file'); + } + file_put_contents($cacheFilePath, $xmlContents); + } catch (Exception $e) { + unlink($cacheFilePath); + throw new Exception('Could not write WSDL cache file: save method returned error: ' . $e->getMessage()); + } + } +} diff --git a/tests/BeSimple/SoapClient/Xml/Path/RelativePathResolverTest.php b/tests/BeSimple/SoapClient/Xml/Path/RelativePathResolverTest.php new file mode 100644 index 0000000..728554e --- /dev/null +++ b/tests/BeSimple/SoapClient/Xml/Path/RelativePathResolverTest.php @@ -0,0 +1,65 @@ +relativePathResolver = new RelativePathResolver(); + } + + /** + * @param string $base + * @param string $relative + * @param string $assertPath + * @dataProvider providePathInfo + */ + public function testResolveRelativePathInUrl($base, $relative, $assertPath) + { + $path = $this->relativePathResolver->resolveRelativePathInUrl($base, $relative); + + self::assertEquals($assertPath, $path); + } + + public function providePathInfo() + { + return [ + [ + 'http://endpoint-location.ltd/', + 'Document1.xsd', + 'http://endpoint-location.ltd/Document1.xsd', + ], + [ + 'http://endpoint-location.ltd:8080/endpoint/', + '../Schemas/Common/Document2.xsd', + 'http://endpoint-location.ltd:8080/Schemas/Common/Document2.xsd', + ], + [ + 'http://endpoint-location.ltd/', + '../Schemas/Common/Document3.xsd', + 'http://endpoint-location.ltd/Schemas/Common/Document3.xsd', + ], + [ + 'http://endpoint-location.ltd/', + '/Document4.xsd', + 'http://endpoint-location.ltd/Document4.xsd', + ], + [ + 'http://endpoint-location.ltd', + '/Document5.xsd', + 'http://endpoint-location.ltd/Document5.xsd', + ], + [ + 'http://endpoint-location.ltd', + 'Document6.xsd', + 'http://endpoint-location.ltd/Document6.xsd', + ] + ]; + } +} diff --git a/tests/BeSimple/SoapClient/Xml/RemoteFileResolverTest.php b/tests/BeSimple/SoapClient/Xml/RemoteFileResolverTest.php new file mode 100644 index 0000000..9e80a14 --- /dev/null +++ b/tests/BeSimple/SoapClient/Xml/RemoteFileResolverTest.php @@ -0,0 +1,42 @@ +remoteFileResolver = new RemoteFileResolver(); + } + + /** + * @param string $wsdlPath + * @param bool $assertIsRemoteFile + * @dataProvider provideWsdlPaths + */ + public function testIsRemoteFile($wsdlPath, $assertIsRemoteFile) + { + $isRemoteFile = $this->remoteFileResolver->isRemoteFile($wsdlPath); + + self::assertEquals($assertIsRemoteFile, $isRemoteFile); + } + + public function provideWsdlPaths() + { + return [ + ['http://endpoint.tld/path/to/wsdl.wsdl', self::FILE_IS_REMOTE], + ['http://endpoint.tld:1944/path/to/wsdl.wsdl', self::FILE_IS_REMOTE], + ['path/to/wsdl.wsdl', self::FILE_IS_NOT_REMOTE], + ['../../path/to/wsdl.wsdl', self::FILE_IS_NOT_REMOTE], + ['/path/to/wsdl.wsdl', self::FILE_IS_NOT_REMOTE], + ]; + } +} diff --git a/tests/BeSimple/SoapClient/Xml/XmlDomDocumentImportReplacerTest.php b/tests/BeSimple/SoapClient/Xml/XmlDomDocumentImportReplacerTest.php new file mode 100644 index 0000000..0f2c4b9 --- /dev/null +++ b/tests/BeSimple/SoapClient/Xml/XmlDomDocumentImportReplacerTest.php @@ -0,0 +1,105 @@ +xmlDomDocumentImportReplacer = new XmlDomDocumentImportReplacer(); + } + + /** + * @param string $xmlSource + * @param Curl $curl + * @param string $schemaPrefix + * @param string $schemaUrl + * @param string $locationAttributeName + * @param string|null $parentFilePath + * @param string|null $assertImportXmlSource + * @dataProvider provideXmlDocumentData + */ + public function testUpdateXmlDocument( + $xmlSource, + Curl $curl, + $schemaPrefix, + $schemaUrl, + $locationAttributeName, + $parentFilePath = null, + $assertImportXmlSource = null + ) { + $wsdl = new DOMDocument(); + $wsdl->loadXML($xmlSource); + + $this->xmlDomDocumentImportReplacer->updateXmlDocument( + $curl, + Cache::TYPE_NONE, + new DOMXPath($wsdl), + $schemaPrefix, + $schemaUrl, + $locationAttributeName, + $parentFilePath + ); + $wsdlSource = $wsdl->saveHTML(); + + self::assertContains( + $assertImportXmlSource, + $wsdlSource + ); + } + + public function provideXmlDocumentData() + { + return [ + 'wsdlWithoutParentPath' => [ + file_get_contents(__DIR__.'/testUpdateXmlDocument.wsdl'), + new Curl(CurlOptionsBuilder::buildDefault()), + Helper::PFX_WSDL, + Helper::NS_WSDL, + 'location', + self::NO_PARENT_FILE_PATH, + '' + ], + 'schemaWithoutParentPath' => [ + file_get_contents(__DIR__.'/testUpdateXmlDocument.wsdl'), + new Curl(CurlOptionsBuilder::buildDefault()), + Helper::PFX_XML_SCHEMA, + Helper::NS_XML_SCHEMA, + 'schemaLocation', + self::NO_PARENT_FILE_PATH, + '' + ], + 'wsdlWithParentPath' => [ + file_get_contents(__DIR__.'/testUpdateXmlDocument.wsdl'), + new Curl(CurlOptionsBuilder::buildDefault()), + Helper::PFX_WSDL, + Helper::NS_WSDL, + 'location', + 'http://endpoint-location.ltd:8080/endpoint/', + '' + ], + 'schemaWithParentPath' => [ + file_get_contents(__DIR__.'/testUpdateXmlDocument.wsdl'), + new Curl(CurlOptionsBuilder::buildDefault()), + Helper::PFX_XML_SCHEMA, + Helper::NS_XML_SCHEMA, + 'schemaLocation', + 'http://endpoint-location.ltd:8080/endpoint/', + '' + ], + ]; + } +} diff --git a/tests/BeSimple/SoapClient/Xml/testUpdateXmlDocument.wsdl b/tests/BeSimple/SoapClient/Xml/testUpdateXmlDocument.wsdl new file mode 100644 index 0000000..209f0ca --- /dev/null +++ b/tests/BeSimple/SoapClient/Xml/testUpdateXmlDocument.wsdl @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + TEST-OPERATION-1 + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file