Problem/Motivation

Drupal 8.5 supports reading, creating, modifying and deleting any content entity, because #2824572: Write EntityResourceTestBase subclasses for every other entity type. was completed. Hurray!

However, there is one exception: File entities can not be created. Because it is not yet possible to upload the file associated with a File entity.

File entities are a special case because unlike other entity types, they correspond to stored files, which can range in size from a few bytes to many gigabytes.

Proposed resolution

A new @RestResource plugin that only supports the creation of file entities. This endpoint will only accept binary data (application/octet-stream), not any file entity representation, although a file entity will be created from this data. Large amounts of data can then be streamed in a request instead. These file entities will have their status field's value set to false, i.e. they are not in a permanent state. The uploaded file must be validated within the context of a file field, as we are currently restricted to this approach in Drupal — as files are not stand alone first-class entities. So all appropriate file entity validations configured for the file field context being used (such as size and extension) will be applied.

A second request can then be made, to reference the created file entity from a referencing entity. When the referencing entity is updated or created, and the file uploaded in the first request becomes referenced, that will in turn make the file permanent (status=true).

Design decisions explained:

Content-Type: application/octet-stream + php://input
Naïve implementations encode (binary) file data as strings in JSON/XML/YAML/… request bodies. But this means much slower file uploads, and more importantly, the need for a very expensive decoding on the receiving (server) side. In PHP in particular, this would mean the file size for uploaded files would be limited by the PHP memory limit (and actually, significantly below that, due to the encoding into a string).
See @damiankloip in #281.
Only allows file uploads in the context of an entity type/bundle's file field
Validation of uploaded files in Drupal today always is configured at the field level. Uploading arbitrary files is dangerous (think uploading HTML/SVG with embedded JS, or malware executables), therefore validation is necessary, validation must come from somewhere. (@Berdir in #291)
This means that we should perform @FieldType=file/@FieldType=image (the latter subclasses the former) validators (as configured in their field definition) even though we're not saving such a field (@damiankloip in #315). That's the only way we can (pragmatically) achieve file uploads without requiring lots of additional configuration to validate and hence guarantee safety. Explained clearly for the first time by @Berdir in #326. It also determines where the file is stored (stream wrapper + subdirectory, see @Berdir in #334)
The resulting must have status=false, because the uploaded file is not yet being used anywhere (because File entities do not have canonical URLs of their own — this is an existing limitation in HEAD).
(Note this contrasts with @damiankloip original vision. Which was: Tying file uploads via REST to some entity type/bundle + field seems strange from a REST perspective (@damiankloip in #299 + #306)., plus his comment in #320. It was also the opposite of @Wim Leers original vision (see #332). It's the discussion with the community and @Berdir in particular that convinced him to go this way, and so that is the consensus that was reached. Also: yes, this is very confusing due to historical reasons.)
One request to upload the file (using "real" PHP upload semantics, response= created File entity), second request to use the file
First framed conceptually by @garphy in #305 and then explicitly by @ibustos in #321. @dagmar in #304 stated that this is also what Contenful does. As @garphy explained in #305: this is also essentially how Drupal is already does it when handling AJAX file uploads. Confirmed by @damiankloip in #315. The first request creates a File entity with status=false, the second request makes the referencing entity "use" the File entity, and will result in that File getting status=TRUE.
How to name the uploaded file?
Considered: query string, path parameter, request header (@damiankloip in #315). @dabito in #319 was the first to point out the Content-Disposition request header, @Wim Leers confirmed that this was the actual recommended best practice by referencing the Mozilla docs at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Dispos... in #328.
How is the uploaded file actually validated?
It was necessary to copy some logic from FileItem::getUploadValidators() + file_save_upload() (#344)
We're validating the Content-Disposition request header (see @damiankloip's comment at #397 and #447)
Also see below: Are edge cases tested? — lots of things that are sort-of-validation that are explicitly tested!
Are edge cases tested?
Yes!
  • Custom upload directories: #387 (including ones with tokens!)
  • Content-Disposition request header values (see @damiankloip's interdiff at #397)
  • Zero-byte file upload (@damiankloip's interdiff at #409)
  • Disallowed file type file upload (@damiankloip's interdiff at #409)
  • Disallowed file size file upload (@damiankloip's interdiff at #410)
  • File upload with size greater than PHP memory limit: manually tested by @blainelang at #414, impossible to test automatically per @damiankloip's comment in #438
  • File with duplicate name (@damiankloip's interdiff at #432 + @tedbow's review in #494 + @damiankloip's interdiff at #507)
  • File with non-ASCII characters in its name (@damiankloip's interdiff at #444)
  • Malicious uploads (@daminakloip's interdiff at #457)
  • Uploads that use a URL that don't actually reference an existing entity type + bundle + field combination are disallowed (@damiankloip's interdiff at #482)
  • 403 in case you don't have entity/field access to the file field for which you're uploading a file (@damiankloip's interdiff at #482)
Access checking
Per the above, we must check entity create access for the referencing entity type and field edit access for the file/image field, which happens in FileUploadResource::validateAndLoadFieldDefinition(). As originally pointed out in #326 by @Berdir and fixed (including explicit test coverage) by @Wim Leers in #487.4
If you're wondering about what "temporary" means …
… @Berdir clearly explains in #329 that there's two kinds:
  1. as in public:// vs temporary://
  2. as in status=TRUE vs status=FALSE

This patch saves the uploaded file to public:// and sets status=FALSE. See #329.

Removal of \Drupal\hal\Normalizer\FileEntityNormalizer::denormalize()
The current code is A) insecure (see @damiankloip in #507), B) nobody could have been using it successfully anyway because HAL's denormalizers fail for file and image fields (see @Wim Leers in #498).

(If there is one comment you're going to read, it should be @Berdir's #334.)

Remaining tasks

None.

User interface changes

None.

API changes

  1. API addition: file_upload @RestResource (path: /file/upload/{entity_type_id}/{bundle}/{field_name} that binary file data can be POSTed/streamed to.
  2. API addition: if a @RestResource plugin already defines a _format or _content_type_format route requirement, ResourceRoutes won't overwrite it with the configured formats. This is necessary to allow binary file uploads: the request body doesn't use any format, it sends binary data. (This was discovered by @damiankloip in #355 — related fixes landed in #2901704: Allow REST routes to use different request formats and content type formats, but this additional capability is landing as part of this issue.)
  3. API addition: added RequestHandler::handleRaw(), which @RestResource plugins can optionally opt in to, to allow their allowed request Content-Type headers to deviate from the configured formats, for example for binary request bodies.
CommentFileSizeAuthor
#581 1927648-581.patch77.57 KBnicrodgers
#579 1927648-579.patch77.69 KBijsbrandy
#579 interdiff_567-579.txt7.44 KBijsbrandy
#566 1927648-567.patch85.81 KBalexpott
#566 558-567-interrdiff.txt857 bytesalexpott
#558 1927648-558.patch85.87 KBalexpott
#558 555-558-interdiff.txt944 bytesalexpott
#555 1927648-555.patch85.73 KBalexpott
#555 1927648-555.test-only.patch85.7 KBalexpott
#555 553-555-interdiff.txt6.16 KBalexpott
#553 544-553-interdiff.txt4.65 KBalexpott
#553 1927648-553.patch84.39 KBalexpott
#544 1927648-544.patch84.89 KBwim leers
#542 interdiff-1927648-542.txt726 bytesdamiankloip
#542 1927648-542.patch84.76 KBdamiankloip
#541 1927648-541.patch84.93 KBwim leers
#541 interdiff-540-541.txt1.09 KBwim leers
#540 interdiff-1927648-540.txt10.46 KBdamiankloip
#540 1927648-540.patch84.73 KBdamiankloip
#534 1927648-534.patch82.06 KBwim leers
#534 interdiff-532-534.txt6.5 KBwim leers
#532 1927648-532.patch79.31 KBwim leers
#532 interdiff-531-532.txt9.57 KBwim leers
#531 1927648-531.patch71.47 KBwim leers
#531 interdiff-530-531.txt1.6 KBwim leers
#530 1927648-530.patch71.67 KBwim leers
#530 interdiff-529-530.txt4.16 KBwim leers
#529 1927648-529.patch68.24 KBwim leers
#519 1927648-519.patch68.8 KBdamiankloip
#517 interdiff-1927648-717.txt6.99 KBdamiankloip
#517 1927648-517.patch68.79 KBdamiankloip
#514 1927648-511.patch66.75 KBwim leers
#512 1927648-512-combined.patch106.17 KBwim leers
#511 1927648-511-combined.patch105.4 KBwim leers
#511 1927648-511-review-do-not-test.patch66.75 KBwim leers
#511 interdiff-507-511.txt1.9 KBwim leers
#509 interdiff-507-509.txt1.35 KBwim leers
#507 1927648-507-combined.patch105.93 KBdamiankloip
#507 1927648-507-review-do-not-test.patch66.39 KBdamiankloip
#507 interdiff-1927648-507.txt5.62 KBdamiankloip
#505 1927648-504-combined.patch104.96 KBwim leers
#505 1927648-504-review-do-not-test.patch65.47 KBwim leers
#505 interdiff-503-504.txt1.03 KBwim leers
#503 1927648-503-combined.patch104.94 KBwim leers
#503 1927648-503-review-do-not-test.patch65.45 KBwim leers
#503 interdiff-502-503.txt5.6 KBwim leers
#502 1927648-502-combined.patch105.07 KBwim leers
#502 1927648-502-review-do-not-test.patch65.58 KBwim leers
#502 interdiff-501-502.txt2.42 KBwim leers
#501 1927648-501-combined.patch105.14 KBwim leers
#501 1927648-501-do-not-test.patch65.15 KBwim leers
#498 1927648-498.patch68.77 KBwim leers
#498 interdiff-489-498.txt7.32 KBwim leers
#487 1927648-487.patch67.58 KBwim leers
#487 interdiff-486-487.txt6.01 KBwim leers
#486 interdiff-1927648-486.txt12.73 KBdamiankloip
#486 1927648-486.patch67.06 KBdamiankloip
#482 interdiff-1927648-481.txt7.01 KBdamiankloip
#482 1927648-481.patch66.08 KBdamiankloip
#479 interdiff-1927648-479.txt1.56 KBdamiankloip
#479 1927648-479.patch67.72 KBdamiankloip
#476 1927648-476.patch68.75 KBwim leers
#476 interdiff.txt795 byteswim leers
#475 1927648-475.patch68.64 KBwim leers
#475 interdiff.txt730 byteswim leers
#468 1927648-468.patch67.07 KBgarphy
#462 interdiff-1927648-462.txt12.52 KBdamiankloip
#462 1927648-462.patch67.07 KBdamiankloip
#457 interdiff-1927648-457.txt12.13 KBdamiankloip
#457 1927648-457.patch73.57 KBdamiankloip
#448 1927648-448.patch74.32 KBtedbow
#448 interdiff-447-448.txt5.08 KBtedbow
#447 interdiff-1927648-447.txt8.5 KBdamiankloip
#447 1927648-447.patch70.25 KBdamiankloip
#444 interdiff-1927648-444.txt3.57 KBdamiankloip
#444 1927648-444.patch66.37 KBdamiankloip
#441 interdiff-1927648-441.txt8.62 KBdamiankloip
#441 1927648-441.patch65.23 KBdamiankloip
#438 interdiff-1927648-438.txt7.03 KBdamiankloip
#438 1927648-438.patch63.42 KBdamiankloip
#436 interdiff-1927648-436.txt2.74 KBdamiankloip
#436 1927648-436.patch62.63 KBdamiankloip
#434 interdiff-1927648-434.txt867 bytesdamiankloip
#434 1927648-434.patch62.54 KBdamiankloip
#432 interdiff-1927648-432.txt4.28 KBdamiankloip
#432 1927648-432.patch62.53 KBdamiankloip
#428 interdiff-1927648-428.txt720 bytesgarphy
#428 1927648-428.patch60.99 KBgarphy
#410 interdiff-1927648-410.txt6.75 KBdamiankloip
#410 1927648-410.patch60.86 KBdamiankloip
#409 interdiff-1927648-409.txt3.85 KBdamiankloip
#409 1927648-409.patch59.45 KBdamiankloip
#408 interdiff-1927648-405.txt15.66 KBdamiankloip
#408 1927648-405.patch57.52 KBdamiankloip
#400 interdiff-1927648-399.txt7.67 KBdamiankloip
#400 1927648-399.patch48.78 KBdamiankloip
#397 interdiff-1927648-397.txt14.12 KBdamiankloip
#397 1927648-397.patch48.36 KBdamiankloip
#395 interdiff-1927648-395.txt11.79 KBdamiankloip
#395 1927648-395.patch45.26 KBdamiankloip
#394 interdiff-1927648-394.txt4.02 KBdamiankloip
#394 1927648-394.patch38.45 KBdamiankloip
#392 1927648-392.patch35.16 KBwim leers
#392 interdiff.txt611 byteswim leers
#391 1927648-389.patch35.18 KBwim leers
#391 interdiff.txt4.92 KBwim leers
#388 1927648-388.patch37.54 KBwim leers
#388 interdiff.txt1.75 KBwim leers
#387 1927648-387.patch37.54 KBwim leers
#387 interdiff.txt2.14 KBwim leers
#386 1927648-385.patch37.52 KBwim leers
#386 interdiff.txt2.62 KBwim leers
#384 1927648-384.patch35.99 KBwim leers
#384 interdiff.txt3.49 KBwim leers
#382 1927648-381.patch35.53 KBwim leers
#382 interdiff.txt3.73 KBwim leers
#380 interdiff-366-379.txt7.81 KBwim leers
#379 1927648-379.patch29.7 KBpnagornyak
#366 interdiff-1927648-366.txt564 bytesdamiankloip
#366 1927648-366.patch33.88 KBdamiankloip
#364 interdiff-1927648-364.txt3.7 KBdamiankloip
#364 1927648-364.patch33.85 KBdamiankloip
#359 interdiff-1927648-359.txt6.66 KBdamiankloip
#359 1927648-359.patch31.65 KBdamiankloip
#356 interdiff-1927648-356.txt2.69 KBdamiankloip
#356 1927648-356.patch26.85 KBdamiankloip
#355 interdiff-1927648-354.txt12.39 KBdamiankloip
#355 1927648-354.patch26.57 KBdamiankloip
#350 interdiff-1927648-350.txt9.02 KBdamiankloip
#350 1927648-350.patch22.15 KBdamiankloip
#344 interdiff-1927648-343.txt6.81 KBdamiankloip
#344 1927648-343.patch23.02 KBdamiankloip
#331 interdiff-1927648-331.txt1.28 KBdamiankloip
#331 1927648-331.patch19.54 KBdamiankloip
#330 interdiff-1927648-329.txt14.11 KBdamiankloip
#330 1927648-329.patch19.46 KBdamiankloip
#322 interdiff-1927648-322.txt5.72 KBibustos
#322 1927648-322.patch17.72 KBibustos
#310 interdiff-1927648-309.txt1.88 KBibustos
#309 1927648-309.patch15.81 KBibustos
#307 interdiff-1927648-307.txt2.42 KBdamiankloip
#307 1927648-307.patch15.47 KBdamiankloip
#294 1927648-294.patch15.56 KBdamiankloip
#293 interdiff-1927648-293.txt3.75 KBdamiankloip
#293 1927648-293.patch15.42 KBdamiankloip
#290 interdiff-1927648-290.txt2.85 KBdamiankloip
#290 1927648-290.patch12.12 KBdamiankloip
#287 interdiff-1927648-287.txt2.52 KBdamiankloip
#287 1927648-287.patch12.09 KBdamiankloip
#283 interdiff-1927648-283.txt8.8 KBdamiankloip
#283 1927648-283.patch11.29 KBdamiankloip
#281 1927648-281.patch2.97 KBdamiankloip
#276 1927648-275.patch21.35 KBkim.pepper
#265 1927648-265.patch24 KBbenjy
#264 1927648-264.patch23.89 KBbenjy
#251 serialize_file_content-1927648-251.patch31.95 KBgarphy
#243 1927648_atop_8.2.x-42d2ce924c739f8b9072628102db400d0bc4fefb.patch32.59 KBbc
#238 1927648_atop_8.2.x-c9e8e141435fc916c6ead2-normalrepo.patch32.44 KBbc
#235 1927648_atop_8.2.x-c9e8e141435fc916c6ead2.patch32.38 KBbc
#223 1927648_223.patch32.4 KBtedbow
#216 1927648_216.patch33.2 KBtedbow
#214 interdiff-1927648-208-214.txt3.96 KBtedbow
#214 1927648_214.patch33.21 KBtedbow
#207 1927648_207.patch33.67 KBtedbow
#207 interdiff-1927648-204-207.txt3.13 KBtedbow
#204 interdiff-1927648-198-204.txt13.42 KBtedbow
#204 1927648_204.patch33.33 KBtedbow
#200 interdiff-1927648-198-200.txt1.85 KBtedbow
#200 1927648-200.patch33.07 KBtedbow
#198 1927648-198.patch32.14 KBmarthinal
#198 interdiff-1927648-197-198.txt4.32 KBmarthinal
#197 1927648-197.patch32.25 KBmarthinal
#197 interdiff-1927648-195-197.txt4.33 KBmarthinal
#195 1927648-195.patch30.43 KBmarthinal
#195 interdiff-1927648-193-195.txt668 bytesmarthinal
#193 1927648-193.patch30.41 KBmarthinal
#193 interdiff-1927648-192-193.txt2.05 KBmarthinal
#192 1927648-192.patch29.49 KBmarthinal
#192 interdiff-1927648-191-192.txt2.49 KBmarthinal
#191 1927648-191.patch27.5 KBmarthinal
#191 interdiff_1927648-189-191.txt8.78 KBmarthinal
#189 1927648-189.patch27.54 KBmarthinal
#189 interdiff-1927648-186-189.txt672 bytesmarthinal
#186 1927648-186.patch27.54 KBmarthinal
#183 1927648-183.patch26.84 KBmarthinal
#183 interdiff-1927648-171-183.txt562 bytesmarthinal
#171 serialize_file_content-1927648-165.patch26.3 KBgcardinal
#168 working-in-dhc.png60.55 KBgcardinal
#164 serialize_file_content-1927648-164.patch26.3 KBeiriksm
#164 interdiff-162-164-1927648.txt872 byteseiriksm
#162 interdiff-160-162.txt8.64 KBeiriksm
#162 serialize_file_content-1927648-162.patch26.19 KBeiriksm
#160 serialize_file_content-1927648-160.patch26.09 KBneclimdul
#158 interdiff.txt7.61 KBdawehner
#158 1927648-158.patch28.92 KBdawehner
#153 1927648-153.patch29 KBdrnikki
#147 1927648-147-reroll.patch29.05 KBkylebrowning
#146 upload_file_rest.png137.43 KBmarthinal
#140 1927648-137-reroll.patch29.06 KBvivekvpandya
#137 interdiff-1927648-133-137.txt2.44 KBmarthinal
#137 1927648-137.patch29.04 KBmarthinal
#133 interdiff-1927648-130-133.txt6.94 KBmarthinal
#133 1927648-133.patch30.37 KBmarthinal
#130 interdiff-1927648-128-130.txt7.92 KBmarthinal
#130 1927648-130.patch26.83 KBmarthinal
#128 interdiff-122-128.txt2.98 KBmarthinal
#128 1927648-128.patch25.97 KBmarthinal
#122 serialize_file_content-1927648-122.patch24.77 KBgaurav.goyal
#113 interdiff-1927648-108-113.txt4.1 KBmarthinal
#113 1927648-113.patch24.92 KBmarthinal
#108 interdiff-106-108.txt5.8 KBmarthinal
#108 1927648-108.patch25.6 KBmarthinal
#106 interdiff-1927648-99-106.txt1.17 KBmarthinal
#106 1927648-106.patch23.2 KBmarthinal
#99 1927648-99.patch22.03 KBmarthinal
#97 rest.png81.14 KBmarthinal
#97 1927648-97.patch22.59 KBmarthinal
#94 rest-permissions.png39.07 KBmarthinal
#90 base64_file_field-1927648-90.patch22.74 KBqueenvictoria
#85 base64_file_field-1927648-85.patch22.72 KBsam152
#66 file-base64-1927648.66.patch22.68 KBlarowlan
#62 file-base64-1927648.62.patch22.68 KBlarowlan
#62 interdiff.txt1.29 KBlarowlan
#56 base64_file_field-1927648-56.patch23.56 KBarla
#49 base64_file_field-1927648-49.interdiff.txt5.11 KBarla
#49 base64_file_field-1927648-49.patch23.6 KBarla
#43 base64_file_field-1927648-43.interdiff.txt2.49 KBarla
#43 base64_file_field-1927648-43.patch21.68 KBarla
#41 base64_file_field-1927648-41.interdiff.txt8.44 KBarla
#41 base64_file_field-1927648-41.patch19.19 KBarla
#35 base64_file_field-1927648-35.patch15.79 KBarla
#29 interdiff.txt4.27 KBjuampynr
#29 drupal-image_field_rest_support-1927648-28.patch2.5 KBjuampynr
#23 drupal-image_field_rest_support-1927648-22.patch2.47 KBjuampynr
#23 interdiff.txt1.78 KBjuampynr
#16 drupal-image_field_rest_support-1927648-16.patch1.88 KBjuampynr

Comments

Anonymous’s picture

We had discussed creating this as a temp file stream and then passing that off to the field. When you create a temp file, is there any way it is accessible via HTTP? If so, I believe it could open up a vulnerability where a malicious script could be base64 encoded and then run. I don't know enough about the file system to be sure.

moshe weitzman’s picture

It would be poor practice but I do think some sites put their temp dir within the docroot. So, your worry is well founded. I debugged our current file upload validation and the key function we have to mimic is file_save_upload. We have to mimic it because it is hard coded to work with uploads (i.e. the $_FILES superglobal).

The key bits from that function are

$file = entity_create($values);
$validators = file_field_widget_upload_validators($field, $instance);
$errors = file_validate($file, $validators);

Unfortunately, this function seems to do a bit of validation of its own (file_validate_extensions, file_munge_filename(), file_validate_name_length) that are not contained within file_validate() call. As a first pass, I would just ignore this extra validation and rely on the file_validate() call.

Also, you will want a stream wrapper which is inline data, so you don't have to save the file before calling file_validate. See the data:// wrapper at http://php.net/manual/en/wrappers.data.php

moshe weitzman’s picture

Issue tags: +WSCCI

Add tag

Anonymous’s picture

We discussed this on the REST call today.

I'm pretty sure that image fields are basically a special kind of entity reference, which reference a file entity. Thus, the URI should be a property of the file entity, which means that we can handle the relationship between the file entity and the referencing entity could be handled in the same way.

Anonymous’s picture

We discussed this with quicksketch on one of the calls. He agreed that the API interaction should be to create the file entity and then use the entity id from that to make another call to create the node that points to it.

To do this, we need Entity API support for files, #1818568: Convert files to the new Entity Field API.

Anonymous’s picture

Because supporting this in serialization is a separate issue, independent of REST, I created #1988426: Support deserialization of file entities in HAL.

For now, we should be able to put in a stub Normalizer that handles the current File class (which is not NG). This approach can be re-evaluated once File in NG.

moshe weitzman’s picture

Issue summary: View changes

Still a gaping hole in our REST support. Come on Internets ...

rcaracaus’s picture

I managed to get this working on GET, sort of.. but it was a hack. What are the next steps to properly get this into core now that the blockers above are resolved?

rteijeiro’s picture

Hi @rcaracaus,

How did you manage to get this working? I'm getting this output:

field_image: [
  {
    target_id: "1",
    display: null,
    description: null,
    alt: "",
    title: "",
    width: "1134",
    height: "1134"
  }
]

I'm not sure if I have to make a new request for image entity or something else in order to get the image url, for example.

webchick’s picture

Priority: Normal » Major

Seems like this should be elevated, given the impact.

rteijeiro’s picture

I'm pretty interested in make this work. If someone can point me how to start working on this (related issues or something else) I will be pleased to contribute :)

clemens.tolboom’s picture

@rteijeiro we do have our admin/help/rest which goes to https://www.drupal.org/documentation/modules/rest and in particular https://www.drupal.org/node/2098511 (POST content)

As pointed out in #0 and #2 checkout devel_generate in particular DevelGenerateFieldImage.

I use UIs on Chrome: Rest console and DHC - REST HTTP API Client but prefer command-line scripts

[Stock response from Dreditor templates and macros.]

Please update the issue summary by applying the template from http://drupal.org/node/1155816.

rteijeiro’s picture

Issue summary: View changes
rteijeiro’s picture

Issue summary: View changes
webchick’s picture

Went to look into this tonight, but discovered that atm Devel Generate's image generation is broken: #2294283: Generate images is broken So can't really use that as a model.

juampynr’s picture

Status: Active » Needs review
StatusFileSize
new1.88 KB

Here is a first stab at this.

Given the following:

  • Page content type has a field_image in it.
  • Rest and Hal modules enabled.
  • POST method for nodes supported by cookie authentication in REST settings.
  • Anonymous users with permission to access POST for content nodes, create Page content types and Create URL aliases

And the following Guzzle 3 request:

<?php

/**
 * @file
 *
 * Performs a request to create a page node 
 */

require 'vendor/autoload.php';

use Guzzle\Http\Client;
use Guzzle\Plugin\Cookie\CookiePlugin;
use Guzzle\Plugin\Cookie\CookieJar\ArrayCookieJar;

$cookiePlugin = new CookiePlugin(new ArrayCookieJar());

$client = new Client('http://d8.local');
$client->addSubscriber($cookiePlugin);

// Encode the image.
$image = base64_encode(file_get_contents('/home/juampy/Pictures/sample.jpg'));

// Create the array of data to post.
$node = array(
  '_links' => array(
    'type' => array(
      'href' => 'http://d8.local/rest/type/node/page'
    )
  ),
  'title' => array(0 => array(
    'value' => 'New node title ' . rand(1, 500),
  )),
  'field_image' => array(0 => array(
    'value' => $image,
    'filename' => 'sample.jpg',
  )),
);
$data = json_encode($node);

// Perform the actual request.
$request = $client->post('entity/node', array(
    'Content-type' => 'application/hal+json',
), $data);
$response = $request->send();

// Print out results.
if ($response->getStatusCode() == 201) {
  print 'Node creation successful!';
}
else {
  print_r($response);
}

With the attached patch image fields get populated. Any feedback is welcome. I will start adding testing coverage.

clemens.tolboom’s picture

I just created a patch for GET in #2310307: File needs CRUD permissions to make REST work on entity/file/{id} so revisit this issue.

The title for this issue is weird as it suggest all 'REST operations'. This seems only the be about POST a parent content type _containing_ an image. Please update the summary :)

Reading the patch and issue comments what's the plan for ie a PDF file aka non-images?

The patch misses handling for image.alt and image.title

In #2 it seems we are coding around the validators. Is that still the biggest problem?

A side note: in #1925618: Ensure Drupal's web services are self-documenting: Swagger support OR rest_api_doc to Drupal core as an experimental module? I try to build the API docs by digging into the exposed entity/bundle/field features. How can I get to (lookup) this new normalizer in context of json and hal+json?

berdir’s picture

Yes, should work for all files. image/file references are both entity references, it would make a lot more sense to treat this like any other reference I think, so we would add this base64 magic on the file resource, so you'd upload your file first, and then reference it using the UUID in the image/file field? You could also have entity_reference fields pointing to files, for example.

juampynr’s picture

Thanks for the feedback! I will give it another go and check out what's going on on the issues that @clemens.tolboom mentioned.

clemens.tolboom’s picture

  1. +++ b/core/modules/hal/src/Normalizer/EntityReferenceItemImageNormalizer.php
    @@ -0,0 +1,33 @@
    +      $filename = rand(1, 500) . $data['filename'];
    

    Why rand() call?

  2. +++ b/core/modules/hal/src/Normalizer/EntityReferenceItemImageNormalizer.php
    @@ -0,0 +1,33 @@
    +      $filename = rand(1, 500) . $data['filename'];
    +      $image = file_save_data($image, 'public://' . $filename);
    

    What about filename transliteration?

  3. +++ b/core/modules/hal/src/Normalizer/EntityReferenceItemImageNormalizer.php
    @@ -0,0 +1,33 @@
    +      $image = file_save_data($image, 'public://' . $filename);
    

    This should not always be public://

juampynr’s picture

Status: Needs work » Needs review

Addressed @clemens.tolboom's suggestions:

* Removed rand() statement.
* Filename is now transliterated.
* Now reading field insance's settings in order to determine where to save the file.

juampynr’s picture

Oopsie. Here are the files.

juampynr’s picture

Next, I will address suggestions in #18 and #19.

juampynr’s picture

Title: Imagefields do not support REST operations » File fields do not support REST operations
Issue summary: View changes

Upgrading scope so it covers file fields and not just image fields.

clemens.tolboom’s picture

Title: File fields do not support REST operations » base64 File field content to support REST POST and PATCH operations
Issue summary: View changes
arla’s picture

Based on the latest patch, I have just implemented something like this for the File entity module. I will make it a patch for core shortly and post it here.

juampynr’s picture

Status: Needs work » Needs review
StatusFileSize
new2.5 KB
new4.27 KB

Here it is :D

Next step, I will create another one that inherit from this normalizer for Images.

juampynr’s picture

Here is the sample script that I have been using. The readline() statement is a sneaky trick to be able to trigger XDebug for the Drupal request and not this script:

<?php

/**
 * @file
 *
 * Simple request to create a node with a file field in it.
 */

require 'vendor/autoload.php';

use Guzzle\Http\Client;
use Guzzle\Plugin\Cookie\CookiePlugin;
use Guzzle\Plugin\Cookie\CookieJar\ArrayCookieJar;

readline();
$cookiePlugin = new CookiePlugin(new ArrayCookieJar());

$client = new Client('http://d8.local');
$client->addSubscriber($cookiePlugin);
$image = base64_encode(file_get_contents('/home/juampy/test.txt'));
$node = array(
  '_links' => array(
    'type' => array(
      'href' => 'http://d8.local/rest/type/node/page'
    )
  ),
  'title' => array(0 => array(
    'value' => 'New node title ' . rand(1, 500),
  )),
  'field_file' => array(0 => array(
    'value' => $image,
    'filename' => 'test.txt',
  )),
);
$data = json_encode($node);

$request = $client->post('entity/node', array(
    'Content-type' => 'application/hal+json',
  ), $data);
$request->getQuery()->set('XDEBUG_SESSION_START', '1');

echo "{$request->getQuery()}\n";

$response = $request->send();
if ($response->getStatusCode() == 201) {
  print 'Node creation successful!';
}
else {
  print_r($response);
}
joshk’s picture

Is there a separate issue for handling the GET side of things? I'm working on a D8 + Angular demo and getting rest responses back (read only even) with image references would make it approximately 1100% hotter.

berdir’s picture

The patch that @Arla is working on makes file/image behave like other entity references (with a UUID reference) and moves the support for passing along the file content as base64 to GET/POST of files, so that should include GET yes.

clemens.tolboom’s picture

@berdir where is that issue patch done by @arla as that sound like a way better solution then this :)

arla’s picture

Status: Needs work » Needs review
StatusFileSize
new15.79 KB

So here's the patch.

I'm not sure how image fields are affected by these changes.

I got lots of help from @Berdir.

clemens.tolboom’s picture

@Arla please add some hints on how to test this manually. I assume you have some code lying around :)

arla’s picture

I have just been running the hal kernel tests EntityTest, FileNormalizeTest and FileFieldNormalizeTest. I suppose REST requests will work as expected assuming the normalization is correct.

berdir’s picture

It works with the same principle as the other patch, but you submit the base64 data first by creating a file entity, then you get a UUID (or you submit it already) and then create a node where you reference the file through that UUID. Just like you would create a node with author, or a node with terms.

clemens.tolboom’s picture

@Arla @Berdir I expected a non base64 solution but misread. So File entity supports temporary files now seemed a problem mentioned in #2 and #18.

#35 is a big improvement :-)

arla’s picture

Status: Needs work » Needs review
StatusFileSize
new19.19 KB
new8.44 KB

Actually, it might make more sense to pull the responsibility of normalizing the field properties (description, alt, etc.) up to EntityReferenceItemNormalizer, like in this patch.

arla’s picture

Status: Needs work » Needs review
StatusFileSize
new21.68 KB
new2.49 KB

By relying on parent::denormalize() in FileEntityNormalizer, we practically bypass the filename and filemime assignment in File::preCreate() (because ContentEntityNormalizer passes an empty $values to create()). Thus I removed a bit from FileDenormalizeTest which was testing just this.

Another option could be be to do ContentEntityNormalizer::denormalize() in a different way, but I haven't looked into that... yet.

Edit: I realized that the point of handling 'patch' differently in denormalize() is precisely to avoid setting default values. So it should make sense to remove that particular test case as I did.

juampynr’s picture

@arla, how can we test this? I tried the following without success:

1. Gave access to anonymous users to file and node entities.
2. Added a file field to the page content type.
3. Created a node with a file.
4. Opened http://d8.local/node/2 >> I can see the file as an embedded entity.
5. Tried to access the file without luck. I guess this is related with #2310307: File needs CRUD permissions to make REST work on entity/file/{id} .
6. What should be the structure to be sent in order to create a file? I can't figure it out by reading the above patch.

berdir’s picture

Yeah, not being able to access the file is an access problem, we've been testing this in combination with https://github.com/md-systems/file_entity, which adds an improved access controller.

The structure to create a file is the same as you get with proper access, a normal content entity serialized to hal+json and additionally 'value' with the base64 encoded file content.

clemens.tolboom’s picture

arla’s picture

Without using file_entity module, you can test it like this:

  1. Add a file field to the page content type
  2. Create a page with a file
  3. Enable module hal
  4. drush cedit rest.settings
    1. substitute basic_auth with cookie
    2. duplicate the entity:node section but change to entity:file
    3. drush cr afterwards
  5. Edit FileAccessControlHandler to make sure checkAccess() returns TRUE (as said, this is treated in #2310307)
  6. Open http://d8/entity/file/1, you should see the file entity in hal+json

To correct #46, the base64 encoded file content goes in 'data', not 'value', per the patch. The name of this JSON property is subject to review, though.

arla’s picture

Status: Needs work » Needs review
StatusFileSize
new23.6 KB
new5.11 KB

Trying to address the failing tests. In essence:

  1. In ContentEntityNormalizer::denormalize(), there seems to be no need to unset and re-set the bundle key field.
  2. In EntityReferenceItemNormalizer::constructValue(), I changed to array_key_exists() because otherwise fields with value NULL are skipped. NULL is to be regarded as not set, so maybe the isset() was correct. But that breaks the expectation that an entity is equal before and after serialization+deserialization, as indicated by the failing FileFieldNormalizeTest.
  3. EntityTest::testComment() seemed to expect that revision_id of the target entity be discarded during serialization (from #2233157: Make the comment entity_id be a reference field). Now that EntityReferenceItemNormalizer includes all field properties, it is kept, and as far as I know that is how it should be.
clemens.tolboom’s picture

In #18 one of my questions was about #2

In #2 it seems we are coding around the validators. Is that still the biggest problem?

A rephrase:

We currently want to upload files by creating a entity with file/image fields containing BASE64 encoded field values.

Why do we do it this way?

Why not similar to 'normal' Drupal UI or like the workflow on https://developers.facebook.com/docs/php/howto/uploadphoto/4.0.0 is to post a file and use the resulting ID for further processing. They use http://php.net/manual/en/class.curlfile.php

What happens if I want to create a node containing lots of images when the REST POST size it to big to handle?

berdir’s picture

I don't understand the question.

We *are* doing separate requests for files, so if you want to upload 10 files, you do 10 POST requests for the files first, then pass all the UUID's when you POST the node.

The only difference is that we pass it along as base64 encoded instead of a multipart POST, but I have no idea if our rest/serializer API supports that.

clemens.tolboom’s picture

@Berdir then please update the summary and title according.

I only scanned the patch and comments so misconcluded.

The steps in @Arla in #48 suggest it's fieldness too.

arla’s picture

Title: base64 File field content to support REST POST and PATCH operations » Serialize file content (base64) to support REST GET/POST/PATCH on file entity
Category: Bug report » Feature request
Issue summary: View changes

Yes, since #33 we have been moving the serializing of file contents from the file field to the referenced file entity. Editing summary accordingly.

clemens.tolboom’s picture

Issue summary: View changes
clemens.tolboom’s picture

Status: Needs review » Needs work
$ patch -p1 < base64_file_field-1927648-49.patch
patching file core/modules/hal/hal.services.yml
patching file core/modules/hal/src/Normalizer/ContentEntityNormalizer.php
Hunk #1 FAILED at 124.
Hunk #2 FAILED at 140.
2 out of 2 hunks FAILED -- saving rejects to file core/modules/hal/src/Normalizer/ContentEntityNormalizer.php.rej
patching file core/modules/hal/src/Normalizer/EntityReferenceItemNormalizer.php
patching file core/modules/hal/src/Normalizer/FileEntityNormalizer.php
patching file core/modules/hal/src/Tests/DenormalizeTest.php
patching file core/modules/hal/src/Tests/EntityTest.php
Hunk #4 succeeded at 182 (offset 8 lines).
patching file core/modules/hal/src/Tests/FileDenormalizeTest.php
patching file core/modules/hal/src/Tests/FileFieldNormalizeTest.php
patching file core/modules/hal/src/Tests/FileNormalizeTest.php
patching file core/modules/hal/src/Tests/NormalizerTestBase.php
Hunk #1 FAILED at 14.
Hunk #2 succeeded at 117 (offset -7 lines).
1 out of 2 hunks FAILED -- saving rejects to file core/modules/hal/src/Tests/NormalizerTestBase.php.rej
patching file core/modules/rest/src/LinkManager/RelationLinkManager.php
arla’s picture

Status: Needs work » Needs review
StatusFileSize
new23.56 KB

Reroll.

larowlan’s picture

Assigned: Unassigned » larowlan

rerolling

dave reid’s picture

How would this work with stream wrappers that don't need to be base-64 encoded, like referring to files on Amazon S3? Is there a way to bypass this?

larowlan’s picture

Yeah I think we need to check for the presence of 'data' and fallback to default, which the current patch doesn't yet do

+++ b/core/modules/hal/src/Normalizer/FileEntityNormalizer.php
@@ -64,12 +38,25 @@ public function normalize($entity, $format = NULL, array $context = array()) {
+    // Avoid 'data' being treated as a field.
+    $file_data = $data['data'][0]['value'];
+    unset($data['data']);
+
+    $entity = parent::denormalize($data, $class, $format, $context);
+
+    // Decode and save to file if it's a new file.
+    if (!isset($context['request_method']) || $context['request_method'] != 'patch') {
+      $file_contents = base64_decode($file_data);
+      $dirname = drupal_dirname($entity->getFileUri());
+      file_prepare_directory($dirname, FILE_CREATE_DIRECTORY);
+      if ($uri = file_unmanaged_save_data($file_contents, $entity->getFileUri())) {
+        $entity->setFileUri($uri);
+      }
+      else {
+        throw new RuntimeException(String::format('Failed to write @filename.', array('@filename' => $entity->getFilename())));
+      }
+    }
+    return $entity;

this bit

larowlan’s picture

Status: Needs work » Needs review
StatusFileSize
new1.29 KB
new22.68 KB

re-roll, EntityTest fails - revision id is being added to entity_id field for comments
doesn't address #60, #61

larowlan’s picture

Assigned: larowlan » Unassigned
larowlan’s picture

larowlan’s picture

Status: Needs work » Needs review
StatusFileSize
new22.68 KB
cloudbull’s picture

May i ask for a sample of json file that use to post for file upload ? I cannot figure out whether it is a client / server side problem.

Thanks
Keith

cloudbull’s picture

Status: Needs review » Needs work

This is the json file i use to post file, tried to put both embedded and field_image no luck

I used angularjs project of #32, added a angularjs images to base64 library.

  $scope.ndata =
        {
         "_links" : {
                "type":
                {
                	"href": mySite + "rest/type/node/article"
                },
	    },
	"_embedded": {
	     file_entity_uri : [
	      {
		"_links": {
		  "type": {
		    "href": mySite + "rest/type/file/file"
		  }
		},
		"lang": "en",
		"values": {
		   "filename": $scope.nodeimage.filename,
		   "file": $scope.nodeimage.base64,
		}
	      }
	    ]
	},
        "langcode" : [
            {
            "value": "en"
            }
        ],
        "field_image" : [
            {
            "value": $scope.nodeimage.base64,
	    "filename": $scope.nodeimage.filename,
            }
        ],
        "title" : {"value": $scope.nodetitle },
        "body" :
          [
            {
            "value": $scope.nodebody,
            "format": "basic_html",
            "summary": "",
            "lang": "en"
            }
          ],
        };

Any help is appreciated.

larowlan’s picture

Status: Needs work » Needs review

Cloudbull please open a new support issue for your particular problem - this issue is about changing the way files are serialized - not providing support for the current format.

cloudbull’s picture

larowlan,

I mean i applied the patch, but things doesn't work. The node is created but file is not uploaded. So, i put my testing json file here for reference.

Keith

larowlan’s picture

@cloudbull - oh sorry - misunderstood

cloudbull’s picture

Status: Needs review » Needs work

So i think the status will be need work ?

berdir’s picture

Status: Needs work » Needs review

No.

The structure for POST'ing is always identical to the structure you get back. So manually create a file on the server, then do a GET on file/ID and change it as you want.

Your example structure is impossible to read without being formatted, but one thing that is wrong for example is that the base64 encoded file contents are in 'data', not 'file'.

cloudbull’s picture

Berdir,

thanks your help.

This is the sample i get from D8 beta2, from the embeded image part. But, I think it need to change in order to fit the base64 content, right ?


    "http://localhost/src/drucloud-distro/rest/relation/node/article/field_image": [
      {
        "_links": {
          "self": {
            "href": "http://localhost/src/drucloud-distro/sites/default/files/field/image/50cuteanimpic6.jpg"
          },
          "type": {
            "href": "http://localhost/src/drucloud-distro/rest/type/file/file"
          }
        },
        "uuid": [
          {
            "value": "404ee0e6-6d9c-4754-9200-c50beb9bdc29"
          }
        ],
        "uri": [
          {
            "value": "http://localhost/src/drucloud-distro/sites/default/files/field/image/50cuteanimpic6.jpg"
          }
        ],
        "lang": "en"
      }
    ]
  },


if #16 / #30 's PHP client is correct, the json should be something like

   $scope.ndata[file_entity_uri] = {
            "value": $scope.nodeimage.base64,
	    "filename": $scope.nodeimage.filename,
   };

But i cannot see any json return from the server is similar to this format. Any idea will be appreciated.

Thanks
Keith

berdir’s picture

That's because they messed up the href to the file, it should be something like file/1, aka a resource that you can fetch. That would contain the data you are looking for :(

Have a look at https://github.com/md-systems/file_entity, that should change that and give you a resource you can work with. Note that the module is in early development phase and might/will break.

cloudbull’s picture

Thanks Berdir, So u mean i need to install this module as a contrib module in order to make post file works ?

berdir’s picture

For now, yes. This issue is one step towards making it work in core, but I'm no sure how far we can or should go here. The referenced permission issue is another step (that should also not be a problem if you use file_entity), the URI/href stuff is yet another problem.

Files have always been a second-class thing in Core, and that's changing only slowly.

The current patch is all about serialization of the file, so it only solves the first part of the issue title. Maybe it should be moved to a separate issue, so we can get it in as a first step without being blocked on having an complete working rest integration.

cloudbull’s picture

OK, thanks Berdir, I will install this module and try GET file/1 first. As for now, without this module, I even cannot do file/1 but ok with node/1 or user/1 in D8 Beta2

skyredwang’s picture

@Berdir, make sense. I don't know how much work has been done in your D8 file_entity on github. But, since D8 support both file and rest, therefore, we need to patch either core or upload your branch to file_entity. I would guess rest integration for file entity needs to go in core.

cloudbull’s picture

Berdir,

I tried to enable the file_entity module and got error below

[Tue Oct 28 20:55:14.114583 2014] [:error] [pid 4811] [client 127.0.0.1:49954] PHP Fatal error: Call to a member function getConfigDependencyName() on a non-object in /var/www/html/src/drucloud-distro/core/lib/Drupal/Core/Entity/EntityDisplayBase.php on line 165, referer: http://localhost/src/drucloud-distro/admin/modules

Is that any update require to work with D8 Beta2 ?

And then calling file/1 will have error below

[Tue Oct 28 20:58:42.583792 2014] [:error] [pid 1531] [client 127.0.0.1:49974] Uncaught PHP Exception Drupal\\Core\\Database\\DatabaseExceptionWrapper: "SQLSTATE[42S22]: Column not found: 1054 Unknown column 'base.type' in 'field list': SELECT base.fid AS fid, base.type AS type, base.uuid AS uuid, base.langcode AS langcode, base.uid AS uid, base.filename AS filename, base.uri AS uri, base.filemime AS filemime, base.filesize AS filesize, base.status AS status, base.created AS created, base.changed AS changed\nFROM \n{file_managed} base\nWHERE (base.fid IN (:db_condition_placeholder_0)) ; Array\n(\n [:db_condition_placeholder_0] => 1\n)\n" at /var/www/html/src/drucloud-distro/core/lib/Drupal/Core/Database/Connection.php line 569

Thanks
Keith

sam152’s picture

+1 RTBC. Installed and exporting/importing entities with file/image fields works as expected. This fixed another issue where some were missing on the field causing SQL errors when importing files.

sam152’s picture

+++ b/core/modules/hal/src/Normalizer/FileEntityNormalizer.php
@@ -25,38 +23,14 @@ class FileEntityNormalizer extends ContentEntityNormalizer {
+    if (!isset($context['included_fields']) || in_array('data', $context['included_fields'])) {
+      // Save base64-encoded file contents to the "data" property.
+      $data['data'][]['value'] = base64_encode(file_get_contents($entity->getFileUri()));
+    }

Hmm, after digging into this a little more, is "included_fields" being overloaded here? Should the base64 be toggled with a different key, say "include_data"? I think there are some places which expect everything in that array to be a valid field name, which data is not (I believe?).

InvalidArgumentException: Field data is unknown. in Drupal\Core\Entity\ContentEntityBase->getTranslatedField() (line 349 of core/lib/Drupal/Core/Entity/ContentEntityBase.php).

sam152’s picture

Status: Needs work » Needs review
StatusFileSize
new22.72 KB

Reroll.

sam152’s picture

Continuing on from #84, if users can skip the base64 encoding, should the default behaviour be to still download the files over HTTP? Would seem useful for larger files.

dave reid’s picture

I also have a suspicion that base 64 encoding is not going to be useful for the majority of sites when using GET operations.

sam152’s picture

So if we were to consolidate the requirements for getting this over the line, they might be:

* Optional base64 encoded assets (based on some context, not sure what specifically just yet).
* Default behavior is to base64 encode?
* Fall back downloads the assets over HTTP.

Would be good to get a decisive direction before doing more development.

queenvictoria’s picture

StatusFileSize
new22.74 KB

I'm not sure if I'm doing the right thing here so please have patience. In 88 and 89 it seems to indicate that including base64 in the HAL response is probably not the right way to do things (possibly just the URI maybe?). However I am trying to get support for base64 in PUT and the method I'm following is to use the format provided by GET.

Firstly I've applied the patch in 85 to the current 8.0.x branch. There is a chuck that doesn't apply which doesn't seem to be related anyway so I've ignored it.

Secondly in order to return the data field I've tried to add 'data' to the included fields. This fails as 'data' is not in the field definition for 'files'.

Finally I've unset the included_fields completely for files as that has the same effect (ie: includes ALL fields). This works well for returning the encoded file in the HAL response.

Again: I don't think this is the right way to proceed. Please use with caution.

larowlan’s picture

Status: Needs work » Needs review

go bot go

jhedstrom’s picture

Version: 8.0.x-dev » 8.1.x-dev

Feature request => 8.1.x.

marthinal’s picture

Issue summary: View changes
Status: Needs work » Needs review
StatusFileSize
new39.07 KB

Hi guys!

I need to upload/create images from an angular app.

If I apply #85 (Reroll) I can upload images but we have a couple of problems :-)

1) The first one is "permissions". I'm checking #2310307: File needs CRUD permissions to make REST work on entity/file/{id} and to be honest I'm not sure if file_entity will be the solution or we could fix it in core. I can create images commenting the access() method on post. Enabling file_entity with the same hal+json i have this error:

error: "Unprocessable Entity: validation failed. type.0: The referenced entity (<em class="placeholder">file_type</em>: <em class="placeholder">file</em>) does not exist. "

I need to verify what's going on..

Should we have these permissions into the core?? At least Add/upload and view... Not sure if this is the way and how complicated it is...

2) Once the file is created we should receive the id needed when creating a node for example.

clemens.tolboom’s picture

@marthinal best thing to do is upload your reroll of #85 so we know what you're talking about :-)

BTW what is wrong with #90? Should we hide it?

marthinal’s picture

@queenvictoria about #90, I found the problems described in #94 and had no time to check your changes.

marthinal’s picture

Issue tags: +DrupalCampSpain2015
StatusFileSize
new22.59 KB
new81.14 KB

About #90, Why not avoid to add "data" property on FileEntityNormalizer::normalize() ?

Rerolled #85.

Need help with #94

@queenvictoria Not sure about your changes, if we want to avoid the "data" property for GET method(because the uri is probably enough) then remove this line :

      // Save base64-encoded file contents to the "data" property.
      $data['data'][]['value'] = base64_encode(file_get_contents($entity->getFileUri()));

AFAIK we don't need this property, maybe I'm missing something...

I've detected that uid is not added when creating a new file.

Image attached(file_managed table).

The second one with uid was added from UI.

marthinal’s picture

StatusFileSize
new22.03 KB

Reroll again. I'm commenting the access check at EntityResource::post() and at FileEntityNormalizer::normalize() this line

$data['data'][]['value'] = base64_encode(file_get_contents($entity->getFileUri()));

in this case to avoid the data property on get requests.

With the current permissions it is not possible to avoid the access method without commenting and not possible to continue working-testing the file creation.

Anyways I'm adding the patch and then you can try to upload files and verify that it works. As commented in #97 missing uid when creating a new file entity.

marthinal’s picture

Version: 8.1.x-dev » 8.0.x-dev
Status: Needs work » Needs review

I had some problems with the version working on another patch and maybe this is the problem...

marthinal’s picture

That was the problem. Cannot apply the patch for 8.1.x

marthinal’s picture

Status: Needs work » Needs review
StatusFileSize
new23.2 KB
new1.17 KB

Found the problem. Interdiff attached.

Does it make sense to have this static method only for Node?

 ->setDefaultValueCallback('Drupal\node\Entity\Node::getCurrentUserId')

If I need the same method for File... not sure about the best way to obtain the uid then. So, maybe a getter for the uid from the Drupal class? Maybe this method is useful in other cases too...

So at this point we can create files and get the file with the uri where the file is located.

marthinal’s picture

Status: Needs work » Needs review
StatusFileSize
new25.6 KB
new5.8 KB

As suggested by @berdir (IRC), we can simply allow to upload files. So, if you have access to the file entity resource then you can upload a file.

Code

--- a/core/modules/file/src/FileAccessControlHandler.php
+++ b/core/modules/file/src/FileAccessControlHandler.php
@@ -65,4 +65,11 @@ protected function getFileReferences(FileInterface $file) {
     return file_get_file_references($file, NULL, EntityStorageInterface::FIELD_LOAD_REVISION, NULL);
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) {
+    return AccessResult::allowed();
+  }
+
 }

But, what about validators? Actually we can upload files without controlling the extension or size for example...
From the UI when creating a node, the first step is to upload the image(in the current context :) ). From rest you need to upload the image before too. But we don't know the content type for example or you don't need this to create an image. So you can create an image and attach it to the content type you want. There's no control.

I think maybe we can do something like this:

1) Create the file from rest. When creating the file, by default it will be temporary. So we need to attach it to a node for example to mark as permanent. If you want to validate as the UI does, then add validators to the json. See FileEntityNormalizer::denormalize().

2) You can create a file for a content type and attach it to another one (if you know the uuid), with a extension not allowed for example. So let's add validators, detecting if the field type == 'file' . I was testing manually and if you upload a file from rest, then attach this uuid to the node and try to create the node, the validation works as expected. To be honest for the moment only tested for the extension validator.

We are validating 2 times. The first one is optional and the second when creating a node.

I think we need flood control here too... otherwise you can upload unlimited number of files. Temporary files are deleted by a cron task.

Not sure if I'm missing something but maybe this is a good first step.

marthinal’s picture

If you want to test validators when creating the file then use something like this:


"_links":{"type":{"href":"http://d8/rest/type/file/file"}},
 "filename":[{"value":"mytest.jpg"}],
 "filemime":[{"value":"image/jpg"}],
"validators":[{"file_validate_extensions":[{"value": "jpg"},{"value": "txt"}]}],

 "data":the file here

marthinal’s picture

Probably the first validation could be done from your app...

marthinal’s picture

Status: Needs work » Needs review

About PATCH. You can add an empty entity reference, the file will be removed from the node and then the status will be set to 0. The file will be removed from cron.


{"_links":{"type":{"href":"http://d8/rest/type/node/page"}},"title":[{"value":"Update title"}], "body":[{"value":"body!!!!"}]
,"_embedded":
{
"http://d8/rest/relation/node/page/field_test_image":[{"uuid":[{"value":""}]}]
}
}

Cannot DELETE the file from rest and not sure why.

marthinal’s picture

StatusFileSize
new24.92 KB
new4.1 KB

Verify that File module is installed and check if the current entity has files/images, then validate the file.

marthinal’s picture

Status: Needs work » Needs review
andypost’s picture

+++ b/core/modules/hal/src/Normalizer/FileEntityNormalizer.php
@@ -25,37 +24,14 @@ class FileEntityNormalizer extends ContentEntityNormalizer {
+    if (!isset($context['included_fields']) || in_array('data', $context['included_fields'])) {
+      // Save base64-encoded file contents to the "data" property.
+      //$data['data'][]['value'] = base64_encode(file_get_contents($entity->getFileUri()));

commented out?

  1. +++ b/core/lib/Drupal.php
    @@ -247,6 +247,15 @@ public static function currentUser() {
    +   * Gets the uid from the current active user.
    +   *
    +   * @return \Drupal\Core\Session\AccountProxyInterface
    +   */
    +  public static function getCurrentUserId() {
    +    return static::getContainer()->get('current_user')->id();
    

    please remove, no reason anymore

  2. +++ b/core/modules/file/src/Entity/File.php
    @@ -242,6 +242,7 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
    +      ->setDefaultValueCallback('Drupal::getCurrentUserId')
    

    just add new method like Node does

dave reid’s picture

Issue tags: +D8Media
gaurav.goyal’s picture

StatusFileSize
new24.77 KB

Attached is the rerolled patch, Need to fix #116

marthinal’s picture

Assigned: Unassigned » marthinal
marthinal’s picture

Status: Needs work » Needs review
StatusFileSize
new25.97 KB
new2.98 KB

Fixed Error 500. Verify if canonical path exists.

marthinal’s picture

Status: Needs work » Needs review
Issue tags: +Barcelona2015
StatusFileSize
new26.83 KB
new7.92 KB

As discussed with @klausi we avoid to add the data when normalizing because we can access to the file using the uri.

vivekvpandya’s picture

Thanks Jose,

The latest patch worked on 8.0.x branch. I have uploaded image and text file, it works fine for both
But here is a small problem file is uploaded but when deserilasing it , file is not getting a proper name which is passed along the request.
for following JSON :

{"_links":
{"type":{"href":"http://localhost/dr8b14/rest/type/file/file"}},
"filename":[{"value":"Batman.jpg"}],
"filemime":[{"value":"image/jpg"}],
"data":[{"value":"some base 64 encoded image "}]
}

The uploaded file is not getting proper name i.e in this case it should be Batman.jpg but it gets file4Inkr8 . It seems like file + 6 random chars .
I think that patch will also work for other types of files.

One more point I would like to make here is this method is fine but base64 encoding for file is 33% larger than original file so for mobile apps this matters and also for videos and audios. So Is the Drupal community planning some other option to upload file via REST without base64 encoding ?

vivekvpandya’s picture

@marthinal , one more question here , How can we specify particular directory name into which particular file should go ?

marthinal’s picture

StatusFileSize
new30.37 KB
new6.94 KB

As discussed with @berdir, the user can create a file and set it as persistent(status=1). This patch fix this problem. By default the user can upload a file and if this file is not referenced by an entity(node,user...) then should be removed by a cron task.

Now we are validating using a constraint. So, we only need use this constraint to validate the field(image,file,video...).

@vivekvpandya

1) Let me check why we are adding a random name.

2) Not sure about large files at this moment. I need to comment with @klausi or @berdir.

3) You need to add "uri" to your request

"uri":[{"value": "public://testfolder"}],

vivekvpandya’s picture

@marthinal,
I tried to upload a image file without mime type info and it worked successfully, So what is the use of specifying mime-type ? Is it there for Drupal's file-type restriction ?
Please add all available fields that can be sent via request JSON in documentation. Also let me know if you require any help for that.

marthinal’s picture

Status: Needs work » Needs review
StatusFileSize
new29.04 KB
new2.44 KB

Deny only edit operations for status field.

vivekvpandya’s picture

After successful creation of file resource on site, with 201 it should also report URI for the file so that it can be used in further requests. I hope https://www.drupal.org/node/2546216 will cover this.

catch’s picture

Category: Feature request » Task
vivekvpandya’s picture

StatusFileSize
new29.06 KB

Re-rolling the patch

marthinal’s picture

Status: Needs work » Needs review

@vivekvpandya #137 applies and works as expected.

vivekvpandya’s picture

@marthinal it works on RC1 for me but not on 8.0.x branch , and on RC1 it returns 200 instead of 201.

marthinal’s picture

Status: Needs work » Needs review
Related issues: +#2587957: Arbitrary files can be referenced in file fields even if validation using form API would fail
StatusFileSize
new137.43 KB

Related issue.

@vivekvpandya This is weird... Attached image using #137 patch on 8.0.x branch.

kylebrowning’s picture

StatusFileSize
new29.05 KB

Re-roll

yesct’s picture

Assigned: marthinal » Unassigned

it's been a while, so unassigning.

drnikki’s picture

This works for me on RC2.

kylebrowning’s picture

Status: Needs review » Reviewed & tested by the community

SO RTBC then?

drnikki’s picture

StatusFileSize
new29 KB

It applied cleanly to rc2 but not to 8.0.x. Here's a reroll of kyle's reroll.

neclimdul’s picture

unselecting all the old patches because RTBC acts weird...

We're using this and it seems to work but I don't understand how these changes are making it work so I'm not personally comfortable re-RTBCing this.

dawehner’s picture

StatusFileSize
new28.92 KB
new7.61 KB

Here are a couple of nitpicks.

PS: I cannot see any tests for the FileValidation constraint

  1. +++ b/core/modules/hal/src/Normalizer/EntityReferenceItemNormalizer.php
    @@ -58,12 +59,19 @@ public function normalize($field_item, $format = NULL, array $context = array())
    +    // Discard the target_id property.
    +    $field_name = $field_item->getParent()->getName();
    +    unset($properties[$field_name][0]['target_id']);
    

    Seems kinda out of scope of this issue to be honest. For me I could totally see value in exposing the target_id as well. Don't see any particular problem with that. In case there are some, maybe we should document this here.

  2. +++ b/core/modules/hal/src/Normalizer/EntityReferenceItemNormalizer.php
    @@ -100,12 +106,19 @@ public function normalize($field_item, $format = NULL, array $context = array())
         if (isset($id)) {
    -      return array('target_id' => $id);
    +      $constructed = array('target_id' => $id);
    +      foreach ($field_item->getProperties() as $property => $value) {
    +        if ($property != 'target_id' && array_key_exists($property, $data)) {
    +          $constructed[$property] = $data[$property];
    +        }
    +      }
    +      return $constructed;
    

    Maybe the unsetting is related with that piece of code? Can we document why we need this here, why can't we just copy all of the properties?

  3. +++ b/core/modules/hal/src/Normalizer/EntityReferenceItemNormalizer.php
    @@ -124,4 +137,21 @@ public function getUuid($data) {
    +   * @param FieldItemInterface $field_item
    

    Nitpick: Needs FQCN

  4. +++ b/core/modules/hal/src/Tests/EntityTest.php
    @@ -222,4 +223,41 @@ public function testComment() {
    +    $user = entity_create('user', array('name' => $this->randomMachineName()));
    ...
    +    $file = entity_create('file', array(
    

    Nitpick: Isn't entity_create() kinda deprecated already?

marthinal’s picture

neclimdul’s picture

StatusFileSize
new26.09 KB

which was committed. reroll without the old constraint patch.

klausi’s picture

Status: Needs review » Needs work
  1. +++ b/core/modules/file/src/FileAccessControlHandler.php
    @@ -68,4 +70,22 @@ protected function getFileReferences(FileInterface $file) {
    +    // No user can add the status of a file to avoid to save as persistent.
    +    if ($field_definition->getName() == 'status' && $operation == 'edit') {
    

    you mean "edit" instead of "add"? Did you mean "No user can edit the status of a file. Prevents saving a new file as persistent before even validating it."

  2. +++ b/core/modules/file/src/Plugin/Validation/Constraint/FileValidationConstraint.php
    @@ -17,6 +17,4 @@
    -class FileValidationConstraint extends Constraint {
    -
    -}
    +class FileValidationConstraint extends Constraint {}
    

    unrelated change not needed for this patch.

  3. +++ b/core/modules/hal/src/Normalizer/FileEntityNormalizer.php
    @@ -64,12 +34,26 @@ public function normalize($entity, $format = NULL, array $context = array()) {
    +    // Avoid 'data' being treated as a field.
    +    $file_data = $data['data'][0]['value'];
    +    unset($data['data']);
    

    why do we avoid treating data as a field? Suggested comment: "File content can be passed base64 encoded in a special "data" property. That property is not a field, so we remove it before denormalizing the rest of the file entity."

  4. +++ b/core/modules/hal/src/Normalizer/FileEntityNormalizer.php
    @@ -64,12 +34,26 @@ public function normalize($entity, $format = NULL, array $context = array()) {
    +      $dirname = \Drupal::service('file_system')->dirname($entity->getFileUri());
    

    the file_system service should be injected instead of using \Drupal.

  5. +++ b/core/modules/hal/src/Normalizer/FileEntityNormalizer.php
    @@ -64,12 +34,26 @@ public function normalize($entity, $format = NULL, array $context = array()) {
    +      file_prepare_directory($dirname, FILE_CREATE_DIRECTORY);
    +      if ($uri = file_unmanaged_save_data($file_contents, $entity->getFileUri())) {
    +        $entity->setFileUri($uri);
    

    We save the file contents to the file system, but we don't keep track of it. Ideally we would later delete unused files after 6 hours, same as we do with managed files. I don't see how we could track that except for saving a managed file, which a denormalizer should not do. So I don't see a solution, just worrying a bit.

  6. +++ b/core/modules/hal/src/Tests/EntityTest.php
    @@ -222,4 +225,43 @@ public function testComment() {
    +      'name' => $this->randomMachineName(),
    

    never use random data in tests, see also #2571183: Deprecate random() usage in tests to avoid random test failures.

  7. +++ b/core/modules/hal/src/Tests/EntityTest.php
    @@ -222,4 +225,43 @@ public function testComment() {
    +    $file_uri = 'public://' . $this->randomMachineName();
    

    same here, please no random test data.

  8. +++ b/core/modules/hal/src/Tests/EntityTest.php
    @@ -222,4 +225,43 @@ public function testComment() {
    +    file_put_contents($file_uri, 'hello world');
    +
    +    $data = file_get_contents($file_uri);
    +    $data = base64_encode($data);
    +    file_put_contents($file_uri, 'hello world');
    

    two times file_put_contents()? Why? Why do we need file_get_contents(), the data is just "hello world"?

  9. +++ b/core/modules/hal/src/Tests/EntityTest.php
    @@ -222,4 +225,43 @@ public function testComment() {
    +    // Use PATCH to avoid trying to create new file on denormalize.
    +    $denormalized_file = $this->serializer->denormalize($normalized, File::class, $this->format, ['request_method' => 'patch']);
    

    I don't understand that comment. The serializer is supposed to create a new file object? why do we need "patch" as context here?

  10. +++ b/core/modules/hal/src/Tests/FileDenormalizeTest.php
    @@ -39,14 +39,18 @@ public function testFileDenormalize() {
    +    $denormalized = $serializer->denormalize($normalized_data, 'Drupal\file\Entity\File', 'hal_json', array('request_method' => 'patch'));
    

    let's use File::class here

  11. +++ b/core/modules/hal/src/Tests/FileFieldNormalizeTest.php
    @@ -0,0 +1,117 @@
    +    $file_name = $this->randomMachineName() . '.txt';
    +    file_put_contents("public://$file_name", $this->randomString());
    

    please, no random data in tests.

eiriksm’s picture

Status: Needs work » Needs review
StatusFileSize
new26.19 KB
new8.64 KB

Fixed all points except 9. Not sure what the logic is here, so I am probably not the right person to answer the question or change the patch.

eiriksm’s picture

Status: Needs work » Needs review
StatusFileSize
new872 bytes
new26.3 KB

Oops, sorry. Undefined index. A bit quick there.

Also updated a test class comment that was not accurate anymore.

lightguardjp’s picture

Does anyone have the hal format or required fields for creating a file resource via REST? I've found this part to be severely lacking in the docs department. Maybe it's because I'm new to drupal, but it's been very frustrating try to guess the magic incantation to make things work via REST calls.

vivekvpandya’s picture

@lightguardjp
Here is an example hal+json

{"_links":
{"type":{"href":http://your drupal 8 web site/rest/type/file/file"}},
"filename":[{"value":"filename"}],
"filemime":[{"value":"MIME type"}],
"uri":[{"value": "private://testfolder"}],
"data":[{"value":" base 64 encoded content of your file"
}]
}

Here you can skip uri filed to put the file in public directory.
Here one issue know is that file gets uploaded with some random name. But perhaps @eiriksm 's patch may have solved it.

lightguardjp’s picture

@vivekvpandya Still having issues, maybe we can talk via email (I sent one earlier). During deserialization it blows up as curl doesn't have the private, public, tempory, etc schemes enabled. If you leave out uri then it blows because it uses it for deserializing.

gcardinal’s picture

StatusFileSize
new60.55 KB

Tested serialize_file_content-1927648-164.patch on 8.0.0 with following request:

{"_links":
{
      "type":{"href":"http://drupal.url/rest/type/file/file"}
},
	"filename":[{"value":"input.jpg"}],
	"filemime":[{"value":"image/jpeg"}],
	"data":[{"value":"base64-image-data"}]
}

Image was converted using base64 command line line tool
Using basic authorization
Content-Type: application/hal+json
POST send to http://drupal.url/entity/file/

File has been successfully uploaded to public folder.

fjen’s picture

I'm getting:
Recoverable fatal error: Argument 1 passed to Drupal\hal\Normalizer\FileEntityNormalizer::__construct() must implement interface Drupal\rest\LinkManager\LinkManagerInterface, instance of Drupal\Core\Entity\EntityManager given, called in /var/www/html/core/lib/Drupal/Component/DependencyInjection/Container.php on line 277 and defined in Drupal\hal\Normalizer\FileEntityNormalizer->__construct() (line 39 of /var/www/html/core/modules/hal/src/Normalizer/FileEntityNormalizer.php).

for testing against patched D 8.0.0 with serialize_file_content-1927648-164.patch. Here is my request:

POST /entity/file/ HTTP/1.1
Host: 192.168.99.100:8787
X-CSRF-Token: IawzS8poVGS9Y-zrlmiRR9PF2ANrAN3AoVE_7lHVaS4
Content-Type: application/hal+json
Cache-Control: no-cache
Postman-Token: 36482acb-f429-2b63-f9b6-1c23edbddcf3

{"_links":{"type":{"href":"http://192.168.99.100:8787/rest/type/file/file"}},
"filename":[{"value":"input.png"}],
"filemime":[{"value":"image/png"}],
"data":[{"value":"data:image/png;base64,iVBORw0KGgoAAAANSUh…"}]
}
gcardinal’s picture

For this to work, you will also need to activate / install "Responsive Image" module under Core

gcardinal’s picture

StatusFileSize
new26.3 KB

Patch #164 used only temp filenames, changed denormalize() function to pass real filename to file_unmanaged_save_data() function, on line #280
Now files are created with provided filename in request.

wim leers’s picture

Issue tags: +Needs tests

I'm missing integration tests that effectively prove that GETting/POSTing/PATCHing a base64-encoded file works, like the title indicates.

berdir’s picture

It doesn't work because that doesn't just depend on this issue. This is just the serialization part. There has been a lot of back and forth but I still think that we should split this up.

Actually using it for REST requests also requires working file permissions. See #2310307: File needs CRUD permissions to make REST work on entity/file/{id} . Note that file_entity adds permissions and it works with that (it also contains an override for this, so it works anyway with file_entity, also without this patch).

gcardinal’s picture

Category: Task » Bug report
Priority: Major » Critical

Setting this one to 'critical' - this one needs urgent attention. Working REST is critical part of a modern CMS and one can't go to 8.1.x and 8.2.x without giving this issue time it needs.

larowlan’s picture

Priority: Critical » Major

Please read the priority values post (https://www.drupal.org/node/45111)

gcardinal’s picture

Priority: Major » Critical

From priority values post (https://www.drupal.org/node/45111)
- Significant regressions in user experience, developer experience, or documentation (at the core maintainers' discretion)

Not being able to get, post or edit image using REST is "Significant regressions in developer experience".

Without this basic functionality it is impossible to integrate Drupal 8 or replace existing Drupal 7 installations.

This should be either adresed or support for REST removed from Drupal 8 - this way so RESTful can be turned back to what it was for Drupal 7. Ignoring this issue for months is not a solution.

klausi’s picture

Priority: Critical » Major
Issue tags: +API-First Initiative

Since the REST module is new in Drupal 8 core this cannot be a regression, because it did not exist in Drupal 7 core.

gcardinal’s picture

In practical sense this is a regression. Because REST is in core now, work on contribution module makes no sense. Based on how popular restful for D7 was, it is not unlikely that many D8 installation also depend on REST for integration.

And as long as this in Major it can stay ignored for month. This is just part of unfinished legacy of Drupal 8 release. Work was simply halted and never completed for Drupal 8 release. Then it got this major priority and unfinished bugged code gets dragget from release to release without people starting on Drupal 8 site even being aware of that basic facts that REST in Drupal 8 is not working for anything else then text.

tstoeckler’s picture

Re @gcardinal: While I agree this is issue is important (not making any suggestion on the issue status, though) it's not correct that it's impossible to work with Rest and Files. You just need the contributed File Entity module at the moment.

gcardinal’s picture

@tstoeckler This patch combined with project/file_entity which is in beta and nothing happened over there since January is not exactly a cocktail anyone would want to build any integration up on.

As for Drupal as a project this is just shameful "thing" that just gets swiped under the carpet. After all, this is not a bug - this is supposed to be part of Drupal 8 that never got finished. Its not like its there and dont work - its simply not there.

marthinal’s picture

Version: 8.0.x-dev » 8.2.x-dev
Status: Needs work » Needs review
StatusFileSize
new562 bytes
new26.84 KB

I think it should be green now...

marthinal’s picture

Status: Needs work » Needs review
StatusFileSize
new27.54 KB

Ok. Should apply for 8.2.x

timmillwood’s picture

Here's how we do this in the Replication module http://cgit.drupalcode.org/replication/tree/src/Normalizer/FileItemNorma...
(used by RELAXed web services and Workspace)

marthinal’s picture

Status: Needs work » Needs review
StatusFileSize
new672 bytes
new27.54 KB

@timmillwood Interesting, thanks for the info.

marthinal’s picture

Status: Needs work » Needs review
StatusFileSize
new8.78 KB
new27.5 KB

1. EntityNormalizeTest
when denormalizing $data, it contains the fid. So removing this assertion.

2. Moving FileFieldNormalizeTest to the Kernel tests.

marthinal’s picture

StatusFileSize
new2.49 KB
new29.49 KB

Adding integration test for POST method

marthinal’s picture

StatusFileSize
new2.05 KB
new30.41 KB

Integration test for GET method.

marthinal’s picture

StatusFileSize
new668 bytes
new30.43 KB

This change makes possible enable multiple services per test.

marthinal’s picture

StatusFileSize
new4.33 KB
new32.25 KB

Ok. The next time I'll try to run only a couple of times the testbot :)

Adding PATCH + DELETE.

I think we can improve the tests. For example we should verify that only the owner can remove a file.

marthinal’s picture

StatusFileSize
new4.32 KB
new32.14 KB

1. Adding integration test to verify that only the owner/admin can remove a file. I'm using the patch #2310307: File needs CRUD permissions to make REST work on entity/file/{id}

2. DELETE: I've detected that the file entity is deleted from DB but seems like the file is renamed. So for example foo.txt will be removed from DB but a new foo_0.txt file appears in sites/default/files/... To be honest I have no idea for the moment about how to fix it. Running cron the file still appears.

3. PATCH: You can change the uri and the filename will be the same. So... if I change the uri and then remove the file entity, the file still appears in sites/default/files and ... AFAIK we need to know the file status(from DB) to remove the file using cron. Probably we cannot remove this file. But I'm not sure if I'm missing...

neclimdul’s picture

what about data in the form of data:image/jpeg;base64,? I'm not sure if that's a consistent browser thing or a quirk of the application I'm interacting with but the encoded data is prefixed with that metadata which with the current path is encoded into the contents corrupting the file.

tedbow’s picture

StatusFileSize
new33.07 KB
new1.85 KB

Because of the weird behavior @marthinal described in #198 I thought it would be good to add a check to make sure the file is actually deleted from the database and file system.

This patches adds those checks. It also checks file is actually in the file system before delete.

anthony thomas’s picture

I'm using JSON format, but when I post file this error is return :

Field data is unknown

My JSON :

{
    "filename":[{"value":"input.jpg"}],
    "filemime":[{"value":"image/jpeg"}],
    "uri": [{"value": "http://drupalvm.dev/toto.jpg"}],
    "data": [{"value": "base 64 encode image value"}]
}
wim leers’s picture

Status: Needs review » Needs work

This is clearly getting there, but we need some polish/clean-up to have this be in a maintainable and understandable state. And we definitely need extra test coverage: it's not clear to me at the moment whether you can POST/PATCH multiple files at once, for example (for entities that have multiple file fields).

  1. +++ b/core/modules/file/src/Entity/File.php
    @@ -272,4 +273,16 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
    +  public static function getCurrentUserId() {
    +    return array(\Drupal::currentUser()->id());
    +  }
    

    Ughhhhh this is awful. But there is a precedent: \Drupal\node\Entity\Node::getCurrentUserId().

    So, fine for now.

  2. +++ b/core/modules/file/src/FileAccessControlHandler.php
    @@ -43,6 +45,16 @@ protected function checkAccess(EntityInterface $entity, $operation, AccountInter
    +      // Only admin users and the file owner can delete the file entity.
    

    This is talking about deleting, but this block of code is executed for both updating and deleting…

  3. +++ b/core/modules/file/src/FileAccessControlHandler.php
    @@ -43,6 +45,16 @@ protected function checkAccess(EntityInterface $entity, $operation, AccountInter
    +      if ($account->hasPermission('administer nodes') || $account->id() == $file_uid[0]['target_id']) {
    

    $file_uid->getTargetIdentifier()

  4. +++ b/core/modules/file/src/FileAccessControlHandler.php
    @@ -43,6 +45,16 @@ protected function checkAccess(EntityInterface $entity, $operation, AccountInter
    +        return AccessResult::allowed();
    ...
    +      return AccessResult::forbidden();
    

    Missing cacheability metadata Per that if-test, these both vary by:

    • the permissions cache context
    • the file entity, since you're using a property on the file entity, so do addCacheableDependency($entity)
  5. +++ b/core/modules/file/src/FileAccessControlHandler.php
    @@ -63,4 +75,23 @@ protected function getFileReferences(FileInterface $file) {
    +  protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) {
    +    return AccessResult::allowed();
    +  }
    +
    

    This means anybody can create file entities. Which is exactly what \Drupal\Core\Entity\EntityAccessControlHandler::checkCreateAccess() does for us.

    So you can remove this.

  6. +++ b/core/modules/file/src/FileAccessControlHandler.php
    @@ -63,4 +75,23 @@ protected function getFileReferences(FileInterface $file) {
    +    if ($field_definition->getName() == 'status' && $operation == 'edit') {
    

    Let's use strict equality.

  7. +++ b/core/modules/file/src/FileAccessControlHandler.php
    @@ -63,4 +75,23 @@ protected function getFileReferences(FileInterface $file) {
    +    return AccessResult::allowed();
    

    Let's instead call the parent method. Which means this then is a pure decorator of the base class for this method.

  8. +++ b/core/modules/hal/hal.services.yml
    @@ -16,7 +16,7 @@ services:
    -    arguments: ['@entity.manager', '@http_client', '@rest.link_manager', '@module_handler']
    +    arguments: ['@rest.link_manager', '@entity.manager', '@module_handler', '@file_system']
    

    Why change the position of every injected service that we keep? We remove one service, and add another, but every single one is in a different position. That's not necessary.

  9. +++ b/core/modules/hal/src/Normalizer/ContentEntityNormalizer.php
    @@ -142,10 +143,9 @@ public function denormalize($data, $class, $format = NULL, array $context = arra
    -      // Unset the bundle key from data, if it's there.
    -      unset($data[$bundle_key]);
    

    Why this change?

  10. +++ b/core/modules/hal/src/Normalizer/EntityReferenceItemNormalizer.php
    @@ -78,15 +86,13 @@ public function normalize($field_item, $format = NULL, array $context = array())
    -        $field_uri => array($embedded),
    +        $field_uri => array($embedded + $properties[$field_name][0]),
    

    This looks… very weird. Could use a comment.

  11. +++ b/core/modules/hal/src/Normalizer/EntityReferenceItemNormalizer.php
    @@ -95,12 +101,19 @@ public function normalize($field_item, $format = NULL, array $context = array())
    +    /** @var FieldItemInterface $field_item */
    
    @@ -119,4 +132,21 @@ public function getUuid($data) {
    +   * @param FieldItemInterface $field_item
    

    Needs FQCN.

  12. +++ b/core/modules/hal/src/Normalizer/EntityReferenceItemNormalizer.php
    @@ -95,12 +101,19 @@ public function normalize($field_item, $format = NULL, array $context = array())
    +        if ($property != 'target_id' && array_key_exists($property, $data)) {
    

    Nit: Strict equality.

    Why not isset()? Can the value of this key be NULL?

  13. +++ b/core/modules/hal/src/Normalizer/FileEntityNormalizer.php
    @@ -20,28 +21,19 @@ class FileEntityNormalizer extends ContentEntityNormalizer {
    -   * Constructs a FileEntityNormalizer object.
    +   * Constructs an FileEntityNormalizer object.
    

    Wrong change.

  14. +++ b/core/modules/hal/src/Normalizer/FileEntityNormalizer.php
    @@ -20,28 +21,19 @@ class FileEntityNormalizer extends ContentEntityNormalizer {
    -   * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
    -   *   The entity manager.
    -   * @param \GuzzleHttp\ClientInterface $http_client
    -   *   The HTTP Client.
    -   * @param \Drupal\rest\LinkManager\LinkManagerInterface $link_manager
    -   *   The hypermedia link manager.
    -   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
    -   *   The module handler.
    +   * @param \Drupal\Core\File\FileSystemInterface $file_system
    +   *   The file system.
        */
    -  public function __construct(EntityManagerInterface $entity_manager, ClientInterface $http_client, LinkManagerInterface $link_manager, ModuleHandlerInterface $module_handler) {
    +  public function __construct(LinkManagerInterface $link_manager, EntityManagerInterface $entity_manager, ModuleHandlerInterface $module_handler, FileSystemInterface $file_system) {
    

    This makes no sense at all.

  15. +++ b/core/modules/hal/src/Normalizer/FileEntityNormalizer.php
    @@ -49,8 +41,6 @@ public function __construct(EntityManagerInterface $entity_manager, ClientInterf
       public function normalize($entity, $format = NULL, array $context = array()) {
         $data = parent::normalize($entity, $format, $context);
    -    // Replace the file url with a full url for the file.
    -    $data['uri'][0]['value'] = $this->getEntityUri($entity);
     
         return $data;
       }
    

    Looks like we can remove this altogether now, since this merely calls the parent and returns the result?

  16. +++ b/core/modules/hal/src/Normalizer/FileEntityNormalizer.php
    @@ -59,12 +49,28 @@ public function normalize($entity, $format = NULL, array $context = array()) {
    +    // File content can be passed base64 encoded in a special "data" property.
    

    So this is a property *under* the file reference field, right? Otherwise, you couldn't send multiple files at once.

  17. +++ b/core/modules/hal/src/Normalizer/FileEntityNormalizer.php
    @@ -59,12 +49,28 @@ public function normalize($entity, $format = NULL, array $context = array()) {
    +    $file_data = $data['data'][0]['value'];
    

    Let's put this 'data' key in a class constant.

  18. +++ b/core/modules/hal/src/Normalizer/FileEntityNormalizer.php
    @@ -59,12 +49,28 @@ public function normalize($entity, $format = NULL, array $context = array()) {
    +    // Decode and save to file if it's a new file.
    +    if (!isset($context['request_method']) || $context['request_method'] != 'patch') {
    

    Why do we not need to decode and save when updating?

  19. +++ b/core/modules/hal/src/Normalizer/FileEntityNormalizer.php
    @@ -59,12 +49,28 @@ public function normalize($entity, $format = NULL, array $context = array()) {
    +      file_prepare_directory($dirname, FILE_CREATE_DIRECTORY);
    +      if ($uri = file_unmanaged_save_data($file_contents, file_build_uri(drupal_basename($entity->getFilename())))) {
    

    This is using lots of deprecated functions. Look at the FileSystem service.

  20. +++ b/core/modules/hal/src/Tests/FileDenormalizeTest.php
    @@ -34,14 +34,18 @@ public function testFileDenormalize() {
    +    $data = file_get_contents($file_params['uri']);
    +    $data = base64_encode($data);
    

    Nit: Let's make this a single line.

  21. +++ b/core/modules/hal/src/Tests/FileDenormalizeTest.php
    @@ -34,14 +34,18 @@ public function testFileDenormalize() {
    +    // Adding data to the entity.
    
    +++ b/core/modules/hal/tests/src/Kernel/EntityNormalizeTest.php
    @@ -203,4 +205,39 @@ public function testComment() {
    +    $normalized['data'][0]['value'] = $data;
    

    This reads far too generically. But the constant I asked for above would already make this much, much clearer.

  22. +++ b/core/modules/hal/src/Tests/FileDenormalizeTest.php
    @@ -34,14 +34,18 @@ public function testFileDenormalize() {
    +    // Use 'patch' to avoid trying to recreate the file.
    
    +++ b/core/modules/hal/tests/src/Kernel/EntityNormalizeTest.php
    @@ -203,4 +205,39 @@ public function testComment() {
    +    // Use PATCH to avoid trying to create new file on denormalize.
    

    That shouldn't be the reason we do this; the reason should be that we test both POST and PATCH.

  23. +++ b/core/modules/hal/src/Tests/FileDenormalizeTest.php
    @@ -49,27 +53,6 @@ public function testFileDenormalize() {
    -    // Try to denormalize with the file uri only.
    

    Why did we remove this?

  24. +++ b/core/modules/hal/tests/src/Kernel/FileFieldNormalizeTest.php
    @@ -0,0 +1,115 @@
    +   * Tests that file field is identical before and after de/serialization.
    ...
    +   * Tests that image field is identical before and after de/serialization.
    

    "de/serialization" -> "(de)serialization"

  25. +++ b/core/modules/hal/tests/src/Kernel/FileFieldNormalizeTest.php
    @@ -0,0 +1,115 @@
    +    // Create a file.
    ...
    +    // Create a file.
    

    Pointless comments.

  26. +++ b/core/modules/hal/tests/src/Kernel/NormalizerTestBase.php
    @@ -130,15 +131,14 @@ protected function setUp() {
    +      new ContentEntityNormalizer($link_manager, $this->container->get('entity.manager'), $this->container->get('module_handler')),      new EntityReferenceItemNormalizer($link_manager, $chain_resolver),
    

    Nit: too many spaces.

  27. +++ b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
    @@ -108,9 +108,14 @@ public function post(EntityInterface $entity = NULL) {
    +      // body. Verify if canonical path exists.
    +      if ($entity->hasLinkTemplate('canonical')) {
    

    Good call! :)

  28. +++ b/core/modules/rest/src/Tests/FileTest.php
    @@ -0,0 +1,130 @@
    +class FileTest extends RESTTestBase {
    

    This is testing using HAL+JSON. But this is the REST module, not the HAL module.

    This test should be moved to the HAL module, and a similar one should be created for the REST module. In this new test, we should test both JSON and XML.

    Then the test moved to HAL can hopefully (but not certainly) reuse parts of this new test.

  29. +++ b/core/modules/rest/src/Tests/FileTest.php
    @@ -0,0 +1,130 @@
    +    $this->enableService('entity:' . $entity_type, 'POST', 'hal_json');
    +    $this->enableService('entity:' . $entity_type, 'GET', 'hal_json');
    +    $this->enableService('entity:' . $entity_type, 'PATCH');
    +    $this->enableService('entity:' . $entity_type, 'DELETE');
    

    Why don't we need hal_json for PATCH?

    (For DELETE, it makes sense, for PATCH, not really.)

    This feels like we have either a bug or missing test coverage.

  30. +++ b/core/modules/rest/src/Tests/FileTest.php
    @@ -0,0 +1,130 @@
    +    // Remove non-accessible fields.
    +    unset($normalized_data['status']);
    +    unset($normalized_data['changed']);
    

    They're accessible, they're just not modifiable?

  31. +++ b/core/modules/rest/src/Tests/RESTTestBase.php
    @@ -258,7 +258,7 @@ protected function entityValues($entity_type) {
    -    $settings = array();
    +    $settings = $resource_type ? $config->get('resources') : [];
    

    Is this change here to allow appending?

  32. +++ b/core/modules/serialization/src/Tests/NormalizerTestBase.php
    @@ -3,6 +3,7 @@
    +use Drupal\hal\Normalizer\FileEntityNormalizer;
    

    Unnecessary change.

wim leers’s picture

tedbow’s picture

Status: Needs work » Needs review
StatusFileSize
new33.33 KB
new13.42 KB

Ok here is patch that address some of @Wim Leers recommendations in #202

Tried to fix the quick stuff and will investigate the other issues next. I will need to catch up on the issue better

1. just a note, no change
2. fixed comment
3,4 skipping for now
5. removed
6. fixed
7. fixed
8. reset position of service arguments
9. skipping
10.
$field_uri => array($embedded + $properties[$field_name][0]),
Split the array merging into a line above with comment.
11. add FQCN
12. added Strict equality.
13. fixed wording
14. Fix constructor to match services arguments changed back in 8
15. remove normalize function
16. updated comment
17. added constant
18. skipping, having looked into it yet
19. only saw 1 function that was deprecated. fixed
20. changed to single line
21. used new constant from 17
22, 23. skipping, having looked into it yet
24. fixed wording
25. removed comments
26. fixed spacing
27. just a note - no fix needed
28, 29. skipping, having looked into it yet
30. fixed comment
31. skipping, having looked into it yet
32. It look like this import is need to me. left it

berdir’s picture

Re #203: I've been trying to argue for a while that this issue is only about correctly *serializing and deserializing* a file entity. And not about actually making it work for rest.module. Then we could do this issue and the other one separately.

Question is which issue is going to add actual rest.module integration tests. Could be the one that happens to land second or a new issue for which this and the other one are child issues. Or we make permission depend on this one first and then add tests. I *think* this is closer as the permission issue still has some rather big unanswered questions (from me) AFAIK.

tedbow’s picture

Status: Needs work » Needs review
StatusFileSize
new3.13 KB
new33.67 KB

Ok, in #204 I didn't see that FileEntityNormalizer was instantiated in the tests. I fixed the arguments to the constructor.

tedbow’s picture

  1. +++ b/core/modules/file/src/FileAccessControlHandler.php
    @@ -87,4 +87,12 @@ protected function checkFieldAccess($operation, FieldDefinitionInterface $field_
    +  /**
    +   * {@inheritdoc}
    +   */
    +  protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) {
    +    // @todo Remove this override after https://www.drupal.org/node/2310307 is fixed.
    +    return AccessResult::allowed();
    +  }
    +
    

    I added this back because the parent::checkCreateAccess depends on getAdminPermission which I don't will work until #2310307: File needs CRUD permissions to make REST work on entity/file/{id} is finished.

  2. +++ b/core/modules/rest/src/Tests/FileTest.php
    @@ -65,7 +66,7 @@ public function testCrudFile() {
    -    $normalized_data['data'][0]['value'] = $data;
    +    $normalized_data[FileEntityNormalizer::FILE_DATA_KEY][0]['value'] = $data;
    

    Using the new class constant

wim leers’s picture

#205/@Berdir: I think it doesn't make sense for this issue to land without integration tests, so IMO it'd be more sensible for #2310307: File needs CRUD permissions to make REST work on entity/file/{id} to land first then. Because it sounds like the FileAccessControlHandler changes here are only being done to make this issue actually work, and cannot actually be committed: those changes must happen in #2310307: File needs CRUD permissions to make REST work on entity/file/{id} ?

berdir’s picture

Well, integration for what exactly? We have integration test for serialization. This does nothing rest specific, so why do we need tests for that, here?

This issue alone already enables use cases, like default_content. rest.module isn't the only thing that uses serialization.

dawehner’s picture

Issue tags: -Needs tests

Well, integration for what exactly? We have integration test for serialization. This does nothing rest specific, so why do we need tests for that, here?

Yeah this issue is only about serialization

  1. +++ b/core/modules/file/src/Entity/File.php
    @@ -272,4 +273,16 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
    +   * @see ::baseFieldDefinitions()
    

    It actually turns out that @see :: is not supported by API providers, nor IDE vendors, see https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Render%21...

    Let's not introduce one even yes, we have used it in quite some places now.

  2. +++ b/core/modules/file/src/FileAccessControlHandler.php
    @@ -43,6 +45,16 @@ protected function checkAccess(EntityInterface $entity, $operation, AccountInter
    +      if ($account->hasPermission('administer nodes') || $account->id() == $file_uid[0]['target_id']) {
    

    Don't we have to encode the owner somehow in the access result object cache contexts?

  3. +++ b/core/modules/hal/src/Normalizer/FileEntityNormalizer.php
    @@ -20,51 +26,55 @@ class FileEntityNormalizer extends ContentEntityNormalizer {
    +      if ($uri = file_unmanaged_save_data($file_contents, file_build_uri($this->fileSystem->basename($entity->getFilename())))) {
    

    I should not have looked up file_unmanaged_save_data()

amateescu’s picture

I could only find a few small coding standards issues:

  1. +++ b/core/modules/file/src/FileAccessControlHandler.php
    @@ -63,4 +75,24 @@ protected function getFileReferences(FileInterface $file) {
    +    // @todo Remove this override after https://www.drupal.org/node/2310307 is fixed.
    

    Comment needs to be wrapped to 80 cols.

  2. +++ b/core/modules/hal/src/Normalizer/FileEntityNormalizer.php
    @@ -20,51 +26,55 @@ class FileEntityNormalizer extends ContentEntityNormalizer {
    +        throw new RuntimeException('failed to write ' . $entity->getFilename());
    

    Capital F :)

  3. +++ b/core/modules/hal/tests/src/Kernel/EntityNormalizeTest.php
    @@ -203,4 +206,39 @@ public function testComment() {
    +  }
     }
    

    Missing empty line.

  4. +++ b/core/modules/hal/tests/src/Kernel/FileFieldNormalizeTest.php
    @@ -0,0 +1,113 @@
    +  public static $modules = [
    +    'entity_test',
    +    'field',
    +    'image',
    +    'hal',
    +    'system',
    +    'file',
    +  ];
    

    We usually put these in a single line.

  5. +++ b/core/modules/rest/src/Tests/RESTTestBase.php
    index fc272f7..c2a2ab3 100644
    --- a/core/modules/serialization/src/Tests/NormalizerTestBase.php
    
    +++ b/core/modules/serialization/src/Tests/NormalizerTestBase.php
    @@ -3,6 +3,7 @@
    +use Drupal\hal\Normalizer\FileEntityNormalizer;
    

    Not needed. Note that this is in the serialization module, to not be confused with the class that has the same name from the hal module.

wim leers’s picture

Woot, great to have @amateescu reviewing this patch!

tedbow’s picture

StatusFileSize
new33.21 KB
new3.96 KB

Address review in #212 and #211 except item 3.

@dawehner wasn't sure what you meant

I should not have looked up file_unmanaged_save_data()

file_unmanaged_save_data is not deprecated.

dawehner’s picture

Status: Needs review » Needs work
+++ b/core/modules/file/src/FileAccessControlHandler.php
@@ -50,7 +50,7 @@ protected function checkAccess(EntityInterface $entity, $operation, AccountInter
       if ($account->hasPermission('administer nodes') || $account->id() == $file_uid[0]['target_id']) {
-        return AccessResult::allowed();
+        return AccessResult::allowed()->addCacheableDependency($account);

IMHO we need to add the permission as cache context here, so use cachePerPermissions

file_unmanaged_save_data is not deprecated.

This was just a humorist comment ...

tedbow’s picture

Status: Needs work » Needs review
StatusFileSize
new33.2 KB

@dawehner I didn't know aobut cachePerPermissions.

Updated to use cachePerPermissions

wim leers’s picture

I still have nits, but I have only 3 critical, commit-blocking questions:

  1. Does this work for files that are larger than PHP's memory limit?
  2. +++ b/core/modules/file/src/Entity/File.php
    index f9569ec..ee21486 100644
    --- a/core/modules/file/src/FileAccessControlHandler.php
    

    Is it okay for these changes to happen here, or do we want #2310307: File needs CRUD permissions to make REST work on entity/file/{id} to happen first? This does the same, but not exactly the same.

    Let's document very clearly in the IS what this issue does and why, and what #2310307: File needs CRUD permissions to make REST work on entity/file/{id} then does, and why it's okay for this to go in first. Especially considering this does not add test coverage.

  3. +++ b/core/modules/hal/src/Normalizer/FileEntityNormalizer.php
    @@ -13,6 +14,11 @@
       /**
    +   * Key used to store actual file data.
    +   */
    +  const FILE_DATA_KEY = 'data';
    

    This sounds like it's impossible to POST an entity that has multiple files attached.

    Let's add a test that proves me wrong.

berdir’s picture

1. No. This obviously has limitations, base64 also needs 30% or so more memory than the binary data.
2. I still believe that permissions do *not* belong in here. Who knows what kind of side effects this has, there could be code there that's relying on create permissions and might expose things to anyone that weren't exposed before (rest still needs a separate permission, but there could be other services or so).
3. This is the file entity. You can only post one file, just like you can only post one node at a time. You can however post 3 files with 3 separate requests and then a node that references all of them with the 4th.

damiankloip’s picture

I agree with berdir here, I do not understand why we're bringing REST module into this. REST != serialization component necessarily. E.g. I maintain serialization, NOT REST :)

damiankloip’s picture

  1. +++ b/core/modules/hal/src/Normalizer/FileEntityNormalizer.php
    @@ -20,51 +26,55 @@ class FileEntityNormalizer extends ContentEntityNormalizer {
    -    $file_data = (string) $this->httpClient->get($data['uri'][0]['value'])->getBody();
    

    I think the current/old method is kind of valid too... would be nice if there was a way to incorporate both.

  2. +++ b/core/modules/hal/src/Normalizer/FileEntityNormalizer.php
    @@ -20,51 +26,55 @@ class FileEntityNormalizer extends ContentEntityNormalizer {
    +    if (!isset($context['request_method']) || $context['request_method'] != 'patch') {
    

    Why should denormalization care about the request method like this? Seems rather hackish. Should this not work based on whether there is encoded binary data present or not?

I am also not clear on why this doesn't just move to serialization module itself? This bakes everything into hal module, so it's not usable with other regular type of decoding, like JSON... If it was the other way, hal could easily use this normalizer anyway.

wim leers’s picture

Status: Needs review » Needs work

Moving to NW for #220, especially:

I am also not clear on why this doesn't just move to serialization module itself? This bakes everything into hal module, so it's not usable with other regular type of decoding, like JSON... If it was the other way, hal could easily use this normalizer anyway.

+100

Also, #2310307: File needs CRUD permissions to make REST work on entity/file/{id} landed, this needs to be rerolled.

tedbow’s picture

Assigned: Unassigned » tedbow

Working on re-roll

tedbow’s picture

Status: Needs work » Needs review
StatusFileSize
new32.4 KB

Just a re-roll

berdir’s picture

  1. +++ b/core/modules/file/src/Entity/File.php
    @@ -232,6 +232,7 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
           ->setLabel(t('User ID'))
           ->setDescription(t('The user ID of the file.'))
    +      ->setDefaultValueCallback('Drupal\file\Entity\File::getCurrentUserId')
           ->setSetting('target_type', 'user');
    

    Related: #1979260: Automatically populate the author default value with the current user

  2. +++ b/core/modules/file/src/FileAccessControlHandler.php
    @@ -96,8 +106,6 @@ protected function checkCreateAccess(AccountInterface $account, array $context,
         // (e.g. an image for a article). A contributed module is free to alter
         // this to allow file entities to be created directly.
    -    // @todo Update comment to mention REST module when
    -    //   https://www.drupal.org/node/1927648 is fixed.
         return AccessResult::neutral();
       }
    

    The question is what we're going to do with this now. Just removing the @todo won't be enough :)

    Per discussion in related issues, we're apparently considering again to upload files as part of field values of e.g. the node, which would change 90% of this patch. It would be simpler, but it would also be more limited.

    I guess we could say that if you need more. you can use file_entity. Or some other module, there's at least another one started to contain various serializers.

wim leers’s picture

tedbow’s picture

The question is what we're going to do with this now. Just removing the @todo won't be enough :)

So what are options here? It seems like we could make file entity type core permissions

  • Administer Files
  • CRUD File permissions

But these would be only be used by REST module. So I think this would be confusing for site administrators.

  1. It could confused easily with confused with permissions dealing with File Field permissions(is this the only way to upload files with REST?).
  2. To configure REST permissions on the FILE Resource you would have to set both the File Entity permission and the File REST Resource permissions

While #2 is true for other entity types with other entity types at least the entity type permission have some meaning with just Drupal Core outside of REST.

return AccessResult::neutral();

I did confirm locally with this returning allowed() all the tests pass so I don't think the re-roll introduced any new/unknown problems.

tedbow’s picture

Assigned: tedbow » Unassigned
wim leers’s picture

Assigned: Unassigned » berdir

Berdir has the most experience and insight on this, so I'd say: let's let Berdir decide.

dawehner’s picture

Per discussion in related issues, we're apparently considering again to upload files as part of field values of e.g. the node, which would change 90% of this patch. It would be simpler, but it would also be more limited.

I'm just trying to be curious here.
Would that cause issues with something like a maximum payload you could manage?

wim leers’s picture

#230:

  • #217.1: Does this work for files that are larger than PHP's memory limit?
  • #218.1: No. This obviously has limitations, base64 also needs 30% or so more memory than the binary data.
berdir’s picture

Assigned: berdir » Unassigned

Yes, but the point is that uploading as part of the node then means that max applies to the sum of all files. Imagine creating a gallery node with 20 images for example. That's just not going to work. While creating them separately will take a while but there's a chance that it will work ;)

I don't feel like I can decide here. All I can offer are some suggestions/ideas.

#227 has the same questions as I asked in #2310307: File needs CRUD permissions to make REST work on entity/file/{id} and there we basically avoided that problem, resulting in not actually solving it for this issue, which was my understanding of the split between the two.

As mentioned before, this is all working quite fine with file_entity, because it has those permissions and also a version of this serializer.

I know that it would derail this even more, but one idea would be to actually bring some of the features of file_entity into core, which is something that @slashrsm and I talked about. Like being able to upload files "into the media library" as a standalone thing. Then we'd need a permission for that and it wouldn't be weird anymore if rest would allow the same. It would also force us to think once more about the whole file usage and auto-delete situation, which is still not really resolved (upload a file, then use it (file usage is now 1), then stop using (file usage is now 0 and it is removed).

Another idea is the the same permission concept that we have for private files. By default, you can see an private file if you can see at least one of the entities that are using it. So we could say you can upload a file if you have access to at least one entity with a file/image field. obviously, that could be a pretty slow access check, if we'd need to do create access checks for 10 different node types until we find one that you're allowed to use. But it would kind of reflect the situtation in the UI. And we already enforce that files are temporary until you actually use them.

But even then, one of the problems that we haven't solved so far is file validation. In core, that's all tied to file/image fields. They control what file types you may upload, how big they can be and so on..

tedbow’s picture

re #224"

Per discussion in related issues, we're apparently considering again to upload files as part of field values of e.g. the node, which would change 90% of this patch. It would be simpler, but it would also be more limited.

If anybody has links to these issues can you post them here so we can see what discussion is going on? Thanks

Would this be an either or situation? Could we support both? I could see clients wanting to do both in different situations.

As @Berdir says if you are uploading a gallery node you are likely to run into upload limit if you have serializing all files into 1 node POST. But then again if you are just posting an article with an image having to post the image first and then post the article which includes a reference to the just posted image seems is 2 posts which could just be 1.

re @Berdir #232

I know that it would derail this even more, but one idea would be to actually bring some of the features of file_entity into core, which is something that @slashrsm and I talked about.

It seems like this will probably happen eventually, especially since @dries mentioned Media as an initiative in Drupalcon NOLA.

One option would be to postpone this issue until core had the ability to upload files separate from file reference fields. Presumably at that point there would be permission for uploading files themselves and then rest could just check that permission.

In the meantime we could open a separate issue for letting file be serialized as part of file reference field.

Then after both issues were finished there would 2 ways to upload files and clients could use which was easier for a situation.

I think the problem with doing this 1 now and adding new CRUD permissions for file entities is that we run into a situation where we will want to change the way permissions work for file entities and then have to introduce a backward compatibility layer. For instance once something like File Entity is in core then we would probably not want file entity level CRUD permissions but rather file bundle level permissions.

Imagine creating a gallery node with 20 images for example. That's just not going to work. While creating them separately will take a while but there's a chance that it will work ;)

If this issue did get postponed but the serializing on file fields got committed would it still be possible to attach 20 images to a single node(or other entity) via PATCH requests? Obviously it wouldn't be ideal versus individual file entity posts and 1 post to the referencing entity.

skyredwang’s picture

I am an Android developer. I agree with @Berdir issue. Not being able to manage many files doesn't sound right. Also, I'd like to point out that for mobile dev, design for offline is a priority, in other words, uploading files based on connectivity conditions is must. Therefore, I would never want to combine 1 article and 1 image into 1 HTTP post to Drupal. It's always ideal to separate them into two requests.

bc’s picture

re-rolled patch for 8.2.x c9e8e141435fc916c6ead204dc48df132e7c5df5

tedbow’s picture

Status: Needs work » Needs review

@bc thanks for re-roll
Setting to "needs review" to trigger tests

bc’s picture

StatusFileSize
new32.44 KB

darn it all, i uploaded a patch made against drupal-core; here's a more testbot-friendly one:

tedbow’s picture

@bc was #238 just a re-roll of #223 or where there other changes. I am wondering about the extra failing tests.

Thanks

bc’s picture

Just a re-roll -- it's pretty strange. I'm double checking my work now.

bc’s picture

Status: Needs work » Needs review
StatusFileSize
new32.59 KB

Missed a line in my reroll. Ugh. Let's see how the tests go this time ;)

tedbow’s picture

@skyredwang re:

I agree with @Berdir issue. Not being able to manage many files doesn't sound right.

and

Therefore, I would never want to combine 1 article and 1 image into 1 HTTP post to Drupal. It's always ideal to separate them into two requests.

I don't disagree with you here but if there is a way to manage files separate from any entity they are attached to then we would need some sort of file permissions.

So then we would have to pick a way to implement those permissions.

Option 1: Create file CRUD permissions and have REST respect those permissions

This has several problems

  1. Do we create these permissions in the File module though without REST enabled these permissions have no meaning(through core web interface)
  2. Do we create these permissions in the REST module for the file entity?
  3. How to avoid the confusion for the site admin that "File Edit permission" actually has no functionality outside rest, i.e. it doesn't apply to File fields?

Option 2: Create REST specific verb permissions for file methods

The also introduces problems

  1. There is already: #2664780: Remove REST's resource- and verb-specific permissions for EntityResource, but provide BC and document why it's necessary for other resources when this lands file REST methods would be different from all other Entity Resources
  2. When media library functionality does get into core as @Berdir suggests in #232 it would introduce File CRUD permissions then we would likely want to remove the File verb permissions we added here and create a backwards compatibility layer like #2664780 is doing for other entities. These just seems confusing for the site admin.(imagine maintaning/developing multiple 8.x some using this bc layer and some not)

Option 3: Treat similar to private allow uploading temporary files based on entity access

From @Berdir comment in #232 again

So we could say you can upload a file if you have access to at least one entity with a file/image field. obviously, that could be a pretty slow access check, if we'd need to do create access checks for 10 different node types until we find one that you're allowed to use. But it would kind of reflect the situtation in the UI. And we already enforce that files are temporary until you actually use them.

Again this option has problems

  1. Slow access checks. Checking create access are across all entity types that have a bundle that has a file field.
  2. What if the user only has edit access then presumably you would need to check every entity that has a file field :( ?

One option, that doesn't seem very clean, to get around the slow access checks would be to have the file upload specify the entity or entity type that will be created that user will be attaching the file to and base the access check on that entity or create operation. But I don't know how you would cleanly specify that in REST post(a custom header?)


I don't think it is ideal to only be able to upload files as part of a field on an entity but we do have to solve the permissions issue we are going to manage them directly.

Once something like file/media library is then this would much easier because it would add file permissions.

But I don't think it is good idea delay file uploads via REST

Thoughts on those options or others?

Of course the other option is upload file as part of file field on another entity. While that doesn't introduce permissions problems it does introduce other problems mention in this issue.

skyredwang’s picture

https://github.com/fago/entity/pull/9 will land soon, I am wondering if it will help?

tedbow’s picture

@skyredwang no that is the Entity API contrib module and won't affect Drupal core

Also building the permissions is not the hard part. The hard part is choosing one of the options I described in #245 or another that we haven't thought of. And then dealing with the downsides of whatever choice we make(they all have downsides)

garphy’s picture

Concerns about the maximum size of the actual "upload" were raised multiple times in this issue but were never actually really addressed.

Maybe the vast majority of current contributors to this issue are not used to manage big files but on the majority of projects I work on, we regularly upload files which size vary from several 100s MB to several GBs.

It's obvious that the approach of trying to base64_decode the payload in RAM then write it to disk won't work, not only because of PHP memory limitation but also because of underlying system itself memory limitations.

And it's worth mentioning that REST client will also have the same issue when trying to build the request. It's probably doable (but tedious) to "stream" the base64 encoded payload from the client to the server, but I can't actually think of a "streamed" approach on the server-side. We need to have the complete JSON (or whatever) request payload before trying to find the base64 payload.

The natural alternative approach would be to address this concern using "plain old" multi-part POST request in which we could have the file payload (obviously) and the required additional metadata needed to create a file_entity. This would (maybe) "break" REST semantics but I would be usable, even for very large files.

I would be more than happy to read followups on that :)

tedbow’s picture

@garphy

Maybe the vast majority of current contributors to this issue are not used to manage big files but on the majority of projects I work on, we regularly upload files which size vary from several 100s MB to several GBs.

Thanks for the input. I agree the serialization method would not work well for files of this size.

I would think though that serialization method would work for the majority of sites. Also just because we add this method does not preclude us adding another method later. Of course work could be done in contrib to prove a method of using REST resources to handle files of the the size you are talking about.

My opinion is that we shouldn't change this issue just because it doesn't work for every use case. Especially if as you say "vast majority of current contributors to this issue" haven't run into this use case. If true that probably points to it being a not very common use case.

garphy’s picture

Ok, I'll then go to implement a custom solution to accept multipart/form-data request that would upload file data and create the corresponding file entity.

What would be great is to be able to "hook" this solution with core REST module by making it accept multipart/form-data requests. Any thought on this ?

garphy’s picture

Status: Needs work » Needs review
StatusFileSize
new31.95 KB

Re-rolled the latest patch

bc’s picture

Here's how I added a POST resource for multipart/form-data uploads: https://gist.github.com/bnchdrff/a388b763bc97f7a1c6107698652cc58d

berdir’s picture

@bc: your approach still loads the file contents into memory s o it actually has pretty much the same limitations, minus the base64 overhead.

And sure, something like that is possible. But doing that through the REST/serialization framework is harder.

bc’s picture

I'm curious how many people are uploading files as serialized data at this point vs how many people are either giving up on d8 or writing hacky custom endpoints like me :)

In the case of a mobile client uploading an image to a d8 server, it's really klutzy and inefficient to serialize the photo on the client -- many phones will lock up / OOM.

garphy’s picture

Here's my approach so far to upload & create a file entity using multipart/form-data and avoid loading the file in memory :

https://www.drupal.org/sandbox/garphy/2770603

It tries to reuse existing pieces like file_save_upload(), so the key for the file in POST data has to be "files[file]".

bc’s picture

Garphy the link doesn't work! I'm curious how you've solved it -- also if anyone else on this issue has stopgap solutions maybe they can be used to inform how the d8 core api could function.

garphy’s picture

Sorry for the wrong link...

Sandbox project page : https://www.drupal.org/sandbox/garphy/2770603
Repo : http://cgit.drupalcode.org/sandbox-garphy-2770603/tree/

The only important piece of code is the controller which basically does

  function handle(Request $request){

    $destination = 'private://uploads';
    file_prepare_directory($destination, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS);
    $file = file_save_upload('file', array(), $destination, 0);
    return JsonResponse::create(['fid' => $file->id()]);

  }

Version: 8.2.x-dev » 8.3.x-dev

Drupal 8.2.0-beta1 was released on August 3, 2016, which means new developments and disruptive changes should now be targeted against the 8.3.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Pedro J. Fernandez’s picture

Hi everyone!

I got this working but the uploaded file is always uploaded in the /sites/default/files folder. I tryed to specify the destination folder adding the "uri" attribute to the hal request, or writting the uri in the filename attribute instead only the name. i.e. public://some_existing_path/filename. For any reason, the file is always created in the /sites/default/files and the destination folder is ignored. Please, I don't know where is the problem. Please, help.

This is the POST body:

{
  "_links": {
    "type": {
    	"href" : "http://www.example.com/rest/type/file/file"
     }
  },
  "filename":[{"value": "public://photos/test11.jpg"}],
  "filemime":[{"value":"image/jpeg"}],
  
  "data": [{ "value" : "/9j/4Rd9RXhpZgAASUkqAAg........ }]

}

The file is created and this is the response: 201 Created

{
  "fid": [
    {
      "value": "77"
    }
  ],
  "uuid": [
    {
      "value": "777ebf25-5f4f-42f3-a570-ac5bfceefa2b"
    }
  ],
  "langcode": [
    {
      "value": "en"
    }
  ],
  "uid": [
    {
      "target_id": "1",
      "target_type": "user",
      "target_uuid": "3096cd62-05b9-417e-841e-d2072c9e93f4",
      "url": "/es/user/1"
    }
  ],
  "filename": [
    {
      "value": "public://photos/test11.jpg"
    }
  ],
  "uri": [
    {
      "value": "public://test11.jpg"
    }
  ],
  "filemime": [
    {
      "value": "image/jpeg"
    }
  ],
  "filesize": [
    {
      "value": 297880
    }
  ],
  "status": [
    {
      "value": false
    }
  ],
  "created": [
    {
      "value": 1471384327
    }
  ],
  "changed": [
    {
      "value": 1471384327
    }
  ]
}

And the file is created in /sites/default/files instead of /sites/default/files/photos.

eugene.ilyin’s picture

@Pedro

I got this working but the uploaded file is always uploaded in the /sites/default/files folder. I tryed to specify the destination folder adding the "uri" attribute to the hal request, or writting the uri in the filename attribute instead only the name. i.e. public://some_existing_path/filename. For any reason, the file is always created in the /sites/default/files and the destination folder is ignored. Please, I don't know where is the problem. Please, help.

I've specified uri:

"uri" : [{"value":"public://project/sad_photo.jpg"}],
and my file has beed saved according to path.

P.S. As I see according to code in patch, now I cannot upload multiple files in one request? Only one file per request. Am I correct?

I need to save bunch of files into the Node and would be nice to avoid 20 requests.

Pedro J. Fernandez’s picture

@eugene.ilyin

Thanks for your fast answer! But I have tryed this and I get the same result.

{
  "_links": {
    "type": {
    	"href" : "http://www.example.com/rest/type/file/file"
     }
  },
  "filename":[{"value": "test122.jpg"}],
  "filemime":[{"value":"image/jpeg"}],
  "uri":[{"value": "public://photos/test122.jpg"}],
  "data": [{ "value" : "/9j/4Rd9RXhpZgAASUkqAAgA .......   "}]
}

And I get this as result:

{
  "fid": [
    {
      "value": "81"
    }
  ],
  "uuid": [
    {
      "value": "217abec0-699a-4b03-a9a2-08d9401627d0"
    }
  ],
  "langcode": [
    {
      "value": "en"
    }
  ],
  "uid": [
    {
      "target_id": "1",
      "target_type": "user",
      "target_uuid": "3096cd62-05b9-417e-841e-d2072c9e93f4",
      "url": "/es/user/1"
    }
  ],
  "filename": [
    {
      "value": "test122.jpg"
    }
  ],
  "uri": [
    {
      "value": "public://test122.jpg"
    }
  ],
  "filemime": [
    {
      "value": "image/jpeg"
    }
  ],
  "filesize": [
    {
      "value": 297880
    }
  ],
  "status": [
    {
      "value": false
    }
  ],
  "created": [
    {
      "value": 1471436748
    }
  ],
  "changed": [
    {
      "value": 1471436748
    }
  ]
}

The patch I have applied is the 1927648_216.patch, not the last one, over a drupal 8.1.8. May be this is the problem.

To mitigate any kind of doubt, I have applied the last patch available (serialize_file_content-1927648-251.patch) in a brand new drupal 8.1.8. I have updated my drupal solution with it and the I tried again.

I have to say that every time I made a change in code, I creared the cache before testing again.

First of all, 403 Forbidden message appeared. I remaind that I have to change something to get it work last time, concretely this in the file core/modules/file/src/FileAccessControlHandler.php:

  protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) {
    // The file entity has no "create" permission because by default Drupal core
    // does not allow creating file entities independently. It allows you to
    // create file entities that are referenced from another entity
    // (e.g. an image for a article). A contributed module is free to alter
    // this to allow file entities to be created directly.
    //return AccessResult::neutral();     <-- Remove neutral and leave allowed here
    return AccessResult::allowed();
  }

Now, 201 Created received, but same behaviour. Files are created in the public:// root folder but destination folder is ignored. Nothing has changed compared with the previous patch.

There are more patches I have to apply apart from this?
I'm a bit frustrated with this issue :-(

eugene.ilyin’s picture

@Pedro, I've used patch from #251

benjy’s picture

StatusFileSize
new23.89 KB

I re-rolled this against 8.1.x as that's what i'm currently developing against. I got this working but note that since #2310307: File needs CRUD permissions to make REST work on entity/file/{id} you need to handle enabling the "create" permission for file entities. I presume installing the File Entity module will solve that, for my use case I created a simple module that allowed access to create file entities to anyone with the restful post entity:file permission. Module is simple for anyone that is interested:

// In the module file
function MODULE_entity_type_alter(array &$entity_types) {
  $entity_types['file']->setAccessClass('Drupal\your_module\FileAccessControlHandler');
}

// In FileAccessControlHandler
<?php

namespace Drupal\your_module;

use Drupal\Core\Access\AccessResult;
use Drupal\Core\Session\AccountInterface;
use Drupal\file\FileAccessControlHandler as CoreFileAccessControlHandler;

class FileAccessControlHandler extends CoreFileAccessControlHandler {

  /**
   * {@inheritdoc}
   */
  protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) {
    return AccessResult::allowedIfHasPermission($account, 'restful post entity:file');
  }
}

Might also be worth pointing out that you have to use application/hal+json as the Content-Type, I was previously using JSON and that doesn't work because the FileNormalizer is in the hal module and hal_json is the only supported format for the normalizer. Finally an example JS payload might help others:

      let response = await fetch(this.baseUrl + '/entity/file?_format=hal_json', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/hal+json',
            'X-CSRF-Token': token,
          },
          body: JSON.stringify({
            _links: {
              type: [{href: this.baseUrl +'/rest/type/file/file'}],
            },
            filename: {
              value: file_name,
            },
            filemime: {
              value: file.content_type,
            },
            data: [{
              value: file.data
            }],
          }),
        });
benjy’s picture

Status: Needs work » Needs review
StatusFileSize
new24 KB

Here's an 8.2.x version

tinom’s picture

I have applied patch 1927648-264.patch to 8.1.x successfully, added File Entity module but now the error is:

rest/type/file/file does not correspond to an entity on this site.

Any idea what I am missing here?

Also, I want to upload the image not as a standalone entity but to a specific object. How can that be done?

larowlan’s picture

Issue tags: +Default content
tedbow’s picture

@Pedro J. Fernandez, @eugene.ilyin, @benjy, @tinom please don't use this issue for support requests for a patch in progress, or post backported patches to previous core versions that this issue does not relate to.

This issue is over 260 comments and it still needs work. It makes it very difficult for people who want to come work on the current patch to understand the issue because they have to read through so many comments. This is even worse if they have to weed through comments that are not about developing the current patch.

Thanks

benjy’s picture

Well, this is the canonical place for the problem of uploading files via REST and 8.1 is still the stable version of Drupal, so if we don't post patches here I'm not sure where else people would find such patches.

kylebrowning’s picture

Im actually going to make a case for switching this to use multipart form encode or maybe BSON, https://en.wikipedia.org/wiki/BSON

- base64 encoding makes file sizes roughly 33% larger than their original binary representations
- base64 encoded data may possibly take longer to process than binary data

This results in loss of data/compression in 2 areas, and strings will be huge. For Example a 1.6kb image is a string 1700 characters long, making a payload to upload a 5 mg image just insane.

What this means is that you will need more memory (in your browser or mobile device) than before just to upload an image/video.

With Mobile devices getting bigger and bigger cameras, this will cause OOM errors in many places. And its not just with mobile, browsers will have this issue too.

With multipart, we can send a fully represented binary object, and save the heavy memory footprint because its not a string. Of course the dev will still resize cause they can't upload a 200 mg video, or 20 mg picture.

#252 addresses this in a very nice way. Im going to provide a patch based on that.

garphy’s picture

#271: +1

I would add :
- the memory consumption is also a server-side issue, you cannot require the server to allocate the file size of each upload in memory, it would obviously not be scalable ;
- there's no way (of I'm aware of) to provide a file upload progress status to the user when submitting a JSON blob.

Evgeny_Yudkin’s picture

I tried attached patches:
1) There are issues with jsonapi (type errors into the serializer constructor);
2) file_entity module worked for me (no need any patches);

By the way, all patches affects "hal" module, but what is somebody wants to create file via json/xml/any other format?

Version: 8.3.x-dev » 8.4.x-dev

Drupal 8.3.0-alpha1 will be released the week of January 30, 2017, which means new developments and disruptive changes should now be targeted against the 8.4.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

kim.pepper’s picture

StatusFileSize
new21.35 KB

Re-roll of #265 for 8.2.x

* core/modules/hal/tests/src/Kernel/EntityNormalizeTest.php was removed in #2824576: Delete old REST test coverage: (Read|Create|Update|Delete)Test, deprecate RESTTestBase

wim leers’s picture

Status: Needs review » Needs work

Thanks! But #271 still needs to be addressed, so back to NW…

wim leers’s picture

Category: Bug report » Task

This is not a bug. It's something very important capability that is missing. Technically, this is a Feature request, but because it's such a glaring omission, I think Task is more appropriate.

kylebrowning’s picture

Should we update the title too Wim?

"Allow REST File uploads via binary."

bradjones1’s picture

The re-roll in #276 no longer applies, I tried some manual re-working but the array syntax patch for core paired with some changes in the test classes in the interim made it a bit unwieldy.

I would agree with assessing the issues re: this only applying to HAL at the moment, along with the concerns over Base64 vis-a-vis upload size. In the interim, rather than waiting on core support for this I believe the FileEntityNormalizer that appeared in these patches could live in custom/contrib land for the time being, albeit without the tight integration with core.

damiankloip’s picture

StatusFileSize
new2.97 KB

OK, here is a new approach. I think it was discussed previously in part. However, it's a new approach from the standpoint that there is new code here. For now, I am disregarding the previous patches. We can revisit/re-integrate stuff needed to this. #280 is correct too, there are a lot of changes to reconcile anyways...

I have spoken to a few people about this. The only way I see to solve this properly is to expose a custom route that only deals with POSTing binary file data. We can then stream the data properly (I think) without always loading the whole file contents into memory. Using this approach locally, I was easily able to save files much much larger than my PHP memory limit. To do this, we obviously need a stand-alone endpoint only for binary data. So creation of the file entity would need to be done in a separate request, so you could obtain an ID. I think that is acceptable though

This patch is still very rough, but for now it just provides a new route and controller to handle POSTing binary data only. So E.g. if you were using curl to do this:

curl -v --header "Content-Type:application/octet-stream" --data-binary @/path/to/your/file http://d8-dev/file/upload/{FID}

wim leers’s picture

So creation of the file entity would need to be done in a separate request, so you could obtain an ID. I think that is acceptable though

Yes.

Especially if this can be combined with https://www.drupal.org/project/subrequests.

damiankloip’s picture

StatusFileSize
new11.29 KB
new8.8 KB

Here is a bit more work on this. Thinking about it, I think we need to do this (which this patch starts to do):

  • As in the previous patch - have an endpoint only for dealing with uploading actual binary file data, requiring a file entity and access to it
  • Remove the hal FileEntityNormalizer, it does the HTTP fetching we don't really want, doesn't use anything from the parent normalizer, and uses ->create() directly to create the entity instance. We have really tried to avoid this, as field level denormalization is never called. This will now make file entities behave like any other content entity, you update fields on it. So REST endpoints would behave as normal, disregarding the actual file data

Problems we need to work out here still:

  • Still decide who is allowed access to this endpoint, via file update access checking or additional/alternative custom logic. We might want to remove the changes to the FileAccessControlHandler logic made in this patch too?
  • Still need field item level normalization I think, as E.g. hal will show the 'self' href as the actual file URL only, we would want this to be the REST file endpoint instead I think? regular formats like JSON should be OK - we get target_id, target_uuid, and URL - so still usable

This obviously all still needs some additional test coverage too!

damiankloip’s picture

StatusFileSize
new12.09 KB
new2.52 KB

Couple of other improvements to the upload controller.

dawehner’s picture

  1. +++ b/core/modules/file/file.routing.yml
    @@ -4,3 +4,14 @@ file.ajax_progress:
    +  requirements:
    +    _entity_access: 'file.update'
    

    Is there no file.create entity access?

  2. +++ b/core/modules/file/src/Controller/FileUploadController.php
    @@ -0,0 +1,96 @@
    +  /**
    +   * {@inheritdoc}
    +   */
    +  public static function create(ContainerInterface $container) {
    +    return new static(
    +      $container->get('file_system')
    +    );
    +  }
    +
    +  /**
    +   * Constructs a FileUploadController instance.
    +   *
    +   * @param \Drupal\Core\File\FileSystem $file_system
    +   */
    +  public function __construct(FileSystem $file_system) {
    +    $this->fileSystem = $file_system;
    +  }
    

    Personally I prefer to have the constructor first, given that its more important and the create() method is just an implementation detail.

  3. +++ b/core/modules/file/src/Controller/FileUploadController.php
    @@ -0,0 +1,96 @@
    +    if ($request->headers->get('Content-Type') !== 'application/octet-stream') {
    +      throw new HttpException(400, '"application/octet-stream" content type must be used to send binary file data');
    +    }
    

    Isn't that a classical 415 error?

  4. +++ b/core/modules/file/src/Controller/FileUploadController.php
    @@ -0,0 +1,96 @@
    +  protected function streamUploadData(File $file) {
    

    Should we use FileInterface here?

  5. +++ b/core/modules/file/src/Controller/FileUploadController.php
    @@ -0,0 +1,96 @@
    +      while(!feof($file_data)) {
    

    Nitpick: Missing space after the while

  6. +++ b/core/modules/file/src/Controller/FileUploadController.php
    @@ -0,0 +1,96 @@
    +      // Move the file to the correct location based on the file entity,
    +      // replacing any existing file.
    

    I'm wondering whether its the right thing to replace existing files.

  7. +++ b/core/modules/file/src/Controller/FileUploadController.php
    @@ -0,0 +1,96 @@
    +      if (!file_unmanaged_move($temp_file_name, $file->getFileUri(), FILE_EXISTS_REPLACE)) {
    +        throw new HttpException(500, 'Temporary file could not be moved to file location');
    ...
    +      $this->getLogger('file system')->error('Temporary file "%path" could not be opened for file upload', ['%path' => $temp_file_name]);
    +      throw new HttpException(500, 'Temporary file could not be opened');
    

    Why do some of those exceptions cause a logging, and some don't?

damiankloip’s picture

Hey Daniel!

so...

1. This is only for updating file data, not the file entities, you would have to already have a file entity at this point. So it's only ever an update kind of?
2. fair
3. Yes! This is the kind of thing that needs to be tidied up. Unsupported type is correct.
4. Yep, meant to use that!
5. fair
6. If you don't replace them, you would get lots of orphaned files? and you would need to update the actual file entity fields too? the size, type, name, everything could change. So then you are back in to create territory?
7. file_unmanaged_move generates its own logging upon failure.

damiankloip’s picture

StatusFileSize
new12.12 KB
new2.85 KB

Let's just make most of those changes now to keep on top of things.

berdir’s picture

File upload in the UI in core always happens *in the context of something* that control upload validation. File validation is configured on the field. Allowed extensions, size, ...

Somehow, we need to be able to respect that also in REST, otherwise you can upload whatever you want. file_entity does have a global file upload, but it also has permissions for that, list of allowed file extensions and so on. All of that is completely non-existent in core.

So IMHO, we need to limit file create/upload to the context of something, we could hardcode a path that contains entity_type/bundle/field/.. or we could try something more generic, but not sure how to do that.

Another idea would be to be able to upload raw binary data into a very temporary state that is *not* in any way accessible, and then you can reference that from the serializer and we create and validate the file entity at that point.

damiankloip’s picture

Good points @berdir, this is certainly something we need to think about, not sure how best to solve that at this point. I guess the trouble is that we need the file content before we can do anything with it, and allowing changing the extension etc.. could pose some security weaknesses?

This is maybe slightly different to file_entity in that case... You could currently upload anything you want for the file data BUT you cannot modify the file name or anything else about the file (entity). So maybe if that was the case, we don't need to validate anything on the upload path? Maybe just if the file entity field values are modified via REST? We could for example, validate the content-length is not above the max upload size or something? As it currently stands, you could upload any binary data you want, but you could not change file path or extension.

Or do you mean we need to think about this regular REST operations on the file entity? E.g. just to be allowed to create a file entity with x extension type.

damiankloip’s picture

StatusFileSize
new15.42 KB
new3.75 KB

WIP on some test coverage for the file data upload portion.

damiankloip’s picture

StatusFileSize
new15.56 KB

Trying to change the test to use BrowserTestBase like most REST tests do. The actual controller code seems to work ok, I am not sure what's going wrong with authenticating a user though. For now the access it just 'TRUE'.

jibran’s picture

Status: Needs work » Needs review
+++ b/core/modules/file/src/Controller/FileUploadController.php
@@ -0,0 +1,99 @@
+    $temp_file_name = $this->fileSystem->tempnam('temporary://', 'file');

Shouldn't this be some random name?

damiankloip’s picture

tempnam() creating a file with a unique name isn't enough?

Also, intentionally didn't mark it as needs review because we don't need the bot to run at this point...

jibran’s picture

tempnam() creating a file with a unique name isn't enough?

Yeah, that's fine. I forgot about that.

we don't need the bot to run at this point.

Sorry about that.

damiankloip’s picture

ok, and ok. I guess :)

damiankloip’s picture

It seems like the best way forward (IMO)may be to not provide this in the context of a node or parent entity (we could do this, but seems like the wrong direction - to try and bring in field level validation amongst other things, seems strange from a REST perspective). File entity adding it’s own endpoint for creating files seems like a good plan for core too. It could then just rely on a new file permission like ‘create file entities’ or something? We could then have a way to create the actual file entity. Then use the endpoint in this patch to post larger amounts of file data if needed? Should we allow smaller amounts of file data to be POSTed inline still (I vote no)?

To achieve this, (I think) we would need:

- The new file permission
- Adjustments to the FileAccessControlHandler to take this into account
- Provide our own REST resource implementation for File entities?
- A way (as berdir mentioned above) to configure a list of allowed file extensions

This would make files kind of a special case I guess, but that seems like a necessary evil if we want some functionality.

dagmar’s picture

Just to add some extra thoughs on this. The binary upload approach is something that is already implemented by other API services like contentful: https://www.contentful.com/developers/docs/references/content-management...

Another idea would be to be able to upload raw binary data into a very temporary state that is *not* in any way accessible, and then you can reference that from the serializer and we create and validate the file entity at that point.

This is what contentful does too.

From contentful docs:

If the association and processing steps are not executed successfully within 24 hours after uploading, the file and its metadata will expire and be deleted from the storage area.

garphy’s picture

TL;DR: My point is that we should have one single API endpoint which handle the file upload (using "real" PHP upload semantics) AND create a temp File entity in one single atomic request.


Good finding @dagmar ! The approach taken by Contentful is IMHO the right one and it's basically how Drupal is already handling file upload through HTML forms when using ajax uploads : the file is uploaded through ajax, stored temporarily and if the form itself is not successfully submitted within a certain time, theses temporary files are/can be deleted. But it's at a higher API level though : it's about creating a temporary File entity upon file upload (in one single request), then associate the File entity to node (or not).

As this issue is currently going, we end up with two options :
- create a temporary File entity which references no actual file, then upload a file by passing the fid in the request ;
- or, upload the file and get a "temporary upload id", then creating a File entity with this upload id.

But maybe these two requests approaches (one for creating a file entity and one for uploading) are equally flawed(ish) no matter the ordering.

Do we actually discussed the fact of having one single API endpoint which handle both the file upload (in a way we get streaming, progress, ...) and file entity creation (like the approach it was suggested in #252 and #258) ? It's not the route we're currently on, but I don't think we properly discussed and dismissed (if needed) this approach.

@Wim Leers, in #282, I don't get if you agree that the current approach needs two requests or if you consider it acceptable. IMHO, you couldn't combine those two requests, even using subrequests, in a way which allow for a streamed upload . I think that you would end up on the same issue again : base64 (or other) encoding, memory consumption, no upload progress, ...

I'd love everyone's feedback there :)

damiankloip’s picture

Well, the main thing the current approach is trying to implement is the binary upload of data part. The rest is up for discussion as far as I'm concerned. Once we have the binary upload working (it does now) the main concern is how we handle permissions around the file creation - this is the same problem to solve regardless of the direction we go in. We also still have the validation issue - but hopefully file validation can happen when the entity is validated and the file validators run at the field level for the file used. I.e. you could upload any file you like, then reference it on a node or something, but if it doesn't match the validation criteria for that field configuration the parent entity would fail validation.

If we go with the temporary file approach, how would you then switch the status of the file entity? You would still need another request I think? So then you could need 3 requests? Or would saving the entity after denormalization then update the file status as it has now been referenced? The other problem with that approach is how we deal with the file name? You would still need another request to rename it, even if we did give it a temporary name before the file is used? OR our endpoint could be /file/create/{filename} or something?

I don't think we should work with temporary file IDs, then create a file entity, I think we should always create the file entity, just with a status of FALSE if we're going down that route.

damiankloip’s picture

Status: Needs work » Needs review
StatusFileSize
new15.47 KB
new2.42 KB

Restoring the hal file entity normalizer so we can have to full file URL back instead.

ibustos’s picture

I can confirm #307 is working great. It would be neat if the response included the file being created/updated instead of just an "Ok" though. Submitting binary data is so much better than base 64. Quick question though, shouldn't this functionality be part of the Rest module instead of the File module?

ibustos’s picture

StatusFileSize
new15.81 KB

I was thinking something along the lines of this.

ibustos’s picture

StatusFileSize
new1.88 KB

Forgot to include interdiff.

Status: Needs review » Needs work

The last submitted patch, 309: 1927648-309.patch, failed testing.

damiankloip’s picture

Thanks for testing! Yes, returning the serialized file data makes sense too. I was thinking similar, just waiting to see if we want to create the file entity too, as per comment #306.

I was also going to move this to REST module I think, it started out in file module whilst I was scratching around. I think we need it in REST module, with the endpoint/route being added when file module is enabled (pretty much always).

damiankloip’s picture

+++ b/core/modules/file/src/Controller/FileUploadController.php
@@ -59,7 +65,9 @@ public function upload(Request $request, FileInterface $file) {
+    $normalized_file = $this->serializer->normalize($file);
+    return new Response($this->serializer->encode($normalized_file, 'json'));

Instead of this, we should just call serialize()

damiankloip’s picture

The only thing that was bugging me a bit (and why I just returned the string before) is its kind of weird to send a binary request and receive a json response? I guess that's an ok default.

Oh and..

Submitting binary data is so much better than base 64

YES!

damiankloip’s picture

Thinking about this some more today, I think this might be a good path to try and go down:

- /file/upload endpoint also creates files, but they are temporary only : To solve - how do we name the file (path parameter, query string, header)?
- Make sure FileItem validation can happen on save (of the parent entity, through constraints), this will allow us to arbitrarily use/reference our uploaded files, but then they could still be validated in the context of where they are being used as things like max file size, and extensions are field specific
- When the parent is saved (FileItem?) if it's referencing a file that is temporary, it needs to set the status for the file entity to make it a permanent file. Could also move file if needed at this point too?
- Create new permissions in file module specifically for uploading temporary files

So the flow would then be:

- Create new temporary file at /file/upload and store the file ID you get as a result
- Use this in a file reference field on a parent entity

Also, me and dawehner were talking, and agreed that it does makes sense to keep the FileUploadController and route definition in the file module, as it is not really implementing anything from REST module.

dabito’s picture

@damiankloip @ibustos

- /file/upload endpoint also creates files, but they are temporary only : To solve - how do we name the file (path parameter, query string, header)?

How do we deal with security here? Where would we get the constraints from for this arbitrary upload?

One solution is to add the entity/bundle/field information to the request, but I'm worried about tightly coupling to the field module.

Another solution is to pass constraints in the request itself, but this could create a sensible vulnerability.

Maybe add a config which defaults to a list of accepted mime types for all uploaded temp files?

berdir’s picture

Yes, I brought that up in #291 as well, some responses in #292 and #299.

I'm not sure yet. I think having good validation for this is absolutely critical and we need to be extra careful here. I get the point about not tying too much to entity/field, but REST is all about entity/field (validation) and field validation falls back to upload validators too.

Either we need to validate it there and upload in context of some field, or we need to ensure the validation happens while it is added to an entity *before* the file is made publicly available.

damiankloip’s picture

Yes, we need to be super careful here. The latter is the only sane way to go I think (I don't know a lot of these systems as well as you do). I am hoping we can only allow uploads of temporary files, they can then only be made into 'real' files once attached to a parent and referenced. If we could have file validation running when entities are saved (or just validated) via some constraints, I think you could E.g. attach a temp file via the ID to your article, then have it validate based on (E.g.) the image field configuration on that. Is that possible?

Currently a lot/all of the validation is just at the form level. If the file is only temporary, and validation happens like it does now. We should be able to utilise current mechanisms for validation of the actual file? Also, this is not going to be a publicly accessible endpoint. If we can, it would be good to keep any specific field knowledge out of these requests.

As berdir suggested a while ago, I think, we could do something similar to file entity module, the endpoint to allow uploads has a whitelist of extensions too. So it could be limited there, and further validated at the field level. It was discussed previously, and I think it's a good option.

dabito’s picture

@Berdir @damiankloip @ibustos

So we basically have the contextualized upload and the arbitrary upload. While the arbitrary upload is decoupled, it is way more insecure. Adding context to this upload process lets us ensure only validated files ever make it into the server.

With the contextualized upload we would require an owner, an entity/bundle and a field.

Entity/bundle could be part of the URL, so the endpoint URL could be /file/entity/bundle/field/upload

I would agree with Berdir's #291 since the above URL looks too long. So maybe just /entity/bundle/field and use PUT request?

Original file name could be passed using the content-disposition header (Do we need to change to multipart/form-data?).
Content-Disposition: file; filename="file1.txt";

damiankloip’s picture

Either way, we still don't have a proper way to validate the files being uploaded right now, as it's all done at the form layer. If we have a whitelist of allowed extensions, I don't think that would be way less secure? Seems like it could be ok.

How do you see the flow working (request wise) if we go down the field context route? Upload file data for /entity/bundle/field get your file ID, then make another request to use that file ID on your parent field reference field? You could still POST data for field X, then attach it to field Y once you have the file ID if you wanted to anyway (unless we move file validation as I mentioned in #318)?

I think Content-Disposition is a response header really?

working on some changes to the patch currently. Won't change too much until we settle on some of these other points.

ibustos’s picture

So the way it would work IMHO, would be as follows:
1. Have users POST to say /file/upload/{entity_type}/{bundle}/{field_name} while optionally specifying a file name (I liked the idea of using Content Dispoition).
2. All validation for that field should be done at this point for security purposes (we don't want users uploading .exe files and the like). Once we have the file we run all validations. If we need to save the file for this purpose, the file must be set to TEMPORARY.
3. Once the file has passed validation, create the File entity (and move it to PERMANENT?), if it doesn't pass, attempt to remove the file.
4. Since users using this functionality are must likely using rest or Jsonapi, in order to append the new file to a field, all they would have to do is just do another POST or PATCH request to that entity endpoint to assign a value for that field.

What are your thoughts on this?

ibustos’s picture

StatusFileSize
new17.72 KB
new5.72 KB

This patch adds an endpoint to create files. The filename is being retrieved from the header using @dabito's suggestion for the time being. Also there is no validation whatsoever yet.

dagmar’s picture

Status: Needs work » Needs review

Status: Needs review » Needs work

The last submitted patch, 322: 1927648-322.patch, failed testing.

damiankloip’s picture

But POSTING to an endpoint like /file/upload/{entity_type}/{bundle}/{field_name} will be fine, but then what's stopping you then using that file for another entity/field instead? Not much, I don't think. This is why I prefer the idea of having a whitelist of file extensions on the endpoint, then actual validation on the referenced file.

berdir’s picture

File/image field entity validation should cover everything that upload covers as well. See \Drupal\file\Plugin\Validation\Constraint\FileValidationConstraintValidator

The problem is to figure out a way to actually validate a file while it's still in temporary:// and *after* that move it to public. That is however actually quite complicated, as we have no such logic right now, we'd need to add something, like a preSave() in FileItem, but based on what should it act?

The advantage of uploading for a specific field is that we don't have to worry about temporary:// IMHO. Because we can just create it as a normal public:// (or whatever is configured on that field) temporary (the status, not the location) file, just like when you upload in the UI. It would also fix the permission problem, because we can easily check edit access to that specific field for that node type. That means you can really only upload files if there's at least one file/image field on an entity type/bundle that you are allowed to edit.

damiankloip’s picture

Nice, thanks berdir. Missed that validator! That is good news.

Yes, it's the moving of the file that could be really problematic indeed. So we should still have an endpoint but the access can check field access for the user instead. I guess you could still just use the file somewhere else, but that doesn't seem to bad, as long as it's valid (which seems will be taken care of already).

@berdir, do you have any preferences about the how we allocate a file name during the upload to this endpoint? As mentioned in #320, it seems the content-disposition header is not really a good choice, as that's really meant to be a response header only.

wim leers’s picture

I've caught up on this issue starting at #291, i.e. from around the time I last reviewed this issue. This issue was re-kick-started around the end of DrupalCon Baltimore (which was April 24–28, 2017, comment #294 was on April 25, 2017), and after that I was on vacation.

I commented on the comments where I think I can add something meaningful, express my agreement, or disagreement.


#304: thanks for pointing at the Contentful example. That's another indicator we should take this approach. Twitter does something similar: https://dev.twitter.com/rest/reference/post/media/upload — and it explicitly returns the number of seconds until it expires. Although it also supports multi-step uploads:://dev.twitter.com/rest/reference/post/media/upload-init.html + https://dev.twitter.com/rest/reference/post/media/upload-append + https://dev.twitter.com/rest/reference/post/media/upload-finalize — for a concrete example of how to use that, see https://blog.twitter.com/2015/rest-api-now-supports-native-video-upload.


#305: I think two requests is acceptable. You're right that my suggestion in #282 was misguided, because when using https://www.drupal.org/project/subrequests, you wouldn't be able to use streamed uploads. So, +1 for two separate requests: one for uploading a file, another for associating it. Or, if uploading many files, N+1 requests: N file upload requests, 1 request to associate them all with the same entity.


#306:

I.e. you could upload any file you like, then reference it on a node or something, but if it doesn't match the validation criteria for that field configuration the parent entity would fail validation.

+1

If we go with the temporary file approach, how would you then switch the status of the file entity?

I think the act of referencing the temporary file from an entity's file field would cause that file to be marked permanent.

Or would saving the entity after denormalization then update the file status as it has now been referenced?

Yes.

The other problem with that approach is how we deal with the file name? You would still need another request to rename it, even if we did give it a temporary name before the file is used? OR our endpoint could be /file/create/{filename} or something?

I think the "upload binary file" request should indeed specify the filename. How exactly, I don't know. Possibilities are:

  1. URL path part (this is what Dropbox does: https://www.dropbox.com/developers-v1/core/docs#files_put
  2. request header
  3. URL query argument.

I don't think we should work with temporary file IDs, then create a file entity, I think we should always create the file entity, just with a status of FALSE if we're going down that route.

AFAICT that'd be okay. But then we'd still need to specify the file name in that request.

The advantage of a separate endpoint for just "raw uploads" would be that we could have more strict (and better tested) clean-up procedures for unused temporary files. But if we feel that this already is sufficiently reliable, well-tested, and so on, then I agree that ideally we'd just reuse the "temporary file while uploaded and made permanent when referenced" strategy that core has already been using for file uploads for many years.


#312:

I was also going to move this to REST module I think, it started out in file module whilst I was scratching around. I think we need it in REST module, with the endpoint/route being added when file module is enabled (pretty much always).

I don't understand this. If it's specifically tied to File entities, then why should it not be provided by the file module?
EDIT: hah, you already came to this conclusion after talking to @dawehner in #315 :)


#315++


#316 + #317 + #318: RE: security. I think this is the answer:

we need to ensure the validation happens while it is added to an entity *before* the file is made publicly available.

… which is to say, this is once again just the "normal" field validation constraints being applied: when saving e.g. a Node that has a file field, then this is what happens:

  1. Upload a file using this new binary file upload resource, which returns a file ID, and stores the file in the temporary:// stream wrapper
  2. POST (or PATCH) a Node that lets the file field reference the file ID from point 1
  3. During this POSTing (or PATCHing), the entity is validated, which causes each modified field to be validated, which includes the file field.
    The file fields' validation constraints run, which are identical to the validation that is ran during file uploads for the "regular form file upload widget".

We should be able to utilise current mechanisms for validation of the actual file?

We should indeed. If we can't, then as part of this issue (or as a blocker of this issue), we should refactor the existing form-tied logic into proper Field Validation Constraints. I'm quite hopeful though, because \Drupal\file\Plugin\Field\FieldType\FileItem has this:

 * @FieldType(
 *   id = "file",
 *   label = @Translation("File"),
 *   description = @Translation("This field stores the ID of a file as an integer value."),
…
 *   constraints = {"ReferenceAccess" = {}, "FileValidation" = {}}
 * )

and \Drupal\file\Plugin\Validation\Constraint\FileValidationConstraintValidator has this:

    $validators = $value->getUploadValidators();

#319: I honestly don't yet see why we'd want/need to tie this to an entity/bundle and a field. What matters, is that a certain file field is allowed to use a particular file. Therefore the validation of the entity that contains the file field is what/when/where the validation should happen.


#326:

File/image field entity validation should cover everything that upload covers as well. See \Drupal\file\Plugin\Validation\Constraint\FileValidationConstraintValidator

Yay, glad to see what I wrote confirmed :)

The problem is to figure out a way to actually validate a file while it's still in temporary:// and *after* that move it to public.

Why is this a problem? Surely we already need to do this for our form-based file uploads too?

That is however actually quite complicated, as we have no such logic right now, we'd need to add something, like a preSave() in FileItem, but based on what should it act?

Apparently we don't?! :O :O :O

The advantage of uploading for a specific field is that we don't have to worry about temporary:// IMHO. Because we can just create it as a normal public:// (or whatever is configured on that field) temporary (the status, not the location) file, just like when you upload in the UI.

… but … that can easily cause you to end up with lots and lots of unused "permanent" files? And it's also semantically wrong/strange/confusing?

Also, the downside is that you have to somehow figure out the particular magical URL for file uploads for every single field + entity type + bundle combination… which is a bad DX.

WRT the Content-Disposition header: it seems it's actually allowed for POST requests when uploading files, see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Dispos.... For a concrete example of how that can be implemented for a REST API, see https://github.com/SparkPay/rest-api/blob/master/uploading_files.md.

Note that another consequence of using multipart request bodies is that we can support uploading multiple files in a single request. But I don't know if it's compatible with application/octet-stream?

berdir’s picture

There are two different kinds of "temporary" concepts. One is the *status* of the file entity. A freshly uploaded file with status temporary is still in public:// and publicly available. Otherwise we wouldn't be able to show that file or e.g. a thumbnail of it.

The other is a file that is in the internal, non-public/accessible temporary:// stream-wrapper/directory. We have no support to automatically move those files. We would just make it a permanent file (file that is not being deleted after 6h) but it's still in the temporary folder and not publicly accessible.

That's why my suggestion is to enforce the upload in context of a field, because then it is the same as upload in the UI. We can make it a public, temporary file that will get deleted after 6h (we don't even need to do anything custom for that), and actually referencing it will make it permanent.

damiankloip’s picture

StatusFileSize
new19.46 KB
new14.11 KB

OK, spoke to Wim and Berdir in IRC for a while and here are some more changes, we all agreed in the end that keeping in bundle and field specific is the way to go. So we can then just create the file in place and don't need to worry about moving it etc... This is still a WIP, just ran out of time today.

This also converts things back to one route/endpoint that can create files, no update only endpoint, as well as converting to a rest resource instead. We should then get serialization handled for us.

There are still some todos, we need the validation in place based on the field definition being used, amongst other things.

damiankloip’s picture

StatusFileSize
new19.54 KB
new1.28 KB

And a couple of other fixes.

wim leers’s picture

we all agreed in the end that keeping in bundle and field specific is the way to go.

I did not agree with that.

The uploading should be agnostic of entity type/bundle/field. Then when you reference the uploaded file from a file field, the necessary validation should run.

damiankloip’s picture

ok, I thought that we agreed it made sense for now to go with entity/bundle/field as we can place the file in it's actual location - like file uploads happen in the UI. If you want to try and also solve how we would move files when they are referenced, let me know :) Otherwise, all uploads would have to just sit in public:// or something. That said, I would still be happy to just upload temp (status = 0) files to public that would be either referenced and made permanent, or deleted on cron runs.

Ideally that would be the case, but I'm not sure we really have much of anything in place to validate and move temporary files when a parent is saved.

berdir’s picture

The uploading should be agnostic of entity type/bundle/field. Then when you reference the uploaded file from a file field, the necessary validation should run.

No, uploading is not agnostic of that. There's not just validation, the field also controls where the file is stored (private:// or public://, but it could also be on amazon s3 through flysystem module or who knows where) as well as the folder (media/YYYY/MM for example).

It is important that we respect that configuration. if this happens to be your CV that you upload to a job application form the you don't want that to end up somewhere public :)

And doing all those things *only* when attaching to an entity is complicated, it is possible to re-use files in multiple places even if those settings are not the same (they only matter for the initial upload), so we would need to figure out when to move and when not to move files and so on.

This is a first step and it reflects how file uploading works in drupal core. After that, we can think about improving security, also for the UI (which is a lot harder than you might think and there's a reason it wasn't done yet), there are ways to integrate better with media enties so you don't need 3 requests there, and contrib modules like file_entity could offer a standalone upload resource. And a module like flysystem_s3 could even offer a REST resource to create a file that was directly uploaded to S3, it already has ajax integration for that.

dabito’s picture

Great job with the rest resource, I really like that strategy. I've two comments:

1) If we're going the Content-Disposition way for filename, we should be using multipart as that's the only way the header should be used in requests. Also there must be a better way to obtain the attribute other than the regex, as the todo mentions.

2) Regarding the agnostic upload, what do you guys think of the quarantined file strategy mentioned earlier in the thread? The full approach could look like this:
- single upload endpoint is requested *
- if it has context then follows the existing path *
- if there is no context, though, it could create a quarantined file entity which is hidden (random or hashed filename located in tmp)
- this file entity can be the upgraded to a permanent file managed entity upon parent entity create / update
- a cron job will delete all quarantined files older than 1 hr
* what we have so far

damiankloip’s picture

Thanks :) I think the rest resource could be a good way to go. It's currently a bit strange with how REST handles allowed content types though... so we might need to look at that a bit more.

1. I don't think we should go with that header, I just left it in for now, along with the todo. I think we should just go with something else that only consists of a filename for now. I think we should stick to the single file per request for now, we need the simplest implementation in IMO, so parking for now is multipart is sensible.

2. I think that might be easier said than done. I'm not sure we can move files like that when a parent is updated at this stage. It get's tricky, and nothing like that currently exists in core. If it was a random or hashed filename, renaming it on parent create is also something that in no way exists. So we would need a way to rename it again, or you need to add another field to file entities.

kylebrowning’s picture

I agree, do single requests for now, this is the last feature IMO for a fully backend capable Drupal. Keep it simple we can always add more later.

Just my opinion though.

garphy’s picture

Basically, Drupal core currently offers no way (through UI) to manage file entities directly. Everything is done through File/Image fields.

So we stuck with either :

  • implement an upload endpoint which need contextual info about the targeted field, or
  • allow the creation of a file entity on its own (but it's a wider scope than just adding an API endpoint)

Providing contextual info about the field seems fragile, as already noted : nothing would prevent from referencing the created file from another entity/field. Still, I do not see clearly how we would manage the temporary/permanent status when POSTing the entity referencing the file.

Allowing the creation of a file entity
is another can of worms but it's probably the best way ultimately. It's basically what the media initiative is working on but I did not manage to find an existing issue on the specific topic of adding a new file to Drupal (adding a "new file" button to admin/content/file).

In the end, we have to face that files are entity as nodes are, we cannot keep on handling it like a piece of content which only belong to one entity like text fields. We have to decouple the lifecycle of a file entity from the lifecycle of its referencing entity (and figure out what happens to validation).

(BTW, the issue title seems pretty outdated now :)

garphy’s picture

Well, per #2825215: Media initiative: Roadmap (if it lands), it seems that we are gonna deprecate File/Image field in favor of using the Media entity type with plain Entityreference fields.

So maybe it's better to focus our effort on "how to upload a file which will create a Media entity referencing a File entity" ?

damiankloip’s picture

I think still being able to upload a file is useful too. Media can then build on that (discussed briefly with Berdir yesterday).

The trouble with the standalone file upload endpoint (which I would prefer too) is that we have to decide where the file will live, so we could default to public:// for example, but then a field could be using a completely different storage location (or even different backend, like s3), and moving the file when the parent is saved seems like a no-go for now. Otherwise, as mentioned up somewhere, we just have a whitelist of file extensions (or maybe just validators that run) for file uploads. Then they can be used, and the parent entity would fail validation if the file did not meet the validation criteria.

Ideally files would be more standalone. but Drupal just doesn't seem to be there just yet. So it seems sensible to go with Berdir's suggestion for now, and implement something more akin to how file uploads work in the UI.

wim leers’s picture

#333: I agree that it's better to have entity type/bundle/field-bound file uploads is better than not having at all. The problem with that though is that we have to support it forever, if we commit it and ship it in 8.4.0. I do definitely think that it's a great first step for this patch, I'm only saying we should very carefully considering the implications of shipping a Drupal release that supports it. But let's worry about that later.

#334: Ugh, I totally forgot about where it may be stored (which can be anywhere when using flysystem). Which suggests that we'll need to ship with it working in the way as described in #330 anyway :(

#335: in #330, the context is provided via the URL path. "Contextless file uploads" are therefore impossible in that approach. And per #334, it seems like it's highly undesirable/unsupportable/will require massive rearchitecting to support contextless file uploads.

#337: Keep it simple we can always add more later. This is the guiding principle for me in my comments. And I was under the impression that contextless file uploads would be simpler, but #334 explains convincingly why that's not.

#338: I very much agree that files with context are fragile, because if a file is uploaded via an image field to a particular file directory and then later referenced from a file field that by default uses a different file directory… then it's still inconsistent in the end. But that's a problem we already have, and would require massive rearchitecting to fix/make clearer. We can't fix that here.

All in all, #340 captures it well:

Ideally files would be more standalone. but Drupal just doesn't seem to be there just yet. So it seems sensible to go with Berdir's suggestion for now, and implement something more akin to how file uploads work in the UI.

We all would like to have contextless/standalone files… but we can't, because that's not how it's been designed to work, and we can't/shouldn't block REST-based file uploads on resolving that architectural problem, because it will likely take years.

So, let's move forward with #331! Any reason why that's not marked as "needs review"? I'm curious what testbot has to say!

damiankloip’s picture

I didn't trigger the bot just yet as the FileUploadTest I wrote for the previous behaviour has not yet been converted to account for this now being a rest resource. I'll take a look at that. I'm not sure it makes sense to extend ResourceTestBase, it provides a lot of stuff we don't really want to use here I think?

wim leers’s picture

#2869387: Subclasses of ResourceTestBase for non-entity resources are required to add pointless code cleans up ResourceTestBase quite a bit. Once that's in, why wouldn't you want to base test coverage off of that test? The current test coverage is not yet testing 4xx responses (except for one 415, but with insufficient rigor). It's also not yet testing in multiple formats.

Very high-level, very rough review below:

  1. +++ b/core/modules/field/tests/src/Kernel/EntityReference/EntityReferenceItemTest.php
    @@ -524,6 +524,7 @@ public function testAutocreateValidation() {
    +      'uid' => 1,
    
    +++ b/core/modules/file/src/Entity/File.php
    @@ -234,6 +234,7 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
    +      ->setDefaultValueCallback('\Drupal\file\Entity\File::getCurrentUserId')
    
    @@ -274,4 +275,16 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
    +  /**
    +   * Default value callback for 'uid' base field definition.
    +   *
    +   * @see \Drupal\file\Entity\File::baseFieldDefinitions::baseFieldDefinitions().
    +   *
    +   * @return array
    +   *   An array of default values.
    +   */
    +  public static function getCurrentUserId() {
    +    return array(\Drupal::currentUser()->id());
    +  }
    

    Should we move this into a separate issue? This looks like a simple enough change that can go in independently?

  2. +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
    @@ -0,0 +1,227 @@
    + *     "canonical" = "/file/upload/{entity_type_id}/{bundle}/{field_name}",
    

    Does it even make sense to have a canonical link relation for this? This REST resource plugin only provides a post() method after all…

  3. +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
    @@ -0,0 +1,227 @@
    +  public function calculateDependencies() {
    +    return [
    +      'module' => ['file']
    +    ];
    +  }
    

    This should not be necessary, because this plugin is provided by this module, hence this would be calculated automatically?

  4. +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
    @@ -0,0 +1,227 @@
    +    $field_definitions = $this->entityFieldManager->getFieldDefinitions($entity_type_id, $bundle);
    +
    +    if (!isset($field_definitions[$field_name])) {
    +      throw new BadRequestHttpException(sprintf('Field "%s" does not exist', $field_name));
    +    }
    +
    +    // @todo check the definition is a file field.
    +    $field_definition = $field_definitions[$field_name];
    +
    +    // Check access.
    +    if (!$field_definition->access('create')) {
    +      throw new AccessDeniedException(sprintf('Access denied for field "%s"', $field_name));
    +    }
    

    I think all this would benefit from being moved into a helper method. All of this is related to access checking.

  5. +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
    @@ -0,0 +1,227 @@
    +  protected function validateOctetStream(Request $request) {
    

    Nit: The name suggests this is validating far more than just the Content-Type request header.

    Also: can be static.

  6. +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
    @@ -0,0 +1,227 @@
    +   * Determines the URI for a file field.
    ...
    +  protected function getUploadLocation(array $settings) {
    

    Nit: mismatch.

    Also: can be static.

damiankloip’s picture

StatusFileSize
new23.02 KB
new6.81 KB

Here is a bit more work on the actual resource validation. I'll wait on the test conversion to see which is the best class to extend from. I have copied the logic from FileItem::getUploadValidators, and maybe a couple of things from file_save_upload. I think maybe we should extract some of that logic into separate functions that are used internally by file_save_upload.

damiankloip’s picture

I didn't know about that issue, if it makes less assumptions about all the things a resource should test, then I have no issues using that. However, it looks like it doesn't really change too much? I'm not sure what benefit we will get from testing things like different formats, they should all fail? unless we use the accept header to determine the format of the response? Would be nice if it could test 403 responses etc.. for us. Seems like a lot of test duplication though. If it's a rest resource don't we already know that it will be protected by a permission?

garphy’s picture

We all would like to have contextless/standalone files… but we can't, because that's not how it's been designed to work, and we can't/shouldn't block REST-based file uploads on resolving that architectural problem, because it will likely take years.

The issue may be that exposing an upload feature as a REST resource of file entities feels like it provides contextless upload.

We could also postpone this until some bits from the media initiative lands in code (specifically the media entity) and provide upload a this level, as the media entity will definitively expose standalone media (including local files) management.

It's just a thought but it may be more relevant than shipping an upload feature bound to file/image field context when we can anticipate that those may be deprecated in favor of "plain" entity reference field targeting media entities.

Or we keep going with the current approach and we'll add a contextless upload alongside media entity later, but we'll have to support both methods for a while then.

wim leers’s picture

We could also postpone this until some bits from the media initiative lands in code (specifically the media entity) and provide upload a this level, as the media entity will definitively expose standalone media (including local files) management.

We should not wait for Media.

berdir’s picture

We could also postpone this until some bits from the media initiative lands
in code (specifically the media entity) and provide upload a this level, as
the media entity will definitively expose standalone media (including local
files) management.

No, it actually doesn't.

An image media bundle is 100% identical to a node type with an image field. It's an entity with an image field. The field controls where the file is saved, validation settings and so on.

So with this approach, what you will need to do to create an article with a media element is:
1. Upload the file in context of media bundle image field.
2. Create the media entity referencing the file.
3. Create the node that references the media entity.

Yes, complicated, but it will also just work. As I already suggested earlier, what we could do is abstract part of that complexity and allow to merge step 1 and 2 together, so that you can directly create a media entity with binary data (you still need to provide the bundle, as you can have multiple image media bundles with different settings). But that's optional and there are quite a few things that still need to be answered there:
* Media entities can have all kinds of metadata, some of that could even be required, so how would you submit that?
* media entities could support multiple images in the same bundle (could be a gallery, or could be different versions of the same file or so)

So it would not replace this, it would just build upon it and it would probably not work for all sites/use cases. This however should.

wim leers’s picture

I'm not sure what benefit we will get from testing things like different formats, they should all fail?

Because in case of file uploads, we just have binary data in the request body, not hal_json or json? Does that mean this is the first request body format-agnostic REST resource in core?

unless we use the accept header to determine the format of the response?

Right, we do normalize the created File entity in the response, which means it's not actually format-agnostic. The request body is, the response body is not.
The request body format is determined by the Content-Type request header, the response body format is determined by the ?_format URL query argument. So I'm not sure I understand what you're getting at?

If it's a rest resource don't we already know that it will be protected by a permission?

No, only REST resource plugins that do not override the default \Drupal\rest\Plugin\ResourceBase::permissions() implementation are guaranteed to have a permission. REST resource plugins that override this can use whichever access control mechanism they like.
For example, \Drupal\rest\Plugin\rest\resource\EntityResource::permissions() overrides it so that it can choose to rely on Entity Access instead. The REST resource being introduced here should do so too.

damiankloip’s picture

StatusFileSize
new22.15 KB
new9.02 KB

OK, here is a new patch with mostly changes from comments in #343:

  1. Reverted that. Should we also revert the change in the access control handler too?
  2. I used the canonical resource in the annotation too so it shows in a couple of places (and rest UI) - it seems canonical URI is used in these kinds of contexts?
  3. Removed
  4. Moved into a validateAndLoadFieldDefinition() method
  5. Renamed to validateOctetStreamContentType()
  6. This method has the token dependency properly now, so probably best to leave it as non-static IMO. I'm not sure I'm a fan of making methods static on a class that is usually an instance. You want to signify it doesn't really have any dependencies?

So, regarding the request and response content type; I guess it makes sense to allow a ?_format to be used, it's just kind of weird as the Content-Type would still have to be application/octet-stream but this also makes sense. Ideally we would just have an Accept header but ... :) So guess it's a strange one, for the actual request we don't really care about any serialization formats, but response we do.

We still need to look at the tests, to convert my initial test coverage to some kind of REST based test. @WimLeers, can you give me some pointers here. I'm not sure if it's best to extend ResourceTestBase or not? We need to set up a file field on a test entity, users with perms to create a parent entity. The requests need to use the method I already have to do a proper request to send the data, but this is already pretty close to what's in ResourceTestBase, so I don't think this should be a problem.

wim leers’s picture

#350:

  1. Eh… don't you need those changes for this patch to work? Or perhaps it was a remnant from an earlier iteration?

    Similarly, if this patch can do without the changes made in FileAccessControlHandler, then that'd be splendid, yes. But I suspect we'll eventually need some changes.

  2. The REST UI module has some significant flaws/bugs that it needs to fix; we shouldn't provide a link relation type URI path if we don't use it! What other places is this relevant for?
  3. Yay!
  4. Yay!
  5. Yay!
  6. Ah, yes, then leave it. And yes, that's what I want to signify. See http://drupal4hu.com/node/416.html.

I don't see what's weird about sending a request with the request body in one format and the response body being in another format, a format that you can specify? Yes, it's uncommon, because 99% of the time, the format of the request body and response body match. But in this case, that wouldn't make sense, would it? Getting a application/octet-stream response body wouldn't actually be able to convey information in a very useful way :D
So this is a place where it's natural for the formats of the request body and response body to differ.
Hopefully you're nodding along while reading this — if not, please elaborate!

On the subject of tests:

  1. Yes, I do think it's desirable to extend ResourceTestBase, because that'll make it easy to create tests in json and hal_json formats and for different authentication providers.
  2. Concretely: implement FileUploadResourceTestBase extends ResourceTestBase, then implement FileUploadJsonAnonTest, FileUploadJsonBasicAuthTest, FileUploadJsonCookieTest and FileUploadHalJsonAnonTest, FileUploadHalJsonBasicAuthTest, FileUploadHalJsonCookieTest. With all of those 6 extending FileUploadResourceTestBase.
  3. Those 6 classes can then each be very thin: see RoleJsonAnonTest + RoleJsonBasicAuthTest + RoleJsonCookieTest as an example.
  4. In a secondary phase (once the above is done, and you have the existing basic test coverage ported to ResourceTestBase and working for all the formats + authentication provider combinations), probably look at EntityResourceTestBase::testPost() to see what kinds of edge cases you want to test. I'm happy to do that for you when we reach that point.
slashrsm’s picture

I agree with @Berdir on that. As Media entity is adopted in core files will become even more hidden part of the system. When the switch is made to fully use media entity files won't appear outside of its scope any more. That said, I think that we need to treat them as such.

As I already suggested earlier, what we could do is abstract part of that complexity and allow to merge step 1 and 2 together, so that you can directly create a media entity with binary data (you still need to provide the bundle, as you can have multiple image media bundles with different settings).

This would make a lot of sense IMO.

damiankloip’s picture

RE #351:

1. So that was needed before as the file entity was created more inline. Now we are controlling things as we have a resource that handles the creation of the file and its data only. So as the patch now does, we just pass the uid from the current user that is injected into the resource.
2. I can't remember where else I thought this might be a problem now, I'll just remove the canonical and we'll see how things go.

Yes, it's only weird really because we use _format as a replacement for content-type and accept depending on the case. Otherwise, it would totally not be weird. So this is just Drupal... otherwise, yes, I'm nodding along :)

Working on the tests now. I'll see how far I get.

dawehner’s picture

  1. +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
    @@ -0,0 +1,341 @@
    +      'status' => 0,
    

    INHO we should add an explanation why we use status: 0

  2. +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
    @@ -0,0 +1,341 @@
    +    return new ModifiedResourceResponse($file, 201);
    

    Can we add the file ID as location header, just like we do in \Drupal\rest\Plugin\rest\resource\EntityResource::post

  3. +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
    @@ -0,0 +1,341 @@
    +      // Move the file to the correct location based on the file entity,
    +      // replacing any existing file.
    +      if (!file_unmanaged_move($temp_file_name, $destination_uri, FILE_EXISTS_REPLACE)) {
    

    Does that mean some user can override the file of some other user? I guess this is not the case, because tempnam will generate a unique filename anyway? Do we need FILE_EXISTS_REPLACE then?

  4. +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
    @@ -0,0 +1,341 @@
    +    // Check access.
    +    if (!$field_definition->access('create')) {
    +      throw new AccessDeniedException(sprintf('Access denied for field "%s"', $field_name));
    +    }
    

    Mh, are we sure we don't want to throw a 404 exception? This exception is not implementing \Symfony\Component\HttpKernel\Exception\HttpExceptionInterface

  5. +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
    @@ -0,0 +1,341 @@
    +    if (!empty($errors)) {
    +      $message = "Unprocessable Entity: file validation failed.\n";
    +      $message .= implode("\n", $errors);
    +
    +      throw new UnprocessableEntityHttpException($message);
    +    }
    

    We should add a todo for #1916302: RFC 7807: "Problem Details for HTTP APIs" — serve REST error responses as application/problem+json, so we actually produce machine readable errors at some point.

  6. +++ b/core/modules/hal/src/Normalizer/FileEntityNormalizer.php
    @@ -55,16 +44,4 @@ public function normalize($entity, $format = NULL, array $context = []) {
     
    -  /**
    -   * {@inheritdoc}
    -   */
    -  public function denormalize($data, $class, $format = NULL, array $context = []) {
    -    $file_data = (string) $this->httpClient->get($data['uri'][0]['value'])->getBody();
    -
    -    $path = 'temporary://' . drupal_basename($data['uri'][0]['value']);
    -    $data['uri'] = file_unmanaged_save_data($file_data, $path);
    -
    -    return $this->entityManager->getStorage('file')->create($data);
    -  }
    -
    

    Isn't that a BC break when we remove this functionality?

damiankloip’s picture

StatusFileSize
new26.57 KB
new12.39 KB

Here is a WIP start on the test coverage. Aside from needing to be tidied up and implemented properly (inc some more edge cases etc..) the first main blocker (after a little debugging) is that ContentTypeHeaderMatcher matches the '_content_type_format' requirement that is added to all REST routes, so our route is then blocked out I think. So we actually get a 415 back in that case. I am also still getting a 403 for the first case, so maybe I am doing something wrong with setting up the test user possibly..

damiankloip’s picture

StatusFileSize
new26.85 KB
new2.69 KB

Daniel!

1. We could just remove this, the default behaviour of a File is to start with status as FALSE.
2. Yeah! added. do we want to use url() like I have done for files, or should we use the same code (pretty much) that EntityResource has. I guess the question is do we like to the file entity or the file itself? I don't think file entities implement the templates needed for that to work.
3. Yeah, we wanted this before when it was only overwriting/updating but don't want it now!
4. Changed to AccessDeniedHttpException - it was meant to be that anyway - good catch
5. What should the @todo say? I'm not sure.
6. Meh. Well, that functionality doesn't really work anyway, it only saves files in temp. I would be very surprised if this was being used, and I think we should definitely not support it either. It has some of the same failings that is a big reason we cannot just have a generic upload endpoint.

Made some of those changes now quickly to keep things up to date with reviews.

wim leers’s picture

#353:

  1. Ok!
  2. Great :)

yes, I'm nodding along :)

:)


#354.2++ — see \Drupal\rest\Plugin\rest\resource\EntityResource::post() for code to copy/paste :)
#354.5: agreed, but seems kinda out of scope here; this is a pre-existing problem. It's okay to be explicit, but at the same time it's not guaranteed this will end up being the solution (it is likely though).


#355:

.) the first main blocker (after a little debugging) is that ContentTypeHeaderMatcher matches the '_content_type_format' requirement that is added to all REST routes, so our route is then blocked out I think. So we actually get a 415 back in that case.

Oh that's interesting! I think it's fair to say that file uploads are the very very very rare exception to the rule. Indeed, file uploads don't want the json, hal_json or whatever formats. You want the application/octet-stream MIME type… for which no format has been defined yet! \Symfony\Component\HttpFoundation\Request::initializeFormats() also doesn't define it.
So, we'll want the File module (or perhaps core) to define a format for it (name TBD, but probably something like 'binary' => ['application/octet-stream']?). Then we have two options:

  1. Either we want a RoutingEvents::ALTER subscriber whose priority is lower than \Drupal\rest\Routing\ResourceRoutes, so that it runs later, and can set $route->setRequirement('_content_type_format', 'binary').
  2. Or we want to update \Drupal\rest\Routing\ResourceRoutes::getRoutesForResourceConfig() so that it doesn't set a _content_type_format requirement if and only if such a route requirement already exists. This then allows us to implement FileUploadResource::getBaseRouteRequirements(), so that we can specify '_content_type_format' => 'binary' there.

I like option 2. Because it puts the control with the @RestResource plugin, and therefore allows self-contained implementations, rather than requiring additional event subscribers to be set up.

damiankloip’s picture

Yes, I totally prefer options 2 also :)

damiankloip’s picture

StatusFileSize
new31.65 KB
new6.66 KB

OK, a few more fixes and a few more problems :):

- \Drupal\rest\EventSubscriber\ResourceResponseSubscriber::getResponseFormat() uses the '_content_type_format' for acceptable requirements only, for responses. This doesn't quite fit our use case here. I have added our service provider and other changes to the rest route building to do what was suggested in #357. However, when we get to the response, we have issues. We can't the use the format (or default to JSON?) but the only acceptable type is 'bin', as that is specified in the '_content_type_format'. Not sure of the best fix yet, but the assumptions in this code on't seem 100% correct, shouldn't we be checking either the _format or the Accept header or something? Otherwise we can never accommodate the valid case we mentioned above for this; POSTing in binary but receiving the serialized file entity in JSON etc.. in the response. Thoughts on this?
- In order to not have the request content normalized for us, we need a new controller method to handle the request, so I made a new handleRaw method for now. This is just c/p from handle() but with the normalization removed. So if we like this idea, we can refactor/tidy it up a bit. Otherwise, we will need a way for the plugin to tell the controller method that it does not need to be serialized. Could be something on the annotation or something.
- My test is failing on the field 'create' access check, not sure why yet. Need to look into this. Probably insufficient perms granted for the test user...

On the plus side, we can remove the custom validation for the application/octet-stream content type as the '_content_type_format' access check can cover that for us now.

damiankloip’s picture

+++ b/core/modules/file/src/FileServiceProvider.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace Drupal\file;
+
+
+use Drupal\Core\DependencyInjection\ContainerBuilder;
+use Drupal\Core\DependencyInjection\ServiceModifierInterface;
+use Drupal\Core\StackMiddleware\NegotiationMiddleware;
+
+/**
+ * Adds 'application/octet-stream' as a known (bin) format.
+ */
+class FileServiceProvider implements ServiceModifierInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function alter(ContainerBuilder $container) {
+    if ($container->has('http_middleware.negotiation') && is_a($container->getDefinition('http_middleware.negotiation')->getClass(), NegotiationMiddleware::class, TRUE)) {
+      $container->getDefinition('http_middleware.negotiation')->addMethodCall('registerFormat', ['bin', ['application/octet-stream']]);
+    }
+  }
+
+}

Sorry, somehow I added this file to the commit for my previous patch and not in my latest interdiff! This also got added.

wim leers’s picture

However, when we get to the response, we have issues. We can't the use the format (or default to JSON?) but the only acceptable type is 'bin', as that is specified in the '_content_type_format'. Not sure of the best fix yet, but the assumptions in this code on't seem 100% correct, shouldn't we be checking either the _format or the Accept header or something? Otherwise we can never accommodate the valid case we mentioned above for this; POSTing in binary but receiving the serialized file entity in JSON etc.. in the response. Thoughts on this?

That's because your POSTing to /file/upload/entity_test/entity_test/field_rest_file_test and not to <code>/file/upload/entity_test/entity_test/field_rest_file_test?_format=json — i.e. you're not yet specifying the acceptable response format! :)
Fixing that causes the response to not be HTML, but json:

-    $uri = Url::fromUri('base:' . static::$postUri);
+    $uri = Url::fromUri('base:' . static::$postUri, ['query' => ['_format' => static::$format]]);
Otherwise, we will need a way for the plugin to tell the controller method that it does not need to be serialized. Could be something on the annotation or something.

What about checking if the format is bin? So:

  $format = $request->getContentType();

+ if ($format === 'bin) {
+   // don't decode nor denormalize…
+ }
+ else {
+   // current decode + denormalize logic…
+ }
On the plus side, we can remove the custom validation for the application/octet-stream content type as the '_content_type_format' access check can cover that for us now.

:)

damiankloip’s picture

Right, But I tried it adding format to the query too, and it seems the same problem happens (forget about access for now): Serialization for the format bin is not supported ...

Which I think is being enforced due to this in \Drupal\rest\EventSubscriber\ResourceResponseSubscriber::getResponseFormat:

$acceptable_request_formats = $route->hasRequirement('_format') ? explode('|', $route->getRequirement('_format')) : [];
    $acceptable_content_type_formats = $route->hasRequirement('_content_type_format') ? explode('|', $route->getRequirement('_content_type_format')) : [];
    $acceptable_formats = $request->isMethodCacheable() ? $acceptable_request_formats : $acceptable_content_type_formats;

So the acceptable formats will use the content type formats, as it is a POST method, but, REST resource plugins only add a _format requirement on GET and HEAD routes, so the acceptable request formats would not be populated for our route yet anyway.

I'm not sure about checking the 'bin' format explicitly in the ResourceHandler itself, then we kind of couple a format defined in the file module to the REST module?

I'm not sure what I'm doing wrong with the access check, I thought if the permission to create the entity was correct it should be ok, but maybe the access checking is not correct?

wim leers’s picture

So the acceptable formats will use the content type formats, as it is a POST method, but, REST resource plugins only add a _format requirement on GET and HEAD routes, so the acceptable request formats would not be populated for our route yet anyway.

D'oh!

So the problem is this:

    $acceptable_formats = $request->isMethodCacheable() ? $acceptable_request_formats : $acceptable_content_type_formats;
…
    if (in_array($requested_format, $acceptable_formats)) {
      return $requested_format;
    }

Because $acceptable_request_formats === ['json'] and $acceptable_content_type_formats === ['bin'], but due to the logic, $acceptable_formats is calculated to be ['bin'].

So my answer is: is this a bug in \Drupal\rest\EventSubscriber\ResourceResponseSubscriber::getResponseFormat()? Remember, that was written when we had no use cases yet for the requested response format not matching the request body format!

I think this change would make sense:

diff --git a/core/modules/rest/src/EventSubscriber/ResourceResponseSubscriber.php b/core/modules/rest/src/EventSubscriber/ResourceResponseSubscriber.php
index df77281..58a863e 100644
--- a/core/modules/rest/src/EventSubscriber/ResourceResponseSubscriber.php
+++ b/core/modules/rest/src/EventSubscriber/ResourceResponseSubscriber.php
@@ -96,7 +96,7 @@ public function getResponseFormat(RouteMatchInterface $route_match, Request $req
     $route = $route_match->getRouteObject();
     $acceptable_request_formats = $route->hasRequirement('_format') ? explode('|', $route->getRequirement('_format')) : [];
     $acceptable_content_type_formats = $route->hasRequirement('_content_type_format') ? explode('|', $route->getRequirement('_content_type_format')) : [];
-    $acceptable_formats = $request->isMethodCacheable() ? $acceptable_request_formats : $acceptable_content_type_formats;
+    $fallback_acceptable_formats = $request->isMethodCacheable() ? $acceptable_request_formats : $acceptable_content_type_formats;
 
     $requested_format = $request->getRequestFormat();
     $content_type_format = $request->getContentType();
@@ -104,7 +104,7 @@ public function getResponseFormat(RouteMatchInterface $route_match, Request $req
     // If an acceptable format is requested, then use that. Otherwise, including
     // and particularly when the client forgot to specify a format, then use
     // heuristics to select the format that is most likely expected.
-    if (in_array($requested_format, $acceptable_formats)) {
+    if (in_array($requested_format, $acceptable_request_formats)) {
       return $requested_format;
     }
     // If a request body is present, then use the format corresponding to the
@@ -114,8 +114,8 @@ public function getResponseFormat(RouteMatchInterface $route_match, Request $req
       return $content_type_format;
     }
     // Otherwise, use the first acceptable format.
-    elseif (!empty($acceptable_formats)) {
-      return $acceptable_formats[0];
+    elseif (!empty($fallback_acceptable_formats)) {
+      return $fallback_acceptable_formats[0];
     }
     // Sometimes, there are no acceptable formats, e.g. DELETE routes.
     else {
damiankloip’s picture

Status: Needs work » Needs review
StatusFileSize
new33.85 KB
new3.7 KB

Hmm, I have made that change (looks sensible) but then we need to add the _format requirement for the resource, then I get 404's somewhere. So I guess _format is being checked/enforced somewhere else earlier in the routing system?

Status: Needs review » Needs work

The last submitted patch, 364: 1927648-364.patch, failed testing.

damiankloip’s picture

StatusFileSize
new33.88 KB
new564 bytes
damiankloip’s picture

Status: Needs work » Needs review

The last submitted patch, 330: 1927648-329.patch, failed testing.

The last submitted patch, 331: 1927648-331.patch, failed testing.

The last submitted patch, 344: 1927648-343.patch, failed testing.

The last submitted patch, 350: 1927648-350.patch, failed testing.

The last submitted patch, 355: 1927648-354.patch, failed testing.

The last submitted patch, 356: 1927648-356.patch, failed testing.

The last submitted patch, 359: 1927648-359.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 366: 1927648-366.patch, failed testing.

damiankloip’s picture

Issue summary: View changes

Updated IS.

damiankloip’s picture

Title: Serialize file content (base64) to support REST GET/POST/PATCH on file entity » Allow creation of file entities from binary data

Better title? Any other suggestions welcome.

**Looks at Wim

garphy’s picture

I just attempted to try the latest patch (#366) to upload a file in the image field of a media entity of bundle image.

I try to call /file/upload/media/image/image?_format=json with the following headers :

Authorization:Bearer ....
Content-Disposition:file;filename="example.txt"
Content-Type:bin

But I always get a 404 and the following response :

{
    "message": "No route found for \"POST /file/upload/media/image/image\""
}

Is it supposed to work and I'm doing something wrong or is it just not ready for real testing yet ?

pnagornyak’s picture

StatusFileSize
new29.7 KB

Hi garphy, The reason why you get 404 is here ResourceRoutes.php:

        // If the route has a format requirement, then verify that the
        // resource has it.
        $format_requirement = $route->getRequirement('_format');
        if ($format_requirement && !in_array($format_requirement, $rest_resource_config->getFormats($method))) {
          continue;
        }

and here FileUploadResource.php:

  protected function getBaseRouteRequirements($method) {
    $requirements = parent::getBaseRouteRequirements($method);

    // Add the content type format access check. This will enforce that all
    // incoming requests can only use the 'application/octet-stream'
    // Content-Type header.
    $requirements['_content_type_format'] = 'bin';
    $requirements['_format'] = implode('|', $this->serializerFormats);// <-- this line should be deleted

    return $requirements;
  }

i made some fixes to the last patch, now it should work, tested with Drupal 8.3.5
P.S. i removed application/octet-stream type, the serializer don`t know about this type (bin), maybe it will be added later

wim leers’s picture

StatusFileSize
new7.81 KB

Here's the interdiff for #379.

@pnagornyak: for next time, please see https://www.drupal.org/documentation/git/interdiff :)

Also queued #379 for testing. If I run FileUploadJsonBasicAuthTest::testPostFileUpload() locally (which failed for #366), then I see an interesting difference:

#366
1) Drupal\Tests\file\Functional\FileUploadJsonBasicAuthTest::testPostFileUpload
Failed asserting that 404 is identical to 415.

/Users/wim.leers/Work/d8/core/modules/file/tests/src/Functional/FileUploadResourceTestBase.php:172
#379
1) Drupal\Tests\file\Functional\FileUploadJsonBasicAuthTest::testPostFileUpload
Failed asserting that 422 is identical to 415.

/Users/wim.leers/Work/d8/core/modules/file/tests/src/Functional/FileUploadResourceTestBase.php:172
pnagornyak’s picture

I didn`t change the tests, so it will fail as it uses Content-Type: application/octet-stream request header (maybe there not only one problem exists).
P.S. i have no experience in testing, that is why i didn`t fix it.

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new3.73 KB
new35.53 KB

Reviewing #379's changes:

  1. +++ /dev/null
    @@ -1,24 +0,0 @@
    -class FileServiceProvider implements ServiceModifierInterface {
    -
    -  /**
    -   * {@inheritdoc}
    -   */
    -  public function alter(ContainerBuilder $container) {
    -    if ($container->has('http_middleware.negotiation') && is_a($container->getDefinition('http_middleware.negotiation')->getClass(), NegotiationMiddleware::class, TRUE)) {
    -      $container->getDefinition('http_middleware.negotiation')->addMethodCall('registerFormat', ['bin', ['application/octet-stream']]);
    -    }
    -  }
    -
    -}
    

    This … seems wrong?

  2. +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
    @@ -215,6 +216,7 @@ protected function streamUploadData($destination_uri) {
    +    return $new_temp_filename;
    

    This seems very wrong.

  3. +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
    @@ -243,7 +245,7 @@ protected function validateAndLoadFieldDefinition($entity_type_id, $bundle, $fie
    -    if (!$field_definition->access('create')) {
    +    if (!\Drupal::entityTypeManager()->getAccessControlHandler($entity_type_id)->fieldAccess('create', $field_definition)) {
    

    This should not be necessary.

  4. +++ b/core/modules/hal/hal.services.yml
    @@ -16,7 +16,7 @@ services:
    -    arguments: ['@entity.manager', '@hal.link_manager', '@module_handler']
    +    arguments: ['@entity.manager', '@http_client', '@hal.link_manager', '@module_handler']
    

    This is definitely wrong. A simple copy/paste mistake perhaps?

  5. +++ b/core/modules/hal/hal.services.yml
    index 58a863e..df77281 100644
    --- a/core/modules/rest/src/EventSubscriber/ResourceResponseSubscriber.php
    

    This is just reverting the changes made to ResourceResponseSubscriber.

    I proposed that in #363. @damiankloip then wrote this in #364:

    Hmm, I have made that change (looks sensible) but then we need to add the _format requirement for the resource, then I get 404's somewhere. So I guess _format is being checked/enforced somewhere else earlier in the routing system?

    This now makes me suspect that this might be related to routing system bugs that I also encountered in #2858482: Simplify REST routing: disallow requesting POST/PATCH in any format, make consistent, and which are being fixed in #2883680: Force all route filters and route enhancers to be non-lazy.


So, back to the patch in #366. First: there's a small mistake in FileUploadResourceTestBase::fileRequest() with the ?_format query string. Next, turns out we indeed need some of the changes in #2858482: Simplify REST routing: disallow requesting POST/PATCH in any format, make consistent. Because this bit in ResourceRoutes is causing the "file upload" route to not be generated:

      // If the route has a format requirement, then verify that the
      // resource has it.
      $format_requirement = $route->getRequirement('_format');
      if ($format_requirement && !in_array($format_requirement, $rest_resource_config->getFormats($method))) {
        continue;
      }

If we import the fix from #2858482, yet still keep #366's existing changes to ResourceRoutes (to not overwrite _content_type_format if it's already set, which this REST resource uniquely needs, to be able to specify a _content_type_format that differs from the other format, so it can accept binary data), then we get further in the tests than #366 and #379!

The failure is then:

1) Drupal\Tests\file\Functional\FileUploadJsonBasicAuthTest::testPostFileUpload
Failed asserting that 403 is identical to 201.

/Users/wim.leers/Work/d8/core/modules/file/tests/src/Functional/FileUploadResourceTestBase.php:178

The actual response body is:

string(70) "{"message":"Access denied for field \u0022field_rest_file_test\u0022"}"

So that's the next step!

(My patch is relative to #366!)

pnagornyak’s picture

This … seems wrong?

+++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
@@ -215,6 +216,7 @@ protected function streamUploadData($destination_uri) {
+    return $new_temp_filename;

i added it as "file_unmanaged_move($temp_file_name, $destination_uri)" returns new file uri, if user1 send a file "avatar.jpg" and user2 send "avatar.jpg" than user2 will get an error but file will be uploaded and not saved into DB

This seems very wrong.

+++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
@@ -243,7 +245,7 @@ protected function validateAndLoadFieldDefinition($entity_type_id, $bundle, $fie
-    if (!$field_definition->access('create')) {
+    if (!\Drupal::entityTypeManager()->getAccessControlHandler($entity_type_id)->fieldAccess('create', $field_definition)) {

This $field_definition->access('create') checks is user has an "administer fields" permission so i though that it wrong

wim leers’s picture

StatusFileSize
new3.49 KB
new35.99 KB

@pnagornyak I wrote #382 in a hurry — it looks much more negative than I intended! :( Sorry about that.

This $field_definition->access('create') checks is user has an "administer fields" permission so i though that it wrong

You're absolutely right! I wrote This should not be necesssary, but I was completely wrong — it's very much necessary.

Bringing that hunk from @pnagornyak's patch in #379 :)

FileUploadJsonBasicAuthTest is now passing!

The last submitted patch, 382: 1927648-381.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

wim leers’s picture

StatusFileSize
new2.62 KB
new37.52 KB

Added test coverage to assert the response of a successful upload.

wim leers’s picture

StatusFileSize
new2.14 KB
new37.54 KB
+++ b/core/modules/file/tests/src/Functional/FileUploadResourceTestBase.php
@@ -0,0 +1,281 @@
+    FieldConfig::create([
+      'entity_type' => 'entity_test',
+      'field_name' => 'field_rest_file_test',
+      'bundle' => 'entity_test',
+      'settings' => [
+        'file_directory' => '',
+        'file_extensions' => 'txt',
+        'max_filesize' => '',
+      ],
+    ])

This is not quite realistic enough: field_image on the article node_type that's installed by default has the file_directory set to a token, so it's uploaded to that particular subdirectory.

We should also be testing with a subdirectory here.

… and that's exactly how a whole bunch of other things in #379 will turn out to be useful after all! #379 was just ahead of its time without a clear explanation :)

So when I specify 'file_directory' => 'foobar',, the "is writable" check fails. The fix from #379 is only partially correct though: it's missing the FILE_CREATE_DIRECTORY directive. And it should replace the existing check, not run before it.

wim leers’s picture

StatusFileSize
new1.75 KB
new37.54 KB
+++ b/core/modules/file/tests/src/Functional/FileUploadResourceTestBase.php
@@ -214,7 +214,7 @@ public function testPostFileUpload() {
-          'value' => 'public://example.txt',
+          'value' => 'public://foobarexample.txt',

@@ -242,7 +242,7 @@ public function testPostFileUpload() {
-    $this->assertSame($file_data, file_get_contents('public://example.txt'));
+    $this->assertSame($file_data, file_get_contents('public://foobarexample.txt'));

These were added in #387 — and it makes the test pass, but it points out another bug. Let's fix that here.

(#379 fixed that too!)

The last submitted patch, 384: 1927648-384.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

The last submitted patch, 386: 1927648-385.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

wim leers’s picture

StatusFileSize
new4.92 KB
new35.18 KB

And this then should most of the other tests pass. It reverts some of what I proposed in #363, as well as some of what I did in #382. ResourceResponseSubscriberTest still fails, but I don't have time to dig deeper into this now.

At least the number of failures should now be manageable again.

(I also removed some methods that were previous required for ResourceTestBase classes, but are not anymore.)

wim leers’s picture

StatusFileSize
new611 bytes
new35.16 KB

This removes one more failure.

wim leers’s picture

Status: Needs review » Needs work
Issue tags: +Needs tests

Next steps: complete all todos, and add a lot more test coverage.

damiankloip’s picture

Status: Needs work » Needs review
StatusFileSize
new38.45 KB
new4.02 KB

The tests make the assumption that uncacheable POST/PUT routes will not have any acceptable response formats specified, so the logic fails when $acceptable_request_formats is empty in ResourceResponseSubsriber::getResponseFormat(). So I think this is the correct fix - to add the _format requirement to these routes too.

damiankloip’s picture

StatusFileSize
new45.26 KB
new11.79 KB

Here are some more test classes:

  • \Drupal\Tests\file\Functional\FileUploadJsonCookieTest
  • \Drupal\Tests\hal\Functional\EntityResource\File\FileUploadHalJsonBasicAuthTest
  • \Drupal\Tests\hal\Functional\EntityResource\File\FileUploadHalJsonCookieTest

(and a \Drupal\Tests\hal\Functional\EntityResource\File\FileUploadHalJsonTestBase class) with a couple of other fixes to help these along.

Questions

  • See @todo in FileUploadHalJsonTestBase - trying to use the applyHalFieldNormalization() method does not give the expected normalization result for file entities. Any ideas about that?
  • Should the FileUploadResourceTestBase move into the rest module? This seems to be a pattern for others. E.g. CommentResourceTestBase
  • We still need to decide if we're going to use the Content-Disposition header or have another one for our file name. We also need to add this as a hard requirement in the patch.
  • If no file name if provided and we use our uniqid() string, should we also use a default extension too?
wim leers’s picture

#394: D'oh! OF COURSE! Perhaps we should move that logic into a protected function generateRouteRequirements() helper method, because #394's interdiff is repeating the same logic in 3 places?
In any case: this looks great :)

#395

  1. +++ b/core/modules/hal/tests/src/Functional/EntityResource/File/FileUploadHalJsonTestBase.php
    @@ -0,0 +1,79 @@
    +    // @todo Look into why apply applyHalFieldNormalization() doesn't work here.
    +    //   - It does not remove the uid property
    +    //   - It add an 'en' langcode to the created property only (suggesting it
    +    //     thinks just that field is translatable).
    +    //   - The file URL needs to be created anyway AFAICT.
    

    - In other entity types, the uid base field is translatable, not for File.
    - Are you sure that that is happening? I don't see the explanation for that either right now.
    - There's lots of weirdness around this, because File::url() calls file_create_url(). The tricky thing is that the uri base field uses \Drupal\Core\Field\Plugin\Field\FieldType\UriItem, but that's for URIs in general. This is a very particular URI, with special treatment. We definitely need to investigate this further.

  2. Yes, and indeed for those reasons. Even though eventually we probably want to move it to the modules that provide the functionality, because eventually we want each module to be responsible for their own normalizations and REST support IMO.
  3. I think we should stick with Content-Disposition for now and make that a hard requirement, add explicit test coverage. Once we have test coverage for it, it'll be easier to change.
  4. Not providing a filename is equivalent with an error I think? Which would mean a 422 response.

Also: even though it's probably pretty rare for anon users to upload files, I do think we also want a FileUpload(Hal)JsonAnonTest test.

damiankloip’s picture

StatusFileSize
new48.36 KB
new14.12 KB

Thanks for the review, Wim.

Moved that test code into a generateRouteRequirements method.

1. a. So with hal, the uid field is meant to be removed as it exists as a relation in _links and _embedded (sorry, the comment was not overly helpful). b. Yes, that's definitely what I have been seeing in the expected normalization data once passed through applyHalFieldNormalization. c. Not sure how we can better handle that, we ideally need to use file_create_url as the url will be specific to each test install. Otherwise we will basically just have to mimic this anyway? other ideas completely welcome though.
2. OK - moved it into the Drupal\Tests\rest\Functional namespace. I didn't go as far as EntityResource as it's technically not an entity resource like all other entity resources, and doesn't extend EntityResourceTestBase.
3 & 4. OK, sounds good to me - added additional enforcement for that. Also improved the regex logic to be a bit more restrictive. Before it accepted empty strings, as well as a a key-value pair in the header like 'not_a_filename=example.txt'.

Yes, I was going to look at implementing the anon tests too, I just didn't quite get on to that yet. I will have a look soon.

wim leers’s picture

Yes, I was going to look at implementing the anon tests too, I just didn't quite get on to that yet. I will have a look soon.

👍

Reply:

  1. a+b) I wasn't clear either I guess — I meant to say that I think the reason it's being treated differently is that the File entity's uid base field is not translatable, unlike all others. But … that also doesn't make sense. We'll need to ddebug the HAL normalization process to figure out why this is happening — i.e. step through \Drupal\hal\Normalizer\ContentEntityNormalizer + \Drupal\hal\Normalizer\EntityReferenceItemNormalizer
    c) This is due to \Drupal\hal\Normalizer\FileEntityNormalizer::normalize() AFAICT> Perhaps we need to change that?
  2. 👍
  3. 👍
  4. 👍

Review:

  1. +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
    @@ -228,6 +220,38 @@ protected function streamUploadData($destination_uri) {
    +      throw new BadRequestHttpException('"Content-Disposition" header is required. A file name in the format "filename=FILENAME" should be provided');
    ...
    +      throw new BadRequestHttpException('No filename found in "Content-Disposition" header. A file name in the format "filename=FILENAME" should be provided');
    

    s/should/must/

    Also: trailing period.

  2. +++ b/core/modules/rest/tests/src/Functional/FileUploadResourceTestBase.php
    @@ -40,6 +40,13 @@
    +  protected $testFileData = 'Hares sit on chairs, and mules sit on stools.';
    

    :P

  3. +++ b/core/modules/rest/tests/src/Functional/FileUploadResourceTestBase.php
    @@ -113,16 +120,32 @@ public function testPostFileUpload() {
    +    // An empty filename in the Content-Disposition header should return a 400.
    +    $response = $this->fileRequest($uri, $this->testFileData, ['Content-Disposition' => 'file; filename=""']);
    ...
    +    // An empty filename in the Content-Disposition header should return a 400.
    +    $response = $this->fileRequest($uri, $this->testFileData, ['Content-Disposition' => 'filename=""']);
    

    Same description, but different header -> one of the descriptions is wrong?

  4. +++ b/core/modules/rest/tests/src/Functional/FileUploadResourceTestBase.php
    index ea93d78988..b24530d33f 100644
    --- a/core/modules/rest/tests/src/Unit/EventSubscriber/ResourceResponseSubscriberTest.php
    

    👍

Version: 8.4.x-dev » 8.5.x-dev

Drupal 8.4.0-alpha1 will be released the week of July 31, 2017, which means new developments and disruptive changes should now be targeted against the 8.5.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

damiankloip’s picture

StatusFileSize
new48.78 KB
new7.67 KB

I got to the bottom of what was going on with the strange applyHalFieldNormalization behaviour I was seeing. FileUploadResource tests are a little different, in that the $entity property on the tests classes is an EntityTest entity (parent entity and field config for the file). So when applyHalFieldNormalization it was applying field data from the EntityTest entity to the normalization data of the file entity - doh! So I think we just don't use that help in this case. File uploads are a special case after all. I have updated the comment to reflect that.

I fixed other docs and string feedback from #398 too. I have left the trailing periods for now though. I usually don't add them, and haven't for any of the code here (I don't think :) ). I am happy to change though - do the standards say there should always be a trailing period?

I tried adding anon tests, but keep getting failures with the file upload resource route being a 404... So will need to debug that further.

kylebrowning’s picture

bumped to 8.5.x :(

wim leers’s picture

I got to the bottom […] doh!

Oh, hah!

+++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
@@ -388,6 +406,7 @@ protected function getBaseRouteRequirements($method) {
+    // Allow all serializer formats to be returned as a response format.
     $requirements['_format'] = implode('|', $this->serializerFormats);

Actually, I think we do not need to set this, because that's determined by the RestResourceConfig config entity?

I tried adding anon tests, but keep getting failures with the file upload resource route being a 404... So will need to debug that further.

Ok.


#401: Yeah… but now we're finally reaching the point where this patch is actually RTBC'able! :) A review from you would be very helpful. And perhaps you can add more edge cases you can think of that we should add test coverage for (see below)?


I tried to review this from a high-level POV — I refrained from nitpicking this time, and focused on the bigger/architectural aspects.

  1. +++ b/core/modules/hal/src/Normalizer/FileEntityNormalizer.php
    @@ -55,16 +44,4 @@ public function normalize($entity, $format = NULL, array $context = []) {
    -  public function denormalize($data, $class, $format = NULL, array $context = []) {
    

    At this point, all that remains is the normalize() method that overrides \Drupal\hal\Normalizer\ContentEntityNormalizer::normalize(), to do this:

        $data = parent::normalize($entity, $format, $context);
        // Replace the file url with a full url for the file.
        $data['uri'][0]['value'] = $this->getEntityUri($entity);
    
    

    I'm now wondering if we shouldn't change that?

    Does it really make sense to only return http://example.com/sites/default/files/blah.png instead of just returning public://blah.png?

    Wouldn't it be more logical to have UriItem's value property still be public://blah.png, and then add a new public_url property that computes http://example.com/sites/default/blah.png from public://blah.png?

    I'd rather not distract this issue with that, but if we change it later, it'll be a BC break.

    Of course, not every UriItem needs this, only URIs that are actually stream wrapper URIs.

    So perhaps what we want is a new StreamWrapperUriItem extends UriItem, and only add this public_url property to StreamWrapper UriItem, not to UriItem?

  2. +++ b/core/modules/hal/tests/src/Functional/FileDenormalizeTest.php
    +++ b/core/modules/hal/tests/src/Functional/FileDenormalizeTest.php
    @@ -41,35 +41,10 @@ public function testFileDenormalize() {
    

    All test coverage in here should be generalized to FileUploadResourceTestBase, so that it's not only tested for for HAL, but for all formats.

  3. +++ b/core/modules/media/tests/src/Functional/MediaFunctionalTestTrait.php
    --- a/core/modules/rest/src/EventSubscriber/ResourceResponseSubscriber.php
    +++ b/core/modules/rest/src/EventSubscriber/ResourceResponseSubscriber.php
    
    +++ b/core/modules/rest/tests/src/Functional/ResourceTestBase.php
    --- a/core/modules/rest/tests/src/Unit/EventSubscriber/ResourceResponseSubscriberTest.php
    +++ b/core/modules/rest/tests/src/Unit/EventSubscriber/ResourceResponseSubscriberTest.php
    

    We should be able to move this to a separate issue and land it there, that'd make the size of this patch smaller, and would make it easier to review. We would have to add a test case that would fail in HEAD though, to prove the need for this change.

  4. +++ b/core/modules/rest/src/RequestHandler.php
    @@ -139,4 +139,47 @@ public function handle(RouteMatchInterface $route_match, Request $request) {
    +  public function handleRaw(RouteMatchInterface $route_match, Request $request) {
    

    This is 90% identical to the existing ::handle(). We're just leaving "the middle part" out.

    Would it be feasible to add two helper functions, one for the "pre" part and one for the "post" part, and then have ::handle() and ::handleRaw() both call those two helpers, so that ::handle() is only different in that it does this extra thing in the middle?

    Or does that not make sense?

    Also note: #2720233: Use arguments resolver in RequestHandler to have consistent parameter ordering will change ::handle() quite a bit.

  5. +++ b/core/modules/rest/tests/src/Functional/FileUploadResourceTestBase.php
    @@ -0,0 +1,300 @@
    +  public function testPostFileUpload() {
    +    $this->initAuthentication();
    +
    +    $this->provisionResource([static::$format], static::$auth ? [static::$auth] : [], ['POST']);
    +
    +    $this->setUpAuthorization('POST');
    +
    +    $uri = Url::fromUri('base:' . static::$postUri);
    +
    +    // The wrong content type header should return a 415 code.
    +    $response = $this->fileRequest($uri, $this->testFileData, ['Content-Type' => static::$mimeType]);
    +    $this->assertSame(415, $response->getStatusCode());
    +
    +    // An empty Content-Disposition header should return a 400.
    +    $response = $this->fileRequest($uri, $this->testFileData, ['Content-Disposition' => '']);
    +    $this->assertSame(400, $response->getStatusCode());
    +
    +    // An empty filename with a context in the Content-Disposition header should
    +    // return a 400.
    +    $response = $this->fileRequest($uri, $this->testFileData, ['Content-Disposition' => 'file; filename=""']);
    +    $this->assertSame(400, $response->getStatusCode());
    +
    +    // An empty filename without a context in the Content-Disposition header
    +    // should return a 400.
    +    $response = $this->fileRequest($uri, $this->testFileData, ['Content-Disposition' => 'filename=""']);
    +    $this->assertSame(400, $response->getStatusCode());
    +
    +    // An invalid key-value pair in the Content-Disposition header should return
    +    // a 400.
    +    $response = $this->fileRequest($uri, $this->testFileData, ['Content-Disposition' => 'not_a_filename="example.txt"']);
    +    $this->assertSame(400, $response->getStatusCode());
    +
    +    // This request will have the default 'application/octet-stream' content
    +    // type header.
    +    $response = $this->fileRequest($uri, $this->testFileData);
    +
    +    $this->assertSame(201, $response->getStatusCode());
    +
    +    $expected = $this->getExpectedNormalizedEntity();
    +    static::recursiveKSort($expected);
    +    $actual = $this->serializer->decode((string) $response->getBody(), static::$format);
    +    static::recursiveKSort($actual);
    +
    +    $this->assertSame($expected, $actual);
    +
    +    // Check the actual file data.
    +    $this->assertSame($this->testFileData, file_get_contents('public://foobar/example.txt'));
    +  }
    +
    

    I think the most important question is: are there more edge cases we should test here?

    Things I can think of:

    1. Generate a file with a size larger than the PHP memory limit, then try to upload this. To prove that we're not bound by the PHP memory limit using this approach.
    2. Try uploading a 0-byte file. This is AFAIK actually valid? In any case it's an edge case worth testing.
    3. Test the MIME type of the uploaded file. (IDK what this should be in the case of a 0-byte file.)
    4. Try to upload a file with a different Content-Type
      header value than application/octet-stream.
    5. In the case of a successful upload, also assert the various fields, such as size,
      MIME type, and so on,
      which \Drupal\Tests\hal\Functional\FileDenormalizeTest actually is testing already.
    6. This means we should test at least two uploads: one zero-byte, one with a size that is larger than the PHP memory limit.
    7. Perhaps even more?
blainelang’s picture

I've been following this issues great progress and want to applaud the efforts. Also wanted to report back on my testing with POSTMAN should someone else be trying to test.

  • Using D8.5 DEV, I was able to apply the patch in #400 successfully: patch -p1 < 1927648-399.patch (in site root directory)
  • Installed the restui module and enabled the File Upload and User Resources (POST methods).
  • Created a new Content Type "media_test" and added two fields one of type image and another of type file.

Initial attempts at a POST to /file/upload failed because I didn't have the X-CSRF-TOKEN header. We need the User resource to get a X-CSRF-TOKEN back with a REST Login API call. A successful POST to /user/login will return the token that can be used in the POST to /file/upload API

{
"current_user": {
"uid": "1",
"roles": [
"authenticated",
"administrator"
],
"name": "admin"
},
"csrf_token": "JDmsm69zitGY6jgf370EiQbw7LK1vzfIZ2olt5HjBEs",
"logout_token": "wOF4SZZy7oNQRh9E1w9uYJQFR5UxyZl_WYVLu8AiA7E"
}

Once I had the CSRF token, I was able to POST successfully: /file/upload/node/media_test/field_media_test_file
Request Header:

'x-csrf-token' => 'JDmsm69zitGY6jgf370EiQbw7LK1vzfIZ2olt5HjBEs',
'content-disposition' => 'file;filename=\\"file1.pdf\\"',
'content-type' => 'application/octet-stream',
'authorization' => 'Basic YWRtaW46cGFzc3dvcmQ='

Note: I had to use 'content-type' => 'application/octet-stream' and not 'content-type' => 'bin' as I was getting an error that BIN was not allowed.

"message": "No route found that matches \"Content-Type: bin\""

I successfully tested uploading image and files to both field types. The files were uploaded to public:// and appear under the admin/content/files with a Temporary status and not used.

Testing files with an extension that has not been allowed, results in a 422 with a clear message indicating the accepted formats. The file is still uploaded to public:// but is not listed under admin/content files.

Tested uploading larger files and was successful uploading up to a 75Meg movie file. Larger files crashed POSTMAN so this may just be a postman issue. My PHP Settings are 256M for MEMORY_LIMIT and 128M for MAX_POST_SIZE.

damiankloip’s picture

Firstly, thanks for testing this! People actually trying this out is very much appreciated.

Note: I had to use 'content-type' => 'application/octet-stream' and not 'content-type' => 'bin' as I was getting an error that BIN was not allowed

The 'bin' format name is internal really, it's a machine name identifier for a format. This is something required by Symfony. E.g. JSON would be 'json' etc..

I'm not sure the CSRF token header makes sense unless cookie auth is being used? I'll look at how REST module handles this in other cases, I thought I mimiced the logic there, but maybe not. Or were you using cookie auth? If so, that's totally expected :)

damiankloip’s picture

Status: Needs review » Needs work

Addressing Wim's feedback from #402 now (wow - over 400 comments already!).

blainelang’s picture

Thanks Damian!

I was using basic auth with postman.

wim leers’s picture

@blainelang: 👏❤️ Thank you so much for giving it a try! There's no conclusion in your comment, but I gather that you're satisfied with how it behaves? :)

I'm not sure the CSRF token header makes sense unless cookie auth is being used?

Indeed.

I was using basic auth with postman.

The CSRF token is not necessary then, it's just ignored. But many tutorials/docs make it sound like you always need the CSRF token, so I totally understand why @blainelang did that.

That's one of my next big tasks: totally revamping the REST docs on Drupal.org!

Again, thank you so much, @blainelang! :)

damiankloip’s picture

Status: Needs work » Needs review
StatusFileSize
new57.52 KB
new15.66 KB

#402 (Great review BTW :)):

1. I thought that initially too, but was assuming it was overridden to provide the full URI as the value as a part of the hal JSON specification?
2. FileDenormalizeTest stuff doesn't really fit in our new FileUploadResourceTestBase as it's not really testing uploads at all, just that a file entity can be denormalized correctly. Which is still supported like before, just without the additional fetching and saving of random file data.
3. Agreed. Created #2901704: Allow REST routes to use different request formats and content type formats. We can remove those changes from this patch when it will pass against 8.5.x without them there.
4. How about we add more helper methods to the class and use more of a compositional pattern within the methods? That seems easier to follow that trying to get both to work with two methods or something? They have slightly different params for creating the response. Or, we could maybe have one method that we pass additional parameters into - the regular handle method would then pass in the unserialized, and handleRaw would pass in nothing. We would need to pass around quite a few parameters though :/ as the deserializing needs some of the same stuff. So the helper methods seem like a good compromise potentially.
5. Will look at this later. I think they all make sense, and I think we are already testing some of those cases.

damiankloip’s picture

StatusFileSize
new59.45 KB
new3.85 KB

OK, RE the last point of #402:

1. I haven't done the larger than PHP memory limit test yet. This has a bit more complexity I think. We need to get the memory limit value, then parse it to bytes (could be a value like '512M'), then write a file that's as big as that. Although to pass this into our request for our test, we will need to make a string out of it anyway? Which then means it'll be larger than our memory limit? So this might not actually be possible for us to test? Unless we can use Guzzle stream to achieve this within the test framework. Instead/additionally we could also test setting a size limit on the field and uploading something greater than that? That should be easier to achieve.
2. Added a test for zero byte files. I also realised that File entities in general cannot have a filesize of 0 set automatically due to the code in \Drupal\file\Entity\File::preSave. So I'll open a separate issue for this, it doesn't really affect us too much here (we set the filesize before anyway, so we can validate the file).
3. We check the mime type when we assert the expected response data and all the other fields. Not sure it's worth us checking the file too. Do you? Related to that, the MIME type for a zero byte file should be the same as one with data, as we decided you MUST provide a file name.
4. Added a new test method that correctly fails when trying to upload a json file (The field is configured to only allow txt files). We already make a request with the test $format as the content type.
5. Same as #3. I don't think we need to assert the actual file fields too, as the serialized data will have the values from the file. Any other advantages to trying to test the entity values directly here?

damiankloip’s picture

Issue tags: -Needs tests
StatusFileSize
new60.86 KB
new6.75 KB

Ah, forgot we had the Bytes class - that would make any conversion to bytes much easier when working out the memory limit :)

Anyway, I also realised that we could have orphaned files that have been moved into the file storage location as the file was moved as part of the streaming method, we ideally need to validate the file entity first (based on the temp file size etc..) THEN move it. The additional testFileUploadLargerFileSize test covers this case. Before my change the last assertion, checking the file doesn't exist was failing, as the file was written/moved, then validation failed.

damiankloip’s picture

Status: Needs review » Needs work

The last submitted patch, 410: 1927648-410.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

damiankloip’s picture

Status: Needs work » Needs review
blainelang’s picture

The plan is to provide a separate endpoint that is only for the creation of file entities. This endpoint will only accept binary data (application/octet-stream), not any file entity representation, although a file entity will be created from this data. Large amounts of data can then be streamed in a request instead

I've built an ember client to test out how best to handle file uploads and have it successfully uploading files and attaching to a content node. The app is uploading a batch of 5+ files at once and attaching them to a node is working. Moved on to testing large file uploads and hit the a few limits and wanted to share:

First tested with a 100MB file and got a HTTP 500 error which was due to an apache setting. Updated it to 768000000 and restarted apache:
mod_fcgid: HTTP request length 134225728 (so far) exceeds MaxRequestLen (134217728), referer: http://localhost:4200/upload
Next were two a PHP setting for post_max_size and upload_max_size that were restricting the uploads to 128M. After increasing them to 768M, I was able to upload files of 750Meg and I suspect larger if I kept kept increasing my local dev env settings - very nice!

Note that even when I was getting the PHP error about max 128M, it was actually uploaded and appeared in the admin/content/files area.
Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException: Unprocessable Entity: validation failed. field_media_test_file.0: The file is 410.93 MB exceeding the maximum file size of 128 MB. in Drupal\rest\Plugin\rest\resource\EntityResource->validate() (line 43 of /Users/blaine/Sites/devdesktop/d8site85/core/modules/rest/src/Plugin/rest/resource/EntityResourceValidationTrait.php)

damiankloip’s picture

Thanks for testing! Did you see this issue with the latest patch in #410 too? That makes sure the file is validated, then moved after. It behaved a bit differently in previous patches.

I wonder if you can stream the request body from your JS client? Never tried that before, personally. That would hopefully mean you don't need a larger request body and max upload limit - but maybe not.

blainelang’s picture

I was using the patch from #400 but restored /core and then re-applied #410 to test. Reset the PHP.ini settings back and re-tested with the Ember App to get a 422 return code and message:
"Unprocessable Entity: file validation failed.↵The file is <em class="placeholder">341.54 MB</em> exceeding the maximum file size of <em class="placeholder">256 MB</em>."

So #410 is returning a 422 status code and handling the PHP Error as well, do not see the file under /admin/content/files now.

Also wanted to test if the patch was checking the field setting. If I change the field settings to 128 MB and try a file > 128MB but < 256 MB then it will upload ok.

I wonder if you can stream the request body from your JS client?

Not sure what you mean. In my Ember App, I'm using the ember-file-upload addon which has a uploadBinary method.

blainelang’s picture

Scanned through the related 8.4x and 8.5x issues and couldn't find an answer to this question - Is there a REST API endpoint to get a list of files?
The reason is that when an app attempts to upload a file with the same name as an existing file, the API returns a 422 with the URI of the file name conflict but not the file ID. The APP may still want to attach that file but how as we need the UUID and FID.

What if we returned the fid and uuid as part of the 422 response? Else I figure, we need a way to query for a matching file name.

blainelang’s picture

I used views to create a REST Export with an exposed filter on the URI field This should work as I was able to use postman to make a request passing in the URI.

blainelang’s picture

I think the current behaviour for when you upload a file that already exists should be changed to be the same as default Drupal. From the UI, I can attach the same image multiple times in the same node/add or node/edit operation and the file names and public URI values will be made unique automatically.

Shouldn't the REST API do the same?

garphy’s picture

I just tested the patch in #410 without much luck :/

I always get a 422 Unprocessable Entity with the following body :

{
    "message": "Unprocessable Entity: validation failed.\nurl.0.value: This value should be of the correct primitive type.\n"
}

This is really strange because the $values array passed to File::create() contains a correct value for "uri" :

array (
  'uid' => '2',
  'filename' => 'test0000.jpg',
  'filemime' => 'image/jpeg',
  'uri' => 'public://2017-08/test0000.jpg',
  'filesize' => 7522394,
)
blainelang’s picture

@garphy, I was getting this error as well if the JSONAPI module was enabled so I think there is an issue here that needs to be resolved.

garphy’s picture

@blainelang : you're right. After uninstalling jsonapi, it worked.

But get rid of jsonapi is not an option right now :) I'll try to dig deeper into that.

garphy’s picture

With jsonapi module enabled, if I comment out the function jsonapi_entity_base_field_info() entirely, I can upload a file.

I think it's related to #2825487: Fix normalization of File entities: file entities should expose the file URL as a computed property on the 'uri' base field. This has been "resolved" in jsonapi by adding a "url" computed property to every File entities.

I don't get why it messes with the uri field, though.

Should we open a related issue in jsonapi queue ?

garphy’s picture

Well... it seems to be a bug in JSONAPI.

Basically, the "url" property it defines has a value of type UriItem which is actually the path of the file relative to the baseUrl. So it's not a valid URI, so it doesn't validate.

Side note : I should have been paying more attention to the error message which indeed related to url, not uri.

I'm going to open an issue there.

wim leers’s picture

Reviewed + rerolled #2901704: Allow REST routes to use different request formats and content type formats. Thanks for splitting that off: it makes sense, and keeps the scope here tighter. Now reviewing this issue!

wim leers’s picture

Status: Needs review » Needs work
Related issues: +#1988426: Support deserialization of file entities in HAL

#408:

  1. The "return full URL" thing was added in #1988426: Support deserialization of file entities in HAL. The IS there says:

    Temporarily, create a FileEntityNormalizer. Once File is an EntityNG entity, and its properties are handled as such, this could potentially be handled in a field normalizer instead.

    This follow-up never happened, in part because there was no @todo

    Ironically, you're the one who added this in the first place, at #1988426-25: Support deserialization of file entities in HAL 😀

    The goal AFAICT was always to add sensible properties to access the full URL…

    Conclusion: we should do that here, otherwise we'll break BC later.

  2. Hm… so you're saying it's kind of a unit test for the file denormalization?
  3. 👏
  4. Looks totally sane :)
  5. 👍

Interdiff review:

  1. +++ b/core/modules/rest/src/RequestHandler.php
    @@ -117,63 +220,22 @@ public function handle(RouteMatchInterface $route_match, Request $request) {
    +   * @TODO is there are requirement that REST resources MUST return a response
    +   *   object? If so, we can type hint that here.
    +   *
    +   * @param $response
    ...
    +   * @return $response
    

    Yes. \Drupal\rest\ResourceResponseInterface

  2. +++ b/core/modules/rest/src/RequestHandler.php
    @@ -60,6 +61,57 @@ public static function create(ContainerInterface $container) {
    +  protected function getRequestMethod(RouteMatchInterface $route_match) {
    
    @@ -74,18 +126,69 @@ public function handle(RouteMatchInterface $route_match, Request $request) {
    +  protected function loadRestResourceConfigFromRouteMatch(RouteMatchInterface $route_match) {
    ...
    +  protected function prepareRouteParameters(RouteMatchInterface $route_match) {
    ...
    +  protected function denormalizeRequestData(Request $request, ResourceInterface $resource, $method) {
    

    These all look like sensible helper methods. It really is something that also just helps make RequestHandler::handle() become more understandable.

    Therefore it's even something we can split off to its own issue, to again make the scope of this issue smaller :)


#409:

  1. Agreed this is more complex. But I also think it's very important. Without it, we don't have strong guarantees that it actually works as intended. I'm fine with deferring this to a follow-up though — I think it's acceptable to first land this with just manually testing the fact that you can upload files of a size larger than the PHP memory limit.
  2. 👍
  3. Good points! Consider my remark moot :)
  4. 👍
  5. D'oh, yeah, I totally bloopered there — obviously you are testing both MIME type as well as everything else via getExpectedNormalizedEntity()'s expected response data.

Interdiff review:

+++ b/core/modules/rest/tests/src/Functional/FileUploadResourceTestBase.php
@@ -152,16 +152,62 @@ public function testPostFileUpload() {
+    $this->assertSame(422, $response->getStatusCode());

Please use \Drupal\Tests\rest\Functional\ResourceTestBase::assertResourceErrorResponse() instead, then we also get test coverage for the message that is returned, ensuring good DX.


#410: NICE! 🙏

Interdiff review:

+++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
@@ -172,22 +172,31 @@ public function post(Request $request, $entity_type_id, $bundle, $field_name) {
+    // Move the file to the correct location after validation.
+    // Move the file to the correct location based on the file entity,
+    // replacing any existing file.

Nit: these comments look strange.


#414: @blainelang wow, you are rocking so much! First #403, then #2901574: Requests to log in (cookie auth) via /user/login?_format=json result in 403 without helpful message, now #414. Thank you so much! ❤️

This tells us we need more tests than I thought — we'll need them for:

  1. "larger than memory limit but smaller than post_max_size" (http://php.net/manual/en/ini.core.php#ini.post-max-size)
  2. larger than post_max_size but smaller than the memory limit
  3. "larger than memory limit but smaller than upload_max_filesize" (http://php.net/manual/en/ini.core.php#ini.upload-max-filesize)
  4. larger than upload_max_filesize but smaller than the memory limit

Thoughts, @damiankloip?


#417 + #418: No, you can't get a list of any entity type via REST until you create a View that lists them and add a "REST Export" display to the view. Via JSON API, you can get a list of all file entities (or entities of any other type) without needing to create a view. This is one of the big advantages of JSON API!

#420 + #421 + #422 + #423 + #424: yes, this is definitely a bug in the JSON API module. It's a temporary work-around:

 * @todo This should probably live in core, but for now we will keep it as a
 * temporary solution. There are similar unresolved efforts already happening
 * there.
blainelang’s picture

@win-leers, very thorough review and followup. If you get a min, can you add your thoughts regarding #419 and how REST API should handle duplicate files as you can't do that presently via a PATCH method.

garphy’s picture

Status: Needs work » Needs review
StatusFileSize
new60.99 KB
new720 bytes

Here's an attempt to deal with the duplicate filename situation by reusing file_unmanaged_prepare().

damiankloip’s picture

@garphy (@blainelang), I think file_unmanaged_move already does what your patch in #428 adds. file_unmanaged_prepare is already called from there, and FILE_EXISTS_RENAME is the default.

So this will handle duplicates by creating a new file with the new name. If we decide we want to return a 4xx response in this case then so be it. I think the current behaviour makes sense though. That matches how Drupal works normally when uploading files via a UI widget. Or does something fail before that in validation or something?

garphy’s picture

@damiankloip, the problem is that file_unmanaged_move() is called after validate().

Maybe when could reverse it ?

Whatever solution we choose, it's definitely better to rename the filename on disk (and keep original filename in database) as it happens when uploading from UI. Otherwise it's a pretty bad DX in my opinion.

wim leers’s picture

Status: Needs review » Needs work

It sounds like we definitely also want test coverage for auto-number-suffix-files-on-duplicate-filename.

damiankloip’s picture

Status: Needs work » Needs review
StatusFileSize
new62.53 KB
new4.28 KB

#430: Yes - I thought that might be the case... so we can slightly expand on your changes and FILE_EXISTS_ERROR when we actually move the file, and handle the creation of the unique file URI/location in the call you added. Seems like a good way to avoid duplication, and some oddities if a file was then renamed again after it was saved.

#431: Agreed, here is that coverage too.

I will look at the EPIC review from #426 a bit later :D

Status: Needs review » Needs work

The last submitted patch, 432: 1927648-432.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

damiankloip’s picture

Status: Needs work » Needs review
StatusFileSize
new62.54 KB
new867 bytes

Missed a spot.

Status: Needs review » Needs work

The last submitted patch, 434: 1927648-434.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

damiankloip’s picture

Status: Needs work » Needs review
StatusFileSize
new62.63 KB
new2.74 KB
wim leers’s picture

Status: Needs review » Needs work

I will look at the EPIC review from #426 a bit later :D

:)


Review of #433+#434+#436:

  1. +++ b/core/modules/rest/tests/src/Functional/FileUploadResourceTestBase.php
    @@ -174,6 +174,43 @@ public function testPostFileUpload() {
    +   * Tests using the file upload POST route with a duplicate file.
    

    Nit: s/duplicate file/duplicate file name/

    Because it could very well be different files with the same name (for example interdiff.txt here on d.o).

  2. +++ b/core/modules/rest/tests/src/Functional/FileUploadResourceTestBase.php
    @@ -306,11 +304,13 @@ protected function getExpectedUnauthorizedAccessMessage($method) {
    +   *   The actual filename for the stored file.
    

    s/actual/expected/

damiankloip’s picture

Status: Needs work » Needs review
StatusFileSize
new63.42 KB
new7.03 KB

This patch does all the other misc doc nits (I think), and the usage of assertResourceErrorResponse.

This tells us we need more tests than I thought — we'll need them for:

"larger than memory limit but smaller than post_max_size" (http://php.net/manual/en/ini.core.php#ini.post-max-size)
larger than post_max_size but smaller than the memory limit
"larger than memory limit but smaller than upload_max_filesize" (http://php.net/manual/en/ini.core.php#ini.upload-max-filesize)
larger than upload_max_filesize but smaller than the memory limit

I would like to do this in theory, I think the trouble we will have in reality is trying to achieve this in the testing framework? I don't think we can stream data from a file in a request from within the test. I guess we could try a test for max_post_size, but this could also become very fragile depending on the memory limit in the php environment running the test... :/ I do however agree with one of your previous comments that we could land this without that, with manual testing that files larger than the php memory limit can be uploaded. We can also make sure the other cases listed above work with manual testing too.

RE the rest of #426;

Conclusion: we should do that here, otherwise we'll break BC later.

Are you saying we should try and make sure we have the computed property first?

Hm… so you're saying it's kind of a unit test for the file denormalization?

Exactly, this basically just tests file denormalization now. Which the FileEntityNormalizer doesn't even test anymore! :) So it doesn't do a great deal IMO. Depending on what we do with the file URL field, this test verging on useless. I guess it doesn't hurt to keep it though. OR we bring back the old functionality too. I think that is inherently insecure (?) though anyway, as it tries to make an HTTP request out to get a file and download it.

I think we're getting relatively close here :)

garphy’s picture

I'm wondering if we're not risking a race condition with the latest patch approach of file name conflict handling.

If another concurrent request, with the exact same filename starts to be processed after the first one has performed the file_unmanaged_prepare() but before the first request has a chance to perform file_unmanaged_move(). This maybe very unlikely but not impossible.

Maybe we should do file_unmanaged_move() before $this->validate() (and catch any exception to delete the file) ?

dawehner’s picture

Here is just my little review ...

  1. +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
    @@ -0,0 +1,425 @@
    +  const FILENAME_REGEX = '@\bfilename=\"(?<filename>.+)\"@';
    

    I was reading for a second about the content disposition header, see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Dispos...

    What is interesting that "filename" and "filename*" are allowed header values:

    filename
    Is followed by a string containing the original name of the file transmitted. The filename is always optional and must not be used blindly by the application: path information should be stripped, and conversion to the server file system rules should be done. This parameter provides mostly indicative information. When used in combination with Content-Disposition: attachment, it is used as the default filename for an eventual 'Save As" dialog presented to the user.
    filename*
    The parameters "filename" and "filename*" differ only in that "filename*" uses the encoding defined in RFC 5987. When both "filename" and "filename*" are present in a single header field value, "filename*" is preferred over "filename" when both are present and understood.

    Maybe we should support unicode filenames and have some form of test for it?

  2. +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
    @@ -0,0 +1,425 @@
    +   * @var \Symfony\Component\Serializer\SerializerInterface|SerializerInterface
    ...
    +   * @var \Drupal\Core\Utility\Token|Token
    

    🔧 Is there a reason we need to typehint for both?

  3. +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
    @@ -0,0 +1,425 @@
    +   * @param string $field_name
    +   *   The field name.
    +   *
    +   * @return \Drupal\rest\ModifiedResourceResponse
    +   *   A 201 response, on success.
    +   */
    +  public function post(Request $request, $entity_type_id, $bundle, $field_name) {
    

    🔧 Should we document the exceptions which can be thrown?

  4. +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
    @@ -0,0 +1,425 @@
    +    // This will take care of altering $file_uri if a file already exists.
    +    file_unmanaged_prepare($temp_file_path, $file_uri);
    

    ... So I'm wondering whether we should have some kind of protection against uploading the same filename twice at the same time ... this would cause triggerable 500's, which might bring down reverse proxies ...

  5. +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
    @@ -0,0 +1,425 @@
    +      // @todo Do we want the File URI (for the actual file) like this?
    +      'Location' => $file->url(),
    

    This is a tough question given that \Drupal\rest\Plugin\rest\resource\EntityResource::post returns the path to the file entity itself ...

  6. +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
    @@ -0,0 +1,425 @@
    +        fwrite($temp_file, fread($file_data, 8192));
    

    We don't take into account the possible return FALSE of fwrite ... we could check for it and in that case throw an exception or so?

  7. +++ b/core/modules/rest/src/RequestHandler.php
    @@ -106,8 +209,8 @@ public function handle(RouteMatchInterface $route_match, Request $request) {
    -        // These two serialization exception types mean there was a problem
    -        // with the structure of the decoded data and it's not valid.
    +          // These two serialization exception types mean there was a problem
    +          // with the structure of the decoded data and it's not valid.
             catch (UnexpectedValueException $e) {
    

    🙃 Unneeded changes ...

damiankloip’s picture

StatusFileSize
new65.23 KB
new8.62 KB

Nice review, as always!

So...

1. We talked about this briefly, it's a great find. I think we should keep our implementation as-is though, and just accept 'filename' in the header value. 'filename*' is kind of weird anyway. I'm not sure we need a distinction between accepting unicode and not. We are at the mercy of the file system, and what our file validation likes anyway. Just to keep this implementation slightly simpler. Also add test case for stripped file, and use basename() to make sure we only ever get a filename to use.
2. Nope. No reason. I blame phpstorm. Removed.
3. Yes. Added @throws for the HttpException
4. Yes, @garphy mentioned similar in #439, I was thinking this yesterday too. I think we should go with a lock on the filename here. This seems like a better approach than writing the file, moving it, then having to clean it up on all validation failures. Added a simple lock.
5. The location header... we still need a resolution on this :)
6. Yes, good idea. We should check the read and the write. Added checks for both, which throw exceptions, log and close the file handle - just to make sure.
7. Fine - reverted ;)

wim leers’s picture

#438:

That interdiff looks like a good step forward :)

I would like to do this in theory, I think the trouble we will have in reality is trying to achieve this in the testing framework?

True :( I agree with your assessment that it's more realistic to do manual testing of this bit.

Are you saying we should try and make sure we have the computed property first?

Yes.

So it doesn't do a great deal IMO

Right… which means it's probably indeed safe to delete.

I think we're getting relatively close here :)

Agreed! 😀


#439: Wouldn't the same problem exist in the form-based file uploading today then? In fact, that would totally explain why 1-5% of the time, uploading interdiff.txt to d.o fails (as in, AJAX spinner keeps going forever, and I have to reload the page).

I'm not too concerned about this, the probability of this happening is low enough that I'm not worried about it for now, and fixing it won't result in API changes. Great critical thinking though :) I'd be fine with addressing this for both REST file uploads and form file uploads in a follow-up issue.


#440:

  1. OMG 😱 A) awesome research, B) eagle-eyed Daniel! 🦅 , C) so so so glad to have you review this at least once :)
  1. Same as #439.
  2. Right, but we're not dealing with a File entity here, we're just dealing with a file. So I think this makes sense?
  3. Nice catch!
  4. Nice catch!

#441:

  1. If I read https://tools.ietf.org/html/rfc5987 correctly, then foo* instead of foo implies opting in to use the "extended annotation". In the extended annotation, you can specify any encoding.

    HOWEVER, it also says: By default, message header field parameters in HTTP ([RFC2616]) messages cannot carry characters outside the ISO-8859-1 character set ([ISO-8859-1])., and https://en.wikipedia.org/wiki/ISO/IEC_8859-1 only allows 8-bit latin characters.

    That would mean emoji and even em-dashes in filenames would be a problem. Perhaps we can get by with just filename (and not filename*) and just pass unicode characters, but it'd mean violating the HTTP spec. Which also means that if your request has to pass proxies or middlewares, that the filename you passed could be stripped away, because it's technically invalid HTTP.

    I think we definitely want a test with a filename that contains unicode. (e.g. Damian's favorite emoji)

Interdiff review:

  1. +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
    @@ -177,6 +188,13 @@ public function post(Request $request, $entity_type_id, $bundle, $field_name) {
    +    $lock_id = $this->generateLockIdFromFileUri($file_uri);
    ...
    +      throw new HttpException(500, sprintf('File "%s" is already locked for writing'));
    

    This looks like a nice solution.

    But I think this should be a 503, with a Retry-After header. Because retrying in a few seconds likely would succeed. This then clearly communicates that the request itself is not at fault, it'll work fine a few seconds later.

  2. +++ b/core/modules/rest/tests/src/Functional/FileUploadResourceTestBase.php
    @@ -209,6 +209,30 @@ public function testPostFileUploadDuplicateFile() {
    +   * Tests using the file upload route with any path prefixes being stripped.
    ...
    +  public function testFileUploadStrippedFilePath() {
    ...
    +    $response = $this->fileRequest($uri, $this->testFileData, ['Content-Disposition' => 'file; filename="directory/example.txt"']);
    ...
    +    // Check the actual file data. It should have been written to the configured
    +    // directory, not /foobar/directory/example.txt.
    +    $this->assertSame($this->testFileData, file_get_contents('public://foobar/example.txt'));
    

    Nice addition!

dawehner’s picture

I'm not too concerned about this, the probability of this happening is low enough that I'm not worried about it for now, and fixing it won't result in API changes. Great critical thinking though :) I'd be fine with addressing this for both REST file uploads and form file uploads in a follow-up issue.

I think one main difference is that by exposing a rest resource I think more people are willing to look into it ... I think making it easy to cause 500s is a problem we should avoid, by well, thinking about it beforehand. If there is just one less site down, this is totally worth doing!

That would mean emoji and even em-dashes in filenames would be a problem. Perhaps we can get by with just filename (and not filename*) and just pass unicode characters, but it'd mean violating the HTTP spec. Which also means that if your request has to pass proxies or middlewares, that the filename you passed could be stripped away, because it's technically invalid HTTP.

I think I agree with @Wim Leers we should implement the HTTP spec instead of our own custom solution ... at least though we should accept filename*.

But I think this should be a 503, with a Retry-After header. Because retrying in a few seconds likely would succeed. This then clearly communicates that the request itself is not at fault, it'll work fine a few seconds later.

Great idea!

damiankloip’s picture

StatusFileSize
new66.37 KB
new3.57 KB

OK, here are some changes to allow for using filename*. I haven't got into any business of trying to encode the string if there is the * present. Not sure we want to be going down that road... So this just adds support for the * but doesn't do anything behaviourally different based on that.

Also, changed the response for the locked file to 503 with Retry-After header. I just used 1 second for the value - seems to be any value is fine in practice.

I will do some cleanup on tests in a bit, and add some of the test cases for the filename* header too.

dawehner’s picture

I could imagine that we could do the encoding bit maybe in a follow up?

Status: Needs review » Needs work

The last submitted patch, 444: 1927648-444.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

damiankloip’s picture

RE the computed file URL property, we already have #2825487: Fix normalization of File entities: file entities should expose the file URL as a computed property on the 'uri' base field which is currently marked postponed. So I guess we need to progress there. Not sure if we should hold up this issue on that though. I guess we need to have the location of the newly uploaded file available :)

This patch:

  • Removes the FileDenormalizeTest, as discussed in previous comments
  • Fixes missing function parameter change (doh!)
  • Adds moar coverage for the various failing header test cases, and moves them into their own test function (was going to do a data provider, but that would do an install per data item :/)
tedbow’s picture

StatusFileSize
new5.08 KB
new74.32 KB
+++ b/core/modules/file/src/FileAccessControlHandler.php
@@ -59,10 +59,11 @@ protected function checkAccess(EntityInterface $entity, $operation, AccountInter
+      // Only admins or the file owner can delete and update the file entity.
+      // @todo Create a new permission to handle this?
+      if ($account->hasPermission('administer nodes') || ($account->id() == $file_uid)) {
+        return AccessResult::allowed()->cachePerPermissions();

Sorry if I missed this in the previous comments but what if the file is referenced in field that is attached to an entity type other than nodes. It seems from previous comments that adding a permission for this is not likely and would be confusing.

Shouldn't we be checking if the user has an admin permission for any of the entity types that file is attached to?

Also it seems like if the user can delete all the entities that reference the file that the user should also be able to delete the file. I imagine there will be many situations that the file is only attached to 1 entity.

Also if the user can update 1 entity that references the file shouldn't that user be able to update the file.

This patch adds \Drupal\file\FileAccessControlHandler::getOperationAccessOnReference which tries to implement this logic.

Status: Needs review » Needs work

The last submitted patch, 448: 1927648-448.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

wim leers’s picture

#444: You've proven that uploading filenames with unicode filenames work, at least when there's no proxy/middleware in between. That's important. It is technically speaking a violation of the HTTP spec though… still not entirely sure how I feel about that. Why do you feel okay/safe doing this?

I don't see explicit test coverage for filename* yet though. #447 added test coverage.


#447: Perhaps it's better to detect the filename* case and throw an appropriate HTTP error response? Because filename* would have that special prefix to specify the used encoding, which this patch wouldn't handle. Better to fail explicitly then. Not sure which HTTP status is the right one. I first thought https://httpstatuses.com/501, but that's apparently for the HTTP method, not for some request header.

RE: computed file URL property. Ugh yes… if we require that we have a computed file URL property before committing this, then this is blocked on #2825487: Fix normalization of File entities: file entities should expose the file URL as a computed property on the 'uri' base field, which is blocked on #2871591: Allow ComplexData in TypedData to specify computed properties that should be exposed in normalization and other contexts, which is in turn blocked on
But … if we don't block this on that issue, then we'll be shipping with REST file upload support of which the responses will change:

I'd rather not distract this issue with that, but if we change it later, it'll be a BC break.

Committing this without that will mean a guaranteed BC break, will it not?

P.S.: It'd be really good to get a review from you of #2871591: Allow ComplexData in TypedData to specify computed properties that should be exposed in normalization and other contexts!


#448: Shouldn't that be handled in a separate issue? Otherwise scope becomes very big here. Should this issue be blocked on that? This is definitely related to #2310307: File needs CRUD permissions to make REST work on entity/file/{id} and #2843139: EntityResource: Provide comprehensive test coverage for File entity, and tighten access control handler. Perhaps it should be done in the latter?

damiankloip’s picture

Yes, it seems like #448 should be done in a separate issue. This issue is big enough, complex enough, and long enough :) However, we do modify the access check here to check for 'administer nodes' here, which is not ideal. So maybe we need to break that out, have the smaller issue, and implement Ted's fixes there (they look sensible). I would say yes to Wim's question in #450 - we should probably block this on doing that AND the typed data normalization/export properties issue....

wim leers’s picture

Title: Allow creation of file entities from binary data » [PP-2] Allow creation of file entities from binary data

Okay so this patch is tantalizingly close to RTBC.

But it seems we're agreeing it's now blocked on two things:

  1. normalization of file entities — #2825487: Fix normalization of File entities: file entities should expose the file URL as a computed property on the 'uri' base field, which is itself blocked on #2871591: Allow ComplexData in TypedData to specify computed properties that should be exposed in normalization and other contexts
  2. access handling — most likely issue to do this in right now is #2843139: EntityResource: Provide comprehensive test coverage for File entity, and tighten access control handler, but it may need another issue
wim leers’s picture

What can still be done here, is the filename* stuff in #450.

aheimlich’s picture

Shouldn't this be doing to same file name munging that file_save_upload does?

tedbow’s picture

#452.1
I would love to see those issues get committed but does it need to postpone this 1. Is it BC break if we add extra fields to a REST response but don't change existing fields? We added extra fields here #2060677: Add target_type, target_uuid to serialized output of entity reference fields in non-HAL formats to existing responses and will likely add them here #2825812: ImageItem should have an "derivatives" computed property, to expose all image style URLs so it seems like we have do similar things before.

wim leers’s picture

#2901998: File size of 0 is not set when file entities are saved landed, hurray! That was another blocker I should've listed in #452, but forgot. So we're staying at PP-2.


I discussed #455 with @tedbow in chat:

Wim Leers [22 hours ago] 
But I _think_ you’re wondering/questioning whether the File Upload issue needs to be blocked on the File Normalization issue?


Ted Bowman [21 hours ago] 
Yes I don’t see why we need before that issue. unless we are saying we can’t add extra keys to normalized output. which I think we have done in the past


Wim Leers [21 hours ago] 
I’d agree if it was just that — but the patch in the normalization returns the public file URL for an existing field’s property (see `\Drupal\hal\Normalizer\FileEntityNormalizer::normalize()`). Which is NOT what that property contains: it contains `public://example.jpg`, not `http://d8.com/sites/default/files/example.jpg`. For the latter, we still need to add a new property. So … we’d be ADDING a property, yes, but we’d also be changing the value assigned to the normalization of the existing property!


Ted Bowman [20 hours ago] 
oh. yeah I didn’t get that.


Ted Bowman [20 hours ago] 
the other thing that I am finding confusing is that if I GET `http://www.octo2.dev/d8_3_rest/node/37?_format=hal_json`  which has file field I *don’t* get the `target_id` for the file back so I am not sure how I would use the new rest endpoint to update or delete the file from there


Ted Bowman [20 hours ago] 
I do get back the `target_id` if I use *json* format. am I missing something


Wim Leers [20 hours ago] 
You’re likely not missing anything, really … there is no test coverage for the REST support of File entities yet.


Wim Leers [20 hours ago] 
So these kinds of bugs are sadly not very surprising


Wim Leers [20 hours ago] 
That’s why https://www.drupal.org/node/2843139 is still open


Ted Bowman [20 hours ago] 
I am looking at that now. I will try again 8.5.x and see if that issue with `target_id` exists


Ted Bowman [20 hours ago] 
I am not sure I know what denormalized hal is suppose to look like. seems like a lot of stuff comes back in json but not hal. for files but also image height etc


Wim Leers [20 hours ago] 
Quite possibly the HAL normalization is broken.


Ted Bowman [20 hours ago] 
will check out existing test coverage



Ted Bowman [20 hours ago] 
@wim.leers looking at HalEntityNormalizationTrait this looks like intentional. but when I look at output there is not way to get to the file. https://gist.github.com/tedbow/06f009900dfbf76dc0d08231b2d54d61


Ted Bowman [20 hours ago] 
`// In the HAL normalization, reference fields are omitted, except for the bundle field.` (edited)


Ted Bowman [20 hours ago] 
you could get to other reference fields because they have links like `http://www.octo2.dev/d8_3_rest/taxonomy/term/1?_format=hal_json` so you know where to post to. but for  files you have `http://www.octo2.dev/d8_3_rest/sites/default/files/2017-09/webform1.png` with no idea where to post to and no file id if you knew the path


Ted Bowman [18 hours ago] 
@wim.leers created an issue https://www.drupal.org/node/2907402

So now we have #2907402: HAL normalization of file fields don't provide file entity id or file entity REST URL … which is basically confirming what I wrote at the top of #426 as being a problem. Let's fix that in #2907402: HAL normalization of file fields don't provide file entity id or file entity REST URL.

damiankloip’s picture

Status: Needs work » Needs review
StatusFileSize
new73.57 KB
new12.13 KB

@aheimlich has a good point in #454 I think. Nice work! I've implemented this in a new patch. As well as throwing an exception when the filename* key is used. Leaving this here for now until we iron out the blocking dependencies.

Rebased too, looks like #2720233 conflicted with this patch, in the RequestHandler class. This should be working ok, we might want to see if we can further refactor the request handler a little though, it might be able to have another method to reuse the argument handling and warning that's triggered.

This interdiff is from #447. as I think we agreed @todbow's work was moving to the other issue?

tedbow’s picture

+++ b/core/modules/file/src/FileAccessControlHandler.php
@@ -59,10 +59,11 @@ protected function checkAccess(EntityInterface $entity, $operation, AccountInter
+      $file_uid = $entity->get('uid')->target_id;
+      // Only admins or the file owner can delete and update the file entity.
+      // @todo Create a new permission to handle this?
+      if ($account->hasPermission('administer nodes') || ($account->id() == $file_uid)) {
+        return AccessResult::allowed()->cachePerPermissions();

I would say we remove 'administer nodes' permission this issue. Won't this be security violation if the file was attached to another entity type that use doesn't have permission to administer,delete or edit.

I think we agreed @todbow's work was moving to the other issue?

Yes I think this issue #2843139: EntityResource: Provide comprehensive test coverage for File entity, and tighten access control handler but still think we should remove the above permission for now and add a todo pointing to #2843139

+++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
@@ -372,6 +383,46 @@ protected function validate(FileInterface $file, FieldDefinitionInterface $field
+    if (!\Drupal::config('system.file')->get('allow_insecure_uploads') && preg_match('/\.(php|pl|py|cgi|asp|js)(\.|$)/i', $filename) && (substr($filename, -4) != '.txt')) {

This copied from file_save_upload, correct?

Should we somehow consolidate this code? at least the insecure file extensions?

Otherwise if we ever need to add another insecure file extension we have to remember to add it to both places.

wim leers’s picture

  1. #458: agreed that the FileAccessControlHandler change shouldn't happen here. I did some archeology: it was added in #1927648-197: Allow creation of file entities from binary data via REST requests by @marthinal. What happens if we just delete this? The test coverage has improved a lot since then (~1.5 year ago), so perhaps it's no longer necessary?
  2. +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
    @@ -355,14 +364,16 @@ protected function validateAndLoadFieldDefinition($entity_type_id, $bundle, $fie
    +   *   AN array of upload validators to pass to file_validate().
    

    Nit: s/AN/An/

  3. +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
    @@ -372,6 +383,46 @@ protected function validate(FileInterface $file, FieldDefinitionInterface $field
    +  protected function prepareFilename($filename, array &$validators) {
    

    This could benefit from an @see file_save_upload() I think?

    (Basically echoing the second half of #458.)

  4. +++ b/core/modules/rest/tests/src/Functional/FileUploadResourceTestBase.php
    @@ -215,24 +189,19 @@ public function testPostFileUploadInvalidHeaders() {
    +    // Using filename* extended format is not currently supported.
    +    $response = $this->fileRequest($uri, $this->testFileData, ['Content-Disposition' => 'filename*="UTF-8 \' \' example.txt"']);
    +    $this->assertResourceErrorResponse(400, 'The extended "filename*" format is currently not supported in the "Content-Disposition header"', $response);
    

    Yay for explicit test coverage.

damiankloip’s picture

Status: Needs review » Postponed

Yes, I was planning to just remove most/all of that stuff but figured we might as well leave this as a passing patch until we can remove it and still have it passing.

#458: Yes, I think moving the insecure file extensions for another function or a constant is a good plan!

#459 We should see if we can just remove this now. I would have to look properly to be sure :)

wim leers’s picture

#2901704: Allow REST routes to use different request formats and content type formats landed! But #2901704 was not reflected in the "PP-2" (see #452), so we remain at that level.

damiankloip’s picture

Status: Postponed » Needs review
StatusFileSize
new67.07 KB
new12.52 KB

Here is a patch that rebases against 8.5.x (so #2901704: Allow REST routes to use different request formats and content type formats changes that we already got committed can be removed from this patch) and removes the changes in FileAccessControlHandler. Let's see how the tests get on!

wim leers’s picture

Title: [PP-2] Allow creation of file entities from binary data » [PP-1] Allow creation of file entities from binary data
Status: Needs review » Postponed

That passed! Which makes sense — we're POSTing a File here, the changes to FileAccessControlHandler were only for PATCH and DELETE. Hurray, we were right in #458+#459 that the access handling improvement/fix doesn't need to block this!

Which means we're down to 1 blocker: #2825487: Fix normalization of File entities: file entities should expose the file URL as a computed property on the 'uri' base field.

damiankloip’s picture

Whoooooop! yes, indeed :) Hopefully..

wim leers’s picture

Title: [PP-1] Allow creation of file entities from binary data » [PP-2] Allow creation of file entities from binary data
wim leers’s picture

Title: [PP-2] Allow creation of file entities from binary data » [PP-1] Allow creation of file entities from binary data
garphy’s picture

Issue tags: +Needs reroll

#462 does not apply cleanly on 8.5.x any more.

garphy’s picture

Issue tags: -Needs reroll
StatusFileSize
new67.07 KB

Rerolled.

webchick’s picture

Issue summary: View changes

Updating the issue summary to indicate at the top what this is postponed on.

benjy’s picture

Applying the patch in #448 with 8.4.x breaks one of my tests. Afterwards I have the XML route enabled even though my config looks like this:

uuid: 6a0edca9-4a10-4a87-9ba3-8af3d3d1d0a7
langcode: en
status: true
dependencies:
  module:
    - serialization
    - unearthed_forum
    - user
id: unearthed_forum_subscription
plugin_id: unearthed_forum_subscription
granularity: resource
configuration:
  methods:
    - GET
    - POST
  formats:
    - json
  authentication:
    - cookie

I end up with the error No route found for the specified format html. in a test when running the following code

$url = Url::fromUri('internal:/api/forum/subscriptions/' . $this->question->id()

This is because AcceptHeaderMatcher::filter can no longer detect the correct default format since there are multiple routes enabled.

// If the route has a format requirement, then verify that the
// resource has it.
$format_requirement = $route->getRequirement('_format');
if ($format_requirement && !in_array($format_requirement, $rest_resource_config->getFormats($method))) {
  continue;
}

This is the hunk of code that was removed that causes the issue. Brining that back fixes my issue.

wim leers’s picture

#470: any chance you wanted to post this to #2858482: Simplify REST routing: disallow requesting POST/PATCH in any format, make consistent, i.e. that you mixed up two issues?

benjy’s picture

I wasn't actually aware of the other issue, looks like the patch there was merged into the patch in #448

wim leers’s picture

@benjy: can you move your question over to #2858482: Simplify REST routing: disallow requesting POST/PATCH in any format, make consistent, if you can reproduce it with just that patch? That's where those changes are actively being worked on. Plus, this issue is approaching 500 comments, so keeping this on-topic as much as possible can only help :) Thanks!

wim leers’s picture

Title: [PP-1] Allow creation of file entities from binary data » Allow creation of file entities from binary data
Assigned: Unassigned » damiankloip
Status: Postponed » Needs work
wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new730 bytes
new68.64 KB

I just talked to @damiankloip, he will get to this soon. Maybe tomorrow, otherwise in the first days of the next year :)

In the mean time, here's a rebased patch already — several conflicts had to be resolved.

Also includes a tiny interdiff to add a placeholder implementation, which is required since #2765959: Make 4xx REST responses cacheable by (Dynamic) Page Cache + comprehensive cacheability test coverage (which landed on November 30, quite a bit after this patch was last green). This allows the tests to at least run (but fail), and not cause a fatal PHP error.

wim leers’s picture

StatusFileSize
new795 bytes
new68.75 KB

#2825487: Fix normalization of File entities: file entities should expose the file URL as a computed property on the 'uri' base field added a new url non-internal computed property. This shows up in the normalization. Updating the expectations with a trivial one-line addition to account for this allows all non-HAL tests to pass.

I'll leave updating HAL tests for @damiankloip. The current tests assume the BC layer behavior. Does it make sense to test the BC layer here if we already have \Drupal\Tests\hal\Functional\EntityResource\File\FileHalJsonAnonTest::testGetBcUriField() for that? I'm not sure, but I personally lean towards no.

Now Damian can focus on the truly interesting bits: I handled the trivial stuff in this comment/patch and the previous one!

The last submitted patch, 475: 1927648-475.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

Status: Needs review » Needs work

The last submitted patch, 476: 1927648-476.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

damiankloip’s picture

Status: Needs work » Needs review
StatusFileSize
new67.72 KB
new1.56 KB

Nice work Wim!

I agree, we shouldn't need to test the BC case here, as we already have explicit coverage for that. As long as we know this returns us a good response in the current normalization format, I think we're good.

Here is a patch to remove the uri override for hal tests. The default behaviour should now give us the correct normalization result I think. I also added a comment for getExpectedUnauthorizedAccessCacheability.

Status: Needs review » Needs work

The last submitted patch, 479: 1927648-479.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

wim leers’s picture

I wrote in #452 that this was tantalizingly close to RTBC, on September 6.

Time for another round of critical, detailed review!

  1. +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
    @@ -0,0 +1,526 @@
    + * File upload resource
    

    Nit: Missing trailing period.

  2. +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
    @@ -0,0 +1,526 @@
    +  /**
    +   * @var \Drupal\Core\File\FileSystem
    +   */
    ...
    +  /**
    +   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
    +   */
    

    Nit: needs documentation lines.

  3. +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
    @@ -0,0 +1,526 @@
    +   * @var \Symfony\Component\Serializer\SerializerInterface|SerializerInterface
    

    Incorrect typehint.

  4. +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
    @@ -0,0 +1,526 @@
    +   *   The token replacement instance.
    +   *
    +   */
    

    Nit: extraneous empty line.

  5. +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
    @@ -0,0 +1,526 @@
    +      'filename' => $prepared_filename,
    +      'filemime' => $this->mimeTypeGuesser->guess($prepared_filename),
    

    Let's use setFilename(), setMimeType() etc.

  6. +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
    @@ -0,0 +1,526 @@
    +    // Validate the file entity against entity level validation and field level
    

    Nit: s/entity level/entity-level/, s/field level/field-level/

  7. +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
    @@ -0,0 +1,526 @@
    +      // @todo Do we want the File URI (for the actual file) like this?
    +      'Location' => $file->url(),
    

    We need to address this @todo still.

  8. +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
    @@ -0,0 +1,526 @@
    +    // 'rb' is needed so reading works correctly on windows environments too.
    

    Nit: s/windows/Windows/

  9. +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
    @@ -0,0 +1,526 @@
    +   * @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
    ...
    +   * @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
    +   * @throws \Symfony\Component\HttpFoundation\File\Exception\AccessDeniedException
    ...
    +   * @throws \Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException
    

    Missing comments explaining *when* these ezxceptions are thrown.

  10. +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
    @@ -0,0 +1,526 @@
    +   * @return \Drupal\Core\Field\FieldDefinitionInterface
    +   *
    

    Missing accompanyhing comment.

  11. +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
    @@ -0,0 +1,526 @@
    +    // @todo check the definition is a file field.
    +    $field_definition = $field_definitions[$field_name];
    

    Another todo that needs to be resolved.

  12. +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
    @@ -0,0 +1,526 @@
    +   *   AN array of upload validators to pass to file_validate().
    

    Nit: s/AN/An/

  13. +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
    @@ -0,0 +1,526 @@
    +    if (!empty($errors)) {
    +      $message = "Unprocessable Entity: file validation failed.\n";
    +      $message .= implode("\n", $errors);
    +
    +      throw new UnprocessableEntityHttpException($message);
    +    }
    

    We "plain text ify" the violations (which can contain HTML), just like \Drupal\rest\Plugin\rest\resource\EntityResourceValidationTrait::validate() already does.

  14. +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
    @@ -0,0 +1,526 @@
    +      '_controller' => 'Drupal\rest\RequestHandler::handleRaw',
    

    Nit: let's use RequestHandler::class.

  15. +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
    @@ -0,0 +1,526 @@
    +   *   THe file URI.
    

    Nit: s/THe/The/

  16. +++ b/core/modules/hal/tests/src/Functional/EntityResource/File/FileUploadHalJsonTestBase.php
    @@ -0,0 +1,76 @@
    +    // Hal adds reference fields in links and embedded.
    

    Nit: The HAL normalization adds entity reference fields to '_links' and '_embedded'.

  17. +++ b/core/modules/media/tests/src/Functional/MediaFunctionalTestTrait.php
    @@ -1,6 +1,7 @@
    +use Drupal\Tests\rest\Functional\FileUploadResourceTestBase;
    

    Nit: unnecessary change, can be reverted.

  18. +++ b/core/modules/media/tests/src/Functional/MediaFunctionalTestTrait.php
    --- a/core/modules/rest/src/EventSubscriber/ResourceResponseSubscriber.php
    +++ b/core/modules/rest/src/EventSubscriber/ResourceResponseSubscriber.php
    

    The changes here appear to revert #2901704: Allow REST routes to use different request formats and content type formats?

  19. +++ b/core/modules/rest/src/EventSubscriber/ResourceResponseSubscriber.php
    --- a/core/modules/rest/src/RequestHandler.php
    +++ b/core/modules/rest/src/RequestHandler.php
    

    FYI: this is going to conflict with #2853460: Simplify RequestHandler: make it no longer ContainerAware, split up ::handle(), but it's definitely going to mean fewer changes are necessary here :)

  20. +++ b/core/modules/rest/tests/src/Functional/FileUploadResourceTestBase.php
    @@ -0,0 +1,593 @@
    +  public function testPostFileUpload() {
    +    $this->initAuthentication();
    +
    +    $this->provisionResource([static::$format], static::$auth ? [static::$auth] : [], ['POST']);
    +
    +    $this->setUpAuthorization('POST');
    

    We should test that a 403 response is sent when doing a request before authorization is granted!

damiankloip’s picture

Status: Needs work » Needs review
StatusFileSize
new66.08 KB
new7.01 KB

ok, this should fix the hal normalization issues, and address some other points me and Wim spoke though:

- Remove Location header setting. We can't do anything meaningful with this right now, and since we return the normalized file entity in the response now, we have all the info available to us currently. This might make sense to add at a later stage WHEN we have a file link template/resource we can reliably generate a link for.
- Implements getExpectedUnauthorizedAccessMessage
- Add comments for null implementations of assertNormalizationEdgeCases (and getExpectedUnauthorizedAccessCacheability in previous patch)
- Check file target type in FileUploadResource::validateAndLoadFieldDefinition
- Add 403 check in testPost and use getExpectedUnauthorizedAccessMessage
- Reverts changes reverted in error to ResourceResponseSubscriber (tests pass locally)

Let's see how this gets on with tests.

edit - x-post with Wim!

Status: Needs review » Needs work

The last submitted patch, 482: 1927648-481.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

wim leers’s picture

#482: reviewed this interdiff too:

  1. +++ b/core/modules/rest/tests/src/Functional/FileUploadResourceTestBase.php
    @@ -424,14 +427,16 @@ public function testFileUploadMaliciousExtension() {
    +    return sprintf('You are not authorized to create this file entity');
    

    No need for sprintf() :)

  2. The two new failures are because FileUploadResource inherits \Drupal\rest\Plugin\ResourceBase::permissions(). But just like EntityResource::permissions() overrides it parent based on the bc_entity_resource_permissions config flag, we want to override it. But without any BC layer, because file uploading was never supported, so no need for BC. So, basically, I think we should just add this to FileUploadResource:
      /**
       * {@inheritdoc}
       */
      public function permissions() {
        // Rely on the Entity Access API rather than REST-specific permissions.
        // @see \Drupal\rest\Plugin\rest\resource\EntityResource::permissions()
        return [];
      }
    
  3. +++ b/core/modules/hal/tests/src/Functional/EntityResource/File/FileUploadHalJsonTestBase.php
    @@ -41,7 +41,8 @@ protected function getExpectedNormalizedEntity($fid = 1, $expected_filename = 'e
    -            'href' => $normalization['uri'][0]['value'],
    +            // This link is generated from File::url().
    +            'href' => file_create_url($normalization['uri'][0]['value']),
    

    Let's add a @todo pointing to #2907402: HAL normalization of file fields don't provide file entity id or file entity REST URL.

damiankloip’s picture

Great, Let me address that soon! duh on on sprintf :) just forgot to remove it when I took the placeholders out of the original string I copied from.

damiankloip’s picture

Status: Needs work » Needs review
StatusFileSize
new67.06 KB
new12.73 KB

ok.. This adresses all of the feedback now (I think) except the last #481.20. I am just out of time today and need to head off. I have left a "// @todo FOR WIM." comment in there. Aside from that, I think this should pass now.

wim leers’s picture

StatusFileSize
new6.01 KB
new67.58 KB
  1. +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
    @@ -151,6 +162,16 @@ public static function create(ContainerInterface $container, array $configuratio
    +    // Access to this resource depends on field level access so no explicit
    +    // permissions are required.
    +    return [];
    

    Let's add some @sees here.

  2. +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
    @@ -239,9 +262,11 @@ public function post(Request $request, $entity_type_id, $bundle, $field_name) {
    +   *   Throws when input data cannot be read, the temporary file cannot be
    
    @@ -290,6 +315,7 @@ protected function streamUploadData() {
    +   *   Throws when the 'Content-Disposition' request header is invalid.
    
    @@ -331,9 +357,13 @@ protected function validateAndParseContentDispositionHeader(Request $request) {
    +   *   Throws when the field does not exist.
    ...
    +   *  Throws when the target type of the field is not a file, or the current
    
    @@ -364,9 +394,10 @@ protected function validateAndLoadFieldDefinition($entity_type_id, $bundle, $fie
    +   *   Throws when there are file validation errors.
    

    s/Throws/Thrown/

  3. +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
    @@ -331,9 +357,13 @@ protected function validateAndParseContentDispositionHeader(Request $request) {
    +   *  user does not have create access for the field.
    

    Incorrect indentation.

  4. +++ b/core/modules/rest/tests/src/Functional/FileUploadResourceTestBase.php
    @@ -135,8 +136,9 @@ public function testPostFileUpload() {
    +    // @todo FOR WIM.
    +    // $response = $this->fileRequest($uri, $this->testFileData);
    +    // $this->assertResourceErrorResponse(403, $this->getExpectedUnauthorizedAccessMessage('POST'), $response);
    

    Done :) (In hopes of this still shipping with 8.5.0.)

    The reason this didn't work is due to one of those weird Field API design leftovers from before it even was in core: there is no create operation, only a edit operation, which is for both the "create" and "update" cases!

    However, when I fix that, field access is still allowed… because we should also have been checking entity access.

    Let's go back to where this approach was introduced. It's based on file_entity maintainer @Berdir's recommendations in #1927648-326: Allow creation of file entities from binary data via REST requests (from May 9, 2017, precisely 8 months ago today!), where he wrote this:

    t would also fix the permission problem, because we can easily check edit access to that specific field for that node type. That means you can really only upload files if there's at least one file/image field on an entity type/bundle that you are allowed to edit.

    Everybody agreed, but everybody overlooked the detail that the patch was only checking field access and not also entity access. Because there were other problems that had to be dealt with to get the patch to work at all. And so we collectively lost sight of this. Until now, because everything's working fine, and access checking was the last thing that didn't yet have test coverage!

    (@damiankloip even mentioned this in #1927648-359: Allow creation of file entities from binary data via REST requests and #1927648-362: Allow creation of file entities from binary data via REST requests: My test is failing on the field 'create' access check, not sure why yet. Need to look into this. Probably insufficient perms granted for the test user... + I'm not sure what I'm doing wrong with the access check, I thought if the permission to create the entity was correct it should be ok, but maybe the access checking is not correct?
    .)

    Fixed now!

wim leers’s picture

Title: Allow creation of file entities from binary data » Allow creation of file entities from binary data via REST requests
Status: Needs review » Needs work
Issue tags: +Needs change record, +Needs issue summary update

So, time for a final review!

  1. +++ b/core/modules/file/src/FileServiceProvider.php
    @@ -0,0 +1,24 @@
    +    if ($container->has('http_middleware.negotiation') && is_a($container->getDefinition('http_middleware.negotiation')->getClass(), NegotiationMiddleware::class, TRUE)) {
    

    This looks weird maybe, but it's how it's done. Identical to the logic in \Drupal\hal\HalServiceProvider::alter().

  2. +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
    @@ -0,0 +1,564 @@
    +/**
    + * File upload resource.
    + *
    + * @RestResource(
    + *   id = "file:upload",
    + *   label = @Translation("File Upload"),
    + *   serialization_class = "Drupal\file\Entity\File",
    + *   uri_paths = {
    + *     "https://www.drupal.org/link-relations/create" = "/file/upload/{entity_type_id}/{bundle}/{field_name}"
    + *   }
    + * )
    + */
    

    This should probably document why we went with this approach. We should summarize @Berdir's comment in #326 for that.

    (We'll also need that for the change record.)

  3. +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
    @@ -0,0 +1,564 @@
    +  const FILENAME_REGEX = '@\bfilename(?<star>\*?)=\"(?<filename>.+)\"@';
    

    Perhaps rename this to REQUEST_HEADER_FILENAME_REGEX?

  4. +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
    @@ -0,0 +1,564 @@
    +  }
    +
    +
    +  /**
    

    Nit: extraneous \n.

  5. +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
    @@ -0,0 +1,564 @@
    +      'uid' => $this->currentUser->id(),
    +      'filename' => $prepared_filename,
    +      'filemime' => $this->mimeTypeGuesser->guess($prepared_filename),
    +      'uri' => $file_uri,
    +      // Set the size. This is done in File::preSave() but we validate the file
    +      // before it is saved.
    +      'filesize' => @filesize($temp_file_path),
    

    Should use the setters where possible. (Already mentioned in #481.5.)

  6. +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
    @@ -0,0 +1,564 @@
    +    $entity_access_control_handler = $this->entityTypeManager->getAccessControlHandler($entity_type_id);
    +
    +    $access_result = $entity_access_control_handler->createAccess($bundle, NULL, [], TRUE)
    

    Nit: I introduced an extraneous \n here in my previous reroll, sorry.

  7. +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
    @@ -0,0 +1,564 @@
    +    // Allow all serializer formats to be returned as a response format.
    +    $requirements['_format'] = implode('|', $this->serializerFormats);
    
    +++ b/core/modules/rest/src/Routing/ResourceRoutes.php
    @@ -108,20 +108,12 @@ protected function getRoutesForResourceConfig(RestResourceConfigInterface $rest_
    -        // If the route has a format requirement, then verify that the
    -        // resource has it.
    -        $format_requirement = $route->getRequirement('_format');
    -        if ($format_requirement && !in_array($format_requirement, $rest_resource_config->getFormats($method))) {
    -          continue;
    -        }
    -
    

    #2858482: Simplify REST routing: disallow requesting POST/PATCH in any format, make consistent has been RTBC for a while and will change this.

    I think it's better to incorporate the bits from #2858482: Simplify REST routing: disallow requesting POST/PATCH in any format, make consistent that will land there, so that we know it actually will work.

    In doing so, we can remove the allowing of all formats in FileUploadResource::getBaseRouteRequirements()!

  8. +++ b/core/modules/hal/tests/src/Functional/EntityResource/File/FileUploadHalJsonTestBase.php
    @@ -0,0 +1,81 @@
    +    return $normalization + [
    +        '_links' => [
    +          'self' => [
    

    Indentation is off here.

  9. +++ b/core/modules/hal/tests/src/Functional/EntityResource/File/FileUploadHalJsonTestBase.php
    @@ -0,0 +1,81 @@
    +  }
    +
    +
    +}
    

    More whitespace clean-up necessary.

  10. +++ b/core/modules/rest/src/EventSubscriber/ResourceResponseSubscriber.php
    @@ -89,7 +89,7 @@ public function onResponse(FilterResponseEvent $event) {
    -   * forget to specify a response format in case of a POST or PATCH. Rather than
    +   * forget to specify a request format in case of a POST or PATCH. Rather than
    
    @@ -119,9 +119,6 @@ public function getResponseFormat(RouteMatchInterface $route_match, Request $req
    -    // If a request body is present, then use the format corresponding to the
    -    // request body's Content-Type for the response, if it's an acceptable
    -    // format for the request.
    

    Unnecessary comment-only changes. Let's revert these.

  11. +++ b/core/modules/rest/src/Routing/ResourceRoutes.php
    @@ -108,20 +108,12 @@ protected function getRoutesForResourceConfig(RestResourceConfigInterface $rest_
    +        // @todo clean this up further in https://www.drupal.org/node/2858482
    

    That patch already is RTBC, we should clean it up here. Most importantly, we need to document the rationale behind this logic.

    Also see the previous point: we should be compatible with what #2858482: Simplify REST routing: disallow requesting POST/PATCH in any format, make consistent does.

  12. +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
    --- /dev/null
    +++ b/core/modules/rest/tests/src/Functional/FileUploadResourceTestBase.php
    

    More whitespace clean-up necessary here.

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new12.45 KB
new66.12 KB

This fixes all whitespace/docs nitpicks (4+6+8+9+10+12).

wim leers’s picture

StatusFileSize
new2.31 KB
new66.36 KB
wim leers’s picture

So that leaves just points 2 + 3 + 5 from #488 to be addressed. Leaving that for @damiankloip.

Really tempted to RTBC this already. Because 2+3+5 really are just nits. But I'm pretty sure committers will raise those too. And for committers to even look at this issue, we definitely need an issue summary at the very least, and most likely a CR to announce this new, long-sought after capability!

(This issue currently has 418 comments, which is the second most of all open issues for Drupal core — only #1356276: Allow profiles to define a base/parent profile has more: 424.)

(This issue also likely has one of the highest numbers of followers: 127!)

Keeping at Needs review, because this definitely could use reviews.

Status: Needs review » Needs work

The last submitted patch, 490: 1927648-490.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

tedbow’s picture

reviewing #489 b/c #490 failed but the interdiff is small.

couldn't figure out why #490 failed

  1. +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
    @@ -0,0 +1,559 @@
    +        if ($read === FALSE) {
    +          // Close the temp file stream.
    +          fclose($temp_file);
    +          $this->logger->error('Input data could not be read');
    +          throw new HttpException(500, 'Input file data could not be read');
    +        }
    +
    +        if (fwrite($temp_file, $read) === FALSE) {
    +          // Close the temp file stream.
    +          fclose($temp_file);
    +          $this->logger->error('Temporary file data for "%path" could not be written', ['%path' => $temp_file_path]);
    +          throw new HttpException(500, 'Temporary file data could not be written');
    +        }
    

    Been a while since I dealt with reading file streams in PHP but I wondering... If we have no exceptions here at the end we call fclose($file_data) to close the input stream but if encounter 1 of the 3 possible exceptions we don't. We only call fclose($temp_file).

    Should call fclose($file_data) in the exception cases?

  2. +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
    --- /dev/null
    +++ b/core/modules/file/tests/src/Functional/FileUploadJsonBasicAuthTest.php
    
    +++ b/core/modules/rest/tests/src/Functional/FileUploadResourceTestBase.php
    @@ -0,0 +1,576 @@
    +    $this->assertTrue(file_exists('public://foobar/example.php_.txt'));
    

    To be extra careful should we:
    $this->assertFalse(file_exists('public://foobar/example.php.txt'));

  3. +++ b/core/modules/rest/tests/src/Functional/FileUploadResourceTestBase.php
    @@ -0,0 +1,576 @@
    +    $this->assertResponseData($expected, $response);
    

    Just to be safe why don't we again check if the file actually exists?

  4. +++ b/core/modules/rest/tests/src/Functional/FileUploadResourceTestBase.php
    @@ -0,0 +1,576 @@
    +    $this->assertResponseData($expected, $response);
    

    Check for file existing also?

berdir’s picture

Title: Allow creation of file entities from binary data via REST requests » Allow creation of file entities from binary data
Status: Needs work » Needs review
Issue tags: -Needs change record, -Needs issue summary update
  1. +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
    @@ -0,0 +1,564 @@
    +    $access_result = $entity_access_control_handler->createAccess($bundle, NULL, [], TRUE)
    +      ->andIf($entity_access_control_handler->fieldAccess('edit', $field_definition, NULL, NULL, TRUE));
    +    if (!($entity_access_control_handler->createAccess($bundle) && $entity_access_control_handler->fieldAccess('edit', $field_definition))) {
    +      throw new AccessDeniedHttpException($access_result->getReason());
    

    Yes, this is certainly better than the previous create access check which doesn't exist on field-level.

    The bundle usage is not quite correct yet, however. While a field is always added to a "bundle", in case of a non-bundleable entity type like user, the bundle is the same as the entity type.

    In that case, it is however not correct to pass that as the bundle to the createAccess() call. I can't imagine it breaking anything, if there is no bundle, then nobody should rely on it being or not being there, but who knows.

    So the currect thing would be to do something like also getting the entity_type and then doing $entity_type->hasKey('bundle') ? $bundle : NULL as the argument to createAccess()

  2. +++ b/core/modules/hal/src/Normalizer/FileEntityNormalizer.php
    @@ -73,16 +69,4 @@ public function normalize($entity, $format = NULL, array $context = []) {
    -  /**
    -   * {@inheritdoc}
    -   */
    -  public function denormalize($data, $class, $format = NULL, array $context = []) {
    -    $file_data = (string) $this->httpClient->get($data['uri'][0]['value'])->getBody();
    -
    

    I didn't even remember we had this stuff.

    Isn't just removing it kind of a BC break even if there is now a better way of doing this? That means it should be deprecated now, not removed? I guess it's tied to the now deprecated opposite feature in normalize(), can we hide it between the same config check? Or alternatively, just look if we have an actual external URL on this property that we could download instead of unconditionally downloading it? then we could do that and trigger a deprecation message? Always doing that seems weird anyway.

  3. +++ b/core/modules/rest/tests/src/Functional/FileUploadResourceTestBase.php
    @@ -0,0 +1,600 @@
    +   * Tests using the file upload POST route.
    +   */
    +  public function testPostFileUpload() {
    +    $this->initAuthentication();
    

    Maybe I missed it, but I think we now don't have an actual test that tests the whole process, might make sense to have that?

    Meaning:

    1. Upload a file
    2. Create an EntityTest or other entity that references that file, with a custom description or so, make sure everything is properly saved. ( Wondering if file description/alt/title currently works at all.. I currently have a custom normalizer in file_entity to support that)

    If we have a follow-up for that, also fine. And it should then probably also be documented somewhere?

    And the same could also be done for the media scenario:

    1. Upload a file
    2. Create a media entity that uses that file
    3. Create an EntityTest/Something entity that uses the media. Make sure everyhting is created properly.

  4. +++ b/core/modules/rest/tests/src/Functional/FileUploadResourceTestBase.php
    @@ -0,0 +1,600 @@
    +    $uri = Url::fromUri('base:' . static::$postUri);
    +
    +    $response = $this->fileRequest($uri, $this->testFileData, ['Content-Disposition' => 'file; filename="directory/example.txt"']);
    +
    +    $this->assertSame(201, $response->getStatusCode());
    +
    +    $expected = $this->getExpectedNormalizedEntity();
    +    $this->assertResponseData($expected, $response);
    +
    +    // Check the actual file data. It should have been written to the configured
    +    // directory, not /foobar/directory/example.txt.
    +    $this->assertSame($this->testFileData, file_get_contents('public://foobar/example.txt'));
    

    what would be the reason someone provides a path and not just a filename? Is there a reason we silently ignore that instead of returning an error?

  5. +++ b/core/modules/rest/tests/src/Functional/FileUploadResourceTestBase.php
    @@ -0,0 +1,600 @@
    +
    +    // Check the actual file data. It should have been written to the configured
    +    // directory, not /foobar/directory/example.txt.
    +    $this->assertSame($this->testFileData, file_get_contents('public://foobar/example-✓.txt'));
    

    Note: This will/would change if we do #2492171: Provide options to sanitize filenames (transliterate, lowercase, replace whitespace, etc). Nothing wrong with it, just pointing out that we'll need to update this test then in that issue.

berdir’s picture

Status: Needs review » Needs work
Issue tags: +Needs change record, +Needs issue summary update

Crap, I didn't want to change the issue title/tags.

wim leers’s picture

Title: Allow creation of file entities from binary data » Allow creation of file entities from binary data via REST requests

#493: Thanks for the review, @tedbow! Thanks for helping us to be even more cautious! :) Leaving those for @damiankloip to address.

#494: Thanks for the review, @Berdir!

  1. Good thing we have an Entity API maintainer review this :)
  2. This should change should have been documented in the issue summary when it was made. So … I set out to scan the entire issue for all mentions of FileEntityNormalizer and summarize the relevant bits here:
    • the same question was asked first by @dawehner in #1927648-354: Allow creation of file entities from binary data via REST requests.6 (in March 2017), answered by @damiankloip in #1927648-356: Allow creation of file entities from binary data via REST requests: Well, that functionality doesn't really work anyway, it only saves files in temp. I would be very surprised if this was being used, and I think we should definitely not support it either. It has some of the same failings that is a big reason we cannot just have a generic upload endpoint.
    • questionable security (@damiankloip at the end of #438: OR we bring back the old functionality too. I think that is inherently insecure (?) though anyway, as it tries to make an HTTP request out to get a file and download it.)
    • FileEntityNormalizer was always meant to be temporary … see #426.1
  3. 👌 Excellent catch! You're absolutely right that we should create an EntityTest entity that actually uses this file.

    For the Media use case, I think that's in scope for #2895573: Decide on normalization of Media module entities. We should add a @todo for that.

  4. Why would somebody do that? Because they got it wrong. Why that doesn't result in an error? Because @dawehner pointed to https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Dispos... in #440.1, and @damiankloip implemented that in #441. That says: path information should be stripped. Let's add that to the docs
  5. Cool!
wim leers’s picture

Assigned: damiankloip » wim leers

Working on addressing #490 failing tests and addressing most of @Berdir's comment.

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new7.32 KB
new68.77 KB

Fixed #496.1 + #496.3 + #496.4. All points in #496 have been addressed. But #496.2 needs further discussion.

Also, for #496.3: test coverage for hal_json is currently impossible because of a massive bug in the HAL module. Which further stresses the fact that nobody can really be using HAL's FileEntityNormalizer::denormalize(), because it's literally impossible to use those files in a @FieldType=file field! 😮 Created #2935738: Entity reference field subclasses (such as file and image fields) lose the non-default properties upon denormalization, in both hal_json and json for that.

(This is relative to #489, still need to figure out why exactly #490 was failing.)

wim leers’s picture

Regarding #496.2: I don't think we should worry about breaking BC for FileEntityNormalizer::denormalize() if A) it was always meant to be temporary, B) it has questionable security, C) any file uploaded through it will always be temporary://, D) if you try to reference files (FileItem) or images (ImageItem) uploaded via it, you can't: #2935738: Entity reference field subclasses (such as file and image fields) lose the non-default properties upon denormalization, in both hal_json and json.

Is there actually anything that uses FileEntityNormalizer::denormalize()? Because even https://www.drupal.org/project/default_content says

Supports files if you have File entity

In other words: it doesn't use/support the FileEntityNormalizer in core's HAL module.

wim leers’s picture

StatusFileSize
new65.15 KB
new105.14 KB

So regarding #490: let's do this properly. Let's rebase this patch on top of both #2858482: Simplify REST routing: disallow requesting POST/PATCH in any format, make consistent (which refactors \Drupal\rest\Routing\ResourceRoutes into sanity) and #2853460: Simplify RequestHandler: make it no longer ContainerAware, split up ::handle() (which refactors \Drupal\rest\RequestHandler into sanity).

That'll make this patch A) simpler, B) more future-proof (because those patches are likely to land very soon, before this). Because, frankly, this patch is hard enough to review and commit without the extensive refactoring of RequestHandler and significant changes to ResourceRoutes. Letting other issues handle those will make this issue's scope more manageable (and will also make the long-overdue issue summary update simpler).

(Actually, #2853460: Simplify RequestHandler: make it no longer ContainerAware, split up ::handle() landed while I was working on this, hurray!)

In #498
 core/modules/rest/src/RequestHandler.php                                                     | 140 ++++++++++++++---
 core/modules/rest/src/Routing/ResourceRoutes.php                                             |  12 +-
…
 15 files changed, 1572 insertions(+), 159 deletions(-)
In this patch
 core/modules/rest/src/RequestHandler.php                                                     |  60 +++++++-
 core/modules/rest/src/Routing/ResourceRoutes.php                                             |   3 +-
…
 15 files changed, 1505 insertions(+), 137 deletions(-)

This resulted in 1927648-501-do-not-test.patch — that's #498 rebased on top of both #2853460: Simplify RequestHandler: make it no longer ContainerAware, split up ::handle() (which already landed) and #2858482: Simplify REST routing: disallow requesting POST/PATCH in any format, make consistent (which is RTBC).

To allow for testing this patch today, I have attached 1927648-501-combined.patch, which includes #2858482.

wim leers’s picture

StatusFileSize
new2.42 KB
new65.58 KB
new105.07 KB

And now re-applying #490's interdiff again actually works fine, because we're building this on top of #2858482: Simplify REST routing: disallow requesting POST/PATCH in any format, make consistent, which makes the code in HEAD much saner.

wim leers’s picture

Assigned: wim leers » damiankloip
StatusFileSize
new5.6 KB
new65.45 KB
new104.94 KB

Fixed coding standards violations.

Back to @damiankloip so he can continue in the morning: review my changes, and address #493 (the changes I made so far were all REST-related, #493 is all remarks related to file management, which @damiankloip is an expert in by now :P)

wim leers’s picture

One last CS fix.

wim leers’s picture

… wow … d.o just discarded the files I attached. That is … interesting.

damiankloip’s picture

@WimLeers, the patch in #505 doesn't apply for me. Can you rebase your local branch that this came from and upload a rerolled patch? Pretty please :D

damiankloip’s picture

This should address the remaining feedback from Wim's review in #488 and Ted's review in #494 (some good points). Wim addressed all of Berdir's feedback, except #494.2. To reiterate what I mentioned previously on that; I am still in favour of removing this, as:

- The saving of the file to a temp location anyway is not really usable by the file entity properly
- No file validation at all. This means essentially any file of any type can be saved here. Security risk #1
- HTTP request is made to retrieve said file, this is Security risk #2 (and probably a good DDOS attack vector too)

wim leers’s picture

Issue summary: View changes
Issue tags: -Needs issue summary update

No file validation at all. This means essentially any file of any type can be saved here. Security risk #1

This alone is reason enough IMO to drop HAL's FileEntityNormalizer::denormalize().

I think all feedback has been addressed. I've updated the IS (which was last updated on June 6 in #376 by Damian!) and created a CR. To update the IS, I re-read the entire issue (well, starting at #281, which is when @damiankloip started working on it thanks to Acquia — my employer — sponsoring him, and started the long process of Doing This Right).

Not RTBC'ing, because this definitely needs to be reviewed by Berdir!

wim leers’s picture

Issue summary: View changes
Issue tags: -Needs change record
StatusFileSize
new1.35 KB

(Oops, forgot to remove Needs change record.)

While working on the IS in #508, I noticed one bit of test coverage was missing: that uploading the file in the first request results in a File entity with status=false (yes, it is!), and then using/referencing the file uploaded in a second request is status=true (sadly it isn't).

IDK yet where the problem lies, I'd have expected \Drupal\file\FileUsage\FileUsageBase::add() would have turned status=false into status=true, but it doesn't yet. Interdiff attached that will fail. Not attaching a patch that includes this interdiff, so that this stays at NR, so that it gets reviews!

berdir’s picture

Yes it works fine, but the entity static cache is why the test doesn't work. Works fine if you reset the cache with "\Drupal::entityTypeManager()->getStorage('file')->resetCache();" before the second call to File::load().

wim leers’s picture

D'OH OF COURSE 🤦‍♂️

Excellent, so that too is working as expected, just like we thought! :)

(The "combined" patch is now includes the latest of #2858482: #2858482-142: Simplify REST routing: disallow requesting POST/PATCH in any format, make consistent.)

wim leers’s picture

StatusFileSize
new106.17 KB

(The "combined" patch is now includes the latest of #2858482: #2858482-142: Simplify REST routing: disallow requesting POST/PATCH in any format, make consistent.)

Well, turns out that #2858482-142 is broken. Canceled #511's test.

Redid the combined patch.

The last submitted patch, 511: 1927648-511-combined.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

wim leers’s picture

StatusFileSize
new66.75 KB

#2858482: Simplify REST routing: disallow requesting POST/PATCH in any format, make consistent also landed. Therefore, no more need to rely on the "combined" patches.

Reuploading #511's "do not review" patch, this time without that suffix.

Version: 8.5.x-dev » 8.6.x-dev

Drupal 8.5.0-alpha1 will be released the week of January 17, 2018, which means new developments and disruptive changes should now be targeted against the 8.6.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

gabesullice’s picture

Status: Needs review » Needs work

First of all... whoa! What a thread and what a patch! Tremendous props to everyone involved.

My nits feel so "nitty" after looking at the history of this.

I'll start with direct code review. I have a few thoughts at the end not directly related to any particular lines of code.

  1. +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
    @@ -0,0 +1,570 @@
    +   * Creates a file from endpoint.
    

    "from an endpoint"?

  2. +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
    @@ -0,0 +1,570 @@
    +   * @param string $entity_type_id
    

    Can this be upcasted? If so, it could eliminate the dependency on the entity type manager.

  3. +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
    @@ -0,0 +1,570 @@
    +   * @param string $bundle
    +   *   The bundle.
    

    I assume this is just the repeated entity type ID for non-bundled entity types? We should document that here since it's not an optional parameter.

  4. +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
    @@ -0,0 +1,570 @@
    +   * @param string $field_name
    +   *   The field name.
    ...
    +    $field_definition = $this->validateAndLoadFieldDefinition($entity_type_id, $bundle, $field_name);
    

    Why not upcast this to a field definition as well? That would let you remove the entity type manager dependency and move field upcasting/validation logic into a reusable thing not tied to file uploads (this could benefit JSON API related/relationship routes).

    Maybe it's because you need the entity type? I'm just "thinking out loud" here, I don't feel strongly about it.

  5. +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
    @@ -0,0 +1,570 @@
    +   * @throws \Symfony\Component\HttpKernel\Exception\HttpException
    +   *   Thrown when temporary files cannot be written, a lock cannot be acquired,
    +   *   or when temporary files cannot be moved to their new location.
    

    What happens when the file already exists? Is this an error? Is it overwritten? If it's overwritten, should the HTTP method be PUT?

  6. +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
    @@ -0,0 +1,570 @@
    +    // This will take care of altering $file_uri if a file already exists.
    +    file_unmanaged_prepare($temp_file_path, $file_uri);
    

    Ah, I see here how existing files are handled.

  7. +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
    @@ -0,0 +1,570 @@
    +      throw new HttpException(503, sprintf('File "%s" is already locked for writing'), NULL, ['Retry-After' => 1]);
    

    Love the appropriate error code!

  8. +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
    @@ -0,0 +1,570 @@
    +        $read = fread($file_data, 8192);
    

    Would be good to document this number, maybe even make it a class constant.

  9. +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
    @@ -0,0 +1,570 @@
    +      throw new BadRequestHttpException('The extended "filename*" format is currently not supported in the "Content-Disposition header"');
    

    s/Disposition header"/Disposition" header/g

  10. +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
    @@ -0,0 +1,570 @@
    +   * @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
    +   *   Thrown when the field does not exist.
    

    Given that the field name is in the request path, shouldn't this throw a 404?

  11. +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
    @@ -0,0 +1,570 @@
    +        return PlainTextOutput::renderFromHtml($error);
    

    Nice.

  12. +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
    @@ -0,0 +1,570 @@
    +   * Prepares the filename to strip out any malicious extensions.
    ...
    +  protected function prepareFilename($filename, array &$validators) {
    

    Why lock this away in a REST resource plugin?

  13. +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
    @@ -0,0 +1,570 @@
    +    if (!\Drupal::config('system.file')->get('allow_insecure_uploads') && preg_match('/\.(php|pl|py|cgi|asp|js)(\.|$)/i', $filename) && (substr($filename, -4) != '.txt')) {
    

    Should this regex be a class constant?

  14. +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
    @@ -0,0 +1,570 @@
    +  protected function getUploadLocation(array $settings) {
    

    Should this be a static method?

  15. +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
    @@ -0,0 +1,570 @@
    +   *   An array suitable for passing to file_save_upload() or the file field
    +   *   element's '#upload_validators' property.
    

    This seems generally useful enough to not be confined to the REST module. Why not a public static method of FileItem?

  16. +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
    @@ -0,0 +1,570 @@
    +  protected function generateLockIdFromFileUri($file_uri) {
    

    Can this be static as well?

  17. +++ b/core/modules/hal/src/Normalizer/FileEntityNormalizer.php
    @@ -73,16 +69,4 @@ public function normalize($entity, $format = NULL, array $context = []) {
    -    $file_data = (string) $this->httpClient->get($data['uri'][0]['value'])->getBody();
    

    Heh, I wish it could be this easy!

  18. +++ b/core/modules/hal/tests/src/Functional/EntityResource/File/FileUploadHalJsonCookieTest.php
    @@ -0,0 +1,19 @@
    +class FileUploadHalJsonCookieTest extends FileUploadHalJsonTestBase {
    

    Let me just say, I love this pattern for testing different many different permutations.


In general, I think this is super impressive. I had a few thoughts while reviewing this that could help make this more useful to the rest of the API-first initiative (namely JSON API, but I imagine this would benefit GraphQL and others as well).

There was obviously a ton of effort put into creating and validating file uploads in a safe, secure and OO fashion. However, all of that effort is being locked away in protected methods inside the REST resource plugin. Most of this logic into the file module as a service like file.upload_handler.

This would make it much easier to leverage the work done here without forcing other modules to enable REST module "just" to allow file uploads.

The REST resource plugin would still exist, but only insofar as it provides the resource path (/file/upload/{entity_type}/{bundle}/{field_name}) and loads the appropriate field definition accordingly. Such that the resource plugin simply become a wrapper around something like this:

$upload_handler = \Drupal::service('file.upload_handler');
$filename = $upload_handler->extractFilenameFromRequest($request);
$file_entity = $upload_handler->handleUploadForField(FieldDefinition $field, $filename);

This would be important for JSON API, which has its own URL path structure as well as a concept of aliased field names.

On another note, I didn't read anything in the IS about multipart/mixed as a Content-Type header. I'm just assuming that was already worked out and determined not to be the right approach here?

damiankloip’s picture

Status: Needs work » Needs review
StatusFileSize
new68.79 KB
new6.99 KB

Nice! Thanks so much for the review, it's hard to get people to review code thoroughly!

1. Fixed
2. Hmm, we need the entity type manager to do the access control checks too. Otherwise, we need to care about creating the access control handler instance ourselves. We could still upcast the entity type though, could be a nice additional to 404 for entity types that don't exist. However, EntityResource, for example, doesn't do this either. So not sure about adding that pattern to this resource (follow up issue to change all maybe).
3. That's kind of a given for entity types that don't use bundles but makes sense to add it anyway I guess. Fixed.
4. Similar to 2, not sure we do this pattern in other places, we also use the entity type manager to get the access control handler. We also try to enforce that it's a file reference field.
5. We use FILE_EXISTS_ERROR here as we only allow posting new files for now.
6. Oh, yeah ok :)
7. :)
8. Good call (not sure why I didn't do that originally). Added a BYTES_TO_READ constant for this.
9. Eagle eye! Fixed.
10. Fair point. changed to 404.
11. :)
12. Hmm, this could be moved into a central service/class. See later comments.
13. This was copied from file module. So Ideally we would have the const there. Added a FILE_INSECURE_EXTENSION_REGEX to file.module. Any better names welcome.
14. getUploadLocation uses the token service that's injected ($this->token), so we can't do that.
15. There is already a method on FileItem for this. Our logic is just a mixture of this and file_save_upload. The problem with trying to use it from the file item is that we're adding files independent of the parent file field (and therefore FileItem) so we don't actually have a FileItem to check. Unless we created an instance just to use that. Which feels...meh. Maybe this code could be shared in ANOTHER place? :)
16. Fixed. Made static.
17. Nice
18. Nice

So that just leaves:

1. Should we move some of this logic into a service in the file module that can be shared? Or should this be a follow up? Not sure. The reason I'm thinking follow up is that the file upload via the UI could also use some of this stuff too. E.g. #15
2. No upcasting. Thoughts on that? Wim?

Status: Needs review » Needs work

The last submitted patch, 517: 1927648-517.patch, failed testing. View results

damiankloip’s picture

Status: Needs work » Needs review
StatusFileSize
new68.8 KB

Whoops - rebase fail.

wim leers’s picture

Discussed this with @damiankloip.

  1. We just also had the API-First Initiative call. https://www.drupal.org/project/graphql will also be working on file upload support. Ideally, they'd also be able to reuse it. But GraphQL is very different from REST/JSONAPI, which are both fairly similar. So the creation of a service that provides the shared logic should definitely be blocked on GraphQL's feedback, otherwise we'll just end up with a bad abstraction.

    Created a follow-up issue: #2940383: [META] Unify file upload logic of REST and JSON:API

  2. Regarding upcasting: I agree that it makes sense for this new @RestResource plugin to follow the same patterns that the existing ones use, and to defer that upcasting nicety to a follow-up, where we can then change it also for existing REST resource plugins.
    (I almost created a follow-up issue, then I saw a flaw in Damian's argument)

    @damiankloip wrote in #517.2 that \Drupal\rest\Plugin\rest\resource\EntityResource also doesn't do this. But that plugin doesn't have a $entity_type_id parameter!

    However … there simply is no entity type paramconverter service in core. Look at \Drupal\entity_test\Controller\EntityTestController::listEntitiesAlphabetically() for example. Same for field definitions.
    Introducing such paramconverters is definitely out of scope here. Very few things would be able to use them too.

gabesullice’s picture

@damiankloip everything looks good to me! And @Wim Leers addressed the only remaining concerns I had.

I believe this still would benefit from a review by @Berdir, so not gonna RTBC quite yet.

wim leers’s picture

Assigned: damiankloip » berdir

Agreed, a @Berdir-review is what we really want/need here … so … assigning to him.

ayalon’s picture

I tested the patch together with default_content module. I wanted to export a taxonomy with a file entity attached.

As soon as I export a file I get this error:

Error: Class 'Drupal\rest\EventSubscriber\ResourceResponseSubscriber' not found in Drupal\serialization\Normalizer\NormalizerBase->addCacheableDependency() (line 95)

I was using drush to export a taxonomy term with default content.
I figured out, that i had to enable the rest module to get it to work.
I can see a file entity exported but it has no binary content. Any hints?

berdir’s picture

Assigned: berdir » Unassigned

@ayalon: This is not meant to be compatible with default_content, it is about REST/HTTP use cases. For default_content, you need https://www.drupal.org/project/better_normalizers for now, see also https://www.drupal.org/project/default_content/issues/2933777. It is not the goal (anymore) to make this work with default_content. Instead, I think default content should add special support for files instead as suggested in the issue above.

Ha, that's nice of you to assign this to me ;) I don't think I have anything else to add, I believe my review from before has been addressed, the last interdiffs look fine to me as well. So all that's left here I think is probably creating/updating change records for example the removal of that file download thing (I agree with that I think, but we should probably still mention it.. with reasons why we remove it. Someone out there probably was relying on it..) and the other API additions (changes?) mentioned in the issue summary.

wim leers’s picture

Assigned: Unassigned » wim leers

@ayalon: that was fixed in #2931765: Regression: \Drupal\hal\LinkManager\LinkManagerBase implicitly depends on REST module.

@Berdir: oh sorry, I thought I understood from IRC conversations from some week sago that you still wanted to get a chance to review the entire patch in-depth before this could become RTBC.

I can definitely work on the CRs.

The good thing of this landing so early in the Drupal 8.6 cycle is that there will be the longest test period possible, and therefore also the longest timeframe possible for a revert if need be.

wim leers’s picture

Assigned: wim leers » Unassigned
Status: Needs review » Reviewed & tested by the community

CRs created:

  1. https://www.drupal.org/node/2941420 to announce this major new feature and how to use it
  2. https://www.drupal.org/node/2941424 to announce the removal of HAL's FileEntityNormalizer::denormalize() and why

So …

  • @Berdir has reviewed this and strongly directed the architecture of this patch numerous times.
  • I've reviewed this in detail numerous times.
  • Serialization module maintainer @damiankloip has self-reviewed this many times.
  • @gabesullice has reviewed this in detail in #516.

It's time to get this in at last. 🎉 🍾

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 519: 1927648-519.patch, failed testing. View results

webchick’s picture

Issue tags: +Needs security review

We talked about this on our committer call today, and there was quite a bit of trepidation to commit this to 8.5.x given it's a massive change with many twisty passages and also security implications. Something that would help build confidence is a review about security specifically (e.g. people embedding PHP in images and the like), so tagging for that.

However, there was broad agreement among those in attendance to committing it as early in the 8.6 cycle as possible, so yay!

wim leers’s picture

Status: Needs work » Reviewed & tested by the community
Related issues: +#2935783: RequestHandler's handle method interacts with the Route object but should be able to just declare arguments
StatusFileSize
new68.24 KB

there was quite a bit of trepidation to commit this to 8.5.x

I would not expect this to be committed to 8.5.x, I think that's too risky. File handling is always complex, but especially so in Drupal's case, with the many abstraction layers involved (entity references, validators, validation-dictated-by-bundle-settings, and so on).

I am very confident this patch is solid, and that it's been required to meet a much higher bar than the original REST module's code (which is also necessary of course, since D8 is out and stable now). Nevertheless, it's realistic/likely there's at least one edge case somewhere that we didn't think of. But given the amount of edge cases that we are testing, I think this is as far as we should take it before it can safely be committed to the next minor.

So, yes, please only commit this to 8.6.x! That gives it a full minor cycle to be tested and hardened, because 8.5.0 is not even released yet! I'm glad to see that that is also what the committers concluded :)


Something that would help build confidence is a review about security specifically (e.g. people embedding PHP in images and the like), so tagging for that.

This uses the exact same validation logic that core already uses … so I disagree this needs security review. But would a security be nice? Yes, absolutely, it's a nice-to-have. Explicit security-related test coverage for "special" edge cases:

  • FileUploadResourceTestBase::testPostFileUpload()'s testing that a 404 response is sent in case of trying to upload a file for a non-existing file field
  • FileUploadResourceTestBase::testFileUploadStrippedFilePath() — inability to upload into specified directories even when they are specified
  • FileUploadResourceTestBase::testFileUploadMaliciousExtension() — self-explanatory
  • … and then all the "typical" edge cases that are tested: file size restrictions being respected, file type restrictions being respected, 403 in case of missing permissions.

#519 no longer applies — #2935783: RequestHandler's handle method interacts with the Route object but should be able to just declare arguments also modified RequestHandler. Fortunately that was an easy conflict to resolve, and in fact it made this patch slightly simpler! :) 0.56 KB smaller :D (No more need for a ::loadRestResourceConfigFromRouteMatch() helper method.)

wim leers’s picture

StatusFileSize
new4.16 KB
new71.67 KB

One thing that we did forget was to scan the existing code base for references to this issue. Because this issue has been around for a long time.

This removes all those @todos that this issue finally allows us to remove 🎉.

wim leers’s picture

StatusFileSize
new1.6 KB
new71.47 KB

In looking at the test coverage code again (see next comment), I noticed some bits in the test coverage that could be simplified. (Making the patch slightly smaller again, 0.2 KB this time.)

wim leers’s picture

Issue tags: +Media Initiative
StatusFileSize
new9.57 KB
new79.31 KB

And finally, even though we added explicit generic test coverage for uploading a file and then using/referencing it (in \Drupal\Tests\rest\Functional\FileUploadResourceTestBase::testPostFileUpload()), there is one particular concrete use case where this is more essential than for all others: Media.

Without file uploads, there literally is no way today (in 8.4 and 8.5) to create Media entities entirely via HTTP APIs. This patch changes that. And in fact, \Drupal\Tests\rest\Functional\EntityResource\Media\MediaResourceTestBase::testPost() was marked as skipped for that very reason!

So, this adds concrete test coverage for uploading a file and using that uploaded file in a Media entity that we then create!

(In doing so, I found a minor bug in FileUploadResource because the generic existing test coverage uses a bundle-less entity type.)

I'd mark this as NW again, but this is merely repeating the same generic test coverage already in the patch for one particular concrete use case, further proving that this patch is solid, so keeping at RTBC.

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 532: 1927648-532.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

wim leers’s picture

Status: Needs work » Reviewed & tested by the community
StatusFileSize
new6.5 KB
new82.06 KB
  1. +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
    @@ -400,7 +400,7 @@ protected function validateAndLoadFieldDefinition($entity_type_id, $bundle, $fie
    -    $access_result = $entity_access_control_handler->createAccess(NULL, NULL, [], TRUE)
    +    $access_result = $entity_access_control_handler->createAccess($bundle, NULL, [], TRUE)
    

    This fix caused failures in some tests, because the tests were assuming this bug!

  2. +++ b/core/modules/rest/tests/src/Functional/EntityResource/Media/MediaResourceTestBase.php
    @@ -265,7 +289,124 @@ protected function getExpectedUnauthorizedAccessMessage($method) {
    +    $this->assertSame($expected_file_entity_normalization, Json::decode((string) $response->getBody()));
    

    This is the other problem in the test coveraget aht I added: this only works for the json format, we need to make it work for the hal_json and xml formats too. The expected normalization should've lived in a separate method that the HAL test coverage could then decorate to add HAL-specific bits.

  3. Finally, the updated Media test coverage to first upload a file did not first initialize authentication, which is necessary for cookie authentication, which is why all cookie test coverage for media also failed.

Fixed all 3 small oversights. Back to RTBC because it's just copy/pasting code and applying the same pattern as all other REST test coverage, there's no original work here that needs extra scrutiny.

larowlan’s picture

Status: Reviewed & tested by the community » Needs review
  1. +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
    @@ -0,0 +1,579 @@
    +          // Close the file streams.
    +          fclose($temp_file);
    +          fclose($file_data);
    +          $this->logger->error('Input data could not be read');
    +          throw new HttpException(500, 'Input file data could not be read');
    ...
    +          // Close the file streams.
    +          fclose($temp_file);
    +          fclose($file_data);
    +          $this->logger->error('Temporary file data for "%path" could not be written', ['%path' => $temp_file_path]);
    +          throw new HttpException(500, 'Temporary file data could not be written');
    ...
    +      // Close the file streams.
    +      fclose($temp_file);
    +      fclose($file_data);
    +      $this->logger->error('Temporary file "%path" could not be opened for file upload', ['%path' => $temp_file_path]);
    +      throw new HttpException(500, 'Temporary file could not be opened');
    

    We're basically doing the same thing three times here with a different log/message. Is there merit in a protected method?

  2. +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
    @@ -0,0 +1,579 @@
    +    // Make sure only the trailing filename is returned.
    +    return basename($filename);
    

    nice

  3. +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
    @@ -0,0 +1,579 @@
    +      throw new NotFoundHttpException(sprintf('Field "%s" does not exist', $field_name));
    

    There's a minor information disclosure here, theoretically someone could use this to validate the presence of field names on the site. Not sure it matters though.

  4. +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
    @@ -0,0 +1,579 @@
    +      throw new AccessDeniedHttpException(sprintf('"%s" is not a file field', $field_name));
    

    We return access denied here - should it be not found as well? Similar comment re information disclosure

  5. +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
    @@ -0,0 +1,579 @@
    +    if (!($entity_access_control_handler->createAccess($bundle) && $entity_access_control_handler->fieldAccess('edit', $field_definition))) {
    

    Is there a reason why we don't use $access_result->isAllowed() here, instead of calculating access again?

  6. +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
    @@ -0,0 +1,579 @@
    +    $extensions = $validators['file_validate_extensions'][0];
    ...
    +    if (!empty($settings['file_extensions'])) {
    +      $validators['file_validate_extensions'] = [$settings['file_extensions']];
    +    }
    

    We assume this is always present, but have a logic path where it might not be. Should we be doing isset check here first before accessing the array members. And if so, indicates missing test coverage for when there is no file extension validation

  7. +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
    @@ -0,0 +1,579 @@
    +    if (!\Drupal::config('system.file')->get('allow_insecure_uploads') && preg_match(FILE_INSECURE_EXTENSION_REGEX, $filename) && (substr($filename, -4) != '.txt')) {
    

    Is there a reason why we didn't inject this - we already inject a lot of services.

  8. +++ b/core/modules/hal/tests/src/Functional/EntityResource/File/FileUploadHalJsonTestBase.php
    @@ -0,0 +1,94 @@
    +abstract class FileUploadHalJsonTestBase extends FileUploadResourceTestBase {
    

    nit: missing a docblock

  9. +++ b/core/modules/rest/tests/src/Functional/FileUploadResourceTestBase.php
    @@ -0,0 +1,650 @@
    +    $response = $this->fileRequest($uri, $this->testFileData, ['Content-Disposition' => 'file; filename="directory/example.txt"']);
    

    Would be nice to add another test here that tested absolute and relative paths, e.g. ../../../etc/shadow and /etc/passwd just for piece of mind. Could use a data-provider for the various flavours of malicious filename

larowlan’s picture

In addition the size of the constructor is a bit of a red-flag that perhaps this plugin is doing too many things.

I did some analysis of where each of the dependencies is used in the class as follows

file system

  • streamUploadData

entity type manager

  • validateAndLoadFieldDefinition

entity field manager

  • validateAndLoadFieldDefinition

current user

  • post

mime type guesser

  • post

token

  • getUploadLocation

lock

  • post

So on that basis I think there is some scope for splitting and refactoring, but that can be a follow up - can we get an issue created for that?

Suggestions for splitting could be something like

  • Create a new binary file post validation service for the validation, inject the field and entity manager into that instead
  • Create a new binary file post service for the post, inject lock, filesystem and mime type into that instead
  • Add a default field value on the File.uid and let the default value set the uid, remove current user.

Then we'd only inject the new binary file post validation service, the new binary file post service and token services.

berdir’s picture

@larowlan: We have already one follow-up to extract a generic service, one argument for not doing it here is that we want to review how json_api and graphql would use it exactly to make sure we really have a service that makes sense and can be reused. See #2940383: [META] Unify file upload logic of REST and JSON:API. We also have several issues to refactor the file system functions and put them in services, which might help with cleaning this up as well.

Considering how big this issue already is (and how slow it makes to work here and do something as simple as posting a comment), I think it makes sense to do that in a follow-up. API design is hard and would probably delay this issue quite a bit more.

That said, relying on having the file uid set automatically through a default value sounds like a relatively simple and useful improvement. Could have a follow-up to remove explicitly setting it from existing places.

pwolanin’s picture

Status: Needs review » Needs work

Looks like some minor fixes needed considering the review from larowlan and a couple nits below.

  1. +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
    @@ -0,0 +1,579 @@
    + *   - The actual files do not need to be stored in another temporary local, to
    

    nit: local -> location

  2. +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
    @@ -0,0 +1,579 @@
    +    if (!file_prepare_directory($destination, FILE_CREATE_DIRECTORY)) {
    

    Seems problematic here to use the global function instead of the file system passed into the construction? I guess we still have to use a lot of global functions for file handling.

  3. +++ b/core/modules/rest/tests/src/Functional/FileUploadResourceTestBase.php
    @@ -0,0 +1,650 @@
    +    $this->assertTrue(file_exists('public://foobar/example.php_.txt'));
    

    It seems like this should use a different filename from the code above to make sure the test is correct?

larowlan’s picture

Status: Needs work » Needs review

@Berdir from #536 (yikes!)

So on that basis I think there is some scope for splitting and refactoring, but that can be a follow up - can we get an issue created for that?

I agree, should be a follow up, and given we already have one created, nothing to do then

damiankloip’s picture

StatusFileSize
new84.73 KB
new10.46 KB

Nice reviews.

#535:

1. I thought about doing this also, but the function would need at least 4 parameters. So seemed to not be much tidier in the end?
3 + 4. Technically it is, for authed users. Should we change both of these to just be not found? with less of a message (maybe log the message we actually send now)?
5. Not sure about this one, Wim did this change I think, so deferring to him :)
6. Very good spot. Changed this up a bit to catch this and added test coverage.
7. Injected
8. Added
9. Added some test coverage for this too.

#538:

1. Changed
2. Not sure why this would be problematic at the moment? This functionality doesn't exist on the file system service. So for now, global file functions it is.
3. Good idea. changed to use different file names.

wim leers’s picture

StatusFileSize
new1.09 KB
new84.93 KB
  1. +++ b/core/modules/rest/tests/src/Functional/FileUploadResourceTestBase.php
    @@ -337,6 +337,34 @@ public function testFileUploadStrippedFilePath() {
    +    $response = $this->fileRequest($uri, $this->testFileData, ['Content-Disposition' => 'file; filename="../../example_2.txt"']);
    ...
    +    $response = $this->fileRequest($uri, $this->testFileData, ['Content-Disposition' => 'file; filename="/etc/passwd"']);
    

    Nice!

  2. +++ b/core/modules/rest/tests/src/Functional/FileUploadResourceTestBase.php
    @@ -473,14 +501,38 @@ public function testFileUploadMaliciousExtension() {
    +  public function testFileUploadNoExtensionSetting() {
    ...
    +    // Test using a masked exploit file.
    +    $response = $this->fileRequest($uri, $this->testFileData, ['Content-Disposition' => 'filename="example.txt"']);
    +    $expected = $this->getExpectedNormalizedEntity(1, 'example.txt', TRUE);
    +
    +    $this->assertResponseData($expected, $response);
    +    $this->assertTrue(file_exists('public://foobar/example.txt'));
    

    I don't understand this test. How is this a masked exploit file?


#535.5: I did that in #487 apparently, because I'm an idiot who writes refactored code and then doesn't actually change the line he was trying to refactor :)

damiankloip’s picture

StatusFileSize
new84.76 KB
new726 bytes

#541: This was a rogue comment from a small hunk that I copied. So, the test is valid, the comment is misleading :) This addresses some missing test coverage from #535.6.

Needed a reroll too.

Status: Needs review » Needs work

The last submitted patch, 542: 1927648-542.patch, failed testing. View results

wim leers’s picture

Status: Needs work » Reviewed & tested by the community
StatusFileSize
new84.89 KB

👍 So … back to RTBC!

Actually, #541 still applies cleanly to 8.6.x's HEAD. Perhaps that was not true when you rolled it, but it is now. Redid #542's interdiff on top of #541. This is equivalent to #544, but does apply to 8.6.x.

kylebrowning’s picture

Yay!

kpv’s picture

Not sure if this belongs to the current issue..

For text fields you can set both
{ "field_text": {"value": "Dramallama"}, }
and
{ "field_text": [{"value": "Dramallama"}], }
when setting data for a request.

For file fields you should always pass it as an array, e.g.:
{ "field_file": [{"target_id": 42}], }

wim leers’s picture

#546: that's definitely out of scope here. Please repeat what you said at #2935738: Entity reference field subclasses (such as file and image fields) lose the non-default properties upon denormalization, in both hal_json and json — the root cause for what you describe lies in that same area of code.

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 544: 1927648-544.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

Anonymous’s picture

Status: Needs work » Reviewed & tested by the community

Unrelated fails

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

Testbot had a hiccup re-rtbcing

alexpott’s picture

Status: Fixed » Reviewed & tested by the community

Whoops lol.

alexpott’s picture

I did not credit:

  • @benjy becausepatches were for previous versions of Drupal
  • @cloudbull because their comments were about getting support on using the patch.
  • @rteijeiro, @jibran, @eugene.ilyin, @Pedro J. Fernandez, @lightguardjp, @alexpott, @kpv, @joshk, @rcaracaus, @vaplas, @YesCT, @jhedstrom, @anty56, @tinom, @bradjones1, @Evgeny_Yudkin, @timmillwood, @tstoeckler, @ayalon, and @fjen left comments that did not directly influence the patch.

I credited @moshe weitzmann, @linclark, @webchick, @slashrsm, @catch because I know they had long offline discussions about this issue and the fact these discussions took place is documented on issue.

alexpott’s picture

Status: Reviewed & tested by the community » Needs review
StatusFileSize
new84.39 KB
new4.65 KB
  1. +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
    @@ -0,0 +1,589 @@
    +    // Make sure only the trailing filename is returned.
    +    return basename($filename);
    

    Maybe we should thrown an error if basename($filename) !== $filename? As atm we're silently rejecting user input atm.

  2. +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
    @@ -0,0 +1,589 @@
    +    $access_result = $entity_access_control_handler->createAccess($bundle, NULL, [], TRUE)
    +      ->andIf($entity_access_control_handler->fieldAccess('edit', $field_definition, NULL, NULL, TRUE));
    +    if (!($entity_access_control_handler->createAccess($bundle) && $entity_access_control_handler->fieldAccess('edit', $field_definition))) {
    

    No need for the duplicate calls to check access - just can do if (!$access_result->isAllowed())

  3. +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
    @@ -0,0 +1,589 @@
    +    if (!empty($validators['file_validate_extensions']) && !empty($validators['file_validate_extensions'][0])) {
    +      // Munge the filename to protect against possible malicious extension
    +      // hiding within an unknown file type (ie: filename.html.foo).
    +      $filename = file_munge_filename($filename, $validators['file_validate_extensions'][0]);
    +    }
    

    This should explain what $validators['file_validate_extensions'][0] is... a list of extensions.

  4. +++ b/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php
    @@ -0,0 +1,589 @@
    +      if (!empty($extensions)) {
    

    $extensions does not exist. Should be if (!empty($validators['file_validate_extensions'][0])) { - this fix implies we have missing test coverage.

alexpott’s picture

Status: Needs review » Needs work

Marking as needs work for #553.1 discussion and for adding tests for #553.4.

alexpott’s picture

Status: Needs work » Needs review
StatusFileSize
new6.16 KB
new85.7 KB
new85.73 KB

the test only patch reverts the fix for #553.4 but has the new test added by this patch. It will fail.

Patch attached addresses the missing tests from #553.4 and does a couple more coding standards cleanups.

As far as I can see all we need now is an opinion from someone else on #553.1.

The last submitted patch, 555: 1927648-555.test-only.patch, failed testing. View results

berdir’s picture

#553.1: I asked the same in #494 (https://www.drupal.org/project/drupal/issues/1927648?page=1#comment-1241...) although as a comment on the test coverage and got this response:

Why would somebody do that? Because they got it wrong. Why that doesn't result in an error? Because @dawehner pointed to https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Dispos... in #440.1, and @damiankloip implemented that in #441. That says: path information should be stripped. Let's add that to the docs

Looks like that was not commented sufficiently?

alexpott’s picture

StatusFileSize
new944 bytes
new85.87 KB

Okay let's add a comment to point to the RFC that Mozilla's docs are based on. Also fixed a stray @see to something not that helpful and also make it inline with the comment above which talks about a validate() method.

berdir’s picture

Status: Needs review » Reviewed & tested by the community

Those changes look good to me and the additional test coverage covers the changes that were done.

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

Committed fd8233c and pushed to 8.6.x. Thanks!

Discussed with @larowlan and he said that @catch agreed we should get this into 8.6.x early to hopefully catch any issues.

Thanks for the great issue summary. It made reviewing the code, commit credits and comments easier.

  • alexpott committed fd8233c on 8.6.x
    Issue #1927648 by damiankloip, Wim Leers, marthinal, tedbow, Arla,...
alexpott’s picture

I did a security review as part of #553 - I especially compared the existing file_save_upload logic and the new flow. I found #553.4 which I addressed and added tests for.

This patch deserves release notes mention in 8.6.0.

gabesullice’s picture

Thank you @alexpott!! This is will be a TREMENDOUS improvement for API-First Drupal :D

  • xjm committed a33fe9c on 8.6.x
    Revert "Issue #1927648 by damiankloip, Wim Leers, marthinal, tedbow,...
xjm’s picture

Status: Fixed » Needs work

Unfortunately the newly added tests are failing on PHP 5.5: https://www.drupal.org/pift-ci-job/929845

Sorry everyone -- have to revert.

alexpott’s picture

Status: Needs work » Reviewed & tested by the community
StatusFileSize
new857 bytes
new85.81 KB

Here's the fix. Straight to RTBC because this is just code removal.

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 566: 1927648-567.patch, failed testing. View results

alexpott’s picture

Status: Needs work » Reviewed & tested by the community

#566 Unrelated fail on Drupal\Tests\node\FunctionalJavascript\NodePreviewLinkTest - lol.

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

Great the PHP5.5 test proves that the PHP7 fail was random and it's not caused by this because that test doesn't try to do anything resty at all.

Committed a766172 and pushed to 8.6.x. Thanks!

Second time lucky.

  • alexpott committed a766172 on 8.6.x
    Issue #1927648 by damiankloip, Wim Leers, marthinal, tedbow, alexpott,...
Anonymous’s picture

Titanic work from titanic people!

wim leers’s picture

#552: wow, impressive thoughtfulness, @alexpott! ❤️

#553:

  1. This is per the spec: the spec only allows passing a filename. We strip path information because otherwise this would be an attack vector! (Upload files to arbitrary locations.)
  2. Huh, weird, I'd swear I already fixed that! Change looks good in any case!
  3. Great catch!
  4. (There was no point 5, but you didn't mention one change in your interdiff: removing one unused parameter for a helper method, nice catch!)

#555: thanks for the test coverage for the fix you added in #553.4 :)

#557 + #558: Thanks Berdir for pointing at that, thanks @alexpott for adding that comment!


#560:

Thanks for the great issue summary. It made reviewing the code, commit credits and comments easier.

❤️ That took me hours, but I'm glad it paid off. Otherwise it'd have taken you hours!


🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉

I do want to call out @damiankloip, whose time Acquia (my employer) sponsored to get this to the finish line. It was a pleasure working with Damian :) Thank you Acquia, for sponsoring this much-missed feature!

I just marked it as done in #2941316: REST: top priorities for Drupal 8.6.x. That's the "top priorities" issue for 8.6, it's been on the "top priorities" list for 2 years, since #2721489: REST: top priorities for Drupal 8.2.x!

wim leers’s picture

chihada’s picture

I've applied the patch and followed the guide coming on https://www.drupal.org/node/2941420 for uploading files, and everything went good, but when I enable the https://www.drupal.org/project/file_entity module, I get the error:

Drupal\Component\Plugin\Exception\PluginNotFoundException: The "entity:file:undefined" plugin does not exist. in Drupal\Core\Plugin\DefaultPluginManager->doGetDefinition() (line 52 of /var/www/html/docroot/core/lib/Drupal/Component/Plugin/Discovery/DiscoveryTrait.php).

wim leers’s picture

#574: Excellent! :) Thank you very much for testing!

The problem you reported is related to either the file_entity module in contrib, or the rest module in core. It's probably the file_entity module. Please create a separate issue for that, and provide steps to reproduce. Thanks!

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.

sharif.elshobkshy’s picture

Hi.

I've applied the patch and now I'm able to upload files. All files are created as application/octet-stream
However, all files are created in the public folder without extension.

This causes:
PDF files not to open (unless the extension is added manually)
Images created and used in media fields are not entirely recognized as images (they appear broken and without preview thumbnails).

Am I missing something or is this how it's supposed to behave?

UPDATE:
I was missing the uri. Now the extension are properly added.

{
	"_links": {
		"type": {
			"href": "http://baseURL/rest/type/file/image"
		}
	},
	"filename": [
		{
			"value": "fileName.jpg"
		}
	],
    "uri": [
        {
            "value": "public://fileName.jpg"
        }
    ],
	"data": [
		{
			"value": "binary data binary data binary data"
		}
	]
}

Thanks,
Sharif.

gabesullice’s picture

@sharif.elshobkshy, I believe you were also probably missing the filename portion of the Content-Disposition request header.

F.E.

Content-Disposition: file; filename="filename.jpg"

I really appreciate that you updated your comment with the solution. Next time, please open a new support request though :) This issue is already now 578 comments long!

ijsbrandy’s picture

StatusFileSize
new7.44 KB
new77.69 KB

Patch not working with drupal/core 8.5.5 due to the fact that the methods in FileResourceTestBase.php and MediaResourceTestBase.php are deleted.

wim leers’s picture

Issue tags: +8.6.0 highlights
nicrodgers’s picture

StatusFileSize
new77.57 KB

I couldn't get patch 579 to apply to 8.5.5, so I've re-rolled the one in 567, hopefully it'll help someone.

xjm’s picture

What needs to be mentioned about this issue for the release notes? It makes sense for the 8.6.0 highlights, but the release notes should be reserved for important upgrade information. Is there something a site owner needs to know before upgrading to this release on account of this issue?

xjm’s picture

Issue tags: -8.6.0 release notes

After reading the CR which mostly describes how to use the functionality, I don't think there's any specific disruption to modules, remote API, or consumers that we need to document for this release. If there is something that site owners who might rely on something in 8.5.x. HEAD should change, please include a description of that and re-tag this issue.

Thanks!

alireza.tabatabaeian’s picture

I'm using drupal 8.7.1, I've enabled this modules :

  • Hal
  • Http Basic Authentication
  • JSON API
  • REST UI
  • RESTful Web Services
  • Serialization

I also have set admin/config/services/jsonapi to

Accept all JSON:API create, read, update, and delete operations

The Problem is I always get the Route Not Found for my requests.