8000 [Mime] Allow multiple parts with the same name in FormDataPart by HypeMC · Pull Request #38323 · symfony/symfony · GitHub
[go: up one dir, main page]

Skip to content

[Mime] Allow multiple parts with the same name in FormDataPart #38323

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

Merged
merged 1 commit into from
Oct 2, 2020
Merged

[Mime] Allow multiple parts with the same name in FormDataPart #38323

merged 1 commit into from
Oct 2, 2020

Conversation

HypeMC
Copy link
Contributor
@HypeMC HypeMC commented Sep 27, 2020
Q A
Branch? master
Bug fix? no
New feature? yes
Deprecations? no
Tickets Fix #38258
License MIT
Doc PR -

Added the possibility to choose whether multiple parts with the same name should be suffixed or not:

$f1 = new FormDataPart([
    'foo' => [
        'one',
        'two',
    ],
]);

var_dump($f1->toString());
Content-Type: multipart/form-data; boundary=6rqazwiG

--6rqazwiG
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: 8bit
Content-Disposition: form-data; name="foo[0]"

one
--6rqazwiG
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: 8bit
Content-Disposition: form-data; name="foo[1]"

two
--6rqazwiG--
$f1 = new FormDataPart([
    ['foo' => 'one'],
    ['foo' => 'two'],
]);

var_dump($f1->toString());
Content-Type: multipart/form-data; boundary=xxW9dGzq

--xxW9dGzq
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: 8bit
Content-Disposition: form-data; name="foo"

one
--xxW9dGzq
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: 8bit
Content-Disposition: form-data; name="foo"

two
--xxW9dGzq--

Only applies to numeric keys:

$f1 = new FormDataPart([
    'foo' => [
        'a' => 'one',
        'b' => 'two',
    ],
], true);

var_dump($f1->toString());
Content-Type: multipart/form-data; boundary=W6qkqgrD

--W6qkqgrD
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: 8bit
Content-Disposition: form-data; name="foo[a]"

one
--W6qkqgrD
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: 8bit
Content-Disposition: form-data; name="foo[b]"

two
--W6qkqgrD--

@nicolas-grekas
Copy link
Member
nicolas-grekas commented Sep 29, 2020

I think it would be better to replace the constructor argument and use a dedicated data structure instead.

My proposal would be this one:

$f1 = new FormDataPart([
    ['foo' => 'one'],
    ['foo' => 'two'],
]);

the logic would be:

when the main array has a numeric key, then enforce 1 === count($value), then take key($value) as the exact key to put in the output.

@HypeMC
Copy link
Contributor Author
HypeMC commented Sep 29, 2020

@nicolas-grekas The PR has been updated according to your suggestion.

A limitation of this approach in comparison to the previous one is if the field name is actually meant to be an integer, eg the following will now throw an exception:

$f1 = new FormDataPart([
    '1' => [
        'a' => 'one',
        'b' => 'two',
    ],
]);

The only way to achieve something like this now would be to manually name the fields appropriately:

$f1 = new FormDataPart([
    ['1[a]' => 'one'],
    ['1[b]' => 'two'],
]);

Since I doubt using integers as field names is common, this shouldn't be an issue, nonetheless I wanted to point it out just in case.

Copy link
Member
@nicolas-grekas nicolas-grekas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only way to achieve something like this now would be to manually name the fields appropriately: [...]

Correct. I also think we can do the change without any practical BC impact. The good news is that if one still wants to use numeric names (who would, but anyway), then it's still possible.

@nicolas-grekas
Copy link
Member

@HypeMC could you please consider sending a PR to the doc to tell about this behavior?

@HypeMC
Copy link
Contributor Author
HypeMC commented Sep 30, 2020

@nicolas-grekas Yep, sure, will do this week, tnx.

@nicolas-grekas
Copy link
Member
nicolas-grekas commented Oct 2, 2020

This looks like a new feature to me btw. We should merge it in master. (now rebased for master)

@nicolas-grekas nicolas-grekas modified the milestones: 4.4, next Oct 2, 2020
@nicolas-grekas nicolas-grekas changed the base branch from 4.4 to master October 2, 2020 09:11
@HypeMC
Copy link
Contributor Author
HypeMC commented Oct 2, 2020

@nicolas-grekas This looks more like a bug to me since it's not possible to make a valid request with the current code. Also, when considering that the actual sprintf part was added as bug in #34032 it really doesn't make sense to view this as anything different. Then there's the fact that the fix is only a couple of lines & doesn't introduce any BC.

Without this fix, users using Sf4 won't be ably to make valid requests until they upgrade. Since some only use LTS versions in their production apps, it'll be some time till they get this fix.

@fabpot
Copy link
Member
fabpot commented Oct 2, 2020

Unfortunately, that's a new feature and as such it must be merged in master. There is no such thing as "a couple of lines" and "no BC breaks" in semantic versioning.

< 8000 a class="d-inline-block" data-hovercard-type="user" data-hovercard-url="/users/fabpot/hovercard" data-octo-click="hovercard-link-click" data-octo-dimensions="link_type:self" href="/fabpot">@fabpot
Copy link
Member
fabpot commented Oct 2, 2020

Thank you @HypeMC.

@HypeMC
Copy link
Contributor Author
HypeMC commented Oct 2, 2020

@fabpot I guess I understand your side as well, I think it really boils down to whether the FormDataPart was originally designed to support this type of behavior in the first place, or not.

In any case, there is a workaround for Sf4 which I mentioned in #38258 so it's not a big deal. Just wanted to give you my 2 cents 😄

@HypeMC HypeMC deleted the allow-multiple-parts-with-the-same-name branch October 2, 2020 13:38
@nicolas-grekas nicolas-grekas modified the milestones: next, 5.2 Oct 5, 2020
@fabpot fabpot mentioned this pull request Oct 5, 2020
fabpot added a commit that referenced this pull request Dec 11, 2021
…gunApiTransport (starred-gijs)

This PR was merged into the 6.1 branch.

Discussion
----------

[Mailer] [Mailgun] Allow multiple TagHeaders with MailgunApiTransport

| Q             | A
| ------------- | ---
| Branch?       | 6.1
| Bug fix?      | no
| New feature?  | yes
| Deprecations? | no
| Tickets       | -
| License       | MIT
| Doc PR        | -

Mailgun allows you to set [3 tags per message](https://documentation.mailgun.com/en/latest/user_manual.html#tagging) but the current implementation in MailgunApiTransport, any consecutive TagHeader will override the previous one. Because it uses FormDataPart to build the payload, by using a integer key, it allows to set multiple parts with the same name #38323
The MailgunHttpTransport already supports multiple TagHeaders and I added a test to prove that.

Commits
-------

fa65173 [Mailer][Mailgun] Allow multiple tags with MailgunApiTransport
@claudiu-cristea
Copy link
Contributor
claudiu-cristea commented Sep 7, 2022

A limitation of this approach in comparison to the previous one is if the field name is actually meant to be an integer, eg the following will now throw an exception:

@HypeMC, meaning that these fields are no more valid? Why?

<input type="hidden" name="bar[0][foo]" value="v1">
<input type="hidden" name="bar[0][baz]" value="v2">

After I've updated a dependency that now requires symfony/mime, I started to see the following exception when posting a form having these kind of fields:

Form field values with integer keys can only have one array element, the key being the field name and the value being the field value, 2 provided.

EDIT: Specifically, it happens in Behat tests and this is my chain of dependencies: drupal/drupal-extension > behat/mink-goutte-driver > fabpot/goutte > symfony/mime

@HypeMC
Copy link
Contributor Author
HypeMC commented Sep 7, 2022

@claudiu-cristea As mentioned in this PR, it is possible, just requires a different syntax:

$f1 = new FormDataPart([
    'bar[0]' => [
        'foo' => 'v1',
        'baz' => 'v2',
    ],
]);

var_dump($f1->toString());
Content-Type: multipart/form-data; boundary=VaX-0OyQ

--VaX-0OyQ
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: 8bit
Content-Disposition: form-data; name="bar[0][foo]"

v1
--VaX-0OyQ
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: 8bit
Content-Disposition: form-data; name="bar[0][baz]"

v2
--VaX-0OyQ--

@claudiu-cristea
Copy link
Contributor

@HypeMC, well this is transparent to me. I'm not creating that array, it's created under the hood. I'm only posting such a form. So, where should occur this change?

@claudiu-cristea
Copy link
Contributor

@HypeMC, instead of throwing the exception, why not solving the issue? Wouldn't this change fix it?

diff --git a/vendor/symfony/mime/Part/Multipart/FormDataPart.php b/vendor/symfony/mime/Part/Multipart/FormDataPart.php
--- a/vendor/symfony/mime/Part/Multipart/FormDataPart.php	
+++ b/vendor/symfony/mime/Part/Multipart/FormDataPart.php	(date 1662536878627)
@@ -59,12 +59,16 @@
 
         $prepare = function ($item, $key, $root = null) use (&$values, &$prepare) {
             if (\is_int($key) && \is_array($item)) {
-                if (1 !== \count($item)) {
-                    throw new InvalidArgumentException(sprintf('Form field values with integer keys can only have one array element, the key being the field name and the value being the field value, %d provided.', \count($item)));
-                }
-
-                $key = key($item);
-                $item = $item[$key];
+                if (1 === \count($item)) {
+                    $key = key($item);
+                    $item = $item[$key];
+                } else {
+                    $items = [];
+                    foreach ($item as $item_key => $itemValue) {
+                        $items[$key . "[$item_key]"] = $itemValue;
+                    }
+                    array_walk($items, $prepare, $root);
+                }
             }
 
             $fieldName = null !== $root ? sprintf('%s[%s]', $root, $key) : $key;

@claudiu-cristea
Copy link
Contributor

@HypeMC the above diff is not quite correct, here's my proposal #47505

@claudiu-cristea
Copy link
Contributor

@HypeMC, yet another problem with this change: the name in form is qux[0][value] (no other items under qux[0]) and is transformed in qux[value], which is wrong. I cannot determine a different array passed to FormDataPart constructor as this is called transparently from HttpClient

@HypeMC
Copy link
Contributor Author
HypeMC commented Sep 7, 2022

@HypeMC, yet another problem with this change: the name in form is qux[0][value] (no other items under qux[0]) and is transformed in qux[value], which is wrong.

Yes, that's the expected behavior for that particular data structure, eg:

$f1 = new FormDataPart([
    'qux' => [
        ['value' => 'v1'],
    ],
]);

is suppose to produce:

Content-Type: multipart/form-data; boundary=zljZ75JO

--zljZ75JO
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: 8bit
Content-Disposition: form-data; name="qux[value]"

v1
--zljZ75JO--

This was done so you could have multiple parts with the same name, eg:

$f1 = new FormDataPart([
    'qux' => [
        ['value' => 'v1'],
        ['value' => 'v2'],
    ],
]);
Content-Type: multipart/form-data; boundary=aUHKdONN

--aUHKdONN
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: 8bit
Content-Disposition: form-data; name="qux[value]"

v1
--aUHKdONN
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: 8bit
Content-Disposition: form-data; name="qux[value]"

v2
--aUHKdONN--

Not sure what the best course of action would be here, perhaps the bug could be patched in the HttpBrowser instead?

@claudiu-cristea
Copy link
Contributor
claudiu-cristea commented Sep 7, 2022

is suppose to produce:

Content-Type: multipart/form-data; boundary=zljZ75JO

--zljZ75JO
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: 8bit
Content-Disposition: form-data; name="qux[value]"

v1
--zljZ75JO–

This is wrong, I'm expecting name="qux[0][value]"

@HypeMC
Copy link
Contributor Author
HypeMC commented Sep 7, 2022

Yes, I understand that, I'm just explaining what the original idea was, for your case one would do:

$f1 = new FormDataPart([
    'qux[0]' => [
        ['value' => 'v1'],
        ['value' => 'v2'],
    ],
]);

But since you're not using FormDataPart directly, I'm not sure what the solution would be. In any case, no one will notice this discussion since the PR is closed, so it should all be mentioned on the issue you've created. Perhaps someone will have an idea how to tackle this problem.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[HttpClient] Multiple form data fields with the same name
5 participants
0