[MM-54486] Copy pasting images from chrome fails (#24718)

This commit is contained in:
M-ZubairAhmed 2023-10-21 05:29:32 +05:30 committed by GitHub
parent 2f5ca43158
commit 7caa5bce25
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 199 additions and 51 deletions

View File

@ -8,8 +8,7 @@ import type {FileInfo} from '@mattermost/types/files';
import {General} from 'mattermost-redux/constants';
import FileUpload from 'components/file_upload/file_upload';
import type {FileUpload as FileUploadClass} from 'components/file_upload/file_upload';
import FileUpload, {type FileUpload as FileUploadClass} from 'components/file_upload/file_upload';
import {shallowWithIntl} from 'tests/helpers/intl-test-helper';
import {clearFileInput} from 'utils/utils';
@ -232,7 +231,11 @@ describe('components/FileUpload', () => {
const event = new Event('paste');
event.preventDefault = jest.fn();
const getAsString = jest.fn();
(event as any).clipboardData = {items: [{getAsString, kind: 'string', type: 'text/plain'}], types: ['text/plain'], getData: () => {}};
(event as any).clipboardData = {items: [{getAsString, kind: 'string', type: 'text/plain'}],
types: ['text/plain'],
getData: () => {
return '';
}};
const wrapper = shallowWithIntl(
<FileUpload

View File

@ -24,6 +24,7 @@ import Constants from 'utils/constants';
import DelayedAction from 'utils/delayed_action';
import dragster from 'utils/dragster';
import {cmdOrCtrlPressed, isKeyPressed} from 'utils/keyboard';
import {hasPlainText, createFileFromClipboardDataItem} from 'utils/paste';
import {
isIosChrome,
isMobileApp,
@ -60,10 +61,6 @@ const holders = defineMessages({
id: 'file_upload.zeroBytesFile',
defaultMessage: 'You are uploading an empty file: {filename}',
},
pasted: {
id: 'file_upload.pasted',
defaultMessage: 'Image Pasted at ',
},
uploadFile: {
id: 'file_upload.upload_files',
defaultMessage: 'Upload files',
@ -447,10 +444,12 @@ export class FileUpload extends PureComponent<Props, State> {
containsEventTarget = (targetElement: HTMLInputElement | null, eventTarget: EventTarget | null) => targetElement && targetElement.contains(eventTarget as Node);
/**
* This paste handler sole responsibility is to detect if the clipboard data contains "files" and pass them to the upload file handler.
*/
pasteUpload = (e: ClipboardEvent) => {
const {formatMessage} = this.props.intl;
if (!e.clipboardData || !e.clipboardData.items || e.clipboardData.getData('text/html')) {
// If the clipboard data doesn't contain anything or it contains plain text, do nothing and let the browser and other handlers do their thing.
if (!e.clipboardData || !e.clipboardData.items || hasPlainText(e.clipboardData)) {
return;
}
@ -461,53 +460,28 @@ export class FileUpload extends PureComponent<Props, State> {
this.props.onUploadError(null);
const items = [];
for (let i = 0; i < e.clipboardData.items.length; i++) {
const item = e.clipboardData.items[i];
const fileClipboardItems = Array.
from(e.clipboardData.items).
filter((item) => item.kind === 'file');
if (item.kind !== 'file') {
continue;
}
items.push(item);
}
if (items && items.length > 0) {
if (fileClipboardItems.length > 0) {
if (!this.props.canUploadFiles) {
this.props.onUploadError(localizeMessage('file_upload.disabled', 'File attachments are disabled.'));
this.props.onUploadError(this.props.intl.formatMessage({id: 'file_upload.disabled', defaultMessage: 'File attachments are disabled.'}));
return;
}
e.preventDefault();
const fileNamePrefixIfNoName = this.props.intl.formatMessage({id: 'file_upload.pasted', defaultMessage: 'Image Pasted at '});
const files = [];
const fileList = fileClipboardItems.
map((fileClipboardItem) => createFileFromClipboardDataItem(fileClipboardItem, fileNamePrefixIfNoName)).
filter((file): file is NonNullable<typeof file> => file !== null);
for (let i = 0; i < items.length; i++) {
const file = items[i].getAsFile();
if (fileList.length > 0) {
// Prevent default will stop event propagation to other handlers such as those in advanced text editor
// so we do that here because we want to only paste the files from the clipboard and not other content.
e.preventDefault();
if (!file) {
continue;
}
const now = new Date();
const hour = now.getHours().toString().padStart(2, '0');
const minute = now.getMinutes().toString().padStart(2, '0');
let ext = '';
if (file.name && file.name.includes('.')) {
ext = file.name.substr(file.name.lastIndexOf('.'));
} else if (items[i].type.includes('/')) {
ext = '.' + items[i].type.split('/')[1].toLowerCase();
}
const name = file.name || formatMessage(holders.pasted) + now.getFullYear() + '-' + (now.getMonth() + 1) + '-' + now.getDate() + ' ' + hour + '-' + minute + ext;
const newFile: File = new File([file], name, {type: file.type});
files.push(newFile);
}
if (files.length > 0) {
this.checkPluginHooksAndUploadFiles(files);
this.checkPluginHooksAndUploadFiles(fileList);
this.props.onFileUploadChange();
}
}
@ -735,7 +709,6 @@ export class FileUpload extends PureComponent<Props, State> {
}
return (
<div className={uploadsRemaining <= 0 ? ' style--none btn-file__disabled' : 'style--none'}>
{bodyAction}
</div>

View File

@ -1,7 +1,16 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {parseHtmlTable, getHtmlTable, formatMarkdownMessage, formatGithubCodePaste, formatMarkdownLinkMessage, isTextUrl} from './paste';
import {
parseHtmlTable,
getHtmlTable,
formatMarkdownMessage,
formatGithubCodePaste,
formatMarkdownLinkMessage,
isTextUrl,
hasPlainText,
createFileFromClipboardDataItem,
} from './paste';
const validClipboardData: any = {
items: [1],
@ -221,3 +230,122 @@ describe('isTextUrl', () => {
expect(isTextUrl(clipboardData)).toBe(false);
});
});
describe('hasPlainText', () => {
test('Should return true when clipboard data has plain text', () => {
const clipboardData = {
...validClipboardData,
types: ['text/plain'],
getData: () => {
return 'plain text';
},
};
expect(hasPlainText(clipboardData)).toBe(true);
});
test('Should return true when clipboard data has plain text along with other types', () => {
const clipboardData = {
...validClipboardData,
types: ['text/html', 'text/plain'],
getData: () => {
return 'plain text';
},
};
expect(hasPlainText(clipboardData)).toBe(true);
});
test('Should return false when clipboard data has empty text', () => {
const clipboardData = {
...validClipboardData,
types: ['text/html', 'text/plain'],
getData: () => {
return '';
},
};
expect(hasPlainText(clipboardData)).toBe(false);
});
test('Should return false when clipboard data doesnt not have plain text type', () => {
const clipboardData = {
...validClipboardData,
types: ['text/html'],
getData: () => {
return 'plain text without type';
},
};
expect(hasPlainText(clipboardData)).toBe(false);
});
});
describe('createFileFromClipboardDataItem', () => {
test('should return a file from a clipboard item', () => {
const item = {
getAsFile: jest.fn(() => ({
name: 'test1.png',
type: 'image/png',
})),
type: 'image/png',
} as unknown as DataTransferItem;
const file = createFileFromClipboardDataItem(item, '') as File;
expect(file).toBeInstanceOf(File);
expect(file.name).toEqual('test1.png');
expect(file.type).toEqual('image/png');
});
test('should return null if getAsFile is not a file', () => {
const item = {
getAsFile: jest.fn(() => null),
} as unknown as DataTransferItem;
const file = createFileFromClipboardDataItem(item, '');
expect(file).toBeNull();
});
test('Should return correct file name when file name is not available', () => {
const item = {
getAsFile: jest.fn(() => ({
type: 'image/jpeg',
})),
type: 'image/jpeg',
} as unknown as DataTransferItem;
const now = new Date();
const file = createFileFromClipboardDataItem(item, 'pasted') as File;
expect(file).toBeInstanceOf(File);
expect(file.name).toBe(`pasted${now.getFullYear()}-${now.getMonth() + 1}-${now.getDate()} ${now.getHours().toString().padStart(2, '0')}-${now.getMinutes().toString().padStart(2, '0')}.jpeg`);
expect(file.type).toBe('image/jpeg');
});
test('Should return correct file extension when file name contains extension', () => {
const item = {
getAsFile: jest.fn(() => ({
name: 'test.jpeg',
})),
type: 'image/jpeg',
} as unknown as DataTransferItem;
const file = createFileFromClipboardDataItem(item, 'pasted') as File;
expect(file.name).toContain('.jpeg');
});
test('Should return correct file extension when file name doesnt contains extension', () => {
const item = {
getAsFile: jest.fn(() => ({
type: 'image/JPEG',
})),
type: 'image/jpeg',
} as unknown as DataTransferItem;
const file = createFileFromClipboardDataItem(item, 'pasted') as File;
expect(file.name).toContain('.jpeg');
});
});

View File

@ -10,6 +10,7 @@ export function parseHtmlTable(html: string): HTMLTableElement | null {
}
export function getHtmlTable(clipboardData: DataTransfer): HTMLTableElement | null {
// Check if clipboard data has html as one of its types
if (Array.from(clipboardData.types).indexOf('text/html') === -1) {
return null;
}
@ -42,6 +43,18 @@ export function isTextUrl(clipboardData: DataTransfer): boolean {
return clipboardText.startsWith('http://') || clipboardText.startsWith('https://');
}
/**
* Checks if the clipboard data contains plain text from list of types.
**/
export function hasPlainText(clipboardData: DataTransfer): boolean {
if (Array.from(clipboardData.types).includes('text/plain')) {
const clipboardText = clipboardData.getData('text/plain');
return clipboardText.trim().length > 0;
}
return false;
}
function isTableWithoutHeaderRow(table: HTMLTableElement): boolean {
return table.querySelectorAll('th').length === 0;
}
@ -140,3 +153,34 @@ export function formatMarkdownLinkMessage({message, clipboardData, selectionStar
const markdownLink = `[${selectedText}](${clipboardUrl})`;
return markdownLink;
}
export function createFileFromClipboardDataItem(item: DataTransferItem, fileNamePrefixIfNoName: string): File | null {
const file = item.getAsFile();
if (!file) {
return null;
}
let ext = '';
if (file.name && file.name.includes('.')) {
ext = file.name.slice(file.name.lastIndexOf('.'));
} else if (item.type.includes('/')) {
ext = '.' + item.type.slice(item.type.lastIndexOf('/') + 1).toLowerCase();
}
let name = '';
if (file.name) {
name = file.name;
} else {
const now = new Date();
const year = now.getFullYear();
const month = now.getMonth() + 1;
const date = now.getDate();
const hour = now.getHours().toString().padStart(2, '0');
const minute = now.getMinutes().toString().padStart(2, '0');
name = `${fileNamePrefixIfNoName}${year}-${month}-${date} ${hour}-${minute}${ext}`;
}
return new File([file as Blob], name, {type: file.type});
}