Skip to content

0015. Permitted media view modes are curation, strict where the author chooses and lenient where the page renders

Status: Accepted

Date: 2026-09-16

Context

allowed_view_modes was enforced in three places with three different answers: a refusal on save (DrupalMedia::validate(), formerly the check at lines 145-148), a 403 from the preview route (MediaPreviewController::access(), formerly a conjunct of $offered), and a silent fallback to the default on the page (DrupalMedia::build(), docs/rendering.md); the client showed the default in the select once a stored view mode fell outside the format's list, but asked the preview endpoint for the raw, unresolved prop (modules/blocknote_media/js/block.tsx), so a stale document could show one view mode in the select and fail the preview with a 403 for the same item, and an author saw a failure notice for a block the public page rendered without complaint.

A view mode is the site's own display configuration, reachable through any formatter a site builder installs, and entity view access is checked on the item at every render (MediaRenderer::build(), modules/blocknote_media/src/MediaRenderer.php:100) and every preview (MediaPreviewController::access(), modules/blocknote_media/src/Controller/MediaPreviewController.php:179); the list of permitted view modes never guarded anything a visitor could not otherwise see, it only decided what an author was offered.

Core's own MediaEmbed filter reads data-view-mode at render time and never consults any per-format allowlist of view modes (web/core/modules/media/src/Plugin/Filter/MediaEmbed.php:240-267); MediaFilterController::preview() runs the same filter as the page, so its preview and the rendered page always agree (web/core/modules/media/src/Controller/MediaFilterController.php:55-88); CKEditor5MediaController::access() checks the filter format's use access and the item's view access only, nothing about the view mode (web/core/modules/ckeditor5/src/Controller/CKEditor5MediaController.php:141-160); core enforces a view mode restriction in its own embed dialog's editor UI alone, never on save and never on the page. A text format that stops allowing a tag does not retroactively make content already using that tag unsaveable, which is the same posture a view mode leaving a format's permitted list deserves.

The site builder's form also offered every media view mode on the site, through getViewModeOptions('media'), even though a view mode exists for a bundle only where an enabled display for it exists (EntityDisplayRepository::getViewModeOptionsByBundle(), web/core/lib/Drupal/Core/Entity/EntityDisplayRepository.php:170,213-241), and it rendered a Default checkbox that permittedViewModes() overrode either way, since the default view mode was always folded in regardless of what was ticked.

ADR 0009 places every transform of a stored document on the programmatic write path, beside DocumentNormalizer::stampVersion(), and states that the widget's form save path does not stamp.

Decision

The permitted view mode list is curation, not a security boundary.

  • The author is offered only permitted view modes, and the site builder's form offers only the view modes a permitted media type has an enabled display for; the default view mode is always permitted and is never rendered as a checkbox. Amended 2026-09-20. The offer is per item as well as per format: DrupalMedia::getEditorSettings() sends viewModesByBundle, the permitted list intersected with each permitted media type's enabled displays through EntityDisplayRepositoryInterface::getViewModeOptionsByBundle(), the preview route names the item's type in a Drupal-Media-Bundle header, the insertion command already carried it, and the toolbar's view mode menu and the block menu's View mode entry offer an item only the view modes its own type can display, with the default alone until the type is known. The site builder's form names, beside each view mode, the permitted types that display it.
  • A stored view mode outside the list is refused nowhere. The page, the preview and the client all resolve it to the default through one rule, DrupalMedia::resolveViewMode(). Amended 2026-09-20. That is the format's rule; a second rule, the type's, has always sat in MediaRenderer::resolveViewMode() (modules/blocknote_media/src/MediaRenderer.php), which renders the default when the item's own type has no enabled display for the view mode, for the page and the preview alike, since both build through that service. The editor now applies both rules, so the mode an author is shown as selected is the one the page renders.
  • The document updater stores the resolved view mode, healing a stale one, through BlockNoteBlockNormalizableInterface, called from DocumentNormalizer::normalize() immediately after the version stamp. The widget's own form save does not call it, because the widget path does not stamp either, and a stale prop sitting unread in storage is inert everywhere else in the system. Amended 2026-09-20. The updater heals against the format's list alone, not against the type's displays: a display disabled today may be enabled tomorrow, the page renders the default while it is disabled, and a stored choice the page ignores costs nothing to keep, while rewriting it would lose the author's choice for good.
  • The permitted media type list keeps its stricter posture from ADR 0006: a type is what the format's own form text promises the author, and a media type outside the list is still refused on save and in the editor's preview. The two lists read the same way on the settings form but are enforced on opposite postures, because a media type is closer to what an author is told they may pick, while a view mode is a presentational detail the site owns and may retire.
  • A later record strips a forbidden text alignment on load and refuses it on save; that is not inconsistent with this one. Alignment is policy under ADR 0010, an editorial choice the format's own rules bound, while a view mode is curation, a fact about what the site can still display, so it is healed rather than refused.

Alternatives considered

Keeping the preview's 403. Rejected. The preview route exists so the editor shows an author what the page will show a visitor; refusing the preview for a view mode the page renders without complaint makes the one route that exists to agree with the page disagree with it instead.

Normalizing the stored view mode in the editor, on load. Rejected. Writing a prop into a block the moment it is loaded would mark a document dirty, and thus changed, for having merely been opened, which is not a write any part of this module otherwise performs.

Normalizing on the widget's own form save. Not taken. The maintainer placed the healing transform beside the updater's version stamp, the widget's form save path does not stamp the version either, and a stale viewMode sitting in a document nobody is reading through the updater is inert: it costs nothing to leave unhealed until the next write that does go through the updater.

Item 5a: letting the host entity's own view mode reach the media block, so a teaser display could render an embedded media block in a smaller view mode than the full view. Rejected. A BlockNote field is a field: a display that does not want that field shown in a teaser simply does not render it there, the way any other field is narrowed per view mode, and a site that genuinely wants one field's content rendered differently per host view mode writes a field formatter, which is where Drupal already expresses exactly that concern. Threading the host's view mode into a block's own build() would need a BlockRenderContext that knows about a display it is never otherwise handed, a lazy builder cache key that is already keyed on uuid, view mode and langcode gaining a fourth, host-view-mode axis, and a per-format mapping table between host view modes and media view modes with no natural home anywhere in the module. Core's own MediaEmbed filter has exactly the same blind spot: it renders one view mode regardless of what page or display context is filtering the text.

Consequences

An author saving a document that carries a view mode the format has stopped permitting is no longer refused, and gets no notice of it: the save proceeds, and the document updater heals the stored value to the default. The debug log entry and the default rendering, instead of the requested one, are the only remaining signs.

A view mode whose last enabled display on a permitted bundle is disabled stops being offered on the settings form, and drops out of the format's dependencies the next time the format itself is saved, exactly as onBlockSettingsDependencyRemoval() already does for a deleted view mode.

A document saved only through the editor's own form keeps a stale viewMode prop until the next write that passes through DocumentUpdater::update() or ::createTranslation(); that stale prop is inert in the meantime; the page, the preview and the client select already show the default in its place.

The base module gains a third optional block interface, BlockNoteBlockNormalizableInterface, and DocumentNormalizer gains a dependency on the block plugin manager while DocumentUpdater gains one on the format registry.

The preview access result no longer varies by the requested view mode, only by the requested item, which simplifies its cache contexts.

Amended 2026-09-20. A block whose preview has not answered, or whose preview failed, draws no view mode control: the item's type is not known, only the default is offered, and a menu of one is not drawn. An item just inserted from the library knows its type from the insertion command, so the control appears at once for it.

References

Drupal core 11.4.6: web/core/modules/media/src/Plugin/Filter/MediaEmbed.php, web/core/modules/media/src/Controller/MediaFilterController.php, web/core/modules/ckeditor5/src/Controller/CKEditor5MediaController.php, web/core/lib/Drupal/Core/Entity/EntityDisplayRepository.php.

This module: modules/blocknote_media/src/Plugin/BlockNoteBlock/DrupalMedia.php, modules/blocknote_media/src/Controller/MediaPreviewController.php, modules/blocknote_media/src/MediaRenderer.php, modules/blocknote_media/js/block.tsx; src/BlockNoteBlockNormalizableInterface.php, src/Document/DocumentNormalizer.php, src/Document/DocumentUpdater.php.

ADR 0005, ADR 0006, ADR 0009, ADR 0010.