From 3bbce756c69a206d07abd31059f0392c26b36096 Mon Sep 17 00:00:00 2001 From: Benoit Socias Date: Wed, 24 Aug 2022 12:07:31 +0000 Subject: [PATCH] [IMP] website, web_editor, *: save dropped images as attachments *: mass_mailing When images are dropped from outside the browser into a website page text paragraph, they are stored as an inline base64-encoded source. This commit marks those images to be saved as attachments when it happens in website. The save happens upon page save similarly to what is done when images are transformed. This commit also applies this behavior for pasted images. task-2928495 closes odoo/odoo#98801 Signed-off-by: Romain Derie (rde) --- .../static/src/js/mass_mailing_html_field.js | 2 +- addons/web_editor/controllers/main.py | 8 +++++++ .../static/src/js/backend/field_html.js | 2 +- .../static/src/js/backend/html_field.js | 2 +- .../js/editor/odoo-editor/src/OdooEditor.js | 18 ++++++++++++-- .../static/src/js/wysiwyg/wysiwyg.js | 24 +++++++++++++++---- .../website/static/src/js/editor/wysiwyg.js | 1 + 7 files changed, 47 insertions(+), 10 deletions(-) diff --git a/addons/mass_mailing/static/src/js/mass_mailing_html_field.js b/addons/mass_mailing/static/src/js/mass_mailing_html_field.js index 16a3e1c978e..2653bda4ca1 100644 --- a/addons/mass_mailing/static/src/js/mass_mailing_html_field.js +++ b/addons/mass_mailing/static/src/js/mass_mailing_html_field.js @@ -99,7 +99,7 @@ export class MassMailingHtmlField extends HtmlField { const $editable = this.wysiwyg.getEditable(); const initialHtml = $editable.html(); await this.wysiwyg.cleanForSave(); - await this.wysiwyg.saveModifiedImages(this.$content); + await this.wysiwyg.savePendingImages(this.$content); await super.commitChanges(); diff --git a/addons/web_editor/controllers/main.py b/addons/web_editor/controllers/main.py index 7a973bd8ca3..d4ae7da9e26 100644 --- a/addons/web_editor/controllers/main.py +++ b/addons/web_editor/controllers/main.py @@ -6,12 +6,14 @@ import logging import re import time import requests +import uuid import werkzeug.exceptions import werkzeug.urls import werkzeug.wrappers from PIL import Image, ImageFont, ImageDraw from lxml import etree from base64 import b64decode, b64encode +from datetime import datetime from odoo.http import request from odoo import http, tools, _, SUPERUSER_ID @@ -208,6 +210,12 @@ class Web_Editor(http.Controller): mimetype = guess_mimetype(data) if mimetype not in SUPPORTED_IMAGE_MIMETYPES: return {'error': format_error_msg} + if not name: + name = '%s-%s%s' % ( + datetime.now().strftime('%Y%m%d%H%M%S'), + str(uuid.uuid4())[:6], + SUPPORTED_IMAGE_MIMETYPES[mimetype], + ) except UserError: # considered as an image by the browser file input, but not # recognized as such by PIL, eg .webp diff --git a/addons/web_editor/static/src/js/backend/field_html.js b/addons/web_editor/static/src/js/backend/field_html.js index c045ec984c4..86d84938710 100644 --- a/addons/web_editor/static/src/js/backend/field_html.js +++ b/addons/web_editor/static/src/js/backend/field_html.js @@ -145,7 +145,7 @@ var FieldHtml = basic_fields.DebouncedField.extend(TranslatableFieldMixin, { this._setValue(this._getValue()); this._isDirty = this.wysiwyg.isDirty(); await fullClean; - await this.wysiwyg.saveModifiedImages(this.$content); + await this.wysiwyg.savePendingImages(this.$content); // Update the value to the fully cleaned version. this._setValue(this._getValue()); _super(); diff --git a/addons/web_editor/static/src/js/backend/html_field.js b/addons/web_editor/static/src/js/backend/html_field.js index 381f07a872a..efcf9cee307 100644 --- a/addons/web_editor/static/src/js/backend/html_field.js +++ b/addons/web_editor/static/src/js/backend/html_field.js @@ -357,7 +357,7 @@ export class HtmlField extends Component { if (this.wysiwyg) { // Avoid listening to changes made during the _toInline process. this.wysiwyg.odooEditor.observerUnactive('commitChanges'); - await this.wysiwyg.saveModifiedImages(); + await this.wysiwyg.savePendingImages(); if (this.props.isInlineStyle) { await this._toInline(); } diff --git a/addons/web_editor/static/src/js/editor/odoo-editor/src/OdooEditor.js b/addons/web_editor/static/src/js/editor/odoo-editor/src/OdooEditor.js index 062cdd76500..7046846dcc9 100644 --- a/addons/web_editor/static/src/js/editor/odoo-editor/src/OdooEditor.js +++ b/addons/web_editor/static/src/js/editor/odoo-editor/src/OdooEditor.js @@ -4220,7 +4220,15 @@ export class OdooEditor extends EventTarget { this.execCommand('insert', fragment); } } else if (files.length && targetSupportsHtmlContent) { - this.addImagesFiles(files).then(html => this.execCommand('insert', this._prepareClipboardData(html))); + this.addImagesFiles(files).then(html => { + const imageNodes = this.execCommand('insert', this._prepareClipboardData(html)); + if (imageNodes && this.options.dropImageAsAttachment) { + // Mark images as having to be saved as attachments. + for (const imageNode of imageNodes) { + imageNode.classList.add('o_b64_image_to_save'); + } + } + }); } else if (clipboardHtml && targetSupportsHtmlContent) { this.execCommand('insert', this._prepareClipboardData(clipboardHtml)); } else { @@ -4438,7 +4446,13 @@ export class OdooEditor extends EventTarget { this.execCommand('insert', this._prepareClipboardData(html)); } else if (fileTransferItems.length) { this.addImagesFiles(fileTransferItems).then(html => { - this.execCommand('insert', this._prepareClipboardData(html)); + const imageNodes = this.execCommand('insert', this._prepareClipboardData(html)); + if (imageNodes && this.options.dropImageAsAttachment) { + // Mark images as having to be saved as attachments. + for (const imageNode of imageNodes) { + imageNode.classList.add('o_b64_image_to_save'); + } + } }); } else if (htmlTransferItem) { htmlTransferItem.getAsString(pastedText => { diff --git a/addons/web_editor/static/src/js/wysiwyg/wysiwyg.js b/addons/web_editor/static/src/js/wysiwyg/wysiwyg.js index 2a378e91d9d..171dc161046 100644 --- a/addons/web_editor/static/src/js/wysiwyg/wysiwyg.js +++ b/addons/web_editor/static/src/js/wysiwyg/wysiwyg.js @@ -212,6 +212,7 @@ const Wysiwyg = Widget.extend({ direction: options.direction || localization.direction || 'ltr', collaborationClientAvatarUrl: `${browser.location.origin}/web/image?model=res.users&field=avatar_128&id=${this.getSession().uid}`, renderingClasses: ['o_dirty', 'o_transform_removal', 'oe_edited_link'], + dropImageAsAttachment: options.dropImageAsAttachment, }, editorCollaborationOptions)); this.odooEditor.addEventListener('contentChanged', function () { @@ -899,7 +900,7 @@ const Wysiwyg = Widget.extend({ await this.cleanForSave(); const editables = this.options.getContentEditableAreas(); - await this.saveModifiedImages(editables.length ? $(editables) : this.$editable); + await this.savePendingImages(editables.length ? $(editables) : this.$editable); await this._saveViewBlocks(); window.removeEventListener('beforeunload', this._onBeforeUnload); @@ -935,15 +936,28 @@ const Wysiwyg = Widget.extend({ }); }, /** - * Create/Update cropped attachments. + * Create/Update attachments for unsaved images. + * (e.g. modified/cropped images, drag & dropped images, pasted images..) * * @param {jQuery} $editable * @returns {Promise} */ - saveModifiedImages: function ($editable = this.$editable) { + savePendingImages($editable = this.$editable) { const defs = _.map($editable, async editableEl => { const {oeModel: resModel, oeId: resId} = editableEl.dataset; - const proms = [...editableEl.querySelectorAll('.o_modified_image_to_save')].map(async el => { + const b64Proms = [...editableEl.querySelectorAll('.o_b64_image_to_save')].map(async el => { + const attachment = await this._rpc({ + route: '/web_editor/attachment/add_data', + params: { + name: el.dataset.fileName || '', + data: el.getAttribute('src').split(',')[1], + is_image: true, + }, + }); + el.setAttribute('src', attachment.image_src); + el.classList.remove('o_b64_image_to_save'); + }); + const modifiedProms = [...editableEl.querySelectorAll('.o_modified_image_to_save')].map(async el => { const isBackground = !el.matches('img'); el.classList.remove('o_modified_image_to_save'); // Modifying an image always creates a copy of the original, even if @@ -969,7 +983,7 @@ const Wysiwyg = Widget.extend({ el.setAttribute('src', newAttachmentSrc); } }); - return Promise.all(proms); + return Promise.all([...b64Proms, ...modifiedProms]); }); return Promise.all(defs); }, diff --git a/addons/website/static/src/js/editor/wysiwyg.js b/addons/website/static/src/js/editor/wysiwyg.js index cfc2664c133..e92b512a7ca 100644 --- a/addons/website/static/src/js/editor/wysiwyg.js +++ b/addons/website/static/src/js/editor/wysiwyg.js @@ -51,6 +51,7 @@ const WebsiteWysiwyg = Wysiwyg.extend({ this.options.toolbarHandler = $('#web_editor-top-edit'); // Do not insert a paragraph after each column added by the column commands: this.options.insertParagraphAfterColumns = false; + this.options.dropImageAsAttachment = true; const $editableWindow = this.$editable[0].ownerDocument.defaultView; // Dropdown menu initialization: handle dropdown openings by hand