Merge remote-tracking branch 'origin/v11/dev' into v12/dev

# Conflicts:
#	src/Umbraco.Web.BackOffice/Controllers/MediaController.cs
#	version.json
This commit is contained in:
Bjarke Berg
2023-08-28 11:39:20 +02:00
20 changed files with 599 additions and 177 deletions

View File

@@ -88,10 +88,6 @@ public sealed class DataTypeCacheRefresher : PayloadCacheRefresherBase<DataTypeC
}
}
// TODO: not sure I like these?
TagsValueConverter.ClearCaches();
SliderValueConverter.ClearCaches();
// refresh the models and cache
_publishedModelFactory.WithSafeLiveFactoryReset(() =>
_publishedSnapshotService.Notify(payloads));

View File

@@ -326,6 +326,9 @@ namespace Umbraco.Cms.Core.DependencyInjection
Services.AddUnique<ICultureImpactFactory>(provider => new CultureImpactFactory(provider.GetRequiredService<IOptionsMonitor<ContentSettings>>()));
Services.AddUnique<IDictionaryService, DictionaryService>();
Services.AddUnique<ITemporaryMediaService, TemporaryMediaService>();
// Register filestream security analyzers
Services.AddUnique<IFileStreamSecurityValidator,FileStreamSecurityValidator>();
}
}
}

View File

@@ -342,6 +342,7 @@
<key alias="createFolderFailed">Failed to create a folder under parent id %0%</key>
<key alias="renameFolderFailed">Failed to rename the folder with id %0%</key>
<key alias="dragAndDropYourFilesIntoTheArea">Drag and drop your file(s) into the area</key>
<key alias="fileSecurityValidationFailure">One or more file security validations have failed</key>
</area>
<area alias="member">
<key alias="createNewMember">Create a new member</key>

View File

@@ -353,6 +353,7 @@
<key alias="renameFolderFailed">Failed to rename the folder with id %0%</key>
<key alias="dragAndDropYourFilesIntoTheArea">Drag and drop your file(s) into the area</key>
<key alias="uploadNotAllowed">Upload is not allowed in this location.</key>
<key alias="fileSecurityValidationFailure">One or more file security validations have failed</key>
</area>
<area alias="member">
<key alias="createNewMember">Create a new member</key>

View File

@@ -340,6 +340,7 @@
<key alias="renameFolderFailed">Kan de map met id %0% niet hernoemen</key>
<key alias="dragAndDropYourFilesIntoTheArea">Sleep en zet je bestand(en) neer in dit gebied</key>
<key alias="uploadNotAllowed">Upload is niet toegelaten in deze locatie.</key>
<key alias="fileSecurityValidationFailure">Een of meerdere veiligheid validaties zijn gefaald voor het bestand</key>
</area>
<area alias="member">
<key alias="createNewMember">Maak nieuw lid aan</key>

View File

@@ -219,96 +219,52 @@ public static class TypeExtensions
/// <returns></returns>
public static PropertyInfo[] GetAllProperties(this Type type)
{
if (type.IsInterface)
{
var propertyInfos = new List<PropertyInfo>();
var considered = new List<Type>();
var queue = new Queue<Type>();
considered.Add(type);
queue.Enqueue(type);
while (queue.Count > 0)
{
Type subType = queue.Dequeue();
foreach (Type subInterface in subType.GetInterfaces())
{
if (considered.Contains(subInterface))
{
continue;
}
considered.Add(subInterface);
queue.Enqueue(subInterface);
}
PropertyInfo[] typeProperties = subType.GetProperties(
BindingFlags.FlattenHierarchy
| BindingFlags.Public
| BindingFlags.NonPublic
| BindingFlags.Instance);
IEnumerable<PropertyInfo> newPropertyInfos = typeProperties
.Where(x => !propertyInfos.Contains(x));
propertyInfos.InsertRange(0, newPropertyInfos);
}
return propertyInfos.ToArray();
}
return type.GetProperties(BindingFlags.FlattenHierarchy
| BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
const BindingFlags bindingFlags = BindingFlags.FlattenHierarchy
| BindingFlags.Public
| BindingFlags.NonPublic
| BindingFlags.Instance;
return type.GetAllMemberInfos(t => t.GetProperties(bindingFlags));
}
/// <summary>
/// Returns all public properties including inherited properties even for interfaces
/// Returns public properties including inherited properties even for interfaces
/// </summary>
/// <param name="type"></param>
/// <returns></returns>
/// <remarks>
/// taken from
/// http://stackoverflow.com/questions/358835/getproperties-to-return-all-properties-for-an-interface-inheritance-hierarchy
/// </remarks>
public static PropertyInfo[] GetPublicProperties(this Type type)
{
if (type.IsInterface)
{
var propertyInfos = new List<PropertyInfo>();
const BindingFlags bindingFlags = BindingFlags.FlattenHierarchy
| BindingFlags.Public
| BindingFlags.Instance;
return type.GetAllMemberInfos(t => t.GetProperties(bindingFlags));
}
var considered = new List<Type>();
var queue = new Queue<Type>();
considered.Add(type);
queue.Enqueue(type);
while (queue.Count > 0)
{
Type subType = queue.Dequeue();
foreach (Type subInterface in subType.GetInterfaces())
{
if (considered.Contains(subInterface))
{
continue;
}
/// <summary>
/// Returns public methods including inherited methods even for interfaces
/// </summary>
/// <param name="type"></param>
/// <returns></returns>
public static MethodInfo[] GetPublicMethods(this Type type)
{
const BindingFlags bindingFlags = BindingFlags.FlattenHierarchy
| BindingFlags.Public
| BindingFlags.Instance;
return type.GetAllMemberInfos(t => t.GetMethods(bindingFlags));
}
considered.Add(subInterface);
queue.Enqueue(subInterface);
}
PropertyInfo[] typeProperties = subType.GetProperties(
BindingFlags.FlattenHierarchy
| BindingFlags.Public
| BindingFlags.Instance);
IEnumerable<PropertyInfo> newPropertyInfos = typeProperties
.Where(x => !propertyInfos.Contains(x));
propertyInfos.InsertRange(0, newPropertyInfos);
}
return propertyInfos.ToArray();
}
return type.GetProperties(BindingFlags.FlattenHierarchy
| BindingFlags.Public | BindingFlags.Instance);
/// <summary>
/// Returns all methods including inherited methods even for interfaces
/// </summary>
/// <remarks>Includes both Public and Non-Public methods</remarks>
/// <param name="type"></param>
/// <returns></returns>
public static MethodInfo[] GetAllMethods(this Type type)
{
const BindingFlags bindingFlags = BindingFlags.FlattenHierarchy
| BindingFlags.Public
| BindingFlags.NonPublic
| BindingFlags.Instance;
return type.GetAllMemberInfos(t => t.GetMethods(bindingFlags));
}
/// <summary>
@@ -512,4 +468,47 @@ public static class TypeExtensions
return attempt;
}
/// <remarks>
/// taken from
/// http://stackoverflow.com/questions/358835/getproperties-to-return-all-properties-for-an-interface-inheritance-hierarchy
/// </remarks>
private static T[] GetAllMemberInfos<T>(this Type type, Func<Type, T[]> getMemberInfos)
where T : MemberInfo
{
if (type.IsInterface is false)
{
return getMemberInfos(type);
}
var memberInfos = new List<T>();
var considered = new List<Type>();
var queue = new Queue<Type>();
considered.Add(type);
queue.Enqueue(type);
while (queue.Count > 0)
{
Type subType = queue.Dequeue();
foreach (Type subInterface in subType.GetInterfaces())
{
if (considered.Contains(subInterface))
{
continue;
}
considered.Add(subInterface);
queue.Enqueue(subInterface);
}
T[] typeMethodInfos = getMemberInfos(subType);
IEnumerable<T> newMethodInfos = typeMethodInfos
.Where(x => !memberInfos.Contains(x));
memberInfos.InsertRange(0, newMethodInfos);
}
return memberInfos.ToArray();
}
}

View File

@@ -1,4 +1,4 @@
using System.Collections.Concurrent;
using System.Globalization;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.Services;
@@ -6,74 +6,123 @@ using Umbraco.Extensions;
namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters;
/// <summary>
/// The slider property value converter.
/// </summary>
/// <seealso cref="Umbraco.Cms.Core.PropertyEditors.PropertyValueConverterBase" />
[DefaultPropertyValueConverter]
public class SliderValueConverter : PropertyValueConverterBase
{
private static readonly ConcurrentDictionary<int, bool> Storages = new();
private readonly IDataTypeService _dataTypeService;
/// <summary>
/// Initializes a new instance of the <see cref="SliderValueConverter" /> class.
/// </summary>
public SliderValueConverter()
{ }
public SliderValueConverter(IDataTypeService dataTypeService) => _dataTypeService =
dataTypeService ?? throw new ArgumentNullException(nameof(dataTypeService));
/// <summary>
/// Initializes a new instance of the <see cref="SliderValueConverter" /> class.
/// </summary>
/// <param name="dataTypeService">The data type service.</param>
[Obsolete("The IDataTypeService is not used anymore. This constructor will be removed in a future version.")]
public SliderValueConverter(IDataTypeService dataTypeService)
{ }
public static void ClearCaches() => Storages.Clear();
/// <summary>
/// Clears the data type configuration caches.
/// </summary>
[Obsolete("Caching of data type configuration is not done anymore. This method will be removed in a future version.")]
public static void ClearCaches()
{ }
/// <inheritdoc />
public override bool IsConverter(IPublishedPropertyType propertyType)
=> propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.Slider);
/// <inheritdoc />
public override Type GetPropertyValueType(IPublishedPropertyType propertyType)
=> IsRangeDataType(propertyType.DataType.Id) ? typeof(Range<decimal>) : typeof(decimal);
=> IsRange(propertyType) ? typeof(Range<decimal>) : typeof(decimal);
/// <inheritdoc />
public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType)
=> PropertyCacheLevel.Element;
/// <inheritdoc />
public override object? ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object? source, bool preview)
{
if (source == null)
bool isRange = IsRange(propertyType);
var sourceString = source?.ToString();
return isRange
? HandleRange(sourceString)
: HandleDecimal(sourceString);
}
private static Range<decimal> HandleRange(string? sourceString)
{
if (sourceString is null)
{
return null;
return new Range<decimal>();
}
if (IsRangeDataType(propertyType.DataType.Id))
{
var rangeRawValues = source.ToString()!.Split(Constants.CharArrays.Comma);
Attempt<decimal> minimumAttempt = rangeRawValues[0].TryConvertTo<decimal>();
Attempt<decimal> maximumAttempt = rangeRawValues[1].TryConvertTo<decimal>();
string[] rangeRawValues = sourceString.Split(Constants.CharArrays.Comma);
if (minimumAttempt.Success && maximumAttempt.Success)
if (TryParseDecimal(rangeRawValues[0], out var minimum))
{
if (rangeRawValues.Length == 1)
{
return new Range<decimal> { Maximum = maximumAttempt.Result, Minimum = minimumAttempt.Result };
// Configuration is probably changed from single to range, return range with same min/max
return new Range<decimal>
{
Minimum = minimum,
Maximum = minimum
};
}
if (rangeRawValues.Length == 2 && TryParseDecimal(rangeRawValues[1], out var maximum))
{
return new Range<decimal>
{
Minimum = minimum,
Maximum = maximum
};
}
}
Attempt<decimal> valueAttempt = source.ToString().TryConvertTo<decimal>();
if (valueAttempt.Success)
return new Range<decimal>();
}
private static decimal HandleDecimal(string? sourceString)
{
if (string.IsNullOrEmpty(sourceString))
{
return valueAttempt.Result;
return default;
}
// Something failed in the conversion of the strings to decimals
return null;
// This used to be a range slider, so we'll assign the minimum value as the new value
if (sourceString.Contains(','))
{
var minimumValueRepresentation = sourceString.Split(Constants.CharArrays.Comma)[0];
if (TryParseDecimal(minimumValueRepresentation, out var minimum))
{
return minimum;
}
}
else if (TryParseDecimal(sourceString, out var value))
{
return value;
}
return default;
}
/// <summary>
/// Discovers if the slider is set to range mode.
/// Helper method for parsing a double consistently
/// </summary>
/// <param name="dataTypeId">
/// The data type id.
/// </param>
/// <returns>
/// The <see cref="bool" />.
/// </returns>
private bool IsRangeDataType(int dataTypeId) =>
private static bool TryParseDecimal(string? representation, out decimal value)
=> decimal.TryParse(representation, NumberStyles.Number, CultureInfo.InvariantCulture, out value);
// 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
Storages.GetOrAdd(dataTypeId, id =>
{
IDataType? dataType = _dataTypeService.GetDataType(id);
SliderConfiguration? configuration = dataType?.ConfigurationAs<SliderConfiguration>();
return configuration?.EnableRange ?? false;
});
private static bool IsRange(IPublishedPropertyType propertyType)
=> propertyType.DataType.ConfigurationAs<SliderConfiguration>()?.EnableRange == true;
}

View File

@@ -1,4 +1,3 @@
using System.Collections.Concurrent;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.Serialization;
@@ -7,69 +6,66 @@ using Umbraco.Extensions;
namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters;
/// <summary>
/// The tags property value converter.
/// </summary>
/// <seealso cref="Umbraco.Cms.Core.PropertyEditors.PropertyValueConverterBase" />
[DefaultPropertyValueConverter]
public class TagsValueConverter : PropertyValueConverterBase
{
private static readonly ConcurrentDictionary<int, bool> Storages = new();
private readonly IDataTypeService _dataTypeService;
private readonly IJsonSerializer _jsonSerializer;
/// <summary>
/// Initializes a new instance of the <see cref="TagsValueConverter" /> class.
/// </summary>
/// <param name="jsonSerializer">The JSON serializer.</param>
/// <exception cref="System.ArgumentNullException">jsonSerializer</exception>
public TagsValueConverter(IJsonSerializer jsonSerializer)
=> _jsonSerializer = jsonSerializer ?? throw new ArgumentNullException(nameof(jsonSerializer));
/// <summary>
/// Initializes a new instance of the <see cref="TagsValueConverter" /> class.
/// </summary>
/// <param name="dataTypeService">The data type service.</param>
/// <param name="jsonSerializer">The JSON serializer.</param>
[Obsolete("The IDataTypeService is not used anymore. This constructor will be removed in a future version.")]
public TagsValueConverter(IDataTypeService dataTypeService, IJsonSerializer jsonSerializer)
{
_dataTypeService = dataTypeService ?? throw new ArgumentNullException(nameof(dataTypeService));
_jsonSerializer = jsonSerializer ?? throw new ArgumentNullException(nameof(jsonSerializer));
}
: this(jsonSerializer)
{ }
public static void ClearCaches() => Storages.Clear();
/// <summary>
/// Clears the data type configuration caches.
/// </summary>
[Obsolete("Caching of data type configuration is not done anymore. This method will be removed in a future version.")]
public static void ClearCaches()
{ }
/// <inheritdoc />
public override bool IsConverter(IPublishedPropertyType propertyType)
=> propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.Tags);
/// <inheritdoc />
public override Type GetPropertyValueType(IPublishedPropertyType propertyType)
=> typeof(IEnumerable<string>);
/// <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)
string? sourceString = source?.ToString();
if (string.IsNullOrEmpty(sourceString))
{
return Array.Empty<string>();
}
// if Json storage type deserialize and return as string array
if (JsonStorageType(propertyType.DataType.Id))
{
var array = source.ToString() is not null
? _jsonSerializer.Deserialize<string[]>(source.ToString()!)
: null;
return array ?? Array.Empty<string>();
}
// Otherwise assume CSV storage type and return as string array
return source.ToString()?.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries);
return IsJson(propertyType)
? _jsonSerializer.Deserialize<string[]>(sourceString) ?? Array.Empty<string>()
: sourceString.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries);
}
public override object? ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object? source, bool preview) => (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
Storages.GetOrAdd(dataTypeId, id =>
{
TagConfiguration? configuration = _dataTypeService.GetDataType(id)?.ConfigurationAs<TagConfiguration>();
return configuration?.StorageType == TagsStorageType.Json;
});
private static bool IsJson(IPublishedPropertyType propertyType)
=> propertyType.DataType.ConfigurationAs<TagConfiguration>()?.StorageType == TagsStorageType.Json;
}

View File

@@ -0,0 +1,38 @@
namespace Umbraco.Cms.Core.Security;
public class FileStreamSecurityValidator : IFileStreamSecurityValidator
{
private readonly IEnumerable<IFileStreamSecurityAnalyzer> _fileAnalyzers;
public FileStreamSecurityValidator(IEnumerable<IFileStreamSecurityAnalyzer> fileAnalyzers)
{
_fileAnalyzers = fileAnalyzers;
}
/// <summary>
/// Analyzes whether the file content is considered safe with registered IFileStreamSecurityAnalyzers
/// </summary>
/// <param name="fileStream">Needs to be a Read seekable stream</param>
/// <returns>Whether the file is considered safe after running the necessary analyzers</returns>
public bool IsConsideredSafe(Stream fileStream)
{
foreach (var fileAnalyzer in _fileAnalyzers)
{
fileStream.Seek(0, SeekOrigin.Begin);
if (!fileAnalyzer.ShouldHandle(fileStream))
{
continue;
}
fileStream.Seek(0, SeekOrigin.Begin);
if (fileAnalyzer.IsConsideredSafe(fileStream) == false)
{
return false;
}
}
fileStream.Seek(0, SeekOrigin.Begin);
// If no analyzer we consider the file to be safe as the implementer has the possibility to add additional analyzers
// Or all analyzers deem te file to be safe
return true;
}
}

View File

@@ -0,0 +1,20 @@
namespace Umbraco.Cms.Core.Security;
public interface IFileStreamSecurityAnalyzer
{
/// <summary>
/// Indicates whether the analyzer should process the file
/// The implementation should be considerably faster than IsConsideredSafe
/// </summary>
/// <param name="fileStream"></param>
/// <returns></returns>
bool ShouldHandle(Stream fileStream);
/// <summary>
/// Analyzes whether the file content is considered safe
/// </summary>
/// <param name="fileStream">Needs to be a Read/Write seekable stream</param>
/// <returns>Whether the file is considered safe</returns>
bool IsConsideredSafe(Stream fileStream);
}

View File

@@ -0,0 +1,11 @@
namespace Umbraco.Cms.Core.Security;
public interface IFileStreamSecurityValidator
{
/// <summary>
/// Analyzes wether the file content is considered safe with registered IFileStreamSecurityAnalyzers
/// </summary>
/// <param name="fileStream">Needs to be a Read seekable stream</param>
/// <returns>Whether the file is considered safe after running the necessary analyzers</returns>
bool IsConsideredSafe(Stream fileStream);
}