diff options
Diffstat (limited to 'core')
15 files changed, 285 insertions, 87 deletions
diff --git a/core/lib/Drupal/Component/Gettext/PoItem.php b/core/lib/Drupal/Component/Gettext/PoItem.php index 7cc32568ad23..89dee6cc0858 100644 --- a/core/lib/Drupal/Component/Gettext/PoItem.php +++ b/core/lib/Drupal/Component/Gettext/PoItem.php @@ -271,7 +271,7 @@ class PoItem { private function formatSingular() { $output = ''; $output .= 'msgid ' . $this->formatString($this->source); - $output .= 'msgstr ' . (isset($this->translation) ? $this->formatString($this->translation) : '""'); + $output .= 'msgstr ' . (isset($this->translation) ? $this->formatString($this->translation) : '""' . "\n"); return $output; } diff --git a/core/lib/Drupal/Core/Config/DatabaseStorage.php b/core/lib/Drupal/Core/Config/DatabaseStorage.php index 60da9de12470..299693f2c779 100644 --- a/core/lib/Drupal/Core/Config/DatabaseStorage.php +++ b/core/lib/Drupal/Core/Config/DatabaseStorage.php @@ -272,7 +272,7 @@ class DatabaseStorage implements StorageInterface { * be unserialized. */ public function decode($raw) { - $data = @unserialize($raw); + $data = @unserialize($raw, ['allowed_classes' => FALSE]); return is_array($data) ? $data : FALSE; } diff --git a/core/lib/Drupal/Core/Render/Renderer.php b/core/lib/Drupal/Core/Render/Renderer.php index 1f26381bb181..0a28501ae71c 100644 --- a/core/lib/Drupal/Core/Render/Renderer.php +++ b/core/lib/Drupal/Core/Render/Renderer.php @@ -615,12 +615,39 @@ class Renderer implements RendererInterface { * {@inheritdoc} */ public function executeInRenderContext(RenderContext $context, callable $callable) { - // Store the current render context. + // When executing in a render context, we need to isolate any bubbled + // context within this method. To allow for async rendering, it's necessary + // to detect if a fiber suspends within a render context. When this happens, + // we swap the previous render context in before suspending upwards, then + // back out again before resuming. $previous_context = $this->getCurrentRenderContext(); - // Set the provided context and call the callable, it will use that context. $this->setCurrentRenderContext($context); - $result = $callable(); + + $fiber = new \Fiber(static fn () => $callable()); + $fiber->start(); + while (!$fiber->isTerminated()) { + if ($fiber->isSuspended()) { + // When ::executeInRenderContext() is executed within a Fiber, which is + // always the case when rendering placeholders, if the callback results + // in this fiber being suspended, we need to suspend again up to the + // parent Fiber. Doing so allows other placeholders to be rendered + // before returning here. + if (\Fiber::getCurrent() !== NULL) { + $this->setCurrentRenderContext($previous_context); + \Fiber::suspend(); + $this->setCurrentRenderContext($context); + } + $fiber->resume(); + } + if (!$fiber->isTerminated()) { + // If we've reached this point, then the fiber has already been started + // and resumed at least once, so may be suspending repeatedly. Avoid + // a spin-lock by waiting for 0.5ms prior to continuing the while loop. + usleep(500); + } + } + $result = $fiber->getReturn(); assert($context->count() <= 1, 'Bubbling failed.'); // Restore the original render context. diff --git a/core/modules/block_content/src/Hook/BlockContentHooks.php b/core/modules/block_content/src/Hook/BlockContentHooks.php index bd1cfd608f14..4eef9bb8580c 100644 --- a/core/modules/block_content/src/Hook/BlockContentHooks.php +++ b/core/modules/block_content/src/Hook/BlockContentHooks.php @@ -29,11 +29,11 @@ class BlockContentHooks { $field_ui = \Drupal::moduleHandler()->moduleExists('field_ui') ? Url::fromRoute('help.page', ['name' => 'field_ui'])->toString() : '#'; $output = ''; $output .= '<h2>' . $this->t('About') . '</h2>'; - $output .= '<p>' . $this->t('The Block Content module allows you to create and manage custom <em>block types</em> and <em>content-containing blocks</em>. For more information, see the <a href=":online-help">online documentation for the Block Content module</a>.', [':online-help' => 'https://www.drupal.org/documentation/modules/block_content']) . '</p>'; + $output .= '<p>' . $this->t('The Block Content module manages the creation, editing, and deletion of content blocks. Content blocks are field-able content entities managed by the <a href=":field">Field module</a>. For more information, see the <a href=":block-content">online documentation for the Block Content module</a>.', [':block-content' => 'https://www.drupal.org/documentation/modules/block_content', ':field' => Url::fromRoute('help.page', ['name' => 'field'])->toString()]) . '</p>'; $output .= '<h2>' . $this->t('Uses') . '</h2>'; $output .= '<dl>'; $output .= '<dt>' . $this->t('Creating and managing block types') . '</dt>'; - $output .= '<dd>' . $this->t('Users with the <em>Administer blocks</em> permission can create and edit block types with fields and display settings, from the <a href=":types">Block types</a> page under the Structure menu. For more information about managing fields and display settings, see the <a href=":field-ui">Field UI module help</a> and <a href=":field">Field module help</a>.', [ + $output .= '<dd>' . $this->t('Users with the <em>Administer block types</em> permission can create and edit block types with fields and display settings, from the <a href=":types">Block types</a> page under the Structure menu. For more information about managing fields and display settings, see the <a href=":field-ui">Field UI module help</a> and <a href=":field">Field module help</a>.', [ ':types' => Url::fromRoute('entity.block_content_type.collection')->toString(), ':field-ui' => $field_ui, ':field' => Url::fromRoute('help.page', [ @@ -41,9 +41,9 @@ class BlockContentHooks { ])->toString(), ]) . '</dd>'; $output .= '<dt>' . $this->t('Creating content blocks') . '</dt>'; - $output .= '<dd>' . $this->t('Users with the <em>Administer blocks</em> permission can create, edit, and delete content blocks of each defined block type, from the <a href=":block-library">Content blocks page</a>. After creating a block, place it in a region from the <a href=":blocks">Block layout page</a>, just like blocks provided by other modules.', [ - ':blocks' => Url::fromRoute('block.admin_display')->toString(), - ':block-library' => Url::fromRoute('entity.block_content.collection')->toString(), + $output .= '<dd>' . $this->t('Users with the <em>Administer block content</em> or <em>Create new content block</em> permissions for an individual block type are able to add content blocks. These can be created on the <a href=":add-content-block">Add content block page</a> or on the <em>Place block</em> modal on the <a href=":block-layout">Block Layout page</a> and are reusable across the entire site. Content blocks created in Layout Builder for a content type or individual node layouts are not reusable and also called inline blocks.', [ + ':add-content-block' => Url::fromRoute('block_content.add_page')->toString(), + ':block-layout' => Url::fromRoute('block.admin_display')->toString(), ]) . '</dd>'; $output .= '</dl>'; return $output; diff --git a/core/modules/file/src/Plugin/Field/FieldType/FileItem.php b/core/modules/file/src/Plugin/Field/FieldType/FileItem.php index f09740667e32..2e9be38dacb6 100644 --- a/core/modules/file/src/Plugin/Field/FieldType/FileItem.php +++ b/core/modules/file/src/Plugin/Field/FieldType/FileItem.php @@ -360,8 +360,11 @@ class FileItem extends EntityReferenceItem { $dirname = static::doGetUploadLocation($settings); \Drupal::service('file_system')->prepareDirectory($dirname, FileSystemInterface::CREATE_DIRECTORY); + // Ensure directory ends with a slash. + $dirname .= str_ends_with($dirname, '/') ? '' : '/'; + // Generate a file entity. - $destination = $dirname . '/' . $random->name(10, TRUE) . '.txt'; + $destination = $dirname . $random->name(10) . '.txt'; $data = $random->paragraphs(3); /** @var \Drupal\file\FileRepositoryInterface $file_repository */ $file_repository = \Drupal::service('file.repository'); diff --git a/core/modules/file/tests/src/Kernel/FileItemTest.php b/core/modules/file/tests/src/Kernel/FileItemTest.php index 09a28b68f0f1..c01cf28a1151 100644 --- a/core/modules/file/tests/src/Kernel/FileItemTest.php +++ b/core/modules/file/tests/src/Kernel/FileItemTest.php @@ -6,12 +6,14 @@ namespace Drupal\Tests\file\Kernel; use Drupal\Core\Field\FieldItemInterface; use Drupal\Core\Field\FieldItemListInterface; +use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\FieldStorageDefinitionInterface; use Drupal\entity_test\Entity\EntityTest; use Drupal\field\Entity\FieldConfig; use Drupal\Tests\field\Kernel\FieldKernelTestBase; use Drupal\field\Entity\FieldStorageConfig; use Drupal\file\Entity\File; +use Drupal\file\Plugin\Field\FieldType\FileItem; use Drupal\user\Entity\Role; /** @@ -155,6 +157,48 @@ class FileItemTest extends FieldKernelTestBase { \Drupal::service('renderer')->renderRoot($output); $this->assertTrue(!empty($entity->file_test->entity)); $this->assertEquals($uri, $entity->file_test->entity->getFileUri()); + + // Test file URIs with empty and custom directories. + $this->validateFileUriForDirectory( + '', 'public://' + ); + $this->validateFileUriForDirectory( + 'custom_directory/subdir', 'public://custom_directory/subdir/' + ); + } + + /** + * Tests file URIs generated for a given file directory. + * + * @param string $file_directory + * The file directory to test (e.g., empty or 'custom_directory/subdir'). + * @param string $expected_start + * The expected starting string of the file URI (e.g., 'public://'). + */ + private function validateFileUriForDirectory(string $file_directory, string $expected_start): void { + // Mock the field definition with the specified file directory. + $definition = $this->createMock(FieldDefinitionInterface::class); + $definition->expects($this->any()) + ->method('getSettings') + ->willReturn([ + 'file_extensions' => 'txt', + 'file_directory' => $file_directory, + 'uri_scheme' => 'public', + 'display_default' => TRUE, + ]); + + // Generate a sample file value. + $value = FileItem::generateSampleValue($definition); + $this->assertNotEmpty($value); + + // Load the file entity and get its URI. + $fid = $value['target_id']; + $file = File::load($fid); + $fileUri = $file->getFileUri(); + + // Verify the file URI starts with the expected protocol and structure. + $this->assertStringStartsWith($expected_start, $fileUri); + $this->assertMatchesRegularExpression('#^' . preg_quote($expected_start, '#') . '[^/]+#', $fileUri); } } diff --git a/core/modules/image/src/Plugin/Field/FieldType/ImageItem.php b/core/modules/image/src/Plugin/Field/FieldType/ImageItem.php index 7ed6b0d3371c..72937d4e79a3 100644 --- a/core/modules/image/src/Plugin/Field/FieldType/ImageItem.php +++ b/core/modules/image/src/Plugin/Field/FieldType/ImageItem.php @@ -389,7 +389,9 @@ class ImageItem extends FileItem { $image->setFileName($file_system->basename($path)); $destination_dir = static::doGetUploadLocation($settings); $file_system->prepareDirectory($destination_dir, FileSystemInterface::CREATE_DIRECTORY); - $destination = $destination_dir . '/' . basename($path); + // Ensure directory ends with a slash. + $destination_dir .= str_ends_with($destination_dir, '/') ? '' : '/'; + $destination = $destination_dir . basename($path); $file = \Drupal::service('file.repository')->move($image, $destination); $images[$extension][$min_resolution][$max_resolution][$file->id()] = $file; } diff --git a/core/modules/image/tests/src/Kernel/ImageItemTest.php b/core/modules/image/tests/src/Kernel/ImageItemTest.php index 0598b25e9db8..55f2503686d4 100644 --- a/core/modules/image/tests/src/Kernel/ImageItemTest.php +++ b/core/modules/image/tests/src/Kernel/ImageItemTest.php @@ -9,6 +9,7 @@ use Drupal\Core\Entity\ContentEntityForm; use Drupal\Core\Entity\Entity\EntityFormDisplay; use Drupal\Core\Entity\EntityDisplayRepositoryInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\FieldItemInterface; use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\Field\FieldStorageDefinitionInterface; @@ -19,6 +20,7 @@ use Drupal\field\Entity\FieldConfig; use Drupal\field\Entity\FieldStorageConfig; use Drupal\file\Entity\File; use Drupal\Tests\field\Kernel\FieldKernelTestBase; +use Drupal\image\Plugin\Field\FieldType\ImageItem; use Drupal\user\Entity\Role; /** @@ -208,6 +210,14 @@ class ImageItemTest extends FieldKernelTestBase { } /** + * Tests image URIs for empty and custom directories. + */ + public function testImageUriDirectories(): void { + $this->validateImageUriForDirectory('', 'public://'); + $this->validateImageUriForDirectory('custom_directory/subdir', 'public://custom_directory/subdir/'); + } + + /** * Tests display_default. */ public function testDisplayDefaultValue(): void { @@ -225,4 +235,36 @@ class ImageItemTest extends FieldKernelTestBase { self::assertEquals(1, $form_state->getValue(['image_test', 0, 'display'])); } + /** + * Validates the image file URI generated for a given file directory. + * + * @param string $file_directory + * The file directory to test (e.g., empty or 'custom_directory/subdir'). + * @param string $expected_start + * The expected starting string of the file URI (e.g., 'public://'). + */ + private function validateImageUriForDirectory(string $file_directory, string $expected_start): void { + // Mock the field definition with the specified file directory. + $definition = $this->createMock(FieldDefinitionInterface::class); + $definition->expects($this->any()) + ->method('getSettings') + ->willReturn([ + 'file_extensions' => 'jpg', + 'file_directory' => $file_directory, + 'uri_scheme' => 'public', + ]); + // Generate sample value and check the URI format. + $value = ImageItem::generateSampleValue($definition); + $this->assertNotEmpty($value); + + // Load the file entity and get its URI. + $fid = $value['target_id']; + $file = File::load($fid); + $fileUri = $file->getFileUri(); + + // Verify the file URI starts with the expected protocol and structure. + $this->assertStringStartsWith($expected_start, $fileUri); + $this->assertMatchesRegularExpression('#^' . preg_quote($expected_start, '#') . '[^/]+#', $fileUri); + } + } diff --git a/core/modules/locale/locale.install b/core/modules/locale/locale.install index 457d37890fa0..d3377f3773eb 100644 --- a/core/modules/locale/locale.install +++ b/core/modules/locale/locale.install @@ -21,12 +21,6 @@ function locale_install(): void { \Drupal::configFactory()->getEditable('locale.settings')->set('translation.path', $directory)->save(); } \Drupal::service('file_system')->prepareDirectory($directory, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS); - - $t_args = [ - ':translate_status' => base_path() . 'admin/reports/translations/check?destination=' . urlencode(base_path() . 'admin/reports/translations'), - ]; - $message = t('Check <a href=":translate_status">available translations</a> for your language(s).', $t_args); - \Drupal::messenger()->addStatus($message); } /** diff --git a/core/modules/locale/tests/src/Functional/LocaleInstallTest.php b/core/modules/locale/tests/src/Functional/LocaleInstallTest.php deleted file mode 100644 index fc81fdb19b52..000000000000 --- a/core/modules/locale/tests/src/Functional/LocaleInstallTest.php +++ /dev/null @@ -1,48 +0,0 @@ -<?php - -declare(strict_types=1); - -namespace Drupal\Tests\locale\Functional; - -use Drupal\Tests\BrowserTestBase; - -/** - * Test installation of Locale module. - * - * @group locale - */ -class LocaleInstallTest extends BrowserTestBase { - - /** - * {@inheritdoc} - */ - protected static $modules = [ - 'system', - 'file', - 'language', - ]; - - /** - * {@inheritdoc} - */ - protected $defaultTheme = 'stark'; - - /** - * Tests Locale install message. - */ - public function testLocaleInstallMessage(): void { - $admin_user = $this->drupalCreateUser([ - 'access administration pages', - 'administer modules', - ]); - $this->drupalLogin($admin_user); - - $edit = []; - $edit['modules[locale][enable]'] = 'locale'; - $this->drupalGet('admin/modules'); - $this->submitForm($edit, 'Install'); - - $this->assertSession()->statusMessageContains('available translations', 'status'); - } - -} diff --git a/core/modules/system/tests/src/Functional/FileTransfer/FileTransferTest.php b/core/modules/system/tests/src/Functional/FileTransfer/FileTransferTest.php index ae157ab56624..fa6bda652deb 100644 --- a/core/modules/system/tests/src/Functional/FileTransfer/FileTransferTest.php +++ b/core/modules/system/tests/src/Functional/FileTransfer/FileTransferTest.php @@ -88,26 +88,10 @@ class FileTransferTest extends BrowserTestBase { */ public function testJail(): void { $source = $this->_buildFakeModule(); - - // This convoluted piece of code is here because our testing framework does - // not support expecting exceptions. - $got_it = FALSE; - try { - $this->testConnection->copyDirectory($source, sys_get_temp_dir()); - } - catch (FileTransferException) { - $got_it = TRUE; - } - $this->assertTrue($got_it, 'Was not able to copy a directory outside of the jailed area.'); - - $got_it = TRUE; - try { - $this->testConnection->copyDirectory($source, $this->root . '/' . PublicStream::basePath()); - } - catch (FileTransferException) { - $got_it = FALSE; - } - $this->assertTrue($got_it, 'Was able to copy a directory inside of the jailed area'); + $this->testConnection->copyDirectory($source, $this->root . '/' . PublicStream::basePath()); + $this->expectException(FileTransferException::class); + $this->expectExceptionMessage('@directory is outside of the @jail'); + $this->testConnection->copyDirectory($source, sys_get_temp_dir()); } } diff --git a/core/profiles/demo_umami/tests/src/FunctionalJavascript/OpenTelemetryFrontPagePerformanceTest.php b/core/profiles/demo_umami/tests/src/FunctionalJavascript/OpenTelemetryFrontPagePerformanceTest.php index 071202256fbc..027d2933a995 100644 --- a/core/profiles/demo_umami/tests/src/FunctionalJavascript/OpenTelemetryFrontPagePerformanceTest.php +++ b/core/profiles/demo_umami/tests/src/FunctionalJavascript/OpenTelemetryFrontPagePerformanceTest.php @@ -50,7 +50,7 @@ class OpenTelemetryFrontPagePerformanceTest extends PerformanceTestBase { $expected = [ 'QueryCount' => 381, - 'CacheGetCount' => 471, + 'CacheGetCount' => 472, 'CacheSetCount' => 467, 'CacheDeleteCount' => 0, 'CacheTagLookupQueryCount' => 49, diff --git a/core/profiles/demo_umami/tests/src/FunctionalJavascript/OpenTelemetryNodePagePerformanceTest.php b/core/profiles/demo_umami/tests/src/FunctionalJavascript/OpenTelemetryNodePagePerformanceTest.php index dd26c2f12638..3045b218bc41 100644 --- a/core/profiles/demo_umami/tests/src/FunctionalJavascript/OpenTelemetryNodePagePerformanceTest.php +++ b/core/profiles/demo_umami/tests/src/FunctionalJavascript/OpenTelemetryNodePagePerformanceTest.php @@ -136,9 +136,12 @@ class OpenTelemetryNodePagePerformanceTest extends PerformanceTestBase { protected function testNodePageWarmCache(): void { // First of all visit the node page to ensure the image style exists. $this->drupalGet('node/1'); + // Allow time for the image style and asset aggregate requests to finish. + sleep(1); $this->clearCaches(); // Now visit a different node page to warm non-path-specific caches. $this->drupalGet('node/2'); + sleep(1); $performance_data = $this->collectPerformanceData(function () { $this->drupalGet('node/1'); }, 'umamiNodePageWarmCache'); diff --git a/core/tests/Drupal/Tests/Component/Gettext/PoItemTest.php b/core/tests/Drupal/Tests/Component/Gettext/PoItemTest.php new file mode 100644 index 000000000000..7026a2ceac5a --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Gettext/PoItemTest.php @@ -0,0 +1,85 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Tests\Component\Gettext; + +use Drupal\Component\Gettext\PoItem; +use PHPUnit\Framework\TestCase; + +/** + * @coversDefaultClass \Drupal\Component\Gettext\PoItem + * @group Gettext + */ +class PoItemTest extends TestCase { + + /** + * @return array + * - Source string + * - Context (optional) + * - Translated string (optional) + * - Expected value + */ + public static function providerStrings(): array { + // cSpell:disable + return [ + [ + '', + NULL, + NULL, + 'msgid ""' . "\n" . 'msgstr ""' . "\n\n", + ], + // Translated String without contesxt. + [ + 'Next', + NULL, + 'Suivant', + 'msgid "Next"' . "\n" . 'msgstr "Suivant"' . "\n\n", + ], + // Translated string with context. + [ + 'Apr', + 'Abbreviated month name', + 'Avr', + 'msgctxt "Abbreviated month name"' . "\n" . 'msgid "Apr"' . "\n" . 'msgstr "Avr"' . "\n\n", + ], + // Translated string with placeholder. + [ + '%email is not a valid email address.', + NULL, + '%email n\'est pas une adresse de courriel valide.', + 'msgid "%email is not a valid email address."' . "\n" . 'msgstr "%email n\'est pas une adresse de courriel valide."' . "\n\n", + ], + // Translated Plural String without context. + [ + ['Installed theme', 'Installed themes'], + NULL, + ['Thème installé', 'Thèmes installés'], + 'msgid "Installed theme"' . "\n" . 'msgid_plural "Installed themes"' . "\n" . 'msgstr[0] "Thème installé"' . "\n" . 'msgstr[1] "Thèmes installés"' . "\n\n", + ], + ]; + // cSpell:enable + } + + /** + * @dataProvider providerStrings + */ + public function testFormat($source, $context, $translation, $expected): void { + $item = new PoItem(); + + $item->setSource($source); + + if (is_array($source)) { + $item->setPlural(TRUE); + } + if (!empty($context)) { + $item->setContext($context); + } + if (!empty($translation)) { + $item->setTranslation($translation); + } + + $this->assertEquals($expected, (string) $item); + } + +} diff --git a/core/tests/Drupal/Tests/Core/Render/RendererTest.php b/core/tests/Drupal/Tests/Core/Render/RendererTest.php index 9c68273365b3..e4fbfa60caf7 100644 --- a/core/tests/Drupal/Tests/Core/Render/RendererTest.php +++ b/core/tests/Drupal/Tests/Core/Render/RendererTest.php @@ -1126,6 +1126,68 @@ class RendererTest extends RendererTestBase { $this->assertFalse($this->renderer->hasRenderContext()); } + /** + * @covers ::executeInRenderContext + */ + public function testExecuteInRenderContext(): void { + $return = $this->renderer->executeInRenderContext(new RenderContext(), function () { + $fiber_callback = function () { + + // Create a #pre_render callback that renders a render array in + // isolation. This has its own #pre_render callback that calls + // Fiber::suspend(). This ensures that suspending a Fiber within + // multiple nested calls to ::executeInRenderContext() doesn't + // allow render context to get out of sync. This simulates similar + // conditions to BigPipe placeholder rendering. + $fiber_suspend_pre_render = function ($elements) { + $fiber_suspend = function ($elements) { + \Fiber::suspend(); + return $elements; + }; + $build = [ + 'foo' => [ + '#markup' => 'foo', + '#pre_render' => [$fiber_suspend], + ], + ]; + $markup = $this->renderer->renderInIsolation($build); + $elements['#markup'] = $markup; + return $elements; + }; + $build = [ + 'foo' => [ + '#pre_render' => [$fiber_suspend_pre_render], + ], + ]; + return $this->renderer->render($build); + }; + + // Build an array of two fibers that executes the code defined above. This + // ensures that Fiber::suspend() is called from within two + // ::renderInIsolation() calls without either having been completed. + $fibers = []; + foreach ([0, 1] as $key) { + $fibers[] = new \Fiber(static fn () => $fiber_callback()); + } + while ($fibers) { + foreach ($fibers as $key => $fiber) { + if ($fiber->isTerminated()) { + unset($fibers[$key]); + continue; + } + if ($fiber->isSuspended()) { + $fiber->resume(); + } + else { + $fiber->start(); + } + } + } + return $fiber->getReturn(); + }); + $this->assertEquals(Markup::create('foo'), $return); + } + } /** |