8000 [Serializer] Add a data: URI normalizer · symfony/symfony@cc7b5af · GitHub
[go: up one dir, main page]

Skip to content

Commit cc7b5af

Browse files
dunglasfabpot
authored andcommitted
[Serializer] Add a data: URI normalizer
1 parent 6005fe5 commit cc7b5af

File tree

6 files changed

+338
-1
lines changed

6 files changed

+338
-1
lines changed
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Serializer\Normalizer;
13+
14+
use Symfony\Component\HttpFoundation\File\File;
15+
use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesser;
16+
use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesserInterface;
17+
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
18+
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
19+
20+
/**
21+
* Normalizes an {@see \SplFileInfo} object to a data URI.
22+
* Denormalizes a data URI to a {@see \SplFileObject} object.
23+
*
24+
* @author Kévin Dunglas <dunglas@gmail.com>
25+
*/
26+
class DataUriNormalizer implements NormalizerInterface, DenormalizerInterface
27+
{
< 67ED code>28+
/**
29+
* @var MimeTypeGuesserInterface
30+
*/
31+
private $mimeTypeGuesser;
32+
33+
public function __construct(MimeTypeGuesserInterface $mimeTypeGuesser = null)
34+
{
35+
if (null === $mimeTypeGuesser && class_exists('Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesser')) {
36+
$mimeTypeGuesser = MimeTypeGuesser::getInstance();
37+
}
38+
39+
$this->mimeTypeGuesser = $mimeTypeGuesser;
40+
}
41+
42+
/**
43+
* {@inheritdoc}
44+
*/
45+
public function normalize($object, $format = null, array $context = array())
46+
{
47+
if (!$object instanceof \SplFileInfo) {
48+
throw new InvalidArgumentException('The object must be an instance of "\SplFileInfo".');
49+
}
50+
51+
$mimeType = $this->getMimeType($object);
52+
$splFileObject = $this->extractSplFileObject($object);
53+
54+
$data = '';
55+
56+
$splFileObject->rewind();
57+
while (!$splFileObject->eof()) {
58+
$data .= $splFileObject->fgets();
59+
}
60+
61+
if ('text' === explode('/', $mimeType, 2)[0]) {
62+
return sprintf('data:%s,%s', $mimeType, rawurlencode($data));
63+
}
64+
65+
return sprintf('data:%s;base64,%s', $mimeType, base64_encode($data));
66+
}
67+
68+
/**
69+
* {@inheritdoc}
70+
*/
71+
public function supportsNormalization($data, $format = null)
72+
{
73+
return $data instanceof \SplFileInfo;
74+
}
75+
76+
/**
77+
* {@inheritdoc}
78+
*
79+
* Regex adapted from Brian Grinstead code.
80+
*
81+
* @see https://gist.github.com/bgrins/6194623
82+
*
83+
* @throws InvalidArgumentException
84+
* @throws UnexpectedValueException
85+
*/
86+
public function denormalize($data, $class, $format = null, array $context = array())
87+
{
88+
if (!preg_match('/^data:([a-z0-9]+\/[a-z0-9]+(;[a-z0-9\-]+\=[a-z0-9\-]+)?)?(;base64)?,[a-z0-9\!\$\&\\\'\,\(\)\*\+\,\;\=\-\.\_\~\:\@\/\?\%\s]*\s*$/i', $data)) {
89+
throw new UnexpectedValueException('The provided "data:" URI is not valid.');
90+
}
91+
92+
try {
93+
switch ($class) {
94+
case 'Symfony\Component\HttpFoundation\File\File':
95+
return new File($data, false);
96+
97+
case 'SplFileObject':
98+
case 'SplFileInfo':
99+
return new \SplFileObject($data);
100+
}
101+
} catch (\RuntimeException $exception) {
102+
throw new UnexpectedValueException($exception->getMessage(), $exception->getCode(), $exception);
103+
}
104+
105+
throw new InvalidArgumentException(sprintf('The class parameter "%s" is not supported. It must be one of "SplFileInfo", "SplFileObject" or "Symfony\Component\HttpFoundation\File\File".', $class));
106+
}
107+
108+
/**
109+
* {@inheritdoc}
110+
*/
111+
public function supportsDenormalization($data, $type, $format = null)
112+
{
113+
$supportedTypes = array(
114+
\SplFileInfo::class => true,
115+
\SplFileObject::class => true,
116+
'Symfony\Component\HttpFoundation\File\File' => true,
117+
);
118+
119+
return isset($supportedTypes[$type]);
120+
}
121+
122+
/**
123+
* Gets the mime type of the object. Defaults to application/octet-stream.
124+
*
125+
* @param \SplFileInfo $object
126+
*
127+
* @return string
128+
*/
129+
private function getMimeType(\SplFileInfo $object)
130+
{
131+
if ($object instanceof File) {
132+
return $object->getMimeType();
133+
}
134+
135+
if ($this->mimeTypeGuesser && $mimeType = $this->mimeTypeGuesser->guess($object->getPathname())) {
136+
return $mimeType;
137+
}
138+
139+
return 'application/octet-stream';
140+
}
141+
142+
/**
143+
* Returns the \SplFileObject instance associated with the given \SplFileInfo instance.
144+
*
145+
* @param \SplFileInfo $object
146+
*
147+
* @return \SplFileObject
148+
*/
149+
private function extractSplFileObject(\SplFileInfo $object)
150+
{
151+
if ($object instanceof \SplFileObject) {
152+
return $object;
153+
}
154+
155+
return $object->openFile();
156+
}
157+
}
Loading
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Kévin Dunglas

src/Symfony/Component/Serializer/Tests/Normalizer/CustomNormalizerTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ public function testInterface()
3232
{
3333
$this->assertInstanceOf('Symfony\Component\Serializer\Normalizer\NormalizerInterface', $this->normalizer);
3434
$this->assertInstanceOf('Symfony\Component\Serializer\Normalizer\DenormalizerInterface', $this->normalizer);
35+
$this->assertInstanceOf('Symfony\Component\Serializer\SerializerAwareInterface', $this->normalizer);
3536
}
3637

3738
public function testSerialize()
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Serializer\Tests\Normalizer;
13+
14+
use Symfony\Component\HttpFoundation\File\File;
15+
use Symfony\Component\Serializer\Normalizer\DataUriNormalizer;
16+
17+
/**
18+
* @author Kévin Dunglas <dunglas@gmail.com>
19+
*/
20+
class DataUriNormalizerTest extends \PHPUnit_Framework_TestCase
21+
{
22+
const TEST_GIF_DATA = 'data:image/gif;base64,R0lGODdhAQABAIAAAP///////ywAAAAAAQABAAACAkQBADs=';
23+
const TEST_TXT_DATA = 'data:text/plain,K%C3%A9vin%20Dunglas%0A';
24+
const TEST_TXT_CONTENT = "Kévin Dunglas\n";
25+
26+
/**
27+
* @var DataUriNormalizer
28+
*/
29+
private $normalizer;
30+
31+
public function setUp()
32+
{
33+
$this->normalizer = new DataUriNormalizer();
34+
}
35+
36+
public function testInterface()
37+
{
38+
$this->assertInstanceOf('Symfony\Component\Serializer\Normalizer\NormalizerInterface', $this->normalizer);
39+
$this->assertInstanceOf('Symfony\Component\Serializer\Normalizer\DenormalizerInterface', $this->normalizer);
40+
}
41+
42+
public function testSupportNormalization()
43+
{
44+
$this->assertFalse($this->normalizer->supportsNormalization(new \stdClass()));
45+
$this->assertTrue($this->normalizer->supportsNormalization(new \SplFileObject('data:,Hello%2C%20World!')));
46+
}
47+
48+
public function testNormalizeHttpFoundationFile()
49+
{
50+
$file = new File(__DIR__.'/../Fixtures/test.gif');
51+
52+
$this->assertSame(self::TEST_GIF_DATA, $this->normalizer->normalize($file));
53+
}
54+
55+
public function testNormalizeSplFileInfo()
56+
{
57+
$file = new \SplFileInfo(__DIR__.'/../Fixtures/test.gif');
58+
59+
$this->assertSame(self::TEST_GIF_DATA, $this->normalizer->normalize($file));
60+
}
61+
62+
public function testNormalizeText()
63+
{
64+
$file = new \SplFileObject(__DIR__.'/../Fixtures/test.txt');
65+
66+
$data = $this->normalizer->normalize($file);
67+
68+
$this->assertSame(self::TEST_TXT_DATA, $data);
69+
$this->assertSame(self::TEST_TXT_CONTENT, file_get_contents($data));
70+
}
71+
72+
public function testSupportsDenormalization()
73+
{
74+
$this->assertFalse($this->normalizer->supportsDenormalization('foo', 'Bar'));
75+
$this->assertTrue($this->normalizer->supportsDenormalization(self::TEST_GIF_DATA, 'SplFileInfo'));
76+
$this->assertTrue($this->normalizer->supportsDenormalization(self::TEST_GIF_DATA, 'SplFileObject'));
77+
$this->assertTrue($this->normalizer->supportsDenormalization(self::TEST_TXT_DATA, 'Symfony\Component\HttpFoundation\File\File'));
78+
}
79+
80+
public function testDenormalizeSplFileInfo()
81+
{
82+
$file = $this->normalizer->denormalize(self::TEST_TXT_DATA, 'SplFileInfo');
83+
84+
$this->assertInstanceOf('SplFileInfo', $file);
85+
$this->assertSame(file_get_contents(self::TEST_TXT_DATA), $this->getContent($file));
86+
}
87+
88+
public function testDenormalizeSplFileObject()
89+
{
90+
$file = $this->normalizer->denormalize(self::TEST_TXT_DATA, 'SplFileObject');
91+
92+
$this->assertInstanceOf('SplFileObject', $file);
93+
$this->assertEquals(file_get_contents(self::TEST_TXT_DATA), $this->getContent($file));
94+
}
95+
96+
public function testDenormalizeHttpFoundationFile()
97+
{
98+
$file = $this->normalizer->denormalize(self::TEST_GIF_DATA, 'Symfony\Component\HttpFoundation\File\File');
99+
100+
$this->assertInstanceOf('Symfony\Component\HttpFoundation\File\File', $file);
101+
$this->assertSame(file_get_contents(self::TEST_GIF_DATA), $this->getContent($file->openFile()));
102+
}
103+
104+
/**
105+
* @expectedException \Symfony\Component\Serializer\Exception\UnexpectedValueException
106+
* @expectedExceptionMessage The provided "data:" URI is not valid.
107+
*/
108+
public function testGiveNotAccessToLocalFiles()
109+
{
110+
$this->normalizer->denormalize('/etc/shadow', 'SplFileObject');
111+
}
112+
113+
/**
114+
* @expectedException \Symfony\Component\Serializer\Exception\UnexpectedValueException
115+
* @dataProvider invalidUriProvider
116+
*/
117+
public function testInvalidData($uri)
118+
{
119+
$this->normalizer->denormalize($uri, 'SplFileObject');
120+
}
121+
122+
public function invalidUriProvider()
123+
{
124+
return array(
125+
array('dataxbase64'),
126+
array('data:HelloWorld'),
127+
array('data:text/html;charset=,%3Ch1%3EHello!%3C%2Fh1%3E'),
128+
array('data:text/html;charset,%3Ch1%3EHello!%3C%2Fh1%3E'),
129+
array('data:base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAABlBMVEUAAAD///+l2Z/dAAAAM0lEQVR4nGP4/5/h/1+G/58ZDrAz3D/McH8yw83NDDeNGe4Ug9C9zwz3gVLMDA/A6P9/AFGGFyjOXZtQAAAAAElFTkSuQmCC'),
130+
array(''),
131+
array('http://wikipedia.org'),
132+
array('base64'),
133+
array('iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAABlBMVEUAAAD///+l2Z/dAAAAM0lEQVR4nGP4/5/h/1+G/58ZDrAz3D/McH8yw83NDDeNGe4Ug9C9zwz3gVLMDA/A6P9/AFGGFyjOXZtQAAAAAElFTkSuQmCC'),
134+
array(' data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIBAMAAAA2IaO4AAAAFVBMVEXk5OTn5+ft7e319fX29vb5+fn///++GUmVAAAALUlEQVQIHWNICnYLZnALTgpmMGYIFWYIZTA2ZFAzTTFlSDFVMwVyQhmAwsYMAKDaBy0axX/iAAAAAElFTkSuQmCC'),
135+
array(' data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIBAMAAAA2IaO4AAAAFVBMVEXk5OTn5+ft7e319fX29vb5+fn///++GUmVAAAALUlEQVQIHWNICnYLZnALTgpmMGYIFWYIZTA2ZFAzTTFlSDFVMwVyQhmAwsYMAKDaBy0axX/iAAAAAElFTkSuQmCC'),
136+
);
137+
}
138+
139+
/**
140+
* @dataProvider validUriProvider
141+
*/
142+
public function testValidData($uri)
143+
{
144+
$this->assertInstanceOf('SplFileObject', $this->normalizer->denormalize($uri, 'SplFileObject'));
145+
}
146+
147+
public function validUriProvider()
148+
{
149+
$data = array(
150+
array('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAABlBMVEUAAAD///+l2Z/dAAAAM0lEQVR4nGP4/5/h/1+G/58ZDrAz3D/McH8yw83NDDeNGe4Ug9C9zwz3gVLMDA/A6P9/AFGGFyjOXZtQAAAAAElFTkSuQmCC'),
151+
array('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIBAMAAAA2IaO4AAAAFVBMVEXk5OTn5+ft7e319fX29vb5+fn///++GUmVAAAALUlEQVQIHWNICnYLZnALTgpmMGYIFWYIZTA2ZFAzTTFlSDFVMwVyQhmAwsYMAKDaBy0axX/iAAAAAElFTkSuQmCC'),
152+
array('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIBAMAAAA2IaO4AAAAFVBMVEXk5OTn5+ft7e319fX29vb5+fn///++GUmVAAAALUlEQVQIHWNICnYLZnALTgpmMGYIFWYIZTA2ZFAzTTFlSDFVMwVyQhmAwsYMAKDaBy0axX/iAAAAAElFTkSuQmCC '),
153+
array('data:,Hello%2C%20World!'),
154+
array('data:text/html,%3Ch1%3EHello%2C%20World!%3C%2Fh1%3E'),
155+
array('data:,A%20brief%20note'),
156+
array('data:text/html;charset=US-ASCII,%3Ch1%3EHello!%3C%2Fh1%3E'),
157+
);
158+
159+
if (!defined('HHVM_VERSION')) {
160+
// See https://github.com/facebook/hhvm/issues/6354
161+
$data[] = array('data:text/plain;charset=utf-8;base64,SGVsbG8gV29ybGQh');
162+
}
163+
164+
return $data;
165+
}
166+
167+
private function getContent(\SplFileObject $file)
168+
{
169+
$buffer = '';
170+
while (!$file->eof()) {
171+
$buffer .= $file->fgets();
172+
}
173+
174+
return $buffer;
175+
}
176+
}

src/Symfony/Component/Serializer/composer.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"symfony/yaml": "~2.8|~3.0",
2424
"symfony/config": "~2.8|~3.0",
2525
"symfony/property-access": "~2.8|~3.0",
26+
"symfony/http-foundation": "~2.8|~3.0",
2627
"doctrine/annotations": "~1.0",
2728
"doctrine/cache": "~1.0"
2829
},
@@ -31,7 +32,8 @@
3132
"doctrine/cache": "For using the default cached annotation reader and metadata cache.",
3233
"symfony/yaml": "For using the default YAML mapping loader.",
3334
"symfony/config": "For using the XML mapping loader.",
34-
"symfony/property-access": "For using the ObjectNormalizer."
35+
"symfony/property-access": "For using the ObjectNormalizer.",
36+
"symfony/http-foundation": "To use the DataUriNormalizer."
3537
},
3638
"autoload": {
3739
"psr-4": { "Symfony\\Component\\Serializer\\": "" }

0 commit comments

Comments
 (0)
0