Files
Umbraco-CMS/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs
Sven Geusens ed7b4ce44f Merge branch 'v13/dev' into v14/dev
# Conflicts:
#	Directory.Packages.props
#	src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs
#	src/Umbraco.Core/Models/Mapping/ContentPropertyDisplayMapper.cs
#	src/Umbraco.Core/Services/DataTypeService.cs
#	src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyValueEditor.cs
#	src/Umbraco.Infrastructure/PropertyEditors/BlockValuePropertyValueEditorBase.cs
#	src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyValueEditor.cs
#	src/Umbraco.Infrastructure/PropertyEditors/MediaPicker3PropertyEditor.cs
#	src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs
#	src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs
2024-02-21 14:39:41 +01:00

290 lines
12 KiB
C#

// Copyright (c) Umbraco.
// See LICENSE for more details.
using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Blocks;
using Umbraco.Cms.Core.Models.Editors;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Strings;
using Umbraco.Cms.Core.Templates;
using Umbraco.Cms.Infrastructure.Macros;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.PropertyEditors;
/// <summary>
/// Represents a rich text property editor.
/// </summary>
[DataEditor(
Constants.PropertyEditors.Aliases.RichText,
ValueType = ValueTypes.Text,
ValueEditorIsReusable = true)]
public class RichTextPropertyEditor : DataEditor
{
private readonly IIOHelper _ioHelper;
private readonly IRichTextPropertyIndexValueFactory _richTextPropertyIndexValueFactory;
/// <summary>
/// The constructor will setup the property editor based on the attribute if one is found.
/// </summary>
public RichTextPropertyEditor(
IDataValueEditorFactory dataValueEditorFactory,
IIOHelper ioHelper,
IRichTextPropertyIndexValueFactory richTextPropertyIndexValueFactory)
: base(dataValueEditorFactory)
{
_ioHelper = ioHelper;
_richTextPropertyIndexValueFactory = richTextPropertyIndexValueFactory;
SupportsReadOnly = true;
}
public override IPropertyIndexValueFactory PropertyIndexValueFactory => _richTextPropertyIndexValueFactory;
/// <summary>
/// Create a custom value editor
/// </summary>
/// <returns></returns>
protected override IDataValueEditor CreateValueEditor() =>
DataValueEditorFactory.Create<RichTextPropertyValueEditor>(Attribute!);
protected override IConfigurationEditor CreateConfigurationEditor() =>
new RichTextConfigurationEditor(_ioHelper);
/// <summary>
/// A custom value editor to ensure that macro syntax is parsed when being persisted and formatted correctly for
/// display in the editor
/// </summary>
internal class RichTextPropertyValueEditor : BlockValuePropertyValueEditorBase<RichTextBlockValue, RichTextBlockLayoutItem>
{
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
private readonly IHtmlSanitizer _htmlSanitizer;
private readonly HtmlImageSourceParser _imageSourceParser;
private readonly HtmlLocalLinkParser _localLinkParser;
private readonly RichTextEditorPastedImages _pastedImages;
private readonly IJsonSerializer _jsonSerializer;
private readonly IContentTypeService _contentTypeService;
private readonly ILogger<RichTextPropertyValueEditor> _logger;
public RichTextPropertyValueEditor(
DataEditorAttribute attribute,
PropertyEditorCollection propertyEditors,
IDataTypeConfigurationCache dataTypeReadCache,
ILogger<RichTextPropertyValueEditor> logger,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
ILocalizedTextService localizedTextService,
IShortStringHelper shortStringHelper,
HtmlImageSourceParser imageSourceParser,
HtmlLocalLinkParser localLinkParser,
RichTextEditorPastedImages pastedImages,
IJsonSerializer jsonSerializer,
IIOHelper ioHelper,
IHtmlSanitizer htmlSanitizer,
IContentTypeService contentTypeService,
IPropertyValidationService propertyValidationService,
DataValueReferenceFactoryCollection dataValueReferenceFactoryCollection)
: base(attribute, propertyEditors, dataTypeReadCache, localizedTextService, logger, shortStringHelper, jsonSerializer, ioHelper, dataValueReferenceFactoryCollection)
{
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
_imageSourceParser = imageSourceParser;
_localLinkParser = localLinkParser;
_pastedImages = pastedImages;
_htmlSanitizer = htmlSanitizer;
_contentTypeService = contentTypeService;
_jsonSerializer = jsonSerializer;
_logger = logger;
Validators.Add(new RichTextEditorBlockValidator(propertyValidationService, CreateBlockEditorValues(), contentTypeService, jsonSerializer, logger));
}
/// <inheritdoc />
public override object? ConfigurationObject
{
get => base.ConfigurationObject;
set
{
if (value == null)
{
throw new ArgumentNullException(nameof(value));
}
if (!(value is RichTextConfiguration configuration))
{
throw new ArgumentException(
$"Expected a {typeof(RichTextConfiguration).Name} instance, but got {value.GetType().Name}.",
nameof(value));
}
base.ConfigurationObject = value;
}
}
/// <summary>
/// Resolve references from <see cref="IDataValueEditor" /> values
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
public override IEnumerable<UmbracoEntityReference> GetReferences(object? value)
{
if (TryParseEditorValue(value, out RichTextEditorValue? richTextEditorValue) is false)
{
return Array.Empty<UmbracoEntityReference>();
}
var references = new List<UmbracoEntityReference>();
// image references from markup
references.AddRange(_imageSourceParser
.FindUdisFromDataAttributes(richTextEditorValue.Markup)
.Select(udi => new UmbracoEntityReference(udi)));
// local link references from markup
references.AddRange(_localLinkParser
.FindUdisFromLocalLinks(richTextEditorValue.Markup)
.WhereNotNull()
.Select(udi => new UmbracoEntityReference(udi)));
// references from blocks
if (richTextEditorValue.Blocks is not null)
{
BlockEditorData<RichTextBlockValue, RichTextBlockLayoutItem>? blockEditorData = ConvertAndClean(richTextEditorValue.Blocks);
if (blockEditorData is not null)
{
references.AddRange(GetBlockValueReferences(blockEditorData.BlockValue));
}
}
return references;
}
public override IEnumerable<ITag> GetTags(object? value, object? dataTypeConfiguration, int? languageId)
{
if (TryParseEditorValue(value, out RichTextEditorValue? richTextEditorValue) is false || richTextEditorValue.Blocks is null)
{
return Array.Empty<ITag>();
}
BlockEditorData<RichTextBlockValue, RichTextBlockLayoutItem>? blockEditorData = ConvertAndClean(richTextEditorValue.Blocks);
if (blockEditorData is null)
{
return Array.Empty<ITag>();
}
return GetBlockValueTags(blockEditorData.BlockValue, languageId);
}
/// <summary>
/// Format the data for the editor
/// </summary>
/// <param name="property"></param>
/// <param name="culture"></param>
/// <param name="segment"></param>
public override object? ToEditor(IProperty property, string? culture = null, string? segment = null)
{
var value = property.GetValue(culture, segment);
if (TryParseEditorValue(value, out RichTextEditorValue? richTextEditorValue) is false)
{
return null;
}
var propertyValueWithMediaResolved = _imageSourceParser.EnsureImageSources(richTextEditorValue.Markup);
var parsed = MacroTagParser.FormatRichTextPersistedDataForEditor(
propertyValueWithMediaResolved,
new Dictionary<string, string>());
richTextEditorValue.Markup = parsed;
// return json convertable object
return CleanAndMapBlocks(richTextEditorValue, blockValue => MapBlockValueToEditor(property, blockValue));
}
/// <summary>
/// Format the data for persistence
/// </summary>
/// <param name="editorValue"></param>
/// <param name="currentValue"></param>
/// <returns></returns>
public override object? FromEditor(ContentPropertyData editorValue, object? currentValue)
{
if (TryParseEditorValue(editorValue.Value, out RichTextEditorValue? richTextEditorValue) is false)
{
return null;
}
Guid userKey = _backOfficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.Key ??
Constants.Security.SuperUserKey;
var config = editorValue.DataTypeConfiguration as RichTextConfiguration;
GuidUdi? mediaParent = config?.MediaParentId;
Guid mediaParentId = mediaParent == null ? Guid.Empty : mediaParent.Guid;
if (string.IsNullOrWhiteSpace(richTextEditorValue.Markup))
{
return null;
}
var parseAndSaveBase64Images = _pastedImages.FindAndPersistEmbeddedImages(
richTextEditorValue.Markup, mediaParentId, userKey);
var parseAndSavedTempImages = _pastedImages
.FindAndPersistPastedTempImagesAsync(parseAndSaveBase64Images, mediaParentId, userKey)
.GetAwaiter()
.GetResult();
var editorValueWithMediaUrlsRemoved = _imageSourceParser.RemoveImageSources(parseAndSavedTempImages);
var parsed = MacroTagParser.FormatRichTextContentForPersistence(editorValueWithMediaUrlsRemoved);
var sanitized = _htmlSanitizer.Sanitize(parsed);
richTextEditorValue.Markup = sanitized.NullOrWhiteSpaceAsNull() ?? string.Empty;
RichTextEditorValue cleanedUpRichTextEditorValue = CleanAndMapBlocks(richTextEditorValue, MapBlockValueFromEditor);
// return json
return RichTextPropertyEditorHelper.SerializeRichTextEditorValue(cleanedUpRichTextEditorValue, _jsonSerializer);
}
private bool TryParseEditorValue(object? value, [NotNullWhen(true)] out RichTextEditorValue? richTextEditorValue)
=> RichTextPropertyEditorHelper.TryParseRichTextEditorValue(value, _jsonSerializer, _logger, out richTextEditorValue);
private RichTextEditorValue CleanAndMapBlocks(RichTextEditorValue richTextEditorValue, Action<RichTextBlockValue> handleMapping)
{
if (richTextEditorValue.Blocks is null)
{
// no blocks defined, store empty block value
return MarkupWithEmptyBlocks();
}
BlockEditorData<RichTextBlockValue, RichTextBlockLayoutItem>? blockEditorData = ConvertAndClean(richTextEditorValue.Blocks);
if (blockEditorData is not null)
{
handleMapping(blockEditorData.BlockValue);
return new RichTextEditorValue
{
Markup = richTextEditorValue.Markup, Blocks = blockEditorData.BlockValue
};
}
// could not deserialize the blocks or handle the mapping, store empty block value
return MarkupWithEmptyBlocks();
RichTextEditorValue MarkupWithEmptyBlocks() => new()
{
Markup = richTextEditorValue.Markup, Blocks = new RichTextBlockValue()
};
}
private BlockEditorData<RichTextBlockValue, RichTextBlockLayoutItem>? ConvertAndClean(RichTextBlockValue blockValue)
{
BlockEditorValues<RichTextBlockValue, RichTextBlockLayoutItem> blockEditorValues = CreateBlockEditorValues();
return blockEditorValues.ConvertAndClean(blockValue);
}
private BlockEditorValues<RichTextBlockValue, RichTextBlockLayoutItem> CreateBlockEditorValues()
=> new(new RichTextEditorBlockDataConverter(), _contentTypeService, _logger);
}
}