Skip to content

Commit 8e8207b

Browse files
committed
Merge branch '6.0' into 6.1
* 6.0: Make FormErrorIterator generic [symfony/mailjet-mailer] Fix invalid mailjet error managment typehint of DkimOptions algorithm wrong Remove extra space in NotificationEmail Fix the usage of the Valid constraints in array-based forms Fix return value of `NullToken::getUser()` [DI] fix `ServiceSubscriberTrait` bug where parent has `__call()` [HttpClient] Fix reading proxy settings from dotenv when curl is used [Process] Don't return executable directories in PhpExecutableFinder Center icons vertically in trace list
2 parents 791ee7d + 480ab47 commit 8e8207b

File tree

14 files changed

+334
-24
lines changed

14 files changed

+334
-24
lines changed

src/Symfony/Bridge/Twig/Mime/NotificationEmail.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public function __construct(Headers $headers = null, AbstractPart $body = null)
4444
{
4545
$missingPackages = [];
4646
if (!class_exists(CssInlinerExtension::class)) {
47-
$missingPackages['twig/cssinliner-extra'] = ' CSS Inliner';
47+
$missingPackages['twig/cssinliner-extra'] = 'CSS Inliner';
4848
}
4949

5050
if (!class_exists(InkyExtension::class)) {

src/Symfony/Component/ErrorHandler/Resources/assets/css/exception.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ header .container { display: flex; justify-content: space-between; }
225225
.trace-line + .trace-line { border-top: var(--border); }
226226
.trace-line:hover { background: var(--base-1); }
227227
.trace-line a { color: var(--base-6); }
228-
.trace-line .icon { opacity: .4; position: absolute; left: 10px; top: 11px; }
228+
.trace-line .icon { opacity: .4; position: absolute; left: 10px; }
229229
.trace-line .icon svg { fill: var(--base-5); height: 16px; width: 16px; }
230230
.trace-line .icon.icon-copy { left: auto; top: auto; padding-left: 5px; display: none }
231231
.trace-line:hover .icon.icon-copy:not(.hidden) { display: inline-block }

src/Symfony/Component/Form/Extension/Validator/Constraints/FormValidator.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ public function validate(mixed $form, Constraint $formConstraint)
113113
foreach ($constraints as $constraint) {
114114
// For the "Valid" constraint, validate the data in all groups
115115
if ($constraint instanceof Valid) {
116-
if (\is_object($data)) {
116+
if (\is_object($data) || \is_array($data)) {
117117
$validator->atPath('data')->validate($data, $constraint, $groups);
118118
}
119119

src/Symfony/Component/Form/FormErrorIterator.php

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,11 @@
2929
*
3030
* @author Bernhard Schussek <bschussek@gmail.com>
3131
*
32-
* @implements \ArrayAccess<int, FormError|FormErrorIterator>
33-
* @implements \RecursiveIterator<int, FormError|FormErrorIterator>
34-
* @implements \SeekableIterator<int, FormError|FormErrorIterator>
32+
* @template T of FormError|FormErrorIterator
33+
*
34+
* @implements \ArrayAccess<int, T>
35+
* @implements \RecursiveIterator<int, T>
36+
* @implements \SeekableIterator<int, T>
3537
*/
3638
class FormErrorIterator implements \RecursiveIterator, \SeekableIterator, \ArrayAccess, \Countable
3739
{
@@ -41,10 +43,14 @@ class FormErrorIterator implements \RecursiveIterator, \SeekableIterator, \Array
4143
public const INDENTATION = ' ';
4244

4345
private FormInterface $form;
46+
47+
/**
48+
* @var list<T>
49+
*/
4450
private array $errors;
4551

4652
/**
47-
* @param list<FormError|self> $errors
53+
* @param list<T> $errors
4854
*
4955
* @throws InvalidArgumentException If the errors are invalid
5056
*/
@@ -72,7 +78,7 @@ public function __toString(): string
7278
$string .= 'ERROR: '.$error->getMessage()."\n";
7379
} else {
7480
/* @var self $error */
75-
$string .= $error->form->getName().":\n";
81+
$string .= $error->getForm()->getName().":\n";
7682
$string .= self::indent((string) $error);
7783
}
7884
}
@@ -90,6 +96,8 @@ public function getForm(): FormInterface
9096

9197
/**
9298
* Returns the current element of the iterator.
99+
*
100+
* @return T An error or an iterator containing nested errors
93101
*/
94102
public function current(): FormError|self
95103
{
@@ -146,6 +154,8 @@ public function offsetExists(mixed $position): bool
146154
*
147155
* @param int $position The position
148156
*
157+
* @return T
158+
*
149159
* @throws OutOfBoundsException If the given position does not exist
150160
*/
151161
public function offsetGet(mixed $position): FormError|self
@@ -192,7 +202,10 @@ public function getChildren(): self
192202
throw new LogicException(sprintf('The current element is not iterable. Use "%s" to get the current element.', self::class.'::current()'));
193203
}
194204

195-
return current($this->errors);
205+
/** @var self $children */
206+
$children = current($this->errors);
207+
208+
return $children;
196209
}
197210

198211
/**

src/Symfony/Component/Form/Tests/Extension/Validator/Constraints/FormValidatorFunctionalTest.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use Symfony\Component\Form\AbstractType;
1616
use Symfony\Component\Form\CallbackTransformer;
1717
use Symfony\Component\Form\Exception\TransformationFailedException;
18+
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
1819
use Symfony\Component\Form\Extension\Core\Type\DateType;
1920
use Symfony\Component\Form\Extension\Core\Type\FormType;
2021
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
@@ -321,6 +322,35 @@ public function testCascadeValidationToChildFormsWithTwoValidConstraints2()
321322
$this->assertSame('children[author].data.email', $violations[1]->getPropertyPath());
322323
}
323324

325+
public function testCascadeValidationToArrayChildForm()
326+
{
327+
$form = $this->formFactory->create(FormType::class, null, [
328+
'data_class' => Review::class,
329+
])
330+
->add('title')
331+
->add('customers', CollectionType::class, [
332+
'mapped' => false,
333+
'entry_type' => CustomerType::class,
334+
'allow_add' => true,
335+
'constraints' => [new Valid()],
336+
]);
337+
338+
$form->submit([
339+
'title' => 'Sample Title',
340+
'customers' => [
341+
['email' => null],
342+
],
343+
]);
344+
345+
$violations = $this->validator->validate($form);
346+
347+
$this->assertCount(2, $violations);
348+
$this->assertSame('This value should not be blank.', $violations[0]->getMessage());
349+
$this->assertSame('data.rating', $violations[0]->getPropertyPath());
350+
$this->assertSame('This value should not be blank.', $violations[1]->getMessage());
351+
$this->assertSame('children[customers].data[0].email', $violations[1]->getPropertyPath());
352+
}
353+
324354
public function testCascadeValidationToChildFormsUsingPropertyPathsValidatedInSequence()
325355
{
326356
$form = $this->formFactory->create(FormType::class, null, [

src/Symfony/Component/HttpClient/CurlHttpClient.php

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,10 @@ public function request(string $method, string $url, array $options = []): Respo
9191
$authority = $url['authority'];
9292
$host = parse_url($authority, \PHP_URL_HOST);
9393
$port = parse_url($authority, \PHP_URL_PORT) ?: ('http:' === $scheme ? 80 : 443);
94+
$proxy = $options['proxy']
95+
?? ('https:' === $url['scheme'] ? $_SERVER['https_proxy'] ?? $_SERVER['HTTPS_PROXY'] ?? null : null)
96+
// Ignore HTTP_PROXY except on the CLI to work around httpoxy set of vulnerabilities
97+
?? $_SERVER['http_proxy'] ?? (\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true) ? $_SERVER['HTTP_PROXY'] ?? null : null) ?? $_SERVER['all_proxy'] ?? $_SERVER['ALL_PROXY'] ?? null;
9498
$url = implode('', $url);
9599

96100
if (!isset($options['normalized_headers']['user-agent'])) {
@@ -106,7 +110,7 @@ public function request(string $method, string $url, array $options = []): Respo
106110
\CURLOPT_MAXREDIRS => 0 < $options['max_redirects'] ? $options['max_redirects'] : 0,
107111
\CURLOPT_COOKIEFILE => '', // Keep track of cookies during redirects
108112
\CURLOPT_TIMEOUT => 0,
109-
\CURLOPT_PROXY => $options['proxy'],
113+
\CURLOPT_PROXY => $proxy,
110114
\CURLOPT_NOPROXY => $options['no_proxy'] ?? $_SERVER['no_proxy'] ?? $_SERVER['NO_PROXY'] ?? '',
111115
\CURLOPT_SSL_VERIFYPEER => $options['verify_peer'],
112116
\CURLOPT_SSL_VERIFYHOST => $options['verify_host'] ? 2 : 0,
@@ -404,8 +408,15 @@ private static function createRedirectResolver(array $options, string $host, int
404408
}
405409

406410
$url = self::parseUrl(curl_getinfo($ch, \CURLINFO_EFFECTIVE_URL));
411+
$url = self::resolveUrl($location, $url);
407412

408-
return implode('', self::resolveUrl($location, $url));
413+
curl_setopt($ch, \CURLOPT_PROXY, $options['proxy']
414+
?? ('https:' === $url['scheme'] ? $_SERVER['https_proxy'] ?? $_SERVER['HTTPS_PROXY'] ?? null : null)
415+
// Ignore HTTP_PROXY except on the CLI to work around httpoxy set of vulnerabilities
416+
?? $_SERVER['http_proxy'] ?? (\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true) ? $_SERVER['HTTP_PROXY'] ?? null : null) ?? $_SERVER['all_proxy'] ?? $_SERVER['ALL_PROXY'] ?? null
417+
);
418+
419+
return implode('', $url);
409420
};
410421
}
411422

src/Symfony/Component/Mailer/Bridge/Mailjet/Tests/Transport/MailjetApiTransportTest.php

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,12 @@
33
namespace Symfony\Component\Mailer\Bridge\Mailjet\Tests\Transport;
44

55
use PHPUnit\Framework\TestCase;
6+
use Symfony\Component\HttpClient\MockHttpClient;
7+
use Symfony\Component\HttpClient\Response\MockResponse;
68
use Symfony\Component\Mailer\Bridge\Mailjet\Transport\MailjetApiTransport;
79
use Symfony\Component\Mailer\Envelope;
10+
use Symfony\Component\Mailer\Exception\HttpTransportException;
11+
use Symfony\Component\Mailer\SentMessage;
812
use Symfony\Component\Mime\Address;
913
use Symfony\Component\Mime\Email;
1014

@@ -84,6 +88,183 @@ public function testPayloadFormat()
8488
$this->assertEquals('Qux', $replyTo['Name']);
8589
}
8690

91+
public function testSendSuccess()
92+
{
93+
$json = json_encode([
94+
'Messages' => [
95+
'foo' => 'bar',
96+
],
97+
]);
98+
99+
$responseHeaders = [
100+
'x-mj-request-guid' => ['baz'],
101+
];
102+
103+
$response = new MockResponse($json, ['response_headers' => $responseHeaders]);
104+
105+
$client = new MockHttpClient($response);
106+
107+
$transport = new MailjetApiTransport(self::USER, self::PASSWORD, $client);
108+
109+
$email = new Email();
110+
$email
111+
->from('foo@example.com')
112+
->to('bar@example.com')
113+
->text('foobar');
114+
115+
$sentMessage = $transport->send($email);
116+
$this->assertInstanceOf(SentMessage::class, $sentMessage);
117+
$this->assertSame('baz', $sentMessage->getMessageId());
118+
}
119+
120+
public function testSendWithDecodingException()
121+
{
122+
$response = new MockResponse('cannot-be-decoded');
123+
124+
$client = new MockHttpClient($response);
125+
126+
$transport = new MailjetApiTransport(self::USER, self::PASSWORD, $client);
127+
128+
$email = new Email();
129+
$email
130+
->from('foo@example.com')
131+
->to('bar@example.com')
132+
->text('foobar');
133+
134+
$this->expectExceptionObject(
135+
new HttpTransportException('Unable to send an email: "cannot-be-decoded" (code 200).', $response)
136+
);
137+
138+
$transport->send($email);
139+
}
140+
141+
public function testSendWithTransportException()
142+
{
143+
$response = new MockResponse('', ['error' => 'foo']);
144+
145+
$client = new MockHttpClient($response);
146+
147+
$transport = new MailjetApiTransport(self::USER, self::PASSWORD, $client);
148+
149+
$email = new Email();
150+
$email
151+
->from('foo@example.com')
152+
->to('bar@example.com')
153+
->text('foobar');
154+
155+
$this->expectExceptionObject(
156+
new HttpTransportException('Could not reach the remote Mailjet server.', $response)
157+
);
158+
159+
$transport->send($email);
160+
}
161+
162+
public function testSendWithBadRequestResponse()
163+
{
164+
$json = json_encode([
165+
'Messages' => [
166+
[
167+
'Errors' => [
168+
[
169+
'ErrorIdentifier' => '8e28ac9c-1fd7-41ad-825f-1d60bc459189',
170+
'ErrorCode' => 'mj-0005',
171+
'StatusCode' => 400,
172+
'ErrorMessage' => 'The To is mandatory but missing from the input',
173+
'ErrorRelatedTo' => ['To'],
174+
],
175+
],
176+
'Status' => 'error',
177+
],
178+
],
179+
]);
180+
181+
$response = new MockResponse($json, ['http_code' => 400]);
182+
183+
$client = new MockHttpClient($response);
184+
185+
$transport = new MailjetApiTransport(self::USER, self::PASSWORD, $client);
186+
187+
$email = new Email();
188+
$email
189+
->from('foo@example.com')
190+
->to('bar@example.com')
191+
->text('foobar');
192+
193+
$this->expectExceptionObject(
194+
new HttpTransportException('Unable to send an email: "The To is mandatory but missing from the input" (code 400).', $response)
195+
);
196+
197+
$transport->send($email);
198+
}
199+
200+
public function testSendWithNoErrorMessageBadRequestResponse()
201+
{
202+
$response = new MockResponse('response-content', ['http_code' => 400]);
203+
204+
$client = new MockHttpClient($response);
205+
206+
$transport = new MailjetApiTransport(self::USER, self::PASSWORD, $client);
207+
208+
$email = new Email();
209+
$email
210+
->from('foo@example.com')
211+
->to('bar@example.com')
212+
->text('foobar');
213+
214+
$this->expectExceptionObject(
215+
new HttpTransportException('Unable to send an email: "response-content" (code 400).', $response)
216+
);
217+
218+
$transport->send($email);
219+
}
220+
221+
/**
222+
* @dataProvider getMalformedResponse
223+
*/
224+
public function testSendWithMalformedResponse(array $body)
225+
{
226+
$json = json_encode($body);
227+
228+
$response = new MockResponse($json);
229+
230+
$client = new MockHttpClient($response);
231+
232+
$transport = new MailjetApiTransport(self::USER, self::PASSWORD, $client);
233+
234+
$email = new Email();
235+
$email
236+
->from('foo@example.com')
237+
->to('bar@example.com')
238+
->text('foobar');
239+
240+
$this->expectExceptionObject(
241+
new HttpTransportException(sprintf('Unable to send an email: "%s" malformed api response.', $json), $response)
242+
);
243+
244+
$transport->send($email);
245+
}
246+
247+
public function getMalformedResponse(): \Generator
248+
{
249+
yield 'Missing Messages key' => [
250+
[
251+
'foo' => 'bar',
252+
],
253+
];
254+
255+
yield 'Messages is not an array' => [
256+
[
257+
'Messages' => 'bar',
258+
],
259+
];
260+
261+
yield 'Messages is an empty array' => [
262+
[
263+
'Messages' => [],
264+
],
265+
];
266+
}
267+
87268
public function testReplyTo()
88269
{
89270
$from = 'foo@example.com';

src/Symfony/Component/Mailer/Bridge/Mailjet/Transport/MailjetApiTransport.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,13 +81,15 @@ protected function doSendApi(SentMessage $sentMessage, Email $email, Envelope $e
8181
$statusCode = $response->getStatusCode();
8282
$result = $response->toArray(false);
8383
} catch (DecodingExceptionInterface $e) {
84-
throw new HttpTransportException('Unable to send an email: '.$response->getContent(false).sprintf(' (code %d).', $statusCode), $response);
84+
throw new HttpTransportException(sprintf('Unable to send an email: "%s" (code %d).', $response->getContent(false), $statusCode), $response);
8585
} catch (TransportExceptionInterface $e) {
8686
throw new HttpTransportException('Could not reach the remote Mailjet server.', $response, 0, $e);
8787
}
8888

8989
if (200 !== $statusCode) {
90-
throw new HttpTransportException('Unable to send an email: '.$result['Message'].sprintf(' (code %d).', $statusCode), $response);
90+
$errorDetails = $result['Messages'][0]['Errors'][0]['ErrorMessage'] ?? $response->getContent(false);
91+
92+
throw new HttpTransportException(sprintf('Unable to send an email: "%s" (code %d).', $errorDetails, $statusCode), $response);
9193
}
9294

9395
// The response needs to contains a 'Messages' key that is an array

0 commit comments

Comments
 (0)