Skip to content

[8023] Image cropper dialog does not open when using XB#4843

Draft
jvega190 wants to merge 2 commits intocraftercms:developfrom
jvega190:bugfix/8023
Draft

[8023] Image cropper dialog does not open when using XB#4843
jvega190 wants to merge 2 commits intocraftercms:developfrom
jvega190:bugfix/8023

Conversation

@jvega190
Copy link
Copy Markdown
Member

@jvega190 jvega190 commented Apr 21, 2026

craftercms/craftercms#8023

Summary by CodeRabbit

  • New Features

    • Image editor dialog to edit images that fail size/dimension checks before placing or uploading.
    • Edited images are accepted and uploaded from the editor, with progress and completion events surfaced.
  • Refactor

    • Centralized image validation logic used across browsing, search, upload, and drag/drop flows.
    • Upload flow now prefers an existing file blob when available before falling back to data-URL conversion.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 21, 2026

Walkthrough

Image size validation was extracted to a shared utility and integrated into upload/placement flows. New Redux actions and dialog handling were added to open an image editor when restrictions fail and to handle edited-image uploads; guest epics were updated to validate and upload edited blobs.

Changes

Cohort / File(s) Summary
Image validation utility
ui/app/src/utils/content.ts
Added exported validateImageRestrictions(path, restrictions?) that loads images with a timeout and checks dimension constraints.
ImagePicker component
ui/app/src/components/FormsEngine/controls/ImagePicker.tsx
Removed local image-size helpers and now imports/uses shared validateImageRestrictions.
Image editor dialog actions
ui/app/src/state/actions/dialogs.ts
Added showImageEditorDialog and imageEdited action creators and associated typings.
Preview concierge dialog handling
ui/app/src/components/PreviewConcierge/PreviewConcierge.tsx
Added handling for showImageEditorDialog to open ImageEditorDialog, localize messages, and dispatch imageEdited on crop.
Content upload service
ui/app/src/services/content.ts
createFileUpload now prefers file.blob when available, falling back to dataUrl -> Blob conversion.
Guest host message handling
ui/guest/src/react/ExperienceBuilder.tsx
Whitelisted imageEdited action in host-message handling so edited-image events are dispatched.
Guest epics & upload flows
ui/guest/src/store/epics/root.ts
Refactored drag/place and desktop-upload epics to call validateImageRestrictions; when validation fails open showImageEditorDialog, otherwise proceed. Added an imageEdited epic that validates policy, uploads edited blob, and emits progress/complete/failure actions.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant EB as Experience Builder
    participant Epic as Guest Epic
    participant Validate as validateImageRestrictions
    participant Dialog as ImageEditorDialog
    participant Policy as Policy Validator
    participant Upload as Upload Service

    User->>EB: Drag/place or upload image
    EB->>Epic: dispatch asset action
    Epic->>Validate: validateImageRestrictions(path or dataUrl, restrictions)
    alt Validation fails
        Validate-->>Epic: false
        Epic->>EB: dispatch showImageEditorDialog(...)
        EB->>Dialog: open editor with restrictions
        User->>Dialog: crop/edit -> returns blob
        Dialog-->>EB: dispatch imageEdited(blob, path)
        EB->>Epic: imageEdited action
        Epic->>Policy: validate action policy
        alt Policy allowed
            Policy-->>Epic: allowed
            Epic->>Upload: upload edited blob
            Upload-->>Epic: progress/complete
            Epic-->>EB: desktopAssetUploadComplete
        else Policy denied
            Policy-->>Epic: not allowed
            Epic-->>EB: desktopAssetUploadFailed
        end
    else Validation passes
        Validate-->>Epic: true
        Epic->>Upload: upload original asset
        Upload-->>Epic: progress/complete
        Epic-->>EB: desktopAssetUploadComplete
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • rart
  • sumerjabri
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description only contains a GitHub issue link with minimal additional context; required template sections are not filled out. Complete the description by filling out the 'Ticket reference or full description' section with details about what was changed and why.
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly references the issue number and describes the specific bug being fixed (image cropper dialog not opening in XB).
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

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 and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ui/app/src/components/PreviewConcierge/PreviewConcierge.tsx`:
- Around line 1133-1165: Add an onClose/cancel path to the ImageEditorDialog
push so cancelling the editor clears the dialog and notifies the guest to stop
the pending upload; specifically, in the showImageEditorDialog.type branch
(where pushDialog and createComponentId('ImageEditorDialog') are used and onCrop
currently sends hostToGuest$.next with imageEdited.type and then
dispatch(popDialog({ id }))), add an onClose/onCancel prop that dispatches
popDialog({ id }) and sends a hostToGuest$.next message (e.g.,
imageEditCancelled.type or a desktopAssetUploadFailed/ended message) with the
original fileName/recordId/uploadPath so the guest side can clean up the started
upload/drag state. Ensure the new message type aligns with the guest epic's
expected cancellation/failure event.

In `@ui/app/src/state/actions/dialogs.ts`:
- Around line 395-397: Update the action payload typings to match actual usage:
widen showImageEditorDialog (created by createAction in the symbol
showImageEditorDialog) from Partial<ImageEditorDialogBaseProps> to include
fileName, recordId, and uploadPath (or replace with a new interface that extends
ImageEditorDialogBaseProps plus these fields) so callers in the guest epic no
longer produce excess property errors; likewise broaden imageEdited (created by
createAction in the symbol imageEdited) from { blob: Blob; path?: string | null
} to include newPath, fileName, recordId, and uploadPath (or define and use a
richer ImageEditedPayload interface) so the host/guest flow and the
destructuring in PreviewConcierge.tsx align with the declared contract.

In `@ui/guest/src/store/epics/root.ts`:
- Around line 293-302: Remove the premature desktopAssetUploadStarted emission
from the branches that call showImageEditorDialog (the block posting
showImageEditorDialog with path/restrictions/fileName/recordId/uploadPath) and
instead dispatch the action that ends the drag state (the existing
desktop-drag-end action used elsewhere in this file) so the UI exits drag mode
when the editor opens; then add desktopAssetUploadStarted({ record }) into the
imageEdited epic's upload path so the upload-start is emitted only when the user
actually commits the crop.
- Around line 538-541: The policy validation is constructing the target path
using blob.name but blob is a Blob (no .name) causing `/undefined`; update the
call to validateActionPolicy to use the carried imageFileName from the action
payload instead of blob.name (i.e. target:
ensureSingleSlash(`${path}/${imageFileName}`)), keeping state.activeSite and
ensureSingleSlash as-is so the policy target is correct.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 4b653b4a-75cb-4bba-a58d-1b413f55d4f4

📥 Commits

Reviewing files that changed from the base of the PR and between 5c8cbd6 and 9308684.

📒 Files selected for processing (7)
  • ui/app/src/components/FormsEngine/controls/ImagePicker.tsx
  • ui/app/src/components/PreviewConcierge/PreviewConcierge.tsx
  • ui/app/src/services/content.ts
  • ui/app/src/state/actions/dialogs.ts
  • ui/app/src/utils/content.ts
  • ui/guest/src/react/ExperienceBuilder.tsx
  • ui/guest/src/store/epics/root.ts

Comment on lines +1133 to +1165
case showImageEditorDialog.type: {
const id = nanoid();
const { path, restrictions, writeContent, fileName, recordId, uploadPath } = action.payload;
const imageRestrictionMessages = getImageRestrictionMessages(restrictions);
dispatch(
pushDialog({
id,
component: createComponentId('ImageEditorDialog'),
props: {
path,
subtitle: (
<FormattedMessage
defaultMessage="The image does not meet the width & height constraints (Width: {width}. Height: {height})."
values={{
width: imageRestrictionMessages.width,
height: imageRestrictionMessages.height
}}
/>
),
restrictions,
writeContent,
onCrop: (blob: Blob, newPath: string) => {
dispatch(popDialog({ id }));
hostToGuest$.next({
type: imageEdited.type,
payload: { blob, newPath, fileName, recordId, uploadPath }
});
}
}
})
);
break;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Handle editor cancellation so XB doesn’t stay in upload state.

The guest epic starts the desktop upload flow before opening this dialog, but this handler only notifies the guest from onCrop. If the user closes/cancels the editor, no cleanup action is sent back, leaving the guest-side upload/drag state active.

Add an onClose/cancel path that pops the dialog and notifies the guest to end or fail the pending upload, or defer desktopAssetUploadStarted until imageEdited is received.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/app/src/components/PreviewConcierge/PreviewConcierge.tsx` around lines
1133 - 1165, Add an onClose/cancel path to the ImageEditorDialog push so
cancelling the editor clears the dialog and notifies the guest to stop the
pending upload; specifically, in the showImageEditorDialog.type branch (where
pushDialog and createComponentId('ImageEditorDialog') are used and onCrop
currently sends hostToGuest$.next with imageEdited.type and then
dispatch(popDialog({ id }))), add an onClose/onCancel prop that dispatches
popDialog({ id }) and sends a hostToGuest$.next message (e.g.,
imageEditCancelled.type or a desktopAssetUploadFailed/ended message) with the
original fileName/recordId/uploadPath so the guest side can clean up the started
upload/drag state. Ensure the new message type aligns with the guest epic's
expected cancellation/failure event.

Comment on lines +395 to +397
export const showImageEditorDialog =
/*#__PURE__*/ createAction<Partial<ImageEditorDialogBaseProps>>('SHOW_IMAGE_EDITOR_DIALOG');
export const imageEdited = /*#__PURE__*/ createAction<{ blob: Blob; path?: string | null }>('IMAGE_EDITED');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Inspect image-editor action payload producers/consumers for fields that must be represented in the action types.

rg -n -C3 'showImageEditorDialog\(|imageEdited\(|type:\s*imageEdited\.type|payload:\s*\{\s*blob' --iglob '*.ts' --iglob '*.tsx'

Repository: craftercms/studio-ui

Length of output: 1812


🏁 Script executed:

# Get the full showImageEditorDialog call at line 294
sed -n '290,310p' ui/guest/src/store/epics/root.ts

Repository: craftercms/studio-ui

Length of output: 685


🏁 Script executed:

# Get the full showImageEditorDialog call at line 407
sed -n '403,420p' ui/guest/src/store/epics/root.ts

Repository: craftercms/studio-ui

Length of output: 711


🏁 Script executed:

# Get the full imageEdited call and context
sed -n '1150,1165p' ui/app/src/components/PreviewConcierge/PreviewConcierge.tsx

Repository: craftercms/studio-ui

Length of output: 418


🏁 Script executed:

# Find the definition of ImageEditorDialogBaseProps
rg -n 'interface ImageEditorDialogBaseProps|type ImageEditorDialogBaseProps' --iglob '*.ts' --iglob '*.tsx'

Repository: craftercms/studio-ui

Length of output: 161


🏁 Script executed:

# Check the actual action declaration in dialogs.ts around line 395-397
sed -n '390,405p' ui/app/src/state/actions/dialogs.ts

Repository: craftercms/studio-ui

Length of output: 468


🏁 Script executed:

# Get the full definition of ImageEditorDialogBaseProps
sed -n '20,50p' ui/app/src/components/ImageEditorDialog/types.ts

Repository: craftercms/studio-ui

Length of output: 615


🏁 Script executed:

# Check if there's a tsconfig or if TypeScript is configured to allow excess properties
cd ui/app && cat tsconfig.json 2>/dev/null | head -30

Repository: craftercms/studio-ui

Length of output: 642


🏁 Script executed:

# Check the guest app tsconfig
cd ui/guest && cat tsconfig.json 2>/dev/null | head -30

Repository: craftercms/studio-ui

Length of output: 926


🏁 Script executed:

# Look at the actual usage context in PreviewConcierge to see where fileName, recordId, uploadPath come from
sed -n '1100,1170p' ui/app/src/components/PreviewConcierge/PreviewConcierge.tsx

Repository: craftercms/studio-ui

Length of output: 2589


Widen the image-editor action payload contracts.

showImageEditorDialog is typed as Partial<ImageEditorDialogBaseProps>, but both call sites in the guest epic pass fileName, recordId, and uploadPath—fields not defined in ImageEditorDialogBaseProps. With TypeScript strict mode enabled ("strict": true), this triggers excess property errors. Similarly, imageEdited is declared as { blob: Blob; path?: string | null }, while the host/guest flow emits it with newPath, fileName, recordId, and uploadPath. The handler in PreviewConcierge.tsx (line 1131) explicitly destructures these extra fields from the payload, confirming they are intentional contract properties.

🔧 Proposed type alignment
+export interface ShowImageEditorDialogPayload extends Partial<ImageEditorDialogBaseProps> {
+	fileName?: string;
+	recordId?: number;
+	uploadPath?: string;
+}
+
+export interface ImageEditedPayload {
+	blob: Blob;
+	path?: string | null;
+	newPath?: string;
+	fileName?: string;
+	recordId?: number;
+	uploadPath?: string;
+}
+
 // region showImageEditorDialog
 export const showImageEditorDialog =
-	/*#__PURE__*/ createAction<Partial<ImageEditorDialogBaseProps>>('SHOW_IMAGE_EDITOR_DIALOG');
-export const imageEdited = /*#__PURE__*/ createAction<{ blob: Blob; path?: string | null }>('IMAGE_EDITED');
+	/*#__PURE__*/ createAction<ShowImageEditorDialogPayload>('SHOW_IMAGE_EDITOR_DIALOG');
+export const imageEdited = /*#__PURE__*/ createAction<ImageEditedPayload>('IMAGE_EDITED');
 // endregion
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const showImageEditorDialog =
/*#__PURE__*/ createAction<Partial<ImageEditorDialogBaseProps>>('SHOW_IMAGE_EDITOR_DIALOG');
export const imageEdited = /*#__PURE__*/ createAction<{ blob: Blob; path?: string | null }>('IMAGE_EDITED');
export interface ShowImageEditorDialogPayload extends Partial<ImageEditorDialogBaseProps> {
fileName?: string;
recordId?: number;
uploadPath?: string;
}
export interface ImageEditedPayload {
blob: Blob;
path?: string | null;
newPath?: string;
fileName?: string;
recordId?: number;
uploadPath?: string;
}
// region showImageEditorDialog
export const showImageEditorDialog =
/*#__PURE__*/ createAction<ShowImageEditorDialogPayload>('SHOW_IMAGE_EDITOR_DIALOG');
export const imageEdited = /*#__PURE__*/ createAction<ImageEditedPayload>('IMAGE_EDITED');
// endregion
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/app/src/state/actions/dialogs.ts` around lines 395 - 397, Update the
action payload typings to match actual usage: widen showImageEditorDialog
(created by createAction in the symbol showImageEditorDialog) from
Partial<ImageEditorDialogBaseProps> to include fileName, recordId, and
uploadPath (or replace with a new interface that extends
ImageEditorDialogBaseProps plus these fields) so callers in the guest epic no
longer produce excess property errors; likewise broaden imageEdited (created by
createAction in the symbol imageEdited) from { blob: Blob; path?: string | null
} to include newPath, fileName, recordId, and uploadPath (or define and use a
richer ImageEditedPayload interface) so the host/guest flow and the
destructuring in PreviewConcierge.tsx align with the declared contract.

Comment on lines +293 to +302
post(
showImageEditorDialog({
path,
restrictions,
fileName: name,
recordId: record.id,
uploadPath
})
);
return of(desktopAssetUploadStarted({ record }));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don’t mark the upload as started before the crop is committed.

Both restriction-failure paths open the editor and immediately emit desktopAssetUploadStarted. If the dialog is closed without cropping, no follow-up imageEdited arrives and the guest can remain in an active upload state. Prefer ending the drag state here and emitting desktopAssetUploadStarted from the imageEdited epic when the upload actually begins.

🔧 Suggested flow adjustment
 														post(
 															showImageEditorDialog({
 																path,
 																restrictions,
 																fileName: name,
 																recordId: record.id,
 																uploadPath
 															})
 														);
-														return of(desktopAssetUploadStarted({ record }));
+														return of(desktopAssetDragEnded());
 																post(
 																	showImageEditorDialog({
 																		path: url,
 																		restrictions,
 																		fileName: file.name,
 																		recordId: record.id,
 																		uploadPath: path
 																	})
 																);
 
-																return of(desktopAssetUploadStarted({ record }));
+																return of(desktopAssetDragEnded());

Then add desktopAssetUploadStarted({ record }) to the imageEdited upload flow when the user actually crops.

Also applies to: 405-416

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/guest/src/store/epics/root.ts` around lines 293 - 302, Remove the
premature desktopAssetUploadStarted emission from the branches that call
showImageEditorDialog (the block posting showImageEditorDialog with
path/restrictions/fileName/recordId/uploadPath) and instead dispatch the action
that ends the drag state (the existing desktop-drag-end action used elsewhere in
this file) so the UI exits drag mode when the editor opens; then add
desktopAssetUploadStarted({ record }) into the imageEdited epic's upload path so
the upload-start is emitted only when the user actually commits the crop.

Comment on lines +538 to +541
validateActionPolicy(state.activeSite, {
type: 'CREATE',
target: ensureSingleSlash(`${path}/${blob.name}`),
contentMetadata: {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Inspect Blob/File name usage around image editing and uploads.

rg -n -C3 '\bblob\.name\b|imageFileName|new File\(|new Blob\(|onCrop' --iglob '*.ts' --iglob '*.tsx'

Repository: craftercms/studio-ui

Length of output: 10949


Use the carried filename for policy validation.

blob is typed as Blob, which has no .name property. This creates a policy target ending in /undefined; use the imageFileName already destructured from the action payload.

🐛 Proposed fix
 						validateActionPolicy(state.activeSite, {
 							type: 'CREATE',
-							target: ensureSingleSlash(`${path}/${blob.name}`),
+							target: ensureSingleSlash(`${path}/${imageFileName}`),
 							contentMetadata: {
 								fileSize: blob.size
 							}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
validateActionPolicy(state.activeSite, {
type: 'CREATE',
target: ensureSingleSlash(`${path}/${blob.name}`),
contentMetadata: {
validateActionPolicy(state.activeSite, {
type: 'CREATE',
target: ensureSingleSlash(`${path}/${imageFileName}`),
contentMetadata: {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/guest/src/store/epics/root.ts` around lines 538 - 541, The policy
validation is constructing the target path using blob.name but blob is a Blob
(no .name) causing `/undefined`; update the call to validateActionPolicy to use
the carried imageFileName from the action payload instead of blob.name (i.e.
target: ensureSingleSlash(`${path}/${imageFileName}`)), keeping state.activeSite
and ensureSingleSlash as-is so the policy target is correct.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ui/app/src/utils/content.ts`:
- Around line 1304-1307: The current validateImageRestrictions function bypasses
validation when isImage(path) returns false, but isImage only checks file
extensions so blob: and data:image/ URLs (created by URL.createObjectURL and
FileReader.readAsDataURL) slip through; update validateImageRestrictions to
treat blob: and data:image/* URLs as images by checking path.startsWith('blob:')
or path.startsWith('data:image/') (or extend isImage to include those schemes),
and only skip validation for non-image schemes; ensure the dimension/crop checks
still run for blob/data URLs when restrictions are provided.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2fafcfab-76f3-4ecb-8ce3-daa48bd39bf4

📥 Commits

Reviewing files that changed from the base of the PR and between 9308684 and 92e4620.

📒 Files selected for processing (1)
  • ui/app/src/utils/content.ts

Comment on lines +1304 to +1307
export function validateImageRestrictions(path: string, restrictions?: ImageRestrictions): Promise<boolean> {
if (!isImage(path)) {
return Promise.resolve(true);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Inspect validateImageRestrictions call sites and nearby object URL creation.
# Expected: If any call site passes URL.createObjectURL(...) or blob/data URLs, the current extension-only guard bypasses validation.

rg -n -C4 '\bvalidateImageRestrictions\s*\(' --type=ts --type=tsx
rg -n -C4 '\bURL\.createObjectURL\s*\(' --type=ts --type=tsx
rg -n -C4 '\bblob:|data:image/' --type=ts --type=tsx

Repository: craftercms/studio-ui

Length of output: 147


🏁 Script executed:

#!/bin/bash
# Re-run with glob patterns instead of rg file type flags
echo "=== Call sites of validateImageRestrictions ===" 
rg -n -C4 'validateImageRestrictions\s*\(' -- '*.ts' '*.tsx'

echo "=== URL.createObjectURL usage ===" 
rg -n -C4 'URL\.createObjectURL' -- '*.ts' '*.tsx'

echo "=== blob: or data:image/ in code ===" 
rg -n 'blob:|data:image/' -- '*.ts' '*.tsx'

Repository: craftercms/studio-ui

Length of output: 472


🏁 Script executed:

#!/bin/bash
# Use -g flag with quoted glob patterns for rg
echo "=== Call sites of validateImageRestrictions ===" 
rg -n -C4 'validateImageRestrictions\s*\(' -g '*.ts' -g '*.tsx'

echo ""
echo "=== URL.createObjectURL usage ===" 
rg -n -C4 'URL\.createObjectURL' -g '*.ts' -g '*.tsx'

echo ""
echo "=== blob: or data:image/ in code ===" 
rg -n 'blob:|data:image/' -g '*.ts' -g '*.tsx'

Repository: craftercms/studio-ui

Length of output: 11229


Don't bypass validation for blob and data image URLs.

Line 1305 treats blob: and data:image/ URLs as non-images because isImage(path) only checks file extensions. This affects at least two flows:

  • ImagePicker file upload (ImagePicker.tsx:213): passes URL.createObjectURL(blob), which bypasses dimension checks
  • Guest editor FileReader (root.ts:402): passes FileReader.readAsDataURL() result, which also bypasses checks

Oversized images can skip the cropper dialog entirely. Add blob and data URL checks alongside the extension check.

Proposed fix
 export function validateImageRestrictions(path: string, restrictions?: ImageRestrictions): Promise<boolean> {
-	if (!isImage(path)) {
+	if (!restrictions || (!isImage(path) && !isBlobUrl(path) && !path.startsWith('data:image/'))) {
 		return Promise.resolve(true);
 	}
 	return new Promise((resolve) => {
-		if (restrictions) {
-			const img = new window.Image();
-			const done = (result: boolean) => resolve(result);
-			const timeout = window.setTimeout(() => done(true), 5000);
-			img.onload = () => {
-				window.clearTimeout(timeout);
-				done(doesImageMeetSizeRestrictions(img, restrictions));
-			};
-			img.onerror = img.onabort = () => {
-				window.clearTimeout(timeout);
-				done(true);
-			};
-			img.src = path;
-		} else {
-			resolve(true);
-		}
+		const img = new window.Image();
+		const done = (result: boolean) => resolve(result);
+		const timeout = window.setTimeout(() => done(true), 5000);
+		img.onload = () => {
+			window.clearTimeout(timeout);
+			done(doesImageMeetSizeRestrictions(img, restrictions));
+		};
+		img.onerror = img.onabort = () => {
+			window.clearTimeout(timeout);
+			done(true);
+		};
+		img.src = path;
 	});
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function validateImageRestrictions(path: string, restrictions?: ImageRestrictions): Promise<boolean> {
if (!isImage(path)) {
return Promise.resolve(true);
}
export function validateImageRestrictions(path: string, restrictions?: ImageRestrictions): Promise<boolean> {
if (!restrictions || (!isImage(path) && !isBlobUrl(path) && !path.startsWith('data:image/'))) {
return Promise.resolve(true);
}
return new Promise((resolve) => {
const img = new window.Image();
const done = (result: boolean) => resolve(result);
const timeout = window.setTimeout(() => done(true), 5000);
img.onload = () => {
window.clearTimeout(timeout);
done(doesImageMeetSizeRestrictions(img, restrictions));
};
img.onerror = img.onabort = () => {
window.clearTimeout(timeout);
done(true);
};
img.src = path;
});
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/app/src/utils/content.ts` around lines 1304 - 1307, The current
validateImageRestrictions function bypasses validation when isImage(path)
returns false, but isImage only checks file extensions so blob: and data:image/
URLs (created by URL.createObjectURL and FileReader.readAsDataURL) slip through;
update validateImageRestrictions to treat blob: and data:image/* URLs as images
by checking path.startsWith('blob:') or path.startsWith('data:image/') (or
extend isImage to include those schemes), and only skip validation for non-image
schemes; ensure the dimension/crop checks still run for blob/data URLs when
restrictions are provided.

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