Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion packages/core/src/asset_manager/view/FileUploader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,26 @@ import html from '../../utils/html';
import { AssetManagerConfig } from '../config/config';
import { UploadFileClb, UploadFileOptions } from '../types';

/**
* Check if a file matches an `accept` attribute value.
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#accept
* Allows everything when empty or a bare wildcard; otherwise supports MIME
* wildcards (`image/*`), exact MIME (`image/png`), and extensions (`.png`).
*/
export function isFileAccepted(file: File, accept?: string | null): boolean {
const acc = (accept || '').trim();
if (!acc || acc === '*' || acc === '*/*') return true;
const type = (file.type || '').toLowerCase();
const name = (file.name || '').toLowerCase();
return acc.split(',').some((raw) => {
const token = raw.trim().toLowerCase();
if (!token) return false;
if (token.startsWith('.')) return name.endsWith(token); // extension
if (token.endsWith('/*')) return type.startsWith(token.slice(0, -1)); // e.g. "image/"
return type === token; // exact MIME
});
}

type FileUploaderTemplateProps = {
pfx: string;
title: string;
Expand Down Expand Up @@ -141,7 +161,14 @@ export default class FileUploaderView extends View {
* */
uploadFile(e: DragEvent, clb?: UploadFileClb, opts?: UploadFileOptions) {
opts; // Options are not used here but can be used by the custom uploadFile function
const files = e.dataTransfer ? e.dataTransfer.files : ((e.target as any)?.files as FileList);
const allFiles = e.dataTransfer ? e.dataTransfer.files : ((e.target as any)?.files as FileList);
// #6032: the `accept` attribute is not enforced by browsers on drag-drop, so filter here.
const accept = this.$el.find('input[type=file]').attr('accept');
const files = Array.prototype.filter.call(allFiles || [], (f: File) => isFileAccepted(f, accept)) as File[];

// All dropped files rejected (e.g. a video on an image upload) -> do nothing.
if (allFiles && allFiles.length && !files.length) return;

const { config } = this;
const { beforeUpload } = config;

Expand Down
51 changes: 50 additions & 1 deletion packages/core/test/specs/asset_manager/view/FileUploader.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,26 @@
import FileUploader from '../../../../src/asset_manager/view/FileUploader';
import FileUploader, { isFileAccepted } from '../../../../src/asset_manager/view/FileUploader';

const f = (type: string, name = 'x') => ({ type, name }) as File;

describe('isFileAccepted (#6032)', () => {
test('allows everything when accept is empty or */*', () => {
expect(isFileAccepted(f('video/mp4', 'a.mp4'), '')).toBe(true);
expect(isFileAccepted(f('video/mp4', 'a.mp4'), '*/*')).toBe(true);
expect(isFileAccepted(f('video/mp4', 'a.mp4'), undefined)).toBe(true);
});
test('matches MIME wildcard (image/*)', () => {
expect(isFileAccepted(f('image/png', 'a.png'), 'image/*')).toBe(true);
expect(isFileAccepted(f('video/mp4', 'a.mp4'), 'image/*')).toBe(false);
});
test('matches exact MIME and comma lists', () => {
expect(isFileAccepted(f('image/png'), 'image/png,image/jpeg')).toBe(true);
expect(isFileAccepted(f('image/gif'), 'image/png,image/jpeg')).toBe(false);
});
test('matches file extensions', () => {
expect(isFileAccepted(f('', 'photo.PNG'), '.png')).toBe(true);
expect(isFileAccepted(f('', 'clip.mp4'), '.png,.jpg')).toBe(false);
});
});

describe('File Uploader', () => {
let obj: FileUploader;
Expand Down Expand Up @@ -72,4 +94,31 @@ describe('File Uploader', () => {
expect(view.uploadFile).toEqual(FileUploader.embedAsBase64);
});
});

describe('Drag-drop respects accept (#6032)', () => {
const makeView = () => {
const customFetch = jest.fn(() => Promise.resolve('[]'));
// headers:{} is REQUIRED: direct construction does NOT merge the module's config
// defaults, and uploadFile reads `config.headers` before fetching - a missing
// headers object throws before customFetch is ever reached.
const view = new FileUploader({ config: { upload: 'http://localhost/up', headers: {}, customFetch } });
document.body.innerHTML = '<div id="fx"></div>';
document.body.querySelector('#fx')!.appendChild(view.render().el);
view.$el.find('input[type=file]').attr('accept', 'image/*'); // what OpenAssets sets for images
return { view, customFetch };
};
const drop = (view: FileUploader, file: File) =>
view.uploadFile({ dataTransfer: { files: [file] }, preventDefault() {} } as any);

test('rejects a dropped video (no upload triggered)', () => {
const { view, customFetch } = makeView();
drop(view, new File(['x'], 'clip.mp4', { type: 'video/mp4' }));
expect(customFetch).not.toHaveBeenCalled();
});
test('still uploads a dropped image', () => {
const { view, customFetch } = makeView();
drop(view, new File(['x'], 'pic.png', { type: 'image/png' }));
expect(customFetch).toHaveBeenCalledTimes(1);
});
});
});
Loading