Overview

As recently pointed out by @isholgueras on the MR for #3518620: Image prop shape matching should be case-insensitive, but JSON Schema doesn't support that, the current 3.1.2.a structured data → matching field instances ⇒ dynamic prop source infrastructure for matching $ref: json-schema-definitions://experience_builder.module/image-uri and $ref: json-schema-definitions://experience_builder.module/image, which rely on this:

    "image-uri": {
      "title": "Image URL",
      "type": "string",
      "format": "uri-reference",
      "pattern": "^(/|https?://)?.*\\.(png|gif|jpg|jpeg|webp)(\\?.*)?(#.*)?$"
    },

getting matched against Drupal core's (semantically equivalent) FileExtension validation constraint, which necessarily requires translating from/to two wildly different representations.

The key problem: the list of file extensions is hardcoded. Which means customizations to the list of allowed image types would be a problem. Recently, AVIF support landed in #3372932: [Meta] High-performance images (nearly) out of the box, which shows how likely this is to happen over time.

The current code dates back to April 2024, _before_ we had even started using d.o issues. This is some of the oldest code in XB and dates back to the research__data_model PoC research branch.

Note: this follows a 2017 best practice!

Blockers discovered while working on this

  1. #3540470: Require 6.x version of `justinrainbow/json-schema` package to get correct JSON Schema validation
  2. #3543770: `noUi: true` SDCs do still show up in the UI
  3. #3543783: `card-with-stream-wrapper-image` test SDC generates an invalid `<img src>`
  4. #3543805: Resolved image file URLs invalid in kernel tests

Proposed resolution

Use contentMediaType.

So then we'd go from:

    "image-uri": {
      "title": "Image URL",
      "type": "string",
      "format": "uri-reference",
      "pattern": "^(/|https?://)?.*\\.(png|gif|jpg|jpeg|webp)(\\?.*)?(#.*)?$"
    },

to:

    "image-uri": {
      "title": "Image URL",
      "type": "string",
      "format": "uri-reference",
      "contentMediaType": "image/*"
    },

🐛 EDIT: This is in adequate — because it would also allow ftp://cat.jpg. Which means we need to keep pattern but make it less specific:

    "image-uri": {
      "title": "Image URL",
      "type": "string",
      "format": "uri-reference",
      "contentMediaType": "image/*",
      "pattern": "^(/|https?://)?(?!.*\\://)[^\\s]+$"
    },

(explanation: see the new SchemaJsonPatternsTest)

… which is not quite exactly how contentMediaType is meant to be used (that's really describing the contents of the string itself), but is … close enough? Something along these lines has been proposed as resourceMediaType.

(Examples of actually defining new JSON Schema formats seem impossible to find.)

This would then also allow us to apply a similar improvement to "video URLs", which is being added at #3524130: Define JSON Schema $refs for linking/embedding videos and linking documents.

User interface changes

None.

CommentFileSizeAuthor
#23 Screenshot 2025-09-01 at 6.35.24 PM.png483.95 KBwim leers

Issue fork canvas-3530351

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

wim leers created an issue. See original summary.

wim leers’s picture

The intent has been from the start to avoid adding "Drupalism" to SDC's use of JSON Schema.

That's why we started using /schema.json and defined $ref: json-schema-definitions://experience_builder.module/image-uri.

However, given that #3499279: Make link widget autocomplete work (for uri and uri-reference props) is proposing to add a variation for format: uri named format: uri+entity_autocomplete (which required a core change: #3516359: ComponentValidator ignores the set validator and creates a new one

So, an alternative HERE could be that we start using custom formats (for type: string). Which is exactly what @lauriii expressed he'd strongly prefer over at #3462705-47: [META] Missing features in SDC needed for XB. (Note that JSON Schema does support custom format attributes: https://json-schema.org/draft/2020-12/json-schema-validation#section-7 + https://json-schema.org/draft/2020-12/json-schema-validation#name-custom...)

Then I propose that we'd use format: uri+mime+image/* or something like that to express this more generically Or better yet: use contentMediaType.

So then we'd go from:

    "image-uri": {
      "title": "Image URL",
      "type": "string",
      "format": "uri-reference",
      "pattern": "^(/|https?://)?.*\\.(png|gif|jpg|jpeg|webp)(\\?.*)?(#.*)?$"
    },

to:

    "image-uri": {
      "title": "Image URL",
      "type": "string",
      "format": "uri-reference",
      "contentMediaType": "image/*"
    },

… which is not quite exactly how contentMediaType is meant to be used (that's really describing the contents of the string itself), but is … close enough? Something along these lines has been proposed as resourceMediaType.

Examples of actually defining new JSON Schema formats seem impossible to find.

This would then also allow us to apply a similar improvement to "video URLs", which is being added at #3524130: Define JSON Schema $refs for linking/embedding videos and linking documents.

wim leers’s picture

Priority: Normal » Critical

Bumping to Critical priority. Because without this, we'll need to keep doing awkward dances/updates and we'll never match all image fields.

IOW: without this, we won't be able to match any image fields or image media on existing Drupal sites that do not allow AVIF uploads. That's bad! XB users will be (rightfully!) baffled and frustrated that they can't use their years worth of uploaded images.

wim leers’s picture

Title: Decouple image (URI) shape matching from specific image file types/extensions » Decouple image+video (URI) shape matching from specific image+video file types/extensions
Version: 0.x-dev » 1.x-dev

Same is true for video, which is currently limited to just MP4.

wim leers’s picture

Assigned: Unassigned » wim leers
Status: Needs review » Active
wim leers’s picture

The pivot from uri-reference to uri should be uneventful, because as described in https://github.com/json-schema/json-schema/issues/81, URI references are a subset of URLs.

But in practice, this does not seem to be the case, due to the utterly broken JSON Schema validation of URIs in justinrainbow/json-schema. Hopefully #3540470-14: Require 6.x version of `justinrainbow/json-schema` package to get correct JSON Schema validation brings relief.

For now, sticking to uri-reference to reign in scope.


Note that even doing

diff --git a/src/Plugin/DataTypeOverride/ComputedFileUrlOverride.php b/src/Plugin/DataTypeOverride/ComputedFileUrlOverride.php
index 55f63306e..647808fd3 100644
--- a/src/Plugin/DataTypeOverride/ComputedFileUrlOverride.php
+++ b/src/Plugin/DataTypeOverride/ComputedFileUrlOverride.php
@@ -42,6 +42,12 @@ class ComputedFileUrlOverride extends Uri {
     $file_url_generator = \Drupal::service('file_url_generator');
     $this->url = $file_url_generator->generateString($uri);
 
+    // In Kernel tests, any files in `public://` get invalid URLs like
+    // `/vfs://root/sites/simpletest/…/files/…`.
+    if (drupal_valid_test_ua()) {
+      $this->url = str_replace('/vfs://', '/', $this->url);
+    }
+
     return $this->url;
   }

to transform

/vfs://root/sites/simpletest/43516299/files/2025-08/generateImage_ZCZktY.jpg?…

to

/root/sites/simpletest/43516299/files/2025-08/generateImage_ZCZktY.jpg?…

doesn't fix it, nor does updating to version 6.4.2 of justinrainbow/json-schema.

It seems https://github.com/jsonrainbow/json-schema/pull/800 still didn't fix it quite right. 😬

wim leers’s picture

Looks like #7 is turning out to become a reality, but for a different reason 🙃

The problem appears to be that pattern: ^(/|https?://)? is not considered valid in 5.3 (which CI is testing with), but is considered valid in 6.4. 🫠 Which I'm developing against locally, and I don't really have a choice, because I'm on a non-ancient version of Composer, which forces this:

$ composer require justinrainbow/json-schema:^5.3
justinrainbow/json-schema is currently present in the require-dev key and you ran the command without the --dev flag, which will move it to the require key.
Do you want to move this requirement? [no]? yes
./composer.json has been updated
Running composer update justinrainbow/json-schema
> Drupal\Composer\Composer::ensureComposerVersion
Loading composer repositories with package information
Updating dependencies
Your requirements could not be resolved to an installable set of packages.

  Problem 1
    - Root composer.json requires justinrainbow/json-schema ^5.3, found justinrainbow/json-schema[5.3.0, 5.x-dev] but these were not loaded, likely because it conflicts with another require.
  Problem 2
    - composer/composer is locked to version 2.8.9 and an update of this package was not requested.
    - composer/composer 2.8.9 requires justinrainbow/json-schema ^6.3.1 -> found justinrainbow/json-schema[dev-master, 6.3.1, ..., 6.x-dev (alias of dev-master)] but it conflicts with your root composer.json require (^5.3).

Use the option --with-all-dependencies (-W) to allow upgrades, downgrades and removals for packages currently locked to specific versions.

Installation failed, reverting ./composer.json and ./composer.lock to their original content.
wim leers’s picture

The 5.3 regex validation logic is apparently this:

    protected function validateRegex($regex)
    {
        return false !== @preg_match('/' . $regex . '/u', '');
    }

https://github.com/jsonrainbow/json-schema/blob/5.3.0/src/JsonSchema/Con...

Which indeed fails: https://3v4l.org/hPCA3.

In 6.3.1 and later it is very different: https://github.com/jsonrainbow/json-schema/blob/6.3.1/src/JsonSchema/Con...

wim leers’s picture

LOLOL reading #3516348: Allow 6.x version of justinrainbow/json-schema points to Composer 2.8.6 vs 2.8.8, while here 2.8.10 is used. https://github.com/composer/composer/releases/tag/2.8.10 points to https://github.com/composer/composer/pull/12376, which is … very much a similar problem 😅 (It touches upon pattern being quite brittle, and literally does what we did in #3518620: Image prop shape matching should be case-insensitive, but JSON Schema doesn't support that!)

wim leers’s picture

The justinrainbow/json-schema version bump didn't work, root cause was #3538439-13: Running tests locally is difficult to match with the CI. Being fixed at #3540470-16: Require 6.x version of `justinrainbow/json-schema` package to get correct JSON Schema validation.

Will continue after the weekend.

wim leers’s picture

Merged in upstream now that #3543770: `noUi: true` SDCs do still show up in the UI is in 👍

wim leers’s picture

New blocker: #3543805: Resolved image file URLs invalid in kernel tests — will make this MR far less overwhelming. Also: #3543783: `card-with-stream-wrapper-image` test SDC generates an invalid `<img src>` is green — awaiting @justafish's approval.

wim leers’s picture

#3543805: Resolved image file URLs invalid in kernel tests is in, bringing that into the branch reduces it from 39 to 27 files.

#3543783: `card-with-stream-wrapper-image` test SDC generates an invalid `<img src>` will bring it down more 👍

wim leers’s picture

wim leers’s picture

wim leers’s picture

wim leers’s picture

Issue summary: View changes
Issue tags: -Needs issue summary update

I won't be able to finish this today. To make card-with-stream-wrapper-image work correctly as well requires further fixes.

Also: the whole pattern (aka Regex) based matching seems brittle. It's better in this MR than it is in HEAD, and pretty proven thanks to the new SchemaJsonPatternsTest, but it's still too brittle.

I propose to stop using pattern altogether and instead introduce x-allowed-schemes, similar to how we already introduced x-required-variables and x-formatting-context. IOW:

    "stream-wrapper-image-uri": {
      "title": "Stream wrapper image URI",
      "type": "string",
      "format": "uri",
      "contentMediaType": "image/*",
      "x-allowed-protocols": ["public"],
    },
    "image-uri": {
      "title": "Image URL",
      "type": "string",
      "format": "uri-reference",
      "contentMediaType": "image/*",
      "x-allowed-protocols": ["http", "https"],
    },

Will refactor towards that on Monday unless I hear counterarguments.

wim leers’s picture

Assigned: wim leers » Unassigned
Status: Active » Needs review
StatusFileSize
new483.95 KB

Green!

This removes 7 @todos and adds 3.

Literally the last failure is

    // Valid stream wrapper, but non-existent file.
    await xBEditor.exitPreview();
    await xBEditor.openLayersPanel();
    await xBEditor.openComponent('Card with stream wrapper image');
    await xBEditor.editComponentProp('src', 'public://balloons2.png');
    await xBEditor.preview();
    const cardStreamWrapperInvalidSrc = previewIframe.locator(
      '.card--with-stream-wrapper-image img.card--image',
    );
    await cardStreamWrapper.scrollIntoViewIfNeeded();
    expect(await cardStreamWrapperInvalidSrc.getAttribute('src')).toContain(
      // The stream wrapper URI must be rewritten to an actually resolvable URL.
      // @see tests/modules/xb_test_sdc/components/card-with-stream-wrapper-image/card-with-stream-wrapper-image.twig
      'files/balloons2.png',
    );

… which is testing with an INVALID stream wrapper URI entered into a input[type=text], which the XB UX is intended to never show/allow, and indeed is no longer the case: it now shows the image field type's widget!

So: removing that bit of test coverage. Will try to refactor it in the morning to make it pick an image from the media library and hence prove it works end-to-end. Will then merge this MR and address #22 in a subsequent MR.

Change record updated: https://www.drupal.org/node/3542579

wim leers’s picture

Status: Needs review » Reviewed & tested by the community

So: removing that bit of test coverage. Will try to refactor it in the morning to make it pick an image from the media library and hence prove it works end-to-end.

Done! (updated test, minor blocking infra fix, update shape matching to use media library for stream wrapper image URIs).

As soon as this is green, will merge this MR and address #22 in a subsequent MR.

wim leers’s picture

Had to add explicit waits because CI is so slow, but it's passing now! 🥳

  • wim leers committed 9048ab84 on 1.x
    Issue #3530351 by wim leers: Decouple image+video (URI) shape matching...
wim leers’s picture

Assigned: Unassigned » wim leers
Status: Reviewed & tested by the community » Needs work

That's in! Now back to Needs work for #22.

wim leers’s picture

Project: Experience Builder » Drupal Canvas

wim leers’s picture

@effulgentsia just +1'd #22 in a meeting 😊👍

wim leers’s picture

Will land this tomorrow AM to reduce the change rate for #3519247 now that it's unblocked again per #3519247-23: Acquia DAM and Canvas integration.

wim leers’s picture

This is now actively blocking #3519247: Acquia DAM and Canvas integration, so picking up again.

wim leers’s picture

Looks like adopting Symfony's UrlValidator does not what we need — I had to read src/Symfony/Component/Validator/Tests/Constraints/UrlValidatorTest.php because the docs at https://symfony.com/doc/current/reference/constraints/Url.html#relativep... are absolute 💩

wim leers’s picture

Assigned: wim leers » penyaskito
Status: Needs work » Reviewed & tested by the community

This should now be green, at last. Manual testing shows it still works fine, including for the e2e test that was failing for a bit (which was uploading a video).

wim leers’s picture

FYI this ended up fixing the matching of Link field instances: #3548298-16: Support linking field types marked SUPPORTED from #3512433 to props in a `ContentTemplate`. 🎉

penyaskito’s picture

+1 RTBC

  • wim leers committed 247bf510 on 1.x
    [#3530351] feat(Shape matching): Stop relying on `pattern` to validate...
wim leers’s picture

Assigned: penyaskito » Unassigned
Status: Reviewed & tested by the community » Fixed

🚢

Now that this issue is closed, please review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, please credit people who helped resolve this issue.

wim leers’s picture

Updated & published the change record: https://www.drupal.org/node/3542579 🎉

wim leers’s picture

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.

wim leers changed the visibility of the branch 3530351-x-allowed-schemes to hidden.

wim leers’s picture