Skip to content
This repository has been archived by the owner on Jan 30, 2020. It is now read-only.

[ContentType] Fix parameters with encoded values #26

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 22 additions & 6 deletions src/Header/ContentType.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,22 @@
namespace Zend\Mail\Header;

use Zend\Mail\Headers;
use Zend\Mime\Mime;

class ContentType implements HeaderInterface
class ContentType implements UnstructuredInterface
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UnstructuredInterface? Unstructured… what, exactly?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a requirement of HeaderWrap for to quote the header.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I missed that the interface already exists. Ugh. Okay, thanks; will change to the 2.5 release.

{
/**
* @var string
*/
protected $type;

/**
* Header encoding
*
* @var string
*/
protected $encoding = 'ASCII';

/**
* @var array
*/
Expand Down Expand Up @@ -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);
}

Expand All @@ -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);
}

/**
Expand Down Expand Up @@ -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;
Expand Down
177 changes: 88 additions & 89 deletions test/Header/ContentTypeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/**
Expand All @@ -55,80 +39,53 @@ 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";');
$this->assertEquals($header->getParameters(), ['name' => 'foo.pdf']);
}

/**
* @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);
}

/**
Expand All @@ -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
}
}