Merge remote-tracking branch 'origin/v11/dev' into v12/dev
This commit is contained in:
@@ -86,7 +86,7 @@ public class DatabaseSchemaCreator
|
||||
};
|
||||
|
||||
private readonly IUmbracoDatabase _database;
|
||||
private readonly IOptionsMonitor<InstallDefaultDataSettings> _defaultDataCreationSettings;
|
||||
private readonly IOptionsMonitor<InstallDefaultDataSettings> _installDefaultDataSettings;
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
private readonly ILogger<DatabaseSchemaCreator> _logger;
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
@@ -105,7 +105,7 @@ public class DatabaseSchemaCreator
|
||||
_loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
|
||||
_umbracoVersion = umbracoVersion ?? throw new ArgumentNullException(nameof(umbracoVersion));
|
||||
_eventAggregator = eventAggregator;
|
||||
_defaultDataCreationSettings = defaultDataCreationSettings;
|
||||
_installDefaultDataSettings = defaultDataCreationSettings; // TODO (V13): Rename this parameter to installDefaultDataSettings.
|
||||
|
||||
if (_database?.SqlContext?.SqlSyntax == null)
|
||||
{
|
||||
@@ -165,7 +165,7 @@ public class DatabaseSchemaCreator
|
||||
var dataCreation = new DatabaseDataCreator(
|
||||
_database, _loggerFactory.CreateLogger<DatabaseDataCreator>(),
|
||||
_umbracoVersion,
|
||||
_defaultDataCreationSettings);
|
||||
_installDefaultDataSettings);
|
||||
foreach (Type table in _orderedTables)
|
||||
{
|
||||
CreateTable(false, table, dataCreation);
|
||||
@@ -442,7 +442,7 @@ public class DatabaseSchemaCreator
|
||||
_database,
|
||||
_loggerFactory.CreateLogger<DatabaseDataCreator>(),
|
||||
_umbracoVersion,
|
||||
_defaultDataCreationSettings));
|
||||
_installDefaultDataSettings));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -226,7 +226,9 @@ namespace Umbraco.Cms.Core.PropertyEditors
|
||||
|
||||
if (html is not null)
|
||||
{
|
||||
var parseAndSavedTempImages = _pastedImages.FindAndPersistPastedTempImages(html, mediaParentId, userId, _imageUrlGenerator);
|
||||
var parseAndSaveBase64Images = _pastedImages.FindAndPersistEmbeddedImages(
|
||||
html, mediaParentId, userId);
|
||||
var parseAndSavedTempImages = _pastedImages.FindAndPersistPastedTempImages(parseAndSaveBase64Images, mediaParentId, userId);
|
||||
var editorValueWithMediaUrlsRemoved = _imageSourceParser.RemoveImageSources(parseAndSavedTempImages);
|
||||
rte.Value = editorValueWithMediaUrlsRemoved;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
// Copyright (c) Umbraco.
|
||||
// See LICENSE for more details.
|
||||
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Web;
|
||||
using HtmlAgilityPack;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.Exceptions;
|
||||
using Umbraco.Cms.Core.Hosting;
|
||||
using Umbraco.Cms.Core.IO;
|
||||
@@ -13,6 +19,7 @@ using Umbraco.Cms.Core.Routing;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Strings;
|
||||
using Umbraco.Cms.Core.Web;
|
||||
using Umbraco.Cms.Web.Common.DependencyInjection;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Core.PropertyEditors;
|
||||
@@ -30,7 +37,11 @@ public sealed class RichTextEditorPastedImages
|
||||
private readonly IShortStringHelper _shortStringHelper;
|
||||
private readonly IUmbracoContextAccessor _umbracoContextAccessor;
|
||||
private readonly string _tempFolderAbsolutePath;
|
||||
private readonly IImageUrlGenerator _imageUrlGenerator;
|
||||
private readonly ContentSettings _contentSettings;
|
||||
private readonly Dictionary<string, GuidUdi> _uploadedImages = new();
|
||||
|
||||
[Obsolete("Use the ctor which takes an IImageUrlGenerator and IOptions<ContentSettings> instead, scheduled for removal in v14")]
|
||||
public RichTextEditorPastedImages(
|
||||
IUmbracoContextAccessor umbracoContextAccessor,
|
||||
ILogger<RichTextEditorPastedImages> logger,
|
||||
@@ -41,6 +52,33 @@ public sealed class RichTextEditorPastedImages
|
||||
MediaUrlGeneratorCollection mediaUrlGenerators,
|
||||
IShortStringHelper shortStringHelper,
|
||||
IPublishedUrlProvider publishedUrlProvider)
|
||||
: this(
|
||||
umbracoContextAccessor,
|
||||
logger,
|
||||
hostingEnvironment,
|
||||
mediaService,
|
||||
contentTypeBaseServiceProvider,
|
||||
mediaFileManager,
|
||||
mediaUrlGenerators,
|
||||
shortStringHelper,
|
||||
publishedUrlProvider,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IImageUrlGenerator>(),
|
||||
StaticServiceProvider.Instance.GetRequiredService<IOptions<ContentSettings>>())
|
||||
{
|
||||
}
|
||||
|
||||
public RichTextEditorPastedImages(
|
||||
IUmbracoContextAccessor umbracoContextAccessor,
|
||||
ILogger<RichTextEditorPastedImages> logger,
|
||||
IHostingEnvironment hostingEnvironment,
|
||||
IMediaService mediaService,
|
||||
IContentTypeBaseServiceProvider contentTypeBaseServiceProvider,
|
||||
MediaFileManager mediaFileManager,
|
||||
MediaUrlGeneratorCollection mediaUrlGenerators,
|
||||
IShortStringHelper shortStringHelper,
|
||||
IPublishedUrlProvider publishedUrlProvider,
|
||||
IImageUrlGenerator imageUrlGenerator,
|
||||
IOptions<ContentSettings> contentSettings)
|
||||
{
|
||||
_umbracoContextAccessor =
|
||||
umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor));
|
||||
@@ -53,15 +91,132 @@ public sealed class RichTextEditorPastedImages
|
||||
_mediaUrlGenerators = mediaUrlGenerators;
|
||||
_shortStringHelper = shortStringHelper;
|
||||
_publishedUrlProvider = publishedUrlProvider;
|
||||
_imageUrlGenerator = imageUrlGenerator;
|
||||
_contentSettings = contentSettings.Value;
|
||||
|
||||
_tempFolderAbsolutePath = _hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.TempImageUploads);
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used by the RTE (and grid RTE) for drag/drop/persisting images
|
||||
/// Used by the RTE (and grid RTE) for converting inline base64 images to Media items.
|
||||
/// </summary>
|
||||
public string FindAndPersistPastedTempImages(string html, Guid mediaParentFolder, int userId, IImageUrlGenerator imageUrlGenerator)
|
||||
/// <param name="html">HTML from the Rich Text Editor property editor.</param>
|
||||
/// <param name="mediaParentFolder"></param>
|
||||
/// <param name="userId"></param>
|
||||
/// <returns>Formatted HTML.</returns>
|
||||
/// <exception cref="NotSupportedException">Thrown if image extension is not allowed</exception>
|
||||
internal string FindAndPersistEmbeddedImages(string html, Guid mediaParentFolder, int userId)
|
||||
{
|
||||
// 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);
|
||||
|
||||
HtmlNodeCollection? imagesWithDataUris = htmlDoc.DocumentNode.SelectNodes("//img");
|
||||
if (imagesWithDataUris is null || imagesWithDataUris.Count is 0)
|
||||
{
|
||||
return html;
|
||||
}
|
||||
|
||||
foreach (HtmlNode? img in imagesWithDataUris)
|
||||
{
|
||||
var srcValue = img.GetAttributeValue("src", string.Empty);
|
||||
|
||||
// Ignore src-less images
|
||||
if (string.IsNullOrEmpty(srcValue))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Take only images that have a "data:image" uri into consideration
|
||||
if (!srcValue.StartsWith("data:image"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create tmp image by scanning the srcValue
|
||||
// the value will look like "data:image/jpg;base64,abc" where the first part
|
||||
// is the mimetype and the second (after the comma) is the image blob
|
||||
Match dataUriInfo = Regex.Match(srcValue, @"^data:\w+\/(?<ext>\w+)[\w\+]*?;(?<encoding>\w+),(?<data>.+)$");
|
||||
|
||||
// If it turns up false, it was probably a false-positive and we can't do anything with it
|
||||
if (dataUriInfo.Success is false)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var ext = dataUriInfo.Groups["ext"].Value.ToLowerInvariant();
|
||||
var encoding = dataUriInfo.Groups["encoding"].Value.ToLowerInvariant();
|
||||
var imageData = dataUriInfo.Groups["data"].Value;
|
||||
|
||||
if (_contentSettings.IsFileAllowedForUpload(ext) is false)
|
||||
{
|
||||
// If the image format is not supported we should probably leave it be
|
||||
// since the user decided to include it.
|
||||
// If we accepted it anyway, they could technically circumvent the allow list for file types,
|
||||
// but the user experience would not be very good if we simply failed to save the content.
|
||||
// Besides, there may be other types of data uri images technically supported by a browser that we cannot handle.
|
||||
_logger.LogWarning(
|
||||
"Performance impact: Could not convert embedded image to a Media item because the file extension {Ext} was not allowed. HTML extract: {OuterHtml}",
|
||||
ext,
|
||||
img.OuterHtml.Length < 100 ? img.OuterHtml : img.OuterHtml[..100]); // only log the first 100 chars because base64 images can be very long
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create an unique folder path to help with concurrent users to avoid filename clash
|
||||
var imageTempPath =
|
||||
_hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.TempImageUploads + Path.DirectorySeparatorChar + Guid.NewGuid());
|
||||
|
||||
// Ensure image temp path exists
|
||||
if (Directory.Exists(imageTempPath) is false)
|
||||
{
|
||||
Directory.CreateDirectory(imageTempPath);
|
||||
}
|
||||
|
||||
// To get the filename, we simply manipulate the mimetype into a filename
|
||||
var filePath = $"image.{ext}";
|
||||
var safeFileName = filePath.ToSafeFileName(_shortStringHelper);
|
||||
var tmpImgPath = imageTempPath + Path.DirectorySeparatorChar + safeFileName;
|
||||
var absoluteTempImagePath = Path.GetFullPath(tmpImgPath);
|
||||
|
||||
// Convert the base64 content to a byte array and save the bytes directly to a file
|
||||
// this method should work for most use-cases
|
||||
if (encoding.Equals("base64"))
|
||||
{
|
||||
System.IO.File.WriteAllBytes(absoluteTempImagePath, Convert.FromBase64String(imageData));
|
||||
}
|
||||
else
|
||||
{
|
||||
System.IO.File.WriteAllText(absoluteTempImagePath, HttpUtility.HtmlDecode(imageData), Encoding.UTF8);
|
||||
}
|
||||
|
||||
// When the temp file has been created, we can persist it
|
||||
PersistMediaItem(mediaParentFolder, userId, img, tmpImgPath);
|
||||
}
|
||||
|
||||
return htmlDoc.DocumentNode.OuterHtml;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used by the RTE (and grid RTE) for drag/drop/persisting images.
|
||||
/// </summary>
|
||||
/// <param name="html">HTML from the Rich Text Editor property editor.</param>
|
||||
/// <param name="mediaParentFolder"></param>
|
||||
/// <param name="userId"></param>
|
||||
/// <param name="imageUrlGenerator"></param>
|
||||
/// <returns>Formatted HTML.</returns>
|
||||
[Obsolete("It is not needed to supply the imageUrlGenerator parameter")]
|
||||
public string FindAndPersistPastedTempImages(string html, Guid mediaParentFolder, int userId, IImageUrlGenerator imageUrlGenerator) =>
|
||||
FindAndPersistPastedTempImages(html, mediaParentFolder, userId);
|
||||
|
||||
/// <summary>
|
||||
/// Used by the RTE (and grid RTE) for drag/drop/persisting images.
|
||||
/// </summary>
|
||||
/// <param name="html">HTML from the Rich Text Editor property editor.</param>
|
||||
/// <param name="mediaParentFolder"></param>
|
||||
/// <param name="userId"></param>
|
||||
/// <returns>Formatted HTML.</returns>
|
||||
public string FindAndPersistPastedTempImages(string html, Guid mediaParentFolder, int userId)
|
||||
{
|
||||
// Find all img's that has data-tmpimg attribute
|
||||
// Use HTML Agility Pack - https://html-agility-pack.net
|
||||
@@ -69,16 +224,11 @@ public sealed class RichTextEditorPastedImages
|
||||
htmlDoc.LoadHtml(html);
|
||||
|
||||
HtmlNodeCollection? tmpImages = htmlDoc.DocumentNode.SelectNodes($"//img[@{TemporaryImageDataAttribute}]");
|
||||
if (tmpImages == null || tmpImages.Count == 0)
|
||||
if (tmpImages is null || tmpImages.Count is 0)
|
||||
{
|
||||
return html;
|
||||
}
|
||||
|
||||
// An array to contain a list of URLs that
|
||||
// we have already processed to avoid dupes
|
||||
var uploadedImages = new Dictionary<string, GuidUdi>();
|
||||
|
||||
|
||||
foreach (HtmlNode? img in tmpImages)
|
||||
{
|
||||
// The data attribute contains the path to the tmp img to persist as a media item
|
||||
@@ -89,116 +239,119 @@ public sealed class RichTextEditorPastedImages
|
||||
continue;
|
||||
}
|
||||
|
||||
var qualifiedTmpImgPath = _hostingEnvironment.MapPathContentRoot(tmpImgPath);
|
||||
|
||||
var absoluteTempImagePath = Path.GetFullPath(_hostingEnvironment.MapPathContentRoot(tmpImgPath));
|
||||
|
||||
if (IsValidPath(absoluteTempImagePath) == false)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var fileName = Path.GetFileName(absoluteTempImagePath);
|
||||
var safeFileName = fileName.ToSafeFileName(_shortStringHelper);
|
||||
|
||||
var mediaItemName = safeFileName.ToFriendlyName();
|
||||
IMedia mediaFile;
|
||||
GuidUdi udi;
|
||||
|
||||
if (uploadedImages.ContainsKey(tmpImgPath) == false)
|
||||
{
|
||||
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);
|
||||
|
||||
FileStream? fileStream = fileInfo.OpenReadWithRetry();
|
||||
if (fileStream == null)
|
||||
{
|
||||
throw new InvalidOperationException("Could not acquire file stream");
|
||||
}
|
||||
|
||||
using (fileStream)
|
||||
{
|
||||
mediaFile.SetValue(_mediaFileManager, _mediaUrlGenerators, _shortStringHelper, _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
|
||||
img.SetAttributeValue("data-udi", udi.ToString());
|
||||
|
||||
// Get the new persisted image URL
|
||||
_umbracoContextAccessor.TryGetUmbracoContext(out IUmbracoContext? umbracoContext);
|
||||
IPublishedContent? mediaTyped = umbracoContext?.Media?.GetById(udi.Guid);
|
||||
if (mediaTyped == null)
|
||||
{
|
||||
throw new PanicException(
|
||||
$"Could not find media by id {udi.Guid} or there was no UmbracoContext available.");
|
||||
}
|
||||
|
||||
var location = mediaTyped.Url(_publishedUrlProvider);
|
||||
|
||||
// Find the width & height attributes as we need to set the imageprocessor QueryString
|
||||
var width = img.GetAttributeValue("width", int.MinValue);
|
||||
var height = img.GetAttributeValue("height", int.MinValue);
|
||||
|
||||
if (width != int.MinValue && height != int.MinValue)
|
||||
{
|
||||
location = imageUrlGenerator.GetImageUrl(new ImageUrlGenerationOptions(location)
|
||||
{
|
||||
ImageCropMode = ImageCropMode.Max,
|
||||
Width = width,
|
||||
Height = height,
|
||||
});
|
||||
}
|
||||
|
||||
img.SetAttributeValue("src", location);
|
||||
|
||||
// Remove the data attribute (so we do not re-process this)
|
||||
img.Attributes.Remove(TemporaryImageDataAttribute);
|
||||
|
||||
// Add to the dictionary to avoid dupes
|
||||
if (uploadedImages.ContainsKey(tmpImgPath) == false)
|
||||
{
|
||||
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
|
||||
{
|
||||
if (folderName is not null)
|
||||
{
|
||||
Directory.Delete(folderName, true);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Could not delete temp file or folder {FileName}", absoluteTempImagePath);
|
||||
}
|
||||
}
|
||||
PersistMediaItem(mediaParentFolder, userId, img, qualifiedTmpImgPath);
|
||||
}
|
||||
|
||||
return htmlDoc.DocumentNode.OuterHtml;
|
||||
}
|
||||
|
||||
private bool IsValidPath(string imagePath)
|
||||
private void PersistMediaItem(Guid mediaParentFolder, int userId, HtmlNode img, string qualifiedTmpImgPath)
|
||||
{
|
||||
return imagePath.StartsWith(_tempFolderAbsolutePath);
|
||||
var absoluteTempImagePath = Path.GetFullPath(qualifiedTmpImgPath);
|
||||
|
||||
if (IsValidPath(absoluteTempImagePath) is false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var fileName = Path.GetFileName(absoluteTempImagePath);
|
||||
var safeFileName = fileName.ToSafeFileName(_shortStringHelper);
|
||||
|
||||
var mediaItemName = safeFileName.ToFriendlyName();
|
||||
GuidUdi udi;
|
||||
|
||||
if (_uploadedImages.ContainsKey(qualifiedTmpImgPath) is false)
|
||||
{
|
||||
var isSvg = qualifiedTmpImgPath.EndsWith(".svg");
|
||||
var mediaType = isSvg
|
||||
? Constants.Conventions.MediaTypes.VectorGraphicsAlias
|
||||
: Constants.Conventions.MediaTypes.Image;
|
||||
|
||||
IMedia mediaFile = mediaParentFolder == Guid.Empty
|
||||
? _mediaService.CreateMedia(mediaItemName, Constants.System.Root, mediaType, userId)
|
||||
: _mediaService.CreateMedia(mediaItemName, mediaParentFolder, mediaType, userId);
|
||||
|
||||
var fileInfo = new FileInfo(absoluteTempImagePath);
|
||||
|
||||
FileStream? fileStream = fileInfo.OpenReadWithRetry();
|
||||
if (fileStream is null)
|
||||
{
|
||||
throw new InvalidOperationException("Could not acquire file stream");
|
||||
}
|
||||
|
||||
using (fileStream)
|
||||
{
|
||||
mediaFile.SetValue(_mediaFileManager, _mediaUrlGenerators, _shortStringHelper,
|
||||
_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[qualifiedTmpImgPath];
|
||||
}
|
||||
|
||||
// Add the UDI to the img element as new data attribute
|
||||
img.SetAttributeValue("data-udi", udi.ToString());
|
||||
|
||||
// Get the new persisted image URL
|
||||
_umbracoContextAccessor.TryGetUmbracoContext(out IUmbracoContext? umbracoContext);
|
||||
IPublishedContent? mediaTyped = umbracoContext?.Media?.GetById(udi.Guid);
|
||||
if (mediaTyped is null)
|
||||
{
|
||||
throw new PanicException(
|
||||
$"Could not find media by id {udi.Guid} or there was no UmbracoContext available.");
|
||||
}
|
||||
|
||||
var location = mediaTyped.Url(_publishedUrlProvider);
|
||||
|
||||
// Find the width & height attributes as we need to set the imageprocessor QueryString
|
||||
var width = img.GetAttributeValue("width", int.MinValue);
|
||||
var height = img.GetAttributeValue("height", int.MinValue);
|
||||
|
||||
if (width != int.MinValue && height != int.MinValue)
|
||||
{
|
||||
location = _imageUrlGenerator.GetImageUrl(new ImageUrlGenerationOptions(location)
|
||||
{
|
||||
ImageCropMode = ImageCropMode.Max,
|
||||
Width = width,
|
||||
Height = height,
|
||||
});
|
||||
}
|
||||
|
||||
img.SetAttributeValue("src", location);
|
||||
|
||||
// Remove the data attribute (so we do not re-process this)
|
||||
img.Attributes.Remove(TemporaryImageDataAttribute);
|
||||
|
||||
// Add to the dictionary to avoid dupes
|
||||
if (_uploadedImages.ContainsKey(qualifiedTmpImgPath) is false)
|
||||
{
|
||||
_uploadedImages.Add(qualifiedTmpImgPath, 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
|
||||
{
|
||||
if (folderName is not null)
|
||||
{
|
||||
Directory.Delete(folderName, true);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Could not delete temp file or folder {FileName}", absoluteTempImagePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsValidPath(string imagePath) => imagePath.StartsWith(_tempFolderAbsolutePath);
|
||||
}
|
||||
|
||||
@@ -293,8 +293,10 @@ public class RichTextPropertyEditor : DataEditor
|
||||
return null;
|
||||
}
|
||||
|
||||
var parseAndSaveBase64Images = _pastedImages.FindAndPersistEmbeddedImages(
|
||||
editorValue.Value.ToString()!, mediaParentId, userId);
|
||||
var parseAndSavedTempImages =
|
||||
_pastedImages.FindAndPersistPastedTempImages(editorValue.Value.ToString()!, mediaParentId, userId, _imageUrlGenerator);
|
||||
_pastedImages.FindAndPersistPastedTempImages(parseAndSaveBase64Images, mediaParentId, userId);
|
||||
var editorValueWithMediaUrlsRemoved = _imageSourceParser.RemoveImageSources(parseAndSavedTempImages);
|
||||
var parsed = MacroTagParser.FormatRichTextContentForPersistence(editorValueWithMediaUrlsRemoved);
|
||||
var sanitized = _htmlSanitizer.Sanitize(parsed);
|
||||
|
||||
@@ -664,4 +664,87 @@ public class ContentControllerTests : UmbracoTestServerTestBase
|
||||
Assert.AreEqual(0, display.Notifications.Count(x => x.NotificationType == NotificationStyle.Warning));
|
||||
});
|
||||
}
|
||||
|
||||
[TestCase(
|
||||
@"<p><img alt src=""data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7""></p>",
|
||||
false)]
|
||||
[TestCase(
|
||||
@"<p><img src=""data:image/svg+xml;utf8,<svg viewBox=""0 0 70 74"" fill=""none"" xmlns=""http://www.w3.org/2000/svg""><rect width=""100%"" height=""100%"" fill=""black""/></svg>""></p>",
|
||||
false)]
|
||||
[TestCase(
|
||||
@"<p><img alt src=""/some/random/image.jpg""></p><p><img alt src=""data:image/jpg;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7""></p>",
|
||||
false)]
|
||||
[TestCase(
|
||||
@"<p><img alt src=""data:image/notallowedextension;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7""></p>",
|
||||
true)]
|
||||
public async Task PostSave_Simple_RichText_With_Base64(string html, bool shouldHaveDataUri)
|
||||
{
|
||||
var url = PrepareApiControllerUrl<ContentController>(x => x.PostSave(null));
|
||||
|
||||
var dataTypeService = GetRequiredService<IDataTypeService>();
|
||||
var contentService = GetRequiredService<IContentService>();
|
||||
var contentTypeService = GetRequiredService<IContentTypeService>();
|
||||
|
||||
var dataType = new DataTypeBuilder()
|
||||
.WithId(0)
|
||||
.WithoutIdentity()
|
||||
.WithDatabaseType(ValueStorageType.Ntext)
|
||||
.AddEditor()
|
||||
.WithAlias(Constants.PropertyEditors.Aliases.TinyMce)
|
||||
.Done()
|
||||
.Build();
|
||||
|
||||
dataTypeService.Save(dataType);
|
||||
|
||||
var contentType = new ContentTypeBuilder()
|
||||
.WithId(0)
|
||||
.AddPropertyType()
|
||||
.WithDataTypeId(dataType.Id)
|
||||
.WithAlias("richText")
|
||||
.WithName("Rich Text")
|
||||
.Done()
|
||||
.WithContentVariation(ContentVariation.Nothing)
|
||||
.Build();
|
||||
|
||||
contentTypeService.Save(contentType);
|
||||
|
||||
var content = new ContentBuilder()
|
||||
.WithId(0)
|
||||
.WithName("Invariant")
|
||||
.WithContentType(contentType)
|
||||
.AddPropertyData()
|
||||
.WithKeyValue("richText", html)
|
||||
.Done()
|
||||
.Build();
|
||||
contentService.SaveAndPublish(content);
|
||||
var model = new ContentItemSaveBuilder()
|
||||
.WithContent(content)
|
||||
.Build();
|
||||
|
||||
// Act
|
||||
var response =
|
||||
await Client.PostAsync(url, new MultipartFormDataContent {{new StringContent(JsonConvert.SerializeObject(model)), "contentItem"}});
|
||||
|
||||
// Assert
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
|
||||
body = body.TrimStart(AngularJsonMediaTypeFormatter.XsrfPrefix);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.AreEqual(HttpStatusCode.OK, response.StatusCode, body);
|
||||
var display = JsonConvert.DeserializeObject<ContentItemDisplay>(body);
|
||||
var bodyText = display.Variants.FirstOrDefault()?.Tabs.FirstOrDefault()?.Properties
|
||||
?.FirstOrDefault(x => x.Alias.Equals("richText"))?.Value?.ToString();
|
||||
Assert.NotNull(bodyText);
|
||||
|
||||
var containsDataUri = bodyText.Contains("data:image");
|
||||
if (shouldHaveDataUri)
|
||||
{
|
||||
Assert.True(containsDataUri, $"Data URIs were expected to be found in the body: {bodyText}");
|
||||
} else {
|
||||
Assert.False(containsDataUri, $"Data URIs were not expected to be found in the body: {bodyText}");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"$schema": "./appsettings-schema.json",
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Warning",
|
||||
@@ -16,5 +17,12 @@
|
||||
"EmptyDatabasesCount": 2,
|
||||
"SQLServerMasterConnectionString": ""
|
||||
}
|
||||
},
|
||||
"Umbraco": {
|
||||
"CMS": {
|
||||
"Content": {
|
||||
"AllowedUploadedFileExtensions": ["jpg", "png", "gif", "svg"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user