From aee034791e7bf54604c4d8cdfc99a48f36ae99b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Bechyn=C4=9B?= Date: Fri, 3 Feb 2017 15:22:37 +0100 Subject: [PATCH] SoapClient large refactoring & tests update --- .gitignore | 2 +- README.md | 102 +- cache/.gitignore | 2 + composer.json | 9 +- composer.lock | 1349 +++++++++++++++++ phpunit.xml.dist | 14 +- src/BeSimple/SoapBundle/Cache.php | 40 +- .../SoapBundle/Soap/SoapAttachmentList.php | 36 + src/BeSimple/SoapBundle/WebServiceContext.php | 19 +- src/BeSimple/SoapClient/Curl.php | 336 ---- src/BeSimple/SoapClient/Curl/Curl.php | 234 +++ src/BeSimple/SoapClient/Curl/CurlOptions.php | 126 ++ .../SoapClient/Curl/CurlOptionsBuilder.php | 79 + src/BeSimple/SoapClient/Curl/CurlResponse.php | 100 ++ .../Http/HttpAuthenticationBasicOptions.php | 34 + .../Http/HttpAuthenticationDigestOptions.php | 11 + .../Curl/Http/HttpAuthenticationInterface.php | 15 + .../Curl/Http/SslCertificateOptions.php | 64 + src/BeSimple/SoapClient/MimeFilter.php | 4 +- src/BeSimple/SoapClient/SoapClient.php | 495 +++--- src/BeSimple/SoapClient/SoapClientBuilder.php | 24 +- .../SoapClientMessageWithAttachments.php | 7 + .../SoapClientNativeDataTransferObject.php | 19 + .../SoapClient/SoapClientOptionsBuilder.php | 29 +- .../SoapClient/SoapFaultWithTracingData.php | 21 + .../{ => SoapOptions}/SoapClientOptions.php | 21 +- src/BeSimple/SoapClient/SoapResponse.php | 37 +- .../SoapClient/SoapResponseFactory.php | 98 ++ .../SoapClient/SoapResponseTracingData.php | 39 + src/BeSimple/SoapClient/Tests/CurlTest.php | 51 +- .../SoapClient/Tests/GenerateTestRequest.php | 8 + .../Tests/GetUKLocationByCounty.php | 8 + src/BeSimple/SoapClient/Tests/Mock/.readme | 23 + .../Mock/MockSwaService.example.response | 18 + .../MockSwaService.example.response.headers | 1 + .../SoapClient/Tests/Mock/MockSwaService.wsdl | 61 + .../Tests/SoapClientBuilderTest.php | 338 ++++- src/BeSimple/SoapClient/Tests/localWsdl.wsdl | 380 +++++ .../soapRequestWithNoAttachments.request | 2 + .../soapRequestWithTwoAttachments.request | 24 + src/BeSimple/SoapClient/WsdlDownloader.php | 253 ++-- src/BeSimple/SoapCommon/Cache.php | 40 +- .../Converter/MtomTypeConverter.php | 5 - .../SoapCommon/Fault/SoapFaultEnum.php | 10 + src/BeSimple/SoapCommon/Mime/MultiPart.php | 5 + src/BeSimple/SoapCommon/Mime/Parser.php | 324 ++-- .../Mime/Parser/ContentTypeParser.php | 64 + .../Mime/Parser/ParsedMimeHeader.php | 37 + .../SoapCommon/Mime/Parser/ParsedPart.php | 34 + .../SoapCommon/Mime/Parser/ParsedPartList.php | 60 + src/BeSimple/SoapCommon/Mime/Part.php | 65 +- src/BeSimple/SoapCommon/Mime/PartFactory.php | 33 + src/BeSimple/SoapCommon/SoapKernel.php | 8 +- src/BeSimple/SoapCommon/SoapMessage.php | 2 +- .../SoapCommon/SoapOptions/SoapOptions.php | 1 + .../SoapCommon/SoapOptionsBuilder.php | 66 +- .../SoapCommon/SoapRequestFactory.php | 48 +- src/BeSimple/SoapCommon/SoapResponse.php | 21 +- .../SoapCommon/Tests/ClassMapTest.php | 36 +- src/BeSimple/SoapServer/MimeFilter.php | 4 +- .../SoapServer/SoapResponseFactory.php | 71 +- src/BeSimple/SoapServer/SoapServer.php | 12 +- src/BeSimple/SoapServer/SoapServerBuilder.php | 2 +- .../Tests/Attachment/Attachment.php | 35 + .../Tests/Attachment/AttachmentCollection.php | 21 + .../SoapServer/Tests/DummyService.php | 106 ++ .../SoapServer/Tests/DummyService.wsdl | 92 ++ .../SoapServer/Tests/DummyServiceHandler.php | 18 + .../DummyServiceHandlerWithAttachments.php | 30 + .../SoapServer/Tests/DummyServiceRequest.php | 11 + .../DummyServiceRequestWithAttachments.php | 28 + .../SoapServer/Tests/DummyServiceResponse.php | 11 + .../DummyServiceResponseWithAttachments.php | 23 + .../Tests/SoapServerBuilderTest.php | 129 +- .../SoapServer/Tests/SoapServerHandler.php | 7 + .../Tests/testHandleRequest.message | 14 + .../Tests/testHandleRequestWithSwa.message | 15 + .../testHandleRequestWithSwa.mimepart.message | 62 + 78 files changed, 4957 insertions(+), 1126 deletions(-) create mode 100644 cache/.gitignore create mode 100644 composer.lock create mode 100644 src/BeSimple/SoapBundle/Soap/SoapAttachmentList.php delete mode 100644 src/BeSimple/SoapClient/Curl.php create mode 100644 src/BeSimple/SoapClient/Curl/Curl.php create mode 100644 src/BeSimple/SoapClient/Curl/CurlOptions.php create mode 100644 src/BeSimple/SoapClient/Curl/CurlOptionsBuilder.php create mode 100644 src/BeSimple/SoapClient/Curl/CurlResponse.php create mode 100644 src/BeSimple/SoapClient/Curl/Http/HttpAuthenticationBasicOptions.php create mode 100644 src/BeSimple/SoapClient/Curl/Http/HttpAuthenticationDigestOptions.php create mode 100644 src/BeSimple/SoapClient/Curl/Http/HttpAuthenticationInterface.php create mode 100644 src/BeSimple/SoapClient/Curl/Http/SslCertificateOptions.php create mode 100644 src/BeSimple/SoapClient/SoapClientMessageWithAttachments.php create mode 100644 src/BeSimple/SoapClient/SoapClientNativeDataTransferObject.php create mode 100644 src/BeSimple/SoapClient/SoapFaultWithTracingData.php rename src/BeSimple/SoapClient/{ => SoapOptions}/SoapClientOptions.php (77%) create mode 100644 src/BeSimple/SoapClient/SoapResponseFactory.php create mode 100644 src/BeSimple/SoapClient/SoapResponseTracingData.php create mode 100644 src/BeSimple/SoapClient/Tests/GenerateTestRequest.php create mode 100644 src/BeSimple/SoapClient/Tests/GetUKLocationByCounty.php create mode 100644 src/BeSimple/SoapClient/Tests/Mock/.readme create mode 100644 src/BeSimple/SoapClient/Tests/Mock/MockSwaService.example.response create mode 100644 src/BeSimple/SoapClient/Tests/Mock/MockSwaService.example.response.headers create mode 100644 src/BeSimple/SoapClient/Tests/Mock/MockSwaService.wsdl create mode 100644 src/BeSimple/SoapClient/Tests/localWsdl.wsdl create mode 100644 src/BeSimple/SoapClient/Tests/soapRequestWithNoAttachments.request create mode 100644 src/BeSimple/SoapClient/Tests/soapRequestWithTwoAttachments.request create mode 100644 src/BeSimple/SoapCommon/Fault/SoapFaultEnum.php create mode 100644 src/BeSimple/SoapCommon/Mime/Parser/ContentTypeParser.php create mode 100644 src/BeSimple/SoapCommon/Mime/Parser/ParsedMimeHeader.php create mode 100644 src/BeSimple/SoapCommon/Mime/Parser/ParsedPart.php create mode 100644 src/BeSimple/SoapCommon/Mime/Parser/ParsedPartList.php create mode 100644 src/BeSimple/SoapCommon/Mime/PartFactory.php create mode 100644 src/BeSimple/SoapServer/Tests/Attachment/Attachment.php create mode 100644 src/BeSimple/SoapServer/Tests/Attachment/AttachmentCollection.php create mode 100644 src/BeSimple/SoapServer/Tests/DummyService.php create mode 100644 src/BeSimple/SoapServer/Tests/DummyService.wsdl create mode 100644 src/BeSimple/SoapServer/Tests/DummyServiceHandler.php create mode 100644 src/BeSimple/SoapServer/Tests/DummyServiceHandlerWithAttachments.php create mode 100644 src/BeSimple/SoapServer/Tests/DummyServiceRequest.php create mode 100644 src/BeSimple/SoapServer/Tests/DummyServiceRequestWithAttachments.php create mode 100644 src/BeSimple/SoapServer/Tests/DummyServiceResponse.php create mode 100644 src/BeSimple/SoapServer/Tests/DummyServiceResponseWithAttachments.php create mode 100644 src/BeSimple/SoapServer/Tests/SoapServerHandler.php create mode 100644 src/BeSimple/SoapServer/Tests/testHandleRequest.message create mode 100644 src/BeSimple/SoapServer/Tests/testHandleRequestWithSwa.message create mode 100644 src/BeSimple/SoapServer/Tests/testHandleRequestWithSwa.mimepart.message diff --git a/.gitignore b/.gitignore index 72bd9c9..48c3ad1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ /vendor/ -composer.lock phpunit.xml +.idea/ diff --git a/README.md b/README.md index e1caac8..d4a0c72 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,36 @@ # BeSimpleSoap -Build SOAP and WSDL based web services +Build SOAP and WSDL based web services. +This fork from 2017 is a refactored version that fixes a lot of errors and provides +better APi, more robust, stable and modern codebase. +See [How to use](#how-to-use) that will help you to understand the magic. # Components BeSimpleSoap consists of five components ... -## BeSimpleSoapBundle - -The BeSimpleSoapBundle is a Symfony2 bundle to build WSDL and SOAP based web services. -For further information see the [README](https://github.com/BeSimple/BeSimpleSoap/blob/master/src/BeSimple/SoapBundle/README.md). - ## BeSimpleSoapClient -The BeSimpleSoapClient is a component that extends the native PHP SoapClient with further features like SwA, MTOM and WS-Security. -For further information see the [README](https://github.com/BeSimple/BeSimpleSoap/blob/master/src/BeSimple/SoapClient/README.md). - -## BeSimpleSoapCommon - -The BeSimpleSoapCommon component contains functionylity shared by both the server and client implementations. -For further information see the [README](https://github.com/BeSimple/BeSimpleSoap/blob/master/src/BeSimple/SoapCommon/README.md). - +**Refactored** BeSimpleSoapClient is a component that extends the native PHP SoapClient with further features like SwA and WS-Security. ## BeSimpleSoapServer -The BeSimpleSoapServer is a component that extends the native PHP SoapServer with further features like SwA, MTOM and WS-Security. -For further information see the [README](https://github.com/BeSimple/BeSimpleSoap/blob/master/src/BeSimple/SoapServer/README.md). +**Refactored** BeSimpleSoapServer is a component that extends the native PHP SoapServer with further features like SwA and WS-Security. + +## BeSimpleSoapCommon + +**Refactored** BeSimpleSoapCommon component contains functionality shared by both the server and client implementations. ## BeSimpleSoapWsdl -For further information see the [README](https://github.com/BeSimple/BeSimpleSoap/blob/master/src/BeSimple/SoapWsdl/README.md). +Currently **unsupported!** For further information see the [README](https://github.com/BeSimple/BeSimpleSoap/blob/master/src/BeSimple/SoapWsdl/README.md). + +## BeSimpleSoapBundle + +Currently **unsupported!** +The BeSimpleSoapBundle is a Symfony2 bundle to build WSDL and SOAP based web services. +For further information see the the original [README](https://github.com/BeSimple/BeSimpleSoap/blob/master/src/BeSimple/SoapBundle/README.md). +*May not work properly since the Symfony libraries were removed.* # Installation @@ -54,3 +55,70 @@ Now you are ready to install the library: ```sh php /usr/local/bin/composer.phar install ``` + +# How to use + +You can investigate the unit tests in order to get a clue. +Forget about associative arrays, multiple extension and silent errors! + +``` +// unit tests for soap client +BeSimple\SoapClient\Tests\SoapClientBuilderTest +// unit tests for soap server +BeSimple\SoapServer\Tests\SoapServerBuilderTest +``` + +## Small example of soap client call + +```php +$soapClientBuilder = new SoapClientBuilder(); +$soapClient = $soapClientBuilder->build( + SoapClientOptionsBuilder::createWithDefaults(), + SoapOptionsBuilder::createWithDefaults('http://path/to/wsdlfile.wsdl') +); +$myRequest = new MyRequest(); +$myRequest->attribute = 'string value'; +$soapResponse = $soapClient->soapCall('myMethod', [$myRequest]); + +var_dump($soapResponse); // Contains Response, Attachments +``` + +### Something wrong?! +Turn on the tracking and catch `SoapFaultWithTracingData` exception to get some sweets :) + +```php +try { + $soapResponse = $soapClient->soapCall('GetUKLocationByCounty', [$getUKLocationByCountyRequest]); +} catch (SoapFaultWithTracingData $fault) { + var_dump($fault->getSoapResponseTracingData()->getLastRequest()); +} +``` + +## Small example of soap server handling + +Starting a SOAP server is a bit more complex. +I would suggest you to inspect SoapServer unit tests to get complete image. +But don't be scared too much, you just need to create a DummyService that will +handle your client SOAP calls. + +```php +$dummyService = new DummyService(); +$classMap = new ClassMap(); +foreach ($dummyService->getClassMap() as $type => $className) { + $classMap->add($type, $className); +} +$soapServerBuilder = new SoapServerBuilder(); +$soapServerOptions = SoapServerOptionsBuilder::createWithDefaults($dummyService); +$soapOptions = SoapOptionsBuilder::createWithClassMap($dummyService->getWsdlPath(), $classMap); +$soapServer = $soapServerBuilder->build($soapServerOptions, $soapOptions); + +$request = $soapServer->createRequest( + $dummyService->getEndpoint(), + 'DummyService.dummyServiceMethod', + 'text/xml;charset=UTF-8', + '' +); +$response = $soapServer->handleRequest($request); + +var_dump($response); // Contains Response, Attachments +``` diff --git a/cache/.gitignore b/cache/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/cache/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/composer.json b/composer.json index 98834c7..4e0bc5d 100644 --- a/composer.json +++ b/composer.json @@ -19,16 +19,14 @@ }, { "name": "Petr Bechyně", - "email": "petr.bechyne@vodafone.com" + "email": "mail@petrbechyne.com" } ], "require": { - "php": ">=5.3.0", + "php": ">=5.3.0|>=7.0", "ext-soap": "*", "ext-curl": "*", "ass/xmlsecurity": "~1.0", - "symfony/framework-bundle": "~2.6|~3.0", - "symfony/twig-bundle": "~2.6|~3.0", "zendframework/zend-mime": "2.1.*" }, "replace": { @@ -41,8 +39,7 @@ "require-dev": { "ext-mcrypt": "*", "mikey179/vfsStream": "~1.0", - "symfony/filesystem": "~2.3", - "symfony/process": "~2.3" + "phpunit/phpunit": "~4.0" }, "autoload": { "psr-0": { "BeSimple\\": "src/" } diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..e88b242 --- /dev/null +++ b/composer.lock @@ -0,0 +1,1349 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "This file is @generated automatically" + ], + "hash": "7601f1ec280c086361e1deaf021f8ca1", + "content-hash": "59372f34124af17d5c35938dfffc6075", + "packages": [ + { + "name": "ass/xmlsecurity", + "version": "v1.1.1", + "source": { + "type": "git", + "url": "https://github.com/aschamberger/XmlSecurity.git", + "reference": "c8976519ebbf6e4d953cd781d09df44b7f65fbb8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/aschamberger/XmlSecurity/zipball/c8976519ebbf6e4d953cd781d09df44b7f65fbb8", + "reference": "c8976519ebbf6e4d953cd781d09df44b7f65fbb8", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "lib-openssl": ">=0.9.0", + "php": ">=5.3.0" + }, + "require-dev": { + "satooshi/php-coveralls": "dev-master" + }, + "suggest": { + "ext-mcrypt": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-0": { + "ass\\XmlSecurity": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Robert Richards", + "email": "rrichards@cdatazone.org" + }, + { + "name": "Andreas Schamberger", + "email": "mail@andreass.net" + } + ], + "description": "The XmlSecurity library is written in PHP for working with XML Encryption and Signatures", + "homepage": "https://github.com/aschamberger/XmlSecurity", + "keywords": [ + "encryption", + "security", + "signature", + "xml" + ], + "time": "2015-05-31 10:10:35" + }, + { + "name": "zendframework/zend-mime", + "version": "2.1.6", + "source": { + "type": "git", + "url": "https://github.com/zendframework/zend-mime.git", + "reference": "066d6eecff586a7fb10e8907c032beaf1a9d6104" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/zendframework/zend-mime/zipball/066d6eecff586a7fb10e8907c032beaf1a9d6104", + "reference": "066d6eecff586a7fb10e8907c032beaf1a9d6104", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "zendframework/zend-stdlib": "self.version" + }, + "require-dev": { + "fabpot/php-cs-fixer": "1.7.*", + "phpunit/phpunit": "~4.0", + "satooshi/php-coveralls": "dev-master", + "zendframework/zend-mail": "self.version" + }, + "suggest": { + "zendframework/zend-mail": "Zend\\Mail component" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.1-dev", + "dev-develop": "2.2-dev" + } + }, + "autoload": { + "psr-4": { + "Zend\\Mime\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "homepage": "https://github.com/zendframework/zend-mime", + "keywords": [ + "mime", + "zf2" + ], + "time": "2013-04-17 13:32:54" + }, + { + "name": "zendframework/zend-stdlib", + "version": "2.1.6", + "source": { + "type": "git", + "url": "https://github.com/zendframework/zend-stdlib.git", + "reference": "0027339961ad3d49f91ee092e23f7269c18cb470" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/zendframework/zend-stdlib/zipball/0027339961ad3d49f91ee092e23f7269c18cb470", + "reference": "0027339961ad3d49f91ee092e23f7269c18cb470", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "fabpot/php-cs-fixer": "1.7.*", + "phpunit/phpunit": "~4.0", + "satooshi/php-coveralls": "dev-master", + "zendframework/zend-eventmanager": "self.version", + "zendframework/zend-filter": "self.version", + "zendframework/zend-serializer": "self.version", + "zendframework/zend-servicemanager": "self.version" + }, + "suggest": { + "pecl-weakref": "Implementation of weak references for Stdlib\\CallbackHandler", + "zendframework/zend-eventmanager": "To support aggregate hydrator usage", + "zendframework/zend-filter": "To support naming strategy hydrator usage", + "zendframework/zend-serializer": "Zend\\Serializer component", + "zendframework/zend-servicemanager": "To support hydrator plugin manager usage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.1-dev", + "dev-develop": "2.2-dev" + } + }, + "autoload": { + "psr-4": { + "Zend\\Stdlib\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "homepage": "https://github.com/zendframework/zend-stdlib", + "keywords": [ + "stdlib", + "zf2" + ], + "time": "2013-04-17 13:32:54" + } + ], + "packages-dev": [ + { + "name": "doctrine/instantiator", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/8e884e78f9f0eb1329e445619e04456e64d8051d", + "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d", + "shasum": "" + }, + "require": { + "php": ">=5.3,<8.0-DEV" + }, + "require-dev": { + "athletic/athletic": "~0.1.8", + "ext-pdo": "*", + "ext-phar": "*", + "phpunit/phpunit": "~4.0", + "squizlabs/php_codesniffer": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "http://ocramius.github.com/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://github.com/doctrine/instantiator", + "keywords": [ + "constructor", + "instantiate" + ], + "time": "2015-06-14 21:17:01" + }, + { + "name": "mikey179/vfsStream", + "version": "v1.6.4", + "source": { + "type": "git", + "url": "https://github.com/mikey179/vfsStream.git", + "reference": "0247f57b2245e8ad2e689d7cee754b45fbabd592" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mikey179/vfsStream/zipball/0247f57b2245e8ad2e689d7cee754b45fbabd592", + "reference": "0247f57b2245e8ad2e689d7cee754b45fbabd592", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.6.x-dev" + } + }, + "autoload": { + "psr-0": { + "org\\bovigo\\vfs\\": "src/main/php" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Frank Kleine", + "homepage": "http://frankkleine.de/", + "role": "Developer" + } + ], + "description": "Virtual file system to mock the real file system in unit tests.", + "homepage": "http://vfs.bovigo.org/", + "time": "2016-07-18 14:02:57" + }, + { + "name": "phpdocumentor/reflection-common", + "version": "1.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "144c307535e82c8fdcaacbcfc1d6d8eeb896687c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/144c307535e82c8fdcaacbcfc1d6d8eeb896687c", + "reference": "144c307535e82c8fdcaacbcfc1d6d8eeb896687c", + "shasum": "" + }, + "require": { + "php": ">=5.5" + }, + "require-dev": { + "phpunit/phpunit": "^4.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": [ + "src" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "Common reflection classes used by phpdocumentor to reflect the code structure", + "homepage": "http://www.phpdoc.org", + "keywords": [ + "FQSEN", + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" + ], + "time": "2015-12-27 11:43:31" + }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "3.1.1", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "8331b5efe816ae05461b7ca1e721c01b46bafb3e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/8331b5efe816ae05461b7ca1e721c01b46bafb3e", + "reference": "8331b5efe816ae05461b7ca1e721c01b46bafb3e", + "shasum": "" + }, + "require": { + "php": ">=5.5", + "phpdocumentor/reflection-common": "^1.0@dev", + "phpdocumentor/type-resolver": "^0.2.0", + "webmozart/assert": "^1.0" + }, + "require-dev": { + "mockery/mockery": "^0.9.4", + "phpunit/phpunit": "^4.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "time": "2016-09-30 07:12:33" + }, + { + "name": "phpdocumentor/type-resolver", + "version": "0.2.1", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "e224fb2ea2fba6d3ad6fdaef91cd09a172155ccb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/e224fb2ea2fba6d3ad6fdaef91cd09a172155ccb", + "reference": "e224fb2ea2fba6d3ad6fdaef91cd09a172155ccb", + "shasum": "" + }, + "require": { + "php": ">=5.5", + "phpdocumentor/reflection-common": "^1.0" + }, + "require-dev": { + "mockery/mockery": "^0.9.4", + "phpunit/phpunit": "^5.2||^4.8.24" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "time": "2016-11-25 06:54:22" + }, + { + "name": "phpspec/prophecy", + "version": "v1.6.2", + "source": { + "type": "git", + "url": "https://github.com/phpspec/prophecy.git", + "reference": "6c52c2722f8460122f96f86346600e1077ce22cb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/6c52c2722f8460122f96f86346600e1077ce22cb", + "reference": "6c52c2722f8460122f96f86346600e1077ce22cb", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.0.2", + "php": "^5.3|^7.0", + "phpdocumentor/reflection-docblock": "^2.0|^3.0.2", + "sebastian/comparator": "^1.1", + "sebastian/recursion-context": "^1.0|^2.0" + }, + "require-dev": { + "phpspec/phpspec": "^2.0", + "phpunit/phpunit": "^4.8 || ^5.6.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.6.x-dev" + } + }, + "autoload": { + "psr-0": { + "Prophecy\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + }, + { + "name": "Marcello Duarte", + "email": "marcello.duarte@gmail.com" + } + ], + "description": "Highly opinionated mocking framework for PHP 5.3+", + "homepage": "https://github.com/phpspec/prophecy", + "keywords": [ + "Double", + "Dummy", + "fake", + "mock", + "spy", + "stub" + ], + "time": "2016-11-21 14:58:47" + }, + { + "name": "phpunit/php-code-coverage", + "version": "2.2.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "eabf68b476ac7d0f73793aada060f1c1a9bf8979" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/eabf68b476ac7d0f73793aada060f1c1a9bf8979", + "reference": "eabf68b476ac7d0f73793aada060f1c1a9bf8979", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "phpunit/php-file-iterator": "~1.3", + "phpunit/php-text-template": "~1.2", + "phpunit/php-token-stream": "~1.3", + "sebastian/environment": "^1.3.2", + "sebastian/version": "~1.0" + }, + "require-dev": { + "ext-xdebug": ">=2.1.4", + "phpunit/phpunit": "~4" + }, + "suggest": { + "ext-dom": "*", + "ext-xdebug": ">=2.2.1", + "ext-xmlwriter": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.2.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "time": "2015-10-06 15:47:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "1.4.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "3cc8f69b3028d0f96a9078e6295d86e9bf019be5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/3cc8f69b3028d0f96a9078e6295d86e9bf019be5", + "reference": "3cc8f69b3028d0f96a9078e6295d86e9bf019be5", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "time": "2016-10-03 07:40:28" + }, + { + "name": "phpunit/php-text-template", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686", + "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "time": "2015-06-21 13:50:34" + }, + { + "name": "phpunit/php-timer", + "version": "1.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "38e9124049cf1a164f1e4537caf19c99bf1eb260" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/38e9124049cf1a164f1e4537caf19c99bf1eb260", + "reference": "38e9124049cf1a164f1e4537caf19c99bf1eb260", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4|~5" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "time": "2016-05-12 18:03:57" + }, + { + "name": "phpunit/php-token-stream", + "version": "1.4.9", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-token-stream.git", + "reference": "3b402f65a4cc90abf6e1104e388b896ce209631b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/3b402f65a4cc90abf6e1104e388b896ce209631b", + "reference": "3b402f65a4cc90abf6e1104e388b896ce209631b", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Wrapper around PHP's tokenizer extension.", + "homepage": "https://github.com/sebastianbergmann/php-token-stream/", + "keywords": [ + "tokenizer" + ], + "time": "2016-11-15 14:06:22" + }, + { + "name": "phpunit/phpunit", + "version": "4.8.34", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "7eb45205d27edd94bd2b3614085ea158bd1e2bca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/7eb45205d27edd94bd2b3614085ea158bd1e2bca", + "reference": "7eb45205d27edd94bd2b3614085ea158bd1e2bca", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-pcre": "*", + "ext-reflection": "*", + "ext-spl": "*", + "php": ">=5.3.3", + "phpspec/prophecy": "^1.3.1", + "phpunit/php-code-coverage": "~2.1", + "phpunit/php-file-iterator": "~1.4", + "phpunit/php-text-template": "~1.2", + "phpunit/php-timer": "^1.0.6", + "phpunit/phpunit-mock-objects": "~2.3", + "sebastian/comparator": "~1.2.2", + "sebastian/diff": "~1.2", + "sebastian/environment": "~1.3", + "sebastian/exporter": "~1.2", + "sebastian/global-state": "~1.0", + "sebastian/version": "~1.0", + "symfony/yaml": "~2.1|~3.0" + }, + "suggest": { + "phpunit/php-invoker": "~1.1" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.8.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "time": "2017-01-26 16:15:36" + }, + { + "name": "phpunit/phpunit-mock-objects", + "version": "2.3.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", + "reference": "ac8e7a3db35738d56ee9a76e78a4e03d97628983" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/ac8e7a3db35738d56ee9a76e78a4e03d97628983", + "reference": "ac8e7a3db35738d56ee9a76e78a4e03d97628983", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.0.2", + "php": ">=5.3.3", + "phpunit/php-text-template": "~1.2", + "sebastian/exporter": "~1.2" + }, + "require-dev": { + "phpunit/phpunit": "~4.4" + }, + "suggest": { + "ext-soap": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.3.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Mock Object library for PHPUnit", + "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/", + "keywords": [ + "mock", + "xunit" + ], + "time": "2015-10-02 06:51:40" + }, + { + "name": "sebastian/comparator", + "version": "1.2.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "2b7424b55f5047b47ac6e5ccb20b2aea4011d9be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2b7424b55f5047b47ac6e5ccb20b2aea4011d9be", + "reference": "2b7424b55f5047b47ac6e5ccb20b2aea4011d9be", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "sebastian/diff": "~1.2", + "sebastian/exporter": "~1.2 || ~2.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "http://www.github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "time": "2017-01-29 09:50:25" + }, + { + "name": "sebastian/diff", + "version": "1.4.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "13edfd8706462032c2f52b4b862974dd46b71c9e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/13edfd8706462032c2f52b4b862974dd46b71c9e", + "reference": "13edfd8706462032c2f52b4b862974dd46b71c9e", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff" + ], + "time": "2015-12-08 07:14:41" + }, + { + "name": "sebastian/environment", + "version": "1.3.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "be2c607e43ce4c89ecd60e75c6a85c126e754aea" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/be2c607e43ce4c89ecd60e75c6a85c126e754aea", + "reference": "be2c607e43ce4c89ecd60e75c6a85c126e754aea", + "shasum": "" + }, + "require": { + "php": "^5.3.3 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8 || ^5.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "http://www.github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "time": "2016-08-18 05:49:44" + }, + { + "name": "sebastian/exporter", + "version": "1.2.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "42c4c2eec485ee3e159ec9884f95b431287edde4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/42c4c2eec485ee3e159ec9884f95b431287edde4", + "reference": "42c4c2eec485ee3e159ec9884f95b431287edde4", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "sebastian/recursion-context": "~1.0" + }, + "require-dev": { + "ext-mbstring": "*", + "phpunit/phpunit": "~4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "http://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "time": "2016-06-17 09:04:28" + }, + { + "name": "sebastian/global-state", + "version": "1.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bc37d50fea7d017d3d340f230811c9f1d7280af4", + "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.2" + }, + "suggest": { + "ext-uopz": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "time": "2015-10-12 03:26:01" + }, + { + "name": "sebastian/recursion-context", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "913401df809e99e4f47b27cdd781f4a258d58791" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/913401df809e99e4f47b27cdd781f4a258d58791", + "reference": "913401df809e99e4f47b27cdd781f4a258d58791", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "http://www.github.com/sebastianbergmann/recursion-context", + "time": "2015-11-11 19:50:13" + }, + { + "name": "sebastian/version", + "version": "1.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "58b3a85e7999757d6ad81c787a1fbf5ff6c628c6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/58b3a85e7999757d6ad81c787a1fbf5ff6c628c6", + "reference": "58b3a85e7999757d6ad81c787a1fbf5ff6c628c6", + "shasum": "" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "time": "2015-06-21 13:59:46" + }, + { + "name": "symfony/yaml", + "version": "v3.2.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "50eadbd7926e31842893c957eca362b21592a97d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/50eadbd7926e31842893c957eca362b21592a97d", + "reference": "50eadbd7926e31842893c957eca362b21592a97d", + "shasum": "" + }, + "require": { + "php": ">=5.5.9" + }, + "require-dev": { + "symfony/console": "~2.8|~3.0" + }, + "suggest": { + "symfony/console": "For validating YAML files using the lint command" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Yaml Component", + "homepage": "https://symfony.com", + "time": "2017-01-03 13:51:32" + }, + { + "name": "webmozart/assert", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/webmozart/assert.git", + "reference": "2db61e59ff05fe5126d152bd0655c9ea113e550f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozart/assert/zipball/2db61e59ff05fe5126d152bd0655c9ea113e550f", + "reference": "2db61e59ff05fe5126d152bd0655c9ea113e550f", + "shasum": "" + }, + "require": { + "php": "^5.3.3 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.6", + "sebastian/version": "^1.0.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "time": "2016-11-23 20:04:58" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=5.3.0|>=7.0", + "ext-soap": "*", + "ext-curl": "*" + }, + "platform-dev": { + "ext-mcrypt": "*" + } +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist index eaf287b..1cb26e7 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -9,6 +9,7 @@ processIsolation="false" stopOnFailure="false" syntaxCheck="false" + stderr="true" bootstrap="vendor/autoload.php" > @@ -18,17 +19,8 @@ - ./src/BeSimple/*/Tests/ + src/BeSimple/SoapClient/Tests/SoapClientBuilderTest.php + src/BeSimple/SoapServer/Tests/SoapServerBuilderTest.php - - - - ./src/BeSimple/ - - ./src/BeSimple/*/Tests - ./src/BeSimple/*/Resources - - - diff --git a/src/BeSimple/SoapBundle/Cache.php b/src/BeSimple/SoapBundle/Cache.php index fe5621f..510b35f 100644 --- a/src/BeSimple/SoapBundle/Cache.php +++ b/src/BeSimple/SoapBundle/Cache.php @@ -13,27 +13,45 @@ namespace BeSimple\SoapBundle; use BeSimple\SoapCommon\Cache as BaseCache; +use BeSimple\SoapCommon\SoapOptions\SoapOptions; +use Exception; /** * @author Francis Besset */ class Cache { - public function __construct($cacheDisabled, $type, $directory, $lifetime = null, $limit = null) + public function __construct(SoapOptions $soapOptions) { - $isEnabled = (Boolean) $cacheDisabled ? BaseCache::DISABLED : BaseCache::ENABLED; + if ($soapOptions->isWsdlCached()) { + $isEnabled = (bool)$soapOptions->isWsdlCached() ? BaseCache::ENABLED : BaseCache::DISABLED; - BaseCache::setEnabled($isEnabled); - - BaseCache::setType($type); - BaseCache::setDirectory($directory); - - if (null !== $lifetime) { - BaseCache::setLifetime($lifetime); + BaseCache::setEnabled($isEnabled); + BaseCache::setType($soapOptions->getWsdlCacheType()); + BaseCache::setDirectory($soapOptions->getWsdlCacheDir()); + } else { + BaseCache::setEnabled(BaseCache::DISABLED); + BaseCache::setType(SoapOptions::SOAP_CACHE_TYPE_NONE); + BaseCache::setDirectory(null); } + } - if (null !== $limit) { - BaseCache::setLimit($limit); + public function validateSettings(SoapOptions $soapOptions) + { + if ($soapOptions->isWsdlCached()) { + if (BaseCache::isEnabled() !== true) { + throw new Exception('WSDL cache could not be set'); + } + if ($soapOptions->getWsdlCacheType() !== (int)BaseCache::getType()) { + throw new Exception('WSDL cache type could not be set, ini settings is: '.BaseCache::getType()); + } + if ($soapOptions->getWsdlCacheDir() !== BaseCache::getDirectory()) { + throw new Exception('WSDL cache dir could not be set, real dir is: '.BaseCache::getDirectory()); + } + } else { + if (BaseCache::isEnabled() !== false) { + throw new Exception('WSDL cache could not be turned off'); + } } } } diff --git a/src/BeSimple/SoapBundle/Soap/SoapAttachmentList.php b/src/BeSimple/SoapBundle/Soap/SoapAttachmentList.php new file mode 100644 index 0000000..e65503a --- /dev/null +++ b/src/BeSimple/SoapBundle/Soap/SoapAttachmentList.php @@ -0,0 +1,36 @@ +soapAttachments = $soapAttachments; + } + + public function hasSoapAttachments() + { + return $this->soapAttachments !== null && count($this->soapAttachments) > 0; + } + + public function getSoapAttachments() + { + return $this->soapAttachments; + } + + public function getSoapAttachmentIds() + { + $ids = []; + foreach ($this->getSoapAttachments() as $soapAttachment) { + $ids[] = $soapAttachment->getId(); + } + + return $ids; + } +} diff --git a/src/BeSimple/SoapBundle/WebServiceContext.php b/src/BeSimple/SoapBundle/WebServiceContext.php index 875659e..20c0dde 100644 --- a/src/BeSimple/SoapBundle/WebServiceContext.php +++ b/src/BeSimple/SoapBundle/WebServiceContext.php @@ -13,6 +13,8 @@ namespace BeSimple\SoapBundle; use BeSimple\SoapBundle\ServiceBinding\ServiceBinder; use BeSimple\SoapCommon\Converter\TypeConverterCollection; +use BeSimple\SoapCommon\SoapOptionsBuilder; +use BeSimple\SoapServer\SoapServerOptionsBuilder; use BeSimple\SoapWsdl\Dumper\Dumper; use BeSimple\SoapServer\SoapServerBuilder; use Symfony\Component\Config\ConfigCache; @@ -102,15 +104,14 @@ class WebServiceContext public function getServerBuilder() { if (null === $this->serverBuilder) { - $this->serverBuilder = SoapServerBuilder::createWithDefaults() - ->withWsdl($this->getWsdlFile()) - ->withClassmap($this->getServiceDefinition()->getTypeRepository()->getClassmap()) - ->withTypeConverters($this->converters) - ; - - if (null !== $this->options['cache_type']) { - $this->serverBuilder->withWsdlCache($this->options['cache_type']); - } + $soapServerBuilder = new SoapServerBuilder(); + $this->serverBuilder = $soapServerBuilder->build( + SoapServerOptionsBuilder::createWithDefaults(), + SoapOptionsBuilder::createWithClassMap( + $this->getWsdlFile(), + $this->getServiceDefinition()->getTypeRepository()->getClassmap() + ) + ); } return $this->serverBuilder; diff --git a/src/BeSimple/SoapClient/Curl.php b/src/BeSimple/SoapClient/Curl.php deleted file mode 100644 index ef48e95..0000000 --- a/src/BeSimple/SoapClient/Curl.php +++ /dev/null @@ -1,336 +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; - -/** - * 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 Options array from SoapClient constructor - * @param int $followLocationMaxRedirects Redirection limit for Location header - */ - 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'])) { - if (false !== $options['proxy_host']) { - $proxyHost = $options['proxy_host'].(isset($options['proxy_port']) ? $options['proxy_port'] : 8080); - } else { - $proxyHost = false; - } - - curl_setopt($this->ch, CURLOPT_PROXY, $proxyHost); - - if (false !== $proxyHost && isset($options['proxy_login'])) { - curl_setopt($this->ch, CURLOPT_PROXYUSERPWD, $options['proxy_login'].':'.$options['proxy_password']); - - if (isset($options['proxy_auth'])) { - curl_setopt($this->ch, CURLOPT_PROXYAUTH, $options['proxy_auth']); - } - } - } - - if (isset($options['login'])) { - curl_setopt($this->ch, CURLOPT_HTTPAUTH, isset($options['extra_options']['http_auth']) ? $options['extra_options']['http_auth'] : 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['ca_info'])) { - curl_setopt($this->ch, CURLOPT_CAINFO, $options['ca_info']); - } - if (isset($options['ca_path'])) { - curl_setopt($this->ch, CURLOPT_CAPATH, $options['ca_path']); - } - } - - /** - * Destructor. - */ - public function __destruct() - { - curl_close($this->ch); - } - - /** - * Execute HTTP request. - * Returns true if request was successful. - * - * @param string $location HTTP location - * @param string $request Request body - * @param array $requestHeaders Request header strings - * @param array $requestOptions An array of request options - * - * @return bool - */ - public function exec($location, $request = null, $requestHeaders = array(), $requestOptions = 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); - } - - if (count($requestOptions) > 0) { - curl_setopt_array($this->ch, $requestOptions); - } - - $this->response = $this->execManualRedirect(); - - 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 int $redirects Current redirection count - * - * @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 - * - * @return 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)); - } -} diff --git a/src/BeSimple/SoapClient/Curl/Curl.php b/src/BeSimple/SoapClient/Curl/Curl.php new file mode 100644 index 0000000..ae283a2 --- /dev/null +++ b/src/BeSimple/SoapClient/Curl/Curl.php @@ -0,0 +1,234 @@ +curlSession = $this->acquireNewCurlSession($options); + $this->options = $options; + } + + public function __destruct() + { + $this->closeCurlSession($this->curlSession); + } + + /** + * @param string $location HTTP location + * @param string $request Request body + * @param array $requestHeaders Request header strings + * @return CurlResponse + */ + public function executeCurlWithCachedSession($location, $request = null, $requestHeaders = []) + { + return $this->executeCurlSession($this->curlSession, $this->options, $location, $request, $requestHeaders); + } + + /** + * @param CurlOptions $options + * @param string $location HTTP location + * @param string $request Request body + * @param array $requestHeaders Request header strings + * @return CurlResponse + */ + public function executeCurl(CurlOptions $options, $location, $request = null, $requestHeaders = []) + { + $curlSession = $this->acquireNewCurlSession($options); + $curlResponse = $this->executeCurlSession($curlSession, $options, $location, $request, $requestHeaders); + $this->closeCurlSession($curlSession); + + return $curlResponse; + } + + private function acquireNewCurlSession(CurlOptions $options) + { + $curlSession = curl_init(); + curl_setopt_array($curlSession, [ + CURLOPT_ENCODING => '', + CURLOPT_SSL_VERIFYPEER => false, + CURLOPT_FAILONERROR => true, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, + CURLOPT_HEADER => true, + CURLOPT_USERAGENT => $options->getUserAgent(), + CURLINFO_HEADER_OUT => true, + CURLOPT_CONNECTTIMEOUT => $options->getConnectionTimeout() + ]); + + return $curlSession; + } + + private function closeCurlSession($curlSession) + { + curl_close($curlSession); + } + + /** + * @param mixed $curlSession Result of curl_init() handle + * @param CurlOptions $options + * @param string $location HTTP location + * @param string $request Request body + * @param array $requestHeaders Request header strings + * @return CurlResponse + */ + private function executeCurlSession($curlSession, CurlOptions $options, $location, $request = null, $requestHeaders = []) + { + curl_setopt($curlSession, CURLOPT_URL, $location); + curl_setopt($curlSession, CURLOPT_HEADER, true); + curl_setopt($curlSession, CURLOPT_RETURNTRANSFER, true); + if (!is_null($request)) { + curl_setopt($curlSession, CURLOPT_POST, true); + curl_setopt($curlSession, CURLOPT_POSTFIELDS, $request); + } else { + curl_setopt($curlSession, CURLOPT_POST, false); + } + if (count($requestHeaders) > 0) { + curl_setopt($curlSession, CURLOPT_HTTPHEADER, $requestHeaders); + } + if (!$options->getSoapCompression() & SOAP_COMPRESSION_ACCEPT) { + curl_setopt($curlSession, CURLOPT_ENCODING, 'identity'); + } + if ($options->hasProxy()) { + $proxyHost = $options->getProxy()->getHost() . $options->getProxy()->getPort(); + curl_setopt($curlSession, CURLOPT_PROXY, $proxyHost); + if ($options->getProxy()->hasCredentials()) { + curl_setopt($curlSession, CURLOPT_PROXYUSERPWD, $options->getProxy()->getLogin() . ':' . $options->getProxy()->getPassword()); + if ($options->getProxy()->hasAuthenticationType()) { + curl_setopt($curlSession, CURLOPT_PROXYAUTH, $options->getProxy()->getAuthenticationType()); + } + } + } + if ($options->hasHttpAuthentication()) { + if ($options->hasHttpAuthenticationBasic()) { + /** @var HttpAuthenticationBasicOptions $httpAuthenticationBasic */ + $httpAuthenticationBasic = $options->getHttpAuthentication(); + curl_setopt($curlSession, CURLOPT_HTTPAUTH, $httpAuthenticationBasic->getAuthenticationType()); + curl_setopt($curlSession, CURLOPT_USERPWD, $httpAuthenticationBasic->getUsername() . ':' . $httpAuthenticationBasic->getPassword()); + } else if ($options->hasHttpAuthenticationDigest()) { + /** @var HttpAuthenticationDigestOptions $httpAuthenticationDigest */ + $httpAuthenticationDigest = $options->getHttpAuthentication(); + curl_setopt($curlSession, CURLOPT_HTTPAUTH, $httpAuthenticationDigest->getAuthenticationType()); + } else { + throw new Exception('Unresolved authentication type: '.get_class($options->getHttpAuthentication())); + } + } + if ($options->hasSslCertificateOptions()) { + $sslCertificateOptions = $options->getSslCertificateOptions(); + curl_setopt($curlSession, CURLOPT_SSLCERT, $sslCertificateOptions->getCertificateLocalPath()); + if ($sslCertificateOptions->hasCertificatePassPhrase()) { + curl_setopt($curlSession, CURLOPT_SSLCERTPASSWD, $sslCertificateOptions->getCertificatePassPhrase()); + } + if ($sslCertificateOptions->hasCertificateAuthorityInfo()) { + curl_setopt($curlSession, CURLOPT_CAINFO, $sslCertificateOptions->getCertificateAuthorityInfo()); + } + if ($sslCertificateOptions->hasCertificateAuthorityPath()) { + curl_setopt($curlSession, CURLOPT_CAPATH, $sslCertificateOptions->hasCertificateAuthorityPath()); + } + } + $executeSoapCallResponse = $this->executeHttpCall($curlSession, $options); + + $httpRequestHeadersAsString = curl_getinfo($curlSession, CURLINFO_HEADER_OUT); + $headerSize = curl_getinfo($curlSession, CURLINFO_HEADER_SIZE); + $httpResponseCode = curl_getinfo($curlSession, CURLINFO_HTTP_CODE); + $httpResponseContentType = curl_getinfo($curlSession, CURLINFO_CONTENT_TYPE);; + preg_match('/HTTP\/(1\.[0-1]+) ([0-9]{3}) (.*)/', $executeSoapCallResponse, $httpResponseMessages); + $httpResponseMessage = trim(array_pop($httpResponseMessages)); + $curlErrorMessage = sprintf( + 'Curl error "%s" with message: %s occurred while connecting to %s', + curl_errno($curlSession), + curl_error($curlSession), + $location + ); + + if ($executeSoapCallResponse === false) { + + return new CurlResponse( + $httpRequestHeadersAsString, + $httpResponseCode, + $httpResponseMessage, + $httpResponseContentType, + self::CURL_FAILED, + $curlErrorMessage + ); + } + + return new CurlResponse( + $httpRequestHeadersAsString, + $httpResponseCode, + $httpResponseMessage, + $httpResponseContentType, + self::CURL_SUCCESS, + null, + substr($executeSoapCallResponse, 0, $headerSize), + substr($executeSoapCallResponse, $headerSize) + ); + } + + /** + * Custom curl_exec wrapper that allows to follow redirects when specific + * http response code is set. SOAP only allows 307. + * + * @param mixed $curlSession Result of curl_init() handle + * @param CurlOptions $options + * @param int $executedRedirects + * @return string|null + * @throws Exception + */ + private function executeHttpCall($curlSession, CurlOptions $options, $executedRedirects = 0) + { + if ($executedRedirects > $options->getFollowLocationMaxRedirects()) { + throw new Exception('Cannot executeHttpCall - too many redirects: ' . $executedRedirects); + } + $curlExecResponse = curl_exec($curlSession); + $httpResponseCode = curl_getinfo($curlSession, CURLINFO_HTTP_CODE); + if ($httpResponseCode === 307) { + $newUrl = $this->getRedirectUrlFromResponseHeaders($curlSession, $curlExecResponse); + curl_setopt($curlSession, CURLOPT_URL, $newUrl); + + return $this->executeHttpCall($curlSession, $options, ++$executedRedirects); + } + + return $curlExecResponse; + } + + private function getRedirectUrlFromResponseHeaders($curlSession, $curlExecResponse) + { + $curlExecResponseHeaders = substr($curlExecResponse, 0, curl_getinfo($curlSession, CURLINFO_HEADER_SIZE)); + $matches = []; + preg_match('/Location:(.*?)\n/', $curlExecResponseHeaders, $matches); + $url = trim(array_pop($matches)); + + if (($url = @parse_url($url)) !== false) { + $lastUrl = parse_url(curl_getinfo($curlSession, 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']; + } + + return $url['scheme'] . '://' . $url['host'] . $url['path'] . ($url['query'] ? '?' . $url['query'] : ''); + } + + throw new Exception('Cannot parse WSDL url redirect: ' . $url); + } +} diff --git a/src/BeSimple/SoapClient/Curl/CurlOptions.php b/src/BeSimple/SoapClient/Curl/CurlOptions.php new file mode 100644 index 0000000..60ef439 --- /dev/null +++ b/src/BeSimple/SoapClient/Curl/CurlOptions.php @@ -0,0 +1,126 @@ +userAgent = $userAgent; + $this->followLocationMaxRedirects = $followLocationMaxRedirects; + $this->soapCompression = $soapCompression; + $this->connectionTimeout = $connectionTimeout; + $this->proxy = $proxy; + $this->httpAuthentication = $httpAuthentication; + $this->sslCertificateOptions = $sslCertificateOptions; + } + + public function getUserAgent() + { + return $this->userAgent; + } + + public function getFollowLocationMaxRedirects() + { + return $this->followLocationMaxRedirects; + } + + public function getSoapCompression() + { + return $this->soapCompression; + } + + public function getConnectionTimeout() + { + return $this->connectionTimeout; + } + + public function getProxy() + { + return $this->proxy; + } + + public function getHttpAuthentication() + { + return $this->httpAuthentication; + } + + public function getSslCertificateOptions() + { + return $this->sslCertificateOptions; + } + + public function hasProxy() + { + return $this->proxy !== null; + } + + public function hasHttpAuthentication() + { + return $this->httpAuthentication !== null; + } + + public function hasSslCertificateOptions() + { + return $this->sslCertificateOptions !== null; + } + + public function hasHttpAuthenticationBasic() + { + if ($this->hasHttpAuthentication()) { + if ($this->getHttpAuthentication() instanceof HttpAuthenticationBasicOptions) { + + return true; + } + } + + return false; + } + + public function hasHttpAuthenticationDigest() + { + if ($this->hasHttpAuthentication()) { + if ($this->getHttpAuthentication() instanceof HttpAuthenticationDigestOptions) { + + return true; + } + } + + return false; + } +} diff --git a/src/BeSimple/SoapClient/Curl/CurlOptionsBuilder.php b/src/BeSimple/SoapClient/Curl/CurlOptionsBuilder.php new file mode 100644 index 0000000..ec49d17 --- /dev/null +++ b/src/BeSimple/SoapClient/Curl/CurlOptionsBuilder.php @@ -0,0 +1,79 @@ +getUserAgent(), + self::DEFAULT_MAX_REDIRECTS, + $soapClientOptions->getCompression(), + self::DEFAULT_CONNECTION_TIMEOUT, + $soapClientOptions->getProxy(), + self::getHttpAuthOptions($soapClientOptions), + self::getSslCertificateOptions($soapClientOptions) + ); + } + + private static function getHttpAuthOptions(SoapClientOptions $soapClientOptions) + { + if ($soapClientOptions->hasAuthentication()) { + if ($soapClientOptions->hasAuthenticationBasic()) { + /** @var SoapServerAuthenticationBasic $basicAuthentication */ + $basicAuthentication = $soapClientOptions->getAuthentication(); + + return new HttpAuthenticationBasicOptions( + $basicAuthentication->getLogin(), + $basicAuthentication->getPassword() + ); + + } else if ($soapClientOptions->hasAuthenticationDigest()) { + + return new HttpAuthenticationDigestOptions(); + + } else { + throw new Exception('Unresolved authentication type: '.get_class($soapClientOptions->getAuthentication())); + } + } + + return null; + } + + private static function getSslCertificateOptions(SoapClientOptions $soapClientOptions) + { + if ($soapClientOptions->hasAuthenticationDigest()) { + /** @var SoapServerAuthenticationDigest $digestAuthentication */ + $digestAuthentication = $soapClientOptions->getAuthentication(); + + return new SslCertificateOptions( + $digestAuthentication->getLocalCert(), + $digestAuthentication->getPassPhrase() + ); + } + + return null; + } +} diff --git a/src/BeSimple/SoapClient/Curl/CurlResponse.php b/src/BeSimple/SoapClient/Curl/CurlResponse.php new file mode 100644 index 0000000..417b873 --- /dev/null +++ b/src/BeSimple/SoapClient/Curl/CurlResponse.php @@ -0,0 +1,100 @@ +httpRequestHeaders = $httpRequestHeaders; + $this->httpResponseStatusCode = $httpResponseStatusCode; + $this->httpResponseStatusMessage = $httpResponseStatusMessage; + $this->httpResponseContentType = $httpResponseContentType; + $this->curlStatus = $curlStatus; + $this->curlErrorMessage = $curlErrorMessage; + $this->responseHeader = $responseHeader; + $this->responseBody = $responseBody; + } + + public function getHttpRequestHeaders() + { + return $this->httpRequestHeaders; + } + + public function getHttpResponseStatusCode() + { + return $this->httpResponseStatusCode; + } + + public function getHttpResponseStatusMessage() + { + return $this->httpResponseStatusMessage; + } + + public function getHttpResponseContentType() + { + return $this->httpResponseContentType; + } + + public function getCurlStatus() + { + return $this->curlStatus; + } + + public function curlStatusSuccess() + { + return $this->curlStatus === Curl::CURL_SUCCESS; + } + + public function curlStatusFailed() + { + return $this->curlStatus === Curl::CURL_FAILED; + } + + public function hasCurlErrorMessage() + { + return $this->curlErrorMessage !== null; + } + + public function getCurlErrorMessage() + { + return $this->curlErrorMessage; + } + + public function hasResponseHeader() + { + return $this->responseHeader !== null; + } + + public function getResponseHeader() + { + return $this->responseHeader; + } + + public function hasResponseBody() + { + return $this->responseBody !== null; + } + + public function getResponseBody() + { + return $this->responseBody; + } +} diff --git a/src/BeSimple/SoapClient/Curl/Http/HttpAuthenticationBasicOptions.php b/src/BeSimple/SoapClient/Curl/Http/HttpAuthenticationBasicOptions.php new file mode 100644 index 0000000..9eac23e --- /dev/null +++ b/src/BeSimple/SoapClient/Curl/Http/HttpAuthenticationBasicOptions.php @@ -0,0 +1,34 @@ +username = $username; + $this->password = $password; + } + + public function getUsername() + { + return $this->username; + } + + public function getPassword() + { + return $this->password; + } + + public function getAuthenticationType() + { + return HttpAuthenticationInterface::AUTHENTICATION_TYPE_BASIC; + } +} diff --git a/src/BeSimple/SoapClient/Curl/Http/HttpAuthenticationDigestOptions.php b/src/BeSimple/SoapClient/Curl/Http/HttpAuthenticationDigestOptions.php new file mode 100644 index 0000000..bdf04df --- /dev/null +++ b/src/BeSimple/SoapClient/Curl/Http/HttpAuthenticationDigestOptions.php @@ -0,0 +1,11 @@ +certificateLocalPath = $certificateLocalPath; + $this->certificatePassPhrase = $certificatePassPhrase; + $this->certificateAuthorityInfo = $certificateAuthorityInfo; + $this->certificateAuthorityPath = $certificateAuthorityPath; + } + + public function getCertificateLocalPath() + { + return $this->certificateLocalPath; + } + + public function getCertificatePassPhrase() + { + return $this->certificatePassPhrase; + } + + public function getCertificateAuthorityInfo() + { + return $this->certificateAuthorityInfo; + } + + public function getCertificateAuthorityPath() + { + return $this->certificateAuthorityPath; + } + + public function hasCertificatePassPhrase() + { + return $this->certificatePassPhrase !== null; + } + + public function hasCertificateAuthorityInfo() + { + return $this->certificateAuthorityInfo !== null; + } + + public function hasCertificateAuthorityPath() + { + return $this->certificateAuthorityPath !== null; + } +} diff --git a/src/BeSimple/SoapClient/MimeFilter.php b/src/BeSimple/SoapClient/MimeFilter.php index 898e48f..609ff3e 100644 --- a/src/BeSimple/SoapClient/MimeFilter.php +++ b/src/BeSimple/SoapClient/MimeFilter.php @@ -19,7 +19,7 @@ use BeSimple\SoapCommon\Mime\Part as MimePart; use BeSimple\SoapCommon\Mime\Part; use BeSimple\SoapCommon\SoapRequest; use BeSimple\SoapCommon\SoapRequestFilter; -use BeSimple\SoapCommon\SoapResponse; +use BeSimple\SoapCommon\SoapResponse as CommonSoapResponse; use BeSimple\SoapCommon\SoapResponseFilter; /** @@ -62,7 +62,7 @@ class MimeFilter implements SoapRequestFilter, SoapResponseFilter return $request; } - public function filterResponse(SoapResponse $response, $attachmentType) + public function filterResponse(CommonSoapResponse $response, $attachmentType) { $multiPartMessage = MimeParser::parseMimeMessage( $response->getContent(), diff --git a/src/BeSimple/SoapClient/SoapClient.php b/src/BeSimple/SoapClient/SoapClient.php index 483c8e7..b5bc649 100644 --- a/src/BeSimple/SoapClient/SoapClient.php +++ b/src/BeSimple/SoapClient/SoapClient.php @@ -12,10 +12,20 @@ namespace BeSimple\SoapClient; +use BeSimple\SoapBundle\Soap\SoapAttachment; +use BeSimple\SoapBundle\Soap\SoapAttachmentList; +use BeSimple\SoapClient\Curl\Curl; +use BeSimple\SoapClient\Curl\CurlOptionsBuilder; +use BeSimple\SoapClient\Curl\CurlResponse; +use BeSimple\SoapClient\SoapOptions\SoapClientOptions; +use BeSimple\SoapCommon\Fault\SoapFaultEnum; +use BeSimple\SoapCommon\Mime\PartFactory; use BeSimple\SoapCommon\SoapKernel; use BeSimple\SoapCommon\SoapOptions\SoapOptions; use BeSimple\SoapCommon\SoapRequest; use BeSimple\SoapCommon\SoapRequestFactory; +use Exception; +use SoapFault; /** * Extended SoapClient that uses a a cURL wrapper for all underlying HTTP @@ -24,73 +34,92 @@ use BeSimple\SoapCommon\SoapRequestFactory; * allows caching of all remote referenced items. * * @author Andreas Schamberger + * @author Petr Bechyně */ class SoapClient extends \SoapClient { - protected $soapVersion; - protected $tracingEnabled; protected $soapClientOptions; protected $soapOptions; - protected $curl; + private $curl; - /** - * 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 = ''; - - /** - * Constructor. - * - * @param SoapClientOptions $soapClientOptions - * @param SoapOptions $soapOptions - */ public function __construct(SoapClientOptions $soapClientOptions, SoapOptions $soapOptions) { - $this->soapVersion = $soapOptions->getSoapVersion(); - $this->tracingEnabled = $soapClientOptions->getTrace(); $this->soapClientOptions = $soapClientOptions; $this->soapOptions = $soapOptions; - - // @todo: refactor SoapClient: do not use $options as array: refactor Curl - $this->curl = new Curl($soapClientOptions->toArray()); - - $wsdlFile = $this->loadWsdl( - $soapOptions->getWsdlFile(), - $soapOptions->getWsdlCacheType() + $this->curl = new Curl( + CurlOptionsBuilder::buildForSoapClient($soapClientOptions) ); - parent::__construct( - $wsdlFile, - $soapClientOptions->toArray() + $soapOptions->toArray() + try { + @parent::__construct( + $this->loadWsdl( + $this->curl, + $soapOptions->getWsdlFile(), + $soapOptions->getWsdlCacheType() + ), + $soapClientOptions->toArray() + $soapOptions->toArray() + ); + } catch (Exception $e) { + throw new SoapFault( + SoapFaultEnum::SOAP_FAULT_SOAP_CLIENT_ERROR, + 'Could not create SoapClient instance with message: '.$e->getMessage() + ); + } + } + + /** + * Avoid using __call directly, it's deprecated even in \SoapClient. + * + * @deprecated + */ + public function __call($function_name, $arguments) + { + throw new Exception( + 'The __call method is deprecated. Use __soapCall/soapCall instead.' ); } /** - * Custom request method to be able to modify the SOAP messages. - * $oneWay parameter is not used at the moment. + * Using __soapCall returns only response string, use soapCall instead. + * + * @param string $functionName + * @param array $arguments + * @param array|null $options + * @param null $inputHeaders + * @param array|null $outputHeaders + * @return string + */ + public function __soapCall($functionName, $arguments, $options = null, $inputHeaders = null, &$outputHeaders = null) + { + return $this->soapCall($functionName, $arguments, $options, $inputHeaders, $outputHeaders)->getContent(); + } + + /** + * @param string $functionName + * @param array $arguments + * @param array|null $options + * @param SoapAttachment[] $soapAttachments + * @param null $inputHeaders + * @param array|null $outputHeaders + * @return SoapResponse + */ + public function soapCall($functionName, array $arguments, array $soapAttachments = [], array $options = null, $inputHeaders = null, array &$outputHeaders = null) + { + ob_start(); + parent::__soapCall($functionName, $arguments, $options, $inputHeaders, $outputHeaders); + $nativeSoapClientRequest = $this->mapNativeDataJsonToDto(ob_get_clean()); + + return $this->performSoapRequest( + $nativeSoapClientRequest->request, + $nativeSoapClientRequest->location, + $nativeSoapClientRequest->action, + $nativeSoapClientRequest->version, + $soapAttachments + ); + } + + /** + * This is not performing any HTTP requests, but it is getting data from SoapClient that are needed for this Client * * @param string $request Request string * @param string $location Location @@ -102,44 +131,105 @@ class SoapClient extends \SoapClient */ public function __doRequest($request, $location, $action, $version, $oneWay = 0) { - $soapRequest = $this->createSoapRequest($location, $action, $version, $request); - $soapResponse = $this->getSoapResponseFromRequest($soapRequest); + $soapClientNativeDataTransferObject = new SoapClientNativeDataTransferObject( + $request, + $location, + $action, + $version + ); + echo json_encode($soapClientNativeDataTransferObject); - return $soapResponse->getContent(); - } - - private function createSoapRequest($location, $action, $version, $request) - { - $soapRequest = SoapRequestFactory::create($location, $action, $version, $request); - if ($this->soapOptions->hasAttachments()) { - $soapKernel = new SoapKernel(); - $soapRequest = $soapKernel->filterRequest( - $soapRequest, - $this->getAttachmentFilters(), - $this->soapOptions->getAttachmentType() - ); - } - - return $soapRequest; + return $request; } /** - * Runs the currently registered request filters on the request, performs - * the HTTP request and runs the response filters. + * Custom request method to be able to modify the SOAP messages. + * $oneWay parameter is not used at the moment. * - * @param SoapRequest $soapRequest SOAP request object + * @param mixed $request Request object + * @param string $location Location + * @param string $action SOAP action + * @param int $version SOAP version + * @param SoapAttachment[] $soapAttachments SOAP attachments array * * @return SoapResponse */ - private function getSoapResponseFromRequest(SoapRequest $soapRequest) + public function performSoapRequest($request, $location, $action, $version, array $soapAttachments = []) { - $soapResponse = $this->performHttpSoapRequest($soapRequest); - if ($this->soapOptions->hasAttachments()) { - $soapKernel = new SoapKernel(); - $soapKernel->filterResponse($soapResponse, $this->getAttachmentFilters(), $this->soapOptions->getAttachmentType()); + $soapRequest = $this->createSoapRequest($location, $action, $version, $request, $soapAttachments); + + return $this->performHttpSoapRequest($soapRequest); + } + + /** @deprecated */ + public function __getLastRequestHeaders() + { + $this->checkTracing(); + + throw new Exception( + 'The __getLastRequestHeaders method is now deprecated. Use callSoapRequest instead and get the tracing information from SoapResponseTracingData.' + ); + } + + /** @deprecated */ + public function __getLastRequest() + { + $this->checkTracing(); + + throw new Exception( + 'The __getLastRequest method is now deprecated. Use callSoapRequest instead and get the tracing information from SoapResponseTracingData.' + ); + } + + /** @deprecated */ + public function __getLastResponseHeaders() + { + $this->checkTracing(); + + throw new Exception( + 'The __getLastResponseHeaders method is now deprecated. Use callSoapRequest instead and get the tracing information from SoapResponseTracingData.' + ); + } + + /** @deprecated */ + public function __getLastResponse() + { + $this->checkTracing(); + + throw new Exception( + 'The __getLastResponse method is now deprecated. Use callSoapRequest instead and get the tracing information from SoapResponseTracingData.' + ); + } + + /** + * @param string $location Location + * @param string $action SOAP action + * @param int $version SOAP version + * @param string $request SOAP request body + * @param SoapAttachment[] $soapAttachments array of SOAP attachments + * + * @return SoapRequest + */ + private function createSoapRequest($location, $action, $version, $request, array $soapAttachments = []) + { + $soapAttachmentList = new SoapAttachmentList($soapAttachments); + $soapRequest = SoapRequestFactory::create($location, $action, $version, $request); + if (count($soapAttachments) > 0) { + if ($this->soapOptions->hasAttachments() === true) { + $soapRequest->setAttachments(PartFactory::createAttachmentParts($soapAttachments)); + $soapRequest = SoapKernel::filterRequest( + $soapRequest, + $this->getAttachmentFilters(), + $this->soapOptions->getAttachmentType() + ); + } else { + throw new Exception( + 'Non SWA SoapClient cannot handle SOAP action '.$action.' with attachments: '.implode(', ', $soapAttachmentList->getSoapAttachmentIds()) + ); + } } - return $soapResponse; + return $soapRequest; } /** @@ -147,147 +237,92 @@ class SoapClient extends \SoapClient * * @param SoapRequest $soapRequest SoapRequest object * @return SoapResponse + * @throws SoapFault */ private function performHttpSoapRequest(SoapRequest $soapRequest) { - // HTTP headers - $soapVersion = $soapRequest->getVersion(); - $soapAction = $soapRequest->getAction(); - if (SOAP_1_1 === $soapVersion) { + if ($soapRequest->getVersion() === SOAP_1_1) { $headers = [ 'Content-Type:' . $soapRequest->getContentType(), - 'SOAPAction: "' . $soapAction . '"', + 'SOAPAction: "' . $soapRequest->getAction() . '"', ]; } else { $headers = [ - 'Content-Type:' . $soapRequest->getContentType() . '; action="' . $soapAction . '"', + 'Content-Type:' . $soapRequest->getContentType() . '; action="' . $soapRequest->getAction() . '"', ]; } - - $location = $soapRequest->getLocation(); - $content = $soapRequest->getContent(); - - $headers = $this->filterRequestHeaders($soapRequest, $headers); - - $options = $this->filterRequestOptions($soapRequest); - - $responseSuccessful = $this->curl->exec( - $location, - $content, - $headers, - $options - ); - - // tracing enabled: store last request header and body - if ($this->tracingEnabled === true) { - $this->lastRequestHeaders = $this->curl->getRequestHeaders(); - $this->lastRequest = $soapRequest->getContent(); - } - // in case of an error while making the http request throw a soapFault - if ($responseSuccessful === false) { - // get error message from curl - $faultstring = $this->curl->getErrorMessage(); - 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(); - } - // wrap response data in SoapResponse object - $soapResponse = SoapResponse::create( - $this->curl->getResponseBody(), + $curlResponse = $this->curl->executeCurlWithCachedSession( $soapRequest->getLocation(), - $soapRequest->getAction(), - $soapRequest->getVersion(), - $this->curl->getResponseContentType() + $soapRequest->getContent(), + $headers + ); + $soapResponseTracingData = new SoapResponseTracingData( + $curlResponse->getHttpRequestHeaders(), + $soapRequest->getContent(), + $curlResponse->getResponseHeader(), + $curlResponse->getResponseBody() ); - return $soapResponse; + if ($curlResponse->curlStatusSuccess()) { + $soapResponse = $this->returnSoapResponseByTracing( + $this->soapClientOptions->getTrace(), + $soapRequest, + $curlResponse, + $soapResponseTracingData + ); + if ($this->soapOptions->hasAttachments()) { + + return SoapKernel::filterResponse( + $soapResponse, + $this->getAttachmentFilters(), + $this->soapOptions->getAttachmentType() + ); + + } else { + + return $soapResponse; + } + } else if ($curlResponse->curlStatusFailed()) { + + return $this->throwSoapFaultByTracing( + $this->soapClientOptions->getTrace(), + SoapFaultEnum::SOAP_FAULT_HTTP.'-'.$curlResponse->getHttpResponseStatusCode(), + $curlResponse->getCurlErrorMessage(), + $soapResponseTracingData + ); + } else { + + return $this->throwSoapFaultByTracing( + $this->soapClientOptions->getTrace(), + SoapFaultEnum::SOAP_FAULT_SOAP_CLIENT_ERROR, + 'Cannot process curl response with unresolved status: ' . $curlResponse->getCurlStatus(), + $soapResponseTracingData + ); + } } /** - * Filters HTTP headers which will be sent - * - * @param SoapRequest $soapRequest SOAP request object - * @param array $headers An array of HTTP headers - * - * @return array - */ - protected function filterRequestHeaders(SoapRequest $soapRequest, array $headers) - { - return $headers; - } - - /** - * Adds additional cURL options for the request - * - * @param SoapRequest $soapRequest SOAP request object - * - * @return array - */ - protected function filterRequestOptions(SoapRequest $soapRequest) - { - return []; - } - - /** - * 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; - } - - /** - * @param string $wsdl - * @param int $wsdlCache + * @param Curl $curl + * @param string $wsdlPath + * @param int $wsdlCacheType * @param bool $resolveRemoteIncludes * * @return string */ - private function loadWsdl($wsdl, $wsdlCache, $resolveRemoteIncludes = true) + private function loadWsdl(Curl $curl, $wsdlPath, $wsdlCacheType, $resolveRemoteIncludes = true) { - $wsdlDownloader = new WsdlDownloader($this->curl, $resolveRemoteIncludes, $wsdlCache); + + $wsdlDownloader = new WsdlDownloader($curl); try { - $cacheFileName = $wsdlDownloader->download($wsdl); - } catch (\RuntimeException $e) { - throw new \SoapFault('WSDL', "SOAP-ERROR: Parsing WSDL: Couldn't load from '" . $wsdl . "'"); + $loadedWsdlFilePath = $wsdlDownloader->getWsdlPath($curl, $wsdlPath, $wsdlCacheType, $resolveRemoteIncludes); + } catch (Exception $e) { + throw new SoapFault( + SoapFaultEnum::SOAP_FAULT_WSDL, + 'Unable to load WsdlPath: ' . $e->getMessage() + ); } - return $cacheFileName; + return $loadedWsdlFilePath; } private function getAttachmentFilters() @@ -302,4 +337,74 @@ class SoapClient extends \SoapClient return $filters; } + + private function mapNativeDataJsonToDto($nativeDataJson) + { + $nativeData = json_decode($nativeDataJson); + + return new SoapClientNativeDataTransferObject( + $nativeData->request, + $nativeData->location, + $nativeData->action, + $nativeData->version + ); + } + + private function returnSoapResponseByTracing( + $isTracingEnabled, + SoapRequest $soapRequest, + CurlResponse $curlResponse, + SoapResponseTracingData $soapResponseTracingData, + array $soapAttachments = [] + ) { + if ($isTracingEnabled === true) { + + return SoapResponseFactory::createWithTracingData( + $curlResponse->getResponseBody(), + $soapRequest->getLocation(), + $soapRequest->getAction(), + $soapRequest->getVersion(), + $curlResponse->getHttpResponseContentType(), + $soapResponseTracingData, + $soapAttachments + ); + + } else { + + return SoapResponseFactory::create( + $curlResponse->getResponseBody(), + $soapRequest->getLocation(), + $soapRequest->getAction(), + $soapRequest->getVersion(), + $curlResponse->getHttpResponseContentType(), + $soapAttachments + ); + } + } + + private function throwSoapFaultByTracing($isTracingEnabled, $soapFaultCode, $soapFaultMessage, SoapResponseTracingData $soapResponseTracingData) + { + if ($isTracingEnabled === true) { + + throw new SoapFaultWithTracingData( + $soapFaultCode, + $soapFaultMessage, + $soapResponseTracingData + ); + + } else { + + throw new SoapFault( + $soapFaultCode, + $soapFaultMessage + ); + } + } + + private function checkTracing() + { + if ($this->soapClientOptions->getTrace() === false) { + throw new Exception('SoapClientOptions tracing disabled, turn on trace attribute'); + } + } } diff --git a/src/BeSimple/SoapClient/SoapClientBuilder.php b/src/BeSimple/SoapClient/SoapClientBuilder.php index 00a406f..02e4b0a 100644 --- a/src/BeSimple/SoapClient/SoapClientBuilder.php +++ b/src/BeSimple/SoapClient/SoapClientBuilder.php @@ -12,22 +12,44 @@ namespace BeSimple\SoapClient; +use BeSimple\SoapBundle\Cache; +use BeSimple\SoapClient\SoapOptions\SoapClientOptions; use BeSimple\SoapCommon\SoapOptions\SoapOptions; +use Exception; +use SoapHeader; /** * Provides a SoapClient instance. * * @author Francis Besset * @author Christian Kerl - * @author Petr Bechyně + * @author Petr Bechyně */ class SoapClientBuilder { public function build(SoapClientOptions $soapClientOptions, SoapOptions $soapOptions) { + $cache = new Cache($soapOptions); + $cache->validateSettings($soapOptions); + return new SoapClient( $soapClientOptions, $soapOptions ); } + + public function buildWithSoapHeader( + SoapClientOptions $soapClientOptions, + SoapOptions $soapOptions, + SoapHeader $soapHeader + ) { + $soapClient = $this->build($soapClientOptions, $soapOptions); + if ($soapClient->__setSoapHeaders($soapHeader) === false) { + throw new Exception( + 'Could not set SoapHeader: '.var_export($soapHeader, true) + ); + } + + return $soapClient; + } } diff --git a/src/BeSimple/SoapClient/SoapClientMessageWithAttachments.php b/src/BeSimple/SoapClient/SoapClientMessageWithAttachments.php new file mode 100644 index 0000000..bffe5c4 --- /dev/null +++ b/src/BeSimple/SoapClient/SoapClientMessageWithAttachments.php @@ -0,0 +1,7 @@ +request = $request; + $this->location = $location; + $this->action = $action; + $this->version = $version; + } +} diff --git a/src/BeSimple/SoapClient/SoapClientOptionsBuilder.php b/src/BeSimple/SoapClient/SoapClientOptionsBuilder.php index ef22a2c..aa21b45 100644 --- a/src/BeSimple/SoapClient/SoapClientOptionsBuilder.php +++ b/src/BeSimple/SoapClient/SoapClientOptionsBuilder.php @@ -12,12 +12,16 @@ namespace BeSimple\SoapClient; +use BeSimple\SoapClient\Curl\CurlOptions; +use BeSimple\SoapClient\SoapOptions\SoapClientOptions; +use BeSimple\SoapClient\SoapServerAuthentication\SoapServerAuthenticationInterface; + /** * Provides a SoapClient instance. * * @author Francis Besset * @author Christian Kerl - * @author Petr Bechyně + * @author Petr Bechyně */ class SoapClientOptionsBuilder { @@ -26,8 +30,29 @@ class SoapClientOptionsBuilder return new SoapClientOptions( SoapClientOptions::SOAP_CLIENT_TRACE_OFF, SoapClientOptions::SOAP_CLIENT_EXCEPTIONS_ON, - 'BeSimpleSoap', + CurlOptions::DEFAULT_USER_AGENT, SoapClientOptions::SOAP_CLIENT_COMPRESSION_NONE ); } + + public static function createWithTracing() + { + return new SoapClientOptions( + SoapClientOptions::SOAP_CLIENT_TRACE_ON, + SoapClientOptions::SOAP_CLIENT_EXCEPTIONS_ON, + CurlOptions::DEFAULT_USER_AGENT, + SoapClientOptions::SOAP_CLIENT_COMPRESSION_NONE + ); + } + + public static function createWithAuthentication(SoapServerAuthenticationInterface $authentication) + { + return new SoapClientOptions( + SoapClientOptions::SOAP_CLIENT_TRACE_ON, + SoapClientOptions::SOAP_CLIENT_EXCEPTIONS_ON, + CurlOptions::DEFAULT_USER_AGENT, + SoapClientOptions::SOAP_CLIENT_COMPRESSION_NONE, + $authentication + ); + } } diff --git a/src/BeSimple/SoapClient/SoapFaultWithTracingData.php b/src/BeSimple/SoapClient/SoapFaultWithTracingData.php new file mode 100644 index 0000000..d62c762 --- /dev/null +++ b/src/BeSimple/SoapClient/SoapFaultWithTracingData.php @@ -0,0 +1,21 @@ +soapResponseTracingData = $soapResponseTracingData; + parent::__construct($code, $message); + } + + public function getSoapResponseTracingData() + { + return $this->soapResponseTracingData; + } +} diff --git a/src/BeSimple/SoapClient/SoapClientOptions.php b/src/BeSimple/SoapClient/SoapOptions/SoapClientOptions.php similarity index 77% rename from src/BeSimple/SoapClient/SoapClientOptions.php rename to src/BeSimple/SoapClient/SoapOptions/SoapClientOptions.php index 4f3a141..fc1e5db 100644 --- a/src/BeSimple/SoapClient/SoapClientOptions.php +++ b/src/BeSimple/SoapClient/SoapOptions/SoapClientOptions.php @@ -1,7 +1,10 @@ authentication !== null; } + public function hasAuthenticationBasic() + { + return $this->hasAuthentication() && $this->getAuthentication() instanceof SoapServerAuthenticationBasic; + } + + public function hasAuthenticationDigest() + { + return $this->hasAuthentication() && $this->getAuthentication() instanceof SoapServerAuthenticationDigest; + } + public function hasProxy() { return $this->proxy !== null; diff --git a/src/BeSimple/SoapClient/SoapResponse.php b/src/BeSimple/SoapClient/SoapResponse.php index cafee41..2a0e851 100644 --- a/src/BeSimple/SoapClient/SoapResponse.php +++ b/src/BeSimple/SoapClient/SoapResponse.php @@ -1,46 +1,13 @@ - * (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; -/** - * SoapResponse class for SoapClient. Provides factory function for response object. - * - * @author Andreas Schamberger - */ class SoapResponse extends CommonSoapResponse { - /** - * Factory function for SoapResponse. - * - * @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 SoapResponse - */ - public static function create($content, $location, $action, $version, $contentType) + public function getResponseContent() { - $response = new SoapResponse(); - $response->setContent($content); - $response->setLocation($location); - $response->setAction($action); - $response->setVersion($version); - $response->setContentType($contentType); - - return $response; + return $this->getContent(); } } diff --git a/src/BeSimple/SoapClient/SoapResponseFactory.php b/src/BeSimple/SoapClient/SoapResponseFactory.php new file mode 100644 index 0000000..d2caf3d --- /dev/null +++ b/src/BeSimple/SoapClient/SoapResponseFactory.php @@ -0,0 +1,98 @@ + + * (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\SoapBundle\Soap\SoapAttachment; +use BeSimple\SoapCommon\Mime\PartFactory; + +/** + * SoapResponseFactory for SoapClient. Provides factory function for SoapResponse object. + * + * @author Andreas Schamberger + * @author Petr Bechyně + */ +class SoapResponseFactory +{ + /** + * Factory method for SoapClient\SoapResponse. + * + * @param string $content Content + * @param string $location Location + * @param string $action SOAP action + * @param string $version SOAP version + * @param string $contentType Content type header + * @param SoapAttachment[] $attachments SOAP attachments + * @return SoapResponse + */ + public static function create( + $content, + $location, + $action, + $version, + $contentType, + array $attachments = [] + ) { + $response = new SoapResponse(); + $response->setContent($content); + $response->setLocation($location); + $response->setAction($action); + $response->setVersion($version); + $response->setContentType($contentType); + if (count($attachments) > 0) { + $response->setAttachments( + self::createAttachmentParts($attachments) + ); + } + + return $response; + } + + /** + * Factory method for SoapClient\SoapResponse with SoapResponseTracingData. + * + * @param string $content Content + * @param string $location Location + * @param string $action SOAP action + * @param string $version SOAP version + * @param string $contentType Content type header + * @param SoapResponseTracingData $tracingData Data value object suitable for tracing SOAP traffic + * @param SoapAttachment[] $attachments SOAP attachments + * @return SoapResponse + */ + public static function createWithTracingData( + $content, + $location, + $action, + $version, + $contentType, + SoapResponseTracingData $tracingData, + array $attachments = [] + ) { + $response = new SoapResponse(); + $response->setContent($content); + $response->setLocation($location); + $response->setAction($action); + $response->setVersion($version); + $response->setContentType($contentType); + if ($tracingData !== null) { + $response->setTracingData($tracingData); + } + if (count($attachments) > 0) { + $response->setAttachments( + PartFactory::createAttachmentParts($attachments) + ); + } + + return $response; + } +} diff --git a/src/BeSimple/SoapClient/SoapResponseTracingData.php b/src/BeSimple/SoapClient/SoapResponseTracingData.php new file mode 100644 index 0000000..c3f13ac --- /dev/null +++ b/src/BeSimple/SoapClient/SoapResponseTracingData.php @@ -0,0 +1,39 @@ +lastRequestHeaders = $lastRequestHeaders; + $this->lastRequest = $lastRequest; + $this->lastResponseHeaders = $lastResponseHeaders; + $this->lastResponse = $lastResponse; + } + + public function getLastRequestHeaders() + { + return $this->lastRequestHeaders; + } + + public function getLastRequest() + { + return $this->lastRequest; + } + + public function getLastResponseHeaders() + { + return $this->lastResponseHeaders; + } + + public function getLastResponse() + { + return $this->lastResponse; + } +} diff --git a/src/BeSimple/SoapClient/Tests/CurlTest.php b/src/BeSimple/SoapClient/Tests/CurlTest.php index 28b6311..be8a80f 100644 --- a/src/BeSimple/SoapClient/Tests/CurlTest.php +++ b/src/BeSimple/SoapClient/Tests/CurlTest.php @@ -12,7 +12,8 @@ namespace BeSimple\SoapClient\Tests; -use BeSimple\SoapClient\Curl; +use BeSimple\SoapClient\Curl\Curl; +use BeSimple\SoapClient\Curl\CurlOptionsBuilder; /** * @author Andreas Schamberger @@ -21,11 +22,12 @@ class CurlTest extends AbstractWebserverTest { public function testExec() { - $curl = new Curl(array( - 'proxy_host' => false, - )); + $curlOptions = CurlOptionsBuilder::buildDefault(); + $curl = new Curl( + $curlOptions + ); - $this->assertTrue($curl->exec(sprintf('http://localhost:%d/curl.txt', WEBSERVER_PORT))); + $this->assertTrue($curl->executeCurl($curlOptions, sprintf('http://localhost:%d/curl.txt', WEBSERVER_PORT))); $this->assertTrue($curl->exec(sprintf('http://localhost:%d/404.txt', WEBSERVER_PORT))); } @@ -81,43 +83,4 @@ class CurlTest extends AbstractWebserverTest $curl->exec(sprintf('http://localhost:%d/curl.txt', WEBSERVER_PORT)); $this->assertEquals('This is a testfile for cURL.', $curl->getResponseBody()); } - - public function testGetResponseContentType() - { - $curl = new Curl(array( - 'proxy_host' => false, - )); - - $curl->exec(sprintf('http://localhost:%d/curl.txt', WEBSERVER_PORT)); - $this->assertEquals('text/plain; charset=UTF-8', $curl->getResponseContentType()); - - $curl->exec(sprintf('http://localhost:%d/404.txt', WEBSERVER_PORT)); - $this->assertEquals('text/html; charset=UTF-8', $curl->getResponseContentType()); - } - - public function testGetResponseHeaders() - { - $curl = new Curl(array( - 'proxy_host' => false, - )); - - $curl->exec(sprintf('http://localhost:%d/curl.txt', WEBSERVER_PORT)); - $this->assertEquals(117 + self::$websererPortLength, strlen($curl->getResponseHeaders())); - - $curl->exec(sprintf('http://localhost:%d/404.txt', WEBSERVER_PORT)); - $this->assertEquals(124 + self::$websererPortLength, strlen($curl->getResponseHeaders())); - } - - public function testGetResponseStatusCode() - { - $curl = new Curl(array( - 'proxy_host' => false, - )); - - $curl->exec(sprintf('http://localhost:%d/curl.txt', WEBSERVER_PORT)); - $this->assertEquals(200, $curl->getResponseStatusCode()); - - $curl->exec(sprintf('http://localhost:%d/404.txt', WEBSERVER_PORT)); - $this->assertEquals(404, $curl->getResponseStatusCode()); - } } diff --git a/src/BeSimple/SoapClient/Tests/GenerateTestRequest.php b/src/BeSimple/SoapClient/Tests/GenerateTestRequest.php new file mode 100644 index 0000000..8a97bc2 --- /dev/null +++ b/src/BeSimple/SoapClient/Tests/GenerateTestRequest.php @@ -0,0 +1,8 @@ +soapCall('generateTest', [$generateTestRequest]); + +You can print the SoapFaultWithTracingData attributes in order to investigate the SoapClient request and request headers. \ No newline at end of file diff --git a/src/BeSimple/SoapClient/Tests/Mock/MockSwaService.example.response b/src/BeSimple/SoapClient/Tests/Mock/MockSwaService.example.response new file mode 100644 index 0000000..a96157d --- /dev/null +++ b/src/BeSimple/SoapClient/Tests/Mock/MockSwaService.example.response @@ -0,0 +1,18 @@ + +--Part_13_58a1b01a466a6.58a1b01a466e8 +Content-Type: application/soap+xml; charset=utf-8 +Content-Transfer-Encoding: 8bit +Content-ID: + + +generateTestReturndummy-attachment.txt + +--Part_13_58a1b01a466a6.58a1b01a466e8 +Content-Type: text/plain; charset=utf-8 +Content-Transfer-Encoding: 8bit +Content-ID: +Content-Location: dummy-attachment.txt + +Hello world! + +--Part_13_58a1b01a466a6.58a1b01a466e8-- diff --git a/src/BeSimple/SoapClient/Tests/Mock/MockSwaService.example.response.headers b/src/BeSimple/SoapClient/Tests/Mock/MockSwaService.example.response.headers new file mode 100644 index 0000000..f73280d --- /dev/null +++ b/src/BeSimple/SoapClient/Tests/Mock/MockSwaService.example.response.headers @@ -0,0 +1 @@ +multipart/related; type="application/soap+xml"; charset=utf-8; boundary=Part_13_58a1b01a466a6.58a1b01a466e8; start="" diff --git a/src/BeSimple/SoapClient/Tests/Mock/MockSwaService.wsdl b/src/BeSimple/SoapClient/Tests/Mock/MockSwaService.wsdl new file mode 100644 index 0000000..045b318 --- /dev/null +++ b/src/BeSimple/SoapClient/Tests/Mock/MockSwaService.wsdl @@ -0,0 +1,61 @@ + + + + + + + + + User name for authorization + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + WSDL file for TestGeneratorService + + + + + diff --git a/src/BeSimple/SoapClient/Tests/SoapClientBuilderTest.php b/src/BeSimple/SoapClient/Tests/SoapClientBuilderTest.php index 0fc68aa..c61e7d7 100644 --- a/src/BeSimple/SoapClient/Tests/SoapClientBuilderTest.php +++ b/src/BeSimple/SoapClient/Tests/SoapClientBuilderTest.php @@ -12,113 +12,286 @@ namespace BeSimple\SoapClient\Tests; +use BeSimple\SoapBundle\Soap\SoapAttachment; +use BeSimple\SoapClient\Curl\CurlOptions; use BeSimple\SoapClient\SoapClientBuilder; +use BeSimple\SoapClient\SoapClientOptionsBuilder; +use BeSimple\SoapClient\SoapFaultWithTracingData; +use BeSimple\SoapClient\SoapOptions\SoapClientOptions; +use BeSimple\SoapCommon\ClassMap; +use BeSimple\SoapCommon\SoapOptions\SoapOptions; +use BeSimple\SoapCommon\SoapOptionsBuilder; +use Exception; +use SoapHeader; class SoapClientBuilderTest extends \PHPUnit_Framework_TestCase { - private $defaultOptions = array( - 'features' => 0, - 'classmap' => array(), - 'typemap' => array(), - ); + const CACHE_DIR = __DIR__ . '/../../../../cache'; + const TEST_ENDPOINT_UK = 'http://www.webservicex.net/uklocation.asmx'; + const TEST_REMOTE_WSDL_UK = 'http://www.webservicex.net/uklocation.asmx?WSDL'; + const TEST_LOCAL_WSDL_UK = __DIR__.'/localWsdl.wsdl'; + const TEST_REMOTE_WSDL_NOT_WORKING = 'http://www.nosuchserverexist.tld/doesnotexist.endpoint?wsdl'; + const TEST_ENDPOINT_SWA = 'https://demo2815480.mockable.io/soap/testGenerator'; + const TEST_REMOTE_WSDL_SWA = 'https://demo2815480.mockable.io/soap/testGenerator?WSDL'; - public function testContruct() + public function testSoapOptionsCreateWithDefaults() { - $options = $this - ->getSoapBuilder() - ->getSoapOptions() - ; + $defaultOptions = SoapOptionsBuilder::createWithDefaults(self::TEST_LOCAL_WSDL_UK); - $this->assertEquals($this->mergeOptions(array()), $options); + self::assertInstanceOf(SoapOptions::class, $defaultOptions); + self::assertEquals(self::TEST_LOCAL_WSDL_UK, $defaultOptions->getWsdlFile()); } - public function testWithTrace() + public function testSoapClientOptionsCreateWithDefaults() { - $builder = $this->getSoapBuilder(); + $defaultOptions = SoapClientOptionsBuilder::createWithDefaults(); - $builder->withTrace(); - $this->assertEquals($this->mergeOptions(array('trace' => true)), $builder->getSoapOptions()); - - $builder->withTrace(false); - $this->assertEquals($this->mergeOptions(array('trace' => false)), $builder->getSoapOptions()); + self::assertInstanceOf(SoapClientOptions::class, $defaultOptions); + self::assertEquals(CurlOptions::DEFAULT_USER_AGENT, $defaultOptions->getUserAgent()); } - public function testWithExceptions() + public function testConstructSoapClientWithDefaults() { - $builder = $this->getSoapBuilder(); + $soapClient = $this->getSoapBuilder()->build( + SoapClientOptionsBuilder::createWithDefaults(), + SoapOptionsBuilder::createWithDefaults(self::TEST_REMOTE_WSDL_UK) + ); - $builder->withExceptions(); - $this->assertEquals($this->mergeOptions(array('exceptions' => true)), $builder->getSoapOptions()); - - $builder->withExceptions(false); - $this->assertEquals($this->mergeOptions(array('exceptions' => false)), $builder->getSoapOptions()); + self::assertInstanceOf(\SoapClient::class, $soapClient); } - public function testWithUserAgent() + public function testConstructSoapClientWithSwaAndClassMapAndCacheDisk() { - $builder = $this->getSoapBuilder(); + $soapOptions = SoapOptionsBuilder::createSwaWithClassMap( + self::TEST_REMOTE_WSDL_UK, + new ClassMap(), + SoapOptions::SOAP_CACHE_TYPE_DISK, + __DIR__.'/../../../../cache' + ); - $builder->withUserAgent('BeSimpleSoap Test'); - $this->assertEquals($this->mergeOptions(array('user_agent' => 'BeSimpleSoap Test')), $builder->getSoapOptions()); + $soapClient = $this->getSoapBuilder()->build( + SoapClientOptionsBuilder::createWithDefaults(), + $soapOptions + ); + + self::assertInstanceOf(\SoapClient::class, $soapClient); } - public function testWithCompression() + public function testConstructSoapClientWithDefaultsAndLocalWsdlFile() { - $builder = $this->getSoapBuilder(); + $soapClient = $this->getSoapBuilder()->build( + SoapClientOptionsBuilder::createWithDefaults(), + SoapOptionsBuilder::createWithDefaults(self::TEST_LOCAL_WSDL_UK) + ); - $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()); + self::assertInstanceOf(\SoapClient::class, $soapClient); } - public function testWithAuthentication() + public function testConstructSoapClientWithSwaAndClassMapAndCacheDiskAndLocalWsdlFile() { - $builder = $this->getSoapBuilder(); + $soapOptions = SoapOptionsBuilder::createSwaWithClassMap( + self::TEST_LOCAL_WSDL_UK, + new ClassMap(), + SoapOptions::SOAP_CACHE_TYPE_DISK, + __DIR__ .'/../../../../cache' + ); - $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()); + $soapClient = $this->getSoapBuilder()->build( + SoapClientOptionsBuilder::createWithDefaults(), + $soapOptions + ); - $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()); + self::assertInstanceOf(\SoapClient::class, $soapClient); } - public function testWithProxy() + public function testSoapCall() { - $builder = $this->getSoapBuilder(); + $soapClient = $this->getSoapBuilder()->build( + SoapClientOptionsBuilder::createWithDefaults(), + SoapOptionsBuilder::createWithDefaults(self::TEST_REMOTE_WSDL_UK) + ); + $getUKLocationByCountyRequest = new GetUKLocationByCounty(); + $getUKLocationByCountyRequest->County = 'London'; + $soapResponse = $soapClient->soapCall('GetUKLocationByCounty', [$getUKLocationByCountyRequest]); - $builder->withProxy('localhost', 8080); - $this->assertEquals($this->mergeOptions(array('proxy_host' => 'localhost', 'proxy_port' => 8080)), $builder->getSoapOptions()); + self::assertContains('GetUKLocationByCountyResult', $soapResponse->getContent()); + self::assertContains('', $soapResponse->getContent()); + self::assertEquals(self::TEST_ENDPOINT_UK, $soapResponse->getLocation()); + } - $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 testSoapCallWithCacheEndpointDownShouldFail() + { + $this->setExpectedException(Exception::class, 'Could not write WSDL cache file: Download failed with message'); - $builder->withProxy('127.0.0.1', 8585, 'foo', 'bar', \CURLAUTH_BASIC); - $this->assertEquals($this->mergeOptions(array('proxy_host' => '127.0.0.1', 'proxy_port' => 8585, 'proxy_login' => 'foo', 'proxy_password' => 'bar', 'proxy_auth' => \CURLAUTH_BASIC)), $builder->getSoapOptions()); + $this->getSoapBuilder()->build( + SoapClientOptionsBuilder::createWithDefaults(), + SoapOptionsBuilder::createWithDefaults( + self::TEST_REMOTE_WSDL_NOT_WORKING, + SoapOptions::SOAP_CACHE_TYPE_DISK, + __DIR__ .'/../../../../cache' + ) + ); + } - $builder->withProxy('127.0.0.1', 8585, 'foo', 'bar', \CURLAUTH_NTLM); - $this->assertEquals($this->mergeOptions(array('proxy_host' => '127.0.0.1', 'proxy_port' => 8585, 'proxy_login' => 'foo', 'proxy_password' => 'bar', 'proxy_auth' => \CURLAUTH_NTLM)), $builder->getSoapOptions()); + public function testSoapCallEndpointDownShouldFail() + { + $this->setExpectedException(Exception::class, 'Parsing WSDL: Couldn\'t load from'); + + $this->getSoapBuilder()->build( + SoapClientOptionsBuilder::createWithDefaults(), + SoapOptionsBuilder::createWithDefaults(self::TEST_REMOTE_WSDL_NOT_WORKING) + ); + } + + public function testSoapCallNoSwaWithAttachmentMustFail() + { + $this->setExpectedException(Exception::class, 'Non SWA SoapClient cannot handle SOAP action'); + + $soapClient = $this->getSoapBuilder()->build( + SoapClientOptionsBuilder::createWithDefaults(), + SoapOptionsBuilder::createWithDefaults(self::TEST_REMOTE_WSDL_UK) + ); + $getUKLocationByCountyRequest = new GetUKLocationByCounty(); + $getUKLocationByCountyRequest->County = 'London'; + + $soapClient->soapCall( + 'GetUKLocationByCounty', + [$getUKLocationByCountyRequest], + [ + new SoapAttachment( + 'first-file.txt', + 'text/plain', + 'unexpected file - no SWA - must fail' + ), + ] + ); + } + + public function testSoapCallSwaWithTwoAttachments() + { + $soapClient = $this->getSoapBuilder()->build( + SoapClientOptionsBuilder::createWithTracing(), + SoapOptionsBuilder::createSwaWithClassMap( + self::TEST_REMOTE_WSDL_UK, + new ClassMap(), + SoapOptions::SOAP_CACHE_TYPE_DISK, + __DIR__ .'/../../../../cache' + ) + ); + $getUKLocationByCountyRequest = new GetUKLocationByCounty(); + $getUKLocationByCountyRequest->County = 'London'; try { - $builder->withProxy('127.0.0.1', 8585, 'foo', 'bar', -100); - - $this->fail('An expected exception has not been raised.'); - } catch (\Exception $e) { - $this->assertInstanceOf('InvalidArgumentException', $e); + $soapResponse = $soapClient->soapCall( + 'GetUKLocationByCounty', + [$getUKLocationByCountyRequest], + [ + new SoapAttachment( + 'first-file.txt', + 'text/plain', + 'hello world' + ), + new SoapAttachment( + 'second-file.txt', + 'text/plain', + 'hello world' + ) + ] + ); + $tracingData = $soapResponse->getTracingData(); + } catch (SoapFaultWithTracingData $e) { + $tracingData = $e->getSoapResponseTracingData(); } + + self::assertEquals( + $this->getContentId($tracingData->getLastRequestHeaders()), + $this->getContentId($tracingData->getLastRequest()), + 'Content ID must match in request XML and Content-Type: ...; start header' + ); + self::assertEquals( + $this->getMultiPartBoundary($tracingData->getLastRequestHeaders()), + $this->getMultiPartBoundary($tracingData->getLastRequest()), + 'MultiPart boundary must match in request XML and Content-Type: ...; boundary header' + ); + self::assertContains('boundary=Part_', $tracingData->getLastRequestHeaders(), 'Headers should link to boundary'); + self::assertContains('start="getLastRequestHeaders(), 'Headers should link to first MultiPart'); + self::assertContains('action="', $tracingData->getLastRequestHeaders(), 'Headers should contain SOAP action'); + self::assertEquals( + $this->removeOneTimeData(file_get_contents(__DIR__.'/soapRequestWithTwoAttachments.request')), + $this->removeOneTimeData($tracingData->getLastRequest()), + 'Requests must match after onetime data were removed' + ); } - public function testCreateWithDefaults() + public function testSoapCallSwaWithNoAttachments() { - $builder = SoapClientBuilder::createClientWithDefaults(); + $soapClient = $this->getSoapBuilder()->build( + SoapClientOptionsBuilder::createWithTracing(), + SoapOptionsBuilder::createSwaWithClassMap( + self::TEST_REMOTE_WSDL_UK, + new ClassMap(), + SoapOptions::SOAP_CACHE_TYPE_DISK, + __DIR__ .'/../../../../cache' + ) + ); + $getUKLocationByCountyRequest = new GetUKLocationByCounty(); + $getUKLocationByCountyRequest->County = 'London'; - $this->assertInstanceOf('BeSimple\SoapClient\SoapClientBuilder', $builder); + try { + $soapResponse = $soapClient->soapCall( + 'GetUKLocationByCounty', + [$getUKLocationByCountyRequest] + ); + $tracingData = $soapResponse->getTracingData(); + } catch (SoapFaultWithTracingData $e) { + $tracingData = $e->getSoapResponseTracingData(); + } - $this->assertEquals($this->mergeOptions(array('soap_version' => SOAP_1_2, 'encoding' => 'UTF-8', 'features' => SOAP_SINGLE_ELEMENT_ARRAYS, 'user_agent' => 'BeSimpleSoap')), $builder->getSoapOptions()); + self::assertNotContains('boundary=Part_', $tracingData->getLastRequestHeaders(), 'Headers should link to boundary'); + self::assertNotContains('start="getLastRequestHeaders(), 'Headers should link to first MultiPart'); + self::assertContains('action="', $tracingData->getLastRequestHeaders(), 'Headers should contain SOAP action'); + self::assertEquals( + file_get_contents(__DIR__.'/soapRequestWithNoAttachments.request'), + $tracingData->getLastRequest(), + 'Requests must match' + ); + } + + /** + * @see This test needs a working SWA endpoint. Examine Tests/Mock directory for details + */ + public function testSoapCallSwaWithAttachmentsOnResponse() + { + $soapClient = $this->getSoapBuilder()->buildWithSoapHeader( + SoapClientOptionsBuilder::createWithTracing(), + SoapOptionsBuilder::createSwaWithClassMapV11( + self::TEST_REMOTE_WSDL_SWA, + new ClassMap([ + 'GenerateTestRequest' => GenerateTestRequest::class, + ]), + SoapOptions::SOAP_CACHE_TYPE_DISK, + __DIR__ . '/../../../../cache' + ), + new SoapHeader('http://schema.testcase', 'SoapHeader', [ + 'user' => 'admin', + ]) + ); + $generateTestRequest = new GenerateTestRequest(); + $generateTestRequest->salutation = 'World'; + + $soapResponse = $soapClient->soapCall('generateTest', [$generateTestRequest]); + $attachments = $soapResponse->getAttachments(); + + self::assertContains('', $soapResponse->getResponseContent()); + self::assertTrue($soapResponse->hasAttachments()); + self::assertCount(1, $attachments); + + $firstAttachment = reset($attachments); + + self::assertEquals('text/plain', $firstAttachment->getHeader('Content-Type')); + + file_put_contents(self::CACHE_DIR . '/testSoapCallSwaWithAttachmentsOnResponse.xml', $soapResponse->getContent()); + file_put_contents(self::CACHE_DIR . '/testSoapCallSwaWithAttachmentsOnResponse.txt', $firstAttachment->getContent()); } private function getSoapBuilder() @@ -126,8 +299,41 @@ class SoapClientBuilderTest extends \PHPUnit_Framework_TestCase return new SoapClientBuilder(); } - private function mergeOptions(array $options) + public function removeOneTimeData($string) { - return array_merge($this->defaultOptions, $options); + $contentId = $this->getContentId($string); + $multiPartBoundary = $this->getMultiPartBoundary($string); + + return str_replace( + $contentId, + '{content-id-placeholder}', + str_replace( + $multiPartBoundary, + '{multipart-boundary-placeholder}', + $string + ) + ); + } + + private function getMultiPartBoundary($string) + { + $realMultiParts = null; + preg_match('/Part\_[0-9]{2}\_[a-zA-Z0-9]{13}\.[a-zA-Z0-9]{13}/', $string, $realMultiParts); + if (count($realMultiParts) > 0) { + return $realMultiParts[0]; + } + + throw new Exception('Could not find real MultiPart boundary'); + } + + private function getContentId($string) + { + $realContentIds = null; + preg_match('/part\-[0-9a-f]{8}\-[0-9a-f]{4}\-[0-9a-f]{4}\-[0-9a-f]{4}\-[0-9a-f]{12}\@response\.info/', $string, $realContentIds); + if (count($realContentIds) > 0) { + return $realContentIds[0]; + } + + throw new Exception('Could not find real contentId'); } } diff --git a/src/BeSimple/SoapClient/Tests/localWsdl.wsdl b/src/BeSimple/SoapClient/Tests/localWsdl.wsdl new file mode 100644 index 0000000..e759048 --- /dev/null +++ b/src/BeSimple/SoapClient/Tests/localWsdl.wsdl @@ -0,0 +1,380 @@ + + + Get UK Postcode,Town,County and Validate UK Address + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Get UK town,Postcode and County by full /partial County + + + + + Get UK town,Postcode and County by full /partial Town + + + + + Get UK town,Postcode and County by Postcode(First Section of Post Code) + + + + + Validate UK address,Use First Section of Poscode for Postcode atribute + + + + + + + Get UK town,Postcode and County by full /partial County + + + + + Get UK town,Postcode and County by full /partial Town + + + + + Get UK town,Postcode and County by Postcode(First Section of Post Code) + + + + + Validate UK address,Use First Section of Poscode for Postcode atribute + + + + + + + Get UK town,Postcode and County by full /partial County + + + + + Get UK town,Postcode and County by full /partial Town + + + + + Get UK town,Postcode and County by Postcode(First Section of Post Code) + + + + + Validate UK address,Use First Section of Poscode for Postcode atribute + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Get UK Postcode,Town,County and Validate UK Address + + + + + + + + + + + + + + diff --git a/src/BeSimple/SoapClient/Tests/soapRequestWithNoAttachments.request b/src/BeSimple/SoapClient/Tests/soapRequestWithNoAttachments.request new file mode 100644 index 0000000..02ec2a1 --- /dev/null +++ b/src/BeSimple/SoapClient/Tests/soapRequestWithNoAttachments.request @@ -0,0 +1,2 @@ + +London diff --git a/src/BeSimple/SoapClient/Tests/soapRequestWithTwoAttachments.request b/src/BeSimple/SoapClient/Tests/soapRequestWithTwoAttachments.request new file mode 100644 index 0000000..4d83591 --- /dev/null +++ b/src/BeSimple/SoapClient/Tests/soapRequestWithTwoAttachments.request @@ -0,0 +1,24 @@ + +--Part_10_589b2dcf4f7fb.589b2dcf4f804 +Content-Type: application/soap+xml; charset=utf-8 +Content-Transfer-Encoding: 8bit +Content-ID: + + +London + +--Part_10_589b2dcf4f7fb.589b2dcf4f804 +Content-Type: application/pdf; charset=utf-8 +Content-Transfer-Encoding: binary +Content-ID: +Content-Location: first-file.txt + +hello world +--Part_10_589b2dcf4f7fb.589b2dcf4f804 +Content-Type: application/pdf; charset=utf-8 +Content-Transfer-Encoding: binary +Content-ID: +Content-Location: second-file.txt + +hello world +--Part_10_589b2dcf4f7fb.589b2dcf4f804-- \ No newline at end of file diff --git a/src/BeSimple/SoapClient/WsdlDownloader.php b/src/BeSimple/SoapClient/WsdlDownloader.php index 43253c2..562cb5b 100644 --- a/src/BeSimple/SoapClient/WsdlDownloader.php +++ b/src/BeSimple/SoapClient/WsdlDownloader.php @@ -12,8 +12,13 @@ namespace BeSimple\SoapClient; +use BeSimple\SoapClient\Curl\Curl; use BeSimple\SoapCommon\Cache; use BeSimple\SoapCommon\Helper; +use DOMDocument; +use DOMElement; +use DOMXPath; +use Exception; /** * Downloads WSDL files with cURL. Uses the WSDL_CACHE_* constants and the @@ -25,148 +30,179 @@ use BeSimple\SoapCommon\Helper; */ class WsdlDownloader { - protected $curl; - protected $resolveRemoteIncludes = true; - protected $cacheEnabled; - protected $cacheDir; - protected $cacheTtl; - /** * @param Curl $curl - * @param int $cacheWsdl = Cache::TYPE_NONE|Cache::WSDL_CACHE_DISK|Cache::WSDL_CACHE_BOTH|Cache::WSDL_CACHE_MEMORY + * @param string $wsdlPath WSDL file URL/path + * @param int $wsdCacheType = Cache::TYPE_NONE|Cache::WSDL_CACHE_DISK|Cache::WSDL_CACHE_BOTH|Cache::WSDL_CACHE_MEMORY * @param boolean $resolveRemoteIncludes - */ - public function __construct(Curl $curl, $cacheWsdl, $resolveRemoteIncludes = true) - { - $this->curl = $curl; - $this->resolveRemoteIncludes = $resolveRemoteIncludes; - - $this->cacheEnabled = $cacheWsdl === Cache::TYPE_NONE ? Cache::DISABLED : Cache::ENABLED == Cache::isEnabled(); - $this->cacheDir = Cache::getDirectory(); - $this->cacheTtl = Cache::getLifetime(); - } - - /** - * Download given WSDL file and return name of cache file. - * - * @param string $wsdl WSDL file URL/path - * * @return string */ - public function download($wsdl) + public function getWsdlPath(Curl $curl, $wsdlPath, $wsdCacheType, $resolveRemoteIncludes = true) { - // download and cache remote WSDL files or local ones where we want to - // resolve remote XSD includes - $isRemoteFile = $this->isRemoteFile($wsdl); - if ($isRemoteFile || $this->resolveRemoteIncludes) { - $cacheFilePath = $this->cacheDir.DIRECTORY_SEPARATOR.'wsdl_'.md5($wsdl).'.cache'; - - if (!$this->cacheEnabled || !file_exists($cacheFilePath) || (filemtime($cacheFilePath) + $this->cacheTtl) < time()) { - if ($isRemoteFile) { - // execute request - $responseSuccessfull = $this->curl->exec($wsdl); - // get content - if ($responseSuccessfull) { - $response = $this->curl->getResponseBody(); - - if ($this->resolveRemoteIncludes) { - $this->resolveRemoteIncludes($response, $cacheFilePath, $wsdl); - } else { - file_put_contents($cacheFilePath, $response); - } - } else { - throw new \ErrorException("SOAP-ERROR: Parsing WSDL: Couldn't load from '" . $wsdl ."'"); - } - } elseif (file_exists($wsdl)) { - $response = file_get_contents($wsdl); - $this->resolveRemoteIncludes($response, $cacheFilePath); - } else { - throw new \ErrorException("SOAP-ERROR: Parsing WSDL: Couldn't load from '" . $wsdl ."'"); + $isRemoteFile = $this->isRemoteFile($wsdlPath); + $isCacheEnabled = $wsdCacheType === Cache::TYPE_NONE ? false : Cache::isEnabled(); + if ($isCacheEnabled === true) { + $cacheFilePath = Cache::getDirectory().DIRECTORY_SEPARATOR.'wsdl_'.md5($wsdlPath).'.cache'; + $isCacheExisting = file_exists($cacheFilePath); + if ($isCacheExisting) { + $fileModificationTime = filemtime($cacheFilePath); + if ($fileModificationTime === false) { + throw new Exception('File modification time could not be get for wsdl path: ' . $cacheFilePath); } + $isCacheValid = ($fileModificationTime + Cache::getLifetime()) >= time(); + } else { + $isCacheExisting = $isCacheValid = false; + } + if ($isCacheExisting === false || $isCacheValid === false) { + $this->writeCacheFile($curl, $wsdCacheType, $wsdlPath, $cacheFilePath, $resolveRemoteIncludes, $isRemoteFile); } - return $cacheFilePath; - } elseif (file_exists($wsdl)) { - return realpath($wsdl); - } + return $this->getLocalWsdlPath($cacheFilePath); - throw new \ErrorException("SOAP-ERROR: Parsing WSDL: Couldn't load from '" . $wsdl ."'"); + } else { + + if ($isRemoteFile === true) { + return $wsdlPath; + } + + return $this->getLocalWsdlPath($wsdlPath); + } + } + + private function writeCacheFile(Curl $curl, $cacheType, $wsdlPath, $cacheFilePath, $resolveRemoteIncludes, $isRemoteFile) + { + if ($isRemoteFile === true) { + $curlResponse = $curl->executeCurlWithCachedSession($wsdlPath); + if ($curlResponse->curlStatusSuccess()) { + if (mb_strlen($curlResponse->getResponseBody()) === 0) { + throw new Exception('Could not write WSDL cache file: curl response empty'); + } + if ($resolveRemoteIncludes === true) { + $document = $this->getXmlFileDOMDocument($curl, $cacheType, $curlResponse->getResponseBody(), $wsdlPath); + $this->saveXmlDOMDocument($document, $cacheFilePath); + } else { + file_put_contents($cacheFilePath, $curlResponse->getResponseBody()); + } + } else { + throw new Exception('Could not write WSDL cache file: Download failed with message: '.$curlResponse->getCurlErrorMessage()); + } + } else { + if (file_exists($wsdlPath)) { + $document = $this->getXmlFileDOMDocument($curl, $cacheType, file_get_contents($wsdlPath)); + $this->saveXmlDOMDocument($document, $cacheFilePath); + } else { + throw new Exception('Could write WSDL cache file: local file does not exist: '.$wsdlPath); + } + } + } + + private function getLocalWsdlPath($wsdlPath) + { + if (file_exists($wsdlPath)) { + + return realpath($wsdlPath); + + } else { + throw new Exception('Could not download WSDL: local file does not exist: '.$wsdlPath); + } } /** - * Do we have a remote file? - * - * @param string $file File URL/path - * + * @param string $wsdlPath File URL/path * @return boolean */ - private function isRemoteFile($file) + private function isRemoteFile($wsdlPath) { - // @parse_url to suppress E_WARNING for invalid urls - if (false !== $url = @parse_url($file)) { - if (isset($url['scheme']) && 'http' === substr($url['scheme'], 0, 4)) { + $parsedUrlOrFalse = @parse_url($wsdlPath); + if ($parsedUrlOrFalse !== false) { + if (isset($parsedUrlOrFalse['scheme']) && substr($parsedUrlOrFalse['scheme'], 0, 4) === 'http') { + return true; + + } else { + + return false; } } - return false; + throw new Exception('Could not determine wsdlPath is remote: '.$wsdlPath); } /** * Resolves remote WSDL/XSD includes within the WSDL files. * - * @param string $xml XML file - * @param string $cacheFilePath Cache file name + * @param Curl $curl + * @param int $cacheType + * @param string $xmlFileSource XML file contents * @param boolean $parentFilePath Parent file name - * - * @return void + * @return DOMDocument */ - private function resolveRemoteIncludes($xml, $cacheFilePath, $parentFilePath = null) + private function getXmlFileDOMDocument(Curl $curl, $cacheType, $xmlFileSource, $parentFilePath = null) { - $doc = new \DOMDocument(); - $doc->loadXML($xml); + $document = new DOMDocument('1.0', 'utf-8'); + if ($document->loadXML($xmlFileSource) === false) { + throw new Exception('Could not save downloaded WSDL cache: '.$xmlFileSource); + } - $xpath = new \DOMXPath($doc); - $xpath->registerNamespace(Helper::PFX_XML_SCHEMA, Helper::NS_XML_SCHEMA); - $xpath->registerNamespace(Helper::PFX_WSDL, Helper::NS_WSDL); + $xpath = new DOMXPath($document); + $this->updateXmlDocument($curl, $cacheType, $xpath, Helper::PFX_WSDL, Helper::NS_WSDL, 'location', $parentFilePath); + $this->updateXmlDocument($curl, $cacheType, $xpath, Helper::PFX_XML_SCHEMA, Helper::NS_XML_SCHEMA, 'schemaLocation', $parentFilePath); - // WSDL include/import - $query = './/'.Helper::PFX_WSDL.':include | .//'.Helper::PFX_WSDL.':import'; - $nodes = $xpath->query($query); + return $document; + } + + private function saveXmlDOMDocument(DOMDocument $document, $cacheFilePath) + { + try { + $xmlContents = $document->saveXML(); + if ($xmlContents === '') { + throw new Exception('Could not write WSDL cache file: DOMDocument returned empty XML file'); + } + file_put_contents($cacheFilePath, $xmlContents); + } catch (Exception $e) { + unlink($cacheFilePath); + throw new Exception('Could not write WSDL cache file: save method returned error: ' . $e->getMessage()); + } + } + + private function updateXmlDocument( + Curl $curl, + $cacheType, + DOMXPath $xpath, + $schemaPrefix, + $schemaUrl, + $locationAttributeName, + $parentFilePath = null + ) { + $xpath->registerNamespace($schemaPrefix, $schemaUrl); + $nodes = $xpath->query('.//'.$schemaPrefix.':include | .//'.$schemaPrefix.':import'); if ($nodes->length > 0) { foreach ($nodes as $node) { - $location = $node->getAttribute('location'); - if ($this->isRemoteFile($location)) { - $location = $this->download($location); - $node->setAttribute('location', $location); - } elseif (null !== $parentFilePath) { - $location = $this->resolveRelativePathInUrl($parentFilePath, $location); - $location = $this->download($location); - $node->setAttribute('location', $location); + /** @var DOMElement $node */ + $locationPath = $node->getAttribute($locationAttributeName); + if ($this->isRemoteFile($locationPath)) { + $node->setAttribute( + $locationAttributeName, + $this->getWsdlPath( + $curl, + $locationPath, + $cacheType, + true + ) + ); + } else if ($parentFilePath !== null) { + $node->setAttribute( + $locationAttributeName, + $this->getWsdlPath( + $curl, + $this->resolveRelativePathInUrl($parentFilePath, $locationPath), + $cacheType, + true + ) + ); } } } - - // XML schema include/import - $query = './/'.Helper::PFX_XML_SCHEMA.':include | .//'.Helper::PFX_XML_SCHEMA.':import'; - $nodes = $xpath->query($query); - if ($nodes->length > 0) { - foreach ($nodes as $node) { - if ($node->hasAttribute('schemaLocation')) { - $schemaLocation = $node->getAttribute('schemaLocation'); - if ($this->isRemoteFile($schemaLocation)) { - $schemaLocation = $this->download($schemaLocation); - $node->setAttribute('schemaLocation', $schemaLocation); - } elseif (null !== $parentFilePath) { - $schemaLocation = $this->resolveRelativePathInUrl($parentFilePath, $schemaLocation); - $schemaLocation = $this->download($schemaLocation); - $node->setAttribute('schemaLocation', $schemaLocation); - } - } - } - } - - $doc->save($cacheFilePath); } /** @@ -205,9 +241,8 @@ class WsdlDownloader // resolve /../ foreach ($parts as $key => $part) { - if ('..' === $part) { + if ($part === '..') { $keyToDelete = $key - 1; - while ($keyToDelete > 0) { if (isset($parts[$keyToDelete])) { unset($parts[$keyToDelete]); diff --git a/src/BeSimple/SoapCommon/Cache.php b/src/BeSimple/SoapCommon/Cache.php index 1029fa6..39f2e60 100644 --- a/src/BeSimple/SoapCommon/Cache.php +++ b/src/BeSimple/SoapCommon/Cache.php @@ -12,6 +12,8 @@ namespace BeSimple\SoapCommon; +use InvalidArgumentException; + /** * @author Francis Besset */ @@ -32,22 +34,22 @@ class Cache self::TYPE_DISK_MEMORY, ]; - static public function getTypes() + public static function getTypes() { return self::$types; } - static public function hasType($cacheType) + public static function hasType($cacheType) { return in_array($cacheType, self::$types); } - static public function isEnabled() + public static function isEnabled() { - return self::iniGet('soap.wsdl_cache_enabled'); + return self::iniGet('soap.wsdl_cache_enabled') === '1'; } - static public function setEnabled($enabled) + public static function setEnabled($enabled) { if (!in_array($enabled, array(self::ENABLED, self::DISABLED), true)) { throw new \InvalidArgumentException(); @@ -56,60 +58,58 @@ class Cache self::iniSet('soap.wsdl_cache_enabled', $enabled); } - static public function getType() + public static function getType() { return self::iniGet('soap.wsdl_cache'); } - static public function setType($type) + public static function setType($type) { if (!in_array($type, self::getTypes(), true)) { - throw new \InvalidArgumentException('The cache type has to be either Cache::TYPE_NONE, Cache::TYPE_DISK, Cache::TYPE_MEMORY or Cache::TYPE_DISK_MEMORY'); + throw new InvalidArgumentException( + 'The cache type has to be either Cache::TYPE_NONE, Cache::TYPE_DISK, Cache::TYPE_MEMORY or Cache::TYPE_DISK_MEMORY' + ); } self::iniSet('soap.wsdl_cache', $type); } - static public function getDirectory() + public static function getDirectory() { return self::iniGet('soap.wsdl_cache_dir'); } - static public function setDirectory($directory) + public static function setDirectory($directory) { - if (!is_dir($directory)) { - mkdir($directory, 0777, true); - } - self::iniSet('soap.wsdl_cache_dir', $directory); } - static public function getLifetime() + public static function getLifetime() { return self::iniGet('soap.wsdl_cache_ttl'); } - static public function setLifetime($lifetime) + public static function setLifetime($lifetime) { self::iniSet('soap.wsdl_cache_ttl', $lifetime); } - static public function getLimit() + public static function getLimit() { return self::iniGet('soap.wsdl_cache_limit'); } - static public function setLimit($limit) + public static function setLimit($limit) { self::iniSet('soap.wsdl_cache_limit', $limit); } - static protected function iniGet($key) + protected static function iniGet($key) { return ini_get($key); } - static protected function iniSet($key, $value) + protected static function iniSet($key, $value) { ini_set($key, $value); } diff --git a/src/BeSimple/SoapCommon/Converter/MtomTypeConverter.php b/src/BeSimple/SoapCommon/Converter/MtomTypeConverter.php index f3bf76f..23cf184 100644 --- a/src/BeSimple/SoapCommon/Converter/MtomTypeConverter.php +++ b/src/BeSimple/SoapCommon/Converter/MtomTypeConverter.php @@ -22,11 +22,6 @@ use BeSimple\SoapCommon\Mime\Part as MimePart; */ class MtomTypeConverter implements TypeConverterInterface { - /** - * @var \BeSimple\SoapCommon\SoapKernel $soapKernel SoapKernel instance - */ - protected $soapKernel = null; - /** * {@inheritDoc} */ diff --git a/src/BeSimple/SoapCommon/Fault/SoapFaultEnum.php b/src/BeSimple/SoapCommon/Fault/SoapFaultEnum.php new file mode 100644 index 0000000..3cc630f --- /dev/null +++ b/src/BeSimple/SoapCommon/Fault/SoapFaultEnum.php @@ -0,0 +1,10 @@ +mainPartContentId; + } } \ No newline at end of file diff --git a/src/BeSimple/SoapCommon/Mime/Parser.php b/src/BeSimple/SoapCommon/Mime/Parser.php index 48f4f4c..63c949f 100644 --- a/src/BeSimple/SoapCommon/Mime/Parser.php +++ b/src/BeSimple/SoapCommon/Mime/Parser.php @@ -12,183 +12,241 @@ namespace BeSimple\SoapCommon\Mime; +use BeSimple\SoapCommon\Mime\Parser\ContentTypeParser; +use BeSimple\SoapCommon\Mime\Parser\ParsedPart; +use BeSimple\SoapCommon\Mime\Parser\ParsedPartList; +use Exception; + /** * Simple Multipart-Mime parser. * * @author Andreas Schamberger + * @author Petr Bechyne */ class Parser { + const HAS_HTTP_REQUEST_HEADERS = true; + const HAS_NO_HTTP_REQUEST_HEADERS = false; + /** * Parse the given Mime-Message and return a \BeSimple\SoapCommon\Mime\MultiPart object. * * @param string $mimeMessage Mime message string - * @param array(string=>string) $headers Array of header elements (e.g. coming from http request) + * @param string[] $headers array(string=>string) of header elements (e.g. coming from http request) * * @return \BeSimple\SoapCommon\Mime\MultiPart */ public static function parseMimeMessage($mimeMessage, array $headers = []) { - $boundary = null; - $start = null; - $multipart = new MultiPart(); - $hitFirstBoundary = false; - $inHeader = true; + $multiPart = new MultiPart(); + $mimeMessageLines = preg_split("/(\n)/", $mimeMessage); // add given headers, e.g. coming from HTTP headers if (count($headers) > 0) { - foreach ($headers as $name => $value) { - if ($name === 'Content-Type') { - self::parseContentTypeHeader($multipart, $name, $value); - $boundary = $multipart->getHeader('Content-Type', 'boundary'); - $start = $multipart->getHeader('Content-Type', 'start'); - } else { - $multipart->setHeader($name, $value); - } - } - $inHeader = false; + self::setMultiPartHeaders($multiPart, $headers); + $hasHttpRequestHeaders = self::HAS_HTTP_REQUEST_HEADERS; + } else { + $hasHttpRequestHeaders = self::HAS_NO_HTTP_REQUEST_HEADERS; } - $content = ''; - $currentPart = $multipart; - $lines = preg_split("/(\r\n)|(\n)/", $mimeMessage); - if (self::hasBoundary($lines)) { - foreach ($lines as $line) { - // ignore http status code and POST * - if (substr($line, 0, 5) == 'HTTP/' || substr($line, 0, 4) == 'POST') { + if (self::hasBoundary($mimeMessageLines)) { + $parsedPartList = self::getPartsFromMimeMessageLines( + $multiPart, + $mimeMessageLines, + $hasHttpRequestHeaders + ); + if ($parsedPartList->hasParts() === false) { + throw new Exception( + 'Could not parse MimeMessage: no Parts for MultiPart given' + ); + } + if ($parsedPartList->hasExactlyOneMainPart() === false) { + throw new Exception( + sprintf( + 'Could not parse MimeMessage %s HTTP headers: unexpected count of main ParsedParts: %s (total: %d)', + $hasHttpRequestHeaders ? 'with' : 'w/o', + implode(', ', $parsedPartList->getPartContentIds()), + $parsedPartList->getMainPartCount() + ) + ); + } + self::appendPartsToMultiPart( + $parsedPartList, + $multiPart + ); + } else { + self::appendSingleMainPartToMultiPart(new Part($mimeMessage), $multiPart); + } + + return $multiPart; + } + + /** + * @param MultiPart $multiPart + * @param string[] $mimeMessageLines + * @param bool $hasHttpHeaders = self::HAS_HTTP_REQUEST_HEADERS|self::HAS_NO_HTTP_REQUEST_HEADERS + * @return ParsedPartList + */ + private static function getPartsFromMimeMessageLines( + MultiPart $multiPart, + array $mimeMessageLines, + $hasHttpHeaders + ) { + $parsedParts = []; + $contentTypeBoundary = $multiPart->getHeader('Content-Type', 'boundary'); + $contentTypeContentIdStart = $multiPart->getHeader('Content-Type', 'start'); + $currentPart = $multiPart; + $messagePartStringContent = ''; + $inHeader = $hasHttpHeaders; + $hitFirstBoundary = false; + + foreach ($mimeMessageLines as $mimeMessageLine) { + // ignore http status code and POST * + if (substr($mimeMessageLine, 0, 5) == 'HTTP/' || substr($mimeMessageLine, 0, 4) == 'POST') { + continue; + } + if (isset($currentHeader)) { + if (isset($mimeMessageLine[0]) && ($mimeMessageLine[0] === ' ' || $mimeMessageLine[0] === "\t")) { + $currentHeader .= $mimeMessageLine; continue; } - if (isset($currentHeader)) { - if (isset($line[0]) && ($line[0] === ' ' || $line[0] === "\t")) { - $currentHeader .= $line; - continue; + if (strpos($currentHeader, ':') !== false) { + list($headerName, $headerValue) = explode(':', $currentHeader, 2); + $headerValue = iconv_mime_decode($headerValue, 0, Part::CHARSET_UTF8); + $parsedMimeHeaders = ContentTypeParser::parseContentTypeHeader($headerName, $headerValue); + foreach ($parsedMimeHeaders as $parsedMimeHeader) { + $currentPart->setHeader( + $parsedMimeHeader->getName(), + $parsedMimeHeader->getValue(), + $parsedMimeHeader->getSubValue() + ); } - if (strpos($currentHeader, ':') !== false) { - list($headerName, $headerValue) = explode(':', $currentHeader, 2); - $headerValue = iconv_mime_decode($headerValue, 0, 'utf-8'); - if (strpos($headerValue, ';') !== false) { - self::parseContentTypeHeader($currentPart, $headerName, $headerValue); - $boundary = $multipart->getHeader('Content-Type', 'boundary'); - $start = $multipart->getHeader('Content-Type', 'start'); - } else { - $currentPart->setHeader($headerName, trim($headerValue)); - } - } - unset($currentHeader); + $contentTypeBoundary = $multiPart->getHeader('Content-Type', 'boundary'); + $contentTypeContentIdStart = $multiPart->getHeader('Content-Type', 'start'); } - if ($inHeader) { - if (trim($line) == '') { - $inHeader = false; - continue; - } - $currentHeader = $line; + unset($currentHeader); + } + if ($inHeader === true) { + if (trim($mimeMessageLine) == '') { + $inHeader = false; continue; - } else { - if (self::isBoundary($line)) { - if (strcmp(trim($line), '--' . $boundary) === 0) { - if ($currentPart instanceof Part) { - $content = substr($content, 0, -1); - self::decodeContent($currentPart, $content); - // check if there is a start parameter given, if not set first part - $isMain = (is_null($start) || $start == $currentPart->getHeader('Content-ID')) ? true : false; - if ($isMain === true) { - $start = $currentPart->getHeader('Content-ID'); - } - $multipart->addPart($currentPart, $isMain); - } - $currentPart = new Part(); - $hitFirstBoundary = true; - $inHeader = true; - $content = ''; - } elseif (strcmp(trim($line), '--' . $boundary . '--') === 0) { - $content = substr($content, 0, -1); - self::decodeContent($currentPart, $content); + } + $currentHeader = $mimeMessageLine; + continue; + } else { + if (self::isBoundary($mimeMessageLine)) { + if (self::isMiddleBoundary($mimeMessageLine, $contentTypeBoundary)) { + if ($currentPart instanceof Part) { + $currentPartContent = self::decodeContent( + $currentPart, + substr($messagePartStringContent, 0, -1) + ); + $currentPart->setContent($currentPartContent); // check if there is a start parameter given, if not set first part - $isMain = (is_null($start) || $start == $currentPart->getHeader('Content-ID')) ? true : false; - if ($isMain === true) { - $start = $currentPart->getHeader('Content-ID'); + if ($contentTypeContentIdStart === null || $currentPart->hasContentId($contentTypeContentIdStart) === true) { + $contentTypeContentIdStart = $currentPart->getHeader('Content-ID'); + $parsedParts[] = new ParsedPart($currentPart, ParsedPart::PART_IS_MAIN); + } else { + $parsedParts[] = new ParsedPart($currentPart, ParsedPart::PART_IS_NOT_MAIN); } - $multipart->addPart($currentPart, $isMain); - $content = ''; } + $currentPart = new Part(); + $hitFirstBoundary = true; + $inHeader = true; + $messagePartStringContent = ''; + } else if (self::isLastBoundary($mimeMessageLine, $contentTypeBoundary)) { + $currentPartContent = self::decodeContent( + $currentPart, + substr($messagePartStringContent, 0, -1) + ); + $currentPart->setContent($currentPartContent); + // check if there is a start parameter given, if not set first part + if ($contentTypeContentIdStart === null || $currentPart->hasContentId($contentTypeContentIdStart) === true) { + $contentTypeContentIdStart = $currentPart->getHeader('Content-ID'); + $parsedParts[] = new ParsedPart($currentPart, ParsedPart::PART_IS_MAIN); + } else { + $parsedParts[] = new ParsedPart($currentPart, ParsedPart::PART_IS_NOT_MAIN); + } + $messagePartStringContent = ''; } else { - if ($hitFirstBoundary === false) { - if (trim($line) !== '') { - $inHeader = true; - $currentHeader = $line; - continue; - } - } - $content .= $line . "\n"; + // else block migrated from https://github.com/progmancod/BeSimpleSoap/commit/bf9437e3bcf35c98c6c2f26aca655ec3d3514694 + // be careful to replace \r\n with \n + $messagePartStringContent .= $mimeMessageLine . "\n"; } + } else { + if ($hitFirstBoundary === false) { + if (trim($mimeMessageLine) !== '') { + $inHeader = true; + $currentHeader = $mimeMessageLine; + continue; + } + } + $messagePartStringContent .= $mimeMessageLine . "\n"; } } - } else { - $multipart->addPart(new Part($mimeMessage), true); } - return $multipart; + return new ParsedPartList($parsedParts); } /** - * Parse a "Content-Type" header with multiple sub values. - * e.g. Content-Type: multipart/related; boundary=boundary; type=text/xml; - * start="<123@abc>" - * - * Based on: https://labs.omniti.com/alexandria/trunk/OmniTI/Mail/Parser.php - * - * @param \BeSimple\SoapCommon\Mime\PartHeader $part Header part - * @param string $headerName Header name - * @param string $headerValue Header value - * + * @param ParsedPartList $parsedPartList + * @param MultiPart $multiPart */ - private static function parseContentTypeHeader(PartHeader $part, $headerName, $headerValue) + private static function appendPartsToMultiPart(ParsedPartList $parsedPartList, MultiPart $multiPart) { - if (strpos($headerValue, ';')) { - list($value, $remainder) = explode(';', $headerValue, 2); - $value = trim($value); - $part->setHeader($headerName, $value); - $remainder = trim($remainder); - while (strlen($remainder) > 0) { - if (!preg_match('/^([a-zA-Z0-9_-]+)=(.{1})/', $remainder, $matches)) { - break; + foreach ($parsedPartList->getParts() as $parsedPart) { + $multiPart->addPart( + $parsedPart->getPart(), + $parsedPart->isMain() + ); + } + } + + private static function appendSingleMainPartToMultiPart(Part $part, MultiPart $multiPart) + { + $multiPart->addPart($part, true); + } + + private static function setMultiPartHeaders(MultiPart $multiPart, $headers) + { + foreach ($headers as $name => $value) { + if ($name === 'Content-Type') { + $parsedMimeHeaders = ContentTypeParser::parseContentTypeHeader($name, $value); + foreach ($parsedMimeHeaders as $parsedMimeHeader) { + $multiPart->setHeader( + $parsedMimeHeader->getName(), + $parsedMimeHeader->getValue(), + $parsedMimeHeader->getSubValue() + ); } - $name = $matches[1]; - $delimiter = $matches[2]; - $remainder = substr($remainder, strlen($name) + 1); - if (!preg_match('/([^;]+)(;)?(\s|$)?/', $remainder, $matches)) { - break; - } - $value = rtrim($matches[1], ';'); - if ($delimiter == "'" || $delimiter == '"') { - $value = trim($value, $delimiter); - } - $part->setHeader($headerName, $name, $value); - $remainder = substr($remainder, strlen($matches[0])); + } else { + $multiPart->setHeader($name, $value); } - } else { - $part->setHeader($headerName, $headerValue); } } /** - * Decodes the content of a Mime part. - * - * @param \BeSimple\SoapCommon\Mime\Part $part Part to add content - * @param string $content Content to decode + * Decodes the content of a Mime part * + * @param Part $part Part to add content + * @param string $partStringContent Content to decode + * @return string $partStringContent decodedContent */ - private static function decodeContent(Part $part, $content) + private static function decodeContent(Part $part, $partStringContent) { $encoding = strtolower($part->getHeader('Content-Transfer-Encoding')); $charset = strtolower($part->getHeader('Content-Type', 'charset')); - if ($encoding == Part::ENCODING_BASE64) { - $content = base64_decode($content); - } elseif ($encoding == Part::ENCODING_QUOTED_PRINTABLE) { - $content = quoted_printable_decode($content); + + if ($encoding === Part::ENCODING_BASE64) { + $partStringContent = base64_decode($partStringContent); + } else if ($encoding === Part::ENCODING_QUOTED_PRINTABLE) { + $partStringContent = quoted_printable_decode($partStringContent); } - if ($charset != 'utf-8') { - $content = iconv($charset, 'utf-8', $content); + + if ($charset !== Part::CHARSET_UTF8) { + return iconv($charset, Part::CHARSET_UTF8, $partStringContent); } - $part->setContent($content); + + return $partStringContent; } private static function hasBoundary(array $lines) @@ -203,8 +261,18 @@ class Parser return false; } - private static function isBoundary($line) + private static function isBoundary($mimeMessageLine) { - return strlen($line) > 0 && $line[0] === "-"; + return strlen($mimeMessageLine) > 0 && $mimeMessageLine[0] === "-"; + } + + private static function isMiddleBoundary($mimeMessageLine, $contentTypeBoundary) + { + return strcmp(trim($mimeMessageLine), '--'.$contentTypeBoundary) === 0; + } + + private static function isLastBoundary($mimeMessageLine, $contentTypeBoundary) + { + return strcmp(trim($mimeMessageLine), '--'.$contentTypeBoundary.'--') === 0; } } \ No newline at end of file diff --git a/src/BeSimple/SoapCommon/Mime/Parser/ContentTypeParser.php b/src/BeSimple/SoapCommon/Mime/Parser/ContentTypeParser.php new file mode 100644 index 0000000..6e403ff --- /dev/null +++ b/src/BeSimple/SoapCommon/Mime/Parser/ContentTypeParser.php @@ -0,0 +1,64 @@ +" + * + * Based on: https://labs.omniti.com/alexandria/trunk/OmniTI/Mail/Parser.php + * + * @param string $headerName Header name + * @param string $headerValue Header value + * @return ParsedMimeHeader[] + */ + public static function parseContentTypeHeader($headerName, $headerValue) + { + if (self::isCompositeHeaderValue($headerValue)) { + $parsedMimeHeaders = self::parseCompositeValue($headerName, $headerValue); + } else { + $parsedMimeHeaders = [ + new ParsedMimeHeader($headerName, trim($headerValue)) + ]; + } + + return $parsedMimeHeaders; + } + + private static function parseCompositeValue($headerName, $headerValue) + { + $parsedMimeHeaders = []; + list($value, $remainder) = explode(';', $headerValue, 2); + $value = trim($value); + $parsedMimeHeaders[] = new ParsedMimeHeader($headerName, $value); + $remainder = trim($remainder); + while (strlen($remainder) > 0) { + if (!preg_match('/^([a-zA-Z0-9_-]+)=(.{1})/', $remainder, $matches)) { + break; + } + $name = $matches[1]; + $delimiter = $matches[2]; + $remainder = substr($remainder, strlen($name) + 1); + // preg_match migrated from https://github.com/progmancod/BeSimpleSoap/commit/6bc8f6a467616c934b0a9792f0efece55054db97 + if (!preg_match('/([^;]+)(;\s*|\s*$)/', $remainder, $matches)) { + break; + } + $value = rtrim($matches[1], ';'); + if ($delimiter == "'" || $delimiter == '"') { + $value = trim($value, $delimiter); + } + $remainder = substr($remainder, strlen($matches[0])); + $parsedMimeHeaders[] = new ParsedMimeHeader($headerName, $name, $value); + } + + return $parsedMimeHeaders; + } + + private static function isCompositeHeaderValue($headerValue) + { + return strpos($headerValue, ';'); + } +} diff --git a/src/BeSimple/SoapCommon/Mime/Parser/ParsedMimeHeader.php b/src/BeSimple/SoapCommon/Mime/Parser/ParsedMimeHeader.php new file mode 100644 index 0000000..8656873 --- /dev/null +++ b/src/BeSimple/SoapCommon/Mime/Parser/ParsedMimeHeader.php @@ -0,0 +1,37 @@ +name = $name; + $this->value = $value; + $this->subValue = $subValue; + } + + public function getName() + { + return $this->name; + } + + public function getValue() + { + return $this->value; + } + + public function getSubValue() + { + return $this->subValue; + } +} diff --git a/src/BeSimple/SoapCommon/Mime/Parser/ParsedPart.php b/src/BeSimple/SoapCommon/Mime/Parser/ParsedPart.php new file mode 100644 index 0000000..a9a90d1 --- /dev/null +++ b/src/BeSimple/SoapCommon/Mime/Parser/ParsedPart.php @@ -0,0 +1,34 @@ +part = $part; + $this->isMain = $isMain; + } + + public function getPart() + { + return $this->part; + } + + public function isMain() + { + return $this->isMain; + } +} diff --git a/src/BeSimple/SoapCommon/Mime/Parser/ParsedPartList.php b/src/BeSimple/SoapCommon/Mime/Parser/ParsedPartList.php new file mode 100644 index 0000000..49cd3d4 --- /dev/null +++ b/src/BeSimple/SoapCommon/Mime/Parser/ParsedPartList.php @@ -0,0 +1,60 @@ +parts = $parts; + } + + public function getMainPartCount() + { + $mainPartsCount = 0; + foreach ($this->getParts() as $parsedPart) { + if ($parsedPart->isMain() === true) { + $mainPartsCount++; + } + } + + return $mainPartsCount; + } + + public function hasExactlyOneMainPart() + { + return $this->getMainPartCount() === 1; + } + + public function getPartContentIds() + { + $partContentIds = []; + foreach ($this->getParts() as $parsedPart) { + $partContentIds[] = $parsedPart->getPart()->getContentId(); + } + + return $partContentIds; + } + + public function getParts() + { + return $this->parts; + } + + public function getPartCount() + { + return count($this->parts); + } + + public function hasParts() + { + return $this->getPartCount() > 0; + } +} diff --git a/src/BeSimple/SoapCommon/Mime/Part.php b/src/BeSimple/SoapCommon/Mime/Part.php index dfddb71..2e76b93 100644 --- a/src/BeSimple/SoapCommon/Mime/Part.php +++ b/src/BeSimple/SoapCommon/Mime/Part.php @@ -28,57 +28,38 @@ use BeSimple\SoapCommon\Helper; */ class Part extends PartHeader { - /** - * Encoding type base 64 - */ const ENCODING_BASE64 = 'base64'; - - /** - * Encoding type binary - */ const ENCODING_BINARY = 'binary'; - - /** - * Encoding type eight bit - */ const ENCODING_EIGHT_BIT = '8bit'; - - /** - * Encoding type seven bit - */ const ENCODING_SEVEN_BIT = '7bit'; - - /** - * Encoding type quoted printable - */ const ENCODING_QUOTED_PRINTABLE = 'quoted-printable'; - /** - * Content. - * - * @var mixed - */ + const CHARSET_UTF8 = 'utf-8'; + + const CONTENT_TYPE_STREAM = 'application/octet-stream'; + const CONTENT_TYPE_PDF = 'application/pdf'; + + /** @var mixed */ protected $content; /** - * Construct new mime object. - * * @param mixed $content Content * @param string $contentType Content type * @param string $charset Charset * @param string $encoding Encoding * @param string $contentId Content id - * */ - public function __construct($content = null, $contentType = 'application/octet-stream', $charset = null, $encoding = self::ENCODING_BINARY, $contentId = null) - { + public function __construct( + $content = null, + $contentType = self::CONTENT_TYPE_STREAM, + $charset = self::CHARSET_UTF8, + $encoding = self::ENCODING_BINARY, + $contentId = null + ) { $this->content = $content; + $this->setHeader('Content-Type', $contentType); - if (!is_null($charset)) { - $this->setHeader('Content-Type', 'charset', $charset); - } else { - $this->setHeader('Content-Type', 'charset', 'utf-8'); - } + $this->setHeader('Content-Type', 'charset', $charset); $this->setHeader('Content-Transfer-Encoding', $encoding); if (is_null($contentId)) { $contentId = $this->generateContentId(); @@ -106,6 +87,16 @@ class Part extends PartHeader return $this->content; } + public function hasContentId($contentTypeContentId) + { + return $contentTypeContentId === $this->getContentId(); + } + + public function getContentId() + { + return $this->getHeader('Content-ID'); + } + /** * Set mime content. * @@ -137,10 +128,10 @@ class Part extends PartHeader { $encoding = strtolower($this->getHeader('Content-Transfer-Encoding')); $charset = strtolower($this->getHeader('Content-Type', 'charset')); - if ($charset != 'utf-8') { - $content = iconv('utf-8', $charset . '//TRANSLIT', $this->content); + if ($charset !== self::CHARSET_UTF8) { + $content = iconv(self::CHARSET_UTF8, $charset.'//TRANSLIT', $this->getContent()); } else { - $content = $this->content; + $content = $this->getContent(); } switch ($encoding) { case self::ENCODING_BASE64: diff --git a/src/BeSimple/SoapCommon/Mime/PartFactory.php b/src/BeSimple/SoapCommon/Mime/PartFactory.php new file mode 100644 index 0000000..ea9b3fb --- /dev/null +++ b/src/BeSimple/SoapCommon/Mime/PartFactory.php @@ -0,0 +1,33 @@ +getContent(), + Part::CONTENT_TYPE_PDF, + Part::CHARSET_UTF8, + Part::ENCODING_BINARY, + $attachment->getId() + ); + } + + /** + * @param SoapAttachment[] $attachments SOAP attachments + * @return Part[] + */ + public static function createAttachmentParts(array $attachments = []) + { + $parts = []; + foreach ($attachments as $attachment) { + $parts[] = self::createFromSoapAttachment($attachment); + } + + return $parts; + } +} diff --git a/src/BeSimple/SoapCommon/SoapKernel.php b/src/BeSimple/SoapCommon/SoapKernel.php index 005ae47..0b1a4ec 100644 --- a/src/BeSimple/SoapCommon/SoapKernel.php +++ b/src/BeSimple/SoapCommon/SoapKernel.php @@ -8,7 +8,7 @@ namespace BeSimple\SoapCommon; * the chain-of-responsibility pattern). * * @author Christian Kerl - * @author Petr Bechyně + * @author Petr Bechyně */ class SoapKernel { @@ -20,7 +20,7 @@ class SoapKernel * @param int $attachmentType = SoapOptions::SOAP_ATTACHMENTS_TYPE_SWA|SoapOptions::ATTACHMENTS_TYPE_MTOM|SoapOptions::ATTACHMENTS_TYPE_BASE64 * @return SoapRequest */ - public function filterRequest(SoapRequest $request, array $filters, $attachmentType) + public static function filterRequest(SoapRequest $request, array $filters, $attachmentType) { foreach ($filters as $filter) { if ($filter instanceof SoapRequestFilter) { @@ -37,9 +37,9 @@ class SoapKernel * @param SoapResponse $response SOAP response * @param SoapRequestFilter[]|SoapResponseFilter[] $filters * @param int $attachmentType = SoapOptions::SOAP_ATTACHMENTS_TYPE_SWA|SoapOptions::ATTACHMENTS_TYPE_MTOM|SoapOptions::ATTACHMENTS_TYPE_BASE64 - * @return SoapResponse + * @return \BeSimple\SoapClient\SoapResponse|\BeSimple\SoapServer\SoapResponse */ - public function filterResponse(SoapResponse $response, array $filters, $attachmentType) + public static function filterResponse(SoapResponse $response, array $filters, $attachmentType) { foreach ($filters as $filter) { if ($filter instanceof SoapResponseFilter) { diff --git a/src/BeSimple/SoapCommon/SoapMessage.php b/src/BeSimple/SoapCommon/SoapMessage.php index e2d42f1..a11c5e6 100644 --- a/src/BeSimple/SoapCommon/SoapMessage.php +++ b/src/BeSimple/SoapCommon/SoapMessage.php @@ -62,7 +62,7 @@ abstract class SoapMessage /** * Mime attachments. * - * @var array(\BeSimple\SoapCommon\Mime\Part) + * @var \BeSimple\SoapCommon\Mime\Part[] */ protected $attachments; diff --git a/src/BeSimple/SoapCommon/SoapOptions/SoapOptions.php b/src/BeSimple/SoapCommon/SoapOptions/SoapOptions.php index ad7702f..8d46324 100644 --- a/src/BeSimple/SoapCommon/SoapOptions/SoapOptions.php +++ b/src/BeSimple/SoapCommon/SoapOptions/SoapOptions.php @@ -66,6 +66,7 @@ class SoapOptions $this->soapFeatures = $features; $this->wsdlFile = $wsdlFile; $this->wsdlCacheType = $wsdlCacheType; + $this->wsdlCacheDir = $wsdlCacheDir; $this->classMap = $classMap; $this->typeConverterCollection = $typeConverterCollection; $this->attachmentType = $attachmentType; diff --git a/src/BeSimple/SoapCommon/SoapOptionsBuilder.php b/src/BeSimple/SoapCommon/SoapOptionsBuilder.php index f79051c..f2c9d8c 100644 --- a/src/BeSimple/SoapCommon/SoapOptionsBuilder.php +++ b/src/BeSimple/SoapCommon/SoapOptionsBuilder.php @@ -18,11 +18,11 @@ use BeSimple\SoapCommon\SoapOptions\SoapOptions; use InvalidArgumentException; /** - * @author Petr Bechyně + * @author Petr Bechyně */ class SoapOptionsBuilder { - static public function createWithDefaults( + public static function createWithDefaults( $wsdlFile, $wsdlCacheType = SoapOptions::SOAP_CACHE_TYPE_NONE, $wsdlCacheDir = null @@ -30,16 +30,37 @@ class SoapOptionsBuilder return self::createWithClassMap($wsdlFile, new ClassMap(), $wsdlCacheType, $wsdlCacheDir); } - static public function createSwaWithClassMap( + public static function createSwaWithClassMap( $wsdlFile, ClassMap $classMap, $wsdlCacheType = SoapOptions::SOAP_CACHE_TYPE_NONE, $wsdlCacheDir = null ) { - return self::createWithClassMap($wsdlFile, $classMap, $wsdlCacheType, $wsdlCacheDir, SoapOptions::SOAP_ATTACHMENTS_TYPE_SWA); + return self::createWithClassMap( + $wsdlFile, + $classMap, + $wsdlCacheType, + $wsdlCacheDir, + SoapOptions::SOAP_ATTACHMENTS_TYPE_SWA + ); } - static public function createWithClassMap( + public static function createSwaWithClassMapV11( + $wsdlFile, + ClassMap $classMap, + $wsdlCacheType = SoapOptions::SOAP_CACHE_TYPE_NONE, + $wsdlCacheDir = null + ) { + return self::createWithClassMapV11( + $wsdlFile, + $classMap, + $wsdlCacheType, + $wsdlCacheDir, + SoapOptions::SOAP_ATTACHMENTS_TYPE_SWA + ); + } + + public static function createWithClassMap( $wsdlFile, ClassMap $classMap, $wsdlCacheType = SoapOptions::SOAP_CACHE_TYPE_NONE, @@ -54,7 +75,8 @@ class SoapOptionsBuilder throw new InvalidArgumentException('Cache dir must be set for this wsdl cache type'); } } - $soapOptions = new SoapOptions( + + return new SoapOptions( SoapOptions::SOAP_VERSION_1_2, SoapOptions::SOAP_ENCODING_UTF8, SoapOptions::SOAP_CONNECTION_KEEP_ALIVE_OFF, @@ -68,7 +90,37 @@ class SoapOptionsBuilder new TypeConverterCollection(), $attachmentType ); + } - return $soapOptions; + public static function createWithClassMapV11( + $wsdlFile, + ClassMap $classMap, + $wsdlCacheType = SoapOptions::SOAP_CACHE_TYPE_NONE, + $wsdlCacheDir = null, + $attachmentType = null + ) { + if (!Cache::hasType($wsdlCacheType)) { + throw new InvalidArgumentException('Invalid cache type'); + } + if ($wsdlCacheType !== SoapOptions::SOAP_CACHE_TYPE_NONE) { + if ($wsdlCacheDir === null) { + throw new InvalidArgumentException('Cache dir must be set for this wsdl cache type'); + } + } + + return new SoapOptions( + SoapOptions::SOAP_VERSION_1_1, + SoapOptions::SOAP_ENCODING_UTF8, + SoapOptions::SOAP_CONNECTION_KEEP_ALIVE_OFF, + new SoapFeatures([ + SoapFeatures::SINGLE_ELEMENT_ARRAYS + ]), + $wsdlFile, + $wsdlCacheType, + $wsdlCacheDir, + $classMap, + new TypeConverterCollection(), + $attachmentType + ); } } diff --git a/src/BeSimple/SoapCommon/SoapRequestFactory.php b/src/BeSimple/SoapCommon/SoapRequestFactory.php index 8d07fae..38888c4 100644 --- a/src/BeSimple/SoapCommon/SoapRequestFactory.php +++ b/src/BeSimple/SoapCommon/SoapRequestFactory.php @@ -7,19 +7,22 @@ class SoapRequestFactory /** * Factory function for SoapRequest. * - * @param string $location Location - * @param string $action SOAP action - * @param string $version SOAP version - * @param string $contentType Content Type - * @param string $content Content - * + * @param string $location Location + * @param string $action SOAP action + * @param string $version SOAP version + * @param string $contentType Content Type + * @param string $content Content * @return SoapRequest */ - public static function create($location, $action, $version, $contentType, $content = null) - { + public static function createWithContentType( + $location, + $action, + $version, + $contentType, + $content = null + ) { $request = new SoapRequest(); - // $content is if unmodified from SoapClient not a php string type! - $request->setContent((string) $content); + $request->setContent($content); $request->setLocation($location); $request->setAction($action); $request->setVersion($version); @@ -27,4 +30,29 @@ class SoapRequestFactory return $request; } + + /** + * Factory function for SoapRequest. + * + * @param string $location Location + * @param string $action SOAP action + * @param string $version SOAP version + * @param string $content Content + * @return SoapRequest + */ + public static function create( + $location, + $action, + $version, + $content = null + ) { + + return self::createWithContentType( + $location, + $action, + $version, + SoapRequest::getContentTypeForVersion($version), + $content + ); + } } diff --git a/src/BeSimple/SoapCommon/SoapResponse.php b/src/BeSimple/SoapCommon/SoapResponse.php index b6815b2..3542a28 100644 --- a/src/BeSimple/SoapCommon/SoapResponse.php +++ b/src/BeSimple/SoapCommon/SoapResponse.php @@ -13,14 +13,31 @@ namespace BeSimple\SoapCommon; -use BeSimple\SoapCommon\SoapMessage; +use BeSimple\SoapClient\SoapResponseTracingData; /** * SOAP response message. * * @author Christian Kerl + * @author Petr Bechyně */ class SoapResponse extends SoapMessage { + /** @var SoapResponseTracingData */ + protected $tracingData; -} \ No newline at end of file + public function hasTracingData() + { + return $this->tracingData !== null; + } + + public function getTracingData() + { + return $this->tracingData; + } + + public function setTracingData(SoapResponseTracingData $tracingData) + { + $this->tracingData = $tracingData; + } +} diff --git a/src/BeSimple/SoapCommon/Tests/ClassMapTest.php b/src/BeSimple/SoapCommon/Tests/ClassMapTest.php index 1722bb1..4c40357 100644 --- a/src/BeSimple/SoapCommon/Tests/ClassMapTest.php +++ b/src/BeSimple/SoapCommon/Tests/ClassMapTest.php @@ -23,43 +23,43 @@ class ClassMapTest extends \PHPUnit_Framework_TestCase { public function testAll() { - $classmap = new ClassMap(); + $classMap = new ClassMap(); - $this->assertSame(array(), $classmap->getAll()); + $this->assertSame([], $classMap->getAll()); } public function testAdd() { - $classmap = new ClassMap(); + $classMap = new ClassMap(); - $classmap->add('foobar', 'BeSimple\SoapCommon\ClassMap'); + $classMap->add('foobar', 'BeSimple\SoapCommon\ClassMap'); $this->setExpectedException('InvalidArgumentException'); - $classmap->add('foobar', 'BeSimple\SoapCommon\ClassMap'); + $classMap->add('foobar', 'BeSimple\SoapCommon\ClassMap'); } public function testGet() { - $classmap = new ClassMap(); + $classMap = new ClassMap(); - $classmap->add('foobar', 'BeSimple\SoapCommon\ClassMap'); - $this->assertSame('BeSimple\SoapCommon\ClassMap', $classmap->get('foobar')); + $classMap->add('foobar', 'BeSimple\SoapCommon\ClassMap'); + $this->assertSame('BeSimple\SoapCommon\ClassMap', $classMap->get('foobar')); - $this->setExpectedException('InvalidArgumentException'); - $classmap->get('bar'); + $this->setExpectedException('Exception'); + $classMap->get('bar'); } public function testAddClassMap() { - $classmap1 = new ClassMap(); - $classmap2 = new ClassMap(); + $classMap1 = new ClassMap(); + $classMap2 = new ClassMap(); - $classmap2->add('foobar', 'BeSimple\SoapCommon\ClassMap'); - $classmap1->addClassMap($classmap2); + $classMap2->add('foobar', 'BeSimple\SoapCommon\ClassMap'); + $classMap1->addClassMap($classMap2); - $this->assertEquals(array('foobar' => 'BeSimple\SoapCommon\ClassMap'), $classmap1->getAll()); + $this->assertEquals(['foobar' => 'BeSimple\SoapCommon\ClassMap'], $classMap1->getAll()); - $this->setExpectedException('InvalidArgumentException'); - $classmap1->addClassMap($classmap2); + $this->setExpectedException('Exception'); + $classMap1->addClassMap($classMap2); } -} \ No newline at end of file +} diff --git a/src/BeSimple/SoapServer/MimeFilter.php b/src/BeSimple/SoapServer/MimeFilter.php index f86842a..091d51e 100644 --- a/src/BeSimple/SoapServer/MimeFilter.php +++ b/src/BeSimple/SoapServer/MimeFilter.php @@ -19,7 +19,7 @@ use BeSimple\SoapCommon\Mime\Part as MimePart; use BeSimple\SoapCommon\Mime\Part; use BeSimple\SoapCommon\SoapRequest; use BeSimple\SoapCommon\SoapRequestFilter; -use BeSimple\SoapCommon\SoapResponse; +use BeSimple\SoapCommon\SoapResponse as CommonSoapResponse; use BeSimple\SoapCommon\SoapResponseFilter; /** @@ -47,7 +47,7 @@ class MimeFilter implements SoapRequestFilter, SoapResponseFilter return $request; } - public function filterResponse(SoapResponse $response, $attachmentType) + public function filterResponse(CommonSoapResponse $response, $attachmentType) { $attachmentsToSend = $response->getAttachments(); if (count($attachmentsToSend) > 0) { diff --git a/src/BeSimple/SoapServer/SoapResponseFactory.php b/src/BeSimple/SoapServer/SoapResponseFactory.php index c10d1ae..7c6cdd7 100644 --- a/src/BeSimple/SoapServer/SoapResponseFactory.php +++ b/src/BeSimple/SoapServer/SoapResponseFactory.php @@ -13,24 +13,30 @@ namespace BeSimple\SoapServer; use BeSimple\SoapBundle\Soap\SoapAttachment; -use BeSimple\SoapCommon\Mime\Part; +use BeSimple\SoapClient\SoapResponseTracingData; +use BeSimple\SoapCommon\Mime\PartFactory; use BeSimple\SoapCommon\SoapMessage; class SoapResponseFactory { /** - * Factory function for SoapResponse. + * Factory function for SoapServer\SoapResponse. * - * @param string $content Content - * @param string $location Location - * @param string $action SOAP action - * @param string $version SOAP version - * @param SoapAttachment[] $attachments SOAP attachments + * @param string $content Content + * @param string $location Location + * @param string $action SOAP action + * @param string $version SOAP version + * @param SoapAttachment[] $attachments SOAP attachments * * @return SoapResponse */ - public static function create($content, $location, $action, $version, $attachments = []) - { + public static function create( + $content, + $location, + $action, + $version, + array $attachments = [] + ) { $response = new SoapResponse(); $response->setContent($content); $response->setLocation($location); @@ -38,10 +44,9 @@ class SoapResponseFactory $response->setVersion($version); $contentType = SoapMessage::getContentTypeForVersion($version); $response->setContentType($contentType); - if (count($attachments) > 0) { $response->setAttachments( - self::createAttachmentParts($attachments) + PartFactory::createAttachmentParts($attachments) ); } @@ -49,23 +54,39 @@ class SoapResponseFactory } /** - * @param SoapAttachment[] $attachments SOAP attachments - * @return Part[] + * Factory function for SoapServer\SoapResponse. + * + * @param string $content Content + * @param string $location Location + * @param string $action SOAP action + * @param string $version SOAP version + * @param SoapResponseTracingData $tracingData Data value object suitable for tracing SOAP traffic + * @param SoapAttachment[] $attachments SOAP attachments + * + * @return SoapResponse */ - private static function createAttachmentParts(array $attachments = []) - { - $parts = []; - foreach ($attachments as $attachment) { - $part = new Part( - $attachment->getContent(), - 'application/pdf', - 'utf-8', - Part::ENCODING_BINARY, - $attachment->getId() + public static function createWithTracingData( + $content, + $location, + $action, + $version, + SoapResponseTracingData $tracingData, + array $attachments = [] + ) { + $response = new SoapResponse(); + $response->setContent($content); + $response->setLocation($location); + $response->setAction($action); + $response->setVersion($version); + $response->setTracingData($tracingData); + $contentType = SoapMessage::getContentTypeForVersion($version); + $response->setContentType($contentType); + if (count($attachments) > 0) { + $response->setAttachments( + PartFactory::createAttachmentParts($attachments) ); - $parts[] = $part; } - return $parts; + return $response; } } diff --git a/src/BeSimple/SoapServer/SoapServer.php b/src/BeSimple/SoapServer/SoapServer.php index 63d73d1..38e64f5 100644 --- a/src/BeSimple/SoapServer/SoapServer.php +++ b/src/BeSimple/SoapServer/SoapServer.php @@ -30,7 +30,7 @@ use InvalidArgumentException; * * @author Andreas Schamberger * @author Christian Kerl - * @author Petr Bechyně + * @author Petr Bechyně */ class SoapServer extends \SoapServer { @@ -85,16 +85,15 @@ class SoapServer extends \SoapServer */ public function createRequest($requestUrl, $soapAction, $requestContentType, $requestContent = null) { - $soapRequest = SoapRequestFactory::create( + $soapRequest = SoapRequestFactory::createWithContentType( $requestUrl, $soapAction, $this->soapVersion, $requestContentType, $requestContent ); - $soapKernel = new SoapKernel(); if ($this->soapOptions->hasAttachments()) { - $soapRequest = $soapKernel->filterRequest( + $soapRequest = SoapKernel::filterRequest( $soapRequest, $this->getAttachmentFilters(), $this->soapOptions->getAttachmentType() @@ -180,7 +179,7 @@ class SoapServer extends \SoapServer * @param SoapAttachment[] $attachments * @return SoapResponse */ - private function createResponse($requestLocation, $soapAction, $soapVersion, $responseContent = null, $attachments = []) + private function createResponse($requestLocation, $soapAction, $soapVersion, $responseContent = null, array $attachments = []) { $soapResponse = SoapResponseFactory::create( $responseContent, @@ -189,9 +188,8 @@ class SoapServer extends \SoapServer $soapVersion, $attachments ); - $soapKernel = new SoapKernel(); if ($this->soapOptions->hasAttachments()) { - $soapResponse = $soapKernel->filterResponse( + $soapResponse = SoapKernel::filterResponse( $soapResponse, $this->getAttachmentFilters(), $this->soapOptions->getAttachmentType() diff --git a/src/BeSimple/SoapServer/SoapServerBuilder.php b/src/BeSimple/SoapServer/SoapServerBuilder.php index 597abb2..f62a2f9 100644 --- a/src/BeSimple/SoapServer/SoapServerBuilder.php +++ b/src/BeSimple/SoapServer/SoapServerBuilder.php @@ -18,7 +18,7 @@ use BeSimple\SoapServer\SoapOptions\SoapServerOptions; /** * SoapServerBuilder provides a SoapServer instance from SoapServerOptions and SoapOptions. * - * @author Petr Bechyně + * @author Petr Bechyně */ class SoapServerBuilder { diff --git a/src/BeSimple/SoapServer/Tests/Attachment/Attachment.php b/src/BeSimple/SoapServer/Tests/Attachment/Attachment.php new file mode 100644 index 0000000..77046b1 --- /dev/null +++ b/src/BeSimple/SoapServer/Tests/Attachment/Attachment.php @@ -0,0 +1,35 @@ +fileName = $fileName; + $this->contentType = $contentType; + $this->content = $content; + } +} diff --git a/src/BeSimple/SoapServer/Tests/Attachment/AttachmentCollection.php b/src/BeSimple/SoapServer/Tests/Attachment/AttachmentCollection.php new file mode 100644 index 0000000..364889e --- /dev/null +++ b/src/BeSimple/SoapServer/Tests/Attachment/AttachmentCollection.php @@ -0,0 +1,21 @@ +attachments = $attachments; + } + + public function hasAttachments() + { + return $this->attachments !== null && count($this->attachments) > 0; + } +} diff --git a/src/BeSimple/SoapServer/Tests/DummyService.php b/src/BeSimple/SoapServer/Tests/DummyService.php new file mode 100644 index 0000000..7dce6dd --- /dev/null +++ b/src/BeSimple/SoapServer/Tests/DummyService.php @@ -0,0 +1,106 @@ +requestHandlerAttachmentsStorage = $requestHandlerAttachmentsStorage; + } + + public function getAttachmentStorage() + { + return $this->requestHandlerAttachmentsStorage; + } + + /** + * @return string[] + */ + public function getClassMap() + { + return [ + 'DummyServiceResponse' => DummyServiceResponse::class, + 'DummyServiceResponseWithAttachments' => DummyServiceResponseWithAttachments::class, + 'DummyServiceRequest' => DummyServiceRequest::class, + 'DummyServiceRequestWithAttachments' => DummyServiceRequestWithAttachments::class, + ]; + } + + /** + * @exclude + * @return string + */ + public function getWsdlPath() + { + $class = new ReflectionClass(static::class); + + return __DIR__.DIRECTORY_SEPARATOR.$class->getShortName().'.wsdl'; + } + + /** + * @return string + */ + public function getEndpoint() + { + return 'http://my.test/soap/dummyService'; + } + + /** + * @param DummyServiceRequest $dummyServiceRequest + * @return DummyServiceResponse + */ + public function dummyServiceMethod(DummyServiceRequest $dummyServiceRequest) + { + $dummyServiceHandler = new DummyServiceHandler(); + + return $dummyServiceHandler->handle($dummyServiceRequest); + } + + /** + * @param DummyServiceRequestWithAttachments $dummyServiceRequestWithAttachments + * @return DummyServiceResponseWithAttachments + */ + public function dummyServiceMethodWithAttachments(DummyServiceRequestWithAttachments $dummyServiceRequestWithAttachments) + { + if ($dummyServiceRequestWithAttachments->hasAttachments() === true) { + $attachmentStorage = $this->getAttachmentStorage(); + $attachments = []; + foreach ($attachmentStorage->getAttachments() as $soapAttachment) { + $attachments[] = new Attachment( + $soapAttachment->getId(), + $soapAttachment->getType(), + $soapAttachment->getContent() + ); + } + $dummyServiceRequestWithAttachments->attachmentCollection = new AttachmentCollection($attachments); + } + + $dummyServiceHandlerWithAttachments = new DummyServiceHandlerWithAttachments(); + $dummyServiceResponseWithAttachments = $dummyServiceHandlerWithAttachments->handle($dummyServiceRequestWithAttachments); + + if ($dummyServiceResponseWithAttachments->hasAttachments() === true) { + $soapAttachments = []; + foreach ($dummyServiceResponseWithAttachments->attachmentCollection->attachments as $attachment) { + $soapAttachments[] = new SoapAttachment( + $attachment->fileName, + $attachment->contentType, + $attachment->content + ); + } + $this->addAttachmentStorage(new RequestHandlerAttachmentsStorage($soapAttachments)); + } + + return $dummyServiceResponseWithAttachments; + } +} diff --git a/src/BeSimple/SoapServer/Tests/DummyService.wsdl b/src/BeSimple/SoapServer/Tests/DummyService.wsdl new file mode 100644 index 0000000..78ec604 --- /dev/null +++ b/src/BeSimple/SoapServer/Tests/DummyService.wsdl @@ -0,0 +1,92 @@ + + + + + + + + + User name for authorization + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + WSDL file for DummyService + + + + + diff --git a/src/BeSimple/SoapServer/Tests/DummyServiceHandler.php b/src/BeSimple/SoapServer/Tests/DummyServiceHandler.php new file mode 100644 index 0000000..b704fe9 --- /dev/null +++ b/src/BeSimple/SoapServer/Tests/DummyServiceHandler.php @@ -0,0 +1,18 @@ +status = true; + + return $response; + } +} diff --git a/src/BeSimple/SoapServer/Tests/DummyServiceHandlerWithAttachments.php b/src/BeSimple/SoapServer/Tests/DummyServiceHandlerWithAttachments.php new file mode 100644 index 0000000..ab3b04a --- /dev/null +++ b/src/BeSimple/SoapServer/Tests/DummyServiceHandlerWithAttachments.php @@ -0,0 +1,30 @@ +status = true; + if ($request->includeAttachments === true) { + if ($request->hasAttachments() === true) { + $attachments = []; + foreach ($request->attachmentCollection->attachments as $attachment) { + $attachments[] = new Attachment($attachment->fileName, $attachment->contentType, $attachment->content); + } + $response->attachmentCollection = new AttachmentCollection($attachments); + } + } + + return $response; + } +} diff --git a/src/BeSimple/SoapServer/Tests/DummyServiceRequest.php b/src/BeSimple/SoapServer/Tests/DummyServiceRequest.php new file mode 100644 index 0000000..c52afba --- /dev/null +++ b/src/BeSimple/SoapServer/Tests/DummyServiceRequest.php @@ -0,0 +1,11 @@ +attachmentCollection !== null && $this->attachmentCollection->hasAttachments(); + } +} diff --git a/src/BeSimple/SoapServer/Tests/DummyServiceResponse.php b/src/BeSimple/SoapServer/Tests/DummyServiceResponse.php new file mode 100644 index 0000000..87857ed --- /dev/null +++ b/src/BeSimple/SoapServer/Tests/DummyServiceResponse.php @@ -0,0 +1,11 @@ +attachmentCollection !== null && $this->attachmentCollection->hasAttachments(); + } +} diff --git a/src/BeSimple/SoapServer/Tests/SoapServerBuilderTest.php b/src/BeSimple/SoapServer/Tests/SoapServerBuilderTest.php index 8fa5eb6..797af77 100644 --- a/src/BeSimple/SoapServer/Tests/SoapServerBuilderTest.php +++ b/src/BeSimple/SoapServer/Tests/SoapServerBuilderTest.php @@ -12,30 +12,139 @@ namespace BeSimple\SoapServer\Tests; +use BeSimple\SoapClient\Tests\SoapClientBuilderTest; +use BeSimple\SoapCommon\ClassMap; +use BeSimple\SoapCommon\SoapOptions\SoapOptions; +use BeSimple\SoapCommon\SoapOptionsBuilder; +use BeSimple\SoapServer\SoapOptions\SoapServerOptions; use BeSimple\SoapServer\SoapServerBuilder; +use BeSimple\SoapServer\SoapServerOptionsBuilder; /** * UnitTest for \BeSimple\SoapServer\SoapServerBuilder * * @author Christian Kerl + * @author Petr Bechyne */ class SoapServerBuilderTest extends \PHPUnit_Framework_TestCase { - public function testUnconfiguredWsdl() - { - $builder = $this->getSoapServerBuilder(); + const TEST_LOCAL_WSDL_UK = SoapClientBuilderTest::TEST_LOCAL_WSDL_UK; + const CACHE_DIR = __DIR__ . '/../../../../cache'; - $this->setExpectedException('InvalidArgumentException'); - $builder->build(); + public function testSoapOptionsCreateWithDefaults() + { + $defaultOptions = SoapOptionsBuilder::createWithDefaults(self::TEST_LOCAL_WSDL_UK); + + self::assertInstanceOf(SoapOptions::class, $defaultOptions); + self::assertEquals(self::TEST_LOCAL_WSDL_UK, $defaultOptions->getWsdlFile()); } - public function testUnconfiguredHandler() + public function testSoapClientOptionsCreateWithDefaults() { - $builder = $this->getSoapServerBuilder(); - $builder->withWsdl('my.wsdl'); + $defaultOptions = SoapServerOptionsBuilder::createWithDefaults(new SoapServerHandler); - $this->setExpectedException('InvalidArgumentException'); - $builder->build(); + self::assertInstanceOf(SoapServerOptions::class, $defaultOptions); + self::assertInstanceOf(SoapServerHandler::class, $defaultOptions->getHandlerInstance()); + } + + public function testSoapServerBuilderBuild() + { + $soapServer = $this->getSoapServerBuilder()->build( + SoapServerOptionsBuilder::createWithDefaults(new SoapServerHandler), + SoapOptionsBuilder::createWithDefaults(self::TEST_LOCAL_WSDL_UK) + ); + $soapRequest = $soapServer->createRequest('request-url', 'soap-action', 'content/type', 'request-content'); + + self::assertEquals('content/type', $soapRequest->getContentType()); + self::assertEquals('soap-action', $soapRequest->getAction()); + self::assertEquals('request-content', $soapRequest->getContent()); + self::assertFalse($soapRequest->hasAttachments()); + self::assertNull($soapRequest->getAttachments()); + } + + public function testHandleRequest() + { + $dummyService = new DummyService(); + $classMap = new ClassMap(); + foreach ($dummyService->getClassMap() as $type => $className) { + $classMap->add($type, $className); + } + $soapServerBuilder = new SoapServerBuilder(); + $soapServerOptions = SoapServerOptionsBuilder::createWithDefaults($dummyService); + $soapOptions = SoapOptionsBuilder::createWithClassMap($dummyService->getWsdlPath(), $classMap); + $soapServer = $soapServerBuilder->build($soapServerOptions, $soapOptions); + + $request = $soapServer->createRequest( + $dummyService->getEndpoint(), + 'DummyService.dummyServiceMethod', + 'text/xml;charset=UTF-8', + file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . 'testHandleRequest.message') + ); + $response = $soapServer->handleRequest($request); + + file_put_contents(self::CACHE_DIR . '/SoapServerTestResponse.xml', $response->getContent()); + + self::assertNotContains("\r\n", $response->getContent(), 'Response cannot contain CRLF line endings'); + self::assertContains('dummyServiceMethodResponse', $response->getContent()); + self::assertSame('DummyService.dummyServiceMethod', $response->getAction()); + self::assertFalse($response->hasAttachments(), 'Response should not contain attachments'); + } + + public function testHandleRequestWithSwa() + { + $dummyService = new DummyService(); + $classMap = new ClassMap(); + foreach ($dummyService->getClassMap() as $type => $className) { + $classMap->add($type, $className); + } + $soapServerBuilder = new SoapServerBuilder(); + $soapServerOptions = SoapServerOptionsBuilder::createWithDefaults($dummyService); + $soapOptions = SoapOptionsBuilder::createSwaWithClassMap($dummyService->getWsdlPath(), $classMap); + $soapServer = $soapServerBuilder->build($soapServerOptions, $soapOptions); + + $request = $soapServer->createRequest( + $dummyService->getEndpoint(), + 'DummyService.dummyServiceMethodWithAttachments', + 'text/xml;charset=UTF-8', + file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . 'testHandleRequestWithSwa.message') + ); + $response = $soapServer->handleRequest($request); + + file_put_contents(self::CACHE_DIR . '/SoapServerTestResponseFromSwaRequestWithNoAttachments.xml', $response->getContent()); + + self::assertNotContains("\r\n", $response->getContent(), 'Response cannot contain CRLF line endings'); + self::assertContains('dummyServiceMethodWithAttachmentsResponse', $response->getContent()); + self::assertSame('DummyService.dummyServiceMethodWithAttachments', $response->getAction()); + self::assertFalse($response->hasAttachments(), 'Response should contain attachments'); + } + + public function testHandleRequestWithSwaResponse() + { + $dummyService = new DummyService(); + $classMap = new ClassMap(); + foreach ($dummyService->getClassMap() as $type => $className) { + $classMap->add($type, $className); + } + $soapServerBuilder = new SoapServerBuilder(); + $soapServerOptions = SoapServerOptionsBuilder::createWithDefaults($dummyService); + $soapOptions = SoapOptionsBuilder::createSwaWithClassMap($dummyService->getWsdlPath(), $classMap); + $soapServer = $soapServerBuilder->build($soapServerOptions, $soapOptions); + + $request = $soapServer->createRequest( + $dummyService->getEndpoint(), + 'DummyService.dummyServiceMethodWithAttachments', + 'multipart/related; type="text/xml"; start=""; boundary="----=_Part_6_2094841787.1482231370463"', + file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . 'testHandleRequestWithSwa.mimepart.message') + ); + $response = $soapServer->handleRequest($request); + + file_put_contents(self::CACHE_DIR . '/SoapServerTestSwaResponseWithAttachments.xml', $response->getContent()); + + self::assertNotContains("\r\n", $response->getContent(), 'Response cannot contain CRLF line endings'); + self::assertContains('dummyServiceMethodWithAttachmentsResponse', $response->getContent()); + self::assertSame('DummyService.dummyServiceMethodWithAttachments', $response->getAction()); + self::assertTrue($response->hasAttachments(), 'Response should contain attachments'); + self::assertCount(2, $response->getAttachments()); } public function getSoapServerBuilder() diff --git a/src/BeSimple/SoapServer/Tests/SoapServerHandler.php b/src/BeSimple/SoapServer/Tests/SoapServerHandler.php new file mode 100644 index 0000000..6758491 --- /dev/null +++ b/src/BeSimple/SoapServer/Tests/SoapServerHandler.php @@ -0,0 +1,7 @@ + + + + admin + + + + + + 1 + + + + diff --git a/src/BeSimple/SoapServer/Tests/testHandleRequestWithSwa.message b/src/BeSimple/SoapServer/Tests/testHandleRequestWithSwa.message new file mode 100644 index 0000000..70aeaf9 --- /dev/null +++ b/src/BeSimple/SoapServer/Tests/testHandleRequestWithSwa.message @@ -0,0 +1,15 @@ + + + + admin + + + + + + 2 + false + + + + diff --git a/src/BeSimple/SoapServer/Tests/testHandleRequestWithSwa.mimepart.message b/src/BeSimple/SoapServer/Tests/testHandleRequestWithSwa.mimepart.message new file mode 100644 index 0000000..a2b7e76 --- /dev/null +++ b/src/BeSimple/SoapServer/Tests/testHandleRequestWithSwa.mimepart.message @@ -0,0 +1,62 @@ + +------=_Part_6_2094841787.1482231370463 +Content-Type: text/xml; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Content-ID: + + + + + admin + + + + + + 3 + true + + + + +------=_Part_6_2094841787.1482231370463 +Content-Type: text/html; charset=us-ascii; name=test-page.html +Content-Transfer-Encoding: 7bit +Content-ID: +Content-Disposition: attachment; name="test-page.html"; filename="test-page.html" + + + + + + Test file page + + + + +

Hello World!

+ + + +------=_Part_6_2094841787.1482231370463 +Content-Type: application/x-sh; name=testscript.sh +Content-Transfer-Encoding: binary +Content-ID: +Content-Disposition: attachment; name="testscript.sh"; filename="testscript.sh" + +#!/bin/sh +### ====================================================================== ### +## ## +## Test Script ## +## ## +### ====================================================================== ### + +------=_Part_6_2094841787.1482231370463--