diff --git a/src/BeSimple/SoapClient/Curl.php b/src/BeSimple/SoapClient/Curl.php new file mode 100644 index 0000000..e7842dd --- /dev/null +++ b/src/BeSimple/SoapClient/Curl.php @@ -0,0 +1,323 @@ + + * (c) Francis Besset + * (c) Andreas Schamberger + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + * + * @link https://github.com/BeSimple/BeSimpleSoapClient + */ + +namespace BeSimple\SoapClient; + +/** + * cURL wrapper class for doing HTTP requests that uses the soap class options. + * + * @author Andreas Schamberger + */ +class Curl +{ + /** + * HTTP User Agent. + * + * @var string + */ + const USER_AGENT = 'PHP-SOAP/\BeSimple\SoapClient'; + + /** + * Curl resource. + * + * @var resource + */ + private $ch; + + /** + * Maximum number of location headers to follow. + * + * @var int + */ + private $followLocationMaxRedirects; + + /** + * Request response data. + * + * @var string + */ + private $response; + + /** + * Constructor. + * + * @param array $options + * @param int $followLocationMaxRedirects + */ + public function __construct( array $options, $followLocationMaxRedirects = 10 ) + { + // set the default HTTP user agent + if ( !isset( $options['user_agent'] ) ) + { + $options['user_agent'] = self::USER_AGENT; + } + $this->followLocationMaxRedirects = $followLocationMaxRedirects; + + // make http request + $this->ch = curl_init(); + $curlOptions = array( + CURLOPT_ENCODING => '', + CURLOPT_SSL_VERIFYPEER => false, + CURLOPT_FAILONERROR => false, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, + CURLOPT_HEADER => true, + CURLOPT_USERAGENT => $options['user_agent'], + CURLINFO_HEADER_OUT => true, + ); + curl_setopt_array( $this->ch, $curlOptions ); + if ( isset( $options['compression'] ) && !( $options['compression'] & SOAP_COMPRESSION_ACCEPT ) ) + { + curl_setopt( $this->ch, CURLOPT_ENCODING, 'identity' ); + } + if ( isset( $options['connection_timeout'] ) ) + { + curl_setopt( $this->ch, CURLOPT_CONNECTTIMEOUT, $options['connection_timeout'] ); + } + if ( isset( $options['proxy_host'] ) ) + { + $port = isset( $options['proxy_port'] ) ? $options['proxy_port'] : 8080; + curl_setopt( $this->ch, CURLOPT_PROXY, $options['proxy_host'] . ':' . $port ); + } + if ( isset( $options['proxy_user'] ) ) + { + curl_setopt( $this->ch, CURLOPT_PROXYUSERPWD, $options['proxy_user'] . ':' . $options['proxy_password'] ); + } + if ( isset( $options['login'] ) ) + { + curl_setopt( $this->ch, CURLOPT_HTTPAUTH, CURLAUTH_ANY ); + curl_setopt( $this->ch, CURLOPT_USERPWD, $options['login'].':'.$options['password'] ); + } + if ( isset( $options['local_cert'] ) ) + { + curl_setopt( $this->ch, CURLOPT_SSLCERT, $options['local_cert'] ); + curl_setopt( $this->ch, CURLOPT_SSLCERTPASSWD, $options['passphrase'] ); + } + } + + /** + * Destructor. + */ + public function __destruct() + { + curl_close( $this->ch ); + } + + /** + * Execute HTTP request. + * Returns true if request was successfull. + * + * @param string $location + * @param string $request + * @param array $requestHeaders + * @return bool + */ + public function exec( $location, $request = null, $requestHeaders = array() ) + { + curl_setopt( $this->ch, CURLOPT_URL, $location); + + if ( !is_null( $request ) ) + { + curl_setopt( $this->ch, CURLOPT_POST, true ); + curl_setopt( $this->ch, CURLOPT_POSTFIELDS, $request ); + } + + if ( count( $requestHeaders ) > 0 ) + { + curl_setopt( $this->ch, CURLOPT_HTTPHEADER, $requestHeaders ); + } + + $this->response = $this->execManualRedirect( $this->followLocationMaxRedirects ); + + return ( $this->response === false ) ? false : true; + } + + /** + * Custom curl_exec wrapper that allows to follow redirects when specific + * http response code is set. SOAP only allows 307. + * + * @param resource $ch + * @param int $maxRedirects + * @param int $redirects + * @return mixed + */ + private function execManualRedirect( $redirects = 0 ) + { + if ( $redirects > $this->followLocationMaxRedirects ) + { + // TODO Redirection limit reached, aborting + return false; + } + curl_setopt( $this->ch, CURLOPT_HEADER, true ); + curl_setopt( $this->ch, CURLOPT_RETURNTRANSFER, true ); + $response = curl_exec( $this->ch ); + $httpResponseCode = curl_getinfo( $this->ch, CURLINFO_HTTP_CODE ); + if ( $httpResponseCode == 307 ) + { + $headerSize = curl_getinfo( $this->ch, CURLINFO_HEADER_SIZE ); + $header = substr( $response, 0, $headerSize ); + $matches = array(); + preg_match( '/Location:(.*?)\n/', $header, $matches ); + $url = trim( array_pop( $matches ) ); + // @parse_url to suppress E_WARNING for invalid urls + if ( ( $url = @parse_url( $url ) ) !== false ) + { + $lastUrl = parse_url( curl_getinfo( $this->ch, CURLINFO_EFFECTIVE_URL ) ); + if ( !isset( $url['scheme'] ) ) + { + $url['scheme'] = $lastUrl['scheme']; + } + if ( !isset( $url['host'] ) ) + { + $url['host'] = $lastUrl['host']; + } + if ( !isset( $url['path'] ) ) + { + $url['path'] = $lastUrl['path']; + } + $newUrl = $url['scheme'] . '://' . $url['host'] . $url['path'] . ( $url['query'] ? '?' . $url['query'] : '' ); + curl_setopt( $this->ch, CURLOPT_URL, $newUrl ); + return $this->execManualRedirect( $redirects++ ); + } + } + return $response; + } + + /** + * Error code mapping from cURL error codes to PHP ext/soap error messages + * (where applicable) + * + * http://curl.haxx.se/libcurl/c/libcurl-errors.html + * + * @var array(int=>string) + */ + protected function getErrorCodeMapping() + { + return array( + 1 => 'Unknown protocol. Only http and https are allowed.', //CURLE_UNSUPPORTED_PROTOCOL + 3 => 'Unable to parse URL', //CURLE_URL_MALFORMAT + 5 => 'Could not connect to host', //CURLE_COULDNT_RESOLVE_PROXY + 6 => 'Could not connect to host', //CURLE_COULDNT_RESOLVE_HOST + 7 => 'Could not connect to host', //CURLE_COULDNT_CONNECT + 9 => 'Could not connect to host', //CURLE_REMOTE_ACCESS_DENIED + 28 => 'Failed Sending HTTP SOAP request', //CURLE_OPERATION_TIMEDOUT + 35 => 'Could not connect to host', //CURLE_SSL_CONNECT_ERROR + 41 => 'Can\'t uncompress compressed response', //CURLE_FUNCTION_NOT_FOUND + 51 => 'Could not connect to host', //CURLE_PEER_FAILED_VERIFICATION + 52 => 'Error Fetching http body, No Content-Length, connection closed or chunked data', //CURLE_GOT_NOTHING + 53 => 'SSL support is not available in this build', //CURLE_SSL_ENGINE_NOTFOUND + 54 => 'SSL support is not available in this build', //CURLE_SSL_ENGINE_SETFAILED + 55 => 'Failed Sending HTTP SOAP request', //CURLE_SEND_ERROR + 56 => 'Error Fetching http body, No Content-Length, connection closed or chunked data', //CURLE_RECV_ERROR + 58 => 'Could not connect to host', //CURLE_SSL_CERTPROBLEM + 59 => 'Could not connect to host', //CURLE_SSL_CIPHER + 60 => 'Could not connect to host', //CURLE_SSL_CACERT + 61 => 'Unknown Content-Encoding', //CURLE_BAD_CONTENT_ENCODING + 65 => 'Failed Sending HTTP SOAP request', //CURLE_SEND_FAIL_REWIND + 66 => 'SSL support is not available in this build', //CURLE_SSL_ENGINE_INITFAILED + 67 => 'Could not connect to host', //CURLE_LOGIN_DENIED + 77 => 'Could not connect to host', //CURLE_SSL_CACERT_BADFILE + 80 => 'Error Fetching http body, No Content-Length, connection closed or chunked data', //CURLE_SSL_SHUTDOWN_FAILED + ); + } + + /** + * Gets the curl error message. + * + * @return string + */ + public function getErrorMessage() + { + $errorCodeMapping = $this->getErrorCodeMapping(); + $errorNumber = curl_errno( $this->ch ); + if ( isset( $errorCodeMapping[$errorNumber] ) ) + { + return $errorCodeMapping[$errorNumber]; + } + return curl_error( $this->ch ); + } + + /** + * Gets the request headers as a string. + * + * @return string + */ + public function getRequestHeaders() + { + return curl_getinfo( $this->ch, CURLINFO_HEADER_OUT ); + } + + /** + * Gets the whole response (including headers) as a string. + * + * @return string + */ + public function getResponse() + { + return $this->response; + } + + /** + * Gets the response body as a string. + * + * @return string + */ + public function getResponseBody() + { + $headerSize = curl_getinfo( $this->ch, CURLINFO_HEADER_SIZE ); + return substr( $this->response, $headerSize ); + } + + /** + * Gets the response content type. + * + * @return string + */ + public function getResponseContentType() + { + return curl_getinfo( $this->ch, CURLINFO_CONTENT_TYPE ); + } + + /** + * Gets the response headers as a string. + * + * @return string + */ + public function getResponseHeaders() + { + $headerSize = curl_getinfo( $this->ch, CURLINFO_HEADER_SIZE ); + return substr( $this->response, 0, $headerSize ); + } + + /** + * Gets the response http status code. + * + * @return string + */ + public function getResponseStatusCode() + { + return curl_getinfo( $this->ch, CURLINFO_HTTP_CODE ); + } + + /** + * Gets the response http status message. + * + * @return string + */ + public function getResponseStatusMessage() + { + preg_match( '/HTTP\/(1\.[0-1]+) ([0-9]{3}) (.*)/', $this->response, $matches ); + return trim( array_pop( $matches ) ); + } +} \ No newline at end of file diff --git a/src/BeSimple/SoapClient/Helper.php b/src/BeSimple/SoapClient/Helper.php new file mode 100644 index 0000000..bb2e5c3 --- /dev/null +++ b/src/BeSimple/SoapClient/Helper.php @@ -0,0 +1,396 @@ + + * (c) Francis Besset + * (c) Andreas Schamberger + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + * + * @link https://github.com/BeSimple/BeSimpleSoapClient + */ + +namespace BeSimple\SoapClient; + +/** + * Soap helper class with static functions that are used in the client and + * server implementations. It also provides namespace and configuration + * constants. + * + * @author Andreas Schamberger + */ +class Helper +{ + /** + * Attachment type: xsd:base64Binary (native in ext/soap). + */ + const ATTACHMENTS_TYPE_BASE64 = 1; + + /** + * Attachment type: MTOM (SOAP Message Transmission Optimization Mechanism). + */ + const ATTACHMENTS_TYPE_MTOM = 2; + + /** + * Attachment type: SWA (SOAP Messages with Attachments). + */ + const ATTACHMENTS_TYPE_SWA = 4; + + /** + * Web Services Security: SOAP Message Security 1.0 (WS-Security 2004) + */ + const NAME_WSS_SMS = 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0'; + + /** + * Web Services Security: SOAP Message Security 1.1 (WS-Security 2004) + */ + const NAME_WSS_SMS_1_1 = 'http://docs.oasis-open.org/wss/oasis-wss-soap-message-security-1.1'; + + /** + * Web Services Security UsernameToken Profile 1.0 + */ + const NAME_WSS_UTP = 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0'; + + /** + * Web Services Security X.509 Certificate Token Profile + */ + const NAME_WSS_X509 = 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0'; + + /** + * Soap 1.1 namespace. + */ + const NS_SOAP_1_1 = 'http://schemas.xmlsoap.org/soap/envelope/'; + + /** + * Soap 1.1 namespace. + */ + const NS_SOAP_1_2 = 'http://www.w3.org/2003/05/soap-envelope/'; + + /** + * Web Services Addressing 1.0 namespace. + */ + const NS_WSA = 'http://www.w3.org/2005/08/addressing'; + + /** + * Web Services Security Extension namespace. + */ + const NS_WSS = 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd'; + + /** + * Web Services Security Utility namespace. + */ + const NS_WSU = 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd'; + + /** + * Describing Media Content of Binary Data in XML namespace. + */ + const NS_XMLMIME = 'http://www.w3.org/2004/11/xmlmime'; + + /** + * XML Schema namespace. + */ + const NS_XML_SCHEMA = 'http://www.w3.org/2001/XMLSchema'; + + /** + * XML Schema instance namespace. + */ + const NS_XML_SCHEMA_INSTANCE = 'http://www.w3.org/2001/XMLSchema-instance'; + + /** + * XML-binary Optimized Packaging namespace. + */ + const NS_XOP = 'http://www.w3.org/2004/08/xop/include'; + + /** + * Web Services Addressing 1.0 prefix. + */ + const PFX_WSA = 'wsa'; + + /** + * Web Services Security Extension namespace. + */ + const PFX_WSS = 'wsse'; + + /** + * Web Services Security Utility namespace prefix. + */ + const PFX_WSU = 'wsu'; + + /** + * Describing Media Content of Binary Data in XML namespace prefix. + */ + const PFX_XMLMIME = 'xmlmime'; + + /** + * XML Schema namespace prefix. + */ + const PFX_XML_SCHEMA = 'xsd'; + + /** + * XML Schema instance namespace prefix. + */ + const PFX_XML_SCHEMA_INSTANCE = 'xsi'; + + /** + * XML-binary Optimized Packaging namespace prefix. + */ + const PFX_XOP = 'xop'; + + /** + * Constant for a request. + */ + const REQUEST = 0; + + /** + * Constant for a response. + */ + const RESPONSE = 1; + + /** + * Wheather to format the XML output or not. + * + * @var boolean + */ + public static $formatXmlOutput = false; + + /** + * Contains previously defined error handler string. + * + * @var string + */ + private static $previousErrorHandler = null; + + /** + * User-defined error handler function to convert errors to exceptions. + * + * @param string $errno + * @param string $errstr + * @param string $errfile + * @param string $errline + * @throws ErrorException + */ + public static function exceptionErrorHandler( $errno, $errstr, $errfile, $errline ) + { + // don't throw exception for errors suppresed with @ + if ( error_reporting() != 0 ) + { + throw new \ErrorException( $errstr, 0, $errno, $errfile, $errline ); + } + } + + /** + * Generate a pseudo-random version 4 UUID. + * + * @see http://de.php.net/manual/en/function.uniqid.php#94959 + * @return string + */ + public static function generateUUID() + { + return sprintf( + '%04x%04x-%04x-%04x-%04x-%04x%04x%04x', + // 32 bits for "time_low" + mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ), + // 16 bits for "time_mid" + mt_rand( 0, 0xffff ), + // 16 bits for "time_hi_and_version", + // four most significant bits holds version number 4 + mt_rand( 0, 0x0fff ) | 0x4000, + // 16 bits, 8 bits for "clk_seq_hi_res", + // 8 bits for "clk_seq_low", + // two most significant bits holds zero and one for variant DCE1.1 + mt_rand( 0, 0x3fff ) | 0x8000, + // 48 bits for "node" + mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ) + ); + } + + /** + * Builds the current URL from the $_SERVER array. + * + * @return string + */ + public static function getCurrentUrl() + { + $url = ''; + if ( isset( $_SERVER['HTTPS'] ) && + ( strtolower( $_SERVER['HTTPS'] ) === 'on' || $_SERVER['HTTPS'] === '1' ) ) + { + $url .= 'https://'; + } + else + { + $url .= 'http://'; + } + $url .= isset( $_SERVER['SERVER_NAME'] ) ? $_SERVER['SERVER_NAME'] : ''; + if ( isset( $_SERVER['SERVER_PORT'] ) && $_SERVER['SERVER_PORT'] != 80 ) + { + $url .= ":{$_SERVER['SERVER_PORT']}"; + } + $url .= isset( $_SERVER['REQUEST_URI'] ) ? $_SERVER['REQUEST_URI'] : ''; + return $url; + } + + /** + * Get SOAP namespace for the given $version. + * + * @param int $version SOAP_1_1|SOAP_1_2 + * @return string + */ + public static function getSoapNamespace( $version ) + { + if ( $version === SOAP_1_2 ) + { + return self::NS_SOAP_1_2; + } + else + { + return self::NS_SOAP_1_1; + } + } + + /** + * Get SOAP version from namespace URI. + * + * @param string $namespace NS_SOAP_1_1|NS_SOAP_1_2 + * @return int SOAP_1_1|SOAP_1_2 + */ + public static function getSoapVersionFromNamespace( $namespace ) + { + if ( $namespace === self::NS_SOAP_1_2 ) + { + return SOAP_1_2; + } + else + { + return SOAP_1_1; + } + } + + /** + * Runs the registered Plugins on the given request $xml. + * + * @param array(\ass\Soap\Plugin) $plugins + * @param int $requestType \ass\Soap\Helper::REQUEST|\ass\Soap\Helper::RESPONSE + * @param string $xml + * @param string $location + * @param string $action + * @param \ass\Soap\WsdlHandler $wsdlHandler + * @return string + */ + public static function runPlugins( array $plugins, $requestType, $xml, $location = null, $action = null, \ass\Soap\WsdlHandler $wsdlHandler = null ) + { + if ( count( $plugins ) > 0 ) + { + // instantiate new dom object + $dom = new \DOMDocument( '1.0' ); + // format the XML if option is set + $dom->formatOutput = self::$formatXmlOutput; + $dom->loadXML( $xml ); + $params = array( + $dom, + $location, + $action, + $wsdlHandler + ); + if ( $requestType == self::REQUEST ) + { + $callMethod = 'modifyRequest'; + } + else + { + $callMethod = 'modifyResponse'; + } + // modify dom + foreach( $plugins AS $plugin ) + { + if ( $plugin instanceof \ass\Soap\Plugin ) + { + call_user_func_array( array( $plugin, $callMethod ), $params ); + } + } + // return the modified xml document + return $dom->saveXML(); + } + // format the XML if option is set + elseif ( self::$formatXmlOutput === true ) + { + $dom = new \DOMDocument( '1.0' ); + $dom->formatOutput = true; + $dom->loadXML( $xml ); + return $dom->saveXML(); + } + return $xml; + } + + /** + * Set custom error handler that converts all php errors to ErrorExceptions + * + * @param boolean $reset + */ + public static function setCustomErrorHandler( $reset = false ) + { + if ( $reset === true && !is_null( self::$previousErrorHandler ) ) + { + set_error_handler( self::$previousErrorHandler ); + self::$previousErrorHandler = null; + } + else + { + self::$previousErrorHandler = set_error_handler( 'ass\\Soap\\Helper::exceptionErrorHandler' ); + } + return self::$previousErrorHandler; + } + + /** + * Build data string to used to bridge ext/soap + * 'SOAP_MIME_ATTACHMENT:cid=...&type=...' + * + * @param string $contentId + * @param string $contentType + * @return string + */ + public static function makeSoapAttachmentDataString( $contentId, $contentType ) + { + $parameter = array( + 'cid' => $contentId, + 'type' => $contentType, + ); + return 'SOAP-MIME-ATTACHMENT:' . http_build_query( $parameter, null, '&' ); + } + + /** + * Parse data string used to bridge ext/soap + * 'SOAP_MIME_ATTACHMENT:cid=...&type=...' + * + * @param string $dataString + * @return array(string=>string) + */ + public static function parseSoapAttachmentDataString( $dataString ) + { + $dataString = substr( $dataString, 21 ); + // get all data + $data = array(); + parse_str( $dataString, $data ); + return $data; + } + + /** + * Function to set proper http status header. + * Neccessary as there is a difference between mod_php and the cgi SAPIs. + * + * @param string $header + */ + public static function setHttpStatusHeader( $header ) + { + if ( substr( php_sapi_name(), 0, 3 ) == 'cgi' ) + { + header( 'Status: ' . $header ); + } + else + { + header( $_SERVER['SERVER_PROTOCOL'] . ' ' . $header ); + } + } +} \ No newline at end of file diff --git a/src/BeSimple/SoapClient/SoapClient.php b/src/BeSimple/SoapClient/SoapClient.php new file mode 100644 index 0000000..a1fc84d --- /dev/null +++ b/src/BeSimple/SoapClient/SoapClient.php @@ -0,0 +1,297 @@ + + * (c) Francis Besset + * (c) Andreas Schamberger + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + * + * @link https://github.com/BeSimple/BeSimpleSoapClient + */ + +namespace BeSimple\SoapClient; + +/** + * Extended SoapClient that uses a a cURL wrapper for all underlying HTTP + * requests in order to use proper authentication for all requests. This also + * adds NTLM support. A custom WSDL downloader resolves remote xsd:includes and + * allows caching of all remote referenced items. + * + * @author Andreas Schamberger + */ +class SoapClient extends \SoapClient +{ + /** + * Last request headers. + * + * @var string + */ + private $lastRequestHeaders = ''; + + /** + * Last request. + * + * @var string + */ + private $lastRequest = ''; + + /** + * Last response headers. + * + * @var string + */ + private $lastResponseHeaders = ''; + + /** + * Last response. + * + * @var string + */ + private $lastResponse = ''; + + /** + * Copy of the parent class' options array + * + * @var array(string=>mixed) + */ + protected $options = array(); + + /** + * Path to WSDL (cache) file. + * + * @var string + */ + private $wsdlFile = null; + + /** + * Extended constructor that saves the options as the parent class' + * property is private. + * + * @param string $wsdl + * @param array(string=>mixed) $options + */ + public function __construct( $wsdl, array $options = array(), TypeConverterCollection $converters = null ) + { + // we want the exceptions option to be set + $options['exceptions'] = true; + // set custom error handler that converts all php errors to ErrorExceptions + Helper::setCustomErrorHandler(); + // we want to make sure we have the soap version to rely on it later + if ( !isset( $options['soap_version'] ) ) + { + $options['soap_version'] = SOAP_1_1; + } + // we want to make sure we have the features option + if ( !isset( $options['features'] ) ) + { + $options['features'] = 0; + } + // set default option to resolve xsd includes + if ( !isset( $options['resolve_xsd_includes'] ) ) + { + $options['resolve_xsd_includes'] = true; + } + // add type converters from TypeConverterCollection + if ( !is_null( $converters ) ) + { + $convertersTypemap = $converters->getTypemap(); + if ( isset( $options['typemap'] ) ) + { + $options['typemap'] = array_merge( $options['typemap'], $convertersTypemap ); + } + else + { + $options['typemap'] = $convertersTypemap; + } + } + // store local copy as ext/soap's property is private + $this->options = $options; + // disable obsolete trace option for native SoapClient as we need to do our own tracing anyways + $options['trace'] = false; + // disable WSDL caching as we handle WSDL caching for remote URLs ourself + $options['cache_wsdl'] = WSDL_CACHE_NONE; + // load WSDL and run parent constructor + // can't be loaded later as we need it already in the parent constructor + $this->wsdlFile = $this->loadWsdl( $wsdl ); + parent::__construct( $this->wsdlFile, $options ); + } + + /** + * Perform HTTP request with cURL. + * + * @param string $request + * @param string $location + * @param string $action + * @return string + */ + private function __doHttpRequest( $request, $location, $action ) + { + // $request is if unmodified from SoapClient not a php string type! + $request = (string)$request; + if ( $this->options['soap_version'] == SOAP_1_2 ) + { + $headers = array( + 'Content-Type: application/soap+xml; charset=utf-8', + ); + } + else + { + $headers = array( + 'Content-Type: text/xml; charset=utf-8', + ); + } + // add SOAPAction header + $headers[] = 'SOAPAction: "' . $action . '"'; + // new curl object for request + $curl = new Curl( $this->options ); + // execute request + $responseSuccessfull = $curl->exec( $location, $request, $headers ); + // tracing enabled: store last request header and body + if ( isset( $this->options['trace'] ) && $this->options['trace'] === true ) + { + $this->lastRequestHeaders = $curl->getRequestHeaders(); + $this->lastRequest = $request; + } + // in case of an error while making the http request throw a soapFault + if ( $responseSuccessfull === false ) + { + // get error message from curl + $faultstring = $curl->getErrorMessage(); + // destruct curl object + unset( $curl ); + throw new \SoapFault( 'HTTP', $faultstring ); + } + // tracing enabled: store last response header and body + if ( isset( $this->options['trace'] ) && $this->options['trace'] === true ) + { + $this->lastResponseHeaders = $curl->getResponseHeaders(); + $this->lastResponse = $curl->getResponseBody(); + } + $response = $curl->getResponseBody(); + // check if we do have a proper soap status code (if not soapfault) +// // TODO +// $responseStatusCode = $curl->getResponseStatusCode(); +// if ( $responseStatusCode >= 400 ) +// { +// $isError = 0; +// $response = trim( $response ); +// if ( strlen( $response ) == 0 ) +// { +// $isError = 1; +// } +// else +// { +// $contentType = $curl->getResponseContentType(); +// if ( $contentType != 'application/soap+xml' +// && $contentType != 'application/soap+xml' ) +// { +// if ( strncmp( $response , "getResponseStatusMessage() ); +// } +// } +// // TODO +// elseif ( $responseStatusCode != 200 && $responseStatusCode != 202 ) +// { +// $dom = new \DOMDocument( '1.0' ); +// $dom->loadXML( $response ); +// if ( $dom->getElementsByTagNameNS( $dom->documentElement->namespaceURI, 'Fault' )->length == 0 ) +// { +// throw new \SoapFault( 'HTTP', 'HTTP response status must be 200 or 202' ); +// } +// } + // destruct curl object + unset( $curl ); + return $response; + } + + /** + * Custom request method to be able to modify the SOAP messages. + * + * @param string $request + * @param string $location + * @param string $action + * @param int $version + * @param int $one_way 0|1 + * @return string + */ + public function __doRequest( $request, $location, $action, $version, $one_way = 0 ) + { + // http request + $response = $this->__doHttpRequest( $request, $location, $action ); + // return SOAP response to ext/soap + return $response; + } + + /** + * Get last request HTTP headers. + * + * @return string + */ + public function __getLastRequestHeaders() + { + return $this->lastRequestHeaders; + } + + /** + * Get last request HTTP body. + * + * @return string + */ + public function __getLastRequest() + { + return $this->lastRequest; + } + + /** + * Get last response HTTP headers. + * + * @return string + */ + public function __getLastResponseHeaders() + { + return $this->lastResponseHeaders; + } + + /** + * Get last response HTTP body. + * + * @return string + */ + public function __getLastResponse() + { + return $this->lastResponse; + } + + /** + * Downloads WSDL files with cURL. Uses all SoapClient options for + * authentication. Uses the WSDL_CACHE_* constants and the 'soap.wsdl_*' + * ini settings. Does only file caching as SoapClient only supports a file + * name parameter. + * + * @param string $wsdl + * @return string + */ + private function loadWsdl( $wsdl ) + { + $wsdlDownloader = new WsdlDownloader( $this->options ); + try + { + $cacheFileName = $wsdlDownloader->download( $wsdl ); + } + catch ( \RuntimeException $e ) + { + throw new \SoapFault( 'WSDL', "SOAP-ERROR: Parsing WSDL: Couldn't load from '" . $wsdl . "' : failed to load external entity \"" . $wsdl . "\"" ); + } + return $cacheFileName; + } +} \ No newline at end of file diff --git a/src/BeSimple/SoapClient/WsdlDownloader.php b/src/BeSimple/SoapClient/WsdlDownloader.php new file mode 100644 index 0000000..b13fe6e --- /dev/null +++ b/src/BeSimple/SoapClient/WsdlDownloader.php @@ -0,0 +1,240 @@ + + * (c) Francis Besset + * (c) Andreas Schamberger + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + * + * @link https://github.com/BeSimple/BeSimpleSoapClient + */ + +namespace BeSimple\SoapClient; + +/** + * Downloads WSDL files with cURL. Uses all SoapClient options for + * authentication. 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 + */ + private $cacheEnabled; + + /** + * Cache dir. + * + * @var string + */ + private $cacheDir; + + /** + * Cache TTL. + * + * @var int + */ + private $cacheTtl; + + /** + * Options array + * + * @var array(string=>mixed) + */ + private $options = array(); + + /** + * Constructor. + */ + public function __construct( $options ) + { + // get current WSDL caching config + $this->cacheEnabled = (bool)ini_get( 'soap.wsdl_cache_enabled' ); + if ( $this->cacheEnabled === true + && isset( $options['cache_wsdl'] ) + && $options['cache_wsdl'] === WSDL_CACHE_NONE + ) + { + $this->cacheEnabled = false; + } + $this->cacheDir = ini_get( 'soap.wsdl_cache_dir' ); + if ( !is_dir( $this->cacheDir ) ) + { + $this->cacheDir = sys_get_temp_dir(); + } + $this->cacheDir = rtrim( $this->cacheDir, '/\\' ); + $this->cacheTtl = ini_get( 'soap.wsdl_cache_ttl' ); + $this->options = $options; + } + + /** + * Download given WSDL file and return name of cache file. + * + * @param string $wsdl + * @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 === true || $this->options['resolve_xsd_includes'] === true ) + { + $cacheFile = $this->cacheDir . DIRECTORY_SEPARATOR . 'wsdl_' . md5( $wsdl ) . '.cache'; + if ( $this->cacheEnabled === false + || !file_exists( $cacheFile ) + || ( filemtime( $cacheFile ) + $this->cacheTtl ) < time() + ) + { + if ( $isRemoteFile === true ) + { + // new curl object for request + $curl = new Curl( $this->options ); + // execute request + $responseSuccessfull = $curl->exec( $wsdl ); + // get content + if ( $responseSuccessfull === true ) + { + $response = $curl->getResponseBody(); + if ( $this->options['resolve_xsd_includes'] === true ) + { + $this->resolveXsdIncludes( $response, $cacheFile, $wsdl ); + } + else + { + file_put_contents( $cacheFile, $response ); + } + } + else + { + throw new \ErrorException( "SOAP-ERROR: Parsing WSDL: Couldn't load from '" . $wsdl ."'" ); + } + } + elseif ( file_exists( $wsdl ) ) + { + $response = file_get_contents( $wsdl ); + $this->resolveXsdIncludes( $response, $cacheFile ); + } + else + { + throw new \ErrorException( "SOAP-ERROR: Parsing WSDL: Couldn't load from '" . $wsdl ."'" ); + } + } + return $cacheFile; + } + elseif ( file_exists( $wsdl ) ) + { + return realpath( $wsdl ); + } + else + { + throw new \ErrorException( "SOAP-ERROR: Parsing WSDL: Couldn't load from '" . $wsdl ."'" ); + } + } + + /** + * Do we have a remote file? + * + * @param string $file + * @return boolean + */ + private function isRemoteFile( $file ) + { + $isRemoteFile = false; + // @parse_url to suppress E_WARNING for invalid urls + if ( ( $url = @parse_url( $file ) ) !== false ) + { + if ( isset( $url['scheme'] ) && substr( $url['scheme'], 0, 4 ) == 'http' ) + { + $isRemoteFile = true; + } + } + return $isRemoteFile; + } + + /** + * Resolves remote XSD includes within the WSDL files. + * + * @param string $xml + * @param string $cacheFile + * @param unknown_type $parentIsRemote + * @return string + */ + private function resolveXsdIncludes( $xml, $cacheFile, $parentFile = null ) + { + $doc = new \DOMDocument(); + $doc->loadXML( $xml ); + $xpath = new \DOMXPath( $doc ); + $xpath->registerNamespace( Helper::PFX_XML_SCHEMA, Helper::NS_XML_SCHEMA ); + $query = './/' . Helper::PFX_XML_SCHEMA . ':include'; + $nodes = $xpath->query( $query ); + if ( $nodes->length > 0 ) + { + foreach ( $nodes as $node ) + { + $schemaLocation = $node->getAttribute( 'schemaLocation' ); + if ( $this->isRemoteFile( $schemaLocation ) ) + { + $schemaLocation = $this->download( $schemaLocation ); + $node->setAttribute( 'schemaLocation', $schemaLocation ); + } + elseif ( !is_null( $parentFile ) ) + { + $schemaLocation = $this->resolveRelativePathInUrl( $parentFile, $schemaLocation ); + $schemaLocation = $this->download( $schemaLocation ); + $node->setAttribute( 'schemaLocation', $schemaLocation ); + } + } + } + $doc->save( $cacheFile ); + } + + /** + * Resolves the relative path to base into an absolute. + * + * @param string $base + * @param string $relative + * @return string + */ + private function resolveRelativePathInUrl( $base, $relative ) + { + $urlParts = parse_url( $base ); + // combine base path with relative path + if ( strrpos( '/', $urlParts['path'] ) === ( strlen( $urlParts['path'] ) - 1 ) ) + { + $path = trim( $urlParts['path'] . $relative ); + } + else + { + $path = trim( dirname( $urlParts['path'] ) . '/' . $relative ); + } + // foo/./bar ==> foo/bar + $path = preg_replace( '~/\./~', '/', $path ); + // remove double slashes + $path = preg_replace( '~/+~', '/', $path ); + // split path by '/' + $parts = explode( '/', $path ); + // resolve /../ + foreach ( $parts as $key => $part ) + { + if ( $part == ".." ) + { + if ( $key-1 >= 0 ) + { + unset( $parts[$key-1] ); + } + unset( $parts[$key] ); + } + } + return $urlParts['scheme'] . '://' . $urlParts['host'] . implode( '/', $parts ); + } +} \ No newline at end of file