Skip to content

Commit ba614ef

Browse files
committed
Merge branch '7.0' into 7.1
* 7.0: [VarExporter] Uniform unitialized property error message under ghost and non-ghost objects [AssetMapper] Ignore comment lines in JavaScriptImportPathCompiler Update configuration path in help message [Validator] Review Albanian translation [Process] Fix Inconsistent Exit Status in proc_get_status for PHP Versions Below 8.3 [Validator] Update Czech (cz) translation Sync translations [Mailer][Postmark][Webhook] Make allowed IPs configurable Review portuguese translations [Validator] Fix fields without constraints in `Collection` deal with fields for which no constraints have been configured [DomCrawler] [Form] Fix the exclusion of <template>
2 parents 0c362d2 + 7877f70 commit ba614ef

File tree

17 files changed

+254
-30
lines changed

17 files changed

+254
-30
lines changed

src/Symfony/Component/AssetMapper/Compiler/JavaScriptImportPathCompiler.php

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,31 @@
2727
*/
2828
final class JavaScriptImportPathCompiler implements AssetCompilerInterface
2929
{
30-
// https://regex101.com/r/qFoeoR/1
31-
private const IMPORT_PATTERN = '/(?:\'(?:[^\'\\\\]|\\\\.)*\'|"(?:[^"\\\\]|\\\\.)*")|(?:import\s*(?:(?:\*\s*as\s+\w+|[\w\s{},*]+)\s*from\s*)?|\bimport\()\s*[\'"`](\.\/[^\'"`]+|(\.\.\/)*[^\'"`]+)[\'"`]\s*[;\)]?/m';
30+
/**
31+
* @see https://regex101.com/r/1iBAIb/1
32+
*/
33+
private const IMPORT_PATTERN = '/
34+
^
35+
(?:\/\/.*) # Lines that start with comments
36+
|
37+
(?:
38+
\'(?:[^\'\\\\]|\\\\.)*\' # Strings enclosed in single quotes
39+
|
40+
"(?:[^"\\\\]|\\\\.)*" # Strings enclosed in double quotes
41+
)
42+
|
43+
(?: # Import statements (script captured)
44+
import\s*
45+
(?:
46+
(?:\*\s*as\s+\w+|\s+[\w\s{},*]+)
47+
\s*from\s*
48+
)?
49+
|
50+
\bimport\(
51+
)
52+
\s*[\'"`](\.\/[^\'"`]+|(\.\.\/)*[^\'"`]+)[\'"`]\s*[;\)]
53+
?
54+
/mx';
3255

3356
public function __construct(
3457
private readonly ImportMapConfigReader $importMapConfigReader,
@@ -42,7 +65,7 @@ public function compile(string $content, MappedAsset $asset, AssetMapperInterfac
4265
return preg_replace_callback(self::IMPORT_PATTERN, function ($matches) use ($asset, $assetMapper, $content) {
4366
$fullImportString = $matches[0][0];
4467

45-
// Ignore enquoted strings (e.g. console.log("import 'foo';")
68+
// Ignore matches that did not capture import statements
4669
if (!isset($matches[1][0])) {
4770
return $fullImportString;
4871
}

src/Symfony/Component/AssetMapper/Tests/Compiler/JavaScriptImportPathCompilerTest.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,16 @@ public static function provideCompileTests(): iterable
177177
'expectedJavaScriptImports' => ['/assets/other.js' => ['lazy' => false, 'asset' => 'other.js', 'add' => true]],
178178
];
179179

180+
yield 'commented_import_on_one_line_then_module_name_on_next_is_not_ok' => [
181+
'input' => "// import \n './other.js';",
182+
'expectedJavaScriptImports' => [],
183+
];
184+
185+
yield 'commented_import_on_one_line_then_import_on_next_is_ok' => [
186+
'input' => "// import\nimport { Foo } from './other.js';",
187+
'expectedJavaScriptImports' => ['/assets/other.js' => ['lazy' => false, 'asset' => 'other.js', 'add' => true]],
188+
];
189+
180190
yield 'importing_a_css_file_is_included' => [
181191
'input' => "import './styles.css';",
182192
'expectedJavaScriptImports' => ['/assets/styles.css' => ['lazy' => false, 'asset' => 'styles.css', 'add' => true]],

src/Symfony/Component/DomCrawler/Form.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -418,14 +418,14 @@ private function initialize(): void
418418
// corresponding elements are either descendants or have a matching HTML5 form attribute
419419
$formId = Crawler::xpathLiteral($this->node->getAttribute('id'));
420420

421-
$fieldNodes = $xpath->query(sprintf('( descendant::input[@form=%s] | descendant::button[@form=%1$s] | descendant::textarea[@form=%1$s] | descendant::select[@form=%1$s] | //form[@id=%1$s]//input[not(@form)] | //form[@id=%1$s]//button[not(@form)] | //form[@id=%1$s]//textarea[not(@form)] | //form[@id=%1$s]//select[not(@form)] )[not(ancestor::template)]', $formId));
421+
$fieldNodes = $xpath->query(sprintf('( descendant::input[@form=%s] | descendant::button[@form=%1$s] | descendant::textarea[@form=%1$s] | descendant::select[@form=%1$s] | //form[@id=%1$s]//input[not(@form)] | //form[@id=%1$s]//button[not(@form)] | //form[@id=%1$s]//textarea[not(@form)] | //form[@id=%1$s]//select[not(@form)] )[( not(ancestor::template) or ancestor::turbo-stream )]', $formId));
422422
foreach ($fieldNodes as $node) {
423423
$this->addField($node);
424424
}
425425
} else {
426426
// do the xpath query with $this->node as the context node, to only find descendant elements
427427
// however, descendant elements with form attribute are not part of this form
428-
$fieldNodes = $xpath->query('( descendant::input[not(@form)] | descendant::button[not(@form)] | descendant::textarea[not(@form)] | descendant::select[not(@form)] )[not(ancestor::template)]', $this->node);
428+
$fieldNodes = $xpath->query('( descendant::input[not(@form)] | descendant::button[not(@form)] | descendant::textarea[not(@form)] | descendant::select[not(@form)] )[( not(ancestor::template) or ancestor::turbo-stream )]', $this->node);
429429
foreach ($fieldNodes as $node) {
430430
$this->addField($node);
431431
}

src/Symfony/Component/DomCrawler/Tests/FormTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,9 @@ public function testGetValues()
432432
$form = $this->createForm('<form><template><input type="text" name="foo" value="foo" /></template><input type="text" name="bar" value="bar" /><input type="submit" /></form>');
433433
$this->assertEquals(['bar' => 'bar'], $form->getValues(), '->getValues() does not include template fields');
434434
$this->assertFalse($form->has('foo'));
435+
436+
$form = $this->createForm('<turbo-stream><template><form><input type="text" name="foo[bar]" value="foo" /><input type="text" name="bar" value="bar" /><select multiple="multiple" name="baz[]"></select><input type="submit" /></form></template></turbo-stream>');
437+
$this->assertEquals(['foo[bar]' => 'foo', 'bar' => 'bar', 'baz' => []], $form->getValues(), '->getValues() returns all form field values from template field inside a turbo-stream');
435438
}
436439

437440
public function testSetValues()
@@ -486,6 +489,9 @@ public function testGetFiles()
486489
$form = $this->createForm('<form method="post"><template><input type="file" name="foo"/></template><input type="text" name="bar" value="bar"/><input type="submit"/></form>');
487490
$this->assertEquals([], $form->getFiles(), '->getFiles() does not include template file fields');
488491
$this->assertFalse($form->has('foo'));
492+
493+
$form = $this->createForm('<turbo-stream><template><form method="post"><input type="file" name="foo[bar]" /><input type="text" name="bar" value="bar" /><input type="submit" /></form></template></turbo-stream>');
494+
$this->assertEquals(['foo[bar]' => ['name' => '', 'type' => '', 'tmp_name' => '', 'error' => 4, 'size' => 0]], $form->getFiles(), '->getFiles() return files fields from template inside turbo-stream');
489495
}
490496

491497
public function testGetPhpFiles()

src/Symfony/Component/Form/Resources/translations/validators.sq.xlf

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@
44
<header>
55
<note>
66
Për fjalët e huaja, të cilat nuk kanë përkthim të drejtpërdrejtë, ju lutemi të ndiqni rregullat e mëposhtme:
7-
a) në rast se emri është akronim i përdorur gjerësisht si i përveçëm, atëherë, emri lakohet pa thonjëza dhe mbaresa shkruhet me vizë ndarëse. Gjinia gjykohet sipas rastit. Shembull: JSON-i (mashkullore)
8-
b) në rast se emri është akronim i papërdorur gjerësisht si i përveçëm, atëherë, emri lakohet pa thonjëza dhe mbaresa shkruhet me vizë ndarëse. Gjinia është femërore. Shembull: URL-ja (femërore)
9-
c) në rast se emri duhet lakuar për shkak të rasës në fjali, atëherë, emri lakohet pa thonjëza dhe mbaresa shkruhet me vizë ndarëse. Shembull: host-i, prej host-it
10-
d) në rast se emri nuk duhet lakuar për shkak të trajtës në fjali, atëherë, emri rrethohet me thonjëzat “”. Shembull: “locale”
7+
a) në rast se emri është akronim i përdorur gjerësisht si i përveçëm, atëherë, emri lakohet pa thonjëza dhe mbaresa shkruhet me vizë ndarëse. Gjinia gjykohet sipas rastit. Shembull: JSON-i (mashkullore)
8+
b) në rast se emri është akronim i papërdorur gjerësisht si i përveçëm, atëherë, emri lakohet pa thonjëza dhe mbaresa shkruhet me vizë ndarëse. Gjinia është femërore. Shembull: URL-ja (femërore)
9+
c) në rast se emri duhet lakuar për shkak të rasës në fjali, atëherë, emri lakohet pa thonjëza dhe mbaresa shkruhet me vizë ndarëse. Shembull: host-i, prej host-it
10+
d) në rast se emri nuk duhet lakuar për shkak të trajtës në fjali, atëherë, emri rrethohet me thonjëzat “”. Shembull: “locale”
1111
</note>
1212
</header>
1313
<body>

src/Symfony/Component/Mailer/Bridge/Postmark/Webhook/PostmarkRequestParser.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,18 @@ final class PostmarkRequestParser extends AbstractRequestParser
2727
{
2828
public function __construct(
2929
private readonly PostmarkPayloadConverter $converter,
30+
31+
// https://postmarkapp.com/support/article/800-ips-for-firewalls#webhooks
32+
// localhost is added for testing
33+
private readonly array $allowedIPs = ['3.134.147.250', '50.31.156.6', '50.31.156.77', '18.217.206.57', '127.0.0.1'],
3034
) {
3135
}
3236

3337
protected function getRequestMatcher(): RequestMatcherInterface
3438
{
3539
return new ChainRequestMatcher([
3640
new MethodRequestMatcher('POST'),
37-
// https://postmarkapp.com/support/article/800-ips-for-firewalls#webhooks
38-
// localhost is added for testing
39-
new IpsRequestMatcher(['3.134.147.250', '50.31.156.6', '50.31.156.77', '18.217.206.57', '127.0.0.1']),
41+
new IpsRequestMatcher($this->allowedIPs),
4042
new IsJsonRequestMatcher(),
4143
]);
4244
}

src/Symfony/Component/PasswordHasher/Command/UserPasswordHashCommand.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ protected function configure(): void
6565
Suppose that you have the following security configuration in your application:
6666
6767
<comment>
68-
# app/config/security.yml
68+
# config/packages/security.yml
6969
security:
7070
password_hashers:
7171
Symfony\Component\Security\Core\User\InMemoryUser: plaintext

src/Symfony/Component/Process/Process.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ class Process implements \IteratorAggregate
8181
private WindowsPipes|UnixPipes $processPipes;
8282

8383
private ?int $latestSignal = null;
84+
private ?int $cachedExitCode = null;
8485

8586
private static ?bool $sigchild = null;
8687

@@ -1291,6 +1292,19 @@ protected function updateStatus(bool $blocking): void
12911292
$this->processInformation = proc_get_status($this->process);
12921293
$running = $this->processInformation['running'];
12931294

1295+
// In PHP < 8.3, "proc_get_status" only returns the correct exit status on the first call.
1296+
// Subsequent calls return -1 as the process is discarded. This workaround caches the first
1297+
// retrieved exit status for consistent results in later calls, mimicking PHP 8.3 behavior.
1298+
if (\PHP_VERSION_ID < 80300) {
1299+
if (!isset($this->cachedExitCode) && !$running && -1 !== $this->processInformation['exitcode']) {
1300+
$this->cachedExitCode = $this->processInformation['exitcode'];
1301+
}
1302+
1303+
if (isset($this->cachedExitCode) && !$running && -1 === $this->processInformation['exitcode']) {
1304+
$this->processInformation['exitcode'] = $this->cachedExitCode;
1305+
}
1306+
}
1307+
12941308
$this->readPipes($running && $blocking, '\\' !== \DIRECTORY_SEPARATOR || !$running);
12951309

12961310
if ($this->fallbackStatus && $this->isSigchildEnabled()) {

src/Symfony/Component/Process/Tests/ProcessTest.php

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1596,6 +1596,60 @@ public function testEnvCaseInsensitiveOnWindows()
15961596
}
15971597
}
15981598

1599+
public function testMultipleCallsToProcGetStatus()
1600+
{
1601+
$process = $this->getProcess('echo foo');
1602+
$process->start(static function () use ($process) {
1603+
return $process->isRunning();
1604+
});
1605+
while ($process->isRunning()) {
1606+
usleep(1000);
1607+
}
1608+
$this->assertSame(0, $process->getExitCode());
1609+
}
1610+
1611+
public function testFailingProcessWithMultipleCallsToProcGetStatus()
1612+
{
1613+
$process = $this->getProcess('exit 123');
1614+
$process->start(static function () use ($process) {
1615+
return $process->isRunning();
1616+
});
1617+
while ($process->isRunning()) {
1618+
usleep(1000);
1619+
}
1620+
$this->assertSame(123, $process->getExitCode());
1621+
}
1622+
1623+
/**
1624+
* @group slow
1625+
*/
1626+
public function testLongRunningProcessWithMultipleCallsToProcGetStatus()
1627+
{
1628+
$process = $this->getProcess('php -r "sleep(1); echo \'done\';"');
1629+
$process->start(static function () use ($process) {
1630+
return $process->isRunning();
1631+
});
1632+
while ($process->isRunning()) {
1633+
usleep(1000);
1634+
}
1635+
$this->assertSame(0, $process->getExitCode());
1636+
}
1637+
1638+
/**
1639+
* @group slow
1640+
*/
1641+
public function testLongRunningProcessWithMultipleCallsToProcGetStatusError()
1642+
{
1643+
$process = $this->getProcess('php -r "sleep(1); echo \'failure\'; exit(123);"');
1644+
$process->start(static function () use ($process) {
1645+
return $process->isRunning();
1646+
});
1647+
while ($process->isRunning()) {
1648+
usleep(1000);
1649+
}
1650+
$this->assertSame(123, $process->getExitCode());
1651+
}
1652+
15991653
/**
16001654
* @group transient-on-windows
16011655
*/

src/Symfony/Component/Validator/Constraints/Collection.php

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ class Collection extends Composite
4343
*/
4444
public function __construct(mixed $fields = null, ?array $groups = null, mixed $payload = null, ?bool $allowExtraFields = null, ?bool $allowMissingFields = null, ?string $extraFieldsMessage = null, ?string $missingFieldsMessage = null)
4545
{
46-
if (\is_array($fields) && ([] === $fields || ($firstField = reset($fields)) instanceof Constraint || ($firstField[0] ?? null) instanceof Constraint)) {
46+
if (self::isFieldsOption($fields)) {
4747
$fields = ['fields' => $fields];
4848
}
4949

@@ -81,4 +81,31 @@ protected function getCompositeOption(): string
8181
{
8282
return 'fields';
8383
}
84+
85+
private static function isFieldsOption($options): bool
86+
{
87+
if (!\is_array($options)) {
88+
return false;
89+
}
90+
91+
foreach ($options as $optionOrField) {
92+
if ($optionOrField instanceof Constraint) {
93+
return true;
94+
}
95+
96+
if (null === $optionOrField) {
97+
continue;
98+
}
99+
100+
if (!\is_array($optionOrField)) {
101+
return false;
102+
}
103+
104+
if ($optionOrField && !($optionOrField[0] ?? null) instanceof Constraint) {
105+
return false;
106+
}
107+
}
108+
109+
return true;
110+
}
84111
}

0 commit comments

Comments
 (0)