.. include:: /Includes.rst.txt
.. _adr-004-image-src-storage-convention:
============================================================
ADR-004: Canonical RTE Image ``src`` Storage Convention
============================================================
:Date: 2026-05-28
:Status: Accepted
:Context: Issues `#778`_, `#837`_; PRs `#779`_, `#839`_, `#840`_
.. _#778: https://github.com/netresearch/t3x-rte_ckeditor_image/issues/778
.. _#837: https://github.com/netresearch/t3x-rte_ckeditor_image/issues/837
.. _#779: https://github.com/netresearch/t3x-rte_ckeditor_image/pull/779
.. _#839: https://github.com/netresearch/t3x-rte_ckeditor_image/pull/839
.. _#840: https://github.com/netresearch/t3x-rte_ckeditor_image/pull/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 ````.**
``config.baseURL`` was deprecated and removed. Modern TYPO3 uses
site configuration's ``base`` key for routing; the rendered HTML
carries no ```` 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 ``RteImageProcessor`` ➜
``ImageTagBuilder::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 (``normalizePublicUrl`` ➜
``srcMatchesPublicUrl`` ➜ ``applyFixes``) 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
:ref:`troubleshooting-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 ``
``
Rendered HTML (after ``absRefPrefix``) ``
``
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 ````.** 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
:ref:`troubleshooting-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``.
Related Documentation
=====================
- :ref:`troubleshooting-image-reference-validation` — the
``validate --fix`` command and what it repairs.
- :ref:`troubleshooting-frontend-issues` — ``config.absRefPrefix``
setup for subpath installs.
- :ref:`adr-003-security-responsibility-boundaries` — pattern for
contracts that delegate to TYPO3 Core.