-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[HttpFoundation] added multipart/form-data for PUT, DELETE and PATCH #10381
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
I just realized I haven't written any tests for this. I'll be updating the pull request with those as soon as I can. |
@@ -26,6 +26,14 @@ | |||
*/ | |||
class UploadedFile extends File | |||
{ | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
remove line break, yeah looking forward to those tests, thanks!
Tests have been added. |
{ | ||
public function getContent($asResource = false) | ||
{ | ||
return file_get_contents(__DIR__ . '/Fixtures/multipart-formdata'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no spaces around dots
I already figured this issue. But IMHO, upload file should not be done with a PUT method. Workflow:
And with this, PUT, POST and GET method are consistent |
Quoting someone else:
See also the lastest HTTP clarifications. That means that multipart/form-data is usually wrong with PUT. I understand that the Form component does encourage this, as simply adding a file element is expected to work. But it would be preferable to fix the Form component and forbid mixing PUT with file elements than working around it and against HTTP semantics. ping @webmozart What you should do instead is PUT the content of your upload to the very same URL that you use to GET it. An other major problem with the proposed patch is that IF the MIME multipart parsing algorithm should be implemented, it should be against the many relevant RFCs, be tested against variations found into the wild, be stream efficient (should not expect that everything fits in memory), etc. Many subjects that the C source code of PHP has implemented, tested and validated in real life situations. But having our own in Symfony is a hard work. |
I agree with @nicolas-grekas here @lyrixx I see an improvement to your workflow: use a 201 status code when creating the new resource rather than a 200 and shouln't the step 4 be using |
@stof I fixed the POST -> PUT (bad copy paste). And I wrote quickly this comment. I agree 201 is better (I update my comment too) ;) |
@lyrixx You said that an upload file should not be done with a PUT, but you did not state why. Can you please elaborate? Thank you for your suggestion, unfortunately your workflow does not match what I am trying to accomplish here. You example shows creating two images via post, that are then referenced by a user updated via get. I am not updating an object that references an file, I am updating the file itself. You say that you should not use a PUT with files, but if I am using a RESTful API, and I want to update a resource that is a file, then PUT is the appropriate method. The reason I use multi-part/form-data is because the file has meta-data on it (think of Mac's x-attributes or Linux's ACLs). @nicolas-grekas Thanks for your input. The quote at the beginning of your post is exactly what I am doing. I am replacing a file with metadata at a specified url using the multipart/form-data request. There is a section in the HTTP Clarifications, which states:
I understand how multipart/form-data PUT requests would not be compliant with this, unless the GET also returns the same multipart/form-data. But how would you propose that I update a file resource that has metadata on it? Given a file http://mydomain.com/files/test.bin, if I cannot use multipart/form-data via PUT to update it's metadata, I would need to expose some kind of abstract API like http://mydomain.com/files/test.bin/metadata. Would that be acceptable? |
It depends how you fetch the metadata with your API I think. |
$rawHeaders = explode("\r\n", $rawHeaders); | ||
$headers = array(); | ||
foreach ($rawHeaders as $header) { | ||
list($name, $value) = explode(':', $header); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just to be sure : explode(':', $header, 2)
, no ? Because, what-if the header content (for a reason or another) has a :
in its value ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AFAIK the : is forbidden inside the header value
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As a bare minimum we should be seeing a 501(not implemented) if the server cannot understand the Content-Type (as per the spec):
Symfony should be doing this by default on a multipart/form-data. I believe right now it is currently sending back a 200 OK response when data is sent via multipart/form-data. It does not mean that multipart/form-data is wrong for PUT. Its a perfectly valid option. To quote a user from the php bug filed on this issue:
|
@greatwitenorth I believe that the reasoning behind not automatically sending a 501 is that just because the Symfony framework does not understand the headers, does not mean that the developer for the specific project using Symfony would not implement something that does. |
Yeah that makes sense, but then that means PUT is broken out of the box with Symfony and not adhering to the spec. It should be a 501 by default since it's "not implemented". Actually by default PUT should just work with multipart/form-data. I think that'd make everyone happy. |
The cited HTTP clarification also allows for transformation of the resource. Therefore, the returned resource need not be identical but representative of the resource created via the PUT request. Additionally, an indicator of the requested data type, either through the accept header or an extension, could be included with a subsequent GET request to indeed return a form identical to that submitted. @greatwitenorth I agree with the assessment that PUT is broken out of the box. The various html specs do indicate that POST and GET are the only valid methods for a form submission, but HTTP has no such restriction. In fact, were rfc6861 to be adopted PUT requests containing |
@@ -242,7 +249,7 @@ public function move($directory, $name = null) | |||
|
|||
$target = $this->getTargetFile($directory, $name); | |||
|
|||
if (!@move_uploaded_file($this->getPathname(), $target)) { | |||
if (!@rename($this->getPathname(), $target)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like this will not work with open_basedir
option http://ua2.php.net/manual/en/ini.core.php#ini.open-basedir
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Surely that would be up to the server configuration? This pull is using sys_get_temp_dir to get the temporary dir for PHP. I would imagine that the server is configured incorrectly if a server admin chooses to set a temporary dir for PHP, and then use open_basedir in such a way that PHP is denied access to the temp dir.
PHP, and by extension, Symfony does not support multipart/form-data requests when using any request method other than POST. This limits the ability to implement RESTful architectures using Symfony. This is a follow up to Pull Request symfony#849 I made a few years ago, and addresses concerns brought up by Saldaek at the time. I have implemented this functionality by manually decoding the php://input stream when the request type is PUT, DELETE or PATCH and the Content-Type header is mutlipart/form-data. The implementation is based on an example by [netcoder at stackoverflow](http://stackoverflow.com/a/9469615). This is necessary due to an underlying limitation of PHP, as discussed here: https://bugs.php.net/bug.php?id=55815. This fixes symfony#9226. __Security Concerns__ The main concern I had while implementing this feature, was working with PHP's assumptions as to what constituted an uploaded file. Since the files that this commit creates are not done so through PHP's usual uploaded file mechanisms, none of the uploaded file functions (such as is_uploaded_file, move_uploaded_file) will work with them. As such, I had to implement a mechanism for recording the uploaded files as they were created, so that Symfony's UploadedFile class could be sure that it was not moving non-uploaded files. To achieve this, I added a static array to the UploadedFile class called $files. Any uploaded files that are created outside of PHP's normal mechanism can be recorded here. UploadedFile can then check this list to ensure that if is_uploaded_file returns false, that the file being moved is in fact an uploaded file. This results in two changes to UploadedFile: Firstly, the is_uploaded_file check had to be expanded to also check if the file was recorded in UploadedFile::$files. Secondly, the move_uploaded_file needed to be changed to rename. The change of move_uploaded_file to rename might raise a few red flags, but I do not believe this is a problem. Before rename is called, UploadedFile calls isValid, which checks that the file either is_uploaded_file, or is a member of UploadedFile::$files. Therefore the use of move_uploaded_file is an unnecessary double check (even without these changes). Fix symfony#9226. Conflicts: src/Symfony/Component/HttpFoundation/Tests/RequestTest.php
I'm not comfortable with implementing our own decoding algo. Instead, this should be done in PHP itself, or at least, PHP should expose its internal implementation. |
PHP, and by extension, Symfony does not support multipart/form-data requests when using any request method other than POST. This limits the ability to implement RESTful architectures using Symfony. This is a follow up to Pull Request #849 I made a few years ago, and addresses concerns brought up by @Seldaek at the time.
I have implemented this functionality by manually decoding the php://input stream when the request type is PUT, DELETE or PATCH and the Content-Type header is mutlipart/form-data. The implementation is based on an example by netcoder at stackoverflow.
This is necessary due to an underlying limitation of PHP, as discussed here: https://bugs.php.net/bug.php?id=55815.
Security Concerns
The main concern I had while implementing this feature, was working with PHP's assumptions as to what constituted an uploaded file. Since the files that this commit creates are not done so through PHP's usual uploaded file mechanisms, none of the uploaded file functions (such as is_uploaded_file, move_uploaded_file) will work with them. As such, I had to implement a mechanism for recording the uploaded files as they were created, so that Symfony's UploadedFile class could be sure that it was not moving non-uploaded files.
To achieve this, I added a static array to the UploadedFile class called $files. Any uploaded files that are created outside of PHP's normal mechanism can be recorded here. UploadedFile can then check this list to ensure that if is_uploaded_file returns false, that the file being moved is in fact an uploaded file. This results in two changes to UploadedFile: Firstly, the is_uploaded_file check had to be expanded to also check if the file was recorded in UploadedFile::$files. Secondly, the move_uploaded_file needed to be changed to rename.
The change of move_uploaded_file to rename might raise a few red flags, but I do not believe this is a problem. Before rename is called, UploadedFile calls isValid, which checks that the file either is_uploaded_file, or is a member of UploadedFile::$files. Therefore the use of move_uploaded_file is an unnecessary double check (even without these changes).
Fix #9226.