annotate core/modules/rest/tests/src/Functional/FileUploadResourceTestBase.php @ 19:fa3358dc1485 tip

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