diff --git a/core/lib/Drupal/Core/Theme/Icon/IconFinder.php b/core/lib/Drupal/Core/Theme/Icon/IconFinder.php index 045e31a67cb2567884ccada60b4bcee7b195da4b..28a2d6984378c24d7a04fc79b532cd6a3b256061 100644 --- a/core/lib/Drupal/Core/Theme/Icon/IconFinder.php +++ b/core/lib/Drupal/Core/Theme/Icon/IconFinder.php @@ -297,7 +297,9 @@ private function processFoundFiles(Finder $finder, string $source, string $path_ $has_icon_pattern = \str_contains($path_info_filename, self::ICON_ID_PATTERN); foreach ($finder as $file) { + /** @var SplFileInfo $file */ $file_absolute_path = $file->getPathName(); + /** @var \Symfony\Component\Finder\SplFileInfo $file */ $icon_id = $file->getFilenameWithoutExtension(); // If an {icon_id} pattern is used, extract it to be used. @@ -305,10 +307,19 @@ private function processFoundFiles(Finder $finder, string $source, string $path_ $icon_id = self::extractIconIdFromFilename($icon_id, $path_info_filename); } + // Source is the url to access the image, based on the absolute path to + // handle icons relative to definition or Drupal root. + $source = str_replace($this->appRoot, '', $file_absolute_path); + // Url generation with `generateString` method rely on `base_path()` that + // will add a prefix based on $GLOBALS['base_path'], default `/`. + // Remove any left slash to allow to url generation with a custom + // base_path. + $source = $this->fileUrlGenerator->generateString(ltrim($source, '/')); + // Icon ID is used as index to avoid duplicates. $result[$icon_id] = [ 'icon_id' => $icon_id, - 'source' => $this->fileUrlGenerator->generateString(str_replace($this->appRoot, '', $file_absolute_path)), + 'source' => $source, 'absolute_path' => $file_absolute_path, 'group' => self::extractGroupFromPath($file->getPath(), $group_position), ]; diff --git a/core/modules/system/tests/modules/icon_test/icon_test.icons.yml b/core/modules/system/tests/modules/icon_test/icon_test.icons.yml index 8896eb500e3942dc813ba3a7c8398d6ff9be0a78..0e82b63b9927dac9e9699c63937d12a1442f3289 100644 --- a/core/modules/system/tests/modules/icon_test/icon_test.icons.yml +++ b/core/modules/system/tests/modules/icon_test/icon_test.icons.yml @@ -48,6 +48,14 @@ test_path: title="{{ title }}" > +test_path_relative_root: + extractor: path + config: + sources: + - /core/misc/druplicon.png + template: >- + {{ icon_id }}: <img src="{{ source }}" width="32" height="32"> + test_svg: enabled: true label: Test svg diff --git a/core/tests/Drupal/KernelTests/Core/Theme/Icon/IconFinderKernelTest.php b/core/tests/Drupal/KernelTests/Core/Theme/Icon/IconFinderKernelTest.php new file mode 100644 index 0000000000000000000000000000000000000000..a9bfb80dfd3a13309485d98e36b48111ec09aec2 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Theme/Icon/IconFinderKernelTest.php @@ -0,0 +1,144 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\KernelTests\Core\Theme\Icon; + +use Drupal\Core\Theme\Icon\IconFinder; +use Drupal\Core\Theme\Icon\IconFinderInterface; +use Drupal\KernelTests\KernelTestBase; +use Psr\Log\LoggerInterface; + +/** + * Test icon sources path generated urls. + * + * Test using the service fileUrlGenerator for a real generateString(). + * It rely on base_path() that we override here to allow tests with custom + * values. + * + * @group icon + * + * @coversDefaultClass \Drupal\Core\Theme\Icon\IconFinder + */ +class IconFinderKernelTest extends KernelTestBase { + + /** + * {@inheritdoc} + */ + protected static $modules = [ + 'system', + ]; + + /** + * The IconFinder instance. + * + * @var \Drupal\Core\Theme\Icon\IconFinderInterface + */ + private IconFinderInterface $iconFinder; + + /** + * The App root instance. + * + * @var string + */ + private string $appRoot; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + + /** @var \Drupal\Core\File\FileUrlGeneratorInterface $fileUrlGenerator */ + $fileUrlGenerator = $this->container->get('file_url_generator'); + $this->appRoot = $this->container->getParameter('app.root'); + + $this->iconFinder = new IconFinder( + $fileUrlGenerator, + $this->createMock(LoggerInterface::class), + $this->appRoot, + ); + } + + /** + * {@inheritdoc} + */ + public function tearDown(): void { + $GLOBALS['base_path'] = '/'; + parent::tearDown(); + } + + /** + * Test the IconFinder::_construct method. + */ + public function testConstructor(): void { + $this->assertInstanceOf(IconFinder::class, $this->iconFinder); + } + + /** + * Data provider for ::testGetFilesFromSourcesPath(). + * + * @return array + * The test cases, to minimize test data, result expected is an array with: + * - icon_id: the expected id + * - source: the expected source value, without left base path, + * processed through generateString(). + */ + public static function providerGetFilesFromSourcesPath(): array { + return [ + [ + [ + // Use real icons for better test, but `/core` are not managed by + // icon_test so they could change and make this test fail. + '/core/misc/druplicon.png', + 'icons/flat/foo.png', + ], + [ + 'druplicon' => 'core/misc/druplicon.png', + 'foo' => 'core/modules/system/tests/modules/icon_test/icons/flat/foo.png', + ], + 'core/modules/system/tests/modules/icon_test', + ], + ]; + } + + /** + * Test the IconFinder::getFilesFromSources method with paths. + * + * @param array<string> $sources + * The list of remote. + * @param array<string, string> $expected + * The expected result as icon_id => source. + * @param string $relativePath + * The relative path to simulate an icon in the module/theme definition. + * + * @dataProvider providerGetFilesFromSourcesPath + */ + public function testGetFilesFromSourcesPath(array $sources, array $expected, string $relativePath): void { + $base_path_test = ['/', '/foo/', '/foo/bar/']; + + foreach ($base_path_test as $base_path) { + // @todo Remove or adapt as part of https://www.drupal.org/node/2529170. + $GLOBALS['base_path'] = $base_path; + + $result = $this->iconFinder->getFilesFromSources( + $sources, + $relativePath, + ); + + // Prepare result array matching processFoundFiles() to minimize test data. + $expected_result = []; + foreach ($expected as $key => $expected_value) { + $expected_result[$key] = [ + 'icon_id' => $key, + 'source' => $base_path . $expected_value, + 'absolute_path' => DRUPAL_ROOT . '/' . $expected_value, + 'group' => NULL, + ]; + } + + $this->assertEquals($result, $expected_result); + } + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Theme/Icon/IconPackManagerKernelTest.php b/core/tests/Drupal/KernelTests/Core/Theme/Icon/IconPackManagerKernelTest.php index 07c6508cb9088466138047039d609bfcc8ab0e49..82073b59411b39830f79e8368dabae8fda7eb5a5 100644 --- a/core/tests/Drupal/KernelTests/Core/Theme/Icon/IconPackManagerKernelTest.php +++ b/core/tests/Drupal/KernelTests/Core/Theme/Icon/IconPackManagerKernelTest.php @@ -28,7 +28,7 @@ class IconPackManagerKernelTest extends KernelTestBase { */ private const TEST_ICON_FULL_ID = 'test_minimal:foo'; - private const EXPECTED_TOTAL_TEST_ICONS = 30; + private const EXPECTED_TOTAL_TEST_ICONS = 31; /** * {@inheritdoc} @@ -128,6 +128,7 @@ public function testListIconPackOptions(): void { 'test_no_settings' => 'test_no_settings (1)', 'test_settings' => 'Test settings (1)', 'test_url_path' => 'Test url path (2)', + 'test_path_relative_root' => 'test_path_relative_root (1)', ]; $this->assertEquals($expected, $actual); @@ -140,6 +141,7 @@ public function testListIconPackOptions(): void { 'test_no_settings' => 'test_no_settings (1)', 'test_settings' => 'Test settings (1)', 'test_url_path' => 'Test url path (2)', + 'test_path_relative_root' => 'test_path_relative_root (1)', ]; $this->assertEquals($expected, $actual); } diff --git a/core/tests/Drupal/Tests/Core/Theme/Icon/IconFinderTest.php b/core/tests/Drupal/Tests/Core/Theme/Icon/IconFinderTest.php index 02139701a8128d2b7d5ccc3bdb1ba8b2e589f685..ccdecd31ae1d5a9ab41e2222c6a196d4484d8d21 100644 --- a/core/tests/Drupal/Tests/Core/Theme/Icon/IconFinderTest.php +++ b/core/tests/Drupal/Tests/Core/Theme/Icon/IconFinderTest.php @@ -18,7 +18,6 @@ class IconFinderTest extends UnitTestCase { private const TEST_ICONS_PATH = 'core/modules/system/tests/modules/icon_test'; - private const TEST_RELATIVE_URL = 'foo/bar'; /** * The file url generator instance. @@ -614,7 +613,7 @@ public function testGetFilesFromSourcesPath(array $sources, array $expected = [] ->expects($this->any()) ->method('generateString') ->willReturnCallback(function ($uri) { - return self::TEST_RELATIVE_URL . $uri; + return base_path() . $uri; }); $result = $this->iconFinder->getFilesFromSources( @@ -630,7 +629,7 @@ public function testGetFilesFromSourcesPath(array $sources, array $expected = [] $group = $expected[$key][2] ?? NULL; $expected_result[$icon_id] = [ 'icon_id' => $icon_id, - 'source' => self::TEST_RELATIVE_URL . '/' . self::TEST_ICONS_PATH . '/' . $filename, + 'source' => base_path() . self::TEST_ICONS_PATH . '/' . $filename, 'absolute_path' => DRUPAL_ROOT . '/' . self::TEST_ICONS_PATH . '/' . $filename, 'group' => $group, ]; @@ -852,3 +851,24 @@ public function testGetFileContents(string $uri, bool $expected): void { } } + +// @todo Remove as part of https://www.drupal.org/node/2529170. +namespace Drupal\Core\Theme\Icon; + +if (!function_exists('base_path')) { + + function base_path(): string { + return '/'; + } + +} + +namespace Drupal\Tests\Core\Theme\Icon; + +if (!function_exists('base_path')) { + + function base_path(): string { + return '/'; + } + +}