Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit acfa9ab

Browse filesBrowse files
committed
feature #16164 [Serializer] Add a data: URI normalizer (dunglas)
This PR was squashed before being merged into the 3.1-dev branch (closes #16164). Discussion ---------- [Serializer] Add a data: URI normalizer | Q | A | ------------- | --- | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | n/a | License | MIT | Doc PR | not yet A new normalizer to transform `\SplFileInfo`, `\SplFileObject` and `\Symfony\Component\HttpFoundation\File\File` to [`data:` URI](https://en.wikipedia.org/wiki/Data_URI_scheme) and the opposite. It's convenient when dealing with the JavaScript `FileReader` API. Usage: ```php $normalizer = new DataUriNormalizer(); echo $normalizer->normalize(new File(__DIR__.'/test.gif')); // data:image/gif;base64,R0lGODdhAQABAIAAAP///////ywAAAAAAQABAAACAkQBADs= echo $normalizer->normalize(new File(__DIR__.'/test.txt')); // data:text/plain,K%C3%A9vin%20Dunglas%0A var_dump($normalizer->denormalize('data:image/gif;base64,R0lGODdhAQABAIAAAP///////ywAAAAAAQABAAACAkQBADs=', 'SplFileObject'); // a SplFileObject (also supports HttpFoundation file if the composant is installed) ``` cc @clementtalleu Commits ------- cc7b5af [Serializer] Add a data: URI normalizer
2 parents cda6cba + cc7b5af commit acfa9ab
Copy full SHA for acfa9ab

File tree

6 files changed

+338
-1
lines changed
Filter options

6 files changed

+338
-1
lines changed
+157Lines 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+
{
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
+1Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Serializer/Tests/Normalizer/CustomNormalizerTest.php
+1Lines 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()
+176Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Serializer/composer.json
+3-1Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"symfony/yaml": "~2.8|~3.0",
2323
"symfony/config": "~2.8|~3.0",
2424
"symfony/property-access": "~2.8|~3.0",
25+
"symfony/http-foundation": "~2.8|~3.0",
2526
"doctrine/annotations": "~1.0",
2627
"doctrine/cache": "~1.0"
2728
},
@@ -30,7 +31,8 @@
3031
"doctrine/cache": "For using the default cached annotation reader and metadata cache.",
3132
"symfony/yaml": "For using the default YAML mapping loader.",
3233
"symfony/config": "For using the XML mapping loader.",
33-
"symfony/property-access": "For using the ObjectNormalizer."
34+
"symfony/property-access": "For using the ObjectNormalizer.",
35+
"symfony/http-foundation": "To use the DataUriNormalizer."
3436
},
3537
"autoload": {
3638
"psr-4": { "Symfony\\Component\\Serializer\\": "" },

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.