[8023] Image cropper dialog does not open when using XB#4843
[8023] Image cropper dialog does not open when using XB#4843jvega190 wants to merge 2 commits intocraftercms:developfrom
Conversation
WalkthroughImage 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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Suggested reviewers
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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
📒 Files selected for processing (7)
ui/app/src/components/FormsEngine/controls/ImagePicker.tsxui/app/src/components/PreviewConcierge/PreviewConcierge.tsxui/app/src/services/content.tsui/app/src/state/actions/dialogs.tsui/app/src/utils/content.tsui/guest/src/react/ExperienceBuilder.tsxui/guest/src/store/epics/root.ts
| 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; | ||
| } |
There was a problem hiding this comment.
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.
| export const showImageEditorDialog = | ||
| /*#__PURE__*/ createAction<Partial<ImageEditorDialogBaseProps>>('SHOW_IMAGE_EDITOR_DIALOG'); | ||
| export const imageEdited = /*#__PURE__*/ createAction<{ blob: Blob; path?: string | null }>('IMAGE_EDITED'); |
There was a problem hiding this comment.
🧩 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.tsRepository: 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.tsRepository: 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.tsxRepository: 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.tsRepository: 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.tsRepository: 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 -30Repository: craftercms/studio-ui
Length of output: 642
🏁 Script executed:
# Check the guest app tsconfig
cd ui/guest && cat tsconfig.json 2>/dev/null | head -30Repository: 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.tsxRepository: 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.
| 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.
| post( | ||
| showImageEditorDialog({ | ||
| path, | ||
| restrictions, | ||
| fileName: name, | ||
| recordId: record.id, | ||
| uploadPath | ||
| }) | ||
| ); | ||
| return of(desktopAssetUploadStarted({ record })); |
There was a problem hiding this comment.
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.
| validateActionPolicy(state.activeSite, { | ||
| type: 'CREATE', | ||
| target: ensureSingleSlash(`${path}/${blob.name}`), | ||
| contentMetadata: { |
There was a problem hiding this comment.
🧩 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.
| 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.
There was a problem hiding this comment.
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
📒 Files selected for processing (1)
ui/app/src/utils/content.ts
| export function validateImageRestrictions(path: string, restrictions?: ImageRestrictions): Promise<boolean> { | ||
| if (!isImage(path)) { | ||
| return Promise.resolve(true); | ||
| } |
There was a problem hiding this comment.
🧩 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=tsxRepository: 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.
| 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.
craftercms/craftercms#8023
Summary by CodeRabbit
New Features
Refactor