ADR-004: Canonical RTE Image src Storage Convention 

Date

2026-05-28

Status

Accepted

Context

Issues #778, #837; PRs #779, #839, #840

Summary 

The extension persists every RTE image src in canonical site-root-relative form with a leading slash (/fileadmin/x). This contract is enforced symmetrically on both ends of the data flow: the storage write path (ImageTagBuilder::makeRelativeSrc) and the validator's strict-equality comparison (RteImageReferenceValidator::srcMatchesPublicUrl). Slashless (fileadmin/x) and protocol-relative (//cdn.example.com/x) forms are out-of-contract: slashless is detected as a defect and repaired; protocol-relative is treated as an external CDN reference and passes through unchanged.

Context 

A series of regressions exposed an implicit contract that had never been written down:

  • #778 reported that getPublicUrl() for the Local FAL driver returns slashless (fileadmin/x) while the RTE stored leading-slash (/fileadmin/x). The validator's naïve string-compare flagged the stored value as SrcMismatch and "fixed" it by stripping the leading slash — silently breaking frontend rendering on every page.
  • The first attempt at #778 (#779) made the comparison tolerant: both slashless and leading-slash were accepted as equivalent. This stopped the over-correction but turned the validator blind to a different defect.
  • #837 surfaced that defect: older upgrade:run versions had stripped the leading slash from existing storage. validate --fix was the intended repair tool but, after the over-correction in #779, it silently treated the broken value as already-correct.
  • #839 tightened the validator to strict equality, restoring repair.
  • #840 closed the loop: ImageTagBuilder::makeRelativeSrc was still producing slashless on save in some paths, which meant fresh inserts would be flagged + rewritten by the next validate run. The storage side now normalises to leading-slash for every local input.

The decision below codifies the convention so it does not get re-broken.

Decision Drivers 

TYPO3 v12+ does not emit ``<base href>``.
config.baseURL was deprecated and removed. Modern TYPO3 uses site configuration's base key for routing; the rendered HTML carries no <base href> tag. A browser therefore resolves slashless src against the current page URL — broken on every non-root page.
Storage and rendering must be decoupled cleanly.
The storage convention should be independent of where the install is served from (root vs subpath). TYPO3's standard mechanism for this is config.absRefPrefix: a leading-slash storage value is prefixed at render time. This keeps every install layout identical at the database level.
Two write paths must not disagree.
The editor saves through RteImageProcessorImageTagBuilder::makeRelativeSrc. The validator/listener writes through RteImageReferenceValidator::fix() and UpdateImageReferences. If these two paths produce different canonical forms, every save-then-validate cycle creates spurious rewrites.
External references must be preserved bit-for-bit.
Protocol-relative (//cdn.example.com/...), scheme URLs (http://, https://, data:, mailto:) reference assets outside the site's FAL. The contract must not coerce them into a site-root-relative form.

Decision 

  1. Canonical storage form. Every local image src is persisted with a leading slash: /fileadmin/.... Slashless storage (fileadmin/...) is treated as a defect and repaired to the canonical form.
  2. External references pass through unchanged. Scheme URLs and protocol-relative URLs are detected via the RFC 3986 scheme grammar pattern ^(?:[a-z][a-z0-9+.\-]*:|//) (PHP PCRE form: #…#i) and returned as-is.
  3. Write paths agree. Both ImageTagBuilder::makeRelativeSrc and the validator's repair path (normalizePublicUrlsrcMatchesPublicUrlapplyFixes) produce the same canonical leading-slash output.
  4. Subpath installs use ``config.absRefPrefix``. The prefix is prepended at render time by TYPO3 Core. Storage stays canonical across root and subpath installs. See Frontend Issues for setup.
  5. Empty ``siteUrl`` is a safety valve. When the editor save path is invoked without a resolved site URL (CLI, certain test contexts), makeRelativeSrc returns the input unchanged. This prevents accidental rewrites in contexts where the site identity is unknown.

Worked Example: Subpath Install 

Given a TYPO3 instance served at https://example.com/~user/ with config.absRefPrefix = /~user/ configured:

Pipeline stage Value
Editor JS urlToRelative() produces /fileadmin/image.jpg
Editor save → makeRelativeSrc() stores /fileadmin/image.jpg
Database column tt_content.bodytext holds <img src="/fileadmin/image.jpg" …>
Rendered HTML (after absRefPrefix) <img src="/~user/fileadmin/image.jpg" …>
Validator normalizePublicUrl() expects /fileadmin/image.jpg
RteImageReferenceValidator::fix() no-op (storage already matches)

The storage column is identical to a site-root install. The subpath only appears in the rendered HTML, applied by TYPO3 Core's render chain. The validator therefore stays site-agnostic — it does not need to know about the subpath at all.

JavaScript / PHP normalisation parity 

The CKEditor-side helper urlToRelative() (Resources/Public/JavaScript/Plugins/typo3image.js) produces the same leading-slash form when inserting a new image, so the PHP makeRelativeSrc() normalises a value that already arrives in the canonical shape on the editor save path. The PHP rewrite covers the defence-in-depth case: server-side imports, pastes from other editors, and any callers that bypass the JS helper still produce canonical storage.

Out of Scope 

  • Emitting ``<base href>``. Removed from TYPO3 Core in v9.5 and never restored; the extension does not attempt to bring it back.
  • Sub-path rendering at the HTML layer. Delegated to config.absRefPrefix, a standard TYPO3 mechanism. The extension does not duplicate this functionality.
  • Per-site validation context. The validator does not currently inject sitePath to compute per-site expected forms. The contract above makes this unnecessary: storage is uniform across sites.
  • Path canonicalisation. makeRelativeSrc does not collapse .. or . segments. The FAL UID round-trip plus the validator's strict-equality check (RteImageReferenceValidator::srcMatchesPublicUrl) reject any src that does not match a real file's normalised public URL, so a smuggled /../../etc/passwd cannot point at content that the FAL would not have served anyway. Whitespace is trimmed up-front to prevent WHATWG-URL bypass of the scheme-grammar guard.
  • HTML output escaping. makeRelativeSrc returns raw text; the caller (ImageTagBuilder::build(), ultimately Fluid) is responsible for HTML-attribute escaping. See the @security block on the method docblock.

Consequences 

Positive 

  • A single storage convention across root and subpath installs.
  • The validator's strict-equality rule is correct for every layout.
  • Save-then-validate cycles are stable: validate after a fresh save flags nothing.
  • External CDN and data: URIs are explicitly preserved.

Negative 

  • Subpath operators with pre-existing slashless storage (from older upgrade:run versions) must run rte_ckeditor_image:validate --fix --table=tt_content once to migrate. See Image Reference Validation.
  • Operators who run a subpath install without config.absRefPrefix configured will see broken images. This is the same broken state as before #840; the convention now makes it explicit and diagnosable.
  • makeRelativeSrc is named for what it used to do (strip the site URL); it now also normalises. The name is retained for backwards compatibility of the public ImageTagBuilderInterface.