Mercurial > hg > isophonics-drupal-site
comparison core/modules/rest/tests/src/Functional/FileUploadResourceTestBase.php @ 17:129ea1e6d783
Update, including to Drupal core 8.6.10
author | Chris Cannam |
---|---|
date | Thu, 28 Feb 2019 13:21:36 +0000 |
parents | |
children | af1871eacc83 |
comparison
equal
deleted
inserted
replaced
16:c2387f117808 | 17:129ea1e6d783 |
---|---|
1 <?php | |
2 | |
3 namespace Drupal\Tests\rest\Functional; | |
4 | |
5 use Drupal\Component\Render\PlainTextOutput; | |
6 use Drupal\Component\Utility\NestedArray; | |
7 use Drupal\Core\Field\FieldStorageDefinitionInterface; | |
8 use Drupal\Core\Url; | |
9 use Drupal\entity_test\Entity\EntityTest; | |
10 use Drupal\field\Entity\FieldConfig; | |
11 use Drupal\field\Entity\FieldStorageConfig; | |
12 use Drupal\file\Entity\File; | |
13 use Drupal\rest\RestResourceConfigInterface; | |
14 use Drupal\user\Entity\User; | |
15 use GuzzleHttp\RequestOptions; | |
16 use Psr\Http\Message\ResponseInterface; | |
17 | |
18 /** | |
19 * Tests binary data file upload route. | |
20 */ | |
21 abstract class FileUploadResourceTestBase extends ResourceTestBase { | |
22 | |
23 use BcTimestampNormalizerUnixTestTrait; | |
24 | |
25 /** | |
26 * {@inheritdoc} | |
27 */ | |
28 public static $modules = ['rest_test', 'entity_test', 'file']; | |
29 | |
30 /** | |
31 * {@inheritdoc} | |
32 */ | |
33 protected static $resourceConfigId = 'file.upload'; | |
34 | |
35 /** | |
36 * The POST URI. | |
37 * | |
38 * @var string | |
39 */ | |
40 protected static $postUri = 'file/upload/entity_test/entity_test/field_rest_file_test'; | |
41 | |
42 /** | |
43 * Test file data. | |
44 * | |
45 * @var string | |
46 */ | |
47 protected $testFileData = 'Hares sit on chairs, and mules sit on stools.'; | |
48 | |
49 /** | |
50 * The test field storage config. | |
51 * | |
52 * @var \Drupal\field\Entity\FieldStorageConfig | |
53 */ | |
54 protected $fieldStorage; | |
55 | |
56 /** | |
57 * The field config. | |
58 * | |
59 * @var \Drupal\field\Entity\FieldConfig | |
60 */ | |
61 protected $field; | |
62 | |
63 /** | |
64 * The parent entity. | |
65 * | |
66 * @var \Drupal\Core\Entity\EntityInterface | |
67 */ | |
68 protected $entity; | |
69 | |
70 /** | |
71 * Created file entity. | |
72 * | |
73 * @var \Drupal\file\Entity\File | |
74 */ | |
75 protected $file; | |
76 | |
77 /** | |
78 * An authenticated user. | |
79 * | |
80 * @var \Drupal\user\UserInterface | |
81 */ | |
82 protected $user; | |
83 | |
84 /** | |
85 * The entity storage for the 'file' entity type. | |
86 * | |
87 * @var \Drupal\Core\Entity\EntityStorageInterface | |
88 */ | |
89 protected $fileStorage; | |
90 | |
91 /** | |
92 * {@inheritdoc} | |
93 */ | |
94 public function setUp() { | |
95 parent::setUp(); | |
96 | |
97 $this->fileStorage = $this->container->get('entity_type.manager') | |
98 ->getStorage('file'); | |
99 | |
100 // Add a file field. | |
101 $this->fieldStorage = FieldStorageConfig::create([ | |
102 'entity_type' => 'entity_test', | |
103 'field_name' => 'field_rest_file_test', | |
104 'type' => 'file', | |
105 'settings' => [ | |
106 'uri_scheme' => 'public', | |
107 ], | |
108 ]) | |
109 ->setCardinality(FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED); | |
110 $this->fieldStorage->save(); | |
111 | |
112 $this->field = FieldConfig::create([ | |
113 'entity_type' => 'entity_test', | |
114 'field_name' => 'field_rest_file_test', | |
115 'bundle' => 'entity_test', | |
116 'settings' => [ | |
117 'file_directory' => 'foobar', | |
118 'file_extensions' => 'txt', | |
119 'max_filesize' => '', | |
120 ], | |
121 ]) | |
122 ->setLabel('Test file field') | |
123 ->setTranslatable(FALSE); | |
124 $this->field->save(); | |
125 | |
126 // Create an entity that a file can be attached to. | |
127 $this->entity = EntityTest::create([ | |
128 'name' => 'Llama', | |
129 'type' => 'entity_test', | |
130 ]); | |
131 $this->entity->setOwnerId(isset($this->account) ? $this->account->id() : 0); | |
132 $this->entity->save(); | |
133 | |
134 // Provision entity_test resource. | |
135 $this->resourceConfigStorage->create([ | |
136 'id' => 'entity.entity_test', | |
137 'granularity' => RestResourceConfigInterface::RESOURCE_GRANULARITY, | |
138 'configuration' => [ | |
139 'methods' => ['POST'], | |
140 'formats' => [static::$format], | |
141 'authentication' => [static::$auth], | |
142 ], | |
143 'status' => TRUE, | |
144 ])->save(); | |
145 | |
146 $this->refreshTestStateAfterRestConfigChange(); | |
147 } | |
148 | |
149 /** | |
150 * Tests using the file upload POST route. | |
151 */ | |
152 public function testPostFileUpload() { | |
153 $this->initAuthentication(); | |
154 | |
155 $this->provisionResource([static::$format], static::$auth ? [static::$auth] : [], ['POST']); | |
156 | |
157 $uri = Url::fromUri('base:' . static::$postUri); | |
158 | |
159 // DX: 403 when unauthorized. | |
160 $response = $this->fileRequest($uri, $this->testFileData); | |
161 $this->assertResourceErrorResponse(403, $this->getExpectedUnauthorizedAccessMessage('POST'), $response); | |
162 | |
163 $this->setUpAuthorization('POST'); | |
164 | |
165 // 404 when the field name is invalid. | |
166 $invalid_uri = Url::fromUri('base:file/upload/entity_test/entity_test/field_rest_file_test_invalid'); | |
167 $response = $this->fileRequest($invalid_uri, $this->testFileData); | |
168 $this->assertResourceErrorResponse(404, 'Field "field_rest_file_test_invalid" does not exist', $response); | |
169 | |
170 // This request will have the default 'application/octet-stream' content | |
171 // type header. | |
172 $response = $this->fileRequest($uri, $this->testFileData); | |
173 $this->assertSame(201, $response->getStatusCode()); | |
174 $expected = $this->getExpectedNormalizedEntity(); | |
175 $this->assertResponseData($expected, $response); | |
176 | |
177 // Check the actual file data. | |
178 $this->assertSame($this->testFileData, file_get_contents('public://foobar/example.txt')); | |
179 | |
180 // Test the file again but using 'filename' in the Content-Disposition | |
181 // header with no 'file' prefix. | |
182 $response = $this->fileRequest($uri, $this->testFileData, ['Content-Disposition' => 'filename="example.txt"']); | |
183 $this->assertSame(201, $response->getStatusCode()); | |
184 $expected = $this->getExpectedNormalizedEntity(2, 'example_0.txt'); | |
185 $this->assertResponseData($expected, $response); | |
186 | |
187 // Check the actual file data. | |
188 $this->assertSame($this->testFileData, file_get_contents('public://foobar/example_0.txt')); | |
189 $this->assertTrue($this->fileStorage->loadUnchanged(1)->isTemporary()); | |
190 | |
191 // Verify that we can create an entity that references the uploaded file. | |
192 $entity_test_post_url = Url::fromRoute('rest.entity.entity_test.POST') | |
193 ->setOption('query', ['_format' => static::$format]); | |
194 $request_options = []; | |
195 $request_options[RequestOptions::HEADERS]['Content-Type'] = static::$mimeType; | |
196 $request_options = NestedArray::mergeDeep($request_options, $this->getAuthenticationRequestOptions('POST')); | |
197 | |
198 $request_options[RequestOptions::BODY] = $this->serializer->encode($this->getNormalizedPostEntity(), static::$format); | |
199 $response = $this->request('POST', $entity_test_post_url, $request_options); | |
200 $this->assertResourceResponse(201, FALSE, $response); | |
201 $this->assertTrue($this->fileStorage->loadUnchanged(1)->isPermanent()); | |
202 $this->assertSame([ | |
203 [ | |
204 'target_id' => '1', | |
205 'display' => NULL, | |
206 'description' => "The most fascinating file ever!", | |
207 ], | |
208 ], EntityTest::load(2)->get('field_rest_file_test')->getValue()); | |
209 } | |
210 | |
211 /** | |
212 * Returns the normalized POST entity referencing the uploaded file. | |
213 * | |
214 * @return array | |
215 * | |
216 * @see ::testPostFileUpload() | |
217 * @see \Drupal\Tests\rest\Functional\EntityResource\EntityTest\EntityTestResourceTestBase::getNormalizedPostEntity() | |
218 */ | |
219 protected function getNormalizedPostEntity() { | |
220 return [ | |
221 'type' => [ | |
222 [ | |
223 'value' => 'entity_test', | |
224 ], | |
225 ], | |
226 'name' => [ | |
227 [ | |
228 'value' => 'Dramallama', | |
229 ], | |
230 ], | |
231 'field_rest_file_test' => [ | |
232 [ | |
233 'target_id' => 1, | |
234 'description' => 'The most fascinating file ever!', | |
235 ], | |
236 ], | |
237 ]; | |
238 } | |
239 | |
240 /** | |
241 * Tests using the file upload POST route with invalid headers. | |
242 */ | |
243 public function testPostFileUploadInvalidHeaders() { | |
244 $this->initAuthentication(); | |
245 | |
246 $this->provisionResource([static::$format], static::$auth ? [static::$auth] : [], ['POST']); | |
247 | |
248 $this->setUpAuthorization('POST'); | |
249 | |
250 $uri = Url::fromUri('base:' . static::$postUri); | |
251 | |
252 // The wrong content type header should return a 415 code. | |
253 $response = $this->fileRequest($uri, $this->testFileData, ['Content-Type' => static::$mimeType]); | |
254 $this->assertResourceErrorResponse(415, sprintf('No route found that matches "Content-Type: %s"', static::$mimeType), $response); | |
255 | |
256 // An empty Content-Disposition header should return a 400. | |
257 $response = $this->fileRequest($uri, $this->testFileData, ['Content-Disposition' => '']); | |
258 $this->assertResourceErrorResponse(400, '"Content-Disposition" header is required. A file name in the format "filename=FILENAME" must be provided', $response); | |
259 | |
260 // An empty filename with a context in the Content-Disposition header should | |
261 // return a 400. | |
262 $response = $this->fileRequest($uri, $this->testFileData, ['Content-Disposition' => 'file; filename=""']); | |
263 $this->assertResourceErrorResponse(400, 'No filename found in "Content-Disposition" header. A file name in the format "filename=FILENAME" must be provided', $response); | |
264 | |
265 // An empty filename without a context in the Content-Disposition header | |
266 // should return a 400. | |
267 $response = $this->fileRequest($uri, $this->testFileData, ['Content-Disposition' => 'filename=""']); | |
268 $this->assertResourceErrorResponse(400, 'No filename found in "Content-Disposition" header. A file name in the format "filename=FILENAME" must be provided', $response); | |
269 | |
270 // An invalid key-value pair in the Content-Disposition header should return | |
271 // a 400. | |
272 $response = $this->fileRequest($uri, $this->testFileData, ['Content-Disposition' => 'not_a_filename="example.txt"']); | |
273 $this->assertResourceErrorResponse(400, 'No filename found in "Content-Disposition" header. A file name in the format "filename=FILENAME" must be provided', $response); | |
274 | |
275 // Using filename* extended format is not currently supported. | |
276 $response = $this->fileRequest($uri, $this->testFileData, ['Content-Disposition' => 'filename*="UTF-8 \' \' example.txt"']); | |
277 $this->assertResourceErrorResponse(400, 'The extended "filename*" format is currently not supported in the "Content-Disposition" header', $response); | |
278 } | |
279 | |
280 /** | |
281 * Tests using the file upload POST route with a duplicate file name. | |
282 * | |
283 * A new file should be created with a suffixed name. | |
284 */ | |
285 public function testPostFileUploadDuplicateFile() { | |
286 $this->initAuthentication(); | |
287 | |
288 $this->provisionResource([static::$format], static::$auth ? [static::$auth] : [], ['POST']); | |
289 | |
290 $this->setUpAuthorization('POST'); | |
291 | |
292 $uri = Url::fromUri('base:' . static::$postUri); | |
293 | |
294 // This request will have the default 'application/octet-stream' content | |
295 // type header. | |
296 $response = $this->fileRequest($uri, $this->testFileData); | |
297 | |
298 $this->assertSame(201, $response->getStatusCode()); | |
299 | |
300 // Make the same request again. The file should be saved as a new file | |
301 // entity that has the same file name but a suffixed file URI. | |
302 $response = $this->fileRequest($uri, $this->testFileData); | |
303 $this->assertSame(201, $response->getStatusCode()); | |
304 | |
305 // Loading expected normalized data for file 2, the duplicate file. | |
306 $expected = $this->getExpectedNormalizedEntity(2, 'example_0.txt'); | |
307 $this->assertResponseData($expected, $response); | |
308 | |
309 // Check the actual file data. | |
310 $this->assertSame($this->testFileData, file_get_contents('public://foobar/example_0.txt')); | |
311 } | |
312 | |
313 /** | |
314 * Tests using the file upload route with any path prefixes being stripped. | |
315 * | |
316 * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition#Directives | |
317 */ | |
318 public function testFileUploadStrippedFilePath() { | |
319 $this->initAuthentication(); | |
320 | |
321 $this->provisionResource([static::$format], static::$auth ? [static::$auth] : [], ['POST']); | |
322 | |
323 $this->setUpAuthorization('POST'); | |
324 | |
325 $uri = Url::fromUri('base:' . static::$postUri); | |
326 | |
327 $response = $this->fileRequest($uri, $this->testFileData, ['Content-Disposition' => 'file; filename="directory/example.txt"']); | |
328 $this->assertSame(201, $response->getStatusCode()); | |
329 $expected = $this->getExpectedNormalizedEntity(); | |
330 $this->assertResponseData($expected, $response); | |
331 | |
332 // Check the actual file data. It should have been written to the configured | |
333 // directory, not /foobar/directory/example.txt. | |
334 $this->assertSame($this->testFileData, file_get_contents('public://foobar/example.txt')); | |
335 | |
336 $response = $this->fileRequest($uri, $this->testFileData, ['Content-Disposition' => 'file; filename="../../example_2.txt"']); | |
337 $this->assertSame(201, $response->getStatusCode()); | |
338 $expected = $this->getExpectedNormalizedEntity(2, 'example_2.txt', TRUE); | |
339 $this->assertResponseData($expected, $response); | |
340 | |
341 // Check the actual file data. It should have been written to the configured | |
342 // directory, not /foobar/directory/example.txt. | |
343 $this->assertSame($this->testFileData, file_get_contents('public://foobar/example_2.txt')); | |
344 $this->assertFalse(file_exists('../../example_2.txt')); | |
345 | |
346 // Check a path from the root. Extensions have to be empty to allow a file | |
347 // with no extension to pass validation. | |
348 $this->field->setSetting('file_extensions', '') | |
349 ->save(); | |
350 $this->refreshTestStateAfterRestConfigChange(); | |
351 | |
352 $response = $this->fileRequest($uri, $this->testFileData, ['Content-Disposition' => 'file; filename="/etc/passwd"']); | |
353 $this->assertSame(201, $response->getStatusCode()); | |
354 $expected = $this->getExpectedNormalizedEntity(3, 'passwd', TRUE); | |
355 // This mime will be guessed as there is no extension. | |
356 $expected['filemime'][0]['value'] = 'application/octet-stream'; | |
357 $this->assertResponseData($expected, $response); | |
358 | |
359 // Check the actual file data. It should have been written to the configured | |
360 // directory, not /foobar/directory/example.txt. | |
361 $this->assertSame($this->testFileData, file_get_contents('public://foobar/passwd')); | |
362 } | |
363 | |
364 /** | |
365 * Tests using the file upload route with a unicode file name. | |
366 */ | |
367 public function testFileUploadUnicodeFilename() { | |
368 $this->initAuthentication(); | |
369 | |
370 $this->provisionResource([static::$format], static::$auth ? [static::$auth] : [], ['POST']); | |
371 | |
372 $this->setUpAuthorization('POST'); | |
373 | |
374 $uri = Url::fromUri('base:' . static::$postUri); | |
375 | |
376 $response = $this->fileRequest($uri, $this->testFileData, ['Content-Disposition' => 'file; filename="example-✓.txt"']); | |
377 $this->assertSame(201, $response->getStatusCode()); | |
378 $expected = $this->getExpectedNormalizedEntity(1, 'example-✓.txt', TRUE); | |
379 $this->assertResponseData($expected, $response); | |
380 $this->assertSame($this->testFileData, file_get_contents('public://foobar/example-✓.txt')); | |
381 } | |
382 | |
383 /** | |
384 * Tests using the file upload route with a zero byte file. | |
385 */ | |
386 public function testFileUploadZeroByteFile() { | |
387 $this->initAuthentication(); | |
388 | |
389 $this->provisionResource([static::$format], static::$auth ? [static::$auth] : [], ['POST']); | |
390 | |
391 $this->setUpAuthorization('POST'); | |
392 | |
393 $uri = Url::fromUri('base:' . static::$postUri); | |
394 | |
395 // Test with a zero byte file. | |
396 $response = $this->fileRequest($uri, NULL); | |
397 $this->assertSame(201, $response->getStatusCode()); | |
398 $expected = $this->getExpectedNormalizedEntity(); | |
399 // Modify the default expected data to account for the 0 byte file. | |
400 $expected['filesize'][0]['value'] = 0; | |
401 $this->assertResponseData($expected, $response); | |
402 | |
403 // Check the actual file data. | |
404 $this->assertSame('', file_get_contents('public://foobar/example.txt')); | |
405 } | |
406 | |
407 /** | |
408 * Tests using the file upload route with an invalid file type. | |
409 */ | |
410 public function testFileUploadInvalidFileType() { | |
411 $this->initAuthentication(); | |
412 | |
413 $this->provisionResource([static::$format], static::$auth ? [static::$auth] : [], ['POST']); | |
414 | |
415 $this->setUpAuthorization('POST'); | |
416 | |
417 $uri = Url::fromUri('base:' . static::$postUri); | |
418 | |
419 // Test with a JSON file. | |
420 $response = $this->fileRequest($uri, '{"test":123}', ['Content-Disposition' => 'filename="example.json"']); | |
421 $this->assertResourceErrorResponse(422, PlainTextOutput::renderFromHtml("Unprocessable Entity: file validation failed.\nOnly files with the following extensions are allowed: <em class=\"placeholder\">txt</em>."), $response); | |
422 | |
423 // Make sure that no file was saved. | |
424 $this->assertEmpty(File::load(1)); | |
425 $this->assertFalse(file_exists('public://foobar/example.txt')); | |
426 } | |
427 | |
428 /** | |
429 * Tests using the file upload route with a file size larger than allowed. | |
430 */ | |
431 public function testFileUploadLargerFileSize() { | |
432 // Set a limit of 50 bytes. | |
433 $this->field->setSetting('max_filesize', 50) | |
434 ->save(); | |
435 $this->refreshTestStateAfterRestConfigChange(); | |
436 | |
437 $this->initAuthentication(); | |
438 | |
439 $this->provisionResource([static::$format], static::$auth ? [static::$auth] : [], ['POST']); | |
440 | |
441 $this->setUpAuthorization('POST'); | |
442 | |
443 $uri = Url::fromUri('base:' . static::$postUri); | |
444 | |
445 // Generate a string larger than the 50 byte limit set. | |
446 $response = $this->fileRequest($uri, $this->randomString(100)); | |
447 $this->assertResourceErrorResponse(422, PlainTextOutput::renderFromHtml("Unprocessable Entity: file validation failed.\nThe file is <em class=\"placeholder\">100 bytes</em> exceeding the maximum file size of <em class=\"placeholder\">50 bytes</em>."), $response); | |
448 | |
449 // Make sure that no file was saved. | |
450 $this->assertEmpty(File::load(1)); | |
451 $this->assertFalse(file_exists('public://foobar/example.txt')); | |
452 } | |
453 | |
454 /** | |
455 * Tests using the file upload POST route with malicious extensions. | |
456 */ | |
457 public function testFileUploadMaliciousExtension() { | |
458 $this->initAuthentication(); | |
459 | |
460 $this->provisionResource([static::$format], static::$auth ? [static::$auth] : [], ['POST']); | |
461 // Allow all file uploads but system.file::allow_insecure_uploads is set to | |
462 // FALSE. | |
463 $this->field->setSetting('file_extensions', '')->save(); | |
464 $this->refreshTestStateAfterRestConfigChange(); | |
465 | |
466 $this->setUpAuthorization('POST'); | |
467 | |
468 $uri = Url::fromUri('base:' . static::$postUri); | |
469 | |
470 $php_string = '<?php print "Drupal"; ?>'; | |
471 | |
472 // Test using a masked exploit file. | |
473 $response = $this->fileRequest($uri, $php_string, ['Content-Disposition' => 'filename="example.php"']); | |
474 // The filename is not munged because .txt is added and it is a known | |
475 // extension to apache. | |
476 $expected = $this->getExpectedNormalizedEntity(1, 'example.php.txt', TRUE); | |
477 // Override the expected filesize. | |
478 $expected['filesize'][0]['value'] = strlen($php_string); | |
479 $this->assertResponseData($expected, $response); | |
480 $this->assertTrue(file_exists('public://foobar/example.php.txt')); | |
481 | |
482 // Add php as an allowed format. Allow insecure uploads still being FALSE | |
483 // should still not allow this. So it should still have a .txt extension | |
484 // appended even though it is not in the list of allowed extensions. | |
485 $this->field->setSetting('file_extensions', 'php') | |
486 ->save(); | |
487 $this->refreshTestStateAfterRestConfigChange(); | |
488 | |
489 $response = $this->fileRequest($uri, $php_string, ['Content-Disposition' => 'filename="example_2.php"']); | |
490 $expected = $this->getExpectedNormalizedEntity(2, 'example_2.php.txt', TRUE); | |
491 // Override the expected filesize. | |
492 $expected['filesize'][0]['value'] = strlen($php_string); | |
493 $this->assertResponseData($expected, $response); | |
494 $this->assertTrue(file_exists('public://foobar/example_2.php.txt')); | |
495 $this->assertFalse(file_exists('public://foobar/example_2.php')); | |
496 | |
497 // Allow .doc file uploads and ensure even a mis-configured apache will not | |
498 // fallback to php because the filename will be munged. | |
499 $this->field->setSetting('file_extensions', 'doc')->save(); | |
500 $this->refreshTestStateAfterRestConfigChange(); | |
501 | |
502 // Test using a masked exploit file. | |
503 $response = $this->fileRequest($uri, $php_string, ['Content-Disposition' => 'filename="example_3.php.doc"']); | |
504 // The filename is munged. | |
505 $expected = $this->getExpectedNormalizedEntity(3, 'example_3.php_.doc', TRUE); | |
506 // Override the expected filesize. | |
507 $expected['filesize'][0]['value'] = strlen($php_string); | |
508 // The file mime should be 'application/msword'. | |
509 $expected['filemime'][0]['value'] = 'application/msword'; | |
510 $this->assertResponseData($expected, $response); | |
511 $this->assertTrue(file_exists('public://foobar/example_3.php_.doc')); | |
512 $this->assertFalse(file_exists('public://foobar/example_3.php.doc')); | |
513 | |
514 // Now allow insecure uploads. | |
515 \Drupal::configFactory() | |
516 ->getEditable('system.file') | |
517 ->set('allow_insecure_uploads', TRUE) | |
518 ->save(); | |
519 // Allow all file uploads. This is very insecure. | |
520 $this->field->setSetting('file_extensions', '')->save(); | |
521 $this->refreshTestStateAfterRestConfigChange(); | |
522 | |
523 $response = $this->fileRequest($uri, $php_string, ['Content-Disposition' => 'filename="example_4.php"']); | |
524 $expected = $this->getExpectedNormalizedEntity(4, 'example_4.php', TRUE); | |
525 // Override the expected filesize. | |
526 $expected['filesize'][0]['value'] = strlen($php_string); | |
527 // The file mime should also now be PHP. | |
528 $expected['filemime'][0]['value'] = 'application/x-httpd-php'; | |
529 $this->assertResponseData($expected, $response); | |
530 $this->assertTrue(file_exists('public://foobar/example_4.php')); | |
531 } | |
532 | |
533 /** | |
534 * Tests using the file upload POST route no extension configured. | |
535 */ | |
536 public function testFileUploadNoExtensionSetting() { | |
537 $this->initAuthentication(); | |
538 | |
539 $this->provisionResource([static::$format], static::$auth ? [static::$auth] : [], ['POST']); | |
540 | |
541 $this->setUpAuthorization('POST'); | |
542 | |
543 $uri = Url::fromUri('base:' . static::$postUri); | |
544 | |
545 $this->field->setSetting('file_extensions', '') | |
546 ->save(); | |
547 $this->refreshTestStateAfterRestConfigChange(); | |
548 | |
549 $response = $this->fileRequest($uri, $this->testFileData, ['Content-Disposition' => 'filename="example.txt"']); | |
550 $expected = $this->getExpectedNormalizedEntity(1, 'example.txt', TRUE); | |
551 | |
552 $this->assertResponseData($expected, $response); | |
553 $this->assertTrue(file_exists('public://foobar/example.txt')); | |
554 } | |
555 | |
556 /** | |
557 * {@inheritdoc} | |
558 */ | |
559 protected function assertNormalizationEdgeCases($method, Url $url, array $request_options) { | |
560 // The file upload resource only accepts binary data, so there are no | |
561 // normalization edge cases to test, as there are no normalized entity | |
562 // representations incoming. | |
563 } | |
564 | |
565 /** | |
566 * {@inheritdoc} | |
567 */ | |
568 protected function getExpectedUnauthorizedAccessMessage($method) { | |
569 return "The following permissions are required: 'administer entity_test content' OR 'administer entity_test_with_bundle content' OR 'create entity_test entity_test_with_bundle entities'."; | |
570 } | |
571 | |
572 /** | |
573 * Gets the expected file entity. | |
574 * | |
575 * @param int $fid | |
576 * The file ID to load and create normalized data for. | |
577 * @param string $expected_filename | |
578 * The expected filename for the stored file. | |
579 * @param bool $expected_as_filename | |
580 * Whether the expected filename should be the filename property too. | |
581 * | |
582 * @return array | |
583 * The expected normalized data array. | |
584 */ | |
585 protected function getExpectedNormalizedEntity($fid = 1, $expected_filename = 'example.txt', $expected_as_filename = FALSE) { | |
586 $author = User::load(static::$auth ? $this->account->id() : 0); | |
587 $file = File::load($fid); | |
588 | |
589 $expected_normalization = [ | |
590 'fid' => [ | |
591 [ | |
592 'value' => (int) $file->id(), | |
593 ], | |
594 ], | |
595 'uuid' => [ | |
596 [ | |
597 'value' => $file->uuid(), | |
598 ], | |
599 ], | |
600 'langcode' => [ | |
601 [ | |
602 'value' => 'en', | |
603 ], | |
604 ], | |
605 'uid' => [ | |
606 [ | |
607 'target_id' => (int) $author->id(), | |
608 'target_type' => 'user', | |
609 'target_uuid' => $author->uuid(), | |
610 'url' => base_path() . 'user/' . $author->id(), | |
611 ], | |
612 ], | |
613 'filename' => [ | |
614 [ | |
615 'value' => $expected_as_filename ? $expected_filename : 'example.txt', | |
616 ], | |
617 ], | |
618 'uri' => [ | |
619 [ | |
620 'value' => 'public://foobar/' . $expected_filename, | |
621 'url' => base_path() . $this->siteDirectory . '/files/foobar/' . rawurlencode($expected_filename), | |
622 ], | |
623 ], | |
624 'filemime' => [ | |
625 [ | |
626 'value' => 'text/plain', | |
627 ], | |
628 ], | |
629 'filesize' => [ | |
630 [ | |
631 'value' => strlen($this->testFileData), | |
632 ], | |
633 ], | |
634 'status' => [ | |
635 [ | |
636 'value' => FALSE, | |
637 ], | |
638 ], | |
639 'created' => [ | |
640 $this->formatExpectedTimestampItemValues($file->getCreatedTime()), | |
641 ], | |
642 'changed' => [ | |
643 $this->formatExpectedTimestampItemValues($file->getChangedTime()), | |
644 ], | |
645 ]; | |
646 | |
647 return $expected_normalization; | |
648 } | |
649 | |
650 /** | |
651 * Performs a file upload request. Wraps the Guzzle HTTP client. | |
652 * | |
653 * @see \GuzzleHttp\ClientInterface::request() | |
654 * | |
655 * @param \Drupal\Core\Url $url | |
656 * URL to request. | |
657 * @param string $file_contents | |
658 * The file contents to send as the request body. | |
659 * @param array $headers | |
660 * Additional headers to send with the request. Defaults will be added for | |
661 * Content-Type and Content-Disposition. | |
662 * | |
663 * @return \Psr\Http\Message\ResponseInterface | |
664 */ | |
665 protected function fileRequest(Url $url, $file_contents, array $headers = []) { | |
666 // Set the format for the response. | |
667 $url->setOption('query', ['_format' => static::$format]); | |
668 | |
669 $request_options = []; | |
670 $request_options[RequestOptions::HEADERS] = $headers + [ | |
671 // Set the required (and only accepted) content type for the request. | |
672 'Content-Type' => 'application/octet-stream', | |
673 // Set the required Content-Disposition header for the file name. | |
674 'Content-Disposition' => 'file; filename="example.txt"', | |
675 ]; | |
676 $request_options[RequestOptions::BODY] = $file_contents; | |
677 $request_options = NestedArray::mergeDeep($request_options, $this->getAuthenticationRequestOptions('POST')); | |
678 | |
679 return $this->request('POST', $url, $request_options); | |
680 } | |
681 | |
682 /** | |
683 * {@inheritdoc} | |
684 */ | |
685 protected function setUpAuthorization($method) { | |
686 switch ($method) { | |
687 case 'GET': | |
688 $this->grantPermissionsToTestedRole(['view test entity']); | |
689 break; | |
690 case 'POST': | |
691 $this->grantPermissionsToTestedRole(['create entity_test entity_test_with_bundle entities', 'access content']); | |
692 break; | |
693 } | |
694 } | |
695 | |
696 /** | |
697 * Asserts expected normalized data matches response data. | |
698 * | |
699 * @param array $expected | |
700 * The expected data. | |
701 * @param \Psr\Http\Message\ResponseInterface $response | |
702 * The file upload response. | |
703 */ | |
704 protected function assertResponseData(array $expected, ResponseInterface $response) { | |
705 static::recursiveKSort($expected); | |
706 $actual = $this->serializer->decode((string) $response->getBody(), static::$format); | |
707 static::recursiveKSort($actual); | |
708 | |
709 $this->assertSame($expected, $actual); | |
710 } | |
711 | |
712 /** | |
713 * {@inheritdoc} | |
714 */ | |
715 protected function getExpectedUnauthorizedAccessCacheability() { | |
716 // There is cacheability metadata to check as file uploads only allows POST | |
717 // requests, which will not return cacheable responses. | |
718 } | |
719 | |
720 } |