Refactor we do not instantly persist the uploaded file to a media item in Umbraco - what if the user undo's or removes it from content we will bloat the media section

* Adds a data-tmpimg attribute to the <img> in the markup, which we scan for when persisting to the DB in the RTE PropertyValueEditor to then perist media items
* TODO: Find a way to get TinyMCE to NOT send a base64 down the wire as the saved content/HTML
This commit is contained in:
Warren Buckley
2019-09-09 16:04:52 +01:00
parent 7d2c92b651
commit 5e394cb62b
3 changed files with 126 additions and 65 deletions

View File

@@ -158,10 +158,6 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s
}
function uploadImageHandler(blobInfo, success, failure, progress){
//TODO: Worth refactoring to Angular $http calls
//Rather than XHR??
let xhr, formData;
xhr = new XMLHttpRequest();
@@ -185,24 +181,20 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s
json = JSON.parse(xhr.responseText);
if (!json || typeof json.location !== 'string') {
if (!json || typeof json.tmpLocation !== 'string') {
failure('Invalid JSON: ' + xhr.responseText);
return;
}
// Put UDI into localstorage (used to update the img with data-udi later on)
localStorageService.set(`tinymce__${json.location}`, json.udi);
// Put temp location into localstorage (used to update the img with data-tmpimg later on)
localStorage.setItem(`tinymce__${blobInfo.blobUri()}`, json.tmpLocation);
success(json.location);
success();
};
formData = new FormData();
formData.append('file', blobInfo.blob(), blobInfo.blob().name);
//TODO: Send Media Parent ID from config
//TODO: How will each RTE pass in this value to this function?!
formData.append('mediaParent', -1);
xhr.send(formData);
}
@@ -215,21 +207,22 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s
if(content.indexOf('<img src="blob:') > -1){
editor.uploadImages(function(data) {
// Once all images have been uploaded
data.forEach(function(item) {
//Select img element
// Select img element
var img = item.element;
//Get img src
// Get img src
var imgSrc = img.getAttribute("src");
var tmpLocation = localStorage.getItem(`tinymce__${imgSrc}`);
//Try & find in localstorage
var udi = localStorageService.get(`tinymce__${imgSrc}`);
// Select the img & add new attr which we can search for
// 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 });
//Select the img & update is attr
tinymce.activeEditor.$(img).attr({ "data-udi": udi });
//Remove key
localStorageService.remove(`tinymce__${imgSrc}`);
// Be a boy scout & cleanup after ourselves
localStorage.removeItem(`tinymce__${imgSrc}`);
});
});
}
@@ -330,6 +323,7 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s
images_upload_handler: uploadImageHandler,
automatic_uploads: false,
images_replace_blob_uris: false,
init_instance_callback: initEvents
};

View File

@@ -1,10 +1,8 @@
using System.Net;
using System.Net.Http;
using System.Web;
using System.Web.Http;
using Umbraco.Core.Services;
using Umbraco.Web.WebApi;
using Constants = Umbraco.Core.Constants;
using Umbraco.Core;
using Umbraco.Web.Mvc;
using Umbraco.Core.IO;
@@ -14,7 +12,6 @@ using Umbraco.Web.Composing;
using Umbraco.Core.Configuration.UmbracoSettings;
using System.Linq;
using System;
using Umbraco.Core.Models.PublishedContent;
namespace Umbraco.Web.Editors
{
@@ -39,12 +36,22 @@ namespace Umbraco.Web.Editors
throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType);
}
var root = IOHelper.MapPath(SystemDirectories.TempFileUploads);
// Backoffice user ID (needed for unique folder path) to help with concurrent users
// to avoid filename clash along with UTC current time
var userId = Security.CurrentUser.Id;
var imageTempPath = IOHelper.MapPath(SystemDirectories.TempImageUploads + "/" + userId + "_" + DateTimeOffset.UtcNow.ToUnixTimeSeconds());
// Ensure it exists
Directory.CreateDirectory(root);
var provider = new MultipartFormDataStreamProvider(root);
// Temp folderpath (Files come in as bodypart & will need to move/saved into imgTempPath
var folderPath = IOHelper.MapPath(SystemDirectories.TempFileUploads);
// Ensure image temp path exists
if(Directory.Exists(imageTempPath) == false)
{
Directory.CreateDirectory(imageTempPath);
}
// File uploaded will be saved as bodypart into TEMP folder
var provider = new MultipartFormDataStreamProvider(folderPath);
var result = await Request.Content.ReadAsMultipartAsync(provider);
// Must have a file
@@ -59,17 +66,8 @@ namespace Umbraco.Web.Editors
return Request.CreateErrorResponse(HttpStatusCode.BadRequest, "Only one file can be uploaded at a time");
}
// Check we have mediaParent as posted data
if (string.IsNullOrEmpty(result.FormData["mediaParent"]))
{
return Request.CreateErrorResponse(HttpStatusCode.BadRequest, "Missing the Media Parent folder to save this image");
}
// Really we should only have one file per request to this endpoint
var file = result.FileData[0];
var parentFolder = Convert.ToInt32(result.FormData["mediaParent"]);
var fileName = file.Headers.ContentDisposition.FileName.Trim(new[] { '\"' }).TrimEnd();
var safeFileName = fileName.ToSafeFileName();
var ext = safeFileName.Substring(safeFileName.LastIndexOf('.') + 1).ToLower();
@@ -80,28 +78,26 @@ namespace Umbraco.Web.Editors
return Request.CreateErrorResponse(HttpStatusCode.BadRequest, "This is not an image filetype extension that is approved");
}
var mediaItemName = fileName.ToFriendlyName();
var f = _mediaService.CreateMedia(mediaItemName, parentFolder, Constants.Conventions.MediaTypes.Image, Security.CurrentUser.Id);
var fileInfo = new FileInfo(file.LocalFileName);
var fs = fileInfo.OpenReadWithRetry();
if (fs == null) throw new InvalidOperationException("Could not acquire file stream");
using (fs)
//var mediaItemName = fileName.ToFriendlyName();
var currentFile = file.LocalFileName;
var newFilePath = imageTempPath + IOHelper.DirSepChar + safeFileName;
var relativeNewFilePath = IOHelper.GetRelativePath(newFilePath);
try
{
f.SetValue(Services.ContentTypeBaseServices, Constants.Conventions.Media.File, fileName, fs);
// Move the file from bodypart to a real filename
// This is what we return from this API so RTE updates img src path
// Until we fully persist & save the media item when persisting
// If we find <img data-temp-img /> data attribute
File.Move(currentFile, newFilePath);
}
catch (Exception ex)
{
// Could be a file permission ex
throw;
}
_mediaService.Save(f, Security.CurrentUser.Id);
// Need to get URL to the media item and its UDI
var udi = f.GetUdi();
// TODO: Check this is the BEST way to get the URL
// Ensuring if they use some CDN & blob storage?!
var mediaTyped = Umbraco.Media(f.Id);
var location = mediaTyped.Url;
return Request.CreateResponse(HttpStatusCode.OK, new { location = location, udi = udi });
return Request.CreateResponse(HttpStatusCode.OK, new { tmpLocation = relativeNewFilePath });
}
}
}

View File

@@ -1,12 +1,15 @@
using System;
using HtmlAgilityPack;
using System;
using System.Collections.Generic;
using System.IO;
using Umbraco.Core;
using Umbraco.Core.IO;
using Umbraco.Core.Logging;
using Umbraco.Core.Macros;
using Umbraco.Core.Models;
using Umbraco.Core.PropertyEditors;
using Umbraco.Core.Services;
using Umbraco.Examine;
using Umbraco.Web.Composing;
using Umbraco.Web.Macros;
using Umbraco.Web.Templates;
@@ -25,18 +28,23 @@ namespace Umbraco.Web.PropertyEditors
Icon = "icon-browser-window")]
public class RichTextPropertyEditor : DataEditor
{
private IMediaService _mediaService;
private IContentTypeBaseServiceProvider _contentTypeBaseServiceProvider;
/// <summary>
/// The constructor will setup the property editor based on the attribute if one is found
/// </summary>
public RichTextPropertyEditor(ILogger logger) : base(logger)
public RichTextPropertyEditor(ILogger logger, IMediaService mediaService, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider) : base(logger)
{
_mediaService = mediaService;
_contentTypeBaseServiceProvider = contentTypeBaseServiceProvider;
}
/// <summary>
/// Create a custom value editor
/// </summary>
/// <returns></returns>
protected override IDataValueEditor CreateValueEditor() => new RichTextPropertyValueEditor(Attribute);
protected override IDataValueEditor CreateValueEditor() => new RichTextPropertyValueEditor(Attribute, _mediaService, _contentTypeBaseServiceProvider);
protected override IConfigurationEditor CreateConfigurationEditor() => new RichTextConfigurationEditor();
@@ -47,9 +55,15 @@ namespace Umbraco.Web.PropertyEditors
/// </summary>
internal class RichTextPropertyValueEditor : DataValueEditor
{
public RichTextPropertyValueEditor(DataEditorAttribute attribute)
private IMediaService _mediaService;
private IContentTypeBaseServiceProvider _contentTypeBaseServiceProvider;
public RichTextPropertyValueEditor(DataEditorAttribute attribute, IMediaService mediaService, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider)
: base(attribute)
{ }
{
_mediaService = mediaService;
_contentTypeBaseServiceProvider = contentTypeBaseServiceProvider;
}
/// <inheritdoc />
public override object Configuration
@@ -71,10 +85,9 @@ namespace Umbraco.Web.PropertyEditors
/// Format the data for the editor
/// </summary>
/// <param name="property"></param>
/// <param name="dataTypeService"></param>
/// <param name="dataTypeService"></param>Rte
/// <param name="culture"></param>
/// <param name="segment"></param>
/// <returns></returns>
/// <param name="segment"></param>te
public override object ToEditor(Property property, IDataTypeService dataTypeService, string culture = null, string segment = null)
{
var val = property.GetValue(culture, segment);
@@ -99,8 +112,66 @@ namespace Umbraco.Web.PropertyEditors
var editorValueWithMediaUrlsRemoved = TemplateUtilities.RemoveMediaUrlsFromTextString(editorValue.Value.ToString());
var parsed = MacroTagParser.FormatRichTextContentForPersistence(editorValueWithMediaUrlsRemoved);
// HTML content when being persisted may j
parsed = FindPastedTempImages(parsed);
return parsed;
}
private string FindPastedTempImages(string html)
{
// Find all img's that has data-tmpimg attribute
// Use HTML Agility Pack - https://html-agility-pack.net
var htmlDoc = new HtmlDocument();
htmlDoc.LoadHtml(html);
var tmpImages = htmlDoc.DocumentNode.SelectNodes("//img[@data-tmpimg]");
if (tmpImages == null || tmpImages.Count == 0)
return html;
var userId = Current.UmbracoContext.Security.CurrentUser.Id;
foreach (var img in tmpImages)
{
// The data attribute contains the path to the tmp img to persist as a media item
var tmpImgPath = img.GetAttributeValue("data-tmpimg", string.Empty);
if (string.IsNullOrEmpty(tmpImgPath))
continue;
var absTmpImgPath = IOHelper.MapPath(tmpImgPath);
var fileName = Path.GetFileName(absTmpImgPath);
var safeFileName = fileName.ToSafeFileName();
// TODO: In future task (get the parent folder from this config) to save the media into
var mediaItemName = safeFileName.ToFriendlyName();
var f = _mediaService.CreateMedia(mediaItemName, -1, Constants.Conventions.MediaTypes.Image, userId);
var fileInfo = new FileInfo(absTmpImgPath);
var fs = fileInfo.OpenReadWithRetry();
if (fs == null) throw new InvalidOperationException("Could not acquire file stream");
using (fs)
{
f.SetValue(_contentTypeBaseServiceProvider, Constants.Conventions.Media.File, safeFileName, fs);
}
_mediaService.Save(f, userId);
// Add the UDI to the img element as new data attribute
var udi = f.GetUdi();
img.SetAttributeValue("data-udi", udi.ToString());
//Get the new persisted image url
var mediaTyped = Current.UmbracoHelper.Media(f.Id);
var location = mediaTyped.Url;
// Remove the data attribute (so we do not re-process this)
img.Attributes.Remove("data-tmpimg");
}
return htmlDoc.DocumentNode.OuterHtml;
}
}
internal class RichTextPropertyIndexValueFactory : IPropertyIndexValueFactory