[BUGFIX] Keep UTF-8 characters unescaped in JsonView
[Packages/TYPO3.CMS.git] / typo3 / sysext / extbase / Tests / Unit / Mvc / View / JsonViewTest.php
1 <?php
2 namespace TYPO3\CMS\Extbase\Tests\Unit\Mvc\View;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 use TYPO3\CMS\Extbase\Mvc\View\JsonView;
18
19 /**
20 * Testcase for the JSON view
21 */
22 class JsonViewTest extends \TYPO3\TestingFramework\Core\Unit\UnitTestCase
23 {
24 /**
25 * @var \TYPO3\CMS\Extbase\Mvc\View\JsonView
26 */
27 protected $view;
28
29 /**
30 * @var \TYPO3\CMS\Extbase\Mvc\Controller\ControllerContext
31 */
32 protected $controllerContext;
33
34 /**
35 * @var \TYPO3\CMS\Extbase\Mvc\Web\Response
36 */
37 protected $response;
38
39 /**
40 * Sets up this test case
41 */
42 protected function setUp()
43 {
44 $this->view = $this->getMockBuilder(\TYPO3\CMS\Extbase\Mvc\View\JsonView::class)
45 ->setMethods(['loadConfigurationFromYamlFile'])
46 ->getMock();
47 $this->controllerContext = $this->createMock(\TYPO3\CMS\Extbase\Mvc\Controller\ControllerContext::class);
48 $this->response = $this->createMock(\TYPO3\CMS\Extbase\Mvc\Web\Response::class);
49 $this->controllerContext->expects($this->any())->method('getResponse')->will($this->returnValue($this->response));
50 $this->view->setControllerContext($this->controllerContext);
51 }
52
53 /**
54 * data provider for testTransformValue()
55 * @return array
56 */
57 public function jsonViewTestData()
58 {
59 $output = [];
60
61 $object = new \stdClass();
62 $object->value1 = 'foo';
63 $object->value2 = 1;
64 $configuration = [];
65 $expected = ['value1' => 'foo', 'value2' => 1];
66 $output[] = [$object, $configuration, $expected, 'all direct child properties should be serialized'];
67
68 $configuration = ['_only' => ['value1']];
69 $expected = ['value1' => 'foo'];
70 $output[] = [$object, $configuration, $expected, 'if "only" properties are specified, only these should be serialized'];
71
72 $configuration = ['_exclude' => ['value1']];
73 $expected = ['value2' => 1];
74 $output[] = [$object, $configuration, $expected, 'if "exclude" properties are specified, they should not be serialized'];
75
76 $object = new \stdClass();
77 $object->value1 = new \stdClass();
78 $object->value1->subvalue1 = 'Foo';
79 $object->value2 = 1;
80 $configuration = [];
81 $expected = ['value2' => 1];
82 $output[] = [$object, $configuration, $expected, 'by default, sub objects of objects should not be serialized.'];
83
84 $object = new \stdClass();
85 $object->value1 = ['subarray' => 'value'];
86 $object->value2 = 1;
87 $configuration = [];
88 $expected = ['value2' => 1];
89 $output[] = [$object, $configuration, $expected, 'by default, sub arrays of objects should not be serialized.'];
90
91 $object = ['foo' => 'bar', 1 => 'baz', 'deep' => ['test' => 'value']];
92 $configuration = [];
93 $expected = ['foo' => 'bar', 1 => 'baz', 'deep' => ['test' => 'value']];
94 $output[] = [$object, $configuration, $expected, 'associative arrays should be serialized deeply'];
95
96 $object = ['foo', 'bar'];
97 $configuration = [];
98 $expected = ['foo', 'bar'];
99 $output[] = [$object, $configuration, $expected, 'numeric arrays should be serialized'];
100
101 $nestedObject = new \stdClass();
102 $nestedObject->value1 = 'foo';
103 $object = [$nestedObject];
104 $configuration = [];
105 $expected = [['value1' => 'foo']];
106 $output[] = [$object, $configuration, $expected, 'array of objects should be serialized'];
107
108 $properties = ['foo' => 'bar', 'prohibited' => 'xxx'];
109 $nestedObject = $this->getMockBuilder($this->getUniqueId('Test'))
110 ->setMethods(['getName', 'getPath', 'getProperties', 'getOther'])
111 ->getMock();
112 $nestedObject->expects($this->any())->method('getName')->will($this->returnValue('name'));
113 $nestedObject->expects($this->any())->method('getPath')->will($this->returnValue('path'));
114 $nestedObject->expects($this->any())->method('getProperties')->will($this->returnValue($properties));
115 $nestedObject->expects($this->never())->method('getOther');
116 $object = $nestedObject;
117 $configuration = [
118 '_only' => ['name', 'path', 'properties'],
119 '_descend' => [
120 'properties' => [
121 '_exclude' => ['prohibited']
122 ]
123 ]
124 ];
125 $expected = [
126 'name' => 'name',
127 'path' => 'path',
128 'properties' => ['foo' => 'bar']
129 ];
130 $output[] = [$object, $configuration, $expected, 'descending into arrays should be possible'];
131
132 $nestedObject = new \stdClass();
133 $nestedObject->value1 = 'foo';
134 $value = new \SplObjectStorage();
135 $value->attach($nestedObject);
136 $configuration = [];
137 $expected = [['value1' => 'foo']];
138 $output[] = [$value, $configuration, $expected, 'SplObjectStorage with objects should be serialized'];
139
140 $dateTimeObject = new \DateTime('2011-02-03T03:15:23', new \DateTimeZone('UTC'));
141 $configuration = [];
142 $expected = '2011-02-03T03:15:23+00:00';
143 $output[] = [$dateTimeObject, $configuration, $expected, 'DateTime object in UTC time zone could not be serialized.'];
144
145 $dateTimeObject = new \DateTime('2013-08-15T15:25:30', new \DateTimeZone('America/Los_Angeles'));
146 $configuration = [];
147 $expected = '2013-08-15T15:25:30-07:00';
148 $output[] = [$dateTimeObject, $configuration, $expected, 'DateTime object in America/Los_Angeles time zone could not be serialized.'];
149 return $output;
150 }
151
152 /**
153 * @test
154 * @dataProvider jsonViewTestData
155 */
156 public function testTransformValue($object, $configuration, $expected, $description)
157 {
158 $jsonView = $this->getAccessibleMock(\TYPO3\CMS\Extbase\Mvc\View\JsonView::class, ['dummy'], [], '', false);
159
160 $actual = $jsonView->_call('transformValue', $object, $configuration);
161
162 $this->assertEquals($expected, $actual, $description);
163 }
164
165 /**
166 * data provider for testTransformValueWithObjectIdentifierExposure()
167 * @return array
168 */
169 public function objectIdentifierExposureTestData()
170 {
171 $output = [];
172
173 $dummyIdentifier = 'e4f40dfc-8c6e-4414-a5b1-6fd3c5cf7a53';
174
175 $object = new \stdClass();
176 $object->value1 = new \stdClass();
177 $configuration = [
178 '_descend' => [
179 'value1' => [
180 '_exposeObjectIdentifier' => true
181 ]
182 ]
183 ];
184
185 $expected = ['value1' => ['__identity' => $dummyIdentifier]];
186 $output[] = [$object, $configuration, $expected, $dummyIdentifier, 'boolean TRUE should result in __identity key'];
187
188 $configuration['_descend']['value1']['_exposedObjectIdentifierKey'] = 'guid';
189 $expected = ['value1' => ['guid' => $dummyIdentifier]];
190 $output[] = [$object, $configuration, $expected, $dummyIdentifier, 'string value should result in string-equal key'];
191
192 return $output;
193 }
194
195 /**
196 * @test
197 * @dataProvider objectIdentifierExposureTestData
198 */
199 public function testTransformValueWithObjectIdentifierExposure($object, $configuration, $expected, $dummyIdentifier, $description)
200 {
201 $persistenceManagerMock = $this->getMockBuilder(\TYPO3\CMS\Extbase\Persistence\Generic\PersistenceManager::class)
202 ->setMethods(['getIdentifierByObject'])
203 ->getMock();
204 $jsonView = $this->getAccessibleMock(\TYPO3\CMS\Extbase\Mvc\View\JsonView::class, ['dummy'], [], '', false);
205 $jsonView->_set('persistenceManager', $persistenceManagerMock);
206
207 $persistenceManagerMock->expects($this->once())->method('getIdentifierByObject')->with($object->value1)->will($this->returnValue($dummyIdentifier));
208
209 $actual = $jsonView->_call('transformValue', $object, $configuration);
210
211 $this->assertEquals($expected, $actual, $description);
212 }
213
214 /**
215 * A data provider
216 */
217 public function exposeClassNameSettingsAndResults()
218 {
219 $className = $this->getUniqueId('DummyClass');
220 $namespace = 'TYPO3\CMS\Extbase\Tests\Unit\Mvc\View\\' . $className;
221 return [
222 [
223 JsonView::EXPOSE_CLASSNAME_FULLY_QUALIFIED,
224 $className,
225 $namespace,
226 ['value1' => ['__class' => $namespace . '\\' . $className]]
227 ],
228 [
229 JsonView::EXPOSE_CLASSNAME_UNQUALIFIED,
230 $className,
231 $namespace,
232 ['value1' => ['__class' => $className]]
233 ],
234 [
235 null,
236 $className,
237 $namespace,
238 ['value1' => []]
239 ]
240 ];
241 }
242
243 /**
244 * @test
245 * @dataProvider exposeClassNameSettingsAndResults
246 */
247 public function viewExposesClassNameFullyIfConfiguredSo($exposeClassNameSetting, $className, $namespace, $expected)
248 {
249 $fullyQualifiedClassName = $namespace . '\\' . $className;
250 if (class_exists($fullyQualifiedClassName) === false) {
251 eval('namespace ' . $namespace . '; class ' . $className . ' {}');
252 }
253
254 $object = new \stdClass();
255 $object->value1 = new $fullyQualifiedClassName();
256 $configuration = [
257 '_descend' => [
258 'value1' => [
259 '_exposeClassName' => $exposeClassNameSetting
260 ]
261 ]
262 ];
263 $reflectionService = $this->getMockBuilder(\TYPO3\CMS\Extbase\Reflection\ReflectionService::class)
264 ->setMethods([ 'getClassNameByObject' ])
265 ->getMock();
266 $reflectionService->expects($this->any())->method('getClassNameByObject')->will($this->returnCallback(function ($object) {
267 return get_class($object);
268 }));
269
270 $jsonView = $this->getAccessibleMock(\TYPO3\CMS\Extbase\Mvc\View\JsonView::class, ['dummy'], [], '', false);
271 $this->inject($jsonView, 'reflectionService', $reflectionService);
272 $actual = $jsonView->_call('transformValue', $object, $configuration);
273 $this->assertEquals($expected, $actual);
274 }
275
276 /**
277 * @test
278 */
279 public function renderSetsContentTypeHeader()
280 {
281 $this->response->expects($this->once())->method('setHeader')->with('Content-Type', 'application/json');
282
283 $this->view->render();
284 }
285
286 /**
287 * @test
288 */
289 public function renderReturnsJsonRepresentationOfAssignedObject()
290 {
291 $object = new \stdClass();
292 $object->foo = 'Foo';
293 $this->view->assign('value', $object);
294
295 $expectedResult = '{"foo":"Foo"}';
296 $actualResult = $this->view->render();
297 $this->assertEquals($expectedResult, $actualResult);
298 }
299
300 /**
301 * @test
302 */
303 public function renderReturnsJsonRepresentationOfAssignedArray()
304 {
305 $array = ['foo' => 'Foo', 'bar' => 'Bar'];
306 $this->view->assign('value', $array);
307
308 $expectedResult = '{"foo":"Foo","bar":"Bar"}';
309 $actualResult = $this->view->render();
310 $this->assertEquals($expectedResult, $actualResult);
311 }
312
313 /**
314 * @test
315 */
316 public function renderReturnsJsonRepresentationOfAssignedSimpleValue()
317 {
318 $value = 'Foo';
319 $this->view->assign('value', $value);
320
321 $expectedResult = '"Foo"';
322 $actualResult = $this->view->render();
323 $this->assertEquals($expectedResult, $actualResult);
324 }
325
326 /**
327 * @test
328 */
329 public function renderKeepsUtf8CharactersUnescaped(): void
330 {
331 $value = 'G├╝rkchen';
332 $this->view->assign('value', $value);
333
334 $actualResult = $this->view->render();
335
336 $expectedResult = '"' . $value . '"';
337 $this->assertSame($expectedResult, $actualResult);
338 }
339
340 /**
341 * @return string[][]
342 */
343 public function escapeCharacterDataProvider(): array
344 {
345 return [
346 'backslash' => ['\\'],
347 'double quote' => ['"'],
348 ];
349 }
350
351 /**
352 * @test
353 * @param string $character
354 * @dataProvider escapeCharacterDataProvider
355 */
356 public function renderEscapesEscapeCharacters(string $character): void
357 {
358 $this->view->assign('value', $character);
359
360 $actualResult = $this->view->render();
361
362 $expectedResult = '"\\' . $character . '"';
363 $this->assertSame($expectedResult, $actualResult);
364 }
365
366 /**
367 * @test
368 */
369 public function renderReturnsNullIfNameOfAssignedVariableIsNotEqualToValue()
370 {
371 $value = 'Foo';
372 $this->view->assign('foo', $value);
373
374 $expectedResult = 'null';
375 $actualResult = $this->view->render();
376 $this->assertEquals($expectedResult, $actualResult);
377 }
378
379 /**
380 * @test
381 */
382 public function renderOnlyRendersVariableWithTheNameValue()
383 {
384 $this->view
385 ->assign('value', 'Value')
386 ->assign('someOtherVariable', 'Foo');
387
388 $expectedResult = '"Value"';
389 $actualResult = $this->view->render();
390 $this->assertEquals($expectedResult, $actualResult);
391 }
392
393 /**
394 * @test
395 */
396 public function setVariablesToRenderOverridesValueToRender()
397 {
398 $value = 'Foo';
399 $this->view->assign('foo', $value);
400 $this->view->setVariablesToRender(['foo']);
401
402 $expectedResult = '"Foo"';
403 $actualResult = $this->view->render();
404 $this->assertEquals($expectedResult, $actualResult);
405 }
406
407 /**
408 * @test
409 */
410 public function renderRendersMultipleValuesIfTheyAreSpecifiedAsVariablesToRender()
411 {
412 $this->view
413 ->assign('value', 'Value1')
414 ->assign('secondValue', 'Value2')
415 ->assign('someOtherVariable', 'Value3');
416 $this->view->setVariablesToRender(['value', 'secondValue']);
417
418 $expectedResult = '{"value":"Value1","secondValue":"Value2"}';
419 $actualResult = $this->view->render();
420 $this->assertEquals($expectedResult, $actualResult);
421 }
422
423 /**
424 * @test
425 */
426 public function renderCanRenderMultipleComplexObjects()
427 {
428 $array = ['foo' => ['bar' => 'Baz']];
429 $object = new \stdClass();
430 $object->foo = 'Foo';
431
432 $this->view
433 ->assign('array', $array)
434 ->assign('object', $object)
435 ->assign('someOtherVariable', 'Value3');
436 $this->view->setVariablesToRender(['array', 'object']);
437
438 $expectedResult = '{"array":{"foo":{"bar":"Baz"}},"object":{"foo":"Foo"}}';
439 $actualResult = $this->view->render();
440 $this->assertEquals($expectedResult, $actualResult);
441 }
442
443 /**
444 * @test
445 */
446 public function renderCanRenderPlainArray()
447 {
448 $array = [['name' => 'Foo', 'secret' => true], ['name' => 'Bar', 'secret' => true]];
449
450 $this->view->assign('value', $array);
451 $this->view->setConfiguration([
452 'value' => [
453 '_descendAll' => [
454 '_only' => ['name']
455 ]
456 ]
457 ]);
458
459 $expectedResult = '[{"name":"Foo"},{"name":"Bar"}]';
460 $actualResult = $this->view->render();
461 $this->assertEquals($expectedResult, $actualResult);
462 }
463
464 /**
465 * @test
466 */
467 public function descendAllKeepsArrayIndexes()
468 {
469 $array = [['name' => 'Foo', 'secret' => true], ['name' => 'Bar', 'secret' => true]];
470
471 $this->view->assign('value', $array);
472 $this->view->setConfiguration([
473 'value' => [
474 '_descendAll' => [
475 '_descendAll' => []
476 ]
477 ]
478 ]);
479
480 $expectedResult = '[{"name":"Foo","secret":true},{"name":"Bar","secret":true}]';
481 $actualResult = $this->view->render();
482 $this->assertEquals($expectedResult, $actualResult);
483 }
484 }