diff --git a/.gitignore b/.gitignore index 61ead86..f8d178e 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,5 @@ -/vendor +vendor +/phpunit.xml +.buildpath +.project +.settings \ No newline at end of file diff --git a/src/BeSimple/SoapServer/MimeFilter.php b/src/BeSimple/SoapServer/MimeFilter.php new file mode 100644 index 0000000..84f16ba --- /dev/null +++ b/src/BeSimple/SoapServer/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\SoapServer; + +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) + { + // array to store attachments + $attachmentsRecieved = array(); + + // check content type if it is a multipart mime message + $requestContentType = $request->getContentType(); + if (false !== stripos($requestContentType, 'multipart/related')) { + // parse mime message + $headers = array( + 'Content-Type' => trim($requestContentType), + ); + $multipart = MimeParser::parseMimeMessage($request->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()); + $request->setContent($content); + $request->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) { + $request->setAttachments($attachmentsRecieved); + } + } + + /** + * Modify the given response XML. + * + * @param \BeSimple\SoapCommon\SoapResponse $response SOAP response + * + * @return void + */ + public function filterResponse(SoapResponse $response) + { + // get attachments from request object + $attachmentsToSend = $response->getAttachments(); + + // build mime message if we have attachments + if (count($attachmentsToSend) > 0) { + $multipart = new MimeMultiPart(); + $soapPart = new MimePart($response->getContent(), 'text/xml', 'utf-8', MimePart::ENCODING_EIGHT_BIT); + $soapVersion = $response->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); + } + $response->setContent($multipart->getMimeMessage()); + + // TODO + $headers = $multipart->getHeadersForHttp(); + list($name, $contentType) = explode(': ', $headers[0]); + + $response->setContentType($contentType); + } + } +} \ No newline at end of file diff --git a/src/BeSimple/SoapServer/SoapKernel.php b/src/BeSimple/SoapServer/SoapKernel.php new file mode 100644 index 0000000..a928738 --- /dev/null +++ b/src/BeSimple/SoapServer/SoapKernel.php @@ -0,0 +1,47 @@ + + * (c) Francis Besset + * (c) Andreas Schamberger + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace BeSimple\SoapServer; + +use BeSimple\SoapCommon\SoapKernel as CommonSoapKernel; +use BeSimple\SoapCommon\SoapRequest; +use BeSimple\SoapCommon\SoapResponse; + +/** + * SoapKernel for Server. + * + * @author Andreas Schamberger + */ +class SoapKernel extends CommonSoapKernel +{ + /** + * {@inheritDoc} + */ + public function filterRequest(SoapRequest $request) + { + parent::filterRequest($request); + + $this->attachments = $request->getAttachments(); + } + + /** + * {@inheritDoc} + */ + public function filterResponse(SoapResponse $response) + { + $response->setAttachments($this->attachments); + $this->attachments = array(); + + parent::filterResponse($response); + } +} \ No newline at end of file diff --git a/src/BeSimple/SoapServer/SoapRequest.php b/src/BeSimple/SoapServer/SoapRequest.php new file mode 100644 index 0000000..8a03d7a --- /dev/null +++ b/src/BeSimple/SoapServer/SoapRequest.php @@ -0,0 +1,71 @@ + + * (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\SoapServer; + +use BeSimple\SoapCommon\SoapRequest as CommonSoapRequest; +use BeSimple\SoapCommon\SoapMessage; + +/** + * SoapRequest class for SoapClient. Provides factory function for request object. + * + * @author Andreas Schamberger + */ +class SoapRequest extends CommonSoapRequest +{ + /** + * Factory function for SoapRequest. + * + * @param string $content Content + * @param string $version SOAP version + * + * @return BeSimple\SoapClient\SoapRequest + */ + public static function create($content, $version) + { + $content = is_null($content) ? file_get_contents("php://input") : $content; + $location = self::getCurrentUrl(); + $action = $_SERVER[SoapMessage::SOAP_ACTION_HEADER]; + $contentType = $_SERVER[SoapMessage::CONTENT_TYPE_HEADER]; + + $request = new SoapRequest(); + // $content is if unmodified from SoapClient not a php string type! + $request->setContent((string) $content); + $request->setLocation($location); + $request->setAction($action); + $request->setVersion($version); + $request->setContentType($contentType); + + return $request; + } + + /** + * Builds the current URL from the $_SERVER array. + * + * @return string + */ + public static function getCurrentUrl() + { + $url = ''; + if (isset($_SERVER['HTTPS']) && (strtolower($_SERVER['HTTPS']) === 'on' || $_SERVER['HTTPS'] === '1')) { + $url .= 'https://'; + } else { + $url .= 'http://'; + } + $url .= isset( $_SERVER['SERVER_NAME'] ) ? $_SERVER['SERVER_NAME'] : ''; + if (isset($_SERVER['SERVER_PORT']) && $_SERVER['SERVER_PORT'] != 80) { + $url .= ":{$_SERVER['SERVER_PORT']}"; + } + $url .= isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : ''; + return $url; + } +} \ No newline at end of file diff --git a/src/BeSimple/SoapServer/SoapResponse.php b/src/BeSimple/SoapServer/SoapResponse.php new file mode 100644 index 0000000..e624fcb --- /dev/null +++ b/src/BeSimple/SoapServer/SoapResponse.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\SoapServer; + +use BeSimple\SoapCommon\SoapResponse as CommonSoapResponse; +use BeSimple\SoapCommon\SoapMessage; + +/** + * SoapResponse class for SoapClient. Provides factory function for response object. + * + * @author Andreas Schamberger + */ +class SoapResponse extends CommonSoapResponse +{ + /** + * Factory function for SoapResponse. + * + * @param string $content Content + * @param string $location Location + * @param string $action SOAP action + * @param string $version SOAP version + * + * @return BeSimple\SoapClient\SoapResponse + */ + public static function create($content, $location, $action, $version) + { + $response = new SoapResponse(); + $response->setContent($content); + $response->setLocation($location); + $response->setAction($action); + $response->setVersion($version); + $contentType = SoapMessage::getContentTypeForVersion($version); + $response->setContentType($contentType); + + return $response; + } + + /** + * Send SOAP response to client. + */ + public function send() + { + // set Content-Type header + header('Content-Type: ' . $this->getContentType()); + // get content to send + $response = $this->getContent(); + // set Content-Length header + header('Content-Length: '. strlen($response)); + // send response to client + echo $response; + } +} \ No newline at end of file diff --git a/src/BeSimple/SoapServer/SoapServer.php b/src/BeSimple/SoapServer/SoapServer.php index afc044c..ced2ad0 100644 --- a/src/BeSimple/SoapServer/SoapServer.php +++ b/src/BeSimple/SoapServer/SoapServer.php @@ -12,18 +12,150 @@ namespace BeSimple\SoapServer; +use BeSimple\SoapCommon\Helper; +use BeSimple\SoapCommon\Converter\MtomTypeConverter; +use BeSimple\SoapCommon\Converter\SwaTypeConverter; + /** + * Extended SoapServer that allows adding filters for SwA, MTOM, ... . + * + * @author Andreas Schamberger * @author Christian Kerl */ class SoapServer extends \SoapServer { + /** + * Soap version. + * + * @var int + */ + protected $soapVersion = SOAP_1_1; + + /** + * Soap kernel. + * + * @var \BeSimple\SoapServer\SoapKernel + */ + protected $soapKernel = null; + + /** + * Constructor. + * + * @param string $wsdl WSDL file + * @param array(string=>mixed) $options Options array + */ public function __construct($wsdl, array $options = array()) { + // store SOAP version + if (isset($options['soap_version'])) { + $this->soapVersion = $options['soap_version']; + } + // create soap kernel instance + $this->soapKernel = new SoapKernel(); + // set up type converter and mime filter + $this->configureMime($options); + // we want the exceptions option to be set + $options['exceptions'] = true; parent::__construct($wsdl, $options); } - public function handle($soap_request = null) + /** + * Custom handle method to be able to modify the SOAP messages. + * + * @param string $request Request string + */ + public function handle($request = null) { - parent::handle($soap_request); + // wrap request data in SoapRequest object + $soapRequest = SoapRequest::create($request, $this->soapVersion); + + // handle actual SOAP request + $soapResponse = $this->handle2($soapRequest); + + // send SOAP response to client + $soapResponse->send(); + } + + /** + * Runs the currently registered request filters on the request, calls the + * necessary functions (through the parent's class handle()) and runs the + * response filters. + * + * @param SoapRequest $soapRequest SOAP request object + * + * @return SoapResponse + */ + public function handle2(SoapRequest $soapRequest) + { + // run SoapKernel on SoapRequest + $this->soapKernel->filterRequest($soapRequest); + + // call parent \SoapServer->handle() and buffer output + ob_start(); + parent::handle($soapRequest->getContent()); + $response = ob_get_clean(); + + // wrap response data in SoapResponse object + $soapResponse = SoapResponse::create( + $response, + $soapRequest->getLocation(), + $soapRequest->getAction(), + $soapRequest->getVersion() + ); + + // run SoapKernel on SoapResponse + $this->soapKernel->filterResponse($soapResponse); + + return $soapResponse; + } + + /** + * Get SoapKernel instance. + * + * @return \BeSimple\SoapServer\SoapKernel + */ + public function getSoapKernel() + { + return $this->soapKernel; + } + + /** + * Configure filter and type converter for SwA/MTOM. + * + * @param array &$options SOAP constructor options array. + * + * @return void + */ + private function configureMime(array &$options) + { + if (isset($options['attachment_type']) && Helper::ATTACHMENTS_TYPE_BASE64 !== $options['attachment_type']) { + // register mime filter in SoapKernel + $mimeFilter = new MimeFilter($options['attachment_type']); + $this->soapKernel->registerFilter($mimeFilter); + // configure type converter + if (Helper::ATTACHMENTS_TYPE_SWA === $options['attachment_type']) { + $converter = new SwaTypeConverter(); + $converter->setKernel($this->soapKernel); + } elseif (Helper::ATTACHMENTS_TYPE_MTOM === $options['attachment_type']) { + $xmlMimeFilter = new XmlMimeFilter($options['attachment_type']); + $this->soapKernel->registerFilter($xmlMimeFilter); + $converter = new MtomTypeConverter(); + $converter->setKernel($this->soapKernel); + } + // configure typemap + if (!isset($options['typemap'])) { + $options['typemap'] = array(); + } + $options['typemap'][] = array( + 'type_name' => $converter->getTypeName(), + 'type_ns' => $converter->getTypeNamespace(), + 'from_xml' => function($input) use ($converter) { + return $converter->convertXmlToPhp($input); + }, + 'to_xml' => function($input) use ($converter) { + return $converter->convertPhpToXml($input); + }, + ); + } } } \ No newline at end of file diff --git a/src/BeSimple/SoapServer/XmlMimeFilter.php b/src/BeSimple/SoapServer/XmlMimeFilter.php new file mode 100644 index 0000000..7562745 --- /dev/null +++ b/src/BeSimple/SoapServer/XmlMimeFilter.php @@ -0,0 +1,75 @@ + + * (c) Francis Besset + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace BeSimple\SoapServer; + +use BeSimple\SoapCommon\FilterHelper; +use BeSimple\SoapCommon\Helper; +use BeSimple\SoapCommon\Mime\MultiPart as MimeMultiPart; +use BeSimple\SoapCommon\Mime\Parser as MimeParser; +use BeSimple\SoapCommon\Mime\Part as MimePart; +use BeSimple\SoapCommon\SoapRequest; +use BeSimple\SoapCommon\SoapRequestFilter; +use BeSimple\SoapCommon\SoapResponse; +use BeSimple\SoapCommon\SoapResponseFilter; + +/** + * XML MIME filter that fixes the namespace of xmime:contentType attribute. + * + * @author Andreas Schamberger + */ +class XmlMimeFilter implements SoapResponseFilter +{ + /** + * Reset all properties to default values. + */ + public function resetFilter() + { + } + + /** + * Modify the given response XML. + * + * @param \BeSimple\SoapCommon\SoapResponse $response SOAP request + * + * @return void + */ + public function filterResponse(SoapResponse $response) + { + // get \DOMDocument from SOAP request + $dom = $response->getContentDocument(); + + // create FilterHelper + $filterHelper = new FilterHelper($dom); + + // add the neccessary namespaces + $filterHelper->addNamespace(Helper::PFX_XMLMIME, Helper::NS_XMLMIME); + + // get xsd:base64binary elements + $xpath = new \DOMXPath($dom); + $xpath->registerNamespace('XOP', Helper::NS_XOP); + $query = '//XOP:Include/..'; + $nodes = $xpath->query($query); + + // exchange attributes + if ($nodes->length > 0) { + foreach ($nodes as $node) { + if ($node->hasAttribute('contentType')) { + $contentType = $node->getAttribute('contentType'); + $node->removeAttribute('contentType'); + $filterHelper->setAttribute($node, Helper::NS_XMLMIME, 'contentType', $contentType); + } + } + } + + } +} \ No newline at end of file