Files
Umbraco-CMS/src/Umbraco.Infrastructure/PropertyEditors/RichTextEditorPastedImages.cs
2023-03-14 08:27:17 +01:00

196 lines
7.8 KiB
C#

// Copyright (c) Umbraco.
// See LICENSE for more details.
using HtmlAgilityPack;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core.Exceptions;
using Umbraco.Cms.Core.Hosting;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Media;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Strings;
using Umbraco.Cms.Core.Web;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.PropertyEditors;
public sealed class RichTextEditorPastedImages
{
private const string TemporaryImageDataAttribute = "data-tmpimg";
private readonly IContentTypeBaseServiceProvider _contentTypeBaseServiceProvider;
private readonly IHostingEnvironment _hostingEnvironment;
private readonly ILogger<RichTextEditorPastedImages> _logger;
private readonly MediaFileManager _mediaFileManager;
private readonly IMediaService _mediaService;
private readonly MediaUrlGeneratorCollection _mediaUrlGenerators;
private readonly IPublishedUrlProvider _publishedUrlProvider;
private readonly IShortStringHelper _shortStringHelper;
private readonly IUmbracoContextAccessor _umbracoContextAccessor;
public RichTextEditorPastedImages(
IUmbracoContextAccessor umbracoContextAccessor,
ILogger<RichTextEditorPastedImages> logger,
IHostingEnvironment hostingEnvironment,
IMediaService mediaService,
IContentTypeBaseServiceProvider contentTypeBaseServiceProvider,
MediaFileManager mediaFileManager,
MediaUrlGeneratorCollection mediaUrlGenerators,
IShortStringHelper shortStringHelper,
IPublishedUrlProvider publishedUrlProvider)
{
_umbracoContextAccessor =
umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_hostingEnvironment = hostingEnvironment;
_mediaService = mediaService ?? throw new ArgumentNullException(nameof(mediaService));
_contentTypeBaseServiceProvider = contentTypeBaseServiceProvider ??
throw new ArgumentNullException(nameof(contentTypeBaseServiceProvider));
_mediaFileManager = mediaFileManager;
_mediaUrlGenerators = mediaUrlGenerators;
_shortStringHelper = shortStringHelper;
_publishedUrlProvider = publishedUrlProvider;
}
/// <summary>
/// Used by the RTE (and grid RTE) for drag/drop/persisting images
/// </summary>
public string FindAndPersistPastedTempImages(string html, Guid mediaParentFolder, int userId, IImageUrlGenerator imageUrlGenerator)
{
// 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? 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<string, GuidUdi>();
foreach (HtmlNode? img in tmpImages)
{
// The data attribute contains the path to the tmp img to persist as a media item
var tmpImgPath = img.GetAttributeValue(TemporaryImageDataAttribute, string.Empty);
if (string.IsNullOrEmpty(tmpImgPath))
{
continue;
}
if (IsValidPath(tmpImgPath) == false)
{
continue;
}
var absoluteTempImagePath = _hostingEnvironment.MapPathContentRoot(tmpImgPath);
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);
}
}
}
return htmlDoc.DocumentNode.OuterHtml;
}
private bool IsValidPath(string imagePath) => imagePath.StartsWith(Constants.SystemDirectories.TempImageUploads);
}