From 574ad003967853e43eb1512173c04e6759537d96 Mon Sep 17 00:00:00 2001 From: Andreas Schamberger Date: Sat, 22 Oct 2011 11:25:26 +0200 Subject: [PATCH 1/5] mime parser --- src/BeSimple/SoapCommon/Mime/MultiPart.php | 180 +++++++++++++++++++ src/BeSimple/SoapCommon/Mime/Parser.php | 183 ++++++++++++++++++++ src/BeSimple/SoapCommon/Mime/Part.php | 165 ++++++++++++++++++ src/BeSimple/SoapCommon/Mime/PartHeader.php | 114 ++++++++++++ 4 files changed, 642 insertions(+) create mode 100644 src/BeSimple/SoapCommon/Mime/MultiPart.php create mode 100644 src/BeSimple/SoapCommon/Mime/Parser.php create mode 100644 src/BeSimple/SoapCommon/Mime/Part.php create mode 100644 src/BeSimple/SoapCommon/Mime/PartHeader.php diff --git a/src/BeSimple/SoapCommon/Mime/MultiPart.php b/src/BeSimple/SoapCommon/Mime/MultiPart.php new file mode 100644 index 0000000..6267b77 --- /dev/null +++ b/src/BeSimple/SoapCommon/Mime/MultiPart.php @@ -0,0 +1,180 @@ + + * (c) Francis Besset + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace BeSimple\SoapCommon; + +/** + * Mime multi part container. + * + * Headers: + * - MIME-Version + * - Content-Type + * - Content-ID + * - Content-Location + * - Content-Description + * + * @author Andreas Schamberger + */ +class MultiPart extends PartHeader +{ + /** + * Content-ID of main part. + * + * @var string + */ + protected $mainPartContentId; + + /** + * Mime parts. + * + * @var array(\BeSimple\SoapCommon\Mime\Part) + */ + protected $parts = array(); + + /** + * Construct new mime object. + * + * @param string $boundary Boundary string + * @return void + */ + public function __construct($boundary = null) + { + $this->setHeader('MIME-Version', '1.0'); + $this->setHeader('Content-Type', 'multipart/related'); + $this->setHeader('Content-Type', 'type', 'text/xml'); + $this->setHeader('Content-Type', 'charset', 'utf-8'); + if (is_null($boundary)) { + $boundary = $this->generateBoundary(); + } + $this->setHeader('Content-Type', 'boundary', $boundary); + } + + /** + * Get mime message of this object (without headers). + * + * @param boolean $withHeaders Returned mime message contains headers + * @return string + */ + public function getMimeMessage($withHeaders = false) + { + $message = ($withHeaders === true) ? $this->generateHeaders() : ""; + // add parts + foreach ($this->parts as $part) { + $message .= "\r\n" . '--' . $this->getHeader('Content-Type', 'boundary') . "\r\n"; + $message .= $part->getMessagePart(); + } + $message .= "\r\n" . '--' . $this->getHeader('Content-Type', 'boundary') . '--'; + return $message; + } + + /** + * Get string array with MIME headers for usage in HTTP header (with CURL). + * + * @return arrray(string) + */ + public function getHeadersForHttp() + { + $allowed = array( + 'Content-Type', + 'Content-Description', + ); + $headers = array(); + foreach ($this->headers as $fieldName => $value) { + if (in_array($fieldName, $allowed)) { + $fieldValue = ''; + if (is_array($value)) { + if (isset($value['@'])) { + $fieldValue .= $value['@']; + } + foreach ($value as $subName => $subValue) { + if ($subName != '@') { + $fieldValue .= '; ' . $subName . '="' . $subValue . '"'; + } + } + } else { + $fieldValue .= $value; + } + // for http only ISO-8859-1 + $headers[] = $fieldName . ': '. iconv('utf-8', 'ISO-8859-1//TRANSLIT', $fieldValue); + } + } + return $headers; + } + + /** + * Add new part to MIME message. + * + * @param \BeSimple\SoapCommon\Mime\Part $part Part that is added + * @param boolean $isMain Is the given part the main part of mime message + * @return void + */ + public function addPart(Part $part, $isMain = false) + { + $contentId = trim($part->getHeader('Content-ID'), '<>'); + if ($isMain === true) { + $this->mainPartContentId = $contentId; + $this->setHeader('Content-Type', 'start', $part->getHeader('Content-ID')); + } + $this->parts[$contentId] = $part; + } + + /** + * Get part with given content id. If there is no content id given it + * returns the main part that is defined through the content-id start + * parameter. + * + * @param string $contentId Content id of desired part + * @return \BeSimple\SoapCommon\Mime\Part|null + */ + public function getPart($contentId = null) + { + if (is_null($contentId)) { + $contentId = $this->mainPartContentId; + } + if (isset($this->parts[$contentId])) { + return $this->parts[$contentId]; + } + return null; + } + + /** + * Get all parts. + * + * @param boolean $includeMainPart Should main part be in result set + * @return array(\BeSimple\SoapCommon\Mime\Part) + */ + public function getParts($includeMainPart = false) + { + if ($includeMainPart === true) { + $parts = $this->parts; + } else { + $parts = array(); + foreach ($this->parts as $cid => $part) { + if ($cid != $this->mainPartContentId) { + $parts[$cid] = $part; + } + } + } + return $parts; + } + + /** + * Returns a unique boundary string. + * + * @return string + */ + protected function generateBoundary() + { + // TODO + return 'urn:uuid:' . \ass\Soap\Helper::generateUUID(); + } +} \ No newline at end of file diff --git a/src/BeSimple/SoapCommon/Mime/Parser.php b/src/BeSimple/SoapCommon/Mime/Parser.php new file mode 100644 index 0000000..c7b815f --- /dev/null +++ b/src/BeSimple/SoapCommon/Mime/Parser.php @@ -0,0 +1,183 @@ + + * (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\SoapCommon; + +/** + * Simple Multipart-Mime parser. + * + * @author Andreas Schamberger + */ +class Parser +{ + /** + * 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) + * @return \BeSimple\SoapCommon\Mime\MultiPart + */ + public static function parseMimeMessage($mimeMessage, array $headers = array()) + { + $boundary = null; + $start = null; + $multipart = new MultiPart(); + // 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); + } + } + } + $hitFirstBoundary = false; + $inHeader = true; + $content = ''; + $currentPart = $multipart; + $lines = preg_split("/\r\n|\n/", $mimeMessage); + foreach ($lines as $line) { + // ignore http status code + if (substr($line, 0, 5) == 'HTTP/') { + continue; + } + if (isset($currentHeader)) { + if (isset($line[0]) && ($line[0] === ' ' || $line[0] === "\t")) { + $currentHeader .= $line; + continue; + } + 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); + } + if ($inHeader) { + if ($line == '') { + $inHeader = false; + continue; + } + $currentHeader = $line; + continue; + } else { + // check if we hit any of the boundaries + if (strlen($line) > 0 && $line[0] == "-") { + if (strcmp(trim($line), '--' . $boundary) === 0) { + if ($currentPart instanceof Part) { + $content = iconv_substr($content, 0, -2, 'utf-8'); + 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 = iconv_substr($content, 0, -2, 'utf-8'); + 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); + $content = ''; + } + } else { + if ($hitFirstBoundary === false) { + if ($line != '') { + $inHeader = true; + $currentHeader = $line; + continue; + } + } + $content .= $line . "\r\n"; + } + } + + } + return $multipart; + } + + /** + * Parse a "Content-Type" header with multiple sub values. + * e.g. Content-Type: Multipart/Related; boundary=boundary; type=text/xml; + * start="<123@abc>" + * + * @see 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 + * @return null + */ + private static function parseContentTypeHeader(PartHeader $part, $headerName, $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_-]+)=(.)/', $remainder, $matches)) { + break; + } + $name = $matches[1]; + $delimiter = $matches[2]; + $pattern = '/(\S+)(\s|$)/'; + $remainder = substr($remainder, strlen($name)+1); + if (!preg_match($pattern, $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])); + } + } + + /** + * Decodes the content of a Mime part. + * + * @param \BeSimple\SoapCommon\Mime\Part $part Part to add content + * @param string $content Content to decode + * @return null + */ + private static function decodeContent(Part $part, $content) + { + $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 ($charset != 'utf-8') { + $content = iconv($charset, 'utf-8', $content); + } + $part->setContent($content); + } +} \ No newline at end of file diff --git a/src/BeSimple/SoapCommon/Mime/Part.php b/src/BeSimple/SoapCommon/Mime/Part.php new file mode 100644 index 0000000..45065ab --- /dev/null +++ b/src/BeSimple/SoapCommon/Mime/Part.php @@ -0,0 +1,165 @@ + + * (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\SoapCommon; + +/** + * Mime part. Everything must be UTF-8. Default charset for text is UTF-8. + * + * Headers: + * - Content-Type + * - Content-Transfer-Encoding + * - Content-ID + * - Content-Location + * - Content-Description + * + * @author Andreas Schamberger + */ +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 + */ + 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 + * @return void + */ + public function __construct($content = null, $contentType = 'application/octet-stream', $charset = null, $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 { // if (substr($contentType, 0, 4) == 'text') { + $this->setHeader('Content-Type', 'charset', 'utf-8'); + } + $this->setHeader('Content-Transfer-Encoding', $encoding); + if (is_null($contentId)) { + $contentId = $this->generateContentId(); + } + $this->setHeader('Content-ID', '<' . $contentId . '>'); + } + + /** + * __toString. + * + * @return mixed + */ + public function __toString() + { + return $this->content; + } + + /** + * Get mime content. + * + * @return mixed + */ + public function getContent() + { + return $this->content; + } + + /** + * Set mime content. + * + * @param mixed $content Content to set + * @return void + */ + public function setContent($content) + { + $this->content = $content; + } + + /** + * Get complete mime message of this object. + * + * @return string + */ + public function getMessagePart() + { + return $this->generateHeaders() . "\r\n" . $this->generateBody(); + } + + /** + * Generate body. + * + * @return string + */ + protected function generateBody() + { + $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); + } else { + $content = $this->content; + } + switch ($encoding) { + case self::ENCODING_BASE64: + return substr(chunk_split(base64_encode($content), 76, "\r\n"), -2); + case self::ENCODING_QUOTED_PRINTABLE: + return quoted_printable_encode($content); + case self::ENCODING_BINARY: + case self::ENCODING_SEVEN_BIT: + case self::ENCODING_EIGHT_BIT: + default: + return preg_replace("/\r\n|\r|\n/", "\r\n", $content); + } + } + + /** + * Returns a unique ID to be used for the Content-ID header. + * + * @return string + */ + protected function generateContentId() + { + // TODO + return 'urn:uuid:' . \ass\Soap\Helper::generateUUID(); + } +} \ No newline at end of file diff --git a/src/BeSimple/SoapCommon/Mime/PartHeader.php b/src/BeSimple/SoapCommon/Mime/PartHeader.php new file mode 100644 index 0000000..4dc3d06 --- /dev/null +++ b/src/BeSimple/SoapCommon/Mime/PartHeader.php @@ -0,0 +1,114 @@ + + * (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\SoapCommon; + +/** + * Mime part base class. + * + * @author Andreas Schamberger + */ +abstract class PartHeader +{ + /** + * Mime headers. + * + * @var array(string=>mixed|array(mixed)) + */ + protected $headers = array(); + + /** + * Add a new header to the mime part. + * + * @param string $name Header name + * @param string $value Header value + * @param string $subValue Is sub value? + * @return void + */ + public function setHeader($name, $value, $subValue = null) + { + if (isset($this->headers[$name]) && !is_null($subValue)) { + if (!is_array($this->headers[$name])) { + $this->headers[$name] = array( + '@' => $this->headers[$name], + $value => $subValue, + ); + } else { + $this->headers[$name][$value] = $subValue; + } + } elseif (isset($this->headers[$name]) && is_array($this->headers[$name]) && isset($this->headers[$name]['@'])) { + $this->headers[$name]['@'] = $value; + } else { + $this->headers[$name] = $value; + } + } + + /** + * Get given mime header. + * + * @param string $name Header name + * @param string $subValue Sub value name + * @return mixed|array(mixed) + */ + public function getHeader($name, $subValue = null) + { + if (isset($this->headers[$name])) { + if (!is_null($subValue)) { + if (is_array($this->headers[$name]) && isset($this->headers[$name][$subValue])) { + return $this->headers[$name][$subValue]; + } else { + return null; + } + } elseif (is_array($this->headers[$name]) && isset($this->headers[$name]['@'])) { + return $this->headers[$name]['@']; + } else { + return $this->headers[$name]; + } + } + return null; + } + + /** + * Generate headers. + * + * @return string + */ + protected function generateHeaders() + { + $charset = strtolower($this->getHeader('Content-Type', 'charset')); + $preferences = array( + 'scheme' => 'Q', + 'input-charset' => 'utf-8', + 'output-charset' => $charset, + ); + $headers = ''; + foreach ($this->headers as $fieldName => $value) { + $fieldValue = ''; + if (is_array($value)) { + if (isset($value['@'])) { + $fieldValue .= $value['@']; + } + foreach ($value as $subName => $subValue) { + if ($subName != '@') { + $fieldValue .= '; ' . $subName . '=' . $subValue; + } + } + } else { + $fieldValue .= $value; + } + // do not use proper encoding as Apache Axis does not understand this + // $headers .= iconv_mime_encode($field_name, $field_value, $preferences) . "\r\n"; + $headers .= $fieldName . ': ' . $fieldValue . "\r\n"; + } + return $headers; + } +} \ No newline at end of file From a1972a209cecff3d478a499dca9ca69d26c442b6 Mon Sep 17 00:00:00 2001 From: Andreas Schamberger Date: Sat, 22 Oct 2011 11:32:26 +0200 Subject: [PATCH 2/5] add helper class fix namespace --- src/BeSimple/SoapCommon/Helper.php | 194 ++++++++++++++++++++ src/BeSimple/SoapCommon/Mime/MultiPart.php | 7 +- src/BeSimple/SoapCommon/Mime/Parser.php | 2 +- src/BeSimple/SoapCommon/Mime/Part.php | 7 +- src/BeSimple/SoapCommon/Mime/PartHeader.php | 2 +- 5 files changed, 204 insertions(+), 8 deletions(-) create mode 100644 src/BeSimple/SoapCommon/Helper.php diff --git a/src/BeSimple/SoapCommon/Helper.php b/src/BeSimple/SoapCommon/Helper.php new file mode 100644 index 0000000..6d11b0f --- /dev/null +++ b/src/BeSimple/SoapCommon/Helper.php @@ -0,0 +1,194 @@ + + * (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\SoapCommon; + +/** + * Soap helper class with static functions that are used in the client and + * server implementations. It also provides namespace and configuration + * constants. + * + * @author Andreas Schamberger + */ +class Helper +{ + /** + * Attachment type: xsd:base64Binary (native in ext/soap). + */ + const ATTACHMENTS_TYPE_BASE64 = 1; + + /** + * Attachment type: MTOM (SOAP Message Transmission Optimization Mechanism). + */ + const ATTACHMENTS_TYPE_MTOM = 2; + + /** + * Attachment type: SWA (SOAP Messages with Attachments). + */ + const ATTACHMENTS_TYPE_SWA = 4; + + /** + * Web Services Security: SOAP Message Security 1.0 (WS-Security 2004) + */ + const NAME_WSS_SMS = 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0'; + + /** + * Web Services Security: SOAP Message Security 1.1 (WS-Security 2004) + */ + const NAME_WSS_SMS_1_1 = 'http://docs.oasis-open.org/wss/oasis-wss-soap-message-security-1.1'; + + /** + * Web Services Security UsernameToken Profile 1.0 + */ + const NAME_WSS_UTP = 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0'; + + /** + * Web Services Security X.509 Certificate Token Profile + */ + const NAME_WSS_X509 = 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0'; + + /** + * Soap 1.1 namespace. + */ + const NS_SOAP_1_1 = 'http://schemas.xmlsoap.org/soap/envelope/'; + + /** + * Soap 1.1 namespace. + */ + const NS_SOAP_1_2 = 'http://www.w3.org/2003/05/soap-envelope/'; + + /** + * Web Services Addressing 1.0 namespace. + */ + const NS_WSA = 'http://www.w3.org/2005/08/addressing'; + + /** + * Web Services Security Extension namespace. + */ + const NS_WSS = 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd'; + + /** + * Web Services Security Utility namespace. + */ + const NS_WSU = 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd'; + + /** + * Describing Media Content of Binary Data in XML namespace. + */ + const NS_XMLMIME = 'http://www.w3.org/2004/11/xmlmime'; + + /** + * XML Schema namespace. + */ + const NS_XML_SCHEMA = 'http://www.w3.org/2001/XMLSchema'; + + /** + * XML Schema instance namespace. + */ + const NS_XML_SCHEMA_INSTANCE = 'http://www.w3.org/2001/XMLSchema-instance'; + + /** + * XML-binary Optimized Packaging namespace. + */ + const NS_XOP = 'http://www.w3.org/2004/08/xop/include'; + + /** + * Web Services Addressing 1.0 prefix. + */ + const PFX_WSA = 'wsa'; + + /** + * Web Services Security Extension namespace. + */ + const PFX_WSS = 'wsse'; + + /** + * Web Services Security Utility namespace prefix. + */ + const PFX_WSU = 'wsu'; + + /** + * Describing Media Content of Binary Data in XML namespace prefix. + */ + const PFX_XMLMIME = 'xmlmime'; + + /** + * XML Schema namespace prefix. + */ + const PFX_XML_SCHEMA = 'xsd'; + + /** + * XML Schema instance namespace prefix. + */ + const PFX_XML_SCHEMA_INSTANCE = 'xsi'; + + /** + * XML-binary Optimized Packaging namespace prefix. + */ + const PFX_XOP = 'xop'; + + /** + * Generate a pseudo-random version 4 UUID. + * + * @see http://de.php.net/manual/en/function.uniqid.php#94959 + * @return string + */ + public static function generateUUID() + { + return sprintf( + '%04x%04x-%04x-%04x-%04x-%04x%04x%04x', + // 32 bits for "time_low" + mt_rand(0, 0xffff), mt_rand(0, 0xffff), + // 16 bits for "time_mid" + mt_rand(0, 0xffff), + // 16 bits for "time_hi_and_version", + // four most significant bits holds version number 4 + mt_rand(0, 0x0fff) | 0x4000, + // 16 bits, 8 bits for "clk_seq_hi_res", + // 8 bits for "clk_seq_low", + // two most significant bits holds zero and one for variant DCE1.1 + mt_rand(0, 0x3fff) | 0x8000, + // 48 bits for "node" + mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff) + ); + } + + /** + * Get SOAP namespace for the given $version. + * + * @param int $version SOAP_1_1|SOAP_1_2 + * @return string + */ + public static function getSoapNamespace($version) + { + if ($version === SOAP_1_2) { + return self::NS_SOAP_1_2; + } else { + return self::NS_SOAP_1_1; + } + } + + /** + * Get SOAP version from namespace URI. + * + * @param string $namespace NS_SOAP_1_1|NS_SOAP_1_2 + * @return int SOAP_1_1|SOAP_1_2 + */ + public static function getSoapVersionFromNamespace($namespace) + { + if ($namespace === self::NS_SOAP_1_2) { + return SOAP_1_2; + } else { + return SOAP_1_1; + } + } +} \ No newline at end of file diff --git a/src/BeSimple/SoapCommon/Mime/MultiPart.php b/src/BeSimple/SoapCommon/Mime/MultiPart.php index 6267b77..810b655 100644 --- a/src/BeSimple/SoapCommon/Mime/MultiPart.php +++ b/src/BeSimple/SoapCommon/Mime/MultiPart.php @@ -10,7 +10,9 @@ * with this source code in the file LICENSE. */ -namespace BeSimple\SoapCommon; +namespace BeSimple\SoapCommon\Mime; + +use BeSimple\SoapCommon\Helper; /** * Mime multi part container. @@ -174,7 +176,6 @@ class MultiPart extends PartHeader */ protected function generateBoundary() { - // TODO - return 'urn:uuid:' . \ass\Soap\Helper::generateUUID(); + return 'urn:uuid:' . Helper::generateUUID(); } } \ No newline at end of file diff --git a/src/BeSimple/SoapCommon/Mime/Parser.php b/src/BeSimple/SoapCommon/Mime/Parser.php index c7b815f..b5b78e0 100644 --- a/src/BeSimple/SoapCommon/Mime/Parser.php +++ b/src/BeSimple/SoapCommon/Mime/Parser.php @@ -10,7 +10,7 @@ * with this source code in the file LICENSE. */ -namespace BeSimple\SoapCommon; +namespace BeSimple\SoapCommon\Mime; /** * Simple Multipart-Mime parser. diff --git a/src/BeSimple/SoapCommon/Mime/Part.php b/src/BeSimple/SoapCommon/Mime/Part.php index 45065ab..88a2ca7 100644 --- a/src/BeSimple/SoapCommon/Mime/Part.php +++ b/src/BeSimple/SoapCommon/Mime/Part.php @@ -10,7 +10,9 @@ * with this source code in the file LICENSE. */ -namespace BeSimple\SoapCommon; +namespace BeSimple\SoapCommon\Mime; + +use BeSimple\SoapCommon\Helper; /** * Mime part. Everything must be UTF-8. Default charset for text is UTF-8. @@ -159,7 +161,6 @@ class Part extends PartHeader */ protected function generateContentId() { - // TODO - return 'urn:uuid:' . \ass\Soap\Helper::generateUUID(); + return 'urn:uuid:' . Helper::generateUUID(); } } \ No newline at end of file diff --git a/src/BeSimple/SoapCommon/Mime/PartHeader.php b/src/BeSimple/SoapCommon/Mime/PartHeader.php index 4dc3d06..fea2694 100644 --- a/src/BeSimple/SoapCommon/Mime/PartHeader.php +++ b/src/BeSimple/SoapCommon/Mime/PartHeader.php @@ -10,7 +10,7 @@ * with this source code in the file LICENSE. */ -namespace BeSimple\SoapCommon; +namespace BeSimple\SoapCommon\Mime; /** * Mime part base class. From 4becca0208cbbc35050d408d107f646aa76d3b15 Mon Sep 17 00:00:00 2001 From: Andreas Schamberger Date: Mon, 31 Oct 2011 11:54:41 +0100 Subject: [PATCH 3/5] fixed parsing of mime headers --- src/BeSimple/SoapCommon/Mime/Parser.php | 32 +++++++++++++------------ 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/src/BeSimple/SoapCommon/Mime/Parser.php b/src/BeSimple/SoapCommon/Mime/Parser.php index b5b78e0..7d98edf 100644 --- a/src/BeSimple/SoapCommon/Mime/Parser.php +++ b/src/BeSimple/SoapCommon/Mime/Parser.php @@ -49,8 +49,9 @@ class Parser $currentPart = $multipart; $lines = preg_split("/\r\n|\n/", $mimeMessage); foreach ($lines as $line) { - // ignore http status code - if (substr($line, 0, 5) == 'HTTP/') { + // ignore http status code and POST * + if ( substr( $line, 0, 5 ) == 'HTTP/' || substr( $line, 0, 4 ) == 'POST') + { continue; } if (isset($currentHeader)) { @@ -58,14 +59,16 @@ class Parser $currentHeader .= $line; continue; } - 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)); + 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); } @@ -123,7 +126,7 @@ class Parser /** * Parse a "Content-Type" header with multiple sub values. - * e.g. Content-Type: Multipart/Related; boundary=boundary; type=text/xml; + * e.g. Content-Type: multipart/related; boundary=boundary; type=text/xml; * start="<123@abc>" * * @see https://labs.omniti.com/alexandria/trunk/OmniTI/Mail/Parser.php @@ -140,18 +143,17 @@ class Parser $part->setHeader($headerName, $value); $remainder = trim($remainder); while (strlen($remainder) > 0) { - if (!preg_match('/^([a-zA-Z0-9_-]+)=(.)/', $remainder, $matches)) { + if (!preg_match('/^([a-zA-Z0-9_-]+)=(.{1})/', $remainder, $matches)) { break; } $name = $matches[1]; $delimiter = $matches[2]; - $pattern = '/(\S+)(\s|$)/'; $remainder = substr($remainder, strlen($name)+1); - if (!preg_match($pattern, $remainder, $matches)) { + if (!preg_match('/([^;]+)(;)?(\s|$)?/', $remainder, $matches)) { break; } $value = rtrim($matches[1], ';'); - if ($delimiter == '\'' || $delimiter == '"') { + if ($delimiter == "'" || $delimiter == '"') { $value = trim($value, $delimiter); } $part->setHeader($headerName, $name, $value); From e0049c906d6ca7c15c15a659206e3815dbd592aa Mon Sep 17 00:00:00 2001 From: Andreas Schamberger Date: Mon, 31 Oct 2011 11:55:14 +0100 Subject: [PATCH 4/5] unit tests --- .gitignore | 4 + src/BeSimple/SoapCommon/Mime/MultiPart.php | 15 +- src/BeSimple/SoapCommon/Mime/PartHeader.php | 54 +++++-- .../SoapCommon/Fixtures/MimePartHeader.php | 9 ++ .../Fixtures/SwA-response-amazon.txt | 29 ++++ .../SoapCommon/Fixtures/SwA-response-axis.txt | 13 ++ .../SoapCommon/Fixtures/WS-I-MTOM-request.txt | 22 +++ .../Fixtures/WS-I-MTOM-request_noheader.txt | 16 ++ .../Fixtures/WS-I-MTOM-response.txt | 17 ++ .../Tests/SoapCommon/Mime/MultiPartTest.php | 144 +++++++++++++++++ .../Tests/SoapCommon/Mime/ParserTest.php | 153 ++++++++++++++++++ .../Tests/SoapCommon/Mime/PartHeaderTest.php | 57 +++++++ .../Tests/SoapCommon/Mime/PartTest.php | 62 +++++++ 13 files changed, 569 insertions(+), 26 deletions(-) create mode 100644 tests/BeSimple/Tests/SoapCommon/Fixtures/MimePartHeader.php create mode 100644 tests/BeSimple/Tests/SoapCommon/Fixtures/SwA-response-amazon.txt create mode 100644 tests/BeSimple/Tests/SoapCommon/Fixtures/SwA-response-axis.txt create mode 100644 tests/BeSimple/Tests/SoapCommon/Fixtures/WS-I-MTOM-request.txt create mode 100644 tests/BeSimple/Tests/SoapCommon/Fixtures/WS-I-MTOM-request_noheader.txt create mode 100644 tests/BeSimple/Tests/SoapCommon/Fixtures/WS-I-MTOM-response.txt create mode 100644 tests/BeSimple/Tests/SoapCommon/Mime/MultiPartTest.php create mode 100644 tests/BeSimple/Tests/SoapCommon/Mime/ParserTest.php create mode 100644 tests/BeSimple/Tests/SoapCommon/Mime/PartHeaderTest.php create mode 100644 tests/BeSimple/Tests/SoapCommon/Mime/PartTest.php diff --git a/.gitignore b/.gitignore index 22d0d82..f8d178e 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,5 @@ vendor +/phpunit.xml +.buildpath +.project +.settings \ No newline at end of file diff --git a/src/BeSimple/SoapCommon/Mime/MultiPart.php b/src/BeSimple/SoapCommon/Mime/MultiPart.php index 810b655..1a3b7e1 100644 --- a/src/BeSimple/SoapCommon/Mime/MultiPart.php +++ b/src/BeSimple/SoapCommon/Mime/MultiPart.php @@ -80,6 +80,7 @@ class MultiPart extends PartHeader /** * Get string array with MIME headers for usage in HTTP header (with CURL). + * Only 'Content-Type' and 'Content-Description' headers are returned. * * @return arrray(string) */ @@ -92,19 +93,7 @@ class MultiPart extends PartHeader $headers = array(); foreach ($this->headers as $fieldName => $value) { if (in_array($fieldName, $allowed)) { - $fieldValue = ''; - if (is_array($value)) { - if (isset($value['@'])) { - $fieldValue .= $value['@']; - } - foreach ($value as $subName => $subValue) { - if ($subName != '@') { - $fieldValue .= '; ' . $subName . '="' . $subValue . '"'; - } - } - } else { - $fieldValue .= $value; - } + $fieldValue = $this->generateHeaderFieldValue($value); // for http only ISO-8859-1 $headers[] = $fieldName . ': '. iconv('utf-8', 'ISO-8859-1//TRANSLIT', $fieldValue); } diff --git a/src/BeSimple/SoapCommon/Mime/PartHeader.php b/src/BeSimple/SoapCommon/Mime/PartHeader.php index fea2694..b6458cd 100644 --- a/src/BeSimple/SoapCommon/Mime/PartHeader.php +++ b/src/BeSimple/SoapCommon/Mime/PartHeader.php @@ -92,23 +92,51 @@ abstract class PartHeader ); $headers = ''; foreach ($this->headers as $fieldName => $value) { - $fieldValue = ''; - if (is_array($value)) { - if (isset($value['@'])) { - $fieldValue .= $value['@']; - } - foreach ($value as $subName => $subValue) { - if ($subName != '@') { - $fieldValue .= '; ' . $subName . '=' . $subValue; - } - } - } else { - $fieldValue .= $value; - } + $fieldValue = $this->generateHeaderFieldValue($value); // do not use proper encoding as Apache Axis does not understand this // $headers .= iconv_mime_encode($field_name, $field_value, $preferences) . "\r\n"; $headers .= $fieldName . ': ' . $fieldValue . "\r\n"; } return $headers; } + + /** + * Generates a header field value from the given value paramater. + * + * @param array(string=>string)|string $value + * @return string + */ + protected function generateHeaderFieldValue($value) + { + $fieldValue = ''; + if (is_array($value)) { + if (isset($value['@'])) { + $fieldValue .= $value['@']; + } + foreach ($value as $subName => $subValue) { + if ($subName != '@') { + $fieldValue .= '; ' . $subName . '=' . $this->quoteValueString($subValue); + } + } + } else { + $fieldValue .= $value; + } + return $fieldValue; + } + + /** + * Quote string with '"' if it contains one of the special characters: + * "(" / ")" / "<" / ">" / "@" / "," / ";" / ":" / "\" / <"> / "/" / "[" / "]" / "?" / "=" + * + * @param string $string + * @return string + */ + private function quoteValueString($string) + { + if (preg_match('~[()<>@,;:\\"/\[\]?=]~', $string)) { + return '"' . $string . '"'; + } else { + return $string; + } + } } \ No newline at end of file diff --git a/tests/BeSimple/Tests/SoapCommon/Fixtures/MimePartHeader.php b/tests/BeSimple/Tests/SoapCommon/Fixtures/MimePartHeader.php new file mode 100644 index 0000000..2ecd558 --- /dev/null +++ b/tests/BeSimple/Tests/SoapCommon/Fixtures/MimePartHeader.php @@ -0,0 +1,9 @@ + +--xxx-MIME-Boundary-xxx-0xa36cb38-0a36cb38-xxx-END-xxx +Content-ID: <0x9d6ad00-0xa19ef48-0x9de7500-0xa4fae78-0xa382698> +Content-Type: application/binary + + + + +--xxx-MIME-Boundary-xxx-0xa36cb38-0a36cb38-xxx-END-xxx-- diff --git a/tests/BeSimple/Tests/SoapCommon/Fixtures/SwA-response-axis.txt b/tests/BeSimple/Tests/SoapCommon/Fixtures/SwA-response-axis.txt new file mode 100644 index 0000000..58cda6f --- /dev/null +++ b/tests/BeSimple/Tests/SoapCommon/Fixtures/SwA-response-axis.txt @@ -0,0 +1,13 @@ +HTTP/1.1 200 OK +Date: Sat, 11 Sep 2010 12:52:57 GMT +Server: Simple-Server/1.1 +Transfer-Encoding: chunked +Content-Type: multipart/related; boundary=MIMEBoundaryurn_uuid_2DB7ABF3DC5BED7FA51284209577582; type="application/soap+xml"; start="<0.urn:uuid:2DB7ABF3DC5BED7FA51284209577583@apache.org>"; action="urn:getVersionResponse" + +--MIMEBoundaryurn_uuid_2DB7ABF3DC5BED7FA51284209577582 +Content-Type: application/soap+xml; charset=utf-8 +Content-Transfer-Encoding: 8bit +Content-ID: <0.urn:uuid:2DB7ABF3DC5BED7FA51284209577583@apache.org> + +urn:getVersionResponseuuid:665aab53-4cef-4435-8934-4c10ed24fd42Hi - the Axis2 version is 1.5.1 +--MIMEBoundaryurn_uuid_2DB7ABF3DC5BED7FA51284209577582-- \ No newline at end of file diff --git a/tests/BeSimple/Tests/SoapCommon/Fixtures/WS-I-MTOM-request.txt b/tests/BeSimple/Tests/SoapCommon/Fixtures/WS-I-MTOM-request.txt new file mode 100644 index 0000000..efe9192 --- /dev/null +++ b/tests/BeSimple/Tests/SoapCommon/Fixtures/WS-I-MTOM-request.txt @@ -0,0 +1,22 @@ +POST http://131.107.72.15/Mtom/svc/service.svc/Soap12MtomUTF8 HTTP/1.1 +Content-Type: multipart/related; type="application/xop+xml";start="";boundary="uuid:0ca0e16e-feb1-426c-97d8-c4508ada5e82+id=7";start-info="application/soap+xml" +Host: 131.107.72.15 +Content-Length: 1941 +Expect: 100-continue +HTTP/1.1 100 Continue + +--uuid:0ca0e16e-feb1-426c-97d8-c4508ada5e82+id=7 +Content-ID: +Content-Transfer-Encoding: 8bit +Content-Type: application/xop+xml;charset=utf-8;type="application/soap+xml" + +http://xmlsoap.org/echoBinaryAsStringurn:uuid:1bf061d6-d532-4b0c-930b-8b8202c38b8ahttp://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymoushttp://131.107.72.15/Mtom/svc/service.svc/Soap12MtomUTF8 + +--uuid:0ca0e16e-feb1-426c-97d8-c4508ada5e82+id=7 +Content-ID: +Content-Transfer-Encoding: binary +Content-Type: application/octet-stream + +H e l l o W o r l d ! H e l l o W o r l d ! H e l l o W o r l d ! H e l l o W o r l d ! H e l l o W o r l d ! H e l l o W o r l d ! H e l l o W o r l d ! H e l l o W o r l d ! H e l l o W o r l d ! H e l l o W o r l d ! H e l l o W o r l d ! H e l l o W o r l d ! H e l l o W o r l d ! H e l l o W o r l d ! H e l l o W o r l d ! H e l l o W o r l d ! H e l l o W o r l d ! H e l l o W o r l d ! H e l l o W o r l d ! H e l l o W o r l d ! H e l l o W o r l d ! H e l l o W o r l d ! H e l l o W o r l d ! H e l l o W o r l d ! H e l l o W o r l d ! H e l l o W o r l d ! H e l l o W o r l d ! H e l l o W o r l d ! H e l l o W o r l d ! H e l l o W o r l d ! H e l l o W o r l d ! H e l l o W o r l d ! + +--uuid:0ca0e16e-feb1-426c-97d8-c4508ada5e82+id=7-- diff --git a/tests/BeSimple/Tests/SoapCommon/Fixtures/WS-I-MTOM-request_noheader.txt b/tests/BeSimple/Tests/SoapCommon/Fixtures/WS-I-MTOM-request_noheader.txt new file mode 100644 index 0000000..f6d9942 --- /dev/null +++ b/tests/BeSimple/Tests/SoapCommon/Fixtures/WS-I-MTOM-request_noheader.txt @@ -0,0 +1,16 @@ + +--uuid:0ca0e16e-feb1-426c-97d8-c4508ada5e82+id=7 +Content-ID: +Content-Transfer-Encoding: 8bit +Content-Type: application/xop+xml;charset=utf-8;type="application/soap+xml" + +http://xmlsoap.org/echoBinaryAsStringurn:uuid:1bf061d6-d532-4b0c-930b-8b8202c38b8ahttp://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymoushttp://131.107.72.15/Mtom/svc/service.svc/Soap12MtomUTF8 + +--uuid:0ca0e16e-feb1-426c-97d8-c4508ada5e82+id=7 +Content-ID: +Content-Transfer-Encoding: binary +Content-Type: application/octet-stream + +H e l l o W o r l d ! H e l l o W o r l d ! H e l l o W o r l d ! H e l l o W o r l d ! H e l l o W o r l d ! H e l l o W o r l d ! H e l l o W o r l d ! H e l l o W o r l d ! H e l l o W o r l d ! H e l l o W o r l d ! H e l l o W o r l d ! H e l l o W o r l d ! H e l l o W o r l d ! H e l l o W o r l d ! H e l l o W o r l d ! H e l l o W o r l d ! H e l l o W o r l d ! H e l l o W o r l d ! H e l l o W o r l d ! H e l l o W o r l d ! H e l l o W o r l d ! H e l l o W o r l d ! H e l l o W o r l d ! H e l l o W o r l d ! H e l l o W o r l d ! H e l l o W o r l d ! H e l l o W o r l d ! H e l l o W o r l d ! H e l l o W o r l d ! H e l l o W o r l d ! H e l l o W o r l d ! H e l l o W o r l d ! + +--uuid:0ca0e16e-feb1-426c-97d8-c4508ada5e82+id=7-- diff --git a/tests/BeSimple/Tests/SoapCommon/Fixtures/WS-I-MTOM-response.txt b/tests/BeSimple/Tests/SoapCommon/Fixtures/WS-I-MTOM-response.txt new file mode 100644 index 0000000..7a76cb4 --- /dev/null +++ b/tests/BeSimple/Tests/SoapCommon/Fixtures/WS-I-MTOM-response.txt @@ -0,0 +1,17 @@ +HTTP/1.1 200 OK +Proxy-Connection: Keep-Alive +Connection: Keep-Alive +Content-Length: 1166 +Via: 1.1 RED-PRXY-03 +Date: Fri, 09 Sep 2005 06:57:22 GMT +Content-Type: multipart/related; type="application/xop+xml";start="";boundary="uuid:b71dc628-ec8f-4422-8a4a-992f041cb94c+id=46";start-info="application/soap+xml" + + + +--uuid:b71dc628-ec8f-4422-8a4a-992f041cb94c+id=46 +Content-ID: +Content-Transfer-Encoding: 8bit +Content-Type: application/xop+xml;charset=utf-8;type="application/soap+xml" + +*urn:uuid:1bf061d6-d532-4b0c-930b-8b8202c38b8ahttp://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymousHello World!Hello World!Hello World!Hello World!Hello World!Hello World!Hello World!Hello World!Hello World!Hello World!Hello World!Hello World!Hello World!Hello World!Hello World!Hello World!Hello World!Hello World!Hello World!Hello World!Hello World!Hello World!Hello World!Hello World!Hello World!Hello World!Hello World!Hello World!Hello World!Hello World!Hello World!Hello World! +--uuid:b71dc628-ec8f-4422-8a4a-992f041cb94c+id=46-- diff --git a/tests/BeSimple/Tests/SoapCommon/Mime/MultiPartTest.php b/tests/BeSimple/Tests/SoapCommon/Mime/MultiPartTest.php new file mode 100644 index 0000000..1d1ab16 --- /dev/null +++ b/tests/BeSimple/Tests/SoapCommon/Mime/MultiPartTest.php @@ -0,0 +1,144 @@ + + * (c) Francis Besset + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace BeSimple\Tests\SoapCommon\Soap; + +use BeSimple\SoapCommon\Mime\MultiPart; +use BeSimple\SoapCommon\Mime\Part; +use BeSimple\SoapCommon\Mime\PartHeader; + +class MultiPartTest extends \PHPUnit_Framework_TestCase +{ + public function testConstructor() + { + $mp = new MultiPart(); + + $this->assertEquals('1.0', $mp->getHeader('MIME-Version')); + $this->assertEquals('multipart/related', $mp->getHeader('Content-Type')); + $this->assertEquals('text/xml', $mp->getHeader('Content-Type', 'type')); + $this->assertEquals('utf-8', $mp->getHeader('Content-Type', 'charset')); + $this->assertRegExp('~urn:uuid:.*~', $mp->getHeader('Content-Type', 'boundary')); + } + + public function testGetMimeMessage() + { + $mp = new MultiPart(); + + /* + string(51) " + --urn:uuid:a81ca327-591e-4656-91a1-8f177ada95b0--" + */ + $this->assertEquals(51, strlen($mp->getMimeMessage())); + + $p = new Part('test'); + $mp->addPart($p, true); + + /* + string(259) " + --urn:uuid:a81ca327-591e-4656-91a1-8f177ada95b0 + Content-Type: application/octet-stream; charset=utf-8 + Content-Transfer-Encoding: binary + Content-ID: + + test + --urn:uuid:a81ca327-591e-4656-91a1-8f177ada95b0--" + */ + $this->assertEquals(259, strlen($mp->getMimeMessage())); + } + + public function testGetMimeMessageWithHeaders() + { + $mp = new MultiPart(); + + /* + string(189) "MIME-Version: 1.0 + Content-Type: multipart/related; type="text/xml"; charset=utf-8; boundary="urn:uuid:231833e2-a23b-410a-862e-250524fc38f6" + + --urn:uuid:231833e2-a23b-410a-862e-250524fc38f6--" + */ + $this->assertEquals(193, strlen($mp->getMimeMessage(true))); + + $p = new Part('test'); + $mp->addPart($p, true); + + /* + string(452) "MIME-Version: 1.0 + Content-Type: multipart/related; type="text/xml"; charset=utf-8; boundary="urn:uuid:231833e2-a23b-410a-862e-250524fc38f6"; start="" + + --urn:uuid:231833e2-a23b-410a-862e-250524fc38f6 + Content-Type: application/octet-stream; charset=utf-8 + Content-Transfer-Encoding: binary + Content-ID: + + test + --urn:uuid:231833e2-a23b-410a-862e-250524fc38f6--" + */ + $this->assertEquals(458, strlen($mp->getMimeMessage(true))); + } + + public function testGetHeadersForHttp() + { + $mp = new MultiPart(); + + $result = array( + 'Content-Type: multipart/related; type="text/xml"; charset=utf-8; boundary="' . $mp->getHeader('Content-Type', 'boundary') . '"', + ); + $this->assertEquals($result, $mp->getHeadersForHttp()); + + $result = array( + 'Content-Type: multipart/related; type="text/xml"; charset=utf-8; boundary="' . $mp->getHeader('Content-Type', 'boundary') . '"', + 'Content-Description: test', + ); + $mp->setHeader('Content-Description', 'test'); + $this->assertEquals($result, $mp->getHeadersForHttp()); + } + + public function testAddGetPart() + { + $mp = new MultiPart(); + + $p = new Part('test'); + $p->setHeader('Content-ID', 'mycontentid'); + $mp->addPart($p); + $this->assertEquals($p, $mp->getPart('mycontentid')); + } + + public function testAddGetPartWithMain() + { + $mp = new MultiPart(); + + $p = new Part('test'); + $mp->addPart($p, true); + $this->assertEquals($p, $mp->getPart()); + } + + public function testGetParts() + { + $mp = new MultiPart(); + + $p1 = new Part('test'); + $mp->addPart($p1, true); + $p2 = new Part('test'); + $mp->addPart($p2); + + $withoutMain = array( + trim($p2->getHeader('Content-ID'),'<>') => $p2, + ); + $this->assertEquals($withoutMain, $mp->getParts()); + + $withMain = array( + trim($p1->getHeader('Content-ID'),'<>') => $p1, + trim($p2->getHeader('Content-ID'),'<>') => $p2, + ); + $this->assertEquals($withMain, $mp->getParts(true)); + } +} \ No newline at end of file diff --git a/tests/BeSimple/Tests/SoapCommon/Mime/ParserTest.php b/tests/BeSimple/Tests/SoapCommon/Mime/ParserTest.php new file mode 100644 index 0000000..8efa1f9 --- /dev/null +++ b/tests/BeSimple/Tests/SoapCommon/Mime/ParserTest.php @@ -0,0 +1,153 @@ + + * (c) Francis Besset + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace BeSimple\Tests\SoapCommon\Soap; + +use BeSimple\SoapCommon\Mime\MultiPart; +use BeSimple\SoapCommon\Mime\Parser; +use BeSimple\SoapCommon\Mime\Part; +use BeSimple\SoapCommon\Mime\PartHeader; + +class ParserTest extends \PHPUnit_Framework_TestCase +{ + public function testParserRequestWsi() + { + $filename = dirname(__DIR__).DIRECTORY_SEPARATOR.'Fixtures/WS-I-MTOM-request.txt'; + $mimeMessage = file_get_contents($filename); + + $mp = Parser::parseMimeMessage($mimeMessage); + $this->assertsForWsiMtomRequest($mp); + } + + public function testParserResponseAmazon() + { + $filename = dirname(__DIR__).DIRECTORY_SEPARATOR.'Fixtures/SwA-response-amazon.txt'; + $mimeMessage = file_get_contents($filename); + + $mp = Parser::parseMimeMessage($mimeMessage); + $this->assertEquals('Fri, 12 Feb 2010 15:46:00 GMT', $mp->getHeader('Date')); + $this->assertEquals('Server', $mp->getHeader('Server')); + $this->assertEquals('1.0', $mp->getHeader('MIME-Version')); + $this->assertEquals('close', $mp->getHeader('Cneonction')); + $this->assertEquals('chunked', $mp->getHeader('Transfer-Encoding')); + $this->assertEquals('multipart/related', $mp->getHeader('Content-Type')); + $this->assertEquals('text/xml', $mp->getHeader('Content-Type', 'type')); + $this->assertEquals('utf-8', $mp->getHeader('Content-Type', 'charset')); + $this->assertEquals('xxx-MIME-Boundary-xxx-0xa36cb38-0a36cb38-xxx-END-xxx', $mp->getHeader('Content-Type', 'boundary')); + + $p1 = $mp->getPart(); + $this->assertEquals('text/xml', $p1->getHeader('Content-Type')); + $this->assertEquals('UTF-8', $p1->getHeader('Content-Type', 'charset')); + $this->assertEquals(389, strlen($p1->getContent())); + + $p2 = $mp->getPart('0x9d6ad00-0xa19ef48-0x9de7500-0xa4fae78-0xa382698'); + $this->assertEquals('binary', $p2->getHeader('Content-Transfer-Encoding')); + $this->assertEquals('application/binary', $p2->getHeader('Content-Type')); + $this->assertEquals(81, strlen($p2->getContent())); + } + + public function testParserResponseAxis() + { + $filename = dirname(__DIR__).DIRECTORY_SEPARATOR.'Fixtures/SwA-response-axis.txt'; + $mimeMessage = file_get_contents($filename); + + $mp = Parser::parseMimeMessage($mimeMessage); + $this->assertEquals('Sat, 11 Sep 2010 12:52:57 GMT', $mp->getHeader('Date')); + $this->assertEquals('Simple-Server/1.1', $mp->getHeader('Server')); + $this->assertEquals('1.0', $mp->getHeader('MIME-Version')); + $this->assertEquals('chunked', $mp->getHeader('Transfer-Encoding')); + $this->assertEquals('multipart/related', $mp->getHeader('Content-Type')); + $this->assertEquals('application/soap+xml', $mp->getHeader('Content-Type', 'type')); + $this->assertEquals('utf-8', $mp->getHeader('Content-Type', 'charset')); + $this->assertEquals('<0.urn:uuid:2DB7ABF3DC5BED7FA51284209577583@apache.org>', $mp->getHeader('Content-Type', 'start')); + $this->assertEquals('urn:getVersionResponse', $mp->getHeader('Content-Type', 'action')); + $this->assertEquals('MIMEBoundaryurn_uuid_2DB7ABF3DC5BED7FA51284209577582', $mp->getHeader('Content-Type', 'boundary')); + + $p1 = $mp->getPart('0.urn:uuid:2DB7ABF3DC5BED7FA51284209577583@apache.org'); + $this->assertEquals('8bit', $p1->getHeader('Content-Transfer-Encoding')); + $this->assertEquals('application/soap+xml', $p1->getHeader('Content-Type')); + $this->assertEquals('utf-8', $p1->getHeader('Content-Type', 'charset')); + $this->assertEquals(499, strlen($p1->getContent())); + } + + public function testParserResponseWsi() + { + $filename = dirname(__DIR__).DIRECTORY_SEPARATOR.'Fixtures/WS-I-MTOM-response.txt'; + $mimeMessage = file_get_contents($filename); + + $mp = Parser::parseMimeMessage($mimeMessage); + $this->assertEquals('Keep-Alive', $mp->getHeader('Proxy-Connection')); + $this->assertEquals('Keep-Alive', $mp->getHeader('Connection')); + $this->assertEquals('1166', $mp->getHeader('Content-Length')); + $this->assertEquals('1.1 RED-PRXY-03', $mp->getHeader('Via')); + $this->assertEquals('Fri, 09 Sep 2005 06:57:22 GMT', $mp->getHeader('Date')); + $this->assertEquals('multipart/related', $mp->getHeader('Content-Type')); + $this->assertEquals('application/xop+xml', $mp->getHeader('Content-Type', 'type')); + $this->assertEquals('utf-8', $mp->getHeader('Content-Type', 'charset')); + $this->assertEquals('', $mp->getHeader('Content-Type', 'start')); + $this->assertEquals('application/soap+xml', $mp->getHeader('Content-Type', 'start-info')); + $this->assertEquals('uuid:b71dc628-ec8f-4422-8a4a-992f041cb94c+id=46', $mp->getHeader('Content-Type', 'boundary')); + + $p1 = $mp->getPart('http://tempuri.org/0'); + $this->assertEquals('8bit', $p1->getHeader('Content-Transfer-Encoding')); + $this->assertEquals('application/xop+xml', $p1->getHeader('Content-Type')); + $this->assertEquals('utf-8', $p1->getHeader('Content-Type', 'charset')); + $this->assertEquals('application/soap+xml', $p1->getHeader('Content-Type', 'type')); + $this->assertEquals(910, strlen($p1->getContent())); + } + + public function testParserWithHeaderArray() + { + $filename = dirname(__DIR__).DIRECTORY_SEPARATOR.'Fixtures/WS-I-MTOM-request_noheader.txt'; + $mimeMessage = file_get_contents($filename); + + $headers = array( + 'Content-Type' => 'multipart/related; type="application/xop+xml";start="";boundary="uuid:0ca0e16e-feb1-426c-97d8-c4508ada5e82+id=7";start-info="application/soap+xml"', + 'Content-Length' => 1941, + 'Host' => '131.107.72.15', + 'Expect' => '100-continue', + ); + + $mp = Parser::parseMimeMessage($mimeMessage, $headers); + $this->assertsForWsiMtomRequest($mp); + } + + /* + * used in: + * - testParserWithHeaderArray + * - testParserRequestWsi + */ + private function assertsForWsiMtomRequest(MultiPart $mp) + { + $this->assertEquals('multipart/related', $mp->getHeader('Content-Type')); + $this->assertEquals('application/xop+xml', $mp->getHeader('Content-Type', 'type')); + $this->assertEquals('utf-8', $mp->getHeader('Content-Type', 'charset')); + $this->assertEquals('', $mp->getHeader('Content-Type', 'start')); + $this->assertEquals('application/soap+xml', $mp->getHeader('Content-Type', 'start-info')); + $this->assertEquals('uuid:0ca0e16e-feb1-426c-97d8-c4508ada5e82+id=7', $mp->getHeader('Content-Type', 'boundary')); + $this->assertEquals('1941', $mp->getHeader('Content-Length')); + $this->assertEquals('131.107.72.15', $mp->getHeader('Host')); + $this->assertEquals('100-continue', $mp->getHeader('Expect')); + + $p1 = $mp->getPart('http://tempuri.org/0'); + $this->assertEquals('8bit', $p1->getHeader('Content-Transfer-Encoding')); + $this->assertEquals('application/xop+xml', $p1->getHeader('Content-Type')); + $this->assertEquals('utf-8', $p1->getHeader('Content-Type', 'charset')); + $this->assertEquals('application/soap+xml', $p1->getHeader('Content-Type', 'type')); + $this->assertEquals(737, strlen($p1->getContent())); + + $p2 = $mp->getPart('http://tempuri.org/1/632618206527087310'); + $this->assertEquals('binary', $p2->getHeader('Content-Transfer-Encoding')); + $this->assertEquals('application/octet-stream', $p2->getHeader('Content-Type')); + $this->assertEquals(769, strlen($p2->getContent())); + } +} diff --git a/tests/BeSimple/Tests/SoapCommon/Mime/PartHeaderTest.php b/tests/BeSimple/Tests/SoapCommon/Mime/PartHeaderTest.php new file mode 100644 index 0000000..ef9f08d --- /dev/null +++ b/tests/BeSimple/Tests/SoapCommon/Mime/PartHeaderTest.php @@ -0,0 +1,57 @@ + + * (c) Francis Besset + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace BeSimple\Tests\SoapCommon\Soap; + +use BeSimple\SoapCommon\Mime\PartHeader; +use BeSimple\Tests\SoapCommon\Fixtures\MimePartHeader; + +class PartHeaderTest extends \PHPUnit_Framework_TestCase +{ + public function testSetGetHeader() + { + $ph = new MimePartHeader(); + $ph->setHeader('Content-Type', 'text/xml'); + $this->assertEquals('text/xml', $ph->getHeader('Content-Type')); + } + + public function testSetGetHeaderSubvalue() + { + $ph = new MimePartHeader(); + $ph->setHeader('Content-Type', 'utf-8', 'charset'); + $this->assertEquals(null, $ph->getHeader('Content-Type', 'charset')); + + $ph->setHeader('Content-Type', 'text/xml'); + $ph->setHeader('Content-Type', 'charset', 'utf-8'); + $this->assertEquals('utf-8', $ph->getHeader('Content-Type', 'charset')); + } + + public function testGenerateHeaders() + { + $ph = new MimePartHeader(); + + $class = new \ReflectionClass($ph); + $method = $class->getMethod('generateHeaders'); + $method->setAccessible(true); + + $this->assertEquals('', $method->invoke($ph)); + + $ph->setHeader('Content-Type', 'text/xml'); + $this->assertEquals("Content-Type: text/xml\r\n", $method->invoke($ph)); + + $ph->setHeader('Content-Type', 'charset', 'utf-8'); + $this->assertEquals("Content-Type: text/xml; charset=utf-8\r\n", $method->invoke($ph)); + + $ph->setHeader('Content-Type', 'type', 'text/xml'); + $this->assertEquals("Content-Type: text/xml; charset=utf-8; type=\"text/xml\"\r\n", $method->invoke($ph)); + } +} \ No newline at end of file diff --git a/tests/BeSimple/Tests/SoapCommon/Mime/PartTest.php b/tests/BeSimple/Tests/SoapCommon/Mime/PartTest.php new file mode 100644 index 0000000..031ba5b --- /dev/null +++ b/tests/BeSimple/Tests/SoapCommon/Mime/PartTest.php @@ -0,0 +1,62 @@ + + * (c) Francis Besset + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace BeSimple\Tests\SoapCommon\Soap; + +use BeSimple\SoapCommon\Mime\Part; +use BeSimple\SoapCommon\Mime\PartHeader; + +class PartTest extends \PHPUnit_Framework_TestCase +{ + public function testConstructor() + { + $p = new Part('', 'text/xml', 'utf-8', Part::ENCODING_BINARY, 'urn:myuniqueresource'); + + $this->assertEquals('', $p->getContent()); + $this->assertEquals('text/xml', $p->getHeader('Content-Type')); + $this->assertEquals('utf-8', $p->getHeader('Content-Type', 'charset')); + $this->assertEquals(Part::ENCODING_BINARY, $p->getHeader('Content-Transfer-Encoding')); + $this->assertEquals('', $p->getHeader('Content-ID')); + } + + public function testDefaultConstructor() + { + $p = new Part(); + + $this->assertNull($p->getContent()); + $this->assertEquals('application/octet-stream', $p->getHeader('Content-Type')); + $this->assertEquals('utf-8', $p->getHeader('Content-Type', 'charset')); + $this->assertEquals(Part::ENCODING_BINARY, $p->getHeader('Content-Transfer-Encoding')); + $this->assertRegExp('~~', $p->getHeader('Content-ID')); + } + + public function testSetContent() + { + $p = new Part(); + + $p->setContent(''); + $this->assertEquals('', $p->getContent()); + } + + public function testGetMessagePart() + { + $p = new Part('', 'text/xml', 'utf-8', Part::ENCODING_BINARY, 'urn:myuniqueresource'); + + $messagePart = "Content-Type: text/xml; charset=utf-8\r\n" . + "Content-Transfer-Encoding: binary\r\n" . + "Content-ID: \r\n" . + "\r\n". + ""; + + $this->assertEquals($messagePart, $p->getMessagePart()); + } +} \ No newline at end of file From da344ad03f439bcb314a37eb86e1836a0a011735 Mon Sep 17 00:00:00 2001 From: Andreas Schamberger Date: Tue, 1 Nov 2011 08:28:08 +0100 Subject: [PATCH 5/5] CS fixes --- src/BeSimple/SoapCommon/Helper.php | 2 +- src/BeSimple/SoapCommon/Mime/Parser.php | 7 +++---- src/BeSimple/SoapCommon/Mime/PartHeader.php | 4 ++-- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/BeSimple/SoapCommon/Helper.php b/src/BeSimple/SoapCommon/Helper.php index 6d11b0f..58f9779 100644 --- a/src/BeSimple/SoapCommon/Helper.php +++ b/src/BeSimple/SoapCommon/Helper.php @@ -159,7 +159,7 @@ class Helper mt_rand(0, 0x3fff) | 0x8000, // 48 bits for "node" mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff) - ); + ); } /** diff --git a/src/BeSimple/SoapCommon/Mime/Parser.php b/src/BeSimple/SoapCommon/Mime/Parser.php index 7d98edf..ab41b97 100644 --- a/src/BeSimple/SoapCommon/Mime/Parser.php +++ b/src/BeSimple/SoapCommon/Mime/Parser.php @@ -50,8 +50,7 @@ class Parser $lines = preg_split("/\r\n|\n/", $mimeMessage); foreach ($lines as $line) { // ignore http status code and POST * - if ( substr( $line, 0, 5 ) == 'HTTP/' || substr( $line, 0, 4 ) == 'POST') - { + if (substr($line, 0, 5) == 'HTTP/' || substr($line, 0, 4) == 'POST') { continue; } if (isset($currentHeader)) { @@ -59,7 +58,7 @@ class Parser $currentHeader .= $line; continue; } - if (strpos($currentHeader,':') !== false) { + if (strpos($currentHeader, ':') !== false) { list($headerName, $headerValue) = explode(':', $currentHeader, 2); $headerValue = iconv_mime_decode($headerValue, 0, 'utf-8'); if (strpos($headerValue, ';') !== false) { @@ -129,7 +128,7 @@ class Parser * e.g. Content-Type: multipart/related; boundary=boundary; type=text/xml; * start="<123@abc>" * - * @see https://labs.omniti.com/alexandria/trunk/OmniTI/Mail/Parser.php + * 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 diff --git a/src/BeSimple/SoapCommon/Mime/PartHeader.php b/src/BeSimple/SoapCommon/Mime/PartHeader.php index b6458cd..8e78bea 100644 --- a/src/BeSimple/SoapCommon/Mime/PartHeader.php +++ b/src/BeSimple/SoapCommon/Mime/PartHeader.php @@ -103,7 +103,7 @@ abstract class PartHeader /** * Generates a header field value from the given value paramater. * - * @param array(string=>string)|string $value + * @param array(string=>string)|string $value Header value * @return string */ protected function generateHeaderFieldValue($value) @@ -128,7 +128,7 @@ abstract class PartHeader * Quote string with '"' if it contains one of the special characters: * "(" / ")" / "<" / ">" / "@" / "," / ";" / ":" / "\" / <"> / "/" / "[" / "]" / "?" / "=" * - * @param string $string + * @param string $string String to quote * @return string */ private function quoteValueString($string)