SoapClient large refactoring & tests update

This commit is contained in:
Petr Bechyně
2017-02-03 15:22:37 +01:00
parent 00ddf149b0
commit aee034791e
78 changed files with 4957 additions and 1126 deletions

View File

@ -181,4 +181,9 @@ class MultiPart extends PartHeader
{
return 'multipart-boundary-' . Helper::generateUUID() . '@response.info';
}
public function getMainPartContentId()
{
return $this->mainPartContentId;
}
}

View File

@ -12,183 +12,241 @@
namespace BeSimple\SoapCommon\Mime;
use BeSimple\SoapCommon\Mime\Parser\ContentTypeParser;
use BeSimple\SoapCommon\Mime\Parser\ParsedPart;
use BeSimple\SoapCommon\Mime\Parser\ParsedPartList;
use Exception;
/**
* Simple Multipart-Mime parser.
*
* @author Andreas Schamberger <mail@andreass.net>
* @author Petr Bechyne <mail@petrbechyne.com>
*/
class Parser
{
const HAS_HTTP_REQUEST_HEADERS = true;
const HAS_NO_HTTP_REQUEST_HEADERS = false;
/**
* Parse the given Mime-Message and return a \BeSimple\SoapCommon\Mime\MultiPart object.
*
* @param string $mimeMessage Mime message string
* @param array(string=>string) $headers Array of header elements (e.g. coming from http request)
* @param string[] $headers array(string=>string) of header elements (e.g. coming from http request)
*
* @return \BeSimple\SoapCommon\Mime\MultiPart
*/
public static function parseMimeMessage($mimeMessage, array $headers = [])
{
$boundary = null;
$start = null;
$multipart = new MultiPart();
$hitFirstBoundary = false;
$inHeader = true;
$multiPart = new MultiPart();
$mimeMessageLines = preg_split("/(\n)/", $mimeMessage);
// add given headers, e.g. coming from HTTP headers
if (count($headers) > 0) {
foreach ($headers as $name => $value) {
if ($name === 'Content-Type') {
self::parseContentTypeHeader($multipart, $name, $value);
$boundary = $multipart->getHeader('Content-Type', 'boundary');
$start = $multipart->getHeader('Content-Type', 'start');
} else {
$multipart->setHeader($name, $value);
}
}
$inHeader = false;
self::setMultiPartHeaders($multiPart, $headers);
$hasHttpRequestHeaders = self::HAS_HTTP_REQUEST_HEADERS;
} else {
$hasHttpRequestHeaders = self::HAS_NO_HTTP_REQUEST_HEADERS;
}
$content = '';
$currentPart = $multipart;
$lines = preg_split("/(\r\n)|(\n)/", $mimeMessage);
if (self::hasBoundary($lines)) {
foreach ($lines as $line) {
// ignore http status code and POST *
if (substr($line, 0, 5) == 'HTTP/' || substr($line, 0, 4) == 'POST') {
if (self::hasBoundary($mimeMessageLines)) {
$parsedPartList = self::getPartsFromMimeMessageLines(
$multiPart,
$mimeMessageLines,
$hasHttpRequestHeaders
);
if ($parsedPartList->hasParts() === false) {
throw new Exception(
'Could not parse MimeMessage: no Parts for MultiPart given'
);
}
if ($parsedPartList->hasExactlyOneMainPart() === false) {
throw new Exception(
sprintf(
'Could not parse MimeMessage %s HTTP headers: unexpected count of main ParsedParts: %s (total: %d)',
$hasHttpRequestHeaders ? 'with' : 'w/o',
implode(', ', $parsedPartList->getPartContentIds()),
$parsedPartList->getMainPartCount()
)
);
}
self::appendPartsToMultiPart(
$parsedPartList,
$multiPart
);
} else {
self::appendSingleMainPartToMultiPart(new Part($mimeMessage), $multiPart);
}
return $multiPart;
}
/**
* @param MultiPart $multiPart
* @param string[] $mimeMessageLines
* @param bool $hasHttpHeaders = self::HAS_HTTP_REQUEST_HEADERS|self::HAS_NO_HTTP_REQUEST_HEADERS
* @return ParsedPartList
*/
private static function getPartsFromMimeMessageLines(
MultiPart $multiPart,
array $mimeMessageLines,
$hasHttpHeaders
) {
$parsedParts = [];
$contentTypeBoundary = $multiPart->getHeader('Content-Type', 'boundary');
$contentTypeContentIdStart = $multiPart->getHeader('Content-Type', 'start');
$currentPart = $multiPart;
$messagePartStringContent = '';
$inHeader = $hasHttpHeaders;
$hitFirstBoundary = false;
foreach ($mimeMessageLines as $mimeMessageLine) {
// ignore http status code and POST *
if (substr($mimeMessageLine, 0, 5) == 'HTTP/' || substr($mimeMessageLine, 0, 4) == 'POST') {
continue;
}
if (isset($currentHeader)) {
if (isset($mimeMessageLine[0]) && ($mimeMessageLine[0] === ' ' || $mimeMessageLine[0] === "\t")) {
$currentHeader .= $mimeMessageLine;
continue;
}
if (isset($currentHeader)) {
if (isset($line[0]) && ($line[0] === ' ' || $line[0] === "\t")) {
$currentHeader .= $line;
continue;
if (strpos($currentHeader, ':') !== false) {
list($headerName, $headerValue) = explode(':', $currentHeader, 2);
$headerValue = iconv_mime_decode($headerValue, 0, Part::CHARSET_UTF8);
$parsedMimeHeaders = ContentTypeParser::parseContentTypeHeader($headerName, $headerValue);
foreach ($parsedMimeHeaders as $parsedMimeHeader) {
$currentPart->setHeader(
$parsedMimeHeader->getName(),
$parsedMimeHeader->getValue(),
$parsedMimeHeader->getSubValue()
);
}
if (strpos($currentHeader, ':') !== false) {
list($headerName, $headerValue) = explode(':', $currentHeader, 2);
$headerValue = iconv_mime_decode($headerValue, 0, 'utf-8');
if (strpos($headerValue, ';') !== false) {
self::parseContentTypeHeader($currentPart, $headerName, $headerValue);
$boundary = $multipart->getHeader('Content-Type', 'boundary');
$start = $multipart->getHeader('Content-Type', 'start');
} else {
$currentPart->setHeader($headerName, trim($headerValue));
}
}
unset($currentHeader);
$contentTypeBoundary = $multiPart->getHeader('Content-Type', 'boundary');
$contentTypeContentIdStart = $multiPart->getHeader('Content-Type', 'start');
}
if ($inHeader) {
if (trim($line) == '') {
$inHeader = false;
continue;
}
$currentHeader = $line;
unset($currentHeader);
}
if ($inHeader === true) {
if (trim($mimeMessageLine) == '') {
$inHeader = false;
continue;
} else {
if (self::isBoundary($line)) {
if (strcmp(trim($line), '--' . $boundary) === 0) {
if ($currentPart instanceof Part) {
$content = substr($content, 0, -1);
self::decodeContent($currentPart, $content);
// check if there is a start parameter given, if not set first part
$isMain = (is_null($start) || $start == $currentPart->getHeader('Content-ID')) ? true : false;
if ($isMain === true) {
$start = $currentPart->getHeader('Content-ID');
}
$multipart->addPart($currentPart, $isMain);
}
$currentPart = new Part();
$hitFirstBoundary = true;
$inHeader = true;
$content = '';
} elseif (strcmp(trim($line), '--' . $boundary . '--') === 0) {
$content = substr($content, 0, -1);
self::decodeContent($currentPart, $content);
}
$currentHeader = $mimeMessageLine;
continue;
} else {
if (self::isBoundary($mimeMessageLine)) {
if (self::isMiddleBoundary($mimeMessageLine, $contentTypeBoundary)) {
if ($currentPart instanceof Part) {
$currentPartContent = self::decodeContent(
$currentPart,
substr($messagePartStringContent, 0, -1)
);
$currentPart->setContent($currentPartContent);
// check if there is a start parameter given, if not set first part
$isMain = (is_null($start) || $start == $currentPart->getHeader('Content-ID')) ? true : false;
if ($isMain === true) {
$start = $currentPart->getHeader('Content-ID');
if ($contentTypeContentIdStart === null || $currentPart->hasContentId($contentTypeContentIdStart) === true) {
$contentTypeContentIdStart = $currentPart->getHeader('Content-ID');
$parsedParts[] = new ParsedPart($currentPart, ParsedPart::PART_IS_MAIN);
} else {
$parsedParts[] = new ParsedPart($currentPart, ParsedPart::PART_IS_NOT_MAIN);
}
$multipart->addPart($currentPart, $isMain);
$content = '';
}
$currentPart = new Part();
$hitFirstBoundary = true;
$inHeader = true;
$messagePartStringContent = '';
} else if (self::isLastBoundary($mimeMessageLine, $contentTypeBoundary)) {
$currentPartContent = self::decodeContent(
$currentPart,
substr($messagePartStringContent, 0, -1)
);
$currentPart->setContent($currentPartContent);
// check if there is a start parameter given, if not set first part
if ($contentTypeContentIdStart === null || $currentPart->hasContentId($contentTypeContentIdStart) === true) {
$contentTypeContentIdStart = $currentPart->getHeader('Content-ID');
$parsedParts[] = new ParsedPart($currentPart, ParsedPart::PART_IS_MAIN);
} else {
$parsedParts[] = new ParsedPart($currentPart, ParsedPart::PART_IS_NOT_MAIN);
}
$messagePartStringContent = '';
} else {
if ($hitFirstBoundary === false) {
if (trim($line) !== '') {
$inHeader = true;
$currentHeader = $line;
continue;
}
}
$content .= $line . "\n";
// else block migrated from https://github.com/progmancod/BeSimpleSoap/commit/bf9437e3bcf35c98c6c2f26aca655ec3d3514694
// be careful to replace \r\n with \n
$messagePartStringContent .= $mimeMessageLine . "\n";
}
} else {
if ($hitFirstBoundary === false) {
if (trim($mimeMessageLine) !== '') {
$inHeader = true;
$currentHeader = $mimeMessageLine;
continue;
}
}
$messagePartStringContent .= $mimeMessageLine . "\n";
}
}
} else {
$multipart->addPart(new Part($mimeMessage), true);
}
return $multipart;
return new ParsedPartList($parsedParts);
}
/**
* Parse a "Content-Type" header with multiple sub values.
* e.g. Content-Type: multipart/related; boundary=boundary; type=text/xml;
* start="<123@abc>"
*
* Based on: https://labs.omniti.com/alexandria/trunk/OmniTI/Mail/Parser.php
*
* @param \BeSimple\SoapCommon\Mime\PartHeader $part Header part
* @param string $headerName Header name
* @param string $headerValue Header value
*
* @param ParsedPartList $parsedPartList
* @param MultiPart $multiPart
*/
private static function parseContentTypeHeader(PartHeader $part, $headerName, $headerValue)
private static function appendPartsToMultiPart(ParsedPartList $parsedPartList, MultiPart $multiPart)
{
if (strpos($headerValue, ';')) {
list($value, $remainder) = explode(';', $headerValue, 2);
$value = trim($value);
$part->setHeader($headerName, $value);
$remainder = trim($remainder);
while (strlen($remainder) > 0) {
if (!preg_match('/^([a-zA-Z0-9_-]+)=(.{1})/', $remainder, $matches)) {
break;
foreach ($parsedPartList->getParts() as $parsedPart) {
$multiPart->addPart(
$parsedPart->getPart(),
$parsedPart->isMain()
);
}
}
private static function appendSingleMainPartToMultiPart(Part $part, MultiPart $multiPart)
{
$multiPart->addPart($part, true);
}
private static function setMultiPartHeaders(MultiPart $multiPart, $headers)
{
foreach ($headers as $name => $value) {
if ($name === 'Content-Type') {
$parsedMimeHeaders = ContentTypeParser::parseContentTypeHeader($name, $value);
foreach ($parsedMimeHeaders as $parsedMimeHeader) {
$multiPart->setHeader(
$parsedMimeHeader->getName(),
$parsedMimeHeader->getValue(),
$parsedMimeHeader->getSubValue()
);
}
$name = $matches[1];
$delimiter = $matches[2];
$remainder = substr($remainder, strlen($name) + 1);
if (!preg_match('/([^;]+)(;)?(\s|$)?/', $remainder, $matches)) {
break;
}
$value = rtrim($matches[1], ';');
if ($delimiter == "'" || $delimiter == '"') {
$value = trim($value, $delimiter);
}
$part->setHeader($headerName, $name, $value);
$remainder = substr($remainder, strlen($matches[0]));
} else {
$multiPart->setHeader($name, $value);
}
} else {
$part->setHeader($headerName, $headerValue);
}
}
/**
* Decodes the content of a Mime part.
*
* @param \BeSimple\SoapCommon\Mime\Part $part Part to add content
* @param string $content Content to decode
* Decodes the content of a Mime part
*
* @param Part $part Part to add content
* @param string $partStringContent Content to decode
* @return string $partStringContent decodedContent
*/
private static function decodeContent(Part $part, $content)
private static function decodeContent(Part $part, $partStringContent)
{
$encoding = strtolower($part->getHeader('Content-Transfer-Encoding'));
$charset = strtolower($part->getHeader('Content-Type', 'charset'));
if ($encoding == Part::ENCODING_BASE64) {
$content = base64_decode($content);
} elseif ($encoding == Part::ENCODING_QUOTED_PRINTABLE) {
$content = quoted_printable_decode($content);
if ($encoding === Part::ENCODING_BASE64) {
$partStringContent = base64_decode($partStringContent);
} else if ($encoding === Part::ENCODING_QUOTED_PRINTABLE) {
$partStringContent = quoted_printable_decode($partStringContent);
}
if ($charset != 'utf-8') {
$content = iconv($charset, 'utf-8', $content);
if ($charset !== Part::CHARSET_UTF8) {
return iconv($charset, Part::CHARSET_UTF8, $partStringContent);
}
$part->setContent($content);
return $partStringContent;
}
private static function hasBoundary(array $lines)
@ -203,8 +261,18 @@ class Parser
return false;
}
private static function isBoundary($line)
private static function isBoundary($mimeMessageLine)
{
return strlen($line) > 0 && $line[0] === "-";
return strlen($mimeMessageLine) > 0 && $mimeMessageLine[0] === "-";
}
private static function isMiddleBoundary($mimeMessageLine, $contentTypeBoundary)
{
return strcmp(trim($mimeMessageLine), '--'.$contentTypeBoundary) === 0;
}
private static function isLastBoundary($mimeMessageLine, $contentTypeBoundary)
{
return strcmp(trim($mimeMessageLine), '--'.$contentTypeBoundary.'--') === 0;
}
}

View File

@ -0,0 +1,64 @@
<?php
namespace BeSimple\SoapCommon\Mime\Parser;
class ContentTypeParser
{
/**
* Parse a "Content-Type" header with multiple sub values.
* e.g. Content-Type: multipart/related; boundary=boundary; type=text/xml;
* start="<123@abc>"
*
* Based on: https://labs.omniti.com/alexandria/trunk/OmniTI/Mail/Parser.php
*
* @param string $headerName Header name
* @param string $headerValue Header value
* @return ParsedMimeHeader[]
*/
public static function parseContentTypeHeader($headerName, $headerValue)
{
if (self::isCompositeHeaderValue($headerValue)) {
$parsedMimeHeaders = self::parseCompositeValue($headerName, $headerValue);
} else {
$parsedMimeHeaders = [
new ParsedMimeHeader($headerName, trim($headerValue))
];
}
return $parsedMimeHeaders;
}
private static function parseCompositeValue($headerName, $headerValue)
{
$parsedMimeHeaders = [];
list($value, $remainder) = explode(';', $headerValue, 2);
$value = trim($value);
$parsedMimeHeaders[] = new ParsedMimeHeader($headerName, $value);
$remainder = trim($remainder);
while (strlen($remainder) > 0) {
if (!preg_match('/^([a-zA-Z0-9_-]+)=(.{1})/', $remainder, $matches)) {
break;
}
$name = $matches[1];
$delimiter = $matches[2];
$remainder = substr($remainder, strlen($name) + 1);
// preg_match migrated from https://github.com/progmancod/BeSimpleSoap/commit/6bc8f6a467616c934b0a9792f0efece55054db97
if (!preg_match('/([^;]+)(;\s*|\s*$)/', $remainder, $matches)) {
break;
}
$value = rtrim($matches[1], ';');
if ($delimiter == "'" || $delimiter == '"') {
$value = trim($value, $delimiter);
}
$remainder = substr($remainder, strlen($matches[0]));
$parsedMimeHeaders[] = new ParsedMimeHeader($headerName, $name, $value);
}
return $parsedMimeHeaders;
}
private static function isCompositeHeaderValue($headerValue)
{
return strpos($headerValue, ';');
}
}

View File

@ -0,0 +1,37 @@
<?php
namespace BeSimple\SoapCommon\Mime\Parser;
class ParsedMimeHeader
{
private $name;
private $value;
private $subValue;
/**
* @param string $name
* @param string $value
* @param string|null $subValue
*/
public function __construct($name, $value, $subValue = null)
{
$this->name = $name;
$this->value = $value;
$this->subValue = $subValue;
}
public function getName()
{
return $this->name;
}
public function getValue()
{
return $this->value;
}
public function getSubValue()
{
return $this->subValue;
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace BeSimple\SoapCommon\Mime\Parser;
use BeSimple\SoapCommon\Mime\Part;
class ParsedPart
{
const PART_IS_MAIN = true;
const PART_IS_NOT_MAIN = false;
private $part;
private $isMain;
/**
* @param Part $part
* @param bool $isMain
*/
public function __construct(Part $part, $isMain)
{
$this->part = $part;
$this->isMain = $isMain;
}
public function getPart()
{
return $this->part;
}
public function isMain()
{
return $this->isMain;
}
}

View File

@ -0,0 +1,60 @@
<?php
namespace BeSimple\SoapCommon\Mime\Parser;
use Exception;
class ParsedPartList
{
private $parts;
/**
* @param ParsedPart[] $parts
*/
public function __construct(array $parts)
{
$this->parts = $parts;
}
public function getMainPartCount()
{
$mainPartsCount = 0;
foreach ($this->getParts() as $parsedPart) {
if ($parsedPart->isMain() === true) {
$mainPartsCount++;
}
}
return $mainPartsCount;
}
public function hasExactlyOneMainPart()
{
return $this->getMainPartCount() === 1;
}
public function getPartContentIds()
{
$partContentIds = [];
foreach ($this->getParts() as $parsedPart) {
$partContentIds[] = $parsedPart->getPart()->getContentId();
}
return $partContentIds;
}
public function getParts()
{
return $this->parts;
}
public function getPartCount()
{
return count($this->parts);
}
public function hasParts()
{
return $this->getPartCount() > 0;
}
}

View File

@ -28,57 +28,38 @@ use BeSimple\SoapCommon\Helper;
*/
class Part extends PartHeader
{
/**
* Encoding type base 64
*/
const ENCODING_BASE64 = 'base64';
/**
* Encoding type binary
*/
const ENCODING_BINARY = 'binary';
/**
* Encoding type eight bit
*/
const ENCODING_EIGHT_BIT = '8bit';
/**
* Encoding type seven bit
*/
const ENCODING_SEVEN_BIT = '7bit';
/**
* Encoding type quoted printable
*/
const ENCODING_QUOTED_PRINTABLE = 'quoted-printable';
/**
* Content.
*
* @var mixed
*/
const CHARSET_UTF8 = 'utf-8';
const CONTENT_TYPE_STREAM = 'application/octet-stream';
const CONTENT_TYPE_PDF = 'application/pdf';
/** @var mixed */
protected $content;
/**
* Construct new mime object.
*
* @param mixed $content Content
* @param string $contentType Content type
* @param string $charset Charset
* @param string $encoding Encoding
* @param string $contentId Content id
*
*/
public function __construct($content = null, $contentType = 'application/octet-stream', $charset = null, $encoding = self::ENCODING_BINARY, $contentId = null)
{
public function __construct(
$content = null,
$contentType = self::CONTENT_TYPE_STREAM,
$charset = self::CHARSET_UTF8,
$encoding = self::ENCODING_BINARY,
$contentId = null
) {
$this->content = $content;
$this->setHeader('Content-Type', $contentType);
if (!is_null($charset)) {
$this->setHeader('Content-Type', 'charset', $charset);
} else {
$this->setHeader('Content-Type', 'charset', 'utf-8');
}
$this->setHeader('Content-Type', 'charset', $charset);
$this->setHeader('Content-Transfer-Encoding', $encoding);
if (is_null($contentId)) {
$contentId = $this->generateContentId();
@ -106,6 +87,16 @@ class Part extends PartHeader
return $this->content;
}
public function hasContentId($contentTypeContentId)
{
return $contentTypeContentId === $this->getContentId();
}
public function getContentId()
{
return $this->getHeader('Content-ID');
}
/**
* Set mime content.
*
@ -137,10 +128,10 @@ class Part extends PartHeader
{
$encoding = strtolower($this->getHeader('Content-Transfer-Encoding'));
$charset = strtolower($this->getHeader('Content-Type', 'charset'));
if ($charset != 'utf-8') {
$content = iconv('utf-8', $charset . '//TRANSLIT', $this->content);
if ($charset !== self::CHARSET_UTF8) {
$content = iconv(self::CHARSET_UTF8, $charset.'//TRANSLIT', $this->getContent());
} else {
$content = $this->content;
$content = $this->getContent();
}
switch ($encoding) {
case self::ENCODING_BASE64:

View File

@ -0,0 +1,33 @@
<?php
namespace BeSimple\SoapCommon\Mime;
use BeSimple\SoapBundle\Soap\SoapAttachment;
class PartFactory
{
public static function createFromSoapAttachment(SoapAttachment $attachment)
{
return new Part(
$attachment->getContent(),
Part::CONTENT_TYPE_PDF,
Part::CHARSET_UTF8,
Part::ENCODING_BINARY,
$attachment->getId()
);
}
/**
* @param SoapAttachment[] $attachments SOAP attachments
* @return Part[]
*/
public static function createAttachmentParts(array $attachments = [])
{
$parts = [];
foreach ($attachments as $attachment) {
$parts[] = self::createFromSoapAttachment($attachment);
}
return $parts;
}
}