Skip to content

fix(#3121826): add on-demand image style delivery for temporary:// staged files#5

Open
Decipher wants to merge 6 commits into
8.x-1.xfrom
feature/3121826-temporary_image_style
Open

fix(#3121826): add on-demand image style delivery for temporary:// staged files#5
Decipher wants to merge 6 commits into
8.x-1.xfrom
feature/3121826-temporary_image_style

Conversation

@Decipher

@Decipher Decipher commented Jun 18, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes broken image thumbnails on the node edit form when File (Field) Paths
stages uploaded files in temporary://. Image style derivatives are now
generated and served on demand via a dedicated delivery route, so the default
temporary://filefield_paths temp location works without switching to
private://.

Closes #3121826

Background

When an image field uses File (Field) Paths, uploaded files are staged in a
temporary location (temporary://filefield_paths by default) and moved to
their final path only when the entity is saved. Drupal core's image style
delivery controller does not handle temporary:// URIs, so derivative URLs
return 404 — causing the "broken thumbnail" symptom reported in the issue.

The workaround was to switch temp_location to private://filefield_paths,
but this requires the private filesystem to be configured and adds access
overhead that isn't needed in most cases.

How it works

  1. hook_file_url_alter() — intercepts derivative URIs matching
    temporary://styles/{style}/temporary/{file} and rewrites them to a
    dedicated route.
  2. Delivery route (/system/files/styles/{image_style}/temporary) —
    delegates to core's ImageStyleDownloadController::deliver() with
    scheme: temporary, so derivative generation and itok validation work
    exactly as they do for public/private files.
  3. Access checker (_ffp_temp_image_style) — restricts the route to
    files within the configured FFP temp subdirectory, preventing arbitrary
    temporary:// file access. When temp_location uses a non-temporary
    scheme (e.g. private://), the checker returns neutral so the normal
    private file delivery path applies.

Changes

File Type Purpose
src/Hook/FileUrlHooks.php New hook_file_url_alter() — rewrites temporary derivative URIs
src/Access/ImageStyleTemporaryAccessCheck.php New Access checker scoped to FFP temp subdirectory
filefield_paths.routing.yml Modified Delivery route for temporary image styles
filefield_paths.services.yml Modified Register access checker + FileUrlHooks service
tests/src/Functional/FileFieldPathsImageStyleTemporaryTest.php New 4 tests: derivative served, itok validation, subdir restriction, private:// unaffected

Test plan

  • DRUPAL_VERSION=10 make lint passes
  • DRUPAL_VERSION=10 make test passes
  • Manual: image field with FFP temp location set to temporary://filefield_paths — thumbnails appear on node edit form

Summary by CodeRabbit

  • New Features
    • Added a dedicated temporary image-style delivery route with automatic URL rewriting to include the original file as a query parameter.
    • Introduced access control for temporary delivery, restricting requests to the configured temporary location and allowed subdirectory.
  • Bug Fixes
    • Hardened image-style delivery against ?file= path traversal attempts (rejected with HTTP 403).
  • Tests
    • Extended functional tests with a traversal-specific negative case.
  • Chores
    • Updated spell-check allowlist for itok and added PHP override annotations plus legacy hook delegation.

@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@Decipher, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 18 minutes and 35 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f00aaedc-3607-40e7-8b6f-ceb2ed539aff

📥 Commits

Reviewing files that changed from the base of the PR and between 21bdba6 and 4ccc6a5.

📒 Files selected for processing (9)
  • .cspell.json
  • filefield_paths.module
  • filefield_paths.routing.yml
  • filefield_paths.services.yml
  • src/Access/ImageStyleTemporaryAccessCheck.php
  • src/Hook/FileUrlHooks.php
  • tests/src/Functional/FileFieldPathsImageStyleTemporaryTest.php
  • tests/src/Kernel/FileUrlHooksTest.php
  • tests/src/Kernel/ImageStyleTemporaryAccessCheckTest.php
📝 Walkthrough

Walkthrough

Adds a new Drupal route and services to serve image style derivatives for files stored in the temporary:// stream. A file_url_alter hook rewrites matching URIs to the new route. An access checker restricts delivery to files within the configured FFP staging subdirectory. Functional tests cover URL rewriting, derivative delivery, and security failure cases. Also modernizes existing code with #[\Deprecated] and #[\Override] attributes.

Changes

Temporary Image Style Delivery

Layer / File(s) Summary
Route definition and service registration
filefield_paths.routing.yml, filefield_paths.services.yml
Adds the filefield_paths.image_style_temporary route at /filefield_paths/image-style/{image_style}/temporary mapped to ImageStyleDownloadController::deliver with no_cache and _ffp_temp_image_style requirement; registers ImageStyleTemporaryAccessCheck (with @config.factory) and FileUrlHooks (autowired) as services.
file_url_alter hook rewrites temporary:// derivative URIs
src/Hook/FileUrlHooks.php, filefield_paths.module
FileUrlHooks injects ConfigFactoryInterface, reads temp_location from settings, exits early when the scheme is not temporary, and rewrites matching temporary://styles/{image_style}/temporary/{file_path} URIs to absolute URLs on the new route. Legacy hook filefield_paths_file_url_alter() forwards the URI by reference to the service.
Access checker validates file paths against temp subdirectory
src/Access/ImageStyleTemporaryAccessCheck.php
ImageStyleTemporaryAccessCheck::access forbids requests with empty file parameter, unresolvable temp_location subdirectory, or paths containing .. traversal segments. Allows only when the normalized file path starts with the resolved subdirectory followed by /; results carry url.query_args:file cache context and config:filefield_paths.settings cache tag.
Functional tests and spell-check allowlist
tests/src/Functional/FileFieldPathsImageStyleTemporaryTest.php, .cspell.json
Covers URL rewriting (HTTP 200 + image Content-Type), missing itok (404), file outside subdirectory (403), absent file param (403), path traversal in file param (403), and no rewriting when temp_location uses private://; adds "itok" to the cspell allowlist.

PHP Attribute Modernization

Layer / File(s) Summary
Deprecation attribute conversions
filefield_paths.module
Replaces @deprecated phpdoc blocks with #[\Deprecated(message: ...)] attributes for six legacy functions while preserving existing @trigger_error() calls: filefield_paths_form_submit(), filefield_paths_element_temp_location_validate(), filefield_paths_batch_update(), _filefield_paths_create_redirect(), filefield_paths_process_string(), and filefield_paths_recommended_temporary_scheme().
Override attribute additions
src/Form/SettingsForm.php, tests/modules/filefield_paths_test/src/StreamWrapper/FileFieldPathsDummyReadOnlyStreamWrapper.php, tests/src/Functional/FileFieldPathsTestBase.php, tests/src/Kernel/RedirectTest.php
Adds #[\Override] attribute to inherited method declarations: SettingsForm::create(), buildForm(), validateForm(), submitForm(); FileFieldPathsDummyReadOnlyStreamWrapper::getName(), getDescription(), getExternalUrl(); FileFieldPathsTestBase::createFileField(); RedirectTest::register(). No method signatures or implementations changed.

Sequence Diagram(s)

sequenceDiagram
    participant Browser as Browser
    participant LegacyHook as filefield_paths_file_url_alter
    participant FileUrlHooks as FileUrlHooks::fileUrlAlter
    participant Router as filefield_paths.image_style_temporary route
    participant AccessCheck as ImageStyleTemporaryAccessCheck
    participant Controller as ImageStyleDownloadController::deliver

    Browser->>LegacyHook: file_url_alter(temporary://styles/ffp/temporary/img.jpg)
    LegacyHook->>FileUrlHooks: fileUrlAlter($uri)
    FileUrlHooks-->>Browser: /filefield_paths/image-style/ffp/temporary?file=filefield_paths/img.jpg&itok=...

    Browser->>Router: GET /filefield_paths/image-style/ffp/temporary?file=...&itok=...
    Router->>AccessCheck: access(request)
    alt file empty, contains .., or outside subdirectory
        AccessCheck-->>Router: AccessResult::forbidden()
        Router-->>Browser: HTTP 403
    else itok invalid
        Controller-->>Browser: HTTP 404
    else valid
        AccessCheck-->>Router: AccessResult::allowed()
        Router->>Controller: deliver(image_style=ffp, scheme=temporary)
        Controller-->>Browser: HTTP 200 image/jpeg
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~28 minutes

Poem

🐇 A temp file hides in streams so deep,
But now a route will guard its sleep.
The hook rewrites, the checker checks,
No ../ tricks—just valid specs!
With itok blessed and #[\Override] blessed,
The fluffiest route you've ever tested. ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: adding on-demand image style delivery for temporary staged files, which aligns with the PR's primary objective of fixing broken image thumbnails.
Docstring Coverage ✅ Passed Docstring coverage is 90.48% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/3121826-temporary_image_style

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@Decipher
Decipher force-pushed the feature/3121826-temporary_image_style branch from a8f056d to 4da2b52 Compare June 18, 2026 09:26
@codecov

codecov Bot commented Jun 18, 2026

Copy link
Copy Markdown

Welcome to Codecov 🎉

Once you merge this PR into your default branch, you're all set! Codecov will compare coverage reports and display results in all future pull requests.

Thanks for integrating Codecov - We've got you covered ☂️

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
tests/src/Functional/FileFieldPathsImageStyleTemporaryTest.php (1)

100-119: ⚡ Quick win

Add a regression test for ../ in file query param.

Current negative-path tests are good, but a traversal-shaped file value (for example filefield_paths/../...) should be explicitly asserted as 403 to lock the boundary.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/src/Functional/FileFieldPathsImageStyleTemporaryTest.php` around lines
100 - 119, Add a new test method in the FileFieldPathsImageStyleTemporaryTest
class that specifically tests path traversal attempts using `../` in the file
query parameter. The test should follow the same pattern as
testFileOutsideSubdirReturns403 and testEmptyFileParamReturns403, constructing a
URL where the file parameter contains a traversal sequence like
filefield_paths/../, then call drupalGet with that URL, and assert the response
status code is 403 to ensure path traversal attempts are properly blocked.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/Access/ImageStyleTemporaryAccessCheck.php`:
- Around line 35-45: The access check in ImageStyleTemporaryAccessCheck needs to
enforce both the temporary scheme and prevent path traversal. First, validate
that the temp_location configuration value starts with the temporary:// scheme
before using it for the prefix check. Additionally, add a check to reject file
paths containing traversal segments like ../ to prevent out-of-scope access
patterns. Both validations should return AccessResult::forbidden() if they fail,
ensuring the str_starts_with check on line 44 cannot be bypassed through path
traversal or non-temporary scheme values.
- Around line 31-48: The ImageStyleTemporaryAccessCheck method has multiple
early return statements that create AccessResult objects without cache metadata.
The early returns at lines 32 and 41 (when file is empty or subdir is invalid)
return AccessResult::forbidden() without any cacheability information, while the
final return statement includes addCacheContexts and addCacheTags calls. Add the
same cache contexts ['url.query_args:file'] and cache tags
['config:filefield_paths.settings'] to all AccessResult returns in the method.
This ensures that all forbidden access decisions are properly cached and
invalidated when the file parameter or configuration changes, preventing stale
cached results.

---

Nitpick comments:
In `@tests/src/Functional/FileFieldPathsImageStyleTemporaryTest.php`:
- Around line 100-119: Add a new test method in the
FileFieldPathsImageStyleTemporaryTest class that specifically tests path
traversal attempts using `../` in the file query parameter. The test should
follow the same pattern as testFileOutsideSubdirReturns403 and
testEmptyFileParamReturns403, constructing a URL where the file parameter
contains a traversal sequence like filefield_paths/../, then call drupalGet with
that URL, and assert the response status code is 403 to ensure path traversal
attempts are properly blocked.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 2328c92f-e5a5-43ba-b3e3-c13f1abaf323

📥 Commits

Reviewing files that changed from the base of the PR and between 5be2c0d and aa3805d.

📒 Files selected for processing (6)
  • .cspell.json
  • filefield_paths.routing.yml
  • filefield_paths.services.yml
  • src/Access/ImageStyleTemporaryAccessCheck.php
  • src/Hook/FileUrlHooks.php
  • tests/src/Functional/FileFieldPathsImageStyleTemporaryTest.php

Comment thread src/Access/ImageStyleTemporaryAccessCheck.php Outdated
Comment thread src/Access/ImageStyleTemporaryAccessCheck.php

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
src/Access/ImageStyleTemporaryAccessCheck.php (1)

48-58: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Normalize $subdir to match $normalized_file handling.

The $file path is normalized on line 53 (leading slashes trimmed), but $subdir is used raw from getTarget(). If the configured temp_location has a trailing slash (e.g., temporary://filefield_paths/), getTarget() returns filefield_paths/, causing the line 58 check to compare against filefield_paths//—which always fails, incorrectly denying legitimate requests.

Suggested fix
-    $subdir = StreamWrapperManager::getTarget($temp_location);
+    $subdir = trim((string) StreamWrapperManager::getTarget($temp_location), '/');
     if (!is_string($subdir) || $subdir === '') {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Access/ImageStyleTemporaryAccessCheck.php` around lines 48 - 58, The
$subdir variable obtained from StreamWrapperManager::getTarget() is not
normalized like $normalized_file is on line 53. When the configured
temp_location has a trailing slash, getTarget() returns a path with a trailing
slash (e.g., "filefield_paths/"), which causes the str_starts_with check on line
58 to create a double slash ("filefield_paths//"), always failing the
comparison. Normalize $subdir by removing trailing slashes using rtrim($subdir,
'/') immediately after retrieving it from getTarget(), ensuring consistent path
formatting for the subsequent comparison with $normalized_file.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@src/Access/ImageStyleTemporaryAccessCheck.php`:
- Around line 48-58: The $subdir variable obtained from
StreamWrapperManager::getTarget() is not normalized like $normalized_file is on
line 53. When the configured temp_location has a trailing slash, getTarget()
returns a path with a trailing slash (e.g., "filefield_paths/"), which causes
the str_starts_with check on line 58 to create a double slash
("filefield_paths//"), always failing the comparison. Normalize $subdir by
removing trailing slashes using rtrim($subdir, '/') immediately after retrieving
it from getTarget(), ensuring consistent path formatting for the subsequent
comparison with $normalized_file.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 9370a0ed-6eee-4380-aa53-70bf944e1381

📥 Commits

Reviewing files that changed from the base of the PR and between aa3805d and 2475b4b.

📒 Files selected for processing (3)
  • filefield_paths.module
  • src/Access/ImageStyleTemporaryAccessCheck.php
  • tests/src/Functional/FileFieldPathsImageStyleTemporaryTest.php
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/src/Functional/FileFieldPathsImageStyleTemporaryTest.php

@Decipher
Decipher force-pushed the feature/3121826-temporary_image_style branch 2 times, most recently from 4bc2db5 to ea168e8 Compare June 22, 2026 11:34
@Decipher
Decipher force-pushed the feature/3121826-temporary_image_style branch from ea168e8 to 01b6b90 Compare June 23, 2026 00:56
Missed during the base-branch Rector fix (test: convert PHPUnit docblock
annotations to attributes) since this file only exists on this branch.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant