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