From c1882639577f4513e1c9f3c00491061be99e9c32 Mon Sep 17 00:00:00 2001 From: Stuart Clark Date: Thu, 18 Jun 2026 05:16:14 +0000 Subject: [PATCH 1/6] fix(#3121826): add on-demand image style delivery for temporary:// staged files --- .cspell.json | 1 + filefield_paths.routing.yml | 12 ++ filefield_paths.services.yml | 9 ++ src/Access/ImageStyleTemporaryAccessCheck.php | 56 +++++++++ src/Hook/FileUrlHooks.php | 30 +++++ .../FileFieldPathsImageStyleTemporaryTest.php | 114 ++++++++++++++++++ 6 files changed, 222 insertions(+) create mode 100644 src/Access/ImageStyleTemporaryAccessCheck.php create mode 100644 src/Hook/FileUrlHooks.php create mode 100644 tests/src/Functional/FileFieldPathsImageStyleTemporaryTest.php diff --git a/.cspell.json b/.cspell.json index 5244804..3e94a6f 100644 --- a/.cspell.json +++ b/.cspell.json @@ -42,6 +42,7 @@ "gnomovision", "hookspec", "icanon", + "itok", "initialisation", "initialised", "jangregor", diff --git a/filefield_paths.routing.yml b/filefield_paths.routing.yml index 7eaa9de..1778de3 100644 --- a/filefield_paths.routing.yml +++ b/filefield_paths.routing.yml @@ -5,3 +5,15 @@ filefield_paths.admin_settings: _title: 'File (Field) Paths settings' requirements: _permission: 'administer site configuration' + +filefield_paths.image_style_temporary: + path: '/system/files/styles/{image_style}/temporary' + defaults: + _controller: '\Drupal\image\Controller\ImageStyleDownloadController::deliver' + scheme: 'temporary' + required_derivative_scheme: 'temporary' + requirements: + _access: 'TRUE' + _ffp_temp_image_style: 'TRUE' + options: + no_cache: TRUE diff --git a/filefield_paths.services.yml b/filefield_paths.services.yml index fcac198..0a3ed62 100644 --- a/filefield_paths.services.yml +++ b/filefield_paths.services.yml @@ -20,6 +20,12 @@ services: class: Drupal\filefield_paths\PathProcessor autowire: true + filefield_paths.access_checker.image_style_temporary: + class: Drupal\filefield_paths\Access\ImageStyleTemporaryAccessCheck + arguments: ['@config.factory'] + tags: + - { name: access_check, applies_to: _ffp_temp_image_style } + # Legacy hook support Drupal\filefield_paths\Hook\EntityWithFileField: class: Drupal\filefield_paths\Hook\EntityWithFileField @@ -45,3 +51,6 @@ services: Drupal\filefield_paths\Hook\Tokens: class: Drupal\filefield_paths\Hook\Tokens autowire: true + Drupal\filefield_paths\Hook\FileUrlHooks: + class: Drupal\filefield_paths\Hook\FileUrlHooks + autowire: true diff --git a/src/Access/ImageStyleTemporaryAccessCheck.php b/src/Access/ImageStyleTemporaryAccessCheck.php new file mode 100644 index 0000000..3b3f235 --- /dev/null +++ b/src/Access/ImageStyleTemporaryAccessCheck.php @@ -0,0 +1,56 @@ +query->get('file') ?? ''); + if ($file === '') { + return AccessResult::forbidden(); + } + + $temp_location = $this->configFactory + ->get('filefield_paths.settings') + ->get('temp_location') ?? ''; + + $scheme = StreamWrapperManager::getScheme($temp_location); + $subdir = StreamWrapperManager::getTarget($temp_location); + + // Only enforce the prefix restriction when the configured temp location + // uses temporary://. Other schemes (private://, public://) are handled by + // their own access mechanisms and should not be blocked here. + if ($scheme !== 'temporary' || $subdir === '' || $subdir === FALSE) { + return AccessResult::neutral(); + } + + $allowed = str_starts_with($file, $subdir . '/'); + + return ($allowed ? AccessResult::allowed() : AccessResult::forbidden()) + ->addCacheContexts(['url.query_args:file']) + ->addCacheTags(['config:filefield_paths.settings']); + } + +} diff --git a/src/Hook/FileUrlHooks.php b/src/Hook/FileUrlHooks.php new file mode 100644 index 0000000..be0d0af --- /dev/null +++ b/src/Hook/FileUrlHooks.php @@ -0,0 +1,30 @@ + $m[1]], + ['query' => ['file' => $m[2]], 'absolute' => TRUE], + )->toString(); + } + } + +} diff --git a/tests/src/Functional/FileFieldPathsImageStyleTemporaryTest.php b/tests/src/Functional/FileFieldPathsImageStyleTemporaryTest.php new file mode 100644 index 0000000..a849104 --- /dev/null +++ b/tests/src/Functional/FileFieldPathsImageStyleTemporaryTest.php @@ -0,0 +1,114 @@ +style = ImageStyle::create(['name' => 'ffp_test', 'label' => 'FFP test']); + $this->style->save(); + + // Copy a core test image into the FFP temp location. + $temp_location = 'temporary://filefield_paths'; + \Drupal::service('file_system')->prepareDirectory( + $temp_location, + FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS, + ); + $source = \Drupal::root() . '/core/tests/fixtures/files/image-1.png'; + $this->imageUri = \Drupal::service('file_system') + ->copy($source, $temp_location . '/image-1.png', FileExists::Replace); + } + + /** + * Tests that image style derivatives are served for temporary:// files. + */ + public function testDerivativeIsServed(): void { + $url = $this->style->buildUrl($this->imageUri); + + $this->drupalGet($url); + $this->assertSession()->statusCodeEquals(200); + $this->assertSession()->responseHeaderContains('Content-Type', 'image/'); + } + + /** + * Tests that a missing or invalid itok returns 404. + */ + public function testMissingTokenReturns404(): void { + $url = $this->style->buildUrl($this->imageUri); + + // Strip the itok parameter. + $url_no_token = preg_replace('/[?&]itok=[^&]+/', '', $url); + + $this->drupalGet($url_no_token); + $this->assertSession()->statusCodeEquals(404); + } + + /** + * Tests that ?file= outside the FFP subdirectory returns 403. + */ + public function testFileOutsideSubdirReturns403(): void { + $url = $this->style->buildUrl($this->imageUri); + + // Replace the FFP subdirectory prefix with a different path. + $url_outside = preg_replace('#(\?|&)file=filefield_paths/#', '$1file=other_module/', $url); + + $this->drupalGet($url_outside); + $this->assertSession()->statusCodeEquals(403); + } + + /** + * Tests private:// temp location leaves private delivery intact. + * + * The alter hook only matches temporary:// derivative URIs, so switching + * the temp location to private:// must not trigger the FFP temporary route. + */ + public function testPrivateTempLocationUnaffected(): void { + \Drupal::configFactory() + ->getEditable('filefield_paths.settings') + ->set('temp_location', 'private://filefield_paths') + ->save(); + + $private_uri = 'private://filefield_paths/image-1.png'; + $derivative_uri = $this->style->buildUri($private_uri); + $this->assertStringStartsWith('private://', $derivative_uri); + } + +} From 33d0c5d6aedbcc99453f18bbad672b4b9949aa75 Mon Sep 17 00:00:00 2001 From: Stuart Clark Date: Thu, 18 Jun 2026 21:39:26 +0000 Subject: [PATCH 2/6] fix(#3121826): harden temporary image style delivery route --- filefield_paths.routing.yml | 3 +- src/Access/ImageStyleTemporaryAccessCheck.php | 9 ++--- src/Hook/FileUrlHooks.php | 20 ++++++++++- .../FileFieldPathsImageStyleTemporaryTest.php | 35 +++++++++++++++---- 4 files changed, 51 insertions(+), 16 deletions(-) diff --git a/filefield_paths.routing.yml b/filefield_paths.routing.yml index 1778de3..d05c81e 100644 --- a/filefield_paths.routing.yml +++ b/filefield_paths.routing.yml @@ -7,13 +7,12 @@ filefield_paths.admin_settings: _permission: 'administer site configuration' filefield_paths.image_style_temporary: - path: '/system/files/styles/{image_style}/temporary' + path: '/filefield_paths/image-style/{image_style}/temporary' defaults: _controller: '\Drupal\image\Controller\ImageStyleDownloadController::deliver' scheme: 'temporary' required_derivative_scheme: 'temporary' requirements: - _access: 'TRUE' _ffp_temp_image_style: 'TRUE' options: no_cache: TRUE diff --git a/src/Access/ImageStyleTemporaryAccessCheck.php b/src/Access/ImageStyleTemporaryAccessCheck.php index 3b3f235..a35d9af 100644 --- a/src/Access/ImageStyleTemporaryAccessCheck.php +++ b/src/Access/ImageStyleTemporaryAccessCheck.php @@ -36,14 +36,9 @@ public function access(Request $request): AccessResultInterface { ->get('filefield_paths.settings') ->get('temp_location') ?? ''; - $scheme = StreamWrapperManager::getScheme($temp_location); $subdir = StreamWrapperManager::getTarget($temp_location); - - // Only enforce the prefix restriction when the configured temp location - // uses temporary://. Other schemes (private://, public://) are handled by - // their own access mechanisms and should not be blocked here. - if ($scheme !== 'temporary' || $subdir === '' || $subdir === FALSE) { - return AccessResult::neutral(); + if (!is_string($subdir) || $subdir === '') { + return AccessResult::forbidden(); } $allowed = str_starts_with($file, $subdir . '/'); diff --git a/src/Hook/FileUrlHooks.php b/src/Hook/FileUrlHooks.php index be0d0af..7a5f70a 100644 --- a/src/Hook/FileUrlHooks.php +++ b/src/Hook/FileUrlHooks.php @@ -4,20 +4,38 @@ namespace Drupal\filefield_paths\Hook; +use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Hook\Attribute\Hook; +use Drupal\Core\StreamWrapper\StreamWrapperManager; use Drupal\Core\Url; /** * File URL hook implementations. */ -final class FileUrlHooks { +final readonly class FileUrlHooks { + + public function __construct( + private ConfigFactoryInterface $configFactory, + ) {} /** * Implements hook_file_url_alter(). + * + * Rewrites image style derivative URLs for files staged in temporary:// so + * that they route through a dedicated delivery controller instead of the + * core temporary stream wrapper (which cannot serve image derivatives). */ // @phpstan-ignore-next-line #[Hook('file_url_alter')] public function fileUrlAlter(string &$uri): void {// phpcs:ignore Squiz.WhiteSpace.FunctionSpacing.Before + $temp_location = $this->configFactory + ->get('filefield_paths.settings') + ->get('temp_location') ?? ''; + + if (StreamWrapperManager::getScheme($temp_location) !== 'temporary') { + return; + } + if (preg_match('#^temporary://styles/([^/]+)/temporary/(.+)$#', $uri, $m)) { $uri = Url::fromRoute( 'filefield_paths.image_style_temporary', diff --git a/tests/src/Functional/FileFieldPathsImageStyleTemporaryTest.php b/tests/src/Functional/FileFieldPathsImageStyleTemporaryTest.php index a849104..b33e3be 100644 --- a/tests/src/Functional/FileFieldPathsImageStyleTemporaryTest.php +++ b/tests/src/Functional/FileFieldPathsImageStyleTemporaryTest.php @@ -42,6 +42,12 @@ class FileFieldPathsImageStyleTemporaryTest extends BrowserTestBase { protected function setUp(): void { parent::setUp(); + // Use temporary:// as the FFP temp location so the URL alter hook fires. + \Drupal::configFactory() + ->getEditable('filefield_paths.settings') + ->set('temp_location', 'temporary://filefield_paths') + ->save(); + // Create a simple image style. $this->style = ImageStyle::create(['name' => 'ffp_test', 'label' => 'FFP test']); $this->style->save(); @@ -57,6 +63,16 @@ protected function setUp(): void { ->copy($source, $temp_location . '/image-1.png', FileExists::Replace); } + /** + * Tests that derivative URLs are rewritten to the FFP route. + */ + public function testUrlIsRewritten(): void { + $url = $this->style->buildUrl($this->imageUri); + $this->assertStringContainsString('/filefield_paths/image-style/ffp_test/temporary', $url); + $this->assertStringContainsString('file=filefield_paths/', $url); + $this->assertStringContainsString('itok=', $url); + } + /** * Tests that image style derivatives are served for temporary:// files. */ @@ -95,10 +111,18 @@ public function testFileOutsideSubdirReturns403(): void { } /** - * Tests private:// temp location leaves private delivery intact. + * Tests that accessing the route without a file parameter returns 403. + */ + public function testEmptyFileParamReturns403(): void { + $this->drupalGet('/filefield_paths/image-style/ffp_test/temporary'); + $this->assertSession()->statusCodeEquals(403); + } + + /** + * Tests that non-temporary temp_location does not trigger URL rewriting. * - * The alter hook only matches temporary:// derivative URIs, so switching - * the temp location to private:// must not trigger the FFP temporary route. + * When temp_location uses private://, the alter hook must not intercept + * derivative URLs, leaving them on the standard private delivery route. */ public function testPrivateTempLocationUnaffected(): void { \Drupal::configFactory() @@ -106,9 +130,8 @@ public function testPrivateTempLocationUnaffected(): void { ->set('temp_location', 'private://filefield_paths') ->save(); - $private_uri = 'private://filefield_paths/image-1.png'; - $derivative_uri = $this->style->buildUri($private_uri); - $this->assertStringStartsWith('private://', $derivative_uri); + $url = $this->style->buildUrl($this->imageUri); + $this->assertStringNotContainsString('/filefield_paths/image-style/', $url); } } From 48cf08d00114dcc2e79f5c61c39cadb42a51d17c Mon Sep 17 00:00:00 2001 From: Stuart Clark Date: Thu, 18 Jun 2026 21:59:17 +0000 Subject: [PATCH 3/6] fix(#3121826): harden access checker cache metadata and path traversal --- src/Access/ImageStyleTemporaryAccessCheck.php | 23 ++++++++++++++----- .../FileFieldPathsImageStyleTemporaryTest.php | 10 ++++++++ 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/src/Access/ImageStyleTemporaryAccessCheck.php b/src/Access/ImageStyleTemporaryAccessCheck.php index a35d9af..1ae57fd 100644 --- a/src/Access/ImageStyleTemporaryAccessCheck.php +++ b/src/Access/ImageStyleTemporaryAccessCheck.php @@ -6,6 +6,7 @@ use Drupal\Core\Access\AccessResult; use Drupal\Core\Access\AccessResultInterface; +use Drupal\Core\Cache\CacheableMetadata; use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Routing\Access\AccessInterface; use Drupal\Core\StreamWrapper\StreamWrapperManager; @@ -27,25 +28,35 @@ public function __construct( * Checks access for the temporary image style delivery route. */ public function access(Request $request): AccessResultInterface { + $cacheability = (new CacheableMetadata()) + ->setCacheContexts(['url.query_args:file']) + ->setCacheTags(['config:filefield_paths.settings']); + $file = (string) ($request->query->get('file') ?? ''); if ($file === '') { - return AccessResult::forbidden(); + return AccessResult::forbidden()->addCacheableDependency($cacheability); } $temp_location = $this->configFactory ->get('filefield_paths.settings') ->get('temp_location') ?? ''; + if (StreamWrapperManager::getScheme($temp_location) !== 'temporary') { + return AccessResult::forbidden()->addCacheableDependency($cacheability); + } + $subdir = StreamWrapperManager::getTarget($temp_location); if (!is_string($subdir) || $subdir === '') { - return AccessResult::forbidden(); + return AccessResult::forbidden()->addCacheableDependency($cacheability); } - $allowed = str_starts_with($file, $subdir . '/'); + $normalized_file = ltrim(str_replace('\\', '/', $file), '/'); + if (preg_match('~(^|/)\.\.(/|$)~', $normalized_file)) { + return AccessResult::forbidden()->addCacheableDependency($cacheability); + } - return ($allowed ? AccessResult::allowed() : AccessResult::forbidden()) - ->addCacheContexts(['url.query_args:file']) - ->addCacheTags(['config:filefield_paths.settings']); + $allowed = str_starts_with($normalized_file, $subdir . '/'); + return AccessResult::allowedIf($allowed)->addCacheableDependency($cacheability); } } diff --git a/tests/src/Functional/FileFieldPathsImageStyleTemporaryTest.php b/tests/src/Functional/FileFieldPathsImageStyleTemporaryTest.php index b33e3be..aabf23a 100644 --- a/tests/src/Functional/FileFieldPathsImageStyleTemporaryTest.php +++ b/tests/src/Functional/FileFieldPathsImageStyleTemporaryTest.php @@ -118,6 +118,16 @@ public function testEmptyFileParamReturns403(): void { $this->assertSession()->statusCodeEquals(403); } + /** + * Tests that a path traversal attempt in ?file= returns 403. + */ + public function testPathTraversalReturns403(): void { + $url = $this->style->buildUrl($this->imageUri); + $url_traversal = preg_replace('#(\?|&)file=filefield_paths/#', '$1file=filefield_paths/../', $url); + $this->drupalGet($url_traversal); + $this->assertSession()->statusCodeEquals(403); + } + /** * Tests that non-temporary temp_location does not trigger URL rewriting. * From 29273da4c1d31046b326195f834f48e10afd8339 Mon Sep 17 00:00:00 2001 From: Stuart Clark Date: Fri, 19 Jun 2026 00:03:50 +0000 Subject: [PATCH 4/6] fix(#3121826): add LegacyHook wrapper for file_url_alter on Drupal 10 --- filefield_paths.module | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/filefield_paths.module b/filefield_paths.module index 79d23c0..aeec238 100644 --- a/filefield_paths.module +++ b/filefield_paths.module @@ -26,6 +26,7 @@ use Drupal\filefield_paths\Hook\FieldWidgetSingleElementForm; use Drupal\filefield_paths\Hook\File; use Drupal\filefield_paths\Hook\FileFieldPathsFieldSettingsLegacy; use Drupal\filefield_paths\Hook\FileFieldPathsProcessFileLegacy; +use Drupal\filefield_paths\Hook\FileUrlHooks; use Drupal\filefield_paths\Hook\LocalTaskAlter; use Drupal\filefield_paths\Hook\Tokens; use Drupal\filefield_paths\MoveFileProcessorInterface; @@ -212,6 +213,12 @@ function filefield_paths_file_presave(FileInterface $file): void {// phpcs:ignor \Drupal::service(File::class)->filePresave($file); } +// @phpstan-ignore-next-line +#[LegacyHook] +function filefield_paths_file_url_alter(string &$uri): void {// phpcs:ignore Drupal.Commenting.FunctionComment.Missing, Squiz.WhiteSpace.FunctionSpacing.Before + \Drupal::service(FileUrlHooks::class)->fileUrlAlter($uri); +} + // @phpstan-ignore-next-line #[LegacyHook] function filefield_paths_local_tasks_alter(array &$local_tasks): void {// phpcs:ignore Drupal.Commenting.FunctionComment.Missing, Squiz.WhiteSpace.FunctionSpacing.Before From 01b6b9022c646c32dc8686c02dc89f7d9da815f0 Mon Sep 17 00:00:00 2001 From: Stuart Clark Date: Sun, 21 Jun 2026 11:05:07 +0000 Subject: [PATCH 5/6] test(#3121826): add kernel tests for image style access check and URL alter hook --- tests/src/Kernel/FileUrlHooksTest.php | 89 +++++++++++++ .../ImageStyleTemporaryAccessCheckTest.php | 122 ++++++++++++++++++ 2 files changed, 211 insertions(+) create mode 100644 tests/src/Kernel/FileUrlHooksTest.php create mode 100644 tests/src/Kernel/ImageStyleTemporaryAccessCheckTest.php diff --git a/tests/src/Kernel/FileUrlHooksTest.php b/tests/src/Kernel/FileUrlHooksTest.php new file mode 100644 index 0000000..cc2b7bf --- /dev/null +++ b/tests/src/Kernel/FileUrlHooksTest.php @@ -0,0 +1,89 @@ + + */ + protected static $modules = [ + 'system', + 'user', + 'file', + 'filefield_paths', + ]; + + /** + * The hook implementation under test. + */ + protected FileUrlHooks $fileUrlHooks; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + $this->fileUrlHooks = $this->container->get(FileUrlHooks::class); + } + + /** + * Sets the FFP temp location config. + */ + protected function setTempLocation(string $temp_location): void { + $this->config('filefield_paths.settings')->set('temp_location', $temp_location)->save(); + } + + /** + * Non-temporary temp_location leaves the URI untouched. + */ + public function testNonTemporarySchemeIsUnaffected(): void { + $this->setTempLocation('private://filefield_paths'); + $uri = 'temporary://styles/thumbnail/temporary/filefield_paths/image.png'; + $this->fileUrlHooks->fileUrlAlter($uri); + $this->assertSame('temporary://styles/thumbnail/temporary/filefield_paths/image.png', $uri); + } + + /** + * A URI that does not match the temporary style pattern is unaffected. + */ + public function testNonMatchingUriIsUnaffected(): void { + $this->setTempLocation('temporary://filefield_paths'); + $uri = 'temporary://filefield_paths/image.png'; + $this->fileUrlHooks->fileUrlAlter($uri); + $this->assertSame('temporary://filefield_paths/image.png', $uri); + } + + /** + * A matching temporary style derivative URI is rewritten to the FFP route. + */ + public function testMatchingUriIsRewritten(): void { + $this->setTempLocation('temporary://filefield_paths'); + $uri = 'temporary://styles/thumbnail/temporary/filefield_paths/image.png'; + $this->fileUrlHooks->fileUrlAlter($uri); + $this->assertStringContainsString('/filefield_paths/image-style/thumbnail/temporary', $uri); + $this->assertStringContainsString('file=', $uri); + $this->assertStringContainsString('filefield_paths', $uri); + } + +} diff --git a/tests/src/Kernel/ImageStyleTemporaryAccessCheckTest.php b/tests/src/Kernel/ImageStyleTemporaryAccessCheckTest.php new file mode 100644 index 0000000..be2f8f1 --- /dev/null +++ b/tests/src/Kernel/ImageStyleTemporaryAccessCheckTest.php @@ -0,0 +1,122 @@ + + */ + protected static $modules = [ + 'system', + 'user', + 'file', + 'filefield_paths', + ]; + + /** + * The access checker under test. + */ + protected ImageStyleTemporaryAccessCheck $accessCheck; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + $this->accessCheck = $this->container->get('filefield_paths.access_checker.image_style_temporary'); + } + + /** + * Sets the FFP temp location config. + */ + protected function setTempLocation(string $temp_location): void { + $this->config('filefield_paths.settings')->set('temp_location', $temp_location)->save(); + } + + /** + * An empty ?file= parameter is forbidden. + */ + public function testEmptyFileIsForbidden(): void { + $this->setTempLocation('temporary://filefield_paths'); + $result = $this->accessCheck->access(Request::create('/')); + $this->assertTrue($result->isForbidden()); + if (!$result instanceof CacheableDependencyInterface) { + $this->fail('Expected a cacheable access result.'); + } + $this->assertSame(['url.query_args:file'], $result->getCacheContexts()); + $this->assertSame(['config:filefield_paths.settings'], $result->getCacheTags()); + } + + /** + * A non-temporary temp_location forbids access regardless of ?file=. + */ + public function testNonTemporarySchemeIsForbidden(): void { + $this->setTempLocation('private://filefield_paths'); + $result = $this->accessCheck->access(Request::create('/', 'GET', ['file' => 'filefield_paths/image.png'])); + $this->assertTrue($result->isForbidden()); + } + + /** + * An empty subdirectory in temp_location forbids access. + */ + public function testEmptySubdirIsForbidden(): void { + $this->setTempLocation('temporary://'); + $result = $this->accessCheck->access(Request::create('/', 'GET', ['file' => 'anything.png'])); + $this->assertTrue($result->isForbidden()); + } + + /** + * A path traversal sequence in ?file= is forbidden. + */ + public function testPathTraversalIsForbidden(): void { + $this->setTempLocation('temporary://filefield_paths'); + $result = $this->accessCheck->access(Request::create('/', 'GET', ['file' => 'filefield_paths/../etc/passwd'])); + $this->assertTrue($result->isForbidden()); + } + + /** + * A file outside the configured subdirectory is not allowed. + * + * AllowedIf(FALSE) returns a neutral result rather than an explicit forbid, + * so this asserts isAllowed() rather than isForbidden() (unlike the other + * negative cases above, which return AccessResult::forbidden() directly). + */ + public function testFileOutsideSubdirIsNotAllowed(): void { + $this->setTempLocation('temporary://filefield_paths'); + $result = $this->accessCheck->access(Request::create('/', 'GET', ['file' => 'other_module/image.png'])); + $this->assertFalse($result->isAllowed()); + } + + /** + * A file inside the configured subdirectory is allowed. + */ + public function testFileInsideSubdirIsAllowed(): void { + $this->setTempLocation('temporary://filefield_paths'); + $result = $this->accessCheck->access(Request::create('/', 'GET', ['file' => 'filefield_paths/image.png'])); + $this->assertTrue($result->isAllowed()); + } + +} From 4ccc6a54b899792b155d5e72fa2fae692c597f57 Mon Sep 17 00:00:00 2001 From: Stuart Clark Date: Tue, 23 Jun 2026 01:32:55 +0000 Subject: [PATCH 6/6] test: convert PHPUnit docblock annotation to attribute Missed during the base-branch Rector fix (test: convert PHPUnit docblock annotations to attributes) since this file only exists on this branch. --- tests/src/Functional/FileFieldPathsImageStyleTemporaryTest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/src/Functional/FileFieldPathsImageStyleTemporaryTest.php b/tests/src/Functional/FileFieldPathsImageStyleTemporaryTest.php index aabf23a..d0dfd89 100644 --- a/tests/src/Functional/FileFieldPathsImageStyleTemporaryTest.php +++ b/tests/src/Functional/FileFieldPathsImageStyleTemporaryTest.php @@ -4,6 +4,7 @@ namespace Drupal\Tests\filefield_paths\Functional; +use PHPUnit\Framework\Attributes\Group; use Drupal\Core\File\FileExists; use Drupal\Core\File\FileSystemInterface; use Drupal\image\Entity\ImageStyle; @@ -14,6 +15,7 @@ * * @group filefield_paths */ +#[Group('filefield_paths')] class FileFieldPathsImageStyleTemporaryTest extends BrowserTestBase { /**