Merge conflicts resolved

This commit is contained in:
Bjarke Berg
2020-08-06 12:59:21 +02:00
235 changed files with 29349 additions and 4064 deletions
+4 -6
View File
@@ -30,11 +30,6 @@ dotnet_naming_symbols.private_fields.applicable_accessibilities = private
dotnet_naming_style.prefix_underscore.capitalization = camel_case
dotnet_naming_style.prefix_underscore.required_prefix = _
# https://github.com/MicrosoftDocs/visualstudio-docs/blob/master/docs/ide/editorconfig-code-style-settings-reference.md
[*.cs]
csharp_style_var_for_built_in_types = true:suggestion
@@ -42,5 +37,8 @@ csharp_style_var_when_type_is_apparent = true:suggestion
csharp_style_var_elsewhere = true:suggestion
csharp_prefer_braces = false : none
[*.{js,less}]
[*.js]
trim_trailing_whitespace = true
[*.less]
trim_trailing_whitespace = false
@@ -0,0 +1,24 @@
name: Microsoft Rich Code Navigation
on:
pull_request:
push:
branches: [ v8/dev ]
jobs:
RichNavigation:
runs-on: windows-latest
steps:
- uses: actions/checkout@v2
- name: Run Umbraco Build Script with Powershell
working-directory: 'build'
run: .\build.ps1
shell: powershell
- name: Microsoft Rich Code Navigation Indexer
uses: microsoft/RichCodeNavIndexer@v0.1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
languages: csharp,typescript
@@ -36,6 +36,11 @@ namespace Umbraco.Core
/// </summary>
public static class Aliases
{
/// <summary>
/// Block List.
/// </summary>
public const string BlockList = "Umbraco.BlockList";
/// <summary>
/// CheckBox List.
/// </summary>
@@ -21,6 +21,7 @@
public const string AnyGuid = "any-guid"; // that one is for tests
public const string Element = "element";
public const string Document = "document";
public const string DocumentBlueprint = "document-blueprint";
@@ -12,7 +12,7 @@ namespace Umbraco.Web.Models.ContentEditing
[DataContract(Name = "content", Namespace = "")]
public class ContentItemSave : IContentSave<IContent>
{
protected ContentItemSave()
public ContentItemSave()
{
UploadedFiles = new List<ContentPropertyFile>();
Variants = new List<ContentVariantSave>();
@@ -44,6 +44,9 @@ namespace Umbraco.Web.Models.Mapping
property.PropertyType.PropertyEditorAlias);
editor = _propertyEditors[Constants.PropertyEditors.Aliases.Label];
if (editor == null)
throw new InvalidOperationException($"Could not resolve the property editor {Constants.PropertyEditors.Aliases.Label}");
}
dest.Id = property.Id;
@@ -1,7 +1,21 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
namespace Umbraco.Core.Models.PublishedContent
{
/// <summary>
/// Represents an <see cref="IPublishedElement"/> type.
/// </summary>
/// <remarks>Instances implementing the <see cref="IPublishedContentType"/> interface should be
/// immutable, ie if the content type changes, then a new instance needs to be created.</remarks>
public interface IPublishedContentType2 : IPublishedContentType
{
/// <summary>
/// Gets the unique key for the content type.
/// </summary>
Guid Key { get; }
}
/// <summary>
/// Represents an <see cref="IPublishedElement"/> type.
/// </summary>
@@ -1,5 +1,6 @@
namespace Umbraco. Core.Models.PublishedContent
{
/// <summary>
/// Creates published content types.
/// </summary>
@@ -3,6 +3,7 @@ using System.Collections;
namespace Umbraco.Core.Models.PublishedContent
{
/// <summary>
/// Provides the published model creation service.
/// </summary>
@@ -9,7 +9,7 @@ namespace Umbraco.Core.Models.PublishedContent
/// </summary>
/// <remarks>Instances of the <see cref="PublishedContentType"/> class are immutable, ie
/// if the content type changes, then a new class needs to be created.</remarks>
public class PublishedContentType : IPublishedContentType
public class PublishedContentType : IPublishedContentType2
{
private readonly IPublishedPropertyType[] _propertyTypes;
@@ -20,7 +20,7 @@ namespace Umbraco.Core.Models.PublishedContent
/// Initializes a new instance of the <see cref="PublishedContentType"/> class with a content type.
/// </summary>
public PublishedContentType(IContentTypeComposition contentType, IPublishedContentTypeFactory factory)
: this(contentType.Id, contentType.Alias, contentType.GetItemType(), contentType.CompositionAliases(), contentType.Variations, contentType.IsElement)
: this(contentType.Key, contentType.Id, contentType.Alias, contentType.GetItemType(), contentType.CompositionAliases(), contentType.Variations, contentType.IsElement)
{
var propertyTypes = contentType.CompositionPropertyTypes
.Select(x => factory.CreatePropertyType(this, x))
@@ -40,8 +40,20 @@ namespace Umbraco.Core.Models.PublishedContent
/// <remarks>
/// <para>Values are assumed to be consistent and are not checked.</para>
/// </remarks>
public PublishedContentType(Guid key, int id, string alias, PublishedItemType itemType, IEnumerable<string> compositionAliases, IEnumerable<PublishedPropertyType> propertyTypes, ContentVariation variations, bool isElement = false)
: this(key, id, alias, itemType, compositionAliases, variations, isElement)
{
var propertyTypesA = propertyTypes.ToArray();
foreach (var propertyType in propertyTypesA)
propertyType.ContentType = this;
_propertyTypes = propertyTypesA;
InitializeIndexes();
}
[Obsolete("Use the overload specifying a key instead")]
public PublishedContentType(int id, string alias, PublishedItemType itemType, IEnumerable<string> compositionAliases, IEnumerable<PublishedPropertyType> propertyTypes, ContentVariation variations, bool isElement = false)
: this (id, alias, itemType, compositionAliases, variations, isElement)
: this (Guid.Empty, id, alias, itemType, compositionAliases, variations, isElement)
{
var propertyTypesA = propertyTypes.ToArray();
foreach (var propertyType in propertyTypesA)
@@ -57,16 +69,26 @@ namespace Umbraco.Core.Models.PublishedContent
/// <remarks>
/// <para>Values are assumed to be consistent and are not checked.</para>
/// </remarks>
public PublishedContentType(Guid key, int id, string alias, PublishedItemType itemType, IEnumerable<string> compositionAliases, Func<IPublishedContentType, IEnumerable<IPublishedPropertyType>> propertyTypes, ContentVariation variations, bool isElement = false)
: this(key, id, alias, itemType, compositionAliases, variations, isElement)
{
_propertyTypes = propertyTypes(this).ToArray();
InitializeIndexes();
}
[Obsolete("Use the overload specifying a key instead")]
public PublishedContentType(int id, string alias, PublishedItemType itemType, IEnumerable<string> compositionAliases, Func<IPublishedContentType, IEnumerable<IPublishedPropertyType>> propertyTypes, ContentVariation variations, bool isElement = false)
: this(id, alias, itemType, compositionAliases, variations, isElement)
: this(Guid.Empty, id, alias, itemType, compositionAliases, variations, isElement)
{
_propertyTypes = propertyTypes(this).ToArray();
InitializeIndexes();
}
private PublishedContentType(int id, string alias, PublishedItemType itemType, IEnumerable<string> compositionAliases, ContentVariation variations, bool isElement)
private PublishedContentType(Guid key, int id, string alias, PublishedItemType itemType, IEnumerable<string> compositionAliases, ContentVariation variations, bool isElement)
{
Key = key;
Id = id;
Alias = alias;
ItemType = itemType;
@@ -115,6 +137,9 @@ namespace Umbraco.Core.Models.PublishedContent
#region Content type
/// <inheritdoc />
public Guid Key { get; }
/// <inheritdoc />
public int Id { get; }
@@ -0,0 +1,24 @@
using System;
namespace Umbraco.Core.Models.PublishedContent
{
public static class PublishedContentTypeExtensions
{
/// <summary>
/// Get the GUID key from an <see cref="IPublishedContentType"/>
/// </summary>
/// <param name="publishedContentType"></param>
/// <param name="key"></param>
/// <returns></returns>
public static bool TryGetKey(this IPublishedContentType publishedContentType, out Guid key)
{
if (publishedContentType is IPublishedContentType2 contentTypeWithKey)
{
key = contentTypeWithKey.Key;
return true;
}
key = Guid.Empty;
return false;
}
}
}
+3 -2
View File
@@ -51,6 +51,7 @@ namespace Umbraco.Core.Models
public Guid Key { get; }
/// <inheritdoc />
public ITemplate DefaultTemplate { get; }
public ContentVariation Variations { get; }
@@ -58,7 +59,7 @@ namespace Umbraco.Core.Models
public string Icon { get; }
public bool IsContainer { get; }
public string Name { get; }
public bool AllowedAsRoot { get; }
@@ -93,5 +94,5 @@ namespace Umbraco.Core.Models
return ((Alias != null ? Alias.GetHashCode() : 0) * 397) ^ Id;
}
}
}
}
}
@@ -30,18 +30,23 @@ namespace Umbraco.Core.PropertyEditors
return value != null && (!(value is string) || string.IsNullOrWhiteSpace((string) value) == false);
}
/// <inheritdoc />
public virtual Type GetPropertyValueType(IPublishedPropertyType propertyType)
=> typeof (object);
/// <inheritdoc />
public virtual PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType)
=> PropertyCacheLevel.Snapshot;
/// <inheritdoc />
public virtual object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview)
=> source;
/// <inheritdoc />
public virtual object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview)
=> inter;
/// <inheritdoc />
public virtual object ConvertIntermediateToXPath(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview)
=> inter?.ToString() ?? string.Empty;
}
@@ -235,5 +235,12 @@ namespace Umbraco.Web.PublishedCache
/// <param name="contentType">The content type.</param>
/// <returns>The contents.</returns>
IEnumerable<IPublishedContent> GetByContentType(IPublishedContentType contentType);
/// <summary>
/// Gets a content type identified by its alias.
/// </summary>
/// <param name="key">The content type key.</param>
/// <returns>The content type, or null.</returns>
IPublishedContentType GetContentType(Guid key);
}
}
@@ -96,8 +96,8 @@ namespace Umbraco.Web.PublishedCache
}
public abstract IPublishedContentType GetContentType(int id);
public abstract IPublishedContentType GetContentType(string alias);
public abstract IPublishedContentType GetContentType(Guid key);
public virtual IEnumerable<IPublishedContent> GetByContentType(IPublishedContentType contentType)
{
+2 -1
View File
@@ -172,7 +172,7 @@ namespace Umbraco.Core
});
}
/// <summary>
/// Registers a custom entity type.
@@ -187,6 +187,7 @@ namespace Umbraco.Core
{ Constants.UdiEntityType.Unknown, UdiType.Unknown },
{ Constants.UdiEntityType.AnyGuid, UdiType.GuidUdi },
{ Constants.UdiEntityType.Element, UdiType.GuidUdi },
{ Constants.UdiEntityType.Document, UdiType.GuidUdi },
{ Constants.UdiEntityType.DocumentBlueprint, UdiType.GuidUdi },
{ Constants.UdiEntityType.Media, UdiType.GuidUdi },
@@ -228,7 +228,7 @@ namespace Umbraco.Web.Compose
}
catch (Exception e)
{
_logger.Error<InstructionProcessTask>("Failed (will repeat).", e);
_logger.Error<InstructionProcessTask>(e, "Failed (will repeat).");
}
return true; // repeat
}
@@ -3,6 +3,8 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Examine;
using Umbraco.Core.Composing;
using Umbraco.Core.Logging;
namespace Umbraco.Examine
{
@@ -12,12 +14,14 @@ namespace Umbraco.Examine
/// </summary>
public class IndexRebuilder
{
private readonly IProfilingLogger _logger;
private readonly IEnumerable<IIndexPopulator> _populators;
public IExamineManager ExamineManager { get; }
public IndexRebuilder(IExamineManager examineManager, IEnumerable<IIndexPopulator> populators)
public IndexRebuilder(IProfilingLogger logger, IExamineManager examineManager, IEnumerable<IIndexPopulator> populators)
{
_populators = populators;
_logger = logger;
ExamineManager = examineManager;
}
@@ -55,7 +59,14 @@ namespace Umbraco.Examine
// run each populator over the indexes
foreach(var populator in _populators)
{
populator.Populate(indexes);
try
{
populator.Populate(indexes);
}
catch (Exception e)
{
_logger.Error<IndexRebuilder>(e, "Index populating failed for populator {Populator}", populator.GetType());
}
}
}
@@ -194,9 +194,9 @@ namespace Umbraco.Core.Migrations.Upgrade
To<MissingContentVersionsIndexes>("{EE288A91-531B-4995-8179-1D62D9AA3E2E}");
To<AddMainDomLock>("{2AB29964-02A1-474D-BD6B-72148D2A53A2}");
// to 8.7.0...
To<MissingDictionaryIndex>("{a78e3369-8ea3-40ec-ad3f-5f76929d2b20}");
//FINAL
}
}
@@ -0,0 +1,45 @@
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
namespace Umbraco.Core.Models.Blocks
{
/// <summary>
/// Convertable block data from json
/// </summary>
public class BlockEditorData
{
private readonly string _propertyEditorAlias;
public static BlockEditorData Empty { get; } = new BlockEditorData();
private BlockEditorData()
{
BlockValue = new BlockValue();
}
public BlockEditorData(string propertyEditorAlias,
IEnumerable<ContentAndSettingsReference> references,
BlockValue blockValue)
{
if (string.IsNullOrWhiteSpace(propertyEditorAlias))
throw new ArgumentException($"'{nameof(propertyEditorAlias)}' cannot be null or whitespace", nameof(propertyEditorAlias));
_propertyEditorAlias = propertyEditorAlias;
BlockValue = blockValue ?? throw new ArgumentNullException(nameof(blockValue));
References = references != null ? new List<ContentAndSettingsReference>(references) : throw new ArgumentNullException(nameof(references));
}
/// <summary>
/// Returns the layout for this specific property editor
/// </summary>
public JToken Layout => BlockValue.Layout.TryGetValue(_propertyEditorAlias, out var layout) ? layout : null;
/// <summary>
/// Returns the reference to the original BlockValue
/// </summary>
public BlockValue BlockValue { get; }
public List<ContentAndSettingsReference> References { get; } = new List<ContentAndSettingsReference>();
}
}
@@ -0,0 +1,43 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System.Linq;
using System.Collections.Generic;
namespace Umbraco.Core.Models.Blocks
{
/// <summary>
/// Converts the block json data into objects
/// </summary>
public abstract class BlockEditorDataConverter
{
private readonly string _propertyEditorAlias;
protected BlockEditorDataConverter(string propertyEditorAlias)
{
_propertyEditorAlias = propertyEditorAlias;
}
public BlockEditorData Deserialize(string json)
{
var value = JsonConvert.DeserializeObject<BlockValue>(json);
if (value.Layout == null)
return BlockEditorData.Empty;
var references = value.Layout.TryGetValue(_propertyEditorAlias, out var layout)
? GetBlockReferences(layout)
: Enumerable.Empty<ContentAndSettingsReference>();
return new BlockEditorData(_propertyEditorAlias, references, value);
}
/// <summary>
/// Return the collection of <see cref="IBlockReference"/> from the block editor's Layout (which could be an array or an object depending on the editor)
/// </summary>
/// <param name="jsonLayout"></param>
/// <returns></returns>
protected abstract IEnumerable<ContentAndSettingsReference> GetBlockReferences(JToken jsonLayout);
}
}
@@ -0,0 +1,36 @@
using System;
using System.Collections.Generic;
using System.Runtime.Serialization;
using Umbraco.Core.Models.PublishedContent;
namespace Umbraco.Core.Models.Blocks
{
/// <summary>
/// The base class for any strongly typed model for a Block editor implementation
/// </summary>
public abstract class BlockEditorModel
{
protected BlockEditorModel(IEnumerable<IPublishedElement> contentData, IEnumerable<IPublishedElement> settingsData)
{
ContentData = contentData ?? throw new ArgumentNullException(nameof(contentData));
SettingsData = settingsData ?? new List<IPublishedContent>();
}
public BlockEditorModel()
{
}
/// <summary>
/// The content data items of the Block List editor
/// </summary>
[DataMember(Name = "contentData")]
public IEnumerable<IPublishedElement> ContentData { get; set; } = new List<IPublishedContent>();
/// <summary>
/// The settings data items of the Block List editor
/// </summary>
[DataMember(Name = "settingsData")]
public IEnumerable<IPublishedElement> SettingsData { get; set; } = new List<IPublishedContent>();
}
}
@@ -0,0 +1,56 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using Umbraco.Core.Serialization;
namespace Umbraco.Core.Models.Blocks
{
/// <summary>
/// Represents a single block's data in raw form
/// </summary>
public class BlockItemData
{
[JsonProperty("contentTypeKey")]
public Guid ContentTypeKey { get; set; }
/// <summary>
/// not serialized, manually set and used during internally
/// </summary>
[JsonIgnore]
public string ContentTypeAlias { get; set; }
[JsonProperty("udi")]
[JsonConverter(typeof(UdiJsonConverter))]
public Udi Udi { get; set; }
[JsonIgnore]
public Guid Key => Udi != null ? ((GuidUdi)Udi).Guid : throw new InvalidOperationException("No Udi assigned");
/// <summary>
/// The remaining properties will be serialized to a dictionary
/// </summary>
/// <remarks>
/// The JsonExtensionDataAttribute is used to put the non-typed properties into a bucket
/// http://www.newtonsoft.com/json/help/html/DeserializeExtensionData.htm
/// NestedContent serializes to string, int, whatever eg
/// "stringValue":"Some String","numericValue":125,"otherNumeric":null
/// </remarks>
[JsonExtensionData]
public Dictionary<string, object> RawPropertyValues { get; set; } = new Dictionary<string, object>();
/// <summary>
/// Used during deserialization to convert the raw property data into data with a property type context
/// </summary>
[JsonIgnore]
public IDictionary<string, BlockPropertyValue> PropertyValues { get; set; } = new Dictionary<string, BlockPropertyValue>();
/// <summary>
/// Used during deserialization to populate the property value/property type of a block item content property
/// </summary>
public class BlockPropertyValue
{
public object Value { get; set; }
public IPropertyType PropertyType { get; set; }
}
}
}
@@ -0,0 +1,22 @@
using Newtonsoft.Json.Linq;
using System.Linq;
using System.Collections.Generic;
namespace Umbraco.Core.Models.Blocks
{
/// <summary>
/// Data converter for the block list property editor
/// </summary>
public class BlockListEditorDataConverter : BlockEditorDataConverter
{
public BlockListEditorDataConverter() : base(Constants.PropertyEditors.Aliases.BlockList)
{
}
protected override IEnumerable<ContentAndSettingsReference> GetBlockReferences(JToken jsonLayout)
{
var blockListLayout = jsonLayout.ToObject<IEnumerable<BlockListLayoutItem>>();
return blockListLayout.Select(x => new ContentAndSettingsReference(x.ContentUdi, x.SettingsUdi)).ToList();
}
}
}
@@ -0,0 +1,19 @@
using Newtonsoft.Json;
using Umbraco.Core.Serialization;
namespace Umbraco.Core.Models.Blocks
{
/// <summary>
/// Used for deserializing the block list layout
/// </summary>
public class BlockListLayoutItem
{
[JsonProperty("contentUdi", Required = Required.Always)]
[JsonConverter(typeof(UdiJsonConverter))]
public Udi ContentUdi { get; set; }
[JsonProperty("settingsUdi")]
[JsonConverter(typeof(UdiJsonConverter))]
public Udi SettingsUdi { get; set; }
}
}
@@ -0,0 +1,51 @@
using System;
using System.Runtime.Serialization;
using Umbraco.Core.Models.PublishedContent;
namespace Umbraco.Core.Models.Blocks
{
/// <summary>
/// Represents a layout item for the Block List editor
/// </summary>
[DataContract(Name = "blockListLayout", Namespace = "")]
public class BlockListLayoutReference : IBlockReference<IPublishedElement>
{
public BlockListLayoutReference(Udi contentUdi, IPublishedElement content, Udi settingsUdi, IPublishedElement settings)
{
ContentUdi = contentUdi ?? throw new ArgumentNullException(nameof(contentUdi));
Content = content ?? throw new ArgumentNullException(nameof(content));
Settings = settings; // can be null
SettingsUdi = settingsUdi; // can be null
}
/// <summary>
/// The Id of the content data item
/// </summary>
[DataMember(Name = "contentUdi")]
public Udi ContentUdi { get; }
/// <summary>
/// The Id of the settings data item
/// </summary>
[DataMember(Name = "settingsUdi")]
public Udi SettingsUdi { get; }
/// <summary>
/// The content data item referenced
/// </summary>
/// <remarks>
/// This is ignored from serialization since it is just a reference to the actual data element
/// </remarks>
[IgnoreDataMember]
public IPublishedElement Content { get; }
/// <summary>
/// The settings data item referenced
/// </summary>
/// <remarks>
/// This is ignored from serialization since it is just a reference to the actual data element
/// </remarks>
[IgnoreDataMember]
public IPublishedElement Settings { get; }
}
}
@@ -0,0 +1,33 @@
using System.Collections.Generic;
using System.Runtime.Serialization;
using Umbraco.Core.Models.PublishedContent;
namespace Umbraco.Core.Models.Blocks
{
/// <summary>
/// The strongly typed model for the Block List editor
/// </summary>
[DataContract(Name = "blockList", Namespace = "")]
public class BlockListModel : BlockEditorModel
{
public static BlockListModel Empty { get; } = new BlockListModel();
private BlockListModel()
{
}
public BlockListModel(IEnumerable<IPublishedElement> contentData, IEnumerable<IPublishedElement> settingsData, IEnumerable<BlockListLayoutReference> layout)
: base(contentData, settingsData)
{
Layout = layout;
}
/// <summary>
/// The layout items of the Block List editor
/// </summary>
[DataMember(Name = "layout")]
public IEnumerable<BlockListLayoutReference> Layout { get; } = new List<BlockListLayoutReference>();
}
}
@@ -0,0 +1,18 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System.Collections.Generic;
namespace Umbraco.Core.Models.Blocks
{
public class BlockValue
{
[JsonProperty("layout")]
public IDictionary<string, JToken> Layout { get; set; }
[JsonProperty("contentData")]
public List<BlockItemData> ContentData { get; set; } = new List<BlockItemData>();
[JsonProperty("settingsData")]
public List<BlockItemData> SettingsData { get; set; } = new List<BlockItemData>();
}
}
@@ -0,0 +1,46 @@
using System.Collections.Generic;
using System;
namespace Umbraco.Core.Models.Blocks
{
public struct ContentAndSettingsReference : IEquatable<ContentAndSettingsReference>
{
public ContentAndSettingsReference(Udi contentUdi, Udi settingsUdi)
{
ContentUdi = contentUdi ?? throw new ArgumentNullException(nameof(contentUdi));
SettingsUdi = settingsUdi;
}
public Udi ContentUdi { get; }
public Udi SettingsUdi { get; }
public override bool Equals(object obj)
{
return obj is ContentAndSettingsReference reference && Equals(reference);
}
public bool Equals(ContentAndSettingsReference other)
{
return EqualityComparer<Udi>.Default.Equals(ContentUdi, other.ContentUdi) &&
EqualityComparer<Udi>.Default.Equals(SettingsUdi, other.SettingsUdi);
}
public override int GetHashCode()
{
var hashCode = 272556606;
hashCode = hashCode * -1521134295 + EqualityComparer<Udi>.Default.GetHashCode(ContentUdi);
hashCode = hashCode * -1521134295 + EqualityComparer<Udi>.Default.GetHashCode(SettingsUdi);
return hashCode;
}
public static bool operator ==(ContentAndSettingsReference left, ContentAndSettingsReference right)
{
return left.Equals(right);
}
public static bool operator !=(ContentAndSettingsReference left, ContentAndSettingsReference right)
{
return !(left == right);
}
}
}
@@ -0,0 +1,26 @@
namespace Umbraco.Core.Models.Blocks
{
/// <summary>
/// Represents a data item reference for a Block editor implementation
/// </summary>
/// <typeparam name="TSettings"></typeparam>
/// <remarks>
/// see: https://github.com/umbraco/rfcs/blob/907f3758cf59a7b6781296a60d57d537b3b60b8c/cms/0011-block-data-structure.md#strongly-typed
/// </remarks>
public interface IBlockReference<TSettings> : IBlockReference
{
TSettings Settings { get; }
}
/// <summary>
/// Represents a data item reference for a Block Editor implementation
/// </summary>
/// <remarks>
/// see: https://github.com/umbraco/rfcs/blob/907f3758cf59a7b6781296a60d57d537b3b60b8c/cms/0011-block-data-structure.md#strongly-typed
/// </remarks>
public interface IBlockReference
{
Udi ContentUdi { get; }
}
}
@@ -111,6 +111,9 @@ namespace Umbraco.Web.Models.ContentEditing
[DataMember(Name = "contentTypeId")]
public int ContentTypeId { get; set; }
[DataMember(Name = "contentTypeKey")]
public Guid ContentTypeKey { get; set; }
[DataMember(Name = "contentTypeAlias", IsRequired = true)]
[Required(AllowEmptyStrings = false)]
public string ContentTypeAlias { get; set; }
@@ -69,7 +69,7 @@ namespace Umbraco.Core.Models
/// Internal property to store the Id of the default template
/// </summary>
[DataMember]
internal int DefaultTemplateId
public int DefaultTemplateId
{
get => _defaultTemplate;
set => SetPropertyValueAndDetectChanges(value, ref _defaultTemplate, nameof(DefaultTemplateId));
@@ -3,6 +3,7 @@ using Umbraco.Core.Composing;
namespace Umbraco.Core.Models.PublishedContent
{
/// <summary>
/// Provides strongly typed published content models services.
/// </summary>
@@ -35,18 +35,18 @@ namespace Umbraco.Core.Models.PublishedContent
/// This method is for tests and is not intended to be used directly from application code.
/// </summary>
/// <remarks>Values are assumed to be consisted and are not checked.</remarks>
internal IPublishedContentType CreateContentType(int id, string alias, Func<IPublishedContentType, IEnumerable<IPublishedPropertyType>> propertyTypes, ContentVariation variations = ContentVariation.Nothing, bool isElement = false)
internal IPublishedContentType CreateContentType(Guid key, int id, string alias, Func<IPublishedContentType, IEnumerable<IPublishedPropertyType>> propertyTypes, ContentVariation variations = ContentVariation.Nothing, bool isElement = false)
{
return new PublishedContentType(id, alias, PublishedItemType.Content, Enumerable.Empty<string>(), propertyTypes, variations, isElement);
return new PublishedContentType(key, id, alias, PublishedItemType.Content, Enumerable.Empty<string>(), propertyTypes, variations, isElement);
}
/// <summary>
/// This method is for tests and is not intended to be used directly from application code.
/// </summary>
/// <remarks>Values are assumed to be consisted and are not checked.</remarks>
internal IPublishedContentType CreateContentType(int id, string alias, IEnumerable<string> compositionAliases, Func<IPublishedContentType, IEnumerable<IPublishedPropertyType>> propertyTypes, ContentVariation variations = ContentVariation.Nothing, bool isElement = false)
internal IPublishedContentType CreateContentType(Guid key, int id, string alias, IEnumerable<string> compositionAliases, Func<IPublishedContentType, IEnumerable<IPublishedPropertyType>> propertyTypes, ContentVariation variations = ContentVariation.Nothing, bool isElement = false)
{
return new PublishedContentType(id, alias, PublishedItemType.Content, compositionAliases, propertyTypes, variations, isElement);
return new PublishedContentType(key, id, alias, PublishedItemType.Content, compositionAliases, propertyTypes, variations, isElement);
}
/// <inheritdoc />
@@ -60,7 +60,7 @@ namespace Umbraco.Core.Models
/// <summary>
/// Returns true if the template is used as a layout for other templates (i.e. it has 'children')
/// </summary>
public bool IsMasterTemplate { get; internal set; }
public bool IsMasterTemplate { get; set; }
public void SetMasterTemplate(ITemplate masterTemplate)
{
@@ -4,6 +4,10 @@ using System.Data.SqlClient;
namespace Umbraco.Core.Persistence.FaultHandling.Strategies
{
// See https://docs.microsoft.com/en-us/azure/azure-sql/database/troubleshoot-common-connectivity-issues
// Also we could just use the nuget package instead https://www.nuget.org/packages/EnterpriseLibrary.TransientFaultHandling/ ?
// but i guess that's not netcore so we'll just leave it.
/// <summary>
/// Provides the transient error detection logic for transient faults that are specific to SQL Azure.
/// </summary>
@@ -71,7 +75,7 @@ namespace Umbraco.Core.Persistence.FaultHandling.Strategies
/// Determines whether the specified exception represents a transient failure that can be compensated by a retry.
/// </summary>
/// <param name="ex">The exception object to be verified.</param>
/// <returns>True if the specified exception is considered as transient, otherwise false.</returns>
/// <returns>true if the specified exception is considered as transient; otherwise, false.</returns>
public bool IsTransient(Exception ex)
{
if (ex != null)
@@ -97,40 +101,50 @@ namespace Umbraco.Core.Persistence.FaultHandling.Strategies
return true;
// SQL Error Code: 40197
// The service has encountered an error processing your request. Please try again.
case 40197:
// SQL Error Code: 10928
// Resource ID: %d. The %s limit for the database is %d and has been reached.
case 10928:
// SQL Error Code: 10929
// Resource ID: %d. The %s minimum guarantee is %d, maximum limit is %d and the current usage for the database is %d.
// However, the server is currently too busy to support requests greater than %d for this database.
case 10929:
// SQL Error Code: 10053
// A transport-level error has occurred when receiving results from the server.
// An established connection was aborted by the software in your host machine.
case 10053:
// SQL Error Code: 10054
// A transport-level error has occurred when sending the request to the server.
// A transport-level error has occurred when sending the request to the server.
// (provider: TCP Provider, error: 0 - An existing connection was forcibly closed by the remote host.)
case 10054:
// SQL Error Code: 10060
// A network-related or instance-specific error occurred while establishing a connection to SQL Server.
// The server was not found or was not accessible. Verify that the instance name is correct and that SQL Server
// is configured to allow remote connections. (provider: TCP Provider, error: 0 - A connection attempt failed
// because the connected party did not properly respond after a period of time, or established connection failed
// A network-related or instance-specific error occurred while establishing a connection to SQL Server.
// The server was not found or was not accessible. Verify that the instance name is correct and that SQL Server
// is configured to allow remote connections. (provider: TCP Provider, error: 0 - A connection attempt failed
// because the connected party did not properly respond after a period of time, or established connection failed
// because connected host has failed to respond.)"}
case 10060:
// SQL Error Code: 40197
// The service has encountered an error processing your request. Please try again.
case 40197:
// SQL Error Code: 40540
// The service has encountered an error processing your request. Please try again.
case 40540:
// SQL Error Code: 40613
// Database XXXX on server YYYY is not currently available. Please retry the connection later. If the problem persists, contact customer
// Database XXXX on server YYYY is not currently available. Please retry the connection later. If the problem persists, contact customer
// support, and provide them the session tracing ID of ZZZZZ.
case 40613:
// SQL Error Code: 40143
// The service has encountered an error processing your request. Please try again.
case 40143:
// SQL Error Code: 233
// The client was unable to establish a connection because of an error during connection initialization process before login.
// Possible causes include the following: the client tried to connect to an unsupported version of SQL Server; the server was too busy
// to accept new connections; or there was a resource limitation (insufficient memory or maximum allowed connections) on the server.
// The client was unable to establish a connection because of an error during connection initialization process before login.
// Possible causes include the following: the client tried to connect to an unsupported version of SQL Server; the server was too busy
// to accept new connections; or there was a resource limitation (insufficient memory or maximum allowed connections) on the server.
// (provider: TCP Provider, error: 0 - An existing connection was forcibly closed by the remote host.)
case 233:
// SQL Error Code: 64
// A connection was successfully established with the server, but then an error occurred during the login process.
// (provider: TCP Provider, error: 0 - The specified network name is no longer available.)
// A connection was successfully established with the server, but then an error occurred during the login process.
// (provider: TCP Provider, error: 0 - The specified network name is no longer available.)
case 64:
// DBNETLIB Error Code: 20
// The instance of SQL Server you attempted to connect to does not support encryption.
@@ -12,6 +12,11 @@ namespace Umbraco.Core.Persistence.Repositories
/// </summary>
void ClearSchedule(DateTime date);
void ClearSchedule(DateTime date, ContentScheduleAction action);
bool HasContentForExpiration(DateTime date);
bool HasContentForRelease(DateTime date);
/// <summary>
/// Gets <see cref="IContent"/> objects having an expiration date before (lower than, or equal to) a specified date.
/// </summary>
@@ -1027,6 +1027,37 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
Database.Execute(sql);
}
/// <inheritdoc />
public void ClearSchedule(DateTime date, ContentScheduleAction action)
{
var a = action.ToString();
var sql = Sql().Delete<ContentScheduleDto>().Where<ContentScheduleDto>(x => x.Date <= date && x.Action == a);
Database.Execute(sql);
}
private Sql GetSqlForHasScheduling(ContentScheduleAction action, DateTime date)
{
var template = SqlContext.Templates.Get("Umbraco.Core.DocumentRepository.GetSqlForHasScheduling", tsql => tsql
.SelectCount()
.From<ContentScheduleDto>()
.Where<ContentScheduleDto>(x => x.Action == SqlTemplate.Arg<string>("action") && x.Date <= SqlTemplate.Arg<DateTime>("date")));
var sql = template.Sql(action.ToString(), date);
return sql;
}
public bool HasContentForExpiration(DateTime date)
{
var sql = GetSqlForHasScheduling(ContentScheduleAction.Expire, date);
return Database.ExecuteScalar<int>(sql) > 0;
}
public bool HasContentForRelease(DateTime date)
{
var sql = GetSqlForHasScheduling(ContentScheduleAction.Release, date);
return Database.ExecuteScalar<int>(sql) > 0;
}
/// <inheritdoc />
public IEnumerable<IContent> GetContentForRelease(DateTime date)
{
@@ -0,0 +1,395 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Umbraco.Core;
using Umbraco.Core.Logging;
using Umbraco.Core.Models;
using Umbraco.Core.Models.Blocks;
using Umbraco.Core.Models.Editors;
using Umbraco.Core.PropertyEditors;
using Umbraco.Core.Services;
using Umbraco.Core.Strings;
using static Umbraco.Core.Models.Blocks.BlockItemData;
namespace Umbraco.Web.PropertyEditors
{
/// <summary>
/// Abstract class for block editor based editors
/// </summary>
public abstract class BlockEditorPropertyEditor : DataEditor
{
public const string ContentTypeKeyPropertyKey = "contentTypeKey";
public const string UdiPropertyKey = "udi";
private readonly ILocalizedTextService _localizedTextService;
private readonly Lazy<PropertyEditorCollection> _propertyEditors;
private readonly IDataTypeService _dataTypeService;
private readonly IContentTypeService _contentTypeService;
public BlockEditorPropertyEditor(
ILogger logger,
Lazy<PropertyEditorCollection> propertyEditors,
IDataTypeService dataTypeService,
IContentTypeService contentTypeService,
ILocalizedTextService localizedTextService,
ILocalizationService localizationService,
IShortStringHelper shortStringHelper)
: base(logger, dataTypeService,localizationService,localizedTextService, shortStringHelper)
{
_localizedTextService = localizedTextService;
_propertyEditors = propertyEditors;
_dataTypeService = dataTypeService;
_contentTypeService = contentTypeService;
}
// has to be lazy else circular dep in ctor
private PropertyEditorCollection PropertyEditors => _propertyEditors.Value;
#region Value Editor
protected override IDataValueEditor CreateValueEditor() => new BlockEditorPropertyValueEditor(Attribute, PropertyEditors, _dataTypeService, _contentTypeService, _localizedTextService, Logger, LocalizationService,ShortStringHelper);
internal class BlockEditorPropertyValueEditor : DataValueEditor, IDataValueReference
{
private readonly PropertyEditorCollection _propertyEditors;
private readonly IDataTypeService _dataTypeService; // TODO: Not used yet but we'll need it to fill in the FromEditor/ToEditor
private readonly ILogger _logger;
private readonly BlockEditorValues _blockEditorValues;
public BlockEditorPropertyValueEditor(DataEditorAttribute attribute, PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, IContentTypeService contentTypeService, ILocalizedTextService textService, ILogger logger, ILocalizationService localizationService, IShortStringHelper shortStringHelper)
: base(dataTypeService, localizationService, textService, shortStringHelper, attribute)
{
_propertyEditors = propertyEditors;
_dataTypeService = dataTypeService;
_logger = logger;
_blockEditorValues = new BlockEditorValues(new BlockListEditorDataConverter(), contentTypeService, _logger);
Validators.Add(new BlockEditorValidator(_blockEditorValues, propertyEditors, dataTypeService, textService));
Validators.Add(new MinMaxValidator(_blockEditorValues, textService));
}
public IEnumerable<UmbracoEntityReference> GetReferences(object value)
{
var rawJson = value == null ? string.Empty : value is string str ? str : value.ToString();
var result = new List<UmbracoEntityReference>();
var blockEditorData = _blockEditorValues.DeserializeAndClean(rawJson);
if (blockEditorData == null)
return Enumerable.Empty<UmbracoEntityReference>();
// loop through all content and settings data
foreach (var row in blockEditorData.BlockValue.ContentData.Concat(blockEditorData.BlockValue.SettingsData))
{
foreach (var prop in row.PropertyValues)
{
var propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias];
var valueEditor = propEditor?.GetValueEditor();
if (!(valueEditor is IDataValueReference reference)) continue;
var val = prop.Value.Value?.ToString();
var refs = reference.GetReferences(val);
result.AddRange(refs);
}
}
return result;
}
#region Convert database // editor
// note: there is NO variant support here
/// <summary>
/// Ensure that sub-editor values are translated through their ToEditor methods
/// </summary>
/// <param name="property"></param>
/// <param name="dataTypeService"></param>
/// <param name="culture"></param>
/// <param name="segment"></param>
/// <returns></returns>
public override object ToEditor(IProperty property, string culture = null, string segment = null)
{
var val = property.GetValue(culture, segment);
BlockEditorData blockEditorData;
try
{
blockEditorData = _blockEditorValues.DeserializeAndClean(val);
}
catch (JsonSerializationException)
{
// if this occurs it means the data is invalid, shouldn't happen but has happened if we change the data format.
return string.Empty;
}
if (blockEditorData == null || blockEditorData.BlockValue.ContentData.Count == 0)
return string.Empty;
foreach (var row in blockEditorData.BlockValue.ContentData)
{
foreach (var prop in row.PropertyValues)
{
// create a temp property with the value
// - force it to be culture invariant as the block editor can't handle culture variant element properties
prop.Value.PropertyType.Variations = ContentVariation.Nothing;
var tempProp = new Property(prop.Value.PropertyType);
tempProp.SetValue(prop.Value.Value);
// convert that temp property, and store the converted value
var propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias];
if (propEditor == null)
{
// NOTE: This logic was borrowed from Nested Content and I'm unsure why it exists.
// if the property editor doesn't exist I think everything will break anyways?
// update the raw value since this is what will get serialized out
row.RawPropertyValues[prop.Key] = tempProp.GetValue()?.ToString();
continue;
}
var dataType = _dataTypeService.GetDataType(prop.Value.PropertyType.DataTypeId);
if (dataType == null)
{
// deal with weird situations by ignoring them (no comment)
row.PropertyValues.Remove(prop.Key);
_logger.Warn<BlockEditorPropertyValueEditor>(
"ToEditor removed property value {PropertyKey} in row {RowId} for property type {PropertyTypeAlias}",
prop.Key, row.Key, property.PropertyType.Alias);
continue;
}
var tempConfig = dataType.Configuration;
var valEditor = propEditor.GetValueEditor(tempConfig);
var convValue = valEditor.ToEditor(tempProp);
// update the raw value since this is what will get serialized out
row.RawPropertyValues[prop.Key] = convValue;
}
}
// return json convertable object
return blockEditorData.BlockValue;
}
/// <summary>
/// Ensure that sub-editor values are translated through their FromEditor methods
/// </summary>
/// <param name="editorValue"></param>
/// <param name="currentValue"></param>
/// <returns></returns>
public override object FromEditor(ContentPropertyData editorValue, object currentValue)
{
if (editorValue.Value == null || string.IsNullOrWhiteSpace(editorValue.Value.ToString()))
return null;
BlockEditorData blockEditorData;
try
{
blockEditorData = _blockEditorValues.DeserializeAndClean(editorValue.Value);
}
catch (JsonSerializationException)
{
// if this occurs it means the data is invalid, shouldn't happen but has happened if we change the data format.
return string.Empty;
}
if (blockEditorData == null || blockEditorData.BlockValue.ContentData.Count == 0)
return string.Empty;
foreach (var row in blockEditorData.BlockValue.ContentData)
{
foreach (var prop in row.PropertyValues)
{
// Fetch the property types prevalue
var propConfiguration = _dataTypeService.GetDataType(prop.Value.PropertyType.DataTypeId).Configuration;
// Lookup the property editor
var propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias];
if (propEditor == null) continue;
// Create a fake content property data object
var contentPropData = new ContentPropertyData(prop.Value.Value, propConfiguration);
// Get the property editor to do it's conversion
var newValue = propEditor.GetValueEditor().FromEditor(contentPropData, prop.Value.Value);
// update the raw value since this is what will get serialized out
row.RawPropertyValues[prop.Key] = newValue;
}
}
// return json
return JsonConvert.SerializeObject(blockEditorData.BlockValue);
}
#endregion
}
/// <summary>
/// Validates the min/max of the block editor
/// </summary>
private class MinMaxValidator : IValueValidator
{
private readonly BlockEditorValues _blockEditorValues;
private readonly ILocalizedTextService _textService;
public MinMaxValidator(BlockEditorValues blockEditorValues, ILocalizedTextService textService)
{
_blockEditorValues = blockEditorValues;
_textService = textService;
}
public IEnumerable<ValidationResult> Validate(object value, string valueType, object dataTypeConfiguration)
{
var blockConfig = (BlockListConfiguration)dataTypeConfiguration;
var blockEditorData = _blockEditorValues.DeserializeAndClean(value);
if ((blockEditorData == null && blockConfig?.ValidationLimit?.Min > 0)
|| (blockEditorData != null && blockEditorData.Layout.Count() < blockConfig?.ValidationLimit?.Min))
{
yield return new ValidationResult(
_textService.Localize("validation/entriesShort", new[] { blockConfig.ValidationLimit.Min.ToString(), (blockConfig.ValidationLimit.Min - blockEditorData.Layout.Count()).ToString() }),
new[] { "minCount" });
}
if (blockEditorData != null && blockEditorData.Layout.Count() > blockConfig?.ValidationLimit?.Max)
{
yield return new ValidationResult(
_textService.Localize("validation/entriesExceed", new[] { blockConfig.ValidationLimit.Max.ToString(), (blockEditorData.Layout.Count() - blockConfig.ValidationLimit.Max).ToString() }),
new[] { "maxCount" });
}
}
}
internal class BlockEditorValidator : ComplexEditorValidator
{
private readonly BlockEditorValues _blockEditorValues;
public BlockEditorValidator(BlockEditorValues blockEditorValues, PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, ILocalizedTextService textService) : base(propertyEditors, dataTypeService, textService)
{
_blockEditorValues = blockEditorValues;
}
protected override IEnumerable<ElementTypeValidationModel> GetElementTypeValidation(object value)
{
var blockEditorData = _blockEditorValues.DeserializeAndClean(value);
if (blockEditorData != null)
{
foreach (var row in blockEditorData.BlockValue.ContentData.Concat(blockEditorData.BlockValue.SettingsData))
{
var elementValidation = new ElementTypeValidationModel(row.ContentTypeAlias, row.Key);
foreach (var prop in row.PropertyValues)
{
elementValidation.AddPropertyTypeValidation(
new PropertyTypeValidationModel(prop.Value.PropertyType, prop.Value.Value));
}
yield return elementValidation;
}
}
}
}
/// <summary>
/// Used to deserialize json values and clean up any values based on the existence of element types and layout structure
/// </summary>
internal class BlockEditorValues
{
private readonly Lazy<Dictionary<Guid, IContentType>> _contentTypes;
private readonly BlockEditorDataConverter _dataConverter;
private readonly ILogger _logger;
public BlockEditorValues(BlockEditorDataConverter dataConverter, IContentTypeService contentTypeService, ILogger logger)
{
_contentTypes = new Lazy<Dictionary<Guid, IContentType>>(() => contentTypeService.GetAll().ToDictionary(c => c.Key));
_dataConverter = dataConverter;
_logger = logger;
}
private IContentType GetElementType(BlockItemData item)
{
_contentTypes.Value.TryGetValue(item.ContentTypeKey, out var contentType);
return contentType;
}
public BlockEditorData DeserializeAndClean(object propertyValue)
{
if (propertyValue == null || string.IsNullOrWhiteSpace(propertyValue.ToString()))
return null;
var blockEditorData = _dataConverter.Deserialize(propertyValue.ToString());
if (blockEditorData.BlockValue.ContentData.Count == 0)
{
// if there's no content ensure there's no settings too
blockEditorData.BlockValue.SettingsData.Clear();
return null;
}
var contentTypePropertyTypes = new Dictionary<string, Dictionary<string, IPropertyType>>();
// filter out any content that isn't referenced in the layout references
foreach(var block in blockEditorData.BlockValue.ContentData.Where(x => blockEditorData.References.Any(r => r.ContentUdi == x.Udi)))
{
ResolveBlockItemData(block, contentTypePropertyTypes);
}
// filter out any settings that isn't referenced in the layout references
foreach (var block in blockEditorData.BlockValue.SettingsData.Where(x => blockEditorData.References.Any(r => r.SettingsUdi == x.Udi)))
{
ResolveBlockItemData(block, contentTypePropertyTypes);
}
// remove blocks that couldn't be resolved
blockEditorData.BlockValue.ContentData.RemoveAll(x => x.ContentTypeAlias.IsNullOrWhiteSpace());
blockEditorData.BlockValue.SettingsData.RemoveAll(x => x.ContentTypeAlias.IsNullOrWhiteSpace());
return blockEditorData;
}
private bool ResolveBlockItemData(BlockItemData block, Dictionary<string, Dictionary<string, IPropertyType>> contentTypePropertyTypes)
{
var contentType = GetElementType(block);
if (contentType == null)
return false;
// get the prop types for this content type but keep a dictionary of found ones so we don't have to keep re-looking and re-creating
// objects on each iteration.
if (!contentTypePropertyTypes.TryGetValue(contentType.Alias, out var propertyTypes))
propertyTypes = contentTypePropertyTypes[contentType.Alias] = contentType.CompositionPropertyTypes.ToDictionary(x => x.Alias, x => x);
var propValues = new Dictionary<string, BlockPropertyValue>();
// find any keys that are not real property types and remove them
foreach (var prop in block.RawPropertyValues.ToList())
{
// doesn't exist so remove it
if (!propertyTypes.TryGetValue(prop.Key, out var propType))
{
block.RawPropertyValues.Remove(prop.Key);
_logger.Warn<BlockEditorValues>("The property {PropertyKey} for block {BlockKey} was removed because the property type {PropertyTypeAlias} was not found on {ContentTypeAlias}",
prop.Key, block.Key, prop.Key, contentType.Alias);
}
else
{
// set the value to include the resolved property type
propValues[prop.Key] = new BlockPropertyValue
{
PropertyType = propType,
Value = prop.Value
};
}
}
block.ContentTypeAlias = contentType.Alias;
block.PropertyValues = propValues;
return true;
}
}
#endregion
}
}
@@ -0,0 +1,76 @@
using Newtonsoft.Json;
using System;
using Umbraco.Core.PropertyEditors;
namespace Umbraco.Web.PropertyEditors
{
/// <summary>
/// The configuration object for the Block List editor
/// </summary>
public class BlockListConfiguration
{
[ConfigurationField("blocks", "Available Blocks", "views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.html", Description = "Define the available blocks.")]
public BlockConfiguration[] Blocks { get; set; }
public class BlockConfiguration
{
[JsonProperty("backgroundColor")]
public string BackgroundColor { get; set; }
[JsonProperty("iconColor")]
public string IconColor { get; set; }
[JsonProperty("thumbnail")]
public string Thumbnail { get; set; }
// TODO: This is named inconsistently in JS but renaming it needs to be done in quite a lot of places, this should be contentElementTypeKey
[JsonProperty("contentTypeKey")]
public Guid ContentElementTypeKey { get; set; }
[JsonProperty("settingsElementTypeKey")]
public Guid? SettingsElementTypeKey { get; set; }
[JsonProperty("view")]
public string View { get; set; }
[JsonProperty("stylesheet")]
public string Stylesheet { get; set; }
[JsonProperty("label")]
public string Label { get; set; }
[JsonProperty("editorSize")]
public string EditorSize { get; set; }
[JsonProperty("forceHideContentEditorInOverlay")]
public bool ForceHideContentEditorInOverlay { get; set; }
}
[ConfigurationField("validationLimit", "Amount", "numberrange", Description = "Set a required range of blocks")]
public NumberRange ValidationLimit { get; set; } = new NumberRange();
public class NumberRange
{
[JsonProperty("min")]
public int? Min { get; set; }
[JsonProperty("max")]
public int? Max { get; set; }
}
[ConfigurationField("useLiveEditing", "Live editing mode", "boolean", Description = "Live editing in editor overlays for live updated custom views.")]
public bool UseLiveEditing { get; set; }
[ConfigurationField("useInlineEditingAsDefault", "Inline editing mode", "boolean", Description = "Use the inline editor as the default block view.")]
public bool UseInlineEditingAsDefault { get; set; }
[ConfigurationField("maxPropertyWidth", "Property editor width", "textstring", Description = "optional css overwrite, example: 800px or 100%")]
public string MaxPropertyWidth { get; set; }
}
}
@@ -0,0 +1,19 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text.RegularExpressions;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Umbraco.Core;
using Umbraco.Core.IO;
using Umbraco.Core.PropertyEditors;
namespace Umbraco.Web.PropertyEditors
{
internal class BlockListConfigurationEditor : ConfigurationEditor<BlockListConfiguration>
{
public BlockListConfigurationEditor(IIOHelper ioHelper) : base(ioHelper)
{
}
}
}
@@ -0,0 +1,49 @@
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using Umbraco.Core;
using Umbraco.Core.IO;
using Umbraco.Core.Logging;
using Umbraco.Core.Models.Blocks;
using Umbraco.Core.PropertyEditors;
using Umbraco.Core.Services;
using Umbraco.Core.Strings;
namespace Umbraco.Web.PropertyEditors
{
/// <summary>
/// Represents a block list property editor.
/// </summary>
[DataEditor(
Constants.PropertyEditors.Aliases.BlockList,
"Block List",
"blocklist",
ValueType = ValueTypes.Json,
Group = Constants.PropertyEditors.Groups.Lists,
Icon = "icon-thumbnail-list")]
public class BlockListPropertyEditor : BlockEditorPropertyEditor
{
private readonly IIOHelper _ioHelper;
public BlockListPropertyEditor(
ILogger logger,
Lazy<PropertyEditorCollection> propertyEditors,
IDataTypeService dataTypeService,
IContentTypeService contentTypeService,
ILocalizedTextService localizedTextService,
IIOHelper ioHelper,
ILocalizationService localizationService,
IShortStringHelper shortStringHelper)
: base(logger, propertyEditors, dataTypeService, contentTypeService, localizedTextService, localizationService, shortStringHelper)
{
_ioHelper = ioHelper;
}
#region Pre Value Editor
protected override IConfigurationEditor CreateConfigurationEditor() => new BlockListConfigurationEditor(_ioHelper);
#endregion
}
}
@@ -0,0 +1,116 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Umbraco.Core;
using Umbraco.Core.Models;
using Umbraco.Core.PropertyEditors;
using Umbraco.Core.Services;
using Umbraco.Web.PropertyEditors.Validation;
namespace Umbraco.Web.PropertyEditors
{
/// <summary>
/// Used to validate complex editors that contain nested editors
/// </summary>
public abstract class ComplexEditorValidator : IValueValidator
{
private readonly PropertyValidationService _propertyValidationService;
public ComplexEditorValidator(PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, ILocalizedTextService textService)
{
_propertyValidationService = new PropertyValidationService(propertyEditors, dataTypeService, textService);
}
/// <summary>
/// Return a single <see cref="ComplexEditorValidationResult"/> for all sub nested validation results in the complex editor
/// </summary>
/// <param name="value"></param>
/// <param name="valueType"></param>
/// <param name="dataTypeConfiguration"></param>
/// <returns></returns>
public IEnumerable<ValidationResult> Validate(object value, string valueType, object dataTypeConfiguration)
{
var elementTypeValues = GetElementTypeValidation(value).ToList();
var rowResults = GetNestedValidationResults(elementTypeValues).ToList();
if (rowResults.Count > 0)
{
var result = new ComplexEditorValidationResult();
foreach(var rowResult in rowResults)
{
result.ValidationResults.Add(rowResult);
}
return result.Yield();
}
return Enumerable.Empty<ValidationResult>();
}
protected abstract IEnumerable<ElementTypeValidationModel> GetElementTypeValidation(object value);
/// <summary>
/// Return a nested validation result per row (Element Type)
/// </summary>
/// <param name="rawValue"></param>
/// <returns></returns>
protected IEnumerable<ComplexEditorElementTypeValidationResult> GetNestedValidationResults(IEnumerable<ElementTypeValidationModel> elements)
{
foreach (var row in elements)
{
var elementTypeValidationResult = new ComplexEditorElementTypeValidationResult(row.ElementTypeAlias, row.Id);
foreach (var prop in row.PropertyTypeValidation)
{
var propValidationResult = new ComplexEditorPropertyTypeValidationResult(prop.PropertyType.Alias);
foreach (var validationResult in _propertyValidationService.ValidatePropertyValue(prop.PropertyType, prop.PostedValue))
{
// add the result to the property results
propValidationResult.AddValidationResult(validationResult);
}
// add the property results to the element type results
if (propValidationResult.ValidationResults.Count > 0)
elementTypeValidationResult.ValidationResults.Add(propValidationResult);
}
if (elementTypeValidationResult.ValidationResults.Count > 0)
{
yield return elementTypeValidationResult;
}
}
}
public class PropertyTypeValidationModel
{
public PropertyTypeValidationModel(IPropertyType propertyType, object postedValue)
{
PostedValue = postedValue;
PropertyType = propertyType ?? throw new ArgumentNullException(nameof(propertyType));
}
public object PostedValue { get; }
public IPropertyType PropertyType { get; }
}
public class ElementTypeValidationModel
{
public ElementTypeValidationModel(string elementTypeAlias, Guid id)
{
ElementTypeAlias = elementTypeAlias;
Id = id;
}
private List<PropertyTypeValidationModel> _list = new List<PropertyTypeValidationModel>();
public IEnumerable<PropertyTypeValidationModel> PropertyTypeValidation => _list;
public string ElementTypeAlias { get; }
public Guid Id { get; }
public void AddPropertyTypeValidation(PropertyTypeValidationModel propValidation) => _list.Add(propValidation);
}
}
}
@@ -3,6 +3,7 @@ using Umbraco.Core.PropertyEditors;
namespace Umbraco.Web.PropertyEditors
{
/// <summary>
/// Represents the configuration for the nested content value editor.
/// </summary>
@@ -1,8 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text.RegularExpressions;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Umbraco.Core;
@@ -16,6 +14,7 @@ using Umbraco.Core.Strings;
namespace Umbraco.Web.PropertyEditors
{
/// <summary>
/// Represents a nested content property editor.
/// </summary>
@@ -31,6 +30,7 @@ namespace Umbraco.Web.PropertyEditors
private readonly Lazy<PropertyEditorCollection> _propertyEditors;
private readonly IContentTypeService _contentTypeService;
private readonly IIOHelper _ioHelper;
private readonly ILocalizedTextService _localizedTextService;
public const string ContentTypeAliasPropertyKey = "ncContentTypeAlias";
@@ -48,6 +48,7 @@ namespace Umbraco.Web.PropertyEditors
_propertyEditors = propertyEditors;
_contentTypeService = contentTypeService;
_ioHelper = ioHelper;
_localizedTextService = localizedTextService;
}
// has to be lazy else circular dep in ctor
@@ -61,25 +62,35 @@ namespace Umbraco.Web.PropertyEditors
#region Value Editor
protected override IDataValueEditor CreateValueEditor() => new NestedContentPropertyValueEditor(DataTypeService, LocalizationService, LocalizedTextService, _contentTypeService, ShortStringHelper, Attribute, PropertyEditors);
protected override IDataValueEditor CreateValueEditor() => new NestedContentPropertyValueEditor(DataTypeService, LocalizationService, LocalizedTextService, _contentTypeService, ShortStringHelper, Attribute, PropertyEditors, Logger);
internal class NestedContentPropertyValueEditor : DataValueEditor, IDataValueReference
{
private readonly PropertyEditorCollection _propertyEditors;
private readonly IContentTypeService _contentTypeService;
private readonly IDataTypeService _dataTypeService;
private readonly ILogger _logger;
private readonly NestedContentValues _nestedContentValues;
private readonly Lazy<Dictionary<string, IContentType>> _contentTypes;
public NestedContentPropertyValueEditor(IDataTypeService dataTypeService, ILocalizationService localizationService, ILocalizedTextService localizedTextService, IContentTypeService contentTypeService, IShortStringHelper shortStringHelper, DataEditorAttribute attribute, PropertyEditorCollection propertyEditors)
public NestedContentPropertyValueEditor(
IDataTypeService dataTypeService,
ILocalizationService localizationService,
ILocalizedTextService localizedTextService,
IContentTypeService contentTypeService,
IShortStringHelper shortStringHelper,
DataEditorAttribute attribute,
PropertyEditorCollection propertyEditors,
ILogger logger)
: base(dataTypeService, localizationService, localizedTextService, shortStringHelper, attribute)
{
_propertyEditors = propertyEditors;
_contentTypeService = contentTypeService;
_dataTypeService = dataTypeService;
_logger = logger;
_nestedContentValues = new NestedContentValues(contentTypeService);
Validators.Add(new NestedContentValidator(propertyEditors, dataTypeService, _nestedContentValues));
Validators.Add(new NestedContentValidator(_nestedContentValues, propertyEditors, dataTypeService, localizedTextService));
_contentTypes = new Lazy<Dictionary<string, IContentType>>(() =>
_contentTypeService.GetAll().ToDictionary(c => c.Alias)
@@ -106,41 +117,41 @@ namespace Umbraco.Web.PropertyEditors
public override string ConvertDbToString(IPropertyType propertyType, object propertyValue, IDataTypeService dataTypeService)
{
var vals = _nestedContentValues.GetPropertyValues(propertyValue, out var deserialized).ToList();
var rows = _nestedContentValues.GetPropertyValues(propertyValue);
if (vals.Count == 0)
if (rows.Count == 0)
return string.Empty;
foreach (var row in vals)
foreach (var row in rows.ToList())
{
if (row.PropType == null)
{
// type not found, and property is not system: just delete the value
if (IsSystemPropertyKey(row.PropKey) == false)
row.JsonRowValue[row.PropKey] = null;
}
else
foreach(var prop in row.PropertyValues.ToList())
{
try
{
// convert the value, and store the converted value
var propEditor = _propertyEditors[row.PropType.PropertyEditorAlias];
var propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias];
if (propEditor == null) continue;
var tempConfig = DataTypeService.GetDataType(row.PropType.DataTypeId).Configuration;
var tempConfig = DataTypeService.GetDataType(prop.Value.PropertyType.DataTypeId).Configuration;
var valEditor = propEditor.GetValueEditor(tempConfig);
var convValue = valEditor.ConvertDbToString(row.PropType, row.JsonRowValue[row.PropKey]?.ToString(), dataTypeService);
row.JsonRowValue[row.PropKey] = convValue;
var convValue = valEditor.ConvertDbToString(prop.Value.PropertyType, prop.Value.Value, dataTypeService);
// update the raw value since this is what will get serialized out
row.RawPropertyValues[prop.Key] = convValue;
}
catch (InvalidOperationException)
catch (InvalidOperationException ex)
{
// deal with weird situations by ignoring them (no comment)
row.JsonRowValue[row.PropKey] = null;
row.RawPropertyValues.Remove(prop.Key);
_logger.Warn<NestedContentPropertyValueEditor>(
ex,
"ConvertDbToString removed property value {PropertyKey} in row {RowId} for property type {PropertyTypeAlias}",
prop.Key, row.Id, propertyType.Alias);
}
}
}
return JsonConvert.SerializeObject(deserialized).ToXmlString<string>();
return JsonConvert.SerializeObject(rows).ToXmlString<string>();
}
#endregion
@@ -151,98 +162,108 @@ namespace Umbraco.Web.PropertyEditors
// note: there is NO variant support here
/// <summary>
/// Ensure that sub-editor values are translated through their ToEditor methods
/// </summary>
/// <param name="property"></param>
/// <param name="dataTypeService"></param>
/// <param name="culture"></param>
/// <param name="segment"></param>
/// <returns></returns>
public override object ToEditor(IProperty property, string culture = null, string segment = null)
{
var val = property.GetValue(culture, segment);
var vals = _nestedContentValues.GetPropertyValues(val, out var deserialized).ToList();
var rows = _nestedContentValues.GetPropertyValues(val);
if (vals.Count == 0)
if (rows.Count == 0)
return string.Empty;
foreach (var row in vals)
foreach (var row in rows.ToList())
{
if (row.PropType == null)
{
// type not found, and property is not system: just delete the value
if (IsSystemPropertyKey(row.PropKey) == false)
row.JsonRowValue[row.PropKey] = null;
}
else
foreach(var prop in row.PropertyValues.ToList())
{
try
{
// create a temp property with the value
// - force it to be culture invariant as NC can't handle culture variant element properties
row.PropType.Variations = ContentVariation.Nothing;
var tempProp = new Property(row.PropType);
tempProp.SetValue(row.JsonRowValue[row.PropKey] == null ? null : row.JsonRowValue[row.PropKey].ToString());
prop.Value.PropertyType.Variations = ContentVariation.Nothing;
var tempProp = new Property(prop.Value.PropertyType);
tempProp.SetValue(prop.Value.Value);
// convert that temp property, and store the converted value
var propEditor = _propertyEditors[row.PropType.PropertyEditorAlias];
var propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias];
if (propEditor == null)
{
row.JsonRowValue[row.PropKey] = tempProp.GetValue()?.ToString();
// update the raw value since this is what will get serialized out
row.RawPropertyValues[prop.Key] = tempProp.GetValue()?.ToString();
continue;
}
var tempConfig = DataTypeService.GetDataType(row.PropType.DataTypeId).Configuration;
var tempConfig = DataTypeService.GetDataType(prop.Value.PropertyType.DataTypeId).Configuration;
var valEditor = propEditor.GetValueEditor(tempConfig);
var convValue = valEditor.ToEditor(tempProp);
row.JsonRowValue[row.PropKey] = convValue == null ? null : JToken.FromObject(convValue);
// update the raw value since this is what will get serialized out
row.RawPropertyValues[prop.Key] = convValue == null ? null : JToken.FromObject(convValue);
}
catch (InvalidOperationException)
catch (InvalidOperationException ex)
{
// deal with weird situations by ignoring them (no comment)
row.JsonRowValue[row.PropKey] = null;
row.RawPropertyValues.Remove(prop.Key);
_logger.Warn<NestedContentPropertyValueEditor>(
ex,
"ToEditor removed property value {PropertyKey} in row {RowId} for property type {PropertyTypeAlias}",
prop.Key, row.Id, property.PropertyType.Alias);
}
}
}
// return json
return deserialized;
// return the object, there's a native json converter for this so it will serialize correctly
return rows;
}
/// <summary>
/// Ensure that sub-editor values are translated through their FromEditor methods
/// </summary>
/// <param name="editorValue"></param>
/// <param name="currentValue"></param>
/// <returns></returns>
public override object FromEditor(ContentPropertyData editorValue, object currentValue)
{
if (editorValue.Value == null || string.IsNullOrWhiteSpace(editorValue.Value.ToString()))
return null;
var vals = _nestedContentValues.GetPropertyValues(editorValue.Value, out var deserialized).ToList();
var rows = _nestedContentValues.GetPropertyValues(editorValue.Value);
if (vals.Count == 0)
if (rows.Count == 0)
return string.Empty;
foreach (var row in vals)
foreach (var row in rows.ToList())
{
if (row.PropType == null)
{
// type not found, and property is not system: just delete the value
if (IsSystemPropertyKey(row.PropKey) == false)
row.JsonRowValue[row.PropKey] = null;
}
else
foreach(var prop in row.PropertyValues.ToList())
{
// Fetch the property types prevalue
var propConfiguration = _dataTypeService.GetDataType(row.PropType.DataTypeId).Configuration;
var propConfiguration = _dataTypeService.GetDataType(prop.Value.PropertyType.DataTypeId).Configuration;
// Lookup the property editor
var propEditor = _propertyEditors[row.PropType.PropertyEditorAlias];
var propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias];
if (propEditor == null) continue;
// Create a fake content property data object
var contentPropData = new ContentPropertyData(row.JsonRowValue[row.PropKey], propConfiguration);
var contentPropData = new ContentPropertyData(prop.Value.Value, propConfiguration);
// Get the property editor to do it's conversion
var newValue = propEditor.GetValueEditor().FromEditor(contentPropData, row.JsonRowValue[row.PropKey]);
var newValue = propEditor.GetValueEditor().FromEditor(contentPropData, prop.Value.Value);
// Store the value back
row.JsonRowValue[row.PropKey] = (newValue == null) ? null : JToken.FromObject(newValue);
// update the raw value since this is what will get serialized out
row.RawPropertyValues[prop.Key] = (newValue == null) ? null : JToken.FromObject(newValue);
}
}
// return json
return JsonConvert.SerializeObject(deserialized);
return JsonConvert.SerializeObject(rows);
}
#endregion
@@ -252,98 +273,57 @@ namespace Umbraco.Web.PropertyEditors
var result = new List<UmbracoEntityReference>();
foreach (var row in _nestedContentValues.GetPropertyValues(rawJson, out _))
foreach (var row in _nestedContentValues.GetPropertyValues(rawJson))
{
if (row.PropType == null) continue;
foreach(var prop in row.PropertyValues)
{
var propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias];
var propEditor = _propertyEditors[row.PropType.PropertyEditorAlias];
var valueEditor = propEditor?.GetValueEditor();
if (!(valueEditor is IDataValueReference reference)) continue;
var valueEditor = propEditor?.GetValueEditor();
if (!(valueEditor is IDataValueReference reference)) continue;
var val = prop.Value.Value?.ToString();
var val = row.JsonRowValue[row.PropKey]?.ToString();
var refs = reference.GetReferences(val);
var refs = reference.GetReferences(val);
result.AddRange(refs);
result.AddRange(refs);
}
}
return result;
}
}
internal class NestedContentValidator : IValueValidator
/// <summary>
/// Validator for nested content to ensure that all nesting of editors is validated
/// </summary>
internal class NestedContentValidator : ComplexEditorValidator
{
private readonly PropertyEditorCollection _propertyEditors;
private readonly IDataTypeService _dataTypeService;
private readonly NestedContentValues _nestedContentValues;
public NestedContentValidator(PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, NestedContentValues nestedContentValues)
public NestedContentValidator(NestedContentValues nestedContentValues, PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, ILocalizedTextService textService)
: base(propertyEditors, dataTypeService, textService)
{
_propertyEditors = propertyEditors;
_dataTypeService = dataTypeService;
_nestedContentValues = nestedContentValues;
}
public IEnumerable<ValidationResult> Validate(object rawValue, string valueType, object dataTypeConfiguration)
protected override IEnumerable<ElementTypeValidationModel> GetElementTypeValidation(object value)
{
var validationResults = new List<ValidationResult>();
foreach(var row in _nestedContentValues.GetPropertyValues(rawValue, out _))
foreach (var row in _nestedContentValues.GetPropertyValues(value))
{
if (row.PropType == null) continue;
var config = _dataTypeService.GetDataType(row.PropType.DataTypeId).Configuration;
var propertyEditor = _propertyEditors[row.PropType.PropertyEditorAlias];
if (propertyEditor == null) continue;
foreach (var validator in propertyEditor.GetValueEditor().Validators)
var elementValidation = new ElementTypeValidationModel(row.ContentTypeAlias, row.Id);
foreach (var prop in row.PropertyValues)
{
foreach (var result in validator.Validate(row.JsonRowValue[row.PropKey], propertyEditor.GetValueEditor().ValueType, config))
{
result.ErrorMessage = "Item " + (row.RowIndex + 1) + " '" + row.PropType.Name + "' " + result.ErrorMessage;
validationResults.Add(result);
elementValidation.AddPropertyTypeValidation(
new PropertyTypeValidationModel(prop.Value.PropertyType, prop.Value.Value));
}
}
// Check mandatory
if (row.PropType.Mandatory)
{
if (row.JsonRowValue[row.PropKey] == null)
{
var message = string.IsNullOrWhiteSpace(row.PropType.MandatoryMessage)
? $"'{row.PropType.Name}' cannot be null"
: row.PropType.MandatoryMessage;
validationResults.Add(new ValidationResult($"Item {(row.RowIndex + 1)}: {message}", new[] { row.PropKey }));
}
else if (row.JsonRowValue[row.PropKey].ToString().IsNullOrWhiteSpace() || (row.JsonRowValue[row.PropKey].Type == JTokenType.Array && !row.JsonRowValue[row.PropKey].HasValues))
{
var message = string.IsNullOrWhiteSpace(row.PropType.MandatoryMessage)
? $"'{row.PropType.Name}' cannot be empty"
: row.PropType.MandatoryMessage;
validationResults.Add(new ValidationResult($"Item {(row.RowIndex + 1)}: {message}", new[] { row.PropKey }));
}
}
// Check regex
if (!row.PropType.ValidationRegExp.IsNullOrWhiteSpace()
&& row.JsonRowValue[row.PropKey] != null && !row.JsonRowValue[row.PropKey].ToString().IsNullOrWhiteSpace())
{
var regex = new Regex(row.PropType.ValidationRegExp);
if (!regex.IsMatch(row.JsonRowValue[row.PropKey].ToString()))
{
var message = string.IsNullOrWhiteSpace(row.PropType.ValidationRegExpMessage)
? $"'{row.PropType.Name}' is invalid, it does not match the correct pattern"
: row.PropType.ValidationRegExpMessage;
validationResults.Add(new ValidationResult($"Item {(row.RowIndex + 1)}: {message}", new[] { row.PropKey }));
}
}
yield return elementValidation;
}
return validationResults;
}
}
/// <summary>
/// Used to deserialize the nested content serialized value
/// </summary>
internal class NestedContentValues
{
private readonly Lazy<Dictionary<string, IContentType>> _contentTypes;
@@ -353,84 +333,111 @@ namespace Umbraco.Web.PropertyEditors
_contentTypes = new Lazy<Dictionary<string, IContentType>>(() => contentTypeService.GetAll().ToDictionary(c => c.Alias));
}
private IContentType GetElementType(JObject item)
private IContentType GetElementType(NestedContentRowValue item)
{
var contentTypeAlias = item[ContentTypeAliasPropertyKey]?.ToObject<string>() ?? string.Empty;
_contentTypes.Value.TryGetValue(contentTypeAlias, out var contentType);
_contentTypes.Value.TryGetValue(item.ContentTypeAlias, out var contentType);
return contentType;
}
public IEnumerable<RowValue> GetPropertyValues(object propertyValue, out List<JObject> deserialized)
/// <summary>
/// Deserialize the raw json property value
/// </summary>
/// <param name="propertyValue"></param>
/// <returns></returns>
public IReadOnlyList<NestedContentRowValue> GetPropertyValues(object propertyValue)
{
var rowValues = new List<RowValue>();
deserialized = null;
if (propertyValue == null || string.IsNullOrWhiteSpace(propertyValue.ToString()))
return Enumerable.Empty<RowValue>();
return new List<NestedContentRowValue>();
deserialized = JsonConvert.DeserializeObject<List<JObject>>(propertyValue.ToString());
var rowValues = JsonConvert.DeserializeObject<List<NestedContentRowValue>>(propertyValue.ToString());
// There was a note here about checking if the result had zero items and if so it would return null, so we'll continue to do that
// The original note was: "Issue #38 - Keep recursive property lookups working"
// Which is from the original NC tracker: https://github.com/umco/umbraco-nested-content/issues/38
// This check should be used everywhere when iterating NC prop values, instead of just the one previous place so that
// empty values don't get persisted when there is nothing, it should actually be null.
if (deserialized == null || deserialized.Count == 0)
return Enumerable.Empty<RowValue>();
if (rowValues == null || rowValues.Count == 0)
return new List<NestedContentRowValue>();
var index = 0;
var contentTypePropertyTypes = new Dictionary<string, Dictionary<string, IPropertyType>>();
foreach (var o in deserialized)
foreach (var row in rowValues)
{
var propValues = o;
var contentType = GetElementType(propValues);
var contentType = GetElementType(row);
if (contentType == null)
continue;
var propertyTypes = contentType.CompositionPropertyTypes.ToDictionary(x => x.Alias, x => x);
var propAliases = propValues.Properties().Select(x => x.Name);
foreach (var propAlias in propAliases)
// get the prop types for this content type but keep a dictionary of found ones so we don't have to keep re-looking and re-creating
// objects on each iteration.
if (!contentTypePropertyTypes.TryGetValue(contentType.Alias, out var propertyTypes))
propertyTypes = contentTypePropertyTypes[contentType.Alias] = contentType.CompositionPropertyTypes.ToDictionary(x => x.Alias, x => x);
// find any keys that are not real property types and remove them
foreach(var prop in row.RawPropertyValues.ToList())
{
propertyTypes.TryGetValue(propAlias, out var propType);
rowValues.Add(new RowValue(propAlias, propType, propValues, index));
if (IsSystemPropertyKey(prop.Key)) continue;
// doesn't exist so remove it
if (!propertyTypes.TryGetValue(prop.Key, out var propType))
{
row.RawPropertyValues.Remove(prop.Key);
}
else
{
// set the value to include the resolved property type
row.PropertyValues[prop.Key] = new NestedContentPropertyValue
{
PropertyType = propType,
Value = prop.Value
};
}
}
index++;
}
return rowValues;
}
internal class RowValue
/// <summary>
/// Used during deserialization to populate the property value/property type of a nested content row property
/// </summary>
internal class NestedContentPropertyValue
{
public RowValue(string propKey, IPropertyType propType, JObject propValues, int index)
{
PropKey = propKey ?? throw new ArgumentNullException(nameof(propKey));
PropType = propType;
JsonRowValue = propValues ?? throw new ArgumentNullException(nameof(propValues));
RowIndex = index;
}
public object Value { get; set; }
public IPropertyType PropertyType { get; set; }
}
/// <summary>
/// The current property key being iterated for the row value
/// </summary>
public string PropKey { get; }
/// <summary>
/// Used to deserialize a nested content row
/// </summary>
internal class NestedContentRowValue
{
[JsonProperty("key")]
public Guid Id{ get; set; }
/// <summary>
/// The <see cref="PropertyType"/> of the value (if any), this may be null
/// </summary>
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("ncContentTypeAlias")]
public string ContentTypeAlias { get; set; }
public IPropertyType PropType { get; }
/// <summary>
/// The json values for the current row
/// The remaining properties will be serialized to a dictionary
/// </summary>
public JObject JsonRowValue { get; }
/// <remarks>
/// The JsonExtensionDataAttribute is used to put the non-typed properties into a bucket
/// http://www.newtonsoft.com/json/help/html/DeserializeExtensionData.htm
/// NestedContent serializes to string, int, whatever eg
/// "stringValue":"Some String","numericValue":125,"otherNumeric":null
/// </remarks>
[JsonExtensionData]
public IDictionary<string, object> RawPropertyValues { get; set; }
/// <summary>
/// The Nested Content row index
/// Used during deserialization to convert the raw property data into data with a property type context
/// </summary>
public int RowIndex { get; }
[JsonIgnore]
public IDictionary<string, NestedContentPropertyValue> PropertyValues { get; set; } = new Dictionary<string, NestedContentPropertyValue>();
}
}
@@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace Umbraco.Web.PropertyEditors.Validation
{
/// <summary>
/// A collection of <see cref="ComplexEditorPropertyTypeValidationResult"/> for an element type within complex editor represented by an Element Type
/// </summary>
/// <remarks>
/// For a more indepth explanation of how server side validation works with the angular app, see this GitHub PR:
/// https://github.com/umbraco/Umbraco-CMS/pull/8339
/// </remarks>
public class ComplexEditorElementTypeValidationResult : ValidationResult
{
public ComplexEditorElementTypeValidationResult(string elementTypeAlias, Guid blockId)
: base(string.Empty)
{
ElementTypeAlias = elementTypeAlias;
BlockId = blockId;
}
public IList<ComplexEditorPropertyTypeValidationResult> ValidationResults { get; } = new List<ComplexEditorPropertyTypeValidationResult>();
/// <summary>
/// The element type alias of the validation result
/// </summary>
/// <remarks>
/// This is useful for debugging purposes but it's not actively used in the angular app
/// </remarks>
public string ElementTypeAlias { get; }
/// <summary>
/// The Block ID of the validation result
/// </summary>
/// <remarks>
/// This is the GUID id of the content item based on the element type
/// </remarks>
public Guid BlockId { get; }
}
}
@@ -0,0 +1,36 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
namespace Umbraco.Web.PropertyEditors.Validation
{
/// <summary>
/// A collection of <see cref="ValidationResult"/> for a property type within a complex editor represented by an Element Type
/// </summary>
/// <remarks>
/// For a more indepth explanation of how server side validation works with the angular app, see this GitHub PR:
/// https://github.com/umbraco/Umbraco-CMS/pull/8339
/// </remarks>
public class ComplexEditorPropertyTypeValidationResult : ValidationResult
{
public ComplexEditorPropertyTypeValidationResult(string propertyTypeAlias)
: base(string.Empty)
{
PropertyTypeAlias = propertyTypeAlias;
}
private readonly List<ValidationResult> _validationResults = new List<ValidationResult>();
public void AddValidationResult(ValidationResult validationResult)
{
if (validationResult is ComplexEditorValidationResult && _validationResults.Any(x => x is ComplexEditorValidationResult))
throw new InvalidOperationException($"Cannot add more than one {typeof(ComplexEditorValidationResult)}");
_validationResults.Add(validationResult);
}
public IReadOnlyList<ValidationResult> ValidationResults => _validationResults;
public string PropertyTypeAlias { get; }
}
}
@@ -0,0 +1,25 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace Umbraco.Web.PropertyEditors.Validation
{
/// <summary>
/// A collection of <see cref="ComplexEditorElementTypeValidationResult"/> for a complex editor represented by an Element Type
/// </summary>
/// <remarks>
/// For example, each <see cref="ComplexEditorValidationResult"/> represents validation results for a row in Nested Content.
///
/// For a more indepth explanation of how server side validation works with the angular app, see this GitHub PR:
/// https://github.com/umbraco/Umbraco-CMS/pull/8339
/// </remarks>
public class ComplexEditorValidationResult : ValidationResult
{
public ComplexEditorValidationResult()
: base(string.Empty)
{
}
public IList<ComplexEditorElementTypeValidationResult> ValidationResults { get; } = new List<ComplexEditorElementTypeValidationResult>();
}
}
@@ -0,0 +1,50 @@
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using Umbraco.Core;
using Umbraco.Core.Models.Blocks;
using Umbraco.Core.Models.PublishedContent;
using Umbraco.Core.PropertyEditors;
using Umbraco.Web.PublishedCache;
namespace Umbraco.Web.PropertyEditors.ValueConverters
{
/// <summary>
/// Converts json block objects into <see cref="IPublishedElement"/>
/// </summary>
public sealed class BlockEditorConverter
{
private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor;
private readonly IPublishedModelFactory _publishedModelFactory;
public BlockEditorConverter(IPublishedSnapshotAccessor publishedSnapshotAccessor, IPublishedModelFactory publishedModelFactory)
{
_publishedSnapshotAccessor = publishedSnapshotAccessor;
_publishedModelFactory = publishedModelFactory;
}
public IPublishedElement ConvertToElement(
BlockItemData data,
PropertyCacheLevel referenceCacheLevel, bool preview)
{
// hack! we need to cast, we have no choice beacuse we cannot make breaking changes.
var publishedContentCache = _publishedSnapshotAccessor.PublishedSnapshot.Content;
// only convert element types - content types will cause an exception when PublishedModelFactory creates the model
var publishedContentType = publishedContentCache.GetContentType(data.ContentTypeKey);
if (publishedContentType == null || publishedContentType.IsElement == false)
return null;
var propertyValues = data.RawPropertyValues;
// Get the udi from the deserialized object. If this is empty we can fallback to checking the 'key' if there is one
var key = (data.Udi is GuidUdi gudi) ? gudi.Guid : Guid.Empty;
if (propertyValues.TryGetValue("key", out var keyo))
Guid.TryParse(keyo.ToString(), out key);
IPublishedElement element = new PublishedElement(publishedContentType, key, propertyValues, preview, referenceCacheLevel, _publishedSnapshotAccessor);
element = _publishedModelFactory.CreateModel(element);
return element;
}
}
}
@@ -0,0 +1,134 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using Umbraco.Core;
using Umbraco.Core.Logging;
using Umbraco.Core.Models.Blocks;
using Umbraco.Core.Models.PublishedContent;
using Umbraco.Core.PropertyEditors;
using Umbraco.Core.PropertyEditors.ValueConverters;
namespace Umbraco.Web.PropertyEditors.ValueConverters
{
[DefaultPropertyValueConverter(typeof(JsonValueConverter))]
public class BlockListPropertyValueConverter : PropertyValueConverterBase
{
private readonly IProfilingLogger _proflog;
private readonly BlockEditorConverter _blockConverter;
private readonly BlockListEditorDataConverter _blockListEditorDataConverter;
public BlockListPropertyValueConverter(IProfilingLogger proflog, BlockEditorConverter blockConverter)
{
_proflog = proflog;
_blockConverter = blockConverter;
_blockListEditorDataConverter = new BlockListEditorDataConverter();
}
/// <inheritdoc />
public override bool IsConverter(IPublishedPropertyType propertyType)
=> propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.BlockList);
/// <inheritdoc />
public override Type GetPropertyValueType(IPublishedPropertyType propertyType) => typeof(BlockListModel);
/// <inheritdoc />
public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType)
=> PropertyCacheLevel.Element;
/// <inheritdoc />
public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview)
{
return source?.ToString();
}
/// <inheritdoc />
public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview)
{
// NOTE: The intermediate object is just a json string, we don't actually convert from source -> intermediate since source is always just a json string
using (_proflog.DebugDuration<BlockListPropertyValueConverter>($"ConvertPropertyToBlockList ({propertyType.DataType.Id})"))
{
var configuration = propertyType.DataType.ConfigurationAs<BlockListConfiguration>();
var blockConfigMap = configuration.Blocks.ToDictionary(x => x.ContentElementTypeKey);
var validSettingElementTypes = blockConfigMap.Values.Select(x => x.SettingsElementTypeKey).Where(x => x.HasValue).Distinct().ToList();
var contentPublishedElements = new Dictionary<Guid, IPublishedElement>();
var settingsPublishedElements = new Dictionary<Guid, IPublishedElement>();
var layout = new List<BlockListLayoutReference>();
var value = (string)inter;
if (string.IsNullOrWhiteSpace(value)) return BlockListModel.Empty;
var converted = _blockListEditorDataConverter.Deserialize(value);
if (converted.BlockValue.ContentData.Count == 0) return BlockListModel.Empty;
var blockListLayout = converted.Layout.ToObject<IEnumerable<BlockListLayoutItem>>();
// convert the content data
foreach (var data in converted.BlockValue.ContentData)
{
if (!blockConfigMap.ContainsKey(data.ContentTypeKey)) continue;
var element = _blockConverter.ConvertToElement(data, referenceCacheLevel, preview);
if (element == null) continue;
contentPublishedElements[element.Key] = element;
}
// convert the settings data
foreach (var data in converted.BlockValue.SettingsData)
{
if (!validSettingElementTypes.Contains(data.ContentTypeKey)) continue;
var element = _blockConverter.ConvertToElement(data, referenceCacheLevel, preview);
if (element == null) continue;
settingsPublishedElements[element.Key] = element;
}
// if there's no elements just return since if there's no data it doesn't matter what is stored in layout
if (contentPublishedElements.Count == 0) return BlockListModel.Empty;
foreach (var layoutItem in blockListLayout)
{
// get the content reference
var contentGuidUdi = (GuidUdi)layoutItem.ContentUdi;
if (!contentPublishedElements.TryGetValue(contentGuidUdi.Guid, out var contentData))
continue;
// get the setting reference
IPublishedElement settingsData = null;
var settingGuidUdi = layoutItem.SettingsUdi != null ? (GuidUdi)layoutItem.SettingsUdi : null;
if (settingGuidUdi != null)
settingsPublishedElements.TryGetValue(settingGuidUdi.Guid, out settingsData);
if (!contentData.ContentType.TryGetKey(out var contentTypeKey))
throw new InvalidOperationException("The content type was not of type " + typeof(IPublishedContentType2));
if (!blockConfigMap.TryGetValue(contentTypeKey, out var blockConfig))
continue;
// this can happen if they have a settings type, save content, remove the settings type, and display the front-end page before saving the content again
// we also ensure that the content type's match since maybe the settings type has been changed after this has been persisted.
if (settingsData != null)
{
if (!settingsData.ContentType.TryGetKey(out var settingsElementTypeKey))
throw new InvalidOperationException("The settings element type was not of type " + typeof(IPublishedContentType2));
if (!blockConfig.SettingsElementTypeKey.HasValue || settingsElementTypeKey != blockConfig.SettingsElementTypeKey)
settingsData = null;
}
var layoutRef = new BlockListLayoutReference(contentGuidUdi, contentData, settingGuidUdi, settingsData);
layout.Add(layoutRef);
}
var model = new BlockListModel(contentPublishedElements.Values, settingsPublishedElements.Values, layout);
return model;
}
}
}
}
@@ -143,12 +143,11 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters
// 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();
//Use Crops if it's not null, otherwise create a new list
var crops = Crops?.ToList() ?? new List<ImageCropperCrop>();
foreach (var configuredCrop in configuredCrops)
{
@@ -38,8 +38,8 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters
{
var contentTypes = propertyType.DataType.ConfigurationAs<NestedContentConfiguration>().ContentTypes;
return contentTypes.Length == 1
? typeof (IEnumerable<>).MakeGenericType(ModelType.For(contentTypes[0].Alias))
: typeof (IEnumerable<IPublishedElement>);
? typeof(IEnumerable<>).MakeGenericType(ModelType.For(contentTypes[0].Alias))
: typeof(IEnumerable<IPublishedElement>);
}
/// <inheritdoc />
@@ -56,7 +56,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters
{
using (_proflog.DebugDuration<NestedContentSingleValueConverter>($"ConvertPropertyToNestedContent ({propertyType.DataType.Id})"))
{
var value = (string) inter;
var value = (string)inter;
if (string.IsNullOrWhiteSpace(value)) return null;
var objects = JsonConvert.DeserializeObject<List<JObject>>(value);
@@ -15,8 +15,10 @@ namespace Umbraco.Web.PublishedCache
/// <remarks>This cache is not snapshotted, so it refreshes any time things change.</remarks>
public class PublishedContentTypeCache
{
// NOTE: These are not concurrent dictionaries because all access is done within a lock
private readonly Dictionary<string, IPublishedContentType> _typesByAlias = new Dictionary<string, IPublishedContentType>();
private readonly Dictionary<int, IPublishedContentType> _typesById = new Dictionary<int, IPublishedContentType>();
private readonly Dictionary<Guid, int> _keyToIdMap = new Dictionary<Guid, int>();
private readonly IContentTypeService _contentTypeService;
private readonly IMediaTypeService _mediaTypeService;
private readonly IMemberTypeService _memberTypeService;
@@ -130,6 +132,42 @@ namespace Umbraco.Web.PublishedCache
}
}
/// <summary>
/// Gets a published content type.
/// </summary>
/// <param name="itemType">An item type.</param>
/// <param name="key">An key.</param>
/// <returns>The published content type corresponding to the item key.</returns>
public IPublishedContentType Get(PublishedItemType itemType, Guid key)
{
try
{
_lock.EnterUpgradeableReadLock();
if (_keyToIdMap.TryGetValue(key, out var id))
return Get(itemType, id);
var type = CreatePublishedContentType(itemType, key);
try
{
_lock.EnterWriteLock();
_keyToIdMap[key] = type.Id;
return _typesByAlias[GetAliasKey(type)] = _typesById[type.Id] = type;
}
finally
{
if (_lock.IsWriteLockHeld)
_lock.ExitWriteLock();
}
}
finally
{
if (_lock.IsUpgradeableReadLockHeld)
_lock.ExitUpgradeableReadLock();
}
}
/// <summary>
/// Gets a published content type.
/// </summary>
@@ -152,7 +190,8 @@ namespace Umbraco.Web.PublishedCache
try
{
_lock.EnterWriteLock();
if (type.TryGetKey(out var key))
_keyToIdMap[key] = type.Id;
return _typesByAlias[aliasKey] = _typesById[type.Id] = type;
}
finally
@@ -188,7 +227,8 @@ namespace Umbraco.Web.PublishedCache
try
{
_lock.EnterWriteLock();
if (type.TryGetKey(out var key))
_keyToIdMap[key] = type.Id;
return _typesByAlias[GetAliasKey(type)] = _typesById[type.Id] = type;
}
finally
@@ -204,27 +244,32 @@ namespace Umbraco.Web.PublishedCache
}
}
private IPublishedContentType CreatePublishedContentType(PublishedItemType itemType, Guid key)
{
IContentTypeComposition contentType = itemType switch
{
PublishedItemType.Content => _contentTypeService.Get(key),
PublishedItemType.Media => _mediaTypeService.Get(key),
PublishedItemType.Member => _memberTypeService.Get(key),
_ => throw new ArgumentOutOfRangeException(nameof(itemType)),
};
if (contentType == null)
throw new Exception($"ContentTypeService failed to find a {itemType.ToString().ToLower()} type with key \"{key}\".");
return _publishedContentTypeFactory.CreateContentType(contentType);
}
private IPublishedContentType CreatePublishedContentType(PublishedItemType itemType, string alias)
{
if (GetPublishedContentTypeByAlias != null)
return GetPublishedContentTypeByAlias(alias);
IContentTypeComposition contentType;
switch (itemType)
IContentTypeComposition contentType = itemType switch
{
case PublishedItemType.Content:
contentType = _contentTypeService.Get(alias);
break;
case PublishedItemType.Media:
contentType = _mediaTypeService.Get(alias);
break;
case PublishedItemType.Member:
contentType = _memberTypeService.Get(alias);
break;
default:
throw new ArgumentOutOfRangeException(nameof(itemType));
}
PublishedItemType.Content => _contentTypeService.Get(alias),
PublishedItemType.Media => _mediaTypeService.Get(alias),
PublishedItemType.Member => _memberTypeService.Get(alias),
_ => throw new ArgumentOutOfRangeException(nameof(itemType)),
};
if (contentType == null)
throw new Exception($"ContentTypeService failed to find a {itemType.ToString().ToLower()} type with alias \"{alias}\".");
@@ -235,23 +280,13 @@ namespace Umbraco.Web.PublishedCache
{
if (GetPublishedContentTypeById != null)
return GetPublishedContentTypeById(id);
IContentTypeComposition contentType;
switch (itemType)
IContentTypeComposition contentType = itemType switch
{
case PublishedItemType.Content:
contentType = _contentTypeService.Get(id);
break;
case PublishedItemType.Media:
contentType = _mediaTypeService.Get(id);
break;
case PublishedItemType.Member:
contentType = _memberTypeService.Get(id);
break;
default:
throw new ArgumentOutOfRangeException(nameof(itemType));
}
PublishedItemType.Content => _contentTypeService.Get(id),
PublishedItemType.Media => _mediaTypeService.Get(id),
PublishedItemType.Member => _memberTypeService.Get(id),
_ => throw new ArgumentOutOfRangeException(nameof(itemType)),
};
if (contentType == null)
throw new Exception($"ContentTypeService failed to find a {itemType.ToString().ToLower()} type with id {id}.");
@@ -259,6 +294,7 @@ namespace Umbraco.Web.PublishedCache
}
// for unit tests - changing the callback must reset the cache obviously
// TODO: Why does this even exist? For testing you'd pass in a mocked service to get by id
private Func<string, IPublishedContentType> _getPublishedContentTypeByAlias;
internal Func<string, IPublishedContentType> GetPublishedContentTypeByAlias
{
@@ -282,6 +318,7 @@ namespace Umbraco.Web.PublishedCache
}
// for unit tests - changing the callback must reset the cache obviously
// TODO: Why does this even exist? For testing you'd pass in a mocked service to get by id
private Func<int, IPublishedContentType> _getPublishedContentTypeById;
internal Func<int, IPublishedContentType> GetPublishedContentTypeById
{
@@ -44,6 +44,7 @@ using Umbraco.Web.Media.EmbedProviders;
using Umbraco.Web.Migrations.PostMigrations;
using Umbraco.Web.Models.PublishedContent;
using Umbraco.Web.PropertyEditors;
using Umbraco.Web.PropertyEditors.ValueConverters;
using Umbraco.Web.PublishedCache;
using Umbraco.Web.Routing;
using Umbraco.Web.Search;
@@ -52,6 +53,7 @@ using Umbraco.Web.Services;
using Umbraco.Web.Templates;
using Umbraco.Web.Trees;
using IntegerValidator = Umbraco.Core.PropertyEditors.Validators.IntegerValidator;
using TextStringValueConverter = Umbraco.Core.PropertyEditors.ValueConverters.TextStringValueConverter;
namespace Umbraco.Core.Runtime
{
@@ -213,6 +215,7 @@ namespace Umbraco.Core.Runtime
composition.RegisterUnique<HtmlImageSourceParser>();
composition.RegisterUnique<HtmlUrlParser>();
composition.RegisterUnique<RichTextEditorPastedImages>();
composition.RegisterUnique<BlockEditorConverter>();
// both TinyMceValueConverter (in Core) and RteMacroRenderingValueConverter (in Web) will be
// discovered when CoreBootManager configures the converters. We HAVE to remove one of them
@@ -29,7 +29,7 @@ namespace Umbraco.Core.Runtime
private SqlServerSyntaxProvider _sqlServerSyntax = new SqlServerSyntaxProvider();
private bool _mainDomChanging = false;
private readonly UmbracoDatabaseFactory _dbFactory;
private bool _hasError;
private bool _errorDuringAcquiring;
private object _locker = new object();
public SqlMainDomLock(ILogger logger, IGlobalSettings globalSettings, IConnectionStrings connectionStrings, IDbProviderFactoryCreator dbProviderFactoryCreator, IHostingEnvironment hostingEnvironment)
@@ -63,25 +63,24 @@ namespace Umbraco.Core.Runtime
_logger.Debug<SqlMainDomLock>("Acquiring lock...");
var db = GetDatabase();
var tempId = Guid.NewGuid().ToString();
using var db = _dbFactory.CreateDatabase();
using var transaction = db.GetTransaction(IsolationLevel.ReadCommitted);
try
{
db.BeginTransaction(IsolationLevel.ReadCommitted);
try
{
// wait to get a write lock
_sqlServerSyntax.WriteLock(db, TimeSpan.FromMilliseconds(millisecondsTimeout), Constants.Locks.MainDom);
}
catch (Exception ex)
catch(SqlException ex)
{
if (IsLockTimeoutException(ex))
{
_logger.Error<SqlMainDomLock>(ex, "Sql timeout occurred, could not acquire MainDom.");
_hasError = true;
_errorDuringAcquiring = true;
return false;
}
@@ -89,15 +88,12 @@ namespace Umbraco.Core.Runtime
throw;
}
var result = InsertLockRecord(tempId); //we change the row to a random Id to signal other MainDom to shutdown
var result = InsertLockRecord(tempId, db); //we change the row to a random Id to signal other MainDom to shutdown
if (result == RecordPersistenceType.Insert)
{
// if we've inserted, then there was no MainDom so we can instantly acquire
// TODO: see the other TODO, could we just delete the row and that would indicate that we
// are MainDom? then we don't leave any orphan rows behind.
InsertLockRecord(_lockId); // so update with our appdomain id
InsertLockRecord(_lockId, db); // so update with our appdomain id
_logger.Debug<SqlMainDomLock>("Acquired with ID {LockId}", _lockId);
return true;
}
@@ -107,23 +103,23 @@ namespace Umbraco.Core.Runtime
}
catch (Exception ex)
{
ResetDatabase();
// unexpected
_logger.Error<SqlMainDomLock>(ex, "Unexpected error, cannot acquire MainDom");
_hasError = true;
_errorDuringAcquiring = true;
return false;
}
finally
{
db?.CompleteTransaction();
transaction.Complete();
}
return await WaitForExistingAsync(tempId, millisecondsTimeout);
}
public Task ListenAsync()
{
if (_hasError)
if (_errorDuringAcquiring)
{
_logger.Warn<SqlMainDomLock>("Could not acquire MainDom, listening is canceled.");
return Task.CompletedTask;
@@ -149,8 +145,15 @@ namespace Umbraco.Core.Runtime
{
while (true)
{
// poll every 1 second
Thread.Sleep(1000);
// poll every couple of seconds
// local testing shows the actual query to be executed from client/server is approx 300ms but would change depending on environment/IO
Thread.Sleep(2000);
if (!_dbFactory.Configured)
{
// if we aren't configured, we just keep looping since we can't query the db
continue;
}
if (!_dbFactory.Configured)
{
@@ -167,20 +170,14 @@ namespace Umbraco.Core.Runtime
if (_cancellationTokenSource.IsCancellationRequested)
return;
var db = GetDatabase();
using var db = _dbFactory.CreateDatabase();
using var transaction = db.GetTransaction(IsolationLevel.ReadCommitted);
try
{
db.BeginTransaction(IsolationLevel.ReadCommitted);
// get a read lock
_sqlServerSyntax.ReadLock(db, Constants.Locks.MainDom);
// TODO: We could in theory just check if the main dom row doesn't exist, that could indicate that
// we are still the maindom. An empty value might be better because then we won't have any orphan rows
// if the app is terminated. Could that work?
if (!IsMainDomValue(_lockId))
if (!IsMainDomValue(_lockId, db))
{
// we are no longer main dom, another one has come online, exit
_mainDomChanging = true;
@@ -190,38 +187,23 @@ namespace Umbraco.Core.Runtime
}
catch (Exception ex)
{
ResetDatabase();
// unexpected
_logger.Error<SqlMainDomLock>(ex, "Unexpected error, listening is canceled.");
_hasError = true;
return;
_logger.Error<SqlMainDomLock>(ex, "Unexpected error during listening.");
// We need to keep on listening unless we've been notified by our own AppDomain to shutdown since
// we don't want to shutdown resources controlled by MainDom inadvertently. We'll just keep listening otherwise.
if (_cancellationTokenSource.IsCancellationRequested)
return;
}
finally
{
db?.CompleteTransaction();
transaction.Complete();
}
}
}
}
private void ResetDatabase()
{
if (_db.InTransaction)
_db.AbortTransaction();
_db.Dispose();
_db = null;
}
private IUmbracoDatabase GetDatabase()
{
if (_db != null)
return _db;
_db = _dbFactory.CreateDatabase();
return _db;
}
/// <summary>
/// Wait for any existing MainDom to release so we can continue booting
/// </summary>
@@ -234,35 +216,51 @@ namespace Umbraco.Core.Runtime
return Task.Run(() =>
{
var db = GetDatabase();
using var db = _dbFactory.CreateDatabase();
var watch = new Stopwatch();
watch.Start();
while(true)
while (true)
{
// poll very often, we need to take over as fast as we can
Thread.Sleep(100);
// local testing shows the actual query to be executed from client/server is approx 300ms but would change depending on environment/IO
Thread.Sleep(1000);
try
var acquired = TryAcquire(db, tempId, updatedTempId);
if (acquired.HasValue)
return acquired.Value;
if (watch.ElapsedMilliseconds >= millisecondsTimeout)
{
db.BeginTransaction(IsolationLevel.ReadCommitted);
return AcquireWhenMaxWaitTimeElapsed(db);
}
}
}, _cancellationTokenSource.Token);
}
// get a read lock
_sqlServerSyntax.ReadLock(db, Constants.Locks.MainDom);
private bool? TryAcquire(IUmbracoDatabase db, string tempId, string updatedTempId)
{
using var transaction = db.GetTransaction(IsolationLevel.ReadCommitted);
try
{
// get a read lock
_sqlServerSyntax.ReadLock(db, Constants.Locks.MainDom);
// the row
var mainDomRows = db.Fetch<KeyValueDto>("SELECT * FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey });
if (mainDomRows.Count == 0 || mainDomRows[0].Value == updatedTempId)
{
// the other main dom has updated our record
// Or the other maindom shutdown super fast and just deleted the record
// which indicates that we
// can acquire it and it has shutdown.
if (mainDomRows.Count == 0 || mainDomRows[0].Value == updatedTempId)
{
// the other main dom has updated our record
// Or the other maindom shutdown super fast and just deleted the record
// which indicates that we
// can acquire it and it has shutdown.
_sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom);
_sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom);
// so now we update the row with our appdomain id
InsertLockRecord(_lockId);
// so now we update the row with our appdomain id
InsertLockRecord(_lockId, db);
_logger.Debug<SqlMainDomLock>("Acquired with ID {LockId}", _lockId);
return true;
}
@@ -272,83 +270,77 @@ namespace Umbraco.Core.Runtime
// another new AppDomain has come online and is wanting to take over. In that case, we will not
// acquire.
_logger.Debug<SqlMainDomLock>("Cannot acquire, another booting application detected.");
return false;
}
}
catch (Exception ex)
{
ResetDatabase();
if (IsLockTimeoutException(ex))
{
_logger.Error<SqlMainDomLock>(ex, "Sql timeout occurred, waiting for existing MainDom is canceled.");
_hasError = true;
return false;
}
// unexpected
_logger.Error<SqlMainDomLock>(ex, "Unexpected error, waiting for existing MainDom is canceled.");
_hasError = true;
return false;
}
finally
{
db?.CompleteTransaction();
}
if (watch.ElapsedMilliseconds >= millisecondsTimeout)
{
// if the timeout has elapsed, it either means that the other main dom is taking too long to shutdown,
// or it could mean that the previous appdomain was terminated and didn't clear out the main dom SQL row
// and it's just been left as an orphan row.
// There's really know way of knowing unless we are constantly updating the row for the current maindom
// which isn't ideal.
// So... we're going to 'just' take over, if the writelock works then we'll assume we're ok
_logger.Debug<SqlMainDomLock>("Timeout elapsed, assuming orphan row, acquiring MainDom.");
try
{
db.BeginTransaction(IsolationLevel.ReadCommitted);
_sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom);
// so now we update the row with our appdomain id
InsertLockRecord(_lockId);
_logger.Debug<SqlMainDomLock>("Acquired with ID {LockId}", _lockId);
return true;
}
catch (Exception ex)
{
ResetDatabase();
if (IsLockTimeoutException(ex))
{
// something is wrong, we cannot acquire, not much we can do
_logger.Error<SqlMainDomLock>(ex, "Sql timeout occurred, could not forcibly acquire MainDom.");
_hasError = true;
return false;
}
_logger.Error<SqlMainDomLock>(ex, "Unexpected error, could not forcibly acquire MainDom.");
_hasError = true;
return false;
}
finally
{
db?.CompleteTransaction();
}
}
_logger.Debug<SqlMainDomLock>("Cannot acquire, another booting application detected.");
return false;
}
}, _cancellationTokenSource.Token);
}
catch (Exception ex)
{
if (IsLockTimeoutException(ex as SqlException))
{
_logger.Error<SqlMainDomLock>(ex, "Sql timeout occurred, waiting for existing MainDom is canceled.");
_errorDuringAcquiring = true;
return false;
}
// unexpected
_logger.Error<SqlMainDomLock>(ex, "Unexpected error, waiting for existing MainDom is canceled.");
_errorDuringAcquiring = true;
return false;
}
finally
{
transaction.Complete();
}
return null; // continue
}
private bool AcquireWhenMaxWaitTimeElapsed(IUmbracoDatabase db)
{
// if the timeout has elapsed, it either means that the other main dom is taking too long to shutdown,
// or it could mean that the previous appdomain was terminated and didn't clear out the main dom SQL row
// and it's just been left as an orphan row.
// There's really know way of knowing unless we are constantly updating the row for the current maindom
// which isn't ideal.
// So... we're going to 'just' take over, if the writelock works then we'll assume we're ok
_logger.Debug<SqlMainDomLock>("Timeout elapsed, assuming orphan row, acquiring MainDom.");
using var transaction = db.GetTransaction(IsolationLevel.ReadCommitted);
try
{
_sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom);
// so now we update the row with our appdomain id
InsertLockRecord(_lockId, db);
_logger.Debug<SqlMainDomLock>("Acquired with ID {LockId}", _lockId);
return true;
}
catch (Exception ex)
{
if (IsLockTimeoutException(ex as SqlException))
{
// something is wrong, we cannot acquire, not much we can do
_logger.Error<SqlMainDomLock>(ex, "Sql timeout occurred, could not forcibly acquire MainDom.");
_errorDuringAcquiring = true;
return false;
}
_logger.Error<SqlMainDomLock>(ex, "Unexpected error, could not forcibly acquire MainDom.");
_errorDuringAcquiring = true;
return false;
}
finally
{
transaction.Complete();
}
}
/// <summary>
/// Inserts or updates the key/value row
/// </summary>
private RecordPersistenceType InsertLockRecord(string id)
private RecordPersistenceType InsertLockRecord(string id, IUmbracoDatabase db)
{
var db = GetDatabase();
return db.InsertOrUpdate(new KeyValueDto
{
Key = MainDomKey,
@@ -361,9 +353,8 @@ namespace Umbraco.Core.Runtime
/// Checks if the DB row value is equals the value
/// </summary>
/// <returns></returns>
private bool IsMainDomValue(string val)
private bool IsMainDomValue(string val, IUmbracoDatabase db)
{
var db = GetDatabase();
return db.ExecuteScalar<int>("SELECT COUNT(*) FROM umbracoKeyValue WHERE [key] = @key AND [value] = @val",
new { key = MainDomKey, val = val }) == 1;
}
@@ -373,7 +364,7 @@ namespace Umbraco.Core.Runtime
/// </summary>
/// <param name="exception"></param>
/// <returns></returns>
private bool IsLockTimeoutException(Exception exception) => exception is SqlException sqlException && sqlException.Number == 1222;
private bool IsLockTimeoutException(SqlException sqlException) => sqlException?.Number == 1222;
#region IDisposable Support
private bool _disposedValue = false; // To detect redundant calls
@@ -392,11 +383,11 @@ namespace Umbraco.Core.Runtime
if (_dbFactory.Configured)
{
var db = GetDatabase();
using var db = _dbFactory.CreateDatabase();
using var transaction = db.GetTransaction(IsolationLevel.ReadCommitted);
try
{
db.BeginTransaction(IsolationLevel.ReadCommitted);
// get a write lock
_sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom);
@@ -409,24 +400,21 @@ namespace Umbraco.Core.Runtime
if (_mainDomChanging)
{
_logger.Debug<SqlMainDomLock>("Releasing MainDom, updating row, new application is booting.");
db.Execute($"UPDATE umbracoKeyValue SET [value] = [value] + '{UpdatedSuffix}' WHERE [key] = @key", new { key = MainDomKey });
var count = db.Execute($"UPDATE umbracoKeyValue SET [value] = [value] + '{UpdatedSuffix}' WHERE [key] = @key", new { key = MainDomKey });
}
else
{
_logger.Debug<SqlMainDomLock>("Releasing MainDom, deleting row, application is shutting down.");
db.Execute("DELETE FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey });
var count = db.Execute("DELETE FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey });
}
}
catch (Exception ex)
{
ResetDatabase();
_logger.Error<SqlMainDomLock>(ex, "Unexpected error during dipsose.");
_hasError = true;
}
finally
{
db?.CompleteTransaction();
ResetDatabase();
transaction.Complete();
}
}
}
@@ -4,6 +4,7 @@ using System.Threading.Tasks;
using Umbraco.Core;
using Umbraco.Core.Configuration.HealthChecks;
using Umbraco.Core.Logging;
using Umbraco.Core.Scoping;
using Umbraco.Core.Sync;
using Umbraco.Web.HealthCheck;
@@ -14,20 +15,31 @@ namespace Umbraco.Web.Scheduling
private readonly IMainDom _mainDom;
private readonly HealthCheckCollection _healthChecks;
private readonly HealthCheckNotificationMethodCollection _notifications;
private readonly IScopeProvider _scopeProvider;
private readonly IProfilingLogger _logger;
private readonly IHealthChecksSettings _healthChecksSettingsConfig;
private readonly IServerRegistrar _serverRegistrar;
private readonly IRuntimeState _runtimeState;
public HealthCheckNotifier(IBackgroundTaskRunner<RecurringTaskBase> runner, int delayMilliseconds, int periodMilliseconds,
HealthCheckCollection healthChecks, HealthCheckNotificationMethodCollection notifications,
IMainDom mainDom, IProfilingLogger logger, IHealthChecksSettings healthChecksSettingsConfig, IServerRegistrar serverRegistrar,
IRuntimeState runtimeState)
public HealthCheckNotifier(
IBackgroundTaskRunner<RecurringTaskBase> runner,
int delayMilliseconds,
int periodMilliseconds,
HealthCheckCollection healthChecks,
HealthCheckNotificationMethodCollection notifications,
IMainDom mainDom,
IProfilingLogger logger,
IHealthChecksSettings healthChecksSettingsConfig,
IServerRegistrar serverRegistrar,
IRuntimeState runtimeState,
IScopeProvider scopeProvider)
: base(runner, delayMilliseconds, periodMilliseconds)
{
_healthChecks = healthChecks;
_notifications = notifications;
_mainDom = mainDom;
_scopeProvider = scopeProvider;
_runtimeState = runtimeState;
_logger = logger;
_healthChecksSettingsConfig = healthChecksSettingsConfig;
_serverRegistrar = serverRegistrar;
@@ -56,6 +68,10 @@ namespace Umbraco.Web.Scheduling
return false; // do NOT repeat, going down
}
// Ensure we use an explicit scope since we are running on a background thread and plugin health
// checks can be making service/database calls so we want to ensure the CallContext/Ambient scope
// isn't used since that can be problematic.
using (var scope = _scopeProvider.CreateScope())
using (_logger.DebugDuration<HealthCheckNotifier>("Health checks executing", "Health checks complete"))
{
var healthCheckConfig = _healthChecksSettingsConfig;
@@ -72,8 +72,7 @@ namespace Umbraco.Web.Scheduling
return false; // do NOT repeat, going down
}
// running on a background task, and Log.CleanLogs uses the old SqlHelper,
// better wrap in a scope and ensure it's all cleaned up and nothing leaks
// Ensure we use an explicit scope since we are running on a background thread.
using (var scope = _scopeProvider.CreateScope())
using (_logger.DebugDuration<LogScrubber>("Log scrubbing executing", "Log scrubbing complete"))
{
@@ -9,16 +9,18 @@ namespace Umbraco.Web.Scheduling
{
public class ScheduledPublishing : RecurringTaskBase
{
private readonly IRuntimeState _runtime;
private readonly IMainDom _mainDom;
private readonly IServerRegistrar _serverRegistrar;
private readonly IContentService _contentService;
private readonly IUmbracoContextFactory _umbracoContextFactory;
private readonly ILogger _logger;
private readonly IMainDom _mainDom;
private readonly IRuntimeState _runtime;
private readonly IServerMessenger _serverMessenger;
private readonly IServerRegistrar _serverRegistrar;
private readonly IUmbracoContextFactory _umbracoContextFactory;
public ScheduledPublishing(IBackgroundTaskRunner<RecurringTaskBase> runner, int delayMilliseconds, int periodMilliseconds,
IRuntimeState runtime, IMainDom mainDom, IServerRegistrar serverRegistrar, IContentService contentService, IUmbracoContextFactory umbracoContextFactory, ILogger logger, IServerMessenger serverMessenger)
public ScheduledPublishing(IBackgroundTaskRunner<RecurringTaskBase> runner, int delayMilliseconds,
int periodMilliseconds,
IRuntimeState runtime, IMainDom mainDom, IServerRegistrar serverRegistrar, IContentService contentService,
IUmbracoContextFactory umbracoContextFactory, ILogger logger, IServerMessenger serverMessenger)
: base(runner, delayMilliseconds, periodMilliseconds)
{
_runtime = runtime;
@@ -30,6 +32,8 @@ namespace Umbraco.Web.Scheduling
_serverMessenger = serverMessenger;
}
public override bool IsAsync => false;
public override bool PerformRun()
{
if (Suspendable.ScheduledPublishing.CanRun == false)
@@ -61,24 +65,27 @@ namespace Umbraco.Web.Scheduling
try
{
// ensure we run with an UmbracoContext, because this may run in a background task,
// yet developers may be using the 'current' UmbracoContext in the event handlers
//
// We don't need an explicit scope here because PerformScheduledPublish creates it's own scope
// so it's safe as it will create it's own ambient scope.
// Ensure we run with an UmbracoContext, because this will run in a background task,
// and developers may be using the UmbracoContext in the event handlers.
// TODO: or maybe not, CacheRefresherComponent already ensures a context when handling events
// - UmbracoContext 'current' needs to be refactored and cleaned up
// - batched messenger should not depend on a current HttpContext
// but then what should be its "scope"? could we attach it to scopes?
// - and we should definitively *not* have to flush it here (should be auto)
//
using (
var contextReference = _umbracoContextFactory.EnsureUmbracoContext())
using (var contextReference = _umbracoContextFactory.EnsureUmbracoContext())
{
try
{
// run
var result = _contentService.PerformScheduledPublish(DateTime.Now);
foreach (var grouped in result.GroupBy(x => x.Result))
_logger.Info<ScheduledPublishing>("Scheduled publishing result: '{StatusCount}' items with status {Status}", grouped.Count(), grouped.Key);
_logger.Info<ScheduledPublishing>(
"Scheduled publishing result: '{StatusCount}' items with status {Status}",
grouped.Count(), grouped.Key);
}
finally
{
@@ -96,7 +103,5 @@ namespace Umbraco.Web.Scheduling
return true; // repeat
}
public override bool IsAsync => false;
}
}
@@ -174,7 +174,7 @@ namespace Umbraco.Web.Scheduling
}
var periodInMilliseconds = healthCheckSettingsConfig.NotificationSettings.PeriodInHours * 60 * 60 * 1000;
var task = new HealthCheckNotifier(_healthCheckRunner, delayInMilliseconds, periodInMilliseconds, healthChecks, notifications, _mainDom, logger, _healthChecksSettingsConfig, _serverRegistrar, _runtime);
var task = new HealthCheckNotifier(_healthCheckRunner, delayInMilliseconds, periodInMilliseconds, healthChecks, notifications, _mainDom, logger, _healthChecksSettingsConfig, _serverRegistrar, _runtime, _scopeProvider);
_healthCheckRunner.TryAdd(task);
return task;
}
@@ -881,7 +881,7 @@ namespace Umbraco.Core.Services.Implement
throw new NotSupportedException($"Culture \"{culture}\" is not supported by invariant content types.");
}
if(content.Name != null && content.Name.Length > 255)
if (content.Name != null && content.Name.Length > 255)
{
throw new InvalidOperationException("Name cannot be more than 255 characters in length.");
}
@@ -1258,7 +1258,7 @@ namespace Umbraco.Core.Services.Implement
if (publishResult == null)
throw new PanicException("publishResult == null - should not happen");
switch(publishResult.Result)
switch (publishResult.Result)
{
case PublishResultType.FailedPublishMandatoryCultureMissing:
//occurs when a mandatory culture was unpublished (which means we tried publishing the document without a mandatory culture)
@@ -1366,24 +1366,90 @@ namespace Umbraco.Core.Services.Implement
/// <inheritdoc />
public IEnumerable<PublishResult> PerformScheduledPublish(DateTime date)
=> PerformScheduledPublishInternal(date).ToList();
// beware! this method yields results, so the returned IEnumerable *must* be
// enumerated for anything to happen - dangerous, so private + exposed via
// the public method above, which forces ToList().
private IEnumerable<PublishResult> PerformScheduledPublishInternal(DateTime date)
{
var allLangs = new Lazy<List<ILanguage>>(() => _languageRepository.GetMany().ToList());
var evtMsgs = EventMessagesFactory.Get();
var results = new List<PublishResult>();
using (var scope = ScopeProvider.CreateScope())
PerformScheduledPublishingRelease(date, results, evtMsgs, allLangs);
PerformScheduledPublishingExpiration(date, results, evtMsgs, allLangs);
return results;
}
private void PerformScheduledPublishingExpiration(DateTime date, List<PublishResult> results, EventMessages evtMsgs, Lazy<List<ILanguage>> allLangs)
{
using var scope = ScopeProvider.CreateScope();
// do a fast read without any locks since this executes often to see if we even need to proceed
if (_documentRepository.HasContentForExpiration(date))
{
// now take a write lock since we'll be updating
scope.WriteLock(Constants.Locks.ContentTree);
var allLangs = _languageRepository.GetMany().ToList();
foreach (var d in _documentRepository.GetContentForExpiration(date))
{
if (d.ContentType.VariesByCulture())
{
//find which cultures have pending schedules
var pendingCultures = d.ContentSchedule.GetPending(ContentScheduleAction.Expire, date)
.Select(x => x.Culture)
.Distinct()
.ToList();
if (pendingCultures.Count == 0)
continue; //shouldn't happen but no point in processing this document if there's nothing there
var saveEventArgs = new ContentSavingEventArgs(d, evtMsgs);
if (scope.Events.DispatchCancelable(Saving, this, saveEventArgs, nameof(Saving)))
{
results.Add(new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, d));
continue;
}
foreach (var c in pendingCultures)
{
//Clear this schedule for this culture
d.ContentSchedule.Clear(c, ContentScheduleAction.Expire, date);
//set the culture to be published
d.UnpublishCulture(c);
}
var result = CommitDocumentChangesInternal(scope, d, saveEventArgs, allLangs.Value, d.WriterId);
if (result.Success == false)
Logger.Error<ContentService>(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result);
results.Add(result);
}
else
{
//Clear this schedule
d.ContentSchedule.Clear(ContentScheduleAction.Expire, date);
var result = Unpublish(d, userId: d.WriterId);
if (result.Success == false)
Logger.Error<ContentService>(null, "Failed to unpublish document id={DocumentId}, reason={Reason}.", d.Id, result.Result);
results.Add(result);
}
}
_documentRepository.ClearSchedule(date, ContentScheduleAction.Expire);
}
scope.Complete();
}
private void PerformScheduledPublishingRelease(DateTime date, List<PublishResult> results, EventMessages evtMsgs, Lazy<List<ILanguage>> allLangs)
{
using var scope = ScopeProvider.CreateScope();
// do a fast read without any locks since this executes often to see if we even need to proceed
if (_documentRepository.HasContentForRelease(date))
{
// now take a write lock since we'll be updating
scope.WriteLock(Constants.Locks.ContentTree);
foreach (var d in _documentRepository.GetContentForRelease(date))
{
PublishResult result;
if (d.ContentType.VariesByCulture())
{
//find which cultures have pending schedules
@@ -1393,11 +1459,14 @@ namespace Umbraco.Core.Services.Implement
.ToList();
if (pendingCultures.Count == 0)
break; //shouldn't happen but no point in continuing if there's nothing there
continue; //shouldn't happen but no point in processing this document if there's nothing there
var saveEventArgs = new ContentSavingEventArgs(d, evtMsgs);
if (scope.Events.DispatchCancelable(Saving, this, saveEventArgs, nameof(Saving)))
yield return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, d);
{
results.Add(new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, d));
continue; // this document is canceled move next
}
var publishing = true;
foreach (var culture in pendingCultures)
@@ -1409,94 +1478,51 @@ namespace Umbraco.Core.Services.Implement
//publish the culture values and validate the property values, if validation fails, log the invalid properties so the develeper has an idea of what has failed
IProperty[] invalidProperties = null;
var impact = CultureImpact.Explicit(culture, IsDefaultCulture(allLangs, culture));
var impact = CultureImpact.Explicit(culture, IsDefaultCulture(allLangs.Value, culture));
var tryPublish = d.PublishCulture(impact) && _propertyValidationService.Value.IsPropertyDataValid(d, out invalidProperties, impact);
if (invalidProperties != null && invalidProperties.Length > 0)
Logger.Warn<ContentService>("Scheduled publishing will fail for document {DocumentId} and culture {Culture} because of invalid properties {InvalidProperties}",
d.Id, culture, string.Join(",", invalidProperties.Select(x => x.Alias)));
publishing &= tryPublish; //set the culture to be published
if (!publishing) break; // no point continuing
if (!publishing) continue; // move to next document
}
PublishResult result;
if (d.Trashed)
result = new PublishResult(PublishResultType.FailedPublishIsTrashed, evtMsgs, d);
else if (!publishing)
result = new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, d);
else
result = CommitDocumentChangesInternal(scope, d, saveEventArgs, allLangs, d.WriterId);
result = CommitDocumentChangesInternal(scope, d, saveEventArgs, allLangs.Value, d.WriterId);
if (result.Success == false)
Logger.Error<ContentService>(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result);
yield return result;
results.Add(result);
}
else
{
//Clear this schedule
d.ContentSchedule.Clear(ContentScheduleAction.Release, date);
result = d.Trashed
var result = d.Trashed
? new PublishResult(PublishResultType.FailedPublishIsTrashed, evtMsgs, d)
: SaveAndPublish(d, userId: d.WriterId);
if (result.Success == false)
Logger.Error<ContentService>(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result);
yield return result;
results.Add(result);
}
}
foreach (var d in _documentRepository.GetContentForExpiration(date))
{
PublishResult result;
if (d.ContentType.VariesByCulture())
{
//find which cultures have pending schedules
var pendingCultures = d.ContentSchedule.GetPending(ContentScheduleAction.Expire, date)
.Select(x => x.Culture)
.Distinct()
.ToList();
_documentRepository.ClearSchedule(date, ContentScheduleAction.Release);
if (pendingCultures.Count == 0)
break; //shouldn't happen but no point in continuing if there's nothing there
var saveEventArgs = new ContentSavingEventArgs(d, evtMsgs);
if (scope.Events.DispatchCancelable(Saving, this, saveEventArgs, nameof(Saving)))
yield return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, d);
foreach (var c in pendingCultures)
{
//Clear this schedule for this culture
d.ContentSchedule.Clear(c, ContentScheduleAction.Expire, date);
//set the culture to be published
d.UnpublishCulture(c);
}
result = CommitDocumentChangesInternal(scope, d, saveEventArgs, allLangs, d.WriterId);
if (result.Success == false)
Logger.Error<ContentService>(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result);
yield return result;
}
else
{
//Clear this schedule
d.ContentSchedule.Clear(ContentScheduleAction.Expire, date);
result = Unpublish(d, userId: d.WriterId);
if (result.Success == false)
Logger.Error<ContentService>(null, "Failed to unpublish document id={DocumentId}, reason={Reason}.", d.Id, result.Result);
yield return result;
}
}
_documentRepository.ClearSchedule(date);
scope.Complete();
}
scope.Complete();
}
// utility 'PublishCultures' func used by SaveAndPublishBranch
@@ -1,18 +1,77 @@
using System.Linq;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Umbraco.Core.Composing;
using Umbraco.Core.Models;
using Umbraco.Core.PropertyEditors;
namespace Umbraco.Core.Services
{
internal class PropertyValidationService : IPropertyValidationService
public class PropertyValidationService : IPropertyValidationService
{
private readonly PropertyEditorCollection _propertyEditors;
private readonly IDataTypeService _dataTypeService;
private readonly ILocalizedTextService _textService;
public PropertyValidationService(PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService)
public PropertyValidationService(PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, ILocalizedTextService textService)
{
_propertyEditors = propertyEditors;
_dataTypeService = dataTypeService;
_textService = textService;
}
public IEnumerable<ValidationResult> ValidatePropertyValue(
IPropertyType propertyType,
object postedValue)
{
if (propertyType is null) throw new ArgumentNullException(nameof(propertyType));
var dataType = _dataTypeService.GetDataType(propertyType.DataTypeId);
if (dataType == null) throw new InvalidOperationException("No data type found by id " + propertyType.DataTypeId);
var editor = _propertyEditors[propertyType.PropertyEditorAlias];
if (editor == null) throw new InvalidOperationException("No property editor found by alias " + propertyType.PropertyEditorAlias);
return ValidatePropertyValue(_textService, editor, dataType, postedValue, propertyType.Mandatory, propertyType.ValidationRegExp, propertyType.MandatoryMessage, propertyType.ValidationRegExpMessage);
}
public static IEnumerable<ValidationResult> ValidatePropertyValue(
ILocalizedTextService textService,
IDataEditor editor,
IDataType dataType,
object postedValue,
bool isRequired,
string validationRegExp,
string isRequiredMessage,
string validationRegExpMessage)
{
// Retrieve default messages used for required and regex validatation. We'll replace these
// if set with custom ones if they've been provided for a given property.
var requiredDefaultMessages = new[]
{
textService.Localize("validation", "invalidNull"),
textService.Localize("validation", "invalidEmpty")
};
var formatDefaultMessages = new[]
{
textService.Localize("validation", "invalidPattern"),
};
var valueEditor = editor.GetValueEditor(dataType.Configuration);
foreach (var validationResult in valueEditor.Validate(postedValue, isRequired, validationRegExp))
{
// If we've got custom error messages, we'll replace the default ones that will have been applied in the call to Validate().
if (isRequired && !string.IsNullOrWhiteSpace(isRequiredMessage) && requiredDefaultMessages.Contains(validationResult.ErrorMessage, StringComparer.OrdinalIgnoreCase))
{
validationResult.ErrorMessage = isRequiredMessage;
}
if (!string.IsNullOrWhiteSpace(validationRegExp) && !string.IsNullOrWhiteSpace(validationRegExpMessage) && formatDefaultMessages.Contains(validationResult.ErrorMessage, StringComparer.OrdinalIgnoreCase))
{
validationResult.ErrorMessage = validationRegExpMessage;
}
yield return validationResult;
}
}
/// <inheritdoc />
@@ -553,8 +553,10 @@ namespace Umbraco.ModelsBuilder.Embedded
if (modelInfos.TryGetValue(typeName, out var modelInfo))
throw new InvalidOperationException($"Both types {type.FullName} and {modelInfo.ModelType.FullName} want to be a model type for content type with alias \"{typeName}\".");
// fixme use Core's ReflectionUtilities.EmitCtor !!
// TODO: use Core's ReflectionUtilities.EmitCtor !!
// Yes .. DynamicMethod is uber slow
// TODO: But perhaps https://docs.microsoft.com/en-us/dotnet/api/system.reflection.emit.constructorbuilder?view=netcore-3.1 is better still?
// See CtorInvokeBenchmarks
var meth = new DynamicMethod(string.Empty, typeof(IPublishedElement), ctorArgTypes, type.Module, true);
var gen = meth.GetILGenerator();
gen.Emit(OpCodes.Ldarg_0);
@@ -384,15 +384,11 @@ namespace Umbraco.Web.PublishedCache.NuCache
#region Content types
public override IPublishedContentType GetContentType(int id)
{
return _snapshot.GetContentType(id);
}
public override IPublishedContentType GetContentType(int id) => _snapshot.GetContentType(id);
public override IPublishedContentType GetContentType(string alias)
{
return _snapshot.GetContentType(alias);
}
public override IPublishedContentType GetContentType(string alias) => _snapshot.GetContentType(alias);
public override IPublishedContentType GetContentType(Guid key) => _snapshot.GetContentType(key);
#endregion
@@ -37,9 +37,14 @@ namespace Umbraco.Web.PublishedCache.NuCache
private readonly IVariationContextAccessor _variationContextAccessor;
private readonly ConcurrentDictionary<int, LinkedNode<ContentNode>> _contentNodes;
private LinkedNode<ContentNode> _root;
private readonly ConcurrentDictionary<int, LinkedNode<IPublishedContentType>> _contentTypesById;
// We must keep separate dictionaries for by id and by alias because we track these in snapshot/layers
// and it is possible that the alias of a content type can be different for the same id in another layer
// whereas the GUID -> INT cross reference can never be different
private readonly ConcurrentDictionary<int, LinkedNode<IPublishedContentType>> _contentTypesById;
private readonly ConcurrentDictionary<string, LinkedNode<IPublishedContentType>> _contentTypesByAlias;
private readonly ConcurrentDictionary<Guid, int> _xmap;
private readonly ConcurrentDictionary<Guid, int> _contentTypeKeyToIdMap;
private readonly ConcurrentDictionary<Guid, int> _contentKeyToIdMap;
private readonly ILogger _logger;
private readonly IPublishedModelFactory _publishedModelFactory;
@@ -76,7 +81,8 @@ namespace Umbraco.Web.PublishedCache.NuCache
_root = new LinkedNode<ContentNode>(new ContentNode(), 0);
_contentTypesById = new ConcurrentDictionary<int, LinkedNode<IPublishedContentType>>();
_contentTypesByAlias = new ConcurrentDictionary<string, LinkedNode<IPublishedContentType>>(StringComparer.InvariantCultureIgnoreCase);
_xmap = new ConcurrentDictionary<Guid, int>();
_contentTypeKeyToIdMap = new ConcurrentDictionary<Guid, int>();
_contentKeyToIdMap = new ConcurrentDictionary<Guid, int>();
_genObjs = new ConcurrentQueue<GenObj>();
_genObj = null; // no initial gen exists
@@ -139,7 +145,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
Monitor.Enter(_wlocko, ref lockInfo.Taken);
lock(_rlocko)
lock (_rlocko)
{
// see SnapDictionary
try { }
@@ -294,8 +300,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
foreach (var type in types)
{
SetValueLocked(_contentTypesById, type.Id, type);
SetValueLocked(_contentTypesByAlias, type.Alias, type);
SetContentTypeLocked(type);
}
}
@@ -321,8 +326,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
foreach (var type in index.Values)
{
SetValueLocked(_contentTypesById, type.Id, type);
SetValueLocked(_contentTypesByAlias, type.Alias, type);
SetContentTypeLocked(type);
}
foreach (var link in _contentNodes.Values)
@@ -357,8 +361,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
// set all new content types
foreach (var type in types)
{
SetValueLocked(_contentTypesById, type.Id, type);
SetValueLocked(_contentTypesByAlias, type.Alias, type);
SetContentTypeLocked(type);
}
// beware! at that point the cache is inconsistent,
@@ -422,8 +425,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
// perform update of refreshed content types
foreach (var type in refreshedTypesA)
{
SetValueLocked(_contentTypesById, type.Id, type);
SetValueLocked(_contentTypesByAlias, type.Alias, type);
SetContentTypeLocked(type);
}
// perform update of content with refreshed content type - from the kits
@@ -641,7 +643,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
kit.Node.PreviousSiblingContentId = existing.PreviousSiblingContentId;
}
_xmap[kit.Node.Uid] = kit.Node.Id;
_contentKeyToIdMap[kit.Node.Uid] = kit.Node.Id;
return true;
}
@@ -737,7 +739,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
// this node becomes the previous node
previousNode = thisNode;
_xmap[kit.Node.Uid] = kit.Node.Id;
_contentKeyToIdMap[kit.Node.Uid] = kit.Node.Id;
}
return ok;
@@ -781,7 +783,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
if (_localDb != null) RegisterChange(kit.Node.Id, kit);
AddTreeNodeLocked(kit.Node, parent);
_xmap[kit.Node.Uid] = kit.Node.Id;
_contentKeyToIdMap[kit.Node.Uid] = kit.Node.Id;
}
return ok;
@@ -836,7 +838,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
if (_localDb != null) RegisterChange(kit.Node.Id, kit);
AddTreeNodeLocked(kit.Node, parent);
_xmap[kit.Node.Uid] = kit.Node.Id;
_contentKeyToIdMap[kit.Node.Uid] = kit.Node.Id;
}
return ok;
@@ -888,11 +890,11 @@ namespace Umbraco.Web.PublishedCache.NuCache
// This should never be null, all code that calls this method is null checking but we've seen
// issues of null ref exceptions in issue reports so we'll double check here
if (content == null) throw new ArgumentNullException(nameof(content));
SetValueLocked(_contentNodes, content.Id, null);
if (_localDb != null) RegisterChange(content.Id, ContentNodeKit.Null);
_xmap.TryRemove(content.Uid, out _);
_contentKeyToIdMap.TryRemove(content.Uid, out _);
var id = content.FirstChildContentId;
while (id > 0)
@@ -1157,6 +1159,15 @@ namespace Umbraco.Web.PublishedCache.NuCache
}
}
private void SetContentTypeLocked(IPublishedContentType type)
{
SetValueLocked(_contentTypesById, type.Id, type);
SetValueLocked(_contentTypesByAlias, type.Alias, type);
// ensure the key/id map is accurate
if (type.TryGetKey(out var key))
_contentTypeKeyToIdMap[key] = type.Id;
}
// set a node (just the node, not the tree)
private void SetValueLocked<TKey, TValue>(ConcurrentDictionary<TKey, LinkedNode<TValue>> dict, TKey key, TValue value)
where TValue : class
@@ -1214,7 +1225,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
public ContentNode Get(Guid uid, long gen)
{
return _xmap.TryGetValue(uid, out var id)
return _contentKeyToIdMap.TryGetValue(uid, out var id)
? GetValue(_contentNodes, id, gen)
: null;
}
@@ -1277,13 +1288,20 @@ namespace Umbraco.Web.PublishedCache.NuCache
return GetValue(_contentTypesByAlias, alias, gen);
}
public IPublishedContentType GetContentType(Guid key, long gen)
{
if (!_contentTypeKeyToIdMap.TryGetValue(key, out var id))
return null;
return GetContentType(id, gen);
}
#endregion
#region Snapshots
public Snapshot CreateSnapshot()
{
lock(_rlocko)
lock (_rlocko)
{
// if no next generation is required, and we already have one,
// use it and create a new snapshot
@@ -1609,6 +1627,13 @@ namespace Umbraco.Web.PublishedCache.NuCache
return _store.GetContentType(alias, _gen);
}
public IPublishedContentType GetContentType(Guid key)
{
if (_gen < 0)
throw new ObjectDisposedException("snapshot" /*+ " (" + _thisCount + ")"*/);
return _store.GetContentType(key, _gen);
}
// this code is here just so you don't try to implement it
// the only way we can iterate over "all" without locking the entire cache forever
// is by shallow cloning the cache, which is quite expensive, so we should probably not do it,
@@ -214,7 +214,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource
if (dto.EditData == null)
{
if (Debugger.IsAttached)
throw new Exception("Missing cmsContentNu edited content for node " + dto.Id + ", consider rebuilding.");
throw new InvalidOperationException("Missing cmsContentNu edited content for node " + dto.Id + ", consider rebuilding.");
_logger.Warn<DatabaseDataSource>("Missing cmsContentNu edited content for node {NodeId}, consider rebuilding.", dto.Id);
}
else
@@ -241,7 +241,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource
if (dto.PubData == null)
{
if (Debugger.IsAttached)
throw new Exception("Missing cmsContentNu published content for node " + dto.Id + ", consider rebuilding.");
throw new InvalidOperationException("Missing cmsContentNu published content for node " + dto.Id + ", consider rebuilding.");
_logger.Warn<DatabaseDataSource>("Missing cmsContentNu published content for node {NodeId}, consider rebuilding.", dto.Id);
}
else
@@ -280,7 +280,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource
private static ContentNodeKit CreateMediaNodeKit(ContentSourceDto dto)
{
if (dto.EditData == null)
throw new Exception("No data for media " + dto.Id);
throw new InvalidOperationException("No data for media " + dto.Id);
var nested = DeserializeNestedData(dto.EditData);
@@ -155,15 +155,11 @@ namespace Umbraco.Web.PublishedCache.NuCache
#region Content types
public override IPublishedContentType GetContentType(int id)
{
return _snapshot.GetContentType(id);
}
public override IPublishedContentType GetContentType(int id) => _snapshot.GetContentType(id);
public override IPublishedContentType GetContentType(string alias)
{
return _snapshot.GetContentType(alias);
}
public override IPublishedContentType GetContentType(string alias) => _snapshot.GetContentType(alias);
public override IPublishedContentType GetContentType(Guid key) => _snapshot.GetContentType(key);
#endregion
+374
View File
@@ -0,0 +1,374 @@
using System;
using System.Threading;
using System.Linq;
using System.Web.Mvc;
using Umbraco.Core.Services;
using Umbraco.Core.Models;
using System.Web;
using System.Web.Hosting;
using System.Web.Routing;
using System.Diagnostics;
using Umbraco.Core.Composing;
using System.Configuration;
using Umbraco.Core.Strings;
// see https://github.com/Shazwazza/UmbracoScripts/tree/master/src/LoadTesting
namespace Umbraco.TestData
{
public class LoadTestController : Controller
{
public LoadTestController(ServiceContext serviceContext, IShortStringHelper shortStringHelper)
{
_serviceContext = serviceContext;
_shortStringHelper = shortStringHelper;
}
private static readonly Random _random = new Random();
private static readonly object _locko = new object();
private static volatile int _containerId = -1;
private const string _containerAlias = "LoadTestContainer";
private const string _contentAlias = "LoadTestContent";
private const int _textboxDefinitionId = -88;
private const int _maxCreate = 1000;
private static readonly string HeadHtml = @"<html>
<head>
<title>LoadTest</title>
<style>
body { font-family: arial; }
a,a:visited { color: blue; }
h1 { margin: 0; padding: 0; font-size: 120%; font-weight: bold; }
h1 a { text-decoration: none; }
div.block { margin: 20px 0; }
ul { margin:0; }
div.ver { font-size: 80%; }
div.head { padding:0 0 10px 0; margin: 0 0 20px 0; border-bottom: 1px solid #cccccc; }
</style>
</head>
<body>
<div class=""head"">
<h1><a href=""/LoadTest"">LoadTest</a></h1>
<div class=""ver"">" + System.Configuration.ConfigurationManager.AppSettings["umbracoConfigurationStatus"] + @"</div>
</div>
";
private const string FootHtml = @"</body>
</html>";
private static readonly string _containerTemplateText = @"
@inherits Umbraco.Web.Mvc.UmbracoViewPage
@{
Layout = null;
var container = Umbraco.ContentAtRoot().OfTypes(""" + _containerAlias + @""").FirstOrDefault();
var contents = container.Children().ToArray();
var groups = contents.GroupBy(x => x.Value<string>(""origin""));
var id = contents.Length > 0 ? contents[0].Id : -1;
var wurl = Request.QueryString[""u""] == ""1"";
var missing = contents.Length > 0 && contents[contents.Length - 1].Id - contents[0].Id >= contents.Length;
}
" + HeadHtml + @"
<div class=""block"">
<span @Html.Raw(missing ? ""style=\""color:red;\"""" : """")>@contents.Length items</span>
<ul>
@foreach (var group in groups)
{
<li>@group.Key: @group.Count()</li>
}
</ul></div>
<div class=""block"">
@foreach (var content in contents)
{
while (content.Id > id)
{
<div style=""color:red;"">@id :: MISSING</div>
id++;
}
if (wurl)
{
<div>@content.Id :: @content.Name :: @content.Url</div>
}
else
{
<div>@content.Id :: @content.Name</div>
} id++;
}
</div>
" + FootHtml;
private readonly ServiceContext _serviceContext;
private readonly IShortStringHelper _shortStringHelper;
private ActionResult ContentHtml(string s)
{
return Content(HeadHtml + s + FootHtml);
}
public ActionResult Index()
{
var res = EnsureInitialize();
if (res != null) return res;
var html = @"Welcome. You can:
<ul>
<li><a href=""/LoadTestContainer"">List existing contents</a> (u:url)</li>
<li><a href=""/LoadTest/Create?o=browser"">Create a content</a> (o:origin, r:restart, n:number)</li>
<li><a href=""/LoadTest/Clear"">Clear all contents</a></li>
<li><a href=""/LoadTest/Domains"">List the current domains in w3wp.exe</a></li>
<li><a href=""/LoadTest/Restart"">Restart the current AppDomain</a></li>
<li><a href=""/LoadTest/Recycle"">Recycle the AppPool</a></li>
<li><a href=""/LoadTest/Die"">Cause w3wp.exe to die</a></li>
</ul>
";
return ContentHtml(html);
}
private ActionResult EnsureInitialize()
{
if (_containerId > 0) return null;
lock (_locko)
{
if (_containerId > 0) return null;
var contentTypeService = _serviceContext.ContentTypeService;
var contentType = contentTypeService.Get(_contentAlias);
if (contentType == null)
return ContentHtml("Not installed, first you must <a href=\"/LoadTest/Install\">install</a>.");
var containerType = contentTypeService.Get(_containerAlias);
if (containerType == null)
return ContentHtml("Panic! Container type is missing.");
var contentService = _serviceContext.ContentService;
var container = contentService.GetPagedOfType(containerType.Id, 0, 100, out _, null).FirstOrDefault();
if (container == null)
return ContentHtml("Panic! Container is missing.");
_containerId = container.Id;
return null;
}
}
public ActionResult Install()
{
var dataTypeService = _serviceContext.DataTypeService;
//var dataType = dataTypeService.GetAll(Constants.DataTypes.DefaultContentListView);
//if (!dict.ContainsKey("pageSize")) dict["pageSize"] = new PreValue("10");
//dict["pageSize"].Value = "200";
//dataTypeService.SavePreValues(dataType, dict);
var contentTypeService = _serviceContext.ContentTypeService;
var contentType = new ContentType(_shortStringHelper, -1)
{
Alias = _contentAlias,
Name = "LoadTest Content",
Description = "Content for LoadTest",
Icon = "icon-document"
};
var def = _serviceContext.DataTypeService.GetDataType(_textboxDefinitionId);
contentType.AddPropertyType(new PropertyType(_shortStringHelper, def)
{
Name = "Origin",
Alias = "origin",
Description = "The origin of the content.",
});
contentTypeService.Save(contentType);
var containerTemplate = ImportTemplate(_serviceContext, _shortStringHelper,
"LoadTestContainer", "LoadTestContainer", _containerTemplateText);
var containerType = new ContentType(_shortStringHelper, -1)
{
Alias = _containerAlias,
Name = "LoadTest Container",
Description = "Container for LoadTest content",
Icon = "icon-document",
AllowedAsRoot = true,
IsContainer = true
};
containerType.AllowedContentTypes = containerType.AllowedContentTypes.Union(new[]
{
new ContentTypeSort(new Lazy<int>(() => contentType.Id), 0, contentType.Alias),
});
containerType.AllowedTemplates = containerType.AllowedTemplates.Union(new[] { containerTemplate });
containerType.SetDefaultTemplate(containerTemplate);
contentTypeService.Save(containerType);
var contentService = _serviceContext.ContentService;
var content = contentService.Create("LoadTestContainer", -1, _containerAlias);
contentService.SaveAndPublish(content);
return ContentHtml("Installed.");
}
public ActionResult Create(int n = 1, int r = 0, string o = null)
{
var res = EnsureInitialize();
if (res != null) return res;
if (r < 0) r = 0;
if (r > 100) r = 100;
var restart = GetRandom(0, 100) > (100 - r);
var contentService = _serviceContext.ContentService;
if (n < 1) n = 1;
if (n > _maxCreate) n = _maxCreate;
for (int i = 0; i < n; i++)
{
var name = Guid.NewGuid().ToString("N").ToUpper() + "-" + (restart ? "R" : "X") + "-" + o;
var content = contentService.Create(name, _containerId, _contentAlias);
content.SetValue("origin", o);
contentService.SaveAndPublish(content);
}
if (restart)
DoRestart();
return ContentHtml("Created " + n + " content"
+ (restart ? ", and restarted" : "")
+ ".");
}
private int GetRandom(int minValue, int maxValue)
{
lock (_locko)
{
return _random.Next(minValue, maxValue);
}
}
public ActionResult Clear()
{
var res = EnsureInitialize();
if (res != null) return res;
var contentType = _serviceContext.ContentTypeService.Get(_contentAlias);
_serviceContext.ContentService.DeleteOfType(contentType.Id);
return ContentHtml("Cleared.");
}
private void DoRestart()
{
HttpContext.User = null;
System.Web.HttpContext.Current.User = null;
Thread.CurrentPrincipal = null;
HttpRuntime.UnloadAppDomain();
}
public ActionResult Restart()
{
DoRestart();
return ContentHtml("Restarted.");
}
public ActionResult Die()
{
var timer = new System.Threading.Timer(_ =>
{
throw new Exception("die!");
});
timer.Change(100, 0);
return ContentHtml("Dying.");
}
public ActionResult Domains()
{
var currentDomain = AppDomain.CurrentDomain;
var currentName = currentDomain.FriendlyName;
var pos = currentName.IndexOf('-');
if (pos > 0) currentName = currentName.Substring(0, pos);
var text = new System.Text.StringBuilder();
text.Append("<div class=\"block\">Process ID: " + Process.GetCurrentProcess().Id + "</div>");
text.Append("<div class=\"block\">");
text.Append("<div>IIS Site: " + HostingEnvironment.ApplicationHost.GetSiteName() + "</div>");
text.Append("<div>App ID: " + currentName + "</div>");
//text.Append("<div>AppPool: " + Zbu.WebManagement.AppPoolHelper.GetCurrentApplicationPoolName() + "</div>");
text.Append("</div>");
text.Append("<div class=\"block\">Domains:<ul>");
text.Append("<li>Not implemented.</li>");
/*
foreach (var domain in Zbu.WebManagement.AppDomainHelper.GetAppDomains().OrderBy(x => x.Id))
{
var name = domain.FriendlyName;
pos = name.IndexOf('-');
if (pos > 0) name = name.Substring(0, pos);
text.Append("<li style=\""
+ (name != currentName ? "color: #cccccc;" : "")
//+ (domain.Id == currentDomain.Id ? "" : "")
+ "\">"
+"[" + domain.Id + "] " + name
+ (domain.IsDefaultAppDomain() ? " (default)" : "")
+ (domain.Id == currentDomain.Id ? " (current)" : "")
+ "</li>");
}
*/
text.Append("</ul></div>");
return ContentHtml(text.ToString());
}
public ActionResult Recycle()
{
return ContentHtml("Not implemented&mdash;please use IIS console.");
}
private static Template ImportTemplate(ServiceContext svces, IShortStringHelper shortStringHelper, string name, string alias, string text, ITemplate master = null)
{
var t = new Template(shortStringHelper, name, alias) { Content = text };
if (master != null)
t.SetMasterTemplate(master);
svces.FileService.SaveTemplate(t);
return t;
}
}
public class TestComponent : IComponent
{
public void Initialize()
{
if (ConfigurationManager.AppSettings["Umbraco.TestData.Enabled"] != "true")
return;
RouteTable.Routes.MapRoute(
name: "LoadTest",
url: "LoadTest/{action}",
defaults: new
{
controller = "LoadTest",
action = "Index"
},
namespaces: new[] { "Umbraco.TestData" }
);
}
public void Terminate()
{
}
}
public class TestComposer : ComponentComposer<TestComponent>, IUserComposer
{
public override void Compose(Composition composition)
{
base.Compose(composition);
if (ConfigurationManager.AppSettings["Umbraco.TestData.Enabled"] != "true")
return;
composition.Register(typeof(LoadTestController), Lifetime.Request);
}
}
}
@@ -42,6 +42,7 @@
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="LoadTestController.cs" />
<Compile Include="SegmentTestController.cs" />
<Compile Include="UmbracoTestDataController.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
@@ -34,7 +34,7 @@ context('Document Types', () => {
cy.get('.umb-search-field').type('Textstring');
// Choose first item
cy.get('ul.umb-card-grid li a[title="Textstring"]').closest("li").click();
cy.get('ul.umb-card-grid li [title="Textstring"]').closest("li").click();
// Save property
cy.get('.btn-success').last().click();
@@ -34,7 +34,7 @@ context('Media Types', () => {
cy.get('.umb-search-field').type('Textstring');
// Choose first item
cy.get('ul.umb-card-grid li a[title="Textstring"]').closest("li").click();
cy.get('ul.umb-card-grid li [title="Textstring"]').closest("li").click();
// Save property
cy.get('.btn-success').last().click();
@@ -32,7 +32,7 @@ context('Member Types', () => {
cy.get('.umb-search-field').type('Textstring');
// Choose first item
cy.get('ul.umb-card-grid li a[title="Textstring"]').closest("li").click();
cy.get('ul.umb-card-grid li [title="Textstring"]').closest("li").click();
// Save property
cy.get('.btn-success').last().click();
@@ -16,6 +16,8 @@ namespace Umbraco.Tests.Benchmarks
// - it's faster to get+invoke the ctor
// - emitting the ctor is unless if invoked only 1
// TODO: Check out https://docs.microsoft.com/en-us/dotnet/api/system.reflection.emit.constructorbuilder?view=netcore-3.1 ?
//[Config(typeof(Config))]
[MemoryDiagnoser]
public class CtorInvokeBenchmarks
@@ -42,9 +42,9 @@ namespace Umbraco.Tests.Cache.PublishedCache
protected override void Initialize()
{
base.Initialize();
var type = new AutoPublishedContentType(22, "myType", new PublishedPropertyType[] { });
var image = new AutoPublishedContentType(23, "Image", new PublishedPropertyType[] { });
var testMediaType = new AutoPublishedContentType(24, "TestMediaType", new PublishedPropertyType[] { });
var type = new AutoPublishedContentType(Guid.NewGuid(), 22, "myType", new PublishedPropertyType[] { });
var image = new AutoPublishedContentType(Guid.NewGuid(), 23, "Image", new PublishedPropertyType[] { });
var testMediaType = new AutoPublishedContentType(Guid.NewGuid(), 24, "TestMediaType", new PublishedPropertyType[] { });
_mediaTypes = new Dictionary<string, PublishedContentType>
{
{ type.Alias, type },
@@ -275,7 +275,7 @@ AnotherContentFinder
public void GetDataEditors()
{
var types = _typeLoader.GetDataEditors();
Assert.AreEqual(39, types.Count());
Assert.AreEqual(40, types.Count());
}
/// <summary>
@@ -536,15 +536,11 @@ namespace Umbraco.Tests.LegacyXmlPublishedCache
#region Content types
public override IPublishedContentType GetContentType(int id)
{
return _contentTypeCache.Get(PublishedItemType.Content, id);
}
public override IPublishedContentType GetContentType(int id) => _contentTypeCache.Get(PublishedItemType.Content, id);
public override IPublishedContentType GetContentType(string alias)
{
return _contentTypeCache.Get(PublishedItemType.Content, alias);
}
public override IPublishedContentType GetContentType(string alias) => _contentTypeCache.Get(PublishedItemType.Content, alias);
public override IPublishedContentType GetContentType(Guid key) => _contentTypeCache.Get(PublishedItemType.Content, key);
#endregion
}
@@ -600,15 +600,11 @@ namespace Umbraco.Tests.LegacyXmlPublishedCache
#region Content types
public override IPublishedContentType GetContentType(int id)
{
return _contentTypeCache.Get(PublishedItemType.Media, id);
}
public override IPublishedContentType GetContentType(int id) => _contentTypeCache.Get(PublishedItemType.Media, id);
public override IPublishedContentType GetContentType(string alias)
{
return _contentTypeCache.Get(PublishedItemType.Media, alias);
}
public override IPublishedContentType GetContentType(string alias) => _contentTypeCache.Get(PublishedItemType.Media, alias);
public override IPublishedContentType GetContentType(Guid key) => _contentTypeCache.Get(PublishedItemType.Media, key);
public override IEnumerable<IPublishedContent> GetByContentType(IPublishedContentType contentType)
{
+9 -2
View File
@@ -448,7 +448,10 @@ namespace Umbraco.Tests.Models
[Test]
public void ContentPublishValuesWithMixedPropertyTypeVariations()
{
var propertyValidationService = new PropertyValidationService(_factory.GetInstance<PropertyEditorCollection>(), _factory.GetInstance<ServiceContext>().DataTypeService);
var propertyValidationService = new PropertyValidationService(
_factory.GetInstance<PropertyEditorCollection>(),
_factory.GetInstance<ServiceContext>().DataTypeService,
_factory.GetInstance<ServiceContext>().TextService);
const string langFr = "fr-FR";
// content type varies by Culture
@@ -580,7 +583,11 @@ namespace Umbraco.Tests.Models
prop.SetValue("a");
Assert.AreEqual("a", prop.GetValue());
Assert.IsNull(prop.GetValue(published: true));
var propertyValidationService = new PropertyValidationService(_factory.GetInstance<PropertyEditorCollection>(), _factory.GetInstance<ServiceContext>().DataTypeService);
var propertyValidationService = new PropertyValidationService(
_factory.GetInstance<PropertyEditorCollection>(),
_factory.GetInstance<ServiceContext>().DataTypeService,
_factory.GetInstance<ServiceContext>().TextService
);
Assert.IsTrue(propertyValidationService.IsPropertyValid(prop));
@@ -0,0 +1,449 @@
using Moq;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Umbraco.Core;
using Umbraco.Core.Logging;
using Umbraco.Core.Models.Blocks;
using Umbraco.Core.Models.PublishedContent;
using Umbraco.Core.PropertyEditors;
using Umbraco.Web.PropertyEditors;
using Umbraco.Web.PropertyEditors.ValueConverters;
using Umbraco.Web.PublishedCache;
namespace Umbraco.Tests.PropertyEditors
{
[TestFixture]
public class BlockListPropertyValueConverterTests
{
private readonly Guid ContentKey1 = Guid.NewGuid();
private readonly Guid ContentKey2 = Guid.NewGuid();
private const string ContentAlias1 = "Test1";
private const string ContentAlias2 = "Test2";
private readonly Guid SettingKey1 = Guid.NewGuid();
private readonly Guid SettingKey2 = Guid.NewGuid();
private const string SettingAlias1 = "Setting1";
private const string SettingAlias2 = "Setting2";
/// <summary>
/// Setup mocks for IPublishedSnapshotAccessor
/// </summary>
/// <returns></returns>
private IPublishedSnapshotAccessor GetPublishedSnapshotAccessor()
{
var test1ContentType = Mock.Of<IPublishedContentType2>(x =>
x.IsElement == true
&& x.Key == ContentKey1
&& x.Alias == ContentAlias1);
var test2ContentType = Mock.Of<IPublishedContentType2>(x =>
x.IsElement == true
&& x.Key == ContentKey2
&& x.Alias == ContentAlias2);
var test3ContentType = Mock.Of<IPublishedContentType2>(x =>
x.IsElement == true
&& x.Key == SettingKey1
&& x.Alias == SettingAlias1);
var test4ContentType = Mock.Of<IPublishedContentType2>(x =>
x.IsElement == true
&& x.Key == SettingKey2
&& x.Alias == SettingAlias2);
var contentCache = new Mock<IPublishedContentCache>();
contentCache.Setup(x => x.GetContentType(ContentKey1)).Returns(test1ContentType);
contentCache.Setup(x => x.GetContentType(ContentKey2)).Returns(test2ContentType);
contentCache.Setup(x => x.GetContentType(SettingKey1)).Returns(test3ContentType);
contentCache.Setup(x => x.GetContentType(SettingKey2)).Returns(test4ContentType);
var publishedSnapshot = Mock.Of<IPublishedSnapshot>(x => x.Content == contentCache.Object);
var publishedSnapshotAccessor = Mock.Of<IPublishedSnapshotAccessor>(x => x.PublishedSnapshot == publishedSnapshot);
return publishedSnapshotAccessor;
}
private BlockListPropertyValueConverter CreateConverter()
{
var publishedSnapshotAccessor = GetPublishedSnapshotAccessor();
var publishedModelFactory = new NoopPublishedModelFactory();
var editor = new BlockListPropertyValueConverter(
Mock.Of<IProfilingLogger>(),
new BlockEditorConverter(publishedSnapshotAccessor, publishedModelFactory));
return editor;
}
private BlockListConfiguration ConfigForMany() => new BlockListConfiguration
{
Blocks = new[] {
new BlockListConfiguration.BlockConfiguration
{
ContentElementTypeKey = ContentKey1,
SettingsElementTypeKey = SettingKey2
},
new BlockListConfiguration.BlockConfiguration
{
ContentElementTypeKey = ContentKey2,
SettingsElementTypeKey = SettingKey1
}
}
};
private BlockListConfiguration ConfigForSingle() => new BlockListConfiguration
{
Blocks = new[] {
new BlockListConfiguration.BlockConfiguration
{
ContentElementTypeKey = ContentKey1
}
}
};
private IPublishedPropertyType GetPropertyType(BlockListConfiguration config)
{
var dataType = new PublishedDataType(1, "test", new Lazy<object>(() => config));
var propertyType = Mock.Of<IPublishedPropertyType>(x =>
x.EditorAlias == Constants.PropertyEditors.Aliases.BlockList
&& x.DataType == dataType);
return propertyType;
}
[Test]
public void Is_Converter_For()
{
var editor = CreateConverter();
Assert.IsTrue(editor.IsConverter(Mock.Of<IPublishedPropertyType>(x => x.EditorAlias == Constants.PropertyEditors.Aliases.BlockList)));
Assert.IsFalse(editor.IsConverter(Mock.Of<IPublishedPropertyType>(x => x.EditorAlias == Constants.PropertyEditors.Aliases.NestedContent)));
}
[Test]
public void Get_Value_Type_Multiple()
{
var editor = CreateConverter();
var config = ConfigForMany();
var dataType = new PublishedDataType(1, "test", new Lazy<object>(() => config));
var propType = Mock.Of<IPublishedPropertyType>(x => x.DataType == dataType);
var valueType = editor.GetPropertyValueType(propType);
// the result is always block list model
Assert.AreEqual(typeof(BlockListModel), valueType);
}
[Test]
public void Get_Value_Type_Single()
{
var editor = CreateConverter();
var config = ConfigForSingle();
var dataType = new PublishedDataType(1, "test", new Lazy<object>(() => config));
var propType = Mock.Of<IPublishedPropertyType>(x => x.DataType == dataType);
var valueType = editor.GetPropertyValueType(propType);
// the result is always block list model
Assert.AreEqual(typeof(BlockListModel), valueType);
}
[Test]
public void Convert_Null_Empty()
{
var editor = CreateConverter();
var config = ConfigForMany();
var propertyType = GetPropertyType(config);
var publishedElement = Mock.Of<IPublishedElement>();
string json = null;
var converted = editor.ConvertIntermediateToObject(publishedElement, propertyType, PropertyCacheLevel.None, json, false) as BlockListModel;
Assert.IsNotNull(converted);
Assert.AreEqual(0, converted.ContentData.Count());
Assert.AreEqual(0, converted.Layout.Count());
json = string.Empty;
converted = editor.ConvertIntermediateToObject(publishedElement, propertyType, PropertyCacheLevel.None, json, false) as BlockListModel;
Assert.IsNotNull(converted);
Assert.AreEqual(0, converted.ContentData.Count());
Assert.AreEqual(0, converted.Layout.Count());
}
[Test]
public void Convert_Valid_Empty_Json()
{
var editor = CreateConverter();
var config = ConfigForMany();
var propertyType = GetPropertyType(config);
var publishedElement = Mock.Of<IPublishedElement>();
var json = "{}";
var converted = editor.ConvertIntermediateToObject(publishedElement, propertyType, PropertyCacheLevel.None, json, false) as BlockListModel;
Assert.IsNotNull(converted);
Assert.AreEqual(0, converted.ContentData.Count());
Assert.AreEqual(0, converted.Layout.Count());
json = @"{
layout: {},
data: []}";
converted = editor.ConvertIntermediateToObject(publishedElement, propertyType, PropertyCacheLevel.None, json, false) as BlockListModel;
Assert.IsNotNull(converted);
Assert.AreEqual(0, converted.ContentData.Count());
Assert.AreEqual(0, converted.Layout.Count());
// Even though there is a layout, there is no data, so the conversion will result in zero elements in total
json = @"
{
layout: {
'" + Constants.PropertyEditors.Aliases.BlockList + @"': [
{
'contentUdi': 'umb://element/e7dba547615b4e9ab4ab2a7674845bc9'
}
]
},
contentData: []
}";
converted = editor.ConvertIntermediateToObject(publishedElement, propertyType, PropertyCacheLevel.None, json, false) as BlockListModel;
Assert.IsNotNull(converted);
Assert.AreEqual(0, converted.ContentData.Count());
Assert.AreEqual(0, converted.Layout.Count());
// Even though there is a layout and data, the data is invalid (missing required keys) so the conversion will result in zero elements in total
json = @"
{
layout: {
'" + Constants.PropertyEditors.Aliases.BlockList + @"': [
{
'contentUdi': 'umb://element/e7dba547615b4e9ab4ab2a7674845bc9'
}
]
},
contentData: [
{
'udi': 'umb://element/e7dba547615b4e9ab4ab2a7674845bc9'
}
]
}";
converted = editor.ConvertIntermediateToObject(publishedElement, propertyType, PropertyCacheLevel.None, json, false) as BlockListModel;
Assert.IsNotNull(converted);
Assert.AreEqual(0, converted.ContentData.Count());
Assert.AreEqual(0, converted.Layout.Count());
// Everthing is ok except the udi reference in the layout doesn't match the data so it will be empty
json = @"
{
layout: {
'" + Constants.PropertyEditors.Aliases.BlockList + @"': [
{
'contentUdi': 'umb://element/1304E1DDAC87439684FE8A399231CB3D'
}
]
},
contentData: [
{
'contentTypeKey': '" + ContentKey1 + @"',
'key': '1304E1DD-0000-4396-84FE-8A399231CB3D'
}
]
}";
converted = editor.ConvertIntermediateToObject(publishedElement, propertyType, PropertyCacheLevel.None, json, false) as BlockListModel;
Assert.IsNotNull(converted);
Assert.AreEqual(1, converted.ContentData.Count());
Assert.AreEqual(0, converted.Layout.Count());
}
[Test]
public void Convert_Valid_Json()
{
var editor = CreateConverter();
var config = ConfigForMany();
var propertyType = GetPropertyType(config);
var publishedElement = Mock.Of<IPublishedElement>();
var json = @"
{
layout: {
'" + Constants.PropertyEditors.Aliases.BlockList + @"': [
{
'contentUdi': 'umb://element/1304E1DDAC87439684FE8A399231CB3D'
}
]
},
contentData: [
{
'contentTypeKey': '" + ContentKey1 + @"',
'udi': 'umb://element/1304E1DDAC87439684FE8A399231CB3D'
}
]
}";
var converted = editor.ConvertIntermediateToObject(publishedElement, propertyType, PropertyCacheLevel.None, json, false) as BlockListModel;
Assert.IsNotNull(converted);
Assert.AreEqual(1, converted.ContentData.Count());
var item0 = converted.ContentData.ElementAt(0);
Assert.AreEqual(Guid.Parse("1304E1DD-AC87-4396-84FE-8A399231CB3D"), item0.Key);
Assert.AreEqual("Test1", item0.ContentType.Alias);
Assert.AreEqual(1, converted.Layout.Count());
var layout0 = converted.Layout.ElementAt(0);
Assert.IsNull(layout0.Settings);
Assert.AreEqual(UdiParser.Parse("umb://element/1304E1DDAC87439684FE8A399231CB3D"), layout0.ContentUdi);
}
[Test]
public void Get_Data_From_Layout_Item()
{
var editor = CreateConverter();
var config = ConfigForMany();
var propertyType = GetPropertyType(config);
var publishedElement = Mock.Of<IPublishedElement>();
var json = @"
{
layout: {
'" + Constants.PropertyEditors.Aliases.BlockList + @"': [
{
'contentUdi': 'umb://element/1304E1DDAC87439684FE8A399231CB3D',
'settingsUdi': 'umb://element/1F613E26CE274898908A561437AF5100'
},
{
'contentUdi': 'umb://element/0A4A416E547D464FABCC6F345C17809A',
'settingsUdi': 'umb://element/63027539B0DB45E7B70459762D4E83DD'
}
]
},
contentData: [
{
'contentTypeKey': '" + ContentKey1 + @"',
'udi': 'umb://element/1304E1DDAC87439684FE8A399231CB3D'
},
{
'contentTypeKey': '" + ContentKey2 + @"',
'udi': 'umb://element/E05A034704424AB3A520E048E6197E79'
},
{
'contentTypeKey': '" + ContentKey2 + @"',
'udi': 'umb://element/0A4A416E547D464FABCC6F345C17809A'
}
],
settingsData: [
{
'contentTypeKey': '" + SettingKey1 + @"',
'udi': 'umb://element/63027539B0DB45E7B70459762D4E83DD'
},
{
'contentTypeKey': '" + SettingKey2 + @"',
'udi': 'umb://element/1F613E26CE274898908A561437AF5100'
},
{
'contentTypeKey': '" + SettingKey2 + @"',
'udi': 'umb://element/BCF4BA3DA40C496C93EC58FAC85F18B9'
}
],
}";
var converted = editor.ConvertIntermediateToObject(publishedElement, propertyType, PropertyCacheLevel.None, json, false) as BlockListModel;
Assert.IsNotNull(converted);
Assert.AreEqual(3, converted.ContentData.Count());
Assert.AreEqual(3, converted.SettingsData.Count());
Assert.AreEqual(2, converted.Layout.Count());
var item0 = converted.Layout.ElementAt(0);
Assert.AreEqual(Guid.Parse("1304E1DD-AC87-4396-84FE-8A399231CB3D"), item0.Content.Key);
Assert.AreEqual("Test1", item0.Content.ContentType.Alias);
Assert.AreEqual(Guid.Parse("1F613E26CE274898908A561437AF5100"), item0.Settings.Key);
Assert.AreEqual("Setting2", item0.Settings.ContentType.Alias);
var item1 = converted.Layout.ElementAt(1);
Assert.AreEqual(Guid.Parse("0A4A416E-547D-464F-ABCC-6F345C17809A"), item1.Content.Key);
Assert.AreEqual("Test2", item1.Content.ContentType.Alias);
Assert.AreEqual(Guid.Parse("63027539B0DB45E7B70459762D4E83DD"), item1.Settings.Key);
Assert.AreEqual("Setting1", item1.Settings.ContentType.Alias);
}
[Test]
public void Data_Item_Removed_If_Removed_From_Config()
{
var editor = CreateConverter();
// The data below expects that ContentKey1 + ContentKey2 + SettingsKey1 + SettingsKey2 exist but only ContentKey2 exists so
// the data should all be filtered.
var config = new BlockListConfiguration
{
Blocks = new[] {
new BlockListConfiguration.BlockConfiguration
{
ContentElementTypeKey = ContentKey2,
SettingsElementTypeKey = null
}
}
};
var propertyType = GetPropertyType(config);
var publishedElement = Mock.Of<IPublishedElement>();
var json = @"
{
layout: {
'" + Constants.PropertyEditors.Aliases.BlockList + @"': [
{
'contentUdi': 'umb://element/1304E1DDAC87439684FE8A399231CB3D',
'settingsUdi': 'umb://element/1F613E26CE274898908A561437AF5100'
},
{
'contentUdi': 'umb://element/0A4A416E547D464FABCC6F345C17809A',
'settingsUdi': 'umb://element/63027539B0DB45E7B70459762D4E83DD'
}
]
},
contentData: [
{
'contentTypeKey': '" + ContentKey1 + @"',
'udi': 'umb://element/1304E1DDAC87439684FE8A399231CB3D'
},
{
'contentTypeKey': '" + ContentKey2 + @"',
'udi': 'umb://element/E05A034704424AB3A520E048E6197E79'
},
{
'contentTypeKey': '" + ContentKey2 + @"',
'udi': 'umb://element/0A4A416E547D464FABCC6F345C17809A'
}
],
settingsData: [
{
'contentTypeKey': '" + SettingKey1 + @"',
'udi': 'umb://element/63027539B0DB45E7B70459762D4E83DD'
},
{
'contentTypeKey': '" + SettingKey2 + @"',
'udi': 'umb://element/1F613E26CE274898908A561437AF5100'
},
{
'contentTypeKey': '" + SettingKey2 + @"',
'udi': 'umb://element/BCF4BA3DA40C496C93EC58FAC85F18B9'
}
],
}";
var converted = editor.ConvertIntermediateToObject(publishedElement, propertyType, PropertyCacheLevel.None, json, false) as BlockListModel;
Assert.IsNotNull(converted);
Assert.AreEqual(2, converted.ContentData.Count());
Assert.AreEqual(0, converted.SettingsData.Count());
Assert.AreEqual(1, converted.Layout.Count());
var item0 = converted.Layout.ElementAt(0);
Assert.AreEqual(Guid.Parse("0A4A416E-547D-464F-ABCC-6F345C17809A"), item0.Content.Key);
Assert.AreEqual("Test2", item0.Content.ContentType.Alias);
Assert.IsNull(item0.Settings);
}
}
}
@@ -95,7 +95,7 @@ namespace Umbraco.Tests.PropertyEditors
})));
var publishedPropType = new PublishedPropertyType(
new PublishedContentType(1234, "test", PublishedItemType.Content, Enumerable.Empty<string>(), Enumerable.Empty<PublishedPropertyType>(), ContentVariation.Nothing),
new PublishedContentType(Guid.NewGuid(),1234, "test", PublishedItemType.Content, Enumerable.Empty<string>(), Enumerable.Empty<PublishedPropertyType>(), ContentVariation.Nothing),
new PropertyType(TestHelper.ShortStringHelper, "test", ValueStorageType.Nvarchar) { DataTypeId = 123 },
new PropertyValueConverterCollection(Enumerable.Empty<IPropertyValueConverter>()),
Mock.Of<IPublishedModelFactory>(), mockPublishedContentTypeFactory.Object);
+21 -21
View File
@@ -44,7 +44,7 @@ namespace Umbraco.Tests.Published
yield return contentTypeFactory.CreatePropertyType(contentType, "prop1", 1);
}
var elementType1 = contentTypeFactory.CreateContentType(1000, "element1", CreatePropertyTypes);
var elementType1 = contentTypeFactory.CreateContentType(Guid.NewGuid(), 1000, "element1", CreatePropertyTypes);
var element1 = new PublishedElement(elementType1, Guid.NewGuid(), new Dictionary<string, object> { { "prop1", "1234" } }, false);
@@ -78,7 +78,7 @@ namespace Umbraco.Tests.Published
=> propertyType.EditorAlias.InvariantEquals("Umbraco.Void");
public Type GetPropertyValueType(IPublishedPropertyType propertyType)
=> typeof (int);
=> typeof(int);
public PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType)
=> PropertyCacheLevel.Element;
@@ -87,10 +87,10 @@ namespace Umbraco.Tests.Published
=> int.TryParse(source as string, out int i) ? i : 0;
public object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview)
=> (int) inter;
=> (int)inter;
public object ConvertIntermediateToXPath(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview)
=> ((int) inter).ToString();
=> ((int)inter).ToString();
}
#endregion
@@ -124,11 +124,11 @@ namespace Umbraco.Tests.Published
yield return contentTypeFactory.CreatePropertyType(contentType, "prop1", 1);
}
var elementType1 = contentTypeFactory.CreateContentType(1000, "element1", CreatePropertyTypes);
var elementType1 = contentTypeFactory.CreateContentType(Guid.NewGuid(), 1000, "element1", CreatePropertyTypes);
var element1 = new PublishedElement(elementType1, Guid.NewGuid(), new Dictionary<string, object> { { "prop1", "1234" } }, false);
var cntType1 = contentTypeFactory.CreateContentType(1001, "cnt1", t => Enumerable.Empty<PublishedPropertyType>());
var cntType1 = contentTypeFactory.CreateContentType(Guid.NewGuid(), 1001, "cnt1", t => Enumerable.Empty<PublishedPropertyType>());
var cnt1 = new SolidPublishedContent(cntType1) { Id = 1234 };
cacheContent[cnt1.Id] = cnt1;
@@ -147,7 +147,7 @@ namespace Umbraco.Tests.Published
}
public bool? IsValue(object value, PropertyValueLevel level)
=> value != null && (!(value is string) || string.IsNullOrWhiteSpace((string) value) == false);
=> value != null && (!(value is string) || string.IsNullOrWhiteSpace((string)value) == false);
public bool IsConverter(IPublishedPropertyType propertyType)
=> propertyType.EditorAlias.InvariantEquals("Umbraco.Void");
@@ -166,10 +166,10 @@ namespace Umbraco.Tests.Published
=> int.TryParse(source as string, out int i) ? i : -1;
public object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview)
=> _publishedSnapshotAccessor.PublishedSnapshot.Content.GetById((int) inter);
=> _publishedSnapshotAccessor.PublishedSnapshot.Content.GetById((int)inter);
public object ConvertIntermediateToXPath(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview)
=> ((int) inter).ToString();
=> ((int)inter).ToString();
}
#endregion
@@ -219,10 +219,10 @@ namespace Umbraco.Tests.Published
yield return contentTypeFactory.CreatePropertyType(contentType, "prop" + i, i);
}
var elementType1 = contentTypeFactory.CreateContentType(1000, "element1", t => CreatePropertyTypes(t, 1));
var elementType2 = contentTypeFactory.CreateContentType(1001, "element2", t => CreatePropertyTypes(t, 2));
var contentType1 = contentTypeFactory.CreateContentType(1002, "content1", t => CreatePropertyTypes(t, 1));
var contentType2 = contentTypeFactory.CreateContentType(1003, "content2", t => CreatePropertyTypes(t, 2));
var elementType1 = contentTypeFactory.CreateContentType(Guid.NewGuid(), 1000, "element1", t => CreatePropertyTypes(t, 1));
var elementType2 = contentTypeFactory.CreateContentType(Guid.NewGuid(), 1001, "element2", t => CreatePropertyTypes(t, 2));
var contentType1 = contentTypeFactory.CreateContentType(Guid.NewGuid(), 1002, "content1", t => CreatePropertyTypes(t, 1));
var contentType2 = contentTypeFactory.CreateContentType(Guid.NewGuid(), 1003, "content2", t => CreatePropertyTypes(t, 2));
var element1 = new PublishedElement(elementType1, Guid.NewGuid(), new Dictionary<string, object> { { "prop1", "val1" } }, false);
var element2 = new PublishedElement(elementType2, Guid.NewGuid(), new Dictionary<string, object> { { "prop2", "1003" } }, false);
@@ -244,22 +244,22 @@ namespace Umbraco.Tests.Published
// can get the actual property Clr type
// ie ModelType gets properly mapped by IPublishedContentModelFactory
// must test ModelClrType with special equals 'cos they are not ref-equals
Assert.IsTrue(ModelType.Equals(typeof (IEnumerable<>).MakeGenericType(ModelType.For("content1")), contentType2.GetPropertyType("prop2").ModelClrType));
Assert.AreEqual(typeof (IEnumerable<PublishedSnapshotTestObjects.TestContentModel1>), contentType2.GetPropertyType("prop2").ClrType);
Assert.IsTrue(ModelType.Equals(typeof(IEnumerable<>).MakeGenericType(ModelType.For("content1")), contentType2.GetPropertyType("prop2").ModelClrType));
Assert.AreEqual(typeof(IEnumerable<PublishedSnapshotTestObjects.TestContentModel1>), contentType2.GetPropertyType("prop2").ClrType);
// can create a model for an element
var model1 = factory.CreateModel(element1);
Assert.IsInstanceOf<PublishedSnapshotTestObjects.TestElementModel1>(model1);
Assert.AreEqual("val1", ((PublishedSnapshotTestObjects.TestElementModel1) model1).Prop1);
Assert.AreEqual("val1", ((PublishedSnapshotTestObjects.TestElementModel1)model1).Prop1);
// can create a model for a published content
var model2 = factory.CreateModel(element2);
Assert.IsInstanceOf<PublishedSnapshotTestObjects.TestElementModel2>(model2);
var mmodel2 = (PublishedSnapshotTestObjects.TestElementModel2) model2;
var mmodel2 = (PublishedSnapshotTestObjects.TestElementModel2)model2;
// and get direct property
Assert.IsInstanceOf<PublishedSnapshotTestObjects.TestContentModel1[]>(model2.Value("prop2"));
Assert.AreEqual(1, ((PublishedSnapshotTestObjects.TestContentModel1[]) model2.Value("prop2")).Length);
Assert.AreEqual(1, ((PublishedSnapshotTestObjects.TestContentModel1[])model2.Value("prop2")).Length);
// and get model property
Assert.IsInstanceOf<IEnumerable<PublishedSnapshotTestObjects.TestContentModel1>>(mmodel2.Prop2);
@@ -276,7 +276,7 @@ namespace Umbraco.Tests.Published
=> propertyType.EditorAlias == "Umbraco.Void";
public override Type GetPropertyValueType(IPublishedPropertyType propertyType)
=> typeof (string);
=> typeof(string);
public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType)
=> PropertyCacheLevel.Element;
@@ -295,7 +295,7 @@ namespace Umbraco.Tests.Published
=> propertyType.EditorAlias == "Umbraco.Void.2";
public override Type GetPropertyValueType(IPublishedPropertyType propertyType)
=> typeof (IEnumerable<>).MakeGenericType(ModelType.For("content1"));
=> typeof(IEnumerable<>).MakeGenericType(ModelType.For("content1"));
public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType)
=> PropertyCacheLevel.Elements;
@@ -308,7 +308,7 @@ namespace Umbraco.Tests.Published
public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview)
{
return ((int[]) inter).Select(x => (PublishedSnapshotTestObjects.TestContentModel1) _publishedSnapshotAccessor.PublishedSnapshot.Content.GetById(x)).ToArray();
return ((int[])inter).Select(x => (PublishedSnapshotTestObjects.TestContentModel1)_publishedSnapshotAccessor.PublishedSnapshot.Content.GetById(x)).ToArray();
}
}
@@ -98,8 +98,8 @@ namespace Umbraco.Tests.Published
.Returns((string alias) =>
{
return alias == "contentN1"
? (IList) new List<TestElementModel>()
: (IList) new List<IPublishedElement>();
? (IList)new List<TestElementModel>()
: (IList)new List<IPublishedElement>();
});
var contentCache = new Mock<IPublishedContentCache>();
@@ -140,9 +140,9 @@ namespace Umbraco.Tests.Published
yield return factory.CreatePropertyType(contentType, "propertyN1", 3);
}
var contentType1 = factory.CreateContentType(1, "content1", CreatePropertyTypes1);
var contentType2 = factory.CreateContentType(2, "content2", CreatePropertyTypes2);
var contentTypeN1 = factory.CreateContentType(2, "contentN1", CreatePropertyTypesN1, isElement: true);
var contentType1 = factory.CreateContentType(Guid.NewGuid(), 1, "content1", CreatePropertyTypes1);
var contentType2 = factory.CreateContentType(Guid.NewGuid(), 2, "content2", CreatePropertyTypes2);
var contentTypeN1 = factory.CreateContentType(Guid.NewGuid(), 2, "contentN1", CreatePropertyTypesN1, isElement: true);
// mocked content cache returns content types
contentCache
@@ -162,7 +162,7 @@ namespace Umbraco.Tests.Published
(var contentType1, _) = CreateContentTypes();
// nested single converter returns the proper value clr type TestModel, and cache level
Assert.AreEqual(typeof (TestElementModel), contentType1.GetPropertyType("property1").ClrType);
Assert.AreEqual(typeof(TestElementModel), contentType1.GetPropertyType("property1").ClrType);
Assert.AreEqual(PropertyCacheLevel.Element, contentType1.GetPropertyType("property1").CacheLevel);
var key = Guid.NewGuid();
@@ -170,7 +170,7 @@ namespace Umbraco.Tests.Published
var content = new SolidPublishedContent(contentType1)
{
Key = key,
Properties = new []
Properties = new[]
{
new TestPublishedProperty(contentType1.GetPropertyType("property1"), $@"[
{{ ""key"": ""{keyA}"", ""propertyN1"": ""foo"", ""ncContentTypeAlias"": ""contentN1"" }}
@@ -181,7 +181,7 @@ namespace Umbraco.Tests.Published
// nested single converter returns proper TestModel value
Assert.IsInstanceOf<TestElementModel>(value);
var valueM = (TestElementModel) value;
var valueM = (TestElementModel)value;
Assert.AreEqual("foo", valueM.PropValue);
Assert.AreEqual(keyA, valueM.Key);
}
@@ -192,7 +192,7 @@ namespace Umbraco.Tests.Published
(_, var contentType2) = CreateContentTypes();
// nested many converter returns the proper value clr type IEnumerable<TestModel>, and cache level
Assert.AreEqual(typeof (IEnumerable<TestElementModel>), contentType2.GetPropertyType("property2").ClrType);
Assert.AreEqual(typeof(IEnumerable<TestElementModel>), contentType2.GetPropertyType("property2").ClrType);
Assert.AreEqual(PropertyCacheLevel.Element, contentType2.GetPropertyType("property2").CacheLevel);
var key = Guid.NewGuid();
@@ -214,7 +214,7 @@ namespace Umbraco.Tests.Published
// nested many converter returns proper IEnumerable<TestModel> value
Assert.IsInstanceOf<IEnumerable<IPublishedElement>>(value);
Assert.IsInstanceOf<IEnumerable<TestElementModel>>(value);
var valueM = ((IEnumerable<TestElementModel>) value).ToArray();
var valueM = ((IEnumerable<TestElementModel>)value).ToArray();
Assert.AreEqual("foo", valueM[0].PropValue);
Assert.AreEqual(keyA, valueM[0].Key);
Assert.AreEqual("bar", valueM[1].PropValue);
@@ -44,7 +44,7 @@ namespace Umbraco.Tests.Published
yield return publishedContentTypeFactory.CreatePropertyType(contentType, "prop1", 1);
}
var setType1 = publishedContentTypeFactory.CreateContentType(1000, "set1", CreatePropertyTypes);
var setType1 = publishedContentTypeFactory.CreateContentType(Guid.NewGuid(), 1000, "set1", CreatePropertyTypes);
// PublishedElementPropertyBase.GetCacheLevels:
//
@@ -125,7 +125,7 @@ namespace Umbraco.Tests.Published
yield return publishedContentTypeFactory.CreatePropertyType(contentType, "prop1", 1);
}
var setType1 = publishedContentTypeFactory.CreateContentType(1000, "set1", CreatePropertyTypes);
var setType1 = publishedContentTypeFactory.CreateContentType(Guid.NewGuid(), 1000, "set1", CreatePropertyTypes);
var elementsCache = new FastDictionaryAppCache();
var snapshotCache = new FastDictionaryAppCache();
@@ -202,7 +202,7 @@ namespace Umbraco.Tests.Published
yield return publishedContentTypeFactory.CreatePropertyType(contentType, "prop1", 1);
}
var setType1 = publishedContentTypeFactory.CreateContentType(1000, "set1", CreatePropertyTypes);
var setType1 = publishedContentTypeFactory.CreateContentType(Guid.NewGuid(), 1000, "set1", CreatePropertyTypes);
Assert.Throws<Exception>(() =>
{
@@ -100,7 +100,7 @@ namespace Umbraco.Tests.PublishedContent
var doc = GetContent(true, 1);
//change a doc type alias
var c = (SolidPublishedContent)doc.Children.ElementAt(0);
c.ContentType = new PublishedContentType(22, "DontMatch", PublishedItemType.Content, Enumerable.Empty<string>(), Enumerable.Empty<PublishedPropertyType>(), ContentVariation.Nothing);
c.ContentType = new PublishedContentType(Guid.NewGuid(), 22, "DontMatch", PublishedItemType.Content, Enumerable.Empty<string>(), Enumerable.Empty<PublishedPropertyType>(), ContentVariation.Nothing);
var dt = doc.ChildrenAsTable(ServiceContext, "Child");
@@ -131,7 +131,7 @@ namespace Umbraco.Tests.PublishedContent
var factory = new PublishedContentTypeFactory(Mock.Of<IPublishedModelFactory>(), new PropertyValueConverterCollection(Array.Empty<IPropertyValueConverter>()), dataTypeService);
var contentTypeAlias = createChildren ? "Parent" : "Child";
var contentType = new PublishedContentType(22, contentTypeAlias, PublishedItemType.Content, Enumerable.Empty<string>(), Enumerable.Empty<PublishedPropertyType>(), ContentVariation.Nothing);
var contentType = new PublishedContentType(Guid.NewGuid(), 22, contentTypeAlias, PublishedItemType.Content, Enumerable.Empty<string>(), Enumerable.Empty<PublishedPropertyType>(), ContentVariation.Nothing);
var d = new SolidPublishedContent(contentType)
{
CreateDate = DateTime.Now,
@@ -75,14 +75,14 @@ namespace Umbraco.Tests.PublishedContent
yield return factory.CreatePropertyType(contentType, "noprop", 1, variations: ContentVariation.Culture);
}
var contentType1 = factory.CreateContentType(1, "ContentType1", Enumerable.Empty<string>(), CreatePropertyTypes1);
var contentType1 = factory.CreateContentType(Guid.NewGuid(), 1, "ContentType1", Enumerable.Empty<string>(), CreatePropertyTypes1);
IEnumerable<IPublishedPropertyType> CreatePropertyTypes2(IPublishedContentType contentType)
{
yield return factory.CreatePropertyType(contentType, "prop3", 1, variations: ContentVariation.Culture);
}
var contentType2 = factory.CreateContentType(2, "contentType2", Enumerable.Empty<string>(), CreatePropertyTypes2);
var contentType2 = factory.CreateContentType(Guid.NewGuid(), 2, "contentType2", Enumerable.Empty<string>(), CreatePropertyTypes2);
var prop1 = new SolidPublishedPropertyWithLanguageVariants
{
@@ -9,6 +9,7 @@ using Umbraco.Tests.Testing;
using Umbraco.Web.Composing;
using Moq;
using Examine;
using System;
namespace Umbraco.Tests.PublishedContent
{
@@ -23,9 +24,9 @@ namespace Umbraco.Tests.PublishedContent
yield return factory.CreatePropertyType(contentType, "prop1", 1);
}
var contentType1 = factory.CreateContentType(1, "ContentType1", Enumerable.Empty<string>(), CreatePropertyTypes);
var contentType2 = factory.CreateContentType(2, "ContentType2", Enumerable.Empty<string>(), CreatePropertyTypes);
var contentType2Sub = factory.CreateContentType(3, "ContentType2Sub", Enumerable.Empty<string>(), CreatePropertyTypes);
var contentType1 = factory.CreateContentType(Guid.NewGuid(), 1, "ContentType1", Enumerable.Empty<string>(), CreatePropertyTypes);
var contentType2 = factory.CreateContentType(Guid.NewGuid(), 2, "ContentType2", Enumerable.Empty<string>(), CreatePropertyTypes);
var contentType2Sub = factory.CreateContentType(Guid.NewGuid(), 3, "ContentType2Sub", Enumerable.Empty<string>(), CreatePropertyTypes);
var content = new SolidPublishedContent(contentType1)
{
@@ -5,18 +5,16 @@ using Umbraco.Core.PropertyEditors;
using Umbraco.Core.PropertyEditors.ValueConverters;
using Umbraco.Tests.TestHelpers;
using Moq;
using Umbraco.Core.Composing;
using Umbraco.Core.IO;
using Umbraco.Core.Logging;
using Umbraco.Core.Models;
using Umbraco.Web.PropertyEditors;
using Umbraco.Core.Services;
using Umbraco.Core.Strings;
using Umbraco.Web;
using Umbraco.Web.Templates;
using Umbraco.Web.Models;
using Umbraco.Web.Routing;
using Umbraco.Core.Media;
using System;
namespace Umbraco.Tests.PublishedContent
{
@@ -73,7 +71,7 @@ namespace Umbraco.Tests.PublishedContent
yield return publishedContentTypeFactory.CreatePropertyType(contentType, "content", 1);
}
var type = new AutoPublishedContentType(0, "anything", CreatePropertyTypes);
var type = new AutoPublishedContentType(Guid.NewGuid(), 0, "anything", CreatePropertyTypes);
ContentTypesCache.GetPublishedContentTypeByAlias = alias => type;
var umbracoContext = GetUmbracoContext("/test");
@@ -89,8 +89,8 @@ namespace Umbraco.Tests.PublishedContent
}
var compositionAliases = new[] { "MyCompositionAlias" };
var anythingType = new AutoPublishedContentType(0, "anything", compositionAliases, CreatePropertyTypes);
var homeType = new AutoPublishedContentType(0, "home", compositionAliases, CreatePropertyTypes);
var anythingType = new AutoPublishedContentType(Guid.NewGuid(), 0, "anything", compositionAliases, CreatePropertyTypes);
var homeType = new AutoPublishedContentType(Guid.NewGuid(), 0, "home", compositionAliases, CreatePropertyTypes);
ContentTypesCache.GetPublishedContentTypeByAlias = alias => alias.InvariantEquals("home") ? homeType : anythingType;
}
@@ -404,8 +404,8 @@ namespace Umbraco.Tests.PublishedContent
[Test]
public void Children_GroupBy_DocumentTypeAlias()
{
var home = new AutoPublishedContentType(22, "Home", new PublishedPropertyType[] { });
var custom = new AutoPublishedContentType(23, "CustomDocument", new PublishedPropertyType[] { });
var home = new AutoPublishedContentType(Guid.NewGuid(), 22, "Home", new PublishedPropertyType[] { });
var custom = new AutoPublishedContentType(Guid.NewGuid(), 23, "CustomDocument", new PublishedPropertyType[] { });
var contentTypes = new Dictionary<string, PublishedContentType>
{
{ home.Alias, home },
@@ -425,8 +425,8 @@ namespace Umbraco.Tests.PublishedContent
[Test]
public void Children_Where_DocumentTypeAlias()
{
var home = new AutoPublishedContentType(22, "Home", new PublishedPropertyType[] { });
var custom = new AutoPublishedContentType(23, "CustomDocument", new PublishedPropertyType[] { });
var home = new AutoPublishedContentType(Guid.NewGuid(), 22, "Home", new PublishedPropertyType[] { });
var custom = new AutoPublishedContentType(Guid.NewGuid(), 23, "CustomDocument", new PublishedPropertyType[] { });
var contentTypes = new Dictionary<string, PublishedContentType>
{
{ home.Alias, home },
@@ -909,7 +909,7 @@ namespace Umbraco.Tests.PublishedContent
yield return factory.CreatePropertyType(contentType, "detached", 1003);
}
var ct = factory.CreateContentType(0, "alias", CreatePropertyTypes);
var ct = factory.CreateContentType(Guid.NewGuid(), 0, "alias", CreatePropertyTypes);
var pt = ct.GetPropertyType("detached");
var prop = new PublishedElementPropertyBase(pt, null, false, PropertyCacheLevel.None, 5548);
Assert.IsInstanceOf<int>(prop.GetValue());
@@ -941,7 +941,7 @@ namespace Umbraco.Tests.PublishedContent
var guid = Guid.NewGuid();
var ct = factory.CreateContentType(0, "alias", CreatePropertyTypes);
var ct = factory.CreateContentType(Guid.NewGuid(), 0, "alias", CreatePropertyTypes);
var c = new ImageWithLegendModel(ct, guid, new Dictionary<string, object>
{
@@ -49,7 +49,7 @@ namespace Umbraco.Tests.PublishedContent
pc.Setup(content => content.Path).Returns("-1,1");
pc.Setup(content => content.Parent).Returns(() => null);
pc.Setup(content => content.Properties).Returns(new Collection<IPublishedProperty>());
pc.Setup(content => content.ContentType).Returns(new PublishedContentType(22, "anything", PublishedItemType.Content, Enumerable.Empty<string>(), Enumerable.Empty<PublishedPropertyType>(), ContentVariation.Nothing));
pc.Setup(content => content.ContentType).Returns(new PublishedContentType(Guid.NewGuid(), 22, "anything", PublishedItemType.Content, Enumerable.Empty<string>(), Enumerable.Empty<PublishedPropertyType>(), ContentVariation.Nothing));
return pc;
}
}
@@ -153,6 +153,11 @@ namespace Umbraco.Tests.PublishedContent
throw new NotImplementedException();
}
public override IPublishedContentType GetContentType(Guid key)
{
throw new NotImplementedException();
}
public override IEnumerable<IPublishedContent> GetByContentType(IPublishedContentType contentType)
{
throw new NotImplementedException();
@@ -376,7 +381,7 @@ namespace Umbraco.Tests.PublishedContent
#endregion
}
class PublishedContentStrong1 : PublishedContentModel
internal class PublishedContentStrong1 : PublishedContentModel
{
public PublishedContentStrong1(IPublishedContent content)
: base(content)
@@ -385,7 +390,7 @@ namespace Umbraco.Tests.PublishedContent
public int StrongValue => this.Value<int>("strongValue");
}
class PublishedContentStrong1Sub : PublishedContentStrong1
internal class PublishedContentStrong1Sub : PublishedContentStrong1
{
public PublishedContentStrong1Sub(IPublishedContent content)
: base(content)
@@ -394,7 +399,7 @@ namespace Umbraco.Tests.PublishedContent
public int AnotherValue => this.Value<int>("anotherValue");
}
class PublishedContentStrong2 : PublishedContentModel
internal class PublishedContentStrong2 : PublishedContentModel
{
public PublishedContentStrong2(IPublishedContent content)
: base(content)
@@ -403,7 +408,7 @@ namespace Umbraco.Tests.PublishedContent
public int StrongValue => this.Value<int>("strongValue");
}
class AutoPublishedContentType : PublishedContentType
internal class AutoPublishedContentType : PublishedContentType
{
private static readonly IPublishedPropertyType Default;
@@ -416,20 +421,20 @@ namespace Umbraco.Tests.PublishedContent
Default = factory.CreatePropertyType("*", 666);
}
public AutoPublishedContentType(int id, string alias, IEnumerable<PublishedPropertyType> propertyTypes)
: base(id, alias, PublishedItemType.Content, Enumerable.Empty<string>(), propertyTypes, ContentVariation.Nothing)
public AutoPublishedContentType(Guid key, int id, string alias, IEnumerable<PublishedPropertyType> propertyTypes)
: base(key, id, alias, PublishedItemType.Content, Enumerable.Empty<string>(), propertyTypes, ContentVariation.Nothing)
{ }
public AutoPublishedContentType(int id, string alias, Func<IPublishedContentType, IEnumerable<IPublishedPropertyType>> propertyTypes)
: base(id, alias, PublishedItemType.Content, Enumerable.Empty<string>(), propertyTypes, ContentVariation.Nothing)
public AutoPublishedContentType(Guid key, int id, string alias, Func<IPublishedContentType, IEnumerable<IPublishedPropertyType>> propertyTypes)
: base(key, id, alias, PublishedItemType.Content, Enumerable.Empty<string>(), propertyTypes, ContentVariation.Nothing)
{ }
public AutoPublishedContentType(int id, string alias, IEnumerable<string> compositionAliases, IEnumerable<PublishedPropertyType> propertyTypes)
: base(id, alias, PublishedItemType.Content, compositionAliases, propertyTypes, ContentVariation.Nothing)
public AutoPublishedContentType(Guid key, int id, string alias, IEnumerable<string> compositionAliases, IEnumerable<PublishedPropertyType> propertyTypes)
: base(key, id, alias, PublishedItemType.Content, compositionAliases, propertyTypes, ContentVariation.Nothing)
{ }
public AutoPublishedContentType(int id, string alias, IEnumerable<string> compositionAliases, Func<IPublishedContentType, IEnumerable<IPublishedPropertyType>> propertyTypes)
: base(id, alias, PublishedItemType.Content, compositionAliases, propertyTypes, ContentVariation.Nothing)
public AutoPublishedContentType(Guid key, int id, string alias, IEnumerable<string> compositionAliases, Func<IPublishedContentType, IEnumerable<IPublishedPropertyType>> propertyTypes)
: base(key, id, alias, PublishedItemType.Content, compositionAliases, propertyTypes, ContentVariation.Nothing)
{ }
public override IPublishedPropertyType GetPropertyType(string alias)
@@ -1,4 +1,5 @@
using System.Linq;
using System;
using System.Linq;
using Moq;
using NUnit.Framework;
using Umbraco.Core;
@@ -27,7 +28,7 @@ namespace Umbraco.Tests.Routing
Mock.Of<IPublishedModelFactory>(),
Mock.Of<IPublishedContentTypeFactory>()),
};
_publishedContentType = new PublishedContentType(0, "Doc", PublishedItemType.Content, Enumerable.Empty<string>(), properties, ContentVariation.Nothing);
_publishedContentType = new PublishedContentType(Guid.NewGuid(), 0, "Doc", PublishedItemType.Content, Enumerable.Empty<string>(), properties, ContentVariation.Nothing);
}
protected override PublishedContentType GetPublishedContentTypeByAlias(string alias)
@@ -1,4 +1,5 @@
using System.Linq;
using System;
using System.Linq;
using Moq;
using NUnit.Framework;
using Umbraco.Core;
@@ -31,7 +32,7 @@ namespace Umbraco.Tests.Routing
factory:Mock.Of<IPublishedContentTypeFactory>()
)
};
_publishedContentType = new PublishedContentType(0, "Doc", PublishedItemType.Content, Enumerable.Empty<string>(), properties, ContentVariation.Nothing);
_publishedContentType = new PublishedContentType(Guid.NewGuid(), 0, "Doc", PublishedItemType.Content, Enumerable.Empty<string>(), properties, ContentVariation.Nothing);
}
protected override PublishedContentType GetPublishedContentTypeByAlias(string alias)
@@ -140,7 +140,7 @@ namespace Umbraco.Tests.Routing
property.SetSourceValue("en", enMediaUrl, true);
property.SetSourceValue("da", daMediaUrl);
var contentType = new PublishedContentType(666, "alias", PublishedItemType.Content, Enumerable.Empty<string>(), new [] { umbracoFilePropertyType }, ContentVariation.Culture);
var contentType = new PublishedContentType(Guid.NewGuid(), 666, "alias", PublishedItemType.Content, Enumerable.Empty<string>(), new [] { umbracoFilePropertyType }, ContentVariation.Culture);
var publishedContent = new SolidPublishedContent(contentType) {Properties = new[] {property}};
var resolvedUrl = GetPublishedUrlProvider(umbracoContext).GetMediaUrl(publishedContent, UrlMode.Auto, "da");
@@ -162,7 +162,7 @@ namespace Umbraco.Tests.Routing
{
var umbracoFilePropertyType = CreatePropertyType(propertyEditorAlias, dataTypeConfiguration, ContentVariation.Nothing);
var contentType = new PublishedContentType(666, "alias", PublishedItemType.Content, Enumerable.Empty<string>(),
var contentType = new PublishedContentType(Guid.NewGuid(), 666, "alias", PublishedItemType.Content, Enumerable.Empty<string>(),
new[] {umbracoFilePropertyType}, ContentVariation.Nothing);
return new SolidPublishedContent(contentType)
@@ -143,7 +143,7 @@ namespace Umbraco.Tests.Routing
frequest.TemplateModel = template;
var umbracoContextAccessor = new TestUmbracoContextAccessor(umbracoContext);
var type = new AutoPublishedContentType(22, "CustomDocument", new PublishedPropertyType[] { });
var type = new AutoPublishedContentType(Guid.NewGuid(), 22, "CustomDocument", new PublishedPropertyType[] { });
ContentTypesCache.GetPublishedContentTypeByAlias = alias => type;
var handler = new RenderRouteHandler(umbracoContext, new TestControllerFactory(umbracoContextAccessor, Mock.Of<ILogger>(), context =>
@@ -174,7 +174,7 @@ namespace Umbraco.Tests.Routing
var requestHandlerSettings = TestHelpers.SettingsForTests.GenerateMockRequestHandlerSettings();
var contentType = new PublishedContentType(666, "alias", PublishedItemType.Content, Enumerable.Empty<string>(), Enumerable.Empty<PublishedPropertyType>(), ContentVariation.Culture);
var contentType = new PublishedContentType(Guid.NewGuid(), 666, "alias", PublishedItemType.Content, Enumerable.Empty<string>(), Enumerable.Empty<PublishedPropertyType>(), ContentVariation.Culture);
var publishedContent = new SolidPublishedContent(contentType) { Id = 1234 };
var publishedContentCache = new Mock<IPublishedContentCache>();
@@ -221,7 +221,7 @@ namespace Umbraco.Tests.Routing
var requestHandlerSettings = TestHelpers.SettingsForTests.GenerateMockRequestHandlerSettings();
var contentType = new PublishedContentType(666, "alias", PublishedItemType.Content, Enumerable.Empty<string>(), Enumerable.Empty<PublishedPropertyType>(), ContentVariation.Culture);
var contentType = new PublishedContentType(Guid.NewGuid(), 666, "alias", PublishedItemType.Content, Enumerable.Empty<string>(), Enumerable.Empty<PublishedPropertyType>(), ContentVariation.Culture);
var publishedContent = new SolidPublishedContent(contentType) { Id = 1234 };
var publishedContentCache = new Mock<IPublishedContentCache>();
@@ -277,7 +277,7 @@ namespace Umbraco.Tests.Routing
var requestHandlerSettings = TestHelpers.SettingsForTests.GenerateMockRequestHandlerSettings();
var contentType = new PublishedContentType(666, "alias", PublishedItemType.Content, Enumerable.Empty<string>(), Enumerable.Empty<PublishedPropertyType>(), ContentVariation.Culture);
var contentType = new PublishedContentType(Guid.NewGuid(), 666, "alias", PublishedItemType.Content, Enumerable.Empty<string>(), Enumerable.Empty<PublishedPropertyType>(), ContentVariation.Culture);
var publishedContent = new SolidPublishedContent(contentType) { Id = 1234 };
var publishedContentCache = new Mock<IPublishedContentCache>();
@@ -1197,7 +1197,7 @@ namespace Umbraco.Tests.Services
Assert.IsFalse(content.HasIdentity);
// content cannot publish values because they are invalid
var propertyValidationService = new PropertyValidationService(Factory.GetInstance<PropertyEditorCollection>(), ServiceContext.DataTypeService);
var propertyValidationService = new PropertyValidationService(Factory.GetInstance<PropertyEditorCollection>(), ServiceContext.DataTypeService, ServiceContext.TextService);
var isValid = propertyValidationService.IsPropertyDataValid(content, out var invalidProperties, CultureImpact.Invariant);
Assert.IsFalse(isValid);
Assert.IsNotEmpty(invalidProperties);
@@ -37,7 +37,7 @@ namespace Umbraco.Tests.Services
var propEditors = new PropertyEditorCollection(new DataEditorCollection(new[] { dataEditor }));
validationService = new PropertyValidationService(propEditors, dataTypeService.Object);
validationService = new PropertyValidationService(propEditors, dataTypeService.Object, Mock.Of<ILocalizedTextService>());
}
[Test]
@@ -64,7 +64,7 @@ namespace Umbraco.Tests.Templates
{
//setup a mock url provider which we'll use for testing
var mediaType = new PublishedContentType(777, "image", PublishedItemType.Media, Enumerable.Empty<string>(), Enumerable.Empty<PublishedPropertyType>(), ContentVariation.Nothing);
var mediaType = new PublishedContentType(Guid.NewGuid(), 777, "image", PublishedItemType.Media, Enumerable.Empty<string>(), Enumerable.Empty<PublishedPropertyType>(), ContentVariation.Nothing);
var media = new Mock<IPublishedContent>();
media.Setup(x => x.ContentType).Returns(mediaType);
var mediaUrlProvider = new Mock<IMediaUrlProvider>();
@@ -55,12 +55,12 @@ namespace Umbraco.Tests.Templates
contentUrlProvider
.Setup(x => x.GetUrl( It.IsAny<IPublishedContent>(), It.IsAny<UrlMode>(), It.IsAny<string>(), It.IsAny<Uri>()))
.Returns(UrlInfo.Url("/my-test-url"));
var contentType = new PublishedContentType(666, "alias", PublishedItemType.Content, Enumerable.Empty<string>(), Enumerable.Empty<PublishedPropertyType>(), ContentVariation.Nothing);
var contentType = new PublishedContentType(Guid.NewGuid(), 666, "alias", PublishedItemType.Content, Enumerable.Empty<string>(), Enumerable.Empty<PublishedPropertyType>(), ContentVariation.Nothing);
var publishedContent = new Mock<IPublishedContent>();
publishedContent.Setup(x => x.Id).Returns(1234);
publishedContent.Setup(x => x.ContentType).Returns(contentType);
var mediaType = new PublishedContentType(777, "image", PublishedItemType.Media, Enumerable.Empty<string>(), Enumerable.Empty<PublishedPropertyType>(), ContentVariation.Nothing);
var mediaType = new PublishedContentType(Guid.NewGuid(), 777, "image", PublishedItemType.Media, Enumerable.Empty<string>(), Enumerable.Empty<PublishedPropertyType>(), ContentVariation.Nothing);
var media = new Mock<IPublishedContent>();
media.Setup(x => x.ContentType).Returns(mediaType);
var mediaUrlProvider = new Mock<IMediaUrlProvider>();
+10 -2
View File
@@ -1,4 +1,5 @@
using System.Linq;
using System;
using System.Linq;
using System.Threading;
using Moq;
using NUnit.Framework;
@@ -6,8 +7,11 @@ using Umbraco.Core;
using Umbraco.Core.Composing;
using Umbraco.Core.Configuration.UmbracoSettings;
using Umbraco.Core.Logging;
using Umbraco.Core.Models;
using Umbraco.Core.Models.PublishedContent;
using Umbraco.Core.PropertyEditors;
using Umbraco.Core.Services;
using Umbraco.Core.Strings;
using Umbraco.Tests.Common;
using Umbraco.Tests.PublishedContent;
using Umbraco.Tests.TestHelpers.Stubs;
@@ -38,7 +42,11 @@ namespace Umbraco.Tests.TestHelpers
// need to specify a custom callback for unit tests
// AutoPublishedContentTypes generates properties automatically
var type = new AutoPublishedContentType(0, "anything", new PublishedPropertyType[] { });
var dataTypeService = new TestObjects.TestDataTypeService(
new DataType(new VoidEditor(Mock.Of<ILogger>(), Mock.Of<IDataTypeService>(), Mock.Of<ILocalizationService>(),Mock.Of<ILocalizedTextService>(), Mock.Of<IShortStringHelper>())) { Id = 1 });
var factory = new PublishedContentTypeFactory(Mock.Of<IPublishedModelFactory>(), new PropertyValueConverterCollection(Array.Empty<IPropertyValueConverter>()), dataTypeService);
var type = new AutoPublishedContentType(Guid.NewGuid(), 0, "anything", new PublishedPropertyType[] { });
ContentTypesCache.GetPublishedContentTypeByAlias = alias => GetPublishedContentTypeByAlias(alias) ?? type;
}

Some files were not shown because too many files have changed in this diff Show More