From fd162d8ca668a1ef9c5089b9475ef5ba8c68d0e0 Mon Sep 17 00:00:00 2001 From: Francis Besset Date: Sat, 3 Sep 2011 21:15:51 +0200 Subject: [PATCH 01/63] Initial commit --- phpunit.xml.dist | 31 ++++ src/BeSimple/SoapClient/Soap/SoapClient.php | 143 ++++++++++++++++ src/BeSimple/SoapClient/Soap/SoapRequest.php | 159 ++++++++++++++++++ .../SoapClient/Soap/Fixtures/foobar.wsdl | 47 ++++++ .../Tests/SoapClient/Soap/SoapClientTest.php | 76 +++++++++ .../Tests/SoapClient/Soap/SoapRequestTest.php | 89 ++++++++++ tests/bootstrap.php | 22 +++ 7 files changed, 567 insertions(+) create mode 100644 phpunit.xml.dist create mode 100644 src/BeSimple/SoapClient/Soap/SoapClient.php create mode 100644 src/BeSimple/SoapClient/Soap/SoapRequest.php create mode 100644 tests/BeSimple/Tests/SoapClient/Soap/Fixtures/foobar.wsdl create mode 100644 tests/BeSimple/Tests/SoapClient/Soap/SoapClientTest.php create mode 100644 tests/BeSimple/Tests/SoapClient/Soap/SoapRequestTest.php create mode 100644 tests/bootstrap.php diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..4f8e547 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,31 @@ + + + + + + ./tests/BeSimple/ + + + + + + benchmark + + + + + + ./src/BeSimple/ + + + diff --git a/src/BeSimple/SoapClient/Soap/SoapClient.php b/src/BeSimple/SoapClient/Soap/SoapClient.php new file mode 100644 index 0000000..61273b1 --- /dev/null +++ b/src/BeSimple/SoapClient/Soap/SoapClient.php @@ -0,0 +1,143 @@ + + * (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\Soap; + +/** + * @author Francis Besset + */ +class SoapClient +{ + protected $wsdl; + protected $soapClient; + + /** + * @param string $wsdl + * @param array $options + */ + public function __construct($wsdl, array $options = array()) + { + $this->wsdl = $wsdl; + $this->setOptions($options); + } + + public function setOptions(array $options) + { + $this->options = array( + 'cache_dir' => null, + 'debug' => false, + ); + + // check option names and live merge, if errors are encountered Exception will be thrown + $invalid = array(); + $isInvalid = false; + foreach ($options as $key => $value) { + if (array_key_exists($key, $this->options)) { + $this->options[$key] = $value; + } else { + $isInvalid = true; + $invalid[] = $key; + } + } + + if ($isInvalid) { + throw new \InvalidArgumentException(sprintf( + 'The "%s" class does not support the following options: "%s".', + get_class($this), + implode('\', \'', $invalid) + )); + } + } + + /** + * @param string $name The name + * @param mixed $value The value + * + * @throws \InvalidArgumentException + */ + public function setOption($name, $value) + { + if (!array_key_exists($name, $this->options)) { + throw new \InvalidArgumentException(sprintf( + 'The "%s" class does not support the "%s" option.', + get_class($this), + $name + )); + } + + $this->options[$name] = $value; + } + + public function getOptions() + { + return $this->options; + } + + /** + * @param string $key The key + * + * @return mixed The value + * + * @throws \InvalidArgumentException + */ + public function getOption($key) + { + if (!array_key_exists($key, $this->options)) { + throw new \InvalidArgumentException(sprintf( + 'The "%s" class does not support the "%s" option.', + get_class($this), + $key + )); + } + + return $this->options[$key]; + } + + /** + * @param SoapRequest $soapRequest + * + * @return mixed + */ + public function send(SoapRequest $soapRequest) + { + return $this->getNativeSoapClient()->__soapCall( + $soapRequest->getFunction(), + $soapRequest->getArguments(), + $soapRequest->getOptions() + ); + } + + /** + * @return \SoapClient + */ + public function getNativeSoapClient() + { + if (!$this->soapClient) { + $this->soapClient = new \SoapClient($this->wsdl, $this->getSoapOptions()); + } + + return $this->soapClient; + } + + /** + * @return array The \SoapClient options + */ + public function getSoapOptions() + { + $options = array(); + + $options['cache_wsdl'] = $this->options['debug'] ? WSDL_CACHE_NONE : WSDL_CACHE_DISK; + $options['trace'] = $this->options['debug']; + + return $options; + } +} \ No newline at end of file diff --git a/src/BeSimple/SoapClient/Soap/SoapRequest.php b/src/BeSimple/SoapClient/Soap/SoapRequest.php new file mode 100644 index 0000000..1963a64 --- /dev/null +++ b/src/BeSimple/SoapClient/Soap/SoapRequest.php @@ -0,0 +1,159 @@ + + * (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\Soap; + +/** + * @author Francis Besset + */ +class SoapRequest +{ + protected $function; + protected $arguments; + protected $options; + + public function __construct($function = null, array $arguments = array(), array $options = array()) + { + $this->function = $function; + $this->arguments = $arguments; + $this->options = $options; + } + + /** + * @return string The function name + */ + public function getFunction() + { + return $this->function; + } + + /** + * @param string The function name + * + * @return SoapRequest + */ + public function setFunction($function) + { + $this->function = $function; + + return $this; + } + + /** + * @return array An array with all arguments + */ + public function getArguments() + { + return $this->arguments; + } + + /** + * @param string The name of the argument + * @param mixed The default value returned if the argument is not exists + * + * @return mixed + */ + public function getArgument($name, $default = null) + { + return $this->hasArgument($name) ? $this->arguments[$name] : $default; + } + + /** + * @param string The name of the argument + * + * @return boolean + */ + public function hasArgument($name) + { + return isset($this->arguments[$name]); + } + + /** + * @param array An array with arguments + * + * @return SoapRequest + */ + public function setArguments(array $arguments) + { + $this->arguments = $arguments; + + return $this; + } + + /** + * @param string The name of argument + * @param mixed The value of argument + * + * @return SoapRequest + */ + public function addArgument($name, $value) + { + $this->arguments[$name] = $value; + + return $this; + } + + /** + * @return array An array with all options + */ + public function getOptions() + { + return $this->options; + } + + /** + * @param string The name of the option + * @param mixed The default value returned if the option is not exists + * + * @return mixed + */ + public function getOption($name, $default = null) + { + return $this->hasOption($name) ? $this->options[$name] : $default; + } + + /** + * @param string The name of the option + * + * @return boolean + */ + public function hasOption($name) + { + return isset($this->options[$name]); + } + + /** + * @param array An array with options + * + * @return SoapRequest + */ + public function setOptions(array $options) + { + $this->options = $options; + + return $this; + } + + /** + * @param string The name of option + * @param mixed The value of option + * + * @return SoapRequest + */ + public function addOption($name, $value) + { + $this->options[$name] = $value; + + return $this; + } + +} \ No newline at end of file diff --git a/tests/BeSimple/Tests/SoapClient/Soap/Fixtures/foobar.wsdl b/tests/BeSimple/Tests/SoapClient/Soap/Fixtures/foobar.wsdl new file mode 100644 index 0000000..a890dd8 --- /dev/null +++ b/tests/BeSimple/Tests/SoapClient/Soap/Fixtures/foobar.wsdl @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/BeSimple/Tests/SoapClient/Soap/SoapClientTest.php b/tests/BeSimple/Tests/SoapClient/Soap/SoapClientTest.php new file mode 100644 index 0000000..e82ca30 --- /dev/null +++ b/tests/BeSimple/Tests/SoapClient/Soap/SoapClientTest.php @@ -0,0 +1,76 @@ + + * (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\Tests\SoapClient\Soap; + +use BeSimple\SoapClient\Soap\SoapClient; + +class SoapClientTest extends \PHPUnit_Framework_TestCase +{ + public function testSetOptions() + { + $soapClient = new SoapClient('foo.wsdl'); + $options = array( + 'cache_dir' => '/tmp', + 'debug' => true, + ); + $soapClient->setOptions($options); + + $this->assertEquals($options, $soapClient->getOptions()); + } + + public function testSetOptionsThrowsAnExceptionIfOptionsDoesNotExists() + { + $soapClient = new SoapClient('foo.wsdl'); + + $this->setExpectedException('InvalidArgumentException'); + $soapClient->setOptions(array('bad_option' => true)); + } + + public function testSetOption() + { + $soapClient = new SoapClient('foo.wsdl'); + $soapClient->setOption('debug', true); + + $this->assertEquals(true, $soapClient->getOption('debug')); + } + + public function testSetOptionThrowsAnExceptionIfOptionDoesNotExists() + { + $soapClient = new SoapClient('foo.wsdl'); + + $this->setExpectedException('InvalidArgumentException'); + $soapClient->setOption('bad_option', 'bar'); + } + + public function testGetOptionThrowsAnExceptionIfOptionDoesNotExists() + { + $soapClient = new SoapClient('foo.wsdl'); + + $this->setExpectedException('InvalidArgumentException'); + $soapClient->getOption('bad_option'); + } + + public function testGetSoapOptions() + { + $soapClient = new SoapClient('foo.wsdl', array('debug' => true)); + + $this->assertEquals(array('cache_wsdl' => WSDL_CACHE_NONE, 'trace' => true), $soapClient->getSoapOptions()); + } + + public function testGetNativeSoapClient() + { + $soapClient = new SoapClient(__DIR__.'/Fixtures/foobar.wsdl', array('debug' => true)); + + $this->assertInstanceOf('SoapClient', $soapClient->getNativeSoapClient()); + } +} \ No newline at end of file diff --git a/tests/BeSimple/Tests/SoapClient/Soap/SoapRequestTest.php b/tests/BeSimple/Tests/SoapClient/Soap/SoapRequestTest.php new file mode 100644 index 0000000..8e06a23 --- /dev/null +++ b/tests/BeSimple/Tests/SoapClient/Soap/SoapRequestTest.php @@ -0,0 +1,89 @@ + + * (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\Tests\SoapClient\Soap; + +use BeSimple\SoapClient\Soap\SoapRequest; + +class SoapRequestTest extends \PHPUnit_Framework_TestCase +{ + public function testSetFunction() + { + $soapRequest = new SoapRequest(); + $soapRequest->setFunction('foo'); + + $this->assertEquals('foo', $soapRequest->getFunction()); + } + + public function testSetArguments() + { + $soapRequest = new SoapRequest(); + $arguments = array( + 'foo' => true, + 'bar' => false, + ); + $soapRequest->setArguments($arguments); + + $this->assertEquals($arguments, $soapRequest->getArguments()); + } + + public function testGetArgument() + { + $soapRequest = new SoapRequest(); + + $this->assertEquals(false, $soapRequest->getArgument('foo', false)); + + $soapRequest->addArgument('foo', 'bar'); + + $this->assertEquals('bar', $soapRequest->getArgument('foo', false)); + } + + public function testSetOptions() + { + $soapRequest = new SoapRequest(); + $options = array( + 'uri' => 'foo', + 'soapaction' => 'bar', + ); + $soapRequest->setOptions($options); + + $this->assertEquals($options, $soapRequest->getOptions()); + } + + public function testGetOption() + { + $soapRequest = new SoapRequest(); + + $this->assertEquals(false, $soapRequest->getOption('soapaction')); + + $soapRequest->addOption('soapaction', 'foo'); + + $this->assertEquals('foo', $soapRequest->getOption('soapaction')); + } + + public function testConstruct() + { + $soapRequest = new SoapRequest(); + + $this->assertNull($soapRequest->getFunction()); + $this->assertEquals(array(), $soapRequest->getArguments()); + $this->assertEquals(array(), $soapRequest->getOptions()); + + $arguments = array('bar' => 'foobar'); + $options = array('soapaction' => 'foobar'); + $soapRequest = new SoapRequest('foo', $arguments, $options); + + $this->assertEquals('foo', $soapRequest->getFunction()); + $this->assertEquals($arguments, $soapRequest->getArguments()); + $this->assertEquals($options, $soapRequest->getOptions()); + } +} \ No newline at end of file diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..80850f4 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,22 @@ + Date: Sun, 4 Sep 2011 00:36:24 +0200 Subject: [PATCH 02/63] Used BeSimple\SoapCommon\Cache to define the cache strategy --- .gitignore | 1 + src/BeSimple/SoapClient/Soap/SoapClient.php | 12 ++++-- .../Tests/SoapClient/Soap/SoapClientTest.php | 10 +++-- tests/bootstrap.php | 7 ++++ vendors.php | 41 +++++++++++++++++++ 5 files changed, 65 insertions(+), 6 deletions(-) create mode 100644 .gitignore create mode 100755 vendors.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..22d0d82 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +vendor diff --git a/src/BeSimple/SoapClient/Soap/SoapClient.php b/src/BeSimple/SoapClient/Soap/SoapClient.php index 61273b1..8b5b187 100644 --- a/src/BeSimple/SoapClient/Soap/SoapClient.php +++ b/src/BeSimple/SoapClient/Soap/SoapClient.php @@ -12,6 +12,8 @@ namespace BeSimple\SoapClient\Soap; +use BeSimple\SoapCommon\Cache; + /** * @author Francis Besset */ @@ -33,8 +35,8 @@ class SoapClient public function setOptions(array $options) { $this->options = array( - 'cache_dir' => null, - 'debug' => false, + 'debug' => false, + 'cache_wsdl' => null, ); // check option names and live merge, if errors are encountered Exception will be thrown @@ -135,7 +137,11 @@ class SoapClient { $options = array(); - $options['cache_wsdl'] = $this->options['debug'] ? WSDL_CACHE_NONE : WSDL_CACHE_DISK; + if (null === $this->options['cache_wsdl']) { + $this->options['cache_wsdl'] = Cache::getType(); + } + + $options['cache_wsdl'] = $this->options['cache_wsdl']; $options['trace'] = $this->options['debug']; return $options; diff --git a/tests/BeSimple/Tests/SoapClient/Soap/SoapClientTest.php b/tests/BeSimple/Tests/SoapClient/Soap/SoapClientTest.php index e82ca30..bb6d03a 100644 --- a/tests/BeSimple/Tests/SoapClient/Soap/SoapClientTest.php +++ b/tests/BeSimple/Tests/SoapClient/Soap/SoapClientTest.php @@ -12,6 +12,7 @@ namespace BeSimple\Tests\SoapClient\Soap; +use BeSimple\SoapCommon\Cache; use BeSimple\SoapClient\Soap\SoapClient; class SoapClientTest extends \PHPUnit_Framework_TestCase @@ -20,8 +21,8 @@ class SoapClientTest extends \PHPUnit_Framework_TestCase { $soapClient = new SoapClient('foo.wsdl'); $options = array( - 'cache_dir' => '/tmp', - 'debug' => true, + 'cache_wsdl' => Cache::TYPE_DISK_MEMORY, + 'debug' => true, ); $soapClient->setOptions($options); @@ -62,9 +63,12 @@ class SoapClientTest extends \PHPUnit_Framework_TestCase public function testGetSoapOptions() { + Cache::setType(Cache::TYPE_MEMORY); $soapClient = new SoapClient('foo.wsdl', array('debug' => true)); + $this->assertEquals(array('cache_wsdl' => Cache::getType(), 'trace' => true), $soapClient->getSoapOptions()); - $this->assertEquals(array('cache_wsdl' => WSDL_CACHE_NONE, 'trace' => true), $soapClient->getSoapOptions()); + $soapClient = new SoapClient('foo.wsdl', array('debug' => false, 'cache_wsdl' => Cache::TYPE_NONE)); + $this->assertEquals(array('cache_wsdl' => Cache::TYPE_NONE, 'trace' => false), $soapClient->getSoapOptions()); } public function testGetNativeSoapClient() diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 80850f4..fdc4599 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -16,6 +16,13 @@ spl_autoload_register(function($class) { if (file_exists($path) && is_readable($path)) { require_once $path; + return true; + } + } else if (0 === strpos($class, 'BeSimple\SoapCommon\\')) { + $path = __DIR__.'/../vendor/besimple-soapcommon/src/'.($class = strtr($class, '\\', '/')).'.php'; + if (file_exists($path) && is_readable($path)) { + require_once $path; + return true; } } diff --git a/vendors.php b/vendors.php new file mode 100755 index 0000000..cf763ae --- /dev/null +++ b/vendors.php @@ -0,0 +1,41 @@ +#!/usr/bin/env php + + * (c) Francis Besset + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +/* + +CAUTION: This file installs the dependencies needed to run the BeSimpleSoapClient test suite. + +https://github.com/BeSimple/BeSimpleSoapClient + +*/ + +if (!is_dir($vendorDir = dirname(__FILE__).'/vendor')) { + mkdir($vendorDir, 0777, true); +} + +$deps = array( + array('besimple-soapcommon', 'http://github.com/BeSimple/BeSimpleSoapCommon.git', 'origin/HEAD'), +); + +foreach ($deps as $dep) { + list($name, $url, $rev) = $dep; + + echo "> Installing/Updating $name\n"; + + $installDir = $vendorDir.'/'.$name; + if (!is_dir($installDir)) { + system(sprintf('git clone %s %s', escapeshellarg($url), escapeshellarg($installDir))); + } + + system(sprintf('cd %s && git fetch origin && git reset --hard %s', escapeshellarg($installDir), escapeshellarg($rev))); +} From 358373dcc8f74b86f77511456264cd3b4cbdfbdf Mon Sep 17 00:00:00 2001 From: Francis Besset Date: Sun, 4 Sep 2011 00:42:51 +0200 Subject: [PATCH 03/63] Moved classes --- src/BeSimple/SoapClient/{Soap => }/SoapClient.php | 2 +- src/BeSimple/SoapClient/{Soap => }/SoapRequest.php | 2 +- .../BeSimple/Tests/SoapClient/{Soap => }/Fixtures/foobar.wsdl | 0 tests/BeSimple/Tests/SoapClient/{Soap => }/SoapClientTest.php | 4 ++-- .../BeSimple/Tests/SoapClient/{Soap => }/SoapRequestTest.php | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) rename src/BeSimple/SoapClient/{Soap => }/SoapClient.php (98%) rename src/BeSimple/SoapClient/{Soap => }/SoapRequest.php (98%) rename tests/BeSimple/Tests/SoapClient/{Soap => }/Fixtures/foobar.wsdl (100%) rename tests/BeSimple/Tests/SoapClient/{Soap => }/SoapClientTest.php (96%) rename tests/BeSimple/Tests/SoapClient/{Soap => }/SoapRequestTest.php (96%) diff --git a/src/BeSimple/SoapClient/Soap/SoapClient.php b/src/BeSimple/SoapClient/SoapClient.php similarity index 98% rename from src/BeSimple/SoapClient/Soap/SoapClient.php rename to src/BeSimple/SoapClient/SoapClient.php index 8b5b187..c335a2e 100644 --- a/src/BeSimple/SoapClient/Soap/SoapClient.php +++ b/src/BeSimple/SoapClient/SoapClient.php @@ -10,7 +10,7 @@ * with this source code in the file LICENSE. */ -namespace BeSimple\SoapClient\Soap; +namespace BeSimple\SoapClient; use BeSimple\SoapCommon\Cache; diff --git a/src/BeSimple/SoapClient/Soap/SoapRequest.php b/src/BeSimple/SoapClient/SoapRequest.php similarity index 98% rename from src/BeSimple/SoapClient/Soap/SoapRequest.php rename to src/BeSimple/SoapClient/SoapRequest.php index 1963a64..0960074 100644 --- a/src/BeSimple/SoapClient/Soap/SoapRequest.php +++ b/src/BeSimple/SoapClient/SoapRequest.php @@ -10,7 +10,7 @@ * with this source code in the file LICENSE. */ -namespace BeSimple\SoapClient\Soap; +namespace BeSimple\SoapClient; /** * @author Francis Besset diff --git a/tests/BeSimple/Tests/SoapClient/Soap/Fixtures/foobar.wsdl b/tests/BeSimple/Tests/SoapClient/Fixtures/foobar.wsdl similarity index 100% rename from tests/BeSimple/Tests/SoapClient/Soap/Fixtures/foobar.wsdl rename to tests/BeSimple/Tests/SoapClient/Fixtures/foobar.wsdl diff --git a/tests/BeSimple/Tests/SoapClient/Soap/SoapClientTest.php b/tests/BeSimple/Tests/SoapClient/SoapClientTest.php similarity index 96% rename from tests/BeSimple/Tests/SoapClient/Soap/SoapClientTest.php rename to tests/BeSimple/Tests/SoapClient/SoapClientTest.php index bb6d03a..7d9d4dd 100644 --- a/tests/BeSimple/Tests/SoapClient/Soap/SoapClientTest.php +++ b/tests/BeSimple/Tests/SoapClient/SoapClientTest.php @@ -10,10 +10,10 @@ * with this source code in the file LICENSE. */ -namespace BeSimple\Tests\SoapClient\Soap; +namespace BeSimple\Tests\SoapClient; use BeSimple\SoapCommon\Cache; -use BeSimple\SoapClient\Soap\SoapClient; +use BeSimple\SoapClient\SoapClient; class SoapClientTest extends \PHPUnit_Framework_TestCase { diff --git a/tests/BeSimple/Tests/SoapClient/Soap/SoapRequestTest.php b/tests/BeSimple/Tests/SoapClient/SoapRequestTest.php similarity index 96% rename from tests/BeSimple/Tests/SoapClient/Soap/SoapRequestTest.php rename to tests/BeSimple/Tests/SoapClient/SoapRequestTest.php index 8e06a23..efb5123 100644 --- a/tests/BeSimple/Tests/SoapClient/Soap/SoapRequestTest.php +++ b/tests/BeSimple/Tests/SoapClient/SoapRequestTest.php @@ -10,9 +10,9 @@ * with this source code in the file LICENSE. */ -namespace BeSimple\Tests\SoapClient\Soap; +namespace BeSimple\Tests\SoapClient; -use BeSimple\SoapClient\Soap\SoapRequest; +use BeSimple\SoapClient\SoapRequest; class SoapRequestTest extends \PHPUnit_Framework_TestCase { From caeb484e19043ffb961fc5ba09e7d9a45bd18d96 Mon Sep 17 00:00:00 2001 From: Francis Besset Date: Sun, 4 Sep 2011 12:15:11 +0200 Subject: [PATCH 04/63] Renamed client cache_wsdl option to cache_type --- src/BeSimple/SoapClient/SoapClient.php | 8 ++++---- tests/BeSimple/Tests/SoapClient/SoapClientTest.php | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/BeSimple/SoapClient/SoapClient.php b/src/BeSimple/SoapClient/SoapClient.php index c335a2e..5bcf5d6 100644 --- a/src/BeSimple/SoapClient/SoapClient.php +++ b/src/BeSimple/SoapClient/SoapClient.php @@ -36,7 +36,7 @@ class SoapClient { $this->options = array( 'debug' => false, - 'cache_wsdl' => null, + 'cache_type' => null, ); // check option names and live merge, if errors are encountered Exception will be thrown @@ -137,11 +137,11 @@ class SoapClient { $options = array(); - if (null === $this->options['cache_wsdl']) { - $this->options['cache_wsdl'] = Cache::getType(); + if (null === $this->options['cache_type']) { + $this->options['cache_type'] = Cache::getType(); } - $options['cache_wsdl'] = $this->options['cache_wsdl']; + $options['cache_wsdl'] = $this->options['cache_type']; $options['trace'] = $this->options['debug']; return $options; diff --git a/tests/BeSimple/Tests/SoapClient/SoapClientTest.php b/tests/BeSimple/Tests/SoapClient/SoapClientTest.php index 7d9d4dd..aa20ec3 100644 --- a/tests/BeSimple/Tests/SoapClient/SoapClientTest.php +++ b/tests/BeSimple/Tests/SoapClient/SoapClientTest.php @@ -21,7 +21,7 @@ class SoapClientTest extends \PHPUnit_Framework_TestCase { $soapClient = new SoapClient('foo.wsdl'); $options = array( - 'cache_wsdl' => Cache::TYPE_DISK_MEMORY, + 'cache_type' => Cache::TYPE_DISK_MEMORY, 'debug' => true, ); $soapClient->setOptions($options); @@ -67,7 +67,7 @@ class SoapClientTest extends \PHPUnit_Framework_TestCase $soapClient = new SoapClient('foo.wsdl', array('debug' => true)); $this->assertEquals(array('cache_wsdl' => Cache::getType(), 'trace' => true), $soapClient->getSoapOptions()); - $soapClient = new SoapClient('foo.wsdl', array('debug' => false, 'cache_wsdl' => Cache::TYPE_NONE)); + $soapClient = new SoapClient('foo.wsdl', array('debug' => false, 'cache_type' => Cache::TYPE_NONE)); $this->assertEquals(array('cache_wsdl' => Cache::TYPE_NONE, 'trace' => false), $soapClient->getSoapOptions()); } From 5790a895716ee48bf7f0122fc10088d601029767 Mon Sep 17 00:00:00 2001 From: Francis Besset Date: Sun, 4 Sep 2011 23:42:03 +0200 Subject: [PATCH 05/63] Added SoapHeader in SoapRequest --- src/BeSimple/SoapClient/SoapClient.php | 19 ++++++++- src/BeSimple/SoapClient/SoapRequest.php | 40 ++++++++++++++++++- .../Tests/SoapClient/SoapClientTest.php | 20 ++++++++++ .../Tests/SoapClient/SoapRequestTest.php | 30 ++++++++++++-- 4 files changed, 104 insertions(+), 5 deletions(-) diff --git a/src/BeSimple/SoapClient/SoapClient.php b/src/BeSimple/SoapClient/SoapClient.php index 5bcf5d6..85eac14 100644 --- a/src/BeSimple/SoapClient/SoapClient.php +++ b/src/BeSimple/SoapClient/SoapClient.php @@ -37,6 +37,7 @@ class SoapClient $this->options = array( 'debug' => false, 'cache_type' => null, + 'namespace' => null, ); // check option names and live merge, if errors are encountered Exception will be thrown @@ -114,10 +115,26 @@ class SoapClient return $this->getNativeSoapClient()->__soapCall( $soapRequest->getFunction(), $soapRequest->getArguments(), - $soapRequest->getOptions() + $soapRequest->getOptions(), + $soapRequest->getHeaders() ); } + /** + * @param string The SoapHeader name + * @param mixed The SoapHeader value + * + * @return \SoapHeader + */ + public function createSoapHeader($name, $value) + { + if (null === $namespace = $this->getOption('namespace')) { + throw new \RuntimeException('You cannot create SoapHeader if you do not specify a namespace.'); + } + + return new \SoapHeader($namespace, $name, $value); + } + /** * @return \SoapClient */ diff --git a/src/BeSimple/SoapClient/SoapRequest.php b/src/BeSimple/SoapClient/SoapRequest.php index 0960074..8973ab9 100644 --- a/src/BeSimple/SoapClient/SoapRequest.php +++ b/src/BeSimple/SoapClient/SoapRequest.php @@ -20,12 +20,14 @@ class SoapRequest protected $function; protected $arguments; protected $options; + protected $headers; - public function __construct($function = null, array $arguments = array(), array $options = array()) + public function __construct($function = null, array $arguments = array(), array $options = array(), array $headers = array()) { $this->function = $function; $this->arguments = $arguments; $this->options = $options; + $this->setHeaders($headers); } /** @@ -143,6 +145,42 @@ class SoapRequest return $this; } + /** + * @return array + */ + public function getHeaders() + { + return $this->headers; + } + + /** + * @param array $headers + * + * @return SoapRequest + */ + public function setHeaders(array $headers) + { + $this->headers = array(); + + foreach ($headers as $header) { + $this->addHeader($header); + } + + return $this; + } + + /** + * @param \SoapHeader $header + * + * @return SoapRequest + */ + public function addHeader(\SoapHeader $header) + { + $this->headers[] = $header; + + return $this; + } + /** * @param string The name of option * @param mixed The value of option diff --git a/tests/BeSimple/Tests/SoapClient/SoapClientTest.php b/tests/BeSimple/Tests/SoapClient/SoapClientTest.php index aa20ec3..985351f 100644 --- a/tests/BeSimple/Tests/SoapClient/SoapClientTest.php +++ b/tests/BeSimple/Tests/SoapClient/SoapClientTest.php @@ -23,6 +23,7 @@ class SoapClientTest extends \PHPUnit_Framework_TestCase $options = array( 'cache_type' => Cache::TYPE_DISK_MEMORY, 'debug' => true, + 'namespace' => 'foo', ); $soapClient->setOptions($options); @@ -61,6 +62,25 @@ class SoapClientTest extends \PHPUnit_Framework_TestCase $soapClient->getOption('bad_option'); } + public function testCreateSoapHeader() + { + $soapClient = new SoapClient('foo.wsdl', array('namespace' => 'http://foobar/soap/User/1.0/')); + $soapHeader = $soapClient->createSoapHeader('foo', 'bar'); + + $this->assertInstanceOf('SoapHeader', $soapHeader); + $this->assertEquals('http://foobar/soap/User/1.0/', $soapHeader->namespace); + $this->assertEquals('foo', $soapHeader->name); + $this->assertEquals('bar', $soapHeader->data); + } + + public function testCreateSoapHeaderThrowsAnExceptionIfNamespaceIsNull() + { + $soapClient = new SoapClient('foo.wsdl'); + + $this->setExpectedException('RuntimeException'); + $soapHeader = $soapClient->createSoapHeader('foo', 'bar'); + } + public function testGetSoapOptions() { Cache::setType(Cache::TYPE_MEMORY); diff --git a/tests/BeSimple/Tests/SoapClient/SoapRequestTest.php b/tests/BeSimple/Tests/SoapClient/SoapRequestTest.php index efb5123..56240c6 100644 --- a/tests/BeSimple/Tests/SoapClient/SoapRequestTest.php +++ b/tests/BeSimple/Tests/SoapClient/SoapRequestTest.php @@ -40,7 +40,8 @@ class SoapRequestTest extends \PHPUnit_Framework_TestCase { $soapRequest = new SoapRequest(); - $this->assertEquals(false, $soapRequest->getArgument('foo', false)); + $this->assertSame(null, $soapRequest->getArgument('foo')); + $this->assertFalse($soapRequest->getArgument('foo', false)); $soapRequest->addArgument('foo', 'bar'); @@ -63,13 +64,30 @@ class SoapRequestTest extends \PHPUnit_Framework_TestCase { $soapRequest = new SoapRequest(); - $this->assertEquals(false, $soapRequest->getOption('soapaction')); + $this->assertSame(null, $soapRequest->getOption('soapaction')); + $this->assertFalse($soapRequest->getOption('soapaction', false)); $soapRequest->addOption('soapaction', 'foo'); $this->assertEquals('foo', $soapRequest->getOption('soapaction')); } + public function testSetHeaders() + { + $soapRequest = new SoapRequest(); + + $this->assertEquals(array(), $soapRequest->getHeaders()); + + $header1 = new \SoapHeader('foobar', 'foo', 'bar'); + $header2 = new \SoapHeader('barfoo', 'bar', 'foo'); + $soapRequest + ->addHeader($header1) + ->addHeader($header2) + ; + + $this->assertSame(array($header1, $header2), $soapRequest->getHeaders()); + } + public function testConstruct() { $soapRequest = new SoapRequest(); @@ -77,13 +95,19 @@ class SoapRequestTest extends \PHPUnit_Framework_TestCase $this->assertNull($soapRequest->getFunction()); $this->assertEquals(array(), $soapRequest->getArguments()); $this->assertEquals(array(), $soapRequest->getOptions()); + $this->assertEquals(array(), $soapRequest->getHeaders()); $arguments = array('bar' => 'foobar'); $options = array('soapaction' => 'foobar'); - $soapRequest = new SoapRequest('foo', $arguments, $options); + $headers = array( + new \SoapHeader('foobar', 'foo', 'bar'), + new \SoapHeader('barfoo', 'bar', 'foo'), + ); + $soapRequest = new SoapRequest('foo', $arguments, $options, $headers); $this->assertEquals('foo', $soapRequest->getFunction()); $this->assertEquals($arguments, $soapRequest->getArguments()); $this->assertEquals($options, $soapRequest->getOptions()); + $this->assertSame($headers, $soapRequest->getHeaders()); } } \ No newline at end of file From c1c5d313504d0dae6a757344656780776cb62633 Mon Sep 17 00:00:00 2001 From: Francis Besset Date: Sat, 10 Sep 2011 19:33:27 +0200 Subject: [PATCH 06/63] Added TypeConverterCollection in SoapClient --- src/BeSimple/SoapClient/SoapClient.php | 37 ++++++++++++++++-- src/BeSimple/SoapClient/SoapRequest.php | 2 +- .../Tests/SoapClient/SoapClientTest.php | 39 ++++++++++++++++--- tests/bootstrap.php | 4 +- 4 files changed, 70 insertions(+), 12 deletions(-) diff --git a/src/BeSimple/SoapClient/SoapClient.php b/src/BeSimple/SoapClient/SoapClient.php index 85eac14..48a471f 100644 --- a/src/BeSimple/SoapClient/SoapClient.php +++ b/src/BeSimple/SoapClient/SoapClient.php @@ -1,7 +1,7 @@ * (c) Francis Besset @@ -13,6 +13,7 @@ namespace BeSimple\SoapClient; use BeSimple\SoapCommon\Cache; +use BeSimple\SoapCommon\Converter\TypeConverterCollection; /** * @author Francis Besset @@ -20,15 +21,17 @@ use BeSimple\SoapCommon\Cache; class SoapClient { protected $wsdl; + protected $converters; protected $soapClient; /** * @param string $wsdl * @param array $options */ - public function __construct($wsdl, array $options = array()) + public function __construct($wsdl, TypeConverterCollection $converters = null, array $options = array()) { - $this->wsdl = $wsdl; + $this->wsdl = $wsdl; + $this->converters = $converters; $this->setOptions($options); } @@ -160,7 +163,35 @@ class SoapClient $options['cache_wsdl'] = $this->options['cache_type']; $options['trace'] = $this->options['debug']; + $options['typemap'] = $this->getTypemap(); return $options; } + + /** + * @return array + */ + protected function getTypemap() + { + $typemap = array(); + + if (!$this->converters) { + return $typemap; + } + + foreach ($this->converters->all() as $typeConverter) { + $typemap[] = array( + 'type_name' => $typeConverter->getTypeName(), + 'type_ns' => $typeConverter->getTypeNamespace(), + 'from_xml' => function($input) use ($typeConverter) { + return $typeConverter->convertXmlToPhp($input); + }, + 'to_xml' => function($input) use ($typeConverter) { + return $typeConverter->convertPhpToXml($input); + }, + ); + } + + return $typemap; + } } \ No newline at end of file diff --git a/src/BeSimple/SoapClient/SoapRequest.php b/src/BeSimple/SoapClient/SoapRequest.php index 8973ab9..178a26a 100644 --- a/src/BeSimple/SoapClient/SoapRequest.php +++ b/src/BeSimple/SoapClient/SoapRequest.php @@ -1,7 +1,7 @@ * (c) Francis Besset diff --git a/tests/BeSimple/Tests/SoapClient/SoapClientTest.php b/tests/BeSimple/Tests/SoapClient/SoapClientTest.php index 985351f..34e9561 100644 --- a/tests/BeSimple/Tests/SoapClient/SoapClientTest.php +++ b/tests/BeSimple/Tests/SoapClient/SoapClientTest.php @@ -13,6 +13,9 @@ namespace BeSimple\Tests\SoapClient; use BeSimple\SoapCommon\Cache; +use BeSimple\SoapCommon\Converter\DateTimeTypeConverter; +use BeSimple\SoapCommon\Converter\DateTypeConverter; +use BeSimple\SoapCommon\Converter\TypeConverterCollection; use BeSimple\SoapClient\SoapClient; class SoapClientTest extends \PHPUnit_Framework_TestCase @@ -64,7 +67,7 @@ class SoapClientTest extends \PHPUnit_Framework_TestCase public function testCreateSoapHeader() { - $soapClient = new SoapClient('foo.wsdl', array('namespace' => 'http://foobar/soap/User/1.0/')); + $soapClient = new SoapClient('foo.wsdl', null, array('namespace' => 'http://foobar/soap/User/1.0/')); $soapHeader = $soapClient->createSoapHeader('foo', 'bar'); $this->assertInstanceOf('SoapHeader', $soapHeader); @@ -84,16 +87,40 @@ class SoapClientTest extends \PHPUnit_Framework_TestCase public function testGetSoapOptions() { Cache::setType(Cache::TYPE_MEMORY); - $soapClient = new SoapClient('foo.wsdl', array('debug' => true)); - $this->assertEquals(array('cache_wsdl' => Cache::getType(), 'trace' => true), $soapClient->getSoapOptions()); + $soapClient = new SoapClient('foo.wsdl', null, array('debug' => true)); + $this->assertEquals(array('cache_wsdl' => Cache::getType(), 'trace' => true, 'typemap' => array()), $soapClient->getSoapOptions()); - $soapClient = new SoapClient('foo.wsdl', array('debug' => false, 'cache_type' => Cache::TYPE_NONE)); - $this->assertEquals(array('cache_wsdl' => Cache::TYPE_NONE, 'trace' => false), $soapClient->getSoapOptions()); + $soapClient = new SoapClient('foo.wsdl', null, array('debug' => false, 'cache_type' => Cache::TYPE_NONE)); + $this->assertEquals(array('cache_wsdl' => Cache::TYPE_NONE, 'trace' => false, 'typemap' => array()), $soapClient->getSoapOptions()); + } + + public function testGetSoapOptionsWithTypemap() + { + $converters = new TypeConverterCollection(); + + $dateTimeTypeConverter = new DateTimeTypeConverter(); + $converters->add($dateTimeTypeConverter); + + $dateTypeConverter = new DateTypeConverter(); + $converters->add($dateTypeConverter); + + $soapClient = new SoapClient('foo.wsdl', $converters); + $soapOptions = $soapClient->getSoapOptions(); + + $this->assertEquals('http://www.w3.org/2001/XMLSchema', $soapOptions['typemap'][0]['type_ns']); + $this->assertEquals('dateTime', $soapOptions['typemap'][0]['type_name']); + $this->assertInstanceOf('Closure', $soapOptions['typemap'][0]['from_xml']); + $this->assertInstanceOf('Closure', $soapOptions['typemap'][0]['to_xml']); + + $this->assertEquals('http://www.w3.org/2001/XMLSchema', $soapOptions['typemap'][1]['type_ns']); + $this->assertEquals('date', $soapOptions['typemap'][1]['type_name']); + $this->assertInstanceOf('Closure', $soapOptions['typemap'][1]['from_xml']); + $this->assertInstanceOf('Closure', $soapOptions['typemap'][1]['to_xml']); } public function testGetNativeSoapClient() { - $soapClient = new SoapClient(__DIR__.'/Fixtures/foobar.wsdl', array('debug' => true)); + $soapClient = new SoapClient(__DIR__.'/Fixtures/foobar.wsdl', null, array('debug' => true)); $this->assertInstanceOf('SoapClient', $soapClient->getNativeSoapClient()); } diff --git a/tests/bootstrap.php b/tests/bootstrap.php index fdc4599..2e0638c 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -11,14 +11,14 @@ spl_autoload_register(function($class) { return true; } - } else if (0 === strpos($class, 'BeSimple\SoapClient\\')) { + } elseif (0 === strpos($class, 'BeSimple\SoapClient\\')) { $path = __DIR__.'/../src/'.($class = strtr($class, '\\', '/')).'.php'; if (file_exists($path) && is_readable($path)) { require_once $path; return true; } - } else if (0 === strpos($class, 'BeSimple\SoapCommon\\')) { + } elseif (0 === strpos($class, 'BeSimple\SoapCommon\\')) { $path = __DIR__.'/../vendor/besimple-soapcommon/src/'.($class = strtr($class, '\\', '/')).'.php'; if (file_exists($path) && is_readable($path)) { require_once $path; From 9cec9c65c4f10519df1a55ac1fe4aef790d75eec Mon Sep 17 00:00:00 2001 From: Francis Besset Date: Tue, 13 Sep 2011 21:03:03 +0200 Subject: [PATCH 07/63] Moved SoapClient::getTypemap() function in TypeConverterCollection --- src/BeSimple/SoapClient/SoapClient.php | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/src/BeSimple/SoapClient/SoapClient.php b/src/BeSimple/SoapClient/SoapClient.php index 48a471f..6832ecb 100644 --- a/src/BeSimple/SoapClient/SoapClient.php +++ b/src/BeSimple/SoapClient/SoapClient.php @@ -173,25 +173,10 @@ class SoapClient */ protected function getTypemap() { - $typemap = array(); - if (!$this->converters) { - return $typemap; + return array(); } - foreach ($this->converters->all() as $typeConverter) { - $typemap[] = array( - 'type_name' => $typeConverter->getTypeName(), - 'type_ns' => $typeConverter->getTypeNamespace(), - 'from_xml' => function($input) use ($typeConverter) { - return $typeConverter->convertXmlToPhp($input); - }, - 'to_xml' => function($input) use ($typeConverter) { - return $typeConverter->convertPhpToXml($input); - }, - ); - } - - return $typemap; + return $this->converters->getTypemap(); } } \ No newline at end of file From d29f0caa8dbbe5481ba9261ed757536747c79b9b Mon Sep 17 00:00:00 2001 From: Francis Besset Date: Fri, 23 Sep 2011 10:23:59 +0200 Subject: [PATCH 08/63] [SoapRequest] Returns null if headers is empty --- src/BeSimple/SoapClient/SoapRequest.php | 6 +++--- tests/BeSimple/Tests/SoapClient/SoapRequestTest.php | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/BeSimple/SoapClient/SoapRequest.php b/src/BeSimple/SoapClient/SoapRequest.php index 178a26a..28f2926 100644 --- a/src/BeSimple/SoapClient/SoapRequest.php +++ b/src/BeSimple/SoapClient/SoapRequest.php @@ -146,11 +146,11 @@ class SoapRequest } /** - * @return array + * @return array|null */ public function getHeaders() { - return $this->headers; + return empty($this->headers) ? null : $this->headers; } /** @@ -194,4 +194,4 @@ class SoapRequest return $this; } -} \ No newline at end of file +} diff --git a/tests/BeSimple/Tests/SoapClient/SoapRequestTest.php b/tests/BeSimple/Tests/SoapClient/SoapRequestTest.php index 56240c6..c56d195 100644 --- a/tests/BeSimple/Tests/SoapClient/SoapRequestTest.php +++ b/tests/BeSimple/Tests/SoapClient/SoapRequestTest.php @@ -76,7 +76,7 @@ class SoapRequestTest extends \PHPUnit_Framework_TestCase { $soapRequest = new SoapRequest(); - $this->assertEquals(array(), $soapRequest->getHeaders()); + $this->assertEquals(null, $soapRequest->getHeaders()); $header1 = new \SoapHeader('foobar', 'foo', 'bar'); $header2 = new \SoapHeader('barfoo', 'bar', 'foo'); @@ -95,7 +95,7 @@ class SoapRequestTest extends \PHPUnit_Framework_TestCase $this->assertNull($soapRequest->getFunction()); $this->assertEquals(array(), $soapRequest->getArguments()); $this->assertEquals(array(), $soapRequest->getOptions()); - $this->assertEquals(array(), $soapRequest->getHeaders()); + $this->assertEquals(null, $soapRequest->getHeaders()); $arguments = array('bar' => 'foobar'); $options = array('soapaction' => 'foobar'); @@ -110,4 +110,4 @@ class SoapRequestTest extends \PHPUnit_Framework_TestCase $this->assertEquals($options, $soapRequest->getOptions()); $this->assertSame($headers, $soapRequest->getHeaders()); } -} \ No newline at end of file +} From a46a42412ef8837ef58c07c44b3a8dce657c266b Mon Sep 17 00:00:00 2001 From: Andreas Schamberger Date: Mon, 26 Sep 2011 15:52:03 +0200 Subject: [PATCH 09/63] Renamed SoapClient to SimpleSoapClient --- .gitignore | 4 ++++ .../{SoapClient.php => SimpleSoapClient.php} | 2 +- ...lientTest.php => SimpleSoapClientTest.php} | 24 +++++++++---------- 3 files changed, 17 insertions(+), 13 deletions(-) rename src/BeSimple/SoapClient/{SoapClient.php => SimpleSoapClient.php} (99%) rename tests/BeSimple/Tests/SoapClient/{SoapClientTest.php => SimpleSoapClientTest.php} (81%) diff --git a/.gitignore b/.gitignore index 22d0d82..f8d178e 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,5 @@ vendor +/phpunit.xml +.buildpath +.project +.settings \ No newline at end of file diff --git a/src/BeSimple/SoapClient/SoapClient.php b/src/BeSimple/SoapClient/SimpleSoapClient.php similarity index 99% rename from src/BeSimple/SoapClient/SoapClient.php rename to src/BeSimple/SoapClient/SimpleSoapClient.php index 6832ecb..64b8991 100644 --- a/src/BeSimple/SoapClient/SoapClient.php +++ b/src/BeSimple/SoapClient/SimpleSoapClient.php @@ -18,7 +18,7 @@ use BeSimple\SoapCommon\Converter\TypeConverterCollection; /** * @author Francis Besset */ -class SoapClient +class SimpleSoapClient { protected $wsdl; protected $converters; diff --git a/tests/BeSimple/Tests/SoapClient/SoapClientTest.php b/tests/BeSimple/Tests/SoapClient/SimpleSoapClientTest.php similarity index 81% rename from tests/BeSimple/Tests/SoapClient/SoapClientTest.php rename to tests/BeSimple/Tests/SoapClient/SimpleSoapClientTest.php index 34e9561..89b8ef7 100644 --- a/tests/BeSimple/Tests/SoapClient/SoapClientTest.php +++ b/tests/BeSimple/Tests/SoapClient/SimpleSoapClientTest.php @@ -16,13 +16,13 @@ use BeSimple\SoapCommon\Cache; use BeSimple\SoapCommon\Converter\DateTimeTypeConverter; use BeSimple\SoapCommon\Converter\DateTypeConverter; use BeSimple\SoapCommon\Converter\TypeConverterCollection; -use BeSimple\SoapClient\SoapClient; +use BeSimple\SoapClient\SimpleSoapClient; class SoapClientTest extends \PHPUnit_Framework_TestCase { public function testSetOptions() { - $soapClient = new SoapClient('foo.wsdl'); + $soapClient = new SimpleSoapClient('foo.wsdl'); $options = array( 'cache_type' => Cache::TYPE_DISK_MEMORY, 'debug' => true, @@ -35,7 +35,7 @@ class SoapClientTest extends \PHPUnit_Framework_TestCase public function testSetOptionsThrowsAnExceptionIfOptionsDoesNotExists() { - $soapClient = new SoapClient('foo.wsdl'); + $soapClient = new SimpleSoapClient('foo.wsdl'); $this->setExpectedException('InvalidArgumentException'); $soapClient->setOptions(array('bad_option' => true)); @@ -43,7 +43,7 @@ class SoapClientTest extends \PHPUnit_Framework_TestCase public function testSetOption() { - $soapClient = new SoapClient('foo.wsdl'); + $soapClient = new SimpleSoapClient('foo.wsdl'); $soapClient->setOption('debug', true); $this->assertEquals(true, $soapClient->getOption('debug')); @@ -51,7 +51,7 @@ class SoapClientTest extends \PHPUnit_Framework_TestCase public function testSetOptionThrowsAnExceptionIfOptionDoesNotExists() { - $soapClient = new SoapClient('foo.wsdl'); + $soapClient = new SimpleSoapClient('foo.wsdl'); $this->setExpectedException('InvalidArgumentException'); $soapClient->setOption('bad_option', 'bar'); @@ -59,7 +59,7 @@ class SoapClientTest extends \PHPUnit_Framework_TestCase public function testGetOptionThrowsAnExceptionIfOptionDoesNotExists() { - $soapClient = new SoapClient('foo.wsdl'); + $soapClient = new SimpleSoapClient('foo.wsdl'); $this->setExpectedException('InvalidArgumentException'); $soapClient->getOption('bad_option'); @@ -67,7 +67,7 @@ class SoapClientTest extends \PHPUnit_Framework_TestCase public function testCreateSoapHeader() { - $soapClient = new SoapClient('foo.wsdl', null, array('namespace' => 'http://foobar/soap/User/1.0/')); + $soapClient = new SimpleSoapClient('foo.wsdl', null, array('namespace' => 'http://foobar/soap/User/1.0/')); $soapHeader = $soapClient->createSoapHeader('foo', 'bar'); $this->assertInstanceOf('SoapHeader', $soapHeader); @@ -78,7 +78,7 @@ class SoapClientTest extends \PHPUnit_Framework_TestCase public function testCreateSoapHeaderThrowsAnExceptionIfNamespaceIsNull() { - $soapClient = new SoapClient('foo.wsdl'); + $soapClient = new SimpleSoapClient('foo.wsdl'); $this->setExpectedException('RuntimeException'); $soapHeader = $soapClient->createSoapHeader('foo', 'bar'); @@ -87,10 +87,10 @@ class SoapClientTest extends \PHPUnit_Framework_TestCase public function testGetSoapOptions() { Cache::setType(Cache::TYPE_MEMORY); - $soapClient = new SoapClient('foo.wsdl', null, array('debug' => true)); + $soapClient = new SimpleSoapClient('foo.wsdl', null, array('debug' => true)); $this->assertEquals(array('cache_wsdl' => Cache::getType(), 'trace' => true, 'typemap' => array()), $soapClient->getSoapOptions()); - $soapClient = new SoapClient('foo.wsdl', null, array('debug' => false, 'cache_type' => Cache::TYPE_NONE)); + $soapClient = new SimpleSoapClient('foo.wsdl', null, array('debug' => false, 'cache_type' => Cache::TYPE_NONE)); $this->assertEquals(array('cache_wsdl' => Cache::TYPE_NONE, 'trace' => false, 'typemap' => array()), $soapClient->getSoapOptions()); } @@ -104,7 +104,7 @@ class SoapClientTest extends \PHPUnit_Framework_TestCase $dateTypeConverter = new DateTypeConverter(); $converters->add($dateTypeConverter); - $soapClient = new SoapClient('foo.wsdl', $converters); + $soapClient = new SimpleSoapClient('foo.wsdl', $converters); $soapOptions = $soapClient->getSoapOptions(); $this->assertEquals('http://www.w3.org/2001/XMLSchema', $soapOptions['typemap'][0]['type_ns']); @@ -120,7 +120,7 @@ class SoapClientTest extends \PHPUnit_Framework_TestCase public function testGetNativeSoapClient() { - $soapClient = new SoapClient(__DIR__.'/Fixtures/foobar.wsdl', null, array('debug' => true)); + $soapClient = new SimpleSoapClient(__DIR__.'/Fixtures/foobar.wsdl', null, array('debug' => true)); $this->assertInstanceOf('SoapClient', $soapClient->getNativeSoapClient()); } From dfb2bcebb816abaa675418c90dcabf09b1d0529f Mon Sep 17 00:00:00 2001 From: Andreas Schamberger Date: Mon, 26 Sep 2011 16:25:56 +0200 Subject: [PATCH 10/63] replace get_class($this) with __CLASS__ --- src/BeSimple/SoapClient/SimpleSoapClient.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/BeSimple/SoapClient/SimpleSoapClient.php b/src/BeSimple/SoapClient/SimpleSoapClient.php index 64b8991..62f115a 100644 --- a/src/BeSimple/SoapClient/SimpleSoapClient.php +++ b/src/BeSimple/SoapClient/SimpleSoapClient.php @@ -58,7 +58,7 @@ class SimpleSoapClient if ($isInvalid) { throw new \InvalidArgumentException(sprintf( 'The "%s" class does not support the following options: "%s".', - get_class($this), + __CLASS__, implode('\', \'', $invalid) )); } @@ -75,7 +75,7 @@ class SimpleSoapClient if (!array_key_exists($name, $this->options)) { throw new \InvalidArgumentException(sprintf( 'The "%s" class does not support the "%s" option.', - get_class($this), + __CLASS__, $name )); } @@ -100,7 +100,7 @@ class SimpleSoapClient if (!array_key_exists($key, $this->options)) { throw new \InvalidArgumentException(sprintf( 'The "%s" class does not support the "%s" option.', - get_class($this), + __CLASS__, $key )); } From 713edb591caf1eb287a753277d5ac3a836cb738d Mon Sep 17 00:00:00 2001 From: Andreas Schamberger Date: Mon, 26 Sep 2011 22:42:21 +0200 Subject: [PATCH 11/63] Initial commit for basic SoapClient enhancements. --- src/BeSimple/SoapClient/Curl.php | 323 +++++++++++++++++ src/BeSimple/SoapClient/Helper.php | 396 +++++++++++++++++++++ src/BeSimple/SoapClient/SoapClient.php | 297 ++++++++++++++++ src/BeSimple/SoapClient/WsdlDownloader.php | 240 +++++++++++++ 4 files changed, 1256 insertions(+) create mode 100644 src/BeSimple/SoapClient/Curl.php create mode 100644 src/BeSimple/SoapClient/Helper.php create mode 100644 src/BeSimple/SoapClient/SoapClient.php create mode 100644 src/BeSimple/SoapClient/WsdlDownloader.php 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 From 06ff1ab70771dd20c2a0940df3029a375ad0fe93 Mon Sep 17 00:00:00 2001 From: Andreas Schamberger Date: Sun, 2 Oct 2011 12:09:19 +0200 Subject: [PATCH 12/63] tests for WsdlDownloader and cs fixes --- src/BeSimple/SoapClient/WsdlDownloader.php | 193 ++++++++--------- .../SoapClient/Fixtures/type_include.xsd | 15 ++ .../xsdinclude/xsdinctest_absolute.xml | 9 + .../xsdinclude/xsdinctest_relative.xml | 9 + .../Tests/SoapClient/WsdlDownloaderTest.php | 205 ++++++++++++++++++ 5 files changed, 327 insertions(+), 104 deletions(-) create mode 100644 tests/BeSimple/Tests/SoapClient/Fixtures/type_include.xsd create mode 100644 tests/BeSimple/Tests/SoapClient/Fixtures/xsdinclude/xsdinctest_absolute.xml create mode 100644 tests/BeSimple/Tests/SoapClient/Fixtures/xsdinclude/xsdinctest_relative.xml create mode 100644 tests/BeSimple/Tests/SoapClient/WsdlDownloaderTest.php diff --git a/src/BeSimple/SoapClient/WsdlDownloader.php b/src/BeSimple/SoapClient/WsdlDownloader.php index b13fe6e..d80803b 100644 --- a/src/BeSimple/SoapClient/WsdlDownloader.php +++ b/src/BeSimple/SoapClient/WsdlDownloader.php @@ -8,7 +8,7 @@ * * 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 */ @@ -54,26 +54,28 @@ class WsdlDownloader /** * Constructor. + * + * @param array $options */ - public function __construct( $options ) + public function __construct(array $options = array()) { // 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 = (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 = 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->cacheDir = rtrim($this->cacheDir, '/\\'); + $this->cacheTtl = ini_get('soap.wsdl_cache_ttl'); $this->options = $options; + if (!isset($this->options['resolve_xsd_includes'])) { + $this->options['resolve_xsd_includes'] = true; + } } /** @@ -82,62 +84,44 @@ class WsdlDownloader * @param string $wsdl * @return string */ - public function download( $wsdl ) + 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 ) - { + $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 ); + $curl = new Curl($this->options); // execute request - $responseSuccessfull = $curl->exec( $wsdl ); + $responseSuccessfull = $curl->exec($wsdl); // get content - if ( $responseSuccessfull === true ) - { + if ($responseSuccessfull === true) { $response = $curl->getResponseBody(); - if ( $this->options['resolve_xsd_includes'] === true ) - { - $this->resolveXsdIncludes( $response, $cacheFile, $wsdl ); - } - else - { - file_put_contents( $cacheFile, $response ); + 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 ."'"); } - 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 ."'" ); + } 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 ."'" ); + } elseif (file_exists($wsdl)) { + return realpath($wsdl); + } else { + throw new \ErrorException("SOAP-ERROR: Parsing WSDL: Couldn't load from '" . $wsdl ."'"); } } @@ -147,14 +131,12 @@ class WsdlDownloader * @param string $file * @return boolean */ - private function isRemoteFile( $file ) + 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' ) - { + if (($url = @parse_url($file)) !== false) { + if (isset($url['scheme']) && substr($url['scheme'], 0, 4) == 'http') { $isRemoteFile = true; } } @@ -169,33 +151,28 @@ class WsdlDownloader * @param unknown_type $parentIsRemote * @return string */ - private function resolveXsdIncludes( $xml, $cacheFile, $parentFile = null ) + 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 ); + $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 ); + $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 ); + $doc->save($cacheFile); } /** @@ -205,36 +182,44 @@ class WsdlDownloader * @param string $relative * @return string */ - private function resolveRelativePathInUrl( $base, $relative ) + private function resolveRelativePathInUrl($base, $relative) { - $urlParts = parse_url( $base ); + $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 ); + if (isset($urlParts['path']) && strpos($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 - $path = preg_replace( '~/\./~', '/', $path ); + $path = preg_replace('~/\./~', '/', $path); // remove double slashes - $path = preg_replace( '~/+~', '/', $path ); + $path = preg_replace('~/+~', '/', $path); // split path by '/' - $parts = explode( '/', $path ); + $parts = explode('/', $path); // resolve /../ - foreach ( $parts as $key => $part ) - { - if ( $part == ".." ) - { - if ( $key-1 >= 0 ) - { - unset( $parts[$key-1] ); + foreach ($parts as $key => $part) { + if ($part == "..") { + $keyToDelete = $key-1; + while ($keyToDelete > 0) { + if (isset($parts[$keyToDelete])) { + unset($parts[$keyToDelete]); + break; + } else { + $keyToDelete--; + } } - unset( $parts[$key] ); + unset($parts[$key]); } } - return $urlParts['scheme'] . '://' . $urlParts['host'] . implode( '/', $parts ); + return $urlParts['scheme'] . '://' . $urlParts['host'] . implode('/', $parts); } } \ No newline at end of file diff --git a/tests/BeSimple/Tests/SoapClient/Fixtures/type_include.xsd b/tests/BeSimple/Tests/SoapClient/Fixtures/type_include.xsd new file mode 100644 index 0000000..a41dd9a --- /dev/null +++ b/tests/BeSimple/Tests/SoapClient/Fixtures/type_include.xsd @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/BeSimple/Tests/SoapClient/Fixtures/xsdinclude/xsdinctest_absolute.xml b/tests/BeSimple/Tests/SoapClient/Fixtures/xsdinclude/xsdinctest_absolute.xml new file mode 100644 index 0000000..dc1b373 --- /dev/null +++ b/tests/BeSimple/Tests/SoapClient/Fixtures/xsdinclude/xsdinctest_absolute.xml @@ -0,0 +1,9 @@ + + + xsdinctest + + + + + + diff --git a/tests/BeSimple/Tests/SoapClient/Fixtures/xsdinclude/xsdinctest_relative.xml b/tests/BeSimple/Tests/SoapClient/Fixtures/xsdinclude/xsdinctest_relative.xml new file mode 100644 index 0000000..58cea74 --- /dev/null +++ b/tests/BeSimple/Tests/SoapClient/Fixtures/xsdinclude/xsdinctest_relative.xml @@ -0,0 +1,9 @@ + + + xsdinctest + + + + + + diff --git a/tests/BeSimple/Tests/SoapClient/WsdlDownloaderTest.php b/tests/BeSimple/Tests/SoapClient/WsdlDownloaderTest.php new file mode 100644 index 0000000..a07aa46 --- /dev/null +++ b/tests/BeSimple/Tests/SoapClient/WsdlDownloaderTest.php @@ -0,0 +1,205 @@ + + * (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; + +use BeSimple\SoapClient\WsdlDownloader; + +class WsdlDownloaderTest extends \PHPUnit_Framework_TestCase +{ + protected $webserverProcessId; + + protected function startPhpWebserver() + { + if ('Windows' == substr(php_uname('s'), 0, 7 )) { + $powershellCommand = "\$app = start-process php.exe -ArgumentList '-S localhost:8000 -t ".__DIR__.DIRECTORY_SEPARATOR."Fixtures' -WindowStyle 'Hidden' -passthru; Echo \$app.Id;"; + $shellCommand = 'powershell -command "& {'.$powershellCommand.'}"'; + } else { + $shellCommand = "nohup php -S localhost:8000 -t ".__DIR__.DIRECTORY_SEPARATOR."Fixtures &"; + } + $output = array(); + exec($shellCommand, $output); + $this->webserverProcessId = $output[0]; // pid is in first element + } + + protected function stopPhpWebserver() + { + if (!is_null($this->webserverProcessId)) { + if ('Windows' == substr(php_uname('s'), 0, 7 )) { + exec('TASKKILL /F /PID ' . $this->webserverProcessId); + } else { + exec('kill ' . $this->webserverProcessId); + } + $this->webserverProcessId = null; + } + } + + public function testDownload() + { + $this->startPhpWebserver(); + + $options = array( + 'resolve_xsd_includes' => true, + ); + $wd = new WsdlDownloader($options); + + $cacheDir = ini_get('soap.wsdl_cache_dir'); + if (!is_dir($cacheDir)) { + $cacheDir = sys_get_temp_dir(); + $cacheDirForRegExp = preg_quote( $cacheDir ); + } + + $tests = array( + 'localWithAbsolutePath' => array( + 'source' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/xsdinclude/xsdinctest_absolute.xml', + 'assertRegExp' => '~.*'.$cacheDirForRegExp.'\\\wsdl_.*\.cache.*~', + ), + 'localWithRelativePath' => array( + 'source' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/xsdinclude/xsdinctest_relative.xml', + 'assertRegExp' => '~.*\.\./type_include\.xsd.*~', + ), + 'remoteWithAbsolutePath' => array( + 'source' => 'http://localhost:8000/xsdinclude/xsdinctest_absolute.xml', + 'assertRegExp' => '~.*'.$cacheDirForRegExp.'\\\wsdl_.*\.cache.*~', + ), + 'remoteWithAbsolutePath' => array( + 'source' => 'http://localhost:8000/xsdinclude/xsdinctest_relative.xml', + 'assertRegExp' => '~.*'.$cacheDirForRegExp.'\\\wsdl_.*\.cache.*~', + ), + ); + + foreach ($tests as $name => $values) { + $cacheFileName = $wd->download($values['source']); + $result = file_get_contents($cacheFileName); + $this->assertRegExp($values['assertRegExp'],$result,$name); + unlink($cacheFileName); + } + + $this->stopPhpWebserver(); + } + + public function testIsRemoteFile() + { + $wd = new WsdlDownloader(); + + $class = new \ReflectionClass($wd); + $method = $class->getMethod('isRemoteFile'); + $method->setAccessible(true); + + $this->assertEquals(true, $method->invoke($wd, 'http://www.php.net/')); + $this->assertEquals(true, $method->invoke($wd, 'http://localhost/')); + $this->assertEquals(true, $method->invoke($wd, 'http://mylocaldomain/')); + $this->assertEquals(true, $method->invoke($wd, 'http://www.php.net/dir/test.html')); + $this->assertEquals(true, $method->invoke($wd, 'http://localhost/dir/test.html')); + $this->assertEquals(true, $method->invoke($wd, 'http://mylocaldomain/dir/test.html')); + $this->assertEquals(true, $method->invoke($wd, 'https://www.php.net/')); + $this->assertEquals(true, $method->invoke($wd, 'https://localhost/')); + $this->assertEquals(true, $method->invoke($wd, 'https://mylocaldomain/')); + $this->assertEquals(true, $method->invoke($wd, 'https://www.php.net/dir/test.html')); + $this->assertEquals(true, $method->invoke($wd, 'https://localhost/dir/test.html')); + $this->assertEquals(true, $method->invoke($wd, 'https://mylocaldomain/dir/test.html')); + $this->assertEquals(false, $method->invoke($wd, 'c:/dir/test.html')); + $this->assertEquals(false, $method->invoke($wd, '/dir/test.html')); + $this->assertEquals(false, $method->invoke($wd, '../dir/test.html')); + } + + public function testResolveXsdIncludes() + { + $this->startPhpWebserver(); + + $options = array( + 'resolve_xsd_includes' => true, + ); + $wd = new WsdlDownloader($options); + + $class = new \ReflectionClass($wd); + $method = $class->getMethod('resolveXsdIncludes'); + $method->setAccessible(true); + + $cacheDir = ini_get('soap.wsdl_cache_dir'); + if (!is_dir($cacheDir)) { + $cacheDir = sys_get_temp_dir(); + $cacheDirForRegExp = preg_quote( $cacheDir ); + } + + $remoteUrlAbsolute = 'http://localhost:8000/xsdinclude/xsdinctest_absolute.xml'; + $remoteUrlRelative = 'http://localhost:8000/xsdinclude/xsdinctest_relative.xml'; + $tests = array( + 'localWithAbsolutePath' => array( + 'source' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/xsdinclude/xsdinctest_absolute.xml', + 'cacheFile' => $cacheDir.'/cache_local_absolute.xml', + 'remoteParentUrl' => null, + 'assertRegExp' => '~.*'.$cacheDirForRegExp.'\\\wsdl_.*\.cache.*~', + ), + 'localWithRelativePath' => array( + 'source' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/xsdinclude/xsdinctest_relative.xml', + 'cacheFile' => $cacheDir.'/cache_local_relative.xml', + 'remoteParentUrl' => null, + 'assertRegExp' => '~.*\.\./type_include\.xsd.*~', + ), + 'remoteWithAbsolutePath' => array( + 'source' => $remoteUrlAbsolute, + 'cacheFile' => $cacheDir.'/cache_remote_absolute.xml', + 'remoteParentUrl' => $remoteUrlAbsolute, + 'assertRegExp' => '~.*'.$cacheDirForRegExp.'\\\wsdl_.*\.cache.*~', + ), + 'remoteWithAbsolutePath' => array( + 'source' => $remoteUrlRelative, + 'cacheFile' => $cacheDir.'/cache_remote_relative.xml', + 'remoteParentUrl' => $remoteUrlRelative, + 'assertRegExp' => '~.*'.$cacheDirForRegExp.'\\\wsdl_.*\.cache.*~', + ), + ); + + foreach ($tests as $name => $values) { + $wsdl = file_get_contents( $values['source'] ); + $method->invoke($wd, $wsdl, $values['cacheFile'],$values['remoteParentUrl']); + $result = file_get_contents($values['cacheFile']); + $this->assertRegExp($values['assertRegExp'],$result,$name); + unlink($values['cacheFile']); + } + + $this->stopPhpWebserver(); + } + + public function testResolveRelativePathInUrl() + { + $wd = new WsdlDownloader(); + + $class = new \ReflectionClass($wd); + $method = $class->getMethod('resolveRelativePathInUrl'); + $method->setAccessible(true); + + $this->assertEquals('http://localhost/test', $method->invoke($wd, 'http://localhost/sub', '/test')); + $this->assertEquals('http://localhost/test', $method->invoke($wd, 'http://localhost/sub/', '/test')); + + $this->assertEquals('http://localhost/test', $method->invoke($wd, 'http://localhost', './test')); + $this->assertEquals('http://localhost/test', $method->invoke($wd, 'http://localhost/', './test')); + + $this->assertEquals('http://localhost/sub/test', $method->invoke($wd, 'http://localhost/sub/sub', './test')); + $this->assertEquals('http://localhost/sub/sub/test', $method->invoke($wd, 'http://localhost/sub/sub/', './test')); + + $this->assertEquals('http://localhost/test', $method->invoke($wd, 'http://localhost/sub/sub', '../test')); + $this->assertEquals('http://localhost/sub/test', $method->invoke($wd, 'http://localhost/sub/sub/', '../test')); + + $this->assertEquals('http://localhost/test', $method->invoke($wd, 'http://localhost/sub/sub/sub', '../../test')); + $this->assertEquals('http://localhost/sub/test', $method->invoke($wd, 'http://localhost/sub/sub/sub/', '../../test')); + + $this->assertEquals('http://localhost/test', $method->invoke($wd, 'http://localhost/sub/sub/sub/sub', '../../../test')); + $this->assertEquals('http://localhost/sub/test', $method->invoke($wd, 'http://localhost/sub/sub/sub/sub/', '../../../test')); + + $this->assertEquals('http://localhost/test', $method->invoke($wd, 'http://localhost/sub/sub/sub', '../../../test')); + $this->assertEquals('http://localhost/test', $method->invoke($wd, 'http://localhost/sub/sub/sub/', '../../../test')); + } +} \ No newline at end of file From 3040718c9d8d4832b2a08152aa7e40300ce7236e Mon Sep 17 00:00:00 2001 From: Andreas Schamberger Date: Sun, 2 Oct 2011 12:23:59 +0200 Subject: [PATCH 13/63] cs fixes --- src/BeSimple/SoapClient/Curl.php | 140 +++++++++++-------------- src/BeSimple/SoapClient/Helper.php | 123 +++++++++------------- src/BeSimple/SoapClient/SoapClient.php | 125 +++++++++------------- 3 files changed, 160 insertions(+), 228 deletions(-) diff --git a/src/BeSimple/SoapClient/Curl.php b/src/BeSimple/SoapClient/Curl.php index e7842dd..bb2dc30 100644 --- a/src/BeSimple/SoapClient/Curl.php +++ b/src/BeSimple/SoapClient/Curl.php @@ -8,7 +8,7 @@ * * 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 */ @@ -55,11 +55,10 @@ class Curl * @param array $options * @param int $followLocationMaxRedirects */ - public function __construct( array $options, $followLocationMaxRedirects = 10 ) + public function __construct(array $options, $followLocationMaxRedirects = 10) { // set the default HTTP user agent - if ( !isset( $options['user_agent'] ) ) - { + if (!isset($options['user_agent'])) { $options['user_agent'] = self::USER_AGENT; } $this->followLocationMaxRedirects = $followLocationMaxRedirects; @@ -75,34 +74,28 @@ class Curl 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' ); + ); + 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['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_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['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['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'] ); + if (isset($options['local_cert'])) { + curl_setopt($this->ch, CURLOPT_SSLCERT, $options['local_cert']); + curl_setopt($this->ch, CURLOPT_SSLCERTPASSWD, $options['passphrase']); } } @@ -111,7 +104,7 @@ class Curl */ public function __destruct() { - curl_close( $this->ch ); + curl_close($this->ch); } /** @@ -123,24 +116,22 @@ class Curl * @param array $requestHeaders * @return bool */ - public function exec( $location, $request = null, $requestHeaders = array() ) + public function exec($location, $request = null, $requestHeaders = array()) { - curl_setopt( $this->ch, CURLOPT_URL, $location); + 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 (!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 ); + if (count($requestHeaders) > 0) { + curl_setopt($this->ch, CURLOPT_HTTPHEADER, $requestHeaders); } - $this->response = $this->execManualRedirect( $this->followLocationMaxRedirects ); + $this->response = $this->execManualRedirect($this->followLocationMaxRedirects); - return ( $this->response === false ) ? false : true; + return ($this->response === false) ? false : true; } /** @@ -152,43 +143,37 @@ class Curl * @param int $redirects * @return mixed */ - private function execManualRedirect( $redirects = 0 ) + private function execManualRedirect($redirects = 0) { - if ( $redirects > $this->followLocationMaxRedirects ) - { + 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 ); + 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 ) ); + 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'] ) ) - { + 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'] ) ) - { + if (!isset($url['host'])) { $url['host'] = $lastUrl['host']; } - if ( !isset( $url['path'] ) ) - { + 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++ ); + $newUrl = $url['scheme'] . '://' . $url['host'] . $url['path'] . ($url['query'] ? '?' . $url['query'] : ''); + curl_setopt($this->ch, CURLOPT_URL, $newUrl); + return $this->execManualRedirect($redirects++); } } return $response; @@ -229,7 +214,7 @@ class Curl 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 - ); + ); } /** @@ -240,12 +225,11 @@ class Curl public function getErrorMessage() { $errorCodeMapping = $this->getErrorCodeMapping(); - $errorNumber = curl_errno( $this->ch ); - if ( isset( $errorCodeMapping[$errorNumber] ) ) - { + $errorNumber = curl_errno($this->ch); + if (isset($errorCodeMapping[$errorNumber])) { return $errorCodeMapping[$errorNumber]; } - return curl_error( $this->ch ); + return curl_error($this->ch); } /** @@ -255,7 +239,7 @@ class Curl */ public function getRequestHeaders() { - return curl_getinfo( $this->ch, CURLINFO_HEADER_OUT ); + return curl_getinfo($this->ch, CURLINFO_HEADER_OUT); } /** @@ -275,8 +259,8 @@ class Curl */ public function getResponseBody() { - $headerSize = curl_getinfo( $this->ch, CURLINFO_HEADER_SIZE ); - return substr( $this->response, $headerSize ); + $headerSize = curl_getinfo($this->ch, CURLINFO_HEADER_SIZE); + return substr($this->response, $headerSize); } /** @@ -286,7 +270,7 @@ class Curl */ public function getResponseContentType() { - return curl_getinfo( $this->ch, CURLINFO_CONTENT_TYPE ); + return curl_getinfo($this->ch, CURLINFO_CONTENT_TYPE); } /** @@ -296,8 +280,8 @@ class Curl */ public function getResponseHeaders() { - $headerSize = curl_getinfo( $this->ch, CURLINFO_HEADER_SIZE ); - return substr( $this->response, 0, $headerSize ); + $headerSize = curl_getinfo($this->ch, CURLINFO_HEADER_SIZE); + return substr($this->response, 0, $headerSize); } /** @@ -307,7 +291,7 @@ class Curl */ public function getResponseStatusCode() { - return curl_getinfo( $this->ch, CURLINFO_HTTP_CODE ); + return curl_getinfo($this->ch, CURLINFO_HTTP_CODE); } /** @@ -317,7 +301,7 @@ class Curl */ public function getResponseStatusMessage() { - preg_match( '/HTTP\/(1\.[0-1]+) ([0-9]{3}) (.*)/', $this->response, $matches ); - return trim( array_pop( $matches ) ); + 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 index bb2e5c3..9c32cb4 100644 --- a/src/BeSimple/SoapClient/Helper.php +++ b/src/BeSimple/SoapClient/Helper.php @@ -8,7 +8,7 @@ * * 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 */ @@ -171,12 +171,11 @@ class Helper * @param string $errline * @throws ErrorException */ - public static function exceptionErrorHandler( $errno, $errstr, $errfile, $errline ) + 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 ); + if (error_reporting() != 0) { + throw new \ErrorException($errstr, 0, $errno, $errfile, $errline); } } @@ -191,19 +190,19 @@ class Helper return sprintf( '%04x%04x-%04x-%04x-%04x-%04x%04x%04x', // 32 bits for "time_low" - mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ), + mt_rand(0, 0xffff), mt_rand(0, 0xffff), // 16 bits for "time_mid" - mt_rand( 0, 0xffff ), + mt_rand(0, 0xffff), // 16 bits for "time_hi_and_version", // four most significant bits holds version number 4 - mt_rand( 0, 0x0fff ) | 0x4000, + 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, + mt_rand(0, 0x3fff) | 0x8000, // 48 bits for "node" - mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ) - ); + mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff) + ); } /** @@ -214,21 +213,17 @@ class Helper public static function getCurrentUrl() { $url = ''; - if ( isset( $_SERVER['HTTPS'] ) && - ( strtolower( $_SERVER['HTTPS'] ) === 'on' || $_SERVER['HTTPS'] === '1' ) ) - { + if (isset($_SERVER['HTTPS']) && + (strtolower($_SERVER['HTTPS']) === 'on' || $_SERVER['HTTPS'] === '1')) { $url .= 'https://'; - } - else - { + } else { $url .= 'http://'; } - $url .= isset( $_SERVER['SERVER_NAME'] ) ? $_SERVER['SERVER_NAME'] : ''; - if ( isset( $_SERVER['SERVER_PORT'] ) && $_SERVER['SERVER_PORT'] != 80 ) - { + $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'] : ''; + $url .= isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : ''; return $url; } @@ -238,14 +233,11 @@ class Helper * @param int $version SOAP_1_1|SOAP_1_2 * @return string */ - public static function getSoapNamespace( $version ) + public static function getSoapNamespace($version) { - if ( $version === SOAP_1_2 ) - { + if ($version === SOAP_1_2) { return self::NS_SOAP_1_2; - } - else - { + } else { return self::NS_SOAP_1_1; } } @@ -256,14 +248,11 @@ class Helper * @param string $namespace NS_SOAP_1_1|NS_SOAP_1_2 * @return int SOAP_1_1|SOAP_1_2 */ - public static function getSoapVersionFromNamespace( $namespace ) + public static function getSoapVersionFromNamespace($namespace) { - if ( $namespace === self::NS_SOAP_1_2 ) - { + if ($namespace === self::NS_SOAP_1_2) { return SOAP_1_2; - } - else - { + } else { return SOAP_1_1; } } @@ -279,46 +268,38 @@ class Helper * @param \ass\Soap\WsdlHandler $wsdlHandler * @return string */ - public static function runPlugins( array $plugins, $requestType, $xml, $location = null, $action = null, \ass\Soap\WsdlHandler $wsdlHandler = null ) + public static function runPlugins(array $plugins, $requestType, $xml, $location = null, $action = null, \ass\Soap\WsdlHandler $wsdlHandler = null) { - if ( count( $plugins ) > 0 ) - { + if (count($plugins) > 0) { // instantiate new dom object - $dom = new \DOMDocument( '1.0' ); + $dom = new \DOMDocument('1.0'); // format the XML if option is set $dom->formatOutput = self::$formatXmlOutput; - $dom->loadXML( $xml ); + $dom->loadXML($xml); $params = array( $dom, $location, $action, $wsdlHandler ); - if ( $requestType == self::REQUEST ) - { + if ($requestType == self::REQUEST) { $callMethod = 'modifyRequest'; - } - else - { + } else { $callMethod = 'modifyResponse'; } // modify dom - foreach( $plugins AS $plugin ) - { - if ( $plugin instanceof \ass\Soap\Plugin ) - { - call_user_func_array( array( $plugin, $callMethod ), $params ); + 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' ); + } elseif (self::$formatXmlOutput === true) { + $dom = new \DOMDocument('1.0'); $dom->formatOutput = true; - $dom->loadXML( $xml ); + $dom->loadXML($xml); return $dom->saveXML(); } return $xml; @@ -329,16 +310,13 @@ class Helper * * @param boolean $reset */ - public static function setCustomErrorHandler( $reset = false ) + public static function setCustomErrorHandler($reset = false) { - if ( $reset === true && !is_null( self::$previousErrorHandler ) ) - { - set_error_handler( self::$previousErrorHandler ); + 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' ); + } else { + self::$previousErrorHandler = set_error_handler('ass\\Soap\\Helper::exceptionErrorHandler'); } return self::$previousErrorHandler; } @@ -351,13 +329,13 @@ class Helper * @param string $contentType * @return string */ - public static function makeSoapAttachmentDataString( $contentId, $contentType ) + public static function makeSoapAttachmentDataString($contentId, $contentType) { $parameter = array( 'cid' => $contentId, 'type' => $contentType, ); - return 'SOAP-MIME-ATTACHMENT:' . http_build_query( $parameter, null, '&' ); + return 'SOAP-MIME-ATTACHMENT:' . http_build_query($parameter, null, '&'); } /** @@ -367,12 +345,12 @@ class Helper * @param string $dataString * @return array(string=>string) */ - public static function parseSoapAttachmentDataString( $dataString ) + public static function parseSoapAttachmentDataString($dataString) { - $dataString = substr( $dataString, 21 ); + $dataString = substr($dataString, 21); // get all data $data = array(); - parse_str( $dataString, $data ); + parse_str($dataString, $data); return $data; } @@ -382,15 +360,12 @@ class Helper * * @param string $header */ - public static function setHttpStatusHeader( $header ) + public static function setHttpStatusHeader($header) { - if ( substr( php_sapi_name(), 0, 3 ) == 'cgi' ) - { - header( 'Status: ' . $header ); - } - else - { - header( $_SERVER['SERVER_PROTOCOL'] . ' ' . $header ); + if ('cgi' == substr(php_sapi_name(), 0, 3)) { + 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 index a1fc84d..b7eacc6 100644 --- a/src/BeSimple/SoapClient/SoapClient.php +++ b/src/BeSimple/SoapClient/SoapClient.php @@ -8,16 +8,16 @@ * * 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 + * 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 @@ -73,37 +73,30 @@ class SoapClient extends \SoapClient * @param string $wsdl * @param array(string=>mixed) $options */ - public function __construct( $wsdl, array $options = array(), TypeConverterCollection $converters = null ) + 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'] ) ) - { + 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'] ) ) - { + if (!isset($options['features'])) { $options['features'] = 0; } // set default option to resolve xsd includes - if ( !isset( $options['resolve_xsd_includes'] ) ) - { + if (!isset($options['resolve_xsd_includes'])) { $options['resolve_xsd_includes'] = true; } // add type converters from TypeConverterCollection - if ( !is_null( $converters ) ) - { + if (!is_null($converters)) { $convertersTypemap = $converters->getTypemap(); - if ( isset( $options['typemap'] ) ) - { - $options['typemap'] = array_merge( $options['typemap'], $convertersTypemap ); - } - else - { + if (isset($options['typemap'])) { + $options['typemap'] = array_merge($options['typemap'], $convertersTypemap); + } else { $options['typemap'] = $convertersTypemap; } } @@ -115,8 +108,8 @@ class SoapClient extends \SoapClient $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 ); + $this->wsdlFile = $this->loadWsdl($wsdl); + parent::__construct($this->wsdlFile, $options); } /** @@ -127,46 +120,40 @@ class SoapClient extends \SoapClient * @param string $action * @return string */ - private function __doHttpRequest( $request, $location, $action ) + 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 ) - { + if ($this->options['soap_version'] == SOAP_1_2) { $headers = array( 'Content-Type: application/soap+xml; charset=utf-8', - ); - } - else - { + ); + } 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 ); + $curl = new Curl($this->options); // execute request - $responseSuccessfull = $curl->exec( $location, $request, $headers ); + $responseSuccessfull = $curl->exec($location, $request, $headers); // tracing enabled: store last request header and body - if ( isset( $this->options['trace'] ) && $this->options['trace'] === true ) - { + 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 ) - { + if ($responseSuccessfull === false) { // get error message from curl $faultstring = $curl->getErrorMessage(); // destruct curl object - unset( $curl ); - throw new \SoapFault( 'HTTP', $faultstring ); + unset($curl); + throw new \SoapFault('HTTP', $faultstring); } // tracing enabled: store last response header and body - if ( isset( $this->options['trace'] ) && $this->options['trace'] === true ) - { + if (isset($this->options['trace']) && $this->options['trace'] === true) { $this->lastResponseHeaders = $curl->getResponseHeaders(); $this->lastResponse = $curl->getResponseBody(); } @@ -174,43 +161,32 @@ class SoapClient extends \SoapClient // check if we do have a proper soap status code (if not soapfault) // // TODO // $responseStatusCode = $curl->getResponseStatusCode(); -// if ( $responseStatusCode >= 400 ) -// { +// if ($responseStatusCode >= 400) { // $isError = 0; -// $response = trim( $response ); -// if ( strlen( $response ) == 0 ) -// { +// $response = trim($response); +// if (strlen($response) == 0) { // $isError = 1; -// } -// else -// { +// } else { // $contentType = $curl->getResponseContentType(); -// if ( $contentType != 'application/soap+xml' -// && $contentType != 'application/soap+xml' ) -// { -// if ( strncmp( $response , "getResponseStatusMessage() ); +// if ($isError == 1) { +// throw new \SoapFault('HTTP', $curl->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' ); +// } 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 ); + unset($curl); return $response; } @@ -224,10 +200,10 @@ class SoapClient extends \SoapClient * @param int $one_way 0|1 * @return string */ - public function __doRequest( $request, $location, $action, $version, $one_way = 0 ) + public function __doRequest($request, $location, $action, $version, $one_way = 0) { // http request - $response = $this->__doHttpRequest( $request, $location, $action ); + $response = $this->__doHttpRequest($request, $location, $action); // return SOAP response to ext/soap return $response; } @@ -281,16 +257,13 @@ class SoapClient extends \SoapClient * @param string $wsdl * @return string */ - private function loadWsdl( $wsdl ) + 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 . "\"" ); + $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; } From cd14afe11e026fa09d803061f81e4844b002768e Mon Sep 17 00:00:00 2001 From: Andreas Schamberger Date: Mon, 3 Oct 2011 21:03:46 +0200 Subject: [PATCH 14/63] fixed file header --- src/BeSimple/SoapClient/Curl.php | 4 ++-- src/BeSimple/SoapClient/Helper.php | 4 ++-- src/BeSimple/SoapClient/SoapClient.php | 4 ++-- src/BeSimple/SoapClient/WsdlDownloader.php | 4 ++-- tests/BeSimple/Tests/SoapClient/WsdlDownloaderTest.php | 7 +++++-- 5 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/BeSimple/SoapClient/Curl.php b/src/BeSimple/SoapClient/Curl.php index bb2dc30..9a11c80 100644 --- a/src/BeSimple/SoapClient/Curl.php +++ b/src/BeSimple/SoapClient/Curl.php @@ -1,10 +1,10 @@ * (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. diff --git a/src/BeSimple/SoapClient/Helper.php b/src/BeSimple/SoapClient/Helper.php index 9c32cb4..cc33f35 100644 --- a/src/BeSimple/SoapClient/Helper.php +++ b/src/BeSimple/SoapClient/Helper.php @@ -1,10 +1,10 @@ * (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. diff --git a/src/BeSimple/SoapClient/SoapClient.php b/src/BeSimple/SoapClient/SoapClient.php index b7eacc6..d48d84a 100644 --- a/src/BeSimple/SoapClient/SoapClient.php +++ b/src/BeSimple/SoapClient/SoapClient.php @@ -1,10 +1,10 @@ * (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. diff --git a/src/BeSimple/SoapClient/WsdlDownloader.php b/src/BeSimple/SoapClient/WsdlDownloader.php index d80803b..4990e12 100644 --- a/src/BeSimple/SoapClient/WsdlDownloader.php +++ b/src/BeSimple/SoapClient/WsdlDownloader.php @@ -1,10 +1,10 @@ * (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. diff --git a/tests/BeSimple/Tests/SoapClient/WsdlDownloaderTest.php b/tests/BeSimple/Tests/SoapClient/WsdlDownloaderTest.php index a07aa46..477bec1 100644 --- a/tests/BeSimple/Tests/SoapClient/WsdlDownloaderTest.php +++ b/tests/BeSimple/Tests/SoapClient/WsdlDownloaderTest.php @@ -1,10 +1,10 @@ * (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. @@ -16,6 +16,9 @@ namespace BeSimple\SoapClient; use BeSimple\SoapClient\WsdlDownloader; +/** +* @author Andreas Schamberger +*/ class WsdlDownloaderTest extends \PHPUnit_Framework_TestCase { protected $webserverProcessId; From 7be719ffc57ae7b2a0cbb18f954c4f537d52e787 Mon Sep 17 00:00:00 2001 From: Francis Besset Date: Sat, 8 Oct 2011 12:22:17 +0200 Subject: [PATCH 15/63] [SoapClient] Moved TypeConverterCollection parameter in the constructor --- src/BeSimple/SoapClient/SoapClient.php | 15 +++++++-------- .../BeSimple/Tests/SoapClient/SoapClientTest.php | 10 +++++----- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/BeSimple/SoapClient/SoapClient.php b/src/BeSimple/SoapClient/SoapClient.php index 6832ecb..9266a0c 100644 --- a/src/BeSimple/SoapClient/SoapClient.php +++ b/src/BeSimple/SoapClient/SoapClient.php @@ -28,10 +28,11 @@ class SoapClient * @param string $wsdl * @param array $options */ - public function __construct($wsdl, TypeConverterCollection $converters = null, array $options = array()) + public function __construct($wsdl, array $options = array(), TypeConverterCollection $converters = null) { $this->wsdl = $wsdl; $this->converters = $converters; + $this->setOptions($options); } @@ -155,17 +156,15 @@ class SoapClient */ public function getSoapOptions() { - $options = array(); - if (null === $this->options['cache_type']) { $this->options['cache_type'] = Cache::getType(); } - $options['cache_wsdl'] = $this->options['cache_type']; - $options['trace'] = $this->options['debug']; - $options['typemap'] = $this->getTypemap(); - - return $options; + return array( + 'cache_wsdl' => $this->options['cache_type'], + 'trace' => $this->options['debug'], + 'typemap' => $this->getTypemap(), + ); } /** diff --git a/tests/BeSimple/Tests/SoapClient/SoapClientTest.php b/tests/BeSimple/Tests/SoapClient/SoapClientTest.php index 34e9561..580bcbe 100644 --- a/tests/BeSimple/Tests/SoapClient/SoapClientTest.php +++ b/tests/BeSimple/Tests/SoapClient/SoapClientTest.php @@ -67,7 +67,7 @@ class SoapClientTest extends \PHPUnit_Framework_TestCase public function testCreateSoapHeader() { - $soapClient = new SoapClient('foo.wsdl', null, array('namespace' => 'http://foobar/soap/User/1.0/')); + $soapClient = new SoapClient('foo.wsdl', array('namespace' => 'http://foobar/soap/User/1.0/')); $soapHeader = $soapClient->createSoapHeader('foo', 'bar'); $this->assertInstanceOf('SoapHeader', $soapHeader); @@ -87,10 +87,10 @@ class SoapClientTest extends \PHPUnit_Framework_TestCase public function testGetSoapOptions() { Cache::setType(Cache::TYPE_MEMORY); - $soapClient = new SoapClient('foo.wsdl', null, array('debug' => true)); + $soapClient = new SoapClient('foo.wsdl', array('debug' => true)); $this->assertEquals(array('cache_wsdl' => Cache::getType(), 'trace' => true, 'typemap' => array()), $soapClient->getSoapOptions()); - $soapClient = new SoapClient('foo.wsdl', null, array('debug' => false, 'cache_type' => Cache::TYPE_NONE)); + $soapClient = new SoapClient('foo.wsdl', array('debug' => false, 'cache_type' => Cache::TYPE_NONE)); $this->assertEquals(array('cache_wsdl' => Cache::TYPE_NONE, 'trace' => false, 'typemap' => array()), $soapClient->getSoapOptions()); } @@ -104,7 +104,7 @@ class SoapClientTest extends \PHPUnit_Framework_TestCase $dateTypeConverter = new DateTypeConverter(); $converters->add($dateTypeConverter); - $soapClient = new SoapClient('foo.wsdl', $converters); + $soapClient = new SoapClient('foo.wsdl', array(), $converters); $soapOptions = $soapClient->getSoapOptions(); $this->assertEquals('http://www.w3.org/2001/XMLSchema', $soapOptions['typemap'][0]['type_ns']); @@ -120,7 +120,7 @@ class SoapClientTest extends \PHPUnit_Framework_TestCase public function testGetNativeSoapClient() { - $soapClient = new SoapClient(__DIR__.'/Fixtures/foobar.wsdl', null, array('debug' => true)); + $soapClient = new SoapClient(__DIR__.'/Fixtures/foobar.wsdl', array('debug' => true)); $this->assertInstanceOf('SoapClient', $soapClient->getNativeSoapClient()); } From 29a388eb7db15283dc491beae34ec01f43cef2d1 Mon Sep 17 00:00:00 2001 From: Francis Besset Date: Sat, 8 Oct 2011 15:07:49 +0200 Subject: [PATCH 16/63] [SoapClient] Added Classmap --- src/BeSimple/SoapClient/SoapClient.php | 18 ++++++++++++- .../Tests/SoapClient/SoapClientTest.php | 26 ++++++++++++++++--- 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/src/BeSimple/SoapClient/SoapClient.php b/src/BeSimple/SoapClient/SoapClient.php index 9266a0c..5e7ba02 100644 --- a/src/BeSimple/SoapClient/SoapClient.php +++ b/src/BeSimple/SoapClient/SoapClient.php @@ -13,6 +13,7 @@ namespace BeSimple\SoapClient; use BeSimple\SoapCommon\Cache; +use BeSimple\SoapCommon\Classmap; use BeSimple\SoapCommon\Converter\TypeConverterCollection; /** @@ -21,6 +22,7 @@ use BeSimple\SoapCommon\Converter\TypeConverterCollection; class SoapClient { protected $wsdl; + protected $classmap; protected $converters; protected $soapClient; @@ -28,9 +30,10 @@ class SoapClient * @param string $wsdl * @param array $options */ - public function __construct($wsdl, array $options = array(), TypeConverterCollection $converters = null) + public function __construct($wsdl, array $options = array(), Classmap $classmap = null, TypeConverterCollection $converters = null) { $this->wsdl = $wsdl; + $this->classmap = $classmap; $this->converters = $converters; $this->setOptions($options); @@ -163,10 +166,23 @@ class SoapClient return array( 'cache_wsdl' => $this->options['cache_type'], 'trace' => $this->options['debug'], + 'classmap' => $this->getClassmap(), 'typemap' => $this->getTypemap(), ); } + /** + * @return array + */ + protected function getClassmap() + { + if (!$this->classmap) { + return array(); + } + + return $this->classmap->all(); + } + /** * @return array */ diff --git a/tests/BeSimple/Tests/SoapClient/SoapClientTest.php b/tests/BeSimple/Tests/SoapClient/SoapClientTest.php index 580bcbe..95b0d37 100644 --- a/tests/BeSimple/Tests/SoapClient/SoapClientTest.php +++ b/tests/BeSimple/Tests/SoapClient/SoapClientTest.php @@ -13,6 +13,7 @@ namespace BeSimple\Tests\SoapClient; use BeSimple\SoapCommon\Cache; +use BeSimple\SoapCommon\Classmap; use BeSimple\SoapCommon\Converter\DateTimeTypeConverter; use BeSimple\SoapCommon\Converter\DateTypeConverter; use BeSimple\SoapCommon\Converter\TypeConverterCollection; @@ -88,10 +89,29 @@ class SoapClientTest extends \PHPUnit_Framework_TestCase { Cache::setType(Cache::TYPE_MEMORY); $soapClient = new SoapClient('foo.wsdl', array('debug' => true)); - $this->assertEquals(array('cache_wsdl' => Cache::getType(), 'trace' => true, 'typemap' => array()), $soapClient->getSoapOptions()); + $this->assertEquals(array('cache_wsdl' => Cache::getType(), 'trace' => true, 'classmap' => array(), 'typemap' => array()), $soapClient->getSoapOptions()); $soapClient = new SoapClient('foo.wsdl', array('debug' => false, 'cache_type' => Cache::TYPE_NONE)); - $this->assertEquals(array('cache_wsdl' => Cache::TYPE_NONE, 'trace' => false, 'typemap' => array()), $soapClient->getSoapOptions()); + $this->assertEquals(array('cache_wsdl' => Cache::TYPE_NONE, 'trace' => false, 'classmap' => array(), 'typemap' => array()), $soapClient->getSoapOptions()); + } + + public function testGetSoapOptionsWithClassmap() + { + $classmap = new Classmap(); + + $soapClient = new SoapClient('foo.wsdl', array(), $classmap); + $soapOptions = $soapClient->getSoapOptions(); + + $this->assertSame(array(), $soapOptions['classmap']); + + $map = array( + 'foobar' => 'BeSimple\SoapClient\SoapClient', + 'barfoo' => 'BeSimple\SoapClient\Tests\SoapClientTest', + ); + $classmap->set($map); + $soapOptions = $soapClient->getSoapOptions(); + + $this->assertSame($map, $soapOptions['classmap']); } public function testGetSoapOptionsWithTypemap() @@ -104,7 +124,7 @@ class SoapClientTest extends \PHPUnit_Framework_TestCase $dateTypeConverter = new DateTypeConverter(); $converters->add($dateTypeConverter); - $soapClient = new SoapClient('foo.wsdl', array(), $converters); + $soapClient = new SoapClient('foo.wsdl', array(), null, $converters); $soapOptions = $soapClient->getSoapOptions(); $this->assertEquals('http://www.w3.org/2001/XMLSchema', $soapOptions['typemap'][0]['type_ns']); From bc9df9d2d7202be7fc98b29db7f885a3c6a25aa6 Mon Sep 17 00:00:00 2001 From: Francis Besset Date: Sat, 8 Oct 2011 22:02:54 +0200 Subject: [PATCH 17/63] [SoapClient] Added exceptions and user_agent options --- src/BeSimple/SoapClient/SoapClient.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/BeSimple/SoapClient/SoapClient.php b/src/BeSimple/SoapClient/SoapClient.php index 5e7ba02..10de074 100644 --- a/src/BeSimple/SoapClient/SoapClient.php +++ b/src/BeSimple/SoapClient/SoapClient.php @@ -45,6 +45,8 @@ class SoapClient 'debug' => false, 'cache_type' => null, 'namespace' => null, + 'exceptions' => true, + 'user_agent' => 'BeSimpleSoap', ); // check option names and live merge, if errors are encountered Exception will be thrown @@ -167,7 +169,9 @@ class SoapClient 'cache_wsdl' => $this->options['cache_type'], 'trace' => $this->options['debug'], 'classmap' => $this->getClassmap(), + 'exceptions' => $this->options['exceptions'], 'typemap' => $this->getTypemap(), + 'user_agent' => $this->options['user_agent'], ); } From 0d59bd254591e61b0f99a00062fa45df7c47607c Mon Sep 17 00:00:00 2001 From: Francis Besset Date: Sun, 9 Oct 2011 20:10:13 +0200 Subject: [PATCH 18/63] Fixed tests --- tests/BeSimple/Tests/SoapClient/SoapClientTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/BeSimple/Tests/SoapClient/SoapClientTest.php b/tests/BeSimple/Tests/SoapClient/SoapClientTest.php index 95b0d37..d947f74 100644 --- a/tests/BeSimple/Tests/SoapClient/SoapClientTest.php +++ b/tests/BeSimple/Tests/SoapClient/SoapClientTest.php @@ -31,7 +31,7 @@ class SoapClientTest extends \PHPUnit_Framework_TestCase ); $soapClient->setOptions($options); - $this->assertEquals($options, $soapClient->getOptions()); + $this->assertEquals(array_merge($options, array('exceptions' => true, 'user_agent' => 'BeSimpleSoap')), $soapClient->getOptions()); } public function testSetOptionsThrowsAnExceptionIfOptionsDoesNotExists() @@ -89,10 +89,10 @@ class SoapClientTest extends \PHPUnit_Framework_TestCase { Cache::setType(Cache::TYPE_MEMORY); $soapClient = new SoapClient('foo.wsdl', array('debug' => true)); - $this->assertEquals(array('cache_wsdl' => Cache::getType(), 'trace' => true, 'classmap' => array(), 'typemap' => array()), $soapClient->getSoapOptions()); + $this->assertEquals(array('cache_wsdl' => Cache::getType(), 'trace' => true, 'classmap' => array(), 'exceptions' => true, 'typemap' => array(), 'user_agent' => 'BeSimpleSoap'), $soapClient->getSoapOptions()); $soapClient = new SoapClient('foo.wsdl', array('debug' => false, 'cache_type' => Cache::TYPE_NONE)); - $this->assertEquals(array('cache_wsdl' => Cache::TYPE_NONE, 'trace' => false, 'classmap' => array(), 'typemap' => array()), $soapClient->getSoapOptions()); + $this->assertEquals(array('cache_wsdl' => Cache::TYPE_NONE, 'trace' => false, 'classmap' => array(), 'exceptions' => true, 'typemap' => array(), 'user_agent' => 'BeSimpleSoap'), $soapClient->getSoapOptions()); } public function testGetSoapOptionsWithClassmap() From 28ed21530d97c2d7131fe73f4e0a0d5c2e874fb0 Mon Sep 17 00:00:00 2001 From: Francis Besset Date: Sun, 9 Oct 2011 20:17:50 +0200 Subject: [PATCH 19/63] Added SoapClientBuilder --- src/BeSimple/SoapClient/SoapClientBuilder.php | 55 +++++++++++++ .../SoapClient/SoapClientBuilderTest.php | 81 +++++++++++++++++++ 2 files changed, 136 insertions(+) create mode 100644 src/BeSimple/SoapClient/SoapClientBuilder.php create mode 100644 tests/BeSimple/Tests/SoapClient/SoapClientBuilderTest.php diff --git a/src/BeSimple/SoapClient/SoapClientBuilder.php b/src/BeSimple/SoapClient/SoapClientBuilder.php new file mode 100644 index 0000000..7134cf6 --- /dev/null +++ b/src/BeSimple/SoapClient/SoapClientBuilder.php @@ -0,0 +1,55 @@ + + * (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\AbstractSoapBuilder; + +/** + * @author Francis Besset + */ +class SoapClientBuilder extends AbstractSoapBuilder +{ + protected $wsdl; + protected $options; + + /** + * @return SoapClientBuilder + */ + static public function createWithDefaults() + { + return parent::createWithDefaults() + ->withUserAgent('BeSimpleSoap') + ; + } + + public function withTrace($trace = true) + { + $this->options['trace'] = $trace; + + return $this; + } + + public function withExceptions($exceptions = true) + { + $this->options['exceptions'] = $exceptions; + + return $this; + } + + public function withUserAgent($userAgent) + { + $this->options['user_agent'] = $userAgent; + + return $this; + } +} \ No newline at end of file diff --git a/tests/BeSimple/Tests/SoapClient/SoapClientBuilderTest.php b/tests/BeSimple/Tests/SoapClient/SoapClientBuilderTest.php new file mode 100644 index 0000000..cad1111 --- /dev/null +++ b/tests/BeSimple/Tests/SoapClient/SoapClientBuilderTest.php @@ -0,0 +1,81 @@ + + * (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\Tests\SoapCommon\Soap; + +use BeSimple\SoapClient\SoapClientBuilder; + +class SoapClientBuilderTest extends \PHPUnit_Framework_TestCase +{ + private $defaultOptions = array( + 'features' => 0, + ); + + public function testContruct() + { + $options = $this + ->getSoapBuilder() + ->getOptions() + ; + + $this->assertEquals($this->mergeOptions(array()), $options); + } + + public function testWithTrace() + { + $builder = $this->getSoapBuilder(); + + $builder->withTrace(); + $this->assertEquals($this->mergeOptions(array('trace' => true)), $builder->getOptions()); + + $builder->withTrace(false); + $this->assertEquals($this->mergeOptions(array('trace' => false)), $builder->getOptions()); + } + + public function testWithExceptions() + { + $builder = $this->getSoapBuilder(); + + $builder->withExceptions(); + $this->assertEquals($this->mergeOptions(array('exceptions' => true)), $builder->getOptions()); + + $builder->withExceptions(false); + $this->assertEquals($this->mergeOptions(array('exceptions' => false)), $builder->getOptions()); + } + + public function testWithUserAgent() + { + $builder = $this->getSoapBuilder(); + + $builder->withUserAgent('BeSimpleSoap Test'); + $this->assertEquals($this->mergeOptions(array('user_agent' => 'BeSimpleSoap Test')), $builder->getOptions()); + } + + public function testCreateWithDefaults() + { + $builder = SoapClientBuilder::createWithDefaults(); + + $this->assertInstanceOf('BeSimple\SoapClient\SoapClientBuilder', $builder); + + $this->assertEquals($this->mergeOptions(array('soap_version' => SOAP_1_2, 'encoding' => 'UTF-8', 'features' => SOAP_SINGLE_ELEMENT_ARRAYS, 'user_agent' => 'BeSimpleSoap')), $builder->getOptions()); + } + + private function getSoapBuilder() + { + return new SoapClientBuilder(); + } + + private function mergeOptions(array $options) + { + return array_merge($this->defaultOptions, $options); + } +} \ No newline at end of file From 46ced393ca55680ecff04cc5018078af506cf341 Mon Sep 17 00:00:00 2001 From: Francis Besset Date: Mon, 10 Oct 2011 00:02:20 +0200 Subject: [PATCH 20/63] Updated SoapClientBuilder --- src/BeSimple/SoapClient/SoapClientBuilder.php | 9 +++------ .../Tests/SoapClient/SoapClientBuilderTest.php | 16 +++++++++------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/BeSimple/SoapClient/SoapClientBuilder.php b/src/BeSimple/SoapClient/SoapClientBuilder.php index 7134cf6..5ec9678 100644 --- a/src/BeSimple/SoapClient/SoapClientBuilder.php +++ b/src/BeSimple/SoapClient/SoapClientBuilder.php @@ -19,9 +19,6 @@ use BeSimple\SoapCommon\AbstractSoapBuilder; */ class SoapClientBuilder extends AbstractSoapBuilder { - protected $wsdl; - protected $options; - /** * @return SoapClientBuilder */ @@ -34,21 +31,21 @@ class SoapClientBuilder extends AbstractSoapBuilder public function withTrace($trace = true) { - $this->options['trace'] = $trace; + $this->soapOptions['trace'] = $trace; return $this; } public function withExceptions($exceptions = true) { - $this->options['exceptions'] = $exceptions; + $this->soapOptions['exceptions'] = $exceptions; return $this; } public function withUserAgent($userAgent) { - $this->options['user_agent'] = $userAgent; + $this->soapOptions['user_agent'] = $userAgent; return $this; } diff --git a/tests/BeSimple/Tests/SoapClient/SoapClientBuilderTest.php b/tests/BeSimple/Tests/SoapClient/SoapClientBuilderTest.php index cad1111..7ba11c2 100644 --- a/tests/BeSimple/Tests/SoapClient/SoapClientBuilderTest.php +++ b/tests/BeSimple/Tests/SoapClient/SoapClientBuilderTest.php @@ -18,13 +18,15 @@ class SoapClientBuilderTest extends \PHPUnit_Framework_TestCase { private $defaultOptions = array( 'features' => 0, + 'classmap' => array(), + 'typemap' => array(), ); public function testContruct() { $options = $this ->getSoapBuilder() - ->getOptions() + ->getSoapOptions() ; $this->assertEquals($this->mergeOptions(array()), $options); @@ -35,10 +37,10 @@ class SoapClientBuilderTest extends \PHPUnit_Framework_TestCase $builder = $this->getSoapBuilder(); $builder->withTrace(); - $this->assertEquals($this->mergeOptions(array('trace' => true)), $builder->getOptions()); + $this->assertEquals($this->mergeOptions(array('trace' => true)), $builder->getSoapOptions()); $builder->withTrace(false); - $this->assertEquals($this->mergeOptions(array('trace' => false)), $builder->getOptions()); + $this->assertEquals($this->mergeOptions(array('trace' => false)), $builder->getSoapOptions()); } public function testWithExceptions() @@ -46,10 +48,10 @@ class SoapClientBuilderTest extends \PHPUnit_Framework_TestCase $builder = $this->getSoapBuilder(); $builder->withExceptions(); - $this->assertEquals($this->mergeOptions(array('exceptions' => true)), $builder->getOptions()); + $this->assertEquals($this->mergeOptions(array('exceptions' => true)), $builder->getSoapOptions()); $builder->withExceptions(false); - $this->assertEquals($this->mergeOptions(array('exceptions' => false)), $builder->getOptions()); + $this->assertEquals($this->mergeOptions(array('exceptions' => false)), $builder->getSoapOptions()); } public function testWithUserAgent() @@ -57,7 +59,7 @@ class SoapClientBuilderTest extends \PHPUnit_Framework_TestCase $builder = $this->getSoapBuilder(); $builder->withUserAgent('BeSimpleSoap Test'); - $this->assertEquals($this->mergeOptions(array('user_agent' => 'BeSimpleSoap Test')), $builder->getOptions()); + $this->assertEquals($this->mergeOptions(array('user_agent' => 'BeSimpleSoap Test')), $builder->getSoapOptions()); } public function testCreateWithDefaults() @@ -66,7 +68,7 @@ class SoapClientBuilderTest extends \PHPUnit_Framework_TestCase $this->assertInstanceOf('BeSimple\SoapClient\SoapClientBuilder', $builder); - $this->assertEquals($this->mergeOptions(array('soap_version' => SOAP_1_2, 'encoding' => 'UTF-8', 'features' => SOAP_SINGLE_ELEMENT_ARRAYS, 'user_agent' => 'BeSimpleSoap')), $builder->getOptions()); + $this->assertEquals($this->mergeOptions(array('soap_version' => SOAP_1_2, 'encoding' => 'UTF-8', 'features' => SOAP_SINGLE_ELEMENT_ARRAYS, 'user_agent' => 'BeSimpleSoap')), $builder->getSoapOptions()); } private function getSoapBuilder() From a9b1bdc71416a2a878cd1be444dcde54b44702ce Mon Sep 17 00:00:00 2001 From: Francis Besset Date: Mon, 10 Oct 2011 00:21:23 +0200 Subject: [PATCH 21/63] Deleted SoapClient --- src/BeSimple/SoapClient/SoapClient.php | 183 +----------------- src/BeSimple/SoapClient/SoapClientBuilder.php | 24 +++ .../Tests/SoapClient/SoapClientTest.php | 147 -------------- 3 files changed, 25 insertions(+), 329 deletions(-) delete mode 100644 tests/BeSimple/Tests/SoapClient/SoapClientTest.php diff --git a/src/BeSimple/SoapClient/SoapClient.php b/src/BeSimple/SoapClient/SoapClient.php index 10de074..b40ad00 100644 --- a/src/BeSimple/SoapClient/SoapClient.php +++ b/src/BeSimple/SoapClient/SoapClient.php @@ -12,190 +12,9 @@ namespace BeSimple\SoapClient; -use BeSimple\SoapCommon\Cache; -use BeSimple\SoapCommon\Classmap; -use BeSimple\SoapCommon\Converter\TypeConverterCollection; - /** * @author Francis Besset */ -class SoapClient +class SoapClient extends \SoapClient { - protected $wsdl; - protected $classmap; - protected $converters; - protected $soapClient; - - /** - * @param string $wsdl - * @param array $options - */ - public function __construct($wsdl, array $options = array(), Classmap $classmap = null, TypeConverterCollection $converters = null) - { - $this->wsdl = $wsdl; - $this->classmap = $classmap; - $this->converters = $converters; - - $this->setOptions($options); - } - - public function setOptions(array $options) - { - $this->options = array( - 'debug' => false, - 'cache_type' => null, - 'namespace' => null, - 'exceptions' => true, - 'user_agent' => 'BeSimpleSoap', - ); - - // check option names and live merge, if errors are encountered Exception will be thrown - $invalid = array(); - $isInvalid = false; - foreach ($options as $key => $value) { - if (array_key_exists($key, $this->options)) { - $this->options[$key] = $value; - } else { - $isInvalid = true; - $invalid[] = $key; - } - } - - if ($isInvalid) { - throw new \InvalidArgumentException(sprintf( - 'The "%s" class does not support the following options: "%s".', - get_class($this), - implode('\', \'', $invalid) - )); - } - } - - /** - * @param string $name The name - * @param mixed $value The value - * - * @throws \InvalidArgumentException - */ - public function setOption($name, $value) - { - if (!array_key_exists($name, $this->options)) { - throw new \InvalidArgumentException(sprintf( - 'The "%s" class does not support the "%s" option.', - get_class($this), - $name - )); - } - - $this->options[$name] = $value; - } - - public function getOptions() - { - return $this->options; - } - - /** - * @param string $key The key - * - * @return mixed The value - * - * @throws \InvalidArgumentException - */ - public function getOption($key) - { - if (!array_key_exists($key, $this->options)) { - throw new \InvalidArgumentException(sprintf( - 'The "%s" class does not support the "%s" option.', - get_class($this), - $key - )); - } - - return $this->options[$key]; - } - - /** - * @param SoapRequest $soapRequest - * - * @return mixed - */ - public function send(SoapRequest $soapRequest) - { - return $this->getNativeSoapClient()->__soapCall( - $soapRequest->getFunction(), - $soapRequest->getArguments(), - $soapRequest->getOptions(), - $soapRequest->getHeaders() - ); - } - - /** - * @param string The SoapHeader name - * @param mixed The SoapHeader value - * - * @return \SoapHeader - */ - public function createSoapHeader($name, $value) - { - if (null === $namespace = $this->getOption('namespace')) { - throw new \RuntimeException('You cannot create SoapHeader if you do not specify a namespace.'); - } - - return new \SoapHeader($namespace, $name, $value); - } - - /** - * @return \SoapClient - */ - public function getNativeSoapClient() - { - if (!$this->soapClient) { - $this->soapClient = new \SoapClient($this->wsdl, $this->getSoapOptions()); - } - - return $this->soapClient; - } - - /** - * @return array The \SoapClient options - */ - public function getSoapOptions() - { - if (null === $this->options['cache_type']) { - $this->options['cache_type'] = Cache::getType(); - } - - return array( - 'cache_wsdl' => $this->options['cache_type'], - 'trace' => $this->options['debug'], - 'classmap' => $this->getClassmap(), - 'exceptions' => $this->options['exceptions'], - 'typemap' => $this->getTypemap(), - 'user_agent' => $this->options['user_agent'], - ); - } - - /** - * @return array - */ - protected function getClassmap() - { - if (!$this->classmap) { - return array(); - } - - return $this->classmap->all(); - } - - /** - * @return array - */ - protected function getTypemap() - { - if (!$this->converters) { - return array(); - } - - return $this->converters->getTypemap(); - } } \ No newline at end of file diff --git a/src/BeSimple/SoapClient/SoapClientBuilder.php b/src/BeSimple/SoapClient/SoapClientBuilder.php index 5ec9678..63c9934 100644 --- a/src/BeSimple/SoapClient/SoapClientBuilder.php +++ b/src/BeSimple/SoapClient/SoapClientBuilder.php @@ -29,6 +29,19 @@ class SoapClientBuilder extends AbstractSoapBuilder ; } + /** + * @return SoapClient + */ + public function build() + { + $this->validateOptions(); + + return new SoapClient($this->optionWsdl, $this->options); + } + + /** + * @return SoapClientBuilder + */ public function withTrace($trace = true) { $this->soapOptions['trace'] = $trace; @@ -36,6 +49,9 @@ class SoapClientBuilder extends AbstractSoapBuilder return $this; } + /** + * @return SoapClientBuilder + */ public function withExceptions($exceptions = true) { $this->soapOptions['exceptions'] = $exceptions; @@ -43,10 +59,18 @@ class SoapClientBuilder extends AbstractSoapBuilder return $this; } + /** + * @return SoapClientBuilder + */ public function withUserAgent($userAgent) { $this->soapOptions['user_agent'] = $userAgent; return $this; } + + protected function validateOptions() + { + $this->validateWsdl(); + } } \ No newline at end of file diff --git a/tests/BeSimple/Tests/SoapClient/SoapClientTest.php b/tests/BeSimple/Tests/SoapClient/SoapClientTest.php deleted file mode 100644 index d947f74..0000000 --- a/tests/BeSimple/Tests/SoapClient/SoapClientTest.php +++ /dev/null @@ -1,147 +0,0 @@ - - * (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\Tests\SoapClient; - -use BeSimple\SoapCommon\Cache; -use BeSimple\SoapCommon\Classmap; -use BeSimple\SoapCommon\Converter\DateTimeTypeConverter; -use BeSimple\SoapCommon\Converter\DateTypeConverter; -use BeSimple\SoapCommon\Converter\TypeConverterCollection; -use BeSimple\SoapClient\SoapClient; - -class SoapClientTest extends \PHPUnit_Framework_TestCase -{ - public function testSetOptions() - { - $soapClient = new SoapClient('foo.wsdl'); - $options = array( - 'cache_type' => Cache::TYPE_DISK_MEMORY, - 'debug' => true, - 'namespace' => 'foo', - ); - $soapClient->setOptions($options); - - $this->assertEquals(array_merge($options, array('exceptions' => true, 'user_agent' => 'BeSimpleSoap')), $soapClient->getOptions()); - } - - public function testSetOptionsThrowsAnExceptionIfOptionsDoesNotExists() - { - $soapClient = new SoapClient('foo.wsdl'); - - $this->setExpectedException('InvalidArgumentException'); - $soapClient->setOptions(array('bad_option' => true)); - } - - public function testSetOption() - { - $soapClient = new SoapClient('foo.wsdl'); - $soapClient->setOption('debug', true); - - $this->assertEquals(true, $soapClient->getOption('debug')); - } - - public function testSetOptionThrowsAnExceptionIfOptionDoesNotExists() - { - $soapClient = new SoapClient('foo.wsdl'); - - $this->setExpectedException('InvalidArgumentException'); - $soapClient->setOption('bad_option', 'bar'); - } - - public function testGetOptionThrowsAnExceptionIfOptionDoesNotExists() - { - $soapClient = new SoapClient('foo.wsdl'); - - $this->setExpectedException('InvalidArgumentException'); - $soapClient->getOption('bad_option'); - } - - public function testCreateSoapHeader() - { - $soapClient = new SoapClient('foo.wsdl', array('namespace' => 'http://foobar/soap/User/1.0/')); - $soapHeader = $soapClient->createSoapHeader('foo', 'bar'); - - $this->assertInstanceOf('SoapHeader', $soapHeader); - $this->assertEquals('http://foobar/soap/User/1.0/', $soapHeader->namespace); - $this->assertEquals('foo', $soapHeader->name); - $this->assertEquals('bar', $soapHeader->data); - } - - public function testCreateSoapHeaderThrowsAnExceptionIfNamespaceIsNull() - { - $soapClient = new SoapClient('foo.wsdl'); - - $this->setExpectedException('RuntimeException'); - $soapHeader = $soapClient->createSoapHeader('foo', 'bar'); - } - - public function testGetSoapOptions() - { - Cache::setType(Cache::TYPE_MEMORY); - $soapClient = new SoapClient('foo.wsdl', array('debug' => true)); - $this->assertEquals(array('cache_wsdl' => Cache::getType(), 'trace' => true, 'classmap' => array(), 'exceptions' => true, 'typemap' => array(), 'user_agent' => 'BeSimpleSoap'), $soapClient->getSoapOptions()); - - $soapClient = new SoapClient('foo.wsdl', array('debug' => false, 'cache_type' => Cache::TYPE_NONE)); - $this->assertEquals(array('cache_wsdl' => Cache::TYPE_NONE, 'trace' => false, 'classmap' => array(), 'exceptions' => true, 'typemap' => array(), 'user_agent' => 'BeSimpleSoap'), $soapClient->getSoapOptions()); - } - - public function testGetSoapOptionsWithClassmap() - { - $classmap = new Classmap(); - - $soapClient = new SoapClient('foo.wsdl', array(), $classmap); - $soapOptions = $soapClient->getSoapOptions(); - - $this->assertSame(array(), $soapOptions['classmap']); - - $map = array( - 'foobar' => 'BeSimple\SoapClient\SoapClient', - 'barfoo' => 'BeSimple\SoapClient\Tests\SoapClientTest', - ); - $classmap->set($map); - $soapOptions = $soapClient->getSoapOptions(); - - $this->assertSame($map, $soapOptions['classmap']); - } - - public function testGetSoapOptionsWithTypemap() - { - $converters = new TypeConverterCollection(); - - $dateTimeTypeConverter = new DateTimeTypeConverter(); - $converters->add($dateTimeTypeConverter); - - $dateTypeConverter = new DateTypeConverter(); - $converters->add($dateTypeConverter); - - $soapClient = new SoapClient('foo.wsdl', array(), null, $converters); - $soapOptions = $soapClient->getSoapOptions(); - - $this->assertEquals('http://www.w3.org/2001/XMLSchema', $soapOptions['typemap'][0]['type_ns']); - $this->assertEquals('dateTime', $soapOptions['typemap'][0]['type_name']); - $this->assertInstanceOf('Closure', $soapOptions['typemap'][0]['from_xml']); - $this->assertInstanceOf('Closure', $soapOptions['typemap'][0]['to_xml']); - - $this->assertEquals('http://www.w3.org/2001/XMLSchema', $soapOptions['typemap'][1]['type_ns']); - $this->assertEquals('date', $soapOptions['typemap'][1]['type_name']); - $this->assertInstanceOf('Closure', $soapOptions['typemap'][1]['from_xml']); - $this->assertInstanceOf('Closure', $soapOptions['typemap'][1]['to_xml']); - } - - public function testGetNativeSoapClient() - { - $soapClient = new SoapClient(__DIR__.'/Fixtures/foobar.wsdl', array('debug' => true)); - - $this->assertInstanceOf('SoapClient', $soapClient->getNativeSoapClient()); - } -} \ No newline at end of file From 9ce673b8cb9eda615fe3934dd9946d2822ed37a9 Mon Sep 17 00:00:00 2001 From: Francis Besset Date: Mon, 10 Oct 2011 00:40:21 +0200 Subject: [PATCH 22/63] Added Authentication in SoapClientBuilder --- src/BeSimple/SoapClient/SoapClientBuilder.php | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/src/BeSimple/SoapClient/SoapClientBuilder.php b/src/BeSimple/SoapClient/SoapClientBuilder.php index 63c9934..fdb7f39 100644 --- a/src/BeSimple/SoapClient/SoapClientBuilder.php +++ b/src/BeSimple/SoapClient/SoapClientBuilder.php @@ -16,9 +16,12 @@ use BeSimple\SoapCommon\AbstractSoapBuilder; /** * @author Francis Besset + * @author Christian Kerl */ class SoapClientBuilder extends AbstractSoapBuilder { + protected $soapOptionAuthentication = array(); + /** * @return SoapClientBuilder */ @@ -36,7 +39,7 @@ class SoapClientBuilder extends AbstractSoapBuilder { $this->validateOptions(); - return new SoapClient($this->optionWsdl, $this->options); + return new SoapClient($this->optionWsdl, $this->getSoapOptions() + $this->soapOptionAuthentication); } /** @@ -69,6 +72,34 @@ class SoapClientBuilder extends AbstractSoapBuilder return $this; } + /** + * @return SoapClientBuilder + */ + public function withBasicAuthentication($username, $password) + { + $this->soapOptionAuthentication = array( + 'authentication' => SOAP_AUTHENTICATION_BASIC, + 'login' => $username, + 'password' => $password + ); + + return $this; + } + + /** + * @return SoapClientBuilder + */ + public function withDigestAuthentication($certificate, $password) + { + $this->soapOptionAuthentication = array( + 'authentication' => SOAP_AUTHENTICATION_DIGEST, + 'local_cert' => $certificate, + 'passphrase' => $password + ); + + return $this; + } + protected function validateOptions() { $this->validateWsdl(); From 24a912b50d175ee9b222c09015711c4a84c47350 Mon Sep 17 00:00:00 2001 From: Francis Besset Date: Mon, 10 Oct 2011 20:13:42 +0200 Subject: [PATCH 23/63] Fixed typo --- src/BeSimple/SoapClient/SoapClientBuilder.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/BeSimple/SoapClient/SoapClientBuilder.php b/src/BeSimple/SoapClient/SoapClientBuilder.php index fdb7f39..20e5dc0 100644 --- a/src/BeSimple/SoapClient/SoapClientBuilder.php +++ b/src/BeSimple/SoapClient/SoapClientBuilder.php @@ -39,7 +39,7 @@ class SoapClientBuilder extends AbstractSoapBuilder { $this->validateOptions(); - return new SoapClient($this->optionWsdl, $this->getSoapOptions() + $this->soapOptionAuthentication); + return new SoapClient($this->wsdl, $this->getSoapOptions() + $this->soapOptionAuthentication); } /** From 55a9b6e2335db1c5b6400baebc11a8cc7ea1610e Mon Sep 17 00:00:00 2001 From: Francis Besset Date: Tue, 11 Oct 2011 21:50:07 +0200 Subject: [PATCH 24/63] Fixed typo and added unit tests --- src/BeSimple/SoapClient/SoapClientBuilder.php | 16 ++++++++++++---- .../Tests/SoapClient/SoapClientBuilderTest.php | 14 ++++++++++++++ 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/BeSimple/SoapClient/SoapClientBuilder.php b/src/BeSimple/SoapClient/SoapClientBuilder.php index 20e5dc0..a8d5e15 100644 --- a/src/BeSimple/SoapClient/SoapClientBuilder.php +++ b/src/BeSimple/SoapClient/SoapClientBuilder.php @@ -39,7 +39,12 @@ class SoapClientBuilder extends AbstractSoapBuilder { $this->validateOptions(); - return new SoapClient($this->wsdl, $this->getSoapOptions() + $this->soapOptionAuthentication); + return new SoapClient($this->wsdl, $this->getSoapOptions()); + } + + public function getSoapOptions() + { + return parent::getSoapOptions() + $this->soapOptionAuthentication; } /** @@ -80,7 +85,7 @@ class SoapClientBuilder extends AbstractSoapBuilder $this->soapOptionAuthentication = array( 'authentication' => SOAP_AUTHENTICATION_BASIC, 'login' => $username, - 'password' => $password + 'password' => $password, ); return $this; @@ -89,14 +94,17 @@ class SoapClientBuilder extends AbstractSoapBuilder /** * @return SoapClientBuilder */ - public function withDigestAuthentication($certificate, $password) + public function withDigestAuthentication($certificate, $passphrase = null) { $this->soapOptionAuthentication = array( 'authentication' => SOAP_AUTHENTICATION_DIGEST, 'local_cert' => $certificate, - 'passphrase' => $password ); + if ($passphrase) { + $this->soapOptionAuthentication['passphrase'] = $passphrase; + } + return $this; } diff --git a/tests/BeSimple/Tests/SoapClient/SoapClientBuilderTest.php b/tests/BeSimple/Tests/SoapClient/SoapClientBuilderTest.php index 7ba11c2..d256a01 100644 --- a/tests/BeSimple/Tests/SoapClient/SoapClientBuilderTest.php +++ b/tests/BeSimple/Tests/SoapClient/SoapClientBuilderTest.php @@ -62,6 +62,20 @@ class SoapClientBuilderTest extends \PHPUnit_Framework_TestCase $this->assertEquals($this->mergeOptions(array('user_agent' => 'BeSimpleSoap Test')), $builder->getSoapOptions()); } + public function testWithAuthentication() + { + $builder = $this->getSoapBuilder(); + + $builder->withDigestAuthentication(__DIR__.'/Fixtures/cert.pem', 'foobar'); + $this->assertEquals($this->mergeOptions(array('authentication' => SOAP_AUTHENTICATION_DIGEST, 'local_cert' => __DIR__.'/Fixtures/cert.pem', 'passphrase' => 'foobar')), $builder->getSoapOptions()); + + $builder->withDigestAuthentication(__DIR__.'/Fixtures/cert.pem'); + $this->assertEquals($this->mergeOptions(array('authentication' => SOAP_AUTHENTICATION_DIGEST, 'local_cert' => __DIR__.'/Fixtures/cert.pem')), $builder->getSoapOptions()); + + $builder->withBasicAuthentication('foo', 'bar'); + $this->assertEquals($this->mergeOptions(array('authentication' => SOAP_AUTHENTICATION_BASIC, 'login' => 'foo', 'password' => 'bar')), $builder->getSoapOptions()); + } + public function testCreateWithDefaults() { $builder = SoapClientBuilder::createWithDefaults(); From d6c9074c942af4395eb8ae10cff77892618d3aeb Mon Sep 17 00:00:00 2001 From: Francis Besset Date: Tue, 11 Oct 2011 22:07:16 +0200 Subject: [PATCH 25/63] Added SoapClientBuilder::withProxy() --- src/BeSimple/SoapClient/SoapClientBuilder.php | 13 +++++++++++++ .../Tests/SoapClient/SoapClientBuilderTest.php | 11 +++++++++++ 2 files changed, 24 insertions(+) diff --git a/src/BeSimple/SoapClient/SoapClientBuilder.php b/src/BeSimple/SoapClient/SoapClientBuilder.php index a8d5e15..9d5cfa2 100644 --- a/src/BeSimple/SoapClient/SoapClientBuilder.php +++ b/src/BeSimple/SoapClient/SoapClientBuilder.php @@ -108,6 +108,19 @@ class SoapClientBuilder extends AbstractSoapBuilder return $this; } + public function withProxy($host, $port, $username = null, $password = null) + { + $this->soapOptions['proxy_host'] = $host; + $this->soapOptions['proxy_port'] = $port; + + if ($username) { + $this->soapOptions['proxy_login'] = $username; + $this->soapOptions['proxy_password'] = $password; + } + + return $this; + } + protected function validateOptions() { $this->validateWsdl(); diff --git a/tests/BeSimple/Tests/SoapClient/SoapClientBuilderTest.php b/tests/BeSimple/Tests/SoapClient/SoapClientBuilderTest.php index d256a01..b04f145 100644 --- a/tests/BeSimple/Tests/SoapClient/SoapClientBuilderTest.php +++ b/tests/BeSimple/Tests/SoapClient/SoapClientBuilderTest.php @@ -76,6 +76,17 @@ class SoapClientBuilderTest extends \PHPUnit_Framework_TestCase $this->assertEquals($this->mergeOptions(array('authentication' => SOAP_AUTHENTICATION_BASIC, 'login' => 'foo', 'password' => 'bar')), $builder->getSoapOptions()); } + public function testWithProxy() + { + $builder = $this->getSoapBuilder(); + + $builder->withProxy('localhost', 8080); + $this->assertEquals($this->mergeOptions(array('proxy_host' => 'localhost', 'proxy_port' => 8080)), $builder->getSoapOptions()); + + $builder->withProxy('127.0.0.1', 8585, 'foo', 'bar'); + $this->assertEquals($this->mergeOptions(array('proxy_host' => '127.0.0.1', 'proxy_port' => 8585, 'proxy_login' => 'foo', 'proxy_password' => 'bar')), $builder->getSoapOptions()); + } + public function testCreateWithDefaults() { $builder = SoapClientBuilder::createWithDefaults(); From 004d88f564ee6bb206adf8085e22c3401bef293c Mon Sep 17 00:00:00 2001 From: Francis Besset Date: Tue, 11 Oct 2011 22:39:55 +0200 Subject: [PATCH 26/63] Added SoapClientBuilder::withCompression() --- src/BeSimple/SoapClient/SoapClientBuilder.php | 10 ++++++++++ .../Tests/SoapClient/SoapClientBuilderTest.php | 11 +++++++++++ 2 files changed, 21 insertions(+) diff --git a/src/BeSimple/SoapClient/SoapClientBuilder.php b/src/BeSimple/SoapClient/SoapClientBuilder.php index 9d5cfa2..65a6f9b 100644 --- a/src/BeSimple/SoapClient/SoapClientBuilder.php +++ b/src/BeSimple/SoapClient/SoapClientBuilder.php @@ -77,6 +77,16 @@ class SoapClientBuilder extends AbstractSoapBuilder return $this; } + public function withCompressionGzip() + { + $this->soapOptions['compression'] = SOAP_COMPRESSION_ACCEPT | SOAP_COMPRESSION_GZIP; + } + + public function withCompressionDeflate() + { + $this->soapOptions['compression'] = SOAP_COMPRESSION_ACCEPT | SOAP_COMPRESSION_DEFLATE; + } + /** * @return SoapClientBuilder */ diff --git a/tests/BeSimple/Tests/SoapClient/SoapClientBuilderTest.php b/tests/BeSimple/Tests/SoapClient/SoapClientBuilderTest.php index b04f145..9b33cce 100644 --- a/tests/BeSimple/Tests/SoapClient/SoapClientBuilderTest.php +++ b/tests/BeSimple/Tests/SoapClient/SoapClientBuilderTest.php @@ -62,6 +62,17 @@ class SoapClientBuilderTest extends \PHPUnit_Framework_TestCase $this->assertEquals($this->mergeOptions(array('user_agent' => 'BeSimpleSoap Test')), $builder->getSoapOptions()); } + public function testWithCompression() + { + $builder = $this->getSoapBuilder(); + + $builder->withCompressionGzip(); + $this->assertEquals($this->mergeOptions(array('compression' => SOAP_COMPRESSION_ACCEPT | SOAP_COMPRESSION_GZIP)), $builder->getSoapOptions()); + + $builder->withCompressionDeflate(); + $this->assertEquals($this->mergeOptions(array('compression' => SOAP_COMPRESSION_ACCEPT | SOAP_COMPRESSION_DEFLATE)), $builder->getSoapOptions()); + } + public function testWithAuthentication() { $builder = $this->getSoapBuilder(); From 98a916957cda0434acaea8a7ef3b58a2d4c1d8f2 Mon Sep 17 00:00:00 2001 From: Andreas Schamberger Date: Sun, 16 Oct 2011 17:18:35 +0200 Subject: [PATCH 27/63] fix file header --- src/BeSimple/SoapClient/Curl.php | 2 -- src/BeSimple/SoapClient/Helper.php | 2 -- src/BeSimple/SoapClient/SoapClient.php | 2 -- src/BeSimple/SoapClient/WsdlDownloader.php | 2 -- tests/BeSimple/Tests/SoapClient/WsdlDownloaderTest.php | 2 -- 5 files changed, 10 deletions(-) diff --git a/src/BeSimple/SoapClient/Curl.php b/src/BeSimple/SoapClient/Curl.php index 9a11c80..c128ae4 100644 --- a/src/BeSimple/SoapClient/Curl.php +++ b/src/BeSimple/SoapClient/Curl.php @@ -8,8 +8,6 @@ * * 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; diff --git a/src/BeSimple/SoapClient/Helper.php b/src/BeSimple/SoapClient/Helper.php index cc33f35..89edeba 100644 --- a/src/BeSimple/SoapClient/Helper.php +++ b/src/BeSimple/SoapClient/Helper.php @@ -8,8 +8,6 @@ * * 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; diff --git a/src/BeSimple/SoapClient/SoapClient.php b/src/BeSimple/SoapClient/SoapClient.php index d48d84a..65b584d 100644 --- a/src/BeSimple/SoapClient/SoapClient.php +++ b/src/BeSimple/SoapClient/SoapClient.php @@ -8,8 +8,6 @@ * * 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; diff --git a/src/BeSimple/SoapClient/WsdlDownloader.php b/src/BeSimple/SoapClient/WsdlDownloader.php index 4990e12..8ccf889 100644 --- a/src/BeSimple/SoapClient/WsdlDownloader.php +++ b/src/BeSimple/SoapClient/WsdlDownloader.php @@ -8,8 +8,6 @@ * * 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; diff --git a/tests/BeSimple/Tests/SoapClient/WsdlDownloaderTest.php b/tests/BeSimple/Tests/SoapClient/WsdlDownloaderTest.php index 477bec1..fc006ae 100644 --- a/tests/BeSimple/Tests/SoapClient/WsdlDownloaderTest.php +++ b/tests/BeSimple/Tests/SoapClient/WsdlDownloaderTest.php @@ -8,8 +8,6 @@ * * 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; From c54b2925fec9ca3479e0a1d5ae21340dd71a1533 Mon Sep 17 00:00:00 2001 From: Andreas Schamberger Date: Sun, 16 Oct 2011 17:35:14 +0200 Subject: [PATCH 28/63] * fixed missing port number in WsdlDownloader * fixed boolean assertions --- src/BeSimple/SoapClient/WsdlDownloader.php | 6 +++- .../Tests/SoapClient/WsdlDownloaderTest.php | 33 ++++++++++--------- 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/src/BeSimple/SoapClient/WsdlDownloader.php b/src/BeSimple/SoapClient/WsdlDownloader.php index 8ccf889..55c7d90 100644 --- a/src/BeSimple/SoapClient/WsdlDownloader.php +++ b/src/BeSimple/SoapClient/WsdlDownloader.php @@ -218,6 +218,10 @@ class WsdlDownloader unset($parts[$key]); } } - return $urlParts['scheme'] . '://' . $urlParts['host'] . implode('/', $parts); + $hostname = $urlParts['scheme'] . '://' . $urlParts['host']; + if (isset($urlParts['port'])) { + $hostname .= ':' . $urlParts['port']; + } + return $hostname . implode('/', $parts); } } \ No newline at end of file diff --git a/tests/BeSimple/Tests/SoapClient/WsdlDownloaderTest.php b/tests/BeSimple/Tests/SoapClient/WsdlDownloaderTest.php index fc006ae..09ec14c 100644 --- a/tests/BeSimple/Tests/SoapClient/WsdlDownloaderTest.php +++ b/tests/BeSimple/Tests/SoapClient/WsdlDownloaderTest.php @@ -98,21 +98,21 @@ class WsdlDownloaderTest extends \PHPUnit_Framework_TestCase $method = $class->getMethod('isRemoteFile'); $method->setAccessible(true); - $this->assertEquals(true, $method->invoke($wd, 'http://www.php.net/')); - $this->assertEquals(true, $method->invoke($wd, 'http://localhost/')); - $this->assertEquals(true, $method->invoke($wd, 'http://mylocaldomain/')); - $this->assertEquals(true, $method->invoke($wd, 'http://www.php.net/dir/test.html')); - $this->assertEquals(true, $method->invoke($wd, 'http://localhost/dir/test.html')); - $this->assertEquals(true, $method->invoke($wd, 'http://mylocaldomain/dir/test.html')); - $this->assertEquals(true, $method->invoke($wd, 'https://www.php.net/')); - $this->assertEquals(true, $method->invoke($wd, 'https://localhost/')); - $this->assertEquals(true, $method->invoke($wd, 'https://mylocaldomain/')); - $this->assertEquals(true, $method->invoke($wd, 'https://www.php.net/dir/test.html')); - $this->assertEquals(true, $method->invoke($wd, 'https://localhost/dir/test.html')); - $this->assertEquals(true, $method->invoke($wd, 'https://mylocaldomain/dir/test.html')); - $this->assertEquals(false, $method->invoke($wd, 'c:/dir/test.html')); - $this->assertEquals(false, $method->invoke($wd, '/dir/test.html')); - $this->assertEquals(false, $method->invoke($wd, '../dir/test.html')); + $this->assertTrue($method->invoke($wd, 'http://www.php.net/')); + $this->assertTrue($method->invoke($wd, 'http://localhost/')); + $this->assertTrue($method->invoke($wd, 'http://mylocaldomain/')); + $this->assertTrue($method->invoke($wd, 'http://www.php.net/dir/test.html')); + $this->assertTrue($method->invoke($wd, 'http://localhost/dir/test.html')); + $this->assertTrue($method->invoke($wd, 'http://mylocaldomain/dir/test.html')); + $this->assertTrue($method->invoke($wd, 'https://www.php.net/')); + $this->assertTrue($method->invoke($wd, 'https://localhost/')); + $this->assertTrue($method->invoke($wd, 'https://mylocaldomain/')); + $this->assertTrue($method->invoke($wd, 'https://www.php.net/dir/test.html')); + $this->assertTrue($method->invoke($wd, 'https://localhost/dir/test.html')); + $this->assertTrue($method->invoke($wd, 'https://mylocaldomain/dir/test.html')); + $this->assertFalse($method->invoke($wd, 'c:/dir/test.html')); + $this->assertFalse($method->invoke($wd, '/dir/test.html')); + $this->assertFalse($method->invoke($wd, '../dir/test.html')); } public function testResolveXsdIncludes() @@ -182,6 +182,9 @@ class WsdlDownloaderTest extends \PHPUnit_Framework_TestCase $method = $class->getMethod('resolveRelativePathInUrl'); $method->setAccessible(true); + $this->assertEquals('http://localhost:8080/test', $method->invoke($wd, 'http://localhost:8080/sub', '/test')); + $this->assertEquals('http://localhost:8080/test', $method->invoke($wd, 'http://localhost:8080/sub/', '/test')); + $this->assertEquals('http://localhost/test', $method->invoke($wd, 'http://localhost/sub', '/test')); $this->assertEquals('http://localhost/test', $method->invoke($wd, 'http://localhost/sub/', '/test')); From cfe1a966abfa490991f5d9c07be38394730511db Mon Sep 17 00:00:00 2001 From: Andreas Schamberger Date: Sun, 16 Oct 2011 19:41:34 +0200 Subject: [PATCH 29/63] added cURL tests --- src/BeSimple/SoapClient/Curl.php | 608 +++++++++--------- tests/BeSimple/Tests/SoapClient/CurlTest.php | 180 ++++++ .../Tests/SoapClient/Fixtures/curl.txt | 1 + 3 files changed, 485 insertions(+), 304 deletions(-) create mode 100644 tests/BeSimple/Tests/SoapClient/CurlTest.php create mode 100644 tests/BeSimple/Tests/SoapClient/Fixtures/curl.txt diff --git a/src/BeSimple/SoapClient/Curl.php b/src/BeSimple/SoapClient/Curl.php index c128ae4..5789853 100644 --- a/src/BeSimple/SoapClient/Curl.php +++ b/src/BeSimple/SoapClient/Curl.php @@ -1,305 +1,305 @@ - - * (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; - -/** - * 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)); - } + + * (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; + +/** + * 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 = array(), $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/tests/BeSimple/Tests/SoapClient/CurlTest.php b/tests/BeSimple/Tests/SoapClient/CurlTest.php new file mode 100644 index 0000000..928eae6 --- /dev/null +++ b/tests/BeSimple/Tests/SoapClient/CurlTest.php @@ -0,0 +1,180 @@ + + * (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\SoapClient\Curl; + +/** +* @author Andreas Schamberger +*/ +class CurlTest extends \PHPUnit_Framework_TestCase +{ + protected $webserverProcessId; + + protected function startPhpWebserver() + { + if ('Windows' == substr(php_uname('s'), 0, 7 )) { + $powershellCommand = "\$app = start-process php.exe -ArgumentList '-S localhost:8000 -t ".__DIR__.DIRECTORY_SEPARATOR."Fixtures' -WindowStyle 'Hidden' -passthru; Echo \$app.Id;"; + $shellCommand = 'powershell -command "& {'.$powershellCommand.'}"'; + } else { + $shellCommand = "nohup php -S localhost:8000 -t ".__DIR__.DIRECTORY_SEPARATOR."Fixtures &"; + } + $output = array(); + exec($shellCommand, $output); + $this->webserverProcessId = $output[0]; // pid is in first element + } + + protected function stopPhpWebserver() + { + if (!is_null($this->webserverProcessId)) { + if ('Windows' == substr(php_uname('s'), 0, 7 )) { + exec('TASKKILL /F /PID ' . $this->webserverProcessId); + } else { + exec('kill ' . $this->webserverProcessId); + } + $this->webserverProcessId = null; + } + } + + public function testExec() + { + $this->startPhpWebserver(); + + $curl = new Curl(); + + $this->assertTrue($curl->exec('http://localhost:8000/curl.txt')); + $this->assertTrue($curl->exec('http://localhost:8000/404.txt')); + + $this->stopPhpWebserver(); + } + + public function testGetErrorMessage() + { + $this->startPhpWebserver(); + + $curl = new Curl(); + + $curl->exec('http://unknown/curl.txt'); + $this->assertEquals('Could not connect to host', $curl->getErrorMessage()); + + $curl->exec('xyz://localhost:8000/@404.txt'); + $this->assertEquals('Unknown protocol. Only http and https are allowed.', $curl->getErrorMessage()); + + $curl->exec(''); + $this->assertEquals('Unable to parse URL', $curl->getErrorMessage()); + + $this->stopPhpWebserver(); + } + + public function testGetRequestHeaders() + { + $this->startPhpWebserver(); + + $curl = new Curl(); + + $curl->exec('http://localhost:8000/curl.txt'); + $this->assertEquals(136, strlen($curl->getRequestHeaders())); + + $curl->exec('http://localhost:8000/404.txt'); + $this->assertEquals(135, strlen($curl->getRequestHeaders())); + + $this->stopPhpWebserver(); + } + + public function testGetResponse() + { + $this->startPhpWebserver(); + + $curl = new Curl(); + + $curl->exec('http://localhost:8000/curl.txt'); + $this->assertEquals(150, strlen($curl->getResponse())); + + $curl->exec('http://localhost:8000/404.txt'); + $this->assertEquals(1282, strlen($curl->getResponse())); + + $this->stopPhpWebserver(); + } + + public function testGetResponseBody() + { + $this->startPhpWebserver(); + + $curl = new Curl(); + + $curl->exec('http://localhost:8000/curl.txt'); + $this->assertEquals('This is a testfile for cURL.', $curl->getResponseBody()); + + $this->stopPhpWebserver(); + } + + public function testGetResponseContentType() + { + $this->startPhpWebserver(); + + $curl = new Curl(); + + $curl->exec('http://localhost:8000/curl.txt'); + $this->assertEquals('text/plain; charset=UTF-8', $curl->getResponseContentType()); + + $curl->exec('http://localhost:8000/404.txt'); + $this->assertEquals('text/html; charset=UTF-8', $curl->getResponseContentType()); + + $this->stopPhpWebserver(); + } + + public function testGetResponseHeaders() + { + $this->startPhpWebserver(); + + $curl = new Curl(); + + $curl->exec('http://localhost:8000/curl.txt'); + $this->assertEquals(122, strlen($curl->getResponseHeaders())); + + $curl->exec('http://localhost:8000/404.txt'); + $this->assertEquals(130, strlen($curl->getResponseHeaders())); + + $this->stopPhpWebserver(); + } + + public function testGetResponseStatusCode() + { + $this->startPhpWebserver(); + + $curl = new Curl(); + + $curl->exec('http://localhost:8000/curl.txt'); + $this->assertEquals(200, $curl->getResponseStatusCode()); + + $curl->exec('http://localhost:8000/404.txt'); + $this->assertEquals(404, $curl->getResponseStatusCode()); + + $this->stopPhpWebserver(); + } + + public function testGetResponseStatusMessage() + { + $this->startPhpWebserver(); + + $curl = new Curl(); + + $curl->exec('http://localhost:8000/curl.txt'); + $this->assertEquals('OK', $curl->getResponseStatusMessage()); + + $curl->exec('http://localhost:8000/404.txt'); + $this->assertEquals('Not Found', $curl->getResponseStatusMessage()); + + $this->stopPhpWebserver(); + } +} \ No newline at end of file diff --git a/tests/BeSimple/Tests/SoapClient/Fixtures/curl.txt b/tests/BeSimple/Tests/SoapClient/Fixtures/curl.txt new file mode 100644 index 0000000..070def3 --- /dev/null +++ b/tests/BeSimple/Tests/SoapClient/Fixtures/curl.txt @@ -0,0 +1 @@ +This is a testfile for cURL. \ No newline at end of file From 101169f7d6599faaa2477e3a8322e54226f5c763 Mon Sep 17 00:00:00 2001 From: Andreas Schamberger Date: Sun, 16 Oct 2011 19:49:24 +0200 Subject: [PATCH 30/63] convert to unix line endings --- src/BeSimple/SoapClient/Helper.php | 736 ++++++++++----------- src/BeSimple/SoapClient/SoapClient.php | 48 +- src/BeSimple/SoapClient/WsdlDownloader.php | 452 ++++++------- 3 files changed, 618 insertions(+), 618 deletions(-) diff --git a/src/BeSimple/SoapClient/Helper.php b/src/BeSimple/SoapClient/Helper.php index 89edeba..f79ab1f 100644 --- a/src/BeSimple/SoapClient/Helper.php +++ b/src/BeSimple/SoapClient/Helper.php @@ -1,369 +1,369 @@ - - * (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; - -/** - * 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 ('cgi' == substr(php_sapi_name(), 0, 3)) { - header('Status: ' . $header); - } else { - header($_SERVER['SERVER_PROTOCOL'] . ' ' . $header); - } - } + + * (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; + +/** + * 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 ('cgi' == substr(php_sapi_name(), 0, 3)) { + 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 index 65b584d..86444da 100644 --- a/src/BeSimple/SoapClient/SoapClient.php +++ b/src/BeSimple/SoapClient/SoapClient.php @@ -22,31 +22,31 @@ namespace BeSimple\SoapClient; */ class SoapClient extends \SoapClient { - /** - * Last request headers. - * - * @var string + /** + * Last request headers. + * + * @var string */ private $lastRequestHeaders = ''; - - /** - * Last request. - * - * @var string + + /** + * Last request. + * + * @var string */ private $lastRequest = ''; - - /** - * Last response headers. - * - * @var string + + /** + * Last response headers. + * + * @var string */ private $lastResponseHeaders = ''; - - /** - * Last response. - * - * @var string + + /** + * Last response. + * + * @var string */ private $lastResponse = ''; @@ -65,7 +65,7 @@ class SoapClient extends \SoapClient private $wsdlFile = null; /** - * Extended constructor that saves the options as the parent class' + * Extended constructor that saves the options as the parent class' * property is private. * * @param string $wsdl @@ -99,16 +99,16 @@ class SoapClient extends \SoapClient } } // store local copy as ext/soap's property is private - $this->options = $options; + $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 + // 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. @@ -186,7 +186,7 @@ class SoapClient extends \SoapClient // destruct curl object unset($curl); return $response; - } + } /** * Custom request method to be able to modify the SOAP messages. diff --git a/src/BeSimple/SoapClient/WsdlDownloader.php b/src/BeSimple/SoapClient/WsdlDownloader.php index 55c7d90..6406e2b 100644 --- a/src/BeSimple/SoapClient/WsdlDownloader.php +++ b/src/BeSimple/SoapClient/WsdlDownloader.php @@ -1,227 +1,227 @@ - - * (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; - -/** - * 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. - * - * @param array $options - */ - public function __construct(array $options = array()) - { - // 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; - if (!isset($this->options['resolve_xsd_includes'])) { - $this->options['resolve_xsd_includes'] = true; - } - } - - /** - * 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 (isset($urlParts['path']) && strpos($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 - $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 == "..") { - $keyToDelete = $key-1; - while ($keyToDelete > 0) { - if (isset($parts[$keyToDelete])) { - unset($parts[$keyToDelete]); - break; - } else { - $keyToDelete--; - } - } - unset($parts[$key]); - } - } - $hostname = $urlParts['scheme'] . '://' . $urlParts['host']; - if (isset($urlParts['port'])) { - $hostname .= ':' . $urlParts['port']; - } - return $hostname . implode('/', $parts); - } + + * (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; + +/** + * 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. + * + * @param array $options + */ + public function __construct(array $options = array()) + { + // 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; + if (!isset($this->options['resolve_xsd_includes'])) { + $this->options['resolve_xsd_includes'] = true; + } + } + + /** + * 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 (isset($urlParts['path']) && strpos($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 + $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 == "..") { + $keyToDelete = $key-1; + while ($keyToDelete > 0) { + if (isset($parts[$keyToDelete])) { + unset($parts[$keyToDelete]); + break; + } else { + $keyToDelete--; + } + } + unset($parts[$key]); + } + } + $hostname = $urlParts['scheme'] . '://' . $urlParts['host']; + if (isset($urlParts['port'])) { + $hostname .= ':' . $urlParts['port']; + } + return $hostname . implode('/', $parts); + } } \ No newline at end of file From eb8e8495ad4c89192a98bcf8795630326f773113 Mon Sep 17 00:00:00 2001 From: Andreas Schamberger Date: Sun, 16 Oct 2011 19:57:29 +0200 Subject: [PATCH 31/63] remove obsolete functions/constants --- src/BeSimple/SoapClient/Helper.php | 168 ----------------------------- 1 file changed, 168 deletions(-) diff --git a/src/BeSimple/SoapClient/Helper.php b/src/BeSimple/SoapClient/Helper.php index f79ab1f..bf261a0 100644 --- a/src/BeSimple/SoapClient/Helper.php +++ b/src/BeSimple/SoapClient/Helper.php @@ -136,16 +136,6 @@ class Helper */ 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. * @@ -153,30 +143,6 @@ class Helper */ 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. * @@ -203,28 +169,6 @@ class Helper ); } - /** - * 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. * @@ -254,116 +198,4 @@ class Helper 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 ('cgi' == substr(php_sapi_name(), 0, 3)) { - header('Status: ' . $header); - } else { - header($_SERVER['SERVER_PROTOCOL'] . ' ' . $header); - } - } } \ No newline at end of file From ccd5d04078fd0efcce0f533c6cf6e81f219ac3dc Mon Sep 17 00:00:00 2001 From: Andreas Schamberger Date: Sat, 22 Oct 2011 11:28:15 +0200 Subject: [PATCH 32/63] added email removed unused variable --- src/BeSimple/SoapClient/Curl.php | 2 +- src/BeSimple/SoapClient/Helper.php | 9 +-------- src/BeSimple/SoapClient/SoapClient.php | 4 +--- src/BeSimple/SoapClient/WsdlDownloader.php | 2 +- tests/BeSimple/Tests/SoapClient/CurlTest.php | 4 ++-- tests/BeSimple/Tests/SoapClient/WsdlDownloaderTest.php | 4 ++-- 6 files changed, 8 insertions(+), 17 deletions(-) diff --git a/src/BeSimple/SoapClient/Curl.php b/src/BeSimple/SoapClient/Curl.php index 5789853..38bc066 100644 --- a/src/BeSimple/SoapClient/Curl.php +++ b/src/BeSimple/SoapClient/Curl.php @@ -15,7 +15,7 @@ namespace BeSimple\SoapClient; /** * cURL wrapper class for doing HTTP requests that uses the soap class options. * - * @author Andreas Schamberger + * @author Andreas Schamberger */ class Curl { diff --git a/src/BeSimple/SoapClient/Helper.php b/src/BeSimple/SoapClient/Helper.php index bf261a0..e22afb9 100644 --- a/src/BeSimple/SoapClient/Helper.php +++ b/src/BeSimple/SoapClient/Helper.php @@ -17,7 +17,7 @@ namespace BeSimple\SoapClient; * server implementations. It also provides namespace and configuration * constants. * - * @author Andreas Schamberger + * @author Andreas Schamberger */ class Helper { @@ -136,13 +136,6 @@ class Helper */ const PFX_XOP = 'xop'; - /** - * Wheather to format the XML output or not. - * - * @var boolean - */ - public static $formatXmlOutput = false; - /** * Generate a pseudo-random version 4 UUID. * diff --git a/src/BeSimple/SoapClient/SoapClient.php b/src/BeSimple/SoapClient/SoapClient.php index 86444da..75c930e 100644 --- a/src/BeSimple/SoapClient/SoapClient.php +++ b/src/BeSimple/SoapClient/SoapClient.php @@ -18,7 +18,7 @@ namespace BeSimple\SoapClient; * adds NTLM support. A custom WSDL downloader resolves remote xsd:includes and * allows caching of all remote referenced items. * - * @author Andreas Schamberger + * @author Andreas Schamberger */ class SoapClient extends \SoapClient { @@ -75,8 +75,6 @@ class SoapClient extends \SoapClient { // 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; diff --git a/src/BeSimple/SoapClient/WsdlDownloader.php b/src/BeSimple/SoapClient/WsdlDownloader.php index 6406e2b..261fd50 100644 --- a/src/BeSimple/SoapClient/WsdlDownloader.php +++ b/src/BeSimple/SoapClient/WsdlDownloader.php @@ -18,7 +18,7 @@ namespace BeSimple\SoapClient; * 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 + * @author Andreas Schamberger */ class WsdlDownloader { diff --git a/tests/BeSimple/Tests/SoapClient/CurlTest.php b/tests/BeSimple/Tests/SoapClient/CurlTest.php index 928eae6..b940b2a 100644 --- a/tests/BeSimple/Tests/SoapClient/CurlTest.php +++ b/tests/BeSimple/Tests/SoapClient/CurlTest.php @@ -15,8 +15,8 @@ namespace BeSimple\SoapClient; use BeSimple\SoapClient\Curl; /** -* @author Andreas Schamberger -*/ + * @author Andreas Schamberger + */ class CurlTest extends \PHPUnit_Framework_TestCase { protected $webserverProcessId; diff --git a/tests/BeSimple/Tests/SoapClient/WsdlDownloaderTest.php b/tests/BeSimple/Tests/SoapClient/WsdlDownloaderTest.php index 09ec14c..326a668 100644 --- a/tests/BeSimple/Tests/SoapClient/WsdlDownloaderTest.php +++ b/tests/BeSimple/Tests/SoapClient/WsdlDownloaderTest.php @@ -15,8 +15,8 @@ namespace BeSimple\SoapClient; use BeSimple\SoapClient\WsdlDownloader; /** -* @author Andreas Schamberger -*/ + * @author Andreas Schamberger + */ class WsdlDownloaderTest extends \PHPUnit_Framework_TestCase { protected $webserverProcessId; From cd0702c449c592a4e3a459cd84e375c0d75c24ed Mon Sep 17 00:00:00 2001 From: Andreas Schamberger Date: Sun, 23 Oct 2011 13:06:01 +0200 Subject: [PATCH 33/63] remove orphan test --- .../Tests/SoapClient/SoapRequestTest.php | 113 ------------------ 1 file changed, 113 deletions(-) delete mode 100644 tests/BeSimple/Tests/SoapClient/SoapRequestTest.php diff --git a/tests/BeSimple/Tests/SoapClient/SoapRequestTest.php b/tests/BeSimple/Tests/SoapClient/SoapRequestTest.php deleted file mode 100644 index c56d195..0000000 --- a/tests/BeSimple/Tests/SoapClient/SoapRequestTest.php +++ /dev/null @@ -1,113 +0,0 @@ - - * (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\Tests\SoapClient; - -use BeSimple\SoapClient\SoapRequest; - -class SoapRequestTest extends \PHPUnit_Framework_TestCase -{ - public function testSetFunction() - { - $soapRequest = new SoapRequest(); - $soapRequest->setFunction('foo'); - - $this->assertEquals('foo', $soapRequest->getFunction()); - } - - public function testSetArguments() - { - $soapRequest = new SoapRequest(); - $arguments = array( - 'foo' => true, - 'bar' => false, - ); - $soapRequest->setArguments($arguments); - - $this->assertEquals($arguments, $soapRequest->getArguments()); - } - - public function testGetArgument() - { - $soapRequest = new SoapRequest(); - - $this->assertSame(null, $soapRequest->getArgument('foo')); - $this->assertFalse($soapRequest->getArgument('foo', false)); - - $soapRequest->addArgument('foo', 'bar'); - - $this->assertEquals('bar', $soapRequest->getArgument('foo', false)); - } - - public function testSetOptions() - { - $soapRequest = new SoapRequest(); - $options = array( - 'uri' => 'foo', - 'soapaction' => 'bar', - ); - $soapRequest->setOptions($options); - - $this->assertEquals($options, $soapRequest->getOptions()); - } - - public function testGetOption() - { - $soapRequest = new SoapRequest(); - - $this->assertSame(null, $soapRequest->getOption('soapaction')); - $this->assertFalse($soapRequest->getOption('soapaction', false)); - - $soapRequest->addOption('soapaction', 'foo'); - - $this->assertEquals('foo', $soapRequest->getOption('soapaction')); - } - - public function testSetHeaders() - { - $soapRequest = new SoapRequest(); - - $this->assertEquals(null, $soapRequest->getHeaders()); - - $header1 = new \SoapHeader('foobar', 'foo', 'bar'); - $header2 = new \SoapHeader('barfoo', 'bar', 'foo'); - $soapRequest - ->addHeader($header1) - ->addHeader($header2) - ; - - $this->assertSame(array($header1, $header2), $soapRequest->getHeaders()); - } - - public function testConstruct() - { - $soapRequest = new SoapRequest(); - - $this->assertNull($soapRequest->getFunction()); - $this->assertEquals(array(), $soapRequest->getArguments()); - $this->assertEquals(array(), $soapRequest->getOptions()); - $this->assertEquals(null, $soapRequest->getHeaders()); - - $arguments = array('bar' => 'foobar'); - $options = array('soapaction' => 'foobar'); - $headers = array( - new \SoapHeader('foobar', 'foo', 'bar'), - new \SoapHeader('barfoo', 'bar', 'foo'), - ); - $soapRequest = new SoapRequest('foo', $arguments, $options, $headers); - - $this->assertEquals('foo', $soapRequest->getFunction()); - $this->assertEquals($arguments, $soapRequest->getArguments()); - $this->assertEquals($options, $soapRequest->getOptions()); - $this->assertSame($headers, $soapRequest->getHeaders()); - } -} From f168e8566adf591ad87fd08dafb53861a9b68458 Mon Sep 17 00:00:00 2001 From: Andreas Schamberger Date: Wed, 2 Nov 2011 12:48:51 +0100 Subject: [PATCH 34/63] Curl instance is now given as parameter to WsdlDownloader + some simplifications in client --- src/BeSimple/SoapClient/SoapClient.php | 130 +++++++++--------- src/BeSimple/SoapClient/WsdlDownloader.php | 49 ++++--- .../Tests/SoapClient/WsdlDownloaderTest.php | 19 ++- 3 files changed, 101 insertions(+), 97 deletions(-) diff --git a/src/BeSimple/SoapClient/SoapClient.php b/src/BeSimple/SoapClient/SoapClient.php index 75c930e..19d5be9 100644 --- a/src/BeSimple/SoapClient/SoapClient.php +++ b/src/BeSimple/SoapClient/SoapClient.php @@ -22,6 +22,27 @@ namespace BeSimple\SoapClient; */ class SoapClient extends \SoapClient { + /** + * Soap version. + * + * @var int + */ + protected $soapVersion = SOAP_1_1; + + /** + * Tracing enabled? + * + * @var boolean + */ + protected $tracingEnabled = false; + + /** + * cURL instance. + * + * @var \BeSimple\SoapClient\Curl + */ + protected $curl = null; + /** * Last request headers. * @@ -51,63 +72,33 @@ class SoapClient extends \SoapClient private $lastResponse = ''; /** - * Copy of the parent class' options array + * Constructor. * - * @var array(string=>mixed) + * @param string $wsdl WSDL file + * @param array(string=>mixed) $options Options array */ - 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) + public function __construct($wsdl, array $options = array()) { + // tracing enabled: store last request/response header and body + if (isset($options['trace']) && $options['trace'] === true) { + $this->tracingEnabled = true; + } + // store SOAP version + if (isset($options['soap_version'])) { + $this->soapVersion = $options['soap_version']; + } + $this->curl = new Curl($options); + $wsdlFile = $this->loadWsdl($wsdl, $options); // we want the exceptions option to be set $options['exceptions'] = true; - // 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); + parent::__construct($wsdlFile, $options); } + /** * Perform HTTP request with cURL. * @@ -120,7 +111,7 @@ class SoapClient extends \SoapClient { // $request is if unmodified from SoapClient not a php string type! $request = (string)$request; - if ($this->options['soap_version'] == SOAP_1_2) { + if ($this->soapVersion == SOAP_1_2) { $headers = array( 'Content-Type: application/soap+xml; charset=utf-8', ); @@ -131,39 +122,35 @@ class SoapClient extends \SoapClient } // add SOAPAction header $headers[] = 'SOAPAction: "' . $action . '"'; - // new curl object for request - $curl = new Curl($this->options); // execute request - $responseSuccessfull = $curl->exec($location, $request, $headers); + $responseSuccessfull = $this->curl->exec($location, $request, $headers); // tracing enabled: store last request header and body - if (isset($this->options['trace']) && $this->options['trace'] === true) { + if ($this->tracingEnabled === 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); + $faultstring = $this->curl->getErrorMessage(); 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(); + if ($this->tracingEnabled === true) { + $this->lastResponseHeaders = $this->curl->getResponseHeaders(); + $this->lastResponse = $this->curl->getResponseBody(); } - $response = $curl->getResponseBody(); + $response = $this->curl->getResponseBody(); // check if we do have a proper soap status code (if not soapfault) // // TODO -// $responseStatusCode = $curl->getResponseStatusCode(); +// $responseStatusCode = $this->curl->getResponseStatusCode(); // if ($responseStatusCode >= 400) { // $isError = 0; // $response = trim($response); // if (strlen($response) == 0) { // $isError = 1; // } else { -// $contentType = $curl->getResponseContentType(); +// $contentType = $this->curl->getResponseContentType(); // if ($contentType != 'application/soap+xml' // && $contentType != 'application/soap+xml') { // if (strncmp($response , "getResponseStatusMessage()); +// throw new \SoapFault('HTTP', $this->curl->getResponseStatusMessage()); // } // } elseif ($responseStatusCode != 200 && $responseStatusCode != 202) { // $dom = new \DOMDocument('1.0'); @@ -181,8 +168,6 @@ class SoapClient extends \SoapClient // throw new \SoapFault('HTTP', 'HTTP response status must be 200 or 202'); // } // } - // destruct curl object - unset($curl); return $response; } @@ -250,12 +235,25 @@ class SoapClient extends \SoapClient * ini settings. Does only file caching as SoapClient only supports a file * name parameter. * - * @param string $wsdl + * @param string $wsdl WSDL file + * @param array(string=>mixed) $options Options array * @return string */ - private function loadWsdl($wsdl) + private function loadWsdl($wsdl, array $options) { - $wsdlDownloader = new WsdlDownloader($this->options); + // option to resolve xsd includes + $resolveXsdIncludes = true; + if (isset($options['resolve_xsd_includes'])) + { + $resolveXsdIncludes = $options['resolve_xsd_includes']; + } + // option to enable cache + $wsdlCache = WSDL_CACHE_DISK; + if (isset($options['cache_wsdl'])) + { + $wsdlCache = $options['cache_wsdl']; + } + $wsdlDownloader = new WsdlDownloader($this->curl, $resolveXsdIncludes, $wsdlCache); try { $cacheFileName = $wsdlDownloader->download($wsdl); } catch (\RuntimeException $e) { diff --git a/src/BeSimple/SoapClient/WsdlDownloader.php b/src/BeSimple/SoapClient/WsdlDownloader.php index 261fd50..540c801 100644 --- a/src/BeSimple/SoapClient/WsdlDownloader.php +++ b/src/BeSimple/SoapClient/WsdlDownloader.php @@ -12,11 +12,14 @@ namespace BeSimple\SoapClient; +// TODO +//use BeSimple\SoapCommon\Helper; + /** - * 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. + * 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 */ @@ -44,24 +47,34 @@ class WsdlDownloader private $cacheTtl; /** - * Options array + * cURL instance for downloads. * - * @var array(string=>mixed) + * @var unknown_type */ - private $options = array(); + private $curl; + + /** + * Resolve XSD includes. + * + * @var boolean + */ + protected $resolveXsdIncludes = true; /** * Constructor. * - * @param array $options + * @param \BeSimple\SoapClient\Curl $curl Curl instance + * @param boolean $resolveXsdIncludes XSD include enabled? + * @param boolean $cacheWsdl Cache constant */ - public function __construct(array $options = array()) + public function __construct(Curl $curl, $resolveXsdIncludes = true, $cacheWsdl = WSDL_CACHE_DISK) { + $this->curl = $curl; + $this->resolveXsdIncludes = $resolveXsdIncludes; // 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) { + && $cacheWsdl === WSDL_CACHE_NONE) { $this->cacheEnabled = false; } $this->cacheDir = ini_get('soap.wsdl_cache_dir'); @@ -70,10 +83,6 @@ class WsdlDownloader } $this->cacheDir = rtrim($this->cacheDir, '/\\'); $this->cacheTtl = ini_get('soap.wsdl_cache_ttl'); - $this->options = $options; - if (!isset($this->options['resolve_xsd_includes'])) { - $this->options['resolve_xsd_includes'] = true; - } } /** @@ -87,20 +96,18 @@ class WsdlDownloader // 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) { + if ($isRemoteFile === true || $this->resolveXsdIncludes === 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); + $responseSuccessfull = $this->curl->exec($wsdl); // get content if ($responseSuccessfull === true) { - $response = $curl->getResponseBody(); - if ($this->options['resolve_xsd_includes'] === true) { + $response = $this->curl->getResponseBody(); + if ($this->resolveXsdIncludes === true) { $this->resolveXsdIncludes($response, $cacheFile, $wsdl); } else { file_put_contents($cacheFile, $response); diff --git a/tests/BeSimple/Tests/SoapClient/WsdlDownloaderTest.php b/tests/BeSimple/Tests/SoapClient/WsdlDownloaderTest.php index 326a668..f294a8b 100644 --- a/tests/BeSimple/Tests/SoapClient/WsdlDownloaderTest.php +++ b/tests/BeSimple/Tests/SoapClient/WsdlDownloaderTest.php @@ -13,6 +13,7 @@ namespace BeSimple\SoapClient; use BeSimple\SoapClient\WsdlDownloader; +use BeSimple\SoapClient\Curl; /** * @author Andreas Schamberger @@ -50,10 +51,8 @@ class WsdlDownloaderTest extends \PHPUnit_Framework_TestCase { $this->startPhpWebserver(); - $options = array( - 'resolve_xsd_includes' => true, - ); - $wd = new WsdlDownloader($options); + $curl = new Curl(); + $wd = new WsdlDownloader($curl); $cacheDir = ini_get('soap.wsdl_cache_dir'); if (!is_dir($cacheDir)) { @@ -92,7 +91,8 @@ class WsdlDownloaderTest extends \PHPUnit_Framework_TestCase public function testIsRemoteFile() { - $wd = new WsdlDownloader(); + $curl = new Curl(); + $wd = new WsdlDownloader($curl); $class = new \ReflectionClass($wd); $method = $class->getMethod('isRemoteFile'); @@ -119,10 +119,8 @@ class WsdlDownloaderTest extends \PHPUnit_Framework_TestCase { $this->startPhpWebserver(); - $options = array( - 'resolve_xsd_includes' => true, - ); - $wd = new WsdlDownloader($options); + $curl = new Curl(); + $wd = new WsdlDownloader($curl); $class = new \ReflectionClass($wd); $method = $class->getMethod('resolveXsdIncludes'); @@ -176,7 +174,8 @@ class WsdlDownloaderTest extends \PHPUnit_Framework_TestCase public function testResolveRelativePathInUrl() { - $wd = new WsdlDownloader(); + $curl = new Curl(); + $wd = new WsdlDownloader($curl); $class = new \ReflectionClass($wd); $method = $class->getMethod('resolveRelativePathInUrl'); From 7b1e2eef938bd169b00f555e1e4e703aabd4b2af Mon Sep 17 00:00:00 2001 From: Andreas Schamberger Date: Sun, 20 Nov 2011 16:11:39 +0100 Subject: [PATCH 35/63] removed orphan SoapRequest --- src/BeSimple/SoapClient/SoapRequest.php | 197 ------------------------ 1 file changed, 197 deletions(-) delete mode 100644 src/BeSimple/SoapClient/SoapRequest.php diff --git a/src/BeSimple/SoapClient/SoapRequest.php b/src/BeSimple/SoapClient/SoapRequest.php deleted file mode 100644 index 28f2926..0000000 --- a/src/BeSimple/SoapClient/SoapRequest.php +++ /dev/null @@ -1,197 +0,0 @@ - - * (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; - -/** - * @author Francis Besset - */ -class SoapRequest -{ - protected $function; - protected $arguments; - protected $options; - protected $headers; - - public function __construct($function = null, array $arguments = array(), array $options = array(), array $headers = array()) - { - $this->function = $function; - $this->arguments = $arguments; - $this->options = $options; - $this->setHeaders($headers); - } - - /** - * @return string The function name - */ - public function getFunction() - { - return $this->function; - } - - /** - * @param string The function name - * - * @return SoapRequest - */ - public function setFunction($function) - { - $this->function = $function; - - return $this; - } - - /** - * @return array An array with all arguments - */ - public function getArguments() - { - return $this->arguments; - } - - /** - * @param string The name of the argument - * @param mixed The default value returned if the argument is not exists - * - * @return mixed - */ - public function getArgument($name, $default = null) - { - return $this->hasArgument($name) ? $this->arguments[$name] : $default; - } - - /** - * @param string The name of the argument - * - * @return boolean - */ - public function hasArgument($name) - { - return isset($this->arguments[$name]); - } - - /** - * @param array An array with arguments - * - * @return SoapRequest - */ - public function setArguments(array $arguments) - { - $this->arguments = $arguments; - - return $this; - } - - /** - * @param string The name of argument - * @param mixed The value of argument - * - * @return SoapRequest - */ - public function addArgument($name, $value) - { - $this->arguments[$name] = $value; - - return $this; - } - - /** - * @return array An array with all options - */ - public function getOptions() - { - return $this->options; - } - - /** - * @param string The name of the option - * @param mixed The default value returned if the option is not exists - * - * @return mixed - */ - public function getOption($name, $default = null) - { - return $this->hasOption($name) ? $this->options[$name] : $default; - } - - /** - * @param string The name of the option - * - * @return boolean - */ - public function hasOption($name) - { - return isset($this->options[$name]); - } - - /** - * @param array An array with options - * - * @return SoapRequest - */ - public function setOptions(array $options) - { - $this->options = $options; - - return $this; - } - - /** - * @return array|null - */ - public function getHeaders() - { - return empty($this->headers) ? null : $this->headers; - } - - /** - * @param array $headers - * - * @return SoapRequest - */ - public function setHeaders(array $headers) - { - $this->headers = array(); - - foreach ($headers as $header) { - $this->addHeader($header); - } - - return $this; - } - - /** - * @param \SoapHeader $header - * - * @return SoapRequest - */ - public function addHeader(\SoapHeader $header) - { - $this->headers[] = $header; - - return $this; - } - - /** - * @param string The name of option - * @param mixed The value of option - * - * @return SoapRequest - */ - public function addOption($name, $value) - { - $this->options[$name] = $value; - - return $this; - } - -} From 7b4b832d61424a7959ef5b7962c3c2bcb1882c44 Mon Sep 17 00:00:00 2001 From: Andreas Schamberger Date: Sun, 20 Nov 2011 18:13:42 +0100 Subject: [PATCH 36/63] added wsdl:include to WsdlDownloader (and also the wsdl:import, xs:import) --- src/BeSimple/SoapClient/SoapClient.php | 14 ++--- src/BeSimple/SoapClient/WsdlDownloader.php | 46 ++++++++++----- .../SoapClient/Fixtures/wsdl_include.wsdl | 15 +++++ .../wsdlinclude/wsdlinctest_absolute.xml | 5 ++ .../wsdlinclude/wsdlinctest_relative.xml | 5 ++ .../Tests/SoapClient/WsdlDownloaderTest.php | 59 ++++++++++++++++++- 6 files changed, 121 insertions(+), 23 deletions(-) create mode 100644 tests/BeSimple/Tests/SoapClient/Fixtures/wsdl_include.wsdl create mode 100644 tests/BeSimple/Tests/SoapClient/Fixtures/wsdlinclude/wsdlinctest_absolute.xml create mode 100644 tests/BeSimple/Tests/SoapClient/Fixtures/wsdlinclude/wsdlinctest_relative.xml diff --git a/src/BeSimple/SoapClient/SoapClient.php b/src/BeSimple/SoapClient/SoapClient.php index 19d5be9..79d7e31 100644 --- a/src/BeSimple/SoapClient/SoapClient.php +++ b/src/BeSimple/SoapClient/SoapClient.php @@ -241,19 +241,17 @@ class SoapClient extends \SoapClient */ private function loadWsdl($wsdl, array $options) { - // option to resolve xsd includes - $resolveXsdIncludes = true; - if (isset($options['resolve_xsd_includes'])) - { - $resolveXsdIncludes = $options['resolve_xsd_includes']; + // option to resolve wsdl/xsd includes + $resolveRemoteIncludes = true; + if (isset($options['resolve_wsdl_remote_includes'])) { + $resolveRemoteIncludes = $options['resolve_wsdl_remote_includes']; } // option to enable cache $wsdlCache = WSDL_CACHE_DISK; - if (isset($options['cache_wsdl'])) - { + if (isset($options['cache_wsdl'])) { $wsdlCache = $options['cache_wsdl']; } - $wsdlDownloader = new WsdlDownloader($this->curl, $resolveXsdIncludes, $wsdlCache); + $wsdlDownloader = new WsdlDownloader($this->curl, $resolveRemoteIncludes, $wsdlCache); try { $cacheFileName = $wsdlDownloader->download($wsdl); } catch (\RuntimeException $e) { diff --git a/src/BeSimple/SoapClient/WsdlDownloader.php b/src/BeSimple/SoapClient/WsdlDownloader.php index 540c801..1894269 100644 --- a/src/BeSimple/SoapClient/WsdlDownloader.php +++ b/src/BeSimple/SoapClient/WsdlDownloader.php @@ -54,23 +54,23 @@ class WsdlDownloader private $curl; /** - * Resolve XSD includes. + * Resolve WSDl/XSD includes. * * @var boolean */ - protected $resolveXsdIncludes = true; + protected $resolveRemoteIncludes = true; /** * Constructor. * - * @param \BeSimple\SoapClient\Curl $curl Curl instance - * @param boolean $resolveXsdIncludes XSD include enabled? - * @param boolean $cacheWsdl Cache constant + * @param \BeSimple\SoapClient\Curl $curl Curl instance + * @param boolean $resolveRemoteIncludes WSDL/XSD include enabled? + * @param boolean $cacheWsdl Cache constant */ - public function __construct(Curl $curl, $resolveXsdIncludes = true, $cacheWsdl = WSDL_CACHE_DISK) + public function __construct(Curl $curl, $resolveRemoteIncludes = true, $cacheWsdl = WSDL_CACHE_DISK) { $this->curl = $curl; - $this->resolveXsdIncludes = $resolveXsdIncludes; + $this->resolveRemoteIncludes = $resolveRemoteIncludes; // get current WSDL caching config $this->cacheEnabled = (bool)ini_get('soap.wsdl_cache_enabled'); if ($this->cacheEnabled === true @@ -96,7 +96,7 @@ class WsdlDownloader // 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->resolveXsdIncludes === true) { + if ($isRemoteFile === true || $this->resolveRemoteIncludes === true) { $cacheFile = $this->cacheDir . DIRECTORY_SEPARATOR . 'wsdl_' . md5($wsdl) . '.cache'; if ($this->cacheEnabled === false || !file_exists($cacheFile) @@ -107,8 +107,8 @@ class WsdlDownloader // get content if ($responseSuccessfull === true) { $response = $this->curl->getResponseBody(); - if ($this->resolveXsdIncludes === true) { - $this->resolveXsdIncludes($response, $cacheFile, $wsdl); + if ($this->resolveRemoteIncludes === true) { + $this->resolveRemoteIncludes($response, $cacheFile, $wsdl); } else { file_put_contents($cacheFile, $response); } @@ -117,7 +117,7 @@ class WsdlDownloader } } elseif (file_exists($wsdl)) { $response = file_get_contents($wsdl); - $this->resolveXsdIncludes($response, $cacheFile); + $this->resolveRemoteIncludes($response, $cacheFile); } else { throw new \ErrorException("SOAP-ERROR: Parsing WSDL: Couldn't load from '" . $wsdl ."'"); } @@ -149,20 +149,38 @@ class WsdlDownloader } /** - * Resolves remote XSD includes within the WSDL files. + * Resolves remote WSDL/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) + private function resolveRemoteIncludes($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'; + $xpath->registerNamespace('wsdl', 'http://schemas.xmlsoap.org/wsdl/'); // TODO add to Helper + // WSDL include/import + $query = './/wsdl:include | .//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 (!is_null($parentFile)) { + $location = $this->resolveRelativePathInUrl($parentFile, $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) { diff --git a/tests/BeSimple/Tests/SoapClient/Fixtures/wsdl_include.wsdl b/tests/BeSimple/Tests/SoapClient/Fixtures/wsdl_include.wsdl new file mode 100644 index 0000000..775240a --- /dev/null +++ b/tests/BeSimple/Tests/SoapClient/Fixtures/wsdl_include.wsdl @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/tests/BeSimple/Tests/SoapClient/Fixtures/wsdlinclude/wsdlinctest_absolute.xml b/tests/BeSimple/Tests/SoapClient/Fixtures/wsdlinclude/wsdlinctest_absolute.xml new file mode 100644 index 0000000..dae033e --- /dev/null +++ b/tests/BeSimple/Tests/SoapClient/Fixtures/wsdlinclude/wsdlinctest_absolute.xml @@ -0,0 +1,5 @@ + + + wsdlincludetest + + diff --git a/tests/BeSimple/Tests/SoapClient/Fixtures/wsdlinclude/wsdlinctest_relative.xml b/tests/BeSimple/Tests/SoapClient/Fixtures/wsdlinclude/wsdlinctest_relative.xml new file mode 100644 index 0000000..8148e60 --- /dev/null +++ b/tests/BeSimple/Tests/SoapClient/Fixtures/wsdlinclude/wsdlinctest_relative.xml @@ -0,0 +1,5 @@ + + + wsdlincludetest + + diff --git a/tests/BeSimple/Tests/SoapClient/WsdlDownloaderTest.php b/tests/BeSimple/Tests/SoapClient/WsdlDownloaderTest.php index f294a8b..14ac58a 100644 --- a/tests/BeSimple/Tests/SoapClient/WsdlDownloaderTest.php +++ b/tests/BeSimple/Tests/SoapClient/WsdlDownloaderTest.php @@ -115,6 +115,63 @@ class WsdlDownloaderTest extends \PHPUnit_Framework_TestCase $this->assertFalse($method->invoke($wd, '../dir/test.html')); } + public function testResolveWsdlIncludes() + { + $this->startPhpWebserver(); + + $curl = new Curl(); + $wd = new WsdlDownloader($curl); + + $class = new \ReflectionClass($wd); + $method = $class->getMethod('resolveRemoteIncludes'); + $method->setAccessible(true); + + $cacheDir = ini_get('soap.wsdl_cache_dir'); + if (!is_dir($cacheDir)) { + $cacheDir = sys_get_temp_dir(); + $cacheDirForRegExp = preg_quote( $cacheDir ); + } + + $remoteUrlAbsolute = 'http://localhost:8000/wsdlinclude/wsdlinctest_absolute.xml'; + $remoteUrlRelative = 'http://localhost:8000/wsdlinclude/wsdlinctest_relative.xml'; + $tests = array( + 'localWithAbsolutePath' => array( + 'source' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/wsdlinclude/wsdlinctest_absolute.xml', + 'cacheFile' => $cacheDir.'/cache_local_absolute.xml', + 'remoteParentUrl' => null, + 'assertRegExp' => '~.*'.$cacheDirForRegExp.'\\\wsdl_.*\.cache.*~', + ), + 'localWithRelativePath' => array( + 'source' => __DIR__.DIRECTORY_SEPARATOR.'Fixtures/wsdlinclude/wsdlinctest_relative.xml', + 'cacheFile' => $cacheDir.'/cache_local_relative.xml', + 'remoteParentUrl' => null, + 'assertRegExp' => '~.*\.\./wsdl_include\.wsdl.*~', + ), + 'remoteWithAbsolutePath' => array( + 'source' => $remoteUrlAbsolute, + 'cacheFile' => $cacheDir.'/cache_remote_absolute.xml', + 'remoteParentUrl' => $remoteUrlAbsolute, + 'assertRegExp' => '~.*'.$cacheDirForRegExp.'\\\wsdl_.*\.cache.*~', + ), + 'remoteWithAbsolutePath' => array( + 'source' => $remoteUrlRelative, + 'cacheFile' => $cacheDir.'/cache_remote_relative.xml', + 'remoteParentUrl' => $remoteUrlRelative, + 'assertRegExp' => '~.*'.$cacheDirForRegExp.'\\\wsdl_.*\.cache.*~', + ), + ); + + foreach ($tests as $name => $values) { + $wsdl = file_get_contents( $values['source'] ); + $method->invoke($wd, $wsdl, $values['cacheFile'],$values['remoteParentUrl']); + $result = file_get_contents($values['cacheFile']); + $this->assertRegExp($values['assertRegExp'],$result,$name); + unlink($values['cacheFile']); + } + + $this->stopPhpWebserver(); + } + public function testResolveXsdIncludes() { $this->startPhpWebserver(); @@ -123,7 +180,7 @@ class WsdlDownloaderTest extends \PHPUnit_Framework_TestCase $wd = new WsdlDownloader($curl); $class = new \ReflectionClass($wd); - $method = $class->getMethod('resolveXsdIncludes'); + $method = $class->getMethod('resolveRemoteIncludes'); $method->setAccessible(true); $cacheDir = ini_get('soap.wsdl_cache_dir'); From 9643fd433336c8389038539245a81a2b0e52e521 Mon Sep 17 00:00:00 2001 From: Andreas Schamberger Date: Sun, 4 Dec 2011 14:24:22 +0100 Subject: [PATCH 37/63] remove obsolete class --- src/BeSimple/SoapClient/SimpleSoapClient.php | 182 ------------------- 1 file changed, 182 deletions(-) delete mode 100644 src/BeSimple/SoapClient/SimpleSoapClient.php diff --git a/src/BeSimple/SoapClient/SimpleSoapClient.php b/src/BeSimple/SoapClient/SimpleSoapClient.php deleted file mode 100644 index 62f115a..0000000 --- a/src/BeSimple/SoapClient/SimpleSoapClient.php +++ /dev/null @@ -1,182 +0,0 @@ - - * (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\Converter\TypeConverterCollection; - -/** - * @author Francis Besset - */ -class SimpleSoapClient -{ - protected $wsdl; - protected $converters; - protected $soapClient; - - /** - * @param string $wsdl - * @param array $options - */ - public function __construct($wsdl, TypeConverterCollection $converters = null, array $options = array()) - { - $this->wsdl = $wsdl; - $this->converters = $converters; - $this->setOptions($options); - } - - public function setOptions(array $options) - { - $this->options = array( - 'debug' => false, - 'cache_type' => null, - 'namespace' => null, - ); - - // check option names and live merge, if errors are encountered Exception will be thrown - $invalid = array(); - $isInvalid = false; - foreach ($options as $key => $value) { - if (array_key_exists($key, $this->options)) { - $this->options[$key] = $value; - } else { - $isInvalid = true; - $invalid[] = $key; - } - } - - if ($isInvalid) { - throw new \InvalidArgumentException(sprintf( - 'The "%s" class does not support the following options: "%s".', - __CLASS__, - implode('\', \'', $invalid) - )); - } - } - - /** - * @param string $name The name - * @param mixed $value The value - * - * @throws \InvalidArgumentException - */ - public function setOption($name, $value) - { - if (!array_key_exists($name, $this->options)) { - throw new \InvalidArgumentException(sprintf( - 'The "%s" class does not support the "%s" option.', - __CLASS__, - $name - )); - } - - $this->options[$name] = $value; - } - - public function getOptions() - { - return $this->options; - } - - /** - * @param string $key The key - * - * @return mixed The value - * - * @throws \InvalidArgumentException - */ - public function getOption($key) - { - if (!array_key_exists($key, $this->options)) { - throw new \InvalidArgumentException(sprintf( - 'The "%s" class does not support the "%s" option.', - __CLASS__, - $key - )); - } - - return $this->options[$key]; - } - - /** - * @param SoapRequest $soapRequest - * - * @return mixed - */ - public function send(SoapRequest $soapRequest) - { - return $this->getNativeSoapClient()->__soapCall( - $soapRequest->getFunction(), - $soapRequest->getArguments(), - $soapRequest->getOptions(), - $soapRequest->getHeaders() - ); - } - - /** - * @param string The SoapHeader name - * @param mixed The SoapHeader value - * - * @return \SoapHeader - */ - public function createSoapHeader($name, $value) - { - if (null === $namespace = $this->getOption('namespace')) { - throw new \RuntimeException('You cannot create SoapHeader if you do not specify a namespace.'); - } - - return new \SoapHeader($namespace, $name, $value); - } - - /** - * @return \SoapClient - */ - public function getNativeSoapClient() - { - if (!$this->soapClient) { - $this->soapClient = new \SoapClient($this->wsdl, $this->getSoapOptions()); - } - - return $this->soapClient; - } - - /** - * @return array The \SoapClient options - */ - public function getSoapOptions() - { - $options = array(); - - if (null === $this->options['cache_type']) { - $this->options['cache_type'] = Cache::getType(); - } - - $options['cache_wsdl'] = $this->options['cache_type']; - $options['trace'] = $this->options['debug']; - $options['typemap'] = $this->getTypemap(); - - return $options; - } - - /** - * @return array - */ - protected function getTypemap() - { - if (!$this->converters) { - return array(); - } - - return $this->converters->getTypemap(); - } -} \ No newline at end of file From 1122df8e127a12574ce315be9d7199a7eae61d1f Mon Sep 17 00:00:00 2001 From: Andreas Schamberger Date: Sun, 11 Dec 2011 21:20:35 +0100 Subject: [PATCH 38/63] WS-Adressing and WS-Security Filters --- src/BeSimple/SoapClient/Curl.php | 8 + src/BeSimple/SoapClient/FilterHelper.php | 172 ++++++ src/BeSimple/SoapClient/Helper.php | 194 ------- src/BeSimple/SoapClient/SoapClient.php | 167 ++++-- src/BeSimple/SoapClient/SoapRequest.php | 47 ++ src/BeSimple/SoapClient/SoapResponse.php | 46 ++ .../SoapClient/WsAddressingFilter.php | 320 ++++++++++ src/BeSimple/SoapClient/WsSecurityFilter.php | 549 ++++++++++++++++++ src/BeSimple/SoapClient/WsdlDownloader.php | 19 +- tests/bootstrap.php | 7 + vendors.php | 1 + 11 files changed, 1270 insertions(+), 260 deletions(-) create mode 100644 src/BeSimple/SoapClient/FilterHelper.php delete mode 100644 src/BeSimple/SoapClient/Helper.php create mode 100644 src/BeSimple/SoapClient/SoapRequest.php create mode 100644 src/BeSimple/SoapClient/SoapResponse.php create mode 100644 src/BeSimple/SoapClient/WsAddressingFilter.php create mode 100644 src/BeSimple/SoapClient/WsSecurityFilter.php diff --git a/src/BeSimple/SoapClient/Curl.php b/src/BeSimple/SoapClient/Curl.php index 38bc066..a8e508e 100644 --- a/src/BeSimple/SoapClient/Curl.php +++ b/src/BeSimple/SoapClient/Curl.php @@ -144,6 +144,7 @@ class Curl private function execManualRedirect($redirects = 0) { if ($redirects > $this->followLocationMaxRedirects) { + // TODO Redirection limit reached, aborting return false; } @@ -171,9 +172,11 @@ class Curl } $newUrl = $url['scheme'] . '://' . $url['host'] . $url['path'] . ($url['query'] ? '?' . $url['query'] : ''); curl_setopt($this->ch, CURLOPT_URL, $newUrl); + return $this->execManualRedirect($redirects++); } } + return $response; } @@ -225,8 +228,10 @@ class Curl $errorCodeMapping = $this->getErrorCodeMapping(); $errorNumber = curl_errno($this->ch); if (isset($errorCodeMapping[$errorNumber])) { + return $errorCodeMapping[$errorNumber]; } + return curl_error($this->ch); } @@ -258,6 +263,7 @@ class Curl public function getResponseBody() { $headerSize = curl_getinfo($this->ch, CURLINFO_HEADER_SIZE); + return substr($this->response, $headerSize); } @@ -279,6 +285,7 @@ class Curl public function getResponseHeaders() { $headerSize = curl_getinfo($this->ch, CURLINFO_HEADER_SIZE); + return substr($this->response, 0, $headerSize); } @@ -300,6 +307,7 @@ class Curl 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/FilterHelper.php b/src/BeSimple/SoapClient/FilterHelper.php new file mode 100644 index 0000000..f25e8bf --- /dev/null +++ b/src/BeSimple/SoapClient/FilterHelper.php @@ -0,0 +1,172 @@ + + * (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; + +/** + * Soap request/response filter helper for manipulating SOAP messages. + * + * @author Andreas Schamberger + */ +class FilterHelper +{ + /** + * DOMDocument on which the helper functions operate. + * + * @var \DOMDocument + */ + protected $domDocument = null; + + /** + * Namespaces added. + * + * @var array(string=>string) + */ + protected $namespaces = array(); + + /** + * Constructor. + * + * @param \DOMDocument $domDocument + */ + public function __construct(\DOMDocument $domDocument) + { + $this->domDocument = $domDocument; + } + + /** + * Add new soap header. + * + * @param \DOMElement $node + * @param boolean $mustUnderstand + * @param string $actor + * @param string $soapVersion + * @return void + */ + public function addHeaderElement(\DOMElement $node, $mustUnderstand = null, $actor = null, $soapVersion = SOAP_1_1) + { + $root = $this->domDocument->documentElement; + $namespace = $root->namespaceURI; + $prefix = $root->prefix; + if (null !== $mustUnderstand) { + $node->appendChild(new \DOMAttr($prefix . ':mustUnderstand', (int)$mustUnderstand)); + } + if (null !== $actor) { + $attributeName = ($soapVersion == SOAP_1_1) ? 'actor' : 'role'; + $node->appendChild(new \DOMAttr($prefix . ':' . $attributeName, $actor)); + } + $nodeListHeader = $root->getElementsByTagNameNS($namespace, 'Header'); + // add header if not there + if ($nodeListHeader->length == 0) { + // new header element + $header = $this->domDocument->createElementNS($namespace, $prefix . ':Header'); + // try to add it before body + $nodeListBody = $root->getElementsByTagNameNS($namespace, 'Body'); + if ($nodeListBody->length == 0) { + $root->appendChild($header); + } else { + $body = $nodeListBody->item(0); + $header = $body->parentNode->insertBefore($header, $body); + } + $header->appendChild($node); + } else { + $nodeListHeader->item(0)->appendChild($node); + } + } + + /** + * Add new soap body element. + * + * @param \DOMElement $node + * @return void + */ + public function addBodyElement(\DOMElement $node) + { + $root = $this->domDocument->documentElement; + $namespace = $root->namespaceURI; + $prefix = $root->prefix; + $nodeList = $this->domDocument->getElementsByTagNameNS($namespace, 'Body'); + // add body if not there + if ($nodeList->length == 0) { + // new body element + $body = $this->domDocument->createElementNS($namespace, $prefix . ':Body'); + $root->appendChild($body); + $body->appendChild($node); + } else { + $nodeList->item(0)->appendChild($node); + } + } + + /** + * Add new namespace to root tag. + * + * @param string $prefix + * @param string $namespaceURI + * @return void + */ + public function addNamespace($prefix, $namespaceURI) + { + if (!isset($this->namespaces[$namespaceURI])) { + $root = $this->domDocument->documentElement; + $root->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:' . $prefix, $namespaceURI); + $this->namespaces[$namespaceURI] = $prefix; + } + } + + /** + * Create new element for given namespace. + * + * @param string $namespaceURI + * @param string $name + * @param string $value + * @return \DOMElement + */ + public function createElement($namespaceURI, $name, $value = null) + { + $prefix = $this->namespaces[$namespaceURI]; + + return $this->domDocument->createElementNS($namespaceURI, $prefix . ':' . $name, $value); + } + + /** + * Add new attribute to element with given namespace. + * + * @param \DOMElement $element + * @param string $namespaceURI + * @param string $name + * @param string $value + * @return void + */ + public function setAttribute(\DOMElement $element, $namespaceURI = null, $name, $value) + { + if (null !== $namespaceURI) { + $prefix = $this->namespaces[$namespaceURI]; + $element->setAttributeNS($namespaceURI, $prefix . ':' . $name, $value); + } else { + $element->setAttribute($name, $value); + } + } + + /** + * Register namespace. + * + * @param string $prefix + * @param string $namespaceURI + * @return void + */ + public function registerNamespace($prefix, $namespaceURI) + { + if (!isset($this->namespaces[$namespaceURI])) { + $this->namespaces[$namespaceURI] = $prefix; + } + } +} \ No newline at end of file diff --git a/src/BeSimple/SoapClient/Helper.php b/src/BeSimple/SoapClient/Helper.php deleted file mode 100644 index e22afb9..0000000 --- a/src/BeSimple/SoapClient/Helper.php +++ /dev/null @@ -1,194 +0,0 @@ - - * (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; - -/** - * 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'; - - /** - * 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) - ); - } - - /** - * 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; - } - } -} \ No newline at end of file diff --git a/src/BeSimple/SoapClient/SoapClient.php b/src/BeSimple/SoapClient/SoapClient.php index 79d7e31..a6386ed 100644 --- a/src/BeSimple/SoapClient/SoapClient.php +++ b/src/BeSimple/SoapClient/SoapClient.php @@ -12,6 +12,8 @@ namespace BeSimple\SoapClient; +use BeSimple\SoapCommon\SoapKernel; + /** * Extended SoapClient that uses a a cURL wrapper for all underlying HTTP * requests in order to use proper authentication for all requests. This also @@ -71,6 +73,13 @@ class SoapClient extends \SoapClient */ private $lastResponse = ''; + /** + * Last response. + * + * @var \BeSimple\SoapCommon\SoapKernel + */ + protected $soapKernel = null; + /** * Constructor. * @@ -89,6 +98,8 @@ class SoapClient extends \SoapClient } $this->curl = new Curl($options); $wsdlFile = $this->loadWsdl($wsdl, $options); + // TODO $wsdlHandler = new WsdlHandler($wsdlFile, $this->soapVersion); + $this->soapKernel = new SoapKernel(); // we want the exceptions option to be set $options['exceptions'] = true; // disable obsolete trace option for native SoapClient as we need to do our own tracing anyways @@ -102,91 +113,88 @@ class SoapClient extends \SoapClient /** * Perform HTTP request with cURL. * - * @param string $request - * @param string $location - * @param string $action - * @return string + * @param SoapRequest $soapRequest + * @return SoapResponse */ - private function __doHttpRequest($request, $location, $action) + private function __doHttpRequest(SoapRequest $soapRequest) { - // $request is if unmodified from SoapClient not a php string type! - $request = (string)$request; - if ($this->soapVersion == 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 . '"'; - // execute request - $responseSuccessfull = $this->curl->exec($location, $request, $headers); + // HTTP headers + $headers = array( + 'Content-Type:' . $soapRequest->getContentType(), + 'SOAPAction: "' . $soapRequest->getAction() . '"', + ); + // execute HTTP request with cURL + $responseSuccessfull = $this->curl->exec($soapRequest->getLocation(), + $soapRequest->getContent(), + $headers); // tracing enabled: store last request header and body if ($this->tracingEnabled === true) { - $this->lastRequestHeaders = $curl->getRequestHeaders(); - $this->lastRequest = $request; + $this->lastRequestHeaders = $this->curl->getRequestHeaders(); + $this->lastRequest = $soapRequest->getContent(); } // in case of an error while making the http request throw a soapFault if ($responseSuccessfull === false) { // get error message from curl $faultstring = $this->curl->getErrorMessage(); - throw new \SoapFault('HTTP', $faultstring); + throw new \SoapFault( 'HTTP', $faultstring ); } // tracing enabled: store last response header and body if ($this->tracingEnabled === true) { $this->lastResponseHeaders = $this->curl->getResponseHeaders(); $this->lastResponse = $this->curl->getResponseBody(); } - $response = $this->curl->getResponseBody(); - // check if we do have a proper soap status code (if not soapfault) -// // TODO -// $responseStatusCode = $this->curl->getResponseStatusCode(); -// if ($responseStatusCode >= 400) { -// $isError = 0; -// $response = trim($response); -// if (strlen($response) == 0) { -// $isError = 1; -// } else { -// $contentType = $this->curl->getResponseContentType(); -// if ($contentType != 'application/soap+xml' -// && $contentType != 'application/soap+xml') { -// if (strncmp($response , "curl->getResponseStatusMessage()); -// } -// } 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'); -// } -// } - return $response; - } + // wrap response data in SoapResponse object + $soapResponse = SoapResponse::create($this->curl->getResponseBody(), + $soapRequest->getLocation(), + $soapRequest->getAction(), + $soapRequest->getVersion(), + $this->curl->getResponseContentType()); + + return $soapResponse; + } /** * Custom request method to be able to modify the SOAP messages. + * $oneWay parameter is not used at the moment. * * @param string $request * @param string $location * @param string $action * @param int $version - * @param int $one_way 0|1 + * @param int $oneWay 0|1 * @return string */ - public function __doRequest($request, $location, $action, $version, $one_way = 0) + public function __doRequest($request, $location, $action, $version, $oneWay = 0) { - // http request - $response = $this->__doHttpRequest($request, $location, $action); + // wrap request data in SoapRequest object + $soapRequest = SoapRequest::create($request, $location, $action, $version); + + // do actual SOAP request + $soapResponse = $this->__doRequest2($soapRequest); + // return SOAP response to ext/soap - return $response; + return $soapResponse->getContent(); + } + + /** + * Runs the currently registered request filters on the request, performs + * the HTTP request and runs the response filters. + * + * @param SoapRequest $soapRequest + * @return SoapResponse + */ + protected function __doRequest2(SoapRequest $soapRequest) + { + // run SoapKernel on SoapRequest + $soapRequest = $this->soapKernel->filterRequest($soapRequest); + + // perform HTTP request with cURL + $soapResponse = $this->__doHttpRequest($soapRequest); + + // run SoapKernel on SoapResponse + $soapResponse = $this->soapKernel->filterResponse($soapResponse); + + return $soapResponse; } /** @@ -229,6 +237,48 @@ class SoapClient extends \SoapClient return $this->lastResponse; } + /** + * Get SoapKernel instance. + * + * @return \BeSimple\SoapCommon\SoapKernel + */ + public function getSoapKernel() + { + return $this->soapKernel; + } + + // TODO finish + protected function isValidSoapResponse() + { + //check if we do have a proper soap status code (if not soapfault) + $responseStatusCode = $this->curl->getResponseStatusCode(); + $response = $this->curl->getResponseBody(); + if ($responseStatusCode >= 400) { + $isError = 0; + $response = trim($response); + if (strlen($response) == 0) { + $isError = 1; + } else { + $contentType = $this->curl->getResponseContentType(); + if ($contentType != 'application/soap+xml' + && $contentType != 'application/soap+xml') { + if (strncmp($response , "curl->getResponseStatusMessage()); + } + } 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'); + } + } + } + /** * Downloads WSDL files with cURL. Uses all SoapClient options for * authentication. Uses the WSDL_CACHE_* constants and the 'soap.wsdl_*' @@ -257,6 +307,7 @@ class SoapClient extends \SoapClient } 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/SoapRequest.php b/src/BeSimple/SoapClient/SoapRequest.php new file mode 100644 index 0000000..afe684f --- /dev/null +++ b/src/BeSimple/SoapClient/SoapRequest.php @@ -0,0 +1,47 @@ + + * (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\SoapRequest as CommonSoapRequest; +use BeSimple\SoapCommon\SoapMessage; + +/** + * SoapRequest class for SoapClient. Provides factory function for request object. + * + * @author Andreas Schamberger + */ +class SoapRequest extends CommonSoapRequest +{ + /** + * Factory function for SoapRequest. + * + * @param string $content + * @param string $location + * @param string $action + * @param string $version + * @return BeSimple\SoapClient\SoapRequest + */ + public static function create($content, $location, $action, $version) + { + $request = new SoapRequest(); + // $content is if unmodified from SoapClient not a php string type! + $request->setContent((string)$content); + $request->setLocation($location); + $request->setAction($action); + $request->setVersion($version); + $contentType = SoapMessage::getContentTypeForVersion($version); + $request->setContentType($contentType); + + return $request; + } +} \ No newline at end of file diff --git a/src/BeSimple/SoapClient/SoapResponse.php b/src/BeSimple/SoapClient/SoapResponse.php new file mode 100644 index 0000000..a9317da --- /dev/null +++ b/src/BeSimple/SoapClient/SoapResponse.php @@ -0,0 +1,46 @@ + + * (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\SoapResponse as CommonSoapResponse; +use BeSimple\SoapCommon\SoapMessage; + +/** + * SoapResponse class for SoapClient. Provides factory function for response object. + * + * @author Andreas Schamberger + */ +class SoapResponse extends CommonSoapResponse +{ + /** + * Factory function for SoapResponse. + * + * @param string $content + * @param string $location + * @param string $action + * @param string $version + * @param string $contentType + * @return BeSimple\SoapClient\SoapResponse + */ + public static function create($content, $location, $action, $version, $contentType) + { + $response = new SoapResponse(); + $response->setContent($content); + $response->setLocation($location); + $response->setAction($action); + $response->setVersion($version); + $response->setContentType($contentType); + + return $response; + } +} \ No newline at end of file diff --git a/src/BeSimple/SoapClient/WsAddressingFilter.php b/src/BeSimple/SoapClient/WsAddressingFilter.php new file mode 100644 index 0000000..67d94c0 --- /dev/null +++ b/src/BeSimple/SoapClient/WsAddressingFilter.php @@ -0,0 +1,320 @@ + + * (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\Helper; +use BeSimple\SoapCommon\SoapRequestFilter; +use BeSimple\SoapCommon\SoapResponseFilter; + +/** + * This plugin implements a subset of the following standards: + * * Web Services Addressing 1.0 - Core + * http://www.w3.org/TR/2006/REC-ws-addr-core + * * Web Services Addressing 1.0 - SOAP Binding + * http://www.w3.org/TR/ws-addr-soap + * + * Per default this plugin uses the SoapClient's $action and $location values + * for wsa:Action and wsa:To. Therefore the only REQUIRED property 'wsa:Action' + * is always set automatically. + * + * Limitation: wsa:From, wsa:FaultTo and wsa:ReplyTo only support the + * wsa:Address element of the endpoint reference at the moment. + * + * @author Andreas Schamberger + */ +class WsAddressingFilter implements SoapRequestFilter, SoapResponseFilter +{ + /** + * (2.1) Endpoint reference (EPR) anonymous default address. + * + * Some endpoints cannot be located with a meaningful IRI; this URI is used + * to allow such endpoints to send and receive messages. The precise meaning + * of this URI is defined by the binding of Addressing to a specific + * protocol and/or the context in which the EPR is used. + * + * @see http://www.w3.org/TR/2006/REC-ws-addr-core-20060509/#predefaddr + */ + const ENDPOINT_REFERENCE_ANONYMOUS = 'http://www.w3.org/2005/08/addressing/anonymous'; + + /** + * (2.1) Endpoint reference (EPR) address for discarting messages. + * + * Messages sent to EPRs whose [address] is this value MUST be discarded + * (i.e. not sent). This URI is typically used in EPRs that designate a + * reply or fault endpoint (see section 3.1 Abstract Property Definitions) + * to indicate that no reply or fault message should be sent. + * + * @see http://www.w3.org/TR/2006/REC-ws-addr-core-20060509/#predefaddr + */ + const ENDPOINT_REFERENCE_NONE = 'http://www.w3.org/2005/08/addressing/none'; + + /** + * (3.1) Predefined value for reply. + * + * Indicates that this is a reply to the message identified by the [message id] IRI. + * + * see http://www.w3.org/TR/2006/REC-ws-addr-core-20060509/#predefrels + */ + const RELATIONSHIP_TYPE_REPLY = 'http://www.w3.org/2005/08/addressing/reply'; + + /** + * FaultTo. + * + * @var string + */ + protected $faultTo; + + /** + * From. + * + * @var string + */ + protected $from; + + /** + * MessageId. + * + * @var string + */ + protected $messageId; + + /** + * List of reference parameters associated with this soap message. + * + * @var unknown_type + */ + protected $referenceParametersSet = array(); + + /** + * List of reference parameters recieved with this soap message. + * + * @var unknown_type + */ + protected $referenceParametersRecieved = array(); + + /** + * RelatesTo. + * + * @var string + */ + protected $relatesTo; + + /** + * RelatesTo@RelationshipType. + * + * @var string + */ + protected $relatesToRelationshipType; + + /** + * ReplyTo. + * + * @var string + */ + protected $replyTo; + + /** + * Add additional reference parameters + * + * @param string $ns + * @param string $pfx + * @param string $parameter + * @param string $value + * @return void + */ + public function addReferenceParameter($ns, $pfx, $parameter, $value) + { + $this->referenceParametersSet[] = array( + 'ns' => $ns, + 'pfx' => $pfx, + 'parameter' => $parameter, + 'value' => $value, + ); + } + + /** + * Get additional reference parameters. + * + * @param string $ns + * @param string $parameter + * @return string|null + */ + public function getReferenceParameter($ns, $parameter) + { + if (isset($this->referenceParametersRecieved[$ns][$parameter])) { + + return $this->referenceParametersRecieved[$ns][$parameter]; + } + + return null; + } + + /** + * Set FaultTo address of type xs:anyURI. + * + * @param string $action + * @return void + */ + public function setFaultTo($faultTo) + { + $this->faultTo = $faultTo; + } + + /** + * Set From address of type xs:anyURI. + * + * @param string $action + * @return void + */ + public function setFrom($from) + { + $this->from = $from; + } + + /** + * Set MessageId of type xs:anyURI. + * Default: UUID v4 e.g. 'uuid:550e8400-e29b-11d4-a716-446655440000' + * + * @param string $messageId + * @return void + */ + public function setMessageId($messageId = null) + { + if (null === $messageId) { + $messageId = 'uuid:' . Helper::generateUUID(); + } + $this->messageId = $messageId; + } + + /** + * Set RelatesTo of type xs:anyURI with the optional relationType + * (of type xs:anyURI). + * + * @param string $relatesTo + * @param string $relationType + * @return void + */ + public function setRelatesTo($relatesTo, $relationType = null) + { + $this->relatesTo = $relatesTo; + if (null !== $relationType && $relationType != self::RELATIONSHIP_TYPE_REPLY) { + $this->relatesToRelationshipType = $relationType; + } + } + + /** + * Set ReplyTo address of type xs:anyURI + * Default: self::ENDPOINT_REFERENCE_ANONYMOUS + * + * @param string $replyTo + * @return void + */ + public function setReplyTo($replyTo = null) + { + if (null === $replyTo) { + $replyTo = self::ENDPOINT_REFERENCE_ANONYMOUS; + } + $this->replyTo = $replyTo; + } + + /** + * Modify the given request XML. + * + * @param SoapRequest $dom + * @return void + */ + public function filterRequest(SoapRequest $request) + { + // get \DOMDocument from SOAP request + $dom = $request->getContentDocument(); + + // create FilterHelper + $filterHelper = new FilterHelper($dom); + + // add the neccessary namespaces + $filterHelper->addNamespace(Helper::PFX_WSA, Helper::NS_WSA); + + $action = $filterHelper->createElement(Helper::NS_WSA, 'Action', $request->getAction()); + $filterHelper->addHeaderElement($action); + + $to = $filterHelper->createElement(Helper::NS_WSA, 'To', $request->getLocation()); + $filterHelper->addHeaderElement($to); + + if (null !== $this->faultTo) { + $faultTo = $filterHelper->createElement(Helper::NS_WSA, 'FaultTo'); + $filterHelper->addHeaderElement($faultTo); + + $address = $filterHelper->createElement(Helper::NS_WSA, 'Address', $this->faultTo); + $faultTo->appendChild($address); + } + + if (null !== $this->from) { + $from = $filterHelper->createElement(Helper::NS_WSA, 'From'); + $filterHelper->addHeaderElement($from); + + $address = $filterHelper->createElement(Helper::NS_WSA, 'Address', $this->from); + $from->appendChild($address); + } + + if (null !== $this->messageId) { + $messageId = $filterHelper->createElement(Helper::NS_WSA, 'MessageID', $this->messageId); + $filterHelper->addHeaderElement($messageId); + } + + if (null !== $this->relatesTo) { + $relatesTo = $filterHelper->createElement(Helper::NS_WSA, 'RelatesTo', $this->relatesTo); + if (null !== $this->relatesToRelationshipType) { + $filterHelper->setAttribute($relatesTo, Helper::NS_WSA, 'RelationshipType', $this->relatesToRelationshipType); + } + $filterHelper->addHeaderElement($relatesTo); + } + + if (null !== $this->replyTo) { + $replyTo = $filterHelper->createElement(Helper::NS_WSA, 'ReplyTo'); + $filterHelper->addHeaderElement($replyTo); + + $address = $filterHelper->createElement(Helper::NS_WSA, 'Address', $this->replyTo); + $replyTo->appendChild($address); + } + + foreach ($this->referenceParametersSet as $rp) { + $filterHelper->addNamespace($rp['pfx'], $rp['ns']); + $parameter = $filterHelper->createElement($rp['ns'], $rp['parameter'], $rp['value']); + $filterHelper->setAttribute($parameter, Helper::NS_WSA, 'IsReferenceParameter', 'true'); + $filterHelper->addHeaderElement($parameter); + } + } + + /** + * Modify the given response XML. + * + * @param SoapResponse $response + * @return void + */ + public function filterResponse(SoapResponse $response) + { + // get \DOMDocument from SOAP response + $dom = $response->getContentDocument(); + + $this->referenceParametersRecieved = array(); + $referenceParameters = $dom->getElementsByTagNameNS(Helper::NS_WSA, 'ReferenceParameters')->item(0); + if (null !== $referenceParameters) { + foreach ($referenceParameters->childNodes as $childNode) { + if (!isset($this->referenceParametersRecieved[$childNode->namespaceURI])) { + $this->referenceParametersRecieved[$childNode->namespaceURI] = array(); + } + $this->referenceParametersRecieved[$childNode->namespaceURI][$childNode->localName] = $childNode->nodeValue; + } + } + } +} \ No newline at end of file diff --git a/src/BeSimple/SoapClient/WsSecurityFilter.php b/src/BeSimple/SoapClient/WsSecurityFilter.php new file mode 100644 index 0000000..8eb368c --- /dev/null +++ b/src/BeSimple/SoapClient/WsSecurityFilter.php @@ -0,0 +1,549 @@ + + * (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 ass\XmlSecurity\DSig as XmlSecurityDSig; +use ass\XmlSecurity\Enc as XmlSecurityEnc; +use ass\XmlSecurity\Key as XmlSecurityKey; + +use BeSimple\SoapCommon\Helper; +use BeSimple\SoapCommon\SoapRequestFilter; +use BeSimple\SoapCommon\SoapResponseFilter; +use BeSimple\SoapCommon\WsSecurityKey; + +/** + * This plugin implements a subset of the following standards: + * * Web Services Security: SOAP Message Security 1.0 (WS-Security 2004) + * http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0.pdf + * * Web Services Security UsernameToken Profile 1.0 + * http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0.pdf + * * Web Services Security X.509 Certificate Token Profile + * http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0.pdf + * + * @author Andreas Schamberger + */ +class WsSecurityFilter implements SoapRequestFilter, SoapResponseFilter +{ + /* + * The date format to be used with {@link \DateTime} + */ + const DATETIME_FORMAT = 'Y-m-d\TH:i:s.000\Z'; + + /** + * (UT 3.1) Password type: plain text. + */ + const PASSWORD_TYPE_TEXT = 0; + + /** + * (UT 3.1) Password type: digest. + */ + const PASSWORD_TYPE_DIGEST = 1; + + /** + * (X509 3.2.1) Reference to a Subject Key Identifier + */ + const TOKEN_REFERENCE_SUBJECT_KEY_IDENTIFIER = 0; + + /** + * (X509 3.2.1) Reference to a Security Token + */ + const TOKEN_REFERENCE_SECURITY_TOKEN = 1; + + /** + * (SMS_1.1 7.3) Key Identifiers + */ + const TOKEN_REFERENCE_THUMBPRINT_SHA1 = 2; + + /** + * Actor. + * + * @var string + */ + protected $actor; + + /** + * (SMS 10) Add security timestamp. + * + * @var boolean + */ + protected $addTimestamp; + + /** + * Encrypt the signature? + * + * @var boolean + */ + protected $encryptSignature; + + /** + * (SMS 10) Security timestamp expires time in seconds. + * + * @var int + */ + protected $expires; + + /** + * (UT 3.1) Password. + * + * @var string + */ + protected $password; + + /** + * (UT 3.1) Password type: text or digest. + * + * @var int + */ + protected $passwordType; + + /** + * Sign all headers. + * + * @var boolean + */ + protected $signAllHeaders; + + /** + * (X509 3.2) Token reference type for encryption. + * + * @var int + */ + protected $tokenReferenceEncryption = null; + + /** + * (X509 3.2) Token reference type for signature. + * + * @var int + */ + protected $tokenReferenceSignature = null; + + /** + * Service WsSecurityKey. + * + * @var \BeSimple\SoapCommon\WsSecurityKey + */ + protected $serviceSecurityKey; + + /** + * (UT 3.1) Username. + * + * @var string + */ + protected $username; + + /** + * User WsSecurityKey. + * + * @var \BeSimple\SoapCommon\WsSecurityKey + */ + protected $userSecurityKey; + + /** + * Constructor. + * + * @param boolean $addTimestamp (SMS 10) Add security timestamp. + * @param int $expires (SMS 10) Security timestamp expires time in seconds. + * @param string $actor + */ + public function __construct($addTimestamp = true, $expires = 300, $actor = null) + { + $this->addTimestamp = $addTimestamp; + $this->expires = $expires; + $this->actor = $actor; + } + + /** + * Add user data. + * + * @param string $username + * @param string $password + * @param int $passwordType self::PASSWORD_TYPE_DIGEST | self::PASSWORD_TYPE_TEXT + * @return void + */ + public function addUserData($username, $password = null, $passwordType = self::PASSWORD_TYPE_DIGEST) + { + $this->username = $username; + $this->password = $password; + $this->passwordType = $passwordType; + } + + /** + * Get service security key. + * + * @param \BeSimple\SoapCommon\WsSecurityKey $serviceSecurityKey + * @return void + */ + public function setServiceSecurityKeyObject(WsSecurityKey $serviceSecurityKey) + { + $this->serviceSecurityKey = $serviceSecurityKey; + } + + /** + * Get user security key. + * + * @param \BeSimple\SoapCommon\WsSecurityKey $userSecurityKey + * @return void + */ + public function setUserSecurityKeyObject(WsSecurityKey $userSecurityKey) + { + $this->userSecurityKey = $userSecurityKey; + } + + /** + * Set security options. + * + * @param int $tokenReference self::TOKEN_REFERENCE_SUBJECT_KEY_IDENTIFIER | self::TOKEN_REFERENCE_SECURITY_TOKEN | self::TOKEN_REFERENCE_THUMBPRINT_SHA1 + * @param boolean $encryptSignature + * @return void + */ + public function setSecurityOptionsEncryption($tokenReference, $encryptSignature = false) + { + $this->tokenReferenceEncryption = $tokenReference; + $this->encryptSignature = $encryptSignature; + } + + /** + * Set security options. + * + * @param int $tokenReference self::TOKEN_REFERENCE_SUBJECT_KEY_IDENTIFIER | self::TOKEN_REFERENCE_SECURITY_TOKEN | self::TOKEN_REFERENCE_THUMBPRINT_SHA1 + * @param boolean $signAllHeaders + * @return void + */ + public function setSecurityOptionsSignature($tokenReference, $signAllHeaders = false) + { + $this->tokenReferenceSignature = $tokenReference; + $this->signAllHeaders = $signAllHeaders; + } + + /** + * Modify the given request XML. + * + * @param SoapRequest $dom + * @return void + */ + public function filterRequest(SoapRequest $request) + { + // get \DOMDocument from SOAP request + $dom = $request->getContentDocument(); + + // create FilterHelper + $filterHelper = new FilterHelper($dom); + + // add the neccessary namespaces + $filterHelper->addNamespace(Helper::PFX_WSS, Helper::NS_WSS); + $filterHelper->addNamespace(Helper::PFX_WSU, Helper::NS_WSU); + $filterHelper->registerNamespace(XmlSecurityDSig::PFX_XMLDSIG, XmlSecurityDSig::NS_XMLDSIG); + + // init timestamp + $dt = new \DateTime('now', new \DateTimeZone('UTC')); + $createdTimestamp = $dt->format(self::DATETIME_FORMAT); + + // create security header + $security = $filterHelper->createElement(Helper::NS_WSS, 'Security'); + $filterHelper->addHeaderElement($security, true, $this->actor, $request->getSoapVersion()); + + if (true === $this->addTimestamp || null !== $this->expires) { + $timestamp = $filterHelper->createElement(Helper::NS_WSU, 'Timestamp'); + $created = $filterHelper->createElement(Helper::NS_WSU, 'Created', $createdTimestamp); + $timestamp->appendChild($created); + if (null !== $this->expires) { + $dt->modify('+' . $this->expires . ' seconds'); + $expiresTimestamp = $dt->format(self::DATETIME_FORMAT); + $expires = $filterHelper->createElement(Helper::NS_WSU, 'Expires', $expiresTimestamp); + $timestamp->appendChild($expires); + } + $security->appendChild($timestamp); + } + + if (null !== $this->username) { + $usernameToken = $filterHelper->createElement(Helper::NS_WSS, 'UsernameToken'); + $security->appendChild($usernameToken); + + $username = $filterHelper->createElement(Helper::NS_WSS, 'Username', $this->username); + $usernameToken->appendChild($username); + + if (null !== $this->password + && (null === $this->userSecurityKey + || (null !== $this->userSecurityKey && !$this->userSecurityKey->hasPrivateKey()))) { + + if (self::PASSWORD_TYPE_DIGEST === $this->passwordType) { + $nonce = mt_rand(); + $password = base64_encode(sha1($nonce . $createdTimestamp . $this->password , true)); + $passwordType = Helper::NAME_WSS_UTP . '#PasswordDigest'; + } else { + $password = $this->password; + $passwordType = Helper::NAME_WSS_UTP . '#PasswordText'; + } + $password = $filterHelper->createElement(Helper::NS_WSS, 'Password', $password); + $filterHelper->setAttribute($password, null, 'Type', $passwordType); + $usernameToken->appendChild($password); + if (self::PASSWORD_TYPE_DIGEST === $this->passwordType) { + $nonce = $filterHelper->createElement(Helper::NS_WSS, 'Nonce', base64_encode($nonce)); + $usernameToken->appendChild($nonce); + + $created = $filterHelper->createElement(Helper::NS_WSU, 'Created', $createdTimestamp); + $usernameToken->appendChild($created); + } + } + } + + if (null !== $this->userSecurityKey && $this->userSecurityKey->hasKeys()) { + $guid = 'CertId-' . Helper::generateUUID(); + // add token references + $keyInfo = null; + if (null !== $this->tokenReferenceSignature) { + $keyInfo = $this->createKeyInfo($filterHelper, $this->tokenReferenceSignature, $guid, $this->userSecurityKey->getPublicKey()); + } + $nodes = $this->createNodeListForSigning($dom, $security); + $signature = XmlSecurityDSig::createSignature($this->userSecurityKey->getPrivateKey(), XmlSecurityDSig::EXC_C14N, $security, null, $keyInfo); + $options = array( + 'id_ns_prefix' => Helper::PFX_WSU, + 'id_prefix_ns' => Helper::NS_WSU, + ); + foreach ($nodes as $node) { + XmlSecurityDSig::addNodeToSignature($signature, $node, XmlSecurityDSig::SHA1, XmlSecurityDSig::EXC_C14N, $options); + } + XmlSecurityDSig::signDocument($signature, $this->userSecurityKey->getPrivateKey(), XmlSecurityDSig::EXC_C14N); + + $publicCertificate = $this->userSecurityKey->getPublicKey()->getX509Certificate(true); + $binarySecurityToken = $filterHelper->createElement(Helper::NS_WSS, 'BinarySecurityToken', $publicCertificate); + $filterHelper->setAttribute($binarySecurityToken, null, 'EncodingType', Helper::NAME_WSS_SMS . '#Base64Binary'); + $filterHelper->setAttribute($binarySecurityToken, null, 'ValueType', Helper::NAME_WSS_X509 . '#X509v3'); + $filterHelper->setAttribute($binarySecurityToken, Helper::NS_WSU, 'Id', $guid); + $security->insertBefore($binarySecurityToken, $signature); + + // encrypt soap document + if (null !== $this->serviceSecurityKey && $this->serviceSecurityKey->hasKeys()) { + $guid = 'EncKey-' . Helper::generateUUID(); + // add token references + $keyInfo = null; + if (null !== $this->tokenReferenceEncryption) { + $keyInfo = $this->createKeyInfo($filterHelper, $this->tokenReferenceEncryption, $guid, $this->serviceSecurityKey->getPublicKey()); + } + $encryptedKey = XmlSecurityEnc::createEncryptedKey($guid, $this->serviceSecurityKey->getPrivateKey(), $this->serviceSecurityKey->getPublicKey(), $security, $signature, $keyInfo); + $referenceList = XmlSecurityEnc::createReferenceList($encryptedKey); + // token reference to encrypted key + $keyInfo = $this->createKeyInfo($filterHelper, self::TOKEN_REFERENCE_SECURITY_TOKEN, $guid); + $nodes = $this->createNodeListForEncryption($dom, $security); + foreach ($nodes as $node) { + $type = XmlSecurityEnc::ELEMENT; + if ($node->localName == 'Body') { + $type = XmlSecurityEnc::CONTENT; + } + XmlSecurityEnc::encryptNode($node, $type, $this->serviceSecurityKey->getPrivateKey(), $referenceList, $keyInfo); + } + } + } + } + + /** + * Modify the given request XML. + * + * @param SoapResponse $response + * @return void + */ + public function filterResponse(SoapResponse $response) + { + // get \DOMDocument from SOAP response + $dom = $response->getContentDocument(); + + // locate security header + $security = $dom->getElementsByTagNameNS(Helper::NS_WSS, 'Security')->item(0); + if (null !== $security) { + // add SecurityTokenReference resolver for KeyInfo + if (null !== $this->serviceSecurityKey) { + $keyResolver = array($this, 'keyInfoSecurityTokenReferenceResolver'); + XmlSecurityDSig::addKeyInfoResolver(Helper::NS_WSS, 'SecurityTokenReference', $keyResolver); + } + // do we have a reference list in header + $referenceList = XmlSecurityEnc::locateReferenceList($security); + // get a list of encrypted nodes + $encryptedNodes = XmlSecurityEnc::locateEncryptedData($dom, $referenceList); + // decrypt them + if (null !== $encryptedNodes) { + foreach ($encryptedNodes as $encryptedNode) { + XmlSecurityEnc::decryptNode($encryptedNode); + } + } + // locate signature node + $signature = XmlSecurityDSig::locateSignature($security); + if (null !== $signature) { + // verify references + $options = array( + 'id_ns_prefix' => Helper::PFX_WSU, + 'id_prefix_ns' => Helper::NS_WSU, + ); + if (XmlSecurityDSig::verifyReferences($signature, $options) !== true) { + throw new \SoapFault('wsse:FailedCheck', 'The signature or decryption was invalid'); + } + // verify signature + if (XmlSecurityDSig::verifyDocumentSignature($signature) !== true) { + throw new \SoapFault('wsse:FailedCheck', 'The signature or decryption was invalid'); + } + } + } + } + + /** + * Adds the configured KeyInfo to the parentNode. + * + * @param FilterHelper $filterHelper + * @param int $tokenReference + * @param \DOMNode $parentNode + * @param string $guid + * @param \ass\XmlSecurity\Key $xmlSecurityKey + * @param \DOMNode $insertBefore + * @return \DOMElement + */ + protected function createKeyInfo(FilterHelper $filterHelper, $tokenReference, $guid, XmlSecurityKey $xmlSecurityKey = null) + { + $keyInfo = $filterHelper->createElement(XmlSecurityDSig::NS_XMLDSIG, 'KeyInfo'); + $securityTokenReference = $filterHelper->createElement(Helper::NS_WSS, 'SecurityTokenReference'); + $keyInfo->appendChild($securityTokenReference); + // security token + if (self::TOKEN_REFERENCE_SECURITY_TOKEN === $tokenReference) { + $reference = $filterHelper->createElement(Helper::NS_WSS, 'Reference'); + $filterHelper->setAttribute($reference, null, 'URI', '#' . $guid); + if (null !== $xmlSecurityKey) { + $filterHelper->setAttribute($reference, null, 'ValueType', Helper::NAME_WSS_X509 . '#X509v3'); + } + $securityTokenReference->appendChild($reference); + // subject key identifier + } elseif (self::TOKEN_REFERENCE_SUBJECT_KEY_IDENTIFIER === $tokenReference && null !== $xmlSecurityKey) { + $keyIdentifier = $filterHelper->createElement(Helper::NS_WSS, 'KeyIdentifier'); + $filterHelper->setAttribute($keyIdentifier, null, 'EncodingType', Helper::NAME_WSS_SMS . '#Base64Binary'); + $filterHelper->setAttribute($keyIdentifier, null, 'ValueType', Helper::NAME_WSS_X509 . '#509SubjectKeyIdentifier'); + $securityTokenReference->appendChild($keyIdentifier); + $certificate = $xmlSecurityKey->getX509SubjectKeyIdentifier(); + $dataNode = new \DOMText($certificate); + $keyIdentifier->appendChild($dataNode); + // thumbprint sha1 + } elseif (self::TOKEN_REFERENCE_THUMBPRINT_SHA1 === $tokenReference && null !== $xmlSecurityKey) { + $keyIdentifier = $filterHelper->createElement(Helper::NS_WSS, 'KeyIdentifier'); + $filterHelper->setAttribute($keyIdentifier, null, 'EncodingType', Helper::NAME_WSS_SMS . '#Base64Binary'); + $filterHelper->setAttribute($keyIdentifier, null, 'ValueType', Helper::NAME_WSS_SMS_1_1 . '#ThumbprintSHA1'); + $securityTokenReference->appendChild($keyIdentifier); + $thumbprintSha1 = base64_encode(sha1(base64_decode($xmlSecurityKey->getX509Certificate(true)), true)); + $dataNode = new \DOMText($thumbprintSha1); + $keyIdentifier->appendChild($dataNode); + } + + return $keyInfo; + } + + /** + * Create a list of \DOMNodes that should be encrypted. + * + * @param \DOMDocument $dom + * @param \DOMElement $security + * @return \DOMNodeList + */ + protected function createNodeListForEncryption(\DOMDocument $dom, \DOMElement $security) + { + $xpath = new \DOMXPath($dom); + $xpath->registerNamespace('SOAP-ENV', $dom->documentElement->namespaceURI); + $xpath->registerNamespace('ds', XmlSecurityDSig::NS_XMLDSIG); + if ($this->encryptSignature === true) { + $query = '//ds:Signature | //SOAP-ENV:Body'; + } else { + $query = '//SOAP-ENV:Body'; + } + + return $xpath->query($query); + } + + /** + * Create a list of \DOMNodes that should be signed. + * + * @param \DOMDocument $dom + * @param \DOMElement $security + * @return array(\DOMNode) + */ + protected function createNodeListForSigning(\DOMDocument $dom, \DOMElement $security) + { + $nodes = array(); + $body = $dom->getElementsByTagNameNS($dom->documentElement->namespaceURI, 'Body')->item(0); + if (null !== $body) { + $nodes[] = $body; + } + foreach ($security->childNodes as $node) { + if (XML_ELEMENT_NODE === $node->nodeType) { + $nodes[] = $node; + } + } + if ($this->signAllHeaders) { + foreach ($security->parentNode->childNodes as $node) { + if (XML_ELEMENT_NODE === $node->nodeType && + Helper::NS_WSS !== $node->namespaceURI) { + $nodes[] = $node; + } + } + } + + return $nodes; + } + + /** + * Gets the referenced node for the given URI. + * + * @param \DOMElement $node + * @param string $uri + * @return \DOMElement + */ + protected function getReferenceNodeForUri(\DOMElement $node, $uri) + { + $url = parse_url($uri); + $referenceId = $url['fragment']; + $query = '//*[@'.Helper::PFX_WSU.':Id="'.$referenceId.'" or @Id="'.$referenceId.'"]'; + $xpath = new \DOMXPath($node->ownerDocument); + $xpath->registerNamespace(Helper::PFX_WSU, Helper::NS_WSU); + + return $xpath->query($query)->item(0); + } + + /** + * Tries to resolve a key from the given \DOMElement. + * + * @param \DOMElement $node + * @param string $algorithm + * @return \ass\XmlSecurity\Key|null + */ + public function keyInfoSecurityTokenReferenceResolver(\DOMElement $node, $algorithm) + { + foreach ($node->childNodes as $key) { + if (Helper::NS_WSS === $key->namespaceURI) { + switch ($key->localName) { + case 'KeyIdentifier': + + return $this->serviceSecurityKey->getPublicKey(); + case 'Reference': + $uri = $key->getAttribute('URI'); + $referencedNode = $this->getReferenceNodeForUri($node, $uri); + + if (XmlSecurityEnc::NS_XMLENC === $referencedNode->namespaceURI + && 'EncryptedKey' == $referencedNode->localName) { + $key = XmlSecurityEnc::decryptEncryptedKey($referencedNode, $this->userSecurityKey->getPrivateKey()); + + return XmlSecurityKey::factory($algorithm, $key, XmlSecurityKey::TYPE_PRIVATE); + } else { + //$valueType = $key->getAttribute('ValueType'); + + return $this->serviceSecurityKey->getPublicKey(); + } + } + } + } + + return null; + } +} \ No newline at end of file diff --git a/src/BeSimple/SoapClient/WsdlDownloader.php b/src/BeSimple/SoapClient/WsdlDownloader.php index 1894269..f3e1ead 100644 --- a/src/BeSimple/SoapClient/WsdlDownloader.php +++ b/src/BeSimple/SoapClient/WsdlDownloader.php @@ -12,8 +12,7 @@ namespace BeSimple\SoapClient; -// TODO -//use BeSimple\SoapCommon\Helper; +use BeSimple\SoapCommon\Helper; /** * Downloads WSDL files with cURL. Uses the WSDL_CACHE_* constants and the @@ -30,28 +29,28 @@ class WsdlDownloader * * @var bool */ - private $cacheEnabled; + protected $cacheEnabled; /** * Cache dir. * * @var string */ - private $cacheDir; + protected $cacheDir; /** * Cache TTL. * * @var int */ - private $cacheTtl; + protected $cacheTtl; /** * cURL instance for downloads. * * @var unknown_type */ - private $curl; + protected $curl; /** * Resolve WSDl/XSD includes. @@ -122,8 +121,10 @@ class WsdlDownloader 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 ."'"); @@ -145,6 +146,7 @@ class WsdlDownloader $isRemoteFile = true; } } + return $isRemoteFile; } @@ -154,7 +156,7 @@ class WsdlDownloader * @param string $xml * @param string $cacheFile * @param unknown_type $parentIsRemote - * @return string + * @return void */ private function resolveRemoteIncludes($xml, $cacheFile, $parentFile = null) { @@ -164,7 +166,7 @@ class WsdlDownloader $xpath->registerNamespace(Helper::PFX_XML_SCHEMA, Helper::NS_XML_SCHEMA); $xpath->registerNamespace('wsdl', 'http://schemas.xmlsoap.org/wsdl/'); // TODO add to Helper // WSDL include/import - $query = './/wsdl:include | .//wsdl:import'; + $query = './/wsdl:include | .//wsdl:import'; // TODO $nodes = $xpath->query($query); if ($nodes->length > 0) { foreach ($nodes as $node) { @@ -247,6 +249,7 @@ class WsdlDownloader if (isset($urlParts['port'])) { $hostname .= ':' . $urlParts['port']; } + return $hostname . implode('/', $parts); } } \ No newline at end of file diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 2e0638c..d2872c6 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -16,6 +16,13 @@ spl_autoload_register(function($class) { if (file_exists($path) && is_readable($path)) { require_once $path; + return true; + } + } elseif (0 === strpos($class, 'ass\XmlSecurity\\')) { + $path = __DIR__.'/../vendor/XmlSecurity/src/'.strtr($class, '\\', '/').'.php'; + if (file_exists($path) && is_readable($path)) { + require_once $path; + return true; } } elseif (0 === strpos($class, 'BeSimple\SoapCommon\\')) { diff --git a/vendors.php b/vendors.php index cf763ae..a0e0703 100644 --- a/vendors.php +++ b/vendors.php @@ -25,6 +25,7 @@ if (!is_dir($vendorDir = dirname(__FILE__).'/vendor')) { $deps = array( array('besimple-soapcommon', 'http://github.com/BeSimple/BeSimpleSoapCommon.git', 'origin/HEAD'), + array('XmlSecurity', 'https://github.com/aschamberger/XmlSecurity.git', 'origin/HEAD'), ); foreach ($deps as $dep) { From 8a886c7eda9e2f308751f4fa705e2ba0011d0e39 Mon Sep 17 00:00:00 2001 From: Andreas Schamberger Date: Sat, 17 Dec 2011 11:10:08 +0100 Subject: [PATCH 39/63] use Helper constants --- src/BeSimple/SoapClient/WsdlDownloader.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/BeSimple/SoapClient/WsdlDownloader.php b/src/BeSimple/SoapClient/WsdlDownloader.php index f3e1ead..5dda4f9 100644 --- a/src/BeSimple/SoapClient/WsdlDownloader.php +++ b/src/BeSimple/SoapClient/WsdlDownloader.php @@ -164,9 +164,9 @@ class WsdlDownloader $doc->loadXML($xml); $xpath = new \DOMXPath($doc); $xpath->registerNamespace(Helper::PFX_XML_SCHEMA, Helper::NS_XML_SCHEMA); - $xpath->registerNamespace('wsdl', 'http://schemas.xmlsoap.org/wsdl/'); // TODO add to Helper + $xpath->registerNamespace(Helper::PFX_WSDL, Helper::NS_WSDL); // WSDL include/import - $query = './/wsdl:include | .//wsdl:import'; // TODO + $query = './/' . Helper::PFX_WSDL . ':include | .//' . Helper::PFX_WSDL . ':import'; $nodes = $xpath->query($query); if ($nodes->length > 0) { foreach ($nodes as $node) { From 2e155db8f9e2885751b90fd25e854f59ce22b112 Mon Sep 17 00:00:00 2001 From: Andreas Schamberger Date: Sat, 17 Dec 2011 16:05:25 +0100 Subject: [PATCH 40/63] CS fixes --- src/BeSimple/SoapClient/Curl.php | 18 ++--- src/BeSimple/SoapClient/FilterHelper.php | 44 +++++++------ src/BeSimple/SoapClient/SoapClient.php | 34 ++++++---- src/BeSimple/SoapClient/SoapRequest.php | 11 ++-- src/BeSimple/SoapClient/SoapResponse.php | 11 ++-- .../SoapClient/WsAddressingFilter.php | 37 +++++++---- src/BeSimple/SoapClient/WsSecurityFilter.php | 66 +++++++++++-------- src/BeSimple/SoapClient/WsdlDownloader.php | 22 ++++--- 8 files changed, 140 insertions(+), 103 deletions(-) diff --git a/src/BeSimple/SoapClient/Curl.php b/src/BeSimple/SoapClient/Curl.php index a8e508e..ae57933 100644 --- a/src/BeSimple/SoapClient/Curl.php +++ b/src/BeSimple/SoapClient/Curl.php @@ -50,8 +50,8 @@ class Curl /** * Constructor. * - * @param array $options - * @param int $followLocationMaxRedirects + * @param array $options Options array from SoapClient constructor + * @param int $followLocationMaxRedirects Redirection limit for Location header */ public function __construct(array $options = array(), $followLocationMaxRedirects = 10) { @@ -109,9 +109,10 @@ class Curl * Execute HTTP request. * Returns true if request was successfull. * - * @param string $location - * @param string $request - * @param array $requestHeaders + * @param string $location HTTP location + * @param string $request Request body + * @param array $requestHeaders Request header strings + * * @return bool */ public function exec($location, $request = null, $requestHeaders = array()) @@ -136,9 +137,8 @@ class Curl * 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 + * @param int $redirects Current redirection count + * * @return mixed */ private function execManualRedirect($redirects = 0) @@ -186,7 +186,7 @@ class Curl * * http://curl.haxx.se/libcurl/c/libcurl-errors.html * - * @var array(int=>string) + * @return array(int=>string) */ protected function getErrorCodeMapping() { diff --git a/src/BeSimple/SoapClient/FilterHelper.php b/src/BeSimple/SoapClient/FilterHelper.php index f25e8bf..cb21c01 100644 --- a/src/BeSimple/SoapClient/FilterHelper.php +++ b/src/BeSimple/SoapClient/FilterHelper.php @@ -36,7 +36,7 @@ class FilterHelper /** * Constructor. * - * @param \DOMDocument $domDocument + * @param \DOMDocument $domDocument SOAP document */ public function __construct(\DOMDocument $domDocument) { @@ -46,10 +46,11 @@ class FilterHelper /** * Add new soap header. * - * @param \DOMElement $node - * @param boolean $mustUnderstand - * @param string $actor - * @param string $soapVersion + * @param \DOMElement $node DOMElement to add + * @param boolean $mustUnderstand SOAP header mustUnderstand attribute + * @param string $actor SOAP actor/role + * @param string $soapVersion SOAP version SOAP_1_1|SOAP_1_2 + * * @return void */ public function addHeaderElement(\DOMElement $node, $mustUnderstand = null, $actor = null, $soapVersion = SOAP_1_1) @@ -58,7 +59,7 @@ class FilterHelper $namespace = $root->namespaceURI; $prefix = $root->prefix; if (null !== $mustUnderstand) { - $node->appendChild(new \DOMAttr($prefix . ':mustUnderstand', (int)$mustUnderstand)); + $node->appendChild(new \DOMAttr($prefix . ':mustUnderstand', (int) $mustUnderstand)); } if (null !== $actor) { $attributeName = ($soapVersion == SOAP_1_1) ? 'actor' : 'role'; @@ -86,7 +87,8 @@ class FilterHelper /** * Add new soap body element. * - * @param \DOMElement $node + * @param \DOMElement $node DOMElement to add + * * @return void */ public function addBodyElement(\DOMElement $node) @@ -109,8 +111,9 @@ class FilterHelper /** * Add new namespace to root tag. * - * @param string $prefix - * @param string $namespaceURI + * @param string $prefix Namespace prefix + * @param string $namespaceURI Namespace URI + * * @return void */ public function addNamespace($prefix, $namespaceURI) @@ -125,9 +128,10 @@ class FilterHelper /** * Create new element for given namespace. * - * @param string $namespaceURI - * @param string $name - * @param string $value + * @param string $namespaceURI Namespace URI + * @param string $name Element name + * @param string $value Element value + * * @return \DOMElement */ public function createElement($namespaceURI, $name, $value = null) @@ -140,13 +144,14 @@ class FilterHelper /** * Add new attribute to element with given namespace. * - * @param \DOMElement $element - * @param string $namespaceURI - * @param string $name - * @param string $value + * @param \DOMElement $element DOMElement to edit + * @param string $namespaceURI Namespace URI + * @param string $name Attribute name + * @param string $value Attribute value + * * @return void */ - public function setAttribute(\DOMElement $element, $namespaceURI = null, $name, $value) + public function setAttribute(\DOMElement $element, $namespaceURI, $name, $value) { if (null !== $namespaceURI) { $prefix = $this->namespaces[$namespaceURI]; @@ -159,8 +164,9 @@ class FilterHelper /** * Register namespace. * - * @param string $prefix - * @param string $namespaceURI + * @param string $prefix Namespace prefix + * @param string $namespaceURI Namespace URI + * * @return void */ public function registerNamespace($prefix, $namespaceURI) diff --git a/src/BeSimple/SoapClient/SoapClient.php b/src/BeSimple/SoapClient/SoapClient.php index a6386ed..b130c7b 100644 --- a/src/BeSimple/SoapClient/SoapClient.php +++ b/src/BeSimple/SoapClient/SoapClient.php @@ -113,7 +113,8 @@ class SoapClient extends \SoapClient /** * Perform HTTP request with cURL. * - * @param SoapRequest $soapRequest + * @param SoapRequest $soapRequest SoapRequest object + * * @return SoapResponse */ private function __doHttpRequest(SoapRequest $soapRequest) @@ -126,7 +127,7 @@ class SoapClient extends \SoapClient // execute HTTP request with cURL $responseSuccessfull = $this->curl->exec($soapRequest->getLocation(), $soapRequest->getContent(), - $headers); + $headers); // tracing enabled: store last request header and body if ($this->tracingEnabled === true) { $this->lastRequestHeaders = $this->curl->getRequestHeaders(); @@ -136,7 +137,7 @@ class SoapClient extends \SoapClient if ($responseSuccessfull === false) { // get error message from curl $faultstring = $this->curl->getErrorMessage(); - throw new \SoapFault( 'HTTP', $faultstring ); + throw new \SoapFault('HTTP', $faultstring); } // tracing enabled: store last response header and body if ($this->tracingEnabled === true) { @@ -144,24 +145,27 @@ class SoapClient extends \SoapClient $this->lastResponse = $this->curl->getResponseBody(); } // wrap response data in SoapResponse object - $soapResponse = SoapResponse::create($this->curl->getResponseBody(), + $soapResponse = SoapResponse::create( + $this->curl->getResponseBody(), $soapRequest->getLocation(), $soapRequest->getAction(), $soapRequest->getVersion(), - $this->curl->getResponseContentType()); + $this->curl->getResponseContentType() + ); return $soapResponse; - } + } - /** + /** * Custom request method to be able to modify the SOAP messages. * $oneWay parameter is not used at the moment. * - * @param string $request - * @param string $location - * @param string $action - * @param int $version - * @param int $oneWay 0|1 + * @param string $request Request string + * @param string $location Location + * @param string $action SOAP action + * @param int $version SOAP version + * @param int $oneWay 0|1 + * * @return string */ public function __doRequest($request, $location, $action, $version, $oneWay = 0) @@ -180,7 +184,8 @@ class SoapClient extends \SoapClient * Runs the currently registered request filters on the request, performs * the HTTP request and runs the response filters. * - * @param SoapRequest $soapRequest + * @param SoapRequest $soapRequest SOAP request object + * * @return SoapResponse */ protected function __doRequest2(SoapRequest $soapRequest) @@ -262,7 +267,7 @@ class SoapClient extends \SoapClient $contentType = $this->curl->getResponseContentType(); if ($contentType != 'application/soap+xml' && $contentType != 'application/soap+xml') { - if (strncmp($response , "mixed) $options Options array + * * @return string */ private function loadWsdl($wsdl, array $options) diff --git a/src/BeSimple/SoapClient/SoapRequest.php b/src/BeSimple/SoapClient/SoapRequest.php index afe684f..00580cb 100644 --- a/src/BeSimple/SoapClient/SoapRequest.php +++ b/src/BeSimple/SoapClient/SoapRequest.php @@ -25,17 +25,18 @@ class SoapRequest extends CommonSoapRequest /** * Factory function for SoapRequest. * - * @param string $content - * @param string $location - * @param string $action - * @param string $version + * @param string $content Content + * @param string $location Location + * @param string $action SOAP action + * @param string $version SOAP version + * * @return BeSimple\SoapClient\SoapRequest */ public static function create($content, $location, $action, $version) { $request = new SoapRequest(); // $content is if unmodified from SoapClient not a php string type! - $request->setContent((string)$content); + $request->setContent((string) $content); $request->setLocation($location); $request->setAction($action); $request->setVersion($version); diff --git a/src/BeSimple/SoapClient/SoapResponse.php b/src/BeSimple/SoapClient/SoapResponse.php index a9317da..24a12c3 100644 --- a/src/BeSimple/SoapClient/SoapResponse.php +++ b/src/BeSimple/SoapClient/SoapResponse.php @@ -25,11 +25,12 @@ class SoapResponse extends CommonSoapResponse /** * Factory function for SoapResponse. * - * @param string $content - * @param string $location - * @param string $action - * @param string $version - * @param string $contentType + * @param string $content Content + * @param string $location Location + * @param string $action SOAP action + * @param string $version SOAP version + * @param string $contentType Content type header + * * @return BeSimple\SoapClient\SoapResponse */ public static function create($content, $location, $action, $version, $contentType) diff --git a/src/BeSimple/SoapClient/WsAddressingFilter.php b/src/BeSimple/SoapClient/WsAddressingFilter.php index 67d94c0..b112f82 100644 --- a/src/BeSimple/SoapClient/WsAddressingFilter.php +++ b/src/BeSimple/SoapClient/WsAddressingFilter.php @@ -126,10 +126,11 @@ class WsAddressingFilter implements SoapRequestFilter, SoapResponseFilter /** * Add additional reference parameters * - * @param string $ns - * @param string $pfx - * @param string $parameter - * @param string $value + * @param string $ns Namespace URI + * @param string $pfx Namespace prefix + * @param string $parameter Parameter name + * @param string $value Parameter value + * * @return void */ public function addReferenceParameter($ns, $pfx, $parameter, $value) @@ -145,8 +146,9 @@ class WsAddressingFilter implements SoapRequestFilter, SoapResponseFilter /** * Get additional reference parameters. * - * @param string $ns - * @param string $parameter + * @param string $ns Namespace URI + * @param string $parameter Parameter name + * * @return string|null */ public function getReferenceParameter($ns, $parameter) @@ -162,7 +164,8 @@ class WsAddressingFilter implements SoapRequestFilter, SoapResponseFilter /** * Set FaultTo address of type xs:anyURI. * - * @param string $action + * @param string $faultTo xs:anyURI + * * @return void */ public function setFaultTo($faultTo) @@ -173,7 +176,8 @@ class WsAddressingFilter implements SoapRequestFilter, SoapResponseFilter /** * Set From address of type xs:anyURI. * - * @param string $action + * @param string $from xs:anyURI + * * @return void */ public function setFrom($from) @@ -185,7 +189,8 @@ class WsAddressingFilter implements SoapRequestFilter, SoapResponseFilter * Set MessageId of type xs:anyURI. * Default: UUID v4 e.g. 'uuid:550e8400-e29b-11d4-a716-446655440000' * - * @param string $messageId + * @param string $messageId xs:anyURI + * * @return void */ public function setMessageId($messageId = null) @@ -200,8 +205,9 @@ class WsAddressingFilter implements SoapRequestFilter, SoapResponseFilter * Set RelatesTo of type xs:anyURI with the optional relationType * (of type xs:anyURI). * - * @param string $relatesTo - * @param string $relationType + * @param string $relatesTo xs:anyURI + * @param string $relationType xs:anyURI + * * @return void */ public function setRelatesTo($relatesTo, $relationType = null) @@ -216,7 +222,8 @@ class WsAddressingFilter implements SoapRequestFilter, SoapResponseFilter * Set ReplyTo address of type xs:anyURI * Default: self::ENDPOINT_REFERENCE_ANONYMOUS * - * @param string $replyTo + * @param string $replyTo xs:anyURI + * * @return void */ public function setReplyTo($replyTo = null) @@ -230,7 +237,8 @@ class WsAddressingFilter implements SoapRequestFilter, SoapResponseFilter /** * Modify the given request XML. * - * @param SoapRequest $dom + * @param SoapRequest $request SOAP request + * * @return void */ public function filterRequest(SoapRequest $request) @@ -298,7 +306,8 @@ class WsAddressingFilter implements SoapRequestFilter, SoapResponseFilter /** * Modify the given response XML. * - * @param SoapResponse $response + * @param SoapResponse $response SOAP response + * * @return void */ public function filterResponse(SoapResponse $response) diff --git a/src/BeSimple/SoapClient/WsSecurityFilter.php b/src/BeSimple/SoapClient/WsSecurityFilter.php index 8eb368c..e8cd60c 100644 --- a/src/BeSimple/SoapClient/WsSecurityFilter.php +++ b/src/BeSimple/SoapClient/WsSecurityFilter.php @@ -152,8 +152,8 @@ class WsSecurityFilter implements SoapRequestFilter, SoapResponseFilter * Constructor. * * @param boolean $addTimestamp (SMS 10) Add security timestamp. - * @param int $expires (SMS 10) Security timestamp expires time in seconds. - * @param string $actor + * @param int $expires (SMS 10) Security timestamp expires time in seconds. + * @param string $actor SOAP actor */ public function __construct($addTimestamp = true, $expires = 300, $actor = null) { @@ -165,9 +165,10 @@ class WsSecurityFilter implements SoapRequestFilter, SoapResponseFilter /** * Add user data. * - * @param string $username - * @param string $password - * @param int $passwordType self::PASSWORD_TYPE_DIGEST | self::PASSWORD_TYPE_TEXT + * @param string $username Username + * @param string $password Password + * @param int $passwordType self::PASSWORD_TYPE_DIGEST | self::PASSWORD_TYPE_TEXT + * * @return void */ public function addUserData($username, $password = null, $passwordType = self::PASSWORD_TYPE_DIGEST) @@ -180,7 +181,8 @@ class WsSecurityFilter implements SoapRequestFilter, SoapResponseFilter /** * Get service security key. * - * @param \BeSimple\SoapCommon\WsSecurityKey $serviceSecurityKey + * @param \BeSimple\SoapCommon\WsSecurityKey $serviceSecurityKey Service security key + * * @return void */ public function setServiceSecurityKeyObject(WsSecurityKey $serviceSecurityKey) @@ -191,7 +193,8 @@ class WsSecurityFilter implements SoapRequestFilter, SoapResponseFilter /** * Get user security key. * - * @param \BeSimple\SoapCommon\WsSecurityKey $userSecurityKey + * @param \BeSimple\SoapCommon\WsSecurityKey $userSecurityKey User security key + * * @return void */ public function setUserSecurityKeyObject(WsSecurityKey $userSecurityKey) @@ -202,8 +205,9 @@ class WsSecurityFilter implements SoapRequestFilter, SoapResponseFilter /** * Set security options. * - * @param int $tokenReference self::TOKEN_REFERENCE_SUBJECT_KEY_IDENTIFIER | self::TOKEN_REFERENCE_SECURITY_TOKEN | self::TOKEN_REFERENCE_THUMBPRINT_SHA1 - * @param boolean $encryptSignature + * @param int $tokenReference self::TOKEN_REFERENCE_SUBJECT_KEY_IDENTIFIER | self::TOKEN_REFERENCE_SECURITY_TOKEN | self::TOKEN_REFERENCE_THUMBPRINT_SHA1 + * @param boolean $encryptSignature Encrypt signature + * * @return void */ public function setSecurityOptionsEncryption($tokenReference, $encryptSignature = false) @@ -215,8 +219,9 @@ class WsSecurityFilter implements SoapRequestFilter, SoapResponseFilter /** * Set security options. * - * @param int $tokenReference self::TOKEN_REFERENCE_SUBJECT_KEY_IDENTIFIER | self::TOKEN_REFERENCE_SECURITY_TOKEN | self::TOKEN_REFERENCE_THUMBPRINT_SHA1 - * @param boolean $signAllHeaders + * @param int $tokenReference self::TOKEN_REFERENCE_SUBJECT_KEY_IDENTIFIER | self::TOKEN_REFERENCE_SECURITY_TOKEN | self::TOKEN_REFERENCE_THUMBPRINT_SHA1 + * @param boolean $signAllHeaders Sign all headers? + * * @return void */ public function setSecurityOptionsSignature($tokenReference, $signAllHeaders = false) @@ -228,7 +233,8 @@ class WsSecurityFilter implements SoapRequestFilter, SoapResponseFilter /** * Modify the given request XML. * - * @param SoapRequest $dom + * @param SoapRequest $request SOAP request to modify + * * @return void */ public function filterRequest(SoapRequest $request) @@ -278,7 +284,7 @@ class WsSecurityFilter implements SoapRequestFilter, SoapResponseFilter if (self::PASSWORD_TYPE_DIGEST === $this->passwordType) { $nonce = mt_rand(); - $password = base64_encode(sha1($nonce . $createdTimestamp . $this->password , true)); + $password = base64_encode(sha1($nonce . $createdTimestamp . $this->password, true)); $passwordType = Helper::NAME_WSS_UTP . '#PasswordDigest'; } else { $password = $this->password; @@ -349,7 +355,8 @@ class WsSecurityFilter implements SoapRequestFilter, SoapResponseFilter /** * Modify the given request XML. * - * @param SoapResponse $response + * @param SoapResponse $response SOAP response to modify + * * @return void */ public function filterResponse(SoapResponse $response) @@ -397,12 +404,11 @@ class WsSecurityFilter implements SoapRequestFilter, SoapResponseFilter /** * Adds the configured KeyInfo to the parentNode. * - * @param FilterHelper $filterHelper - * @param int $tokenReference - * @param \DOMNode $parentNode - * @param string $guid - * @param \ass\XmlSecurity\Key $xmlSecurityKey - * @param \DOMNode $insertBefore + * @param FilterHelper $filterHelper Filter helper object + * @param int $tokenReference Token reference type + * @param string $guid Unique ID + * @param \ass\XmlSecurity\Key $xmlSecurityKey XML security key + * * @return \DOMElement */ protected function createKeyInfo(FilterHelper $filterHelper, $tokenReference, $guid, XmlSecurityKey $xmlSecurityKey = null) @@ -444,8 +450,9 @@ class WsSecurityFilter implements SoapRequestFilter, SoapResponseFilter /** * Create a list of \DOMNodes that should be encrypted. * - * @param \DOMDocument $dom - * @param \DOMElement $security + * @param \DOMDocument $dom DOMDocument to query + * @param \DOMElement $security Security element + * * @return \DOMNodeList */ protected function createNodeListForEncryption(\DOMDocument $dom, \DOMElement $security) @@ -465,8 +472,9 @@ class WsSecurityFilter implements SoapRequestFilter, SoapResponseFilter /** * Create a list of \DOMNodes that should be signed. * - * @param \DOMDocument $dom - * @param \DOMElement $security + * @param \DOMDocument $dom DOMDocument to query + * @param \DOMElement $security Security element + * * @return array(\DOMNode) */ protected function createNodeListForSigning(\DOMDocument $dom, \DOMElement $security) @@ -496,8 +504,9 @@ class WsSecurityFilter implements SoapRequestFilter, SoapResponseFilter /** * Gets the referenced node for the given URI. * - * @param \DOMElement $node - * @param string $uri + * @param \DOMElement $node Node + * @param string $uri URI + * * @return \DOMElement */ protected function getReferenceNodeForUri(\DOMElement $node, $uri) @@ -514,8 +523,9 @@ class WsSecurityFilter implements SoapRequestFilter, SoapResponseFilter /** * Tries to resolve a key from the given \DOMElement. * - * @param \DOMElement $node - * @param string $algorithm + * @param \DOMElement $node Node where to resolve the key + * @param string $algorithm XML security key algorithm + * * @return \ass\XmlSecurity\Key|null */ public function keyInfoSecurityTokenReferenceResolver(\DOMElement $node, $algorithm) diff --git a/src/BeSimple/SoapClient/WsdlDownloader.php b/src/BeSimple/SoapClient/WsdlDownloader.php index 5dda4f9..828fe1c 100644 --- a/src/BeSimple/SoapClient/WsdlDownloader.php +++ b/src/BeSimple/SoapClient/WsdlDownloader.php @@ -1,4 +1,4 @@ -curl = $curl; $this->resolveRemoteIncludes = $resolveRemoteIncludes; // get current WSDL caching config - $this->cacheEnabled = (bool)ini_get('soap.wsdl_cache_enabled'); + $this->cacheEnabled = (bool) ini_get('soap.wsdl_cache_enabled'); if ($this->cacheEnabled === true && $cacheWsdl === WSDL_CACHE_NONE) { $this->cacheEnabled = false; @@ -87,7 +87,8 @@ class WsdlDownloader /** * Download given WSDL file and return name of cache file. * - * @param string $wsdl + * @param string $wsdl WSDL file URL/path + * * @return string */ public function download($wsdl) @@ -134,7 +135,8 @@ class WsdlDownloader /** * Do we have a remote file? * - * @param string $file + * @param string $file File URL/path + * * @return boolean */ private function isRemoteFile($file) @@ -153,9 +155,10 @@ class WsdlDownloader /** * Resolves remote WSDL/XSD includes within the WSDL files. * - * @param string $xml - * @param string $cacheFile - * @param unknown_type $parentIsRemote + * @param string $xml XML file + * @param string $cacheFile Cache file name + * @param boolean $parentFile Parent file name + * * @return void */ private function resolveRemoteIncludes($xml, $cacheFile, $parentFile = null) @@ -203,8 +206,9 @@ class WsdlDownloader /** * Resolves the relative path to base into an absolute. * - * @param string $base - * @param string $relative + * @param string $base Base path + * @param string $relative Relative path + * * @return string */ private function resolveRelativePathInUrl($base, $relative) From aa35e9e172ac5a29d399a4ec7527fcaf73fff495 Mon Sep 17 00:00:00 2001 From: Andreas Schamberger Date: Sat, 17 Dec 2011 20:36:56 +0100 Subject: [PATCH 41/63] working WS-Addressing and WS-Security filters --- src/BeSimple/SoapClient/SoapClient.php | 4 +-- .../SoapClient/WsAddressingFilter.php | 25 ++++++++++++--- src/BeSimple/SoapClient/WsSecurityFilter.php | 31 ++++++++++++++++--- src/BeSimple/SoapClient/WsdlDownloader.php | 2 +- 4 files changed, 50 insertions(+), 12 deletions(-) diff --git a/src/BeSimple/SoapClient/SoapClient.php b/src/BeSimple/SoapClient/SoapClient.php index b130c7b..5e34385 100644 --- a/src/BeSimple/SoapClient/SoapClient.php +++ b/src/BeSimple/SoapClient/SoapClient.php @@ -191,13 +191,13 @@ class SoapClient extends \SoapClient protected function __doRequest2(SoapRequest $soapRequest) { // run SoapKernel on SoapRequest - $soapRequest = $this->soapKernel->filterRequest($soapRequest); + $this->soapKernel->filterRequest($soapRequest); // perform HTTP request with cURL $soapResponse = $this->__doHttpRequest($soapRequest); // run SoapKernel on SoapResponse - $soapResponse = $this->soapKernel->filterResponse($soapResponse); + $this->soapKernel->filterResponse($soapResponse); return $soapResponse; } diff --git a/src/BeSimple/SoapClient/WsAddressingFilter.php b/src/BeSimple/SoapClient/WsAddressingFilter.php index b112f82..e1093b1 100644 --- a/src/BeSimple/SoapClient/WsAddressingFilter.php +++ b/src/BeSimple/SoapClient/WsAddressingFilter.php @@ -13,7 +13,9 @@ namespace BeSimple\SoapClient; use BeSimple\SoapCommon\Helper; +use BeSimple\SoapCommon\SoapRequest as CommonSoapRequest; use BeSimple\SoapCommon\SoapRequestFilter; +use BeSimple\SoapCommon\SoapResponse as CommonSoapResponse; use BeSimple\SoapCommon\SoapResponseFilter; /** @@ -161,6 +163,21 @@ class WsAddressingFilter implements SoapRequestFilter, SoapResponseFilter return null; } + /** + * Reset all properties to default values. + */ + public function resetFilter() + { + $this->faultTo = null; + $this->from = null; + $this->messageId = null; + $this->referenceParametersRecieved = array(); + $this->referenceParametersSet = array(); + $this->relatesTo = null; + $this->relatesToRelationshipType = null; + $this->replyTo = null; + } + /** * Set FaultTo address of type xs:anyURI. * @@ -237,11 +254,11 @@ class WsAddressingFilter implements SoapRequestFilter, SoapResponseFilter /** * Modify the given request XML. * - * @param SoapRequest $request SOAP request + * @param \BeSimple\SoapCommon\SoapRequest $request SOAP request * * @return void */ - public function filterRequest(SoapRequest $request) + public function filterRequest(CommonSoapRequest $request) { // get \DOMDocument from SOAP request $dom = $request->getContentDocument(); @@ -306,11 +323,11 @@ class WsAddressingFilter implements SoapRequestFilter, SoapResponseFilter /** * Modify the given response XML. * - * @param SoapResponse $response SOAP response + * @param \BeSimple\SoapCommon\SoapResponse $response SOAP response * * @return void */ - public function filterResponse(SoapResponse $response) + public function filterResponse(CommonSoapResponse $response) { // get \DOMDocument from SOAP response $dom = $response->getContentDocument(); diff --git a/src/BeSimple/SoapClient/WsSecurityFilter.php b/src/BeSimple/SoapClient/WsSecurityFilter.php index e8cd60c..fec0060 100644 --- a/src/BeSimple/SoapClient/WsSecurityFilter.php +++ b/src/BeSimple/SoapClient/WsSecurityFilter.php @@ -17,7 +17,9 @@ use ass\XmlSecurity\Enc as XmlSecurityEnc; use ass\XmlSecurity\Key as XmlSecurityKey; use BeSimple\SoapCommon\Helper; +use BeSimple\SoapCommon\SoapRequest as CommonSoapRequest; use BeSimple\SoapCommon\SoapRequestFilter; +use BeSimple\SoapCommon\SoapResponse as CommonSoapResponse; use BeSimple\SoapCommon\SoapResponseFilter; use BeSimple\SoapCommon\WsSecurityKey; @@ -178,6 +180,25 @@ class WsSecurityFilter implements SoapRequestFilter, SoapResponseFilter $this->passwordType = $passwordType; } + /** + * Reset all properties to default values. + */ + public function resetFilter() + { + $this->actor = null; + $this->addTimestamp = null; + $this->encryptSignature = null; + $this->expires = null; + $this->password = null; + $this->passwordType = null; + $this->serviceSecurityKey = null; + $this->signAllHeaders = null; + $this->tokenReferenceEncryption = null; + $this->tokenReferenceSignature = null; + $this->username = null; + $this->userSecurityKey = null; + } + /** * Get service security key. * @@ -233,11 +254,11 @@ class WsSecurityFilter implements SoapRequestFilter, SoapResponseFilter /** * Modify the given request XML. * - * @param SoapRequest $request SOAP request to modify + * @param \BeSimple\SoapCommon\SoapRequest $request SOAP request * * @return void */ - public function filterRequest(SoapRequest $request) + public function filterRequest(CommonSoapRequest $request) { // get \DOMDocument from SOAP request $dom = $request->getContentDocument(); @@ -256,7 +277,7 @@ class WsSecurityFilter implements SoapRequestFilter, SoapResponseFilter // create security header $security = $filterHelper->createElement(Helper::NS_WSS, 'Security'); - $filterHelper->addHeaderElement($security, true, $this->actor, $request->getSoapVersion()); + $filterHelper->addHeaderElement($security, true, $this->actor, $request->getVersion()); if (true === $this->addTimestamp || null !== $this->expires) { $timestamp = $filterHelper->createElement(Helper::NS_WSU, 'Timestamp'); @@ -355,11 +376,11 @@ class WsSecurityFilter implements SoapRequestFilter, SoapResponseFilter /** * Modify the given request XML. * - * @param SoapResponse $response SOAP response to modify + * @param \BeSimple\SoapCommon\SoapResponse $response SOAP response * * @return void */ - public function filterResponse(SoapResponse $response) + public function filterResponse(CommonSoapResponse $response) { // get \DOMDocument from SOAP response $dom = $response->getContentDocument(); diff --git a/src/BeSimple/SoapClient/WsdlDownloader.php b/src/BeSimple/SoapClient/WsdlDownloader.php index 828fe1c..453eaba 100644 --- a/src/BeSimple/SoapClient/WsdlDownloader.php +++ b/src/BeSimple/SoapClient/WsdlDownloader.php @@ -1,4 +1,4 @@ - Date: Sun, 18 Dec 2011 09:07:36 +0100 Subject: [PATCH 42/63] remove method for now (could be a filter later) --- src/BeSimple/SoapClient/SoapClient.php | 38 +++----------------------- 1 file changed, 4 insertions(+), 34 deletions(-) diff --git a/src/BeSimple/SoapClient/SoapClient.php b/src/BeSimple/SoapClient/SoapClient.php index 5e34385..5c6f2a2 100644 --- a/src/BeSimple/SoapClient/SoapClient.php +++ b/src/BeSimple/SoapClient/SoapClient.php @@ -125,9 +125,11 @@ class SoapClient extends \SoapClient 'SOAPAction: "' . $soapRequest->getAction() . '"', ); // execute HTTP request with cURL - $responseSuccessfull = $this->curl->exec($soapRequest->getLocation(), + $responseSuccessfull = $this->curl->exec( + $soapRequest->getLocation(), $soapRequest->getContent(), - $headers); + $headers + ); // tracing enabled: store last request header and body if ($this->tracingEnabled === true) { $this->lastRequestHeaders = $this->curl->getRequestHeaders(); @@ -252,38 +254,6 @@ class SoapClient extends \SoapClient return $this->soapKernel; } - // TODO finish - protected function isValidSoapResponse() - { - //check if we do have a proper soap status code (if not soapfault) - $responseStatusCode = $this->curl->getResponseStatusCode(); - $response = $this->curl->getResponseBody(); - if ($responseStatusCode >= 400) { - $isError = 0; - $response = trim($response); - if (strlen($response) == 0) { - $isError = 1; - } else { - $contentType = $this->curl->getResponseContentType(); - if ($contentType != 'application/soap+xml' - && $contentType != 'application/soap+xml') { - if (strncmp($response, "curl->getResponseStatusMessage()); - } - } 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'); - } - } - } - /** * Downloads WSDL files with cURL. Uses all SoapClient options for * authentication. Uses the WSDL_CACHE_* constants and the 'soap.wsdl_*' From 8546fb45af25bb4cce3b84d592ce5c1ea4f4f516 Mon Sep 17 00:00:00 2001 From: Andreas Schamberger Date: Sun, 18 Dec 2011 13:03:07 +0100 Subject: [PATCH 43/63] doc comment fixes --- src/BeSimple/SoapClient/SoapClientBuilder.php | 92 ++++++++++++++++--- 1 file changed, 78 insertions(+), 14 deletions(-) diff --git a/src/BeSimple/SoapClient/SoapClientBuilder.php b/src/BeSimple/SoapClient/SoapClientBuilder.php index 65a6f9b..3759f13 100644 --- a/src/BeSimple/SoapClient/SoapClientBuilder.php +++ b/src/BeSimple/SoapClient/SoapClientBuilder.php @@ -15,25 +15,35 @@ namespace BeSimple\SoapClient; use BeSimple\SoapCommon\AbstractSoapBuilder; /** + * Fluent interface builder for SoapClient instance. + * * @author Francis Besset * @author Christian Kerl */ class SoapClientBuilder extends AbstractSoapBuilder { + /** + * Authentication options. + * + * @var array(string=>mixed) + */ protected $soapOptionAuthentication = array(); /** - * @return SoapClientBuilder + * Create new instance with default options. + * + * @return \BeSimple\SoapClient\SoapClientBuilder */ - static public function createWithDefaults() + public static function createWithDefaults() { return parent::createWithDefaults() - ->withUserAgent('BeSimpleSoap') - ; + ->withUserAgent('BeSimpleSoap'); } /** - * @return SoapClient + * Finally returns a SoapClient instance. + * + * @return \BeSimple\SoapClient\SoapClient */ public function build() { @@ -42,13 +52,22 @@ class SoapClientBuilder extends AbstractSoapBuilder return new SoapClient($this->wsdl, $this->getSoapOptions()); } + /** + * Get final array of SOAP options. + * + * @return array(string=>mixed) + */ public function getSoapOptions() { return parent::getSoapOptions() + $this->soapOptionAuthentication; } /** - * @return SoapClientBuilder + * Configure option 'trace'. + * + * @param boolean $trace Enable/Disable + * + * @return \BeSimple\SoapClient\SoapClientBuilder */ public function withTrace($trace = true) { @@ -58,7 +77,11 @@ class SoapClientBuilder extends AbstractSoapBuilder } /** - * @return SoapClientBuilder + * Configure option 'exceptions'. + * + * @param boolean $exceptions Enable/Disable + * + * @return \BeSimple\SoapClient\SoapClientBuilder */ public function withExceptions($exceptions = true) { @@ -68,7 +91,11 @@ class SoapClientBuilder extends AbstractSoapBuilder } /** - * @return SoapClientBuilder + * Configure option 'user_agent'. + * + * @param string $userAgent User agent string + * + * @return \BeSimple\SoapClient\SoapClientBuilder */ public function withUserAgent($userAgent) { @@ -77,18 +104,37 @@ class SoapClientBuilder extends AbstractSoapBuilder return $this; } + /** + * Enable gzip compression. + * + * @return \BeSimple\SoapClient\SoapClientBuilder + */ public function withCompressionGzip() { $this->soapOptions['compression'] = SOAP_COMPRESSION_ACCEPT | SOAP_COMPRESSION_GZIP; - } - public function withCompressionDeflate() - { - $this->soapOptions['compression'] = SOAP_COMPRESSION_ACCEPT | SOAP_COMPRESSION_DEFLATE; + return $this; } /** - * @return SoapClientBuilder + * Enable deflate compression. + * + * @return \BeSimple\SoapClient\SoapClientBuilder + */ + public function withCompressionDeflate() + { + $this->soapOptions['compression'] = SOAP_COMPRESSION_ACCEPT | SOAP_COMPRESSION_DEFLATE; + + return $this; + } + + /** + * Configure basic authentication + * + * @param string $username Username + * @param string $password Password + * + * @return \BeSimple\SoapClient\SoapClientBuilder */ public function withBasicAuthentication($username, $password) { @@ -102,7 +148,12 @@ class SoapClientBuilder extends AbstractSoapBuilder } /** - * @return SoapClientBuilder + * Configure digest authentication. + * + * @param string $certificate Certificate + * @param string $passphrase Passphrase + * + * @return \BeSimple\SoapClient\SoapClientBuilder */ public function withDigestAuthentication($certificate, $passphrase = null) { @@ -118,6 +169,16 @@ class SoapClientBuilder extends AbstractSoapBuilder return $this; } + /** + * Configure proxy. + * + * @param string $host Host + * @param int $port Port + * @param string $username Username + * @param string $password Password + * + * @return \BeSimple\SoapClient\SoapClientBuilder + */ public function withProxy($host, $port, $username = null, $password = null) { $this->soapOptions['proxy_host'] = $host; @@ -131,6 +192,9 @@ class SoapClientBuilder extends AbstractSoapBuilder return $this; } + /** + * Validate options. + */ protected function validateOptions() { $this->validateWsdl(); From 7cc928a111e8e6dfc6bf5aad42fa143c40c8d87d Mon Sep 17 00:00:00 2001 From: Andreas Schamberger Date: Sun, 18 Dec 2011 14:34:15 +0100 Subject: [PATCH 44/63] skeleton of MimeFilter --- src/BeSimple/SoapClient/MimeFilter.php | 112 +++++++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 src/BeSimple/SoapClient/MimeFilter.php diff --git a/src/BeSimple/SoapClient/MimeFilter.php b/src/BeSimple/SoapClient/MimeFilter.php new file mode 100644 index 0000000..3549ed1 --- /dev/null +++ b/src/BeSimple/SoapClient/MimeFilter.php @@ -0,0 +1,112 @@ + + * (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\Helper; +use BeSimple\SoapCommon\Mime\MultiPart as MimeMultiPart; +use BeSimple\SoapCommon\Mime\Parser as MimeParser; +use BeSimple\SoapCommon\Mime\Part as MimePart; +use BeSimple\SoapCommon\SoapRequest as CommonSoapRequest; +use BeSimple\SoapCommon\SoapRequestFilter; +use BeSimple\SoapCommon\SoapResponse as CommonSoapResponse; +use BeSimple\SoapCommon\SoapResponseFilter; + +/** + * MIME filter. + * + * @author Andreas Schamberger + */ +class MimeFilter implements SoapRequestFilter, SoapResponseFilter +{ + /** + * Reset all properties to default values. + */ + public function resetFilter() + { + } + + /** + * Modify the given request XML. + * + * @param \BeSimple\SoapCommon\SoapRequest $request SOAP request + * + * @return void + */ + public function filterRequest(CommonSoapRequest $request) + { + // TODO get from request object + $attachmentsToSend = array(); + + // build mime message if we have attachments + if (count($attachmentsToSend) > 0) { + $multipart = new MimeMultiPart(); + $soapPart = new MimePart($request->getContent(), 'text/xml', 'utf-8', MimePart::ENCODING_EIGHT_BIT); + $soapVersion = $request->getVersion(); + // change content type headers for MTOM with SOAP 1.1 + // TODO attachment type option!!!!!!!!!1 + if ($soapVersion == SOAP_1_1 && $this->options['features_mime'] & Helper::ATTACHMENTS_TYPE_MTOM) { + $multipart->setHeader('Content-Type', 'type', 'application/xop+xml'); + $multipart->setHeader('Content-Type', 'start-info', 'text/xml'); + $soapPart->setHeader('Content-Type', 'application/xop+xml'); + $soapPart->setHeader('Content-Type', 'type', 'text/xml'); + } + // change content type headers for SOAP 1.2 + elseif ($soapVersion == SOAP_1_2) { + $multipart->setHeader('Content-Type', 'type', 'application/soap+xml'); + $soapPart->setHeader('Content-Type', 'application/soap+xml'); + } + $multipart->addPart($soapPart, true); + foreach ($attachmentsToSend as $cid => $attachment) { + $multipart->addPart($attachment, false); + } + $request = $multipart->getMimeMessage(); + $headers = $multipart->getHeadersForHttp(); + } + } + + /** + * Modify the given response XML. + * + * @param \BeSimple\SoapCommon\SoapResponse $response SOAP response + * + * @return void + */ + public function filterResponse(CommonSoapResponse $response) + { + $attachmentsRecieved = array(); + + // check content type if it is a multipart mime message + $responseContentType = $response->getContentType(); + if (false !== stripos($responseContentType, 'multipart/related')) { + // parse mime message + $headers = array( + 'Content-Type' => $responseContentType, + ); + $multipart = MimeParser::parseMimeMessage($response->getContent(), $headers); + // get soap payload and update SoapResponse object + $soapPart = $multipart->getPart(); + $response->setContent($soapPart->getContent()); + $response->setContentType($soapPart->getHeader('Content-Type')); + // store attachments + $attachments = $multipart->getParts(false); + foreach ($attachments as $cid => $attachment) { + $attachmentsRecieved[$cid] = $attachment; + } + } + + // add attachment to request object + if (count($attachmentsRecieved) > 0) { + // TODO add to response object + } + } +} \ No newline at end of file From c5ccd89949b6f2bfa35a6e1326413b55673f18e1 Mon Sep 17 00:00:00 2001 From: Andreas Schamberger Date: Tue, 3 Jan 2012 18:50:45 +0100 Subject: [PATCH 45/63] Added SwA to Client --- src/BeSimple/SoapClient/MimeFilter.php | 44 +++++++-- src/BeSimple/SoapClient/MtomTypeConverter.php | 93 +++++++++++++++++++ src/BeSimple/SoapClient/SoapClient.php | 51 ++++++++++ src/BeSimple/SoapClient/SwaTypeConverter.php | 82 ++++++++++++++++ 4 files changed, 261 insertions(+), 9 deletions(-) create mode 100644 src/BeSimple/SoapClient/MtomTypeConverter.php create mode 100644 src/BeSimple/SoapClient/SwaTypeConverter.php diff --git a/src/BeSimple/SoapClient/MimeFilter.php b/src/BeSimple/SoapClient/MimeFilter.php index 3549ed1..f5c4616 100644 --- a/src/BeSimple/SoapClient/MimeFilter.php +++ b/src/BeSimple/SoapClient/MimeFilter.php @@ -28,11 +28,29 @@ use BeSimple\SoapCommon\SoapResponseFilter; */ class MimeFilter implements SoapRequestFilter, SoapResponseFilter { + /** + * Attachment type. + * + * @var int Helper::ATTACHMENTS_TYPE_SWA | Helper::ATTACHMENTS_TYPE_MTOM + */ + protected $attachmentType = Helper::ATTACHMENTS_TYPE_SWA; + + /** + * Constructor. + * + * @param int $attachmentType Helper::ATTACHMENTS_TYPE_SWA | Helper::ATTACHMENTS_TYPE_MTOM + */ + public function __construct($attachmentType) + { + $this->attachmentType = $attachmentType; + } + /** * Reset all properties to default values. */ public function resetFilter() { + $this->attachmentType = Helper::ATTACHMENTS_TYPE_SWA; } /** @@ -44,8 +62,8 @@ class MimeFilter implements SoapRequestFilter, SoapResponseFilter */ public function filterRequest(CommonSoapRequest $request) { - // TODO get from request object - $attachmentsToSend = array(); + // get attachments from request object + $attachmentsToSend = $request->getAttachments(); // build mime message if we have attachments if (count($attachmentsToSend) > 0) { @@ -53,8 +71,7 @@ class MimeFilter implements SoapRequestFilter, SoapResponseFilter $soapPart = new MimePart($request->getContent(), 'text/xml', 'utf-8', MimePart::ENCODING_EIGHT_BIT); $soapVersion = $request->getVersion(); // change content type headers for MTOM with SOAP 1.1 - // TODO attachment type option!!!!!!!!!1 - if ($soapVersion == SOAP_1_1 && $this->options['features_mime'] & Helper::ATTACHMENTS_TYPE_MTOM) { + if ($soapVersion == SOAP_1_1 && $this->attachmentType & Helper::ATTACHMENTS_TYPE_MTOM) { $multipart->setHeader('Content-Type', 'type', 'application/xop+xml'); $multipart->setHeader('Content-Type', 'start-info', 'text/xml'); $soapPart->setHeader('Content-Type', 'application/xop+xml'); @@ -69,8 +86,13 @@ class MimeFilter implements SoapRequestFilter, SoapResponseFilter foreach ($attachmentsToSend as $cid => $attachment) { $multipart->addPart($attachment, false); } - $request = $multipart->getMimeMessage(); + $request->setContent($multipart->getMimeMessage()); + + // TODO $headers = $multipart->getHeadersForHttp(); + list($name, $contentType) = explode(': ', $headers[0]); + + $request->setContentType($contentType); } } @@ -83,6 +105,7 @@ class MimeFilter implements SoapRequestFilter, SoapResponseFilter */ public function filterResponse(CommonSoapResponse $response) { + // array to store attachments $attachmentsRecieved = array(); // check content type if it is a multipart mime message @@ -90,12 +113,15 @@ class MimeFilter implements SoapRequestFilter, SoapResponseFilter if (false !== stripos($responseContentType, 'multipart/related')) { // parse mime message $headers = array( - 'Content-Type' => $responseContentType, + 'Content-Type' => trim($responseContentType), ); $multipart = MimeParser::parseMimeMessage($response->getContent(), $headers); // get soap payload and update SoapResponse object $soapPart = $multipart->getPart(); - $response->setContent($soapPart->getContent()); + // convert href -> myhref for external references as PHP throws exception in this case + // http://svn.php.net/viewvc/php/php-src/branches/PHP_5_4/ext/soap/php_encoding.c?view=markup#l3436 + $content = preg_replace('/href=(?!#)/', 'myhref=', $soapPart->getContent()); + $response->setContent($content); $response->setContentType($soapPart->getHeader('Content-Type')); // store attachments $attachments = $multipart->getParts(false); @@ -104,9 +130,9 @@ class MimeFilter implements SoapRequestFilter, SoapResponseFilter } } - // add attachment to request object + // add attachments to response object if (count($attachmentsRecieved) > 0) { - // TODO add to response object + $response->setAttachments($attachmentsRecieved); } } } \ No newline at end of file diff --git a/src/BeSimple/SoapClient/MtomTypeConverter.php b/src/BeSimple/SoapClient/MtomTypeConverter.php new file mode 100644 index 0000000..ab74f83 --- /dev/null +++ b/src/BeSimple/SoapClient/MtomTypeConverter.php @@ -0,0 +1,93 @@ + + * (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\Helper; +use BeSimple\SoapCommon\Mime\Part as MimePart; +use BeSimple\SoapCommon\SoapKernel; +use BeSimple\SoapCommon\SoapRequest as CommonSoapRequest; +use BeSimple\SoapCommon\SoapResponse as CommonSoapResponse; +use BeSimple\SoapCommon\Converter\TypeConverterInterface; + +/** + * MTOM type converter. + * + * @author Andreas Schamberger + */ +class MtomTypeConverter +{ + /** + * {@inheritDoc} + */ + public function getTypeNamespace() + { + return 'http://www.w3.org/2001/XMLSchema'; + } + + /** + * {@inheritDoc} + */ + public function getTypeName() + { + return 'base64Binary'; + } + + /** + * {@inheritDoc} + */ + public function convertXmlToPhp($data, $soapKernel) + { + $doc = new \DOMDocument(); + $doc->loadXML($data); + + $includes = $doc->getElementsByTagNameNS(Helper::NS_XOP, 'Include'); + $include = $includes->item(0); + + $ref = $include->getAttribute('myhref'); + + if ('cid:' === substr($ref, 0, 4)) { + $contentId = urldecode(substr($ref, 4)); + + if (null !== ($part = $soapKernel->getAttachment($contentId))) { + + return $part->getContent(); + } else { + + return null; + } + } + + return $data; + } + + /** + * {@inheritDoc} + */ + public function convertPhpToXml($data, $soapKernel) + { + $part = new MimePart($data); + $contentId = trim($part->getHeader('Content-ID'), '<>'); + + $soapKernel->addAttachment($part); + + $doc = new \DOMDocument(); + $node = $doc->createElement($this->getTypeName()); + + // add xop:Include element + $xinclude = $doc->createElementNS(Helper::NS_XOP, Helper::PFX_XOP . ':Include'); + $xinclude->setAttribute('href', 'cid:' . $contentId); + $node->appendChild($xinclude); + + return $doc->saveXML(); + } +} diff --git a/src/BeSimple/SoapClient/SoapClient.php b/src/BeSimple/SoapClient/SoapClient.php index 5c6f2a2..23b529a 100644 --- a/src/BeSimple/SoapClient/SoapClient.php +++ b/src/BeSimple/SoapClient/SoapClient.php @@ -12,6 +12,7 @@ namespace BeSimple\SoapClient; +use BeSimple\SoapCommon\Helper; use BeSimple\SoapCommon\SoapKernel; /** @@ -24,6 +25,13 @@ use BeSimple\SoapCommon\SoapKernel; */ class SoapClient extends \SoapClient { + /** + * SOAP attachment type. + * + * @var int + */ + protected $attachmentType = Helper::ATTACHMENTS_TYPE_BASE64; + /** * Soap version. * @@ -96,10 +104,16 @@ class SoapClient extends \SoapClient if (isset($options['soap_version'])) { $this->soapVersion = $options['soap_version']; } + // attachment handling + if (isset($options['attachment_type'])) { + $this->attachmentType = $options['attachment_type']; + } $this->curl = new Curl($options); $wsdlFile = $this->loadWsdl($wsdl, $options); // TODO $wsdlHandler = new WsdlHandler($wsdlFile, $this->soapVersion); $this->soapKernel = new SoapKernel(); + // set up type converter and mime filter + $this->configureMime($options); // we want the exceptions option to be set $options['exceptions'] = true; // disable obsolete trace option for native SoapClient as we need to do our own tracing anyways @@ -244,6 +258,43 @@ class SoapClient extends \SoapClient return $this->lastResponse; } + /** + * Configure filter and type converter for SwA/MTOM. + * + * @param array &$options SOAP constructor options array. + * + * @return void + */ + private function configureMime(array &$options) + { + if (Helper::ATTACHMENTS_TYPE_BASE64 !== $this->attachmentType) { + // register mime filter in SoapKernel + $mimeFilter = new MimeFilter($this->attachmentType); + $this->soapKernel->registerFilter($mimeFilter); + // configure type converter + if (Helper::ATTACHMENTS_TYPE_SWA === $this->attachmentType) { + $converter = new SwaTypeConverter(); + } elseif (Helper::ATTACHMENTS_TYPE_MTOM === $this->attachmentType) { + $converter = new MtomTypeConverter(); + } + // configure typemap + if (!isset($options['typemap'])) { + $options['typemap'] = array(); + } + $soapKernel = $this->soapKernel; + $options['typemap'][] = array( + 'type_name' => $converter->getTypeName(), + 'type_ns' => $converter->getTypeNamespace(), + 'from_xml' => function($input) use ($converter, $soapKernel) { + return $converter->convertXmlToPhp($input, $soapKernel); + }, + 'to_xml' => function($input) use ($converter, $soapKernel) { + return $converter->convertPhpToXml($input, $soapKernel); + }, + ); + } + } + /** * Get SoapKernel instance. * diff --git a/src/BeSimple/SoapClient/SwaTypeConverter.php b/src/BeSimple/SoapClient/SwaTypeConverter.php new file mode 100644 index 0000000..a7f50ea --- /dev/null +++ b/src/BeSimple/SoapClient/SwaTypeConverter.php @@ -0,0 +1,82 @@ + + * (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\Helper; +use BeSimple\SoapCommon\Mime\Part as MimePart; +use BeSimple\SoapCommon\SoapKernel; +use BeSimple\SoapCommon\SoapRequest as CommonSoapRequest; +use BeSimple\SoapCommon\SoapResponse as CommonSoapResponse; +use BeSimple\SoapCommon\Converter\TypeConverterInterface; + +/** + * SwA type converter. + * + * @author Andreas Schamberger + */ +class SwaTypeConverter +{ + /** + * {@inheritDoc} + */ + public function getTypeNamespace() + { + return 'http://www.w3.org/2001/XMLSchema'; + } + + /** + * {@inheritDoc} + */ + public function getTypeName() + { + return 'base64Binary'; + } + + /** + * {@inheritDoc} + */ + public function convertXmlToPhp($data, $soapKernel) + { + $doc = new \DOMDocument(); + $doc->loadXML($data); + + $ref = $doc->documentElement->getAttribute('myhref'); + + if ('cid:' === substr($ref, 0, 4)) { + $contentId = urldecode(substr($ref, 4)); + + if (null !== ($part = $soapKernel->getAttachment($contentId))) { + + return $part->getContent(); + } else { + + return null; + } + } + + return $data; + } + + /** + * {@inheritDoc} + */ + public function convertPhpToXml($data, $soapKernel) + { + $part = new MimePart($data); + $contentId = trim($part->getHeader('Content-ID'), '<>'); + + $soapKernel->addAttachment($part); + + return sprintf('<%s href="%s"/>', $this->getTypeName(), $contentId); + } +} From a4e99899418f7eee6dcc3cdaefecd664f90f9ebf Mon Sep 17 00:00:00 2001 From: Andreas Schamberger Date: Thu, 5 Jan 2012 14:15:39 +0100 Subject: [PATCH 46/63] fixed mtom type converter --- src/BeSimple/SoapClient/MtomTypeConverter.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/BeSimple/SoapClient/MtomTypeConverter.php b/src/BeSimple/SoapClient/MtomTypeConverter.php index ab74f83..37fc80b 100644 --- a/src/BeSimple/SoapClient/MtomTypeConverter.php +++ b/src/BeSimple/SoapClient/MtomTypeConverter.php @@ -82,6 +82,7 @@ class MtomTypeConverter $doc = new \DOMDocument(); $node = $doc->createElement($this->getTypeName()); + $doc->appendChild($node); // add xop:Include element $xinclude = $doc->createElementNS(Helper::NS_XOP, Helper::PFX_XOP . ':Include'); From daadd04657f06e4beb7d7a8bd5f9f55a4731000c Mon Sep 17 00:00:00 2001 From: Andreas Schamberger Date: Thu, 5 Jan 2012 14:21:31 +0100 Subject: [PATCH 47/63] added Apache Axis2 interop tests (including own SwA service implementation and deployable service archives for all tests) --- tests/AxisInterop/MTOM.php | 47 +++++ tests/AxisInterop/MTOM.wsdl | 89 +++++++++ tests/AxisInterop/SwA.php | 86 ++++++++ tests/AxisInterop/SwA.wsdl | 162 +++++++++++++++ tests/AxisInterop/SwA/build.xml | 38 ++++ .../SwA/resources/META-INF/services.xml | 15 ++ .../besimple/service/BeSimpleSwaService.java | 78 ++++++++ tests/AxisInterop/WsAddressing.php | 75 +++++++ tests/AxisInterop/WsSecuritySigEnc.php | 116 +++++++++++ tests/AxisInterop/WsSecuritySigEnc.wsdl | 184 ++++++++++++++++++ tests/AxisInterop/WsSecurityUserPass.php | 81 ++++++++ tests/AxisInterop/WsSecurityUserPass.wsdl | 184 ++++++++++++++++++ .../axis_services/besimple-swa.aar | Bin 0 -> 3086 bytes .../axis_services/library-signencr.aar | Bin 0 -> 62646 bytes .../axis_services/library-username-digest.aar | Bin 0 -> 60057 bytes .../AxisInterop/axis_services/sample-mtom.aar | Bin 0 -> 39769 bytes tests/AxisInterop/axis_services/version2.aar | Bin 0 -> 1817 bytes tests/AxisInterop/clientcert.pem | 17 ++ tests/AxisInterop/clientkey.pem | 14 ++ tests/AxisInterop/image.jpg | Bin 0 -> 75596 bytes tests/AxisInterop/servercert.pem | 17 ++ 21 files changed, 1203 insertions(+) create mode 100644 tests/AxisInterop/MTOM.php create mode 100644 tests/AxisInterop/MTOM.wsdl create mode 100644 tests/AxisInterop/SwA.php create mode 100644 tests/AxisInterop/SwA.wsdl create mode 100644 tests/AxisInterop/SwA/build.xml create mode 100644 tests/AxisInterop/SwA/resources/META-INF/services.xml create mode 100644 tests/AxisInterop/SwA/src/besimple/service/BeSimpleSwaService.java create mode 100644 tests/AxisInterop/WsAddressing.php create mode 100644 tests/AxisInterop/WsSecuritySigEnc.php create mode 100644 tests/AxisInterop/WsSecuritySigEnc.wsdl create mode 100644 tests/AxisInterop/WsSecurityUserPass.php create mode 100644 tests/AxisInterop/WsSecurityUserPass.wsdl create mode 100644 tests/AxisInterop/axis_services/besimple-swa.aar create mode 100644 tests/AxisInterop/axis_services/library-signencr.aar create mode 100644 tests/AxisInterop/axis_services/library-username-digest.aar create mode 100644 tests/AxisInterop/axis_services/sample-mtom.aar create mode 100644 tests/AxisInterop/axis_services/version2.aar create mode 100644 tests/AxisInterop/clientcert.pem create mode 100644 tests/AxisInterop/clientkey.pem create mode 100644 tests/AxisInterop/image.jpg create mode 100644 tests/AxisInterop/servercert.pem diff --git a/tests/AxisInterop/MTOM.php b/tests/AxisInterop/MTOM.php new file mode 100644 index 0000000..cbfe886 --- /dev/null +++ b/tests/AxisInterop/MTOM.php @@ -0,0 +1,47 @@ +'; + +$options = array( + 'soap_version' => SOAP_1_1, + 'features' => SOAP_SINGLE_ELEMENT_ARRAYS, // make sure that result is array for size=1 + 'trace' => true, // enables use of the methods SoapClient->__getLastRequest, SoapClient->__getLastRequestHeaders, SoapClient->__getLastResponse and SoapClient->__getLastResponseHeaders + 'attachment_type' => BeSimpleSoapHelper::ATTACHMENTS_TYPE_MTOM, + 'cache_wsdl' => WSDL_CACHE_NONE, +); + +/* + * Deploy "axis_services/sample-mtom.aar" to Apache Axis2 to get this + * example to work. + * + * Apache Axis2 MTOM example. + * + */ +$sc = new BeSimpleSoapClient('MTOM.wsdl', $options); + +//var_dump($sc->__getFunctions()); +//var_dump($sc->__getTypes()); + +try { + + $attachment = new stdClass(); + $attachment->fileName = 'test123.txt'; + $attachment->binaryData = 'This is a test.'; + + var_dump($sc->attachment($attachment)); + +} catch (Exception $e) { + var_dump($e); +} + +// var_dump( +// $sc->__getLastRequestHeaders(), +// $sc->__getLastRequest(), +// $sc->__getLastResponseHeaders(), +// $sc->__getLastResponse() +// ); \ No newline at end of file diff --git a/tests/AxisInterop/MTOM.wsdl b/tests/AxisInterop/MTOM.wsdl new file mode 100644 index 0000000..178ee35 --- /dev/null +++ b/tests/AxisInterop/MTOM.wsdl @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/AxisInterop/SwA.php b/tests/AxisInterop/SwA.php new file mode 100644 index 0000000..0cf5b77 --- /dev/null +++ b/tests/AxisInterop/SwA.php @@ -0,0 +1,86 @@ +'; + +$options = array( + 'soap_version' => SOAP_1_1, + 'features' => SOAP_SINGLE_ELEMENT_ARRAYS, // make sure that result is array for size=1 + 'trace' => true, // enables use of the methods SoapClient->__getLastRequest, SoapClient->__getLastRequestHeaders, SoapClient->__getLastResponse and SoapClient->__getLastResponseHeaders + 'attachment_type' => BeSimpleSoapHelper::ATTACHMENTS_TYPE_SWA, + 'cache_wsdl' => WSDL_CACHE_NONE, +); + +/* + * Deploy "axis_services/besimple-swa.aar" to Apache Axis2 to get this + * example to work. + * + * Run ant to rebuild aar. + * + * Example based on: + * http://axis.apache.org/axis2/java/core/docs/mtom-guide.html#a3 + * http://wso2.org/library/1675 + * + * Doesn't work directly with ?wsdl served by Apache Axis! + * + */ + +$sc = new BeSimpleSoapClient('SwA.wsdl', $options); + +//var_dump($sc->__getFunctions()); +//var_dump($sc->__getTypes()); + +try { + $file = new stdClass(); + $file->name = 'upload.txt'; + $file->data = 'This is a test text!'; + $result = $sc->uploadFile($file); + + var_dump( + $result->return + ); + + $file = new stdClass(); + $file->name = 'upload.txt'; + $result = $sc->downloadFile($file); + + var_dump( + $result->data + ); + + $file = new stdClass(); + $file->name = 'image.jpg'; // source: http://www.freeimageslive.com/galleries/light/pics/swirl3768.jpg + $file->data = file_get_contents('image.jpg'); + $result = $sc->uploadFile($file); + + var_dump( + $result->return + ); + + $crc32 = crc32($file->data); + + $file = new stdClass(); + $file->name = 'image.jpg'; + $result = $sc->downloadFile($file); + + file_put_contents('image2.jpg', $result->data); + + + var_dump( + crc32($result->data) === $crc32 + ); + +} catch (Exception $e) { + var_dump($e); +} + +// var_dump( +// $sc->__getLastRequestHeaders(), +// $sc->__getLastRequest(), +// $sc->__getLastResponseHeaders(), +// $sc->__getLastResponse() +// ); \ No newline at end of file diff --git a/tests/AxisInterop/SwA.wsdl b/tests/AxisInterop/SwA.wsdl new file mode 100644 index 0000000..a4de7e0 --- /dev/null +++ b/tests/AxisInterop/SwA.wsdl @@ -0,0 +1,162 @@ + + + BeSimpleSwaService + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/AxisInterop/SwA/build.xml b/tests/AxisInterop/SwA/build.xml new file mode 100644 index 0000000..f5261ed --- /dev/null +++ b/tests/AxisInterop/SwA/build.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/AxisInterop/SwA/resources/META-INF/services.xml b/tests/AxisInterop/SwA/resources/META-INF/services.xml new file mode 100644 index 0000000..8b49f87 --- /dev/null +++ b/tests/AxisInterop/SwA/resources/META-INF/services.xml @@ -0,0 +1,15 @@ + + + BeSimple test service for SwA. + true + besimple.service.BeSimpleSwaService + + urn:uploadFile + + + + urn:downloadFile + + + + diff --git a/tests/AxisInterop/SwA/src/besimple/service/BeSimpleSwaService.java b/tests/AxisInterop/SwA/src/besimple/service/BeSimpleSwaService.java new file mode 100644 index 0000000..b173e15 --- /dev/null +++ b/tests/AxisInterop/SwA/src/besimple/service/BeSimpleSwaService.java @@ -0,0 +1,78 @@ +package besimple.service; + +import java.io.File; +import java.io.FileOutputStream; + +import javax.xml.namespace.QName; + +import javax.activation.DataHandler; +import javax.activation.FileDataSource; + +import org.apache.axiom.attachments.Attachments; +import org.apache.axiom.om.OMAbstractFactory; +import org.apache.axiom.om.OMAttribute; +import org.apache.axiom.om.OMElement; +import org.apache.axiom.om.OMFactory; +import org.apache.axiom.om.OMNamespace; + +import org.apache.axis2.context.MessageContext; +import org.apache.axis2.context.OperationContext; +import org.apache.axis2.wsdl.WSDLConstants; + +public class BeSimpleSwaService { + + String namespace = "http://service.besimple"; + + public OMElement uploadFile(OMElement element) throws Exception { + OMElement dataElement = (OMElement)element.getFirstChildWithName(new QName(namespace, "data")); + OMAttribute hrefAttribute = dataElement.getAttribute(new QName("href")); + + String contentID = hrefAttribute.getAttributeValue(); + contentID = contentID.trim(); + if (contentID.substring(0, 3).equalsIgnoreCase("cid")) { + contentID = contentID.substring(4); + } + OMElement nameElement = (OMElement)element.getFirstChildWithName(new QName(namespace, "name")); + String name = nameElement.getText(); + + MessageContext msgCtx = MessageContext.getCurrentMessageContext(); + Attachments attachment = msgCtx.getAttachmentMap(); + DataHandler dataHandler = attachment.getDataHandler(contentID); + + File file = new File(name); + FileOutputStream fileOutputStream = new FileOutputStream(file); + dataHandler.writeTo(fileOutputStream); + fileOutputStream.flush(); + fileOutputStream.close(); + + OMFactory factory = OMAbstractFactory.getOMFactory(); + OMNamespace omNs = factory.createOMNamespace(namespace, "swa"); + OMElement wrapperElement = factory.createOMElement("uploadFileResponse", omNs); + OMElement returnElement = factory.createOMElement("return", omNs, wrapperElement); + returnElement.setText("File saved succesfully."); + + return wrapperElement; + } + + public OMElement downloadFile(OMElement element) throws Exception { + OMElement nameElement = (OMElement)element.getFirstChildWithName(new QName(namespace, "name")); + String name = nameElement.getText(); + + MessageContext msgCtxIn = MessageContext.getCurrentMessageContext(); + OperationContext operationContext = msgCtxIn.getOperationContext(); + MessageContext msgCtxOut = operationContext.getMessageContext(WSDLConstants.MESSAGE_LABEL_OUT_VALUE); + + FileDataSource fileDataSource = new FileDataSource(name); + DataHandler dataHandler = new DataHandler(fileDataSource); + + String contentID = "cid:" + msgCtxOut.addAttachment(dataHandler); + + OMFactory factory = OMAbstractFactory.getOMFactory(); + OMNamespace omNs = factory.createOMNamespace(namespace, "swa"); + OMElement wrapperElement = factory.createOMElement("downloadFileResponse", omNs); + OMElement dataElement = factory.createOMElement("data", omNs, wrapperElement); + dataElement.addAttribute("href", contentID, null); + + return wrapperElement; + } +} diff --git a/tests/AxisInterop/WsAddressing.php b/tests/AxisInterop/WsAddressing.php new file mode 100644 index 0000000..9ec1bcc --- /dev/null +++ b/tests/AxisInterop/WsAddressing.php @@ -0,0 +1,75 @@ +'; + +$options = array( + 'soap_version' => SOAP_1_2, + 'features' => SOAP_SINGLE_ELEMENT_ARRAYS, // make sure that result is array for size=1 + 'trace' => true, // enables use of the methods SoapClient->__getLastRequest, SoapClient->__getLastRequestHeaders, SoapClient->__getLastResponse and SoapClient->__getLastResponseHeaders +); + +/* + * Deploy "axis_services/version2.aar" to Apache Axis2 to get this example to + * work. + * + * To rebuild the "axis_services/version2.aar" the following steps need to be + * done to build a working Apache Axis2 version service with SOAP session + * enabled. + * + * 1) Go to $AXIS_HOME/samples/version and edit the following files: + * + * resources/META-INF/services.xml: + * + * ... + * + * + * build.xml: + * replace version.aar with version2.aar + * + * 2) Run ant build.xml in "$AXIS_HOME/samples/version" + * + */ + +$sc = new BeSimpleSoapClient('http://localhost:8080/axis2/services/Version2?wsdl', $options); +$soapKernel = $sc->getSoapKernel(); +$wsaFilter = new BeSimpleWsAddressingFilter(); +$soapKernel->registerFilter($wsaFilter); + +//var_dump($sc->__getFunctions()); +//var_dump($sc->__getTypes()); + +try { + $wsaFilter->setReplyTo(BeSimpleWsAddressingFilter::ENDPOINT_REFERENCE_ANONYMOUS); + $wsaFilter->setMessageId(); + + var_dump($sc->getVersion()); + + $soapSessionId1 = $wsaFilter->getReferenceParameter('http://ws.apache.org/namespaces/axis2', 'ServiceGroupId'); + echo 'ID1: ' .$soapSessionId1 . PHP_EOL; + + $wsaFilter->addReferenceParameter('http://ws.apache.org/namespaces/axis2', 'axis2', 'ServiceGroupId', $soapSessionId1); + + var_dump($sc->getVersion()); + + $soapSessionId2 = $wsaFilter->getReferenceParameter('http://ws.apache.org/namespaces/axis2', 'ServiceGroupId'); + echo 'ID2: ' . $soapSessionId2 . PHP_EOL; + + if ($soapSessionId1 == $soapSessionId2) { + echo PHP_EOL; + echo 'SOAP session worked :)'; + } +} catch (Exception $e) { + var_dump($e); +} + +// var_dump( +// $sc->__getLastRequestHeaders(), +// $sc->__getLastRequest(), +// $sc->__getLastResponseHeaders(), +// $sc->__getLastResponse() +// ); \ No newline at end of file diff --git a/tests/AxisInterop/WsSecuritySigEnc.php b/tests/AxisInterop/WsSecuritySigEnc.php new file mode 100644 index 0000000..d6a31fb --- /dev/null +++ b/tests/AxisInterop/WsSecuritySigEnc.php @@ -0,0 +1,116 @@ +'; + +$options = array( + 'soap_version' => SOAP_1_2, + 'features' => SOAP_SINGLE_ELEMENT_ARRAYS, // make sure that result is array for size=1 + 'trace' => true, // enables use of the methods SoapClient->__getLastRequest, SoapClient->__getLastRequestHeaders, SoapClient->__getLastResponse and SoapClient->__getLastResponseHeaders +); + +/* + * Deploy "axis_services/library-signencr.aar" to Apache Axis2 to get this + * example to work. + * + * Links: + * http://www.dcc.uchile.cl/~pcamacho/tutorial/web/xmlsec/xmlsec.html + * http://www.aleksey.com/xmlsec/xmldsig-verifier.html + * + * Using code from axis example: + * http://www.ibm.com/developerworks/java/library/j-jws5/index.html + * + * Download key tool to export private key + * http://couchpotato.net/pkeytool/ + * + * keytool -export -alias serverkey -keystore server.keystore -storepass nosecret -file servercert.cer + * openssl x509 -out servercert.pem -outform pem -in servercert.pem -inform der + * + * keytool -export -alias clientkey -keystore client.keystore -storepass nosecret -file clientcert.cer + * openssl x509 -out clientcert.pem -outform pem -in clientcert.pem -inform der + * java -jar pkeytool.jar -exportkey -keystore client.keystore -storepass nosecret -keypass clientpass -rfc -alias clientkey -file clientkey.pem + * + * C:\Program Files\Java\jre6\bin\keytool -export -alias serverkey -keystore server.keystore -storepass nosecret -file servercert.cer + * C:\xampp\apache\bin\openssl x509 -out servercert.pem -outform pem -in servercert.cer -inform der + * + * C:\Program Files\Java\jre6\bin\keytool -export -alias clientkey -keystore client.keystore -storepass nosecret -file clientcert.cer + * C:\xampp\apache\bin\openssl x509 -out clientcert.pem -outform pem -in clientcert.cer -inform der + * java -jar C:\axis2\pkeytool\pkeytool.jar -exportkey -keystore client.keystore -storepass nosecret -keypass clientpass -rfc -alias clientkey -file clientkey.pem + * + * build.properties: + * server-policy=hash-policy-server.xml + * + * allows both text and digest! + */ + +class getBook {} +class getBookResponse {} +class getBooksByType {} +class getBooksByTypeResponse {} +class addBook {} +class addBookResponse {} +class BookInformation {} + +$options['classmap'] = array( + 'getBook' => 'getBook', + 'getBookResponse' => 'getBookResponse', + 'getBooksByType' => 'getBooksByType', + 'getBooksByTypeResponse' => 'getBooksByTypeResponse', + 'addBook' => 'addBook', + 'addBookResponse' => 'addBookResponse', + 'BookInformation' => 'BookInformation', +); + +$sc = new BeSimpleSoapClient('WsSecuritySigEnc.wsdl', $options); + +$wssFilter = new BeSimpleWsSecurityFilter(); +// user key for signature and encryption +$securityKeyUser = new BeSimpleWsSecurityKey(); +$securityKeyUser->addPrivateKey(XmlSecurityKey::RSA_SHA1, 'clientkey.pem', true); +$securityKeyUser->addPublicKey(XmlSecurityKey::RSA_SHA1, 'clientcert.pem', true); +$wssFilter->setUserSecurityKeyObject($securityKeyUser); +// service key for encryption +$securityKeyService = new BeSimpleWsSecurityKey(); +$securityKeyService->addPrivateKey(XmlSecurityKey::TRIPLEDES_CBC); +$securityKeyService->addPublicKey(XmlSecurityKey::RSA_1_5, 'servercert.pem', true); +$wssFilter->setServiceSecurityKeyObject($securityKeyService); +// TOKEN_REFERENCE_SUBJECT_KEY_IDENTIFIER | TOKEN_REFERENCE_SECURITY_TOKEN | TOKEN_REFERENCE_THUMBPRINT_SHA1 +$wssFilter->setSecurityOptionsSignature(BeSimpleWsSecurityFilter::TOKEN_REFERENCE_SECURITY_TOKEN); +$wssFilter->setSecurityOptionsEncryption(BeSimpleWsSecurityFilter::TOKEN_REFERENCE_THUMBPRINT_SHA1); + +$soapKernel = $sc->getSoapKernel(); +$soapKernel->registerFilter($wssFilter); + +//var_dump($sc->__getFunctions()); +//var_dump($sc->__getTypes()); + +try { + $gb = new getBook(); + $gb->isbn = '0061020052'; + var_dump($sc->getBook($gb)); + + $ab = new addBook(); + $ab->isbn = '0445203498'; + $ab->title = 'The Dragon Never Sleeps'; + $ab->author = 'Cook, Glen'; + $ab->type = 'scifi'; + var_dump($sc->addBook($ab)); + + // getBooksByType("scifi"); +} catch (Exception $e) { + var_dump($e); +} + +//var_dump( +// $sc->__getLastRequestHeaders(), +// $sc->__getLastRequest(), +// $sc->__getLastResponseHeaders(), +// $sc->__getLastResponse() +//); diff --git a/tests/AxisInterop/WsSecuritySigEnc.wsdl b/tests/AxisInterop/WsSecuritySigEnc.wsdl new file mode 100644 index 0000000..620ea51 --- /dev/null +++ b/tests/AxisInterop/WsSecuritySigEnc.wsdl @@ -0,0 +1,184 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/AxisInterop/WsSecurityUserPass.php b/tests/AxisInterop/WsSecurityUserPass.php new file mode 100644 index 0000000..595d9ba --- /dev/null +++ b/tests/AxisInterop/WsSecurityUserPass.php @@ -0,0 +1,81 @@ +'; + +$options = array( + 'soap_version' => SOAP_1_2, + 'features' => SOAP_SINGLE_ELEMENT_ARRAYS, // make sure that result is array for size=1 + 'trace' => true, // enables use of the methods SoapClient->__getLastRequest, SoapClient->__getLastRequestHeaders, SoapClient->__getLastResponse and SoapClient->__getLastResponseHeaders +); + +/* + * Deploy "axis_services/library-username-digest.aar" to Apache Axis2 to get + * this example to work. + * + * Using code from axis example: + * http://www.ibm.com/developerworks/java/library/j-jws4/index.html + * + * build.properties: + * server-policy=hash-policy-server.xml + * + * allows both text and digest! + */ + +class getBook {} +class getBookResponse {} +class getBooksByType {} +class getBooksByTypeResponse {} +class addBook {} +class addBookResponse {} +class BookInformation {} + +$options['classmap'] = array( + 'getBook' => 'getBook', + 'getBookResponse' => 'getBookResponse', + 'getBooksByType' => 'getBooksByType', + 'getBooksByTypeResponse' => 'getBooksByTypeResponse', + 'addBook' => 'addBook', + 'addBookResponse' => 'addBookResponse', + 'BookInformation' => 'BookInformation', +); + +$sc = new BeSimpleSoapClient('WsSecurityUserPass.wsdl', $options); + +$wssFilter = new BeSimpleWsSecurityFilter(true, 600); +$wssFilter->addUserData('libuser', 'books', BeSimpleWsSecurityFilter::PASSWORD_TYPE_TEXT); +//$wssFilter->addUserData( 'libuser', 'books', BeSimpleWsSecurityFilter::PASSWORD_TYPE_DIGEST ); + +$soapKernel = $sc->getSoapKernel(); +$soapKernel->registerFilter($wssFilter); + +//var_dump($sc->__getFunctions()); +//var_dump($sc->__getTypes()); + +try { + $gb = new getBook(); + $gb->isbn = '0061020052'; + var_dump($sc->getBook($gb)); + + $ab = new addBook(); + $ab->isbn = '0445203498'; + $ab->title = 'The Dragon Never Sleeps'; + $ab->author = 'Cook, Glen'; + $ab->type = 'scifi'; + var_dump($sc->addBook($ab)); + + // getBooksByType("scifi"); +} catch (Exception $e) { + var_dump($e); +} + +//var_dump( +// $sc->__getLastRequestHeaders(), +// $sc->__getLastRequest(), +// $sc->__getLastResponseHeaders(), +// $sc->__getLastResponse() +//); diff --git a/tests/AxisInterop/WsSecurityUserPass.wsdl b/tests/AxisInterop/WsSecurityUserPass.wsdl new file mode 100644 index 0000000..6e72411 --- /dev/null +++ b/tests/AxisInterop/WsSecurityUserPass.wsdl @@ -0,0 +1,184 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/AxisInterop/axis_services/besimple-swa.aar b/tests/AxisInterop/axis_services/besimple-swa.aar new file mode 100644 index 0000000000000000000000000000000000000000..bb417501c6b82065fd824b9e9f5606f7ace03988 GIT binary patch literal 3086 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 NfEz&Y$zuY-{{hb=>3#qJ literal 0 HcmV?d00001 diff --git a/tests/AxisInterop/axis_services/sample-mtom.aar b/tests/AxisInterop/axis_services/sample-mtom.aar new file mode 100644 index 0000000000000000000000000000000000000000..a215edc3bce4914b467c01e42740098366898cd4 GIT binary patch literal 39769 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 M0S1ldSJ%J&AKm90i2wiq literal 0 HcmV?d00001 diff --git a/tests/AxisInterop/axis_services/version2.aar b/tests/AxisInterop/axis_services/version2.aar new file mode 100644 index 0000000000000000000000000000000000000000..df7666263dff3f858e85450d396d001d463d29b6 GIT binary patch literal 1817 zcmWIWW@h1H0D()f!PZ~~l;C7wVeoYgan$wnbJPEKih&^js+5D_B2ekm!xI+80hML} zu?P^umHImRdAhj@A8pEU}(z_wBn!k4&$tPQQ6ng-d*P;=S45 z@11#cxu&j?@dLkHW#^#;*03g9g(Y26zf3;8_w7u#iV1U9%y=4fAas^}4D;SaY?>Dy zFKr78iq~wtT5~;8%VeH##9F~n91}YyYtkfv`g1kwHXIi z)fiZw?R==Z;l$ijmV|SBd*9YxmslqIVBX_|wZA5*)ZRayBOvP?tG@W+lZi@k&%YN7 z{bT#0B4yGR^!s+9Z{~Zy15=a)ZclvB_I1md#Pq|a7BBZcoAzjKl`Et-HqT_3kw%%(APh8%q8@ z+GxM&>s_7+msNZpYWarEiFmFNYH(y_0Ur%3*?d!x-3K8Ag11)CG=HStt$mM3_s$FaCp{FRIABGjF3y-IAFByDYa0#xAk< zvwy92ZB1lG5Ta_ygZR$)UVlk*(xm{Y<(nevD7okqUuYNKiWCRr>;FW z!S>sFR_BFUwuSfhWp{j-WFFHsBlP~myt(Z`KQj%ax2GHlSm*Xfub}m;wb3r|M3vg} z>(qCgUSRGzr;6wC)UB^{_dnludhfE>Zs(V#qR*E+?od$kT5?cn|FiAR?sL4Z9`o3| zJ!BE1Y%|Z%`>uVr%*~ZjX4p->woG(;OQ}az*_x1kRprwMn8ZwVBqQ1~6Ij(Q{-1u% z;8J$c#T}x4XAG`!*LpA0{iGmp&-Ad{)AuFy?0#D=-B7j9xn=XfPdjkK<=3`%Zr@t^ zBH($=8}*weuJh{FSxFjf<=ezoX{MMv(<96%@pyY-CZ&o&t4mKcc L0p_B5RuB&W0u80x literal 0 HcmV?d00001 diff --git a/tests/AxisInterop/clientcert.pem b/tests/AxisInterop/clientcert.pem new file mode 100644 index 0000000..f433d48 --- /dev/null +++ b/tests/AxisInterop/clientcert.pem @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE----- +MIICoDCCAgkCBEnhw2IwDQYJKoZIhvcNAQEFBQAwgZYxCzAJBgNVBAYTAk5aMRMw +EQYDVQQIEwpXZWxsaW5ndG9uMRowGAYDVQQHExFQYXJhcGFyYXVtdSBCZWFjaDEq +MCgGA1UEChMhU29zbm9za2kgU29mdHdhcmUgQXNzb2NpYXRlcyBMdGQuMRAwDgYD +VQQLEwdVbmtub3duMRgwFgYDVQQDEw9EZW5uaXMgU29zbm9za2kwHhcNMDkwNDEy +MTAzMzA2WhcNMzYwODI3MTAzMzA2WjCBljELMAkGA1UEBhMCTloxEzARBgNVBAgT +CldlbGxpbmd0b24xGjAYBgNVBAcTEVBhcmFwYXJhdW11IEJlYWNoMSowKAYDVQQK +EyFTb3Nub3NraSBTb2Z0d2FyZSBBc3NvY2lhdGVzIEx0ZC4xEDAOBgNVBAsTB1Vu +a25vd24xGDAWBgNVBAMTD0Rlbm5pcyBTb3Nub3NraTCBnzANBgkqhkiG9w0BAQEF +AAOBjQAwgYkCgYEAhOVyNK8xyxtb4DnKtU6mF9KoiFqCk7eKoLE26+9h410CtTkx +zWAfgnR+8i+LPbdsPY+yXAo6NYpCCKolXfDLe+AG2GwnMZGrIl6+BLF3hqTmIXBF +TLGUmC7A7uBTivaWgdH1w3hb33rASoVU67BVtQ3QQi99juZX4vU9o9pScocCAwEA +ATANBgkqhkiG9w0BAQUFAAOBgQBMNPo1KAGbz8Jl6HGbtAcetieSJ3bEAXmv1tcj +ysBS67AXzdu1Ac+onHh2EpzBM7kuGbw+trU+AhulooPpewIQRApXP1F0KHRDcbqW +jwvknS6HnomN9572giLGKn2601bHiRUj35hiA8aLmMUBppIRPFFAoQ0QUBCPx+m8 +/0n33w== +-----END CERTIFICATE----- diff --git a/tests/AxisInterop/clientkey.pem b/tests/AxisInterop/clientkey.pem new file mode 100644 index 0000000..a47f923 --- /dev/null +++ b/tests/AxisInterop/clientkey.pem @@ -0,0 +1,14 @@ +-----BEGIN PRIVATE KEY----- +MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAITlcjSvMcsbW+A5yrVOphfSqIha +gpO3iqCxNuvvYeNdArU5Mc1gH4J0fvIviz23bD2PslwKOjWKQgiqJV3wy3vgBthsJzGRqyJevgSx +d4ak5iFwRUyxlJguwO7gU4r2loHR9cN4W996wEqFVOuwVbUN0EIvfY7mV+L1PaPaUnKHAgMBAAEC +gYAZ6UqtLwN8YGc3fs0hMKZ9upsViuAuwPiMgED/G3twgzAF+ZLWQkmie+hMfCyf6eV200+pVm0n +Bz/8xH/oowxpX0Kk3szoB4vFghjU84GKUcrbhu/NRIm7l3drnfbzqhQkHDCx6n1CotI4Gs49cDWu +4uEAuxJkEIVY553unZjZgQJBAOJVIallNKmD0iQlvtWRmRzpmYDjt9vhNY6WBTIOx6SDn9SRaoSA +fkipQ2HXo04r78TQ674+zfZ1lRTkFG7px6ECQQCWUPHp3pSZOM1oGzJrNvNaw+MizZAZjq34npHm +9GRquFLG7BlCaI9QNGE7pN2ryYsYCRUMaM2e4GR0tUXxVGknAkAgrxqFU9AfCqI2Bh1gyf3KZxF7 +w2axofwR8ygc6nV6FGfoUneHWubhp0/LuVAj4cRmL6Vbe8ZSaPh2Y9lviuMBAkEAicP8Q+1E4j1m +PPEYP51oYprANOiUFmhnWEL00+jPk+QFsd03tV6hYs/vAbwzkjuwqMHCMdJoCiH8z95IEUvc5wJA +MvLOuZdu4dmhOXg/YKsbMSPjFNEVskLQNSXqw6O2wIrpPg1NQvBBAOTbiuZj3vind4VPos1wc4vB +QocvdUC6dA== +-----END PRIVATE KEY----- diff --git a/tests/AxisInterop/image.jpg b/tests/AxisInterop/image.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4331bf5c632a48e5b8d84a5609cc04588769102f GIT binary patch literal 75596 zcmb4qQ*-wkEbFlgZrq{)d0v*W3NnUA?Mm z^~3J2-Mjy;|J?_m%S+2i10WzE0J8r&;O`bd0sspQ0}BHU3kw4a2L}s}h>nPefPjdN ziiV7ihmDVqhmDI%NJ9OEkcg5P7nh8QjFN_yo`Ig=3o{!t9UC7p!AmHHO5aAKA z5D~HH2yqGN{?F}iKL7(BQU{6*3W5Rvi2(tH0r7VjKnwst!2Fl(|8LNcQ2+9T0KosV zs-puSp#F`aVc}q);o$z6AOMh1&=@e7ZSv@oUWk< zR1$8fcw9iUu)#%c$?&wsyL)PL_Y#^?jr7BRJ_#WJQ2zq{uZR$kP=J3s^v}kR0f2;n zgocKOfrf;D{4vNG6-%f2#?sUA9tV=>>3C#Ex|-JPW^ zp~aT5vbe=+p)a)@#EgPG94yJCdaPwOH-u`H$HWQniSDcp|6T@A^gTt*T#7_*y! z$jU|C2k_-rK`h|F7u4BthI>?$)Y%i0Nqfqojvv`Q4@ZPs##HK#ZO-8@2bOlEQb!~D}zF8Mt@&eoD47KusH-E-H$9MKU<3$y) zzS(z^24KcBH-3JdK&&O^!cWi$0p21vGou8X0i>nG+xE`qw-uiie|h!AbbTn0mi&F# z$N6VaC`%I5Cr9Go`!8wJR|#>L*mTvpEFBZ8X4a=KmQ2FN0tPS zXfl2wlE{_iZ4jG7$HI*v+zPa-!SQf&U@C#q7ThV6Zi?(%(RNXvj8`sS2<{&{X}~1} z^t^Ku7ix(j%JDyu-xCx?7V+qA93zN#cE87ulT^hDC6=zR;TY=}O4RnDE$LA%|)T#&S zcf5vhTkX3iE|b-Y$LTFpTV|JnyPX>pCd;WMg0AVb#~E5;hZL>??MK^?3_?;HA=W5& znqbO=6n6eGA6(4@PcF-MylUTKZa%8*kou||3zn{gX4Q60>4?PbGSc4((gk=H3|RtB z9&RP&Bd_}zKvnbbHZozaqFuQICuZ%@cI)eu7UmFeU-IdMTb^3Q1H+*PG z7kQ}?74YpN<}veA5l4lMGZz-967cH5i76#t75cajAxF{iBA<#a=BVSz{~wqc85-$~ zF{MMw8tRP69;Xw{kkRH&)tcpC<_>;OL`pajnsmaRWR9Fhm(HYIz^6}m1L-Rr769!n zV8;&#&*FDv-)Xwl^WuMFG%vb#02ci`znYqv^E$i@HFF)M5Bz|_p|%pMc#gUm9h((fSFClDAxoLA zB6Ir>Uy-x-J5GJ&_IL;k8gWBnsn9>g(DUbC1Vki^$w|qAvg9X6INtw}oH|y)-%Q)B~2B`oe*rcC#6qY9#qADd?wIKXtX8_ZN`- z68o(SC3Tx3&)60k)_23^{1Eb2nK$WS8TrQb_cBKxM%pSrscAJyk2{wJO&gaiaJ{`^ zLdfXpIKjQHfdkcXqFCKP9jmZH;NGeOMkx;nP-0H=9TAg{sC{J`f>fz*1l@C1KzI8> zw&C3~#iiNuexv45Nz5T{99l+%qQ4<>CA{?y0F3o?95i z{tU)4Uk$z^;n8xnB)9mQwR-Y$M{OTu?YUG9IZgc!*Hd=yQzt7b>S>{6OL~136i%Be zGbZYdxMsZblJM9@)yr4e6LYSmUfZ~K+e|*|Dua2~Z)oC14Bg2>Q5(a0Tgj=jy3k23 zylq^9yJNaJS3cOI97-YtwBAq-ID~4;8}`5&+X8BZZSVAL*AC=Eg%4oLE!fB{&)!g< z+L}tZ#tny+-cdiSI=p02Ye!KbqvRdUx7jF412-=wrz5ygqLe9Q*j2f+f+m<0Eg_ZB z&iIKo63dTlL!&gyH71&`v;6&Dg3GR&!llrP)Pl#1$H@NXc;iUUaQiFd8iaf2kXSc_ zh(hN*y=rU6snDK!Y{YgfF_+#kttpQH!$7l;J2hPyb4JA`Bg$0v8jMcF=}>6H{KV#x zzJ8)YV|wDIC~hdJ9IetsMSENvB?*Mf>b{#&tTvwYfumjP%tbsDTh9&pD)X~=)1t9* zP0WpXt&K77tiyg$c`*2-%oh76oC|3g?EF#=y<#S>V|Dg5$~dD^VYVDp?zJN?11vQ} zWQpvQJ;Z`{A+7j~5@{eyo(kI+>+K#O3%C=Qw?Gx=Y$U;lzc%2`wRzd$h}qAt+5r2o z=ge43-pNNDcMHLDNPL-M5$`i+Exg=1j%AE=k(E*2>GTh?KTWu#+vJ^vFjd(P5#?~@ zA$(?Z2AxXw4I!sik?NUbd6i?!18LX6v;VUo-sJg8q7I`UTY~(PaOQZaXT^ftgG4X5DFxe;CQ)>N>BGG3h>(L)P ziZnzx`L8#>7(`bk{~Q;`BnzASTm+HkS=m>bVlcD=ED)Lp7T4V+{ z>1u_1m)weJm~>~6I$j?5I@dzVg|DfTi_DeBr?dkjM*{!rhn!NAxebggWlf-MFgrc7 z{V^KOT;P+iYhT7fkLU`lVf*Nu#!r_1LT~ zW1rz+_Q|iDP+e*L7ocAyEGXX8r$lvQVz zA@(D~N4;AVuiz>FrJbXC=j47-rZrCN1$N0JN_ly9+Rq8(WsE4GjFQsghF-^`dTA4A zFCD{KxzgE*>DcU*@dG*z=Q##aYflfp+jgHbp&zliAY>gn_PNu1F?uDDQ!{yYL*Q>Y z%!(ZtHqWVcYw47zcK@to+vl@wx0jkfBoah$kYPSIN<9p>hTvOT+KzOlXR8=LzEV&1 zvW`qLd{G&*y8w<#RLBAg^zG=K_bH(?F}`lRx1eBq#QI-Mt5fs+!_jmFq$hU4$V!- zTvhp&vfmrws`=}E8{fY27Na8-4B#LUD|fPtuHFeeNwTl<*lAOlZRe|6k|>jKLiEM9 z0!_d@AmOVTY)sjWyduOk?6k}{s2_k%6Z6FJrOpNzPh66~={P!uO*k%$x(>z>62ar$ zMW0fu!BY11QKaZY5dY|jH>0H)PBUu4nk~XLrP7rAFdIE|{p1Yqjpm009N|tlpQ~C1 z(Ir-FJPvZ{t1v_dTrcy4CpOaI4F}LBdRYcHJfQ+kj~__gobj~R^nbiD=(#;;=*MB!JkL*gxHVow5~0WM2^LJ!;( zm}Y!;9B+ScudvwsmPU3gd!5p7fpt}F1s{dICSFDwWf(c>~f*=SNbmiv#Ci zZc~%6iz5QW2jZD^@AFT9#)yCc;qc62W5$%tf-xy7af{KmcCNj8{=O>vRGcj)Q|4t@ zwQ>#q!HPT54;G^lGh*izsyOVEMFVi6&W5*bI-KTE#Ywz#coq^SM7w;4(VFilp^^MQ zq{lzJ&I&ec9ObvzVr6AA`O2hA23-V>XbP;lbKlK&Xb0)3ef^5ulI4ax$`#W@m{{_76Lv|ZBanYP|7_NrruM-uOL!jN+&(Ybkp$62JO!{rPst>mxBc3tNs z*H>$?ZSX;vFNgB_PBS<>*n^z9^gsHb+3^lLSBY+=rAF&!Q8; zZ?)p{NIC2&IUSkO*IGBzGbO!}nYz-U+8JvywQU8nsQ1R*XX7NmfV_xj#Vyp*o|+`;eHF|o zrhwOUFws*7onh{km#SC!H>LA1tRLOw@GVE~K+7&%1DRbTRNC01QlXo!4a!On0eGUx zTWi#l1DrS8o(-fMebo!UW~k6_b}k(*;J6#*=qN_YO8e2_U>l2@O6nOx*wt+58ZSsDsBUOg_O2n2(!I^8=8-L zfn|z5l^`@46@LQmsXL;2652I9=L)bNHLTeaG;SL;beE+>cU|tDB!rLfT62m3)&b^I zSBKh=3=5BrZFg_3q+Ym~y0DeC1decG4n$y}W>(~Ew84(EUhURoq^CFW|0z@JGa|(4 zBuVdY#Es{`O>}_9$-AbRl~9OP+;m(>*}aCwO#FgVmuKFPeWvAOcw^pvIyJxkj>Cb{gteD>}m%q;Jz|uAJizt-V3Qm%6F_z-`}S{%<;Np zL6|2W1gse{pLRn&%u>$c5~V^V)H$3b_n+GIaO^|unKnriwItRI>oT$(rpoq)01zGP zRy+qMm}$MBixyWxxdXgdSyHMx51=Lhq_CHXgg;#5XIw1eDJCS{3>sg`%}61f!;A20 z!I9z*&ujlEkRL3pOlCC$K0aoM2-*fntvX}kmvX&vAsVT&k?yfCtA4$2{q&nlB1wd*osNS9`FXW9vlV)6_G zj1mzWQ;M<(GM;%5k*w1Ntp@%j>iGJW!|$}r-h+}92co!U6XiCvCJ*auE`CGWdcknI_2U`4{OCckwu|_d(g$;W4Ng{tZjPj94MWqNJTX5Gwbb|wTr^`BX`xjFobZ5lM$klcP!xSp(x@6-S z&zH21s-MQieXhH*cRJo^ruoK(7Q&OABVp19HIDvxhM`{j3(d-Ce-bIn_6V69yrXk%y`5 zzH!!;Vq(NPcCV=CrU`J@c)V^tW6WX$^=OUL$Rs>`@l!w5vLqxik8-NOK{>|zVn0rM z0jEM$dUR)zjZ|!JKLeSqVW*?epQ$!Tdw41!O0>COF(@q`Bg$P}!QZn;zWhZ~qKh{d=qX#6_}<;Qj7aaBS^ zh>!(?ot7n=@s(Zgs@24Fm9H-kY$R}iF;~D^3oHj-P6^qRUOK$|?vcKXuSCKq5$?jX z?V7aWqA|8-77L0SvaZRo#B0(?(1k@*!%}|5aPn<4(MQ9A53uhkZQJtJl8^s}Qtt2_ zNI>bPZ1!XGF+|rY%Jm3tPv784Jqh&`coVtm}>8T4bTo zYx5ea;)I%%QBlU%6744Gxds+SBFqXVvwxMK;g6v~~|(`nF{ya`s?f+vE#v z6W%G5R5F9+)@*@&ZUQ!r@uE?Cj_PRR~%R3jzAwc&Pfe zDaH1%r}ZIj)&30_F4d1_M2qYC2mnBrvn<%ij~_d6b}XP9mwiJFN(pGC1>(rldzGb( z%n}p%0^i zB@fS8;-f;HkPl}%V`7FzNHzG6`tFxvQ0l4khna*9)pWGY`xyaTMs_S&sRP`hG_MBM z|F~C`PC=-x~PVob5!QVqsL~kBdCg)Df7gp z2LulX7Fb~4feM#}}8oM#i4 zZ7Mj$y52bS?*0p)m)>)(6}-`K&cRH&bQx(nZHjuyI=IhkNYnmaJclzF0SCs-19Hs0snhJ`8Fel z#HCT?)Zz5nH@>s{jKvd}MlNQi0D{A;HB?fPE)kpRxzc!fy2k)r%1-txmlwz+L4|f4 zf8meI-YLJyG+uJ(o>L+#qcgU2wYZ9>(b}K<0V#yZEsFUjW1&^%r2{D1xifIeVc>LjMPb zPNhhxw8lx9xpp46@wQo?INo<|H6DKdXJD%R*4Oq_1=sQ0R0U6mdO(hOAL>!BY^|bT zLWURF=6kZz`p`!)orv9K*0)LQ<2){yFmrFw!P``tICBz>tN80O=lxZ|<2FvzQy=v% z3uh6}F70MiG3aym+2!SyUP4AUPsk3CJmF!`phqu!r$cCs3*cbI>+~&}^yvqIlHuwo={}y&r73k?t=#hPauBkxI1%^+Y{{A{ z$Ir!Gx~pCF!v8u$X$pNi`*0{641*Kn&`a@7VUk#wGj})(T(o0)9KIOn$YG8yl*WIq9&f+Kkt7Rk*YQ?+>N>CA0i++wq!y zP~pwM!FP*ppU*Q$ui8(^bZg>{5_9MwMOO2W&Ucp|POCAC-x>W`q5viyow4kmjJ3JR zA4c&)PbabB+8ujRtR#O^Lwg_9hC~p-Q=HOgGih!mx^^dC+Oz_o)D55rwVBm*Xz_X1 zho~wPec-pKl#QY&78Kgy!s+M_kVNx0&5rTcd91_yV!$t4Y(Etccxh-A>Y@F7H*H7llO-k%_e7#Z|b-#DM?Cv#L>g4A_Y&R@YIeYcfZfs?*$ zuypfIRG6)tz;h_PSyJYa(T-olP3CnXS_Ybcn382>o#zepI0*zx&}^G11>@BK(D5nty=AgI9-T|sua!U*OH6RbOdBxhs{&Eo0B*a3g9 zGuU)8f04_|yTG@K=VW;GX5CsrG%LGPEo-w|$c`KAP1R9Gf{4pTcyV|p23OzXvGbpb zC0GHf1gIzP6EBnkjuh*3W9dqpiGEOymLp1|1E)*tEQ~OEL5l^~M+?xZn$a~lr$bO< zZ@P`AAJLEhE*Gd6RW-hLh&--nE;w5SV)cV`|5Z2D)f^T$JOcy-0vn>(l6f0MIOI!` z67;fyS(0p5&2->u?^^L5`$A@^h^b8t)iK?j2D=?&G~v;UDja@dn3M)Cu%~!IMv@BJ zUjRW7r|sDmxw|(tqtvb4Ux1{_b+fMhHg2@{b~O?;nX^1rm#dXH(+x0gyC-lH{W6{* zriUZ_(euHwb$ambvXF-JQG=3kw%lFuchTKl0PGB`q4f4z)=s*&H)U|xduhqL+^CtO z7Vu!^FJO?Z_P5}gKIPlD&8DHRH^D3Xj&V9h;=)nNNai|3v8!C$tIMQ8wny)v-i~2-(XHybskWdQt*#ek*a`D({N6CD7`vgD zIIL2tfoM%>U*+4}WI?-TTCF3Tq2lan2?Fx%OIkoOk`<%tT>!Lge!-^Hrf};f$Jh76 zhM?iT5rLVWO)VjP+$t{_+>Z>BIv+xT5&BxPjaiBGicbwvoG<0Rl=4aEf;N=Ku+_BQ zgNuO(qkrfeNOP*mMbNn|r_r{FFQG3Deuo4>bg)t@eN=vDj(W!{E^j&qewnyf!g}js zy~u)FtPoiH11cHk_ta`@s9nb6_4ZiIrlKOQZ{y-8{3h3-$UC1PPFMa zq0(Kv@s!srNi?CX@rr}*WuKq5iW5BV*@Wd*5PGsZs35Nf&&+V}% zD=0(BWShHnZ_iBnn?c3)YVgJ>S)Mbc!s6> zeyIh`d|M+0@mi#z;x)*XJP;*2a)wV6yp{myOC%-#%0Lp46r*VHAB&htB+DPwem(^9 zPD0nBeaT3-92wtmoJ76PSKQkC+BZLKI=t(n?Wi}fP9^esR3AUaPBS*y3h(`*Xhkj% z8oSSwx`DOH1#2>)3BR!vE6p4US0SzyaFQINY|>E%iY3AIhj&5??7Y0Z9z>~~YP9rJ zvK-3Gai=15tBT`W8LPH&cHzbVlwavt^1mDD_tN zf-p-8*d1yWVtx*IB)N1zj+Jb4;h;E9;I#@=4q2J!h%IjVV=Xrb%BGM(-yNgnHx0bC66~Z0sDBZY!y7 z#iU1@d5*XC+t(ehj`6+)OAo-=zBIFa8xd@#G%MG`Xhd)}D{`{jigQuaQ|zsa5(6E8 zm@1|=nqOtRLS4fU%x{oc$$xsrg~ew6X;2Q9Q>UbK{|J^ z(9F@3#`ei$7ldOt%GOSn{f^yJX29A_o#5TdEM!2-NYRv@hq=hy7mH|{g%}@txLIYJ ztv7!-bqb+r>|S4=E!?^1ViZABR-f2>TaB7(H+kfmQHAW0=T`d3k@}VbZUg^2^Xv`I0(~RC1cRXFxud-4&kd zuF(_znmJ*RzWJe5A{onLj(+Paajmr6zq`Z!Qov-NK1O8GOD1@AJGQGaxAbOIMy$le zKq!W@SJ(%8zA~?YlG`zsG{1&{t8g{4O|)8vaHj!p}xUo_i%w0 zTXoF?y=m1kO=vVENq1-#tp%2Ee&EKdn;yd4cvGx12a1_rs)+pDGd{U!o%Z;KNGjD{+VOw7&5U; z|0jp}mW4a`UYpj~eMrJJm5t%s%(cwZW}lf5)%2_DzlztsyEOhznahy6h*nR0^h+?b zJu;f}mlA{|+y-g&iS*scr|STD$y5u32C}p0#NdJxZX8uIfYL(Bax<6FfjwE-e`LLh1zv1Dhg9|Q2iE#1rwgE_;+$O*V{Smn zU&0@`Uo!DAP-CVfjBvoBB8*CI-5A(KVSr+t+ilEl6>Bvm&&*_?E1F$oQK8S$;ZGsW z7bI&1EfXP_gGnLSh*_tpqvEU+#=hc0dE?3>+YqHOc^{+2y73*D10~A%Q@UFeFd8>6 z^3Ym4sS!{XI!Ra#r~Lo(C^#BjBEFmry~-CDSVrP1HC&N4EJE?`oLf9k8Z@9=hD)Q-(+HE;Mt z4j$9E@$!1dv4k$7#OR%Vh3GE;G`cMEGS}`Ak?Wn_cdGdrcy!!JA3@77w=4~KH12$K z>avZYqR^YH5^@tISca0Y^PDeuE5fggO>nKDQc4!x@gb4BQ3-B_bp9NnZ^R0`PID5B zvuFMRojHDoT5OlQ25+B8(T~}+w_}QEQa+58{foM};3vD#YDaGCP#ufS-mSdbNHA6U zc!x6&sm%g*q5!wCt_O)-Ez`WiiZD?^r0TqSQAx^?+O^E$fmKB}y5m;t2~Inu-l5bs z*KuqjQM)B7Z4MB-UDoVt@;Yg5>6QYq>3qxFg?L_d=CBdD111HbGa*tg;3D6zc1xy+ zIORcA@hBSwkyK9tAJwH(@%$OXU&R^or=ai24HLI8B-2uR0WV7=xMDcXQLd?El9FvW z@I|YzTwozRuCG%m#cEG$OC5zHLY4k>J2KyAf7+u*=l@ zY3Q$E0PeLRp-N)u=va}<}~j z_DP9Qw|0z8W?l!BN~1Gu7*`VOnOeFI3^!NAaTY0}$($XW&$ac1eJ3$7%NitqXb17} zd!Zs2$D#|y16isbU@Jc7*&DowCmj5bLtOH{)7UFhLm1lqy{Q!k6-a;97enzz*r=&~ zW6?OfLHw0W)QcL9(og=?fGabO@|1Tu&6SU3egRy^>P%H=w^xn>9iX|a8N7vX&6*Ct z@<<*(Gb|QxW^fb;yl1j_ZVf$=muTWE*ODHJS`NN1&7ZD&DL^mK8S_-uq(m{lvTDvY zXuIX<&wI^?LmeW*%DtL(d4UNS3{0{+=B-O*(E1iKd+S;F>@@VE05_D2T*o;s@>IVx z4l|}r08_GjRCnlZQsNb48W*yUsjb1uq>Q|40NK58RnEkUnw(WAm{MDcFz_H zXJmiKgjMvPY-4aSoR+R@I0Nyylso0ZM5hANx+Z|o(;`sydbOP)t<}BO9NCR%%y*dw zgzd?lz*8bCX%c_6331?etMR9*#QiB4R&Vt4`3c{Oo;uI%f}CDHcdjRXjPl10G_)cc zUV>vS@}PtPXUb`8Y$|?|e*&Jpb$*T?I{JL7lLc$kLQi9J$f4TF9p{R}Qe6$eiwOrC zla#}n^>u4nvwZdjzDEXW#G~I1#=z;yG&S9js&`{wd@uuG%(e1 zep6Tgn`@R+%u#Ag1KOi5LcMb zPy_#2c*@GlJxtdusY#%sP(mnt_eczn>n+8zHEa+g)=>$x*zbJw@56#`t`&lG zP7KZNGA^yo}&zQNCIu^l*y6Q4*wG-<5o*8FD4f-MP8u+ck#%&}V z5RI#}+B{vBG*lb5|8!1gd)jjPmfmAGOB7K!x+HPF`9BT3OTY5IOa}&vAjGW@MG=t> zOf7K=yQCa}ZVN&?u@Se5ZgfEy}lfA+^HNSCfuvEHX6q8Tg_r_extB&k$FNSAnGcjYt!=ppVN zCtH6FySW*}?XLQK0!^J}WKLX8ssKkeM z<+UWol?&e5bsNHiADSJ0%M{_xD1xqodG>D9b|e-D1%%ePneQ#FIl90_Z?d#=>Y$_+PhPr9>oXyR3{(|dlm z3$jfXN6t<7myexIOR6cA)>4~%Feh#@U;Ea}87`=3`HdFnX-$9nZR^~@QQ%1rF9#o5 z7`LG^TqE;k_%IDtTJV0}5ADWzE!=RJKBaN?Fa~rf_+?Su-^Vq0R}*UFO52D`XpQ*s zhr^K2AQ=qGnA-C*OaDNr1y%ZMf-`3U?P`g!1~=1#0LJKGvy}(+lT-96gzZGbxNHDG6uq(MO2(;^?|H^NJ`6t4*za zUx-9>GVxU_odk0BJGgH>bDF}{X`^>XD|}nf=+WU&{kwt;ny*y@lH+d1S}b0|0;MRv zP}zDz6877`_jDw#FE|dFZD`S%1Iq7F&JeBQgcqRp7acRbh1uf_+JsO>;@k2*S7_r| z++Kx$D)ul^e*X}Lz3{$8vmOhggMc?E3z|t-p-sB{Lm#emEf%KmOFls^)x5B~4$4f8 z-)tr>&PVgyMtNZxY1`#_lN<%_N$61W41FIU|77p>SDJg-a*0rgD!bZ*C?w-&53P!Q zzJIKC$MRpmN!M|7m?h&bs-7S**m@~0 zHeq^UreLPoWQO?KoNnkl<;^SqCknNF;}o$=>MIu)$~HEP<+8eZmW&FiDL)cSX+qvZ zi92|yFgfaVzC3| zJz7!QWblpe%ZMOlw)qq1;=?|pvHk@#^Wot9A>|7pgdFdu zok3&SnUB)rJN8P}@25DI@2MHBKt(J0AGH zbRL*p!5wDYX>I25pb~!lb~m}BC8)~@MeLgjTdri>US~#W%|3oGZL1Mxwb4nBW+A`v z#m;buO=K|;zV(`#3L}J=DW)4hhPFVJ>q{4-USWL_LFT&|Z(7$uPhalv`55`lM~&N$ zJ@L>P|H2}c4ej+u&tJflyQb&*KTK%IIz(veP^B6<$v8X+ ztI0PCR7m`hvh~yZZ1(Jxr7g0Kl`FBz#7^b2T;^@*gHUW!o{;QiLA4QMxTQEj6y}R= zo3fn~Mo3h0*C*gZD}dal>qj6+gKGsD6@_KlRS6AE=>p%|gJ|c%u-bFCLLvD)pI0{? z8E!L3ia=!f(DfU50{%m$_aFB)?*pT2{=#cR^Rbv9M%;2z-Pu1VjGBEwFJqn7r>nIL zu(h>H4}0D55JctBmtT#_A4)Cz$~R}syfS+B#_%mYexUBWN4%bdOk)T%-=c2j$|aZG z`@Vjo=CPjsjDbZ)s|oquvH1#L;NVqZbe=Eg7jx-O9x1nzi*G%$?P`U`m*3AXeZ+rZ ztw8C*;c-yya~!hkr%rsg%6}X@&&{}4PF%-XK0010Yd)>Fe<&T{jHhE`{>NbBG-bwN zK!7~ooN~pmjc@0MPv@H3kYtJ|JW_^6&Bg9s!GbDc=MZpV-=OM1tt7xF#S|v0g_LUbtk9-zL7AHyGnv7R+4~SCm1_E*8kZeH=}_}tM1S2HU0rL?l-Uku zPRe@X1<`Q}W7TXcs|oK;9y`m)y`}9m-F()5eXfUrkCx`3ymYOW*O?klHHL)GP*JIRU5=TWmP=^arlfJrCuf#t#?Xi5r##FG^7#vWAPP&>5~d5GRq?f%qXTsQG-6M$G8f6%-@@CDhJ z4a+28@L~*yiYroPK*i?EYX=#FO_!J(I{!UzunOofsT5hee{#p`)S*ySZlCZ>C=fp7 z-2dClLu*J5*|+b={LCLipU$FV^K#+HQ~c*uQu?sgemW(sYpx>>`x=8syJG7$-gqsw z%>4Zdf+txxZK}SB7qzh?3i+glL-x;`wpnv6LK zDFAa{$+7)W$h9G8y2;ztoaQ(+D29kcVm1-)(O?cKCg@c-Y_PiT){o-(GtL=z>o4F~ zE$Jlbvz$`l$*J*P=4HLl`o`3fE3ig5@`kihdIUKY!ZOKJJF>Q|lB!gF)#G~!%j|kx zT*8K7tU{o;<@aA|?Dn2xk%sNzpdXHV@0tt(6!RpExdSWS%S(d}&>Sm#&M{e}EA&h0 zS}pjo@h8VsGzN*&SN4`aF{+mSoQ!>Dkdu%OkKn^Kdi37AXNinRMAlck;yc^EpbRm2 zsiz#Y{7C$aQeAya$q`}D1@6{7wKC zy{1^UwO$lZW0`nA#a3H~?Cvh}@Al_%nxwR`S65c{=9osaB|T!3&`sIMMc-Y>&#JfG z*fLwrWe*MO7^$;ffW2j!K)<``f|%@B-X}xxP$y#^dE+Yb!>+8qU{^Tcnf_i{P-X8H znd0mz%V1HiZSDK{7hvu~yQsJfBLaD3seUj^dnY+`C;3M^lD~~FfV#7PFLlzerM+NQ z9yE%!V@5bH90Di8uCIPMBh={b7DGC$9kt>I)`A7;=599_t0pyvp*Vf;_@|+Kw<7cd zA6yHQJ55-qX-waC1I{qmdkv3Mzb9wuBH!6K@z{b**CoXlmX}6%m2j1jolp++@tSK# zdHL(8cD}Bft*9+`vWmH7qtx?7@P?`F~V{!F`>d3+(7;E2dE~| zR4croaC-S6KXX`V-|&-c%*DT6ptykX#`Qy!GBkh+CrsgBcA*Z}*rbj9U?V6ZhRhh1 zXCgA~16h~GW}Ex9zD-bSDi`C};+ZJr+n@N=!fl9>8%uoARPI%U5UzmlD@OyhNVJ$T z6*iotA$+zbv75xh=XtT(!FH#-RIRf!0=7MccENtu2!8=6<#Y;7$KM)@+TNIdF^yU+ z^Tj_U*(v|z59ji!IGG`~SXy2b+a)M>?48QsjfmYfV}cCg#3G>Bt=*kUq+)N7D zN9AEB7odR!?oQt`k|GD5E!$X@OyUU@U;hH20&rP_BhbD@{SN?jK#IS~W|nowYWsC1 zI_at5j_;AA`&RuvJ;VBa756kJ?UZBGq65X~Nw(ft^=ve)9ZC7D(x)A!Xe?v3;gnrP z&5XVO0K1I&g+<}mMZGMF9&U;3b*x7FhMz&u#8hgVMfoTmKjQ;eyc z>y^jEDy#)QsJig#3*|KDZQduR_K3WqA0&x3>l1Ldq>tr@d~CC*rm4&<90HPxj;29y z$7{=qZbp*b1>opMR}@+#+WbyK{<%0ml-u<~PqbB%=`7R8KQxLWJUXi0qrA#+Vbvh& z(Qd19)KFDA3uJ>M#bx&ew`QJS(#UGnxlfM0i1PyIq^X zQPhRRb)vN)Und}RQS#?>mj=V4wS7+e8(euN9fRfG;yP%IYQ+U&AfDxZHWzRtb-FSb znmf+`4ge`;VR!q)ryGTUEDe&>s+!S`u@HI@QHD2My6_fGwm?nD0P<2lRU2Ddbqj+C z$m_JY-=gP|PpCN1eLx>BR|9K{@fS>0z3+XR{LQkLei~c2Sa=~_hdHI$bsa)9)NgRA zklciyK5aBY)`IFshm+%VV3!Sh*>dKOG{M4wjE6a%)!7=6Gp!!wCAk9y^AeYK3tsAc z;LH~po2OOdgEWs>x+`W~&R00LK)yyaaa?Uf2UR+7MiIO(3>pEw!5rurkqPN70_|^@ zUKQ>HWfndPz}z04yY5oHuBB-fj1)7VaUzI_F*!iW=H~K6$1CRmG(32PCmkeA9oUDH zQ@xW~-s?#BByvR+WV#V&44O~~c^BrF^+U+9p2Mc7@grHiz<9ZIR74N&2lO=|as(tlQw{x5AN%Zz^GBIzAS3C4PAR|UJ4+Xw3NB<5~a8vUVV^d$Kv9kjN%4JWPE%Tb$k zu|4%*Z>?aGX`_tXC?DEaxLYXmB&H|J$!Th3dEajY%cOS_N%2tc$0OhqPR3Z^ZFKxZjcBcgh63yQuJ$Je1a|;$;ICr0 z2>$^5TG9Uih-du^u~%U1lQDv|!|*z0TmJw`ml6K}^}lcA4i@ZDh5+-quJ$Kcv+uTm z-<6fxrx~JY9*2j*)6QqP{^4YguHtv^u(}h~u9d$Bt;C;7Z`FSy!CzL&gLZyv?sdEq zhi4`lfJd=hquMq?Z%;)_8R{JW0R2RyRBa_p{{V84zxYUx#i(bk(wg-)?#-2GHfDJ= zYjb(0-Ln$K`@xsQt34LfSJ8`8h$9DnN_IEk7i#vS7OS>;x`tc*lZ!qUTQ(!4SFidt z+J9men`5e_ZP^iXd4NbqMX>mR9ol&uIz~z|EOQmLg?(}HsCoTSFygqhnu?mEZRa^i z=ZNc+R^LVd-HEPg)|Q|ikhr5E*y_=1(^Rx`bPjLCBO*ARQ5JmillhC3jC(PPRsiWJ z-sX?I_S)XtE@|RwM|s5=%HC&S81_1 zr@+zaRhVrncV0?{`~rQLZU-15yJ%n_Z*4OPzn-g>c*%W!h{|d2ukC;T04_kWxD7jl zX2z*Y`9#-A`UO3PVbnFHqKx7-4LoC~5szz$Ptw=yrVrl2@Oi5yzwabw{{T09lh51i zNCMhy8a6a~q0*1gu4z=nRNCO4Xa4|7FaH4PrR~4DJw>s&Sj!~zS>M0y<}bI9(O9Wy zX{i_+PfrYBeg*)ts-3Rd$=!IHMF8N@sX;}xn`9>;&Vs|6E((LgR+i^HusDj|YK#8> z^lSLOLxxoLvN|RR$uZ0#?oVZQH)a>YKb9^Hyv9Wz`6?o zhN_;C(bUuc9l506E~~2vMpWO#j=C5=UWA;cuHQyIJW>~L7V$`I4>}7UG(+DV&R2ut zn3}d2v@dPfuCDeWnq38ocW!oHYH_rN*Rw$JQRft4I!Hk15*>-he_h2Kk}9JzWwhU! zgQ+R@)5hBqD}0(tuN+!2pue?y>wt7^y{I}RBJl#CkrQ7fFUQ=t_RUH&~Zf3&^-5}7p`jC}1MMH+6#3?y3Aokm{c{a7m5?Rh$HHB6SDz%9@ zRGYkz0qu>61TPq5aRimh%Bf;+b`tM32U(6)w2<0R0OEC5NwTqo**+lrN8Y*`#%M?- zQfJyK(sgKH94v-rJyc(r=KVM=Z@E=T^=5$jsHum1w!m>g&yK~phh7;%q|{>76^AR0 zo5&_RcLQskI;^7}qpB;ZZj%}94XkXKHSKnW_Em>UcZcYf$5FQ8JL0Vf;y!MZrvUT=#FG-6U?2w?0PSDfSZDSssvA$ig~lxT#&Y89JXbG zCz9rnMuz*;oY;)T z`lq9-cmuR~B=F8uR8q8^_qSABAU)uca`>o9lor3pxffI%=|+XdTXOA9lP{3o@y#@S zGhgDpQIrO>I2RtbvL5!{;bk>SJJ`{PQ)H8Qoz5LVPI^pjA)_n0xO>jxeZiJ83B9gLL&Dk@WIRb={!N(yx*Igt`XaF4-L}@`Sg_7t< zOI<6+edWogRX7gooZkaw9BNs6U&RSoUmIF=8@DG^Or; zy+Xw^9OCH!H{EUsn1pt|;oz*J6bV5hOVivzyN{7d70=VW;nX<--pCDZmd?*hH%@5f zK~O}4Ya~D3oBse3Ix5LB0@)pZ8>qOO4*aLui>DD}IYzxyYDHLNqu4e|*+6L%JFcmx zR?V1Ydzv|}mPeCuNigTOR=9gqAz<1)gu1?6?v=$LSIMoONj(Dbr*LrwGj5=-HkgBP z86F{9;X*jTkEp~XWTYGU0Fp5&ow#4fBI_vScYCuo=$eZQtdqRhWVbyPRO#DW9PLGP z7#M`sHx}|pIE1HhD3@W?d9wCT&C5mSVD-&zXH!XU1as5wgtS4;bfoHBEQgxDT;8*- z(G+Q>%135z*2$AcQg|BVdmb(!3GX4*%BMcvtt7Y~{yEt@ZHV>KA)WoRig()yRET{(46(4#()!|sFN<1eU!CF`^SHoIw-1m$nR>rGF%~tSZ+K& zs#(NvsL0Do)BazPRpLI8WV61-Dmp5HHG9#N_)S~Q*d>1}3#(bj>0`fVO&dVq2gwUI zKiU#WyMGA+$U2oeM0Iz$8x{vxb#qHUjE4MFohHuC_YCqeqsR5!f{z*yxzB>^zYONy z#Mjp^bKKzewa0k^#Uth=*?Wbh_bd=#RN=%+R^2y{Z}7S&4apIYFA(;B(PN^Omti03o75>HgI*byX#P%}C-@^GC&wxJ^p|v#h^5&$Zq+tB* zGM{PrEceq|eP;{wa_6P9c_1GYIS4;HZ}JANUnJWq@?0aVd~H8=J1~FxfTL%|rk>(i zT_yL2z0UZmu&Zg@=KE5;CA?e@nxt@aqqI;|ZMKDkVk2-39t;UzSw8-;^3hj?K;PZB zis`CoKC?jaPr-`W8K%nu)sKs+jI1j};H;hf5QFfg(z`W;eM(QZj?=OXtk%NcOV)PXyzGS{s&7{&wN!pc_(4 zj{g7)uFFvNu)8bwa4EP(aA=UPtz>O1XaUL*#J$;{Atgl3Un|~EW_9YC6;qR>WTLGd z5zz=Jt0HNjUsn{M);L&`r@cc7>JI4j@JbJ>AZd|o!=h!TA*aPHlE%#d_b5^6H@l>; zO33SXaFbq^8f{=a(hYlC_l1-pY-~4ln)t4os2bhi_^olQx)UE*a?D2s!CfQ0 zRw$3GBLll&ntmuwfD_dW$l4Q~yu^cW5`&z5c{J#&uCJB)4HsKcEBD^ZXF=7X#K|IO zuJU}+y*inPg03~PKSC}O?SPSUU=YqWR>|+JqqW0Mofkg0dxfKEw^bDk5!e?x{S=)% zaWn&vo`_~0QSLoYJw&d#Bq>@-DJ6B?n50m)_AH1t{LR$WKiM|qpK6s#xlyGQhpDjE zM8N0ezg5|r1Wwi8l2AxG5I7IO=d@|)F*8g1&yS4h%r6mNG@Y=fS%R%wx~ zXi=`MeeHL6H3v~OO+$z+b%!#m7$j0O?YMnTCw&RJ8P#ye-9t(6Q7?tfc@DOQ;U**B zH*wKEYiw0UHY8n71ozIyp=Q7!m-lZh@hh#9yhAT?i>5OdckFyta|1*TUd~m*3*jN- z^6yac#Vk%DRyn7t(c)wGkU-|2b{8FMZ7Uk#*FurhOL1FB%QKKIb7Y|>ch(B=i0pKF zm?SJbj^$pf*~-W@YkNV_7|Ba!1+L79GVtNzmdShCTn`o5aQdj7-i6}b3T4cAJ>>SszOy+&iS@!doRV6A(NjIex88ZF=R_cgH2U_rR*Ef@l9rTDQkoHUr z=f2CKOG5YRHt8A~1EZ$N2KX;vRdf!-K*X5o zDfel>^HqBMMYpqe_@N`JzT#F>NR>*<%4(gy64$sgC~bWS2lP%yRYR_3z|r|*C-qsB zb+2u^#>c89n1j#WKGZTsUo-U*E-zz5lCG2czQWyOD7rf82FLc!x#S}YG-e>=Lu=h_ z4bXIA{%7hoMJ){md{zd!@I@1vIp}@}%r_T-r01eziRC+o=(BNMEycH$BSTjymAZ=* zlEYJLR{cj*V%3crJYEMpgN8A--S))(h{&h5l|yFzs$tlk7F}>yI%NK6}8#zO;Vq?rvoYDDr>A3&9HDgWg;TAcuqUelS)1`Y5-gcGsRvJrSBRpt zvL^9L_iWSMwiTM)@!OS!lQM0EX!T}me}n+DQ;HMBT{iql#Ax7$H%V-ej}sF^`sxrY zT++C4n12{m7Pc|>mwwD0v$OD&IhJhf zvVCI-`@`D2D`Md9dad?@@UCamh+COv&dVpHF!4Us<0}sLk>;yl--+`|16@g5n0<|z zmLW7-%!PNhhw;0`SK7{{EHLvcao=H+C&ck^aZB`w^c|s4A~FEGz3ZWe@fEnNWp~h? z?V{rHPg0cGq5MPIpqmz($xpH^-!xT59ITy=q8@sqsj6YVmcLb9f*o}_^->gVV320j zQZr*HvTM>~4j|bkO4z}$2iTf?Y`7pDQ)WAjMS^_6P#=oBJM7R`YwH~p1d>V(k&yfM zfC!1<4}^o>Bxn%KZW&yY7a-U$q#R-7=^0C@r7Vz)niShlBY%P0PZF`RzOSmzLcaK=v zC{sAwGmn}iJZ?I!7XiUx_(*NE&D>vf?_$P~0J`pC?^UMjfIZH(T~N9iK@J9v4IX7L zF=(~h>`i2OaU0W(rIFj3d=So%$~HRL4smyHPsZVLPMs{54jVJvGQ{!+MJ-8zJh&IO z+<_=MgJ^&vFMF)Udz7st6B`Fu4+Self_7vV{{Zl;6xcLcT-^95PY|Qjoa)9TRvkoK zKpC7*lCZ;uOz;^TPH{WUt`&;|!eXS0wR~mIJAqdWIyMS7IncB?@v`$P;+&p`mx;&a ze+s#j;y7Djh}^NS9LF9?5MiP222y#8Eka$8ma?(a)XW2OU99AliEO+OQ4<+1lYs+r z(8Va}aKpr4tsC95I;W$fg~IWd4mVuHAc^j8qr;F62V1EsxSbYe5E|jW=m`jRgxvI9 zF|6LFWMtdD$b(25AueNgUvfaEiNKD@x4#uB+2%g8ldPpA-zf1^P=-k1%ifW#NFw_C!rm&1AnpcL(Z-2t4ELDil5}>YCX<%{Q*I&a$W7+%_^|c|c z05oqPtEDyp1Z*9IO(TgRNqtn)Pua3bA|!8T1;tYo<+0{g@g&uwp;^RsDE3ZuG`D#B zuHvZG)V_}^qL5kRLJHJt=`p%`U??kuxpd0*3){s~F(~RLo!?nS*~~P|>kF+)q`I-N zb>}ZMlV0U{@Wdm^K)K;%UBvJhDkaVvklNP+dbH03O_RS+%U?j-xisg~G~7060OSLH zYq}L=wmn!)t%{r*F38An?>DNJp}_!cwGdeJxQsb@Ww0$wNz=_Q9Hpoi9d*| zJ_~`>K6vHG=Cm8$*7&YEt-muo{7d*>5=F|IdU}}~XbHC$>H>2Vs&sU(Wi&3DR|3|y z+9Ii<5k}FGp!81Lj~;7^`ky}K9M?$8WY~;DB$WlR=Qnp}eS8&mdO0a$ah@_IajiG70q-736*Uii z!(E}`gAj*mZ*=ZYp$e57(jerxxRZ3YX5n&)Z8)~%;~_hPQdokf#-q>45f)*6Q}NQh z!ud*(rNU>M)nl*VojVteD*#3jkXUbR)ZHe{)VK`H4dz{I3zpho8OT0=CL1|3WVpljSXxi9LBIx^`U zPZL4jNYNf-Wxym|K<#wZKUX=F%`6{z$CpI~c{^O;rz({umTk=I=@JO(aY$yvgmFf} za@hAcbyQ4ZvP@u>&Kg+ILqkQB&MXX%kV_-vzWl{j6yj3oMVLwss|2g3W!^W9z^w*4N{2XRVIXlQM7c1*BhPB_sIjrRT{er{ z(!$e|xsy5<0zQn05t#j>n;1N`D?n{Ut=X^+r8?Oqk=D#f=dA+2$^l9FsMLjfOHmgL)Z;0gfo;(ajq32~`_CM^L~`n&ZW&$2v_5 z;9vuNf_;OvAjAU#IhRo-P7+@dDmaeDRmRgTDeV|Ks-1N$O)JZ0W7m>+u4`Mx-Mp*x;|>@RGbVFdXEaNlARCnOZhG^m9@sFT+TYhjVy^ zOvd(e62f2ouAR*s?6{>)X=$nB)xy=Kd#wm(yEhceoPYLHgm57>CcC#TSvGMoO}XN{ zNg5KGX(O8Bi77J+a@ZwzAidf-NjD28dxBrpRvx%s-btR~p)xE;?ulKY=%j`gFYAQA zs=yjZ5y>IA6C%Wil3!>~1t!ca6YG0Z&BDWN-Uz5jWkPI$Z zFllXI!cT&1tg6FEg>NjWz&a_&@CuiDu{->wI@}7RY1q5iQAl-$X^0o! zthTi8@v>0a`pys&Ka<@c#gjd6X2P)(&y& zl@2NnRnV)|P>D6MB?f4o)}1cis`|=GOs3CwhE$m-X^VY*59KLcSeaX?nblJkJ3vBU zM($Ty9YzaDCwbD3@|DQ&8cQ3#KLQGrsxE~-5`ZFqBAoPT2)JvHlF%81LmPXB3-dQb zLx#64+aw_MHc09&=DJ3E6%@wAFx#p!EFO2XS+*+i9R^ZEdooWP_e3`hb9Z)kKbR$T zqukDE6u59nZFlL?=(b=?ZHu1bdJ8LCaOnxKEYtHM;J|9;2eFCI{t%JZis~z?81Bln zctis)aFN9s6%0(#v4A)bt`xf;Su1nO=pz3B5&HX-j5{Mt!Fr5Af!w2Q{wh+&L3JT4 zWhK<4#Rgwhwj~hQj~&ZFg96$5a?<%7BZlzlG(JHePbL9MPi0R`FU_U@02R+XW%frj z@$2P_We*O+>S~_W!3*Eu30G_7WzG+)3+xy959t{f7s^WT7v5uNcb1oDUpHe3u0c3S9179tz6Pwh9q7g*G1D;%bSA8~; zEm+-CI`VrWlE=-jh9zqc**EuHQtf#Xth88M6o6!4zk*4{7<3IbPRCl;Z*P*6r6;+^ zR;_BDV7ps#^6M=2-z~Zjx7?09XM&uoUaoxmkpWoxv@m&*w|Pn>9%_kmO>j&;5vn5pJ|Irg(WOe*zY4% z8C$eCjPViLV-$J(X6xx+VLM)9jSxm#KVl9w6Br4h@0Q0BWk@cRv3B=oLuK zQu~=@d)#-D;*uC$Ymt6HU1u3R*_|qq^H0#eHx$|YJbz|jmBVpkKT?{g+Yj~-wXOWd zf@<`6vAnYkIEo33zepN0g;|$=&_gccaCfSqP9h*Ze0>ayp)=?)CDHoK-^w zhFFRH;OT7@j+hw8abt<*LX@JB$6Ayk+C=0;xLAs4wDu*9R6wnTCerx*_g+I|{B_Dee#VMd13qTkD05J>Hc$#t2=e;a# z1*UIJPoAQ}=aA_O{fa`LXR4$Y$Q$CmgNp>%j4oSM*LqBi-ZK|8{{WPJsi`)zW3Zx4 z1`^2}O^#-^-ttemFEHPhEr_vMzvvN%&3YVjt_@%SDre4BrY$*XakTE0^y zO;nC2c`hXTN(9}`CTZT_e-XOe8j|x%(A>3Y`RaNr8zc6&WAMg3v~*07;OHE|uYn|= za$=|HNu9w(M8>pl4~td#}H|s^b%hpm()BQ)a!6cCESUZmAj(j>x?{H9CG+q*`H~ z2EOiaC!xBwY`)O4j-W8b_HH)4)>|GSu7k)aDm+5(13ie~maZL52Cg18BGiqKAIF*^ z29SqLcUbOJ%yVjS3Ys%bB-R92L#BOvAdi}UBnp;OB*CDInMC8p+Q0XNadr1 zg(j0%`;g?bE1cnR%$uf6f#bTsZOB;`>URd)xGl`;kji#kmpuOfD7VFTDI|JVK#eah z8^Gk{&hwsr1B#PvHfE66obIkDx;iJD2DT=608i?(tdaw$k|3EwJmirG1DlSBxNzXZ zd1?i@M%>o(Ix8g>&s0^v)skIwxUl!Db{DZEWpznxV~`g<)2imFic6`~=c!g|94eNHqqC0gA9>oJdN_3VFFvJr zGsMLBjH+oJfH6!xxP>#Jl(SvOE~Cw5J5Pa}GN=c#;oFE?Dye-TWb`&(?z); z8oE}Rm%1jmD~-S>a#R!njXAQP5Q~n^Co3)!jckr^v}WA`eD1?G0%%&|13)8)N*H8M z&>LHZI z@y_F848?YDEo{OkWngi&&_`!32{Mmj)ApMUYb5RB)vna(xmBl+DJI2tIuxa?iB-T; zz3fxwz_K*jL4O4cEJdy@JHoupaO2(bRh&iwDqKTtb;pRqMJvS~ULu5Jy)J*qw1pI1r+@=Fs0^U++JdrbZ1PbfICp~Jdq+gK+sH6 z6UY!QwAyc@8uu^}O&C9!);|>C`ZUA=0nk&lej@$Ib9DZpdCDJBGpcEs2(&Ru#-0J= zRAf(+QrKki?F!N1`ZSe{%=+pW?ab~|7!Jwd^afFhONG|vAh7yDW>04IIb$iMb*U-i z=%n0T%$=T)@#>Iy7oVDajh}Iensq-lwhRkr_n5HJ{{S4H)hV!v~2aN1tCy zIlokgNHY^`b7nb!Jq^lVVIltRKOl8mW2kV3_nL5^da$4QlG*4-y3>MNtUuLmd;b6s zex|xXn1fgY#QF@I>$3&w-B^N6m65Y&*3WBTwatqxD0c`v>(k zYvum>80#tQNbH_X`l1;^^4o|cK1FJC!B!u3*Wq^jvBndzI~pytHT0x%1)n9k=iA)R z#2v1Z^?$*YM?)YsH1dn`2jrxnz$@k<@le#czre3mG2@Os>WiPtFdqfTV#k_0(mBoR zHU9uquNg15s3nb)S61~Yzq6QzB=_z^K|t|0bi^chD&+ba!37^|e!rvCuOere%J zigtbAJ)tYbPr4D}r)|+%Zp_!`%4r@wiP<(w6Un73i17+;Dq8&HcLTpRRO_O|Viz?L zx52<6%ul)%;%Z+>BC0lD5_5yKmlAOwmKLeH)mbNB7cH&m{{VrgHWjfO$x`V$4)5quH2WbF9805x+_RSx`Yu0e zU||-lk=*Xqh39R38%OVEC&XLek-Dstc*zAtvvNdFVwoBx`)f=caT|#0P|H)WoIG3U{sP~!Z*~6wx4eHR^+{&OGWU|YNB;mQAF2{f zxq$kV`i>-~UAJHjDcf{ZYW5vN8~w2?HbR6R^wQt>hsiw-$>Gh^nO*$DYhQHnY7+57xf;V|+uPe~V{MMUGIfyem5SE5kI z>vm-RXW3?NtZ~13-en0qx$WORda5c7?#iqL-t#rs>0--xnHPxFHux$%SgIpum6kgD zpHgQxz>tM}l4i4|lcBjzu(#{%I`FpNpV-)C!z&!qn-P0_0I~?-Qg)wdi;Hs3F0#K! zlGg2{9WGJ~&IEmlJ@cFNw*&6JW6nb{Jt*y>Rh1O8vS$@*GIY(JqvI9}B zuO#(KZC+N9bL6)xF6R9@e_yewOwF2s)|R3$dJ=vKLbGM#XYV0CEp`42B%ZWp7JZX% z#B{N6@VaG$eOUhhr+1q*gN%Zsw3Gp?!gH z1a(cx$E#y64X4&m)YatMoF+frQn}XQqf1^T5qzzyQw zAs$i#FASm8dzw-DN2Lc!2MhlIydNbWS@fABU;Unr48NR{@UqS7=8QY{thnUssL3h^ z2^xA`QDJE=gD(!lr{(_uCXZS4p@_*IT6k&UaQn?^KNSsI^pTHA8HiO_yYMlsBg_Ru zmPs=Z*#HNrDT!ma1kGd66-9@pzEaV_(2meWrl)K1sn}zi5UDLY!I+WpQ1v*B(pbw- zeX1-#=Wmh3codgVwCFRq^Rl9hR>6cg2HNDZ8@#2O0xvtPQ zVp7?Koo@uB_}Lu>&~xi%0cN$M$u#^@`U7L&pBPC$_h(;`dUxuJTJ+!^d6Bv`(Hkv$ z-hS}_Zn{fq3x=fh7gMalPD_Xz=1O)9Cg(_@9x1ManH9k^vuIpXtZSt8SZ=JN3X!h6 zT*jV_{)zVCPqt@EA@CMkF|#7P+DU8=V&mcy@zOs`<~YY9;rc5a7$A^p*?S2F zV>K{^r}2=|Jh!Phb`F{%eLoVWjE)UAKKm%k7fp3JrH`W>9IyLp{tW7wvN&#IJ}Blz zERD+NJ;S2DvkTc;CXrEyI%ki;r|6cl%-Q_{%$@&x=9qJv{AWot~aHWAc-erZ!}z99CN(o#bU27`^1RZi04^Zxp! z%x@-}DK%DKVse%$r)c|@$hIz$G96b?-8g^pr0Js0g2n$X5@_#6G1xsT)scjJ;g=+;EuZq%%`*v)G+jk$X7 zL&=HPNq*7S>vYQlFh|1IQ(ta3M#zbpq!)fvifyOEp3)2K2NJQjPgZVo*e0)1^Ku?N z>G=-qb>TGL?AqkotR;;kPneDYQ+AhVW5nD(kh=%}L3 zEO+t>%Z6YSlg8J&QV$VjNc!c?(Txop$w`wMta!5=@ZO0X3bH3sg;}d7V_2rcaGF+* z(nx_E$Ws)x^cE4*%EuAF6@v-tC2=luyve#gj#*-Gc$LA!k<~_cN7bB_JTb|9Q@b~S zb2JfR5cPFUri{2ZGq?bvc3%{Zs}EuAgPb%v>XnUFK?E*)-ZTq~E6roZ;5AbtMH6Tq z_f<2J0V+v*J|(x>8&SkM6f|JymkP7qU_;>jQ8uy=*>*)8STB zliY}WU93-xlIIO90;pn?<&qZVSLx}bizKYKdw!}n62k=Nv^E6wUY#p5s&Y;ybu>Sf zMPf9?!ufUZS_|-Iyc>`R=76ifgZDgWtm$S}Ji3s+hK2r-tJut%wr{lzrFM8Ys*c>w2 zYaOM@H|o49-+c)K#a0g zYU8SYI;Psj=fxgbb_0SZ4RnY5#~X4_GZB=3c>8#$Smv=d0>{li8GNK=Y4#*|bVrpMr$%XN-8qck$ppmHgNDLSIclgI`lx9j_Y2_y{2vg>ioJoR8k&w zPF)VAAk>F%ceyg-cG=$L9ahOAb=pmmx>g><&!hqOy^F$d?awN@YK_2i$AWNGHKSJ) zxea1}w-?@Y>Xg|vxi?TT)rSvVILfhY9q(HqtWI{rOs2_kJd?jg49cN>_UcM9imD$- ziSIq6Vc@x-+c4yoP|yxK5`o{Clyw{I%MwXPNsnNi(GCd9@y#hl(d|wd5CCA23-Qf$ z$1Lr6PASf#T0#4NBxRiD+;1BBr|ARS;t8?6?4|zzY3UpZj|hNxuD5IBjF!iR(vRg) zdH#|PRa&x#x+3RPV1t19tv&yVKcvXhtdz_ONVQ!@1-pxd>WVIT?Nfq zFE6=VFjdkx7DV?mkp&$T4z6d}!6=jXO0iRIMDjklggLYX3n;8=TyB~!+6+Qx zMQh!qG-^rdpkZFDteuys4(*r4o^+&?Uz&UNGZ&Gc>dYNXfE5uwGl|McmcF6Z;9Qy* zm2=zvyEwND<#Z_2?vAXui*CIcDt0Fv2Hk{xFDqA{u-NOrxsU$Qe^0b8#ahyuQt;8yA$jDK(Ug*}uUr8@V{DnJYv4=ZMKBG=eO;55Isnp@MK1?gn zWB9B9^^4*5eYK-&sh}`3_K-{iVhUKmA&x!?Z&6pI~1S*URp9 zAETUI@79ns4kku~SNb=_j`*Sjtf50oiB?SV>1(HRj~>vZNmEc-FIgezWLnjGuR~?T zKbd+z)7r77(c3{7BSZEA7XPO8r$53 zxKsMsxq7{7zeHT=@Ew?A`}pv9t;oBV-tzR^U@@e`iAY^BWA#@mqnTDNK;LEI- z9vV-2kK6b!U9QH!MVV1IL%94GowOS<2DG?GXh-5dJE|+Cg`?MRJzNq!Hk=D~k>8c> zhc2G&zsNZ_zci%(044pqZvQ-5~M_FT^R@ zJ#lo)3l70pDl6qI)9npyMsC`&R}MY<1sNrCUgL7|Pdk+l9wQBiBwwISI}?OB0$pxJ zS~ODG7mXOlM2U}2j9`s#Tbk*UKOB+9 z%?%u^n|?J==LVO#z6jbu%FA0=;5B{6jh9Q|j9u;*9LlB3h}08e_gMQnlqw^*H@c?37?RcwHt|gy zN>_h+ehIRpHxlS-BE8&O;<+v@;l4Z5A&yQis51W5(|Ff|r9BtEzeQ zx`#{1IOsf;9-Y#>{op!oQYO3+w2o{)j^cZEjB00 z$_~1?ww^-cr^R;CjkRM=HjT+SDt^xS#$N>mU5ki`Y%g(lf`h2U=aq$wIa$d^85|AO zokX@gxlLa{fWVv$0XP_VH7GIMlyaSDOF-qSP#qGR8^H$0*q#wQ>H+p6lx)@;ZMhWa zZS7{+Es)Abs_$YZ3L1$wG3Ut~qvo;)V@`poZH(W&qLZe`rV5D_uUfYZD^wDL&i?$4E%_qi`L zzF4>TB=KF5L4ypQYh`1PfKwEkFI_X51Tqdr+ABM@u8NW6)ReT*le{|bzrcHtH9LPZ zON4bMFn5T+Wxat8CL&%~TlXg_)n(;Flv@ReObL8WYe(KYl%zWg6io)V5T%6iynx%v z_f-mwvUILFrjhsjPA>SU*>c_%>>d zy{aPA@zX-ly29t=qUyHK5R~_;OF`hq+x1yL){^Gbnl8B;u4Z{v%nxGaUGIpek-2!M zbeEzhsw>@-Bo=E9`*A_bDRWK74oW^pxv}QYWGSCGwqt&)rZkdT6@+iFO+j-tX+av9 zHL)Vxl9?--d&FC=tJ4hQx=Ftvp2g!kXvbr#aBj9Ig5;Bu-t+B#3OupqHgr8%Pta;7?WBimGSW!yR^TTz&A zb^0Nsgu?8uxNzdVaMQ$-yp<-bG9;mUo^Bd^PZVuDuWPw=x`~O;%b_;CkqwGVi(He{ z4FJ(#imAYXac-!_&f-8PMO$PN7i%?#jmkC*oMdqz+QlPCTO!9zRb!RM*ikH;?gHfT zP;p5c1=!JkWT}iUp0^Nqj+UNjUCE5ld9HeqOHxycOEU~$dATpMc$=vjCP5(u(%HTm zl_!VnMkzBvs(e(;g&wDAGi!y8E5W{nf;mp!&To^7VwdiN;xRb7L9 z8mf#DYhB}22UCbT8^qTh#O`agw?%r|Xd$YgaL(4s-C9<6Q-w>}I%(=6?(P8UO06uf zbH52FR9BXS9wywGpBWZ9*aZnCS&LC}d0AOI2;_)|5FB_Ys>&AT?tpKui_)hxYctq{ zmcVVSGsa9{f{@u{BO}}A$#t-HZ@2^wBI*30)-3^DSf)@zOCyOP;h=ElpzEvVq>!=g z0Qsvkfi?qh;Hi5}49%)^j%h3#wA_kFHb{5tr-6GfGaHe|yz5z39abB5ojg-UtPMcl z$!kQLsT5mZjXK-Mn&HPBTkC7hR9{*xpt?hAblf#t;TMerTt+_?F;#F=Bci& zaXK{FHfvtpEQi7lJPWVw2^#mc;F8^-AntX%RV-G#{nVjY?n`_j@u`sLTy5f`duN@%rFN3L?v!k9G)WOq@x0viToXF`{H~*mr5qQpE%aK( z<03@LZ=p8vUNX7ES1A7gwX*X}x3DB5VDXn=;_heOyV^7j_w!NjY=qk9MBA9|FE?7k z@&3e%r)My^>1+d61sGvGTvALU@m&o->&vkw>_iV$$%j{+Kqx-Qb0v2m1XwMooM|wF zTOqxPNp$ab5}Pa|kX{kEjq6#1JeaJ`JJ$n;7cRC36t>cysM&*2IfshqbFHA!I@(dD z^h|WZ+ln@0r64d|Y@B3nB_9nLc9{wCM?r?Y#CI*?jjU2M`;pL76R8??QdD$-&Ii5Z z6wMY6a|@08?5F84mqr0(b{y@hg-XgjMsth^dP+9N&|Wm*vEqezGC#R% zj#`z`MRh2okpqf?Wn&w12BZZEi$+a30B&cPZNiZnXmh5d7MgnUjF5JToVMRds`!(JP2_1w&~X47!~`H$FJghum1q0 zP>wxR>sWx20MS4$zj3;=_pFXdb(=d?KJ3vV!f-~NOe7D)_aB2&dn_I~T+>VkCx({~ zh*{^?HDqY}Cf$nFJA8-94j*~5Ds4Skrb#v%2nHCVl=5^zpQ%m^UWO=6SX{?{8*Dv| z1!bnbp~Ls3Hfp9j)$B0$fK0+~;+yj&YPy(GNbVrbBY~9}=1}N_U@P=4TEX zE|N~7$YwTEndIV1=rV5AbUDX@?|6*}TH(rjzk8JDdzl^yEe^h<1f!U4a@P(-b*ypJ zl2h@5ED6wZO|hcdyn>aW`^5Ppak3^n+@;pNkr8RQ^%|i=T1CaecFnM{)1A@GHbhF( zRS!~E=n+M*Kh-7P3KkZySeEbNN-~yS$oZVc8&W3ag;C(#LStEV+N9?fGx;j-cL!Vc$3 z0~_rnN=33&`|7GFY1lZLfbVl|qbyB0{K%892>i&YHjf}3c_-OVri7BNux!pA$nRN= zG0))GAujmY84ee-@Q_4Rv%G#O0~&mQ{ZwU*hfBY~H;a!%-{bkQpUH>AD}K@tTzH-6 zDvSx?htcB?=mp4Hz{GIc5L*@%NYl;Wy!+B)+9{(2w8Ea9uGY~+*8CSdv5n}X?k6=- z?GIP~0JA8d*^GY>7o@`~pAa_9lEd0m?LX1pCdL<6*3i)dygp##>>8_PFO1d7!yMH! zco%3@PPTf6YQ~q7#Or0RT3wM?)U8vp_x}145bS0LOxC)$9(=F+&8CXu+Ek1mV4@onmHivA=X#OHyqVu(TF|CAb$UPYo}X z5vzfF(@7fD6KI>=b!(j~MxpZC%~v*tY*n>oxMfkA9wa+=iB-7qDr`n9&5_KQ@KKd) zs;2}s@5tkF1GlKckHxRK8aO8z-cMf59h)4gklJY^z3sg?FRN8!|1z5cZN`xh~Ux98-Z=wc=Arh9?J$B zSsM$S?(2=p=aoq(dz{mhuX7>VotbqhGQit{eNnBIGFCn(7P+hFs_Z?rH@hy=z^wag zwpFeVmY4uW@z46Mh*VQXcBr)6%pB)yVRYG%*08WeRntjJA!~v5Aqy|Oi!CdhbIi98 zW`1iVIzxNrv0ac0>g%R#FTUw+#LUGBh&$4qBgplWZ)o5wxJVict#)kBsN*Dg(39;( z(+tMhkTJIUo1-Z*$1@)Gm$V%~HoBxY)M!0yOjF^q%FyOD?r$&&2*|K$+;oah@~H|9 zp%7E{bnv;X$5QZCvl zo;_CixTf2*b2ob#{$ifd3T=a(oU9u9GwtA=k7sJ#{{VM?vvP7xps054%nv`BWMlOp zn|5Bstb=(4jh3te<^nuO2~63FmmgLk{89jh82wZC$rwEubl=ytK#|%3{DELzWU<*w zfHOJtqsCUH#j^#*#-f#nnRV1Fv5Kc1CXo09gNj#!-IicFEN_kVET?55V|)0GC_j`t z+!L4?nNp7x55>_N2|j z00e`v(H3T9Z8_;;3WOM*Cee0HGf(lZjAIjdv!<2aCg^3K)$D6exhVaRdstoDQ@C;u z0S?`kaosI5e~MauXX7E0FYXzWDyR*ViGp&WJAV4 z62`(;wb@K?U!Q6dZO7}Mqk}*4(%Fq-A>G#woy0%o?!6!K6srA)j>6ZpT+_zjIEh-F zrr8N5aAcCx)1UW`2jG+St|K7UQV->7dK$b%-`~q_Hb~uXsd^2TtLneLprAQ@Oau9*yWRvb(E_xOg=W*uo65}ix}1mHJ}8m@?27>C%Zg} z;wnksGa_oRtTBxak_^1En=0-jHUmsu+tNz+b$K4ea;%6=O(R{uDIsH6^zIYAvKLr5 zjmxJC4lA>4?!dv%8q$ar59w2C0^HJm4htRlQ?qBP!6oAoZatn zCyR<2o|b1>#=t3ROf<=t*73F1Tw@q56@#!T*(!z&YGLvvI|j*z-?Xibgm4WdLc18M zKiOMVP8RFw!rbzu$Fh`e9qRHA@G8&NH3WL@A$6FH?LM=R zwEKd~xn?^hOb(Uz9P)ou;Mod7+qV(`^=I`}f2^u4>$!QjoZ#r^Eot`+zU7Q;lsWR8 zMnCfZ095hVrk35fqy+LUx9Y1S6R5q*+?*K2P&N-MTBma~`Z8_19^&kxz7JtblVYZ5 zyKAYP?NE)zYC+ZII5D~SU29bCY_GX0&5WpVx23=jTdv8mRWXa2x?(zC00d1R7b^2HX}1vKdKJglRo?o5crBKv z)&2pJUCTV1Er7DwYinQrLGwmX;QJ{Pa@Et<%AAkx*Ire#x*_%H=;s*B_9r1Fb4%N*$pRZRX z!pTV1OAdu@UA^1&V009gt@^U%6=o9@h0);Hc5WAoHNB*# ziR5)QL`O}O_q#S8ulXUsq`C^@O*`3KEZ4qoUPqJEuChlHa4wS!Pz!dm0y#Ch`${L$ zV2ov8Aaf?=2N(bo7c2xIl) zE@2$CQJSAk2{|XZaKg4?mT+HPQY3#6!=Up}`$LD}^f-uZ1dV>0w43EBK03;(c!-Xq zv~kl_9X&%>Bq8PUzj-!LrH84W@?@b2=W=tp>@$c}xzBT7mg8}7z;hTc4)YVH9%U;sbIJHeR?qR;R;+d$1 z8ofxnf-*JmqQ1HzqT__S6*@8xBdbcN2W( z+%!kiRJaR+yO>aLz2KBYa+Z=a5pioE23RSvAP6@tc;XbrHajMbl!+P7ilq$AH$Fi) z$(#tbU{XpEmg2anZeD#Ro}m4cfN|iFEIQn1?e|rfCUb3k-aC}FJyYxj?%qVD%Hy*V z*4QrYVf#yw$tkU4g~o|&out^a*bxnduNP(giCnF?9IQ7R2mk_nQE`l*4EAwKr=)Q< zu_|JRXDTb49ULxiS7;YKN|9T#6)MI$$%E0v*YUHI%@zw*=La$?vwhU|4YIO8NeriJJMqW9o#|jTDpna zi6U+sSt)2P-F_%jtthfMd~HnWcRq02Et(Rw(=nhv+qv{veN{1u?rFB27tnaD5=`f@ z;_W-U7H_p!4`-TC(oOY0s`a4rEl+X9R9czKq9;p*;{XEcUDTq)Ww8mPPo;qNl-Uk< zMcj-r=s2zjZgdJy(Fi(?S347$)<+?@AZ%e8Svtm(rI8(h2XRYmBx*&IsE(+N!b?dG zCftH6widf%zgwh=a&=wo+%z}QQa3{=Sav$Rx^^J=u9}B9+(ghiCVMwuis+5UntaS; zm`f^1&_=`JoOJTo5gP&sLgQ-!lp=7@+#{KXD`6tCRvznP_9Yt0-fx+M*o6=s_Oe@G za9cADMmj6%hK-a9axUh*`xwl(AhEPOXWSQ3Q zxf2uT($6Ypp93TS@au&qN3+4s4P;Jr`Vy7au-^(RK+H(YT#bpf4js(eepPy)*o;m> z-p>x3BeyxtuvR)hqkKfW?Mx82i7p4=qby`&yok>iMjOc)gb-Bb1b_kfsVaIo$+3{z zpjxaO0Ygq2YcUqJ!+ToLFYQMPEtSHbcb3fY_??+^%O0PXQ<`2QP5Bd-?2O8b4U$J$ zjeL&e1=WuZ*-TcYqb)IGPU9wlTYqct%5r@Um7>}VtODpdjkl<9A_xG#5_c#|5n6jI z5tbtpKO`;touJrhfCi?aj69kytTcNyLrLosA>v%Ab8TKFDbeAh@{6FS#;cyfCaRLh zeclC>94gOtMpQ91JkznKKCchM*=+Gf2LZhWF49s5S&ehK6Qa!H#2{;$>1HGy4oy$B za;FrGf@vamOy@CKVOX*7#ea(I(`j+$R8$C~VRN}OUfv~18raw`E2C#J=>o?aiPTF8 z4wok~4U$EBLqlYh@*a2iBJhodUKX3tn@O4{(>dZ+2#EFXqQ`cnte+H_w1+fJC%y)S z6^7z;@V&08Ys?M%h1Vq!k7IOIbitG~q5uZ3-~y!zbEQ2H_*^`&)il=S9%z9%W3{hh zY^%7djuYR0TSCQ#dtr)8nsHhF~ zxl%N=I*T{lr`bVG5WLece=ztXV3vXpQ#8&T8Z14fRn$w$jyF|XLO&0}A1NKv*6PCr z8z|x#SR|KsLE?s!7%X8CF_ChxxkJTXXE%Gxoye8XJ|$1FB~z@Cmv&@*-)iU<-qGzx zHYg2&dg-Q${;?yXY=^9y0RZr@O0eT(dueI^02%f!`aCz(lD0tIfV2R36tkD`fR5}S z+yTE;(F+=VLPtb$44VRAXy8(ai-QgMsSyBnjfuy}I>=-i7!R>*h&h=X6Jss0j_G}q zE(ndmO16rii?$07Vs*5YF0jboGLBSi`jKKL8(u9u7h6sXjiLZ}DR$A+j_7j$@w!-` znWI^wze2WD4j~oj_t3_?DVz?B_bGu%A=T9p=#<2kMf|*FyG?5;@2#Dabb!=?6J0M?(y4t_khmyP=Iw4^mceyec%Xj4iz$JrD z(RV%1_bThL0uKKGYDAT;b&mzD_csl`rIc027_(Th(6D`A9ebS@n_F|;KJ{O8 z<9Ih+ZAFfT>u4StEUc^}bF5MHxS7$in+|HA+3s{UvL%&H15=yLD~wq=&{9R(Sz9b% zhYKjGfbimMZNl6XT|IM{2J$&6DC?U$ayVO>nvPwLbxKtB)C|-)ukMl(UpSf8coqrz z+Ge<3E0UvxZv>2_9;&IW%fc~|xcq`L-r%`I_64Un2FQq6H{Js(eA2zmxN-1Vv{8II z07%xzEZ_=?#Au_9rQlo1S)#+-;w~MpL`lP=dvZv?Th&Tak5)E&8TNMCz-!>0gd6A$ z&kgGD`z#*tV2Q*X*;-B1-44Ox5|NUIW{>xPFniY=t42GqC5Wd5tFfL#6puCyKE!P< z8uyWzfSw#yr$_X4S(Lh8MuyxT^6!F`r`c>Ck}&%0PA>1P*oYr9?ogI49W^;+kD|7B z{fx4o29o1K6s0~6_gDskN1wrI0;gpuNDYP61;2X4{7CpEzicqtU-c>=)66tC;H2?0 zepr!s%ChoPf1L`24$D?d8d*;^Q^YdE+Etqk*y);WI|EzQ{{SSJHr{Dp_Hn&~lUfhP z>yl&D6E^v(Ea>rypDNJchVdi%rFnpc0-AM7x&Qg z6RCY7y?@b^*KFNEoyHj6b^EM8B_AfwRflU&BY%m6ev4WCrNrSS(A7Q~ zTk=fFwD^p&USC3S{M76x;ErnFPrv#yolEN#>;8;!*hGu~xue>=Fo{jA82wh@&7`Rj zS@?b*E4tG)c0HtX33Rwd7JO;ILyJ*wA8>K1bceZzGYgTo`y+MJgib}oM%@-jJiVH0lb$URq%4=w#t zJXY}~Lqyr**;;nni{N+3K52|Xgd9bXg zw;?G_&9u0i*^$%AM-iBnvf^K1pEUMyUZYTb6XMQwrGh37eX~!&6aI-YJoda9Ip_Y0 zJ-D@FZ($|R5=!eaTHx!D`*S3r!^C$+am4*g)BTet=#F?vb*?0ID{yPQoZ;Gd~E->sy&%*0Zj0ppG zhOiHQsD@y>8BHJ}Y2~)3y{Et`MXL)t#lgK285EO0ASKTHEd?vX&*qrCyxy{^{{RMp zYxYK#VCsB1k8^mtcf6agOSd$1@BYC-3}c>954lmRb6eP#K& zGjDK6@bs6OSNw^};r@AF_hju}-q*_SvmKLo04?uPRhvhN)R0X>+;{*y=wwSk`rOKTLzRM*rJWSRbMvRpK-b<54q6TGYCF2psD zynyBL1xY(*BMr+N8*db{#KUNdFA$=%iJLYSM|{EVJO%8M_ej>08yW`v+r6s!Cd-z& z#z~=!aJ!Thb_YikhquKbY=4&Sw~{cc88P9Dr#HD%mS;VrI#b{frzTrGxZk3lo`(d9 z+1-k|cK!(9%6+7y8TNKA^f6Y<{{X=wnj7u~b-}KYeurYJzEu4PNXTK)x_)Aw24X`P z!UMSv)k?b_pn-*ycvJz$d6C8FNp_yF#OR}ykTZi=z$~hxIefmX+3HJGA5+j^_0p1E z1KPpG+7D`RxK=}+S>cEd1=+p|xrpsMVh-)@fR+^W@P(a$~G88Nq^xjt%&jNa`b zWjQZ1G)k)6CWVK$sWVDHyI}oP6@Jr1_T_?@HG_6zqNAFp$Tc?VkW7+0xJevO1<_87 zBfg|1@pmm!W79s_T*eMf7xYwXig`JPXcb-=y2rcB(Q-KxM;6i7A6tBh+u?x-5{kM1Yo%>e_Ms zVxJSzUq?GZQ>dbQIhh{DkBWX@qr7S#-ik(Z2C6aouPx&#cSoberF%KA zQn}kx@PWE;OBGSHcWI*|_*IHOqwGlAG}w3@Yhxd(a$SR~jhB+M3TPkw^GWz6=V|S5 z@o8MLYUO zNDsuI@r$Bu1jG{7xcDWJEr2XH>a7&m=EhUWf2%9yw+GdW zdw@k$ZEnVf4$o%pdAg;@JWWoJe&5NRY2QdcZ{((BoY;0`mEOux_YKv)4X@cj``w#_ zkMZ2SQMMZ={npLH{{Wu#xr)C-_Wn)BskaULeP5+N7S`rsI1rEhmRQ92R~dp5@eqR%)XWtUE28{xAIX%46oF*-+tuebQ52`%MPVn zM$zoNQs##Uk>GHapr3+reY3y|e#>X5kMX_d^;?fqq+hYPv*`!z{FMe_jwg8k05y}* zVS_LQ?f9!*AJPRaHk9}_7{GWK@9{{f`bL;Zni@PdGB`GwpQySVO3&r@2NJ7xQTHJk zg3QsGta%5BjFfF2%+^+)?DW-Ak-sEH%1Hex<3}a)yNLnm{KS+4v_4L}kcjYHU zGgo~Vulf}{L+F?1{RTs^*tP8JF1JoSnMAi_F=miP8Ch@PP{(g^$2!KgupSqAPD!>n zk?l;vd z!mh2dQp3jI6r22(Np|X}Gqa|rr-8q8v4q>pa)-n=ryP`vPdFSh41oI(6VE)LG&DBf zg|^L2(5E)ZK}^(iWsE3 z3G+)h)c3e8@UmVS4pe9#TupnlJP<9Q0XA_tAL|$Sl|*(oINalZ1lYyuAAbF=il&e< z7q?Zv)*fgs389Fm)e%;T$`IHQm`O(K^RmYzWPnpzI=e|kQm zsnu-lT5j>xW=zE9um$eFij%^zd0~(-?*=k??kQaq^#i1vzf(lREhIgx!Xn+-On@Jn zeXpluiF9<$ZvomWeN4GPm4w*!NNr(~KIrdwS#AP-NxC<8xp^|sIlbtqug|rA%?Bx< zcXl_^H~F4_RaT0wS10Eqe z1K<`r=`s(#QoSMW)$TeXu(X#z@F|+x_P%F#StG5;)I0P;&9v1G*3CEn0DR(pD=qYw z1Ei#Rr5TI0bwIz0D-TU=6g+5CG^4dOU|0v6^N^p_HtnaY0{L>DgoE)}!Kouo*xRD( zDuYeT?>#j~gHN&@Qm5F`8?}|EgNW+#Z|_PQU0WOby`y|3aQ#+l>a4_CXMyIp>V{wU zuP3V93UB2`;M44CAGS4*7cO(WWp~X#7T(uJayKtqzW)Ffl=?$J{p;5R;2Vh#u?||3 z`A|7}PfHrv+j^H9$YUq2Q}GGYwyl+!W`>GE`Js&diy;yMb&!uF>~0pgke?(T5{vmk z%F^hKT0OC$i1)DiH#ej#2!pn`aT;N?W5OogWX+Amw=!jFTyWUj$`6{{r5@hqcuGvw zq1$XVhg>d|{{ZlURJ(14xc>mzRHObIR$&xDr!ad@fkZYFA?}v=-sp1FyL+&5RJ(hc zG21*f?IE}|5j@=s59peQYfMsSr^B%5&o**Y(mO5lxg*0-T>`MQ zqos?in|WU57Ko|Y)reuX7{2)Gi2hOXRmiB_Yl3aRiYhul33G|Jh)C&BUnr{$LO&`d z#@6EQAiAS5{uJTbEMBArzNO%PO+E@T7-`v*lXK8rNzxdFyC~fA?oggZv{5-zq_t5b zu)~SeJ*^dU8}pE>?h}W|+HvUS5EkxJ3!ZPx`tMH?wpg=5gTO~ zl8`zg1IF?S$uS9sXy%TZ&H(-6qOVd!UhplVu(t41*l5+$2;4WZA&S@0yf!*9AZQwrclant zUMzTo)Y5=S-h5Ebs4g~bg0y0ytR`cxan$8D9MpXB<~FsFwKdr(-4|vvAH8)7`2zuS zy{c6wZ4j;ZCYiK&gGGfzzS5H>EnPmYe2(%}udQB9!3oCV(vQ5nr8u2Y#K*Z{W+PU^ zzCe0};Hs^YjFwfmVvlzBFEt(e&y=zeBB%OR+&2Kk;pnRm6Umwb-%daSzj(ZL?OQrGGXl!tJ5PF1C!4;z500x2vreO2)CIQ{FTYX}Y0e(mEh&-BADYUt+NY7z zs^@Ovcdenhh?c3{hFnNJkWMpz)Q9GcgJ{FMJ`BO~CY8v~r#yVsQwhNA$ z`6a~Hms-Ufh}=@kCOc6h^JZ>g$)PxL`nelt%6yb8bhA7Q0uj*R(z_@i4-%bHU4~UI z7|ddz19=>8)h(6sp7wL(oTI?ykbqk9PU|%4tTbCASs@K=EVtzT?fs}qRN>}n0o6-# z9-gLm`mQ?jQxw^zvuxs2-4@CiZRt}{(u?XPloxqfPE%6gG$QVOMRbFBjAduEs81J9 zXlwf%)oZSrm6}=!9Pk`0FS~hGS`C)OWoUd9?{hi4hJ1BLw`gLgX&7}41QKvByWRRA z-Hzk4%3k{1qwuk%pnDe`O4aA(Ryka%&ucH={eLB|4cR(o(}P?InZv!2)&WKBjs;0U zh6$-ln(NyuoqMQu<2aiSXlq;v?&j3_YEu}_Jylr`XH_q0eKLkvc&JjPTRM|^5sezk zqHb43KDrS+PX%W1CxpGTCn6a-K>n7s8EeZpiYV;$FkmSoUcvX!bL_ zhUVA0<(xp#O;zC4FiKkBEx?kH>^@M@nel_s$`>n@5n-nK>Ee}O)1mh%DDbyD+c*h- zRpdIz-8k5-aM=<60CaKDdLw-e!kLc=4j1hOx-_Ro6xQFo)8s*~wrtJzr{kd4)(6jWHwtJs1)0**IyaJCxp|Q~Ih-0CZ`Cghr7+A%E%x0T zBsZ8?8zFObBbsGwq!E-SFJfBf>P74kW|_ivlWTNNO8|DF*Z3^tVzG@gTd+8vVyo1$ zPT1TKR)LCAmqY`3TSZl_qj;OuC)k=Y16h@bFvp$6bTRkV_fmq9{m>XkQdf;6q1u73 zRM!`VM&xz4r7{6a9nCHX zifj^D4vnxwFv=)_Y#QmPUhJCP-Q{O)1m>GRPc&V|V@Qp8u?r-JWecUy|J z$%H~RX>ITLlW}2@+<}u$r5wh(D}9cZ%W}9PI$5UNRhiT@>~-AWEy*lLMArJn-9TF~ zZi?$E-JxLDnld_9+V&*$Qt>{GukNwt-Ba;Z#vNwEo2Agq!*V7pI!BO82powk7Q4K6by z&)PViM)q4PX3EP`GY1~hRTpndOF&8v}eGuW?84HY6 zRPA-!2_uMcM6(icAV%QEjt$ssAr8=Zqp7Q6be3!MQqP_ew16LVq7Nl;spjRmN=qwsJ7GNv%RH~he!_V?@{%WNmDs%j#lz2 z0vt`ISG}&W8?JXM?lrbGP7>3?#%p%=oK+!7Mco=o6n-s$wx+g8F$p3vhepF-8@MQD z&Kh(&b3s05Na9>g&l{xW915$I-iMz$&Q9oZSqq$O?_x_{ZK6(1cNFYkUdSu58n!tL zb8kn%L?5fSCZS_(yHzzy4rU83if~M=MG-!0-7hPPeIuW zxL*8KvRh%J6@5tRN;4A%V`mlWo3`K^*-APR;9Od4Hz@j+n{ErH8(2pxy$Gqb7ebc1 zQV8~`DXDWsn)XrkF_^qjk*o(H&Q15K*3#l87WNDWGlrya3dvVIP*c81Mo4wjh9kHJc}orAt}L3c(pj%bHdAxH0FuMGDBiZ0d(Mg ziEO93Utw*)dlgh9QCu9zxFjgC7M`d{FkJeUD`b#uAYmoOmt7JRNT<`)TkdYQraCQx0&r*e)OcsE4r zVm8nPeRj!EzyX&SqPgI}=YEKmV0%9Vd8NeXk^G{k{Ygxqzp0F}zx@;jLgS`Or`bgJ z5#1{P00etC3vOv^Y92onfq^Z!&KF8gVT94Qk(K4-C(HI@C-!9~EFNjC*+%CcO&9nn zY8{Wns%&GVoQ@>6%4%J)o->vSVuicD!Vuqmlyz3qRZk{o%~2sgjzSU3F35BpO|dl} z_lYT>vD8Z_9_{GZHg^lBllx992AkUpONy$$7Ln%pS`S@QQex6x&X9x6qRVBM)#$&Q zn_lJe2ViuM{{U5}t!TcoxJEDFosS=;rxG#>96k_t5(~)o=(Cqsm%F_3d`fY(*>rIl zq;#PC&A-XA+|XUMH4lVDDqPl$!WIU67fvo}n$t7iBip#h;%umN)zSBxBc0PKA()hL z?-Cz0I8@fiylF?Fb#5izA?2gqy6WKXT0(gORXghlUf%3)Y5pbF)i8s<+Kxz+;K!kF zj=ixQ!NUBMj5w%!K_1KRRA$w(+Oyc?RVE`8Zv!B)O6jI7=q9dQDluUsW+>R!lj}R5 zf{%3f-+T2zzNw|Tn!)0jePy=+b)!iwkvKsui6Zjht%&MVMdb7YiV`ZyDiTJS*;VtC4)`6w{o0)WUdD*$hMo*ReXosB1%HX&zdmp@rbzAtSDe5*qoOZd^#< zq^av=HUzl%9m`l2EHv%3cMR8}M6YvQsP`{na!DgkcQ%3S#}+!57tzcD_b*X*)_ZC82Tc8T1J!73Ch-HE3t`#ns z@V%^ciQRCin2eQFwFQ#8#O^i%7jV{rqMQrj%=)3UYqSRkE=&IVr4q_(zV zZe?)rO<~pwkOYU_B=o-b2mY!bH&UL?; zOLi_}Kn@4Qs0<;-Nw0IKHDJgc8Qp%VSdTz?Lo9nqz(ZaJ*gBxt^tuBP4!g348yhID z-OX{#;twk$VZN|iTPVgD@$A+ zdf$46shXaWTNqsMKNFIjJBxHT98goS)eS5Txf_v4$Z^=jY;Z7Bv8S%9ljD;UX60qL zv?v;=rKf$I2t$ca5}PUxWQo~vikTWi<#8LW$qwr0=x*!vH&E`BgS~sTCyF))5fZMg zapVeZ)lRu3#^-vB?G5!Su9R~|$Q)4fQc1pJ?Ob%^>Pn3=>bfL%1+Fg?knNqo=#|@F z*3-d92_M9OonZv(<$|hQl%towYog+)ER+W3aN%VW1Hg8J+Kgk~9dcX(4Hk^9TB4E9 zg70u{m(-;RSjMzTD~NdjmW>)Yo2aOq=hNA)Ik;+DT}caczcfoe&JGb<2xGjjgI~Zrb@>o1K>$*NghYUP`fu>1Z=sP#H@jvf0FNRX4bj zN!IGgjE9O~9mPSm0#8IG7J0bIHza{92u{YszRBTEL9j%{P2IiLP-h~G2kdS&-j@^t z==kZwn_;~Z1t<>HyVYY%bqne-1wP`@?o(KN58RbWOQz*Hhf<8-5vA4A%C1o%${H8g z7ai(aIwI!S*o1vGGMpb~He_>38p#=i@ChH4xrMl>n&=$TKz+whw2I6+ zjFLkGN&F>Nu}m(UHN~%YwRXa)o`s=VGb`Hqm&ZyVum#4~REIQ+jq2~V@xs#Ejc>hE zBn@cpojR|Z@jJTC=bekbeNBoMVZ3noiR&TxrAr}4qSEyj-o$#F$hfAL5C zDYjAIxx3WWK!1oo=~ZUR@uzHksbWX}0Jc8Wv&XP*yB4?5V0msSYW#aa{{Tu%6u1yQ z`dXvYwIB4Vu$lXe=h~H={{XgkwQTY1YoggDJ_ft>R`K2nL z*+Y$;Grgn)5y4<@nX#_93BBF1EE%_zSwUfvOT81~HV*}u+2BIDS-FdZaZs2T(5Qi(_c;Rp(X6?hA&dLM?p| z<9)-P#eiZ}8EtSpmpwjR-LV}3P#v@Q-5|)~ccAMT&gAo|g~=~=olwuLXgXM^o4D~@ z%5SMk>ab>Lly!ExU&%E3_h05vKn>SkEF#w$C3P4aox=S&*0MvY9^*}JM;+xp9^y1NL$)$EA|YrS(<_#WV~mp8>Yrr#>v9mT8uJOpwA%Jeip4OoEr_<{ z<7al^xdzMIdgvzm(YaLFB*{^`bRn2~mySx2_zx;ZRf{p87vozY{%s?3oVJ>u)q zUpdm}nJPC9Ib|9P9!gq4-UJQ&)l<+*+30o=bFPD;DXZ#cU7hlc`}H=?5#{5%uw;L5H zi@@f{+qT|FSgG7d09^2~P(#>hg6+}9=(r&+Xas7ArlEU?%nw6#FG)Z}x7lXRuq72E zzUFNdvy%Q4yjhlOv zEHW&(1UX|a7PnP0_Xfn`P5cyg6%*s50JIMjM65lSWsi9CQfpFgRy90Pns7-sDh8?s zf={rOWqd{8wZhgMR7G5D5*9e|+=?=}UvqCIwDu>Qz}*2R+|gBZ>}b1hegGi^P7b?9 zzA5ZI#~^SHEj9GmsY;%;ogg54Kd#=Q5M+YQ&;h$S4wBfot?3!Uz2$&2B6?<^L#A;DLj8=6Qr zx@#Qsi;2eWD~=#)7ehDzme}vTY@*$aw4Lc~PZ-HMQgw zpfYLoA@O)eqBOGPbx}=(*jcx-xYbdKsS8>S$3+uQL`WUaYGoUoRpWVFhiy3~<+epb zNaMjwicsARf^sNW(l@72Ic$jNs0iXbl!P>`BIIsFv8`y)zU2I1o)!tLk)+PS00Ubz zb1GvEiJ7-K{dWZ)6XIvAT0D`I)zLCQE)R)Psdlsy&RZ5bLdG}@gQb$sdS^s$n|VA` zH`b10&Q{x!T=(=-4$L~;Pc&RI@@qm zifZ?UhhI+=j6$+WBLqB;HI@yj-r$_v^k{z=e=^h6%|bxQjAr~PSw^vqOi5X!K%FbkZh&)isH1Abh3yK1h16E$;zy* zc=g@p?n<^!sn*HJlnX9S^g}lz8L^`#64^;R%6+oCsu7+k_8k@BCRz=T_O7=Su`8{j zU|gjDY^KPeIj3W$&g)-ZsD~p2iZi?uqJ5yc+)q^@4Gr`qUnE*{T-@Pdg+$i}Me1G79aTqwdqf!Uqu;KNSgLvuv9=<>D9eMY5-aVc3;u1}NUv zpTaVQY{T|$SjFd?DLcbSB{@FTQO4Us!fm?h9IimPX4M$N) z=#1jLzP7e{f-EI@a2$sZauiaV+go)~02}GJXuKmgx%MxjO4=Vn+rEHIr6lROI4N(# zUN$KB+FCDRqIt30TplY0HUP>u5;XvO(UkOdkN{2mQ;iz$2>KY@`J14yL7}etl8``I z8yjGB=DO(1ss@?___krla$aD*XX$RL1nw~4?(yCgMy5#_iAeSWMfD2lIkgn81ZX#^ zKWOJXy4%X7G@aPdeMxwl%9ev-9C#JUsB$on4bO@il2=CXK+F{MPxOd|j;OfUR_(C? z=22IB6ojyrySl=JfncYx?+xKaV^{%jGgE*$g<=h}t~5o&;9F-Kjv)o08@ZQyBg2R{)1rDh zH(OGzxYiStnV`^d(L#dI;hRnym6e*%l$Ew4y%Q+vy4qdZ+oF^?2)<)vfcWl8Ya@OE zSx61FDaTQQMb5q{_~dDI#nbJv;q6AlrqJ9pjj-NAk)wr;kPBTk8=Bi++>NG@px({% zO2>+G!fuvEy_!5}$sZUFus2+_CuV6jfTn<0huhkNk|qIg(rl+C zg|4@F4;3FQUB`i~x+4m!s3PtS6EgzVwlRjBHpmJ5+v5nDA z$=FU)?A2Z~vjs?Lamni8jNH2Y0uEea2A<saG*MVa{1&a40u znK-RPgbd_!3cT8&4+OtYw*l-+#xj~G#Y4`narUC=s~XuDa}5rvsp)TcBK8ZhOzYb+ z++pKV&gPzKqBKpEJ-6aGpz3i_*xbe)<8_oKA1R)S*S(O_7q#|v%6f85>NR?4tA~0^ zyD~$_BXC00*Gyx#Xe^NwnmML0>+Tmo&fKJvzmn-p=Y7HEl)2Y)kTp_X`WvDqHq;>} zc_VU#MkJnvKWUnKX1>~4L29lz{03SwK)G(Jq!?|{e9k(Xja&CTd_-+9_}cEN_}c9> zMMFg+j^un8bM3iOoe47Z@bUn>KtsQM0AY;sMKUXJN5w$?K@sQ<9G6^aSnh@zNo+G>ghd78PD<%jBd+fU~53};^042 zbX`YSa-t&WoDpBd^H2!H?KU8D(HodJEZ7WCkpsjeWqwp72Q3PEQ#AlMe zMX^<{<45Nq`6WrQHO%|eeo`Nj+2hr{gnN{*+YQUb zy%pix8;Lc#Q8e`vu>PqH*!)TE?$F$g>Gm@aAe@vf z2k}hXg}BgNP4F(I1+bgjDNiyN?kqmVeurc#`#>Ev8&5|Ud;*jGhpc-;y)lGxWDh^0 z4-o~_E_CC&Ge~JAdxl7yd>V-ubxxmV?*JC1MX*(GZ||z@&6LOWDfjF&alA5Wslhw@ zM@JvTrRM41aXFeP?q)2*Dp`S3Mao72SyySW&1&f8R6yNNO$}Ad93!K5?Q^N@-K_q9crD@{{*|??vPe@tH z(v)r1c$M2TJ3T8655(WXAl^=H=2XM~!_V%+j9@h&W`0(mR%=OH$!gQV+49 zmi#`>-xYfph>^{%dH1B2Cac;$BZ|^FM?0%)JB9YL- z2Mb*LN^E1;fp{*gC!lqgJg8QenVho2)~URW8fS(?=gF&DFRqH6i)~}6qavOdnnvR8 z@d_Hh8=7dyo^o5uHma+P*1AYsTcF`(Y(5V&yvt#5D*piTYD!jZo+mx;RU?0b%^u|y zHZMH-$tfrEI_jGxNiOd>*6~k6K5?NVm6v>I!aF0{h8_^|#>J=8ONjelAS{T6Y@+$K zJybRv-qFCi=G;;8Ijq!pXtbiXL2z3I%Tz$hAM8zb)*JOG66b13PSX<5=e?l1{oC+L z<75npAowR^Jz`6Ok^050fY=hC*{$sDcinaFk12GIeVe!sij#tq5>(W;9K^AH$_L3Q z5jBI%tn#xI6*Ur1DRFa-mpl`@M~I5Lg9{!+QScnp39;0YO6#Q+O=wy%P*y~&b+Ox4 zpE9`W_FCCN^HE1GT~`|5d$uaMhWnDo5Mk3O2~pw!ukSDc%|>MaSc9rqaKCxljtgcA zC0Sa+>oK?2ik-qI4|Z(3l}V2lupQ&cT5tvt042WEvYDl^GUtZ0o3468SkN@L#W7Ih zxNhPQRdSmP1Aag$rXti<(qd6XLD|N8@8!W&8&b{3GJh6=qE}}zNEkSfqat9wx3?=R zMdd?VLP{Ezw|R1w!>1xOf>?0Z;6j}U~a7_^py7%yqP zlzgsiZ{F_(El>9Gi|<3$QDzq!9!d(Pq;JpyN5?A;hT#C|TN|uc;5w9a6pj@(aMPmp zDM4lzhF;u?bx>sNNBVOv248wt)*uoZ+xRG1oaVfO-Q3cmkP%=F(`FvXm-@)a3kye> zP#D8xjm7bX*zn4W1;O?@c`m)T8DHw3G*s+tsI5A-Kt{gh=GMwM=%2*sgl;d5q_=vH zYptMYR8zRQT49Z?nb)x?ev>&i%u@}`ym3#*46h?C*3LSQaroUc&9jkVxfe#n#)H^SmENv{);kv6$9=_Kb3GpJ->G^`Bvrcy};z3b46y>VDP)S zK~d*Dz5zK=w)6p=%r&n5I){~-7^MeTf)B-K;yEv@PWBX`44>@X>?rc_>}=6}3&dN0T-Ryn?B$jg z{Fi3i{7L@+_kaiGV?v}hDNpxC{{X_4Dd#`jzx;^u@$4JNvMue~+)<~yRPp2z-M4s~ z+pS!u#>%3~T9f7eMbFl9d|K65 zZW&ndtZg1H3Fxqzi|Sz>E=QJ+VA>svTzh+8lf5Hv);6UxZK{dP;Q>8iX;r4-5)s~- z5#R|=MT3i7)3)E?qsqhSHduBnqi(7s8Azl9{{Y8o<2KS}KlfCI`-y;RprgVf2lubH zMxsU$O<|LH`Q{{V{8q1lB~TfZC6^VxN{ zbt890cJ+}CQlDUDq}8lS)NQ>~j_$Sr;Y`7Aa_HM?(;ik#7=I*f9{_`H9{{#WRy)X) zWj-4#-S!j%`C%(^^p@!f{{RwRQMmTQ5-z-TwI$z`*7zr6$MI^`+DFz&)z2uHu==n| za<(3M+}00yUNFYVa@5z-#yN>|`Moh7nDxzoOh0>w5ZeM{OuG=)Ca$l^Omnh!yB z;Wir#t`Dtzyt^n*#l$Avi(&DL$4fMh2Yj;FA1060HdWDoe zb}@T)aoqX2M>}Zy`JsZA58;nDz1Kobw3SW;uB((Z>6Na^Jp>7p0h=e)R&cnX4XN4r6#{MWrMLPlq4(QXuahnHPhcYZlhX4*zZ zTe!y1(wV5nC7t2ZR5|V(T=G>x@;%nMK4m)>GEr2Zx(9<5sbu=X=epx_$v+ia@)LbM z>H?kLXgjypdY1&{Zy=e)2~sI&JZc_EE^#SwPD8J95ar-+9>vx?6WtmmXiHrNx^m%T za)8AiSxEgx?dr~UmZA1=h+(6@Cg3xMlvni|@oo5d)RJ9xc>w9w5Fe!j}3>lgU z;-zRQ$#XylF+8^j!h!OCx8_yHT4)9y9T~7{6pEcdc zDP7zc+0Gr^d2UYzxSKQ(JW{(7=7$hHi4J4q*4lDGyN35V;L~=xRxp8cbHPSql|~9l z2savlM-+UF2fSOVip1l5z%|aZ2Sm_ABv6SjbDVT>H%+)3*pfOaJW80drzik13(Dyx-KjzIkyqo`^iG!u?XDGAT=9JNckSywc6>; zO~ZvWSds@o6wVz-8%ek~9QZ8cHPH`Cfd&vydE9s_EhRHzfvx>faZ(JV*!9tHu-uCq zcqqBwVs2LjbO$zJVvy=!U)}_x#6;MQEV{^DoM=iJ!I%fpm|O#z59*XDA!*VW{DL{u z$2cJs6#*n1xA99Vg1U(S#Z3rsyBdF&a+HS-r+bB&zlxQ^FiC1$+cW{qTxd3Gnl`l0 zcM-vIRK#0ma975{pHj0&vW95d_~d8>IbE{yQ?NSW{Oqr=!xRk-iGiSU3DK$7Rn5#! z=a&3Jrfc*u`kXtmkb=uNZ@97ws?K3xk5!^=R4jY5GB60#j9dQ?@l1QmGv?rD)6# zl6|;wxGv4AxY2CAe9Z^$-*{i2>N!72G!`Op$W%e_cB6}pg%WWpYHkh;1r=@^!?H(?r;fH5% zk!dA0Qn!N!fn`qG7{dXUVSjDCs7j5hr(m#gdFik8NbvKYnna!!txr7tG&Wc5j4^j9 zDWi>phW`M9BM#ceO(1OS^SZ$uwGz43hyXh3QP^%RJkvC}#D1te60jeI(`#es*l{X3 zv~0}H$do=SjZxwB?tA8DU^D|$>{%@>Q{K=+$UO>6+~FgHj_x>K<#VhmPgZg-3yPMk ze;?W@Dy5Oa_I2zWKSiF^I;GW4b2AfuJOZ;SbTa91B<-m8}@N0n?F5OLaC&WdttjYe8ol zO2lN5I5y3=<`8m1ZF^Z+1V+QEZPX@E;_bmv#WHp)GDCLU1s#dd`b{I)O^vix=h0cE ztZ65ji<*(mE)oJ1Dp68aeMOZuC1S|kpRN(y!IIqax8k`T6D{oNpkd_c*gsW7`p}*7 zU(F}$5%`xs9wAjyG((0pt+Vk8A<)#1(KaY4!?na{$NXdRS^3039)%eWP8#)TyfHkQPw&-_#J zONxRQ4KX?){8QWXS>M(ln57EvE#4^cmvHfWngp100sjEBD4pR@or?|3-tBcC{{ZZ@ zXMb2E9ep9G*HE`7dlI4__<*zj08S>|?K~H~Ka;)7aX#Z`U0HsHnv^_= zaXLnheWQ=SF3%1c-PoiKKar9jhzfz}+{s<^B-*S=+B&+T$AEbQ*buX0bq{T%sFlN!kgDyRb+KI$y~`$+$Ylh!M7!}j zo0nYpT_oaeAll?8(G#!9C`Zhc+1U1BX1V0)apsWv$wBp#O@NcQu68FivYVlqhR)-y zo%-Fxlhtq%Z*sgN%57t&^tjk_-OVH+apIWdO(bcpxY-dLM2$7)(NE!9KRs0&Dj51` z<96XJ;f_9A)Qmei<~v%fJOjS-llmyS@lMwj)Qaj=8%}fIaqv&JJifKdx(yY#>KK3- z>+tc=ImiA~<1l(rX1frh7W|JtqFy>T6%}63b-~wid63C&>vSB|s9^MfTw*Xdepnor z9R(0}wWqDA&&_T+`}#3>i~5-ZOHAimF;Zd+0jX0k(cz$6)zlYsrvv_F(**!{25P5x zF{AZcm|dAUR8s!{(GXxZ0Y4Qu15i}6VxS~z!Sp@4EZiTJB+J_|`317xOvk+=rv7cE?0LzcgY z#u`}5MIp=DI{i56oP!OLXNP1i8|(tpro$;^cnzh4C>zUuNUC~BU>1rPoBSbjyi>8r z{7+30l~vIOI0+(i9c=;81yK%G_kbsr)`_&T)3SR{NXbYV9YU&4S3|q%_TQwgRi{zQ zTOCv}5}Z@9SCx$bmo$YNP#E!j=X>^c9^x(0r4^96jH=hOBtKr}Rwg zeS+$>(5S;e8)4b|l!GWUGQq0jhhfxHxB*F6_j3;)C1;@7S~uD9K{Ffqr5~VCo-IV? ztB!>|%+8Mol2-v69lqO@ijQTdXbya5G2!!CO&%3TNIu2}gUsqtVkpMu4{GI}E>hU@ zs`#v)ky;s~xFtgf83`Q_l{iD%3y1@v)g_2Emtz=DUC0L*+Fa?q4nG!xQbxRlhCXkh znny(AcI0vJ3HISrJlI0wJQeBqt{)A_orI6U8%OAxcGS{0NZRKS&qVxLy^(Hew7jq@ z)LT1KHl{Ms;H)%TGZPvN#GZ+_ZAoxFlq8fm$7*Gk(n#2Wt8qS#$Z6ZV?4ttSd(~E=uKI!ki><79(XKb;b=4d^dZV@&NIlj60DVg} zemh$`GSpU0BY8SS4=x~FMrVh4lVt!*lF93z1V>_6&~-ja>YQVuIu$s>M2SaY3}Eh; zIP^LuU5PlGhFaWIcVutg(&tSR?St5kP}oIm@>IrSB)i$ zVeNITY{!XK8cHU>8>uIr;HvS}I1n$nSZFZ_WQYeFJ-TXA$|F4sE*|DF)^q)db+a20 z5YwL(4NF}UrwzM9xN3^4khEMsPD`JK@m%L{_^uA<<(1vyN{hrQV}<3H?+ysq>ST^r z9UY+j&YV*{!ePUN$)kk>W6?gbeW z1w}KM_>OPH0y2h=JWLr?gAK!78O<;aA;JU4rQEag8+;cnTysk49Mf`2?9QKph+GVDZsJ0&Z9WX> zTqbdEd+zaD9?*<3ox(~R27zt+&qbDP6-)*Rn)yD^T(k{Jwi3Lm>tIux%$b3)>3yd} znAiy8Waw*oFFH3Qt|PSbOA>Tl4IC1KMYkr@WSEfqoyl2<;FQogC3zx-NJewWuID_H zjqpkh=$gYM#@gae(tl8W6;==RjHdaz%`OWe=KDY z2FwS$Na_!SWTD1L*`iZKPT~)islf>+y4wz_{{SAzOhj>mU zzeKMQ1-lW~rnhU<&4e$_!s%_NI+8t$Om@fALM;3_k#v%Fx8hUrY=ot^4a3JrRWtsi zyv#d;nWVM4GZw<@TEq^4JSlX!W3=M5d3JheeSfOjI#e|L7hu`VQ_S`=XaO+UjxrmwKAeOAC z(>~200xUP9TPeyfe;cb9z$PyMI)logmiVWgvvo}u$wu%$4NDK|k=KWD9eRDr*pnp0zx_^V4WTB{6{#15v& zm^ON{L!?XtmTUg1a>YdEc(^X8s>4YjBJK?*sy;eWT1(z+I&W6GeVCXZv~xi5QZVe? z1NV;1d@i9ZQaP$)q1>d1pom?p&hEOSaQLd_cr^6p_ulOXv0a&lQB#3-w0{+VPWoLO z11ORv+k;m;t7h38(#Gb}F}NNRM_Cg~s-8Vilm{mmQ>}euzR~s&hRWcrq62A#rMr`UA$tffocoa-nW8G^u>q4j5`2#?i z^zG&v%atojg+op(9QPiU3bMs^I;Y}EY-f~vJv5|Qk3B4`R2wcujIC<`^D5XQjxB5l zR}{?HjVw3qCuR8qbs1Kajw$16)Y2kpC?Td`Ye3VYGriM)tgmfte3a(V%ajc*bw;YK zk>l+pC!)FSJ{<#MeDxIo%Sc{Y_F6Rio zw)X0{$qQY)YsoFG09#SaB=%qtX4u~P1Z)%=2x%IVsvudmx5Y6?K?S5-qZ= zqE5z4(sJlEPDEVCf(ZwrKNQ0y!|L4|fi3%{$Z-q6+*e{r>~tWiDGXIny`Z?0t<}Zh z)6%uuw7r!|nw~<_xEtMDFhcj!WRrN`WjV&$f?dG`E|Sg}|4-*WC@S(y-P zo&gha#Vc4=3)Du2pn&H1%#SE_WMlO**oc2VNMM4>B+D zRZx!Sp7P&BLQ~-{c>$~>ok;1pqqPakAgZliO%=9aIpbYzO->V=oedwjN8FJC^dc9tvA%sXmKcGX=OZNy;?tA z>BizDOLrS|3WrCDxJ=M@6+c)5DI%$j+B(-9ul-j$r5pa{cy!}((E651OCzFgUL=x$ zgB{SZ&CKTbD)TDkoQR98-T)|y=|wG5uH$R)O$;=4XF7vzr@1?eV{^{uFt{;;?vI+O z;&Zecn;Y;%RN_#%x(k&9iBz^y2-4gaR8(oL2aIx*w#E6H68km-tHOLx$;hW0Lm}Y2 zRTtO_l)D49k^Up>O3N7g$JmM(d+eN#4td~8Y^iiiaHfochsvjjpXlQ zajfW7q-@1)>>}5xL^EV@0^Hq;f6R_#I--|Sq}Fsx5uQrb`zu|jF@StRSlLSQ+xA2J z$mc_`WowE3*R-d9Wot!=hhI%Fkpc|yAd%gu19@JqW zqyGS)P@fmbf96%_unCE~ntTxrz@7J4ZlK{0>ttOhTUPyF@=8mzxNE<0fnl+pd0c)a zJqFkq8EB|5){b6|)A0&2ECuKHjVHqC=Is2J{{UBc9Lq{vl*M%EY{Q?n)h-6q_t2+gxgAX;mXf z~-CtM-N0i~fhOLDb5z$+9yWsck)N@$I>Rw3#>FP< zy8PCL&4Q}Te$obYx%nqX!AR4Y^)%zD1uMo=?kmU8UooAw7qoYakBI?3$hjSQ5>|}e zjG>GI{6(Pgvq#Ml2F_q?9qHzk38@unBlO|uFcW5MZpxswhSf5 zugPf4p~30M?6At2+&=J$$KzyIq!^qXM}j;aWHvITELHYICE_>o$JawYrNS`^hTbZ9 z9j@@hr<$6k**rz|Y`Iw8Zj%211!I<{9hdhMP(lwE$O-tNq@&s@jU=p&A^8oy0ZrDE z>X8{@>PxIwxXnJv_G0tZ29L@ z1M1ECH6~>DnHf}1%~#O{NsHkWgX~9Wcx7$v=1_GTT5o76o8tFh;FjHxWyWf!5z&L@ zmHR748^MyL_VNoZ3g1vJS#H}SrrJvPgOsu{`CIcr)MC}o1GK1pj||Gk^(GL;{iN5o zIfCP&+3Iojrl0wVDU{*z#CX-Dys-8uPpg_)2<;qv6ZF*8l@0v_KAKqSLCL_p}?!<%ba#&^A#&kv%#bprrp7!>Mo;f zCAZkqv<_}488(`>SAPAm^k_+vqa^9#XW18cC}M@WT==Cp(?_d}W5nvH4aI6DxqU>A zJr{i}$zhf5JlslN3+l=8Q@_O`^DZSz3oc)A`BiqmY~qGTGM=2i++1`~_>DlOpv?Bh z-8hv|lByS*yfN1|IYIqZsA1H#W$@S-OIa(HijA|bc{vPG1JD&8F45OdaQe5~@~B4E z)3a7nu%5c7qQt|lBZe?fCa$goR>iGkJ2Hp$P z;*v3z?K{hI_ZC=ZEeYB2YRlwda6JWpO$w^UoY~|u9z2zGmC}+s#FNC7WL2Gmdub#V z91gcCj)j)Px*2vv;As##rntIC0dNbI4E9akHPJhTTm-FxdvLEe0BwSjs4h5cVEO}it1 z1a=<;taY5rc_CYh$ZdBYB8;QNoacd<`%rN+l&!FP#fZ2eC$a^k+UdD#$vWKmr3^^M z%{M6I7vcqyCpZ%8_$G`*H@Z%!^I4s@ZN1BltS5LE>txm%>;bUJoD$Q1h}OdAHsla_ zSvl!e+|Un#U!^_&0DDBO3#&N6h_jtz)lO2d zt+WbgCd_`ofy~)I4j&`iV%B+(pRs=$L*tw*}h8*TPz_5QZk)@&_H#3=H zkr*;Iz!Kzr1`}UWK#B+3@o-Y9QCn6@rAse~*pZ{dTPt$1Mh_yLb{7;bv*ZMxL0RxT z2+~u7RPbaxxFef{QwcjmcOCk#K2??XJc;8{lS$}j81~FTQA%9ld+sHwfl-80(#hG$ zBlO*%TjtY3*r*O$0J3eOf}UDwq?OrnjrU#nEKHt{W6;3jWmY<}y%ZENixTG_6(@$t zPYkvZ=yzr+r%fczBU^<(h0;qS+HYb`ofo50QMRn~Wg91%mI&q{?lHw1I^{^JsFl$Z zp56w^LJTuAEax5D!tNn~#~mJk}~T6)q1qaU^v>is!mV3s?m8 zbu*Y=;6Whq(L__WSIqM3WxoQw2+dgSj8h=-%J&M_i>P@k-NdDB3&U{_sP{24Q zl}3*?wCyGw>c<^!k|%L>?`0b5mW>0F4N=m`IHWkF!>4t%ew`ATFdCAfduJ2BMEhI_ z-m<6|)qM%ZS2O?tu?oXaiaJ^j%#&fvsxw?0#CfUw4q*3oZy*9J6(c*H6sLU|HA#pD zmpHn>C|pjs%~V`Tu}#-F!8D9%Id7tmk@@2%c~@05B|k7%q*;ZaZ{iTMZ@unQaL!2T z7iD-N?$n^=bS3Ypu79G;Nw3QKXjp&VPQm)4slJNqo$E4%zxalqg3qV=M8{<}HDq`+ zCckg7`)P>I-vedixK@8MI-IM0OVOHt`Y@6*J6fzhKNpvL0y>ZA#xQQ|PKz%#vELxE zY4*Dqoc*RDP=B1E&OeAd)~lTO$bM^Zsz0CD7OMXM?t2@&n*)SWk?9z`VE#x)>Qvqd zuy{RN3*oPtrHwW@8o^`Bd0#bsX;SxH^%5T!60I9Cwz!@L6WK*n_`~&Z4$%1}o*tuL z4D}XtYRW$aO@Cy43g4srEspNCrNjJ2kI@$f#9IwcsQv zU+E(mBF7S|zxahmm9F=Z&hS{b{{VCRo6qcK5*=|&NaX1pie}2-%^5}?ANk$$Up3V2 zopnRAO;J%HJIkI^ZQ7cv==nd)R^;lxoIj^mlGdyK{!gVHvX~nVXVS5wtd0ZXo0A3E z)->jhm4lycZ~2$a&e}@1{rP>jUO4?xZ+ktugn25TTMF>n-DG5^{IP#U^N*&?Y)w!A^Wc=~*&F`=d(F@VzfVtg#r-BPd9&!lwHrFYe@KJM zBl1lhxWIt0l0rW$dVE*U9-hbO`JZnE<6j6mNg?nyTlDC6W?m6bt7p(hZg3EE?Qi}N z`K~)}FpQ+NvpdKv0hp8*xpcsuWhmki6Wnx`-Fu_@d_Lj*CXd|RoAiZ-hY{Gk4tauj z{UBju?#xe;!9`po8@>J-FE+idb%W$+9ITJ&@DJ%!A4lqMvF+{<;dgf6^k_e-QQN#4 z_PtN}N6lc2s}|B%k0iEKx##!p9!P$l0bhjC`WzG(9z%Yg;V!wAot(mnVWB`T;Ag%i1YybRTYhGu0x1KM;?0yvEJ1Doh8t2 zM-H{hTK@oUFda=S6AdwMY0Leo(btDAp%m87I=NVFl&JB6?AO6by$ zLUN~vhS5Q=V|a|i?J8z-d`dcuVl|g^PLa(U^<1XGF)FD65SU}OnY+H_aKdokLZj@R zAuc+S-2&pRRuR$(95w4xroJ0orWvY3i%TZWduR$w@2X|Hc&1}c;%>A!;bdTCXum!v zJbAId3!An!;xElfVlL9L7~>O?Sxn-w^R#Q4)=*WGO6O<|x!p@*k1UM1lWT5OStQLR z?{78gLN`}EygY8M79R8KNXCy8^l!V{nr#OlqgeBMcYKqXoEiw%!83u-;#e%g#*$uo z9TMVm8u8o${Pj_co!yYJ>tyJAfCrDYbGVRjWYlvy)@cIca7{@xjUc(!)iFUGO!8%8 zTzmqsN3#`mLA#rMbt)2Zi`3?vp-ucusBJB2n8gflak0CsqL|5E?&DnV4OX8^ur%Us z*0yr<_*g8Ctl4^KASQ|+(mkq+83-(~0`VFUN)gn|A;W4MF7c3cM7B8Tv^u+rm#%^F zx$HLt8xW%^8Cj7M3alPBxDRFCyk0EtP#V2zpC#lywIrlfY=C~Yhqjct^Ai^squxw5M^U+Dt zY@KCE@0_EeYuj^5agOY+mlliI-FdR~b=A^6iFp0xLm1m`Dd_e(7)7Odb@kAt;zw(f zb?K$KN9_(>Nzr+M(l`dV+mf?ln=~RV^nBO8rE?F!%N#pX<#?WA;f|Pqo_|gnT_(BbkTRS zaAC^V9|fF&aC%u>*X6ysBTIPSR6KN&yL%3*Uk;#}Vj~ce?pC^#rjeUVP$n?E_leVz zdLfn8Amu#Ork7-K2mbOd!M}=U1`m3Wi)CpbfA-8qAb5z{Tz^2JEJPPmo>;0&OW*d? zsz!1FX5AAEP9zY*Z^p{mPlw>J7Y8QnNd2+g=EMWd4udz%+v@tXbxY%%ww)i5U+8AZhHirKI z66Kvry%mlMxQs*cs~y93W{R4`he+B5@&YnATa-;2g89i8<2R8gb z;-IOUSoh^)BzWWjRgN7v-sep`1ZM>-)-8KQiHgu;iNG*#Jr-@WG%l-P5xkZ(?i^Nu zM}StQ;|rXrdt0Ino|OK&wu)XzX;-#L~j6CS+J<4);7ZD2v_Uan|JWf>>SSt)eMNbPuSrb6; zQ9Db9vRW6u7@2mBY%Emmv|S(;xDjwgmo#S6x#vQxqa`btHakSvDV%t??kW=tq%DS5 z%ck?qUAFd=LtG+zO9Q*j94x6g{vgaN0-3~-E5=s**SkheQAgI%shX0Pb0gX*#F(W; zj+u>=9LCz*r>iPENu2&9-3IyY({-v%U7k&8sY$bA6LN->_XW5KWO)*i~B-BA}g0ki+VWCJqOBthQ$Y0NjtIl!r9j z=&w$-*?LvAEfsEa#%=dOXSLh8KG{218hRm!yO(rjtJ1#3Dho-_qcYrcL02;- zSAn?~PVppdKNMi~aUi#|BRg3M-dA(H{p8SLJkyPg@?08>n__`?G~-F;lE}5yI@s)) zCOlIX!g!QiP7NNms8%yn^F@^-p=dh>#WO*zyAOzn~1{2OMxU5qf+iJLJt7Fa&$yI zMieGLh}m>a&BUR=uw5GQD_A+${{UzWt#qLAz`E(7rMIO4bA3^o}#juA)Rd zP?9c0m+8416+7NPc5TgBsdjS^m|2vimi%Nk{?#`@v6Yh$9_)=9!&*uCC{Dd7Zz3|q zLOZgZ6&G+^Sam3tQ4QyRMSP~%=p&KBs;9U)y{)&!6-|TSlyCca#Gr28^ZEtPEOkGY z?B{sc^wgolND1z-&OQoOj|8op_bEsm-J-ZR8N#Tuih5@|#N{5qgp+IE9mO>)O$U>1 zed=!!T3%TCl6C7(%MVZrU6QVvAZ;6Nb#C+SQ`EZ{SX$}lA*Xn-ZM~`vqiZoYwzf5Z#?cz;H57-u3skG5=jt1!OJlv0WQ+k$m@yn4ozsX zt2Ul)B={$wq?PaE>`buqVlu)~j@B#;$FU3M6*-5F?<6gDBU3zis%CS*;e8OMjp4j} z!a<>XfVnmuP*9~8aK@Yyy0Ht3R5|y#-@@t$M*TlUY^cLSjfg(g$ndL;8rzO)+<2v- z-wLpdY*yliOK&9;9V^{#;R?qV6C=nCX(ytSrP;cv24==U>Qv>4LeCcn^(&NAcJ923 zatdCajxlcTVmYf79?8li23G(%1!AMX>1n_vxttdrOhSE6I<`IwXL7xQ)5yn+k&xbk z-t}*Rg{=j{UP(<5!rM0^Y?^gcK-)FXCCe#hJi4`AS9=eVyb@UK;^C&tt~mwWW{zZ} zIm(vS#SAq8&1lT+3Zdc>7fE4jD{j<@s-2ooA#S!rMS``1SGPBTC*ZpkXrsT62=yyWW3rO%!`_l(bgV6n zrsZ2crem$!NoMqFG7K(-jTvN|o{^85Ze}qjEL2v-2AgLXWX~WZol2Ucz~N@BYe4W< zI!ro;Q26I@@dNZyETqEXv2<<&tb_ClQ^xd1Elg~i>QtbhfrJY=$Ca$7Fw7nG9;8LL z(?V3UD==D7c0k<2j%23oRWm!W!~p0@<(*oy@}_dmwOKEMFY*^nvvh2a?OAya`|7Lv zO@jIhBbp){$2f~ya+7w|+}V&ck2D@3wB4Nt>sY?u?rol4p`-kodU=C3D|*Uojlfa;*CnQm*?T zfwJ@z(ps|;W4b{ZarfCi+AW`@aI`fQJznt7a389KeN3*@xCATYbFZ21AukSZ%^{U3 z%grbLKfz^wBZpJS4JI2$6NeWxy!**sn`veQ7~~L-_Y4fbu~yp)h`Si)9|DPo4y|^v z(@m7}VF3;d7nlx(HRksuX2ztbH+FZE#mG5osvd3*QPCSovzVnwymW*PWU0y>lEj@6 zY|f4`#`=^T=)SK`0yUPgQ)R*oYW!Q#+l}icXtlFsgp>hdJi| z0P3YIT_v#z_>C2!)XyQA`K&JC$)QTpP*zmP(*zP&@4kUvfx@V_8fb{*K}<=sSY#Is zn>R?>qbl;BmM1*1lb`)K0%6z;(YEu*)|0~5MgGrGon$X_x*ZgKM%U9Z_Kp(a%#IJQXc?z_}Y zRc%a8AP(W&sSF}zsrY1D+|ix1^2kn5){b{cJ4|A?Xk2f>X3Uyd4|(nfu86iu=P-7X zW&m#mJHtgCFT?4shKCN;u*d_UyLg-Gk%;Y7u?+OlHz3sJDw#)JPY?mo<^y@+eo6CM zD$Y>W!2@+XZl}OW-o&{oYFEpl8?=*FSC?W~lU&xGF(Dl6E!`>NA%WvSawf zxVsw$P}*3x05_g%B-=`6wpTT*01?T0ICSl1dKBW_EUj3{=YFSp70x+|FA{{sP~mGJ zudRq&m0MD?JSvw?!XEN;UTAZT!t0oKh2Tity+Y3Vkp0fsw!|e8N!+~CaGZ^#l<8k# zlI(Sn-4{;MJQ5>_ke`A(5pud|!pyuQW=>&_%?@5k=QhtnVXnz28^KaCv=gHAsqA`D zd7y@7r|n=Q07yYllE;=6F$8jDAOm6)W8f^QB;R_p<7T9Z$~{Cfh1^1LQ8;j0G&>)& z`EF~;Jd020knNYi8tV#|xN{4Z^#1;gvf=G>A7vA{6u(1o;I>w5p3mC;nQt4W{{UsM zBKr!HZhL|%bo)O*r%!8{3n<^oC{jC;**|0OOt)=wed!I@{1wi(r(4>xsZa0dGMz5w zM%qX43%-c^+RI^f#^C^IhzohHGh=XvIR5}{0QFmqPw(hI5}A=Uj!Ry}chIEdwezwV zLk6|&KG2_~*(?nH`l+r1lgRxR{YojgmJJMf zVxVRD_fNzl$L$7ZSzFa!7ZHqip@&`#kQ7vV1zIA`Ll_SN59kpN z+Kf%_xRy^0%aUy-EhB7YeYp3Qz9IESe0SLF8v$7(OPe$x^6cI6MN(`WhCtexdoVb( z1M#wpIKCqAL6G}XPOPnetuwF6`ux&9c!x-A=;qMPzYb@H8m@ z0NR?_<@-v9<#zBKqNAW;BTanMt)KzGwWp_bHx7(;iCE$2!(>hUZ5~}7?RT56ePv92 zVD}#t7~DPf2eKsp01A3K8A;KH!q!u29l?^C3X-}9XvqEPF;+y^YY<8LzEf)l1L6|C zt(y06)2Tz?H^H^VzO{}dG9hHRs=yz+W%}xs783FSHnNx5{5GAH&2?j1`0c1Vk9w5n z2%=M*BNoWk)(7d?x5y`(R$IA&UgL`djpHReWBEtP8rkrcjUkK2C|r*cx?Jrgi*_SGY9Z|D+Q_fH#3(s7YIbX# zuRebz0(SPOpWR1p1Y9H?w62rY?%#k_jvBm9ns{7H6|^T^fmW7~*D-_mTz^Et!75_a z!R{xM9r8j&wUw-G$Km+tZ|+NafPP40fMZn$ydKfi zJLHIV+*LW20ig2QL=4j zBOvwig1FtJ#GEW;u;*l2#J;E!<{H(a$4|1)Y45*8imi^0Su?tPx_&R{tIfu$T_7@4 zym{F-Q;pOTTH2SE_@q>;-^@uuwrwTsS)tgvD}Ailk3;=Zzhm&GyX_^+IUoL24s2GW z-qXLy#Rn|`t0Yw0eC|Ezr1q5S4O#CqqBwf;-l3))oWNrMIm~UpI7@B?v+WuhfAh6v zrm^l8dz^Zdp*gmMt;y8uHfMyX)}u8ju$`Zb-M)@`7quwCv(UQk6xZQn@>yGIh&f3< z)ZnWlOPd}=4n|#M#dtY&sLe;~90o1UgxiKfejzLNhYO9Q#cu|*1(nS=+3HC=u7GYP z=wEEM=uwY{C)Juh9|P1ocBh1xhdCiEW>3D`#u+%42rm7%ZTwvAW4zds>GR)yfRAP)DupOEBM8 zI4+pssmEk>#+535R2J&zIo;=<73Njkl5eK%P=+|cvx)Et*xws;X!2Ca6|x=4TS4~E zGs(y_=81ulpI!Fo4bxB;O5gyuiY@fghU!QMa0wljgk@KtG_9wmlRU-2-18}HGifmR z%VDYP?sO85RCw`G-&YBmM4*=SRcv<&btJjsYm9f-T>@856?AN*RXX1Uo~5rDucWG) z^kXpK;1pZxrMG>oAahjcv1d~{%q5_H6kK~s3ADh!%GN`JZ4uWRDhkM-kTOAV@;Y5T zPuPaoHvTFNn?*uM9fOB5N_XpaIOd29r>^99QekVMW`cN_3n7FTo$ey`ONG7SX7ze1 z#CX<60v&H8dvRvCT)STtCq<%8n+wFHbvsL<0LSoI#?nY7mCtL#ukKdNTNH@E;2r?7 z&ueCg_Kq@=>*yCv8g|uBOCNxj7xGXRy0YTcF_sn^7Zm{JYuyWr6BV07!NjEJ zbT-p%>0-oc7Yt+_Q_O^w2*te)bePm_yR4oKEq3Jm6yoy@yZ}jf>1v(hlzxft+ngP(w0L~P{%HXH5p}Bch+Ro^iB@ZIU~CyJ zkIv0#H`rygS;^XZhc`o)*qi;Osv7pjb&^xygTc${V!Sl(=QY?U zC;+=^GWaw4uRe;A&eo{g{35b=@yd<@Qan@9V>ON=EOH+*mEfiI1D2_K<~5J2=<*v@ zk2sv0Wd%NKGOT!(ACk@4ieiLXIHvcfn}}ls{_anDPEt$f6=AAgSWn z-=bWuz?*KyI++hbs$&>}HvNSo1-QK34*j932e~672mPBsN0pqqzfpJ%zccmz3*0i{ z#huJHbJfKq)if|0x{{JH`QLK0YMgF)UPI@V$Ei@&*sPhX(VN7lkB7=Fx}b~Zw?Gpvl4qaZzryOP!)f&?$kOX` zV1C~@m&(W^@EWN4oxGMqaWU`&th`;mu_XIbk&x}cl~sVGx(u+CR;d@6_R2{O^Iyj2 z^+HLt6Fd;o1^F~8o@}lFH}y_t7PtTsI0aRPrnW_uDskBs5spsr>$=`cpIA#E)$a2l zSKB8G8=@hR#8?6o@M5|Q;+Dvl6;d>M?mh)@)IHnWeW(U8?$X}WR5a4KfB`97B7}3W zWHPrbO_2|l zB)oOxo&&_Eg;0=Ok$y@PMc)L=YjRJqv4RPZli;(05>Z>CQN@{iq?6#d$&T-WZW&{C zgp!JIj5oT#N}E<77&hibx7TbN~=!7BB#BJ z_j0-q6{Tqb(Sd#_s%*j8WH(Ztio(Hg4sG{H<&l9^ah1wbrHU(p>yf^}5l>u3fH;o2 zCvluUNarRP-0*jXw&-YZnz9Knn1=%+gKzC!H;Y$n?Nf}ScF9;Kj%L`99m#Tdj{|U+ ziwi6b?JD45ZNco!tAH- zxMd#oGX#u;eT*?cKM~*UE7Mjm{{VXF4=|+a$?S;nuIxF7VRZ4l3aaJ_e+wF10=Z!L zJ{-o0&V5ggD&Pjtw;rU>qD*9cp!Fvn5$)(lXS!W4H}8 zkUDr;B6MQp4a$LuO3_aCIf2Y}nXIL2_L8Qy0}z>kgjm?0daU9Vu{t9IUwdlj0lhX7 zjY#P*QjDVuPFH0EXt2W*pFBFob+W5TDH;b^Uu!4LnmIRaRD5?2d{UVr^-A^S2Wy^` zD@m`26jc^GzEiPcW;0+(9F#NMNz`hR98QWEni1ERLeR?dD9pzYx^W3WBQuAARqAPs zPSVOoIT<+X#Z_8GBVJ7HRa4)4lXV$TPDeQ(BLO;D16?4EnLrmpO3Lh&S-E+NFL~qJa=GVDQ}Z$tjHSCWzesUEyO{Z>Wa zg(=+1X8!;TZ6BiG`bn;gS}DT^O9jcqw^Vjw(?^Zkf$)|lF*V@9Z!@!}%UC=p-OXej`&tuLPIAn|+W4FaWO|v^f{UfEw zIl0Xr600)p&19avWQWZ#?Xs#mdQ84WZYuhaywxT17hl;7Ykaa(u<|oE^h;*Q*BvFq zX(JtFKP8_(YG-lQSzLTdZQDgNjqf4xD{-Us1DvDW(%rIAGxxaNE^bY^`6WfOSZLOK zaf63-pVej`UP^c$YUsu$Km4XuSJZBDpF>f82Y|=B7m@xA1NB{I6SLO)OiG~m0J8y# z%zybyZY3`?>PhuuEMyHd;Wz>HaS5&0DpwM~+lb0Hlz%Kd+)XTCIAJc1-ZqLvYt|Z2f0rO4`pThvx5@9|=^;x*Yr3Z>w#H7Ce z0D3lk!OcL?8-QU%-T0eFfeX&T@OIn(0I2EEg`W&YLNzO`#3UMKvN>OH^GML+pxO2` z=c|#&rT$BWn>xe&>xWQQf9cW_`^Do_$kC@yJ;zye$J8xo~3l{aRa)_cKH&sw!ZxRso44c zsH!mRBdXba8(AU#@*DJwFCD3${k?}qfT-hM9`xwU(3wz&V{sXD{idlttxNouKE0`V z{rrc_s*G-NJZy-FmXW^6)fo0S5td)9WN`J|N#^A9Yn;}4ru|e51d=p%sMj69f(mXL z5_4}tO|>p|;sS6=6ZbX*O17t5zKjwus!`-KNK+P_uDIjtFj9!Wdax4APU z&N^70sZ!b@5cih0FDd8tgER3eZ9r#>&?g~;GNZ=Q9nSM*2 zbyq?X#9u@SNkyh&+c%lfOi@v~Yjz^dzVHxkCEm1*%0BM|Z7w4uZ5^A~E%-{|t16P& z0_%ILmkcXsVk{4NH|o2zR8&+n8H_>FKe`D}_8D9ZSkue&t^mb6}LVGj^T>f{pEl@wrM<&^oeLK4&?tw(+l$4x)*| zXZB2aj`|KO+k=YuPNh+G2kkGY8YZW!%G(4)w9*Ef#_{3`5!5nBb#J!8Rb?JBi+X^~ zS1Y$@hJ8$+Z#Pm-pmp`hf8qqx=k56L{- zIxB)@kHp;^(F?B<0?)qlZfWK4S01x#;JVkZU2^#~r zxjx9p-2^FyQ$fL!IP`VDL`-zdX#W6LVe0(f(JQEslEkirJS_uoH&XOWbjHnz?>z<4 zP%*Kc3@q?yzs(lMz}pwp7jzf>RA*BABc65Kh9jYyHQd0_)T5~~IzbWj&YFz^YFDyC zOKJ;Szzq|WRWVb)yCVya6TNcIQ^i#$FfdG8oLcr~mqEc*Z&{f~0_}b( zMqFZ9Au+hJ!;!kGRu>7}_i1x}@k>TBnzcbQHKp|?D`p*&fSd-q?y&Dw$?9Q-O91x{ zrpVYBCVN`sz7heh|PDqlE#GL1Mo=N6S(wmDqJ6~4#C{h7;8+awOiybfIj}9hqjf*r@ z@&MVOb3q!Bz@aNKS5>;w003yNUk&^J!;^I1X&YeX76CRcjpp!gJlvy5}^EQI+Y z9gIdd3;3p=jB;r9CV33nA7%qc!aKL;a*d9Hnji}yW}Iz*iiUPEAsPY)u@5FF`y0pr z>Tb4Zt&MbxPiY$&VCI3)fSYw}AaB2y%~RyW#og z){X}0qljx@k>U`pvPG}c+Bg&ZPD&V=Z>yAa0{;LNQx$K26t>Rdb#ltTV{wsuqs1L8 zRxUbqU1ligj{B8W!m2}GV(ki)l2(UYYE_{-lu}YgIMU~FPP7oZ$eeg083Va5k&GKY zeMPaRgUlxfh)*`1+>+Z)c&e&$D4@n#-l=1D^zf!U)2yW@f5BEXrPy@_3f7C=I@=S) z653B6VsW%~wZSUNgojk*4#tb>6Rj=a0wuJcl%tq((oD{tR zSf?YGu~kAtsIjpi-Nh_Fns8H|e!!O4Ip5f*qQm6(n!rW@=1M4GWvzG* zk1!X?5ireneYB5mx&f+oSH}U(8FRyir0-mh6x*>^hI*#;Af%;pNxOhdO-$0?-h%Fz zD$D4ROu*~qY5YhkmQup|TxsUIV^;eds!lSqE3T!u0KMKpEMbMiM>vn+{;Eb=$QbK} z<^!r3*uZJK8rJkyQk&SWc|DIsgtU>F&hP;l9uXKIUE{=AM$3)7>)IbAG?=^GH@Kd6 zTP0QtOt7+wmNRF|W8rk*^jQA;-Q{Ie^!c6lwCcEOWp=%xvfK$h*f?@~vfJr!G*}k+ zo0N~Nz%~;w4ukqCk(J-=Jg$`4$QnGAxpZz3Yjz=jR!~oxN2N(2ws^;n{{RZKDqIK~ z(UDGC-lYdd!njzR8TWRPqp4YD=Ix>$%~f5sj=hOUlDW+t!@q)8MZE;b%PiFmF0*Bf z5&rz#kB*X0JA`ad(d8<5hFrM7uN*sm`^9(UhWT-UeF^Wo)ny4HgLT1DI!by*njKy=HDe#RM!Pwv=CxQcS6*hAYJu zcXM)-lIV%JSR$6Ppplk0pCva(vv`&;(P&LnHMuE5hhlg(Q3Mwg} zj-|jz?9T6Mqf(tqUKT)=Jue-?L!8hZfM?Ebm9$@EyN8`a4)@u_wZDz++Fj` z=ehT}T&BdG6wQ&ZA;9RXcrVcZ7pQzQI1&YvVZ{j05D{3E;&k+;T=2i5MLorCu{q*w^e7YYSXQdfq8* ztG0k%{Zx5<&Q}9<;Nr(?s%wVipQQ2{t~zqzmnawJP;t4}Cg_j>7dz41*8#*~al4@c z{8S{8%Eu5u3e+<-mUDF~I}pRYA%)ErdElc`lX}Efx{yw(mwsRZOR7z^ko!{=v~$y* z_%=W|IC0T=@|>dV(HTvmLDg)(dDrtzn#S%LsOJHFNxEz}H0Yx1G)dHCjJ0+eARTyjLcM`bCco!g??9KkGk&<*%<2#AOcMf!o+%-t;ZJ@A2 zW+3R40dT#RbsIKz7RmnrFi%G)aT+N0Li_PWM9UpXxK)JY_Z~8nByheSC>8eeIzzeQsrfYhe{{YOSCfSXY zq>l1c(hm!K*9xhIj)^z;B;!gNfB1a29uWTk_HOnk#tSjs5%w>nTZe35cYd}Z)wxIL zh;i6@Xtk;m5y+3wD)DKpp(pqYa9tVmTQEwF5j)t5k7uf!G=;~&E${2Ch>iNNyP?cd z&erTzI|zn^$bU7@#Hw*SQjpG3TSb^Vc5bgr!r|nKi)AXGYerVXF5m3rPWv1lEB

R%yr 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 Date: Sat, 7 Jan 2012 13:18:52 +0100 Subject: [PATCH 48/63] moved code to common --- src/BeSimple/SoapClient/FilterHelper.php | 178 ------------------ src/BeSimple/SoapClient/MimeFilter.php | 138 -------------- src/BeSimple/SoapClient/MtomTypeConverter.php | 94 --------- src/BeSimple/SoapClient/SoapClient.php | 50 +---- src/BeSimple/SoapClient/SwaTypeConverter.php | 82 -------- .../SoapClient/WsAddressingFilter.php | 1 + src/BeSimple/SoapClient/WsSecurityFilter.php | 1 + 7 files changed, 3 insertions(+), 541 deletions(-) delete mode 100644 src/BeSimple/SoapClient/FilterHelper.php delete mode 100644 src/BeSimple/SoapClient/MimeFilter.php delete mode 100644 src/BeSimple/SoapClient/MtomTypeConverter.php delete mode 100644 src/BeSimple/SoapClient/SwaTypeConverter.php diff --git a/src/BeSimple/SoapClient/FilterHelper.php b/src/BeSimple/SoapClient/FilterHelper.php deleted file mode 100644 index cb21c01..0000000 --- a/src/BeSimple/SoapClient/FilterHelper.php +++ /dev/null @@ -1,178 +0,0 @@ - - * (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; - -/** - * Soap request/response filter helper for manipulating SOAP messages. - * - * @author Andreas Schamberger - */ -class FilterHelper -{ - /** - * DOMDocument on which the helper functions operate. - * - * @var \DOMDocument - */ - protected $domDocument = null; - - /** - * Namespaces added. - * - * @var array(string=>string) - */ - protected $namespaces = array(); - - /** - * Constructor. - * - * @param \DOMDocument $domDocument SOAP document - */ - public function __construct(\DOMDocument $domDocument) - { - $this->domDocument = $domDocument; - } - - /** - * Add new soap header. - * - * @param \DOMElement $node DOMElement to add - * @param boolean $mustUnderstand SOAP header mustUnderstand attribute - * @param string $actor SOAP actor/role - * @param string $soapVersion SOAP version SOAP_1_1|SOAP_1_2 - * - * @return void - */ - public function addHeaderElement(\DOMElement $node, $mustUnderstand = null, $actor = null, $soapVersion = SOAP_1_1) - { - $root = $this->domDocument->documentElement; - $namespace = $root->namespaceURI; - $prefix = $root->prefix; - if (null !== $mustUnderstand) { - $node->appendChild(new \DOMAttr($prefix . ':mustUnderstand', (int) $mustUnderstand)); - } - if (null !== $actor) { - $attributeName = ($soapVersion == SOAP_1_1) ? 'actor' : 'role'; - $node->appendChild(new \DOMAttr($prefix . ':' . $attributeName, $actor)); - } - $nodeListHeader = $root->getElementsByTagNameNS($namespace, 'Header'); - // add header if not there - if ($nodeListHeader->length == 0) { - // new header element - $header = $this->domDocument->createElementNS($namespace, $prefix . ':Header'); - // try to add it before body - $nodeListBody = $root->getElementsByTagNameNS($namespace, 'Body'); - if ($nodeListBody->length == 0) { - $root->appendChild($header); - } else { - $body = $nodeListBody->item(0); - $header = $body->parentNode->insertBefore($header, $body); - } - $header->appendChild($node); - } else { - $nodeListHeader->item(0)->appendChild($node); - } - } - - /** - * Add new soap body element. - * - * @param \DOMElement $node DOMElement to add - * - * @return void - */ - public function addBodyElement(\DOMElement $node) - { - $root = $this->domDocument->documentElement; - $namespace = $root->namespaceURI; - $prefix = $root->prefix; - $nodeList = $this->domDocument->getElementsByTagNameNS($namespace, 'Body'); - // add body if not there - if ($nodeList->length == 0) { - // new body element - $body = $this->domDocument->createElementNS($namespace, $prefix . ':Body'); - $root->appendChild($body); - $body->appendChild($node); - } else { - $nodeList->item(0)->appendChild($node); - } - } - - /** - * Add new namespace to root tag. - * - * @param string $prefix Namespace prefix - * @param string $namespaceURI Namespace URI - * - * @return void - */ - public function addNamespace($prefix, $namespaceURI) - { - if (!isset($this->namespaces[$namespaceURI])) { - $root = $this->domDocument->documentElement; - $root->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:' . $prefix, $namespaceURI); - $this->namespaces[$namespaceURI] = $prefix; - } - } - - /** - * Create new element for given namespace. - * - * @param string $namespaceURI Namespace URI - * @param string $name Element name - * @param string $value Element value - * - * @return \DOMElement - */ - public function createElement($namespaceURI, $name, $value = null) - { - $prefix = $this->namespaces[$namespaceURI]; - - return $this->domDocument->createElementNS($namespaceURI, $prefix . ':' . $name, $value); - } - - /** - * Add new attribute to element with given namespace. - * - * @param \DOMElement $element DOMElement to edit - * @param string $namespaceURI Namespace URI - * @param string $name Attribute name - * @param string $value Attribute value - * - * @return void - */ - public function setAttribute(\DOMElement $element, $namespaceURI, $name, $value) - { - if (null !== $namespaceURI) { - $prefix = $this->namespaces[$namespaceURI]; - $element->setAttributeNS($namespaceURI, $prefix . ':' . $name, $value); - } else { - $element->setAttribute($name, $value); - } - } - - /** - * Register namespace. - * - * @param string $prefix Namespace prefix - * @param string $namespaceURI Namespace URI - * - * @return void - */ - public function registerNamespace($prefix, $namespaceURI) - { - if (!isset($this->namespaces[$namespaceURI])) { - $this->namespaces[$namespaceURI] = $prefix; - } - } -} \ No newline at end of file diff --git a/src/BeSimple/SoapClient/MimeFilter.php b/src/BeSimple/SoapClient/MimeFilter.php deleted file mode 100644 index f5c4616..0000000 --- a/src/BeSimple/SoapClient/MimeFilter.php +++ /dev/null @@ -1,138 +0,0 @@ - - * (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\Helper; -use BeSimple\SoapCommon\Mime\MultiPart as MimeMultiPart; -use BeSimple\SoapCommon\Mime\Parser as MimeParser; -use BeSimple\SoapCommon\Mime\Part as MimePart; -use BeSimple\SoapCommon\SoapRequest as CommonSoapRequest; -use BeSimple\SoapCommon\SoapRequestFilter; -use BeSimple\SoapCommon\SoapResponse as CommonSoapResponse; -use BeSimple\SoapCommon\SoapResponseFilter; - -/** - * MIME filter. - * - * @author Andreas Schamberger - */ -class MimeFilter implements SoapRequestFilter, SoapResponseFilter -{ - /** - * Attachment type. - * - * @var int Helper::ATTACHMENTS_TYPE_SWA | Helper::ATTACHMENTS_TYPE_MTOM - */ - protected $attachmentType = Helper::ATTACHMENTS_TYPE_SWA; - - /** - * Constructor. - * - * @param int $attachmentType Helper::ATTACHMENTS_TYPE_SWA | Helper::ATTACHMENTS_TYPE_MTOM - */ - public function __construct($attachmentType) - { - $this->attachmentType = $attachmentType; - } - - /** - * Reset all properties to default values. - */ - public function resetFilter() - { - $this->attachmentType = Helper::ATTACHMENTS_TYPE_SWA; - } - - /** - * Modify the given request XML. - * - * @param \BeSimple\SoapCommon\SoapRequest $request SOAP request - * - * @return void - */ - public function filterRequest(CommonSoapRequest $request) - { - // get attachments from request object - $attachmentsToSend = $request->getAttachments(); - - // build mime message if we have attachments - if (count($attachmentsToSend) > 0) { - $multipart = new MimeMultiPart(); - $soapPart = new MimePart($request->getContent(), 'text/xml', 'utf-8', MimePart::ENCODING_EIGHT_BIT); - $soapVersion = $request->getVersion(); - // change content type headers for MTOM with SOAP 1.1 - if ($soapVersion == SOAP_1_1 && $this->attachmentType & Helper::ATTACHMENTS_TYPE_MTOM) { - $multipart->setHeader('Content-Type', 'type', 'application/xop+xml'); - $multipart->setHeader('Content-Type', 'start-info', 'text/xml'); - $soapPart->setHeader('Content-Type', 'application/xop+xml'); - $soapPart->setHeader('Content-Type', 'type', 'text/xml'); - } - // change content type headers for SOAP 1.2 - elseif ($soapVersion == SOAP_1_2) { - $multipart->setHeader('Content-Type', 'type', 'application/soap+xml'); - $soapPart->setHeader('Content-Type', 'application/soap+xml'); - } - $multipart->addPart($soapPart, true); - foreach ($attachmentsToSend as $cid => $attachment) { - $multipart->addPart($attachment, false); - } - $request->setContent($multipart->getMimeMessage()); - - // TODO - $headers = $multipart->getHeadersForHttp(); - list($name, $contentType) = explode(': ', $headers[0]); - - $request->setContentType($contentType); - } - } - - /** - * Modify the given response XML. - * - * @param \BeSimple\SoapCommon\SoapResponse $response SOAP response - * - * @return void - */ - public function filterResponse(CommonSoapResponse $response) - { - // array to store attachments - $attachmentsRecieved = array(); - - // check content type if it is a multipart mime message - $responseContentType = $response->getContentType(); - if (false !== stripos($responseContentType, 'multipart/related')) { - // parse mime message - $headers = array( - 'Content-Type' => trim($responseContentType), - ); - $multipart = MimeParser::parseMimeMessage($response->getContent(), $headers); - // get soap payload and update SoapResponse object - $soapPart = $multipart->getPart(); - // convert href -> myhref for external references as PHP throws exception in this case - // http://svn.php.net/viewvc/php/php-src/branches/PHP_5_4/ext/soap/php_encoding.c?view=markup#l3436 - $content = preg_replace('/href=(?!#)/', 'myhref=', $soapPart->getContent()); - $response->setContent($content); - $response->setContentType($soapPart->getHeader('Content-Type')); - // store attachments - $attachments = $multipart->getParts(false); - foreach ($attachments as $cid => $attachment) { - $attachmentsRecieved[$cid] = $attachment; - } - } - - // add attachments to response object - if (count($attachmentsRecieved) > 0) { - $response->setAttachments($attachmentsRecieved); - } - } -} \ No newline at end of file diff --git a/src/BeSimple/SoapClient/MtomTypeConverter.php b/src/BeSimple/SoapClient/MtomTypeConverter.php deleted file mode 100644 index 37fc80b..0000000 --- a/src/BeSimple/SoapClient/MtomTypeConverter.php +++ /dev/null @@ -1,94 +0,0 @@ - - * (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\Helper; -use BeSimple\SoapCommon\Mime\Part as MimePart; -use BeSimple\SoapCommon\SoapKernel; -use BeSimple\SoapCommon\SoapRequest as CommonSoapRequest; -use BeSimple\SoapCommon\SoapResponse as CommonSoapResponse; -use BeSimple\SoapCommon\Converter\TypeConverterInterface; - -/** - * MTOM type converter. - * - * @author Andreas Schamberger - */ -class MtomTypeConverter -{ - /** - * {@inheritDoc} - */ - public function getTypeNamespace() - { - return 'http://www.w3.org/2001/XMLSchema'; - } - - /** - * {@inheritDoc} - */ - public function getTypeName() - { - return 'base64Binary'; - } - - /** - * {@inheritDoc} - */ - public function convertXmlToPhp($data, $soapKernel) - { - $doc = new \DOMDocument(); - $doc->loadXML($data); - - $includes = $doc->getElementsByTagNameNS(Helper::NS_XOP, 'Include'); - $include = $includes->item(0); - - $ref = $include->getAttribute('myhref'); - - if ('cid:' === substr($ref, 0, 4)) { - $contentId = urldecode(substr($ref, 4)); - - if (null !== ($part = $soapKernel->getAttachment($contentId))) { - - return $part->getContent(); - } else { - - return null; - } - } - - return $data; - } - - /** - * {@inheritDoc} - */ - public function convertPhpToXml($data, $soapKernel) - { - $part = new MimePart($data); - $contentId = trim($part->getHeader('Content-ID'), '<>'); - - $soapKernel->addAttachment($part); - - $doc = new \DOMDocument(); - $node = $doc->createElement($this->getTypeName()); - $doc->appendChild($node); - - // add xop:Include element - $xinclude = $doc->createElementNS(Helper::NS_XOP, Helper::PFX_XOP . ':Include'); - $xinclude->setAttribute('href', 'cid:' . $contentId); - $node->appendChild($xinclude); - - return $doc->saveXML(); - } -} diff --git a/src/BeSimple/SoapClient/SoapClient.php b/src/BeSimple/SoapClient/SoapClient.php index 23b529a..c299901 100644 --- a/src/BeSimple/SoapClient/SoapClient.php +++ b/src/BeSimple/SoapClient/SoapClient.php @@ -25,13 +25,6 @@ use BeSimple\SoapCommon\SoapKernel; */ class SoapClient extends \SoapClient { - /** - * SOAP attachment type. - * - * @var int - */ - protected $attachmentType = Helper::ATTACHMENTS_TYPE_BASE64; - /** * Soap version. * @@ -104,16 +97,12 @@ class SoapClient extends \SoapClient if (isset($options['soap_version'])) { $this->soapVersion = $options['soap_version']; } - // attachment handling - if (isset($options['attachment_type'])) { - $this->attachmentType = $options['attachment_type']; - } $this->curl = new Curl($options); $wsdlFile = $this->loadWsdl($wsdl, $options); // TODO $wsdlHandler = new WsdlHandler($wsdlFile, $this->soapVersion); $this->soapKernel = new SoapKernel(); // set up type converter and mime filter - $this->configureMime($options); + $this->soapKernel->configureMime($options); // we want the exceptions option to be set $options['exceptions'] = true; // disable obsolete trace option for native SoapClient as we need to do our own tracing anyways @@ -258,43 +247,6 @@ class SoapClient extends \SoapClient return $this->lastResponse; } - /** - * Configure filter and type converter for SwA/MTOM. - * - * @param array &$options SOAP constructor options array. - * - * @return void - */ - private function configureMime(array &$options) - { - if (Helper::ATTACHMENTS_TYPE_BASE64 !== $this->attachmentType) { - // register mime filter in SoapKernel - $mimeFilter = new MimeFilter($this->attachmentType); - $this->soapKernel->registerFilter($mimeFilter); - // configure type converter - if (Helper::ATTACHMENTS_TYPE_SWA === $this->attachmentType) { - $converter = new SwaTypeConverter(); - } elseif (Helper::ATTACHMENTS_TYPE_MTOM === $this->attachmentType) { - $converter = new MtomTypeConverter(); - } - // configure typemap - if (!isset($options['typemap'])) { - $options['typemap'] = array(); - } - $soapKernel = $this->soapKernel; - $options['typemap'][] = array( - 'type_name' => $converter->getTypeName(), - 'type_ns' => $converter->getTypeNamespace(), - 'from_xml' => function($input) use ($converter, $soapKernel) { - return $converter->convertXmlToPhp($input, $soapKernel); - }, - 'to_xml' => function($input) use ($converter, $soapKernel) { - return $converter->convertPhpToXml($input, $soapKernel); - }, - ); - } - } - /** * Get SoapKernel instance. * diff --git a/src/BeSimple/SoapClient/SwaTypeConverter.php b/src/BeSimple/SoapClient/SwaTypeConverter.php deleted file mode 100644 index a7f50ea..0000000 --- a/src/BeSimple/SoapClient/SwaTypeConverter.php +++ /dev/null @@ -1,82 +0,0 @@ - - * (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\Helper; -use BeSimple\SoapCommon\Mime\Part as MimePart; -use BeSimple\SoapCommon\SoapKernel; -use BeSimple\SoapCommon\SoapRequest as CommonSoapRequest; -use BeSimple\SoapCommon\SoapResponse as CommonSoapResponse; -use BeSimple\SoapCommon\Converter\TypeConverterInterface; - -/** - * SwA type converter. - * - * @author Andreas Schamberger - */ -class SwaTypeConverter -{ - /** - * {@inheritDoc} - */ - public function getTypeNamespace() - { - return 'http://www.w3.org/2001/XMLSchema'; - } - - /** - * {@inheritDoc} - */ - public function getTypeName() - { - return 'base64Binary'; - } - - /** - * {@inheritDoc} - */ - public function convertXmlToPhp($data, $soapKernel) - { - $doc = new \DOMDocument(); - $doc->loadXML($data); - - $ref = $doc->documentElement->getAttribute('myhref'); - - if ('cid:' === substr($ref, 0, 4)) { - $contentId = urldecode(substr($ref, 4)); - - if (null !== ($part = $soapKernel->getAttachment($contentId))) { - - return $part->getContent(); - } else { - - return null; - } - } - - return $data; - } - - /** - * {@inheritDoc} - */ - public function convertPhpToXml($data, $soapKernel) - { - $part = new MimePart($data); - $contentId = trim($part->getHeader('Content-ID'), '<>'); - - $soapKernel->addAttachment($part); - - return sprintf('<%s href="%s"/>', $this->getTypeName(), $contentId); - } -} diff --git a/src/BeSimple/SoapClient/WsAddressingFilter.php b/src/BeSimple/SoapClient/WsAddressingFilter.php index e1093b1..721fe87 100644 --- a/src/BeSimple/SoapClient/WsAddressingFilter.php +++ b/src/BeSimple/SoapClient/WsAddressingFilter.php @@ -12,6 +12,7 @@ namespace BeSimple\SoapClient; +use BeSimple\SoapCommon\FilterHelper; use BeSimple\SoapCommon\Helper; use BeSimple\SoapCommon\SoapRequest as CommonSoapRequest; use BeSimple\SoapCommon\SoapRequestFilter; diff --git a/src/BeSimple/SoapClient/WsSecurityFilter.php b/src/BeSimple/SoapClient/WsSecurityFilter.php index fec0060..cb3fc7c 100644 --- a/src/BeSimple/SoapClient/WsSecurityFilter.php +++ b/src/BeSimple/SoapClient/WsSecurityFilter.php @@ -16,6 +16,7 @@ use ass\XmlSecurity\DSig as XmlSecurityDSig; use ass\XmlSecurity\Enc as XmlSecurityEnc; use ass\XmlSecurity\Key as XmlSecurityKey; +use BeSimple\SoapCommon\FilterHelper; use BeSimple\SoapCommon\Helper; use BeSimple\SoapCommon\SoapRequest as CommonSoapRequest; use BeSimple\SoapCommon\SoapRequestFilter; From 840fa56931c952689b03bfb094d8c7ef38d84288 Mon Sep 17 00:00:00 2001 From: Andreas Schamberger Date: Sun, 8 Jan 2012 09:30:50 +0100 Subject: [PATCH 49/63] mime filter is not generic --- src/BeSimple/SoapClient/MimeFilter.php | 138 +++++++++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 src/BeSimple/SoapClient/MimeFilter.php diff --git a/src/BeSimple/SoapClient/MimeFilter.php b/src/BeSimple/SoapClient/MimeFilter.php new file mode 100644 index 0000000..beee156 --- /dev/null +++ b/src/BeSimple/SoapClient/MimeFilter.php @@ -0,0 +1,138 @@ + + * (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\Helper; +use BeSimple\SoapCommon\Mime\MultiPart as MimeMultiPart; +use BeSimple\SoapCommon\Mime\Parser as MimeParser; +use BeSimple\SoapCommon\Mime\Part as MimePart; +use BeSimple\SoapCommon\SoapRequest; +use BeSimple\SoapCommon\SoapRequestFilter; +use BeSimple\SoapCommon\SoapResponse; +use BeSimple\SoapCommon\SoapResponseFilter; + +/** + * MIME filter. + * + * @author Andreas Schamberger + */ +class MimeFilter implements SoapRequestFilter, SoapResponseFilter +{ + /** + * Attachment type. + * + * @var int Helper::ATTACHMENTS_TYPE_SWA | Helper::ATTACHMENTS_TYPE_MTOM + */ + protected $attachmentType = Helper::ATTACHMENTS_TYPE_SWA; + + /** + * Constructor. + * + * @param int $attachmentType Helper::ATTACHMENTS_TYPE_SWA | Helper::ATTACHMENTS_TYPE_MTOM + */ + public function __construct($attachmentType) + { + $this->attachmentType = $attachmentType; + } + + /** + * Reset all properties to default values. + */ + public function resetFilter() + { + $this->attachmentType = Helper::ATTACHMENTS_TYPE_SWA; + } + + /** + * Modify the given request XML. + * + * @param \BeSimple\SoapCommon\SoapRequest $request SOAP request + * + * @return void + */ + public function filterRequest(SoapRequest $request) + { + // get attachments from request object + $attachmentsToSend = $request->getAttachments(); + + // build mime message if we have attachments + if (count($attachmentsToSend) > 0) { + $multipart = new MimeMultiPart(); + $soapPart = new MimePart($request->getContent(), 'text/xml', 'utf-8', MimePart::ENCODING_EIGHT_BIT); + $soapVersion = $request->getVersion(); + // change content type headers for MTOM with SOAP 1.1 + if ($soapVersion == SOAP_1_1 && $this->attachmentType & Helper::ATTACHMENTS_TYPE_MTOM) { + $multipart->setHeader('Content-Type', 'type', 'application/xop+xml'); + $multipart->setHeader('Content-Type', 'start-info', 'text/xml'); + $soapPart->setHeader('Content-Type', 'application/xop+xml'); + $soapPart->setHeader('Content-Type', 'type', 'text/xml'); + } + // change content type headers for SOAP 1.2 + elseif ($soapVersion == SOAP_1_2) { + $multipart->setHeader('Content-Type', 'type', 'application/soap+xml'); + $soapPart->setHeader('Content-Type', 'application/soap+xml'); + } + $multipart->addPart($soapPart, true); + foreach ($attachmentsToSend as $cid => $attachment) { + $multipart->addPart($attachment, false); + } + $request->setContent($multipart->getMimeMessage()); + + // TODO + $headers = $multipart->getHeadersForHttp(); + list($name, $contentType) = explode(': ', $headers[0]); + + $request->setContentType($contentType); + } + } + + /** + * Modify the given response XML. + * + * @param \BeSimple\SoapCommon\SoapResponse $response SOAP response + * + * @return void + */ + public function filterResponse(SoapResponse $response) + { + // array to store attachments + $attachmentsRecieved = array(); + + // check content type if it is a multipart mime message + $responseContentType = $response->getContentType(); + if (false !== stripos($responseContentType, 'multipart/related')) { + // parse mime message + $headers = array( + 'Content-Type' => trim($responseContentType), + ); + $multipart = MimeParser::parseMimeMessage($response->getContent(), $headers); + // get soap payload and update SoapResponse object + $soapPart = $multipart->getPart(); + // convert href -> myhref for external references as PHP throws exception in this case + // http://svn.php.net/viewvc/php/php-src/branches/PHP_5_4/ext/soap/php_encoding.c?view=markup#l3436 + $content = preg_replace('/href=(?!#)/', 'myhref=', $soapPart->getContent()); + $response->setContent($content); + $response->setContentType($soapPart->getHeader('Content-Type')); + // store attachments + $attachments = $multipart->getParts(false); + foreach ($attachments as $cid => $attachment) { + $attachmentsRecieved[$cid] = $attachment; + } + } + + // add attachments to response object + if (count($attachmentsRecieved) > 0) { + $response->setAttachments($attachmentsRecieved); + } + } +} \ No newline at end of file From efc6500ead195f8f9f67537f066ff5fb815b053f Mon Sep 17 00:00:00 2001 From: Andreas Schamberger Date: Sun, 8 Jan 2012 09:50:23 +0100 Subject: [PATCH 50/63] replace internal type converter interface with cleaner solution --- src/BeSimple/SoapClient/SoapClient.php | 43 +++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/src/BeSimple/SoapClient/SoapClient.php b/src/BeSimple/SoapClient/SoapClient.php index c299901..90d8f1d 100644 --- a/src/BeSimple/SoapClient/SoapClient.php +++ b/src/BeSimple/SoapClient/SoapClient.php @@ -14,6 +14,8 @@ namespace BeSimple\SoapClient; use BeSimple\SoapCommon\Helper; use BeSimple\SoapCommon\SoapKernel; +use BeSimple\SoapCommon\Converter\MtomTypeConverter; +use BeSimple\SoapCommon\Converter\SwaTypeConverter; /** * Extended SoapClient that uses a a cURL wrapper for all underlying HTTP @@ -102,7 +104,7 @@ class SoapClient extends \SoapClient // TODO $wsdlHandler = new WsdlHandler($wsdlFile, $this->soapVersion); $this->soapKernel = new SoapKernel(); // set up type converter and mime filter - $this->soapKernel->configureMime($options); + $this->configureMime($options); // we want the exceptions option to be set $options['exceptions'] = true; // disable obsolete trace option for native SoapClient as we need to do our own tracing anyways @@ -257,6 +259,45 @@ class SoapClient extends \SoapClient return $this->soapKernel; } + /** + * Configure filter and type converter for SwA/MTOM. + * + * @param array &$options SOAP constructor options array. + * + * @return void + */ + private function configureMime(array &$options) + { + if (isset($options['attachment_type']) && Helper::ATTACHMENTS_TYPE_BASE64 !== $options['attachment_type']) { + // register mime filter in SoapKernel + $mimeFilter = new MimeFilter($options['attachment_type']); + $this->soapKernel->registerFilter($mimeFilter); + // configure type converter + if (Helper::ATTACHMENTS_TYPE_SWA === $options['attachment_type']) { + $converter = new SwaTypeConverter(); + $converter->setKernel($this->soapKernel); + } elseif (Helper::ATTACHMENTS_TYPE_MTOM === $options['attachment_type']) { + $converter = new MtomTypeConverter(); + $converter->setKernel($this->soapKernel); + } + // configure typemap + if (!isset($options['typemap'])) { + $options['typemap'] = array(); + } + $soapKernel = $this->soapKernel; + $options['typemap'][] = array( + 'type_name' => $converter->getTypeName(), + 'type_ns' => $converter->getTypeNamespace(), + 'from_xml' => function($input) use ($converter) { + return $converter->convertXmlToPhp($input); + }, + 'to_xml' => function($input) use ($converter) { + return $converter->convertPhpToXml($input); + }, + ); + } + } + /** * Downloads WSDL files with cURL. Uses all SoapClient options for * authentication. Uses the WSDL_CACHE_* constants and the 'soap.wsdl_*' From 375cd7debbdd4157572402d860d7c3c9d8d16461 Mon Sep 17 00:00:00 2001 From: Andreas Schamberger Date: Sun, 15 Jan 2012 11:42:21 +0100 Subject: [PATCH 51/63] CS --- .../Tests/SoapClient/WsdlDownloaderTest.php | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/tests/BeSimple/Tests/SoapClient/WsdlDownloaderTest.php b/tests/BeSimple/Tests/SoapClient/WsdlDownloaderTest.php index 14ac58a..198341d 100644 --- a/tests/BeSimple/Tests/SoapClient/WsdlDownloaderTest.php +++ b/tests/BeSimple/Tests/SoapClient/WsdlDownloaderTest.php @@ -24,11 +24,12 @@ class WsdlDownloaderTest extends \PHPUnit_Framework_TestCase protected function startPhpWebserver() { - if ('Windows' == substr(php_uname('s'), 0, 7 )) { - $powershellCommand = "\$app = start-process php.exe -ArgumentList '-S localhost:8000 -t ".__DIR__.DIRECTORY_SEPARATOR."Fixtures' -WindowStyle 'Hidden' -passthru; Echo \$app.Id;"; + $dir = __DIR__.DIRECTORY_SEPARATOR.'Fixtures'; + if ('Windows' == substr(php_uname('s'), 0, 7)) { + $powershellCommand = "\$app = start-process php.exe -ArgumentList '-S localhost:8000 -t ".$dir."' -WindowStyle 'Hidden' -passthru; Echo \$app.Id;"; $shellCommand = 'powershell -command "& {'.$powershellCommand.'}"'; } else { - $shellCommand = "nohup php -S localhost:8000 -t ".__DIR__.DIRECTORY_SEPARATOR."Fixtures &"; + $shellCommand = "nohup php -S localhost:8000 -t ".$dir." &"; } $output = array(); exec($shellCommand, $output); @@ -38,7 +39,7 @@ class WsdlDownloaderTest extends \PHPUnit_Framework_TestCase protected function stopPhpWebserver() { if (!is_null($this->webserverProcessId)) { - if ('Windows' == substr(php_uname('s'), 0, 7 )) { + if ('Windows' == substr(php_uname('s'), 0, 7)) { exec('TASKKILL /F /PID ' . $this->webserverProcessId); } else { exec('kill ' . $this->webserverProcessId); @@ -57,7 +58,7 @@ class WsdlDownloaderTest extends \PHPUnit_Framework_TestCase $cacheDir = ini_get('soap.wsdl_cache_dir'); if (!is_dir($cacheDir)) { $cacheDir = sys_get_temp_dir(); - $cacheDirForRegExp = preg_quote( $cacheDir ); + $cacheDirForRegExp = preg_quote($cacheDir); } $tests = array( @@ -82,7 +83,7 @@ class WsdlDownloaderTest extends \PHPUnit_Framework_TestCase foreach ($tests as $name => $values) { $cacheFileName = $wd->download($values['source']); $result = file_get_contents($cacheFileName); - $this->assertRegExp($values['assertRegExp'],$result,$name); + $this->assertRegExp($values['assertRegExp'], $result, $name); unlink($cacheFileName); } @@ -129,7 +130,7 @@ class WsdlDownloaderTest extends \PHPUnit_Framework_TestCase $cacheDir = ini_get('soap.wsdl_cache_dir'); if (!is_dir($cacheDir)) { $cacheDir = sys_get_temp_dir(); - $cacheDirForRegExp = preg_quote( $cacheDir ); + $cacheDirForRegExp = preg_quote($cacheDir); } $remoteUrlAbsolute = 'http://localhost:8000/wsdlinclude/wsdlinctest_absolute.xml'; @@ -162,10 +163,10 @@ class WsdlDownloaderTest extends \PHPUnit_Framework_TestCase ); foreach ($tests as $name => $values) { - $wsdl = file_get_contents( $values['source'] ); - $method->invoke($wd, $wsdl, $values['cacheFile'],$values['remoteParentUrl']); + $wsdl = file_get_contents($values['source']); + $method->invoke($wd, $wsdl, $values['cacheFile'], $values['remoteParentUrl']); $result = file_get_contents($values['cacheFile']); - $this->assertRegExp($values['assertRegExp'],$result,$name); + $this->assertRegExp($values['assertRegExp'], $result, $name); unlink($values['cacheFile']); } @@ -186,7 +187,7 @@ class WsdlDownloaderTest extends \PHPUnit_Framework_TestCase $cacheDir = ini_get('soap.wsdl_cache_dir'); if (!is_dir($cacheDir)) { $cacheDir = sys_get_temp_dir(); - $cacheDirForRegExp = preg_quote( $cacheDir ); + $cacheDirForRegExp = preg_quote($cacheDir); } $remoteUrlAbsolute = 'http://localhost:8000/xsdinclude/xsdinctest_absolute.xml'; @@ -219,10 +220,10 @@ class WsdlDownloaderTest extends \PHPUnit_Framework_TestCase ); foreach ($tests as $name => $values) { - $wsdl = file_get_contents( $values['source'] ); - $method->invoke($wd, $wsdl, $values['cacheFile'],$values['remoteParentUrl']); + $wsdl = file_get_contents($values['source']); + $method->invoke($wd, $wsdl, $values['cacheFile'], $values['remoteParentUrl']); $result = file_get_contents($values['cacheFile']); - $this->assertRegExp($values['assertRegExp'],$result,$name); + $this->assertRegExp($values['assertRegExp'], $result, $name); unlink($values['cacheFile']); } From f0b596be99820082ed23c9402c28eb4de2b7fb75 Mon Sep 17 00:00:00 2001 From: Andreas Schamberger Date: Sun, 15 Jan 2012 11:44:05 +0100 Subject: [PATCH 52/63] CS --- tests/BeSimple/Tests/SoapClient/CurlTest.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/BeSimple/Tests/SoapClient/CurlTest.php b/tests/BeSimple/Tests/SoapClient/CurlTest.php index b940b2a..d669f4c 100644 --- a/tests/BeSimple/Tests/SoapClient/CurlTest.php +++ b/tests/BeSimple/Tests/SoapClient/CurlTest.php @@ -23,11 +23,12 @@ class CurlTest extends \PHPUnit_Framework_TestCase protected function startPhpWebserver() { - if ('Windows' == substr(php_uname('s'), 0, 7 )) { - $powershellCommand = "\$app = start-process php.exe -ArgumentList '-S localhost:8000 -t ".__DIR__.DIRECTORY_SEPARATOR."Fixtures' -WindowStyle 'Hidden' -passthru; Echo \$app.Id;"; + $dir = __DIR__.DIRECTORY_SEPARATOR.'Fixtures'; + if ('Windows' == substr(php_uname('s'), 0, 7)) { + $powershellCommand = "\$app = start-process php.exe -ArgumentList '-S localhost:8000 -t ".$dir."' -WindowStyle 'Hidden' -passthru; Echo \$app.Id;"; $shellCommand = 'powershell -command "& {'.$powershellCommand.'}"'; } else { - $shellCommand = "nohup php -S localhost:8000 -t ".__DIR__.DIRECTORY_SEPARATOR."Fixtures &"; + $shellCommand = "nohup php -S localhost:8000 -t ".$dir." &"; } $output = array(); exec($shellCommand, $output); From 107a2904c1adc95298b061d19ac02da475baf6cf Mon Sep 17 00:00:00 2001 From: Andreas Schamberger Date: Sun, 29 Jan 2012 14:38:12 +0100 Subject: [PATCH 53/63] add XML mime filter --- src/BeSimple/SoapClient/SoapClient.php | 2 + src/BeSimple/SoapClient/XmlMimeFilter.php | 75 +++++++++++++++++++++++ tests/AxisInterop/MTOM.php | 27 +++++++- tests/AxisInterop/MTOM.wsdl | 4 +- tests/bootstrap.php | 2 +- 5 files changed, 105 insertions(+), 5 deletions(-) create mode 100644 src/BeSimple/SoapClient/XmlMimeFilter.php diff --git a/src/BeSimple/SoapClient/SoapClient.php b/src/BeSimple/SoapClient/SoapClient.php index 90d8f1d..afa6515 100644 --- a/src/BeSimple/SoapClient/SoapClient.php +++ b/src/BeSimple/SoapClient/SoapClient.php @@ -277,6 +277,8 @@ class SoapClient extends \SoapClient $converter = new SwaTypeConverter(); $converter->setKernel($this->soapKernel); } elseif (Helper::ATTACHMENTS_TYPE_MTOM === $options['attachment_type']) { + $xmlMimeFilter = new XmlMimeFilter($options['attachment_type']); + $this->soapKernel->registerFilter($xmlMimeFilter); $converter = new MtomTypeConverter(); $converter->setKernel($this->soapKernel); } diff --git a/src/BeSimple/SoapClient/XmlMimeFilter.php b/src/BeSimple/SoapClient/XmlMimeFilter.php new file mode 100644 index 0000000..142a271 --- /dev/null +++ b/src/BeSimple/SoapClient/XmlMimeFilter.php @@ -0,0 +1,75 @@ + + * (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\FilterHelper; +use BeSimple\SoapCommon\Helper; +use BeSimple\SoapCommon\Mime\MultiPart as MimeMultiPart; +use BeSimple\SoapCommon\Mime\Parser as MimeParser; +use BeSimple\SoapCommon\Mime\Part as MimePart; +use BeSimple\SoapCommon\SoapRequest; +use BeSimple\SoapCommon\SoapRequestFilter; +use BeSimple\SoapCommon\SoapResponse; +use BeSimple\SoapCommon\SoapResponseFilter; + +/** + * XML MIME filter that fixes the namespace of xmime:contentType attribute. + * + * @author Andreas Schamberger + */ +class XmlMimeFilter implements SoapRequestFilter +{ + /** + * Reset all properties to default values. + */ + public function resetFilter() + { + } + + /** + * Modify the given request XML. + * + * @param \BeSimple\SoapCommon\SoapRequest $request SOAP request + * + * @return void + */ + public function filterRequest(SoapRequest $request) + { + // get \DOMDocument from SOAP request + $dom = $request->getContentDocument(); + + // create FilterHelper + $filterHelper = new FilterHelper($dom); + + // add the neccessary namespaces + $filterHelper->addNamespace(Helper::PFX_XMLMIME, Helper::NS_XMLMIME); + + // get xsd:base64binary elements + $xpath = new \DOMXPath($dom); + $xpath->registerNamespace('XOP', Helper::NS_XOP); + $query = '//XOP:Include/..'; + $nodes = $xpath->query($query); + + // exchange attributes + if ($nodes->length > 0) { + foreach ($nodes as $node) { + if ($node->hasAttribute('contentType')) { + $contentType = $node->getAttribute('contentType'); + $node->removeAttribute('contentType'); + $filterHelper->setAttribute($node, Helper::NS_XMLMIME, 'contentType', $contentType); + } + } + } + + } +} \ No newline at end of file diff --git a/tests/AxisInterop/MTOM.php b/tests/AxisInterop/MTOM.php index cbfe886..2579c1d 100644 --- a/tests/AxisInterop/MTOM.php +++ b/tests/AxisInterop/MTOM.php @@ -7,12 +7,32 @@ require '../bootstrap.php'; echo '
';
 
+class base64Binary
+{
+    public $_;
+    public $contentType;
+}
+
+class AttachmentType
+{
+    public $fileName;
+    public $binaryData;
+}
+
+class AttachmentRequest extends AttachmentType
+{
+}
+
 $options = array(
     'soap_version'    => SOAP_1_1,
     'features'        => SOAP_SINGLE_ELEMENT_ARRAYS, // make sure that result is array for size=1
     'trace'           => true, // enables use of the methods  SoapClient->__getLastRequest,  SoapClient->__getLastRequestHeaders,  SoapClient->__getLastResponse and  SoapClient->__getLastResponseHeaders
     'attachment_type' => BeSimpleSoapHelper::ATTACHMENTS_TYPE_MTOM,
     'cache_wsdl'      => WSDL_CACHE_NONE,
+    'classmap'        => array(
+        'base64Binary'      => 'base64Binary',
+        'AttachmentRequest' => 'AttachmentRequest',
+    ),
 );
 
 /*
@@ -28,10 +48,13 @@ $sc = new BeSimpleSoapClient('MTOM.wsdl', $options);
 //var_dump($sc->__getTypes());
 
 try {
+    $b64 = new base64Binary();
+    $b64->_ = 'This is a test. :)';
+    $b64->contentType = 'text/plain';
 
-    $attachment = new stdClass();
+    $attachment = new AttachmentRequest();
     $attachment->fileName = 'test123.txt';
-    $attachment->binaryData = 'This is a test.';
+    $attachment->binaryData = $b64;
 
     var_dump($sc->attachment($attachment));
 
diff --git a/tests/AxisInterop/MTOM.wsdl b/tests/AxisInterop/MTOM.wsdl
index 178ee35..f0c9a6d 100644
--- a/tests/AxisInterop/MTOM.wsdl
+++ b/tests/AxisInterop/MTOM.wsdl
@@ -80,10 +80,10 @@
   
   
     
-      
+      
     
     
-      
+      
     
   
 
diff --git a/tests/bootstrap.php b/tests/bootstrap.php
index d2872c6..2fa215e 100644
--- a/tests/bootstrap.php
+++ b/tests/bootstrap.php
@@ -26,7 +26,7 @@ spl_autoload_register(function($class) {
             return true;
         }
     } elseif (0 === strpos($class, 'BeSimple\SoapCommon\\')) {
-        $path = __DIR__.'/../vendor/besimple-soapcommon/src/'.($class = strtr($class, '\\', '/')).'.php';
+        $path = __DIR__.'/../../BeSimpleSoapCommon/src/'.($class = strtr($class, '\\', '/')).'.php';
         if (file_exists($path) && is_readable($path)) {
             require_once $path;
 

From 3954d071121b6d1ea7f4717f188a991c6c33cb22 Mon Sep 17 00:00:00 2001
From: Andreas Schamberger 
Date: Sun, 29 Jan 2012 17:46:39 +0100
Subject: [PATCH 54/63] fixed broken bootstrap

---
 tests/bootstrap.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/tests/bootstrap.php b/tests/bootstrap.php
index 2fa215e..d2872c6 100644
--- a/tests/bootstrap.php
+++ b/tests/bootstrap.php
@@ -26,7 +26,7 @@ spl_autoload_register(function($class) {
             return true;
         }
     } elseif (0 === strpos($class, 'BeSimple\SoapCommon\\')) {
-        $path = __DIR__.'/../../BeSimpleSoapCommon/src/'.($class = strtr($class, '\\', '/')).'.php';
+        $path = __DIR__.'/../vendor/besimple-soapcommon/src/'.($class = strtr($class, '\\', '/')).'.php';
         if (file_exists($path) && is_readable($path)) {
             require_once $path;
 

From 7f96a20f6671d282a40d9b0683775d8804b2df14 Mon Sep 17 00:00:00 2001
From: Andreas Schamberger 
Date: Sun, 15 Jan 2012 11:42:21 +0100
Subject: [PATCH 55/63] Several WS and CS fixes

---
 src/BeSimple/SoapClient/Curl.php              |  4 +--
 src/BeSimple/SoapClient/SoapClient.php        |  2 +-
 src/BeSimple/SoapClient/SoapClientBuilder.php |  1 +
 tests/AxisInterop/MTOM.php                    | 16 +++++++++-
 tests/AxisInterop/SwA.php                     |  2 --
 tests/AxisInterop/WsAddressing.php            |  2 --
 tests/AxisInterop/WsSecurityUserPass.php      |  2 --
 tests/BeSimple/Tests/SoapClient/CurlTest.php  |  7 +++--
 .../Tests/SoapClient/WsdlDownloaderTest.php   | 29 ++++++++++---------
 9 files changed, 38 insertions(+), 27 deletions(-)

diff --git a/src/BeSimple/SoapClient/Curl.php b/src/BeSimple/SoapClient/Curl.php
index ae57933..54e40cf 100644
--- a/src/BeSimple/SoapClient/Curl.php
+++ b/src/BeSimple/SoapClient/Curl.php
@@ -72,7 +72,7 @@ class Curl
             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');
@@ -215,7 +215,7 @@ class Curl
             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
-       );
+        );
     }
 
     /**
diff --git a/src/BeSimple/SoapClient/SoapClient.php b/src/BeSimple/SoapClient/SoapClient.php
index 90d8f1d..c5b9117 100644
--- a/src/BeSimple/SoapClient/SoapClient.php
+++ b/src/BeSimple/SoapClient/SoapClient.php
@@ -77,7 +77,7 @@ class SoapClient extends \SoapClient
     private $lastResponse = '';
 
     /**
-     * Last response.
+     * Soap kernel.
      *
      * @var \BeSimple\SoapCommon\SoapKernel
      */
diff --git a/src/BeSimple/SoapClient/SoapClientBuilder.php b/src/BeSimple/SoapClient/SoapClientBuilder.php
index 3759f13..b2ceb61 100644
--- a/src/BeSimple/SoapClient/SoapClientBuilder.php
+++ b/src/BeSimple/SoapClient/SoapClientBuilder.php
@@ -13,6 +13,7 @@
 namespace BeSimple\SoapClient;
 
 use BeSimple\SoapCommon\AbstractSoapBuilder;
+use BeSimple\SoapCommon\Helper;
 
 /**
  * Fluent interface builder for SoapClient instance.
diff --git a/tests/AxisInterop/MTOM.php b/tests/AxisInterop/MTOM.php
index cbfe886..8f091bc 100644
--- a/tests/AxisInterop/MTOM.php
+++ b/tests/AxisInterop/MTOM.php
@@ -5,7 +5,21 @@ use BeSimple\SoapClient\SoapClient as BeSimpleSoapClient;
 
 require '../bootstrap.php';
 
-echo '
';
+class base64Binary
+{
+    public $_;
+    public $contentType;
+}
+
+class AttachmentType
+{
+    public $fileName;
+    public $binaryData;
+}
+
+class AttachmentRequest extends AttachmentType
+{
+}
 
 $options = array(
     'soap_version'    => SOAP_1_1,
diff --git a/tests/AxisInterop/SwA.php b/tests/AxisInterop/SwA.php
index 0cf5b77..eaf1bc6 100644
--- a/tests/AxisInterop/SwA.php
+++ b/tests/AxisInterop/SwA.php
@@ -5,8 +5,6 @@ use BeSimple\SoapClient\SoapClient as BeSimpleSoapClient;
 
 require '../bootstrap.php';
 
-echo '
';
-
 $options = array(
     'soap_version'    => SOAP_1_1,
     'features'        => SOAP_SINGLE_ELEMENT_ARRAYS, // make sure that result is array for size=1
diff --git a/tests/AxisInterop/WsAddressing.php b/tests/AxisInterop/WsAddressing.php
index 9ec1bcc..dacabcb 100644
--- a/tests/AxisInterop/WsAddressing.php
+++ b/tests/AxisInterop/WsAddressing.php
@@ -5,8 +5,6 @@ use BeSimple\SoapClient\WsAddressingFilter as BeSimpleWsAddressingFilter;
 
 require '../bootstrap.php';
 
-echo '
';
-
 $options = array(
     'soap_version' => SOAP_1_2,
     'features'     => SOAP_SINGLE_ELEMENT_ARRAYS, // make sure that result is array for size=1
diff --git a/tests/AxisInterop/WsSecurityUserPass.php b/tests/AxisInterop/WsSecurityUserPass.php
index 595d9ba..231f1e5 100644
--- a/tests/AxisInterop/WsSecurityUserPass.php
+++ b/tests/AxisInterop/WsSecurityUserPass.php
@@ -5,8 +5,6 @@ use BeSimple\SoapClient\WsSecurityFilter as BeSimpleWsSecurityFilter;
 
 require '../bootstrap.php';
 
-echo '
';
-
 $options = array(
     'soap_version' => SOAP_1_2,
     'features'     => SOAP_SINGLE_ELEMENT_ARRAYS, // make sure that result is array for size=1
diff --git a/tests/BeSimple/Tests/SoapClient/CurlTest.php b/tests/BeSimple/Tests/SoapClient/CurlTest.php
index b940b2a..d669f4c 100644
--- a/tests/BeSimple/Tests/SoapClient/CurlTest.php
+++ b/tests/BeSimple/Tests/SoapClient/CurlTest.php
@@ -23,11 +23,12 @@ class CurlTest extends \PHPUnit_Framework_TestCase
 
     protected function startPhpWebserver()
     {
-        if ('Windows' == substr(php_uname('s'), 0, 7 )) {
-            $powershellCommand = "\$app = start-process php.exe -ArgumentList '-S localhost:8000 -t ".__DIR__.DIRECTORY_SEPARATOR."Fixtures' -WindowStyle 'Hidden' -passthru; Echo \$app.Id;";
+            $dir = __DIR__.DIRECTORY_SEPARATOR.'Fixtures';
+        if ('Windows' == substr(php_uname('s'), 0, 7)) {
+            $powershellCommand = "\$app = start-process php.exe -ArgumentList '-S localhost:8000 -t ".$dir."' -WindowStyle 'Hidden' -passthru; Echo \$app.Id;";
             $shellCommand = 'powershell -command "& {'.$powershellCommand.'}"';
         } else {
-            $shellCommand = "nohup php -S localhost:8000 -t ".__DIR__.DIRECTORY_SEPARATOR."Fixtures &";
+            $shellCommand = "nohup php -S localhost:8000 -t ".$dir." &";
         }
         $output = array();
         exec($shellCommand, $output);
diff --git a/tests/BeSimple/Tests/SoapClient/WsdlDownloaderTest.php b/tests/BeSimple/Tests/SoapClient/WsdlDownloaderTest.php
index 14ac58a..198341d 100644
--- a/tests/BeSimple/Tests/SoapClient/WsdlDownloaderTest.php
+++ b/tests/BeSimple/Tests/SoapClient/WsdlDownloaderTest.php
@@ -24,11 +24,12 @@ class WsdlDownloaderTest extends \PHPUnit_Framework_TestCase
 
     protected function startPhpWebserver()
     {
-        if ('Windows' == substr(php_uname('s'), 0, 7 )) {
-            $powershellCommand = "\$app = start-process php.exe -ArgumentList '-S localhost:8000 -t ".__DIR__.DIRECTORY_SEPARATOR."Fixtures' -WindowStyle 'Hidden' -passthru; Echo \$app.Id;";
+        $dir = __DIR__.DIRECTORY_SEPARATOR.'Fixtures';
+        if ('Windows' == substr(php_uname('s'), 0, 7)) {
+            $powershellCommand = "\$app = start-process php.exe -ArgumentList '-S localhost:8000 -t ".$dir."' -WindowStyle 'Hidden' -passthru; Echo \$app.Id;";
             $shellCommand = 'powershell -command "& {'.$powershellCommand.'}"';
         } else {
-            $shellCommand = "nohup php -S localhost:8000 -t ".__DIR__.DIRECTORY_SEPARATOR."Fixtures &";
+            $shellCommand = "nohup php -S localhost:8000 -t ".$dir." &";
         }
         $output = array();
         exec($shellCommand, $output);
@@ -38,7 +39,7 @@ class WsdlDownloaderTest extends \PHPUnit_Framework_TestCase
     protected function stopPhpWebserver()
     {
         if (!is_null($this->webserverProcessId)) {
-            if ('Windows' == substr(php_uname('s'), 0, 7 )) {
+            if ('Windows' == substr(php_uname('s'), 0, 7)) {
                 exec('TASKKILL /F /PID ' . $this->webserverProcessId);
             } else {
                 exec('kill ' . $this->webserverProcessId);
@@ -57,7 +58,7 @@ class WsdlDownloaderTest extends \PHPUnit_Framework_TestCase
         $cacheDir = ini_get('soap.wsdl_cache_dir');
         if (!is_dir($cacheDir)) {
             $cacheDir = sys_get_temp_dir();
-            $cacheDirForRegExp = preg_quote( $cacheDir );
+            $cacheDirForRegExp = preg_quote($cacheDir);
         }
 
         $tests = array(
@@ -82,7 +83,7 @@ class WsdlDownloaderTest extends \PHPUnit_Framework_TestCase
         foreach ($tests as $name => $values) {
             $cacheFileName = $wd->download($values['source']);
             $result = file_get_contents($cacheFileName);
-            $this->assertRegExp($values['assertRegExp'],$result,$name);
+            $this->assertRegExp($values['assertRegExp'], $result, $name);
             unlink($cacheFileName);
         }
 
@@ -129,7 +130,7 @@ class WsdlDownloaderTest extends \PHPUnit_Framework_TestCase
         $cacheDir = ini_get('soap.wsdl_cache_dir');
         if (!is_dir($cacheDir)) {
             $cacheDir = sys_get_temp_dir();
-            $cacheDirForRegExp = preg_quote( $cacheDir );
+            $cacheDirForRegExp = preg_quote($cacheDir);
         }
 
         $remoteUrlAbsolute = 'http://localhost:8000/wsdlinclude/wsdlinctest_absolute.xml';
@@ -162,10 +163,10 @@ class WsdlDownloaderTest extends \PHPUnit_Framework_TestCase
         );
 
         foreach ($tests as $name => $values) {
-            $wsdl = file_get_contents( $values['source'] );
-            $method->invoke($wd, $wsdl, $values['cacheFile'],$values['remoteParentUrl']);
+            $wsdl = file_get_contents($values['source']);
+            $method->invoke($wd, $wsdl, $values['cacheFile'], $values['remoteParentUrl']);
             $result = file_get_contents($values['cacheFile']);
-            $this->assertRegExp($values['assertRegExp'],$result,$name);
+            $this->assertRegExp($values['assertRegExp'], $result, $name);
             unlink($values['cacheFile']);
         }
 
@@ -186,7 +187,7 @@ class WsdlDownloaderTest extends \PHPUnit_Framework_TestCase
         $cacheDir = ini_get('soap.wsdl_cache_dir');
         if (!is_dir($cacheDir)) {
             $cacheDir = sys_get_temp_dir();
-            $cacheDirForRegExp = preg_quote( $cacheDir );
+            $cacheDirForRegExp = preg_quote($cacheDir);
         }
 
         $remoteUrlAbsolute = 'http://localhost:8000/xsdinclude/xsdinctest_absolute.xml';
@@ -219,10 +220,10 @@ class WsdlDownloaderTest extends \PHPUnit_Framework_TestCase
         );
 
         foreach ($tests as $name => $values) {
-            $wsdl = file_get_contents( $values['source'] );
-            $method->invoke($wd, $wsdl, $values['cacheFile'],$values['remoteParentUrl']);
+            $wsdl = file_get_contents($values['source']);
+            $method->invoke($wd, $wsdl, $values['cacheFile'], $values['remoteParentUrl']);
             $result = file_get_contents($values['cacheFile']);
-            $this->assertRegExp($values['assertRegExp'],$result,$name);
+            $this->assertRegExp($values['assertRegExp'], $result, $name);
             unlink($values['cacheFile']);
         }
 

From 7819c316444389df5d6f0ea3f7f3d4ae970139c2 Mon Sep 17 00:00:00 2001
From: Andreas Schamberger 
Date: Sun, 29 Jan 2012 14:38:12 +0100
Subject: [PATCH 56/63] add XML mime filter

---
 src/BeSimple/SoapClient/SoapClient.php    |  2 +
 src/BeSimple/SoapClient/XmlMimeFilter.php | 75 +++++++++++++++++++++++
 tests/AxisInterop/MTOM.php                | 27 +++++++-
 tests/AxisInterop/MTOM.wsdl               |  4 +-
 4 files changed, 104 insertions(+), 4 deletions(-)
 create mode 100644 src/BeSimple/SoapClient/XmlMimeFilter.php

diff --git a/src/BeSimple/SoapClient/SoapClient.php b/src/BeSimple/SoapClient/SoapClient.php
index c5b9117..d264090 100644
--- a/src/BeSimple/SoapClient/SoapClient.php
+++ b/src/BeSimple/SoapClient/SoapClient.php
@@ -277,6 +277,8 @@ class SoapClient extends \SoapClient
                 $converter = new SwaTypeConverter();
                 $converter->setKernel($this->soapKernel);
             } elseif (Helper::ATTACHMENTS_TYPE_MTOM === $options['attachment_type']) {
+                $xmlMimeFilter = new XmlMimeFilter($options['attachment_type']);
+                $this->soapKernel->registerFilter($xmlMimeFilter);
                 $converter = new MtomTypeConverter();
                 $converter->setKernel($this->soapKernel);
             }
diff --git a/src/BeSimple/SoapClient/XmlMimeFilter.php b/src/BeSimple/SoapClient/XmlMimeFilter.php
new file mode 100644
index 0000000..142a271
--- /dev/null
+++ b/src/BeSimple/SoapClient/XmlMimeFilter.php
@@ -0,0 +1,75 @@
+
+ * (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\FilterHelper;
+use BeSimple\SoapCommon\Helper;
+use BeSimple\SoapCommon\Mime\MultiPart as MimeMultiPart;
+use BeSimple\SoapCommon\Mime\Parser as MimeParser;
+use BeSimple\SoapCommon\Mime\Part as MimePart;
+use BeSimple\SoapCommon\SoapRequest;
+use BeSimple\SoapCommon\SoapRequestFilter;
+use BeSimple\SoapCommon\SoapResponse;
+use BeSimple\SoapCommon\SoapResponseFilter;
+
+/**
+ * XML MIME filter that fixes the namespace of xmime:contentType attribute.
+ *
+ * @author Andreas Schamberger 
+ */
+class XmlMimeFilter implements SoapRequestFilter
+{
+    /**
+     * Reset all properties to default values.
+     */
+    public function resetFilter()
+    {
+    }
+
+    /**
+     * Modify the given request XML.
+     *
+     * @param \BeSimple\SoapCommon\SoapRequest $request SOAP request
+     *
+     * @return void
+     */
+    public function filterRequest(SoapRequest $request)
+    {
+        // get \DOMDocument from SOAP request
+        $dom = $request->getContentDocument();
+
+        // create FilterHelper
+        $filterHelper = new FilterHelper($dom);
+
+        // add the neccessary namespaces
+        $filterHelper->addNamespace(Helper::PFX_XMLMIME, Helper::NS_XMLMIME);
+
+        // get xsd:base64binary elements
+        $xpath = new \DOMXPath($dom);
+        $xpath->registerNamespace('XOP', Helper::NS_XOP);
+        $query = '//XOP:Include/..';
+        $nodes = $xpath->query($query);
+
+        // exchange attributes
+        if ($nodes->length > 0) {
+            foreach ($nodes as $node) {
+                if ($node->hasAttribute('contentType')) {
+                    $contentType = $node->getAttribute('contentType');
+                    $node->removeAttribute('contentType');
+                    $filterHelper->setAttribute($node, Helper::NS_XMLMIME, 'contentType', $contentType);
+                }
+            }
+        }
+
+    }
+}
\ No newline at end of file
diff --git a/tests/AxisInterop/MTOM.php b/tests/AxisInterop/MTOM.php
index 8f091bc..08d8723 100644
--- a/tests/AxisInterop/MTOM.php
+++ b/tests/AxisInterop/MTOM.php
@@ -21,12 +21,32 @@ class AttachmentRequest extends AttachmentType
 {
 }
 
+class base64Binary
+{
+    public $_;
+    public $contentType;
+}
+
+class AttachmentType
+{
+    public $fileName;
+    public $binaryData;
+}
+
+class AttachmentRequest extends AttachmentType
+{
+}
+
 $options = array(
     'soap_version'    => SOAP_1_1,
     'features'        => SOAP_SINGLE_ELEMENT_ARRAYS, // make sure that result is array for size=1
     'trace'           => true, // enables use of the methods  SoapClient->__getLastRequest,  SoapClient->__getLastRequestHeaders,  SoapClient->__getLastResponse and  SoapClient->__getLastResponseHeaders
     'attachment_type' => BeSimpleSoapHelper::ATTACHMENTS_TYPE_MTOM,
     'cache_wsdl'      => WSDL_CACHE_NONE,
+    'classmap'        => array(
+        'base64Binary'      => 'base64Binary',
+        'AttachmentRequest' => 'AttachmentRequest',
+    ),
 );
 
 /*
@@ -42,10 +62,13 @@ $sc = new BeSimpleSoapClient('MTOM.wsdl', $options);
 //var_dump($sc->__getTypes());
 
 try {
+    $b64 = new base64Binary();
+    $b64->_ = 'This is a test. :)';
+    $b64->contentType = 'text/plain';
 
-    $attachment = new stdClass();
+    $attachment = new AttachmentRequest();
     $attachment->fileName = 'test123.txt';
-    $attachment->binaryData = 'This is a test.';
+    $attachment->binaryData = $b64;
 
     var_dump($sc->attachment($attachment));
 
diff --git a/tests/AxisInterop/MTOM.wsdl b/tests/AxisInterop/MTOM.wsdl
index 178ee35..f0c9a6d 100644
--- a/tests/AxisInterop/MTOM.wsdl
+++ b/tests/AxisInterop/MTOM.wsdl
@@ -80,10 +80,10 @@
   
   
     
-      
+      
     
     
-      
+      
     
   
 

From 8b10219f7302d7e84e67ac59cba2dc31940c991b Mon Sep 17 00:00:00 2001
From: Andreas Schamberger 
Date: Sat, 21 Apr 2012 20:24:19 +0200
Subject: [PATCH 57/63] =?UTF-8?q?add=20builder=20functions=20f=C3=BCr=20co?=
 =?UTF-8?q?nfiguring=20soap=20attachment=20handling?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/BeSimple/SoapClient/SoapClientBuilder.php | 36 +++++++++++++++++++
 1 file changed, 36 insertions(+)

diff --git a/src/BeSimple/SoapClient/SoapClientBuilder.php b/src/BeSimple/SoapClient/SoapClientBuilder.php
index b2ceb61..cce4a9a 100644
--- a/src/BeSimple/SoapClient/SoapClientBuilder.php
+++ b/src/BeSimple/SoapClient/SoapClientBuilder.php
@@ -193,6 +193,42 @@ class SoapClientBuilder extends AbstractSoapBuilder
         return $this;
     }
 
+    /**
+    * SOAP attachment type Base64.
+    *
+    * @return \BeSimple\SoapServer\SoapServerBuilder
+    */
+    public function withBase64Attachments()
+    {
+        $this->options['attachment_type'] = Helper::ATTACHMENTS_TYPE_BASE64;
+
+        return $this;
+    }
+
+    /**
+     * SOAP attachment type SwA.
+     *
+     * @return \BeSimple\SoapServer\SoapServerBuilder
+     */
+    public function withSwaAttachments()
+    {
+        $this->options['attachment_type'] = Helper::ATTACHMENTS_TYPE_SWA;
+
+        return $this;
+    }
+
+    /**
+     * SOAP attachment type MTOM.
+     *
+     * @return \BeSimple\SoapServer\SoapServerBuilder
+     */
+    public function withMtomAttachments()
+    {
+        $this->options['attachment_type'] = Helper::ATTACHMENTS_TYPE_MTOM;
+
+        return $this;
+    }
+
     /**
      * Validate options.
      */

From 51a971ed3379a5f000540569ae0d06f8ff445f4b Mon Sep 17 00:00:00 2001
From: Andreas Schamberger 
Date: Sun, 22 Apr 2012 18:08:40 +0200
Subject: [PATCH 58/63] added soap client specific soap kernel

---
 src/BeSimple/SoapClient/SoapClient.php |  6 ++--
 src/BeSimple/SoapClient/SoapKernel.php | 47 ++++++++++++++++++++++++++
 2 files changed, 49 insertions(+), 4 deletions(-)
 create mode 100644 src/BeSimple/SoapClient/SoapKernel.php

diff --git a/src/BeSimple/SoapClient/SoapClient.php b/src/BeSimple/SoapClient/SoapClient.php
index d264090..d0d99e8 100644
--- a/src/BeSimple/SoapClient/SoapClient.php
+++ b/src/BeSimple/SoapClient/SoapClient.php
@@ -13,7 +13,6 @@
 namespace BeSimple\SoapClient;
 
 use BeSimple\SoapCommon\Helper;
-use BeSimple\SoapCommon\SoapKernel;
 use BeSimple\SoapCommon\Converter\MtomTypeConverter;
 use BeSimple\SoapCommon\Converter\SwaTypeConverter;
 
@@ -79,7 +78,7 @@ class SoapClient extends \SoapClient
     /**
      * Soap kernel.
      *
-     * @var \BeSimple\SoapCommon\SoapKernel
+     * @var \BeSimple\SoapClient\SoapKernel
      */
     protected $soapKernel = null;
 
@@ -252,7 +251,7 @@ class SoapClient extends \SoapClient
     /**
      * Get SoapKernel instance.
      *
-     * @return \BeSimple\SoapCommon\SoapKernel
+     * @return \BeSimple\SoapClient\SoapKernel
      */
     public function getSoapKernel()
     {
@@ -286,7 +285,6 @@ class SoapClient extends \SoapClient
             if (!isset($options['typemap'])) {
                 $options['typemap'] = array();
             }
-            $soapKernel = $this->soapKernel;
             $options['typemap'][] = array(
                 'type_name' => $converter->getTypeName(),
                 'type_ns'   => $converter->getTypeNamespace(),
diff --git a/src/BeSimple/SoapClient/SoapKernel.php b/src/BeSimple/SoapClient/SoapKernel.php
new file mode 100644
index 0000000..949798e
--- /dev/null
+++ b/src/BeSimple/SoapClient/SoapKernel.php
@@ -0,0 +1,47 @@
+
+ * (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.
+ */
+
+namespace BeSimple\SoapClient;
+
+use BeSimple\SoapCommon\SoapKernel as CommonSoapKernel;
+use BeSimple\SoapCommon\SoapRequest;
+use BeSimple\SoapCommon\SoapResponse;
+
+/**
+ * SoapKernel for Client.
+ *
+ * @author Andreas Schamberger 
+ */
+class SoapKernel extends CommonSoapKernel
+{
+    /**
+     * {@inheritDoc}
+     */
+    public function filterRequest(SoapRequest $request)
+    {
+        $request->setAttachments($this->attachments);
+        $this->attachments = array();
+
+        parent::filterRequest($request);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function filterResponse(SoapResponse $response)
+    {
+        parent::filterResponse($response);
+
+        $this->attachments = $response->getAttachments();
+    }
+}
\ No newline at end of file

From 64002c61d2dcd7e784b41e91ac6507ab7bc2f335 Mon Sep 17 00:00:00 2001
From: Andreas Schamberger 
Date: Sun, 22 Apr 2012 18:10:21 +0200
Subject: [PATCH 59/63] server interop test

---
 tests/ServerInterop/MTOM.php       | 61 ++++++++++++++++++++
 tests/ServerInterop/MTOM.wsdl      | 89 ++++++++++++++++++++++++++++++
 tests/ServerInterop/MTOMServer.php | 53 ++++++++++++++++++
 3 files changed, 203 insertions(+)
 create mode 100644 tests/ServerInterop/MTOM.php
 create mode 100644 tests/ServerInterop/MTOM.wsdl
 create mode 100644 tests/ServerInterop/MTOMServer.php

diff --git a/tests/ServerInterop/MTOM.php b/tests/ServerInterop/MTOM.php
new file mode 100644
index 0000000..4f815b9
--- /dev/null
+++ b/tests/ServerInterop/MTOM.php
@@ -0,0 +1,61 @@
+ SOAP_1_1,
+    'features'        => SOAP_SINGLE_ELEMENT_ARRAYS, // make sure that result is array for size=1
+    'trace'           => true, // enables use of the methods  SoapClient->__getLastRequest,  SoapClient->__getLastRequestHeaders,  SoapClient->__getLastResponse and  SoapClient->__getLastResponseHeaders
+    'attachment_type' => BeSimpleSoapHelper::ATTACHMENTS_TYPE_MTOM,
+    'cache_wsdl'      => WSDL_CACHE_NONE,
+    'classmap'        => array(
+        'base64Binary'      => 'base64Binary',
+        'AttachmentRequest' => 'AttachmentRequest',
+    ),
+);
+
+$sc = new BeSimpleSoapClient('MTOM.wsdl', $options);
+
+//var_dump($sc->__getFunctions());
+//var_dump($sc->__getTypes());
+
+try {
+    $b64 = new base64Binary();
+    $b64->_ = 'This is a test. :)';
+    $b64->contentType = 'text/plain';
+
+    $attachment = new AttachmentRequest();
+    $attachment->fileName = 'test123.txt';
+    $attachment->binaryData = $b64;
+
+    var_dump($sc->attachment($attachment));
+
+} catch (Exception $e) {
+    var_dump($e);
+}
+
+// var_dump(
+//     $sc->__getLastRequestHeaders(),
+//     $sc->__getLastRequest(),
+//     $sc->__getLastResponseHeaders(),
+//     $sc->__getLastResponse()
+// );
\ No newline at end of file
diff --git a/tests/ServerInterop/MTOM.wsdl b/tests/ServerInterop/MTOM.wsdl
new file mode 100644
index 0000000..7772015
--- /dev/null
+++ b/tests/ServerInterop/MTOM.wsdl
@@ -0,0 +1,89 @@
+
+
+  
+    
+            
+            
+                
+                    
+                    
+                
+            
+            
+            
+        
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+  
+  
+    
+    
+  
+  
+    
+    
+  
+  
+    
+      
+    
+      
+    
+    
+  
+  
+    
+    
+      
+      
+        
+      
+      
+        
+      
+    
+  
+  
+    
+    
+      
+      
+        
+      
+      
+        
+      
+    
+  
+  
+    
+      
+    
+    
+      
+    
+  
+
diff --git a/tests/ServerInterop/MTOMServer.php b/tests/ServerInterop/MTOMServer.php
new file mode 100644
index 0000000..5deee38
--- /dev/null
+++ b/tests/ServerInterop/MTOMServer.php
@@ -0,0 +1,53 @@
+ SOAP_1_1,
+    'features'        => SOAP_SINGLE_ELEMENT_ARRAYS, // make sure that result is array for size=1
+    'attachment_type' => BeSimpleSoapHelper::ATTACHMENTS_TYPE_MTOM,
+    'cache_wsdl'      => WSDL_CACHE_NONE,
+    'classmap'        => array(
+        'base64Binary'      => 'base64Binary',
+        'AttachmentRequest' => 'AttachmentRequest',
+    ),
+);
+
+class Mtom
+{
+    public function attachment(AttachmentRequest $attachment)
+    {
+        $b64 = $attachment->binaryData;
+
+        file_put_contents('test.txt', var_export(array(
+            $attachment->fileName,
+            $b64->_,
+            $b64->contentType
+        ), true));
+
+        return 'done';
+    }
+}
+
+$ss = new BeSimpleSoapServer('MTOM.wsdl', $options);
+$ss->setClass('Mtom');
+$ss->handle();

From 5b4f19d032ffbf48e292c73efdf16794cbb347ff Mon Sep 17 00:00:00 2001
From: Christian Scheb 
Date: Tue, 31 Jul 2012 16:57:35 +0200
Subject: [PATCH 60/63] Schemas should only included when there is a
 "schemaLocation" attribute

---
 src/BeSimple/SoapClient/WsdlDownloader.php | 18 ++++++++++--------
 1 file changed, 10 insertions(+), 8 deletions(-)

diff --git a/src/BeSimple/SoapClient/WsdlDownloader.php b/src/BeSimple/SoapClient/WsdlDownloader.php
index 453eaba..79e8423 100644
--- a/src/BeSimple/SoapClient/WsdlDownloader.php
+++ b/src/BeSimple/SoapClient/WsdlDownloader.php
@@ -189,14 +189,16 @@ class WsdlDownloader
         $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);
+                if ( $node->hasAttribute('schemaLocation') ) {
+                    $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);
+                    }
                 }
             }
         }

From 2a82f02db34d09478847845e7fd55e1048ec219b Mon Sep 17 00:00:00 2001
From: Christian Scheb 
Date: Thu, 2 Aug 2012 10:13:49 +0200
Subject: [PATCH 61/63] Code formating changes

---
 src/BeSimple/SoapClient/WsdlDownloader.php | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/BeSimple/SoapClient/WsdlDownloader.php b/src/BeSimple/SoapClient/WsdlDownloader.php
index 79e8423..9f62fe4 100644
--- a/src/BeSimple/SoapClient/WsdlDownloader.php
+++ b/src/BeSimple/SoapClient/WsdlDownloader.php
@@ -189,12 +189,12 @@ class WsdlDownloader
         $nodes = $xpath->query($query);
         if ($nodes->length > 0) {
             foreach ($nodes as $node) {
-                if ( $node->hasAttribute('schemaLocation') ) {
+                if ($node->hasAttribute('schemaLocation')) {
                     $schemaLocation = $node->getAttribute('schemaLocation');
                     if ($this->isRemoteFile($schemaLocation)) {
                         $schemaLocation = $this->download($schemaLocation);
                         $node->setAttribute('schemaLocation', $schemaLocation);
-                    } elseif (!is_null($parentFile)) {
+                    } elseif (null !== $parentFile) {
                         $schemaLocation = $this->resolveRelativePathInUrl($parentFile, $schemaLocation);
                         $schemaLocation = $this->download($schemaLocation);
                         $node->setAttribute('schemaLocation', $schemaLocation);

From 6a4fba600fa9b51861866c6e7d16f1e737c98096 Mon Sep 17 00:00:00 2001
From: Francis Besset 
Date: Mon, 6 Aug 2012 11:15:23 +0200
Subject: [PATCH 62/63] Added contributors file

---
 CONTRIBUTORS.md | 3 +++
 1 file changed, 3 insertions(+)
 create mode 100644 CONTRIBUTORS.md

diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
new file mode 100644
index 0000000..c79e9ef
--- /dev/null
+++ b/CONTRIBUTORS.md
@@ -0,0 +1,3 @@
+* [Francis Besset](https://github.com/francisbesset)
+* [aschamberger](https://github.com/aschamberger)
+* [Scheb](https://github.com/Scheb)

From 41301775bf7ca3cc8f486929c0d7484dce4c7393 Mon Sep 17 00:00:00 2001
From: Francis Besset 
Date: Tue, 15 Jan 2013 22:42:43 +0100
Subject: [PATCH 63/63] Added composer.json

---
 composer.json | 33 +++++++++++++++++++++++++++++++++
 1 file changed, 33 insertions(+)
 create mode 100644 composer.json

diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..0c5e927
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,33 @@
+{
+    "name": "besimple/soap-client",
+    "type": "library",
+    "description": "Build and consume SOAP Client based web services",
+    "keywords": [ "soap", "soap-client" ],
+    "homepage": "https://github.com/BeSimple/BeSimpleSoapClient",
+    "license": "MIT",
+    "authors": [
+        {
+            "name": "Francis Besset",
+            "email": "francis.besset@gmail.com"
+        },
+        {
+            "name": "Christian Kerl",
+            "email": "christian-kerl@web.de"
+        },
+        {
+            "name": "Andreas Schamberger",
+            "email": "mail@andreass.net"
+        }
+    ],
+    "require": {
+        "php": ">=5.3.0",
+        "besimple/soap-common": "dev-master",
+        "ass/xmlsecurity": "dev-master"
+    },
+    "autoload": {
+        "psr-0": {
+            "BeSimple\\SoapClient": "BeSimple/SoapClient/src/"
+        }
+    },
+    "target-dir": "BeSimple/SoapClient"
+}