diff --git a/src/BeSimple/SoapClient/MimeFilter.php b/src/BeSimple/SoapClient/MimeFilter.php index 86a0912..d217680 100644 --- a/src/BeSimple/SoapClient/MimeFilter.php +++ b/src/BeSimple/SoapClient/MimeFilter.php @@ -37,12 +37,12 @@ class MimeFilter implements SoapRequestFilter, SoapResponseFilter $soapPart = new MimePart($request->getContent(), 'text/xml', 'utf-8', MimePart::ENCODING_EIGHT_BIT); $soapVersion = $request->getVersion(); - if ($soapVersion == SOAP_1_1 && $attachmentType & Helper::ATTACHMENTS_TYPE_MTOM) { + if ($soapVersion === SOAP_1_1 && $attachmentType & Helper::ATTACHMENTS_TYPE_MTOM) { $multipart->setHeader('Content-Type', 'type', 'application/xop+xml'); $multipart->setHeader('Content-Type', 'start-info', 'text/xml'); $soapPart->setHeader('Content-Type', 'application/xop+xml'); $soapPart->setHeader('Content-Type', 'type', 'text/xml'); - } elseif ($soapVersion == SOAP_1_2) { + } elseif ($soapVersion === SOAP_1_2) { $multipart->setHeader('Content-Type', 'type', 'application/soap+xml'); $soapPart->setHeader('Content-Type', 'application/soap+xml'); } diff --git a/src/BeSimple/SoapClient/SoapClient.php b/src/BeSimple/SoapClient/SoapClient.php index 25215b4..17a8ea9 100644 --- a/src/BeSimple/SoapClient/SoapClient.php +++ b/src/BeSimple/SoapClient/SoapClient.php @@ -19,6 +19,8 @@ use BeSimple\SoapClient\Curl\CurlOptionsBuilder; use BeSimple\SoapClient\Curl\CurlResponse; use BeSimple\SoapClient\SoapOptions\SoapClientOptions; use BeSimple\SoapCommon\Fault\SoapFaultEnum; +use BeSimple\SoapCommon\Fault\SoapFaultPrefixEnum; +use BeSimple\SoapCommon\Fault\SoapFaultSourceGetter; use BeSimple\SoapCommon\Mime\PartFactory; use BeSimple\SoapCommon\SoapKernel; use BeSimple\SoapCommon\SoapOptions\SoapOptions; @@ -38,8 +40,11 @@ use SoapFault; */ class SoapClient extends \SoapClient { + /** @var SoapClientOptions */ protected $soapClientOptions; + /** @var SoapOptions */ protected $soapOptions; + /** @var Curl */ private $curl; /** @var SoapAttachment[] */ private $soapAttachmentsOnRequestStorage; @@ -110,12 +115,38 @@ class SoapClient extends \SoapClient public function soapCall($functionName, array $arguments, array $soapAttachments = [], array $options = null, $inputHeaders = null, array &$outputHeaders = null) { $this->setSoapAttachmentsOnRequestToStorage($soapAttachments); - $soapResponseAsObject = parent::__soapCall($functionName, $arguments, $options, $inputHeaders, $outputHeaders); + try { - $soapResponse = $this->getSoapResponseFromStorage(); - $soapResponse->setResponseObject($soapResponseAsObject); + $soapResponseAsObject = parent::__soapCall($functionName, $arguments, $options, $inputHeaders, $outputHeaders); + $soapResponse = $this->getSoapResponseFromStorage(); + $soapResponse->setResponseObject($soapResponseAsObject); - return $soapResponse; + return $soapResponse; + + } catch (SoapFault $soapFault) { + if (SoapFaultSourceGetter::isNativeSoapFault($soapFault)) { + $soapResponse = $this->getSoapResponseFromStorage(); + if ($soapResponse instanceof SoapResponse) { + $soapFault = $this->throwSoapFaultByTracing( + SoapFaultPrefixEnum::PREFIX_PHP . '-' . $soapFault->getCode(), + $soapFault->getMessage(), + new SoapResponseTracingData( + 'Content-Type: ' . $soapResponse->getRequest()->getContentType(), + $soapResponse->getRequest()->getContent(), + 'Content-Type: ' . $soapResponse->getContentType(), + $soapResponse->getResponseContent() + ) + ); + } else { + $soapFault = new SoapFault( + SoapFaultPrefixEnum::PREFIX_PHP . '-unresolved', + 'Got SoapFault message with no response: '.$soapFault->getMessage() + ); + } + } + + throw $soapFault; + } } /** @@ -268,7 +299,6 @@ class SoapClient extends \SoapClient if ($curlResponse->curlStatusSuccess()) { $soapResponse = $this->returnSoapResponseByTracing( - $this->soapClientOptions->getTrace(), $soapRequest, $curlResponse, $soapResponseTracingData @@ -288,16 +318,14 @@ class SoapClient extends \SoapClient } else if ($curlResponse->curlStatusFailed()) { return $this->throwSoapFaultByTracing( - $this->soapClientOptions->getTrace(), - SoapFaultEnum::SOAP_FAULT_HTTP.'-'.$curlResponse->getHttpResponseStatusCode(), + SoapFaultPrefixEnum::PREFIX_DEFAULT.'-'.SoapFaultEnum::SOAP_FAULT_HTTP.'-'.$curlResponse->getHttpResponseStatusCode(), $curlResponse->getCurlErrorMessage(), $soapResponseTracingData ); } else { return $this->throwSoapFaultByTracing( - $this->soapClientOptions->getTrace(), - SoapFaultEnum::SOAP_FAULT_SOAP_CLIENT_ERROR, + SoapFaultPrefixEnum::PREFIX_DEFAULT.'-'.SoapFaultEnum::SOAP_FAULT_SOAP_CLIENT_ERROR, 'Cannot process curl response with unresolved status: ' . $curlResponse->getCurlStatus(), $soapResponseTracingData ); @@ -341,19 +369,16 @@ class SoapClient extends \SoapClient } private function returnSoapResponseByTracing( - $isTracingEnabled, SoapRequest $soapRequest, CurlResponse $curlResponse, SoapResponseTracingData $soapResponseTracingData, array $soapAttachments = [] ) { - if ($isTracingEnabled === true) { + if ($this->soapClientOptions->getTrace() === true) { return SoapResponseFactory::createWithTracingData( + $soapRequest, $curlResponse->getResponseBody(), - $soapRequest->getLocation(), - $soapRequest->getAction(), - $soapRequest->getVersion(), $curlResponse->getHttpResponseContentType(), $soapResponseTracingData, $soapAttachments @@ -372,9 +397,15 @@ class SoapClient extends \SoapClient } } - private function throwSoapFaultByTracing($isTracingEnabled, $soapFaultCode, $soapFaultMessage, SoapResponseTracingData $soapResponseTracingData) + /** + * @param string $soapFaultCode + * @param string $soapFaultMessage + * @param SoapResponseTracingData $soapResponseTracingData + * @throws SoapFault + */ + private function throwSoapFaultByTracing($soapFaultCode, $soapFaultMessage, SoapResponseTracingData $soapResponseTracingData) { - if ($isTracingEnabled === true) { + if ($this->soapClientOptions->getTrace() === true) { throw new SoapFaultWithTracingData( $soapFaultCode, diff --git a/src/BeSimple/SoapClient/SoapResponse.php b/src/BeSimple/SoapClient/SoapResponse.php index adc146b..587ca88 100644 --- a/src/BeSimple/SoapClient/SoapResponse.php +++ b/src/BeSimple/SoapClient/SoapResponse.php @@ -2,6 +2,7 @@ namespace BeSimple\SoapClient; +use BeSimple\SoapCommon\SoapRequest; use BeSimple\SoapCommon\SoapResponse as CommonSoapResponse; class SoapResponse extends CommonSoapResponse @@ -10,6 +11,8 @@ class SoapResponse extends CommonSoapResponse protected $responseObject; /** @var SoapResponseTracingData */ protected $tracingData; + /** @var SoapRequest */ + protected $request; public function getResponseContent() { @@ -40,4 +43,14 @@ class SoapResponse extends CommonSoapResponse { $this->tracingData = $tracingData; } + + public function setRequest(SoapRequest $request) + { + $this->request = $request; + } + + public function getRequest() + { + return $this->request; + } } diff --git a/src/BeSimple/SoapClient/SoapResponseFactory.php b/src/BeSimple/SoapClient/SoapResponseFactory.php index d2caf3d..0c88c28 100644 --- a/src/BeSimple/SoapClient/SoapResponseFactory.php +++ b/src/BeSimple/SoapClient/SoapResponseFactory.php @@ -14,6 +14,7 @@ namespace BeSimple\SoapClient; use BeSimple\SoapBundle\Soap\SoapAttachment; use BeSimple\SoapCommon\Mime\PartFactory; +use BeSimple\SoapCommon\SoapRequest; /** * SoapResponseFactory for SoapClient. Provides factory function for SoapResponse object. @@ -50,7 +51,7 @@ class SoapResponseFactory $response->setContentType($contentType); if (count($attachments) > 0) { $response->setAttachments( - self::createAttachmentParts($attachments) + PartFactory::createAttachmentParts($attachments) ); } @@ -60,29 +61,26 @@ class SoapResponseFactory /** * Factory method for SoapClient\SoapResponse with SoapResponseTracingData. * + * @param SoapRequest $soapRequest related request object * @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( + SoapRequest $soapRequest, $content, - $location, - $action, - $version, $contentType, SoapResponseTracingData $tracingData, array $attachments = [] ) { $response = new SoapResponse(); + $response->setRequest($soapRequest); $response->setContent($content); - $response->setLocation($location); - $response->setAction($action); - $response->setVersion($version); + $response->setLocation($soapRequest->getLocation()); + $response->setAction($soapRequest->getAction()); + $response->setVersion($soapRequest->getVersion()); $response->setContentType($contentType); if ($tracingData !== null) { $response->setTracingData($tracingData); diff --git a/src/BeSimple/SoapCommon/Fault/SoapFaultPrefixEnum.php b/src/BeSimple/SoapCommon/Fault/SoapFaultPrefixEnum.php new file mode 100644 index 0000000..a864f79 --- /dev/null +++ b/src/BeSimple/SoapCommon/Fault/SoapFaultPrefixEnum.php @@ -0,0 +1,9 @@ +getCode(), $defaultPrefix) === 0) { + + return false; + } + + return true; + } +} diff --git a/src/BeSimple/SoapCommon/Mime/Boundary/MimeBoundaryAnalyser.php b/src/BeSimple/SoapCommon/Mime/Boundary/MimeBoundaryAnalyser.php new file mode 100644 index 0000000..5192d14 --- /dev/null +++ b/src/BeSimple/SoapCommon/Mime/Boundary/MimeBoundaryAnalyser.php @@ -0,0 +1,51 @@ + 0 && $mimeMessageLine[0] === "-"; + } + + /** + * @param string $mimeMessageLine + * @param string $mimeTypeBoundary + * @return bool + */ + public static function isMessageLineMiddleBoundary($mimeMessageLine, $mimeTypeBoundary) + { + return strcmp(trim($mimeMessageLine), '--'.$mimeTypeBoundary) === 0; + } + + /** + * @param string $mimeMessageLine + * @param string $mimeTypeBoundary + * @return bool + */ + public static function isMessageLineLastBoundary($mimeMessageLine, $mimeTypeBoundary) + { + return strcmp(trim($mimeMessageLine), '--'.$mimeTypeBoundary.'--') === 0; + } +} diff --git a/src/BeSimple/SoapCommon/Mime/Parser.php b/src/BeSimple/SoapCommon/Mime/Parser.php index 0888d65..c39b5ad 100644 --- a/src/BeSimple/SoapCommon/Mime/Parser.php +++ b/src/BeSimple/SoapCommon/Mime/Parser.php @@ -12,9 +12,10 @@ namespace BeSimple\SoapCommon\Mime; +use BeSimple\SoapCommon\Mime\Boundary\MimeBoundaryAnalyser; use BeSimple\SoapCommon\Mime\Parser\ContentTypeParser; -use BeSimple\SoapCommon\Mime\Parser\ParsedPart; use BeSimple\SoapCommon\Mime\Parser\ParsedPartList; +use BeSimple\SoapCommon\Mime\Parser\ParsedPartsGetter; use Exception; /** @@ -25,9 +26,6 @@ use Exception; */ 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. * @@ -39,16 +37,26 @@ class Parser public static function parseMimeMessage($mimeMessage, array $headers = []) { $multiPart = new MultiPart(); - $mimeMessageLines = preg_split("/(\n)/", $mimeMessage); + $mimeMessageLines = explode("\n", $mimeMessage); + $mimeMessageLineCount = count($mimeMessageLines); + if ($mimeMessageLineCount <= 1) { + throw new Exception( + sprintf( + 'Cannot process message of %d characters: got unexpectable low number of lines: %s', + mb_strlen($mimeMessage), + (string)$mimeMessageLineCount + ) + ); + } // add given headers, e.g. coming from HTTP headers if (count($headers) > 0) { self::setMultiPartHeaders($multiPart, $headers); - $hasHttpRequestHeaders = self::HAS_HTTP_REQUEST_HEADERS; + $hasHttpRequestHeaders = ParsedPartsGetter::HAS_HTTP_REQUEST_HEADERS; } else { - $hasHttpRequestHeaders = self::HAS_NO_HTTP_REQUEST_HEADERS; + $hasHttpRequestHeaders = ParsedPartsGetter::HAS_NO_HTTP_REQUEST_HEADERS; } - if (self::hasBoundary($mimeMessageLines)) { - $parsedPartList = self::getPartsFromMimeMessageLines( + if (MimeBoundaryAnalyser::hasMessageBoundary($mimeMessageLines) === true) { + $parsedPartList = ParsedPartsGetter::getPartsFromMimeMessageLines( $multiPart, $mimeMessageLines, $hasHttpRequestHeaders @@ -79,115 +87,6 @@ class Parser 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 (strpos($currentHeader, ':') !== false) { - list($headerName, $headerValue) = explode(':', $currentHeader, 2); - $headerValueWithNoCrAtTheEnd = trim($headerValue); - $headerValue = iconv_mime_decode($headerValueWithNoCrAtTheEnd, 0, Part::CHARSET_UTF8); - $parsedMimeHeaders = ContentTypeParser::parseContentTypeHeader($headerName, $headerValue); - foreach ($parsedMimeHeaders as $parsedMimeHeader) { - $currentPart->setHeader( - $parsedMimeHeader->getName(), - $parsedMimeHeader->getValue(), - $parsedMimeHeader->getSubValue() - ); - } - $contentTypeBoundary = $multiPart->getHeader('Content-Type', 'boundary'); - $contentTypeContentIdStart = $multiPart->getHeader('Content-Type', 'start'); - } - unset($currentHeader); - } - if ($inHeader === true) { - if (trim($mimeMessageLine) == '') { - $inHeader = false; - continue; - } - $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 - 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); - } - } - $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 { - // 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"; - } - } - } - - return new ParsedPartList($parsedParts); - } - /** * @param ParsedPartList $parsedPartList * @param MultiPart $multiPart @@ -224,56 +123,4 @@ class Parser } } } - - /** - * 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, $partStringContent) - { - $encoding = strtolower($part->getHeader('Content-Transfer-Encoding')); - $charset = strtolower($part->getHeader('Content-Type', 'charset')); - - if ($encoding === Part::ENCODING_BASE64) { - $partStringContent = base64_decode($partStringContent); - } else if ($encoding === Part::ENCODING_QUOTED_PRINTABLE) { - $partStringContent = quoted_printable_decode($partStringContent); - } - - if ($charset !== Part::CHARSET_UTF8) { - return iconv($charset, Part::CHARSET_UTF8, $partStringContent); - } - - return $partStringContent; - } - - private static function hasBoundary(array $lines) - { - foreach ($lines as $line) { - if (self::isBoundary($line)) { - - return true; - } - } - - return false; - } - - private static function isBoundary($mimeMessageLine) - { - 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/ParsedPartsGetter.php b/src/BeSimple/SoapCommon/Mime/Parser/ParsedPartsGetter.php new file mode 100644 index 0000000..b277f2d --- /dev/null +++ b/src/BeSimple/SoapCommon/Mime/Parser/ParsedPartsGetter.php @@ -0,0 +1,179 @@ +getHeader('Content-Type', 'boundary'); + if ($contentTypeBoundary === null) { + throw new Exception( + 'Unable to get Content-Type boundary from given MultiPart: ' . var_export($multiPart->getHeaders(), true) + ); + } + $contentTypeContentIdStart = $multiPart->getHeader('Content-Type', 'start'); + if ($contentTypeContentIdStart === null) { + throw new Exception( + 'Unable to get Content-Type start from given MultiPart: ' . var_export($multiPart->getHeaders(), true) + ); + } + $currentPart = $multiPart; + $messagePartStringContent = ''; + $inHeader = $hasHttpHeaders; + $hitFirstBoundary = false; + foreach ($mimeMessageLines as $mimeMessageLine) { + 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 (strpos($currentHeader, ':') !== false) { + list($headerName, $headerValue) = explode(':', $currentHeader, 2); + $headerValueWithNoCrAtTheEnd = trim($headerValue); + try { + $headerValue = iconv_mime_decode($headerValueWithNoCrAtTheEnd, 0, Part::CHARSET_UTF8); + } catch (Exception $e) { + if ($hitFirstBoundary === false) { + throw new Exception( + 'Unable to parse message: cannot parse headers before hitting the first boundary' + ); + } + throw new Exception( + sprintf( + 'Unable to get header value: possible parsing message contents of %s characters in header parser: %s', + mb_strlen($headerValueWithNoCrAtTheEnd), + $e->getMessage() + ) + ); + } + $parsedMimeHeaders = ContentTypeParser::parseContentTypeHeader($headerName, $headerValue); + foreach ($parsedMimeHeaders as $parsedMimeHeader) { + $currentPart->setHeader( + $parsedMimeHeader->getName(), + $parsedMimeHeader->getValue(), + $parsedMimeHeader->getSubValue() + ); + } + $contentTypeBoundary = $multiPart->getHeader('Content-Type', 'boundary'); + $contentTypeContentIdStart = $multiPart->getHeader('Content-Type', 'start'); + } + unset($currentHeader); + } + if ($inHeader === true) { + if (trim($mimeMessageLine) === '') { + $inHeader = false; + continue; + } + $currentHeader = $mimeMessageLine; + continue; + } else { + if (MimeBoundaryAnalyser::isMessageLineBoundary($mimeMessageLine)) { + if (MimeBoundaryAnalyser::isMessageLineMiddleBoundary($mimeMessageLine, $contentTypeBoundary)) { + if ($currentPart instanceof Part) { + $currentPartContent = self::decodeContent( + $currentPart, + substr($messagePartStringContent, 0, -1) + ); + if ($currentPartContent[strlen($currentPartContent) - 1] === "\r") { + // temporary hack: if there is a CRLF before any middle boundary, then the remaining CR must be removed + $currentPartContent = substr($currentPartContent, 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); + } + } + $currentPart = new Part(); + $hitFirstBoundary = true; + $inHeader = true; + $messagePartStringContent = ''; + } else if (MimeBoundaryAnalyser::isMessageLineLastBoundary($mimeMessageLine, $contentTypeBoundary)) { + $currentPartContent = self::decodeContent( + $currentPart, + substr($messagePartStringContent, 0, -1) + ); + if ($currentPartContent[strlen($currentPartContent) - 1] === "\r") { + // temporary hack: if there is a CRLF before last boundary, then the remaining CR must be removed + $currentPartContent = substr($currentPartContent, 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 { + // 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"; + } + } + } + + return new ParsedPartList($parsedParts); + } + + /** + * 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, $partStringContent) + { + $encoding = strtolower($part->getHeader('Content-Transfer-Encoding')); + $charset = strtolower($part->getHeader('Content-Type', 'charset')); + + if ($encoding === Part::ENCODING_BASE64) { + $partStringContent = base64_decode($partStringContent); + } else if ($encoding === Part::ENCODING_QUOTED_PRINTABLE) { + $partStringContent = quoted_printable_decode($partStringContent); + } + + if ($charset !== Part::CHARSET_UTF8) { + return iconv($charset, Part::CHARSET_UTF8, $partStringContent); + } + + return $partStringContent; + } +} diff --git a/src/BeSimple/SoapCommon/Mime/PartHeader.php b/src/BeSimple/SoapCommon/Mime/PartHeader.php index 1b58fff..b9453aa 100644 --- a/src/BeSimple/SoapCommon/Mime/PartHeader.php +++ b/src/BeSimple/SoapCommon/Mime/PartHeader.php @@ -75,6 +75,11 @@ abstract class PartHeader return null; } + public function getHeaders() + { + return $this->headers; + } + /** * Generate headers. * diff --git a/src/BeSimple/SoapCommon/SoapMessage.php b/src/BeSimple/SoapCommon/SoapMessage.php index a11c5e6..f528ec3 100644 --- a/src/BeSimple/SoapCommon/SoapMessage.php +++ b/src/BeSimple/SoapCommon/SoapMessage.php @@ -13,6 +13,8 @@ namespace BeSimple\SoapCommon; +use DOMDocument; + /** * Base class for SoapRequest and SoapResponse. * @@ -73,13 +75,6 @@ abstract class SoapMessage */ protected $content; - /** - * - * Enter description here ... - * @var \DOMDocument - */ - protected $contentDomDocument = null; - /** * Message content type. * @@ -170,11 +165,6 @@ abstract class SoapMessage */ public function getContent() { - if ($this->contentDomDocument !== null) { - $this->content = $this->contentDomDocument->saveXML(); - $this->contentDomDocument = null; - } - return $this->content; } @@ -186,9 +176,6 @@ abstract class SoapMessage public function setContent($content) { $this->content = $content; - if (null !== $this->contentDomDocument) { - $this->contentDomDocument->loadXML($this->content); - } } /** @@ -198,12 +185,10 @@ abstract class SoapMessage */ public function getContentDocument() { - if (null === $this->contentDomDocument) { - $this->contentDomDocument = new \DOMDocument(); - $this->contentDomDocument->loadXML($this->content); - } + $contentDomDocument = new DOMDocument(); + $contentDomDocument->loadXML($this->content); - return $this->contentDomDocument; + return $contentDomDocument; } /** diff --git a/src/BeSimple/SoapServer/SoapServer.php b/src/BeSimple/SoapServer/SoapServer.php index 38e64f5..dcc4eba 100644 --- a/src/BeSimple/SoapServer/SoapServer.php +++ b/src/BeSimple/SoapServer/SoapServer.php @@ -204,8 +204,12 @@ class SoapServer extends \SoapServer $attachments = []; if ($soapRequest->hasAttachments()) { foreach ($soapRequest->getAttachments() as $attachment) { + $fileName = $attachment->getHeader('Content-Disposition', 'filename'); + if ($fileName === null) { + $fileName = basename($attachment->getHeader('Content-Location')); + } $attachments[] = new SoapAttachment( - $attachment->getHeader('Content-Disposition', 'filename'), + $fileName, $attachment->getHeader('Content-Type'), $attachment->getContent() ); diff --git a/src/BeSimple/SoapServer/SoapServerOptionsBuilder.php b/src/BeSimple/SoapServer/SoapServerOptionsBuilder.php index 3c3ed45..f393363 100644 --- a/src/BeSimple/SoapServer/SoapServerOptionsBuilder.php +++ b/src/BeSimple/SoapServer/SoapServerOptionsBuilder.php @@ -6,7 +6,7 @@ use BeSimple\SoapServer\SoapOptions\SoapServerOptions; class SoapServerOptionsBuilder { - static public function createWithDefaults($handlerClassOrObject) + public static function createWithDefaults($handlerClassOrObject) { return new SoapServerOptions( $handlerClassOrObject, diff --git a/tests/BeSimple/SoapClient/SoapClientTest.php b/tests/BeSimple/SoapClient/SoapClientTest.php index 1542814..defa714 100644 --- a/tests/BeSimple/SoapClient/SoapClientTest.php +++ b/tests/BeSimple/SoapClient/SoapClientTest.php @@ -3,6 +3,8 @@ namespace BeSimple\SoapClient; use BeSimple\SoapBundle\Soap\SoapAttachment; +use BeSimple\SoapClient\Curl\CurlOptions; +use BeSimple\SoapClient\SoapOptions\SoapClientOptions; use BeSimple\SoapCommon\ClassMap; use BeSimple\SoapCommon\SoapOptions\SoapOptions; use BeSimple\SoapCommon\SoapOptionsBuilder; @@ -182,8 +184,14 @@ class SoapClientTest extends PHPUnit_Framework_TestCase 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(self::FIXTURES_DIR.'/soapRequestWithTwoAttachments.request')), - $this->removeOneTimeData($tracingData->getLastRequest()), + $this->removeOneTimeData( + file_get_contents( + self::FIXTURES_DIR.'/Message/Request/GetUKLocationByCounty.request.mimepart.message' + ) + ), + $this->removeOneTimeData( + $tracingData->getLastRequest() + ), 'Requests must match after onetime data were removed' ); } @@ -216,7 +224,7 @@ class SoapClientTest extends PHPUnit_Framework_TestCase self::assertNotContains('start="getLastRequestHeaders(), 'Headers should link to first MultiPart'); self::assertContains('action="', $tracingData->getLastRequestHeaders(), 'Headers should contain SOAP action'); self::assertStringEqualsFile( - self::FIXTURES_DIR.'/soapRequestWithNoAttachments.request', + self::FIXTURES_DIR.'/Message/Request/GetUKLocationByCounty.request.message', $tracingData->getLastRequest(), 'Requests must match' ); diff --git a/tests/BeSimple/SoapCommon/Mime/Boundary/MimeBoundaryAnalyserTest.php b/tests/BeSimple/SoapCommon/Mime/Boundary/MimeBoundaryAnalyserTest.php new file mode 100644 index 0000000..a872a3a --- /dev/null +++ b/tests/BeSimple/SoapCommon/Mime/Boundary/MimeBoundaryAnalyserTest.php @@ -0,0 +1,75 @@ +setExpectedException(Exception::class, $failedTestCaseFailMessage); + } + $parsedPartsList = ParsedPartsGetter::getPartsFromMimeMessageLines( + $multiPart, + $mimeMessageLines, + $hasHttpRequestHeaders + ); + + if ($testCaseShouldFail === false) { + self::assertInstanceOf(ParsedPartList::class, $parsedPartsList); + self::assertGreaterThanOrEqual(3, $parsedPartsList->getPartCount()); + self::assertTrue($parsedPartsList->hasExactlyOneMainPart()); + } + } + + public function provideMimeMessageLines() + { + $mimePartWithHeadersForSwaResponse = new MultiPart(); + $mimePartWithHeadersForSwaResponse->setHeader('Content-Type', 'boundary', 'Part_13_58e3bc35f3743.58e3bc35f376f'); + $mimePartWithHeadersForSwaResponse->setHeader('Content-Type', 'start', ''); + + $mimePartWithWrongHeadersForSwaResponse = new MultiPart(); + $mimePartWithWrongHeadersForSwaResponse->setHeader('Content-Type', 'boundary', 'non-existing'); + $mimePartWithWrongHeadersForSwaResponse->setHeader('Content-Type', 'start', ''); + + $mimePartWithHeadersForSwaRequest = new MultiPart(); + $mimePartWithHeadersForSwaRequest->setHeader('Content-Type', 'boundary', '----=_Part_6_2094841787.1482231370463'); + $mimePartWithHeadersForSwaRequest->setHeader('Content-Type', 'start', ''); + + return [ + 'ParseSwaResponseWith2FilesAnd1BinaryFile' => [ + $mimePartWithHeadersForSwaResponse, + $this->getMessageLinesFromMimeMessage(__DIR__.'/../../../../Fixtures/Message/Response/dummyServiceMethodWithOutgoingLargeSwa.response.mimepart.message'), + ParsedPartsGetter::HAS_HTTP_REQUEST_HEADERS, + self::TEST_CASE_SHOULD_NOT_FAIL + ], + 'ParseSwaResponseWith2FilesAnd1BinaryFileShouldFailWithNoHeaders' => [ + new MultiPart(), + $this->getMessageLinesFromMimeMessage(__DIR__.'/../../../../Fixtures/Message/Response/dummyServiceMethodWithOutgoingLargeSwa.response.mimepart.message'), + ParsedPartsGetter::HAS_NO_HTTP_REQUEST_HEADERS, + self::TEST_CASE_SHOULD_FAIL, + 'Unable to get Content-Type boundary' + ], + 'ParseSwaResponseWith2FilesAnd1BinaryFileShouldFailWithWrongHeaders' => [ + $mimePartWithWrongHeadersForSwaResponse, + $this->getMessageLinesFromMimeMessage(__DIR__.'/../../../../Fixtures/Message/Response/dummyServiceMethodWithOutgoingLargeSwa.response.mimepart.message'), + ParsedPartsGetter::HAS_HTTP_REQUEST_HEADERS, + self::TEST_CASE_SHOULD_FAIL, + 'cannot parse headers before hitting the first boundary' + ], + 'ParseSwaRequestWith2Files' => [ + $mimePartWithHeadersForSwaRequest, + $this->getMessageLinesFromMimeMessage(__DIR__ . '/../../../../Fixtures/Message/Request/dummyServiceMethodWithAttachments.request.mimepart.message'), + ParsedPartsGetter::HAS_HTTP_REQUEST_HEADERS, + self::TEST_CASE_SHOULD_NOT_FAIL + ], + 'ParseSwaRequestWith2FilesShouldFailWithNoHeaders' => [ + new MultiPart(), + $this->getMessageLinesFromMimeMessage(__DIR__ . '/../../../../Fixtures/Message/Request/dummyServiceMethodWithAttachments.request.mimepart.message'), + ParsedPartsGetter::HAS_NO_HTTP_REQUEST_HEADERS, + self::TEST_CASE_SHOULD_FAIL, + 'Unable to get Content-Type boundary' + ], + ]; + } + + private function getMessageLinesFromMimeMessage($filePath) + { + if (file_exists($filePath) === false) { + self::fail('Please, update tests data provider - file not found: '.$filePath); + } + + return explode("\n", file_get_contents($filePath)); + } +} diff --git a/tests/BeSimple/SoapServer/SoapServerTest.php b/tests/BeSimple/SoapServer/SoapServerTest.php index abb05bc..10f4175 100644 --- a/tests/BeSimple/SoapServer/SoapServerTest.php +++ b/tests/BeSimple/SoapServer/SoapServerTest.php @@ -46,7 +46,7 @@ class SoapServerTest extends PHPUnit_Framework_TestCase $dummyService->getEndpoint(), 'DummyService.dummyServiceMethod', 'text/xml;charset=UTF-8', - file_get_contents(self::FIXTURES_DIR.DIRECTORY_SEPARATOR.'testHandleRequest.message') + file_get_contents(self::FIXTURES_DIR.'/Message/Request/dummyServiceMethod.message.request') ); $response = $soapServer->handleRequest($request); @@ -74,7 +74,7 @@ class SoapServerTest extends PHPUnit_Framework_TestCase $dummyService->getEndpoint(), 'DummyService.dummyServiceMethodWithAttachments', 'text/xml;charset=UTF-8', - file_get_contents(self::FIXTURES_DIR.DIRECTORY_SEPARATOR.'testHandleRequestWithSwa.message') + file_get_contents(self::FIXTURES_DIR.'/Message/Request/dummyServiceMethodWithAttachments.request.message') ); $response = $soapServer->handleRequest($request); @@ -83,7 +83,7 @@ class SoapServerTest extends PHPUnit_Framework_TestCase 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'); + self::assertFalse($response->hasAttachments(), 'Response should not contain attachments'); } public function testHandleRequestWithSwaResponse() @@ -102,7 +102,7 @@ class SoapServerTest extends PHPUnit_Framework_TestCase $dummyService->getEndpoint(), 'DummyService.dummyServiceMethodWithAttachments', 'multipart/related; type="text/xml"; start=""; boundary="----=_Part_6_2094841787.1482231370463"', - file_get_contents(self::FIXTURES_DIR.DIRECTORY_SEPARATOR.'testHandleRequestWithSwa.mimepart.message') + file_get_contents(self::FIXTURES_DIR.'/Message/Request/dummyServiceMethodWithAttachments.request.mimepart.message') ); $response = $soapServer->handleRequest($request); diff --git a/tests/BeSimple/SoapServerAndSoapClientCommunicationTest.php b/tests/BeSimple/SoapServerAndSoapClientCommunicationTest.php new file mode 100644 index 0000000..d2e476b --- /dev/null +++ b/tests/BeSimple/SoapServerAndSoapClientCommunicationTest.php @@ -0,0 +1,266 @@ +localWebServerProcess = popen('php -S localhost:8000 > /dev/null 2>&1 &', 'r'); + } + + public function tearDown() + { + pclose($this->localWebServerProcess); + } + + public function testHandleRequestWithLargeSwaResponse() + { + $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.dummyServiceMethodWithOutgoingLargeSwa', + 'text/xml;charset=UTF-8', + file_get_contents(self::FIXTURES_DIR.'/Message/Request/dummyServiceMethodWithOutgoingLargeSwa.request.message') + ); + $response = $soapServer->handleRequest($request); + + file_put_contents(self::CACHE_DIR . '/content-type-soap-server-response.xml', $response->getContentType()); + file_put_contents(self::CACHE_DIR . '/multipart-message-soap-server-response.xml', $response->getContent()); + if ($response->hasAttachments() === true) { + foreach ($response->getAttachments() as $attachment) { + $fileName = preg_replace('/\<|\>/', '', $attachment->getContentId()); + file_put_contents(self::CACHE_DIR . DIRECTORY_SEPARATOR . 'attachment-server-response-' . $fileName, $attachment->getContent()); + + self::assertRegExp('/filename\.(docx|html|txt)/', $fileName); + } + } else { + self::fail('Response should contain attachments'); + } + + self::assertContains('dummyServiceMethodWithOutgoingLargeSwaResponse', $response->getContent()); + self::assertSame('DummyService.dummyServiceMethodWithOutgoingLargeSwa', $response->getAction()); + + self::assertEquals( + filesize(self::LARGE_SWA_FILE), + filesize(self::CACHE_DIR.'/attachment-server-response-filename.docx'), + 'File cannot differ after transport from SoapClient to SoapServer' + ); + } + + public function testSoapCallSwaWithLargeSwaResponse() + { + $soapClient = $this->getSoapBuilder()->buildWithSoapHeader( + SoapClientOptionsBuilder::createWithEndpointLocation( + self::TEST_HTTP_URL.'/SwaSenderEndpoint.php' + ), + SoapOptionsBuilder::createSwaWithClassMap( + self::TEST_HTTP_URL.'/SwaSenderEndpoint.php?wsdl', + new ClassMap([ + 'GenerateTestRequest' => GenerateTestRequest::class, + ]), + SoapOptions::SOAP_CACHE_TYPE_NONE + ), + new SoapHeader('http://schema.testcase', 'SoapHeader', [ + 'user' => 'admin', + ]) + ); + + $request = new DummyServiceMethodWithOutgoingLargeSwaRequest(); + $request->dummyAttribute = 1; + + $soapResponse = $soapClient->soapCall('dummyServiceMethodWithOutgoingLargeSwa', [$request]); + $attachments = $soapResponse->getAttachments(); + + self::assertContains('', $soapResponse->getResponseContent()); + self::assertTrue($soapResponse->hasAttachments(), 'Response should contain attachments'); + self::assertCount(3, $attachments); + + file_put_contents(self::CACHE_DIR . '/multipart-message-soap-client-response.xml', $soapResponse->getContent()); + foreach ($soapResponse->getAttachments() as $attachment) { + $fileName = preg_replace('/\<|\>/', '', $attachment->getContentId()); + file_put_contents(self::CACHE_DIR . DIRECTORY_SEPARATOR . 'attachment-client-response-' . $fileName, $attachment->getContent()); + + self::assertRegExp('/filename\.(docx|html|txt)/', $fileName); + } + + self::assertEquals( + filesize(self::LARGE_SWA_FILE), + filesize(self::CACHE_DIR.'/attachment-client-response-filename.docx'), + 'File cannot differ after transport from SoapClient to SoapServer' + ); + } + + public function testSoapCallWithLargeSwaRequest() + { + $soapClient = $this->getSoapBuilder()->buildWithSoapHeader( + SoapClientOptionsBuilder::createWithEndpointLocation( + self::TEST_HTTP_URL.'/SwaReceiverEndpoint.php' + ), + SoapOptionsBuilder::createSwaWithClassMap( + self::TEST_HTTP_URL.'/SwaReceiverEndpoint.php?wsdl', + new ClassMap([ + 'DummyServiceMethodWithIncomingLargeSwaRequest' => DummyServiceMethodWithIncomingLargeSwaRequest::class, + ]), + SoapOptions::SOAP_CACHE_TYPE_NONE + ), + new SoapHeader('http://schema.testcase', 'SoapHeader', [ + 'user' => 'admin', + ]) + ); + + $request = new DummyServiceMethodWithIncomingLargeSwaRequest(); + $request->dummyAttribute = 1; + + try { + $soapResponse = $soapClient->soapCall( + 'dummyServiceMethodWithIncomingLargeSwa', + [$request], + [ + new SoapAttachment('filename.txt', 'text/plain', 'plaintext file'), + new SoapAttachment('filename.html', 'text/html', 'Hello world'), + new SoapAttachment( + 'filename.docx', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + file_get_contents(self::LARGE_SWA_FILE) + ), + ] + ); + + self::assertContains('dummyServiceMethodWithIncomingLargeSwa', $soapResponse->getRequest()->getContent()); + self::assertContains('', $soapResponse->getResponseContent()); + self::assertTrue($soapResponse->getRequest()->hasAttachments(), 'Response MUST contain attachments'); + self::assertFalse($soapResponse->hasAttachments(), 'Response MUST NOT contain attachments'); + + foreach ($soapResponse->getRequest()->getAttachments() as $attachment) { + file_put_contents(self::CACHE_DIR . '/attachment-client-request-'.trim($attachment->getContentId(), '<>'), $attachment->getContent()); + } + file_put_contents(self::CACHE_DIR . '/content-type-soap-client-request.xml', $soapResponse->getRequest()->getContentType()); + file_put_contents(self::CACHE_DIR.'/multipart-message-soap-client-request.xml', $soapResponse->getRequest()->getContent()); + + self::assertEquals( + filesize(self::LARGE_SWA_FILE), + filesize(self::CACHE_DIR.'/attachment-client-request-filename.docx'), + 'File cannot differ after transport from SoapClient to SoapServer' + ); + + } catch (SoapFaultWithTracingData $e) { + self::fail( + 'Endpoint did not return expected response: '.var_export($e->getSoapResponseTracingData()->getLastResponse(), true) + ); + } + } + + public function testHandleRequestWithLargeSwaRequest() + { + $previousSoapClientCallContentTypeCacheFile = self::CACHE_DIR.'/content-type-soap-client-request.xml'; + $previousSoapClientCallMessageBodyCacheFile = self::CACHE_DIR.'/multipart-message-soap-client-request.xml'; + if (file_exists($previousSoapClientCallContentTypeCacheFile) === false || file_exists($previousSoapClientCallMessageBodyCacheFile) === false) { + self::fail('Cannot load data from cache: run testSoapCallWithLargeSwaRequest to get the data.'); + } + $previousSoapClientCallContentType = file_get_contents($previousSoapClientCallContentTypeCacheFile); + $previousSoapClientCallMessageBody = file_get_contents($previousSoapClientCallMessageBodyCacheFile); + + $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.dummyServiceMethodWithIncomingLargeSwa', + $previousSoapClientCallContentType, + $previousSoapClientCallMessageBody + ); + $response = $soapServer->handleRequest($request); + + self::assertContains('dummyServiceMethodWithIncomingLargeSwaResponse', $response->getContent()); + self::assertSame('DummyService.dummyServiceMethodWithIncomingLargeSwa', $response->getAction()); + self::assertEquals( + filesize(self::LARGE_SWA_FILE), + filesize(self::CACHE_DIR.'/attachment-server-request-filename.docx'), + 'File cannot differ after transport from SoapClient to SoapServer' + ); + } + + public function testHandleRequestWithLargeSwaRequestAndMixedCrLf() + { + $soapClientCallContentType = file_get_contents(self::FIXTURES_DIR.'/Message/Request/dummyServiceMethodWithIncomingLargeSwaAndMixedCrLf.contenttypeheaders'); + $soapClientCallMessageBody = file_get_contents(self::FIXTURES_DIR.'/Message/Request/dummyServiceMethodWithIncomingLargeSwaAndMixedCrLf.request.mimepart.message'); + + $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.dummyServiceMethodWithIncomingLargeSwa', + $soapClientCallContentType, + $soapClientCallMessageBody + ); + $response = $soapServer->handleRequest($request); + + self::assertContains('dummyServiceMethodWithIncomingLargeSwaResponse', $response->getContent()); + self::assertSame('DummyService.dummyServiceMethodWithIncomingLargeSwa', $response->getAction()); + self::assertEquals( + filesize(self::LARGE_SWA_FILE), + filesize(self::CACHE_DIR.'/attachment-server-request-oldfilename.docx'), + 'File cannot differ after transport from SoapClient to SoapServer' + ); + } + + private function getSoapBuilder() + { + return new SoapClientBuilder(); + } + + public function getSoapServerBuilder() + { + return new SoapServerBuilder(); + } +} diff --git a/tests/Fixtures/Attachment/MessageWithAttachmentsTrait.php b/tests/Fixtures/Attachment/MessageWithAttachmentsTrait.php new file mode 100644 index 0000000..3767947 --- /dev/null +++ b/tests/Fixtures/Attachment/MessageWithAttachmentsTrait.php @@ -0,0 +1,16 @@ +attachmentCollection !== null && $this->attachmentCollection->hasAttachments(); + } +} diff --git a/tests/Fixtures/DummyService.php b/tests/Fixtures/DummyService.php index a3df4de..0981e1e 100644 --- a/tests/Fixtures/DummyService.php +++ b/tests/Fixtures/DummyService.php @@ -34,6 +34,10 @@ class DummyService implements AttachmentsHandlerInterface 'DummyServiceResponseWithAttachments' => DummyServiceResponseWithAttachments::class, 'DummyServiceRequest' => DummyServiceRequest::class, 'DummyServiceRequestWithAttachments' => DummyServiceRequestWithAttachments::class, + 'DummyServiceMethodWithOutgoingLargeSwaRequest' => DummyServiceMethodWithOutgoingLargeSwaRequest::class, + 'DummyServiceMethodWithOutgoingLargeSwaResponse' => DummyServiceMethodWithOutgoingLargeSwaResponse::class, + 'DummyServiceMethodWithIncomingLargeSwaRequest' => DummyServiceMethodWithIncomingLargeSwaRequest::class, + 'DummyServiceMethodWithIncomingLargeSwaResponse' => DummyServiceMethodWithIncomingLargeSwaResponse::class, ]; } @@ -67,6 +71,54 @@ class DummyService implements AttachmentsHandlerInterface return $dummyServiceHandler->handle($dummyServiceRequest); } + /** + * @param DummyServiceMethodWithOutgoingLargeSwaRequest $dummyServiceRequest + * @return DummyServiceMethodWithOutgoingLargeSwaResponse + */ + public function dummyServiceMethodWithOutgoingLargeSwa(DummyServiceMethodWithOutgoingLargeSwaRequest $dummyServiceRequest) + { + $dummyServiceHandler = new DummyServiceHandlerWithOutgoingLargeSwa(); + + $dummyServiceResponseWithAttachments = $dummyServiceHandler->handle($dummyServiceRequest); + + 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; + } + + /** + * @param DummyServiceMethodWithIncomingLargeSwaRequest $dummyServiceRequest + * @return DummyServiceMethodWithIncomingLargeSwaResponse + */ + public function dummyServiceMethodWithIncomingLargeSwa(DummyServiceMethodWithIncomingLargeSwaRequest $dummyServiceRequest) + { + $dummyServiceHandler = new DummyServiceHandlerWithIncomingLargeSwa(); + $attachmentStorageContents = $this->getAttachmentStorage()->getAttachments(); + if (count($attachmentStorageContents) > 0) { + $attachments = []; + foreach ($attachmentStorageContents as $soapAttachment) { + $attachments[] = new Attachment( + $soapAttachment->getId(), + $soapAttachment->getType(), + $soapAttachment->getContent() + ); + } + $dummyServiceRequest->attachmentCollection = new AttachmentCollection($attachments); + } + + return $dummyServiceHandler->handle($dummyServiceRequest); + } + /** * @param DummyServiceRequestWithAttachments $dummyServiceRequestWithAttachments * @return DummyServiceResponseWithAttachments diff --git a/tests/Fixtures/DummyService.wsdl b/tests/Fixtures/DummyService.wsdl index 78ec604..1d97dfd 100644 --- a/tests/Fixtures/DummyService.wsdl +++ b/tests/Fixtures/DummyService.wsdl @@ -22,6 +22,26 @@ + + + + + + + + + + + + + + + + + + + + @@ -41,12 +61,24 @@ + + + + + + + + + + + + @@ -55,6 +87,14 @@ + + + + + + + + @@ -72,6 +112,26 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/tests/Fixtures/DummyServiceHandlerWithIncomingLargeSwa.php b/tests/Fixtures/DummyServiceHandlerWithIncomingLargeSwa.php new file mode 100644 index 0000000..e576304 --- /dev/null +++ b/tests/Fixtures/DummyServiceHandlerWithIncomingLargeSwa.php @@ -0,0 +1,27 @@ +hasAttachments() === true) { + foreach ($request->attachmentCollection->attachments as $attachment) { + file_put_contents( + __DIR__.'/../../cache/attachment-server-request-'.$attachment->fileName, + $attachment->content + ); + } + } + + $response = new DummyServiceMethodWithIncomingLargeSwaResponse(); + $response->status = true; + + return $response; + } +} diff --git a/tests/Fixtures/DummyServiceHandlerWithOutgoingLargeSwa.php b/tests/Fixtures/DummyServiceHandlerWithOutgoingLargeSwa.php new file mode 100644 index 0000000..f8edc2c --- /dev/null +++ b/tests/Fixtures/DummyServiceHandlerWithOutgoingLargeSwa.php @@ -0,0 +1,32 @@ +status = true; + + $response->attachmentCollection = new AttachmentCollection([ + new Attachment('filename.txt', 'text/plain', 'plaintext file'), + new Attachment('filename.html', 'text/html', 'Hello world'), + new Attachment( + 'filename.docx', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + file_get_contents(SoapServerAndSoapClientCommunicationTest::LARGE_SWA_FILE) + ), + ]); + + return $response; + } +} diff --git a/tests/Fixtures/DummyServiceMethodWithIncomingLargeSwaRequest.php b/tests/Fixtures/DummyServiceMethodWithIncomingLargeSwaRequest.php new file mode 100644 index 0000000..bde561f --- /dev/null +++ b/tests/Fixtures/DummyServiceMethodWithIncomingLargeSwaRequest.php @@ -0,0 +1,15 @@ +attachmentCollection !== null && $this->attachmentCollection->hasAttachments(); - } } diff --git a/tests/Fixtures/DummyServiceResponseWithAttachments.php b/tests/Fixtures/DummyServiceResponseWithAttachments.php index 96671a8..e47b3d4 100644 --- a/tests/Fixtures/DummyServiceResponseWithAttachments.php +++ b/tests/Fixtures/DummyServiceResponseWithAttachments.php @@ -2,22 +2,14 @@ namespace Fixtures; -use Fixtures\Attachment\AttachmentCollection; +use Fixtures\Attachment\MessageWithAttachmentsTrait; class DummyServiceResponseWithAttachments { + use MessageWithAttachmentsTrait; + /** * @var bool $status */ public $status; - - /** - * @var AttachmentCollection $attachmentCollection - */ - public $attachmentCollection; - - public function hasAttachments() - { - return $this->attachmentCollection !== null && $this->attachmentCollection->hasAttachments(); - } } diff --git a/tests/Fixtures/soapRequestWithNoAttachments.request b/tests/Fixtures/Message/Request/GetUKLocationByCounty.request.message similarity index 100% rename from tests/Fixtures/soapRequestWithNoAttachments.request rename to tests/Fixtures/Message/Request/GetUKLocationByCounty.request.message diff --git a/tests/Fixtures/soapRequestWithTwoAttachments.request b/tests/Fixtures/Message/Request/GetUKLocationByCounty.request.mimepart.message similarity index 100% rename from tests/Fixtures/soapRequestWithTwoAttachments.request rename to tests/Fixtures/Message/Request/GetUKLocationByCounty.request.mimepart.message diff --git a/tests/Fixtures/testHandleRequest.message b/tests/Fixtures/Message/Request/dummyServiceMethod.message.request similarity index 100% rename from tests/Fixtures/testHandleRequest.message rename to tests/Fixtures/Message/Request/dummyServiceMethod.message.request diff --git a/tests/Fixtures/testHandleRequestWithSwa.message b/tests/Fixtures/Message/Request/dummyServiceMethodWithAttachments.request.message similarity index 100% rename from tests/Fixtures/testHandleRequestWithSwa.message rename to tests/Fixtures/Message/Request/dummyServiceMethodWithAttachments.request.message diff --git a/tests/Fixtures/testHandleRequestWithSwa.mimepart.message b/tests/Fixtures/Message/Request/dummyServiceMethodWithAttachments.request.mimepart.message similarity index 100% rename from tests/Fixtures/testHandleRequestWithSwa.mimepart.message rename to tests/Fixtures/Message/Request/dummyServiceMethodWithAttachments.request.mimepart.message diff --git a/tests/Fixtures/Message/Request/dummyServiceMethodWithIncomingLargeSwaAndMixedCrLf.contenttypeheaders b/tests/Fixtures/Message/Request/dummyServiceMethodWithIncomingLargeSwaAndMixedCrLf.contenttypeheaders new file mode 100644 index 0000000..51d09a5 --- /dev/null +++ b/tests/Fixtures/Message/Request/dummyServiceMethodWithIncomingLargeSwaAndMixedCrLf.contenttypeheaders @@ -0,0 +1 @@ +multipart/related; type="application/soap+xml"; charset=utf-8; boundary="urn:uuid:340c4456-d650-4ddb-ae83-b13cf6077326"; start=""; action="DummyService.dummyServiceMethodWithIncomingLargeSwa" \ No newline at end of file diff --git a/tests/Fixtures/Message/Request/dummyServiceMethodWithIncomingLargeSwaAndMixedCrLf.request.mimepart.message b/tests/Fixtures/Message/Request/dummyServiceMethodWithIncomingLargeSwaAndMixedCrLf.request.mimepart.message new file mode 100644 index 0000000..e3b5522 Binary files /dev/null and b/tests/Fixtures/Message/Request/dummyServiceMethodWithIncomingLargeSwaAndMixedCrLf.request.mimepart.message differ diff --git a/tests/Fixtures/Message/Request/dummyServiceMethodWithOutgoingLargeSwa.request.message b/tests/Fixtures/Message/Request/dummyServiceMethodWithOutgoingLargeSwa.request.message new file mode 100644 index 0000000..ab21b9d --- /dev/null +++ b/tests/Fixtures/Message/Request/dummyServiceMethodWithOutgoingLargeSwa.request.message @@ -0,0 +1,14 @@ + + + + admin + + + + + + 1 + + + + diff --git a/tests/Fixtures/Message/Response/dummyServiceMethodWithIncomingLargeSwa.response.message b/tests/Fixtures/Message/Response/dummyServiceMethodWithIncomingLargeSwa.response.message new file mode 100644 index 0000000..e018129 --- /dev/null +++ b/tests/Fixtures/Message/Response/dummyServiceMethodWithIncomingLargeSwa.response.message @@ -0,0 +1,10 @@ + + + + + + true + + + + diff --git a/tests/Fixtures/Message/Response/dummyServiceMethodWithOutgoingLargeSwa.response.mimepart.message b/tests/Fixtures/Message/Response/dummyServiceMethodWithOutgoingLargeSwa.response.mimepart.message new file mode 100644 index 0000000..4ed82a2 Binary files /dev/null and b/tests/Fixtures/Message/Response/dummyServiceMethodWithOutgoingLargeSwa.response.mimepart.message differ diff --git a/tests/Fixtures/large-test-file.docx b/tests/Fixtures/large-test-file.docx new file mode 100644 index 0000000..f49ab6b Binary files /dev/null and b/tests/Fixtures/large-test-file.docx differ diff --git a/tests/SwaReceiverEndpoint.php b/tests/SwaReceiverEndpoint.php new file mode 100644 index 0000000..5eea081 --- /dev/null +++ b/tests/SwaReceiverEndpoint.php @@ -0,0 +1,15 @@ +fault( + 911, + 'Cannot load data from cache: run soap server testHandleRequestWithLargeSwaResponse to get the data.' + ); +} + +header('Content-type: '.file_get_contents($contentTypeFromCache)); +echo file_get_contents($multiPartMessageFromCache);