From 11cab899f0579dcf517fbeef9f26797e674c6437 Mon Sep 17 00:00:00 2001 From: Kevin Ansfield Date: Tue, 25 Apr 2023 15:58:07 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20Fixed=20images=20sometimes=20bei?= =?UTF-8?q?ng=20stored=20as=20`data:`=20URLs=20when=20copy/pasting=20from?= =?UTF-8?q?=20other=20editors=20(#16707)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../addon/components/koenig-card-image.js | 41 +++++++++++++++++-- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/ghost/admin/lib/koenig-editor/addon/components/koenig-card-image.js b/ghost/admin/lib/koenig-editor/addon/components/koenig-card-image.js index 4e9db8ede4..73da658672 100644 --- a/ghost/admin/lib/koenig-editor/addon/components/koenig-card-image.js +++ b/ghost/admin/lib/koenig-editor/addon/components/koenig-card-image.js @@ -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;