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:
@@ -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
|
||||
};
|
||||
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user