From 2b2d40b3ac058c65569c93b4e18277c3010f91e6 Mon Sep 17 00:00:00 2001 From: Andreas Schamberger Date: Sat, 7 Jan 2012 13:19:22 +0100 Subject: [PATCH] moved code from client to common --- .../InternalTypeConverterInterface.php | 58 ++++++ .../Converter/MtomTypeConverter.php | 96 ++++++++++ .../SoapCommon/Converter/SwaTypeConverter.php | 82 ++++++++ .../Converter/TypeConverterInterface.php | 26 +++ src/BeSimple/SoapCommon/FilterHelper.php | 178 ++++++++++++++++++ src/BeSimple/SoapCommon/MimeFilter.php | 138 ++++++++++++++ src/BeSimple/SoapCommon/SoapKernel.php | 40 +++- 7 files changed, 617 insertions(+), 1 deletion(-) create mode 100644 src/BeSimple/SoapCommon/Converter/InternalTypeConverterInterface.php create mode 100644 src/BeSimple/SoapCommon/Converter/MtomTypeConverter.php create mode 100644 src/BeSimple/SoapCommon/Converter/SwaTypeConverter.php create mode 100644 src/BeSimple/SoapCommon/FilterHelper.php create mode 100644 src/BeSimple/SoapCommon/MimeFilter.php diff --git a/src/BeSimple/SoapCommon/Converter/InternalTypeConverterInterface.php b/src/BeSimple/SoapCommon/Converter/InternalTypeConverterInterface.php new file mode 100644 index 0000000..9c86b66 --- /dev/null +++ b/src/BeSimple/SoapCommon/Converter/InternalTypeConverterInterface.php @@ -0,0 +1,58 @@ + + * (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\Converter; + +use BeSimple\SoapCommon\SoapKernel; + +/** + * Internal type converter interface. + * + * @author Andreas Schamberger + * @author Christian Kerl + */ +interface InternalTypeConverterInterface +{ + /** + * Get type namespace. + * + * @return string + */ + function getTypeNamespace(); + + /** + * Get type name. + * + * @return string + */ + function getTypeName(); + + /** + * Convert given XML string to PHP type. + * + * @param string $data XML string + * @param \BeSimple\SoapCommon\SoapKernel $soapKernel SoapKernel instance + * + * @return mixed + */ + function convertXmlToPhp($data, SoapKernel $soapKernel); + + /** + * Convert PHP type to XML string. + * + * @param mixed $data PHP type + * @param \BeSimple\SoapCommon\SoapKernel $soapKernel SoapKernel instance + * + * @return string + */ + function convertPhpToXml($data, SoapKernel $soapKernel); +} \ No newline at end of file diff --git a/src/BeSimple/SoapCommon/Converter/MtomTypeConverter.php b/src/BeSimple/SoapCommon/Converter/MtomTypeConverter.php new file mode 100644 index 0000000..a9709e2 --- /dev/null +++ b/src/BeSimple/SoapCommon/Converter/MtomTypeConverter.php @@ -0,0 +1,96 @@ + + * (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\Converter; + +use BeSimple\SoapCommon\Helper; +use BeSimple\SoapCommon\Mime\Part as MimePart; +use BeSimple\SoapCommon\SoapKernel; +use BeSimple\SoapCommon\SoapRequest as CommonSoapRequest; +use BeSimple\SoapCommon\SoapResponse as CommonSoapResponse; +use BeSimple\SoapCommon\Converter\InternalTypeConverterInterface; + +/** + * MTOM type converter. + * + * @author Andreas Schamberger + */ +class MtomTypeConverter implements InternalTypeConverterInterface +{ + /** + * {@inheritDoc} + */ + public function getTypeNamespace() + { + return 'http://www.w3.org/2001/XMLSchema'; + } + + /** + * {@inheritDoc} + */ + public function getTypeName() + { + return 'base64Binary'; + } + + /** + * {@inheritDoc} + */ + public function convertXmlToPhp($data, SoapKernel $soapKernel) + { + $doc = new \DOMDocument(); + $doc->loadXML($data); + + $includes = $doc->getElementsByTagNameNS(Helper::NS_XOP, 'Include'); + $include = $includes->item(0); + + // convert href -> myhref for external references as PHP throws exception in this case + // http://svn.php.net/viewvc/php/php-src/branches/PHP_5_4/ext/soap/php_encoding.c?view=markup#l3436 + $ref = $include->getAttribute('myhref'); + + if ('cid:' === substr($ref, 0, 4)) { + $contentId = urldecode(substr($ref, 4)); + + if (null !== ($part = $soapKernel->getAttachment($contentId))) { + + return $part->getContent(); + } else { + + return null; + } + } + + return $data; + } + + /** + * {@inheritDoc} + */ + public function convertPhpToXml($data, SoapKernel $soapKernel) + { + $part = new MimePart($data); + $contentId = trim($part->getHeader('Content-ID'), '<>'); + + $soapKernel->addAttachment($part); + + $doc = new \DOMDocument(); + $node = $doc->createElement($this->getTypeName()); + $doc->appendChild($node); + + // add xop:Include element + $xinclude = $doc->createElementNS(Helper::NS_XOP, Helper::PFX_XOP . ':Include'); + $xinclude->setAttribute('href', 'cid:' . $contentId); + $node->appendChild($xinclude); + + return $doc->saveXML(); + } +} diff --git a/src/BeSimple/SoapCommon/Converter/SwaTypeConverter.php b/src/BeSimple/SoapCommon/Converter/SwaTypeConverter.php new file mode 100644 index 0000000..32ba467 --- /dev/null +++ b/src/BeSimple/SoapCommon/Converter/SwaTypeConverter.php @@ -0,0 +1,82 @@ + + * (c) Francis Besset + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace BeSimple\SoapCommon\Converter; + +use BeSimple\SoapCommon\Helper; +use BeSimple\SoapCommon\Mime\Part as MimePart; +use BeSimple\SoapCommon\SoapKernel; +use BeSimple\SoapCommon\Converter\InternalTypeConverterInterface; + +/** + * SwA type converter. + * + * @author Andreas Schamberger + */ +class SwaTypeConverter implements InternalTypeConverterInterface +{ + /** + * {@inheritDoc} + */ + public function getTypeNamespace() + { + return 'http://www.w3.org/2001/XMLSchema'; + } + + /** + * {@inheritDoc} + */ + public function getTypeName() + { + return 'base64Binary'; + } + + /** + * {@inheritDoc} + */ + public function convertXmlToPhp($data, SoapKernel $soapKernel) + { + $doc = new \DOMDocument(); + $doc->loadXML($data); + + // convert href -> myhref for external references as PHP throws exception in this case + // http://svn.php.net/viewvc/php/php-src/branches/PHP_5_4/ext/soap/php_encoding.c?view=markup#l3436 + $ref = $doc->documentElement->getAttribute('myhref'); + + if ('cid:' === substr($ref, 0, 4)) { + $contentId = urldecode(substr($ref, 4)); + + if (null !== ($part = $soapKernel->getAttachment($contentId))) { + + return $part->getContent(); + } else { + + return null; + } + } + + return $data; + } + + /** + * {@inheritDoc} + */ + public function convertPhpToXml($data, SoapKernel $soapKernel) + { + $part = new MimePart($data); + $contentId = trim($part->getHeader('Content-ID'), '<>'); + + $soapKernel->addAttachment($part); + + return sprintf('<%s href="%s"/>', $this->getTypeName(), $contentId); + } +} diff --git a/src/BeSimple/SoapCommon/Converter/TypeConverterInterface.php b/src/BeSimple/SoapCommon/Converter/TypeConverterInterface.php index 4de7d7f..e390963 100644 --- a/src/BeSimple/SoapCommon/Converter/TypeConverterInterface.php +++ b/src/BeSimple/SoapCommon/Converter/TypeConverterInterface.php @@ -13,15 +13,41 @@ namespace BeSimple\SoapCommon\Converter; /** + * Type converter interface. + * * @author Christian Kerl */ interface TypeConverterInterface { + /** + * Get type namespace. + * + * @return string + */ function getTypeNamespace(); + /** + * Get type name. + * + * @return string + */ function getTypeName(); + /** + * Convert given XML string to PHP type. + * + * @param string $data XML string + * + * @return mixed + */ function convertXmlToPhp($data); + /** + * Convert PHP type to XML string. + * + * @param mixed $data PHP type + * + * @return string + */ function convertPhpToXml($data); } \ No newline at end of file diff --git a/src/BeSimple/SoapCommon/FilterHelper.php b/src/BeSimple/SoapCommon/FilterHelper.php new file mode 100644 index 0000000..01a1717 --- /dev/null +++ b/src/BeSimple/SoapCommon/FilterHelper.php @@ -0,0 +1,178 @@ + + * (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 request/response filter helper for manipulating SOAP messages. + * + * @author Andreas Schamberger + */ +class FilterHelper +{ + /** + * DOMDocument on which the helper functions operate. + * + * @var \DOMDocument + */ + protected $domDocument = null; + + /** + * Namespaces added. + * + * @var array(string=>string) + */ + protected $namespaces = array(); + + /** + * Constructor. + * + * @param \DOMDocument $domDocument SOAP document + */ + public function __construct(\DOMDocument $domDocument) + { + $this->domDocument = $domDocument; + } + + /** + * Add new soap header. + * + * @param \DOMElement $node DOMElement to add + * @param boolean $mustUnderstand SOAP header mustUnderstand attribute + * @param string $actor SOAP actor/role + * @param string $soapVersion SOAP version SOAP_1_1|SOAP_1_2 + * + * @return void + */ + public function addHeaderElement(\DOMElement $node, $mustUnderstand = null, $actor = null, $soapVersion = SOAP_1_1) + { + $root = $this->domDocument->documentElement; + $namespace = $root->namespaceURI; + $prefix = $root->prefix; + if (null !== $mustUnderstand) { + $node->appendChild(new \DOMAttr($prefix . ':mustUnderstand', (int) $mustUnderstand)); + } + if (null !== $actor) { + $attributeName = ($soapVersion == SOAP_1_1) ? 'actor' : 'role'; + $node->appendChild(new \DOMAttr($prefix . ':' . $attributeName, $actor)); + } + $nodeListHeader = $root->getElementsByTagNameNS($namespace, 'Header'); + // add header if not there + if ($nodeListHeader->length == 0) { + // new header element + $header = $this->domDocument->createElementNS($namespace, $prefix . ':Header'); + // try to add it before body + $nodeListBody = $root->getElementsByTagNameNS($namespace, 'Body'); + if ($nodeListBody->length == 0) { + $root->appendChild($header); + } else { + $body = $nodeListBody->item(0); + $header = $body->parentNode->insertBefore($header, $body); + } + $header->appendChild($node); + } else { + $nodeListHeader->item(0)->appendChild($node); + } + } + + /** + * Add new soap body element. + * + * @param \DOMElement $node DOMElement to add + * + * @return void + */ + public function addBodyElement(\DOMElement $node) + { + $root = $this->domDocument->documentElement; + $namespace = $root->namespaceURI; + $prefix = $root->prefix; + $nodeList = $this->domDocument->getElementsByTagNameNS($namespace, 'Body'); + // add body if not there + if ($nodeList->length == 0) { + // new body element + $body = $this->domDocument->createElementNS($namespace, $prefix . ':Body'); + $root->appendChild($body); + $body->appendChild($node); + } else { + $nodeList->item(0)->appendChild($node); + } + } + + /** + * Add new namespace to root tag. + * + * @param string $prefix Namespace prefix + * @param string $namespaceURI Namespace URI + * + * @return void + */ + public function addNamespace($prefix, $namespaceURI) + { + if (!isset($this->namespaces[$namespaceURI])) { + $root = $this->domDocument->documentElement; + $root->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:' . $prefix, $namespaceURI); + $this->namespaces[$namespaceURI] = $prefix; + } + } + + /** + * Create new element for given namespace. + * + * @param string $namespaceURI Namespace URI + * @param string $name Element name + * @param string $value Element value + * + * @return \DOMElement + */ + public function createElement($namespaceURI, $name, $value = null) + { + $prefix = $this->namespaces[$namespaceURI]; + + return $this->domDocument->createElementNS($namespaceURI, $prefix . ':' . $name, $value); + } + + /** + * Add new attribute to element with given namespace. + * + * @param \DOMElement $element DOMElement to edit + * @param string $namespaceURI Namespace URI + * @param string $name Attribute name + * @param string $value Attribute value + * + * @return void + */ + public function setAttribute(\DOMElement $element, $namespaceURI, $name, $value) + { + if (null !== $namespaceURI) { + $prefix = $this->namespaces[$namespaceURI]; + $element->setAttributeNS($namespaceURI, $prefix . ':' . $name, $value); + } else { + $element->setAttribute($name, $value); + } + } + + /** + * Register namespace. + * + * @param string $prefix Namespace prefix + * @param string $namespaceURI Namespace URI + * + * @return void + */ + public function registerNamespace($prefix, $namespaceURI) + { + if (!isset($this->namespaces[$namespaceURI])) { + $this->namespaces[$namespaceURI] = $prefix; + } + } +} \ No newline at end of file diff --git a/src/BeSimple/SoapCommon/MimeFilter.php b/src/BeSimple/SoapCommon/MimeFilter.php new file mode 100644 index 0000000..068deb7 --- /dev/null +++ b/src/BeSimple/SoapCommon/MimeFilter.php @@ -0,0 +1,138 @@ + + * (c) Francis Besset + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace BeSimple\SoapCommon; + +use BeSimple\SoapCommon\Helper; +use BeSimple\SoapCommon\Mime\MultiPart as MimeMultiPart; +use BeSimple\SoapCommon\Mime\Parser as MimeParser; +use BeSimple\SoapCommon\Mime\Part as MimePart; +use BeSimple\SoapCommon\SoapRequest; +use BeSimple\SoapCommon\SoapRequestFilter; +use BeSimple\SoapCommon\SoapResponse; +use BeSimple\SoapCommon\SoapResponseFilter; + +/** + * MIME filter. + * + * @author Andreas Schamberger + */ +class MimeFilter implements SoapRequestFilter, SoapResponseFilter +{ + /** + * Attachment type. + * + * @var int Helper::ATTACHMENTS_TYPE_SWA | Helper::ATTACHMENTS_TYPE_MTOM + */ + protected $attachmentType = Helper::ATTACHMENTS_TYPE_SWA; + + /** + * Constructor. + * + * @param int $attachmentType Helper::ATTACHMENTS_TYPE_SWA | Helper::ATTACHMENTS_TYPE_MTOM + */ + public function __construct($attachmentType) + { + $this->attachmentType = $attachmentType; + } + + /** + * Reset all properties to default values. + */ + public function resetFilter() + { + $this->attachmentType = Helper::ATTACHMENTS_TYPE_SWA; + } + + /** + * Modify the given request XML. + * + * @param \BeSimple\SoapCommon\SoapRequest $request SOAP request + * + * @return void + */ + public function filterRequest(SoapRequest $request) + { + // get attachments from request object + $attachmentsToSend = $request->getAttachments(); + + // build mime message if we have attachments + if (count($attachmentsToSend) > 0) { + $multipart = new MimeMultiPart(); + $soapPart = new MimePart($request->getContent(), 'text/xml', 'utf-8', MimePart::ENCODING_EIGHT_BIT); + $soapVersion = $request->getVersion(); + // change content type headers for MTOM with SOAP 1.1 + if ($soapVersion == SOAP_1_1 && $this->attachmentType & Helper::ATTACHMENTS_TYPE_MTOM) { + $multipart->setHeader('Content-Type', 'type', 'application/xop+xml'); + $multipart->setHeader('Content-Type', 'start-info', 'text/xml'); + $soapPart->setHeader('Content-Type', 'application/xop+xml'); + $soapPart->setHeader('Content-Type', 'type', 'text/xml'); + } + // change content type headers for SOAP 1.2 + elseif ($soapVersion == SOAP_1_2) { + $multipart->setHeader('Content-Type', 'type', 'application/soap+xml'); + $soapPart->setHeader('Content-Type', 'application/soap+xml'); + } + $multipart->addPart($soapPart, true); + foreach ($attachmentsToSend as $cid => $attachment) { + $multipart->addPart($attachment, false); + } + $request->setContent($multipart->getMimeMessage()); + + // TODO + $headers = $multipart->getHeadersForHttp(); + list($name, $contentType) = explode(': ', $headers[0]); + + $request->setContentType($contentType); + } + } + + /** + * Modify the given response XML. + * + * @param \BeSimple\SoapCommon\SoapResponse $response SOAP response + * + * @return void + */ + public function filterResponse(SoapResponse $response) + { + // array to store attachments + $attachmentsRecieved = array(); + + // check content type if it is a multipart mime message + $responseContentType = $response->getContentType(); + if (false !== stripos($responseContentType, 'multipart/related')) { + // parse mime message + $headers = array( + 'Content-Type' => trim($responseContentType), + ); + $multipart = MimeParser::parseMimeMessage($response->getContent(), $headers); + // get soap payload and update SoapResponse object + $soapPart = $multipart->getPart(); + // convert href -> myhref for external references as PHP throws exception in this case + // http://svn.php.net/viewvc/php/php-src/branches/PHP_5_4/ext/soap/php_encoding.c?view=markup#l3436 + $content = preg_replace('/href=(?!#)/', 'myhref=', $soapPart->getContent()); + $response->setContent($content); + $response->setContentType($soapPart->getHeader('Content-Type')); + // store attachments + $attachments = $multipart->getParts(false); + foreach ($attachments as $cid => $attachment) { + $attachmentsRecieved[$cid] = $attachment; + } + } + + // add attachments to response object + if (count($attachmentsRecieved) > 0) { + $response->setAttachments($attachmentsRecieved); + } + } +} \ No newline at end of file diff --git a/src/BeSimple/SoapCommon/SoapKernel.php b/src/BeSimple/SoapCommon/SoapKernel.php index a49b2b1..f0ead8c 100644 --- a/src/BeSimple/SoapCommon/SoapKernel.php +++ b/src/BeSimple/SoapCommon/SoapKernel.php @@ -15,9 +15,10 @@ namespace BeSimple\SoapCommon; use BeSimple\SoapCommon\Mime\Part as MimePart; +use BeSimple\SoapCommon\Converter\MtomTypeConverter; +use BeSimple\SoapCommon\Converter\SwaTypeConverter; use BeSimple\SoapCommon\SoapRequest; use BeSimple\SoapCommon\SoapResponse; - use BeSimple\SoapCommon\SoapRequestFilter; use BeSimple\SoapCommon\SoapResponseFilter; @@ -133,4 +134,41 @@ class SoapKernel $this->attachments = $response->getAttachments(); } + + /** + * Configure filter and type converter for SwA/MTOM. + * + * @param array &$options SOAP constructor options array. + * + * @return void + */ + public function configureMime(array &$options) + { + if (isset($options['attachment_type']) && Helper::ATTACHMENTS_TYPE_BASE64 !== $options['attachment_type']) { + // register mime filter in SoapKernel + $mimeFilter = new MimeFilter($options['attachment_type']); + $this->registerFilter($mimeFilter); + // configure type converter + if (Helper::ATTACHMENTS_TYPE_SWA === $options['attachment_type']) { + $converter = new SwaTypeConverter(); + } elseif (Helper::ATTACHMENTS_TYPE_MTOM === $options['attachment_type']) { + $converter = new MtomTypeConverter(); + } + // configure typemap + if (!isset($options['typemap'])) { + $options['typemap'] = array(); + } + $soapKernel = $this; + $options['typemap'][] = array( + 'type_name' => $converter->getTypeName(), + 'type_ns' => $converter->getTypeNamespace(), + 'from_xml' => function($input) use ($converter, $soapKernel) { + return $converter->convertXmlToPhp($input, $soapKernel); + }, + 'to_xml' => function($input) use ($converter, $soapKernel) { + return $converter->convertPhpToXml($input, $soapKernel); + }, + ); + } + } } \ No newline at end of file