diff --git a/src/Header/ContentType.php b/src/Header/ContentType.php index 45852c05..eb520d5b 100644 --- a/src/Header/ContentType.php +++ b/src/Header/ContentType.php @@ -10,14 +10,22 @@ namespace Zend\Mail\Header; use Zend\Mail\Headers; +use Zend\Mime\Mime; -class ContentType implements HeaderInterface +class ContentType implements UnstructuredInterface { /** * @var string */ protected $type; + /** + * Header encoding + * + * @var string + */ + protected $encoding = 'ASCII'; + /** * @var array */ @@ -66,6 +74,12 @@ public function getFieldValue($format = HeaderInterface::FORMAT_RAW) $values = [$prepared]; foreach ($this->parameters as $attribute => $value) { + if (HeaderInterface::FORMAT_ENCODED === $format && !Mime::isPrintable($value)) { + $this->encoding = 'UTF-8'; + $value = HeaderWrap::wrap($value, $this); + $this->encoding = 'ASCII'; + } + $values[] = sprintf('%s="%s"', $attribute, $value); } @@ -74,18 +88,18 @@ public function getFieldValue($format = HeaderInterface::FORMAT_RAW) public function setEncoding($encoding) { - // This header must be always in US-ASCII + $this->encoding = $encoding; return $this; } public function getEncoding() { - return 'ASCII'; + return $this->encoding; } public function toString() { - return 'Content-Type: ' . $this->getFieldValue(); + return 'Content-Type: ' . $this->getFieldValue(HeaderInterface::FORMAT_ENCODED); } /** @@ -135,8 +149,10 @@ public function addParameter($name, $value) if (! HeaderValue::isValid($name)) { throw new Exception\InvalidArgumentException('Invalid content-type parameter name detected'); } - if (! HeaderValue::isValid($value)) { - throw new Exception\InvalidArgumentException('Invalid content-type parameter value detected'); + if (! HeaderWrap::canBeEncoded($value)) { + throw new Exception\InvalidArgumentException( + 'Parameter value must be composed of printable US-ASCII or UTF-8 characters.' + ); } $this->parameters[$name] = $value; diff --git a/test/Header/ContentTypeTest.php b/test/Header/ContentTypeTest.php index 2b6230a8..24824557 100644 --- a/test/Header/ContentTypeTest.php +++ b/test/Header/ContentTypeTest.php @@ -10,37 +10,21 @@ namespace ZendTest\Mail\Header; use Zend\Mail\Header\ContentType; +use Zend\Mail\Header\Exception\InvalidArgumentException; +use Zend\Mail\Header\HeaderInterface; +use Zend\Mail\Header\UnstructuredInterface; /** * @group Zend_Mail */ class ContentTypeTest extends \PHPUnit_Framework_TestCase { - public function testContentTypeFromStringCreatesValidContentTypeHeader() + public function testImplementsHeaderInterface() { - $contentTypeHeader = ContentType::fromString('Content-Type: xxx/yyy'); - $this->assertInstanceOf('Zend\Mail\Header\HeaderInterface', $contentTypeHeader); - $this->assertInstanceOf('Zend\Mail\Header\ContentType', $contentTypeHeader); - } - - public function testContentTypeGetFieldNameReturnsHeaderName() - { - $contentTypeHeader = new ContentType(); - $this->assertEquals('Content-Type', $contentTypeHeader->getFieldName()); - } + $header = new ContentType(); - public function testContentTypeGetFieldValueReturnsProperValue() - { - $contentTypeHeader = new ContentType(); - $contentTypeHeader->setType('foo/bar'); - $this->assertEquals('foo/bar', $contentTypeHeader->getFieldValue()); - } - - public function testContentTypeToStringReturnsHeaderFormattedString() - { - $contentTypeHeader = new ContentType(); - $contentTypeHeader->setType('foo/bar'); - $this->assertEquals("Content-Type: foo/bar", $contentTypeHeader->toString()); + $this->assertInstanceOf(UnstructuredInterface::class, $header); + $this->assertInstanceOf(HeaderInterface::class, $header); } /** @@ -55,26 +39,6 @@ public function testTrailingSemiColonFromString() $this->assertEquals(['boundary' => 'Apple-Mail=_1B852F10-F9C6-463D-AADD-CD503A5428DD'], $params); } - public function testProvidingParametersIntroducesHeaderFolding() - { - $header = new ContentType(); - $header->setType('application/x-unit-test'); - $header->addParameter('charset', 'us-ascii'); - $string = $header->toString(); - - $this->assertContains("Content-Type: application/x-unit-test;", $string); - $this->assertContains(";\r\n charset=\"us-ascii\"", $string); - } - - public function testExtractsExtraInformationFromContentType() - { - $contentTypeHeader = ContentType::fromString( - 'Content-Type: multipart/alternative; boundary="Apple-Mail=_1B852F10-F9C6-463D-AADD-CD503A5428DD"' - ); - $params = $contentTypeHeader->getParameters(); - $this->assertEquals($params, ['boundary' => 'Apple-Mail=_1B852F10-F9C6-463D-AADD-CD503A5428DD']); - } - public function testExtractsExtraInformationWithoutBeingConfusedByTrailingSemicolon() { $header = ContentType::fromString('Content-Type: application/pdf;name="foo.pdf";'); @@ -82,53 +46,46 @@ public function testExtractsExtraInformationWithoutBeingConfusedByTrailingSemico } /** - * @group #2728 - * - * Tests setting different MIME types + * @dataProvider setTypeProvider */ - public function testSetContentType() + public function testFromString($type, $parameters, $fieldValue, $expectedToString) { - $header = new ContentType(); - - $header->setType('application/vnd.ms-excel'); - $this->assertEquals('Content-Type: application/vnd.ms-excel', $header->toString()); - - $header->setType('application/rss+xml'); - $this->assertEquals('Content-Type: application/rss+xml', $header->toString()); - - $header->setType('video/mp4'); - $this->assertEquals('Content-Type: video/mp4', $header->toString()); - - $header->setType('message/rfc822'); - $this->assertEquals('Content-Type: message/rfc822', $header->toString()); + $header = ContentType::fromString($expectedToString); + + $this->assertInstanceOf(ContentType::class, $header); + $this->assertEquals('Content-Type', $header->getFieldName(), 'getFieldName() value not match'); + $this->assertEquals($type, $header->getType(), 'getType() value not match'); + $this->assertEquals($fieldValue, $header->getFieldValue(), 'getFieldValue() value not match'); + $this->assertEquals($parameters, $header->getParameters(), 'getParameters() value not match'); + $this->assertEquals($expectedToString, $header->toString(), 'toString() value not match'); } /** - * @group ZF2015-04 + * @dataProvider setTypeProvider */ - public function testFromStringRaisesExceptionForInvalidName() + public function testSetType($type, $parameters, $fieldValue, $expectedToString) { - $this->setExpectedException('Zend\Mail\Header\Exception\InvalidArgumentException', 'header name'); - $header = ContentType::fromString('Content-Type' . chr(32) . ': text/html'); - } + $header = new ContentType(); - public function headerLines() - { - return [ - 'newline' => ["Content-Type: text/html;\nlevel=1"], - 'cr-lf' => ["Content-Type: text/html\r\n;level=1",], - 'multiline' => ["Content-Type: text/html;\r\nlevel=1\r\nq=0.1"], - ]; + $header->setType($type); + foreach ($parameters as $name => $value) { + $header->addParameter($name, $value); + } + + $this->assertEquals('Content-Type', $header->getFieldName(), 'getFieldName() value not match'); + $this->assertEquals($type, $header->getType(), 'getType() value not match'); + $this->assertEquals($fieldValue, $header->getFieldValue(), 'getFieldValue() value not match'); + $this->assertEquals($parameters, $header->getParameters(), 'getParameters() value not match'); + $this->assertEquals($expectedToString, $header->toString(), 'toString() value not match'); } /** - * @dataProvider headerLines - * @group ZF2015-04 + * @dataProvider invalidHeaderLinesProvider */ - public function testFromStringRaisesExceptionForNonFoldingMultilineValues($headerLine) + public function testFromStringThrowException($headerLine, $expectedException, $exceptionMessage) { - $this->setExpectedException('Zend\Mail\Header\Exception\InvalidArgumentException', 'header value'); - $header = ContentType::fromString($headerLine); + $this->setExpectedException($expectedException, $exceptionMessage); + ContentType::fromString($headerLine); } /** @@ -142,24 +99,66 @@ public function testFromStringHandlesContinuations() } /** - * @group ZF2015-04 + * @dataProvider invalidParametersProvider */ - public function testAddParameterRaisesInvalidArgumentExceptionForInvalidParameterName() + public function testAddParameterThrowException($paramName, $paramValue, $expectedException, $exceptionMessage) { $header = new ContentType(); $header->setType('text/html'); - $this->setExpectedException('Zend\Mail\Header\Exception\InvalidArgumentException', 'parameter name'); - $header->addParameter("b\r\na\rr\n", "baz"); + + $this->setExpectedException($expectedException, $exceptionMessage); + $header->addParameter($paramName, $paramValue); } - /** - * @group ZF2015-04 - */ - public function testAddParameterRaisesInvalidArgumentExceptionForInvalidParameterValue() + public function setTypeProvider() { - $header = new ContentType(); - $header->setType('text/html'); - $this->setExpectedException('Zend\Mail\Header\Exception\InvalidArgumentException', 'parameter value'); - $header->addParameter('foo', "\nbar\r\nbaz\r"); + $foldingHeaderLine = "Content-Type: foo/baz;\r\n charset=\"us-ascii\""; + $foldingFieldValue = "foo/baz;\r\n charset=\"us-ascii\""; + + $encodedHeaderLine = "Content-Type: foo/baz;\r\n name=\"=?UTF-8?Q?=C3=93?=\""; + $encodedFieldValue = "foo/baz;\r\n name=\"Ó\""; + + // @codingStandardsIgnoreStart + return [ + // Description => [$type, $parameters, $fieldValue, toString()] + // @group #2728 + 'foo/a.b-c' => ['foo/a.b-c', [], 'foo/a.b-c', 'Content-Type: foo/a.b-c'], + 'foo/a+b' => ['foo/a+b' , [], 'foo/a+b' , 'Content-Type: foo/a+b'], + 'foo/baz' => ['foo/baz' , [], 'foo/baz' , 'Content-Type: foo/baz'], + 'parameter use header folding' => ['foo/baz' , ['charset' => 'us-ascii'], $foldingFieldValue, $foldingHeaderLine], + 'encoded characters' => ['foo/baz' , ['name' => 'Ó'], $encodedFieldValue, $encodedHeaderLine], + ]; + // @codingStandardsIgnoreEnd + } + + public function invalidParametersProvider() + { + $invalidArgumentException = InvalidArgumentException::class; + + // @codingStandardsIgnoreStart + return [ + // Description => [param name, param value, expected exception, exception message contain] + + // @group ZF2015-04 + 'invalid name' => ["b\r\na\rr\n", 'baz', $invalidArgumentException, 'parameter name'], + ]; + // @codingStandardsIgnoreEnd + } + + public function invalidHeaderLinesProvider() + { + $invalidArgumentException = InvalidArgumentException::class; + + // @codingStandardsIgnoreStart + return [ + // Description => [header line, expected exception, exception message contain] + + // @group ZF2015-04 + 'invalid name' => ['Content-Type' . chr(32) . ': text/html', $invalidArgumentException, 'header name'], + 'newline' => ["Content-Type: text/html;\nlevel=1", $invalidArgumentException, 'header value'], + 'cr-lf' => ["Content-Type: text/html\r\n;level=1", $invalidArgumentException, 'header value'], + 'multiline' => ["Content-Type: text/html;\r\nlevel=1\r\nq=0.1", $invalidArgumentException, 'header value'], + ]; + // @codingStandardsIgnoreEnd } }