# 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
290 lines
12 KiB
C#
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);
|
|
}
|
|
}
|