🐛 Fixed images sometimes being stored as data: URLs when copy/pasting from other editors (#16707)

refs https://github.com/TryGhost/Team/issues/2887

Images could sometimes be pasted into the editor (noticed especially with Google Docs) with `data:` URLs rather than typical `https:` URLs. That causes problems because data URLs are large binary blobs that get stored in the `posts` table and passed through many areas of the system that doesn't expect large binary blobs, causing knock-on effects.

- added handling to our editor's image card to detect when the card is displayed in the editor with a `data:` URL and if it was then it converts it to a file and uploads it so the image can be stored and displayed the same way as any other image
 - handles uploads on both paste and opening a post in the editor that was previously saved with a `data:` URL
This commit is contained in:
Kevin Ansfield 2023-04-25 15:58:07 +01:00 committed by GitHub
parent 1f643884af
commit 11cab899f0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -5,6 +5,7 @@ import {
IMAGE_MIME_TYPES
} from 'ghost-admin/components/gh-image-uploader';
import {action, computed, set, setProperties} from '@ember/object';
import {fetch} from 'fetch';
import {utils as ghostHelperUtils} from '@tryghost/helpers';
import {isEmpty} from '@ember/utils';
import {run} from '@ember/runloop';
@ -12,6 +13,30 @@ import {inject as service} from '@ember/service';
const {countWords} = ghostHelperUtils;
async function dataSrcToFile(src, fileName) {
if (!src.startsWith('data:')) {
return;
}
const mimeType = src.split(',')[0].split(':')[1].split(';')[0];
if (!fileName) {
let uuid;
try {
uuid = window.crypto.randomUUID();
} catch (e) {
uuid = Math.random().toString(36).substring(2, 15);
}
const extension = mimeType.split('/')[1];
fileName = `data-src-image-${uuid}.${extension}`;
}
const blob = await fetch(src).then(it => it.blob());
const file = new File([blob], fileName, {type: mimeType, lastModified: new Date()});
return file;
}
@classic
export default class KoenigCardImage extends Component {
@service ui;
@ -159,11 +184,21 @@ export default class KoenigCardImage extends Component {
didReceiveAttrs() {
super.didReceiveAttrs(...arguments);
// `payload.files` can be set if we have an externaly set image that
// if payload.src is a data attribute something has gone wrong and we're
// storing binary data in the payload. Grab the data and upload it to
// convert to a proper ULR
if (this.payload.src?.startsWith('data:')) {
const file = dataSrcToFile(this.payload.src, this.payload.fileName, this.payload.mimeType);
this.payload.files = [file];
}
// `payload.files` can be set if we have an externally set image that
// should be uploaded. Typical example would be from a paste or drag/drop
if (!isEmpty(this.payload.files)) {
run.schedule('afterRender', this, function () {
this.set('files', this.payload.files);
run.schedule('afterRender', this, async function () {
// files can be a promise if converted from data:
const files = await Promise.all(this.payload.files);
this.set('files', files);
// we don't want to persist any file data in the document
delete this.payload.files;