334 lines
13 KiB
C#
334 lines
13 KiB
C#
// Copyright (c) Umbraco.
|
|
// See LICENSE for more details.
|
|
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Umbraco.Cms.Core.DependencyInjection;
|
|
using Umbraco.Cms.Core.IO;
|
|
using Umbraco.Cms.Core.Media;
|
|
using Umbraco.Cms.Core.Models;
|
|
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.Examine;
|
|
using Umbraco.Cms.Infrastructure.Macros;
|
|
using Umbraco.Cms.Infrastructure.Templates;
|
|
using Umbraco.Extensions;
|
|
|
|
namespace Umbraco.Cms.Core.PropertyEditors;
|
|
|
|
/// <summary>
|
|
/// Represents a rich text property editor.
|
|
/// </summary>
|
|
[DataEditor(
|
|
Constants.PropertyEditors.Aliases.TinyMce,
|
|
"Rich Text Editor",
|
|
"rte",
|
|
ValueType = ValueTypes.Text,
|
|
HideLabel = false,
|
|
Group = Constants.PropertyEditors.Groups.RichContent,
|
|
Icon = "icon-browser-window",
|
|
ValueEditorIsReusable = true)]
|
|
public class RichTextPropertyEditor : DataEditor
|
|
{
|
|
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
|
|
private readonly IEditorConfigurationParser _editorConfigurationParser;
|
|
private readonly HtmlImageSourceParser _imageSourceParser;
|
|
private readonly IImageUrlGenerator _imageUrlGenerator;
|
|
private readonly IIOHelper _ioHelper;
|
|
private readonly HtmlLocalLinkParser _localLinkParser;
|
|
private readonly IHtmlMacroParameterParser _macroParameterParser;
|
|
private readonly RichTextEditorPastedImages _pastedImages;
|
|
|
|
[Obsolete("Use the constructor which takes an IHtmlMacroParameterParser instead")]
|
|
public RichTextPropertyEditor(
|
|
IDataValueEditorFactory dataValueEditorFactory,
|
|
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
|
|
HtmlImageSourceParser imageSourceParser,
|
|
HtmlLocalLinkParser localLinkParser,
|
|
RichTextEditorPastedImages pastedImages,
|
|
IIOHelper ioHelper,
|
|
IImageUrlGenerator imageUrlGenerator,
|
|
IHtmlMacroParameterParser macroParameterParser)
|
|
: this(
|
|
dataValueEditorFactory,
|
|
backOfficeSecurityAccessor,
|
|
imageSourceParser,
|
|
localLinkParser,
|
|
pastedImages,
|
|
ioHelper,
|
|
imageUrlGenerator,
|
|
macroParameterParser,
|
|
StaticServiceProvider.Instance.GetRequiredService<IEditorConfigurationParser>())
|
|
{
|
|
}
|
|
|
|
[Obsolete("Use the constructor which takes an IHtmlMacroParameterParser instead")]
|
|
public RichTextPropertyEditor(
|
|
IDataValueEditorFactory dataValueEditorFactory,
|
|
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
|
|
HtmlImageSourceParser imageSourceParser,
|
|
HtmlLocalLinkParser localLinkParser,
|
|
RichTextEditorPastedImages pastedImages,
|
|
IIOHelper ioHelper,
|
|
IImageUrlGenerator imageUrlGenerator)
|
|
: this(
|
|
dataValueEditorFactory,
|
|
backOfficeSecurityAccessor,
|
|
imageSourceParser,
|
|
localLinkParser,
|
|
pastedImages,
|
|
ioHelper,
|
|
imageUrlGenerator,
|
|
StaticServiceProvider.Instance.GetRequiredService<IHtmlMacroParameterParser>(),
|
|
StaticServiceProvider.Instance.GetRequiredService<IEditorConfigurationParser>())
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// The constructor will setup the property editor based on the attribute if one is found.
|
|
/// </summary>
|
|
public RichTextPropertyEditor(
|
|
IDataValueEditorFactory dataValueEditorFactory,
|
|
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
|
|
HtmlImageSourceParser imageSourceParser,
|
|
HtmlLocalLinkParser localLinkParser,
|
|
RichTextEditorPastedImages pastedImages,
|
|
IIOHelper ioHelper,
|
|
IImageUrlGenerator imageUrlGenerator,
|
|
IHtmlMacroParameterParser macroParameterParser,
|
|
IEditorConfigurationParser editorConfigurationParser)
|
|
: base(dataValueEditorFactory)
|
|
{
|
|
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
|
|
_imageSourceParser = imageSourceParser;
|
|
_localLinkParser = localLinkParser;
|
|
_pastedImages = pastedImages;
|
|
_ioHelper = ioHelper;
|
|
_imageUrlGenerator = imageUrlGenerator;
|
|
_macroParameterParser = macroParameterParser;
|
|
_editorConfigurationParser = editorConfigurationParser;
|
|
SupportsReadOnly = true;
|
|
}
|
|
|
|
public override IPropertyIndexValueFactory PropertyIndexValueFactory => new 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, _editorConfigurationParser);
|
|
|
|
/// <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 : DataValueEditor, IDataValueReference
|
|
{
|
|
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
|
|
private readonly IHtmlSanitizer _htmlSanitizer;
|
|
private readonly HtmlImageSourceParser _imageSourceParser;
|
|
private readonly IImageUrlGenerator _imageUrlGenerator;
|
|
private readonly HtmlLocalLinkParser _localLinkParser;
|
|
private readonly IHtmlMacroParameterParser _macroParameterParser;
|
|
private readonly RichTextEditorPastedImages _pastedImages;
|
|
|
|
public RichTextPropertyValueEditor(
|
|
DataEditorAttribute attribute,
|
|
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
|
|
ILocalizedTextService localizedTextService,
|
|
IShortStringHelper shortStringHelper,
|
|
HtmlImageSourceParser imageSourceParser,
|
|
HtmlLocalLinkParser localLinkParser,
|
|
RichTextEditorPastedImages pastedImages,
|
|
IImageUrlGenerator imageUrlGenerator,
|
|
IJsonSerializer jsonSerializer,
|
|
IIOHelper ioHelper,
|
|
IHtmlSanitizer htmlSanitizer,
|
|
IHtmlMacroParameterParser macroParameterParser)
|
|
: base(localizedTextService, shortStringHelper, jsonSerializer, ioHelper, attribute)
|
|
{
|
|
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
|
|
_imageSourceParser = imageSourceParser;
|
|
_localLinkParser = localLinkParser;
|
|
_pastedImages = pastedImages;
|
|
_imageUrlGenerator = imageUrlGenerator;
|
|
_htmlSanitizer = htmlSanitizer;
|
|
_macroParameterParser = macroParameterParser;
|
|
}
|
|
|
|
[Obsolete("Use the constructor which takes an HtmlMacroParameterParser instead")]
|
|
public RichTextPropertyValueEditor(
|
|
DataEditorAttribute attribute,
|
|
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
|
|
ILocalizedTextService localizedTextService,
|
|
IShortStringHelper shortStringHelper,
|
|
HtmlImageSourceParser imageSourceParser,
|
|
HtmlLocalLinkParser localLinkParser,
|
|
RichTextEditorPastedImages pastedImages,
|
|
IImageUrlGenerator imageUrlGenerator,
|
|
IJsonSerializer jsonSerializer,
|
|
IIOHelper ioHelper,
|
|
IHtmlSanitizer htmlSanitizer)
|
|
: this(
|
|
attribute,
|
|
backOfficeSecurityAccessor,
|
|
localizedTextService,
|
|
shortStringHelper,
|
|
imageSourceParser,
|
|
localLinkParser,
|
|
pastedImages,
|
|
imageUrlGenerator,
|
|
jsonSerializer,
|
|
ioHelper,
|
|
htmlSanitizer,
|
|
StaticServiceProvider.Instance.GetRequiredService<IHtmlMacroParameterParser>())
|
|
{
|
|
}
|
|
|
|
/// <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;
|
|
|
|
HideLabel = configuration.HideLabel;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resolve references from <see cref="IDataValueEditor" /> values
|
|
/// </summary>
|
|
/// <param name="value"></param>
|
|
/// <returns></returns>
|
|
public IEnumerable<UmbracoEntityReference> GetReferences(object? value)
|
|
{
|
|
var asString = value == null ? string.Empty : value is string str ? str : value.ToString()!;
|
|
|
|
foreach (Udi udi in _imageSourceParser.FindUdisFromDataAttributes(asString))
|
|
{
|
|
yield return new UmbracoEntityReference(udi);
|
|
}
|
|
|
|
foreach (Udi? udi in _localLinkParser.FindUdisFromLocalLinks(asString))
|
|
{
|
|
if (udi is not null)
|
|
{
|
|
yield return new UmbracoEntityReference(udi);
|
|
}
|
|
}
|
|
|
|
// TODO: Detect Macros too ... but we can save that for a later date, right now need to do media refs
|
|
// UPDATE: We are getting the Macros in 'FindUmbracoEntityReferencesFromEmbeddedMacros' - perhaps we just return the macro Udis here too or do they need their own relationAlias?
|
|
foreach (UmbracoEntityReference umbracoEntityReference in _macroParameterParser
|
|
.FindUmbracoEntityReferencesFromEmbeddedMacros(asString))
|
|
{
|
|
yield return umbracoEntityReference;
|
|
}
|
|
}
|
|
|
|
/// <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 val = property.GetValue(culture, segment);
|
|
if (val == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var propertyValueWithMediaResolved = _imageSourceParser.EnsureImageSources(val.ToString()!);
|
|
var parsed = MacroTagParser.FormatRichTextPersistedDataForEditor(
|
|
propertyValueWithMediaResolved,
|
|
new Dictionary<string, string>());
|
|
return parsed;
|
|
}
|
|
|
|
/// <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 (editorValue.Value == null)
|
|
{
|
|
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(editorValue.Value.ToString()))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var parseAndSavedTempImages = _pastedImages
|
|
.FindAndPersistPastedTempImagesAsync(editorValue.Value.ToString()!, mediaParentId, userKey, _imageUrlGenerator)
|
|
.GetAwaiter()
|
|
.GetResult();
|
|
var editorValueWithMediaUrlsRemoved = _imageSourceParser.RemoveImageSources(parseAndSavedTempImages);
|
|
var parsed = MacroTagParser.FormatRichTextContentForPersistence(editorValueWithMediaUrlsRemoved);
|
|
var sanitized = _htmlSanitizer.Sanitize(parsed);
|
|
|
|
return sanitized.NullOrWhiteSpaceAsNull();
|
|
}
|
|
}
|
|
|
|
internal class RichTextPropertyIndexValueFactory : IPropertyIndexValueFactory
|
|
{
|
|
public IEnumerable<KeyValuePair<string, IEnumerable<object?>>> GetIndexValues(IProperty property, string? culture, string? segment, bool published, IEnumerable<string> availableCultures)
|
|
{
|
|
var val = property.GetValue(culture, segment, published);
|
|
|
|
if (!(val is string strVal))
|
|
{
|
|
yield break;
|
|
}
|
|
|
|
// index the stripped HTML values
|
|
yield return new KeyValuePair<string, IEnumerable<object?>>(
|
|
property.Alias,
|
|
new object[] { strVal.StripHtml() });
|
|
|
|
// store the raw value
|
|
yield return new KeyValuePair<string, IEnumerable<object?>>(
|
|
$"{UmbracoExamineFieldNames.RawFieldPrefix}{property.Alias}", new object[] { strVal });
|
|
}
|
|
|
|
[Obsolete("Use the overload with the 'availableCultures' parameter instead, scheduled for removal in v14")]
|
|
public IEnumerable<KeyValuePair<string, IEnumerable<object?>>> GetIndexValues(IProperty property, string? culture, string? segment, bool published)
|
|
=> GetIndexValues(property, culture, segment, published);
|
|
}
|
|
}
|