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 }
|