Moved PropertyEditors from Umbraco.Core to Umbraco.Infrastructure

This commit is contained in:
Bjarke Berg
2019-12-09 09:00:00 +01:00
parent d90534769c
commit 2ea8e7cdd0
104 changed files with 377 additions and 204 deletions

View File

@@ -0,0 +1,57 @@
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Xml;
using Newtonsoft.Json;
using Umbraco.Core.Collections;
namespace Umbraco.Core
{
/// <summary>
/// Provides object extension methods.
/// </summary>
public static class ObjectJsonExtensions
{
private static readonly ConcurrentDictionary<Type, Dictionary<string, object>> ToObjectTypes = new ConcurrentDictionary<Type, Dictionary<string, object>>();
/// <summary>
/// Converts an object's properties into a dictionary.
/// </summary>
/// <param name="obj">The object to convert.</param>
/// <param name="namer">A property namer function.</param>
/// <returns>A dictionary containing each properties.</returns>
public static Dictionary<string, object> ToObjectDictionary<T>(T obj, Func<PropertyInfo, string> namer = null)
{
if (obj == null) return new Dictionary<string, object>();
string DefaultNamer(PropertyInfo property)
{
var jsonProperty = property.GetCustomAttribute<JsonPropertyAttribute>();
return jsonProperty?.PropertyName ?? property.Name;
}
var t = obj.GetType();
if (namer == null) namer = DefaultNamer;
if (!ToObjectTypes.TryGetValue(t, out var properties))
{
properties = new Dictionary<string, object>();
foreach (var p in t.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy))
properties[namer(p)] = ReflectionUtilities.EmitPropertyGetter<T, object>(p);
ToObjectTypes[t] = properties;
}
return properties.ToDictionary(x => x.Key, x => ((Func<T, object>) x.Value)(obj));
}
}
}

View File

@@ -0,0 +1,154 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using Umbraco.Core.Serialization;
namespace Umbraco.Core.PropertyEditors
{
/// <summary>
/// Represents a data type configuration editor.
/// </summary>
public class ConfigurationEditor : IConfigurationEditor
{
private IDictionary<string, object> _defaultConfiguration;
/// <summary>
/// Initializes a new instance of the <see cref="ConfigurationEditor"/> class.
/// </summary>
public ConfigurationEditor()
{
Fields = new List<ConfigurationField>();
_defaultConfiguration = new Dictionary<string, object>();
}
/// <summary>
/// Initializes a new instance of the <see cref="ConfigurationEditor"/> class.
/// </summary>
protected ConfigurationEditor(List<ConfigurationField> fields)
{
Fields = fields;
}
/// <summary>
/// Gets the fields.
/// </summary>
[JsonProperty("fields")]
public List<ConfigurationField> Fields { get; }
/// <summary>
/// Gets a field by its property name.
/// </summary>
/// <remarks>Can be used in constructors to add infos to a field that has been defined
/// by a property marked with the <see cref="ConfigurationFieldAttribute"/>.</remarks>
protected ConfigurationField Field(string name)
=> Fields.First(x => x.PropertyName == name);
/// <summary>
/// Gets the configuration as a typed object.
/// </summary>
public static TConfiguration ConfigurationAs<TConfiguration>(object obj)
{
if (obj == null) return default;
if (obj is TConfiguration configuration) return configuration;
throw new InvalidCastException($"Cannot cast configuration of type {obj.GetType().Name} to {typeof(TConfiguration).Name}.");
}
/// <summary>
/// Converts a configuration object into a serialized database value.
/// </summary>
public static string ToDatabase(object configuration)
=> configuration == null ? null : JsonConvert.SerializeObject(configuration, ConfigurationJsonSettings);
/// <inheritdoc />
[JsonProperty("defaultConfig")]
public virtual IDictionary<string, object> DefaultConfiguration {
get => _defaultConfiguration;
set => _defaultConfiguration = value;
}
/// <inheritdoc />
public virtual object DefaultConfigurationObject => DefaultConfiguration;
/// <inheritdoc />
public virtual bool IsConfiguration(object obj) => obj is IDictionary<string, object>;
/// <inheritdoc />
public virtual object FromDatabase(string configurationJson)
=> string.IsNullOrWhiteSpace(configurationJson)
? new Dictionary<string, object>()
: JsonConvert.DeserializeObject<Dictionary<string, object>>(configurationJson);
/// <inheritdoc />
public virtual object FromConfigurationEditor(IDictionary<string, object> editorValues, object configuration)
{
// by default, return the posted dictionary
// but only keep entries that have a non-null/empty value
// rest will fall back to default during ToConfigurationEditor()
var keys = editorValues.Where(x =>
x.Value == null || x.Value is string stringValue && string.IsNullOrWhiteSpace(stringValue))
.Select(x => x.Key).ToList();
foreach (var key in keys) editorValues.Remove(key);
return editorValues;
}
/// <inheritdoc />
public virtual IDictionary<string, object> ToConfigurationEditor(object configuration)
{
// editors that do not override ToEditor/FromEditor have their configuration
// as a dictionary of <string, object> and, by default, we merge their default
// configuration with their current configuration
if (configuration == null)
configuration = new Dictionary<string, object>();
if (!(configuration is IDictionary<string, object> c))
throw new ArgumentException($"Expecting a {typeof(Dictionary<string,object>).Name} instance but got {configuration.GetType().Name}.", nameof(configuration));
// clone the default configuration, and apply the current configuration values
var d = new Dictionary<string, object>(DefaultConfiguration);
foreach (var (key, value) in c)
d[key] = value;
return d;
}
/// <inheritdoc />
public virtual IDictionary<string, object> ToValueEditor(object configuration)
=> ToConfigurationEditor(configuration);
/// <summary>
/// Gets the custom json serializer settings for configurations.
/// </summary>
public static JsonSerializerSettings ConfigurationJsonSettings { get; } = new JsonSerializerSettings
{
ContractResolver = new ConfigurationCustomContractResolver(),
Converters = new List<JsonConverter>(new[]{new FuzzyBooleanConverter()})
};
private class ConfigurationCustomContractResolver : DefaultContractResolver
{
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
// base.CreateProperty deals with [JsonProperty("name")]
var property = base.CreateProperty(member, memberSerialization);
// override with our custom attribute, if any
var attribute = member.GetCustomAttribute<ConfigurationFieldAttribute>();
if (attribute != null) property.PropertyName = attribute.Key;
// for value types,
// don't try to deserialize nulls (in legacy json)
// no impact on serialization (value cannot be null)
if (member is PropertyInfo propertyInfo && propertyInfo.PropertyType.IsValueType)
property.NullValueHandling = NullValueHandling.Ignore;
return property;
}
}
}
}

View File

@@ -0,0 +1,197 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Umbraco.Core.Composing;
using Umbraco.Core.IO;
namespace Umbraco.Core.PropertyEditors
{
/// <summary>
/// Represents a data type configuration editor with a typed configuration.
/// </summary>
public abstract class ConfigurationEditor<TConfiguration> : ConfigurationEditor
where TConfiguration : new()
{
/// <summary>
/// Initializes a new instance of the <see cref="ConfigurationEditor{TConfiguration}"/> class.
/// </summary>
protected ConfigurationEditor(IIOHelper ioHelper)
: base(DiscoverFields(ioHelper))
{ }
/// <summary>
/// Discovers fields from configuration properties marked with the field attribute.
/// </summary>
private static List<ConfigurationField> DiscoverFields(IIOHelper ioHelper)
{
var fields = new List<ConfigurationField>();
var properties = TypeHelper.CachedDiscoverableProperties(typeof(TConfiguration));
foreach (var property in properties)
{
var attribute = property.GetCustomAttribute<ConfigurationFieldAttribute>(false);
if (attribute == null) continue;
ConfigurationField field;
var attributeView = ioHelper.ResolveVirtualUrl(attribute.View);
// if the field does not have its own type, use the base type
if (attribute.Type == null)
{
field = new ConfigurationField
{
// if the key is empty then use the property name
Key = string.IsNullOrWhiteSpace(attribute.Key) ? property.Name : attribute.Key,
Name = attribute.Name,
PropertyName = property.Name,
PropertyType = property.PropertyType,
Description = attribute.Description,
HideLabel = attribute.HideLabel,
View = attributeView
};
fields.Add(field);
continue;
}
// if the field has its own type, instantiate it
try
{
field = (ConfigurationField) Activator.CreateInstance(attribute.Type);
}
catch (Exception ex)
{
throw new Exception($"Failed to create an instance of type \"{attribute.Type}\" for property \"{property.Name}\" of configuration \"{typeof(TConfiguration).Name}\" (see inner exception).", ex);
}
// then add it, and overwrite values if they are assigned in the attribute
fields.Add(field);
field.PropertyName = property.Name;
field.PropertyType = property.PropertyType;
if (!string.IsNullOrWhiteSpace(attribute.Key))
field.Key = attribute.Key;
// if the key is still empty then use the property name
if (string.IsNullOrWhiteSpace(field.Key))
field.Key = property.Name;
if (!string.IsNullOrWhiteSpace(attribute.Name))
field.Name = attribute.Name;
if (!string.IsNullOrWhiteSpace(attribute.View))
field.View = attributeView;
if (!string.IsNullOrWhiteSpace(attribute.Description))
field.Description = attribute.Description;
if (attribute.HideLabelSettable.HasValue)
field.HideLabel = attribute.HideLabel;
}
return fields;
}
/// <inheritdoc />
public override IDictionary<string, object> DefaultConfiguration => ToConfigurationEditor(DefaultConfigurationObject);
/// <inheritdoc />
public override object DefaultConfigurationObject => new TConfiguration();
/// <inheritdoc />
public override bool IsConfiguration(object obj)
=> obj is TConfiguration;
/// <inheritdoc />
public override object FromDatabase(string configuration)
{
try
{
if (string.IsNullOrWhiteSpace(configuration)) return new TConfiguration();
return JsonConvert.DeserializeObject<TConfiguration>(configuration, ConfigurationJsonSettings);
}
catch (Exception e)
{
throw new InvalidOperationException($"Failed to parse configuration \"{configuration}\" as \"{typeof(TConfiguration).Name}\" (see inner exception).", e);
}
}
/// <inheritdoc />
public sealed override object FromConfigurationEditor(IDictionary<string, object> editorValues, object configuration)
{
return FromConfigurationEditor(editorValues, (TConfiguration) configuration);
}
/// <summary>
/// Converts the configuration posted by the editor.
/// </summary>
/// <param name="editorValues">The configuration object posted by the editor.</param>
/// <param name="configuration">The current configuration object.</param>
public virtual TConfiguration FromConfigurationEditor(IDictionary<string, object> editorValues, TConfiguration configuration)
{
// note - editorValue contains a mix of CLR types (string, int...) and JToken
// turning everything back into a JToken... might not be fastest but is simplest
// for now
var o = new JObject();
foreach (var field in Fields)
{
// field only, JsonPropertyAttribute is ignored here
// only keep fields that have a non-null/empty value
// rest will fall back to default during ToObject()
if (editorValues.TryGetValue(field.Key, out var value) && value != null && (!(value is string stringValue) || !string.IsNullOrWhiteSpace(stringValue)))
{
if (value is JToken jtoken)
{
//if it's a jtoken then set it
o[field.PropertyName] = jtoken;
}
else if (field.PropertyType == typeof(bool) && value is string sBool)
{
//if it's a boolean property type but a string is found, try to do a conversion
var converted = sBool.TryConvertTo<bool>();
if (converted)
o[field.PropertyName] = converted.Result;
}
else
{
//default behavior
o[field.PropertyName] = JToken.FromObject(value);
}
}
}
return o.ToObject<TConfiguration>();
}
/// <inheritdoc />
public sealed override IDictionary<string, object> ToConfigurationEditor(object configuration)
{
return ToConfigurationEditor((TConfiguration) configuration);
}
/// <summary>
/// Converts configuration values to values for the editor.
/// </summary>
/// <param name="configuration">The configuration.</param>
public virtual Dictionary<string, object> ToConfigurationEditor(TConfiguration configuration)
{
string FieldNamer(PropertyInfo property)
{
// try the field
var field = property.GetCustomAttribute<ConfigurationFieldAttribute>();
if (field != null) return field.Key;
// but the property may not be a field just an extra thing
var json = property.GetCustomAttribute<JsonPropertyAttribute>();
return json?.PropertyName ?? property.Name;
}
return ObjectJsonExtensions.ToObjectDictionary(configuration, FieldNamer);
}
}
}

View File

@@ -0,0 +1,203 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.Serialization;
using Umbraco.Composing;
using Umbraco.Core.Composing;
using Umbraco.Core.Logging;
using Umbraco.Core.Services;
using Umbraco.Core.Strings;
namespace Umbraco.Core.PropertyEditors
{
/// <summary>
/// Represents a data editor.
/// </summary>
/// <remarks>
/// <para>Editors can be deserialized from e.g. manifests, which is. why the class is not abstract,
/// the json serialization attributes are required, and the properties have an internal setter.</para>
/// </remarks>
[DebuggerDisplay("{" + nameof(DebuggerDisplay) + "(),nq}")]
[HideFromTypeFinder]
[DataContract]
public class DataEditor : IDataEditor
{
private readonly IDataTypeService _dataTypeService;
private readonly ILocalizationService _localizationService;
private readonly IShortStringHelper _shortStringHelper;
private IDictionary<string, object> _defaultConfiguration;
/// <summary>
/// Initializes a new instance of the <see cref="DataEditor"/> class.
/// </summary>
public DataEditor(ILogger logger, IDataTypeService dataTypeService, ILocalizationService localizationService, IShortStringHelper shortStringHelper, EditorType type = EditorType.PropertyValue)
{
_dataTypeService = dataTypeService;
_localizationService = localizationService;
_shortStringHelper = shortStringHelper;
Logger = logger ?? throw new ArgumentNullException(nameof(logger));
// defaults
Type = type;
Icon = Constants.Icons.PropertyEditor;
Group = Constants.PropertyEditors.Groups.Common;
// assign properties based on the attribute, if it is found
Attribute = GetType().GetCustomAttribute<DataEditorAttribute>(false);
if (Attribute == null) return;
Alias = Attribute.Alias;
Type = Attribute.Type;
Name = Attribute.Name;
Icon = Attribute.Icon;
Group = Attribute.Group;
IsDeprecated = Attribute.IsDeprecated;
}
/// <summary>
/// Gets the editor attribute.
/// </summary>
protected DataEditorAttribute Attribute { get; }
/// <summary>
/// Gets a logger.
/// </summary>
protected ILogger Logger { get; }
/// <inheritdoc />
[DataMember(Name = "alias", IsRequired = true)]
public string Alias { get; internal set; }
/// <inheritdoc />
[IgnoreDataMember]
public EditorType Type { get; }
/// <inheritdoc />
[DataMember(Name = "name", IsRequired = true)]
public string Name { get; internal set; }
/// <inheritdoc />
[DataMember(Name = "icon")]
public string Icon { get; internal set; }
/// <inheritdoc />
[DataMember(Name = "group")]
public string Group { get; internal set; }
/// <inheritdoc />
[IgnoreDataMember]
public bool IsDeprecated { get; }
/// <inheritdoc />
/// <remarks>
/// <para>If an explicit value editor has been assigned, then this explicit
/// instance is returned. Otherwise, a new instance is created by CreateValueEditor.</para>
/// <para>The instance created by CreateValueEditor is not cached, i.e.
/// a new instance is created each time the property value is retrieved. The
/// property editor is a singleton, and the value editor cannot be a singleton
/// since it depends on the datatype configuration.</para>
/// <para>Technically, it could be cached by datatype but let's keep things
/// simple enough for now.</para>
/// </remarks>
// TODO: point of that one? shouldn't we always configure?
public IDataValueEditor GetValueEditor() => ExplicitValueEditor ?? CreateValueEditor();
/// <inheritdoc />
/// <remarks>
/// <para>If an explicit value editor has been assigned, then this explicit
/// instance is returned. Otherwise, a new instance is created by CreateValueEditor,
/// and configured with the configuration.</para>
/// <para>The instance created by CreateValueEditor is not cached, i.e.
/// a new instance is created each time the property value is retrieved. The
/// property editor is a singleton, and the value editor cannot be a singleton
/// since it depends on the datatype configuration.</para>
/// <para>Technically, it could be cached by datatype but let's keep things
/// simple enough for now.</para>
/// </remarks>
public IDataValueEditor GetValueEditor(object configuration)
{
// if an explicit value editor has been set (by the manifest parser)
// then return it, and ignore the configuration, which is going to be
// empty anyways
if (ExplicitValueEditor != null)
return ExplicitValueEditor;
var editor = CreateValueEditor();
((DataValueEditor) editor).Configuration = configuration; // TODO: casting is bad
return editor;
}
/// <summary>
/// Gets or sets an explicit value editor.
/// </summary>
/// <remarks>Used for manifest data editors.</remarks>
[DataMember(Name = "editor")]
public IDataValueEditor ExplicitValueEditor { get; set; }
/// <inheritdoc />
/// <remarks>
/// <para>If an explicit configuration editor has been assigned, then this explicit
/// instance is returned. Otherwise, a new instance is created by CreateConfigurationEditor.</para>
/// <para>The instance created by CreateConfigurationEditor is not cached, i.e.
/// a new instance is created each time. The property editor is a singleton, and although the
/// configuration editor could technically be a singleton too, we'd rather not keep configuration editor
/// cached.</para>
/// </remarks>
public IConfigurationEditor GetConfigurationEditor() => ExplicitConfigurationEditor ?? CreateConfigurationEditor();
/// <summary>
/// Gets or sets an explicit configuration editor.
/// </summary>
/// <remarks>Used for manifest data editors.</remarks>
[DataMember(Name = "config")]
public IConfigurationEditor ExplicitConfigurationEditor { get; set; }
/// <inheritdoc />
[DataMember(Name = "defaultConfig")]
public IDictionary<string, object> DefaultConfiguration
{
// for property value editors, get the ConfigurationEditor.DefaultConfiguration
// else fallback to a default, empty dictionary
get => _defaultConfiguration ?? ((Type & EditorType.PropertyValue) > 0 ? GetConfigurationEditor().DefaultConfiguration : (_defaultConfiguration = new Dictionary<string, object>()));
set => _defaultConfiguration = value;
}
/// <inheritdoc />
public virtual IPropertyIndexValueFactory PropertyIndexValueFactory => new DefaultPropertyIndexValueFactory();
/// <summary>
/// Creates a value editor instance.
/// </summary>
/// <returns></returns>
protected virtual IDataValueEditor CreateValueEditor()
{
if (Attribute == null)
throw new InvalidOperationException("The editor does not specify a view.");
return new DataValueEditor(_dataTypeService, _localizationService, _shortStringHelper, Attribute);
}
/// <summary>
/// Creates a configuration editor instance.
/// </summary>
protected virtual IConfigurationEditor CreateConfigurationEditor()
{
var editor = new ConfigurationEditor();
// pass the default configuration if this is not a property value editor
if((Type & EditorType.PropertyValue) == 0)
{
editor.DefaultConfiguration = _defaultConfiguration;
}
return editor;
}
/// <summary>
/// Provides a summary of the PropertyEditor for use with the <see cref="DebuggerDisplayAttribute"/>.
/// </summary>
protected virtual string DebuggerDisplay()
{
return $"Name: {Name}, Alias: {Alias}";
}
}
}

View File

@@ -0,0 +1,372 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Linq;
using System.Xml.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Umbraco.Composing;
using Umbraco.Core.Composing;
using Umbraco.Core.Logging;
using Umbraco.Core.Models;
using Umbraco.Core.Models.Editors;
using Umbraco.Core.PropertyEditors.Validators;
using Umbraco.Core.Services;
using Umbraco.Core.Strings;
namespace Umbraco.Core.PropertyEditors
{
/// <summary>
/// Represents a value editor.
/// </summary>
public class DataValueEditor : IDataValueEditor
{
private readonly ILocalizedTextService _localizedTextService;
private readonly IShortStringHelper _shortStringHelper;
protected IDataTypeService DataTypeService { get; }
protected ILocalizationService LocalizationService { get; }
/// <summary>
/// Initializes a new instance of the <see cref="DataValueEditor"/> class.
/// </summary>
public DataValueEditor(IDataTypeService dataTypeService, ILocalizationService localizationService, ILocalizedTextService localizedTextService, IShortStringHelper shortStringHelper) // for tests, and manifest
{
_localizedTextService = localizedTextService;
_shortStringHelper = shortStringHelper;
ValueType = ValueTypes.String;
Validators = new List<IValueValidator>();
DataTypeService = dataTypeService;
LocalizationService = localizationService;
}
/// <summary>
/// Initializes a new instance of the <see cref="DataValueEditor"/> class.
/// </summary>
public DataValueEditor(IDataTypeService dataTypeService, ILocalizationService localizationService, IShortStringHelper shortStringHelper, DataEditorAttribute attribute)
{
if (attribute == null) throw new ArgumentNullException(nameof(attribute));
_shortStringHelper = shortStringHelper;
var view = attribute.View;
if (string.IsNullOrWhiteSpace(view))
throw new ArgumentException("The attribute does not specify a view.", nameof(attribute));
View = view;
ValueType = attribute.ValueType;
HideLabel = attribute.HideLabel;
DataTypeService = dataTypeService;
LocalizationService = localizationService;
}
/// <summary>
/// Gets or sets the value editor configuration.
/// </summary>
public virtual object Configuration { get; set; }
/// <summary>
/// Gets or sets the editor view.
/// </summary>
/// <remarks>
/// <para>The view can be three things: (1) the full virtual path, or (2) the relative path to the current Umbraco
/// folder, or (3) a view name which maps to views/propertyeditors/{view}/{view}.html.</para>
/// </remarks>
[JsonProperty("view", Required = Required.Always)]
public string View { get; set; }
/// <summary>
/// The value type which reflects how it is validated and stored in the database
/// </summary>
[JsonProperty("valueType")]
public string ValueType { get; set; }
/// <inheritdoc />
public IEnumerable<ValidationResult> Validate(object value, bool required, string format)
{
List<ValidationResult> results = null;
var r = Validators.SelectMany(v => v.Validate(value, ValueType, Configuration)).ToList();
if (r.Any()) { results = r; }
// mandatory and regex validators cannot be part of valueEditor.Validators because they
// depend on values that are not part of the configuration, .Mandatory and .ValidationRegEx,
// so they have to be explicitly invoked here.
if (required)
{
r = RequiredValidator.ValidateRequired(value, ValueType).ToList();
if (r.Any()) { if (results == null) results = r; else results.AddRange(r); }
}
var stringValue = value?.ToString();
if (!string.IsNullOrWhiteSpace(format) && !string.IsNullOrWhiteSpace(stringValue))
{
r = FormatValidator.ValidateFormat(value, ValueType, format).ToList();
if (r.Any()) { if (results == null) results = r; else results.AddRange(r); }
}
return results ?? Enumerable.Empty<ValidationResult>();
}
/// <summary>
/// A collection of validators for the pre value editor
/// </summary>
[JsonProperty("validation")]
public List<IValueValidator> Validators { get; private set; } = new List<IValueValidator>();
/// <summary>
/// Gets the validator used to validate the special property type -level "required".
/// </summary>
public virtual IValueRequiredValidator RequiredValidator => new RequiredValidator(_localizedTextService);
/// <summary>
/// Gets the validator used to validate the special property type -level "format".
/// </summary>
public virtual IValueFormatValidator FormatValidator => new RegexValidator(_localizedTextService);
/// <summary>
/// If this is true than the editor will be displayed full width without a label
/// </summary>
[JsonProperty("hideLabel")]
public bool HideLabel { get; set; }
/// <summary>
/// Set this to true if the property editor is for display purposes only
/// </summary>
public virtual bool IsReadOnly => false;
/// <summary>
/// Used to try to convert the string value to the correct CLR type based on the DatabaseDataType specified for this value editor
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
internal Attempt<object> TryConvertValueToCrlType(object value)
{
if (value is JValue)
value = value.ToString();
//this is a custom check to avoid any errors, if it's a string and it's empty just make it null
if (value is string s && string.IsNullOrWhiteSpace(s))
value = null;
Type valueType;
//convert the string to a known type
switch (ValueTypes.ToStorageType(ValueType))
{
case ValueStorageType.Ntext:
case ValueStorageType.Nvarchar:
valueType = typeof(string);
break;
case ValueStorageType.Integer:
//ensure these are nullable so we can return a null if required
//NOTE: This is allowing type of 'long' because I think json.net will deserialize a numerical value as long
// instead of int. Even though our db will not support this (will get truncated), we'll at least parse to this.
valueType = typeof(long?);
//if parsing is successful, we need to return as an Int, we're only dealing with long's here because of json.net, we actually
//don't support long values and if we return a long value it will get set as a 'long' on the Property.Value (object) and then
//when we compare the values for dirty tracking we'll be comparing an int -> long and they will not match.
var result = value.TryConvertTo(valueType);
return result.Success && result.Result != null
? Attempt<object>.Succeed((int)(long)result.Result)
: result;
case ValueStorageType.Decimal:
//ensure these are nullable so we can return a null if required
valueType = typeof(decimal?);
break;
case ValueStorageType.Date:
//ensure these are nullable so we can return a null if required
valueType = typeof(DateTime?);
break;
default:
throw new ArgumentOutOfRangeException();
}
return value.TryConvertTo(valueType);
}
/// <summary>
/// A method to deserialize the string value that has been saved in the content editor
/// to an object to be stored in the database.
/// </summary>
/// <param name="editorValue"></param>
/// <param name="currentValue">
/// The current value that has been persisted to the database for this editor. This value may be useful for
/// how the value then get's deserialized again to be re-persisted. In most cases it will probably not be used.
/// </param>
/// <param name="languageId"></param>
/// <param name="segment"></param>
/// <returns></returns>
/// <remarks>
/// By default this will attempt to automatically convert the string value to the value type supplied by ValueType.
///
/// If overridden then the object returned must match the type supplied in the ValueType, otherwise persisting the
/// value to the DB will fail when it tries to validate the value type.
/// </remarks>
public virtual object FromEditor(ContentPropertyData editorValue, object currentValue)
{
//if it's json but it's empty json, then return null
if (ValueType.InvariantEquals(ValueTypes.Json) && editorValue.Value != null && editorValue.Value.ToString().DetectIsEmptyJson())
{
return null;
}
var result = TryConvertValueToCrlType(editorValue.Value);
if (result.Success == false)
{
Current.Logger.Warn<DataValueEditor>("The value {EditorValue} cannot be converted to the type {StorageTypeValue}", editorValue.Value, ValueTypes.ToStorageType(ValueType));
return null;
}
return result.Result;
}
/// <summary>
/// A method used to format the database value to a value that can be used by the editor
/// </summary>
/// <param name="property"></param>
/// <param name="dataTypeService"></param>
/// <param name="culture"></param>
/// <param name="segment"></param>
/// <returns></returns>
/// <remarks>
/// The object returned will automatically be serialized into json notation. For most property editors
/// the value returned is probably just a string but in some cases a json structure will be returned.
/// </remarks>
public virtual object ToEditor(IProperty property, string culture = null, string segment = null)
{
var val = property.GetValue(culture, segment);
if (val == null) return string.Empty;
switch (ValueTypes.ToStorageType(ValueType))
{
case ValueStorageType.Ntext:
case ValueStorageType.Nvarchar:
//if it is a string type, we will attempt to see if it is json stored data, if it is we'll try to convert
//to a real json object so we can pass the true json object directly to angular!
var asString = val.ToString();
if (asString.DetectIsJson())
{
try
{
var json = JsonConvert.DeserializeObject(asString);
return json;
}
catch
{
//swallow this exception, we thought it was json but it really isn't so continue returning a string
}
}
return asString;
case ValueStorageType.Integer:
case ValueStorageType.Decimal:
//Decimals need to be formatted with invariant culture (dots, not commas)
//Anything else falls back to ToString()
var decim = val.TryConvertTo<decimal>();
return decim.Success
? decim.Result.ToString(NumberFormatInfo.InvariantInfo)
: val.ToString();
case ValueStorageType.Date:
var date = val.TryConvertTo<DateTime?>();
if (date.Success == false || date.Result == null)
{
return string.Empty;
}
//Dates will be formatted as yyyy-MM-dd HH:mm:ss
return date.Result.Value.ToIsoString();
default:
throw new ArgumentOutOfRangeException();
}
}
// TODO: the methods below should be replaced by proper property value convert ToXPath usage!
/// <summary>
/// Converts a property to Xml fragments.
/// </summary>
public IEnumerable<XElement> ConvertDbToXml(IProperty property, bool published)
{
published &= property.PropertyType.SupportsPublishing;
var nodeName = property.PropertyType.Alias.ToSafeAlias(_shortStringHelper);
foreach (var pvalue in property.Values)
{
var value = published ? pvalue.PublishedValue : pvalue.EditedValue;
if (value == null || value is string stringValue && string.IsNullOrWhiteSpace(stringValue))
continue;
var xElement = new XElement(nodeName);
if (pvalue.Culture != null)
xElement.Add(new XAttribute("lang", pvalue.Culture));
if (pvalue.Segment != null)
xElement.Add(new XAttribute("segment", pvalue.Segment));
var xValue = ConvertDbToXml(property.PropertyType, value);
xElement.Add(xValue);
yield return xElement;
}
}
/// <summary>
/// Converts a property value to an Xml fragment.
/// </summary>
/// <remarks>
/// <para>By default, this returns the value of ConvertDbToString but ensures that if the db value type is
/// NVarchar or NText, the value is returned as a CDATA fragment - else it's a Text fragment.</para>
/// <para>Returns an XText or XCData instance which must be wrapped in a element.</para>
/// <para>If the value is empty we will not return as CDATA since that will just take up more space in the file.</para>
/// </remarks>
public XNode ConvertDbToXml(IPropertyType propertyType, object value)
{
//check for null or empty value, we don't want to return CDATA if that is the case
if (value == null || value.ToString().IsNullOrWhiteSpace())
{
return new XText(ConvertDbToString(propertyType, value));
}
switch (ValueTypes.ToStorageType(ValueType))
{
case ValueStorageType.Date:
case ValueStorageType.Integer:
case ValueStorageType.Decimal:
return new XText(ConvertDbToString(propertyType, value));
case ValueStorageType.Nvarchar:
case ValueStorageType.Ntext:
//put text in cdata
return new XCData(ConvertDbToString(propertyType, value));
default:
throw new ArgumentOutOfRangeException();
}
}
/// <summary>
/// Converts a property value to a string.
/// </summary>
public virtual string ConvertDbToString(IPropertyType propertyType, object value)
{
if (value == null)
return string.Empty;
switch (ValueTypes.ToStorageType(ValueType))
{
case ValueStorageType.Nvarchar:
case ValueStorageType.Ntext:
return value.ToXmlString<string>();
case ValueStorageType.Integer:
case ValueStorageType.Decimal:
return value.ToXmlString(value.GetType());
case ValueStorageType.Date:
//treat dates differently, output the format as xml format
var date = value.TryConvertTo<DateTime?>();
if (date.Success == false || date.Result == null)
return string.Empty;
return date.Result.ToXmlString<DateTime>();
default:
throw new ArgumentOutOfRangeException();
}
}
}
}

View File

@@ -0,0 +1,35 @@
using System.Collections.Generic;
using Umbraco.Core.IO;
namespace Umbraco.Core.PropertyEditors
{
/// <summary>
/// Represents the configuration for the label value editor.
/// </summary>
public class LabelConfigurationEditor : ConfigurationEditor<LabelConfiguration>
{
public LabelConfigurationEditor(IIOHelper ioHelper) : base(ioHelper)
{
}
/// <inheritdoc />
public override LabelConfiguration FromConfigurationEditor(IDictionary<string, object> editorValues, LabelConfiguration configuration)
{
var newConfiguration = new LabelConfiguration();
// get the value type
// not simply deserializing Json because we want to validate the valueType
if (editorValues.TryGetValue(Constants.PropertyEditors.ConfigurationKeys.DataValueType, out var valueTypeObj)
&& valueTypeObj is string stringValue)
{
if (!string.IsNullOrWhiteSpace(stringValue) && ValueTypes.IsValue(stringValue)) // validate
newConfiguration.ValueType = stringValue;
}
return newConfiguration;
}
}
}

View File

@@ -0,0 +1,54 @@
using Umbraco.Composing;
using Umbraco.Core.Composing;
using Umbraco.Core.IO;
using Umbraco.Core.Logging;
using Umbraco.Core.Services;
using Umbraco.Core.Strings;
namespace Umbraco.Core.PropertyEditors
{
/// <summary>
/// Represents a property editor for label properties.
/// </summary>
[DataEditor(
Constants.PropertyEditors.Aliases.Label,
"Label",
"readonlyvalue",
Icon = "icon-readonly")]
public class LabelPropertyEditor : DataEditor
{
private readonly IIOHelper _ioHelper;
private readonly IDataTypeService _dataTypeService;
private readonly ILocalizationService _localizationService;
private readonly IShortStringHelper _shortStringHelper;
/// <summary>
/// Initializes a new instance of the <see cref="LabelPropertyEditor"/> class.
/// </summary>
public LabelPropertyEditor(ILogger logger, IIOHelper ioHelper, IDataTypeService dataTypeService, ILocalizationService localizationService, IShortStringHelper shortStringHelper)
: base(logger, dataTypeService, localizationService, shortStringHelper)
{
_ioHelper = ioHelper;
_dataTypeService = dataTypeService;
_localizationService = localizationService;
_shortStringHelper = shortStringHelper;
}
/// <inheritdoc />
protected override IDataValueEditor CreateValueEditor() => new LabelPropertyValueEditor(_dataTypeService, _localizationService, _shortStringHelper, Attribute);
/// <inheritdoc />
protected override IConfigurationEditor CreateConfigurationEditor() => new LabelConfigurationEditor(_ioHelper);
// provides the property value editor
internal class LabelPropertyValueEditor : DataValueEditor
{
public LabelPropertyValueEditor(IDataTypeService dataTypeService, ILocalizationService localizationService, IShortStringHelper shortStringHelper, DataEditorAttribute attribute)
: base(dataTypeService, localizationService, shortStringHelper, attribute)
{ }
/// <inheritdoc />
public override bool IsReadOnly => true;
}
}
}

View File

@@ -0,0 +1,61 @@
using System;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Umbraco.Core.Models.PublishedContent;
namespace Umbraco.Core.PropertyEditors.ValueConverters
{
[DefaultPropertyValueConverter]
public class ColorPickerValueConverter : PropertyValueConverterBase
{
public override bool IsConverter(IPublishedPropertyType propertyType)
=> propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.ColorPicker);
public override Type GetPropertyValueType(IPublishedPropertyType propertyType)
=> UseLabel(propertyType) ? typeof(PickedColor) : typeof(string);
public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType)
=> PropertyCacheLevel.Element;
public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview)
{
var useLabel = UseLabel(propertyType);
if (source == null) return useLabel ? null : string.Empty;
var ssource = source.ToString();
if (ssource.DetectIsJson())
{
try
{
var jo = JsonConvert.DeserializeObject<JObject>(ssource);
if (useLabel) return new PickedColor(jo["value"].ToString(), jo["label"].ToString());
return jo["value"].ToString();
}
catch { /* not json finally */ }
}
if (useLabel) return new PickedColor(ssource, ssource);
return ssource;
}
private bool UseLabel(IPublishedPropertyType propertyType)
{
return ConfigurationEditor.ConfigurationAs<ColorPickerConfiguration>(propertyType.DataType.Configuration).UseLabel;
}
public class PickedColor
{
public PickedColor(string color, string label)
{
Color = color;
Label = label;
}
public string Color { get; }
public string Label { get; }
public override string ToString() => Color;
}
}
}

View File

@@ -0,0 +1,115 @@
using System;
using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Umbraco.Core.Composing;
using Umbraco.Core.Configuration.Grid;
using Umbraco.Core.Logging;
using Umbraco.Core.Models.PublishedContent;
using Umbraco.Composing;
namespace Umbraco.Core.PropertyEditors.ValueConverters
{
/// <summary>
/// This ensures that the grid config is merged in with the front-end value
/// </summary>
[DefaultPropertyValueConverter(typeof(JsonValueConverter))] //this shadows the JsonValueConverter
public class GridValueConverter : JsonValueConverter
{
private readonly IGridConfig _config;
public GridValueConverter(PropertyEditorCollection propertyEditors, IGridConfig config)
: base(propertyEditors)
{
_config = config;
}
public override bool IsConverter(IPublishedPropertyType propertyType)
=> propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.Grid);
public override Type GetPropertyValueType(IPublishedPropertyType propertyType)
=> typeof (JToken);
public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType)
=> PropertyCacheLevel.Element;
public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview)
{
if (source == null) return null;
var sourceString = source.ToString();
if (sourceString.DetectIsJson())
{
try
{
var obj = JsonConvert.DeserializeObject<JObject>(sourceString);
//so we have the grid json... we need to merge in the grid's configuration values with the values
// we've saved in the database so that when the front end gets this value, it is up-to-date.
var sections = GetArray(obj, "sections");
foreach (var section in sections.Cast<JObject>())
{
var rows = GetArray(section, "rows");
foreach (var row in rows.Cast<JObject>())
{
var areas = GetArray(row, "areas");
foreach (var area in areas.Cast<JObject>())
{
var controls = GetArray(area, "controls");
foreach (var control in controls.Cast<JObject>())
{
var editor = control.Value<JObject>("editor");
if (editor != null)
{
var alias = editor.Value<string>("alias");
if (alias.IsNullOrWhiteSpace() == false)
{
//find the alias in config
var found = _config.EditorsConfig.Editors.FirstOrDefault(x => x.Alias == alias);
if (found != null)
{
//add/replace the editor value with the one from config
var serialized = new JObject();
serialized["name"] = found.Name;
serialized["alias"] = found.Alias;
serialized["view"] = found.View;
serialized["render"] = found.Render;
serialized["icon"] = found.Icon;
serialized["config"] = JObject.FromObject(found.Config);
control["editor"] = serialized;
}
}
}
}
}
}
}
return obj;
}
catch (Exception ex)
{
Current.Logger.Error<GridValueConverter>(ex, "Could not parse the string '{JsonString}' to a json object", sourceString);
}
}
//it's not json, just return the string
return sourceString;
}
private JArray GetArray(JObject obj, string propertyName)
{
JToken token;
if (obj.TryGetValue(propertyName, out token))
{
var asArray = token as JArray;
return asArray ?? new JArray();
}
return new JArray();
}
}
}

View File

@@ -0,0 +1,398 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Runtime.Serialization;
using System.Text;
using Newtonsoft.Json;
using Umbraco.Core.Serialization;
using Umbraco.Core.Strings;
namespace Umbraco.Core.PropertyEditors.ValueConverters
{
/// <summary>
/// Represents a value of the image cropper value editor.
/// </summary>
[JsonConverter(typeof(NoTypeConverterJsonConverter<ImageCropperValue>))]
[TypeConverter(typeof(ImageCropperValueTypeConverter))]
[DataContract(Name="imageCropDataSet")]
public class ImageCropperValue : IHtmlEncodedString, IEquatable<ImageCropperValue>
{
/// <summary>
/// Gets or sets the value source image.
/// </summary>
[DataMember(Name="src")]
public string Src { get; set;}
/// <summary>
/// Gets or sets the value focal point.
/// </summary>
[DataMember(Name = "focalPoint")]
public ImageCropperFocalPoint FocalPoint { get; set; }
/// <summary>
/// Gets or sets the value crops.
/// </summary>
[DataMember(Name = "crops")]
public IEnumerable<ImageCropperCrop> Crops { get; set; }
/// <inheritdoc />
public override string ToString()
{
return Crops != null ? (Crops.Any() ? JsonConvert.SerializeObject(this) : Src) : string.Empty;
}
/// <inheritdoc />
public string ToHtmlString() => Src;
/// <summary>
/// Gets a crop.
/// </summary>
public ImageCropperCrop GetCrop(string alias)
{
if (Crops == null)
return null;
return string.IsNullOrWhiteSpace(alias)
? Crops.FirstOrDefault()
: Crops.FirstOrDefault(x => x.Alias.InvariantEquals(alias));
}
public void AppendCropBaseUrl(StringBuilder url, ImageCropperCrop crop, bool defaultCrop, bool preferFocalPoint)
{
if (preferFocalPoint && HasFocalPoint()
|| crop != null && crop.Coordinates == null && HasFocalPoint()
|| defaultCrop && HasFocalPoint())
{
url.Append("?center=");
url.Append(FocalPoint.Top.ToString(CultureInfo.InvariantCulture));
url.Append(",");
url.Append(FocalPoint.Left.ToString(CultureInfo.InvariantCulture));
url.Append("&mode=crop");
}
else if (crop != null && crop.Coordinates != null && preferFocalPoint == false)
{
url.Append("?crop=");
url.Append(crop.Coordinates.X1.ToString(CultureInfo.InvariantCulture)).Append(",");
url.Append(crop.Coordinates.Y1.ToString(CultureInfo.InvariantCulture)).Append(",");
url.Append(crop.Coordinates.X2.ToString(CultureInfo.InvariantCulture)).Append(",");
url.Append(crop.Coordinates.Y2.ToString(CultureInfo.InvariantCulture));
url.Append("&cropmode=percentage");
}
else
{
url.Append("?anchor=center");
url.Append("&mode=crop");
}
}
/// <summary>
/// Gets the value image url for a specified crop.
/// </summary>
public string GetCropUrl(string alias, bool useCropDimensions = true, bool useFocalPoint = false, string cacheBusterValue = null)
{
var crop = GetCrop(alias);
// could not find a crop with the specified, non-empty, alias
if (crop == null && !string.IsNullOrWhiteSpace(alias))
return null;
var url = new StringBuilder();
AppendCropBaseUrl(url, crop, string.IsNullOrWhiteSpace(alias), useFocalPoint);
if (crop != null && useCropDimensions)
{
url.Append("&width=").Append(crop.Width);
url.Append("&height=").Append(crop.Height);
}
if (cacheBusterValue != null)
url.Append("&rnd=").Append(cacheBusterValue);
return url.ToString();
}
/// <summary>
/// Gets the value image url for a specific width and height.
/// </summary>
public string GetCropUrl(int width, int height, bool useFocalPoint = false, string cacheBusterValue = null)
{
var url = new StringBuilder();
AppendCropBaseUrl(url, null, true, useFocalPoint);
url.Append("&width=").Append(width);
url.Append("&height=").Append(height);
if (cacheBusterValue != null)
url.Append("&rnd=").Append(cacheBusterValue);
return url.ToString();
}
/// <summary>
/// Determines whether the value has a focal point.
/// </summary>
/// <returns></returns>
public bool HasFocalPoint()
=> FocalPoint != null && (FocalPoint.Left != 0.5m || FocalPoint.Top != 0.5m);
/// <summary>
/// Determines whether the value has a specified crop.
/// </summary>
public bool HasCrop(string alias)
=> Crops.Any(x => x.Alias == alias);
/// <summary>
/// Determines whether the value has a source image.
/// </summary>
public bool HasImage()
=> !string.IsNullOrWhiteSpace(Src);
/// <summary>
/// Applies a configuration.
/// </summary>
/// <remarks>Ensures that all crops defined in the configuration exists in the value.</remarks>
public void ApplyConfiguration(ImageCropperConfiguration configuration)
{
// merge the crop values - the alias + width + height comes from
// configuration, but each crop can store its own coordinates
if (Crops == null) return;
var configuredCrops = configuration?.Crops;
if (configuredCrops == null) return;
var crops = Crops.ToList();
foreach (var configuredCrop in configuredCrops)
{
var crop = crops.FirstOrDefault(x => x.Alias == configuredCrop.Alias);
if (crop != null)
{
// found, apply the height & width
crop.Width = configuredCrop.Width;
crop.Height = configuredCrop.Height;
}
else
{
// not found, add
crops.Add(new ImageCropperCrop
{
Alias = configuredCrop.Alias,
Width = configuredCrop.Width,
Height = configuredCrop.Height
});
}
}
// assume we don't have to remove the crops in value, that
// are not part of configuration anymore?
Crops = crops;
}
#region IEquatable
/// <inheritdoc />
public bool Equals(ImageCropperValue other)
=> ReferenceEquals(this, other) || Equals(this, other);
/// <inheritdoc />
public override bool Equals(object obj)
=> ReferenceEquals(this, obj) || obj is ImageCropperValue other && Equals(this, other);
private static bool Equals(ImageCropperValue left, ImageCropperValue right)
=> ReferenceEquals(left, right) // deals with both being null, too
|| !ReferenceEquals(left, null) && !ReferenceEquals(right, null)
&& string.Equals(left.Src, right.Src)
&& Equals(left.FocalPoint, right.FocalPoint)
&& left.ComparableCrops.SequenceEqual(right.ComparableCrops);
private IEnumerable<ImageCropperCrop> ComparableCrops
=> Crops?.OrderBy(x => x.Alias) ?? Enumerable.Empty<ImageCropperCrop>();
public static bool operator ==(ImageCropperValue left, ImageCropperValue right)
=> Equals(left, right);
public static bool operator !=(ImageCropperValue left, ImageCropperValue right)
=> !Equals(left, right);
public override int GetHashCode()
{
unchecked
{
// properties are, practically, readonly
// ReSharper disable NonReadonlyMemberInGetHashCode
var hashCode = Src?.GetHashCode() ?? 0;
hashCode = (hashCode*397) ^ (FocalPoint?.GetHashCode() ?? 0);
hashCode = (hashCode*397) ^ (Crops?.GetHashCode() ?? 0);
return hashCode;
// ReSharper restore NonReadonlyMemberInGetHashCode
}
}
#endregion
[DataContract(Name = "imageCropFocalPoint")]
public class ImageCropperFocalPoint : IEquatable<ImageCropperFocalPoint>
{
[DataMember(Name = "left")]
public decimal Left { get; set; }
[DataMember(Name = "top")]
public decimal Top { get; set; }
#region IEquatable
/// <inheritdoc />
public bool Equals(ImageCropperFocalPoint other)
=> ReferenceEquals(this, other) || Equals(this, other);
/// <inheritdoc />
public override bool Equals(object obj)
=> ReferenceEquals(this, obj) || obj is ImageCropperFocalPoint other && Equals(this, other);
private static bool Equals(ImageCropperFocalPoint left, ImageCropperFocalPoint right)
=> ReferenceEquals(left, right) // deals with both being null, too
|| !ReferenceEquals(left, null) && !ReferenceEquals(right, null)
&& left.Left == right.Left
&& left.Top == right.Top;
public static bool operator ==(ImageCropperFocalPoint left, ImageCropperFocalPoint right)
=> Equals(left, right);
public static bool operator !=(ImageCropperFocalPoint left, ImageCropperFocalPoint right)
=> !Equals(left, right);
public override int GetHashCode()
{
unchecked
{
// properties are, practically, readonly
// ReSharper disable NonReadonlyMemberInGetHashCode
return (Left.GetHashCode()*397) ^ Top.GetHashCode();
// ReSharper restore NonReadonlyMemberInGetHashCode
}
}
#endregion
}
[DataContract(Name = "imageCropData")]
public class ImageCropperCrop : IEquatable<ImageCropperCrop>
{
[DataMember(Name = "alias")]
public string Alias { get; set; }
[DataMember(Name = "width")]
public int Width { get; set; }
[DataMember(Name = "height")]
public int Height { get; set; }
[DataMember(Name = "coordinates")]
public ImageCropperCropCoordinates Coordinates { get; set; }
#region IEquatable
/// <inheritdoc />
public bool Equals(ImageCropperCrop other)
=> ReferenceEquals(this, other) || Equals(this, other);
/// <inheritdoc />
public override bool Equals(object obj)
=> ReferenceEquals(this, obj) || obj is ImageCropperCrop other && Equals(this, other);
private static bool Equals(ImageCropperCrop left, ImageCropperCrop right)
=> ReferenceEquals(left, right) // deals with both being null, too
|| !ReferenceEquals(left, null) && !ReferenceEquals(right, null)
&& string.Equals(left.Alias, right.Alias)
&& left.Width == right.Width
&& left.Height == right.Height
&& Equals(left.Coordinates, right.Coordinates);
public static bool operator ==(ImageCropperCrop left, ImageCropperCrop right)
=> Equals(left, right);
public static bool operator !=(ImageCropperCrop left, ImageCropperCrop right)
=> !Equals(left, right);
public override int GetHashCode()
{
unchecked
{
// properties are, practically, readonly
// ReSharper disable NonReadonlyMemberInGetHashCode
var hashCode = Alias?.GetHashCode() ?? 0;
hashCode = (hashCode*397) ^ Width;
hashCode = (hashCode*397) ^ Height;
hashCode = (hashCode*397) ^ (Coordinates?.GetHashCode() ?? 0);
return hashCode;
// ReSharper restore NonReadonlyMemberInGetHashCode
}
}
#endregion
}
[DataContract(Name = "imageCropCoordinates")]
public class ImageCropperCropCoordinates : IEquatable<ImageCropperCropCoordinates>
{
[DataMember(Name = "x1")]
public decimal X1 { get; set; }
[DataMember(Name = "y1")]
public decimal Y1 { get; set; }
[DataMember(Name = "x2")]
public decimal X2 { get; set; }
[DataMember(Name = "y2")]
public decimal Y2 { get; set; }
#region IEquatable
/// <inheritdoc />
public bool Equals(ImageCropperCropCoordinates other)
=> ReferenceEquals(this, other) || Equals(this, other);
/// <inheritdoc />
public override bool Equals(object obj)
=> ReferenceEquals(this, obj) || obj is ImageCropperCropCoordinates other && Equals(this, other);
private static bool Equals(ImageCropperCropCoordinates left, ImageCropperCropCoordinates right)
=> ReferenceEquals(left, right) // deals with both being null, too
|| !ReferenceEquals(left, null) && !ReferenceEquals(right, null)
&& left.X1 == right.X1
&& left.X2 == right.X2
&& left.Y1 == right.Y1
&& left.Y2 == right.Y2;
public static bool operator ==(ImageCropperCropCoordinates left, ImageCropperCropCoordinates right)
=> Equals(left, right);
public static bool operator !=(ImageCropperCropCoordinates left, ImageCropperCropCoordinates right)
=> !Equals(left, right);
public override int GetHashCode()
{
unchecked
{
// properties are, practically, readonly
// ReSharper disable NonReadonlyMemberInGetHashCode
var hashCode = X1.GetHashCode();
hashCode = (hashCode*397) ^ Y1.GetHashCode();
hashCode = (hashCode*397) ^ X2.GetHashCode();
hashCode = (hashCode*397) ^ Y2.GetHashCode();
return hashCode;
// ReSharper restore NonReadonlyMemberInGetHashCode
}
}
#endregion
}
}
}

View File

@@ -0,0 +1,56 @@
using System;
using System.Globalization;
using Newtonsoft.Json;
using Umbraco.Core.Composing;
using Umbraco.Core.Logging;
using Umbraco.Core.Models.PublishedContent;
using Umbraco.Composing;
namespace Umbraco.Core.PropertyEditors.ValueConverters
{
/// <summary>
/// Represents a value converter for the image cropper value editor.
/// </summary>
[DefaultPropertyValueConverter(typeof(JsonValueConverter))]
public class ImageCropperValueConverter : PropertyValueConverterBase
{
/// <inheritdoc />
public override bool IsConverter(IPublishedPropertyType propertyType)
=> propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.ImageCropper);
/// <inheritdoc />
public override Type GetPropertyValueType(IPublishedPropertyType propertyType)
=> typeof (ImageCropperValue);
/// <inheritdoc />
public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType)
=> PropertyCacheLevel.Element;
/// <inheritdoc />
public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview)
{
if (source == null) return null;
var sourceString = source.ToString();
ImageCropperValue value;
try
{
value = JsonConvert.DeserializeObject<ImageCropperValue>(sourceString, new JsonSerializerSettings
{
Culture = CultureInfo.InvariantCulture,
FloatParseHandling = FloatParseHandling.Decimal
});
}
catch (Exception ex)
{
// cannot deserialize, assume it may be a raw image url
Current.Logger.Error<ImageCropperValueConverter>(ex, "Could not deserialize string '{JsonString}' into an image cropper value.", sourceString);
value = new ImageCropperValue { Src = sourceString };
}
value.ApplyConfiguration(propertyType.DataType.ConfigurationAs<ImageCropperConfiguration>());
return value;
}
}
}

View File

@@ -0,0 +1,34 @@
using System;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using Newtonsoft.Json.Linq;
using Umbraco.Core.Composing;
namespace Umbraco.Core.PropertyEditors.ValueConverters
{
/// <summary>
/// Converts <see cref="ImageCropperValue"/> to string or JObject (why?).
/// </summary>
public class ImageCropperValueTypeConverter : TypeConverter
{
private static readonly Type[] ConvertableTypes = { typeof(JObject) };
public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
{
return ConvertableTypes.Any(x => TypeHelper.IsTypeAssignableFrom(x, destinationType))
|| CanConvertFrom(context, destinationType);
}
public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
{
var cropperValue = value as ImageCropperValue;
if (cropperValue == null)
return null;
return TypeHelper.IsTypeAssignableFrom<JObject>(destinationType)
? JObject.FromObject(cropperValue)
: base.ConvertTo(context, culture, value, destinationType);
}
}
}

View File

@@ -0,0 +1,71 @@
using System;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Umbraco.Core.Composing;
using Umbraco.Core.Logging;
using Umbraco.Core.Models.PublishedContent;
using Umbraco.Composing;
namespace Umbraco.Core.PropertyEditors.ValueConverters
{
/// <summary>
/// The default converter for all property editors that expose a JSON value type
/// </summary>
/// <remarks>
/// Since this is a default (umbraco) converter it will be ignored if another converter found conflicts with this one.
/// </remarks>
[DefaultPropertyValueConverter]
public class JsonValueConverter : PropertyValueConverterBase
{
private readonly PropertyEditorCollection _propertyEditors;
/// <summary>
/// Initializes a new instance of the <see cref="JsonValueConverter"/> class.
/// </summary>
public JsonValueConverter(PropertyEditorCollection propertyEditors)
{
_propertyEditors = propertyEditors;
}
/// <summary>
/// It is a converter for any value type that is "JSON"
/// </summary>
/// <param name="propertyType"></param>
/// <returns></returns>
public override bool IsConverter(IPublishedPropertyType propertyType)
{
return _propertyEditors.TryGet(propertyType.EditorAlias, out var editor)
&& editor.GetValueEditor().ValueType.InvariantEquals(ValueTypes.Json);
}
public override Type GetPropertyValueType(IPublishedPropertyType propertyType)
=> typeof (JToken);
public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType)
=> PropertyCacheLevel.Element;
public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview)
{
if (source == null) return null;
var sourceString = source.ToString();
if (sourceString.DetectIsJson())
{
try
{
var obj = JsonConvert.DeserializeObject(sourceString);
return obj;
}
catch (Exception ex)
{
Current.Logger.Error<JsonValueConverter>(ex, "Could not parse the string '{JsonString}' to a json object", sourceString);
}
}
//it's not json, just return the string
return sourceString;
}
// TODO: Now to convert that to XPath!
}
}

View File

@@ -0,0 +1,85 @@
using System;
using System.Globalization;
using Umbraco.Core.Models.PublishedContent;
namespace Umbraco.Core.PropertyEditors.ValueConverters
{
/// <summary>
/// We need this property converter so that we always force the value of a label to be a string
/// </summary>
/// <remarks>
/// Without a property converter defined for the label type, the value will be converted with
/// the `ConvertUsingDarkMagic` method which will try to parse the value into it's correct type, but this
/// can cause issues if the string is detected as a number and then strips leading zeros.
/// Example: http://issues.umbraco.org/issue/U4-7929
/// </remarks>
[DefaultPropertyValueConverter]
public class LabelValueConverter : PropertyValueConverterBase
{
public override bool IsConverter(IPublishedPropertyType propertyType)
=> Constants.PropertyEditors.Aliases.Label.Equals(propertyType.EditorAlias);
public override Type GetPropertyValueType(IPublishedPropertyType propertyType)
{
var valueType = ConfigurationEditor.ConfigurationAs<LabelConfiguration>(propertyType.DataType.Configuration);
switch (valueType.ValueType)
{
case ValueTypes.DateTime:
case ValueTypes.Date:
return typeof(DateTime);
case ValueTypes.Time:
return typeof(TimeSpan);
case ValueTypes.Decimal:
return typeof(decimal);
case ValueTypes.Integer:
return typeof(int);
case ValueTypes.Bigint:
return typeof(long);
default: // everything else is a string
return typeof(string);
}
}
public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType)
=> PropertyCacheLevel.Element;
public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview)
{
var valueType = ConfigurationEditor.ConfigurationAs<LabelConfiguration>(propertyType.DataType.Configuration);
switch (valueType.ValueType)
{
case ValueTypes.DateTime:
case ValueTypes.Date:
if (source is DateTime sourceDateTime)
return sourceDateTime;
if (source is string sourceDateTimeString)
return DateTime.TryParse(sourceDateTimeString, CultureInfo.InvariantCulture, DateTimeStyles.None, out var dt) ? dt : DateTime.MinValue;
return DateTime.MinValue;
case ValueTypes.Time:
if (source is DateTime sourceTime)
return sourceTime.TimeOfDay;
if (source is string sourceTimeString)
return TimeSpan.TryParse(sourceTimeString, CultureInfo.InvariantCulture, out var ts) ? ts : TimeSpan.Zero;
return TimeSpan.Zero;
case ValueTypes.Decimal:
if (source is decimal sourceDecimal) return sourceDecimal;
if (source is string sourceDecimalString)
return decimal.TryParse(sourceDecimalString, NumberStyles.Any, CultureInfo.InvariantCulture, out var d) ? d : 0;
if (source is double sourceDouble)
return Convert.ToDecimal(sourceDouble);
return (decimal) 0;
case ValueTypes.Integer:
if (source is int sourceInt) return sourceInt;
if (source is string sourceIntString)
return int.TryParse(sourceIntString, out var i) ? i : 0;
return 0;
case ValueTypes.Bigint:
if (source is string sourceLongString)
return long.TryParse(sourceLongString, out var i) ? i : 0;
return (long) 0;
default: // everything else is a string
return source?.ToString() ?? string.Empty;
}
}
}
}

View File

@@ -0,0 +1,40 @@
using System;
using Umbraco.Core.Models.PublishedContent;
using Umbraco.Core.Strings;
namespace Umbraco.Core.PropertyEditors.ValueConverters
{
[DefaultPropertyValueConverter]
public class MarkdownEditorValueConverter : PropertyValueConverterBase
{
public override bool IsConverter(IPublishedPropertyType propertyType)
=> Constants.PropertyEditors.Aliases.MarkdownEditor.Equals(propertyType.EditorAlias);
public override Type GetPropertyValueType(IPublishedPropertyType propertyType)
=> typeof (IHtmlEncodedString);
// PropertyCacheLevel.Content is ok here because that converter does not parse {locallink} nor executes macros
public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType)
=> PropertyCacheLevel.Element;
public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview)
{
// in xml a string is: string
// in the database a string is: string
// default value is: null
return source;
}
public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview)
{
// source should come from ConvertSource and be a string (or null) already
return new HtmlEncodedString(inter == null ? string.Empty : (string) inter);
}
public override object ConvertIntermediateToXPath(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview)
{
// source should come from ConvertSource and be a string (or null) already
return inter?.ToString() ?? string.Empty;
}
}
}

View File

@@ -0,0 +1,87 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using Umbraco.Core.Models;
using Umbraco.Core.Models.PublishedContent;
using Umbraco.Core.Services;
namespace Umbraco.Core.PropertyEditors.ValueConverters
{
[DefaultPropertyValueConverter]
public class SliderValueConverter : PropertyValueConverterBase
{
private readonly IDataTypeService _dataTypeService;
public SliderValueConverter(IDataTypeService dataTypeService)
{
_dataTypeService = dataTypeService ?? throw new ArgumentNullException(nameof(dataTypeService));
}
public override bool IsConverter(IPublishedPropertyType propertyType)
=> propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.Slider);
public override Type GetPropertyValueType(IPublishedPropertyType propertyType)
=> IsRangeDataType(propertyType.DataType.Id) ? typeof (Range<decimal>) : typeof (decimal);
public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType)
=> PropertyCacheLevel.Element;
public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object source, bool preview)
{
if (source == null)
return null;
if (IsRangeDataType(propertyType.DataType.Id))
{
var rangeRawValues = source.ToString().Split(',');
var minimumAttempt = rangeRawValues[0].TryConvertTo<decimal>();
var maximumAttempt = rangeRawValues[1].TryConvertTo<decimal>();
if (minimumAttempt.Success && maximumAttempt.Success)
{
return new Range<decimal> { Maximum = maximumAttempt.Result, Minimum = minimumAttempt.Result };
}
}
var valueAttempt = source.ToString().TryConvertTo<decimal>();
if (valueAttempt.Success)
return valueAttempt.Result;
// Something failed in the conversion of the strings to decimals
return null;
}
/// <summary>
/// Discovers if the slider is set to range mode.
/// </summary>
/// <param name="dataTypeId">
/// The data type id.
/// </param>
/// <returns>
/// The <see cref="bool"/>.
/// </returns>
private bool IsRangeDataType(int dataTypeId)
{
// GetPreValuesCollectionByDataTypeId is cached at repository level;
// still, the collection is deep-cloned so this is kinda expensive,
// better to cache here + trigger refresh in DataTypeCacheRefresher
// TODO: this is cheap now, remove the caching
return Storages.GetOrAdd(dataTypeId, id =>
{
var dataType = _dataTypeService.GetDataType(id);
var configuration = dataType.ConfigurationAs<SliderConfiguration>();
return configuration.EnableRange;
});
}
private static readonly ConcurrentDictionary<int, bool> Storages = new ConcurrentDictionary<int, bool>();
public static void ClearCaches()
{
Storages.Clear();
}
}
}

View File

@@ -0,0 +1,80 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Umbraco.Core.Models;
using Umbraco.Core.Models.PublishedContent;
using Umbraco.Core.Services;
namespace Umbraco.Core.PropertyEditors.ValueConverters
{
[DefaultPropertyValueConverter]
public class TagsValueConverter : PropertyValueConverterBase
{
private readonly IDataTypeService _dataTypeService;
public TagsValueConverter(IDataTypeService dataTypeService)
{
_dataTypeService = dataTypeService ?? throw new ArgumentNullException(nameof(dataTypeService));
}
public override bool IsConverter(IPublishedPropertyType propertyType)
=> propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.Tags);
public override Type GetPropertyValueType(IPublishedPropertyType propertyType)
=> typeof (IEnumerable<string>);
public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType)
=> PropertyCacheLevel.Element;
public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview)
{
if (source == null) return Array.Empty<string>();
// if Json storage type deserialize and return as string array
if (JsonStorageType(propertyType.DataType.Id))
{
var jArray = JsonConvert.DeserializeObject<JArray>(source.ToString());
return jArray.ToObject<string[]>() ?? Array.Empty<string>();
}
// Otherwise assume CSV storage type and return as string array
return source.ToString().Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries);
}
public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object source, bool preview)
{
return (string[]) source;
}
/// <summary>
/// Discovers if the tags data type is storing its data in a Json format
/// </summary>
/// <param name="dataTypeId">
/// The data type id.
/// </param>
/// <returns>
/// The <see cref="bool"/>.
/// </returns>
private bool JsonStorageType(int dataTypeId)
{
// GetDataType(id) is cached at repository level; still, there is some
// deep-cloning involved (expensive) - better cache here + trigger
// refresh in DataTypeCacheRefresher
return Storages.GetOrAdd(dataTypeId, id =>
{
var configuration = _dataTypeService.GetDataType(id).ConfigurationAs<TagConfiguration>();
return configuration.StorageType == TagsStorageType.Json;
});
}
private static readonly ConcurrentDictionary<int, bool> Storages = new ConcurrentDictionary<int, bool>();
public static void ClearCaches()
{
Storages.Clear();
}
}
}

View File

@@ -0,0 +1,43 @@
using System;
using Umbraco.Core.Models.PublishedContent;
using Umbraco.Core.Strings;
namespace Umbraco.Core.PropertyEditors.ValueConverters
{
/// <summary>
/// Value converter for the RTE so that it always returns IHtmlString so that Html.Raw doesn't have to be used.
/// </summary>
[DefaultPropertyValueConverter]
public class TinyMceValueConverter : PropertyValueConverterBase
{
public override bool IsConverter(IPublishedPropertyType propertyType)
=> propertyType.EditorAlias == Constants.PropertyEditors.Aliases.TinyMce;
public override Type GetPropertyValueType(IPublishedPropertyType propertyType)
=> typeof (IHtmlEncodedString);
// PropertyCacheLevel.Content is ok here because that converter does not parse {locallink} nor executes macros
public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType)
=> PropertyCacheLevel.Element;
public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview)
{
// in xml a string is: string
// in the database a string is: string
// default value is: null
return source;
}
public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview)
{
// source should come from ConvertSource and be a string (or null) already
return new HtmlEncodedString(inter == null ? string.Empty : (string)inter);
}
public override object ConvertIntermediateToXPath(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview)
{
// source should come from ConvertSource and be a string (or null) already
return inter;
}
}
}

View File

@@ -0,0 +1,41 @@
using Umbraco.Core.Composing;
using Umbraco.Core.Logging;
using Umbraco.Core.Services;
using Umbraco.Core.Strings;
namespace Umbraco.Core.PropertyEditors
{
/// <summary>
/// Represents a void editor.
/// </summary>
/// <remarks>Can be used in some places where an editor is needed but no actual
/// editor is available. Not to be used otherwise. Not discovered, and therefore
/// not part of the editors collection.</remarks>
[HideFromTypeFinder]
public class VoidEditor : DataEditor
{
/// <summary>
/// Initializes a new instance of the <see cref="VoidEditor"/> class.
/// </summary>
/// <param name="aliasSuffix">An optional alias suffix.</param>
/// <param name="logger">A logger.</param>
/// <remarks>The default alias of the editor is "Umbraco.Void". When a suffix is provided,
/// it is appended to the alias. Eg if the suffix is "Foo" the alias is "Umbraco.Void.Foo".</remarks>
public VoidEditor(string aliasSuffix, ILogger logger, IDataTypeService dataTypeService, ILocalizationService localizationService, IShortStringHelper shortStringHelper)
: base(logger, dataTypeService, localizationService, shortStringHelper)
{
Alias = "Umbraco.Void";
if (string.IsNullOrWhiteSpace(aliasSuffix)) return;
Alias += "." + aliasSuffix;
}
/// <summary>
/// Initializes a new instance of the <see cref="VoidEditor"/> class.
/// </summary>
/// <param name="logger">A logger.</param>
/// <remarks>The alias of the editor is "Umbraco.Void".</remarks>
public VoidEditor(ILogger logger, IDataTypeService dataTypeService, ILocalizationService localizationService, IShortStringHelper shortStringHelper)
: this(null, logger, dataTypeService, localizationService, shortStringHelper)
{ }
}
}

View File

@@ -41,4 +41,16 @@
<ProjectReference Include="..\Umbraco.Abstractions\Umbraco.Abstractions.csproj" />
</ItemGroup>
<ItemGroup>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>Umbraco.Core</_Parameter1>
</AssemblyAttribute>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>Umbraco.Tests</_Parameter1>
</AssemblyAttribute>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>Umbraco.Tests.Benchmarks</_Parameter1>
</AssemblyAttribute>
</ItemGroup>
</Project>