* (c) Francis Besset * * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ namespace BeSimple\SoapClient; use BeSimple\SoapCommon\Cache; use BeSimple\SoapCommon\Helper; /** * Downloads WSDL files with cURL. Uses the WSDL_CACHE_* constants and the * 'soap.wsdl_*' ini settings. Does only file caching as SoapClient only * supports a file name parameter. The class also resolves remote XML schema * includes. * * @author Andreas Schamberger */ class WsdlDownloader { /** * Cache enabled. * * @var bool */ protected $cacheEnabled; /** * Cache dir. * * @var string */ protected $cacheDir; /** * Cache TTL. * * @var int */ protected $cacheTtl; /** * cURL instance for downloads. * * @var unknown_type */ protected $curl; /** * Resolve WSDl/XSD includes. * * @var boolean */ protected $resolveRemoteIncludes = true; /** * Constructor. * * @param \BeSimple\SoapClient\Curl $curl Curl instance * @param boolean $resolveRemoteIncludes WSDL/XSD include enabled? * @param boolean $cacheWsdl Cache constant */ public function __construct(Curl $curl, $resolveRemoteIncludes = true, $cacheWsdl = Cache::TYPE_DISK) { $this->curl = $curl; $this->resolveRemoteIncludes = (Boolean) $resolveRemoteIncludes; // get current WSDL caching config $this->cacheEnabled = $cacheWsdl === Cache::TYPE_NONE ? Cache::DISABLED : Cache::ENABLED == Cache::isEnabled(); if ($this->cacheEnabled) { $this->cacheDir = Cache::getDirectory(); $this->cacheTtl = Cache::getLifetime(); } } /** * Download given WSDL file and return name of cache file. * * @param string $wsdl WSDL file URL/path * * @return string */ public function download($wsdl) { // download and cache remote WSDL files or local ones where we want to // resolve remote XSD includes $isRemoteFile = $this->isRemoteFile($wsdl); if ($isRemoteFile || $this->resolveRemoteIncludes) { $cacheFilePath = $this->cacheDir.DIRECTORY_SEPARATOR.'wsdl_'.md5($wsdl).'.cache'; if (!$this->cacheEnabled || !file_exists($cacheFilePath) || (filemtime($cacheFilePath) + $this->cacheTtl) < time()) { if ($isRemoteFile) { // execute request $responseSuccessfull = $this->curl->exec($wsdl); // get content if ($responseSuccessfull) { $response = $this->curl->getResponseBody(); if ($this->resolveRemoteIncludes) { $this->resolveRemoteIncludes($response, $cacheFilePath, $wsdl); } else { file_put_contents($cacheFilePath, $response); } } else { throw new \ErrorException("SOAP-ERROR: Parsing WSDL: Couldn't load from '" . $wsdl ."'"); } } elseif (file_exists($wsdl)) { $response = file_get_contents($wsdl); $this->resolveRemoteIncludes($response, $cacheFilePath); } else { throw new \ErrorException("SOAP-ERROR: Parsing WSDL: Couldn't load from '" . $wsdl ."'"); } } return $cacheFilePath; } elseif (file_exists($wsdl)) { return realpath($wsdl); } throw new \ErrorException("SOAP-ERROR: Parsing WSDL: Couldn't load from '" . $wsdl ."'"); } /** * Do we have a remote file? * * @param string $file File URL/path * * @return boolean */ private function isRemoteFile($file) { // @parse_url to suppress E_WARNING for invalid urls if (false !== $url = @parse_url($file)) { if (isset($url['scheme']) && 'http' === substr($url['scheme'], 0, 4)) { return true; } } return false; } /** * Resolves remote WSDL/XSD includes within the WSDL files. * * @param string $xml XML file * @param string $cacheFilePath Cache file name * @param boolean $parentFilePath Parent file name * * @return void */ private function resolveRemoteIncludes($xml, $cacheFilePath, $parentFilePath = null) { $doc = new \DOMDocument(); $doc->loadXML($xml); $xpath = new \DOMXPath($doc); $xpath->registerNamespace(Helper::PFX_XML_SCHEMA, Helper::NS_XML_SCHEMA); $xpath->registerNamespace(Helper::PFX_WSDL, Helper::NS_WSDL); // WSDL include/import $query = './/'.Helper::PFX_WSDL.':include | .//'.Helper::PFX_WSDL.':import'; $nodes = $xpath->query($query); if ($nodes->length > 0) { foreach ($nodes as $node) { $location = $node->getAttribute('location'); if ($this->isRemoteFile($location)) { $location = $this->download($location); $node->setAttribute('location', $location); } elseif (null !== $parentFilePath) { $location = $this->resolveRelativePathInUrl($parentFilePath, $location); $location = $this->download($location); $node->setAttribute('location', $location); } } } // XML schema include/import $query = './/'.Helper::PFX_XML_SCHEMA.':include | .//'.Helper::PFX_XML_SCHEMA.':import'; $nodes = $xpath->query($query); if ($nodes->length > 0) { foreach ($nodes as $node) { if ($node->hasAttribute('schemaLocation')) { $schemaLocation = $node->getAttribute('schemaLocation'); if ($this->isRemoteFile($schemaLocation)) { $schemaLocation = $this->download($schemaLocation); $node->setAttribute('schemaLocation', $schemaLocation); } elseif (null !== $parentFilePath) { $schemaLocation = $this->resolveRelativePathInUrl($parentFilePath, $schemaLocation); $schemaLocation = $this->download($schemaLocation); $node->setAttribute('schemaLocation', $schemaLocation); } } } } $doc->save($cacheFilePath); } /** * 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']) && '/' === $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); } }