From 1b4fdf17575852b936468a4d16a67be9cca2cbd3 Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Fri, 20 Sep 2019 12:51:06 +0100 Subject: [PATCH 1/6] Update FindAndPersistPastedTempImages method to deal with duplicate images in the same RTE to only upload it once --- .../Templates/TemplateUtilities.cs | 76 ++++++++++++------- 1 file changed, 48 insertions(+), 28 deletions(-) diff --git a/src/Umbraco.Web/Templates/TemplateUtilities.cs b/src/Umbraco.Web/Templates/TemplateUtilities.cs index edccbcc9d4..7134d088a6 100644 --- a/src/Umbraco.Web/Templates/TemplateUtilities.cs +++ b/src/Umbraco.Web/Templates/TemplateUtilities.cs @@ -1,5 +1,6 @@ using HtmlAgilityPack; using System; +using System.Collections.Generic; using System.IO; using System.Text.RegularExpressions; using Umbraco.Core; @@ -201,7 +202,11 @@ namespace Umbraco.Web.Templates var tmpImages = htmlDoc.DocumentNode.SelectNodes($"//img[@{TemporaryImageDataAttribute}]"); if (tmpImages == null || tmpImages.Count == 0) return html; - + + // An array to contain a list of URLs that + // we have already processed to avoid dupes + var uploadedImages = new Dictionary(); + foreach (var img in tmpImages) { // The data attribute contains the path to the tmp img to persist as a media item @@ -209,55 +214,70 @@ namespace Umbraco.Web.Templates if (string.IsNullOrEmpty(tmpImgPath)) continue; - + var absoluteTempImagePath = IOHelper.MapPath(tmpImgPath); var fileName = Path.GetFileName(absoluteTempImagePath); var safeFileName = fileName.ToSafeFileName(); var mediaItemName = safeFileName.ToFriendlyName(); IMedia mediaFile; + GuidUdi udi; - if(mediaParentFolder == Guid.Empty) - mediaFile = mediaService.CreateMedia(mediaItemName, Constants.System.Root, Constants.Conventions.MediaTypes.Image, userId); - else - mediaFile = mediaService.CreateMedia(mediaItemName, mediaParentFolder, Constants.Conventions.MediaTypes.Image, userId); - - var fileInfo = new FileInfo(absoluteTempImagePath); - - var fileStream = fileInfo.OpenReadWithRetry(); - if (fileStream == null) throw new InvalidOperationException("Could not acquire file stream"); - using (fileStream) + if (uploadedImages.ContainsKey(tmpImgPath) == false) { - mediaFile.SetValue(contentTypeBaseServiceProvider, Constants.Conventions.Media.File, safeFileName, fileStream); - } + if (mediaParentFolder == Guid.Empty) + mediaFile = mediaService.CreateMedia(mediaItemName, Constants.System.Root, Constants.Conventions.MediaTypes.Image, userId); + else + mediaFile = mediaService.CreateMedia(mediaItemName, mediaParentFolder, Constants.Conventions.MediaTypes.Image, userId); - mediaService.Save(mediaFile, userId); + var fileInfo = new FileInfo(absoluteTempImagePath); + + var fileStream = fileInfo.OpenReadWithRetry(); + if (fileStream == null) throw new InvalidOperationException("Could not acquire file stream"); + using (fileStream) + { + mediaFile.SetValue(contentTypeBaseServiceProvider, Constants.Conventions.Media.File, safeFileName, fileStream); + } + + mediaService.Save(mediaFile, userId); + + udi = mediaFile.GetUdi(); + } + else + { + // Already been uploaded & we have it's UDI + udi = uploadedImages[tmpImgPath]; + } // Add the UDI to the img element as new data attribute - var udi = mediaFile.GetUdi(); img.SetAttributeValue("data-udi", udi.ToString()); //Get the new persisted image url - var mediaTyped = Current.UmbracoHelper.Media(mediaFile.Id); + var mediaTyped = Current.UmbracoHelper.Media(udi.Guid); var location = mediaTyped.Url; img.SetAttributeValue("src", location); // Remove the data attribute (so we do not re-process this) img.Attributes.Remove(TemporaryImageDataAttribute); - // Delete folder & image now its saved in media - // The folder should contain one image - as a unique guid folder created - // for each image uploaded from TinyMceController - var folderName = Path.GetDirectoryName(absoluteTempImagePath); - try - { - Directory.Delete(folderName, true); - } - catch (Exception ex) + // Add to the dictionary to avoid dupes + if(uploadedImages.ContainsKey(tmpImgPath) == false) { - logger.Error(typeof(TemplateUtilities), ex, "Could not delete temp file or folder {FileName}", absoluteTempImagePath); + uploadedImages.Add(tmpImgPath, udi); + + // Delete folder & image now its saved in media + // The folder should contain one image - as a unique guid folder created + // for each image uploaded from TinyMceController + var folderName = Path.GetDirectoryName(absoluteTempImagePath); + try + { + Directory.Delete(folderName, true); + } + catch (Exception ex) + { + logger.Error(typeof(TemplateUtilities), ex, "Could not delete temp file or folder {FileName}", absoluteTempImagePath); + } } - } return htmlDoc.DocumentNode.OuterHtml; From 92d10db2aba14b7e67a1299df17203a12a2c608d Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Fri, 20 Sep 2019 12:52:21 +0100 Subject: [PATCH 2/6] Removing the code to remove the item from blobCache - fixes Shan's reported bug --- .../src/common/services/tinymce.service.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js b/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js index 370a15a455..5d93efe772 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js @@ -228,10 +228,6 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s // When its being persisted in RTE property editor // To create a media item & delete this tmp one etc tinymce.activeEditor.$(img).attr({ "data-tmpimg": tmpLocation }); - - // We need to remove the image from the cache, otherwise we can't handle if we upload the exactly - // same image twice - tinymce.activeEditor.editorUpload.blobCache.removeByUri(imgSrc); }); }); } From fdfaf0671752b6bd991897a4820c5810b11f5b6e Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Fri, 20 Sep 2019 12:53:30 +0100 Subject: [PATCH 3/6] By removing the blobCache code - reintroduced a bug where if the same image was put into the RTE the data-tmpimg attribute is not applied to the duplicate image which this fixes --- .../src/common/services/tinymce.service.js | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js b/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js index 5d93efe772..2030cf12b8 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js @@ -230,6 +230,26 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s tinymce.activeEditor.$(img).attr({ "data-tmpimg": tmpLocation }); }); }); + + // Get all img where src starts with blob: AND does NOT have a data=tmpimg attribute + // This is most likely seen as a duplicate image that has already been uploaded + // editor.uploadImages() does not give us any indiciation that the image been uploaded already + var blobImageWithNoTmpImgAttribute = editor.dom.select("img[src^='blob:']:not([data-tmpimg])"); + + //For each of these selected items + blobImageWithNoTmpImgAttribute.forEach(imageElement => { + var blobSrcUri = editor.dom.getAttrib(imageElement, "src"); + + // Select/find the same image uploaded with the matching SRC uri + var uploadedImage = editor.dom.select(`img[src="${blobSrcUri}"][data-tmpimg]`); + + if(uploadedImage){ + // Get the value of the tmpimg - so we can apply it to the matched/duplicate image + var dataTmpImg = editor.dom.getAttrib(uploadedImage, "data-tmpimg"); + editor.dom.setAttrib(imageElement, "data-tmpimg", dataTmpImg); + } + }); + } }); } From 7728559ce47c9931d0888c5a0af88444d4a9824b Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Tue, 24 Sep 2019 11:47:33 +0100 Subject: [PATCH 4/6] Rather than tryinbg to find off an inserted img in the DOM use the localStorage Uses the Angular localStorage lib that falls back to cookies as opposed to native browser localStorage Clears out tinymce__ localstorage keys when we save the node to ensure we dont have loads stored for the user --- .../components/content/edit.controller.js | 9 ++++++++- .../src/common/services/tinymce.service.js | 19 ++++++++++--------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js index a7d02cc2ab..aa01df35dc 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js @@ -4,7 +4,7 @@ function ContentEditController($rootScope, $scope, $routeParams, $q, $window, appState, contentResource, entityResource, navigationService, notificationsService, serverValidationManager, contentEditingHelper, localizationService, formHelper, umbRequestHelper, - editorState, $http, eventsService, overlayService, $location) { + editorState, $http, eventsService, overlayService, $location, localStorageService) { var evts = []; var infiniteMode = $scope.infiniteModel && $scope.infiniteModel.infiniteMode; @@ -189,6 +189,13 @@ $scope.page.saveButtonState = "success"; $scope.page.buttonGroupState = "success"; })); + + evts.push(eventsService.on("content.saved", function(){ + // Clear out localstorage keys that start with tinymce__ + // When we save/perist a content node + // NOTE: clearAll supports a RegEx pattern of items to remove + localStorageService.clearAll(/^tinymce__/); + })); } /** diff --git a/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js b/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js index cd85dba029..5a8041191e 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js @@ -7,7 +7,7 @@ * A service containing all logic for all of the Umbraco TinyMCE plugins */ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, stylesheetResource, macroResource, macroService, - $routeParams, umbRequestHelper, angularHelper, userService, editorService, entityResource, eventsService) { + $routeParams, umbRequestHelper, angularHelper, userService, editorService, entityResource, eventsService, localStorageService) { //These are absolutely required in order for the macros to render inline //we put these as extended elements because they get merged on top of the normal allowed elements by tiny mce @@ -203,7 +203,7 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s } // Put temp location into localstorage (used to update the img with data-tmpimg later on) - localStorage.setItem(`tinymce__${blobInfo.blobUri()}`, json.tmpLocation); + localStorageService.set(`tinymce__${blobInfo.blobUri()}`, json.tmpLocation); // We set the img src url to be the same as we started // The Blob URI is stored in TinyMce's cache @@ -234,7 +234,7 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s // Get img src var imgSrc = img.getAttribute("src"); - var tmpLocation = localStorage.getItem(`tinymce__${imgSrc}`); + var tmpLocation = localStorageService.get(`tinymce__${imgSrc}`) // Select the img & add new attr which we can search for // When its being persisted in RTE property editor @@ -252,13 +252,14 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s blobImageWithNoTmpImgAttribute.forEach(imageElement => { var blobSrcUri = editor.dom.getAttrib(imageElement, "src"); - // Select/find the same image uploaded with the matching SRC uri - var uploadedImage = editor.dom.select(`img[src="${blobSrcUri}"][data-tmpimg]`); + // Find the same image uploaded (Should be in LocalStorage) + // May already exist in the editor as duplicate image + // OR added to the RTE, deleted & re-added again + // So lets fetch the tempurl out of localstorage for that blob URI item + var tmpLocation = localStorageService.get(`tinymce__${blobSrcUri}`) - if(uploadedImage){ - // Get the value of the tmpimg - so we can apply it to the matched/duplicate image - var dataTmpImg = editor.dom.getAttrib(uploadedImage, "data-tmpimg"); - editor.dom.setAttrib(imageElement, "data-tmpimg", dataTmpImg); + if(tmpLocation){ + editor.dom.setAttrib(imageElement, "data-tmpimg", tmpLocation); } }); From f43a6a9004b48176e7b2a126c4207a8310672da9 Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Tue, 24 Sep 2019 12:00:39 +0100 Subject: [PATCH 5/6] Fix unit test --- .../test/unit/app/propertyeditors/rte-controller.spec.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Umbraco.Web.UI.Client/test/unit/app/propertyeditors/rte-controller.spec.js b/src/Umbraco.Web.UI.Client/test/unit/app/propertyeditors/rte-controller.spec.js index f813ec24a8..715861de0b 100644 --- a/src/Umbraco.Web.UI.Client/test/unit/app/propertyeditors/rte-controller.spec.js +++ b/src/Umbraco.Web.UI.Client/test/unit/app/propertyeditors/rte-controller.spec.js @@ -13,6 +13,8 @@ describe('RTE controller tests', function () { } } + beforeEach(module('LocalStorageModule')); + beforeEach(module('umbraco', function ($provide) { $provide.value('tinyMceAssets', []); })); From 5d364230875afe555afeb307825f46ad96a864a0 Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Tue, 24 Sep 2019 13:47:04 +0100 Subject: [PATCH 6/6] After merge - makes sure to resize the deleted & then re-added same image using the same sizing function --- src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js b/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js index b89a15db41..b671bcec00 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js @@ -266,6 +266,7 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s var tmpLocation = localStorageService.get(`tinymce__${blobSrcUri}`) if(tmpLocation){ + sizeImageInEditor(editor, imageElement); editor.dom.setAttrib(imageElement, "data-tmpimg", tmpLocation); } });