From 13f7ee8a5fff4c3e584cb8d4405e8ac12060ed8d Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 21 Oct 2019 19:40:06 +1100 Subject: [PATCH 001/202] Adds IDataValueReference --- .../PropertyEditors/IDataValueEditor.cs | 19 +++++++++++++++++++ .../PropertyEditors/IDataValueReference.cs | 17 +++++++++++++++++ src/Umbraco.Core/Umbraco.Core.csproj | 1 + 3 files changed, 37 insertions(+) create mode 100644 src/Umbraco.Core/PropertyEditors/IDataValueReference.cs diff --git a/src/Umbraco.Core/PropertyEditors/IDataValueEditor.cs b/src/Umbraco.Core/PropertyEditors/IDataValueEditor.cs index cb68531cc7..a02fa71ec7 100644 --- a/src/Umbraco.Core/PropertyEditors/IDataValueEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/IDataValueEditor.cs @@ -7,6 +7,7 @@ using Umbraco.Core.Services; namespace Umbraco.Core.PropertyEditors { + /// /// Represents an editor for editing data values. /// @@ -63,8 +64,26 @@ namespace Umbraco.Core.PropertyEditors // TODO: / deal with this when unplugging the xml cache // why property vs propertyType? services should be injected! etc... + + /// + /// Used for serializing an item for packaging + /// + /// + /// + /// + /// + /// IEnumerable ConvertDbToXml(Property property, IDataTypeService dataTypeService, ILocalizationService localizationService, bool published); + + /// + /// Used for serializing an item for packaging + /// + /// + /// + /// + /// XNode ConvertDbToXml(PropertyType propertyType, object value, IDataTypeService dataTypeService); + string ConvertDbToString(PropertyType propertyType, object value, IDataTypeService dataTypeService); } } diff --git a/src/Umbraco.Core/PropertyEditors/IDataValueReference.cs b/src/Umbraco.Core/PropertyEditors/IDataValueReference.cs new file mode 100644 index 0000000000..d7d848f1bf --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/IDataValueReference.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; + +namespace Umbraco.Core.PropertyEditors +{ + /// + /// Used to resolve references from values + /// + public interface IDataValueReference + { + /// + /// Returns any references contained in the value + /// + /// + /// + IEnumerable GetReferences(object value); + } +} diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 43d168f442..be1686df4f 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -270,6 +270,7 @@ + From a7dfba58d10c6f91a5a59ac7e1969ac1ea1abbfc Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 21 Oct 2019 22:49:39 +1100 Subject: [PATCH 002/202] Removes singleton accessors from repositories --- .../Repositories/Implement/ContentRepositoryBase.cs | 5 +++-- .../Implement/DocumentBlueprintRepository.cs | 5 +++-- .../Repositories/Implement/DocumentRepository.cs | 5 +++-- .../Repositories/Implement/MediaRepository.cs | 5 +++-- .../Repositories/Implement/MemberRepository.cs | 5 +++-- .../Repositories/ContentTypeRepositoryTest.cs | 3 ++- .../Persistence/Repositories/DocumentRepositoryTest.cs | 2 +- .../Persistence/Repositories/DomainRepositoryTest.cs | 5 +++-- .../Persistence/Repositories/MediaRepositoryTest.cs | 5 +++-- .../Persistence/Repositories/MemberRepositoryTest.cs | 3 ++- .../Repositories/PublicAccessRepositoryTest.cs | 5 +++-- .../Persistence/Repositories/TagRepositoryTest.cs | 7 ++++--- .../Persistence/Repositories/TemplateRepositoryTest.cs | 3 ++- .../Persistence/Repositories/UserRepositoryTest.cs | 5 +++-- .../Services/ContentServicePerformanceTest.cs | 9 +++++---- src/Umbraco.Tests/Services/ContentServiceTests.cs | 2 +- 16 files changed, 44 insertions(+), 30 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs index 7ab73f3f2d..a6ae2fc7e0 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs @@ -33,17 +33,18 @@ namespace Umbraco.Core.Persistence.Repositories.Implement where TEntity : class, IUmbracoEntity where TRepository : class, IRepository { - protected ContentRepositoryBase(IScopeAccessor scopeAccessor, AppCaches cache, ILanguageRepository languageRepository, ILogger logger) + protected ContentRepositoryBase(IScopeAccessor scopeAccessor, AppCaches cache, ILanguageRepository languageRepository, ILogger logger, PropertyEditorCollection propertyEditors) : base(scopeAccessor, cache, logger) { LanguageRepository = languageRepository; + PropertyEditors = propertyEditors; } protected abstract TRepository This { get; } protected ILanguageRepository LanguageRepository { get; } - protected PropertyEditorCollection PropertyEditors => Current.PropertyEditors; // TODO: inject + protected PropertyEditorCollection PropertyEditors { get; } #region Versions diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentBlueprintRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentBlueprintRepository.cs index d137d7ac76..c654e1a6c2 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentBlueprintRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentBlueprintRepository.cs @@ -2,6 +2,7 @@ using Umbraco.Core.Cache; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Logging; +using Umbraco.Core.PropertyEditors; using Umbraco.Core.Scoping; namespace Umbraco.Core.Persistence.Repositories.Implement @@ -17,8 +18,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement /// internal class DocumentBlueprintRepository : DocumentRepository, IDocumentBlueprintRepository { - public DocumentBlueprintRepository(IScopeAccessor scopeAccessor, AppCaches appCaches, ILogger logger, IContentTypeRepository contentTypeRepository, ITemplateRepository templateRepository, ITagRepository tagRepository, ILanguageRepository languageRepository) - : base(scopeAccessor, appCaches, logger, contentTypeRepository, templateRepository, tagRepository, languageRepository) + public DocumentBlueprintRepository(IScopeAccessor scopeAccessor, AppCaches appCaches, ILogger logger, IContentTypeRepository contentTypeRepository, ITemplateRepository templateRepository, ITagRepository tagRepository, ILanguageRepository languageRepository, PropertyEditorCollection propertyEditors) + : base(scopeAccessor, appCaches, logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, propertyEditors) { } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs index 2649b9993f..eb392e86cb 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs @@ -12,6 +12,7 @@ using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Factories; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.SqlSyntax; +using Umbraco.Core.PropertyEditors; using Umbraco.Core.Scoping; using Umbraco.Core.Services; @@ -30,8 +31,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement private readonly ContentByGuidReadRepository _contentByGuidReadRepository; private readonly IScopeAccessor _scopeAccessor; - public DocumentRepository(IScopeAccessor scopeAccessor, AppCaches appCaches, ILogger logger, IContentTypeRepository contentTypeRepository, ITemplateRepository templateRepository, ITagRepository tagRepository, ILanguageRepository languageRepository) - : base(scopeAccessor, appCaches, languageRepository, logger) + public DocumentRepository(IScopeAccessor scopeAccessor, AppCaches appCaches, ILogger logger, IContentTypeRepository contentTypeRepository, ITemplateRepository templateRepository, ITagRepository tagRepository, ILanguageRepository languageRepository, PropertyEditorCollection propertyEditors) + : base(scopeAccessor, appCaches, languageRepository, logger, propertyEditors) { _contentTypeRepository = contentTypeRepository ?? throw new ArgumentNullException(nameof(contentTypeRepository)); _templateRepository = templateRepository ?? throw new ArgumentNullException(nameof(templateRepository)); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs index 25828b8126..ed3eb589bd 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs @@ -12,6 +12,7 @@ using Umbraco.Core.Models.Entities; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Factories; using Umbraco.Core.Persistence.Querying; +using Umbraco.Core.PropertyEditors; using Umbraco.Core.Scoping; using Umbraco.Core.Services; using static Umbraco.Core.Persistence.NPocoSqlExtensions.Statics; @@ -27,8 +28,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement private readonly ITagRepository _tagRepository; private readonly MediaByGuidReadRepository _mediaByGuidReadRepository; - public MediaRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IMediaTypeRepository mediaTypeRepository, ITagRepository tagRepository, ILanguageRepository languageRepository) - : base(scopeAccessor, cache, languageRepository, logger) + public MediaRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IMediaTypeRepository mediaTypeRepository, ITagRepository tagRepository, ILanguageRepository languageRepository, PropertyEditorCollection propertyEditors) + : base(scopeAccessor, cache, languageRepository, logger, propertyEditors) { _mediaTypeRepository = mediaTypeRepository ?? throw new ArgumentNullException(nameof(mediaTypeRepository)); _tagRepository = tagRepository ?? throw new ArgumentNullException(nameof(tagRepository)); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs index 892122dff9..4aaa064976 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs @@ -10,6 +10,7 @@ using Umbraco.Core.Models.Entities; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Factories; using Umbraco.Core.Persistence.Querying; +using Umbraco.Core.PropertyEditors; using Umbraco.Core.Scoping; using Umbraco.Core.Services; using static Umbraco.Core.Persistence.NPocoSqlExtensions.Statics; @@ -25,8 +26,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement private readonly ITagRepository _tagRepository; private readonly IMemberGroupRepository _memberGroupRepository; - public MemberRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IMemberTypeRepository memberTypeRepository, IMemberGroupRepository memberGroupRepository, ITagRepository tagRepository, ILanguageRepository languageRepository) - : base(scopeAccessor, cache, languageRepository, logger) + public MemberRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IMemberTypeRepository memberTypeRepository, IMemberGroupRepository memberGroupRepository, ITagRepository tagRepository, ILanguageRepository languageRepository, PropertyEditorCollection propertyEditors) + : base(scopeAccessor, cache, languageRepository, logger, propertyEditors) { _memberTypeRepository = memberTypeRepository ?? throw new ArgumentNullException(nameof(memberTypeRepository)); _tagRepository = tagRepository ?? throw new ArgumentNullException(nameof(tagRepository)); diff --git a/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs index f953b9cce6..b8b1b03fc4 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs @@ -8,6 +8,7 @@ using Umbraco.Core.Models; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Repositories.Implement; +using Umbraco.Core.PropertyEditors; using Umbraco.Core.Scoping; using Umbraco.Tests.TestHelpers; using Umbraco.Tests.TestHelpers.Entities; @@ -35,7 +36,7 @@ namespace Umbraco.Tests.Persistence.Repositories var commonRepository = new ContentTypeCommonRepository(scopeAccessor, templateRepository, AppCaches.Disabled); contentTypeRepository = new ContentTypeRepository(scopeAccessor, AppCaches.Disabled, Logger, commonRepository, langRepository); var languageRepository = new LanguageRepository(scopeAccessor, AppCaches.Disabled, Logger); - var repository = new DocumentRepository(scopeAccessor, AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository); + var repository = new DocumentRepository(scopeAccessor, AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, Factory.GetInstance()); return repository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/DocumentRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/DocumentRepositoryTest.cs index 4d62ec8301..424ab145bc 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/DocumentRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/DocumentRepositoryTest.cs @@ -69,7 +69,7 @@ namespace Umbraco.Tests.Persistence.Repositories var commonRepository = new ContentTypeCommonRepository(scopeAccessor, templateRepository, appCaches); var languageRepository = new LanguageRepository(scopeAccessor, appCaches, Logger); contentTypeRepository = new ContentTypeRepository(scopeAccessor, appCaches, Logger, commonRepository, languageRepository); - var repository = new DocumentRepository(scopeAccessor, appCaches, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository); + var repository = new DocumentRepository(scopeAccessor, appCaches, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, Factory.GetInstance()); return repository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/DomainRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/DomainRepositoryTest.cs index 628f8d75a7..65b5e8365e 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/DomainRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/DomainRepositoryTest.cs @@ -3,9 +3,10 @@ using System.Data; using System.Linq; using Moq; using NUnit.Framework; -using Umbraco.Core.Configuration.UmbracoSettings; +using Umbraco.Core; using Umbraco.Core.Models; using Umbraco.Core.Persistence.Repositories.Implement; +using Umbraco.Core.PropertyEditors; using Umbraco.Core.Scoping; using Umbraco.Tests.TestHelpers; using Umbraco.Tests.TestHelpers.Entities; @@ -25,7 +26,7 @@ namespace Umbraco.Tests.Persistence.Repositories var commonRepository = new ContentTypeCommonRepository(accessor, templateRepository, AppCaches); languageRepository = new LanguageRepository(accessor, Core.Cache.AppCaches.Disabled, Logger); contentTypeRepository = new ContentTypeRepository(accessor, Core.Cache.AppCaches.Disabled, Logger, commonRepository, languageRepository); - documentRepository = new DocumentRepository(accessor, Core.Cache.AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository); + documentRepository = new DocumentRepository(accessor, Core.Cache.AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, Factory.GetInstance()); var domainRepository = new DomainRepository(accessor, Core.Cache.AppCaches.Disabled, Logger); return domainRepository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs index e2123df9e3..462c44d325 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs @@ -4,7 +4,7 @@ using Moq; using NUnit.Framework; using Umbraco.Core.Cache; using Umbraco.Core.Configuration.UmbracoSettings; -using Umbraco.Core.IO; +using Umbraco.Core; using Umbraco.Core.Models; using Umbraco.Core.Models.Entities; using Umbraco.Core.Persistence; @@ -17,6 +17,7 @@ using Umbraco.Core.Persistence.Repositories.Implement; using Umbraco.Core.Scoping; using Umbraco.Tests.Testing; using Umbraco.Core.Services; +using Umbraco.Core.PropertyEditors; namespace Umbraco.Tests.Persistence.Repositories { @@ -41,7 +42,7 @@ namespace Umbraco.Tests.Persistence.Repositories var languageRepository = new LanguageRepository(scopeAccessor, appCaches, Logger); mediaTypeRepository = new MediaTypeRepository(scopeAccessor, appCaches, Logger, commonRepository, languageRepository); var tagRepository = new TagRepository(scopeAccessor, appCaches, Logger); - var repository = new MediaRepository(scopeAccessor, appCaches, Logger, mediaTypeRepository, tagRepository, Mock.Of()); + var repository = new MediaRepository(scopeAccessor, appCaches, Logger, mediaTypeRepository, tagRepository, Mock.Of(), Factory.GetInstance()); return repository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs index 17b16ad7ab..0b23240615 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs @@ -15,6 +15,7 @@ using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Persistence.Repositories.Implement; +using Umbraco.Core.PropertyEditors; using Umbraco.Core.Scoping; using Umbraco.Tests.TestHelpers; using Umbraco.Tests.TestHelpers.Entities; @@ -35,7 +36,7 @@ namespace Umbraco.Tests.Persistence.Repositories memberTypeRepository = new MemberTypeRepository(accessor, AppCaches.Disabled, Logger, commonRepository, languageRepository); memberGroupRepository = new MemberGroupRepository(accessor, AppCaches.Disabled, Logger); var tagRepo = new TagRepository(accessor, AppCaches.Disabled, Logger); - var repository = new MemberRepository(accessor, AppCaches.Disabled, Logger, memberTypeRepository, memberGroupRepository, tagRepo, Mock.Of()); + var repository = new MemberRepository(accessor, AppCaches.Disabled, Logger, memberTypeRepository, memberGroupRepository, tagRepo, Mock.Of(), Factory.GetInstance()); return repository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/PublicAccessRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/PublicAccessRepositoryTest.cs index 56041c24aa..3398e7d24d 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/PublicAccessRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/PublicAccessRepositoryTest.cs @@ -3,10 +3,11 @@ using System.Collections.Generic; using System.Linq; using Moq; using NUnit.Framework; -using Umbraco.Core.Configuration.UmbracoSettings; +using Umbraco.Core; using Umbraco.Core.Models; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Repositories.Implement; +using Umbraco.Core.PropertyEditors; using Umbraco.Core.Scoping; using Umbraco.Tests.TestHelpers; using Umbraco.Tests.TestHelpers.Entities; @@ -310,7 +311,7 @@ namespace Umbraco.Tests.Persistence.Repositories var commonRepository = new ContentTypeCommonRepository(accessor, templateRepository, AppCaches); var languageRepository = new LanguageRepository(accessor, AppCaches, Logger); contentTypeRepository = new ContentTypeRepository(accessor, AppCaches, Logger, commonRepository, languageRepository); - var repository = new DocumentRepository(accessor, AppCaches, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository); + var repository = new DocumentRepository(accessor, AppCaches, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, Factory.GetInstance()); return repository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/TagRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/TagRepositoryTest.cs index e3de2c2892..9e7593f4ea 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/TagRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/TagRepositoryTest.cs @@ -2,11 +2,12 @@ using Moq; using NUnit.Framework; using Umbraco.Core.Cache; -using Umbraco.Core.Configuration.UmbracoSettings; +using Umbraco.Core; using Umbraco.Core.IO; using Umbraco.Core.Models; using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Persistence.Repositories.Implement; +using Umbraco.Core.PropertyEditors; using Umbraco.Core.Scoping; using Umbraco.Tests.TestHelpers; using Umbraco.Tests.TestHelpers.Entities; @@ -958,7 +959,7 @@ namespace Umbraco.Tests.Persistence.Repositories var commonRepository = new ContentTypeCommonRepository(accessor, templateRepository, AppCaches.Disabled); var languageRepository = new LanguageRepository(accessor, AppCaches.Disabled, Logger); contentTypeRepository = new ContentTypeRepository(accessor, AppCaches.Disabled, Logger, commonRepository, languageRepository); - var repository = new DocumentRepository(accessor, AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository); + var repository = new DocumentRepository(accessor, AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, Factory.GetInstance()); return repository; } @@ -970,7 +971,7 @@ namespace Umbraco.Tests.Persistence.Repositories var commonRepository = new ContentTypeCommonRepository(accessor, templateRepository, AppCaches.Disabled); var languageRepository = new LanguageRepository(accessor, AppCaches.Disabled, Logger); mediaTypeRepository = new MediaTypeRepository(accessor, AppCaches.Disabled, Logger, commonRepository, languageRepository); - var repository = new MediaRepository(accessor, AppCaches.Disabled, Logger, mediaTypeRepository, tagRepository, Mock.Of()); + var repository = new MediaRepository(accessor, AppCaches.Disabled, Logger, mediaTypeRepository, tagRepository, Mock.Of(), Factory.GetInstance()); return repository; } } diff --git a/src/Umbraco.Tests/Persistence/Repositories/TemplateRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/TemplateRepositoryTest.cs index b0f9a5335b..941ba84351 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/TemplateRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/TemplateRepositoryTest.cs @@ -12,6 +12,7 @@ using Umbraco.Core.IO; using Umbraco.Core.Models; using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Persistence.Repositories.Implement; +using Umbraco.Core.PropertyEditors; using Umbraco.Core.Scoping; using Umbraco.Tests.TestHelpers; using Umbraco.Tests.TestHelpers.Entities; @@ -241,7 +242,7 @@ namespace Umbraco.Tests.Persistence.Repositories var commonRepository = new ContentTypeCommonRepository(ScopeProvider, templateRepository, AppCaches); var languageRepository = new LanguageRepository((IScopeAccessor) ScopeProvider, AppCaches.Disabled, Logger); var contentTypeRepository = new ContentTypeRepository((IScopeAccessor) ScopeProvider, AppCaches.Disabled, Logger, commonRepository, languageRepository); - var contentRepo = new DocumentRepository((IScopeAccessor) ScopeProvider, AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository); + var contentRepo = new DocumentRepository((IScopeAccessor) ScopeProvider, AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, Factory.GetInstance()); var contentType = MockedContentTypes.CreateSimpleContentType("umbTextpage2", "Textpage"); ServiceContext.FileService.SaveTemplate(contentType.DefaultTemplate); // else, FK violation on contentType! diff --git a/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs index 3e5919d7f3..c406a5c704 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs @@ -14,6 +14,7 @@ using Umbraco.Tests.TestHelpers; using Umbraco.Tests.TestHelpers.Entities; using Umbraco.Tests.Testing; using Umbraco.Core.Persistence; +using Umbraco.Core.PropertyEditors; namespace Umbraco.Tests.Persistence.Repositories { @@ -29,7 +30,7 @@ namespace Umbraco.Tests.Persistence.Repositories var languageRepository = new LanguageRepository(accessor, AppCaches, Logger); mediaTypeRepository = new MediaTypeRepository(accessor, AppCaches, Mock.Of(), commonRepository, languageRepository); var tagRepository = new TagRepository(accessor, AppCaches, Mock.Of()); - var repository = new MediaRepository(accessor, AppCaches, Mock.Of(), mediaTypeRepository, tagRepository, Mock.Of()); + var repository = new MediaRepository(accessor, AppCaches, Mock.Of(), mediaTypeRepository, tagRepository, Mock.Of(), Factory.GetInstance()); return repository; } @@ -47,7 +48,7 @@ namespace Umbraco.Tests.Persistence.Repositories var commonRepository = new ContentTypeCommonRepository(accessor, templateRepository, AppCaches); var languageRepository = new LanguageRepository(accessor, AppCaches, Logger); contentTypeRepository = new ContentTypeRepository(accessor, AppCaches, Logger, commonRepository, languageRepository); - var repository = new DocumentRepository(accessor, AppCaches, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository); + var repository = new DocumentRepository(accessor, AppCaches, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, Factory.GetInstance()); return repository; } diff --git a/src/Umbraco.Tests/Services/ContentServicePerformanceTest.cs b/src/Umbraco.Tests/Services/ContentServicePerformanceTest.cs index ef80672baf..c77bb71494 100644 --- a/src/Umbraco.Tests/Services/ContentServicePerformanceTest.cs +++ b/src/Umbraco.Tests/Services/ContentServicePerformanceTest.cs @@ -14,6 +14,7 @@ using Umbraco.Core.Models; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Persistence.Repositories.Implement; +using Umbraco.Core.PropertyEditors; using Umbraco.Core.Scoping; using Umbraco.Tests.TestHelpers; using Umbraco.Tests.TestHelpers.Entities; @@ -168,7 +169,7 @@ namespace Umbraco.Tests.Services var commonRepository = new ContentTypeCommonRepository((IScopeAccessor)provider, tRepository, AppCaches); var languageRepository = new LanguageRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger); var ctRepository = new ContentTypeRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger, commonRepository, languageRepository); - var repository = new DocumentRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger, ctRepository, tRepository, tagRepo, languageRepository); + var repository = new DocumentRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger, ctRepository, tRepository, tagRepo, languageRepository, Factory.GetInstance()); // Act Stopwatch watch = Stopwatch.StartNew(); @@ -202,7 +203,7 @@ namespace Umbraco.Tests.Services var commonRepository = new ContentTypeCommonRepository((IScopeAccessor)provider, tRepository, AppCaches); var languageRepository = new LanguageRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger); var ctRepository = new ContentTypeRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger, commonRepository, languageRepository); - var repository = new DocumentRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger, ctRepository, tRepository, tagRepo, languageRepository); + var repository = new DocumentRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger, ctRepository, tRepository, tagRepo, languageRepository, Factory.GetInstance()); // Act Stopwatch watch = Stopwatch.StartNew(); @@ -234,7 +235,7 @@ namespace Umbraco.Tests.Services var commonRepository = new ContentTypeCommonRepository((IScopeAccessor) provider, tRepository, AppCaches); var languageRepository = new LanguageRepository((IScopeAccessor)provider, AppCaches.Disabled, Logger); var ctRepository = new ContentTypeRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger, commonRepository, languageRepository); - var repository = new DocumentRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger, ctRepository, tRepository, tagRepo, languageRepository); + var repository = new DocumentRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger, ctRepository, tRepository, tagRepo, languageRepository, Factory.GetInstance()); // Act var contents = repository.GetMany(); @@ -269,7 +270,7 @@ namespace Umbraco.Tests.Services var commonRepository = new ContentTypeCommonRepository((IScopeAccessor)provider, tRepository, AppCaches); var languageRepository = new LanguageRepository((IScopeAccessor)provider, AppCaches.Disabled, Logger); var ctRepository = new ContentTypeRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger, commonRepository, languageRepository); - var repository = new DocumentRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger, ctRepository, tRepository, tagRepo, languageRepository); + var repository = new DocumentRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger, ctRepository, tRepository, tagRepo, languageRepository, Factory.GetInstance()); // Act var contents = repository.GetMany(); diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs index e26e764cd1..65c3b9f8a7 100644 --- a/src/Umbraco.Tests/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs @@ -3166,7 +3166,7 @@ namespace Umbraco.Tests.Services var commonRepository = new ContentTypeCommonRepository(accessor, templateRepository, AppCaches); var languageRepository = new LanguageRepository(accessor, AppCaches.Disabled, Logger); contentTypeRepository = new ContentTypeRepository(accessor, AppCaches.Disabled, Logger, commonRepository, languageRepository); - var repository = new DocumentRepository(accessor, AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository); + var repository = new DocumentRepository(accessor, AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, Factory.GetInstance()); return repository; } From a78a4ff8075ace6b97d49b4b20c992c5c7647a33 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 21 Oct 2019 22:56:02 +1100 Subject: [PATCH 003/202] Less statics, new InternalLinkParserTests, better testing support on UDI, less singletons --- .../PropertyEditors/IDataValueReference.cs | 2 +- src/Umbraco.Core/Udi.cs | 2 +- src/Umbraco.Tests/Umbraco.Tests.csproj | 1 + .../Web/InternalLinkParserTests.cs | 88 ++++++++++++++++++ .../Web/TemplateUtilitiesTests.cs | 71 +-------------- .../MarkdownEditorValueConverter.cs | 9 +- .../RteMacroRenderingValueConverter.cs | 6 +- .../TextStringValueConverter.cs | 8 +- src/Umbraco.Web/Routing/UrlProvider.cs | 13 +++ src/Umbraco.Web/Runtime/WebInitialComposer.cs | 2 + .../Templates/InternalLinkParser.cs | 91 +++++++++++++++++++ .../Templates/TemplateUtilities.cs | 73 ++------------- src/Umbraco.Web/Umbraco.Web.csproj | 1 + src/Umbraco.Web/UmbracoComponentRenderer.cs | 6 +- 14 files changed, 232 insertions(+), 141 deletions(-) create mode 100644 src/Umbraco.Tests/Web/InternalLinkParserTests.cs create mode 100644 src/Umbraco.Web/Templates/InternalLinkParser.cs diff --git a/src/Umbraco.Core/PropertyEditors/IDataValueReference.cs b/src/Umbraco.Core/PropertyEditors/IDataValueReference.cs index d7d848f1bf..e71642f8a3 100644 --- a/src/Umbraco.Core/PropertyEditors/IDataValueReference.cs +++ b/src/Umbraco.Core/PropertyEditors/IDataValueReference.cs @@ -3,7 +3,7 @@ namespace Umbraco.Core.PropertyEditors { /// - /// Used to resolve references from values + /// Resolve references from values /// public interface IDataValueReference { diff --git a/src/Umbraco.Core/Udi.cs b/src/Umbraco.Core/Udi.cs index c7297b8c09..e7d00fffa5 100644 --- a/src/Umbraco.Core/Udi.cs +++ b/src/Umbraco.Core/Udi.cs @@ -233,7 +233,7 @@ namespace Umbraco.Core // just pick every service connectors - just making sure that not two of them // would register the same entity type, with different udi types (would not make // much sense anyways). - var connectors = Current.TypeLoader.GetTypes(); + var connectors = Current.HasFactory ? (Current.TypeLoader?.GetTypes() ?? Enumerable.Empty()) : Enumerable.Empty(); var result = new Dictionary(); foreach (var connector in connectors) { diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 39826fcc38..e73caf4517 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -253,6 +253,7 @@ + diff --git a/src/Umbraco.Tests/Web/InternalLinkParserTests.cs b/src/Umbraco.Tests/Web/InternalLinkParserTests.cs new file mode 100644 index 0000000000..815ce408ee --- /dev/null +++ b/src/Umbraco.Tests/Web/InternalLinkParserTests.cs @@ -0,0 +1,88 @@ +using System; +using System.Linq; +using System.Web; +using Moq; +using NUnit.Framework; +using Umbraco.Core.Configuration.UmbracoSettings; +using Umbraco.Core.Models; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.Services; +using Umbraco.Tests.TestHelpers; +using Umbraco.Tests.Testing.Objects.Accessors; +using Umbraco.Web; +using Umbraco.Web.PublishedCache; +using Umbraco.Web.Routing; +using Umbraco.Web.Templates; + +namespace Umbraco.Tests.Web +{ + [TestFixture] + public class InternalLinkParserTests + { + [TestCase("", "")] + [TestCase("hello href=\"{localLink:1234}\" world ", "hello href=\"/my-test-url\" world ")] + [TestCase("hello href=\"{localLink:umb://document/9931BDE0-AAC3-4BAB-B838-909A7B47570E}\" world ", "hello href=\"/my-test-url\" world ")] + [TestCase("hello href=\"{localLink:umb://document/9931BDE0AAC34BABB838909A7B47570E}\" world ", "hello href=\"/my-test-url\" world ")] + [TestCase("hello href=\"{localLink:umb://media/9931BDE0AAC34BABB838909A7B47570E}\" world ", "hello href=\"/media/1001/my-image.jpg\" world ")] + //this one has an invalid char so won't match + [TestCase("hello href=\"{localLink:umb^://document/9931BDE0-AAC3-4BAB-B838-909A7B47570E}\" world ", "hello href=\"{localLink:umb^://document/9931BDE0-AAC3-4BAB-B838-909A7B47570E}\" world ")] + [TestCase("hello href=\"{localLink:umb://document-type/9931BDE0-AAC3-4BAB-B838-909A7B47570E}\" world ", "hello href=\"#\" world ")] + public void ParseLocalLinks(string input, string result) + { + var serviceCtxMock = new TestObjects(null).GetServiceContextMock(); + + //setup a mock url provider which we'll use for testing + var testUrlProvider = new Mock(); + testUrlProvider + .Setup(x => x.GetUrl(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(UrlInfo.Url("/my-test-url")); + + var globalSettings = SettingsForTests.GenerateMockGlobalSettings(); + + var contentType = new PublishedContentType(666, "alias", PublishedItemType.Content, Enumerable.Empty(), Enumerable.Empty(), ContentVariation.Nothing); + var publishedContent = new Mock(); + publishedContent.Setup(x => x.Id).Returns(1234); + publishedContent.Setup(x => x.ContentType).Returns(contentType); + var contentCache = new Mock(); + contentCache.Setup(x => x.GetById(It.IsAny())).Returns(publishedContent.Object); + contentCache.Setup(x => x.GetById(It.IsAny())).Returns(publishedContent.Object); + var mediaType = new PublishedContentType(777, "image", PublishedItemType.Media, Enumerable.Empty(), Enumerable.Empty(), ContentVariation.Nothing); + var media = new Mock(); + media.Setup(x => x.Url).Returns("/media/1001/my-image.jpg"); + media.Setup(x => x.ContentType).Returns(mediaType); + var mediaCache = new Mock(); + mediaCache.Setup(x => x.GetById(It.IsAny())).Returns(media.Object); + mediaCache.Setup(x => x.GetById(It.IsAny())).Returns(media.Object); + var snapshot = new Mock(); + snapshot.Setup(x => x.Content).Returns(contentCache.Object); + snapshot.Setup(x => x.Media).Returns(mediaCache.Object); + var snapshotService = new Mock(); + snapshotService.Setup(x => x.CreatePublishedSnapshot(It.IsAny())).Returns(snapshot.Object); + var mediaUrlProvider = new Mock(); + mediaUrlProvider.Setup(x => x.GetMediaUrl(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(UrlInfo.Url("/media/1001/my-image.jpg")); + + var umbracoContextAccessor = new TestUmbracoContextAccessor(); + + var umbracoContextFactory = new UmbracoContextFactory( + umbracoContextAccessor, + snapshotService.Object, + new TestVariationContextAccessor(), + new TestDefaultCultureAccessor(), + Mock.Of(section => section.WebRouting == Mock.Of(routingSection => routingSection.UrlProviderMode == "Auto")), + globalSettings, + new UrlProviderCollection(new[] { testUrlProvider.Object }), + new MediaUrlProviderCollection(new[] { mediaUrlProvider.Object }), + Mock.Of()); + + using (var reference = umbracoContextFactory.EnsureUmbracoContext(Mock.Of())) + { + var linkParser = new InternalLinkParser(umbracoContextAccessor); + + var output = linkParser.ParseInternalLinks(input); + + Assert.AreEqual(result, output); + } + } + } +} diff --git a/src/Umbraco.Tests/Web/TemplateUtilitiesTests.cs b/src/Umbraco.Tests/Web/TemplateUtilitiesTests.cs index 3a5405548b..ef23330431 100644 --- a/src/Umbraco.Tests/Web/TemplateUtilitiesTests.cs +++ b/src/Umbraco.Tests/Web/TemplateUtilitiesTests.cs @@ -1,6 +1,4 @@ using System; -using System.Linq; -using System.Web; using Moq; using NUnit.Framework; using Umbraco.Core; @@ -9,20 +7,16 @@ 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.Services; using Umbraco.Tests.TestHelpers; using Umbraco.Tests.Testing.Objects.Accessors; -using Umbraco.Web; -using Umbraco.Web.PublishedCache; -using Umbraco.Web.Routing; using Umbraco.Web.Security; -using Umbraco.Web.Templates; using Umbraco.Core.Configuration; using Umbraco.Core.IO; namespace Umbraco.Tests.Web { + [TestFixture] public class TemplateUtilitiesTests { @@ -59,67 +53,6 @@ namespace Umbraco.Tests.Web Current.Reset(); } - [TestCase("", "")] - [TestCase("hello href=\"{localLink:1234}\" world ", "hello href=\"/my-test-url\" world ")] - [TestCase("hello href=\"{localLink:umb://document/9931BDE0-AAC3-4BAB-B838-909A7B47570E}\" world ", "hello href=\"/my-test-url\" world ")] - [TestCase("hello href=\"{localLink:umb://document/9931BDE0AAC34BABB838909A7B47570E}\" world ", "hello href=\"/my-test-url\" world ")] - [TestCase("hello href=\"{localLink:umb://media/9931BDE0AAC34BABB838909A7B47570E}\" world ", "hello href=\"/media/1001/my-image.jpg\" world ")] - //this one has an invalid char so won't match - [TestCase("hello href=\"{localLink:umb^://document/9931BDE0-AAC3-4BAB-B838-909A7B47570E}\" world ", "hello href=\"{localLink:umb^://document/9931BDE0-AAC3-4BAB-B838-909A7B47570E}\" world ")] - [TestCase("hello href=\"{localLink:umb://document-type/9931BDE0-AAC3-4BAB-B838-909A7B47570E}\" world ", "hello href=\"#\" world ")] - public void ParseLocalLinks(string input, string result) - { - var serviceCtxMock = new TestObjects(null).GetServiceContextMock(); - - //setup a mock entity service from the service context to return an integer for a GUID - var entityService = Mock.Get(serviceCtxMock.EntityService); - //entityService.Setup(x => x.GetId(It.IsAny(), It.IsAny())) - // .Returns((Guid id, UmbracoObjectTypes objType) => - // { - // return Attempt.Succeed(1234); - // }); - - //setup a mock url provider which we'll use for testing - var testUrlProvider = new Mock(); - testUrlProvider - .Setup(x => x.GetUrl(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Returns((UmbracoContext umbCtx, IPublishedContent content, UrlMode mode, string culture, Uri url) => UrlInfo.Url("/my-test-url")); - - var globalSettings = SettingsForTests.GenerateMockGlobalSettings(); - - var contentType = new PublishedContentType(666, "alias", PublishedItemType.Content, Enumerable.Empty(), Enumerable.Empty(), ContentVariation.Nothing); - var publishedContent = Mock.Of(); - Mock.Get(publishedContent).Setup(x => x.Id).Returns(1234); - Mock.Get(publishedContent).Setup(x => x.ContentType).Returns(contentType); - var contentCache = Mock.Of(); - Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny())).Returns(publishedContent); - Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny())).Returns(publishedContent); - var snapshot = Mock.Of(); - Mock.Get(snapshot).Setup(x => x.Content).Returns(contentCache); - var snapshotService = Mock.Of(); - Mock.Get(snapshotService).Setup(x => x.CreatePublishedSnapshot(It.IsAny())).Returns(snapshot); - var media = Mock.Of(); - Mock.Get(media).Setup(x => x.Url).Returns("/media/1001/my-image.jpg"); - var mediaCache = Mock.Of(); - Mock.Get(mediaCache).Setup(x => x.GetById(It.IsAny())).Returns(media); - - var umbracoContextFactory = new UmbracoContextFactory( - Umbraco.Web.Composing.Current.UmbracoContextAccessor, - snapshotService, - new TestVariationContextAccessor(), - new TestDefaultCultureAccessor(), - Mock.Of(section => section.WebRouting == Mock.Of(routingSection => routingSection.UrlProviderMode == "Auto")), - globalSettings, - new UrlProviderCollection(new[] { testUrlProvider.Object }), - new MediaUrlProviderCollection(Enumerable.Empty()), - Mock.Of()); - - using (var reference = umbracoContextFactory.EnsureUmbracoContext(Mock.Of())) - { - var output = TemplateUtilities.ParseInternalLinks(input, reference.UmbracoContext.UrlProvider, mediaCache); - - Assert.AreEqual(result, output); - } - } + } } diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/MarkdownEditorValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/MarkdownEditorValueConverter.cs index e11f3e0d3a..98413c7b70 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/MarkdownEditorValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/MarkdownEditorValueConverter.cs @@ -12,6 +12,13 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters [DefaultPropertyValueConverter] public class MarkdownEditorValueConverter : PropertyValueConverterBase { + private readonly InternalLinkParser _localLinkParser; + + public MarkdownEditorValueConverter(InternalLinkParser localLinkParser) + { + _localLinkParser = localLinkParser; + } + public override bool IsConverter(IPublishedPropertyType propertyType) => Constants.PropertyEditors.Aliases.MarkdownEditor == propertyType.EditorAlias; @@ -27,7 +34,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters var sourceString = source.ToString(); // ensures string is parsed for {localLink} and urls are resolved correctly - sourceString = TemplateUtilities.ParseInternalLinks(sourceString, preview, Current.UmbracoContext); + sourceString = _localLinkParser.ParseInternalLinks(sourceString, preview); sourceString = TemplateUtilities.ResolveUrlsFromTextString(sourceString); return sourceString; diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/RteMacroRenderingValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/RteMacroRenderingValueConverter.cs index d5e1f841ea..95cf2cfc85 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/RteMacroRenderingValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/RteMacroRenderingValueConverter.cs @@ -24,6 +24,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters { private readonly IUmbracoContextAccessor _umbracoContextAccessor; private readonly IMacroRenderer _macroRenderer; + private readonly InternalLinkParser _internalLinkParser; public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) { @@ -32,10 +33,11 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters return PropertyCacheLevel.Snapshot; } - public RteMacroRenderingValueConverter(IUmbracoContextAccessor umbracoContextAccessor, IMacroRenderer macroRenderer) + public RteMacroRenderingValueConverter(IUmbracoContextAccessor umbracoContextAccessor, IMacroRenderer macroRenderer, InternalLinkParser internalLinkParser) { _umbracoContextAccessor = umbracoContextAccessor; _macroRenderer = macroRenderer; + _internalLinkParser = internalLinkParser; } // NOT thread-safe over a request because it modifies the @@ -81,7 +83,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters var sourceString = source.ToString(); // ensures string is parsed for {localLink} and urls and media are resolved correctly - sourceString = TemplateUtilities.ParseInternalLinks(sourceString, preview, Current.UmbracoContext); + sourceString = _internalLinkParser.ParseInternalLinks(sourceString, preview); sourceString = TemplateUtilities.ResolveUrlsFromTextString(sourceString); sourceString = TemplateUtilities.ResolveMediaFromTextString(sourceString); diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/TextStringValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/TextStringValueConverter.cs index b8ad1477b4..ee49536d9d 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/TextStringValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/TextStringValueConverter.cs @@ -11,11 +11,17 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters [DefaultPropertyValueConverter] public class TextStringValueConverter : PropertyValueConverterBase { + public TextStringValueConverter(InternalLinkParser internalLinkParser) + { + _internalLinkParser = internalLinkParser; + } + private static readonly string[] PropertyTypeAliases = { Constants.PropertyEditors.Aliases.TextBox, Constants.PropertyEditors.Aliases.TextArea }; + private readonly InternalLinkParser _internalLinkParser; public override bool IsConverter(IPublishedPropertyType propertyType) => PropertyTypeAliases.Contains(propertyType.EditorAlias); @@ -32,7 +38,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters var sourceString = source.ToString(); // ensures string is parsed for {localLink} and urls are resolved correctly - sourceString = TemplateUtilities.ParseInternalLinks(sourceString, preview, Current.UmbracoContext); + sourceString = _internalLinkParser.ParseInternalLinks(sourceString, preview); sourceString = TemplateUtilities.ResolveUrlsFromTextString(sourceString); return sourceString; diff --git a/src/Umbraco.Web/Routing/UrlProvider.cs b/src/Umbraco.Web/Routing/UrlProvider.cs index 59e39fa80a..d42639b781 100644 --- a/src/Umbraco.Web/Routing/UrlProvider.cs +++ b/src/Umbraco.Web/Routing/UrlProvider.cs @@ -75,6 +75,7 @@ namespace Umbraco.Web.Routing private UrlMode GetMode(bool absolute) => absolute ? UrlMode.Absolute : Mode; private IPublishedContent GetDocument(int id) => _umbracoContext.Content.GetById(id); private IPublishedContent GetDocument(Guid id) => _umbracoContext.Content.GetById(id); + private IPublishedContent GetMedia(Guid id) => _umbracoContext.Media.GetById(id); /// /// Gets the url of a published content. @@ -184,6 +185,18 @@ namespace Umbraco.Web.Routing #region GetMediaUrl + /// + /// Gets the url of a media item. + /// + /// + /// + /// + /// + /// + /// + public string GetMediaUrl(Guid id, UrlMode mode = UrlMode.Default, string culture = null, string propertyAlias = Constants.Conventions.Media.File, Uri current = null) + => GetMediaUrl(GetMedia(id), mode, culture, propertyAlias, current); + /// /// Gets the url of a media item. /// diff --git a/src/Umbraco.Web/Runtime/WebInitialComposer.cs b/src/Umbraco.Web/Runtime/WebInitialComposer.cs index 87c0f46fba..82137bbd9d 100644 --- a/src/Umbraco.Web/Runtime/WebInitialComposer.cs +++ b/src/Umbraco.Web/Runtime/WebInitialComposer.cs @@ -107,6 +107,8 @@ namespace Umbraco.Web.Runtime composition.RegisterUnique(); composition.RegisterUnique(); + composition.RegisterUnique(); + // register the umbraco helper - this is Transient! very important! // also, if not level.Run, we cannot really use the helper (during upgrade...) // so inject a "void" helper (not exactly pretty but...) diff --git a/src/Umbraco.Web/Templates/InternalLinkParser.cs b/src/Umbraco.Web/Templates/InternalLinkParser.cs new file mode 100644 index 0000000000..0d8a480a90 --- /dev/null +++ b/src/Umbraco.Web/Templates/InternalLinkParser.cs @@ -0,0 +1,91 @@ +using System; +using System.Text.RegularExpressions; +using Umbraco.Core; +using Umbraco.Core.Logging; +using Umbraco.Web.PublishedCache; +using Umbraco.Web.Routing; + +namespace Umbraco.Web.Templates +{ + /// + /// Utility class used to parse internal links + /// + public sealed class InternalLinkParser + { + + private static readonly Regex LocalLinkPattern = new Regex(@"href=""[/]?(?:\{|\%7B)localLink:([a-zA-Z0-9-://]+)(?:\}|\%7D)", + RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); + + private readonly IUmbracoContextAccessor _umbracoContextAccessor; + + public InternalLinkParser(IUmbracoContextAccessor umbracoContextAccessor) + { + _umbracoContextAccessor = umbracoContextAccessor; + } + + public string ParseInternalLinks(string text, bool preview) + { + if (_umbracoContextAccessor.UmbracoContext == null) + throw new InvalidOperationException("Could not parse internal links, there is no current UmbracoContext"); + + if (!preview) + return ParseInternalLinks(text); + + using (_umbracoContextAccessor.UmbracoContext.ForcedPreview(preview)) // force for url provider + { + return ParseInternalLinks(text); + } + } + + /// + /// Parses the string looking for the {localLink} syntax and updates them to their correct links. + /// + /// + /// + /// + public string ParseInternalLinks(string text) + { + if (_umbracoContextAccessor.UmbracoContext == null) + throw new InvalidOperationException("Could not parse internal links, there is no current UmbracoContext"); + + var urlProvider = _umbracoContextAccessor.UmbracoContext.UrlProvider; + + // Parse internal links + var tags = LocalLinkPattern.Matches(text); + foreach (Match tag in tags) + { + if (tag.Groups.Count > 0) + { + var id = tag.Groups[1].Value; //.Remove(tag.Groups[1].Value.Length - 1, 1); + + //The id could be an int or a UDI + if (Udi.TryParse(id, out var udi)) + { + var guidUdi = udi as GuidUdi; + if (guidUdi != null) + { + var newLink = "#"; + if (guidUdi.EntityType == Constants.UdiEntityType.Document) + newLink = urlProvider.GetUrl(guidUdi.Guid); + else if (guidUdi.EntityType == Constants.UdiEntityType.Media) + newLink = urlProvider.GetMediaUrl(guidUdi.Guid); + + if (newLink == null) + newLink = "#"; + + text = text.Replace(tag.Value, "href=\"" + newLink); + } + } + + if (int.TryParse(id, out var intId)) + { + var newLink = urlProvider.GetUrl(intId); + text = text.Replace(tag.Value, "href=\"" + newLink); + } + } + } + + return text; + } + } +} diff --git a/src/Umbraco.Web/Templates/TemplateUtilities.cs b/src/Umbraco.Web/Templates/TemplateUtilities.cs index 58d3ed341e..1092be73e2 100644 --- a/src/Umbraco.Web/Templates/TemplateUtilities.cs +++ b/src/Umbraco.Web/Templates/TemplateUtilities.cs @@ -15,8 +15,6 @@ using File = System.IO.File; namespace Umbraco.Web.Templates { - //NOTE: I realize there is only one class in this namespace but I'm pretty positive that there will be more classes in - //this namespace once we start migrating and cleaning up more code. /// /// Utility class used for templates @@ -24,7 +22,14 @@ namespace Umbraco.Web.Templates public static class TemplateUtilities { const string TemporaryImageDataAttribute = "data-tmpimg"; + + private static readonly Regex ResolveUrlPattern = new Regex("(=[\"\']?)(\\W?\\~(?:.(?![\"\']?\\s+(?:\\S+)=|[>\"\']))+.)[\"\']?", + RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); + private static readonly Regex ResolveImgPattern = new Regex(@"(]*src="")([^""\?]*)([^""]*""[^>]*data-udi="")([^""]*)(""[^>]*>)", + RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); + + [Obsolete("Inject and use an instance of InternalLinkParser instead")] internal static string ParseInternalLinks(string text, bool preview, UmbracoContext umbracoContext) { using (umbracoContext.ForcedPreview(preview)) // force for url provider @@ -35,69 +40,9 @@ namespace Umbraco.Web.Templates return text; } - /// - /// Parses the string looking for the {localLink} syntax and updates them to their correct links. - /// - /// - /// - /// + [Obsolete("Inject and use an instance of InternalLinkParser instead")] public static string ParseInternalLinks(string text, UrlProvider urlProvider) => - ParseInternalLinks(text, urlProvider, Current.UmbracoContext.MediaCache); - - // TODO: Replace mediaCache with media url provider - internal static string ParseInternalLinks(string text, UrlProvider urlProvider, IPublishedMediaCache mediaCache) - { - if (urlProvider == null) throw new ArgumentNullException(nameof(urlProvider)); - if (mediaCache == null) throw new ArgumentNullException(nameof(mediaCache)); - - // Parse internal links - var tags = LocalLinkPattern.Matches(text); - foreach (Match tag in tags) - { - if (tag.Groups.Count > 0) - { - var id = tag.Groups[1].Value; //.Remove(tag.Groups[1].Value.Length - 1, 1); - - //The id could be an int or a UDI - if (Udi.TryParse(id, out var udi)) - { - var guidUdi = udi as GuidUdi; - if (guidUdi != null) - { - var newLink = "#"; - if (guidUdi.EntityType == Constants.UdiEntityType.Document) - newLink = urlProvider.GetUrl(guidUdi.Guid); - else if (guidUdi.EntityType == Constants.UdiEntityType.Media) - newLink = mediaCache.GetById(guidUdi.Guid)?.Url; - - if (newLink == null) - newLink = "#"; - - text = text.Replace(tag.Value, "href=\"" + newLink); - } - } - - if (int.TryParse(id, out var intId)) - { - var newLink = urlProvider.GetUrl(intId); - text = text.Replace(tag.Value, "href=\"" + newLink); - } - } - } - - return text; - } - - - // static compiled regex for faster performance - private static readonly Regex LocalLinkPattern = new Regex(@"href=""[/]?(?:\{|\%7B)localLink:([a-zA-Z0-9-://]+)(?:\}|\%7D)", - RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); - - private static readonly Regex ResolveUrlPattern = new Regex("(=[\"\']?)(\\W?\\~(?:.(?![\"\']?\\s+(?:\\S+)=|[>\"\']))+.)[\"\']?", - RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); - - private static readonly Regex ResolveImgPattern = new Regex(@"(]*src="")([^""\?]*)([^""]*""[^>]*data-udi="")([^""]*)(""[^>]*>)", - RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); + Current.Factory.GetInstance().ParseInternalLinks(text); /// /// The RegEx matches any HTML attribute values that start with a tilde (~), those that match are passed to ResolveUrl to replace the tilde with the application path. diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index cf14996888..616ed908e1 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -247,6 +247,7 @@ + diff --git a/src/Umbraco.Web/UmbracoComponentRenderer.cs b/src/Umbraco.Web/UmbracoComponentRenderer.cs index f6c3d30da3..805b9267f9 100644 --- a/src/Umbraco.Web/UmbracoComponentRenderer.cs +++ b/src/Umbraco.Web/UmbracoComponentRenderer.cs @@ -27,12 +27,14 @@ namespace Umbraco.Web private readonly IUmbracoContextAccessor _umbracoContextAccessor; private readonly IMacroRenderer _macroRenderer; private readonly ITemplateRenderer _templateRenderer; + private readonly InternalLinkParser _internalLinkParser; - public UmbracoComponentRenderer(IUmbracoContextAccessor umbracoContextAccessor, IMacroRenderer macroRenderer, ITemplateRenderer templateRenderer) + public UmbracoComponentRenderer(IUmbracoContextAccessor umbracoContextAccessor, IMacroRenderer macroRenderer, ITemplateRenderer templateRenderer, InternalLinkParser internalLinkParser) { _umbracoContextAccessor = umbracoContextAccessor; _macroRenderer = macroRenderer; _templateRenderer = templateRenderer ?? throw new ArgumentNullException(nameof(templateRenderer)); + _internalLinkParser = internalLinkParser; } /// @@ -157,7 +159,7 @@ namespace Umbraco.Web _umbracoContextAccessor.UmbracoContext.HttpContext.Response.ContentType = contentType; //Now, we need to ensure that local links are parsed - html = TemplateUtilities.ParseInternalLinks(output.ToString(), _umbracoContextAccessor.UmbracoContext.UrlProvider); + html = _internalLinkParser.ParseInternalLinks(output.ToString()); } } From 8ccebd8006b8eba225fb496d67c3e1b4364e5675 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 21 Oct 2019 23:04:25 +1100 Subject: [PATCH 004/202] Revert "Removes singleton accessors from repositories" - this causes a circular ref?! --- .../Repositories/Implement/ContentRepositoryBase.cs | 5 ++--- .../Implement/DocumentBlueprintRepository.cs | 5 ++--- .../Repositories/Implement/DocumentRepository.cs | 5 ++--- .../Repositories/Implement/MediaRepository.cs | 5 ++--- .../Repositories/Implement/MemberRepository.cs | 5 ++--- .../Repositories/ContentTypeRepositoryTest.cs | 3 +-- .../Persistence/Repositories/DocumentRepositoryTest.cs | 2 +- .../Persistence/Repositories/DomainRepositoryTest.cs | 5 ++--- .../Persistence/Repositories/MediaRepositoryTest.cs | 5 ++--- .../Persistence/Repositories/MemberRepositoryTest.cs | 3 +-- .../Repositories/PublicAccessRepositoryTest.cs | 5 ++--- .../Persistence/Repositories/TagRepositoryTest.cs | 7 +++---- .../Persistence/Repositories/TemplateRepositoryTest.cs | 3 +-- .../Persistence/Repositories/UserRepositoryTest.cs | 5 ++--- .../Services/ContentServicePerformanceTest.cs | 9 ++++----- src/Umbraco.Tests/Services/ContentServiceTests.cs | 2 +- 16 files changed, 30 insertions(+), 44 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs index a6ae2fc7e0..7ab73f3f2d 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs @@ -33,18 +33,17 @@ namespace Umbraco.Core.Persistence.Repositories.Implement where TEntity : class, IUmbracoEntity where TRepository : class, IRepository { - protected ContentRepositoryBase(IScopeAccessor scopeAccessor, AppCaches cache, ILanguageRepository languageRepository, ILogger logger, PropertyEditorCollection propertyEditors) + protected ContentRepositoryBase(IScopeAccessor scopeAccessor, AppCaches cache, ILanguageRepository languageRepository, ILogger logger) : base(scopeAccessor, cache, logger) { LanguageRepository = languageRepository; - PropertyEditors = propertyEditors; } protected abstract TRepository This { get; } protected ILanguageRepository LanguageRepository { get; } - protected PropertyEditorCollection PropertyEditors { get; } + protected PropertyEditorCollection PropertyEditors => Current.PropertyEditors; // TODO: inject #region Versions diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentBlueprintRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentBlueprintRepository.cs index c654e1a6c2..d137d7ac76 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentBlueprintRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentBlueprintRepository.cs @@ -2,7 +2,6 @@ using Umbraco.Core.Cache; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Logging; -using Umbraco.Core.PropertyEditors; using Umbraco.Core.Scoping; namespace Umbraco.Core.Persistence.Repositories.Implement @@ -18,8 +17,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement /// internal class DocumentBlueprintRepository : DocumentRepository, IDocumentBlueprintRepository { - public DocumentBlueprintRepository(IScopeAccessor scopeAccessor, AppCaches appCaches, ILogger logger, IContentTypeRepository contentTypeRepository, ITemplateRepository templateRepository, ITagRepository tagRepository, ILanguageRepository languageRepository, PropertyEditorCollection propertyEditors) - : base(scopeAccessor, appCaches, logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, propertyEditors) + public DocumentBlueprintRepository(IScopeAccessor scopeAccessor, AppCaches appCaches, ILogger logger, IContentTypeRepository contentTypeRepository, ITemplateRepository templateRepository, ITagRepository tagRepository, ILanguageRepository languageRepository) + : base(scopeAccessor, appCaches, logger, contentTypeRepository, templateRepository, tagRepository, languageRepository) { } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs index eb392e86cb..2649b9993f 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs @@ -12,7 +12,6 @@ using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Factories; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.SqlSyntax; -using Umbraco.Core.PropertyEditors; using Umbraco.Core.Scoping; using Umbraco.Core.Services; @@ -31,8 +30,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement private readonly ContentByGuidReadRepository _contentByGuidReadRepository; private readonly IScopeAccessor _scopeAccessor; - public DocumentRepository(IScopeAccessor scopeAccessor, AppCaches appCaches, ILogger logger, IContentTypeRepository contentTypeRepository, ITemplateRepository templateRepository, ITagRepository tagRepository, ILanguageRepository languageRepository, PropertyEditorCollection propertyEditors) - : base(scopeAccessor, appCaches, languageRepository, logger, propertyEditors) + public DocumentRepository(IScopeAccessor scopeAccessor, AppCaches appCaches, ILogger logger, IContentTypeRepository contentTypeRepository, ITemplateRepository templateRepository, ITagRepository tagRepository, ILanguageRepository languageRepository) + : base(scopeAccessor, appCaches, languageRepository, logger) { _contentTypeRepository = contentTypeRepository ?? throw new ArgumentNullException(nameof(contentTypeRepository)); _templateRepository = templateRepository ?? throw new ArgumentNullException(nameof(templateRepository)); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs index ed3eb589bd..25828b8126 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs @@ -12,7 +12,6 @@ using Umbraco.Core.Models.Entities; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Factories; using Umbraco.Core.Persistence.Querying; -using Umbraco.Core.PropertyEditors; using Umbraco.Core.Scoping; using Umbraco.Core.Services; using static Umbraco.Core.Persistence.NPocoSqlExtensions.Statics; @@ -28,8 +27,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement private readonly ITagRepository _tagRepository; private readonly MediaByGuidReadRepository _mediaByGuidReadRepository; - public MediaRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IMediaTypeRepository mediaTypeRepository, ITagRepository tagRepository, ILanguageRepository languageRepository, PropertyEditorCollection propertyEditors) - : base(scopeAccessor, cache, languageRepository, logger, propertyEditors) + public MediaRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IMediaTypeRepository mediaTypeRepository, ITagRepository tagRepository, ILanguageRepository languageRepository) + : base(scopeAccessor, cache, languageRepository, logger) { _mediaTypeRepository = mediaTypeRepository ?? throw new ArgumentNullException(nameof(mediaTypeRepository)); _tagRepository = tagRepository ?? throw new ArgumentNullException(nameof(tagRepository)); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs index 4aaa064976..892122dff9 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs @@ -10,7 +10,6 @@ using Umbraco.Core.Models.Entities; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Factories; using Umbraco.Core.Persistence.Querying; -using Umbraco.Core.PropertyEditors; using Umbraco.Core.Scoping; using Umbraco.Core.Services; using static Umbraco.Core.Persistence.NPocoSqlExtensions.Statics; @@ -26,8 +25,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement private readonly ITagRepository _tagRepository; private readonly IMemberGroupRepository _memberGroupRepository; - public MemberRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IMemberTypeRepository memberTypeRepository, IMemberGroupRepository memberGroupRepository, ITagRepository tagRepository, ILanguageRepository languageRepository, PropertyEditorCollection propertyEditors) - : base(scopeAccessor, cache, languageRepository, logger, propertyEditors) + public MemberRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IMemberTypeRepository memberTypeRepository, IMemberGroupRepository memberGroupRepository, ITagRepository tagRepository, ILanguageRepository languageRepository) + : base(scopeAccessor, cache, languageRepository, logger) { _memberTypeRepository = memberTypeRepository ?? throw new ArgumentNullException(nameof(memberTypeRepository)); _tagRepository = tagRepository ?? throw new ArgumentNullException(nameof(tagRepository)); diff --git a/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs index b8b1b03fc4..f953b9cce6 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs @@ -8,7 +8,6 @@ using Umbraco.Core.Models; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Repositories.Implement; -using Umbraco.Core.PropertyEditors; using Umbraco.Core.Scoping; using Umbraco.Tests.TestHelpers; using Umbraco.Tests.TestHelpers.Entities; @@ -36,7 +35,7 @@ namespace Umbraco.Tests.Persistence.Repositories var commonRepository = new ContentTypeCommonRepository(scopeAccessor, templateRepository, AppCaches.Disabled); contentTypeRepository = new ContentTypeRepository(scopeAccessor, AppCaches.Disabled, Logger, commonRepository, langRepository); var languageRepository = new LanguageRepository(scopeAccessor, AppCaches.Disabled, Logger); - var repository = new DocumentRepository(scopeAccessor, AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, Factory.GetInstance()); + var repository = new DocumentRepository(scopeAccessor, AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository); return repository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/DocumentRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/DocumentRepositoryTest.cs index 424ab145bc..4d62ec8301 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/DocumentRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/DocumentRepositoryTest.cs @@ -69,7 +69,7 @@ namespace Umbraco.Tests.Persistence.Repositories var commonRepository = new ContentTypeCommonRepository(scopeAccessor, templateRepository, appCaches); var languageRepository = new LanguageRepository(scopeAccessor, appCaches, Logger); contentTypeRepository = new ContentTypeRepository(scopeAccessor, appCaches, Logger, commonRepository, languageRepository); - var repository = new DocumentRepository(scopeAccessor, appCaches, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, Factory.GetInstance()); + var repository = new DocumentRepository(scopeAccessor, appCaches, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository); return repository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/DomainRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/DomainRepositoryTest.cs index 65b5e8365e..628f8d75a7 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/DomainRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/DomainRepositoryTest.cs @@ -3,10 +3,9 @@ using System.Data; using System.Linq; using Moq; using NUnit.Framework; -using Umbraco.Core; +using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Models; using Umbraco.Core.Persistence.Repositories.Implement; -using Umbraco.Core.PropertyEditors; using Umbraco.Core.Scoping; using Umbraco.Tests.TestHelpers; using Umbraco.Tests.TestHelpers.Entities; @@ -26,7 +25,7 @@ namespace Umbraco.Tests.Persistence.Repositories var commonRepository = new ContentTypeCommonRepository(accessor, templateRepository, AppCaches); languageRepository = new LanguageRepository(accessor, Core.Cache.AppCaches.Disabled, Logger); contentTypeRepository = new ContentTypeRepository(accessor, Core.Cache.AppCaches.Disabled, Logger, commonRepository, languageRepository); - documentRepository = new DocumentRepository(accessor, Core.Cache.AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, Factory.GetInstance()); + documentRepository = new DocumentRepository(accessor, Core.Cache.AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository); var domainRepository = new DomainRepository(accessor, Core.Cache.AppCaches.Disabled, Logger); return domainRepository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs index 462c44d325..e2123df9e3 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs @@ -4,7 +4,7 @@ using Moq; using NUnit.Framework; using Umbraco.Core.Cache; using Umbraco.Core.Configuration.UmbracoSettings; -using Umbraco.Core; +using Umbraco.Core.IO; using Umbraco.Core.Models; using Umbraco.Core.Models.Entities; using Umbraco.Core.Persistence; @@ -17,7 +17,6 @@ using Umbraco.Core.Persistence.Repositories.Implement; using Umbraco.Core.Scoping; using Umbraco.Tests.Testing; using Umbraco.Core.Services; -using Umbraco.Core.PropertyEditors; namespace Umbraco.Tests.Persistence.Repositories { @@ -42,7 +41,7 @@ namespace Umbraco.Tests.Persistence.Repositories var languageRepository = new LanguageRepository(scopeAccessor, appCaches, Logger); mediaTypeRepository = new MediaTypeRepository(scopeAccessor, appCaches, Logger, commonRepository, languageRepository); var tagRepository = new TagRepository(scopeAccessor, appCaches, Logger); - var repository = new MediaRepository(scopeAccessor, appCaches, Logger, mediaTypeRepository, tagRepository, Mock.Of(), Factory.GetInstance()); + var repository = new MediaRepository(scopeAccessor, appCaches, Logger, mediaTypeRepository, tagRepository, Mock.Of()); return repository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs index 0b23240615..17b16ad7ab 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs @@ -15,7 +15,6 @@ using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Persistence.Repositories.Implement; -using Umbraco.Core.PropertyEditors; using Umbraco.Core.Scoping; using Umbraco.Tests.TestHelpers; using Umbraco.Tests.TestHelpers.Entities; @@ -36,7 +35,7 @@ namespace Umbraco.Tests.Persistence.Repositories memberTypeRepository = new MemberTypeRepository(accessor, AppCaches.Disabled, Logger, commonRepository, languageRepository); memberGroupRepository = new MemberGroupRepository(accessor, AppCaches.Disabled, Logger); var tagRepo = new TagRepository(accessor, AppCaches.Disabled, Logger); - var repository = new MemberRepository(accessor, AppCaches.Disabled, Logger, memberTypeRepository, memberGroupRepository, tagRepo, Mock.Of(), Factory.GetInstance()); + var repository = new MemberRepository(accessor, AppCaches.Disabled, Logger, memberTypeRepository, memberGroupRepository, tagRepo, Mock.Of()); return repository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/PublicAccessRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/PublicAccessRepositoryTest.cs index 3398e7d24d..56041c24aa 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/PublicAccessRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/PublicAccessRepositoryTest.cs @@ -3,11 +3,10 @@ using System.Collections.Generic; using System.Linq; using Moq; using NUnit.Framework; -using Umbraco.Core; +using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Models; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Repositories.Implement; -using Umbraco.Core.PropertyEditors; using Umbraco.Core.Scoping; using Umbraco.Tests.TestHelpers; using Umbraco.Tests.TestHelpers.Entities; @@ -311,7 +310,7 @@ namespace Umbraco.Tests.Persistence.Repositories var commonRepository = new ContentTypeCommonRepository(accessor, templateRepository, AppCaches); var languageRepository = new LanguageRepository(accessor, AppCaches, Logger); contentTypeRepository = new ContentTypeRepository(accessor, AppCaches, Logger, commonRepository, languageRepository); - var repository = new DocumentRepository(accessor, AppCaches, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, Factory.GetInstance()); + var repository = new DocumentRepository(accessor, AppCaches, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository); return repository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/TagRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/TagRepositoryTest.cs index 9e7593f4ea..e3de2c2892 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/TagRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/TagRepositoryTest.cs @@ -2,12 +2,11 @@ using Moq; using NUnit.Framework; using Umbraco.Core.Cache; -using Umbraco.Core; +using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.IO; using Umbraco.Core.Models; using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Persistence.Repositories.Implement; -using Umbraco.Core.PropertyEditors; using Umbraco.Core.Scoping; using Umbraco.Tests.TestHelpers; using Umbraco.Tests.TestHelpers.Entities; @@ -959,7 +958,7 @@ namespace Umbraco.Tests.Persistence.Repositories var commonRepository = new ContentTypeCommonRepository(accessor, templateRepository, AppCaches.Disabled); var languageRepository = new LanguageRepository(accessor, AppCaches.Disabled, Logger); contentTypeRepository = new ContentTypeRepository(accessor, AppCaches.Disabled, Logger, commonRepository, languageRepository); - var repository = new DocumentRepository(accessor, AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, Factory.GetInstance()); + var repository = new DocumentRepository(accessor, AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository); return repository; } @@ -971,7 +970,7 @@ namespace Umbraco.Tests.Persistence.Repositories var commonRepository = new ContentTypeCommonRepository(accessor, templateRepository, AppCaches.Disabled); var languageRepository = new LanguageRepository(accessor, AppCaches.Disabled, Logger); mediaTypeRepository = new MediaTypeRepository(accessor, AppCaches.Disabled, Logger, commonRepository, languageRepository); - var repository = new MediaRepository(accessor, AppCaches.Disabled, Logger, mediaTypeRepository, tagRepository, Mock.Of(), Factory.GetInstance()); + var repository = new MediaRepository(accessor, AppCaches.Disabled, Logger, mediaTypeRepository, tagRepository, Mock.Of()); return repository; } } diff --git a/src/Umbraco.Tests/Persistence/Repositories/TemplateRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/TemplateRepositoryTest.cs index 941ba84351..b0f9a5335b 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/TemplateRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/TemplateRepositoryTest.cs @@ -12,7 +12,6 @@ using Umbraco.Core.IO; using Umbraco.Core.Models; using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Persistence.Repositories.Implement; -using Umbraco.Core.PropertyEditors; using Umbraco.Core.Scoping; using Umbraco.Tests.TestHelpers; using Umbraco.Tests.TestHelpers.Entities; @@ -242,7 +241,7 @@ namespace Umbraco.Tests.Persistence.Repositories var commonRepository = new ContentTypeCommonRepository(ScopeProvider, templateRepository, AppCaches); var languageRepository = new LanguageRepository((IScopeAccessor) ScopeProvider, AppCaches.Disabled, Logger); var contentTypeRepository = new ContentTypeRepository((IScopeAccessor) ScopeProvider, AppCaches.Disabled, Logger, commonRepository, languageRepository); - var contentRepo = new DocumentRepository((IScopeAccessor) ScopeProvider, AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, Factory.GetInstance()); + var contentRepo = new DocumentRepository((IScopeAccessor) ScopeProvider, AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository); var contentType = MockedContentTypes.CreateSimpleContentType("umbTextpage2", "Textpage"); ServiceContext.FileService.SaveTemplate(contentType.DefaultTemplate); // else, FK violation on contentType! diff --git a/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs index c406a5c704..3e5919d7f3 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs @@ -14,7 +14,6 @@ using Umbraco.Tests.TestHelpers; using Umbraco.Tests.TestHelpers.Entities; using Umbraco.Tests.Testing; using Umbraco.Core.Persistence; -using Umbraco.Core.PropertyEditors; namespace Umbraco.Tests.Persistence.Repositories { @@ -30,7 +29,7 @@ namespace Umbraco.Tests.Persistence.Repositories var languageRepository = new LanguageRepository(accessor, AppCaches, Logger); mediaTypeRepository = new MediaTypeRepository(accessor, AppCaches, Mock.Of(), commonRepository, languageRepository); var tagRepository = new TagRepository(accessor, AppCaches, Mock.Of()); - var repository = new MediaRepository(accessor, AppCaches, Mock.Of(), mediaTypeRepository, tagRepository, Mock.Of(), Factory.GetInstance()); + var repository = new MediaRepository(accessor, AppCaches, Mock.Of(), mediaTypeRepository, tagRepository, Mock.Of()); return repository; } @@ -48,7 +47,7 @@ namespace Umbraco.Tests.Persistence.Repositories var commonRepository = new ContentTypeCommonRepository(accessor, templateRepository, AppCaches); var languageRepository = new LanguageRepository(accessor, AppCaches, Logger); contentTypeRepository = new ContentTypeRepository(accessor, AppCaches, Logger, commonRepository, languageRepository); - var repository = new DocumentRepository(accessor, AppCaches, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, Factory.GetInstance()); + var repository = new DocumentRepository(accessor, AppCaches, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository); return repository; } diff --git a/src/Umbraco.Tests/Services/ContentServicePerformanceTest.cs b/src/Umbraco.Tests/Services/ContentServicePerformanceTest.cs index c77bb71494..ef80672baf 100644 --- a/src/Umbraco.Tests/Services/ContentServicePerformanceTest.cs +++ b/src/Umbraco.Tests/Services/ContentServicePerformanceTest.cs @@ -14,7 +14,6 @@ using Umbraco.Core.Models; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Persistence.Repositories.Implement; -using Umbraco.Core.PropertyEditors; using Umbraco.Core.Scoping; using Umbraco.Tests.TestHelpers; using Umbraco.Tests.TestHelpers.Entities; @@ -169,7 +168,7 @@ namespace Umbraco.Tests.Services var commonRepository = new ContentTypeCommonRepository((IScopeAccessor)provider, tRepository, AppCaches); var languageRepository = new LanguageRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger); var ctRepository = new ContentTypeRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger, commonRepository, languageRepository); - var repository = new DocumentRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger, ctRepository, tRepository, tagRepo, languageRepository, Factory.GetInstance()); + var repository = new DocumentRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger, ctRepository, tRepository, tagRepo, languageRepository); // Act Stopwatch watch = Stopwatch.StartNew(); @@ -203,7 +202,7 @@ namespace Umbraco.Tests.Services var commonRepository = new ContentTypeCommonRepository((IScopeAccessor)provider, tRepository, AppCaches); var languageRepository = new LanguageRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger); var ctRepository = new ContentTypeRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger, commonRepository, languageRepository); - var repository = new DocumentRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger, ctRepository, tRepository, tagRepo, languageRepository, Factory.GetInstance()); + var repository = new DocumentRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger, ctRepository, tRepository, tagRepo, languageRepository); // Act Stopwatch watch = Stopwatch.StartNew(); @@ -235,7 +234,7 @@ namespace Umbraco.Tests.Services var commonRepository = new ContentTypeCommonRepository((IScopeAccessor) provider, tRepository, AppCaches); var languageRepository = new LanguageRepository((IScopeAccessor)provider, AppCaches.Disabled, Logger); var ctRepository = new ContentTypeRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger, commonRepository, languageRepository); - var repository = new DocumentRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger, ctRepository, tRepository, tagRepo, languageRepository, Factory.GetInstance()); + var repository = new DocumentRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger, ctRepository, tRepository, tagRepo, languageRepository); // Act var contents = repository.GetMany(); @@ -270,7 +269,7 @@ namespace Umbraco.Tests.Services var commonRepository = new ContentTypeCommonRepository((IScopeAccessor)provider, tRepository, AppCaches); var languageRepository = new LanguageRepository((IScopeAccessor)provider, AppCaches.Disabled, Logger); var ctRepository = new ContentTypeRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger, commonRepository, languageRepository); - var repository = new DocumentRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger, ctRepository, tRepository, tagRepo, languageRepository, Factory.GetInstance()); + var repository = new DocumentRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger, ctRepository, tRepository, tagRepo, languageRepository); // Act var contents = repository.GetMany(); diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs index 65c3b9f8a7..e26e764cd1 100644 --- a/src/Umbraco.Tests/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs @@ -3166,7 +3166,7 @@ namespace Umbraco.Tests.Services var commonRepository = new ContentTypeCommonRepository(accessor, templateRepository, AppCaches); var languageRepository = new LanguageRepository(accessor, AppCaches.Disabled, Logger); contentTypeRepository = new ContentTypeRepository(accessor, AppCaches.Disabled, Logger, commonRepository, languageRepository); - var repository = new DocumentRepository(accessor, AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, Factory.GetInstance()); + var repository = new DocumentRepository(accessor, AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository); return repository; } From 4f9e0fcb92e55757b7a0591fa7fa59e4ff257899 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 21 Oct 2019 23:53:14 +1100 Subject: [PATCH 005/202] No more using TemplateUtilities --- .../Implement/ContentRepositoryBase.cs | 3 +- .../PublishedContentTestBase.cs | 5 +- .../PublishedContent/PublishedContentTests.cs | 4 +- .../Web/InternalLinkParserTests.cs | 2 +- .../PropertyEditors/GridPropertyEditor.cs | 16 +- .../PropertyEditors/RichTextPropertyEditor.cs | 41 ++-- .../MarkdownEditorValueConverter.cs | 8 +- .../RteMacroRenderingValueConverter.cs | 13 +- .../TextStringValueConverter.cs | 8 +- src/Umbraco.Web/Runtime/WebInitialComposer.cs | 2 + .../Templates/InternalLinkParser.cs | 14 +- src/Umbraco.Web/Templates/MediaParser.cs | 186 +++++++++++++++ .../Templates/TemplateUtilities.cs | 213 ++---------------- src/Umbraco.Web/Templates/UrlParser.cs | 61 +++++ src/Umbraco.Web/Umbraco.Web.csproj | 2 + src/Umbraco.Web/UmbracoComponentRenderer.cs | 2 +- 16 files changed, 337 insertions(+), 243 deletions(-) create mode 100644 src/Umbraco.Web/Templates/MediaParser.cs create mode 100644 src/Umbraco.Web/Templates/UrlParser.cs diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs index 7ab73f3f2d..7ab9f10e1c 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs @@ -24,6 +24,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement internal sealed class ContentRepositoryBase { /// + /// /// This is used for unit tests ONLY /// public static bool ThrowOnWarning = false; @@ -43,7 +44,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected ILanguageRepository LanguageRepository { get; } - protected PropertyEditorCollection PropertyEditors => Current.PropertyEditors; // TODO: inject + protected PropertyEditorCollection PropertyEditors => Current.PropertyEditors; // TODO: inject ... this causes circular refs, not sure which refs they are though #region Versions diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentTestBase.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentTestBase.cs index f1e2bf20d6..1fa3384d08 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentTestBase.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentTestBase.cs @@ -11,6 +11,7 @@ using Umbraco.Core.Models; using Umbraco.Web.PropertyEditors; using Umbraco.Core.Services; using Umbraco.Web; +using Umbraco.Web.Templates; namespace Umbraco.Tests.PublishedContent { @@ -38,9 +39,11 @@ namespace Umbraco.Tests.PublishedContent base.Initialize(); var converters = Factory.GetInstance(); + var umbracoCtxAccessor = Mock.Of(); + var logger = Mock.Of(); var dataTypeService = new TestObjects.TestDataTypeService( - new DataType(new RichTextPropertyEditor(Mock.Of(), Mock.Of(), Mock.Of(), Mock.Of())) { Id = 1 }); + new DataType(new RichTextPropertyEditor(logger, umbracoCtxAccessor, new MediaParser(umbracoCtxAccessor, logger, Mock.Of(), Mock.Of()))) { Id = 1 }); var publishedContentTypeFactory = new PublishedContentTypeFactory(Mock.Of(), converters, dataTypeService); diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs index 6ef632bf90..d2f7283ee5 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs @@ -21,6 +21,7 @@ using Umbraco.Tests.TestHelpers; using Umbraco.Tests.Testing; using Umbraco.Web.Models.PublishedContent; using Umbraco.Web.PropertyEditors; +using Umbraco.Web.Templates; namespace Umbraco.Tests.PublishedContent { @@ -45,11 +46,12 @@ namespace Umbraco.Tests.PublishedContent var mediaService = Mock.Of(); var contentTypeBaseServiceProvider = Mock.Of(); var umbracoContextAccessor = Mock.Of(); + var mediaParser = new MediaParser(umbracoContextAccessor, logger, mediaService, contentTypeBaseServiceProvider); var dataTypeService = new TestObjects.TestDataTypeService( new DataType(new VoidEditor(logger)) { Id = 1 }, new DataType(new TrueFalsePropertyEditor(logger)) { Id = 1001 }, - new DataType(new RichTextPropertyEditor(logger, mediaService, contentTypeBaseServiceProvider, umbracoContextAccessor)) { Id = 1002 }, + new DataType(new RichTextPropertyEditor(logger, umbracoContextAccessor, mediaParser)) { Id = 1002 }, new DataType(new IntegerPropertyEditor(logger)) { Id = 1003 }, new DataType(new TextboxPropertyEditor(logger)) { Id = 1004 }, new DataType(new MediaPickerPropertyEditor(logger)) { Id = 1005 }); diff --git a/src/Umbraco.Tests/Web/InternalLinkParserTests.cs b/src/Umbraco.Tests/Web/InternalLinkParserTests.cs index 815ce408ee..6cdff240b8 100644 --- a/src/Umbraco.Tests/Web/InternalLinkParserTests.cs +++ b/src/Umbraco.Tests/Web/InternalLinkParserTests.cs @@ -79,7 +79,7 @@ namespace Umbraco.Tests.Web { var linkParser = new InternalLinkParser(umbracoContextAccessor); - var output = linkParser.ParseInternalLinks(input); + var output = linkParser.EnsureInternalLinks(input); Assert.AreEqual(result, output); } diff --git a/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs index f782f09289..bec28e33fd 100644 --- a/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs @@ -28,14 +28,16 @@ namespace Umbraco.Web.PropertyEditors private IMediaService _mediaService; private IContentTypeBaseServiceProvider _contentTypeBaseServiceProvider; private IUmbracoContextAccessor _umbracoContextAccessor; + private readonly MediaParser _mediaParser; private ILogger _logger; - public GridPropertyEditor(ILogger logger, IMediaService mediaService, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, IUmbracoContextAccessor umbracoContextAccessor) + public GridPropertyEditor(ILogger logger, IMediaService mediaService, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, IUmbracoContextAccessor umbracoContextAccessor, MediaParser mediaParser) : base(logger) { _mediaService = mediaService; _contentTypeBaseServiceProvider = contentTypeBaseServiceProvider; _umbracoContextAccessor = umbracoContextAccessor; + _mediaParser = mediaParser; _logger = logger; } @@ -45,7 +47,7 @@ namespace Umbraco.Web.PropertyEditors /// Overridden to ensure that the value is validated /// /// - protected override IDataValueEditor CreateValueEditor() => new GridPropertyValueEditor(Attribute, _mediaService, _contentTypeBaseServiceProvider, _umbracoContextAccessor, _logger); + protected override IDataValueEditor CreateValueEditor() => new GridPropertyValueEditor(Attribute, _mediaService, _contentTypeBaseServiceProvider, _umbracoContextAccessor, _logger, _mediaParser); protected override IConfigurationEditor CreateConfigurationEditor() => new GridConfigurationEditor(); @@ -55,14 +57,16 @@ namespace Umbraco.Web.PropertyEditors private IContentTypeBaseServiceProvider _contentTypeBaseServiceProvider; private IUmbracoContextAccessor _umbracoContextAccessor; private ILogger _logger; + private readonly MediaParser _mediaParser; - public GridPropertyValueEditor(DataEditorAttribute attribute, IMediaService mediaService, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, IUmbracoContextAccessor umbracoContextAccessor, ILogger logger) + public GridPropertyValueEditor(DataEditorAttribute attribute, IMediaService mediaService, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, IUmbracoContextAccessor umbracoContextAccessor, ILogger logger, MediaParser _mediaParser) : base(attribute) { _mediaService = mediaService; _contentTypeBaseServiceProvider = contentTypeBaseServiceProvider; _umbracoContextAccessor = umbracoContextAccessor; _logger = logger; + this._mediaParser = _mediaParser; } /// @@ -97,8 +101,8 @@ namespace Umbraco.Web.PropertyEditors // Parse the HTML var html = rte.Value?.ToString(); - var parseAndSavedTempImages = TemplateUtilities.FindAndPersistPastedTempImages(html, mediaParentId, userId, _mediaService, _contentTypeBaseServiceProvider, _logger); - var editorValueWithMediaUrlsRemoved = TemplateUtilities.RemoveMediaUrlsFromTextString(parseAndSavedTempImages); + var parseAndSavedTempImages = _mediaParser.FindAndPersistPastedTempImages(html, mediaParentId, userId); + var editorValueWithMediaUrlsRemoved = _mediaParser.RemoveImageSources(parseAndSavedTempImages); rte.Value = editorValueWithMediaUrlsRemoved; } @@ -127,7 +131,7 @@ namespace Umbraco.Web.PropertyEditors { var html = rte.Value?.ToString(); - var propertyValueWithMediaResolved = TemplateUtilities.ResolveMediaFromTextString(html); + var propertyValueWithMediaResolved = _mediaParser.EnsureImageSources(html); rte.Value = propertyValueWithMediaResolved; } diff --git a/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs index 3eed40c8bf..03dc7b6694 100644 --- a/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs @@ -24,27 +24,24 @@ namespace Umbraco.Web.PropertyEditors Icon = "icon-browser-window")] public class RichTextPropertyEditor : DataEditor { - private IMediaService _mediaService; - private IContentTypeBaseServiceProvider _contentTypeBaseServiceProvider; private IUmbracoContextAccessor _umbracoContextAccessor; - private ILogger _logger; + private readonly MediaParser _mediaParser; /// /// The constructor will setup the property editor based on the attribute if one is found /// - public RichTextPropertyEditor(ILogger logger, IMediaService mediaService, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, IUmbracoContextAccessor umbracoContextAccessor) : base(logger) + public RichTextPropertyEditor(ILogger logger, IUmbracoContextAccessor umbracoContextAccessor, MediaParser mediaParser) + : base(logger) { - _mediaService = mediaService; - _contentTypeBaseServiceProvider = contentTypeBaseServiceProvider; _umbracoContextAccessor = umbracoContextAccessor; - _logger = logger; + _mediaParser = mediaParser; } /// /// Create a custom value editor /// /// - protected override IDataValueEditor CreateValueEditor() => new RichTextPropertyValueEditor(Attribute, _mediaService, _contentTypeBaseServiceProvider, _umbracoContextAccessor, _logger); + protected override IDataValueEditor CreateValueEditor() => new RichTextPropertyValueEditor(Attribute, _umbracoContextAccessor, _mediaParser); protected override IConfigurationEditor CreateConfigurationEditor() => new RichTextConfigurationEditor(); @@ -53,20 +50,16 @@ namespace Umbraco.Web.PropertyEditors /// /// A custom value editor to ensure that macro syntax is parsed when being persisted and formatted correctly for display in the editor /// - internal class RichTextPropertyValueEditor : DataValueEditor + internal class RichTextPropertyValueEditor : DataValueEditor, IDataValueReference { - private IMediaService _mediaService; - private IContentTypeBaseServiceProvider _contentTypeBaseServiceProvider; private IUmbracoContextAccessor _umbracoContextAccessor; - private ILogger _logger; + private readonly MediaParser _mediaParser; - public RichTextPropertyValueEditor(DataEditorAttribute attribute, IMediaService mediaService, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, IUmbracoContextAccessor umbracoContextAccessor, ILogger logger) + public RichTextPropertyValueEditor(DataEditorAttribute attribute, IUmbracoContextAccessor umbracoContextAccessor, MediaParser _mediaParser) : base(attribute) { - _mediaService = mediaService; - _contentTypeBaseServiceProvider = contentTypeBaseServiceProvider; _umbracoContextAccessor = umbracoContextAccessor; - _logger = logger; + this._mediaParser = _mediaParser; } /// @@ -98,7 +91,7 @@ namespace Umbraco.Web.PropertyEditors if (val == null) return null; - var propertyValueWithMediaResolved = TemplateUtilities.ResolveMediaFromTextString(val.ToString()); + var propertyValueWithMediaResolved = _mediaParser.EnsureImageSources(val.ToString()); var parsed = MacroTagParser.FormatRichTextPersistedDataForEditor(propertyValueWithMediaResolved, new Dictionary()); return parsed; } @@ -120,12 +113,22 @@ namespace Umbraco.Web.PropertyEditors var mediaParent = config?.MediaParentId; var mediaParentId = mediaParent == null ? Guid.Empty : mediaParent.Guid; - var parseAndSavedTempImages = TemplateUtilities.FindAndPersistPastedTempImages(editorValue.Value.ToString(), mediaParentId, userId, _mediaService, _contentTypeBaseServiceProvider, _logger); - var editorValueWithMediaUrlsRemoved = TemplateUtilities.RemoveMediaUrlsFromTextString(parseAndSavedTempImages); + var parseAndSavedTempImages = _mediaParser.FindAndPersistPastedTempImages(editorValue.Value.ToString(), mediaParentId, userId); + var editorValueWithMediaUrlsRemoved = _mediaParser.RemoveImageSources(parseAndSavedTempImages); var parsed = MacroTagParser.FormatRichTextContentForPersistence(editorValueWithMediaUrlsRemoved); return parsed; } + + /// + /// Resolve references from values + /// + /// + /// + public IEnumerable GetReferences(object value) + { + throw new NotImplementedException(); + } } internal class RichTextPropertyIndexValueFactory : IPropertyIndexValueFactory diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/MarkdownEditorValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/MarkdownEditorValueConverter.cs index 98413c7b70..578a4cad06 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/MarkdownEditorValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/MarkdownEditorValueConverter.cs @@ -13,10 +13,12 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters public class MarkdownEditorValueConverter : PropertyValueConverterBase { private readonly InternalLinkParser _localLinkParser; + private readonly UrlParser _urlResolver; - public MarkdownEditorValueConverter(InternalLinkParser localLinkParser) + public MarkdownEditorValueConverter(InternalLinkParser localLinkParser, UrlParser urlResolver) { _localLinkParser = localLinkParser; + _urlResolver = urlResolver; } public override bool IsConverter(IPublishedPropertyType propertyType) @@ -34,8 +36,8 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters var sourceString = source.ToString(); // ensures string is parsed for {localLink} and urls are resolved correctly - sourceString = _localLinkParser.ParseInternalLinks(sourceString, preview); - sourceString = TemplateUtilities.ResolveUrlsFromTextString(sourceString); + sourceString = _localLinkParser.EnsureInternalLinks(sourceString, preview); + sourceString = _urlResolver.EnsureUrls(sourceString); return sourceString; } diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/RteMacroRenderingValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/RteMacroRenderingValueConverter.cs index 95cf2cfc85..88c1429b16 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/RteMacroRenderingValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/RteMacroRenderingValueConverter.cs @@ -25,6 +25,8 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters private readonly IUmbracoContextAccessor _umbracoContextAccessor; private readonly IMacroRenderer _macroRenderer; private readonly InternalLinkParser _internalLinkParser; + private readonly UrlParser _urlResolver; + private readonly MediaParser _mediaParser; public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) { @@ -33,11 +35,14 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters return PropertyCacheLevel.Snapshot; } - public RteMacroRenderingValueConverter(IUmbracoContextAccessor umbracoContextAccessor, IMacroRenderer macroRenderer, InternalLinkParser internalLinkParser) + public RteMacroRenderingValueConverter(IUmbracoContextAccessor umbracoContextAccessor, IMacroRenderer macroRenderer, + InternalLinkParser internalLinkParser, UrlParser urlResolver, MediaParser mediaParser) { _umbracoContextAccessor = umbracoContextAccessor; _macroRenderer = macroRenderer; _internalLinkParser = internalLinkParser; + _urlResolver = urlResolver; + _mediaParser = mediaParser; } // NOT thread-safe over a request because it modifies the @@ -83,9 +88,9 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters var sourceString = source.ToString(); // ensures string is parsed for {localLink} and urls and media are resolved correctly - sourceString = _internalLinkParser.ParseInternalLinks(sourceString, preview); - sourceString = TemplateUtilities.ResolveUrlsFromTextString(sourceString); - sourceString = TemplateUtilities.ResolveMediaFromTextString(sourceString); + sourceString = _internalLinkParser.EnsureInternalLinks(sourceString, preview); + sourceString = _urlResolver.EnsureUrls(sourceString); + sourceString = _mediaParser.EnsureImageSources(sourceString); // ensure string is parsed for macros and macros are executed correctly sourceString = RenderRteMacros(sourceString, preview); diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/TextStringValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/TextStringValueConverter.cs index ee49536d9d..1b85d6e608 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/TextStringValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/TextStringValueConverter.cs @@ -11,9 +11,10 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters [DefaultPropertyValueConverter] public class TextStringValueConverter : PropertyValueConverterBase { - public TextStringValueConverter(InternalLinkParser internalLinkParser) + public TextStringValueConverter(InternalLinkParser internalLinkParser, UrlParser urlParser) { _internalLinkParser = internalLinkParser; + _urlParser = urlParser; } private static readonly string[] PropertyTypeAliases = @@ -22,6 +23,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters Constants.PropertyEditors.Aliases.TextArea }; private readonly InternalLinkParser _internalLinkParser; + private readonly UrlParser _urlParser; public override bool IsConverter(IPublishedPropertyType propertyType) => PropertyTypeAliases.Contains(propertyType.EditorAlias); @@ -38,8 +40,8 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters var sourceString = source.ToString(); // ensures string is parsed for {localLink} and urls are resolved correctly - sourceString = _internalLinkParser.ParseInternalLinks(sourceString, preview); - sourceString = TemplateUtilities.ResolveUrlsFromTextString(sourceString); + sourceString = _internalLinkParser.EnsureInternalLinks(sourceString, preview); + sourceString = _urlParser.EnsureUrls(sourceString); return sourceString; } diff --git a/src/Umbraco.Web/Runtime/WebInitialComposer.cs b/src/Umbraco.Web/Runtime/WebInitialComposer.cs index 82137bbd9d..1b3128388d 100644 --- a/src/Umbraco.Web/Runtime/WebInitialComposer.cs +++ b/src/Umbraco.Web/Runtime/WebInitialComposer.cs @@ -108,6 +108,8 @@ namespace Umbraco.Web.Runtime composition.RegisterUnique(); composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(); // register the umbraco helper - this is Transient! very important! // also, if not level.Run, we cannot really use the helper (during upgrade...) diff --git a/src/Umbraco.Web/Templates/InternalLinkParser.cs b/src/Umbraco.Web/Templates/InternalLinkParser.cs index 0d8a480a90..32d7d42eac 100644 --- a/src/Umbraco.Web/Templates/InternalLinkParser.cs +++ b/src/Umbraco.Web/Templates/InternalLinkParser.cs @@ -23,17 +23,23 @@ namespace Umbraco.Web.Templates _umbracoContextAccessor = umbracoContextAccessor; } - public string ParseInternalLinks(string text, bool preview) + /// + /// Parses the string looking for the {localLink} syntax and updates them to their correct links. + /// + /// + /// + /// + public string EnsureInternalLinks(string text, bool preview) { if (_umbracoContextAccessor.UmbracoContext == null) throw new InvalidOperationException("Could not parse internal links, there is no current UmbracoContext"); if (!preview) - return ParseInternalLinks(text); + return EnsureInternalLinks(text); using (_umbracoContextAccessor.UmbracoContext.ForcedPreview(preview)) // force for url provider { - return ParseInternalLinks(text); + return EnsureInternalLinks(text); } } @@ -43,7 +49,7 @@ namespace Umbraco.Web.Templates /// /// /// - public string ParseInternalLinks(string text) + public string EnsureInternalLinks(string text) { if (_umbracoContextAccessor.UmbracoContext == null) throw new InvalidOperationException("Could not parse internal links, there is no current UmbracoContext"); diff --git a/src/Umbraco.Web/Templates/MediaParser.cs b/src/Umbraco.Web/Templates/MediaParser.cs new file mode 100644 index 0000000000..9a3f8def3c --- /dev/null +++ b/src/Umbraco.Web/Templates/MediaParser.cs @@ -0,0 +1,186 @@ +using HtmlAgilityPack; +using System; +using System.Collections.Generic; +using System.IO; +using System.Text.RegularExpressions; +using Umbraco.Core; +using Umbraco.Core.Exceptions; +using Umbraco.Core.IO; +using Umbraco.Core.Logging; +using Umbraco.Core.Models; +using Umbraco.Core.Services; + +namespace Umbraco.Web.Templates +{ + public sealed class MediaParser + { + public MediaParser(IUmbracoContextAccessor umbracoContextAccessor, ILogger logger, IMediaService mediaService, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider) + { + _umbracoContextAccessor = umbracoContextAccessor; + _logger = logger; + _mediaService = mediaService; + _contentTypeBaseServiceProvider = contentTypeBaseServiceProvider; + } + + private static readonly Regex ResolveImgPattern = new Regex(@"(]*src="")([^""\?]*)([^""]*""[^>]*data-udi="")([^""]*)(""[^>]*>)", + RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); + private readonly IUmbracoContextAccessor _umbracoContextAccessor; + private readonly ILogger _logger; + private readonly IMediaService _mediaService; + private readonly IContentTypeBaseServiceProvider _contentTypeBaseServiceProvider; + const string TemporaryImageDataAttribute = "data-tmpimg"; + + /// + /// Parses the string looking for Umbraco image tags and updates them to their up-to-date image sources. + /// + /// + /// + /// Umbraco image tags are identified by their data-udi attributes + public string EnsureImageSources(string text) + { + // don't attempt to proceed without a context + if (_umbracoContextAccessor?.UmbracoContext?.Media == null) + { + return text; + } + + return ResolveImgPattern.Replace(text, match => + { + // match groups: + // - 1 = from the beginning of the image tag until src attribute value begins + // - 2 = the src attribute value excluding the querystring (if present) + // - 3 = anything after group 2 and before the data-udi attribute value begins + // - 4 = the data-udi attribute value + // - 5 = anything after group 4 until the image tag is closed + var udi = match.Groups[4].Value; + if (udi.IsNullOrWhiteSpace() || GuidUdi.TryParse(udi, out var guidUdi) == false) + { + return match.Value; + } + var media = _umbracoContextAccessor?.UmbracoContext?.Media.GetById(guidUdi.Guid); + if (media == null) + { + // image does not exist - we could choose to remove the image entirely here (return empty string), + // but that would leave the editors completely in the dark as to why the image doesn't show + return match.Value; + } + + var url = media.Url; + return $"{match.Groups[1].Value}{url}{match.Groups[3].Value}{udi}{match.Groups[5].Value}"; + }); + } + + /// + /// Removes media urls from <img> tags where a data-udi attribute is present + /// + /// + /// + internal string RemoveImageSources(string text) + // see comment in ResolveMediaFromTextString for group reference + => ResolveImgPattern.Replace(text, "$1$3$4$5"); + + internal string FindAndPersistPastedTempImages(string html, Guid mediaParentFolder, int userId) + { + // Find all img's that has data-tmpimg attribute + // Use HTML Agility Pack - https://html-agility-pack.net + var htmlDoc = new HtmlDocument(); + htmlDoc.LoadHtml(html); + + var tmpImages = htmlDoc.DocumentNode.SelectNodes($"//img[@{TemporaryImageDataAttribute}]"); + if (tmpImages == null || tmpImages.Count == 0) + return html; + + // An array to contain a list of URLs that + // we have already processed to avoid dupes + var uploadedImages = new Dictionary(); + + foreach (var img in tmpImages) + { + // The data attribute contains the path to the tmp img to persist as a media item + var tmpImgPath = img.GetAttributeValue(TemporaryImageDataAttribute, string.Empty); + + if (string.IsNullOrEmpty(tmpImgPath)) + continue; + + var absoluteTempImagePath = IOHelper.MapPath(tmpImgPath); + var fileName = Path.GetFileName(absoluteTempImagePath); + var safeFileName = fileName.ToSafeFileName(); + + var mediaItemName = safeFileName.ToFriendlyName(); + IMedia mediaFile; + GuidUdi udi; + + if (uploadedImages.ContainsKey(tmpImgPath) == false) + { + if (mediaParentFolder == Guid.Empty) + mediaFile = _mediaService.CreateMedia(mediaItemName, Constants.System.Root, Constants.Conventions.MediaTypes.Image, userId); + else + mediaFile = _mediaService.CreateMedia(mediaItemName, mediaParentFolder, Constants.Conventions.MediaTypes.Image, userId); + + var fileInfo = new FileInfo(absoluteTempImagePath); + + var fileStream = fileInfo.OpenReadWithRetry(); + if (fileStream == null) throw new InvalidOperationException("Could not acquire file stream"); + using (fileStream) + { + mediaFile.SetValue(_contentTypeBaseServiceProvider, Constants.Conventions.Media.File, safeFileName, fileStream); + } + + _mediaService.Save(mediaFile, userId); + + udi = mediaFile.GetUdi(); + } + else + { + // Already been uploaded & we have it's UDI + udi = uploadedImages[tmpImgPath]; + } + + // Add the UDI to the img element as new data attribute + img.SetAttributeValue("data-udi", udi.ToString()); + + // Get the new persisted image url + var mediaTyped = _umbracoContextAccessor?.UmbracoContext?.Media.GetById(udi.Guid); + if (mediaTyped == null) + throw new PanicException($"Could not find media by id {udi.Guid} or there was no UmbracoContext available."); + + var location = mediaTyped.Url; + + // Find the width & height attributes as we need to set the imageprocessor QueryString + var width = img.GetAttributeValue("width", int.MinValue); + var height = img.GetAttributeValue("height", int.MinValue); + + if (width != int.MinValue && height != int.MinValue) + { + location = $"{location}?width={width}&height={height}&mode=max"; + } + + img.SetAttributeValue("src", location); + + // Remove the data attribute (so we do not re-process this) + img.Attributes.Remove(TemporaryImageDataAttribute); + + // Add to the dictionary to avoid dupes + if (uploadedImages.ContainsKey(tmpImgPath) == false) + { + uploadedImages.Add(tmpImgPath, udi); + + // Delete folder & image now its saved in media + // The folder should contain one image - as a unique guid folder created + // for each image uploaded from TinyMceController + var folderName = Path.GetDirectoryName(absoluteTempImagePath); + try + { + Directory.Delete(folderName, true); + } + catch (Exception ex) + { + _logger.Error(typeof(MediaParser), ex, "Could not delete temp file or folder {FileName}", absoluteTempImagePath); + } + } + } + + return htmlDoc.DocumentNode.OuterHtml; + } + } +} diff --git a/src/Umbraco.Web/Templates/TemplateUtilities.cs b/src/Umbraco.Web/Templates/TemplateUtilities.cs index 1092be73e2..d4bae38147 100644 --- a/src/Umbraco.Web/Templates/TemplateUtilities.cs +++ b/src/Umbraco.Web/Templates/TemplateUtilities.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Text.RegularExpressions; using Umbraco.Core; using Umbraco.Core.IO; using Umbraco.Core.Logging; @@ -16,19 +15,9 @@ using File = System.IO.File; namespace Umbraco.Web.Templates { - /// - /// Utility class used for templates - /// + [Obsolete("This class is obsolete, all methods have been moved to other classes such as InternalLinkHelper, UrlResolver and MediaParser")] public static class TemplateUtilities { - const string TemporaryImageDataAttribute = "data-tmpimg"; - - private static readonly Regex ResolveUrlPattern = new Regex("(=[\"\']?)(\\W?\\~(?:.(?![\"\']?\\s+(?:\\S+)=|[>\"\']))+.)[\"\']?", - RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); - - private static readonly Regex ResolveImgPattern = new Regex(@"(]*src="")([^""\?]*)([^""]*""[^>]*data-udi="")([^""]*)(""[^>]*>)", - RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); - [Obsolete("Inject and use an instance of InternalLinkParser instead")] internal static string ParseInternalLinks(string text, bool preview, UmbracoContext umbracoContext) { @@ -41,201 +30,27 @@ namespace Umbraco.Web.Templates } [Obsolete("Inject and use an instance of InternalLinkParser instead")] - public static string ParseInternalLinks(string text, UrlProvider urlProvider) => - Current.Factory.GetInstance().ParseInternalLinks(text); + public static string ParseInternalLinks(string text, UrlProvider urlProvider) + => Current.Factory.GetInstance().EnsureInternalLinks(text); - /// - /// The RegEx matches any HTML attribute values that start with a tilde (~), those that match are passed to ResolveUrl to replace the tilde with the application path. - /// - /// - /// - /// - /// When used with a Virtual-Directory set-up, this would resolve all URLs correctly. - /// The recommendation is that the "ResolveUrlsFromTextString" option (in umbracoSettings.config) is set to false for non-Virtual-Directory installs. - /// + [Obsolete("Inject and use an instance of UrlResolver")] public static string ResolveUrlsFromTextString(string text) - { - if (Current.Configs.Settings().Content.ResolveUrlsFromTextString == false) return text; - - using (var timer = Current.ProfilingLogger.DebugDuration(typeof(IOHelper), "ResolveUrlsFromTextString starting", "ResolveUrlsFromTextString complete")) - { - // find all relative urls (ie. urls that contain ~) - var tags = ResolveUrlPattern.Matches(text); - Current.Logger.Debug(typeof(IOHelper), "After regex: {Duration} matched: {TagsCount}", timer.Stopwatch.ElapsedMilliseconds, tags.Count); - foreach (Match tag in tags) - { - var url = ""; - if (tag.Groups[1].Success) - url = tag.Groups[1].Value; - - // The richtext editor inserts a slash in front of the url. That's why we need this little fix - // if (url.StartsWith("/")) - // text = text.Replace(url, ResolveUrl(url.Substring(1))); - // else - if (String.IsNullOrEmpty(url) == false) - { - var resolvedUrl = (url.Substring(0, 1) == "/") ? IOHelper.ResolveUrl(url.Substring(1)) : IOHelper.ResolveUrl(url); - text = text.Replace(url, resolvedUrl); - } - } - } - - return text; - } + => Current.Factory.GetInstance().EnsureUrls(text); + [Obsolete("Use StringExtensions.CleanForXss instead")] public static string CleanForXss(string text, params char[] ignoreFromClean) - { - return text.CleanForXss(ignoreFromClean); - } + => text.CleanForXss(ignoreFromClean); - /// - /// Parses the string looking for Umbraco image tags and updates them to their up-to-date image sources. - /// - /// - /// - /// Umbraco image tags are identified by their data-udi attributes + [Obsolete("Use MediaParser.EnsureImageSources instead")] public static string ResolveMediaFromTextString(string text) - { - // don't attempt to proceed without a context - if (Current.UmbracoContext == null || Current.UmbracoContext.Media == null) - { - return text; - } - - return ResolveImgPattern.Replace(text, match => - { - // match groups: - // - 1 = from the beginning of the image tag until src attribute value begins - // - 2 = the src attribute value excluding the querystring (if present) - // - 3 = anything after group 2 and before the data-udi attribute value begins - // - 4 = the data-udi attribute value - // - 5 = anything after group 4 until the image tag is closed - var udi = match.Groups[4].Value; - if(udi.IsNullOrWhiteSpace() || GuidUdi.TryParse(udi, out var guidUdi) == false) - { - return match.Value; - } - var media = Current.UmbracoContext.Media.GetById(guidUdi.Guid); - if(media == null) - { - // image does not exist - we could choose to remove the image entirely here (return empty string), - // but that would leave the editors completely in the dark as to why the image doesn't show - return match.Value; - } - - var url = media.Url; - return $"{match.Groups[1].Value}{url}{match.Groups[3].Value}{udi}{match.Groups[5].Value}"; - }); - } - - /// - /// Removes media urls from <img> tags where a data-udi attribute is present - /// - /// - /// + => Current.Factory.GetInstance().EnsureImageSources(text); + + [Obsolete("Use MediaParser.RemoveImageSources instead")] internal static string RemoveMediaUrlsFromTextString(string text) - // see comment in ResolveMediaFromTextString for group reference - => ResolveImgPattern.Replace(text, "$1$3$4$5"); + => Current.Factory.GetInstance().RemoveImageSources(text); + [Obsolete("Use MediaParser.RemoveImageSources instead")] internal static string FindAndPersistPastedTempImages(string html, Guid mediaParentFolder, int userId, IMediaService mediaService, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, ILogger logger) - { - // Find all img's that has data-tmpimg attribute - // Use HTML Agility Pack - https://html-agility-pack.net - var htmlDoc = new HtmlDocument(); - htmlDoc.LoadHtml(html); - - var tmpImages = htmlDoc.DocumentNode.SelectNodes($"//img[@{TemporaryImageDataAttribute}]"); - if (tmpImages == null || tmpImages.Count == 0) - return html; - - // An array to contain a list of URLs that - // we have already processed to avoid dupes - var uploadedImages = new Dictionary(); - - foreach (var img in tmpImages) - { - // The data attribute contains the path to the tmp img to persist as a media item - var tmpImgPath = img.GetAttributeValue(TemporaryImageDataAttribute, string.Empty); - - if (string.IsNullOrEmpty(tmpImgPath)) - continue; - - var absoluteTempImagePath = IOHelper.MapPath(tmpImgPath); - var fileName = Path.GetFileName(absoluteTempImagePath); - var safeFileName = fileName.ToSafeFileName(); - - var mediaItemName = safeFileName.ToFriendlyName(); - IMedia mediaFile; - GuidUdi udi; - - if (uploadedImages.ContainsKey(tmpImgPath) == false) - { - if (mediaParentFolder == Guid.Empty) - mediaFile = mediaService.CreateMedia(mediaItemName, Constants.System.Root, Constants.Conventions.MediaTypes.Image, userId); - else - mediaFile = mediaService.CreateMedia(mediaItemName, mediaParentFolder, Constants.Conventions.MediaTypes.Image, userId); - - var fileInfo = new FileInfo(absoluteTempImagePath); - - var fileStream = fileInfo.OpenReadWithRetry(); - if (fileStream == null) throw new InvalidOperationException("Could not acquire file stream"); - using (fileStream) - { - mediaFile.SetValue(contentTypeBaseServiceProvider, Constants.Conventions.Media.File, safeFileName, fileStream); - } - - mediaService.Save(mediaFile, userId); - - udi = mediaFile.GetUdi(); - } - else - { - // Already been uploaded & we have it's UDI - udi = uploadedImages[tmpImgPath]; - } - - // Add the UDI to the img element as new data attribute - img.SetAttributeValue("data-udi", udi.ToString()); - - // Get the new persisted image url - var mediaTyped = Current.UmbracoHelper.Media(udi.Guid); - var location = mediaTyped.Url; - - // Find the width & height attributes as we need to set the imageprocessor QueryString - var width = img.GetAttributeValue("width", int.MinValue); - var height = img.GetAttributeValue("height", int.MinValue); - - if(width != int.MinValue && height != int.MinValue) - { - location = $"{location}?width={width}&height={height}&mode=max"; - } - - img.SetAttributeValue("src", location); - - // Remove the data attribute (so we do not re-process this) - img.Attributes.Remove(TemporaryImageDataAttribute); - - // Add to the dictionary to avoid dupes - if(uploadedImages.ContainsKey(tmpImgPath) == false) - { - uploadedImages.Add(tmpImgPath, udi); - - // Delete folder & image now its saved in media - // The folder should contain one image - as a unique guid folder created - // for each image uploaded from TinyMceController - var folderName = Path.GetDirectoryName(absoluteTempImagePath); - try - { - Directory.Delete(folderName, true); - } - catch (Exception ex) - { - logger.Error(typeof(TemplateUtilities), ex, "Could not delete temp file or folder {FileName}", absoluteTempImagePath); - } - } - } - - return htmlDoc.DocumentNode.OuterHtml; - } + => Current.Factory.GetInstance().FindAndPersistPastedTempImages(html, mediaParentFolder, userId); } } diff --git a/src/Umbraco.Web/Templates/UrlParser.cs b/src/Umbraco.Web/Templates/UrlParser.cs new file mode 100644 index 0000000000..e5c40b7365 --- /dev/null +++ b/src/Umbraco.Web/Templates/UrlParser.cs @@ -0,0 +1,61 @@ +using System.Text.RegularExpressions; +using Umbraco.Core.Configuration.UmbracoSettings; +using Umbraco.Core.IO; +using Umbraco.Core.Logging; + +namespace Umbraco.Web.Templates +{ + public sealed class UrlParser + { + private readonly IContentSection _contentSection; + private readonly IProfilingLogger _logger; + + private static readonly Regex ResolveUrlPattern = new Regex("(=[\"\']?)(\\W?\\~(?:.(?![\"\']?\\s+(?:\\S+)=|[>\"\']))+.)[\"\']?", + RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); + + public UrlParser(IContentSection contentSection, IProfilingLogger logger) + { + _contentSection = contentSection; + _logger = logger; + } + + /// + /// The RegEx matches any HTML attribute values that start with a tilde (~), those that match are passed to ResolveUrl to replace the tilde with the application path. + /// + /// + /// + /// + /// When used with a Virtual-Directory set-up, this would resolve all URLs correctly. + /// The recommendation is that the "ResolveUrlsFromTextString" option (in umbracoSettings.config) is set to false for non-Virtual-Directory installs. + /// + public string EnsureUrls(string text) + { + if (_contentSection.ResolveUrlsFromTextString == false) return text; + + using (var timer = _logger.DebugDuration(typeof(IOHelper), "ResolveUrlsFromTextString starting", "ResolveUrlsFromTextString complete")) + { + // find all relative urls (ie. urls that contain ~) + var tags = ResolveUrlPattern.Matches(text); + _logger.Debug(typeof(IOHelper), "After regex: {Duration} matched: {TagsCount}", timer.Stopwatch.ElapsedMilliseconds, tags.Count); + foreach (Match tag in tags) + { + var url = ""; + if (tag.Groups[1].Success) + url = tag.Groups[1].Value; + + // The richtext editor inserts a slash in front of the url. That's why we need this little fix + // if (url.StartsWith("/")) + // text = text.Replace(url, ResolveUrl(url.Substring(1))); + // else + if (string.IsNullOrEmpty(url) == false) + { + var resolvedUrl = (url.Substring(0, 1) == "/") ? IOHelper.ResolveUrl(url.Substring(1)) : IOHelper.ResolveUrl(url); + text = text.Replace(url, resolvedUrl); + } + } + } + + return text; + } + } +} diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 616ed908e1..74ac3f65f3 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -248,6 +248,8 @@ + + diff --git a/src/Umbraco.Web/UmbracoComponentRenderer.cs b/src/Umbraco.Web/UmbracoComponentRenderer.cs index 805b9267f9..c0f83fd1af 100644 --- a/src/Umbraco.Web/UmbracoComponentRenderer.cs +++ b/src/Umbraco.Web/UmbracoComponentRenderer.cs @@ -159,7 +159,7 @@ namespace Umbraco.Web _umbracoContextAccessor.UmbracoContext.HttpContext.Response.ContentType = contentType; //Now, we need to ensure that local links are parsed - html = _internalLinkParser.ParseInternalLinks(output.ToString()); + html = _internalLinkParser.EnsureInternalLinks(output.ToString()); } } From f8e04eb1d624fbefcb39b889a2babeb7bbe255bb Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 22 Oct 2019 00:53:52 +1100 Subject: [PATCH 006/202] Adds tests for MediaParser and simplifies UmbracoContext mocking --- .../PropertyEditors/ImageCropperTest.cs | 1 + .../Testing/Objects/TestDataSource.cs | 3 + .../Objects/TestUmbracoContextFactory.cs | 49 ++++++++++ src/Umbraco.Tests/Umbraco.Tests.csproj | 2 + .../Web/InternalLinkParserTests.cs | 46 ++++----- src/Umbraco.Tests/Web/MediaParserTests.cs | 97 +++++++++++++++++++ src/Umbraco.Web/Templates/MediaParser.cs | 12 ++- 7 files changed, 176 insertions(+), 34 deletions(-) create mode 100644 src/Umbraco.Tests/Testing/Objects/TestUmbracoContextFactory.cs create mode 100644 src/Umbraco.Tests/Web/MediaParserTests.cs diff --git a/src/Umbraco.Tests/PropertyEditors/ImageCropperTest.cs b/src/Umbraco.Tests/PropertyEditors/ImageCropperTest.cs index 8d2ab84d35..433ba64b38 100644 --- a/src/Umbraco.Tests/PropertyEditors/ImageCropperTest.cs +++ b/src/Umbraco.Tests/PropertyEditors/ImageCropperTest.cs @@ -24,6 +24,7 @@ using Umbraco.Web.PropertyEditors; namespace Umbraco.Tests.PropertyEditors { + [TestFixture] public class ImageCropperTest { diff --git a/src/Umbraco.Tests/Testing/Objects/TestDataSource.cs b/src/Umbraco.Tests/Testing/Objects/TestDataSource.cs index 0291715e46..4476a7464e 100644 --- a/src/Umbraco.Tests/Testing/Objects/TestDataSource.cs +++ b/src/Umbraco.Tests/Testing/Objects/TestDataSource.cs @@ -1,12 +1,15 @@ using System; using System.Collections.Generic; using System.Linq; +using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Scoping; +using Umbraco.Web; using Umbraco.Web.PublishedCache.NuCache; using Umbraco.Web.PublishedCache.NuCache.DataSource; namespace Umbraco.Tests.Testing.Objects { + internal class TestDataSource : IDataSource { public TestDataSource(params ContentNodeKit[] kits) diff --git a/src/Umbraco.Tests/Testing/Objects/TestUmbracoContextFactory.cs b/src/Umbraco.Tests/Testing/Objects/TestUmbracoContextFactory.cs new file mode 100644 index 0000000000..7f891a2580 --- /dev/null +++ b/src/Umbraco.Tests/Testing/Objects/TestUmbracoContextFactory.cs @@ -0,0 +1,49 @@ +using Moq; +using Umbraco.Core.Configuration; +using Umbraco.Core.Configuration.UmbracoSettings; +using Umbraco.Core.Services; +using Umbraco.Tests.TestHelpers; +using Umbraco.Tests.Testing.Objects.Accessors; +using Umbraco.Web; +using Umbraco.Web.PublishedCache; +using Umbraco.Web.Routing; + +namespace Umbraco.Tests.Testing.Objects +{ + /// + /// Simplify creating test UmbracoContext's + /// + public class TestUmbracoContextFactory + { + public static IUmbracoContextFactory Create(IGlobalSettings globalSettings = null, IUrlProvider urlProvider = null, + IMediaUrlProvider mediaUrlProvider = null, + IUmbracoContextAccessor umbracoContextAccessor = null) + { + if (globalSettings == null) globalSettings = SettingsForTests.GenerateMockGlobalSettings(); + if (urlProvider == null) urlProvider = Mock.Of(); + if (mediaUrlProvider == null) mediaUrlProvider = Mock.Of(); + if (umbracoContextAccessor == null) umbracoContextAccessor = new TestUmbracoContextAccessor(); + + var contentCache = new Mock(); + var mediaCache = new Mock(); + var snapshot = new Mock(); + snapshot.Setup(x => x.Content).Returns(contentCache.Object); + snapshot.Setup(x => x.Media).Returns(mediaCache.Object); + var snapshotService = new Mock(); + snapshotService.Setup(x => x.CreatePublishedSnapshot(It.IsAny())).Returns(snapshot.Object); + + var umbracoContextFactory = new UmbracoContextFactory( + umbracoContextAccessor, + snapshotService.Object, + new TestVariationContextAccessor(), + new TestDefaultCultureAccessor(), + Mock.Of(section => section.WebRouting == Mock.Of(routingSection => routingSection.UrlProviderMode == "Auto")), + globalSettings, + new UrlProviderCollection(new[] { urlProvider }), + new MediaUrlProviderCollection(new[] { mediaUrlProvider }), + Mock.Of()); + + return umbracoContextFactory; + } + } +} diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index e73caf4517..20b29a3147 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -212,6 +212,7 @@ + @@ -254,6 +255,7 @@ + diff --git a/src/Umbraco.Tests/Web/InternalLinkParserTests.cs b/src/Umbraco.Tests/Web/InternalLinkParserTests.cs index 6cdff240b8..0a948a8617 100644 --- a/src/Umbraco.Tests/Web/InternalLinkParserTests.cs +++ b/src/Umbraco.Tests/Web/InternalLinkParserTests.cs @@ -8,6 +8,7 @@ using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Services; using Umbraco.Tests.TestHelpers; +using Umbraco.Tests.Testing.Objects; using Umbraco.Tests.Testing.Objects.Accessors; using Umbraco.Web; using Umbraco.Web.PublishedCache; @@ -16,6 +17,7 @@ using Umbraco.Web.Templates; namespace Umbraco.Tests.Web { + [TestFixture] public class InternalLinkParserTests { @@ -29,54 +31,40 @@ namespace Umbraco.Tests.Web [TestCase("hello href=\"{localLink:umb://document-type/9931BDE0-AAC3-4BAB-B838-909A7B47570E}\" world ", "hello href=\"#\" world ")] public void ParseLocalLinks(string input, string result) { - var serviceCtxMock = new TestObjects(null).GetServiceContextMock(); - //setup a mock url provider which we'll use for testing - var testUrlProvider = new Mock(); - testUrlProvider + var contentUrlProvider = new Mock(); + contentUrlProvider .Setup(x => x.GetUrl(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(UrlInfo.Url("/my-test-url")); - - var globalSettings = SettingsForTests.GenerateMockGlobalSettings(); - var contentType = new PublishedContentType(666, "alias", PublishedItemType.Content, Enumerable.Empty(), Enumerable.Empty(), ContentVariation.Nothing); var publishedContent = new Mock(); publishedContent.Setup(x => x.Id).Returns(1234); publishedContent.Setup(x => x.ContentType).Returns(contentType); - var contentCache = new Mock(); - contentCache.Setup(x => x.GetById(It.IsAny())).Returns(publishedContent.Object); - contentCache.Setup(x => x.GetById(It.IsAny())).Returns(publishedContent.Object); + var mediaType = new PublishedContentType(777, "image", PublishedItemType.Media, Enumerable.Empty(), Enumerable.Empty(), ContentVariation.Nothing); var media = new Mock(); - media.Setup(x => x.Url).Returns("/media/1001/my-image.jpg"); media.Setup(x => x.ContentType).Returns(mediaType); - var mediaCache = new Mock(); - mediaCache.Setup(x => x.GetById(It.IsAny())).Returns(media.Object); - mediaCache.Setup(x => x.GetById(It.IsAny())).Returns(media.Object); - var snapshot = new Mock(); - snapshot.Setup(x => x.Content).Returns(contentCache.Object); - snapshot.Setup(x => x.Media).Returns(mediaCache.Object); - var snapshotService = new Mock(); - snapshotService.Setup(x => x.CreatePublishedSnapshot(It.IsAny())).Returns(snapshot.Object); var mediaUrlProvider = new Mock(); mediaUrlProvider.Setup(x => x.GetMediaUrl(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(UrlInfo.Url("/media/1001/my-image.jpg")); var umbracoContextAccessor = new TestUmbracoContextAccessor(); - var umbracoContextFactory = new UmbracoContextFactory( - umbracoContextAccessor, - snapshotService.Object, - new TestVariationContextAccessor(), - new TestDefaultCultureAccessor(), - Mock.Of(section => section.WebRouting == Mock.Of(routingSection => routingSection.UrlProviderMode == "Auto")), - globalSettings, - new UrlProviderCollection(new[] { testUrlProvider.Object }), - new MediaUrlProviderCollection(new[] { mediaUrlProvider.Object }), - Mock.Of()); + var umbracoContextFactory = TestUmbracoContextFactory.Create( + urlProvider: contentUrlProvider.Object, + mediaUrlProvider: mediaUrlProvider.Object, + umbracoContextAccessor: umbracoContextAccessor); using (var reference = umbracoContextFactory.EnsureUmbracoContext(Mock.Of())) { + var contentCache = Mock.Get(reference.UmbracoContext.Content); + contentCache.Setup(x => x.GetById(It.IsAny())).Returns(publishedContent.Object); + contentCache.Setup(x => x.GetById(It.IsAny())).Returns(publishedContent.Object); + + var mediaCache = Mock.Get(reference.UmbracoContext.Media); + mediaCache.Setup(x => x.GetById(It.IsAny())).Returns(media.Object); + mediaCache.Setup(x => x.GetById(It.IsAny())).Returns(media.Object); + var linkParser = new InternalLinkParser(umbracoContextAccessor); var output = linkParser.EnsureInternalLinks(input); diff --git a/src/Umbraco.Tests/Web/MediaParserTests.cs b/src/Umbraco.Tests/Web/MediaParserTests.cs new file mode 100644 index 0000000000..7c9e7576b5 --- /dev/null +++ b/src/Umbraco.Tests/Web/MediaParserTests.cs @@ -0,0 +1,97 @@ +using Umbraco.Core.Logging; +using Moq; +using NUnit.Framework; +using Umbraco.Core.Services; +using Umbraco.Tests.Testing.Objects.Accessors; +using Umbraco.Web.Templates; +using Umbraco.Web; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Web.Routing; +using Umbraco.Tests.Testing.Objects; +using System.Web; +using System; +using System.Linq; +using Umbraco.Core.Models; + +namespace Umbraco.Tests.Web +{ + [TestFixture] + public class MediaParserTests + { + [Test] + public void Remove_Image_Sources() + { + var logger = Mock.Of(); + var umbracoContextAccessor = new TestUmbracoContextAccessor(); + var mediaParser = new MediaParser(umbracoContextAccessor, logger, Mock.Of(), Mock.Of()); + + var result = mediaParser.RemoveImageSources(@"

+

+ +

+

+

+

"); + + Assert.AreEqual(@"

+

+ +

+

+

+

", result); + } + + [Test] + public void Ensure_Image_Sources() + { + //setup a mock url provider which we'll use for testing + + var mediaType = new PublishedContentType(777, "image", PublishedItemType.Media, Enumerable.Empty(), Enumerable.Empty(), ContentVariation.Nothing); + var media = new Mock(); + media.Setup(x => x.ContentType).Returns(mediaType); + var mediaUrlProvider = new Mock(); + mediaUrlProvider.Setup(x => x.GetMediaUrl(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(UrlInfo.Url("/media/1001/my-image.jpg")); + + var umbracoContextAccessor = new TestUmbracoContextAccessor(); + + var umbracoContextFactory = TestUmbracoContextFactory.Create( + mediaUrlProvider: mediaUrlProvider.Object, + umbracoContextAccessor: umbracoContextAccessor); + + using (var reference = umbracoContextFactory.EnsureUmbracoContext(Mock.Of())) + { + var mediaCache = Mock.Get(reference.UmbracoContext.Media); + mediaCache.Setup(x => x.GetById(It.IsAny())).Returns(media.Object); + + var mediaParser = new MediaParser(umbracoContextAccessor, Mock.Of(), Mock.Of(), Mock.Of()); + + var result = mediaParser.EnsureImageSources(@"

+

+ +

+

+

+

+

+

+

"); + + Assert.AreEqual(@"

+

+ +

+

+

+

+

+

+

", result); + + } + + + } + } +} diff --git a/src/Umbraco.Web/Templates/MediaParser.cs b/src/Umbraco.Web/Templates/MediaParser.cs index 9a3f8def3c..4130b21da2 100644 --- a/src/Umbraco.Web/Templates/MediaParser.cs +++ b/src/Umbraco.Web/Templates/MediaParser.cs @@ -9,6 +9,7 @@ using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Services; +using Umbraco.Web.Routing; namespace Umbraco.Web.Templates { @@ -39,11 +40,13 @@ namespace Umbraco.Web.Templates public string EnsureImageSources(string text) { // don't attempt to proceed without a context - if (_umbracoContextAccessor?.UmbracoContext?.Media == null) + if (_umbracoContextAccessor?.UmbracoContext?.UrlProvider == null) { return text; } + var urlProvider = _umbracoContextAccessor.UmbracoContext.UrlProvider; + return ResolveImgPattern.Replace(text, match => { // match groups: @@ -57,16 +60,15 @@ namespace Umbraco.Web.Templates { return match.Value; } - var media = _umbracoContextAccessor?.UmbracoContext?.Media.GetById(guidUdi.Guid); - if (media == null) + var mediaUrl = urlProvider.GetMediaUrl(guidUdi.Guid); + if (mediaUrl == null) { // image does not exist - we could choose to remove the image entirely here (return empty string), // but that would leave the editors completely in the dark as to why the image doesn't show return match.Value; } - var url = media.Url; - return $"{match.Groups[1].Value}{url}{match.Groups[3].Value}{udi}{match.Groups[5].Value}"; + return $"{match.Groups[1].Value}{mediaUrl}{match.Groups[3].Value}{udi}{match.Groups[5].Value}"; }); } From bb2a3a5e3dc365b472a2e9f4a896bfd21b7256a3 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 22 Oct 2019 11:09:21 +1100 Subject: [PATCH 007/202] stub class for udi parser --- src/Umbraco.Web/Templates/MediaParser.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/Umbraco.Web/Templates/MediaParser.cs b/src/Umbraco.Web/Templates/MediaParser.cs index 4130b21da2..2d20602e8e 100644 --- a/src/Umbraco.Web/Templates/MediaParser.cs +++ b/src/Umbraco.Web/Templates/MediaParser.cs @@ -13,6 +13,22 @@ using Umbraco.Web.Routing; namespace Umbraco.Web.Templates { + /// + /// Parses out UDIs in strings + /// + public sealed class UdiParser + { + /// + /// Parses out UDIs from an html string based on 'data-udi' html attributes + /// + /// + /// + public IEnumerable ParseUdisFromDataAttributes(string text) + { + + } + } + public sealed class MediaParser { public MediaParser(IUmbracoContextAccessor umbracoContextAccessor, ILogger logger, IMediaService mediaService, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider) From 6bb8a15a878346a9188577a2cca966df1e927bdf Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 22 Oct 2019 15:07:01 +1100 Subject: [PATCH 008/202] Adds UdiParserTests --- .../{Web => Templates}/MediaParserTests.cs | 7 ++-- src/Umbraco.Tests/Templates/UdiParserTests.cs | 28 +++++++++++++++ src/Umbraco.Tests/Umbraco.Tests.csproj | 3 +- src/Umbraco.Web/Templates/MediaParser.cs | 15 -------- src/Umbraco.Web/Templates/UdiParser.cs | 34 +++++++++++++++++++ src/Umbraco.Web/Umbraco.Web.csproj | 1 + 6 files changed, 69 insertions(+), 19 deletions(-) rename src/Umbraco.Tests/{Web => Templates}/MediaParserTests.cs (98%) create mode 100644 src/Umbraco.Tests/Templates/UdiParserTests.cs create mode 100644 src/Umbraco.Web/Templates/UdiParser.cs diff --git a/src/Umbraco.Tests/Web/MediaParserTests.cs b/src/Umbraco.Tests/Templates/MediaParserTests.cs similarity index 98% rename from src/Umbraco.Tests/Web/MediaParserTests.cs rename to src/Umbraco.Tests/Templates/MediaParserTests.cs index 7c9e7576b5..f7b5933a52 100644 --- a/src/Umbraco.Tests/Web/MediaParserTests.cs +++ b/src/Umbraco.Tests/Templates/MediaParserTests.cs @@ -13,8 +13,9 @@ using System; using System.Linq; using Umbraco.Core.Models; -namespace Umbraco.Tests.Web +namespace Umbraco.Tests.Templates { + [TestFixture] public class MediaParserTests { @@ -46,7 +47,7 @@ namespace Umbraco.Tests.Web public void Ensure_Image_Sources() { //setup a mock url provider which we'll use for testing - + var mediaType = new PublishedContentType(777, "image", PublishedItemType.Media, Enumerable.Empty(), Enumerable.Empty(), ContentVariation.Nothing); var media = new Mock(); media.Setup(x => x.ContentType).Returns(mediaType); @@ -91,7 +92,7 @@ namespace Umbraco.Tests.Web } - + } } } diff --git a/src/Umbraco.Tests/Templates/UdiParserTests.cs b/src/Umbraco.Tests/Templates/UdiParserTests.cs new file mode 100644 index 0000000000..eca7f6c4f0 --- /dev/null +++ b/src/Umbraco.Tests/Templates/UdiParserTests.cs @@ -0,0 +1,28 @@ +using NUnit.Framework; +using Umbraco.Web.Templates; +using System.Linq; +using Umbraco.Core; + +namespace Umbraco.Tests.Templates +{ + [TestFixture] + public class UdiParserTests + { + [Test] + public void Returns_Udi_From_Data_Udi_Html_Attributes() + { + var input = @"

+

+ +
+

"; + + var parser = new UdiParser(); + var result = parser.ParseUdisFromDataAttributes(input).ToList(); + Assert.AreEqual(2, result.Count); + Assert.AreEqual(Udi.Parse("umb://media/D4B18427A1544721B09AC7692F35C264"), result[0]); + Assert.AreEqual(Udi.Parse("umb://media-type/B726D735E4C446D58F703F3FBCFC97A5"), result[1]); + } + + } +} diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 20b29a3147..519f36012c 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -159,6 +159,7 @@ + @@ -255,7 +256,7 @@ - + diff --git a/src/Umbraco.Web/Templates/MediaParser.cs b/src/Umbraco.Web/Templates/MediaParser.cs index 2d20602e8e..071c6a5696 100644 --- a/src/Umbraco.Web/Templates/MediaParser.cs +++ b/src/Umbraco.Web/Templates/MediaParser.cs @@ -13,21 +13,6 @@ using Umbraco.Web.Routing; namespace Umbraco.Web.Templates { - /// - /// Parses out UDIs in strings - /// - public sealed class UdiParser - { - /// - /// Parses out UDIs from an html string based on 'data-udi' html attributes - /// - /// - /// - public IEnumerable ParseUdisFromDataAttributes(string text) - { - - } - } public sealed class MediaParser { diff --git a/src/Umbraco.Web/Templates/UdiParser.cs b/src/Umbraco.Web/Templates/UdiParser.cs new file mode 100644 index 0000000000..8bb5f8c579 --- /dev/null +++ b/src/Umbraco.Web/Templates/UdiParser.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using Umbraco.Core; + +namespace Umbraco.Web.Templates +{ + /// + /// Parses out UDIs in strings + /// + public sealed class UdiParser + { + private static readonly Regex DataUdiAttributeRegex = new Regex(@"data-udi=\\?(?:""|')(?umb://[A-z0-9\-]+/[A-z0-9]+)\\?(?:""|')", + RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); + + /// + /// Parses out UDIs from an html string based on 'data-udi' html attributes + /// + /// + /// + public IEnumerable ParseUdisFromDataAttributes(string text) + { + var matches = DataUdiAttributeRegex.Matches(text); + if (matches.Count == 0) + yield break; + + foreach (Match match in matches) + { + if (match.Groups.Count == 2 && Udi.TryParse(match.Groups[1].Value, out var udi)) + yield return udi; + } + } + } +} diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 74ac3f65f3..276cdf9ebc 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -249,6 +249,7 @@ + From 1adf5a30f361c4d9b779c28af35229c79db1e68c Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 22 Oct 2019 15:48:47 +1100 Subject: [PATCH 009/202] Renames some stuff, updates RTE editor to return media/content refs and adds tests for locallink parsing --- .../PublishedContentTestBase.cs | 2 +- .../PublishedContent/PublishedContentTests.cs | 2 +- ...rserTests.cs => ImageSourceParserTests.cs} | 27 ++++++++- .../Templates/LocalLinkParserTests.cs | 34 +++++++++++ src/Umbraco.Tests/Templates/UdiParserTests.cs | 28 --------- src/Umbraco.Tests/Umbraco.Tests.csproj | 4 +- .../Web/InternalLinkParserTests.cs | 2 +- .../PropertyEditors/GridPropertyEditor.cs | 8 +-- .../PropertyEditors/RichTextPropertyEditor.cs | 12 ++-- .../MarkdownEditorValueConverter.cs | 4 +- .../RteMacroRenderingValueConverter.cs | 6 +- .../TextStringValueConverter.cs | 4 +- src/Umbraco.Web/Runtime/WebInitialComposer.cs | 4 +- .../{MediaParser.cs => ImageSourceParser.cs} | 27 ++++++++- ...ternalLinkParser.cs => LocalLinkParser.cs} | 58 +++++++++++++------ .../Templates/TemplateUtilities.cs | 8 +-- src/Umbraco.Web/Templates/UdiParser.cs | 34 ----------- src/Umbraco.Web/Umbraco.Web.csproj | 5 +- src/Umbraco.Web/UmbracoComponentRenderer.cs | 4 +- 19 files changed, 156 insertions(+), 117 deletions(-) rename src/Umbraco.Tests/Templates/{MediaParserTests.cs => ImageSourceParserTests.cs} (69%) create mode 100644 src/Umbraco.Tests/Templates/LocalLinkParserTests.cs delete mode 100644 src/Umbraco.Tests/Templates/UdiParserTests.cs rename src/Umbraco.Web/Templates/{MediaParser.cs => ImageSourceParser.cs} (86%) rename src/Umbraco.Web/Templates/{InternalLinkParser.cs => LocalLinkParser.cs} (63%) delete mode 100644 src/Umbraco.Web/Templates/UdiParser.cs diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentTestBase.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentTestBase.cs index 1fa3384d08..fec852f97a 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentTestBase.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentTestBase.cs @@ -43,7 +43,7 @@ namespace Umbraco.Tests.PublishedContent var logger = Mock.Of(); var dataTypeService = new TestObjects.TestDataTypeService( - new DataType(new RichTextPropertyEditor(logger, umbracoCtxAccessor, new MediaParser(umbracoCtxAccessor, logger, Mock.Of(), Mock.Of()))) { Id = 1 }); + new DataType(new RichTextPropertyEditor(logger, umbracoCtxAccessor, new ImageSourceParser(umbracoCtxAccessor, logger, Mock.Of(), Mock.Of()))) { Id = 1 }); var publishedContentTypeFactory = new PublishedContentTypeFactory(Mock.Of(), converters, dataTypeService); diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs index d2f7283ee5..95d00998a1 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs @@ -46,7 +46,7 @@ namespace Umbraco.Tests.PublishedContent var mediaService = Mock.Of(); var contentTypeBaseServiceProvider = Mock.Of(); var umbracoContextAccessor = Mock.Of(); - var mediaParser = new MediaParser(umbracoContextAccessor, logger, mediaService, contentTypeBaseServiceProvider); + var mediaParser = new ImageSourceParser(umbracoContextAccessor, logger, mediaService, contentTypeBaseServiceProvider); var dataTypeService = new TestObjects.TestDataTypeService( new DataType(new VoidEditor(logger)) { Id = 1 }, diff --git a/src/Umbraco.Tests/Templates/MediaParserTests.cs b/src/Umbraco.Tests/Templates/ImageSourceParserTests.cs similarity index 69% rename from src/Umbraco.Tests/Templates/MediaParserTests.cs rename to src/Umbraco.Tests/Templates/ImageSourceParserTests.cs index f7b5933a52..d383ab02df 100644 --- a/src/Umbraco.Tests/Templates/MediaParserTests.cs +++ b/src/Umbraco.Tests/Templates/ImageSourceParserTests.cs @@ -12,19 +12,40 @@ using System.Web; using System; using System.Linq; using Umbraco.Core.Models; +using Umbraco.Core; namespace Umbraco.Tests.Templates { + [TestFixture] - public class MediaParserTests + public class ImageSourceParserTests { + [Test] + public void Returns_Udis_From_Data_Udi_Html_Attributes() + { + var input = @"

+

+ +
+

"; + + var logger = Mock.Of(); + var umbracoContextAccessor = new TestUmbracoContextAccessor(); + var mediaParser = new ImageSourceParser(umbracoContextAccessor, logger, Mock.Of(), Mock.Of()); + + var result = mediaParser.FindUdisFromDataAttributes(input).ToList(); + Assert.AreEqual(2, result.Count); + Assert.AreEqual(Udi.Parse("umb://media/D4B18427A1544721B09AC7692F35C264"), result[0]); + Assert.AreEqual(Udi.Parse("umb://media-type/B726D735E4C446D58F703F3FBCFC97A5"), result[1]); + } + [Test] public void Remove_Image_Sources() { var logger = Mock.Of(); var umbracoContextAccessor = new TestUmbracoContextAccessor(); - var mediaParser = new MediaParser(umbracoContextAccessor, logger, Mock.Of(), Mock.Of()); + var mediaParser = new ImageSourceParser(umbracoContextAccessor, logger, Mock.Of(), Mock.Of()); var result = mediaParser.RemoveImageSources(@"

@@ -66,7 +87,7 @@ namespace Umbraco.Tests.Templates var mediaCache = Mock.Get(reference.UmbracoContext.Media); mediaCache.Setup(x => x.GetById(It.IsAny())).Returns(media.Object); - var mediaParser = new MediaParser(umbracoContextAccessor, Mock.Of(), Mock.Of(), Mock.Of()); + var mediaParser = new ImageSourceParser(umbracoContextAccessor, Mock.Of(), Mock.Of(), Mock.Of()); var result = mediaParser.EnsureImageSources(@"

diff --git a/src/Umbraco.Tests/Templates/LocalLinkParserTests.cs b/src/Umbraco.Tests/Templates/LocalLinkParserTests.cs new file mode 100644 index 0000000000..30c79b3115 --- /dev/null +++ b/src/Umbraco.Tests/Templates/LocalLinkParserTests.cs @@ -0,0 +1,34 @@ +using NUnit.Framework; +using System.Linq; +using Umbraco.Core; +using Umbraco.Tests.Testing.Objects.Accessors; +using Umbraco.Web.Templates; + +namespace Umbraco.Tests.Templates +{ + [TestFixture] + public class LocalLinkParserTests + { + [Test] + public void Returns_Udis_From_LocalLinks() + { + var input = @"

+

+ + hello +
+

+hello +

"; + + var umbracoContextAccessor = new TestUmbracoContextAccessor(); + var parser = new LocalLinkParser(umbracoContextAccessor); + + var result = parser.FindUdisFromLocalLinks(input).ToList(); + + Assert.AreEqual(2, result.Count); + Assert.AreEqual(Udi.Parse("umb://document/C093961595094900AAF9170DDE6AD442"), result[0]); + Assert.AreEqual(Udi.Parse("umb://document-type/2D692FCB070B4CDA92FB6883FDBFD6E2"), result[1]); + } + } +} diff --git a/src/Umbraco.Tests/Templates/UdiParserTests.cs b/src/Umbraco.Tests/Templates/UdiParserTests.cs deleted file mode 100644 index eca7f6c4f0..0000000000 --- a/src/Umbraco.Tests/Templates/UdiParserTests.cs +++ /dev/null @@ -1,28 +0,0 @@ -using NUnit.Framework; -using Umbraco.Web.Templates; -using System.Linq; -using Umbraco.Core; - -namespace Umbraco.Tests.Templates -{ - [TestFixture] - public class UdiParserTests - { - [Test] - public void Returns_Udi_From_Data_Udi_Html_Attributes() - { - var input = @"

-

- -
-

"; - - var parser = new UdiParser(); - var result = parser.ParseUdisFromDataAttributes(input).ToList(); - Assert.AreEqual(2, result.Count); - Assert.AreEqual(Udi.Parse("umb://media/D4B18427A1544721B09AC7692F35C264"), result[0]); - Assert.AreEqual(Udi.Parse("umb://media-type/B726D735E4C446D58F703F3FBCFC97A5"), result[1]); - } - - } -} diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 519f36012c..d9583b9393 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -159,7 +159,7 @@ - + @@ -256,7 +256,7 @@ - + diff --git a/src/Umbraco.Tests/Web/InternalLinkParserTests.cs b/src/Umbraco.Tests/Web/InternalLinkParserTests.cs index 0a948a8617..49da8d6566 100644 --- a/src/Umbraco.Tests/Web/InternalLinkParserTests.cs +++ b/src/Umbraco.Tests/Web/InternalLinkParserTests.cs @@ -65,7 +65,7 @@ namespace Umbraco.Tests.Web mediaCache.Setup(x => x.GetById(It.IsAny())).Returns(media.Object); mediaCache.Setup(x => x.GetById(It.IsAny())).Returns(media.Object); - var linkParser = new InternalLinkParser(umbracoContextAccessor); + var linkParser = new LocalLinkParser(umbracoContextAccessor); var output = linkParser.EnsureInternalLinks(input); diff --git a/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs index bec28e33fd..fa6346fdd8 100644 --- a/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs @@ -28,10 +28,10 @@ namespace Umbraco.Web.PropertyEditors private IMediaService _mediaService; private IContentTypeBaseServiceProvider _contentTypeBaseServiceProvider; private IUmbracoContextAccessor _umbracoContextAccessor; - private readonly MediaParser _mediaParser; + private readonly ImageSourceParser _mediaParser; private ILogger _logger; - public GridPropertyEditor(ILogger logger, IMediaService mediaService, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, IUmbracoContextAccessor umbracoContextAccessor, MediaParser mediaParser) + public GridPropertyEditor(ILogger logger, IMediaService mediaService, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, IUmbracoContextAccessor umbracoContextAccessor, ImageSourceParser mediaParser) : base(logger) { _mediaService = mediaService; @@ -57,9 +57,9 @@ namespace Umbraco.Web.PropertyEditors private IContentTypeBaseServiceProvider _contentTypeBaseServiceProvider; private IUmbracoContextAccessor _umbracoContextAccessor; private ILogger _logger; - private readonly MediaParser _mediaParser; + private readonly ImageSourceParser _mediaParser; - public GridPropertyValueEditor(DataEditorAttribute attribute, IMediaService mediaService, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, IUmbracoContextAccessor umbracoContextAccessor, ILogger logger, MediaParser _mediaParser) + public GridPropertyValueEditor(DataEditorAttribute attribute, IMediaService mediaService, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, IUmbracoContextAccessor umbracoContextAccessor, ILogger logger, ImageSourceParser _mediaParser) : base(attribute) { _mediaService = mediaService; diff --git a/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs index 03dc7b6694..4c7df4163c 100644 --- a/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using Umbraco.Core; using Umbraco.Core.Logging; using Umbraco.Core.Models; @@ -25,12 +26,13 @@ namespace Umbraco.Web.PropertyEditors public class RichTextPropertyEditor : DataEditor { private IUmbracoContextAccessor _umbracoContextAccessor; - private readonly MediaParser _mediaParser; + private readonly ImageSourceParser _mediaParser; + /// /// The constructor will setup the property editor based on the attribute if one is found /// - public RichTextPropertyEditor(ILogger logger, IUmbracoContextAccessor umbracoContextAccessor, MediaParser mediaParser) + public RichTextPropertyEditor(ILogger logger, IUmbracoContextAccessor umbracoContextAccessor, ImageSourceParser mediaParser) : base(logger) { _umbracoContextAccessor = umbracoContextAccessor; @@ -53,9 +55,9 @@ namespace Umbraco.Web.PropertyEditors internal class RichTextPropertyValueEditor : DataValueEditor, IDataValueReference { private IUmbracoContextAccessor _umbracoContextAccessor; - private readonly MediaParser _mediaParser; + private readonly ImageSourceParser _mediaParser; - public RichTextPropertyValueEditor(DataEditorAttribute attribute, IUmbracoContextAccessor umbracoContextAccessor, MediaParser _mediaParser) + public RichTextPropertyValueEditor(DataEditorAttribute attribute, IUmbracoContextAccessor umbracoContextAccessor, ImageSourceParser _mediaParser) : base(attribute) { _umbracoContextAccessor = umbracoContextAccessor; @@ -127,7 +129,7 @@ namespace Umbraco.Web.PropertyEditors /// public IEnumerable GetReferences(object value) { - throw new NotImplementedException(); + return _mediaParser.FindUdisFromDataAttributes(value == null ? string.Empty : value is string str ? str : value.ToString()).ToList(); } } diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/MarkdownEditorValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/MarkdownEditorValueConverter.cs index 578a4cad06..e8a2ac11a6 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/MarkdownEditorValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/MarkdownEditorValueConverter.cs @@ -12,10 +12,10 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters [DefaultPropertyValueConverter] public class MarkdownEditorValueConverter : PropertyValueConverterBase { - private readonly InternalLinkParser _localLinkParser; + private readonly LocalLinkParser _localLinkParser; private readonly UrlParser _urlResolver; - public MarkdownEditorValueConverter(InternalLinkParser localLinkParser, UrlParser urlResolver) + public MarkdownEditorValueConverter(LocalLinkParser localLinkParser, UrlParser urlResolver) { _localLinkParser = localLinkParser; _urlResolver = urlResolver; diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/RteMacroRenderingValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/RteMacroRenderingValueConverter.cs index 88c1429b16..2caac9e1f4 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/RteMacroRenderingValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/RteMacroRenderingValueConverter.cs @@ -24,9 +24,9 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters { private readonly IUmbracoContextAccessor _umbracoContextAccessor; private readonly IMacroRenderer _macroRenderer; - private readonly InternalLinkParser _internalLinkParser; + private readonly LocalLinkParser _internalLinkParser; private readonly UrlParser _urlResolver; - private readonly MediaParser _mediaParser; + private readonly ImageSourceParser _mediaParser; public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) { @@ -36,7 +36,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters } public RteMacroRenderingValueConverter(IUmbracoContextAccessor umbracoContextAccessor, IMacroRenderer macroRenderer, - InternalLinkParser internalLinkParser, UrlParser urlResolver, MediaParser mediaParser) + LocalLinkParser internalLinkParser, UrlParser urlResolver, ImageSourceParser mediaParser) { _umbracoContextAccessor = umbracoContextAccessor; _macroRenderer = macroRenderer; diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/TextStringValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/TextStringValueConverter.cs index 1b85d6e608..5efc2ee2db 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/TextStringValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/TextStringValueConverter.cs @@ -11,7 +11,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters [DefaultPropertyValueConverter] public class TextStringValueConverter : PropertyValueConverterBase { - public TextStringValueConverter(InternalLinkParser internalLinkParser, UrlParser urlParser) + public TextStringValueConverter(LocalLinkParser internalLinkParser, UrlParser urlParser) { _internalLinkParser = internalLinkParser; _urlParser = urlParser; @@ -22,7 +22,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters Constants.PropertyEditors.Aliases.TextBox, Constants.PropertyEditors.Aliases.TextArea }; - private readonly InternalLinkParser _internalLinkParser; + private readonly LocalLinkParser _internalLinkParser; private readonly UrlParser _urlParser; public override bool IsConverter(IPublishedPropertyType propertyType) diff --git a/src/Umbraco.Web/Runtime/WebInitialComposer.cs b/src/Umbraco.Web/Runtime/WebInitialComposer.cs index 1b3128388d..2f78ac9732 100644 --- a/src/Umbraco.Web/Runtime/WebInitialComposer.cs +++ b/src/Umbraco.Web/Runtime/WebInitialComposer.cs @@ -107,9 +107,9 @@ namespace Umbraco.Web.Runtime composition.RegisterUnique(); composition.RegisterUnique(); - composition.RegisterUnique(); + composition.RegisterUnique(); composition.RegisterUnique(); - composition.RegisterUnique(); + composition.RegisterUnique(); // register the umbraco helper - this is Transient! very important! // also, if not level.Run, we cannot really use the helper (during upgrade...) diff --git a/src/Umbraco.Web/Templates/MediaParser.cs b/src/Umbraco.Web/Templates/ImageSourceParser.cs similarity index 86% rename from src/Umbraco.Web/Templates/MediaParser.cs rename to src/Umbraco.Web/Templates/ImageSourceParser.cs index 071c6a5696..6a0bba4998 100644 --- a/src/Umbraco.Web/Templates/MediaParser.cs +++ b/src/Umbraco.Web/Templates/ImageSourceParser.cs @@ -14,9 +14,9 @@ using Umbraco.Web.Routing; namespace Umbraco.Web.Templates { - public sealed class MediaParser + public sealed class ImageSourceParser { - public MediaParser(IUmbracoContextAccessor umbracoContextAccessor, ILogger logger, IMediaService mediaService, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider) + public ImageSourceParser(IUmbracoContextAccessor umbracoContextAccessor, ILogger logger, IMediaService mediaService, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider) { _umbracoContextAccessor = umbracoContextAccessor; _logger = logger; @@ -32,6 +32,27 @@ namespace Umbraco.Web.Templates private readonly IContentTypeBaseServiceProvider _contentTypeBaseServiceProvider; const string TemporaryImageDataAttribute = "data-tmpimg"; + private static readonly Regex DataUdiAttributeRegex = new Regex(@"data-udi=\\?(?:""|')(?umb://[A-z0-9\-]+/[A-z0-9]+)\\?(?:""|')", + RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); + + /// + /// Parses out UDIs from an html string based on 'data-udi' html attributes + /// + /// + /// + public IEnumerable FindUdisFromDataAttributes(string text) + { + var matches = DataUdiAttributeRegex.Matches(text); + if (matches.Count == 0) + yield break; + + foreach (Match match in matches) + { + if (match.Groups.Count == 2 && Udi.TryParse(match.Groups[1].Value, out var udi)) + yield return udi; + } + } + /// /// Parses the string looking for Umbraco image tags and updates them to their up-to-date image sources. /// @@ -178,7 +199,7 @@ namespace Umbraco.Web.Templates } catch (Exception ex) { - _logger.Error(typeof(MediaParser), ex, "Could not delete temp file or folder {FileName}", absoluteTempImagePath); + _logger.Error(typeof(ImageSourceParser), ex, "Could not delete temp file or folder {FileName}", absoluteTempImagePath); } } } diff --git a/src/Umbraco.Web/Templates/InternalLinkParser.cs b/src/Umbraco.Web/Templates/LocalLinkParser.cs similarity index 63% rename from src/Umbraco.Web/Templates/InternalLinkParser.cs rename to src/Umbraco.Web/Templates/LocalLinkParser.cs index 32d7d42eac..f394f56d85 100644 --- a/src/Umbraco.Web/Templates/InternalLinkParser.cs +++ b/src/Umbraco.Web/Templates/LocalLinkParser.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Text.RegularExpressions; using Umbraco.Core; using Umbraco.Core.Logging; @@ -10,7 +11,7 @@ namespace Umbraco.Web.Templates /// /// Utility class used to parse internal links /// - public sealed class InternalLinkParser + public sealed class LocalLinkParser { private static readonly Regex LocalLinkPattern = new Regex(@"href=""[/]?(?:\{|\%7B)localLink:([a-zA-Z0-9-://]+)(?:\}|\%7D)", @@ -18,11 +19,20 @@ namespace Umbraco.Web.Templates private readonly IUmbracoContextAccessor _umbracoContextAccessor; - public InternalLinkParser(IUmbracoContextAccessor umbracoContextAccessor) + public LocalLinkParser(IUmbracoContextAccessor umbracoContextAccessor) { _umbracoContextAccessor = umbracoContextAccessor; } + internal IEnumerable FindUdisFromLocalLinks(string text) + { + foreach ((int? intId, GuidUdi udi, string tagValue) in FindLocalLinkIds(text)) + { + if (udi != null) + yield return udi; // In v8, we only care abuot UDIs + } + } + /// /// Parses the string looking for the {localLink} syntax and updates them to their correct links. /// @@ -56,6 +66,33 @@ namespace Umbraco.Web.Templates var urlProvider = _umbracoContextAccessor.UmbracoContext.UrlProvider; + foreach((int? intId, GuidUdi udi, string tagValue) in FindLocalLinkIds(text)) + { + if (udi != null) + { + var newLink = "#"; + if (udi.EntityType == Constants.UdiEntityType.Document) + newLink = urlProvider.GetUrl(udi.Guid); + else if (udi.EntityType == Constants.UdiEntityType.Media) + newLink = urlProvider.GetMediaUrl(udi.Guid); + + if (newLink == null) + newLink = "#"; + + text = text.Replace(tagValue, "href=\"" + newLink); + } + else if (intId.HasValue) + { + var newLink = urlProvider.GetUrl(intId.Value); + text = text.Replace(tagValue, "href=\"" + newLink); + } + } + + return text; + } + + private IEnumerable<(int? intId, GuidUdi udi, string tagValue)> FindLocalLinkIds(string text) + { // Parse internal links var tags = LocalLinkPattern.Matches(text); foreach (Match tag in tags) @@ -69,29 +106,16 @@ namespace Umbraco.Web.Templates { var guidUdi = udi as GuidUdi; if (guidUdi != null) - { - var newLink = "#"; - if (guidUdi.EntityType == Constants.UdiEntityType.Document) - newLink = urlProvider.GetUrl(guidUdi.Guid); - else if (guidUdi.EntityType == Constants.UdiEntityType.Media) - newLink = urlProvider.GetMediaUrl(guidUdi.Guid); - - if (newLink == null) - newLink = "#"; - - text = text.Replace(tag.Value, "href=\"" + newLink); - } + yield return (null, guidUdi, tag.Value); } if (int.TryParse(id, out var intId)) { - var newLink = urlProvider.GetUrl(intId); - text = text.Replace(tag.Value, "href=\"" + newLink); + yield return (intId, null, tag.Value); } } } - return text; } } } diff --git a/src/Umbraco.Web/Templates/TemplateUtilities.cs b/src/Umbraco.Web/Templates/TemplateUtilities.cs index d4bae38147..db0366dbf3 100644 --- a/src/Umbraco.Web/Templates/TemplateUtilities.cs +++ b/src/Umbraco.Web/Templates/TemplateUtilities.cs @@ -31,7 +31,7 @@ namespace Umbraco.Web.Templates [Obsolete("Inject and use an instance of InternalLinkParser instead")] public static string ParseInternalLinks(string text, UrlProvider urlProvider) - => Current.Factory.GetInstance().EnsureInternalLinks(text); + => Current.Factory.GetInstance().EnsureInternalLinks(text); [Obsolete("Inject and use an instance of UrlResolver")] public static string ResolveUrlsFromTextString(string text) @@ -43,14 +43,14 @@ namespace Umbraco.Web.Templates [Obsolete("Use MediaParser.EnsureImageSources instead")] public static string ResolveMediaFromTextString(string text) - => Current.Factory.GetInstance().EnsureImageSources(text); + => Current.Factory.GetInstance().EnsureImageSources(text); [Obsolete("Use MediaParser.RemoveImageSources instead")] internal static string RemoveMediaUrlsFromTextString(string text) - => Current.Factory.GetInstance().RemoveImageSources(text); + => Current.Factory.GetInstance().RemoveImageSources(text); [Obsolete("Use MediaParser.RemoveImageSources instead")] internal static string FindAndPersistPastedTempImages(string html, Guid mediaParentFolder, int userId, IMediaService mediaService, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, ILogger logger) - => Current.Factory.GetInstance().FindAndPersistPastedTempImages(html, mediaParentFolder, userId); + => Current.Factory.GetInstance().FindAndPersistPastedTempImages(html, mediaParentFolder, userId); } } diff --git a/src/Umbraco.Web/Templates/UdiParser.cs b/src/Umbraco.Web/Templates/UdiParser.cs deleted file mode 100644 index 8bb5f8c579..0000000000 --- a/src/Umbraco.Web/Templates/UdiParser.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Text.RegularExpressions; -using Umbraco.Core; - -namespace Umbraco.Web.Templates -{ - /// - /// Parses out UDIs in strings - /// - public sealed class UdiParser - { - private static readonly Regex DataUdiAttributeRegex = new Regex(@"data-udi=\\?(?:""|')(?umb://[A-z0-9\-]+/[A-z0-9]+)\\?(?:""|')", - RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); - - /// - /// Parses out UDIs from an html string based on 'data-udi' html attributes - /// - /// - /// - public IEnumerable ParseUdisFromDataAttributes(string text) - { - var matches = DataUdiAttributeRegex.Matches(text); - if (matches.Count == 0) - yield break; - - foreach (Match match in matches) - { - if (match.Groups.Count == 2 && Udi.TryParse(match.Groups[1].Value, out var udi)) - yield return udi; - } - } - } -} diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 276cdf9ebc..8fc06c75c2 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -247,9 +247,8 @@ - - - + + diff --git a/src/Umbraco.Web/UmbracoComponentRenderer.cs b/src/Umbraco.Web/UmbracoComponentRenderer.cs index c0f83fd1af..83c8a7f0fa 100644 --- a/src/Umbraco.Web/UmbracoComponentRenderer.cs +++ b/src/Umbraco.Web/UmbracoComponentRenderer.cs @@ -27,9 +27,9 @@ namespace Umbraco.Web private readonly IUmbracoContextAccessor _umbracoContextAccessor; private readonly IMacroRenderer _macroRenderer; private readonly ITemplateRenderer _templateRenderer; - private readonly InternalLinkParser _internalLinkParser; + private readonly LocalLinkParser _internalLinkParser; - public UmbracoComponentRenderer(IUmbracoContextAccessor umbracoContextAccessor, IMacroRenderer macroRenderer, ITemplateRenderer templateRenderer, InternalLinkParser internalLinkParser) + public UmbracoComponentRenderer(IUmbracoContextAccessor umbracoContextAccessor, IMacroRenderer macroRenderer, ITemplateRenderer templateRenderer, LocalLinkParser internalLinkParser) { _umbracoContextAccessor = umbracoContextAccessor; _macroRenderer = macroRenderer; From aee3b9f9d28b810e893cfd73ad4c0c35504f5371 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 22 Oct 2019 15:52:09 +1100 Subject: [PATCH 010/202] updates rte editor to return local link udis --- .../PropertyEditors/RichTextPropertyEditor.cs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs index 4c7df4163c..8cb9536182 100644 --- a/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs @@ -56,12 +56,14 @@ namespace Umbraco.Web.PropertyEditors { private IUmbracoContextAccessor _umbracoContextAccessor; private readonly ImageSourceParser _mediaParser; + private readonly LocalLinkParser _localLinkParser; - public RichTextPropertyValueEditor(DataEditorAttribute attribute, IUmbracoContextAccessor umbracoContextAccessor, ImageSourceParser _mediaParser) + public RichTextPropertyValueEditor(DataEditorAttribute attribute, IUmbracoContextAccessor umbracoContextAccessor, ImageSourceParser mediaParser, LocalLinkParser localLinkParser) : base(attribute) { _umbracoContextAccessor = umbracoContextAccessor; - this._mediaParser = _mediaParser; + _mediaParser = mediaParser; + _localLinkParser = localLinkParser; } /// @@ -129,7 +131,15 @@ namespace Umbraco.Web.PropertyEditors /// public IEnumerable GetReferences(object value) { - return _mediaParser.FindUdisFromDataAttributes(value == null ? string.Empty : value is string str ? str : value.ToString()).ToList(); + var asString = value == null ? string.Empty : value is string str ? str : value.ToString(); + + foreach (var udi in _mediaParser.FindUdisFromDataAttributes(asString)) + yield return udi; + + foreach (var udi in _localLinkParser.FindUdisFromLocalLinks(asString)) + yield return udi; + + //TODO: Detect Macros too ... but we can save that for a later date, right now need to do media refs } } From 611ba7de46e9aa4c113ae6e1c081a7a994b594f7 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 22 Oct 2019 16:12:31 +1100 Subject: [PATCH 011/202] fix build --- src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs index 8cb9536182..2b3e749953 100644 --- a/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs @@ -27,23 +27,25 @@ namespace Umbraco.Web.PropertyEditors { private IUmbracoContextAccessor _umbracoContextAccessor; private readonly ImageSourceParser _mediaParser; - + private readonly LocalLinkParser _localLinkParser; + /// /// The constructor will setup the property editor based on the attribute if one is found /// - public RichTextPropertyEditor(ILogger logger, IUmbracoContextAccessor umbracoContextAccessor, ImageSourceParser mediaParser) + public RichTextPropertyEditor(ILogger logger, IUmbracoContextAccessor umbracoContextAccessor, ImageSourceParser mediaParser, LocalLinkParser localLinkParser) : base(logger) { _umbracoContextAccessor = umbracoContextAccessor; _mediaParser = mediaParser; + _localLinkParser = localLinkParser; } /// /// Create a custom value editor /// /// - protected override IDataValueEditor CreateValueEditor() => new RichTextPropertyValueEditor(Attribute, _umbracoContextAccessor, _mediaParser); + protected override IDataValueEditor CreateValueEditor() => new RichTextPropertyValueEditor(Attribute, _umbracoContextAccessor, _mediaParser, _localLinkParser); protected override IConfigurationEditor CreateConfigurationEditor() => new RichTextConfigurationEditor(); From 832803a9f64d7ab3a8f6dbb2b94f59f75f7f3318 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 23 Oct 2019 14:35:43 +1100 Subject: [PATCH 012/202] fix build --- .../PublishedContent/PublishedContentTestBase.cs | 6 ++++-- src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentTestBase.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentTestBase.cs index fec852f97a..6c68fecdd2 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentTestBase.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentTestBase.cs @@ -39,11 +39,13 @@ namespace Umbraco.Tests.PublishedContent base.Initialize(); var converters = Factory.GetInstance(); - var umbracoCtxAccessor = Mock.Of(); + var umbracoContextAccessor = Mock.Of(); var logger = Mock.Of(); + var imageSourceParser = new ImageSourceParser(umbracoContextAccessor, logger, Mock.Of(), Mock.Of()); + var localLinkParser = new LocalLinkParser(umbracoContextAccessor); var dataTypeService = new TestObjects.TestDataTypeService( - new DataType(new RichTextPropertyEditor(logger, umbracoCtxAccessor, new ImageSourceParser(umbracoCtxAccessor, logger, Mock.Of(), Mock.Of()))) { Id = 1 }); + new DataType(new RichTextPropertyEditor(logger, umbracoContextAccessor, imageSourceParser, localLinkParser)) { Id = 1 }); var publishedContentTypeFactory = new PublishedContentTypeFactory(Mock.Of(), converters, dataTypeService); diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs index 95d00998a1..333f3ca7c0 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs @@ -47,11 +47,12 @@ namespace Umbraco.Tests.PublishedContent var contentTypeBaseServiceProvider = Mock.Of(); var umbracoContextAccessor = Mock.Of(); var mediaParser = new ImageSourceParser(umbracoContextAccessor, logger, mediaService, contentTypeBaseServiceProvider); + var localLinkParser = new LocalLinkParser(umbracoContextAccessor); var dataTypeService = new TestObjects.TestDataTypeService( new DataType(new VoidEditor(logger)) { Id = 1 }, new DataType(new TrueFalsePropertyEditor(logger)) { Id = 1001 }, - new DataType(new RichTextPropertyEditor(logger, umbracoContextAccessor, mediaParser)) { Id = 1002 }, + new DataType(new RichTextPropertyEditor(logger, umbracoContextAccessor, mediaParser, localLinkParser)) { Id = 1002 }, new DataType(new IntegerPropertyEditor(logger)) { Id = 1003 }, new DataType(new TextboxPropertyEditor(logger)) { Id = 1004 }, new DataType(new MediaPickerPropertyEditor(logger)) { Id = 1005 }); From 17fd09fe3df0576dc6823cf3fa8d2d9d695a0912 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 23 Oct 2019 14:55:18 +1100 Subject: [PATCH 013/202] Naming --- .../PublishedContentTestBase.cs | 4 +-- .../PublishedContent/PublishedContentTests.cs | 6 ++-- .../Templates/ImageSourceParserTests.cs | 12 +++---- .../Templates/LocalLinkParserTests.cs | 2 +- src/Umbraco.Tests/Umbraco.Tests.csproj | 2 +- ...erTests.cs => HtmlLocalLinkParserTests.cs} | 4 +-- .../PropertyEditors/GridPropertyEditor.cs | 32 ++++++------------- .../PropertyEditors/RichTextPropertyEditor.cs | 26 +++++++-------- .../MarkdownEditorValueConverter.cs | 10 +++--- .../RteMacroRenderingValueConverter.cs | 20 ++++++------ .../TextStringValueConverter.cs | 10 +++--- src/Umbraco.Web/Runtime/WebInitialComposer.cs | 6 ++-- ...urceParser.cs => HtmlImageSourceParser.cs} | 15 ++++++--- ...alLinkParser.cs => HtmlLocalLinkParser.cs} | 4 +-- .../{UrlParser.cs => HtmlUrlParser.cs} | 4 +-- .../Templates/TemplateUtilities.cs | 24 +++++++------- src/Umbraco.Web/Umbraco.Web.csproj | 6 ++-- src/Umbraco.Web/UmbracoComponentRenderer.cs | 8 ++--- 18 files changed, 95 insertions(+), 100 deletions(-) rename src/Umbraco.Tests/Web/{InternalLinkParserTests.cs => HtmlLocalLinkParserTests.cs} (97%) rename src/Umbraco.Web/Templates/{ImageSourceParser.cs => HtmlImageSourceParser.cs} (92%) rename src/Umbraco.Web/Templates/{LocalLinkParser.cs => HtmlLocalLinkParser.cs} (97%) rename src/Umbraco.Web/Templates/{UrlParser.cs => HtmlUrlParser.cs} (95%) diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentTestBase.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentTestBase.cs index 6c68fecdd2..497c621963 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentTestBase.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentTestBase.cs @@ -42,8 +42,8 @@ namespace Umbraco.Tests.PublishedContent var umbracoContextAccessor = Mock.Of(); var logger = Mock.Of(); - var imageSourceParser = new ImageSourceParser(umbracoContextAccessor, logger, Mock.Of(), Mock.Of()); - var localLinkParser = new LocalLinkParser(umbracoContextAccessor); + var imageSourceParser = new HtmlImageSourceParser(umbracoContextAccessor, logger, Mock.Of(), Mock.Of()); + var localLinkParser = new HtmlLocalLinkParser(umbracoContextAccessor); var dataTypeService = new TestObjects.TestDataTypeService( new DataType(new RichTextPropertyEditor(logger, umbracoContextAccessor, imageSourceParser, localLinkParser)) { Id = 1 }); diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs index 333f3ca7c0..9f5dc81986 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs @@ -46,13 +46,13 @@ namespace Umbraco.Tests.PublishedContent var mediaService = Mock.Of(); var contentTypeBaseServiceProvider = Mock.Of(); var umbracoContextAccessor = Mock.Of(); - var mediaParser = new ImageSourceParser(umbracoContextAccessor, logger, mediaService, contentTypeBaseServiceProvider); - var localLinkParser = new LocalLinkParser(umbracoContextAccessor); + var imageSourceParser = new HtmlImageSourceParser(umbracoContextAccessor, logger, mediaService, contentTypeBaseServiceProvider); + var linkParser = new HtmlLocalLinkParser(umbracoContextAccessor); var dataTypeService = new TestObjects.TestDataTypeService( new DataType(new VoidEditor(logger)) { Id = 1 }, new DataType(new TrueFalsePropertyEditor(logger)) { Id = 1001 }, - new DataType(new RichTextPropertyEditor(logger, umbracoContextAccessor, mediaParser, localLinkParser)) { Id = 1002 }, + new DataType(new RichTextPropertyEditor(logger, umbracoContextAccessor, imageSourceParser, linkParser)) { Id = 1002 }, new DataType(new IntegerPropertyEditor(logger)) { Id = 1003 }, new DataType(new TextboxPropertyEditor(logger)) { Id = 1004 }, new DataType(new MediaPickerPropertyEditor(logger)) { Id = 1005 }); diff --git a/src/Umbraco.Tests/Templates/ImageSourceParserTests.cs b/src/Umbraco.Tests/Templates/ImageSourceParserTests.cs index d383ab02df..ca01caf344 100644 --- a/src/Umbraco.Tests/Templates/ImageSourceParserTests.cs +++ b/src/Umbraco.Tests/Templates/ImageSourceParserTests.cs @@ -32,9 +32,9 @@ namespace Umbraco.Tests.Templates var logger = Mock.Of(); var umbracoContextAccessor = new TestUmbracoContextAccessor(); - var mediaParser = new ImageSourceParser(umbracoContextAccessor, logger, Mock.Of(), Mock.Of()); + var imageSourceParser = new HtmlImageSourceParser(umbracoContextAccessor, logger, Mock.Of(), Mock.Of()); - var result = mediaParser.FindUdisFromDataAttributes(input).ToList(); + var result = imageSourceParser.FindUdisFromDataAttributes(input).ToList(); Assert.AreEqual(2, result.Count); Assert.AreEqual(Udi.Parse("umb://media/D4B18427A1544721B09AC7692F35C264"), result[0]); Assert.AreEqual(Udi.Parse("umb://media-type/B726D735E4C446D58F703F3FBCFC97A5"), result[1]); @@ -45,9 +45,9 @@ namespace Umbraco.Tests.Templates { var logger = Mock.Of(); var umbracoContextAccessor = new TestUmbracoContextAccessor(); - var mediaParser = new ImageSourceParser(umbracoContextAccessor, logger, Mock.Of(), Mock.Of()); + var imageSourceParser = new HtmlImageSourceParser(umbracoContextAccessor, logger, Mock.Of(), Mock.Of()); - var result = mediaParser.RemoveImageSources(@"

+ var result = imageSourceParser.RemoveImageSources(@"

@@ -87,9 +87,9 @@ namespace Umbraco.Tests.Templates var mediaCache = Mock.Get(reference.UmbracoContext.Media); mediaCache.Setup(x => x.GetById(It.IsAny())).Returns(media.Object); - var mediaParser = new ImageSourceParser(umbracoContextAccessor, Mock.Of(), Mock.Of(), Mock.Of()); + var imageSourceParser = new HtmlImageSourceParser(umbracoContextAccessor, Mock.Of(), Mock.Of(), Mock.Of()); - var result = mediaParser.EnsureImageSources(@"

+ var result = imageSourceParser.EnsureImageSources(@"

diff --git a/src/Umbraco.Tests/Templates/LocalLinkParserTests.cs b/src/Umbraco.Tests/Templates/LocalLinkParserTests.cs index 30c79b3115..e09d71196e 100644 --- a/src/Umbraco.Tests/Templates/LocalLinkParserTests.cs +++ b/src/Umbraco.Tests/Templates/LocalLinkParserTests.cs @@ -22,7 +22,7 @@ namespace Umbraco.Tests.Templates

"; var umbracoContextAccessor = new TestUmbracoContextAccessor(); - var parser = new LocalLinkParser(umbracoContextAccessor); + var parser = new HtmlLocalLinkParser(umbracoContextAccessor); var result = parser.FindUdisFromLocalLinks(input).ToList(); diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index d9583b9393..fa654ad4a4 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -255,7 +255,7 @@ - + diff --git a/src/Umbraco.Tests/Web/InternalLinkParserTests.cs b/src/Umbraco.Tests/Web/HtmlLocalLinkParserTests.cs similarity index 97% rename from src/Umbraco.Tests/Web/InternalLinkParserTests.cs rename to src/Umbraco.Tests/Web/HtmlLocalLinkParserTests.cs index 49da8d6566..e6a0abeb4c 100644 --- a/src/Umbraco.Tests/Web/InternalLinkParserTests.cs +++ b/src/Umbraco.Tests/Web/HtmlLocalLinkParserTests.cs @@ -19,7 +19,7 @@ namespace Umbraco.Tests.Web { [TestFixture] - public class InternalLinkParserTests + public class HtmlLocalLinkParserTests { [TestCase("", "")] [TestCase("hello href=\"{localLink:1234}\" world ", "hello href=\"/my-test-url\" world ")] @@ -65,7 +65,7 @@ namespace Umbraco.Tests.Web mediaCache.Setup(x => x.GetById(It.IsAny())).Returns(media.Object); mediaCache.Setup(x => x.GetById(It.IsAny())).Returns(media.Object); - var linkParser = new LocalLinkParser(umbracoContextAccessor); + var linkParser = new HtmlLocalLinkParser(umbracoContextAccessor); var output = linkParser.EnsureInternalLinks(input); diff --git a/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs index fa6346fdd8..6481099f45 100644 --- a/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs @@ -25,20 +25,14 @@ namespace Umbraco.Web.PropertyEditors Group = Constants.PropertyEditors.Groups.RichContent)] public class GridPropertyEditor : DataEditor { - private IMediaService _mediaService; - private IContentTypeBaseServiceProvider _contentTypeBaseServiceProvider; private IUmbracoContextAccessor _umbracoContextAccessor; - private readonly ImageSourceParser _mediaParser; - private ILogger _logger; + private readonly HtmlImageSourceParser _imageSourceParser; - public GridPropertyEditor(ILogger logger, IMediaService mediaService, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, IUmbracoContextAccessor umbracoContextAccessor, ImageSourceParser mediaParser) + public GridPropertyEditor(ILogger logger, IUmbracoContextAccessor umbracoContextAccessor, HtmlImageSourceParser imageSourceParser) : base(logger) { - _mediaService = mediaService; - _contentTypeBaseServiceProvider = contentTypeBaseServiceProvider; _umbracoContextAccessor = umbracoContextAccessor; - _mediaParser = mediaParser; - _logger = logger; + _imageSourceParser = imageSourceParser; } public override IPropertyIndexValueFactory PropertyIndexValueFactory => new GridPropertyIndexValueFactory(); @@ -47,26 +41,20 @@ namespace Umbraco.Web.PropertyEditors /// Overridden to ensure that the value is validated ///
/// - protected override IDataValueEditor CreateValueEditor() => new GridPropertyValueEditor(Attribute, _mediaService, _contentTypeBaseServiceProvider, _umbracoContextAccessor, _logger, _mediaParser); + protected override IDataValueEditor CreateValueEditor() => new GridPropertyValueEditor(Attribute, _umbracoContextAccessor, _imageSourceParser); protected override IConfigurationEditor CreateConfigurationEditor() => new GridConfigurationEditor(); internal class GridPropertyValueEditor : DataValueEditor { - private IMediaService _mediaService; - private IContentTypeBaseServiceProvider _contentTypeBaseServiceProvider; private IUmbracoContextAccessor _umbracoContextAccessor; - private ILogger _logger; - private readonly ImageSourceParser _mediaParser; + private readonly HtmlImageSourceParser _imageSourceParser; - public GridPropertyValueEditor(DataEditorAttribute attribute, IMediaService mediaService, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, IUmbracoContextAccessor umbracoContextAccessor, ILogger logger, ImageSourceParser _mediaParser) + public GridPropertyValueEditor(DataEditorAttribute attribute, IUmbracoContextAccessor umbracoContextAccessor, HtmlImageSourceParser imageSourceParser) : base(attribute) { - _mediaService = mediaService; - _contentTypeBaseServiceProvider = contentTypeBaseServiceProvider; _umbracoContextAccessor = umbracoContextAccessor; - _logger = logger; - this._mediaParser = _mediaParser; + _imageSourceParser = imageSourceParser; } /// @@ -101,8 +89,8 @@ namespace Umbraco.Web.PropertyEditors // Parse the HTML var html = rte.Value?.ToString(); - var parseAndSavedTempImages = _mediaParser.FindAndPersistPastedTempImages(html, mediaParentId, userId); - var editorValueWithMediaUrlsRemoved = _mediaParser.RemoveImageSources(parseAndSavedTempImages); + var parseAndSavedTempImages = _imageSourceParser.FindAndPersistPastedTempImages(html, mediaParentId, userId); + var editorValueWithMediaUrlsRemoved = _imageSourceParser.RemoveImageSources(parseAndSavedTempImages); rte.Value = editorValueWithMediaUrlsRemoved; } @@ -131,7 +119,7 @@ namespace Umbraco.Web.PropertyEditors { var html = rte.Value?.ToString(); - var propertyValueWithMediaResolved = _mediaParser.EnsureImageSources(html); + var propertyValueWithMediaResolved = _imageSourceParser.EnsureImageSources(html); rte.Value = propertyValueWithMediaResolved; } diff --git a/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs index 2b3e749953..0dbe5426a2 100644 --- a/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs @@ -26,18 +26,18 @@ namespace Umbraco.Web.PropertyEditors public class RichTextPropertyEditor : DataEditor { private IUmbracoContextAccessor _umbracoContextAccessor; - private readonly ImageSourceParser _mediaParser; - private readonly LocalLinkParser _localLinkParser; + private readonly HtmlImageSourceParser _imageSourceParser; + private readonly HtmlLocalLinkParser _localLinkParser; /// /// The constructor will setup the property editor based on the attribute if one is found /// - public RichTextPropertyEditor(ILogger logger, IUmbracoContextAccessor umbracoContextAccessor, ImageSourceParser mediaParser, LocalLinkParser localLinkParser) + public RichTextPropertyEditor(ILogger logger, IUmbracoContextAccessor umbracoContextAccessor, HtmlImageSourceParser imageSourceParser, HtmlLocalLinkParser localLinkParser) : base(logger) { _umbracoContextAccessor = umbracoContextAccessor; - _mediaParser = mediaParser; + _imageSourceParser = imageSourceParser; _localLinkParser = localLinkParser; } @@ -45,7 +45,7 @@ namespace Umbraco.Web.PropertyEditors /// Create a custom value editor /// /// - protected override IDataValueEditor CreateValueEditor() => new RichTextPropertyValueEditor(Attribute, _umbracoContextAccessor, _mediaParser, _localLinkParser); + protected override IDataValueEditor CreateValueEditor() => new RichTextPropertyValueEditor(Attribute, _umbracoContextAccessor, _imageSourceParser, _localLinkParser); protected override IConfigurationEditor CreateConfigurationEditor() => new RichTextConfigurationEditor(); @@ -57,14 +57,14 @@ namespace Umbraco.Web.PropertyEditors internal class RichTextPropertyValueEditor : DataValueEditor, IDataValueReference { private IUmbracoContextAccessor _umbracoContextAccessor; - private readonly ImageSourceParser _mediaParser; - private readonly LocalLinkParser _localLinkParser; + private readonly HtmlImageSourceParser _imageSourceParser; + private readonly HtmlLocalLinkParser _localLinkParser; - public RichTextPropertyValueEditor(DataEditorAttribute attribute, IUmbracoContextAccessor umbracoContextAccessor, ImageSourceParser mediaParser, LocalLinkParser localLinkParser) + public RichTextPropertyValueEditor(DataEditorAttribute attribute, IUmbracoContextAccessor umbracoContextAccessor, HtmlImageSourceParser imageSourceParser, HtmlLocalLinkParser localLinkParser) : base(attribute) { _umbracoContextAccessor = umbracoContextAccessor; - _mediaParser = mediaParser; + _imageSourceParser = imageSourceParser; _localLinkParser = localLinkParser; } @@ -97,7 +97,7 @@ namespace Umbraco.Web.PropertyEditors if (val == null) return null; - var propertyValueWithMediaResolved = _mediaParser.EnsureImageSources(val.ToString()); + var propertyValueWithMediaResolved = _imageSourceParser.EnsureImageSources(val.ToString()); var parsed = MacroTagParser.FormatRichTextPersistedDataForEditor(propertyValueWithMediaResolved, new Dictionary()); return parsed; } @@ -119,8 +119,8 @@ namespace Umbraco.Web.PropertyEditors var mediaParent = config?.MediaParentId; var mediaParentId = mediaParent == null ? Guid.Empty : mediaParent.Guid; - var parseAndSavedTempImages = _mediaParser.FindAndPersistPastedTempImages(editorValue.Value.ToString(), mediaParentId, userId); - var editorValueWithMediaUrlsRemoved = _mediaParser.RemoveImageSources(parseAndSavedTempImages); + var parseAndSavedTempImages = _imageSourceParser.FindAndPersistPastedTempImages(editorValue.Value.ToString(), mediaParentId, userId); + var editorValueWithMediaUrlsRemoved = _imageSourceParser.RemoveImageSources(parseAndSavedTempImages); var parsed = MacroTagParser.FormatRichTextContentForPersistence(editorValueWithMediaUrlsRemoved); return parsed; @@ -135,7 +135,7 @@ namespace Umbraco.Web.PropertyEditors { var asString = value == null ? string.Empty : value is string str ? str : value.ToString(); - foreach (var udi in _mediaParser.FindUdisFromDataAttributes(asString)) + foreach (var udi in _imageSourceParser.FindUdisFromDataAttributes(asString)) yield return udi; foreach (var udi in _localLinkParser.FindUdisFromLocalLinks(asString)) diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/MarkdownEditorValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/MarkdownEditorValueConverter.cs index e8a2ac11a6..c62a79d283 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/MarkdownEditorValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/MarkdownEditorValueConverter.cs @@ -12,13 +12,13 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters [DefaultPropertyValueConverter] public class MarkdownEditorValueConverter : PropertyValueConverterBase { - private readonly LocalLinkParser _localLinkParser; - private readonly UrlParser _urlResolver; + private readonly HtmlLocalLinkParser _localLinkParser; + private readonly HtmlUrlParser _urlParser; - public MarkdownEditorValueConverter(LocalLinkParser localLinkParser, UrlParser urlResolver) + public MarkdownEditorValueConverter(HtmlLocalLinkParser localLinkParser, HtmlUrlParser urlParser) { _localLinkParser = localLinkParser; - _urlResolver = urlResolver; + _urlParser = urlParser; } public override bool IsConverter(IPublishedPropertyType propertyType) @@ -37,7 +37,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters // ensures string is parsed for {localLink} and urls are resolved correctly sourceString = _localLinkParser.EnsureInternalLinks(sourceString, preview); - sourceString = _urlResolver.EnsureUrls(sourceString); + sourceString = _urlParser.EnsureUrls(sourceString); return sourceString; } diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/RteMacroRenderingValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/RteMacroRenderingValueConverter.cs index 2caac9e1f4..3ab502742c 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/RteMacroRenderingValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/RteMacroRenderingValueConverter.cs @@ -24,9 +24,9 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters { private readonly IUmbracoContextAccessor _umbracoContextAccessor; private readonly IMacroRenderer _macroRenderer; - private readonly LocalLinkParser _internalLinkParser; - private readonly UrlParser _urlResolver; - private readonly ImageSourceParser _mediaParser; + private readonly HtmlLocalLinkParser _linkParser; + private readonly HtmlUrlParser _urlParser; + private readonly HtmlImageSourceParser _imageSourceParser; public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) { @@ -36,13 +36,13 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters } public RteMacroRenderingValueConverter(IUmbracoContextAccessor umbracoContextAccessor, IMacroRenderer macroRenderer, - LocalLinkParser internalLinkParser, UrlParser urlResolver, ImageSourceParser mediaParser) + HtmlLocalLinkParser linkParser, HtmlUrlParser urlParser, HtmlImageSourceParser imageSourceParser) { _umbracoContextAccessor = umbracoContextAccessor; _macroRenderer = macroRenderer; - _internalLinkParser = internalLinkParser; - _urlResolver = urlResolver; - _mediaParser = mediaParser; + _linkParser = linkParser; + _urlParser = urlParser; + _imageSourceParser = imageSourceParser; } // NOT thread-safe over a request because it modifies the @@ -88,9 +88,9 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters var sourceString = source.ToString(); // ensures string is parsed for {localLink} and urls and media are resolved correctly - sourceString = _internalLinkParser.EnsureInternalLinks(sourceString, preview); - sourceString = _urlResolver.EnsureUrls(sourceString); - sourceString = _mediaParser.EnsureImageSources(sourceString); + sourceString = _linkParser.EnsureInternalLinks(sourceString, preview); + sourceString = _urlParser.EnsureUrls(sourceString); + sourceString = _imageSourceParser.EnsureImageSources(sourceString); // ensure string is parsed for macros and macros are executed correctly sourceString = RenderRteMacros(sourceString, preview); diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/TextStringValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/TextStringValueConverter.cs index 5efc2ee2db..939a658407 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/TextStringValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/TextStringValueConverter.cs @@ -11,9 +11,9 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters [DefaultPropertyValueConverter] public class TextStringValueConverter : PropertyValueConverterBase { - public TextStringValueConverter(LocalLinkParser internalLinkParser, UrlParser urlParser) + public TextStringValueConverter(HtmlLocalLinkParser linkParser, HtmlUrlParser urlParser) { - _internalLinkParser = internalLinkParser; + _linkParser = linkParser; _urlParser = urlParser; } @@ -22,8 +22,8 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters Constants.PropertyEditors.Aliases.TextBox, Constants.PropertyEditors.Aliases.TextArea }; - private readonly LocalLinkParser _internalLinkParser; - private readonly UrlParser _urlParser; + private readonly HtmlLocalLinkParser _linkParser; + private readonly HtmlUrlParser _urlParser; public override bool IsConverter(IPublishedPropertyType propertyType) => PropertyTypeAliases.Contains(propertyType.EditorAlias); @@ -40,7 +40,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters var sourceString = source.ToString(); // ensures string is parsed for {localLink} and urls are resolved correctly - sourceString = _internalLinkParser.EnsureInternalLinks(sourceString, preview); + sourceString = _linkParser.EnsureInternalLinks(sourceString, preview); sourceString = _urlParser.EnsureUrls(sourceString); return sourceString; diff --git a/src/Umbraco.Web/Runtime/WebInitialComposer.cs b/src/Umbraco.Web/Runtime/WebInitialComposer.cs index 2f78ac9732..5ccb16a1a5 100644 --- a/src/Umbraco.Web/Runtime/WebInitialComposer.cs +++ b/src/Umbraco.Web/Runtime/WebInitialComposer.cs @@ -107,9 +107,9 @@ namespace Umbraco.Web.Runtime composition.RegisterUnique(); composition.RegisterUnique(); - composition.RegisterUnique(); - composition.RegisterUnique(); - composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(); // register the umbraco helper - this is Transient! very important! // also, if not level.Run, we cannot really use the helper (during upgrade...) diff --git a/src/Umbraco.Web/Templates/ImageSourceParser.cs b/src/Umbraco.Web/Templates/HtmlImageSourceParser.cs similarity index 92% rename from src/Umbraco.Web/Templates/ImageSourceParser.cs rename to src/Umbraco.Web/Templates/HtmlImageSourceParser.cs index 6a0bba4998..b36542167c 100644 --- a/src/Umbraco.Web/Templates/ImageSourceParser.cs +++ b/src/Umbraco.Web/Templates/HtmlImageSourceParser.cs @@ -14,9 +14,9 @@ using Umbraco.Web.Routing; namespace Umbraco.Web.Templates { - public sealed class ImageSourceParser + public sealed class HtmlImageSourceParser { - public ImageSourceParser(IUmbracoContextAccessor umbracoContextAccessor, ILogger logger, IMediaService mediaService, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider) + public HtmlImageSourceParser(IUmbracoContextAccessor umbracoContextAccessor, ILogger logger, IMediaService mediaService, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider) { _umbracoContextAccessor = umbracoContextAccessor; _logger = logger; @@ -99,10 +99,17 @@ namespace Umbraco.Web.Templates ///
/// /// - internal string RemoveImageSources(string text) + public string RemoveImageSources(string text) // see comment in ResolveMediaFromTextString for group reference => ResolveImgPattern.Replace(text, "$1$3$4$5"); + /// + /// Used by the RTE (and grid RTE) for drag/drop/persisting images + /// + /// + /// + /// + /// internal string FindAndPersistPastedTempImages(string html, Guid mediaParentFolder, int userId) { // Find all img's that has data-tmpimg attribute @@ -199,7 +206,7 @@ namespace Umbraco.Web.Templates } catch (Exception ex) { - _logger.Error(typeof(ImageSourceParser), ex, "Could not delete temp file or folder {FileName}", absoluteTempImagePath); + _logger.Error(typeof(HtmlImageSourceParser), ex, "Could not delete temp file or folder {FileName}", absoluteTempImagePath); } } } diff --git a/src/Umbraco.Web/Templates/LocalLinkParser.cs b/src/Umbraco.Web/Templates/HtmlLocalLinkParser.cs similarity index 97% rename from src/Umbraco.Web/Templates/LocalLinkParser.cs rename to src/Umbraco.Web/Templates/HtmlLocalLinkParser.cs index f394f56d85..f65a7183b7 100644 --- a/src/Umbraco.Web/Templates/LocalLinkParser.cs +++ b/src/Umbraco.Web/Templates/HtmlLocalLinkParser.cs @@ -11,7 +11,7 @@ namespace Umbraco.Web.Templates /// /// Utility class used to parse internal links /// - public sealed class LocalLinkParser + public sealed class HtmlLocalLinkParser { private static readonly Regex LocalLinkPattern = new Regex(@"href=""[/]?(?:\{|\%7B)localLink:([a-zA-Z0-9-://]+)(?:\}|\%7D)", @@ -19,7 +19,7 @@ namespace Umbraco.Web.Templates private readonly IUmbracoContextAccessor _umbracoContextAccessor; - public LocalLinkParser(IUmbracoContextAccessor umbracoContextAccessor) + public HtmlLocalLinkParser(IUmbracoContextAccessor umbracoContextAccessor) { _umbracoContextAccessor = umbracoContextAccessor; } diff --git a/src/Umbraco.Web/Templates/UrlParser.cs b/src/Umbraco.Web/Templates/HtmlUrlParser.cs similarity index 95% rename from src/Umbraco.Web/Templates/UrlParser.cs rename to src/Umbraco.Web/Templates/HtmlUrlParser.cs index e5c40b7365..5b78477579 100644 --- a/src/Umbraco.Web/Templates/UrlParser.cs +++ b/src/Umbraco.Web/Templates/HtmlUrlParser.cs @@ -5,7 +5,7 @@ using Umbraco.Core.Logging; namespace Umbraco.Web.Templates { - public sealed class UrlParser + public sealed class HtmlUrlParser { private readonly IContentSection _contentSection; private readonly IProfilingLogger _logger; @@ -13,7 +13,7 @@ namespace Umbraco.Web.Templates private static readonly Regex ResolveUrlPattern = new Regex("(=[\"\']?)(\\W?\\~(?:.(?![\"\']?\\s+(?:\\S+)=|[>\"\']))+.)[\"\']?", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); - public UrlParser(IContentSection contentSection, IProfilingLogger logger) + public HtmlUrlParser(IContentSection contentSection, IProfilingLogger logger) { _contentSection = contentSection; _logger = logger; diff --git a/src/Umbraco.Web/Templates/TemplateUtilities.cs b/src/Umbraco.Web/Templates/TemplateUtilities.cs index db0366dbf3..bcc6691864 100644 --- a/src/Umbraco.Web/Templates/TemplateUtilities.cs +++ b/src/Umbraco.Web/Templates/TemplateUtilities.cs @@ -15,10 +15,10 @@ using File = System.IO.File; namespace Umbraco.Web.Templates { - [Obsolete("This class is obsolete, all methods have been moved to other classes such as InternalLinkHelper, UrlResolver and MediaParser")] + [Obsolete("This class is obsolete, all methods have been moved to other classes: HtmlLocalLinkParser, HtmlUrlParser and HtmlImageSourceParser")] public static class TemplateUtilities { - [Obsolete("Inject and use an instance of InternalLinkParser instead")] + [Obsolete("Inject and use an instance of HtmlLocalLinkParser instead")] internal static string ParseInternalLinks(string text, bool preview, UmbracoContext umbracoContext) { using (umbracoContext.ForcedPreview(preview)) // force for url provider @@ -29,28 +29,28 @@ namespace Umbraco.Web.Templates return text; } - [Obsolete("Inject and use an instance of InternalLinkParser instead")] + [Obsolete("Inject and use an instance of HtmlLocalLinkParser instead")] public static string ParseInternalLinks(string text, UrlProvider urlProvider) - => Current.Factory.GetInstance().EnsureInternalLinks(text); + => Current.Factory.GetInstance().EnsureInternalLinks(text); - [Obsolete("Inject and use an instance of UrlResolver")] + [Obsolete("Inject and use an instance of HtmlUrlParser")] public static string ResolveUrlsFromTextString(string text) - => Current.Factory.GetInstance().EnsureUrls(text); + => Current.Factory.GetInstance().EnsureUrls(text); [Obsolete("Use StringExtensions.CleanForXss instead")] public static string CleanForXss(string text, params char[] ignoreFromClean) => text.CleanForXss(ignoreFromClean); - [Obsolete("Use MediaParser.EnsureImageSources instead")] + [Obsolete("Use HtmlImageSourceParser.EnsureImageSources instead")] public static string ResolveMediaFromTextString(string text) - => Current.Factory.GetInstance().EnsureImageSources(text); + => Current.Factory.GetInstance().EnsureImageSources(text); - [Obsolete("Use MediaParser.RemoveImageSources instead")] + [Obsolete("Use HtmlImageSourceParser.RemoveImageSources instead")] internal static string RemoveMediaUrlsFromTextString(string text) - => Current.Factory.GetInstance().RemoveImageSources(text); + => Current.Factory.GetInstance().RemoveImageSources(text); - [Obsolete("Use MediaParser.RemoveImageSources instead")] + [Obsolete("Use HtmlImageSourceParser.FindAndPersistPastedTempImages instead")] internal static string FindAndPersistPastedTempImages(string html, Guid mediaParentFolder, int userId, IMediaService mediaService, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, ILogger logger) - => Current.Factory.GetInstance().FindAndPersistPastedTempImages(html, mediaParentFolder, userId); + => Current.Factory.GetInstance().FindAndPersistPastedTempImages(html, mediaParentFolder, userId); } } diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 8fc06c75c2..84645f3b2d 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -247,9 +247,9 @@ - - - + + + diff --git a/src/Umbraco.Web/UmbracoComponentRenderer.cs b/src/Umbraco.Web/UmbracoComponentRenderer.cs index 83c8a7f0fa..01c696fd2d 100644 --- a/src/Umbraco.Web/UmbracoComponentRenderer.cs +++ b/src/Umbraco.Web/UmbracoComponentRenderer.cs @@ -27,14 +27,14 @@ namespace Umbraco.Web private readonly IUmbracoContextAccessor _umbracoContextAccessor; private readonly IMacroRenderer _macroRenderer; private readonly ITemplateRenderer _templateRenderer; - private readonly LocalLinkParser _internalLinkParser; + private readonly HtmlLocalLinkParser _linkParser; - public UmbracoComponentRenderer(IUmbracoContextAccessor umbracoContextAccessor, IMacroRenderer macroRenderer, ITemplateRenderer templateRenderer, LocalLinkParser internalLinkParser) + public UmbracoComponentRenderer(IUmbracoContextAccessor umbracoContextAccessor, IMacroRenderer macroRenderer, ITemplateRenderer templateRenderer, HtmlLocalLinkParser linkParser) { _umbracoContextAccessor = umbracoContextAccessor; _macroRenderer = macroRenderer; _templateRenderer = templateRenderer ?? throw new ArgumentNullException(nameof(templateRenderer)); - _internalLinkParser = internalLinkParser; + _linkParser = linkParser; } /// @@ -159,7 +159,7 @@ namespace Umbraco.Web _umbracoContextAccessor.UmbracoContext.HttpContext.Response.ContentType = contentType; //Now, we need to ensure that local links are parsed - html = _internalLinkParser.EnsureInternalLinks(output.ToString()); + html = _linkParser.EnsureInternalLinks(output.ToString()); } } From ba8c1df017d1595abfeee2dbb659b116406fdcd3 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 23 Oct 2019 15:38:14 +1100 Subject: [PATCH 014/202] fixing tests --- src/Umbraco.Tests/Testing/UmbracoTestBase.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Umbraco.Tests/Testing/UmbracoTestBase.cs b/src/Umbraco.Tests/Testing/UmbracoTestBase.cs index 7e72a5aefb..ec265bd540 100644 --- a/src/Umbraco.Tests/Testing/UmbracoTestBase.cs +++ b/src/Umbraco.Tests/Testing/UmbracoTestBase.cs @@ -41,6 +41,7 @@ using Umbraco.Web.Composing.CompositionExtensions; using Umbraco.Web.Sections; using Current = Umbraco.Core.Composing.Current; using FileSystems = Umbraco.Core.IO.FileSystems; +using Umbraco.Web.Templates; namespace Umbraco.Tests.Testing { @@ -230,6 +231,10 @@ namespace Umbraco.Tests.Testing .Append(); Composition.RegisterUnique(); + Composition.RegisterUnique(); + Composition.RegisterUnique(); + Composition.RegisterUnique(); + } protected virtual void ComposeMisc() From c831c9de53b59a39734f3f38e06f89031cfb9b69 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 23 Oct 2019 16:30:22 +1100 Subject: [PATCH 015/202] uses nameof in attributes --- src/Umbraco.Web/Templates/TemplateUtilities.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Web/Templates/TemplateUtilities.cs b/src/Umbraco.Web/Templates/TemplateUtilities.cs index bcc6691864..62d25fa794 100644 --- a/src/Umbraco.Web/Templates/TemplateUtilities.cs +++ b/src/Umbraco.Web/Templates/TemplateUtilities.cs @@ -15,10 +15,10 @@ using File = System.IO.File; namespace Umbraco.Web.Templates { - [Obsolete("This class is obsolete, all methods have been moved to other classes: HtmlLocalLinkParser, HtmlUrlParser and HtmlImageSourceParser")] + [Obsolete("This class is obsolete, all methods have been moved to other classes: " + nameof(HtmlLocalLinkParser) + ", " + nameof(HtmlUrlParser) + " and " + nameof(HtmlImageSourceParser))] public static class TemplateUtilities { - [Obsolete("Inject and use an instance of HtmlLocalLinkParser instead")] + [Obsolete("Inject and use an instance of " + nameof(HtmlLocalLinkParser) + " instead")] internal static string ParseInternalLinks(string text, bool preview, UmbracoContext umbracoContext) { using (umbracoContext.ForcedPreview(preview)) // force for url provider @@ -29,27 +29,27 @@ namespace Umbraco.Web.Templates return text; } - [Obsolete("Inject and use an instance of HtmlLocalLinkParser instead")] + [Obsolete("Inject and use an instance of " + nameof(HtmlLocalLinkParser) + " instead")] public static string ParseInternalLinks(string text, UrlProvider urlProvider) => Current.Factory.GetInstance().EnsureInternalLinks(text); - [Obsolete("Inject and use an instance of HtmlUrlParser")] + [Obsolete("Inject and use an instance of " + nameof(HtmlUrlParser))] public static string ResolveUrlsFromTextString(string text) => Current.Factory.GetInstance().EnsureUrls(text); - [Obsolete("Use StringExtensions.CleanForXss instead")] + [Obsolete("Use " + nameof(StringExtensions) + "." + nameof(StringExtensions.CleanForXss) + " instead")] public static string CleanForXss(string text, params char[] ignoreFromClean) => text.CleanForXss(ignoreFromClean); - [Obsolete("Use HtmlImageSourceParser.EnsureImageSources instead")] + [Obsolete("Use " + nameof(HtmlImageSourceParser) + "." + nameof(HtmlImageSourceParser.EnsureImageSources) + " instead")] public static string ResolveMediaFromTextString(string text) => Current.Factory.GetInstance().EnsureImageSources(text); - [Obsolete("Use HtmlImageSourceParser.RemoveImageSources instead")] + [Obsolete("Use " + nameof(HtmlImageSourceParser) + "." + nameof(HtmlImageSourceParser.RemoveImageSources) + " instead")] internal static string RemoveMediaUrlsFromTextString(string text) => Current.Factory.GetInstance().RemoveImageSources(text); - [Obsolete("Use HtmlImageSourceParser.FindAndPersistPastedTempImages instead")] + [Obsolete("Use " + nameof(HtmlImageSourceParser) + "." + nameof(HtmlImageSourceParser.FindAndPersistPastedTempImages) + " instead")] internal static string FindAndPersistPastedTempImages(string html, Guid mediaParentFolder, int userId, IMediaService mediaService, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, ILogger logger) => Current.Factory.GetInstance().FindAndPersistPastedTempImages(html, mediaParentFolder, userId); } From 9303a497328116a3b995eefaa6dcb7509c226799 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 23 Oct 2019 19:08:03 +1100 Subject: [PATCH 016/202] Moves the copy/paste rte stuff to a separate service, injects lazy property editors and relation service into the base content repositories --- .../Implement/ContentRepositoryBase.cs | 28 +++- .../Implement/DocumentBlueprintRepository.cs | 7 +- .../Implement/DocumentRepository.cs | 20 ++- .../Repositories/Implement/MediaRepository.cs | 6 +- .../Implement/MemberRepository.cs | 7 +- .../PropertyEditorCollection.cs | 4 +- .../Repositories/ContentTypeRepositoryTest.cs | 6 +- .../Repositories/DocumentRepositoryTest.cs | 5 +- .../Repositories/DomainRepositoryTest.cs | 9 +- .../Repositories/MediaRepositoryTest.cs | 6 +- .../Repositories/MemberRepositoryTest.cs | 6 +- .../PublicAccessRepositoryTest.cs | 6 +- .../Repositories/TagRepositoryTest.cs | 54 ++++--- .../Repositories/TemplateRepositoryTest.cs | 12 +- .../Repositories/UserRepositoryTest.cs | 12 +- .../PublishedContentTestBase.cs | 5 +- .../PublishedContent/PublishedContentTests.cs | 5 +- .../Services/ContentServicePerformanceTest.cs | 43 +++--- .../Services/ContentServiceTests.cs | 5 +- .../Templates/ImageSourceParserTests.cs | 9 +- src/Umbraco.Tests/Testing/UmbracoTestBase.cs | 2 + .../PropertyEditors/GridPropertyEditor.cs | 13 +- .../RichTextEditorPastedImages.cs | 143 ++++++++++++++++++ .../PropertyEditors/RichTextPropertyEditor.cs | 12 +- src/Umbraco.Web/Runtime/WebInitialComposer.cs | 2 + .../Templates/HtmlImageSourceParser.cs | 133 +--------------- .../Templates/TemplateUtilities.cs | 5 +- src/Umbraco.Web/Umbraco.Web.csproj | 1 + 28 files changed, 346 insertions(+), 220 deletions(-) create mode 100644 src/Umbraco.Web/PropertyEditors/RichTextEditorPastedImages.cs diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs index 7ab9f10e1c..eede78f4c7 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs @@ -31,20 +31,37 @@ namespace Umbraco.Core.Persistence.Repositories.Implement } internal abstract class ContentRepositoryBase : NPocoRepositoryBase, IContentRepository - where TEntity : class, IUmbracoEntity + where TEntity : class, IContentBase where TRepository : class, IRepository { - protected ContentRepositoryBase(IScopeAccessor scopeAccessor, AppCaches cache, ILanguageRepository languageRepository, ILogger logger) + private readonly Lazy _propertyEditors; + + /// + /// + /// + /// + /// + /// + /// + /// + /// Lazy property value collection - must be lazy because we have a circular dependency since some property editors require services, yet these services require property editors + /// + protected ContentRepositoryBase(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, + ILanguageRepository languageRepository, IRelationRepository relationRepository, + Lazy propertyEditors) : base(scopeAccessor, cache, logger) { LanguageRepository = languageRepository; + RelationRepository = relationRepository; + _propertyEditors = propertyEditors; } protected abstract TRepository This { get; } protected ILanguageRepository LanguageRepository { get; } + protected IRelationRepository RelationRepository { get; } - protected PropertyEditorCollection PropertyEditors => Current.PropertyEditors; // TODO: inject ... this causes circular refs, not sure which refs they are though + protected PropertyEditorCollection PropertyEditors => _propertyEditors.Value; #region Versions @@ -798,5 +815,10 @@ namespace Umbraco.Core.Persistence.Repositories.Implement } #endregion + + protected void PersistRelations(TEntity entity) + { + //foreach(var p in entity.) + } } } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentBlueprintRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentBlueprintRepository.cs index d137d7ac76..de766ee5aa 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentBlueprintRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentBlueprintRepository.cs @@ -2,6 +2,7 @@ using Umbraco.Core.Cache; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Logging; +using Umbraco.Core.PropertyEditors; using Umbraco.Core.Scoping; namespace Umbraco.Core.Persistence.Repositories.Implement @@ -17,8 +18,10 @@ namespace Umbraco.Core.Persistence.Repositories.Implement /// internal class DocumentBlueprintRepository : DocumentRepository, IDocumentBlueprintRepository { - public DocumentBlueprintRepository(IScopeAccessor scopeAccessor, AppCaches appCaches, ILogger logger, IContentTypeRepository contentTypeRepository, ITemplateRepository templateRepository, ITagRepository tagRepository, ILanguageRepository languageRepository) - : base(scopeAccessor, appCaches, logger, contentTypeRepository, templateRepository, tagRepository, languageRepository) + public DocumentBlueprintRepository(IScopeAccessor scopeAccessor, AppCaches appCaches, ILogger logger, + IContentTypeRepository contentTypeRepository, ITemplateRepository templateRepository, ITagRepository tagRepository, ILanguageRepository languageRepository, IRelationRepository relationRepository, + Lazy propertyEditorCollection) + : base(scopeAccessor, appCaches, logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, propertyEditorCollection) { } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs index 2649b9993f..067086ea6b 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs @@ -12,6 +12,7 @@ using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Factories; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.SqlSyntax; +using Umbraco.Core.PropertyEditors; using Umbraco.Core.Scoping; using Umbraco.Core.Services; @@ -30,8 +31,23 @@ namespace Umbraco.Core.Persistence.Repositories.Implement private readonly ContentByGuidReadRepository _contentByGuidReadRepository; private readonly IScopeAccessor _scopeAccessor; - public DocumentRepository(IScopeAccessor scopeAccessor, AppCaches appCaches, ILogger logger, IContentTypeRepository contentTypeRepository, ITemplateRepository templateRepository, ITagRepository tagRepository, ILanguageRepository languageRepository) - : base(scopeAccessor, appCaches, languageRepository, logger) + /// + /// Constructor + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// Lazy property value collection - must be lazy because we have a circular dependency since some property editors require services, yet these services require property editors + /// + public DocumentRepository(IScopeAccessor scopeAccessor, AppCaches appCaches, ILogger logger, + IContentTypeRepository contentTypeRepository, ITemplateRepository templateRepository, ITagRepository tagRepository, ILanguageRepository languageRepository, IRelationRepository relationRepository, + Lazy propertyEditors) + : base(scopeAccessor, appCaches, logger, languageRepository, relationRepository, propertyEditors) { _contentTypeRepository = contentTypeRepository ?? throw new ArgumentNullException(nameof(contentTypeRepository)); _templateRepository = templateRepository ?? throw new ArgumentNullException(nameof(templateRepository)); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs index 25828b8126..0da35145cc 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs @@ -12,6 +12,7 @@ using Umbraco.Core.Models.Entities; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Factories; using Umbraco.Core.Persistence.Querying; +using Umbraco.Core.PropertyEditors; using Umbraco.Core.Scoping; using Umbraco.Core.Services; using static Umbraco.Core.Persistence.NPocoSqlExtensions.Statics; @@ -27,8 +28,9 @@ namespace Umbraco.Core.Persistence.Repositories.Implement private readonly ITagRepository _tagRepository; private readonly MediaByGuidReadRepository _mediaByGuidReadRepository; - public MediaRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IMediaTypeRepository mediaTypeRepository, ITagRepository tagRepository, ILanguageRepository languageRepository) - : base(scopeAccessor, cache, languageRepository, logger) + public MediaRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IMediaTypeRepository mediaTypeRepository, ITagRepository tagRepository, ILanguageRepository languageRepository, IRelationRepository relationRepository, + Lazy propertyEditorCollection) + : base(scopeAccessor, cache, logger, languageRepository, relationRepository, propertyEditorCollection) { _mediaTypeRepository = mediaTypeRepository ?? throw new ArgumentNullException(nameof(mediaTypeRepository)); _tagRepository = tagRepository ?? throw new ArgumentNullException(nameof(tagRepository)); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs index 892122dff9..5274e99cc9 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs @@ -10,6 +10,7 @@ using Umbraco.Core.Models.Entities; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Factories; using Umbraco.Core.Persistence.Querying; +using Umbraco.Core.PropertyEditors; using Umbraco.Core.Scoping; using Umbraco.Core.Services; using static Umbraco.Core.Persistence.NPocoSqlExtensions.Statics; @@ -25,8 +26,10 @@ namespace Umbraco.Core.Persistence.Repositories.Implement private readonly ITagRepository _tagRepository; private readonly IMemberGroupRepository _memberGroupRepository; - public MemberRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IMemberTypeRepository memberTypeRepository, IMemberGroupRepository memberGroupRepository, ITagRepository tagRepository, ILanguageRepository languageRepository) - : base(scopeAccessor, cache, languageRepository, logger) + public MemberRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, + IMemberTypeRepository memberTypeRepository, IMemberGroupRepository memberGroupRepository, ITagRepository tagRepository, ILanguageRepository languageRepository, IRelationRepository relationRepository, + Lazy propertyEditors) + : base(scopeAccessor, cache, logger, languageRepository, relationRepository, propertyEditors) { _memberTypeRepository = memberTypeRepository ?? throw new ArgumentNullException(nameof(memberTypeRepository)); _tagRepository = tagRepository ?? throw new ArgumentNullException(nameof(tagRepository)); diff --git a/src/Umbraco.Core/PropertyEditors/PropertyEditorCollection.cs b/src/Umbraco.Core/PropertyEditors/PropertyEditorCollection.cs index 712a66e55d..21854b63c1 100644 --- a/src/Umbraco.Core/PropertyEditors/PropertyEditorCollection.cs +++ b/src/Umbraco.Core/PropertyEditors/PropertyEditorCollection.cs @@ -4,6 +4,8 @@ using Umbraco.Core.Manifest; namespace Umbraco.Core.PropertyEditors { + + public class PropertyEditorCollection : BuilderCollectionBase { public PropertyEditorCollection(DataEditorCollection dataEditors, ManifestParser manifestParser) @@ -27,4 +29,4 @@ namespace Umbraco.Core.PropertyEditors return editor != null; } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs index f953b9cce6..4d2e8bc999 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs @@ -8,6 +8,7 @@ using Umbraco.Core.Models; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Repositories.Implement; +using Umbraco.Core.PropertyEditors; using Umbraco.Core.Scoping; using Umbraco.Tests.TestHelpers; using Umbraco.Tests.TestHelpers.Entities; @@ -35,7 +36,10 @@ namespace Umbraco.Tests.Persistence.Repositories var commonRepository = new ContentTypeCommonRepository(scopeAccessor, templateRepository, AppCaches.Disabled); contentTypeRepository = new ContentTypeRepository(scopeAccessor, AppCaches.Disabled, Logger, commonRepository, langRepository); var languageRepository = new LanguageRepository(scopeAccessor, AppCaches.Disabled, Logger); - var repository = new DocumentRepository(scopeAccessor, AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository); + var relationTypeRepository = new RelationTypeRepository(scopeAccessor, AppCaches.Disabled, Logger); + var relationRepository = new RelationRepository(scopeAccessor, Logger, relationTypeRepository); + var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); + var repository = new DocumentRepository(scopeAccessor, AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, propertyEditors); return repository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/DocumentRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/DocumentRepositoryTest.cs index 4d62ec8301..c2f897df80 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/DocumentRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/DocumentRepositoryTest.cs @@ -69,7 +69,10 @@ namespace Umbraco.Tests.Persistence.Repositories var commonRepository = new ContentTypeCommonRepository(scopeAccessor, templateRepository, appCaches); var languageRepository = new LanguageRepository(scopeAccessor, appCaches, Logger); contentTypeRepository = new ContentTypeRepository(scopeAccessor, appCaches, Logger, commonRepository, languageRepository); - var repository = new DocumentRepository(scopeAccessor, appCaches, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository); + var relationTypeRepository = new RelationTypeRepository(scopeAccessor, AppCaches.Disabled, Logger); + var relationRepository = new RelationRepository(scopeAccessor, Logger, relationTypeRepository); + var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); + var repository = new DocumentRepository(scopeAccessor, appCaches, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, propertyEditors); return repository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/DomainRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/DomainRepositoryTest.cs index 628f8d75a7..16a59d789f 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/DomainRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/DomainRepositoryTest.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Data; using System.Linq; using Moq; @@ -6,6 +7,7 @@ using NUnit.Framework; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Models; using Umbraco.Core.Persistence.Repositories.Implement; +using Umbraco.Core.PropertyEditors; using Umbraco.Core.Scoping; using Umbraco.Tests.TestHelpers; using Umbraco.Tests.TestHelpers.Entities; @@ -25,7 +27,10 @@ namespace Umbraco.Tests.Persistence.Repositories var commonRepository = new ContentTypeCommonRepository(accessor, templateRepository, AppCaches); languageRepository = new LanguageRepository(accessor, Core.Cache.AppCaches.Disabled, Logger); contentTypeRepository = new ContentTypeRepository(accessor, Core.Cache.AppCaches.Disabled, Logger, commonRepository, languageRepository); - documentRepository = new DocumentRepository(accessor, Core.Cache.AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository); + var relationTypeRepository = new RelationTypeRepository(accessor, Core.Cache.AppCaches.Disabled, Logger); + var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository); + var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); + documentRepository = new DocumentRepository(accessor, Core.Cache.AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, propertyEditors); var domainRepository = new DomainRepository(accessor, Core.Cache.AppCaches.Disabled, Logger); return domainRepository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs index e2123df9e3..c976912220 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs @@ -17,6 +17,7 @@ using Umbraco.Core.Persistence.Repositories.Implement; using Umbraco.Core.Scoping; using Umbraco.Tests.Testing; using Umbraco.Core.Services; +using Umbraco.Core.PropertyEditors; namespace Umbraco.Tests.Persistence.Repositories { @@ -41,7 +42,10 @@ namespace Umbraco.Tests.Persistence.Repositories var languageRepository = new LanguageRepository(scopeAccessor, appCaches, Logger); mediaTypeRepository = new MediaTypeRepository(scopeAccessor, appCaches, Logger, commonRepository, languageRepository); var tagRepository = new TagRepository(scopeAccessor, appCaches, Logger); - var repository = new MediaRepository(scopeAccessor, appCaches, Logger, mediaTypeRepository, tagRepository, Mock.Of()); + var relationTypeRepository = new RelationTypeRepository(scopeAccessor, AppCaches.Disabled, Logger); + var relationRepository = new RelationRepository(scopeAccessor, Logger, relationTypeRepository); + var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); + var repository = new MediaRepository(scopeAccessor, appCaches, Logger, mediaTypeRepository, tagRepository, Mock.Of(), relationRepository, propertyEditors); return repository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs index 17b16ad7ab..9361bd5909 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs @@ -15,6 +15,7 @@ using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Persistence.Repositories.Implement; +using Umbraco.Core.PropertyEditors; using Umbraco.Core.Scoping; using Umbraco.Tests.TestHelpers; using Umbraco.Tests.TestHelpers.Entities; @@ -35,7 +36,10 @@ namespace Umbraco.Tests.Persistence.Repositories memberTypeRepository = new MemberTypeRepository(accessor, AppCaches.Disabled, Logger, commonRepository, languageRepository); memberGroupRepository = new MemberGroupRepository(accessor, AppCaches.Disabled, Logger); var tagRepo = new TagRepository(accessor, AppCaches.Disabled, Logger); - var repository = new MemberRepository(accessor, AppCaches.Disabled, Logger, memberTypeRepository, memberGroupRepository, tagRepo, Mock.Of()); + var relationTypeRepository = new RelationTypeRepository(accessor, AppCaches.Disabled, Logger); + var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository); + var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); + var repository = new MemberRepository(accessor, AppCaches.Disabled, Logger, memberTypeRepository, memberGroupRepository, tagRepo, Mock.Of(), relationRepository, propertyEditors); return repository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/PublicAccessRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/PublicAccessRepositoryTest.cs index 56041c24aa..8cabc18d2c 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/PublicAccessRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/PublicAccessRepositoryTest.cs @@ -7,6 +7,7 @@ using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Models; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Repositories.Implement; +using Umbraco.Core.PropertyEditors; using Umbraco.Core.Scoping; using Umbraco.Tests.TestHelpers; using Umbraco.Tests.TestHelpers.Entities; @@ -310,7 +311,10 @@ namespace Umbraco.Tests.Persistence.Repositories var commonRepository = new ContentTypeCommonRepository(accessor, templateRepository, AppCaches); var languageRepository = new LanguageRepository(accessor, AppCaches, Logger); contentTypeRepository = new ContentTypeRepository(accessor, AppCaches, Logger, commonRepository, languageRepository); - var repository = new DocumentRepository(accessor, AppCaches, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository); + var relationTypeRepository = new RelationTypeRepository(accessor, AppCaches, Logger); + var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository); + var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); + var repository = new DocumentRepository(accessor, AppCaches, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, propertyEditors); return repository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/TagRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/TagRepositoryTest.cs index e3de2c2892..61aec9f74c 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/TagRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/TagRepositoryTest.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System; +using System.Linq; using Moq; using NUnit.Framework; using Umbraco.Core.Cache; @@ -7,6 +8,7 @@ using Umbraco.Core.IO; using Umbraco.Core.Models; using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Persistence.Repositories.Implement; +using Umbraco.Core.PropertyEditors; using Umbraco.Core.Scoping; using Umbraco.Tests.TestHelpers; using Umbraco.Tests.TestHelpers.Entities; @@ -74,7 +76,7 @@ namespace Umbraco.Tests.Persistence.Repositories var provider = TestObjects.GetScopeProvider(Logger); using (ScopeProvider.CreateScope()) { - var contentRepository = CreateContentRepository(provider, out var contentTypeRepository); + var contentRepository = CreateDocumentRepository(provider, out var contentTypeRepository); // create data to relate to var contentType = MockedContentTypes.CreateSimpleContentType("test", "Test"); @@ -104,7 +106,7 @@ namespace Umbraco.Tests.Persistence.Repositories var provider = TestObjects.GetScopeProvider(Logger); using (ScopeProvider.CreateScope()) { - var contentRepository = CreateContentRepository(provider, out var contentTypeRepository); + var contentRepository = CreateDocumentRepository(provider, out var contentTypeRepository); //create data to relate to var contentType = MockedContentTypes.CreateSimpleContentType("test", "Test"); @@ -143,7 +145,7 @@ namespace Umbraco.Tests.Persistence.Repositories var provider = TestObjects.GetScopeProvider(Logger); using (ScopeProvider.CreateScope()) { - var contentRepository = CreateContentRepository(provider, out var contentTypeRepository); + var contentRepository = CreateDocumentRepository(provider, out var contentTypeRepository); //create data to relate to var contentType = MockedContentTypes.CreateSimpleContentType("test", "Test"); @@ -185,7 +187,7 @@ namespace Umbraco.Tests.Persistence.Repositories var provider = TestObjects.GetScopeProvider(Logger); using (ScopeProvider.CreateScope()) { - var contentRepository = CreateContentRepository(provider, out var contentTypeRepository); + var contentRepository = CreateDocumentRepository(provider, out var contentTypeRepository); //create data to relate to var contentType = MockedContentTypes.CreateSimpleContentType("test", "Test"); @@ -225,7 +227,7 @@ namespace Umbraco.Tests.Persistence.Repositories var provider = TestObjects.GetScopeProvider(Logger); using (ScopeProvider.CreateScope()) { - var contentRepository = CreateContentRepository(provider, out var contentTypeRepository); + var contentRepository = CreateDocumentRepository(provider, out var contentTypeRepository); //create data to relate to var contentType = MockedContentTypes.CreateSimpleContentType("test", "Test"); @@ -261,7 +263,7 @@ namespace Umbraco.Tests.Persistence.Repositories var provider = TestObjects.GetScopeProvider(Logger); using (ScopeProvider.CreateScope()) { - var contentRepository = CreateContentRepository(provider, out var contentTypeRepository); + var contentRepository = CreateDocumentRepository(provider, out var contentTypeRepository); //create data to relate to var contentType = MockedContentTypes.CreateSimpleContentType("test", "Test"); @@ -305,7 +307,7 @@ namespace Umbraco.Tests.Persistence.Repositories var provider = TestObjects.GetScopeProvider(Logger); using (ScopeProvider.CreateScope()) { - var contentRepository = CreateContentRepository(provider, out var contentTypeRepository); + var contentRepository = CreateDocumentRepository(provider, out var contentTypeRepository); //create data to relate to var contentType = MockedContentTypes.CreateSimpleContentType("test", "Test"); @@ -349,7 +351,7 @@ namespace Umbraco.Tests.Persistence.Repositories var provider = TestObjects.GetScopeProvider(Logger); using (ScopeProvider.CreateScope()) { - var contentRepository = CreateContentRepository(provider, out var contentTypeRepository); + var contentRepository = CreateDocumentRepository(provider, out var contentTypeRepository); //create data to relate to var contentType = MockedContentTypes.CreateSimpleContentType("test", "Test"); @@ -394,7 +396,7 @@ namespace Umbraco.Tests.Persistence.Repositories var provider = TestObjects.GetScopeProvider(Logger); using (ScopeProvider.CreateScope()) { - var contentRepository = CreateContentRepository(provider, out var contentTypeRepository); + var contentRepository = CreateDocumentRepository(provider, out var contentTypeRepository); //create data to relate to var contentType = MockedContentTypes.CreateSimpleContentType("test", "Test"); @@ -429,7 +431,7 @@ namespace Umbraco.Tests.Persistence.Repositories var provider = TestObjects.GetScopeProvider(Logger); using (ScopeProvider.CreateScope()) { - var contentRepository = CreateContentRepository(provider, out var contentTypeRepository); + var contentRepository = CreateDocumentRepository(provider, out var contentTypeRepository); //create data to relate to var contentType = MockedContentTypes.CreateSimpleContentType("test", "Test"); @@ -469,7 +471,7 @@ namespace Umbraco.Tests.Persistence.Repositories var provider = TestObjects.GetScopeProvider(Logger); using (ScopeProvider.CreateScope()) { - var contentRepository = CreateContentRepository(provider, out var contentTypeRepository); + var contentRepository = CreateDocumentRepository(provider, out var contentTypeRepository); //create data to relate to var contentType = MockedContentTypes.CreateSimpleContentType("test", "Test"); @@ -513,7 +515,7 @@ namespace Umbraco.Tests.Persistence.Repositories var provider = TestObjects.GetScopeProvider(Logger); using (ScopeProvider.CreateScope()) { - var contentRepository = CreateContentRepository(provider, out var contentTypeRepository); + var contentRepository = CreateDocumentRepository(provider, out var contentTypeRepository); //create data to relate to var contentType = MockedContentTypes.CreateSimpleContentType("test", "Test"); @@ -557,7 +559,7 @@ namespace Umbraco.Tests.Persistence.Repositories var provider = TestObjects.GetScopeProvider(Logger); using (ScopeProvider.CreateScope()) { - var contentRepository = CreateContentRepository(provider, out var contentTypeRepository); + var contentRepository = CreateDocumentRepository(provider, out var contentTypeRepository); //create data to relate to var contentType = MockedContentTypes.CreateSimpleContentType("test", "Test"); @@ -601,7 +603,7 @@ namespace Umbraco.Tests.Persistence.Repositories var provider = TestObjects.GetScopeProvider(Logger); using (ScopeProvider.CreateScope()) { - var contentRepository = CreateContentRepository(provider, out var contentTypeRepository); + var contentRepository = CreateDocumentRepository(provider, out var contentTypeRepository); //create data to relate to var contentType = MockedContentTypes.CreateSimpleContentType("test", "Test"); @@ -646,7 +648,7 @@ namespace Umbraco.Tests.Persistence.Repositories var provider = TestObjects.GetScopeProvider(Logger); using (ScopeProvider.CreateScope()) { - var contentRepository = CreateContentRepository(provider, out var contentTypeRepository); + var contentRepository = CreateDocumentRepository(provider, out var contentTypeRepository); var mediaRepository = CreateMediaRepository(provider, out var mediaTypeRepository); //create data to relate to @@ -703,7 +705,7 @@ namespace Umbraco.Tests.Persistence.Repositories var provider = TestObjects.GetScopeProvider(Logger); using (ScopeProvider.CreateScope()) { - var contentRepository = CreateContentRepository(provider, out var contentTypeRepository); + var contentRepository = CreateDocumentRepository(provider, out var contentTypeRepository); var mediaRepository = CreateMediaRepository(provider, out var mediaTypeRepository); //create data to relate to @@ -755,7 +757,7 @@ namespace Umbraco.Tests.Persistence.Repositories var provider = TestObjects.GetScopeProvider(Logger); using (var scope = ScopeProvider.CreateScope()) { - var contentRepository = CreateContentRepository(provider, out var contentTypeRepository); + var contentRepository = CreateDocumentRepository(provider, out var contentTypeRepository); //create data to relate to var contentType = MockedContentTypes.CreateSimpleContentType("test", "Test"); @@ -791,7 +793,7 @@ namespace Umbraco.Tests.Persistence.Repositories var provider = TestObjects.GetScopeProvider(Logger); using (ScopeProvider.CreateScope()) { - var contentRepository = CreateContentRepository(provider, out var contentTypeRepository); + var contentRepository = CreateDocumentRepository(provider, out var contentTypeRepository); var mediaRepository = CreateMediaRepository(provider, out var mediaTypeRepository); //create data to relate to @@ -871,7 +873,7 @@ namespace Umbraco.Tests.Persistence.Repositories var provider = TestObjects.GetScopeProvider(Logger); using (ScopeProvider.CreateScope()) { - var contentRepository = CreateContentRepository(provider, out var contentTypeRepository); + var contentRepository = CreateDocumentRepository(provider, out var contentTypeRepository); var mediaRepository = CreateMediaRepository(provider, out var mediaTypeRepository); //create data to relate to @@ -950,7 +952,7 @@ namespace Umbraco.Tests.Persistence.Repositories return new TagRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger); } - private DocumentRepository CreateContentRepository(IScopeProvider provider, out ContentTypeRepository contentTypeRepository) + private DocumentRepository CreateDocumentRepository(IScopeProvider provider, out ContentTypeRepository contentTypeRepository) { var accessor = (IScopeAccessor) provider; var templateRepository = new TemplateRepository(accessor, AppCaches.Disabled, Logger, TestObjects.GetFileSystemsMock()); @@ -958,7 +960,10 @@ namespace Umbraco.Tests.Persistence.Repositories var commonRepository = new ContentTypeCommonRepository(accessor, templateRepository, AppCaches.Disabled); var languageRepository = new LanguageRepository(accessor, AppCaches.Disabled, Logger); contentTypeRepository = new ContentTypeRepository(accessor, AppCaches.Disabled, Logger, commonRepository, languageRepository); - var repository = new DocumentRepository(accessor, AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository); + var relationTypeRepository = new RelationTypeRepository(accessor, AppCaches.Disabled, Logger); + var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository); + var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); + var repository = new DocumentRepository(accessor, AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, propertyEditors); return repository; } @@ -970,7 +975,10 @@ namespace Umbraco.Tests.Persistence.Repositories var commonRepository = new ContentTypeCommonRepository(accessor, templateRepository, AppCaches.Disabled); var languageRepository = new LanguageRepository(accessor, AppCaches.Disabled, Logger); mediaTypeRepository = new MediaTypeRepository(accessor, AppCaches.Disabled, Logger, commonRepository, languageRepository); - var repository = new MediaRepository(accessor, AppCaches.Disabled, Logger, mediaTypeRepository, tagRepository, Mock.Of()); + var relationTypeRepository = new RelationTypeRepository(accessor, AppCaches.Disabled, Logger); + var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository); + var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); + var repository = new MediaRepository(accessor, AppCaches.Disabled, Logger, mediaTypeRepository, tagRepository, Mock.Of(), relationRepository, propertyEditors); return repository; } } diff --git a/src/Umbraco.Tests/Persistence/Repositories/TemplateRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/TemplateRepositoryTest.cs index b0f9a5335b..7bf113dfc3 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/TemplateRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/TemplateRepositoryTest.cs @@ -12,6 +12,7 @@ using Umbraco.Core.IO; using Umbraco.Core.Models; using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Persistence.Repositories.Implement; +using Umbraco.Core.PropertyEditors; using Umbraco.Core.Scoping; using Umbraco.Tests.TestHelpers; using Umbraco.Tests.TestHelpers.Entities; @@ -237,11 +238,14 @@ namespace Umbraco.Tests.Persistence.Repositories { var templateRepository = CreateRepository(ScopeProvider); - var tagRepository = new TagRepository((IScopeAccessor) ScopeProvider, AppCaches.Disabled, Logger); + var tagRepository = new TagRepository(ScopeProvider, AppCaches.Disabled, Logger); var commonRepository = new ContentTypeCommonRepository(ScopeProvider, templateRepository, AppCaches); - var languageRepository = new LanguageRepository((IScopeAccessor) ScopeProvider, AppCaches.Disabled, Logger); - var contentTypeRepository = new ContentTypeRepository((IScopeAccessor) ScopeProvider, AppCaches.Disabled, Logger, commonRepository, languageRepository); - var contentRepo = new DocumentRepository((IScopeAccessor) ScopeProvider, AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository); + var languageRepository = new LanguageRepository(ScopeProvider, AppCaches.Disabled, Logger); + var contentTypeRepository = new ContentTypeRepository(ScopeProvider, AppCaches.Disabled, Logger, commonRepository, languageRepository); + var relationTypeRepository = new RelationTypeRepository(ScopeProvider, AppCaches.Disabled, Logger); + var relationRepository = new RelationRepository(ScopeProvider, Logger, relationTypeRepository); + var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); + var contentRepo = new DocumentRepository(ScopeProvider, AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, propertyEditors); var contentType = MockedContentTypes.CreateSimpleContentType("umbTextpage2", "Textpage"); ServiceContext.FileService.SaveTemplate(contentType.DefaultTemplate); // else, FK violation on contentType! diff --git a/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs index 3e5919d7f3..01a5573119 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs @@ -14,6 +14,8 @@ using Umbraco.Tests.TestHelpers; using Umbraco.Tests.TestHelpers.Entities; using Umbraco.Tests.Testing; using Umbraco.Core.Persistence; +using Umbraco.Core.PropertyEditors; +using System; namespace Umbraco.Tests.Persistence.Repositories { @@ -29,7 +31,10 @@ namespace Umbraco.Tests.Persistence.Repositories var languageRepository = new LanguageRepository(accessor, AppCaches, Logger); mediaTypeRepository = new MediaTypeRepository(accessor, AppCaches, Mock.Of(), commonRepository, languageRepository); var tagRepository = new TagRepository(accessor, AppCaches, Mock.Of()); - var repository = new MediaRepository(accessor, AppCaches, Mock.Of(), mediaTypeRepository, tagRepository, Mock.Of()); + var relationTypeRepository = new RelationTypeRepository(accessor, AppCaches.Disabled, Logger); + var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository); + var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); + var repository = new MediaRepository(accessor, AppCaches, Mock.Of(), mediaTypeRepository, tagRepository, Mock.Of(), relationRepository, propertyEditors); return repository; } @@ -47,7 +52,10 @@ namespace Umbraco.Tests.Persistence.Repositories var commonRepository = new ContentTypeCommonRepository(accessor, templateRepository, AppCaches); var languageRepository = new LanguageRepository(accessor, AppCaches, Logger); contentTypeRepository = new ContentTypeRepository(accessor, AppCaches, Logger, commonRepository, languageRepository); - var repository = new DocumentRepository(accessor, AppCaches, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository); + var relationTypeRepository = new RelationTypeRepository(accessor, AppCaches.Disabled, Logger); + var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository); + var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); + var repository = new DocumentRepository(accessor, AppCaches, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, propertyEditors); return repository; } diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentTestBase.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentTestBase.cs index 497c621963..a96cad4076 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentTestBase.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentTestBase.cs @@ -42,10 +42,11 @@ namespace Umbraco.Tests.PublishedContent var umbracoContextAccessor = Mock.Of(); var logger = Mock.Of(); - var imageSourceParser = new HtmlImageSourceParser(umbracoContextAccessor, logger, Mock.Of(), Mock.Of()); + var imageSourceParser = new HtmlImageSourceParser(umbracoContextAccessor); + var pastedImages = new RichTextEditorPastedImages(umbracoContextAccessor, logger, Mock.Of(), Mock.Of()); var localLinkParser = new HtmlLocalLinkParser(umbracoContextAccessor); var dataTypeService = new TestObjects.TestDataTypeService( - new DataType(new RichTextPropertyEditor(logger, umbracoContextAccessor, imageSourceParser, localLinkParser)) { Id = 1 }); + new DataType(new RichTextPropertyEditor(logger, umbracoContextAccessor, imageSourceParser, localLinkParser, pastedImages)) { Id = 1 }); var publishedContentTypeFactory = new PublishedContentTypeFactory(Mock.Of(), converters, dataTypeService); diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs index 9f5dc81986..be63e3b5da 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs @@ -46,13 +46,14 @@ namespace Umbraco.Tests.PublishedContent var mediaService = Mock.Of(); var contentTypeBaseServiceProvider = Mock.Of(); var umbracoContextAccessor = Mock.Of(); - var imageSourceParser = new HtmlImageSourceParser(umbracoContextAccessor, logger, mediaService, contentTypeBaseServiceProvider); + var imageSourceParser = new HtmlImageSourceParser(umbracoContextAccessor); + var pastedImages = new RichTextEditorPastedImages(umbracoContextAccessor, logger, mediaService, contentTypeBaseServiceProvider); var linkParser = new HtmlLocalLinkParser(umbracoContextAccessor); var dataTypeService = new TestObjects.TestDataTypeService( new DataType(new VoidEditor(logger)) { Id = 1 }, new DataType(new TrueFalsePropertyEditor(logger)) { Id = 1001 }, - new DataType(new RichTextPropertyEditor(logger, umbracoContextAccessor, imageSourceParser, linkParser)) { Id = 1002 }, + new DataType(new RichTextPropertyEditor(logger, umbracoContextAccessor, imageSourceParser, linkParser, pastedImages)) { Id = 1002 }, new DataType(new IntegerPropertyEditor(logger)) { Id = 1003 }, new DataType(new TextboxPropertyEditor(logger)) { Id = 1004 }, new DataType(new MediaPickerPropertyEditor(logger)) { Id = 1005 }); diff --git a/src/Umbraco.Tests/Services/ContentServicePerformanceTest.cs b/src/Umbraco.Tests/Services/ContentServicePerformanceTest.cs index ef80672baf..5de5d9bd16 100644 --- a/src/Umbraco.Tests/Services/ContentServicePerformanceTest.cs +++ b/src/Umbraco.Tests/Services/ContentServicePerformanceTest.cs @@ -14,6 +14,7 @@ using Umbraco.Core.Models; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Persistence.Repositories.Implement; +using Umbraco.Core.PropertyEditors; using Umbraco.Core.Scoping; using Umbraco.Tests.TestHelpers; using Umbraco.Tests.TestHelpers.Entities; @@ -39,6 +40,20 @@ namespace Umbraco.Tests.Services Composition.Register(); } + private DocumentRepository CreateDocumentRepository(IScopeProvider provider) + { + var tRepository = new TemplateRepository((IScopeAccessor)provider, AppCaches.Disabled, Logger, TestObjects.GetFileSystemsMock()); + var tagRepo = new TagRepository((IScopeAccessor)provider, AppCaches.Disabled, Logger); + var commonRepository = new ContentTypeCommonRepository((IScopeAccessor)provider, tRepository, AppCaches); + var languageRepository = new LanguageRepository((IScopeAccessor)provider, AppCaches.Disabled, Logger); + var ctRepository = new ContentTypeRepository((IScopeAccessor)provider, AppCaches.Disabled, Logger, commonRepository, languageRepository); + var relationTypeRepository = new RelationTypeRepository((IScopeAccessor)provider, AppCaches.Disabled, Logger); + var relationRepository = new RelationRepository((IScopeAccessor)provider, Logger, relationTypeRepository); + var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); + var repository = new DocumentRepository((IScopeAccessor)provider, AppCaches.Disabled, Logger, ctRepository, tRepository, tagRepo, languageRepository, relationRepository, propertyEditors); + return repository; + } + [Test] public void Profiler() { @@ -163,12 +178,7 @@ namespace Umbraco.Tests.Services var provider = TestObjects.GetScopeProvider(Logger); using (var scope = provider.CreateScope()) { - var tRepository = new TemplateRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger, TestObjects.GetFileSystemsMock()); - var tagRepo = new TagRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger); - var commonRepository = new ContentTypeCommonRepository((IScopeAccessor)provider, tRepository, AppCaches); - var languageRepository = new LanguageRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger); - var ctRepository = new ContentTypeRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger, commonRepository, languageRepository); - var repository = new DocumentRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger, ctRepository, tRepository, tagRepo, languageRepository); + var repository = CreateDocumentRepository(provider); // Act Stopwatch watch = Stopwatch.StartNew(); @@ -197,12 +207,7 @@ namespace Umbraco.Tests.Services var provider = TestObjects.GetScopeProvider(Logger); using (var scope = provider.CreateScope()) { - var tRepository = new TemplateRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger, TestObjects.GetFileSystemsMock()); - var tagRepo = new TagRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger); - var commonRepository = new ContentTypeCommonRepository((IScopeAccessor)provider, tRepository, AppCaches); - var languageRepository = new LanguageRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger); - var ctRepository = new ContentTypeRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger, commonRepository, languageRepository); - var repository = new DocumentRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger, ctRepository, tRepository, tagRepo, languageRepository); + var repository = CreateDocumentRepository(provider); // Act Stopwatch watch = Stopwatch.StartNew(); @@ -229,12 +234,7 @@ namespace Umbraco.Tests.Services var provider = TestObjects.GetScopeProvider(Logger); using (var scope = provider.CreateScope()) { - var tRepository = new TemplateRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger, TestObjects.GetFileSystemsMock()); - var tagRepo = new TagRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger); - var commonRepository = new ContentTypeCommonRepository((IScopeAccessor) provider, tRepository, AppCaches); - var languageRepository = new LanguageRepository((IScopeAccessor)provider, AppCaches.Disabled, Logger); - var ctRepository = new ContentTypeRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger, commonRepository, languageRepository); - var repository = new DocumentRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger, ctRepository, tRepository, tagRepo, languageRepository); + var repository = CreateDocumentRepository(provider); // Act var contents = repository.GetMany(); @@ -264,12 +264,7 @@ namespace Umbraco.Tests.Services var provider = TestObjects.GetScopeProvider(Logger); using (var scope = provider.CreateScope()) { - var tRepository = new TemplateRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger, TestObjects.GetFileSystemsMock()); - var tagRepo = new TagRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger); - var commonRepository = new ContentTypeCommonRepository((IScopeAccessor)provider, tRepository, AppCaches); - var languageRepository = new LanguageRepository((IScopeAccessor)provider, AppCaches.Disabled, Logger); - var ctRepository = new ContentTypeRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger, commonRepository, languageRepository); - var repository = new DocumentRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger, ctRepository, tRepository, tagRepo, languageRepository); + var repository = CreateDocumentRepository(provider); // Act var contents = repository.GetMany(); diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs index e26e764cd1..bd97772d12 100644 --- a/src/Umbraco.Tests/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs @@ -3166,7 +3166,10 @@ namespace Umbraco.Tests.Services var commonRepository = new ContentTypeCommonRepository(accessor, templateRepository, AppCaches); var languageRepository = new LanguageRepository(accessor, AppCaches.Disabled, Logger); contentTypeRepository = new ContentTypeRepository(accessor, AppCaches.Disabled, Logger, commonRepository, languageRepository); - var repository = new DocumentRepository(accessor, AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository); + var relationTypeRepository = new RelationTypeRepository(accessor, AppCaches.Disabled, Logger); + var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository); + var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); + var repository = new DocumentRepository(accessor, AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, propertyEditors); return repository; } diff --git a/src/Umbraco.Tests/Templates/ImageSourceParserTests.cs b/src/Umbraco.Tests/Templates/ImageSourceParserTests.cs index ca01caf344..dbe5654117 100644 --- a/src/Umbraco.Tests/Templates/ImageSourceParserTests.cs +++ b/src/Umbraco.Tests/Templates/ImageSourceParserTests.cs @@ -13,6 +13,7 @@ using System; using System.Linq; using Umbraco.Core.Models; using Umbraco.Core; +using Umbraco.Web.PropertyEditors; namespace Umbraco.Tests.Templates { @@ -32,8 +33,8 @@ namespace Umbraco.Tests.Templates var logger = Mock.Of(); var umbracoContextAccessor = new TestUmbracoContextAccessor(); - var imageSourceParser = new HtmlImageSourceParser(umbracoContextAccessor, logger, Mock.Of(), Mock.Of()); - + var imageSourceParser = new HtmlImageSourceParser(umbracoContextAccessor); + var result = imageSourceParser.FindUdisFromDataAttributes(input).ToList(); Assert.AreEqual(2, result.Count); Assert.AreEqual(Udi.Parse("umb://media/D4B18427A1544721B09AC7692F35C264"), result[0]); @@ -45,7 +46,7 @@ namespace Umbraco.Tests.Templates { var logger = Mock.Of(); var umbracoContextAccessor = new TestUmbracoContextAccessor(); - var imageSourceParser = new HtmlImageSourceParser(umbracoContextAccessor, logger, Mock.Of(), Mock.Of()); + var imageSourceParser = new HtmlImageSourceParser(umbracoContextAccessor); var result = imageSourceParser.RemoveImageSources(@"

@@ -87,7 +88,7 @@ namespace Umbraco.Tests.Templates var mediaCache = Mock.Get(reference.UmbracoContext.Media); mediaCache.Setup(x => x.GetById(It.IsAny())).Returns(media.Object); - var imageSourceParser = new HtmlImageSourceParser(umbracoContextAccessor, Mock.Of(), Mock.Of(), Mock.Of()); + var imageSourceParser = new HtmlImageSourceParser(umbracoContextAccessor); var result = imageSourceParser.EnsureImageSources(@"

diff --git a/src/Umbraco.Tests/Testing/UmbracoTestBase.cs b/src/Umbraco.Tests/Testing/UmbracoTestBase.cs index ec265bd540..ffc49b47ac 100644 --- a/src/Umbraco.Tests/Testing/UmbracoTestBase.cs +++ b/src/Umbraco.Tests/Testing/UmbracoTestBase.cs @@ -42,6 +42,7 @@ using Umbraco.Web.Sections; using Current = Umbraco.Core.Composing.Current; using FileSystems = Umbraco.Core.IO.FileSystems; using Umbraco.Web.Templates; +using Umbraco.Web.PropertyEditors; namespace Umbraco.Tests.Testing { @@ -234,6 +235,7 @@ namespace Umbraco.Tests.Testing Composition.RegisterUnique(); Composition.RegisterUnique(); Composition.RegisterUnique(); + Composition.RegisterUnique(); } diff --git a/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs index 6481099f45..4eea36300b 100644 --- a/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs @@ -12,6 +12,7 @@ using Umbraco.Web.Templates; namespace Umbraco.Web.PropertyEditors { + /// /// Represents a grid property and parameter editor. /// @@ -27,12 +28,14 @@ namespace Umbraco.Web.PropertyEditors { private IUmbracoContextAccessor _umbracoContextAccessor; private readonly HtmlImageSourceParser _imageSourceParser; + private readonly RichTextEditorPastedImages _pastedImages; - public GridPropertyEditor(ILogger logger, IUmbracoContextAccessor umbracoContextAccessor, HtmlImageSourceParser imageSourceParser) + public GridPropertyEditor(ILogger logger, IUmbracoContextAccessor umbracoContextAccessor, HtmlImageSourceParser imageSourceParser, RichTextEditorPastedImages pastedImages) : base(logger) { _umbracoContextAccessor = umbracoContextAccessor; _imageSourceParser = imageSourceParser; + _pastedImages = pastedImages; } public override IPropertyIndexValueFactory PropertyIndexValueFactory => new GridPropertyIndexValueFactory(); @@ -41,7 +44,7 @@ namespace Umbraco.Web.PropertyEditors /// Overridden to ensure that the value is validated ///
/// - protected override IDataValueEditor CreateValueEditor() => new GridPropertyValueEditor(Attribute, _umbracoContextAccessor, _imageSourceParser); + protected override IDataValueEditor CreateValueEditor() => new GridPropertyValueEditor(Attribute, _umbracoContextAccessor, _imageSourceParser, _pastedImages); protected override IConfigurationEditor CreateConfigurationEditor() => new GridConfigurationEditor(); @@ -49,12 +52,14 @@ namespace Umbraco.Web.PropertyEditors { private IUmbracoContextAccessor _umbracoContextAccessor; private readonly HtmlImageSourceParser _imageSourceParser; + private readonly RichTextEditorPastedImages _pastedImages; - public GridPropertyValueEditor(DataEditorAttribute attribute, IUmbracoContextAccessor umbracoContextAccessor, HtmlImageSourceParser imageSourceParser) + public GridPropertyValueEditor(DataEditorAttribute attribute, IUmbracoContextAccessor umbracoContextAccessor, HtmlImageSourceParser imageSourceParser, RichTextEditorPastedImages pastedImages) : base(attribute) { _umbracoContextAccessor = umbracoContextAccessor; _imageSourceParser = imageSourceParser; + _pastedImages = pastedImages; } /// @@ -89,7 +94,7 @@ namespace Umbraco.Web.PropertyEditors // Parse the HTML var html = rte.Value?.ToString(); - var parseAndSavedTempImages = _imageSourceParser.FindAndPersistPastedTempImages(html, mediaParentId, userId); + var parseAndSavedTempImages = _pastedImages.FindAndPersistPastedTempImages(html, mediaParentId, userId); var editorValueWithMediaUrlsRemoved = _imageSourceParser.RemoveImageSources(parseAndSavedTempImages); rte.Value = editorValueWithMediaUrlsRemoved; diff --git a/src/Umbraco.Web/PropertyEditors/RichTextEditorPastedImages.cs b/src/Umbraco.Web/PropertyEditors/RichTextEditorPastedImages.cs new file mode 100644 index 0000000000..0b2a607f8b --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/RichTextEditorPastedImages.cs @@ -0,0 +1,143 @@ +using HtmlAgilityPack; +using System; +using System.Collections.Generic; +using System.IO; +using Umbraco.Core; +using Umbraco.Core.Exceptions; +using Umbraco.Core.IO; +using Umbraco.Core.Logging; +using Umbraco.Core.Models; +using Umbraco.Core.Services; +using Umbraco.Web.Templates; + +namespace Umbraco.Web.PropertyEditors +{ + public sealed class RichTextEditorPastedImages + { + private readonly IUmbracoContextAccessor _umbracoContextAccessor; + private readonly ILogger _logger; + private readonly IMediaService _mediaService; + private readonly IContentTypeBaseServiceProvider _contentTypeBaseServiceProvider; + + const string TemporaryImageDataAttribute = "data-tmpimg"; + + public RichTextEditorPastedImages(IUmbracoContextAccessor umbracoContextAccessor, ILogger logger, IMediaService mediaService, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider) + { + _umbracoContextAccessor = umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _mediaService = mediaService ?? throw new ArgumentNullException(nameof(mediaService)); + _contentTypeBaseServiceProvider = contentTypeBaseServiceProvider ?? throw new ArgumentNullException(nameof(contentTypeBaseServiceProvider)); + } + + /// + /// Used by the RTE (and grid RTE) for drag/drop/persisting images + /// + /// + /// + /// + /// + internal string FindAndPersistPastedTempImages(string html, Guid mediaParentFolder, int userId) + { + // Find all img's that has data-tmpimg attribute + // Use HTML Agility Pack - https://html-agility-pack.net + var htmlDoc = new HtmlDocument(); + htmlDoc.LoadHtml(html); + + var tmpImages = htmlDoc.DocumentNode.SelectNodes($"//img[@{TemporaryImageDataAttribute}]"); + if (tmpImages == null || tmpImages.Count == 0) + return html; + + // An array to contain a list of URLs that + // we have already processed to avoid dupes + var uploadedImages = new Dictionary(); + + foreach (var img in tmpImages) + { + // The data attribute contains the path to the tmp img to persist as a media item + var tmpImgPath = img.GetAttributeValue(TemporaryImageDataAttribute, string.Empty); + + if (string.IsNullOrEmpty(tmpImgPath)) + continue; + + var absoluteTempImagePath = IOHelper.MapPath(tmpImgPath); + var fileName = Path.GetFileName(absoluteTempImagePath); + var safeFileName = fileName.ToSafeFileName(); + + var mediaItemName = safeFileName.ToFriendlyName(); + IMedia mediaFile; + GuidUdi udi; + + if (uploadedImages.ContainsKey(tmpImgPath) == false) + { + if (mediaParentFolder == Guid.Empty) + mediaFile = _mediaService.CreateMedia(mediaItemName, Constants.System.Root, Constants.Conventions.MediaTypes.Image, userId); + else + mediaFile = _mediaService.CreateMedia(mediaItemName, mediaParentFolder, Constants.Conventions.MediaTypes.Image, userId); + + var fileInfo = new FileInfo(absoluteTempImagePath); + + var fileStream = fileInfo.OpenReadWithRetry(); + if (fileStream == null) throw new InvalidOperationException("Could not acquire file stream"); + using (fileStream) + { + mediaFile.SetValue(_contentTypeBaseServiceProvider, Constants.Conventions.Media.File, safeFileName, fileStream); + } + + _mediaService.Save(mediaFile, userId); + + udi = mediaFile.GetUdi(); + } + else + { + // Already been uploaded & we have it's UDI + udi = uploadedImages[tmpImgPath]; + } + + // Add the UDI to the img element as new data attribute + img.SetAttributeValue("data-udi", udi.ToString()); + + // Get the new persisted image url + var mediaTyped = _umbracoContextAccessor?.UmbracoContext?.Media.GetById(udi.Guid); + if (mediaTyped == null) + throw new PanicException($"Could not find media by id {udi.Guid} or there was no UmbracoContext available."); + + var location = mediaTyped.Url; + + // Find the width & height attributes as we need to set the imageprocessor QueryString + var width = img.GetAttributeValue("width", int.MinValue); + var height = img.GetAttributeValue("height", int.MinValue); + + if (width != int.MinValue && height != int.MinValue) + { + location = $"{location}?width={width}&height={height}&mode=max"; + } + + img.SetAttributeValue("src", location); + + // Remove the data attribute (so we do not re-process this) + img.Attributes.Remove(TemporaryImageDataAttribute); + + // Add to the dictionary to avoid dupes + if (uploadedImages.ContainsKey(tmpImgPath) == false) + { + uploadedImages.Add(tmpImgPath, udi); + + // Delete folder & image now its saved in media + // The folder should contain one image - as a unique guid folder created + // for each image uploaded from TinyMceController + var folderName = Path.GetDirectoryName(absoluteTempImagePath); + try + { + Directory.Delete(folderName, true); + } + catch (Exception ex) + { + _logger.Error(typeof(HtmlImageSourceParser), ex, "Could not delete temp file or folder {FileName}", absoluteTempImagePath); + } + } + } + + return htmlDoc.DocumentNode.OuterHtml; + } + } +} diff --git a/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs index 0dbe5426a2..0287bdfefe 100644 --- a/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs @@ -28,24 +28,26 @@ namespace Umbraco.Web.PropertyEditors private IUmbracoContextAccessor _umbracoContextAccessor; private readonly HtmlImageSourceParser _imageSourceParser; private readonly HtmlLocalLinkParser _localLinkParser; + private readonly RichTextEditorPastedImages _pastedImages; /// /// The constructor will setup the property editor based on the attribute if one is found /// - public RichTextPropertyEditor(ILogger logger, IUmbracoContextAccessor umbracoContextAccessor, HtmlImageSourceParser imageSourceParser, HtmlLocalLinkParser localLinkParser) + public RichTextPropertyEditor(ILogger logger, IUmbracoContextAccessor umbracoContextAccessor, HtmlImageSourceParser imageSourceParser, HtmlLocalLinkParser localLinkParser, RichTextEditorPastedImages pastedImages) : base(logger) { _umbracoContextAccessor = umbracoContextAccessor; _imageSourceParser = imageSourceParser; _localLinkParser = localLinkParser; + _pastedImages = pastedImages; } /// /// Create a custom value editor /// /// - protected override IDataValueEditor CreateValueEditor() => new RichTextPropertyValueEditor(Attribute, _umbracoContextAccessor, _imageSourceParser, _localLinkParser); + protected override IDataValueEditor CreateValueEditor() => new RichTextPropertyValueEditor(Attribute, _umbracoContextAccessor, _imageSourceParser, _localLinkParser, _pastedImages); protected override IConfigurationEditor CreateConfigurationEditor() => new RichTextConfigurationEditor(); @@ -59,13 +61,15 @@ namespace Umbraco.Web.PropertyEditors private IUmbracoContextAccessor _umbracoContextAccessor; private readonly HtmlImageSourceParser _imageSourceParser; private readonly HtmlLocalLinkParser _localLinkParser; + private readonly RichTextEditorPastedImages _pastedImages; - public RichTextPropertyValueEditor(DataEditorAttribute attribute, IUmbracoContextAccessor umbracoContextAccessor, HtmlImageSourceParser imageSourceParser, HtmlLocalLinkParser localLinkParser) + public RichTextPropertyValueEditor(DataEditorAttribute attribute, IUmbracoContextAccessor umbracoContextAccessor, HtmlImageSourceParser imageSourceParser, HtmlLocalLinkParser localLinkParser, RichTextEditorPastedImages pastedImages) : base(attribute) { _umbracoContextAccessor = umbracoContextAccessor; _imageSourceParser = imageSourceParser; _localLinkParser = localLinkParser; + _pastedImages = pastedImages; } /// @@ -119,7 +123,7 @@ namespace Umbraco.Web.PropertyEditors var mediaParent = config?.MediaParentId; var mediaParentId = mediaParent == null ? Guid.Empty : mediaParent.Guid; - var parseAndSavedTempImages = _imageSourceParser.FindAndPersistPastedTempImages(editorValue.Value.ToString(), mediaParentId, userId); + var parseAndSavedTempImages = _pastedImages.FindAndPersistPastedTempImages(editorValue.Value.ToString(), mediaParentId, userId); var editorValueWithMediaUrlsRemoved = _imageSourceParser.RemoveImageSources(parseAndSavedTempImages); var parsed = MacroTagParser.FormatRichTextContentForPersistence(editorValueWithMediaUrlsRemoved); diff --git a/src/Umbraco.Web/Runtime/WebInitialComposer.cs b/src/Umbraco.Web/Runtime/WebInitialComposer.cs index 5ccb16a1a5..4de5e8627a 100644 --- a/src/Umbraco.Web/Runtime/WebInitialComposer.cs +++ b/src/Umbraco.Web/Runtime/WebInitialComposer.cs @@ -41,6 +41,7 @@ using Umbraco.Web.Tour; using Umbraco.Web.Trees; using Umbraco.Web.WebApi; using Current = Umbraco.Web.Composing.Current; +using Umbraco.Web.PropertyEditors; namespace Umbraco.Web.Runtime { @@ -110,6 +111,7 @@ namespace Umbraco.Web.Runtime composition.RegisterUnique(); composition.RegisterUnique(); composition.RegisterUnique(); + composition.RegisterUnique(); // register the umbraco helper - this is Transient! very important! // also, if not level.Run, we cannot really use the helper (during upgrade...) diff --git a/src/Umbraco.Web/Templates/HtmlImageSourceParser.cs b/src/Umbraco.Web/Templates/HtmlImageSourceParser.cs index b36542167c..66a1cee5bb 100644 --- a/src/Umbraco.Web/Templates/HtmlImageSourceParser.cs +++ b/src/Umbraco.Web/Templates/HtmlImageSourceParser.cs @@ -1,36 +1,20 @@ -using HtmlAgilityPack; -using System; -using System.Collections.Generic; -using System.IO; +using System.Collections.Generic; using System.Text.RegularExpressions; using Umbraco.Core; -using Umbraco.Core.Exceptions; -using Umbraco.Core.IO; -using Umbraco.Core.Logging; -using Umbraco.Core.Models; -using Umbraco.Core.Services; -using Umbraco.Web.Routing; namespace Umbraco.Web.Templates { public sealed class HtmlImageSourceParser { - public HtmlImageSourceParser(IUmbracoContextAccessor umbracoContextAccessor, ILogger logger, IMediaService mediaService, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider) + public HtmlImageSourceParser(IUmbracoContextAccessor umbracoContextAccessor) { _umbracoContextAccessor = umbracoContextAccessor; - _logger = logger; - _mediaService = mediaService; - _contentTypeBaseServiceProvider = contentTypeBaseServiceProvider; } private static readonly Regex ResolveImgPattern = new Regex(@"(]*src="")([^""\?]*)([^""]*""[^>]*data-udi="")([^""]*)(""[^>]*>)", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); - private readonly IUmbracoContextAccessor _umbracoContextAccessor; - private readonly ILogger _logger; - private readonly IMediaService _mediaService; - private readonly IContentTypeBaseServiceProvider _contentTypeBaseServiceProvider; - const string TemporaryImageDataAttribute = "data-tmpimg"; + private readonly IUmbracoContextAccessor _umbracoContextAccessor; private static readonly Regex DataUdiAttributeRegex = new Regex(@"data-udi=\\?(?:""|')(?umb://[A-z0-9\-]+/[A-z0-9]+)\\?(?:""|')", RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); @@ -103,115 +87,6 @@ namespace Umbraco.Web.Templates // see comment in ResolveMediaFromTextString for group reference => ResolveImgPattern.Replace(text, "$1$3$4$5"); - /// - /// Used by the RTE (and grid RTE) for drag/drop/persisting images - /// - /// - /// - /// - /// - internal string FindAndPersistPastedTempImages(string html, Guid mediaParentFolder, int userId) - { - // Find all img's that has data-tmpimg attribute - // Use HTML Agility Pack - https://html-agility-pack.net - var htmlDoc = new HtmlDocument(); - htmlDoc.LoadHtml(html); - - var tmpImages = htmlDoc.DocumentNode.SelectNodes($"//img[@{TemporaryImageDataAttribute}]"); - if (tmpImages == null || tmpImages.Count == 0) - return html; - - // An array to contain a list of URLs that - // we have already processed to avoid dupes - var uploadedImages = new Dictionary(); - - foreach (var img in tmpImages) - { - // The data attribute contains the path to the tmp img to persist as a media item - var tmpImgPath = img.GetAttributeValue(TemporaryImageDataAttribute, string.Empty); - - if (string.IsNullOrEmpty(tmpImgPath)) - continue; - - var absoluteTempImagePath = IOHelper.MapPath(tmpImgPath); - var fileName = Path.GetFileName(absoluteTempImagePath); - var safeFileName = fileName.ToSafeFileName(); - - var mediaItemName = safeFileName.ToFriendlyName(); - IMedia mediaFile; - GuidUdi udi; - - if (uploadedImages.ContainsKey(tmpImgPath) == false) - { - if (mediaParentFolder == Guid.Empty) - mediaFile = _mediaService.CreateMedia(mediaItemName, Constants.System.Root, Constants.Conventions.MediaTypes.Image, userId); - else - mediaFile = _mediaService.CreateMedia(mediaItemName, mediaParentFolder, Constants.Conventions.MediaTypes.Image, userId); - - var fileInfo = new FileInfo(absoluteTempImagePath); - - var fileStream = fileInfo.OpenReadWithRetry(); - if (fileStream == null) throw new InvalidOperationException("Could not acquire file stream"); - using (fileStream) - { - mediaFile.SetValue(_contentTypeBaseServiceProvider, Constants.Conventions.Media.File, safeFileName, fileStream); - } - - _mediaService.Save(mediaFile, userId); - - udi = mediaFile.GetUdi(); - } - else - { - // Already been uploaded & we have it's UDI - udi = uploadedImages[tmpImgPath]; - } - - // Add the UDI to the img element as new data attribute - img.SetAttributeValue("data-udi", udi.ToString()); - - // Get the new persisted image url - var mediaTyped = _umbracoContextAccessor?.UmbracoContext?.Media.GetById(udi.Guid); - if (mediaTyped == null) - throw new PanicException($"Could not find media by id {udi.Guid} or there was no UmbracoContext available."); - - var location = mediaTyped.Url; - - // Find the width & height attributes as we need to set the imageprocessor QueryString - var width = img.GetAttributeValue("width", int.MinValue); - var height = img.GetAttributeValue("height", int.MinValue); - - if (width != int.MinValue && height != int.MinValue) - { - location = $"{location}?width={width}&height={height}&mode=max"; - } - - img.SetAttributeValue("src", location); - - // Remove the data attribute (so we do not re-process this) - img.Attributes.Remove(TemporaryImageDataAttribute); - - // Add to the dictionary to avoid dupes - if (uploadedImages.ContainsKey(tmpImgPath) == false) - { - uploadedImages.Add(tmpImgPath, udi); - - // Delete folder & image now its saved in media - // The folder should contain one image - as a unique guid folder created - // for each image uploaded from TinyMceController - var folderName = Path.GetDirectoryName(absoluteTempImagePath); - try - { - Directory.Delete(folderName, true); - } - catch (Exception ex) - { - _logger.Error(typeof(HtmlImageSourceParser), ex, "Could not delete temp file or folder {FileName}", absoluteTempImagePath); - } - } - } - - return htmlDoc.DocumentNode.OuterHtml; - } + } } diff --git a/src/Umbraco.Web/Templates/TemplateUtilities.cs b/src/Umbraco.Web/Templates/TemplateUtilities.cs index 62d25fa794..f796985d39 100644 --- a/src/Umbraco.Web/Templates/TemplateUtilities.cs +++ b/src/Umbraco.Web/Templates/TemplateUtilities.cs @@ -8,6 +8,7 @@ using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Services; using Umbraco.Web.Composing; +using Umbraco.Web.PropertyEditors; using Umbraco.Web.PublishedCache; using Umbraco.Web.Routing; using File = System.IO.File; @@ -49,8 +50,8 @@ namespace Umbraco.Web.Templates internal static string RemoveMediaUrlsFromTextString(string text) => Current.Factory.GetInstance().RemoveImageSources(text); - [Obsolete("Use " + nameof(HtmlImageSourceParser) + "." + nameof(HtmlImageSourceParser.FindAndPersistPastedTempImages) + " instead")] + [Obsolete("Use " + nameof(HtmlImageSourceParser) + "." + nameof(RichTextEditorPastedImages.FindAndPersistPastedTempImages) + " instead")] internal static string FindAndPersistPastedTempImages(string html, Guid mediaParentFolder, int userId, IMediaService mediaService, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, ILogger logger) - => Current.Factory.GetInstance().FindAndPersistPastedTempImages(html, mediaParentFolder, userId); + => Current.Factory.GetInstance().FindAndPersistPastedTempImages(html, mediaParentFolder, userId); } } diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index cf3de1de71..18b8ad938d 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -228,6 +228,7 @@ + From 193892f0847bec49f4f7891e8dd296c10627a7a1 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 24 Oct 2019 16:48:21 +1100 Subject: [PATCH 017/202] Creates method to create the relations based on the property editor's returned reference but have discovered a gotcha for relations, so next step is to resolve that. --- src/Umbraco.Core/Constants-Conventions.cs | 32 ++++++++-- .../Migrations/Install/DatabaseDataCreator.cs | 10 +++ .../Models/Editors/ContentPropertyFile.cs | 1 + .../Models/Editors/UmbracoEntityReference.cs | 55 ++++++++++++++++ .../Repositories/IRelationRepository.cs | 9 ++- .../Implement/ContentRepositoryBase.cs | 62 ++++++++++++++++++- .../Implement/DocumentBlueprintRepository.cs | 4 +- .../Implement/DocumentRepository.cs | 4 +- .../Repositories/Implement/MediaRepository.cs | 4 +- .../Implement/MemberRepository.cs | 4 +- .../Implement/RelationRepository.cs | 16 +++++ .../SqlSyntax/SqlSyntaxProviderExtensions.cs | 2 + .../PropertyEditors/IDataValueReference.cs | 3 +- src/Umbraco.Core/Services/IRelationService.cs | 2 +- .../Services/Implement/RelationService.cs | 4 +- src/Umbraco.Core/Udi.cs | 2 +- src/Umbraco.Core/Umbraco.Core.csproj | 1 + .../Repositories/ContentTypeRepositoryTest.cs | 2 +- .../Repositories/DocumentRepositoryTest.cs | 2 +- .../Repositories/DomainRepositoryTest.cs | 2 +- .../Repositories/MediaRepositoryTest.cs | 2 +- .../Repositories/MemberRepositoryTest.cs | 2 +- .../PublicAccessRepositoryTest.cs | 2 +- .../Repositories/TagRepositoryTest.cs | 4 +- .../Repositories/TemplateRepositoryTest.cs | 2 +- .../Repositories/UserRepositoryTest.cs | 4 +- .../Services/ContentServicePerformanceTest.cs | 2 +- .../Services/ContentServiceTests.cs | 2 +- .../Services/RelationServiceTests.cs | 2 + .../PropertyEditors/RichTextPropertyEditor.cs | 7 ++- 30 files changed, 213 insertions(+), 37 deletions(-) create mode 100644 src/Umbraco.Core/Models/Editors/UmbracoEntityReference.cs diff --git a/src/Umbraco.Core/Constants-Conventions.cs b/src/Umbraco.Core/Constants-Conventions.cs index 6c9407667a..25d259b7d1 100644 --- a/src/Umbraco.Core/Constants-Conventions.cs +++ b/src/Umbraco.Core/Constants-Conventions.cs @@ -309,32 +309,52 @@ namespace Umbraco.Core public static class RelationTypes { /// - /// ContentType name for default relation type "Relate Document On Copy". + /// Name for default relation type "Related Media". + /// + public const string RelatedMediaName = "Related Media"; + + /// + /// Alias for default relation type "Related Media" + /// + public const string RelatedMediaAlias = "umbMedia"; + + /// + /// Name for default relation type "Related Document". + /// + public const string RelatedDocumentName = "Related Document"; + + /// + /// Alias for default relation type "Related Document" + /// + public const string RelatedDocumentAlias = "umbDocument"; + + /// + /// Name for default relation type "Relate Document On Copy". /// public const string RelateDocumentOnCopyName = "Relate Document On Copy"; /// - /// ContentType alias for default relation type "Relate Document On Copy". + /// Alias for default relation type "Relate Document On Copy". /// public const string RelateDocumentOnCopyAlias = "relateDocumentOnCopy"; /// - /// ContentType name for default relation type "Relate Parent Document On Delete". + /// Name for default relation type "Relate Parent Document On Delete". /// public const string RelateParentDocumentOnDeleteName = "Relate Parent Document On Delete"; /// - /// ContentType alias for default relation type "Relate Parent Document On Delete". + /// Alias for default relation type "Relate Parent Document On Delete". /// public const string RelateParentDocumentOnDeleteAlias = "relateParentDocumentOnDelete"; /// - /// ContentType name for default relation type "Relate Parent Media Folder On Delete". + /// Name for default relation type "Relate Parent Media Folder On Delete". /// public const string RelateParentMediaFolderOnDeleteName = "Relate Parent Media Folder On Delete"; /// - /// ContentType alias for default relation type "Relate Parent Media Folder On Delete". + /// Alias for default relation type "Relate Parent Media Folder On Delete". /// public const string RelateParentMediaFolderOnDeleteAlias = "relateParentMediaFolderOnDelete"; } diff --git a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs index 94d8cfbc62..1027840995 100644 --- a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs +++ b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs @@ -318,6 +318,16 @@ namespace Umbraco.Core.Migrations.Install relationType = new RelationTypeDto { Id = 3, Alias = Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteAlias, ChildObjectType = Constants.ObjectTypes.Media, ParentObjectType = Constants.ObjectTypes.Media, Dual = false, Name = Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteName }; relationType.UniqueId = (relationType.Alias + "____" + relationType.Name).ToGuid(); _database.Insert(Constants.DatabaseSchema.Tables.RelationType, "id", false, relationType); + + //TODO: We need to decide if we are going to change the relations APIs since it's pretty crappy that we have to explicitly define all relation type object type combinations... + + relationType = new RelationTypeDto { Id = 4, Alias = Constants.Conventions.RelationTypes.RelatedMediaAlias, ChildObjectType = Constants.ObjectTypes.Media, ParentObjectType = Constants.ObjectTypes.Document, Dual = false, Name = Constants.Conventions.RelationTypes.RelatedMediaName }; + relationType.UniqueId = (relationType.Alias + "____" + relationType.Name).ToGuid(); + _database.Insert(Constants.DatabaseSchema.Tables.RelationType, "id", false, relationType); + + relationType = new RelationTypeDto { Id = 4, Alias = Constants.Conventions.RelationTypes.RelatedDocumentAlias, ChildObjectType = Constants.ObjectTypes.Document, ParentObjectType = Constants.ObjectTypes.Document, Dual = false, Name = Constants.Conventions.RelationTypes.RelatedDocumentName }; + relationType.UniqueId = (relationType.Alias + "____" + relationType.Name).ToGuid(); + _database.Insert(Constants.DatabaseSchema.Tables.RelationType, "id", false, relationType); } private void CreateKeyValueData() diff --git a/src/Umbraco.Core/Models/Editors/ContentPropertyFile.cs b/src/Umbraco.Core/Models/Editors/ContentPropertyFile.cs index ac236e1fdd..225e29a8a1 100644 --- a/src/Umbraco.Core/Models/Editors/ContentPropertyFile.cs +++ b/src/Umbraco.Core/Models/Editors/ContentPropertyFile.cs @@ -1,5 +1,6 @@ namespace Umbraco.Core.Models.Editors { + /// /// Represents an uploaded file for a property. /// diff --git a/src/Umbraco.Core/Models/Editors/UmbracoEntityReference.cs b/src/Umbraco.Core/Models/Editors/UmbracoEntityReference.cs new file mode 100644 index 0000000000..f5121988f5 --- /dev/null +++ b/src/Umbraco.Core/Models/Editors/UmbracoEntityReference.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; + +namespace Umbraco.Core.Models.Editors +{ + /// + /// Used to track reference to other entities in a property value + /// + public struct UmbracoEntityReference : IEquatable + { + private static readonly UmbracoEntityReference _empty = new UmbracoEntityReference(Udi.UnknownTypeUdi.Instance, string.Empty); + + public UmbracoEntityReference(Udi udi, string relationTypeAlias) + { + Udi = udi ?? throw new ArgumentNullException(nameof(udi)); + RelationTypeAlias = relationTypeAlias ?? throw new ArgumentNullException(nameof(relationTypeAlias)); + } + + public static UmbracoEntityReference Empty() => _empty; + + public static bool IsEmpty(UmbracoEntityReference reference) => reference == Empty(); + + public Udi Udi { get; } + public string RelationTypeAlias { get; } + + public override bool Equals(object obj) + { + return obj is UmbracoEntityReference reference && Equals(reference); + } + + public bool Equals(UmbracoEntityReference other) + { + return EqualityComparer.Default.Equals(Udi, other.Udi) && + RelationTypeAlias == other.RelationTypeAlias; + } + + public override int GetHashCode() + { + var hashCode = -487348478; + hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(Udi); + hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(RelationTypeAlias); + return hashCode; + } + + public static bool operator ==(UmbracoEntityReference left, UmbracoEntityReference right) + { + return left.Equals(right); + } + + public static bool operator !=(UmbracoEntityReference left, UmbracoEntityReference right) + { + return !(left == right); + } + } +} diff --git a/src/Umbraco.Core/Persistence/Repositories/IRelationRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IRelationRepository.cs index 51d7656d8a..00ab158d83 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IRelationRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IRelationRepository.cs @@ -4,6 +4,13 @@ namespace Umbraco.Core.Persistence.Repositories { public interface IRelationRepository : IReadWriteQueryRepository { - + /// + /// Deletes all relations for a parent for any specified relation type alias + /// + /// + /// + /// A list of relation types to match for deletion, if none are specified then all relations for this parent id are deleted + /// + void DeleteByParent(int parentId, params string[] relationTypeAliases); } } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs index eede78f4c7..3bb57dca9f 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs @@ -9,6 +9,7 @@ using Umbraco.Core.Composing; using Umbraco.Core.Events; using Umbraco.Core.Logging; using Umbraco.Core.Models; +using Umbraco.Core.Models.Editors; using Umbraco.Core.Models.Entities; using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Persistence.Dtos; @@ -47,12 +48,13 @@ namespace Umbraco.Core.Persistence.Repositories.Implement /// Lazy property value collection - must be lazy because we have a circular dependency since some property editors require services, yet these services require property editors /// protected ContentRepositoryBase(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, - ILanguageRepository languageRepository, IRelationRepository relationRepository, + ILanguageRepository languageRepository, IRelationRepository relationRepository, IRelationTypeRepository relationTypeRepository, Lazy propertyEditors) : base(scopeAccessor, cache, logger) { LanguageRepository = languageRepository; RelationRepository = relationRepository; + RelationTypeRepository = relationTypeRepository; _propertyEditors = propertyEditors; } @@ -60,6 +62,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected ILanguageRepository LanguageRepository { get; } protected IRelationRepository RelationRepository { get; } + protected IRelationTypeRepository RelationTypeRepository { get; } protected PropertyEditorCollection PropertyEditors => _propertyEditors.Value; @@ -818,7 +821,62 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected void PersistRelations(TEntity entity) { - //foreach(var p in entity.) + var trackedRelations = new List(); + + foreach (var p in entity.Properties) + { + if (!PropertyEditors.TryGet(p.PropertyType.PropertyEditorAlias, out var editor)) continue; + if (!(editor is IDataValueReference reference)) continue; + + //TODO: Support variants/segments! This is not required for this initial prototype which is why there is a check here + if (!p.PropertyType.VariesByNothing()) continue; + + var val = p.GetValue(); // get the invariant value + var refs = reference.GetReferences(val); + trackedRelations.AddRange(refs); + } + + if (trackedRelations.Count == 0) return; + + //First delete all relations for this entity + var relationTypes = trackedRelations.Select(x => x.RelationTypeAlias).ToArray(); + RelationRepository.DeleteByParent(entity.Id, relationTypes); + + var udiToGuids = trackedRelations.Select(x => x.Udi as GuidUdi) + .ToDictionary(x => (Udi)x, x => x.Guid); + + //lookup in the DB all INT ids for the GUIDs and chuck into a dictionary + var keyToIds = Database.Fetch(Sql().Select(x => x.NodeId, x => x.UniqueId).From().WhereIn(x => x.UniqueId, udiToGuids.Values)) + .ToDictionary(x => x.UniqueId, x => x.NodeId); + + var allRelationTypes = RelationTypeRepository.GetMany(Array.Empty()) + .ToDictionary(x => x.Alias, x => x); + + foreach(var rel in trackedRelations) + { + if (!allRelationTypes.TryGetValue(rel.RelationTypeAlias, out var relationType)) + throw new InvalidOperationException($"The relation type {rel.RelationTypeAlias} does not exist"); + + if (!udiToGuids.TryGetValue(rel.Udi, out var guid)) + continue; // This shouldn't happen! + + if (!keyToIds.TryGetValue(guid, out var id)) + continue; // This shouldn't happen! + + //Create new relation + //TODO: This is N+1, we could do this all in one operation, just need a new method on the relations repo + RelationRepository.Save(new Relation(entity.Id, id, relationType)); + } + + } + + private class NodeIdKey + { + [Column("id")] + public int NodeId { get; set; } + + [Column("uniqueId")] + public Guid UniqueId { get; set; } } } } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentBlueprintRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentBlueprintRepository.cs index de766ee5aa..60d4026ad5 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentBlueprintRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentBlueprintRepository.cs @@ -19,9 +19,9 @@ namespace Umbraco.Core.Persistence.Repositories.Implement internal class DocumentBlueprintRepository : DocumentRepository, IDocumentBlueprintRepository { public DocumentBlueprintRepository(IScopeAccessor scopeAccessor, AppCaches appCaches, ILogger logger, - IContentTypeRepository contentTypeRepository, ITemplateRepository templateRepository, ITagRepository tagRepository, ILanguageRepository languageRepository, IRelationRepository relationRepository, + IContentTypeRepository contentTypeRepository, ITemplateRepository templateRepository, ITagRepository tagRepository, ILanguageRepository languageRepository, IRelationRepository relationRepository, IRelationTypeRepository relationTypeRepository, Lazy propertyEditorCollection) - : base(scopeAccessor, appCaches, logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, propertyEditorCollection) + : base(scopeAccessor, appCaches, logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditorCollection) { } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs index 067086ea6b..ce95875209 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs @@ -45,9 +45,9 @@ namespace Umbraco.Core.Persistence.Repositories.Implement /// Lazy property value collection - must be lazy because we have a circular dependency since some property editors require services, yet these services require property editors /// public DocumentRepository(IScopeAccessor scopeAccessor, AppCaches appCaches, ILogger logger, - IContentTypeRepository contentTypeRepository, ITemplateRepository templateRepository, ITagRepository tagRepository, ILanguageRepository languageRepository, IRelationRepository relationRepository, + IContentTypeRepository contentTypeRepository, ITemplateRepository templateRepository, ITagRepository tagRepository, ILanguageRepository languageRepository, IRelationRepository relationRepository, IRelationTypeRepository relationTypeRepository, Lazy propertyEditors) - : base(scopeAccessor, appCaches, logger, languageRepository, relationRepository, propertyEditors) + : base(scopeAccessor, appCaches, logger, languageRepository, relationRepository, relationTypeRepository, propertyEditors) { _contentTypeRepository = contentTypeRepository ?? throw new ArgumentNullException(nameof(contentTypeRepository)); _templateRepository = templateRepository ?? throw new ArgumentNullException(nameof(templateRepository)); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs index 0da35145cc..0423ac9125 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs @@ -28,9 +28,9 @@ namespace Umbraco.Core.Persistence.Repositories.Implement private readonly ITagRepository _tagRepository; private readonly MediaByGuidReadRepository _mediaByGuidReadRepository; - public MediaRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IMediaTypeRepository mediaTypeRepository, ITagRepository tagRepository, ILanguageRepository languageRepository, IRelationRepository relationRepository, + public MediaRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IMediaTypeRepository mediaTypeRepository, ITagRepository tagRepository, ILanguageRepository languageRepository, IRelationRepository relationRepository, IRelationTypeRepository relationTypeRepository, Lazy propertyEditorCollection) - : base(scopeAccessor, cache, logger, languageRepository, relationRepository, propertyEditorCollection) + : base(scopeAccessor, cache, logger, languageRepository, relationRepository, relationTypeRepository, propertyEditorCollection) { _mediaTypeRepository = mediaTypeRepository ?? throw new ArgumentNullException(nameof(mediaTypeRepository)); _tagRepository = tagRepository ?? throw new ArgumentNullException(nameof(tagRepository)); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs index 5274e99cc9..c36143d09a 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs @@ -27,9 +27,9 @@ namespace Umbraco.Core.Persistence.Repositories.Implement private readonly IMemberGroupRepository _memberGroupRepository; public MemberRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, - IMemberTypeRepository memberTypeRepository, IMemberGroupRepository memberGroupRepository, ITagRepository tagRepository, ILanguageRepository languageRepository, IRelationRepository relationRepository, + IMemberTypeRepository memberTypeRepository, IMemberGroupRepository memberGroupRepository, ITagRepository tagRepository, ILanguageRepository languageRepository, IRelationRepository relationRepository, IRelationTypeRepository relationTypeRepository, Lazy propertyEditors) - : base(scopeAccessor, cache, logger, languageRepository, relationRepository, propertyEditors) + : base(scopeAccessor, cache, logger, languageRepository, relationRepository, relationTypeRepository, propertyEditors) { _memberTypeRepository = memberTypeRepository ?? throw new ArgumentNullException(nameof(memberTypeRepository)); _tagRepository = tagRepository ?? throw new ArgumentNullException(nameof(tagRepository)); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/RelationRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/RelationRepository.cs index 4b4af505b8..88e28e6ab8 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/RelationRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/RelationRepository.cs @@ -9,6 +9,7 @@ using Umbraco.Core.Models.Entities; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Factories; using Umbraco.Core.Persistence.Querying; +using Umbraco.Core.Persistence.SqlSyntax; using Umbraco.Core.Scoping; namespace Umbraco.Core.Persistence.Repositories.Implement @@ -157,5 +158,20 @@ namespace Umbraco.Core.Persistence.Repositories.Implement } #endregion + + public void DeleteByParent(int parentId, params string[] relationTypeAliases) + { + var subQuery = Sql().Select(x => x.Id) + .From() + .InnerJoin().On(x => x.RelationType, x => x.Id) + .Where(x => x.ParentId == parentId); + + if (relationTypeAliases.Length > 0) + { + subQuery.WhereIn(x => x.Alias, relationTypeAliases); + } + + Database.Execute(Sql().Delete().WhereIn(x => x.Id, subQuery)); + } } } diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderExtensions.cs b/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderExtensions.cs index f7cf480830..b829f1fbc5 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderExtensions.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderExtensions.cs @@ -35,6 +35,8 @@ namespace Umbraco.Core.Persistence.SqlSyntax /// public static Sql GetDeleteSubquery(this ISqlSyntaxProvider sqlProvider, string tableName, string columnName, Sql subQuery, WhereInType whereInType = WhereInType.In) { + //TODO: This is no longer necessary since this used to be a specific requirement for MySql! + // Now we can do a Delete + sub query, see RelationRepository.DeleteByParent for example return new Sql(string.Format( diff --git a/src/Umbraco.Core/PropertyEditors/IDataValueReference.cs b/src/Umbraco.Core/PropertyEditors/IDataValueReference.cs index e71642f8a3..8c0806a4a4 100644 --- a/src/Umbraco.Core/PropertyEditors/IDataValueReference.cs +++ b/src/Umbraco.Core/PropertyEditors/IDataValueReference.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using Umbraco.Core.Models.Editors; namespace Umbraco.Core.PropertyEditors { @@ -12,6 +13,6 @@ namespace Umbraco.Core.PropertyEditors /// /// /// - IEnumerable GetReferences(object value); + IEnumerable GetReferences(object value); } } diff --git a/src/Umbraco.Core/Services/IRelationService.cs b/src/Umbraco.Core/Services/IRelationService.cs index ef22632d6e..0f339688de 100644 --- a/src/Umbraco.Core/Services/IRelationService.cs +++ b/src/Umbraco.Core/Services/IRelationService.cs @@ -47,7 +47,7 @@ namespace Umbraco.Core.Services ///
/// to retrieve Relations for /// An enumerable list of objects - IEnumerable GetAllRelationsByRelationType(RelationType relationType); + IEnumerable GetAllRelationsByRelationType(IRelationType relationType); /// /// Gets all objects by their 's Id diff --git a/src/Umbraco.Core/Services/Implement/RelationService.cs b/src/Umbraco.Core/Services/Implement/RelationService.cs index 405c3a2800..9de3492e09 100644 --- a/src/Umbraco.Core/Services/Implement/RelationService.cs +++ b/src/Umbraco.Core/Services/Implement/RelationService.cs @@ -96,7 +96,7 @@ namespace Umbraco.Core.Services.Implement /// /// to retrieve Relations for /// An enumerable list of objects - public IEnumerable GetAllRelationsByRelationType(RelationType relationType) + public IEnumerable GetAllRelationsByRelationType(IRelationType relationType) { return GetAllRelationsByRelationType(relationType.Id); } @@ -642,6 +642,8 @@ namespace Umbraco.Core.Services.Implement var query = Query().Where(x => x.RelationTypeId == relationType.Id); relations.AddRange(_relationRepository.Get(query).ToList()); + //TODO: N+1, we should be able to do this in a single call + foreach (var relation in relations) _relationRepository.Delete(relation); diff --git a/src/Umbraco.Core/Udi.cs b/src/Umbraco.Core/Udi.cs index e7d00fffa5..ea3ec0ed2d 100644 --- a/src/Umbraco.Core/Udi.cs +++ b/src/Umbraco.Core/Udi.cs @@ -368,7 +368,7 @@ namespace Umbraco.Core return (udi1 == udi2) == false; } - private class UnknownTypeUdi : Udi + internal class UnknownTypeUdi : Udi { private UnknownTypeUdi() : base("unknown", "umb://unknown/") diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 2bd8bfc4fe..65118a877a 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -245,6 +245,7 @@ + diff --git a/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs index 4d2e8bc999..07ca7d238d 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs @@ -39,7 +39,7 @@ namespace Umbraco.Tests.Persistence.Repositories var relationTypeRepository = new RelationTypeRepository(scopeAccessor, AppCaches.Disabled, Logger); var relationRepository = new RelationRepository(scopeAccessor, Logger, relationTypeRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var repository = new DocumentRepository(scopeAccessor, AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, propertyEditors); + var repository = new DocumentRepository(scopeAccessor, AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors); return repository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/DocumentRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/DocumentRepositoryTest.cs index c2f897df80..45dc3de2e3 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/DocumentRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/DocumentRepositoryTest.cs @@ -72,7 +72,7 @@ namespace Umbraco.Tests.Persistence.Repositories var relationTypeRepository = new RelationTypeRepository(scopeAccessor, AppCaches.Disabled, Logger); var relationRepository = new RelationRepository(scopeAccessor, Logger, relationTypeRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var repository = new DocumentRepository(scopeAccessor, appCaches, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, propertyEditors); + var repository = new DocumentRepository(scopeAccessor, appCaches, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors); return repository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/DomainRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/DomainRepositoryTest.cs index 16a59d789f..27ea92fed6 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/DomainRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/DomainRepositoryTest.cs @@ -30,7 +30,7 @@ namespace Umbraco.Tests.Persistence.Repositories var relationTypeRepository = new RelationTypeRepository(accessor, Core.Cache.AppCaches.Disabled, Logger); var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - documentRepository = new DocumentRepository(accessor, Core.Cache.AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, propertyEditors); + documentRepository = new DocumentRepository(accessor, Core.Cache.AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors); var domainRepository = new DomainRepository(accessor, Core.Cache.AppCaches.Disabled, Logger); return domainRepository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs index c976912220..93587506e8 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs @@ -45,7 +45,7 @@ namespace Umbraco.Tests.Persistence.Repositories var relationTypeRepository = new RelationTypeRepository(scopeAccessor, AppCaches.Disabled, Logger); var relationRepository = new RelationRepository(scopeAccessor, Logger, relationTypeRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var repository = new MediaRepository(scopeAccessor, appCaches, Logger, mediaTypeRepository, tagRepository, Mock.Of(), relationRepository, propertyEditors); + var repository = new MediaRepository(scopeAccessor, appCaches, Logger, mediaTypeRepository, tagRepository, Mock.Of(), relationRepository, relationTypeRepository, propertyEditors); return repository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs index 9361bd5909..72b4691639 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs @@ -39,7 +39,7 @@ namespace Umbraco.Tests.Persistence.Repositories var relationTypeRepository = new RelationTypeRepository(accessor, AppCaches.Disabled, Logger); var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var repository = new MemberRepository(accessor, AppCaches.Disabled, Logger, memberTypeRepository, memberGroupRepository, tagRepo, Mock.Of(), relationRepository, propertyEditors); + var repository = new MemberRepository(accessor, AppCaches.Disabled, Logger, memberTypeRepository, memberGroupRepository, tagRepo, Mock.Of(), relationRepository, relationTypeRepository, propertyEditors); return repository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/PublicAccessRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/PublicAccessRepositoryTest.cs index 8cabc18d2c..84a0f608f7 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/PublicAccessRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/PublicAccessRepositoryTest.cs @@ -314,7 +314,7 @@ namespace Umbraco.Tests.Persistence.Repositories var relationTypeRepository = new RelationTypeRepository(accessor, AppCaches, Logger); var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var repository = new DocumentRepository(accessor, AppCaches, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, propertyEditors); + var repository = new DocumentRepository(accessor, AppCaches, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors); return repository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/TagRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/TagRepositoryTest.cs index 61aec9f74c..a4155639be 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/TagRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/TagRepositoryTest.cs @@ -963,7 +963,7 @@ namespace Umbraco.Tests.Persistence.Repositories var relationTypeRepository = new RelationTypeRepository(accessor, AppCaches.Disabled, Logger); var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var repository = new DocumentRepository(accessor, AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, propertyEditors); + var repository = new DocumentRepository(accessor, AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors); return repository; } @@ -978,7 +978,7 @@ namespace Umbraco.Tests.Persistence.Repositories var relationTypeRepository = new RelationTypeRepository(accessor, AppCaches.Disabled, Logger); var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var repository = new MediaRepository(accessor, AppCaches.Disabled, Logger, mediaTypeRepository, tagRepository, Mock.Of(), relationRepository, propertyEditors); + var repository = new MediaRepository(accessor, AppCaches.Disabled, Logger, mediaTypeRepository, tagRepository, Mock.Of(), relationRepository, relationTypeRepository, propertyEditors); return repository; } } diff --git a/src/Umbraco.Tests/Persistence/Repositories/TemplateRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/TemplateRepositoryTest.cs index 7bf113dfc3..e996c4f6a1 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/TemplateRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/TemplateRepositoryTest.cs @@ -245,7 +245,7 @@ namespace Umbraco.Tests.Persistence.Repositories var relationTypeRepository = new RelationTypeRepository(ScopeProvider, AppCaches.Disabled, Logger); var relationRepository = new RelationRepository(ScopeProvider, Logger, relationTypeRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var contentRepo = new DocumentRepository(ScopeProvider, AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, propertyEditors); + var contentRepo = new DocumentRepository(ScopeProvider, AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors); var contentType = MockedContentTypes.CreateSimpleContentType("umbTextpage2", "Textpage"); ServiceContext.FileService.SaveTemplate(contentType.DefaultTemplate); // else, FK violation on contentType! diff --git a/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs index 01a5573119..0438e2193b 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs @@ -34,7 +34,7 @@ namespace Umbraco.Tests.Persistence.Repositories var relationTypeRepository = new RelationTypeRepository(accessor, AppCaches.Disabled, Logger); var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var repository = new MediaRepository(accessor, AppCaches, Mock.Of(), mediaTypeRepository, tagRepository, Mock.Of(), relationRepository, propertyEditors); + var repository = new MediaRepository(accessor, AppCaches, Mock.Of(), mediaTypeRepository, tagRepository, Mock.Of(), relationRepository, relationTypeRepository, propertyEditors); return repository; } @@ -55,7 +55,7 @@ namespace Umbraco.Tests.Persistence.Repositories var relationTypeRepository = new RelationTypeRepository(accessor, AppCaches.Disabled, Logger); var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var repository = new DocumentRepository(accessor, AppCaches, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, propertyEditors); + var repository = new DocumentRepository(accessor, AppCaches, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors); return repository; } diff --git a/src/Umbraco.Tests/Services/ContentServicePerformanceTest.cs b/src/Umbraco.Tests/Services/ContentServicePerformanceTest.cs index 5de5d9bd16..d7729c19c1 100644 --- a/src/Umbraco.Tests/Services/ContentServicePerformanceTest.cs +++ b/src/Umbraco.Tests/Services/ContentServicePerformanceTest.cs @@ -50,7 +50,7 @@ namespace Umbraco.Tests.Services var relationTypeRepository = new RelationTypeRepository((IScopeAccessor)provider, AppCaches.Disabled, Logger); var relationRepository = new RelationRepository((IScopeAccessor)provider, Logger, relationTypeRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var repository = new DocumentRepository((IScopeAccessor)provider, AppCaches.Disabled, Logger, ctRepository, tRepository, tagRepo, languageRepository, relationRepository, propertyEditors); + var repository = new DocumentRepository((IScopeAccessor)provider, AppCaches.Disabled, Logger, ctRepository, tRepository, tagRepo, languageRepository, relationRepository, relationTypeRepository, propertyEditors); return repository; } diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs index bd97772d12..1f53767dbb 100644 --- a/src/Umbraco.Tests/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs @@ -3169,7 +3169,7 @@ namespace Umbraco.Tests.Services var relationTypeRepository = new RelationTypeRepository(accessor, AppCaches.Disabled, Logger); var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var repository = new DocumentRepository(accessor, AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, propertyEditors); + var repository = new DocumentRepository(accessor, AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors); return repository; } diff --git a/src/Umbraco.Tests/Services/RelationServiceTests.cs b/src/Umbraco.Tests/Services/RelationServiceTests.cs index cfef50a330..ad24d2345a 100644 --- a/src/Umbraco.Tests/Services/RelationServiceTests.cs +++ b/src/Umbraco.Tests/Services/RelationServiceTests.cs @@ -23,5 +23,7 @@ namespace Umbraco.Tests.Services Assert.AreEqual(rt.Name, "repeatedEventOccurence"); } + + //TODO: Create a relation for entities of the wrong Entity Type (GUID) based on the Relation Type's defined parent/child object types } } diff --git a/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs index 0287bdfefe..ed3f484a4e 100644 --- a/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs @@ -4,6 +4,7 @@ using System.Linq; using Umbraco.Core; using Umbraco.Core.Logging; using Umbraco.Core.Models; +using Umbraco.Core.Models.Editors; using Umbraco.Core.PropertyEditors; using Umbraco.Core.Services; using Umbraco.Examine; @@ -135,15 +136,15 @@ namespace Umbraco.Web.PropertyEditors ///
/// /// - public IEnumerable GetReferences(object value) + public IEnumerable GetReferences(object value) { var asString = value == null ? string.Empty : value is string str ? str : value.ToString(); foreach (var udi in _imageSourceParser.FindUdisFromDataAttributes(asString)) - yield return udi; + yield return new UmbracoEntityReference(udi, Constants.Conventions.RelationTypes.RelatedMediaAlias); foreach (var udi in _localLinkParser.FindUdisFromLocalLinks(asString)) - yield return udi; + yield return new UmbracoEntityReference(udi, Constants.Conventions.RelationTypes.RelatedMediaAlias); //TODO: Detect Macros too ... but we can save that for a later date, right now need to do media refs } From ae64fe49be574ffd553c0b1026f64f356809ab7e Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 24 Oct 2019 21:32:00 +1100 Subject: [PATCH 018/202] Allows relation types to not have specific object types, updates IRelation to return the actual object types for the ID references, now relations can be more flexible without being strangely tied to specific object types based on the relation type. --- .../Compose/RelateOnCopyComponent.cs | 7 +- .../Compose/RelateOnTrashComponent.cs | 4 +- .../Migrations/Install/DatabaseDataCreator.cs | 2 +- .../Migrations/Upgrade/UmbracoPlan.cs | 4 + .../V_8_5_0/UpdateRelationTypeTable.cs | 21 ++++++ src/Umbraco.Core/Models/IRelation.cs | 9 ++- src/Umbraco.Core/Models/IRelationType.cs | 4 +- src/Umbraco.Core/Models/Relation.cs | 31 ++++++++ src/Umbraco.Core/Models/RelationType.cs | 32 ++++---- .../Persistence/Dtos/RelationDto.cs | 8 ++ .../Persistence/Dtos/RelationTypeDto.cs | 11 ++- .../Persistence/Factories/RelationFactory.cs | 18 +---- .../Factories/RelationTypeFactory.cs | 4 +- .../Repositories/IRelationRepository.cs | 3 +- .../Implement/RelationRepository.cs | 59 ++++++++------- .../Services/Implement/RelationService.cs | 18 +++-- src/Umbraco.Core/Umbraco.Core.csproj | 1 + src/Umbraco.Tests/Models/RelationTests.cs | 4 +- src/Umbraco.Tests/Models/RelationTypeTests.cs | 4 +- .../Repositories/RelationRepositoryTest.cs | 12 ++- .../RelationTypeRepositoryTest.cs | 8 +- .../Services/ContentServiceTests.cs | 2 +- .../Services/RelationServiceTests.cs | 73 ++++++++++++++++++- .../Editors/RelationTypeController.cs | 6 +- .../ContentEditing/RelationTypeDisplay.cs | 4 +- .../Models/Mapping/RelationMapDefinition.cs | 4 +- 26 files changed, 248 insertions(+), 105 deletions(-) create mode 100644 src/Umbraco.Core/Migrations/Upgrade/V_8_5_0/UpdateRelationTypeTable.cs diff --git a/src/Umbraco.Core/Compose/RelateOnCopyComponent.cs b/src/Umbraco.Core/Compose/RelateOnCopyComponent.cs index 63a7e170da..b56ff8b87e 100644 --- a/src/Umbraco.Core/Compose/RelateOnCopyComponent.cs +++ b/src/Umbraco.Core/Compose/RelateOnCopyComponent.cs @@ -26,10 +26,11 @@ namespace Umbraco.Core.Compose if (relationType == null) { - relationType = new RelationType(Constants.ObjectTypes.Document, + relationType = new RelationType(Constants.Conventions.RelationTypes.RelateDocumentOnCopyAlias, + Constants.Conventions.RelationTypes.RelateDocumentOnCopyName, + true, Constants.ObjectTypes.Document, - Constants.Conventions.RelationTypes.RelateDocumentOnCopyAlias, - Constants.Conventions.RelationTypes.RelateDocumentOnCopyName) { IsBidirectional = true }; + Constants.ObjectTypes.Document); relationService.Save(relationType); } diff --git a/src/Umbraco.Core/Compose/RelateOnTrashComponent.cs b/src/Umbraco.Core/Compose/RelateOnTrashComponent.cs index 8371f9b279..4e01c50fc6 100644 --- a/src/Umbraco.Core/Compose/RelateOnTrashComponent.cs +++ b/src/Umbraco.Core/Compose/RelateOnTrashComponent.cs @@ -63,7 +63,7 @@ namespace Umbraco.Core.Compose var documentObjectType = Constants.ObjectTypes.Document; const string relationTypeName = Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteName; - relationType = new RelationType(documentObjectType, documentObjectType, relationTypeAlias, relationTypeName); + relationType = new RelationType(relationTypeName, relationTypeAlias, false, documentObjectType, documentObjectType); relationService.Save(relationType); } @@ -106,7 +106,7 @@ namespace Umbraco.Core.Compose { var documentObjectType = Constants.ObjectTypes.Document; const string relationTypeName = Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteName; - relationType = new RelationType(documentObjectType, documentObjectType, relationTypeAlias, relationTypeName); + relationType = new RelationType(relationTypeName, relationTypeAlias, false, documentObjectType, documentObjectType); relationService.Save(relationType); } foreach (var item in e.MoveInfoCollection) diff --git a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs index 1027840995..78e22ef28e 100644 --- a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs +++ b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs @@ -325,7 +325,7 @@ namespace Umbraco.Core.Migrations.Install relationType.UniqueId = (relationType.Alias + "____" + relationType.Name).ToGuid(); _database.Insert(Constants.DatabaseSchema.Tables.RelationType, "id", false, relationType); - relationType = new RelationTypeDto { Id = 4, Alias = Constants.Conventions.RelationTypes.RelatedDocumentAlias, ChildObjectType = Constants.ObjectTypes.Document, ParentObjectType = Constants.ObjectTypes.Document, Dual = false, Name = Constants.Conventions.RelationTypes.RelatedDocumentName }; + relationType = new RelationTypeDto { Id = 5, Alias = Constants.Conventions.RelationTypes.RelatedDocumentAlias, ChildObjectType = Constants.ObjectTypes.Document, ParentObjectType = Constants.ObjectTypes.Document, Dual = false, Name = Constants.Conventions.RelationTypes.RelatedDocumentName }; relationType.UniqueId = (relationType.Alias + "____" + relationType.Name).ToGuid(); _database.Insert(Constants.DatabaseSchema.Tables.RelationType, "id", false, relationType); } diff --git a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs index e8fd3414ec..e5e335d309 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs @@ -6,6 +6,7 @@ using Umbraco.Core.Migrations.Upgrade.Common; using Umbraco.Core.Migrations.Upgrade.V_8_0_0; using Umbraco.Core.Migrations.Upgrade.V_8_0_1; using Umbraco.Core.Migrations.Upgrade.V_8_1_0; +using Umbraco.Core.Migrations.Upgrade.V_8_5_0; namespace Umbraco.Core.Migrations.Upgrade { @@ -182,6 +183,9 @@ namespace Umbraco.Core.Migrations.Upgrade To("{0372A42B-DECF-498D-B4D1-6379E907EB94}"); To("{5B1E0D93-F5A3-449B-84BA-65366B84E2D4}"); + // to 8.5.0... + To("{4759A294-9860-46BC-99F9-B4C975CAE580}"); + //FINAL } } diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_5_0/UpdateRelationTypeTable.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_5_0/UpdateRelationTypeTable.cs new file mode 100644 index 0000000000..ed21935488 --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_5_0/UpdateRelationTypeTable.cs @@ -0,0 +1,21 @@ +using Umbraco.Core.Persistence.Dtos; + +namespace Umbraco.Core.Migrations.Upgrade.V_8_5_0 +{ + public class UpdateRelationTypeTable : MigrationBase + { + public UpdateRelationTypeTable(IMigrationContext context) + : base(context) + { } + + public override void Migrate() + { + + Alter.Table(Constants.DatabaseSchema.Tables.RelationType).AlterColumn("parentObjectType").AsGuid().Nullable(); + Alter.Table(Constants.DatabaseSchema.Tables.RelationType).AlterColumn("childObjectType").AsGuid().Nullable(); + + //TODO: We have to update this field to ensure it's not null, we can just copy across the name since that is not nullable + Alter.Table(Constants.DatabaseSchema.Tables.RelationType).AlterColumn("alias").AsString(100).NotNullable(); + } + } +} diff --git a/src/Umbraco.Core/Models/IRelation.cs b/src/Umbraco.Core/Models/IRelation.cs index 745216fba1..6bd348d72f 100644 --- a/src/Umbraco.Core/Models/IRelation.cs +++ b/src/Umbraco.Core/Models/IRelation.cs @@ -1,4 +1,5 @@ -using System.Runtime.Serialization; +using System; +using System.Runtime.Serialization; using Umbraco.Core.Models.Entities; namespace Umbraco.Core.Models @@ -11,12 +12,18 @@ namespace Umbraco.Core.Models [DataMember] int ParentId { get; set; } + [DataMember] + Guid ParentObjectType { get; set; } + /// /// Gets or sets the Child Id of the Relation (Destination) /// [DataMember] int ChildId { get; set; } + [DataMember] + Guid ChildObjectType { get; set; } + /// /// Gets or sets the for the Relation /// diff --git a/src/Umbraco.Core/Models/IRelationType.cs b/src/Umbraco.Core/Models/IRelationType.cs index 8bbe657427..9253fae8ab 100644 --- a/src/Umbraco.Core/Models/IRelationType.cs +++ b/src/Umbraco.Core/Models/IRelationType.cs @@ -29,13 +29,13 @@ namespace Umbraco.Core.Models /// /// Corresponds to the NodeObjectType in the umbracoNode table [DataMember] - Guid ParentObjectType { get; set; } + Guid? ParentObjectType { get; set; } /// /// Gets or sets the Childs object type id /// /// Corresponds to the NodeObjectType in the umbracoNode table [DataMember] - Guid ChildObjectType { get; set; } + Guid? ChildObjectType { get; set; } } } diff --git a/src/Umbraco.Core/Models/Relation.cs b/src/Umbraco.Core/Models/Relation.cs index f5d13c70c4..8e3a073a96 100644 --- a/src/Umbraco.Core/Models/Relation.cs +++ b/src/Umbraco.Core/Models/Relation.cs @@ -17,12 +17,37 @@ namespace Umbraco.Core.Models private IRelationType _relationType; private string _comment; + /// + /// Constructor for constructing the entity to be created + /// + /// + /// + /// + /// + /// public Relation(int parentId, int childId, IRelationType relationType) { _parentId = parentId; _childId = childId; _relationType = relationType; } + + /// + /// Constructor for reconstructing the entity from the data source + /// + /// + /// + /// + /// + /// + public Relation(int parentId, int childId, Guid parentObjectType, Guid childObjectType, IRelationType relationType) + { + _parentId = parentId; + _childId = childId; + _relationType = relationType; + ParentObjectType = parentObjectType; + ChildObjectType = childObjectType; + } /// @@ -35,6 +60,9 @@ namespace Umbraco.Core.Models set => SetPropertyValueAndDetectChanges(value, ref _parentId, nameof(ParentId)); } + [DataMember] + public Guid ParentObjectType { get; set; } + /// /// Gets or sets the Child Id of the Relation (Destination) /// @@ -45,6 +73,9 @@ namespace Umbraco.Core.Models set => SetPropertyValueAndDetectChanges(value, ref _childId, nameof(ChildId)); } + [DataMember] + public Guid ChildObjectType { get; set; } + /// /// Gets or sets the for the Relation /// diff --git a/src/Umbraco.Core/Models/RelationType.cs b/src/Umbraco.Core/Models/RelationType.cs index 259b7bc4ef..ea62cecab7 100644 --- a/src/Umbraco.Core/Models/RelationType.cs +++ b/src/Umbraco.Core/Models/RelationType.cs @@ -15,24 +15,26 @@ namespace Umbraco.Core.Models private string _name; private string _alias; private bool _isBidrectional; - private Guid _parentObjectType; - private Guid _childObjectType; + private Guid? _parentObjectType; + private Guid? _childObjectType; - public RelationType(Guid childObjectType, Guid parentObjectType, string alias) + //TODO: Should we put back the broken ctors with obsolete attributes? + + public RelationType(string alias, string name) + : this(name, alias, false, null, null) { - if (string.IsNullOrWhiteSpace(alias)) throw new ArgumentNullOrEmptyException(nameof(alias)); - _childObjectType = childObjectType; - _parentObjectType = parentObjectType; + } + + public RelationType(string name, string alias, bool isBidrectional, Guid? parentObjectType, Guid? childObjectType) + { + _name = name; _alias = alias; - Name = _alias; + _isBidrectional = isBidrectional; + _parentObjectType = parentObjectType; + _childObjectType = childObjectType; } - public RelationType(Guid childObjectType, Guid parentObjectType, string alias, string name) - : this(childObjectType, parentObjectType, alias) - { - if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullOrEmptyException(nameof(name)); - Name = name; - } + /// /// Gets or sets the Name of the RelationType @@ -69,7 +71,7 @@ namespace Umbraco.Core.Models /// /// Corresponds to the NodeObjectType in the umbracoNode table [DataMember] - public Guid ParentObjectType + public Guid? ParentObjectType { get => _parentObjectType; set => SetPropertyValueAndDetectChanges(value, ref _parentObjectType, nameof(ParentObjectType)); @@ -80,7 +82,7 @@ namespace Umbraco.Core.Models /// /// Corresponds to the NodeObjectType in the umbracoNode table [DataMember] - public Guid ChildObjectType + public Guid? ChildObjectType { get => _childObjectType; set => SetPropertyValueAndDetectChanges(value, ref _childObjectType, nameof(ChildObjectType)); diff --git a/src/Umbraco.Core/Persistence/Dtos/RelationDto.cs b/src/Umbraco.Core/Persistence/Dtos/RelationDto.cs index f1fd3007d7..b21866eb8b 100644 --- a/src/Umbraco.Core/Persistence/Dtos/RelationDto.cs +++ b/src/Umbraco.Core/Persistence/Dtos/RelationDto.cs @@ -34,5 +34,13 @@ namespace Umbraco.Core.Persistence.Dtos [Column("comment")] [Length(1000)] public string Comment { get; set; } + + [ResultColumn] + [Column("parentObjectType")] + public Guid ParentObjectType { get; set; } + + [ResultColumn] + [Column("childObjectType")] + public Guid ChildObjectType { get; set; } } } diff --git a/src/Umbraco.Core/Persistence/Dtos/RelationTypeDto.cs b/src/Umbraco.Core/Persistence/Dtos/RelationTypeDto.cs index e972192844..d3e107d23f 100644 --- a/src/Umbraco.Core/Persistence/Dtos/RelationTypeDto.cs +++ b/src/Umbraco.Core/Persistence/Dtos/RelationTypeDto.cs @@ -9,7 +9,7 @@ namespace Umbraco.Core.Persistence.Dtos [ExplicitColumns] internal class RelationTypeDto { - public const int NodeIdSeed = 4; + public const int NodeIdSeed = 10; [Column("id")] [PrimaryKeyColumn(IdentitySeed = NodeIdSeed)] @@ -23,17 +23,20 @@ namespace Umbraco.Core.Persistence.Dtos public bool Dual { get; set; } [Column("parentObjectType")] - public Guid ParentObjectType { get; set; } + [NullSetting(NullSetting = NullSettings.Null)] + public Guid? ParentObjectType { get; set; } [Column("childObjectType")] - public Guid ChildObjectType { get; set; } + [NullSetting(NullSetting = NullSettings.Null)] + public Guid? ChildObjectType { get; set; } [Column("name")] + [NullSetting(NullSetting = NullSettings.NotNull)] [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoRelationType_name")] public string Name { get; set; } [Column("alias")] - [NullSetting(NullSetting = NullSettings.Null)] + [NullSetting(NullSetting = NullSettings.NotNull)] [Length(100)] [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoRelationType_alias")] public string Alias { get; set; } diff --git a/src/Umbraco.Core/Persistence/Factories/RelationFactory.cs b/src/Umbraco.Core/Persistence/Factories/RelationFactory.cs index 6abb858e94..d8f100cdbe 100644 --- a/src/Umbraco.Core/Persistence/Factories/RelationFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/RelationFactory.cs @@ -3,20 +3,11 @@ using Umbraco.Core.Persistence.Dtos; namespace Umbraco.Core.Persistence.Factories { - internal class RelationFactory + internal static class RelationFactory { - private readonly IRelationType _relationType; - - public RelationFactory(IRelationType relationType) + public static IRelation BuildEntity(RelationDto dto, IRelationType relationType) { - _relationType = relationType; - } - - #region Implementation of IEntityFactory - - public IRelation BuildEntity(RelationDto dto) - { - var entity = new Relation(dto.ParentId, dto.ChildId, _relationType); + var entity = new Relation(dto.ParentId, dto.ChildId, dto.ParentObjectType, dto.ChildObjectType, relationType); try { @@ -37,7 +28,7 @@ namespace Umbraco.Core.Persistence.Factories } } - public RelationDto BuildDto(IRelation entity) + public static RelationDto BuildDto(IRelation entity) { var dto = new RelationDto { @@ -54,6 +45,5 @@ namespace Umbraco.Core.Persistence.Factories return dto; } - #endregion } } diff --git a/src/Umbraco.Core/Persistence/Factories/RelationTypeFactory.cs b/src/Umbraco.Core/Persistence/Factories/RelationTypeFactory.cs index ca6928a0a1..edd87fec68 100644 --- a/src/Umbraco.Core/Persistence/Factories/RelationTypeFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/RelationTypeFactory.cs @@ -9,7 +9,7 @@ namespace Umbraco.Core.Persistence.Factories public static IRelationType BuildEntity(RelationTypeDto dto) { - var entity = new RelationType(dto.ChildObjectType, dto.ParentObjectType, dto.Alias); + var entity = new RelationType(dto.Name, dto.Alias, dto.Dual, dto.ChildObjectType, dto.ParentObjectType); try { @@ -17,8 +17,6 @@ namespace Umbraco.Core.Persistence.Factories entity.Id = dto.Id; entity.Key = dto.UniqueId; - entity.IsBidirectional = dto.Dual; - entity.Name = dto.Name; // reset dirty initial properties (U4-1946) entity.ResetDirtyProperties(false); diff --git a/src/Umbraco.Core/Persistence/Repositories/IRelationRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IRelationRepository.cs index 00ab158d83..9a2e42568e 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IRelationRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IRelationRepository.cs @@ -1,4 +1,5 @@ -using Umbraco.Core.Models; +using System.Collections.Generic; +using Umbraco.Core.Models; namespace Umbraco.Core.Persistence.Repositories { diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/RelationRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/RelationRepository.cs index 88e28e6ab8..ab6b02afb4 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/RelationRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/RelationRepository.cs @@ -11,6 +11,7 @@ using Umbraco.Core.Persistence.Factories; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.SqlSyntax; using Umbraco.Core.Scoping; +using static Umbraco.Core.Persistence.NPocoSqlExtensions.Statics; namespace Umbraco.Core.Persistence.Repositories.Implement { @@ -40,10 +41,9 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var relationType = _relationTypeRepository.Get(dto.RelationType); if (relationType == null) - throw new Exception(string.Format("RelationType with Id: {0} doesn't exist", dto.RelationType)); + throw new InvalidOperationException(string.Format("RelationType with Id: {0} doesn't exist", dto.RelationType)); - var factory = new RelationFactory(relationType); - return DtoToEntity(dto, factory); + return DtoToEntity(dto, relationType); } protected override IEnumerable PerformGetAll(params int[] ids) @@ -68,26 +68,17 @@ namespace Umbraco.Core.Persistence.Repositories.Implement private IEnumerable DtosToEntities(IEnumerable dtos) { - // in most cases, the relation type will be the same for all of them, - // plus we've ordered the relations by type, so try to allocate as few - // factories as possible - bearing in mind that relation types are cached - RelationFactory factory = null; - var relationTypeId = -1; + //NOTE: This is N+1, BUT ALL relation types are cached so shouldn't matter - return dtos.Select(x => - { - if (relationTypeId != x.RelationType) - factory = new RelationFactory(_relationTypeRepository.Get(relationTypeId = x.RelationType)); - return DtoToEntity(x, factory); - }).ToList(); + return dtos.Select(x => DtoToEntity(x, _relationTypeRepository.Get(x.RelationType))).ToList(); } - private static IRelation DtoToEntity(RelationDto dto, RelationFactory factory) + private static IRelation DtoToEntity(RelationDto dto, IRelationType relationType) { - var entity = factory.BuildEntity(dto); + var entity = RelationFactory.BuildEntity(dto, relationType); // reset dirty initial properties (U4-1946) - ((BeingDirtyBase)entity).ResetDirtyProperties(false); + entity.ResetDirtyProperties(false); return entity; } @@ -98,14 +89,18 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override Sql GetBaseQuery(bool isCount) { - var sql = Sql(); + if (isCount) + { + return Sql().SelectCount().From(); + } - sql = isCount - ? sql.SelectCount() - : sql.Select(); + var sql = Sql().Select() + .AndSelect("uchild", x => Alias(x.NodeObjectType, "childObjectType")) + .AndSelect("uparent", x => Alias(x.NodeObjectType, "parentObjectType")) + .From() + .InnerJoin("uchild").On((rel, node) => rel.ChildId == node.NodeId, aliasRight: "uchild") + .InnerJoin("uparent").On((rel, node) => rel.ParentId == node.NodeId, aliasRight: "uparent"); - sql - .From(); return sql; } @@ -137,11 +132,12 @@ namespace Umbraco.Core.Persistence.Repositories.Implement { entity.AddingEntity(); - var factory = new RelationFactory(entity.RelationType); - var dto = factory.BuildDto(entity); + var dto = RelationFactory.BuildDto(entity); var id = Convert.ToInt32(Database.Insert(dto)); + entity.Id = id; + PopulateObjectTypes(entity); entity.ResetDirtyProperties(); } @@ -150,10 +146,11 @@ namespace Umbraco.Core.Persistence.Repositories.Implement { entity.UpdatingEntity(); - var factory = new RelationFactory(entity.RelationType); - var dto = factory.BuildDto(entity); + var dto = RelationFactory.BuildDto(entity); Database.Update(dto); + PopulateObjectTypes(entity); + entity.ResetDirtyProperties(); } @@ -173,5 +170,13 @@ namespace Umbraco.Core.Persistence.Repositories.Implement Database.Execute(Sql().Delete().WhereIn(x => x.Id, subQuery)); } + + private void PopulateObjectTypes(IRelation entity) + { + var nodes = Database.Fetch(Sql().Select().From().Where(x => x.NodeId == entity.ChildId || x.NodeId == entity.ParentId)) + .ToDictionary(x => x.NodeId, x => x.NodeObjectType); + entity.ParentObjectType = nodes[entity.ParentId].Value; + entity.ChildObjectType = nodes[entity.ChildId].Value; + } } } diff --git a/src/Umbraco.Core/Services/Implement/RelationService.cs b/src/Umbraco.Core/Services/Implement/RelationService.cs index 9de3492e09..cc0a3e23bc 100644 --- a/src/Umbraco.Core/Services/Implement/RelationService.cs +++ b/src/Umbraco.Core/Services/Implement/RelationService.cs @@ -288,7 +288,7 @@ namespace Umbraco.Core.Services.Implement /// An public IUmbracoEntity GetChildEntityFromRelation(IRelation relation) { - var objectType = ObjectTypes.GetUmbracoObjectType(relation.RelationType.ChildObjectType); + var objectType = ObjectTypes.GetUmbracoObjectType(relation.ChildObjectType); return _entityService.Get(relation.ChildId, objectType); } @@ -299,7 +299,7 @@ namespace Umbraco.Core.Services.Implement /// An public IUmbracoEntity GetParentEntityFromRelation(IRelation relation) { - var objectType = ObjectTypes.GetUmbracoObjectType(relation.RelationType.ParentObjectType); + var objectType = ObjectTypes.GetUmbracoObjectType(relation.ParentObjectType); return _entityService.Get(relation.ParentId, objectType); } @@ -310,8 +310,8 @@ namespace Umbraco.Core.Services.Implement /// Returns a Tuple with Parent (item1) and Child (item2) public Tuple GetEntitiesFromRelation(IRelation relation) { - var childObjectType = ObjectTypes.GetUmbracoObjectType(relation.RelationType.ChildObjectType); - var parentObjectType = ObjectTypes.GetUmbracoObjectType(relation.RelationType.ParentObjectType); + var childObjectType = ObjectTypes.GetUmbracoObjectType(relation.ChildObjectType); + var parentObjectType = ObjectTypes.GetUmbracoObjectType(relation.ParentObjectType); var child = _entityService.Get(relation.ChildId, childObjectType); var parent = _entityService.Get(relation.ParentId, parentObjectType); @@ -328,7 +328,7 @@ namespace Umbraco.Core.Services.Implement { foreach (var relation in relations) { - var objectType = ObjectTypes.GetUmbracoObjectType(relation.RelationType.ChildObjectType); + var objectType = ObjectTypes.GetUmbracoObjectType(relation.ChildObjectType); yield return _entityService.Get(relation.ChildId, objectType); } } @@ -342,7 +342,7 @@ namespace Umbraco.Core.Services.Implement { foreach (var relation in relations) { - var objectType = ObjectTypes.GetUmbracoObjectType(relation.RelationType.ParentObjectType); + var objectType = ObjectTypes.GetUmbracoObjectType(relation.ParentObjectType); yield return _entityService.Get(relation.ParentId, objectType); } } @@ -356,8 +356,8 @@ namespace Umbraco.Core.Services.Implement { foreach (var relation in relations) { - var childObjectType = ObjectTypes.GetUmbracoObjectType(relation.RelationType.ChildObjectType); - var parentObjectType = ObjectTypes.GetUmbracoObjectType(relation.RelationType.ParentObjectType); + var childObjectType = ObjectTypes.GetUmbracoObjectType(relation.ChildObjectType); + var parentObjectType = ObjectTypes.GetUmbracoObjectType(relation.ParentObjectType); var child = _entityService.Get(relation.ChildId, childObjectType); var parent = _entityService.Get(relation.ParentId, parentObjectType); @@ -379,6 +379,8 @@ namespace Umbraco.Core.Services.Implement if (relationType.HasIdentity == false) Save(relationType); + //TODO: We don't check if this exists first, it will throw some sort of data integrity exception if it already exists, is that ok? + var relation = new Relation(parentId, childId, relationType); using (var scope = ScopeProvider.CreateScope()) diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 65118a877a..1f83bd2807 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -245,6 +245,7 @@ + diff --git a/src/Umbraco.Tests/Models/RelationTests.cs b/src/Umbraco.Tests/Models/RelationTests.cs index c62dcdc6eb..91560abbb3 100644 --- a/src/Umbraco.Tests/Models/RelationTests.cs +++ b/src/Umbraco.Tests/Models/RelationTests.cs @@ -12,7 +12,7 @@ namespace Umbraco.Tests.Models [Test] public void Can_Deep_Clone() { - var item = new Relation(9, 8, new RelationType(Guid.NewGuid(), Guid.NewGuid(), "test") + var item = new Relation(9, 8, new RelationType("test", "test", false, Guid.NewGuid(), Guid.NewGuid()) { Id = 66 }) @@ -52,7 +52,7 @@ namespace Umbraco.Tests.Models { var ss = new SerializationService(new JsonNetSerializer()); - var item = new Relation(9, 8, new RelationType(Guid.NewGuid(), Guid.NewGuid(), "test") + var item = new Relation(9, 8, new RelationType("test", "test", false, Guid.NewGuid(), Guid.NewGuid()) { Id = 66 }) diff --git a/src/Umbraco.Tests/Models/RelationTypeTests.cs b/src/Umbraco.Tests/Models/RelationTypeTests.cs index 9d8fdcdf25..4555b6366f 100644 --- a/src/Umbraco.Tests/Models/RelationTypeTests.cs +++ b/src/Umbraco.Tests/Models/RelationTypeTests.cs @@ -12,7 +12,7 @@ namespace Umbraco.Tests.Models [Test] public void Can_Deep_Clone() { - var item = new RelationType(Guid.NewGuid(), Guid.NewGuid(), "test") + var item = new RelationType("test", "test", false, Guid.NewGuid(), Guid.NewGuid()) { Id = 66, CreateDate = DateTime.Now, @@ -48,7 +48,7 @@ namespace Umbraco.Tests.Models { var ss = new SerializationService(new JsonNetSerializer()); - var item = new RelationType(Guid.NewGuid(), Guid.NewGuid(), "test") + var item = new RelationType("test", "test", false, Guid.NewGuid(), Guid.NewGuid()) { Id = 66, CreateDate = DateTime.Now, diff --git a/src/Umbraco.Tests/Persistence/Repositories/RelationRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/RelationRepositoryTest.cs index cea7f44b71..8d9f82a776 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/RelationRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/RelationRepositoryTest.cs @@ -260,8 +260,16 @@ namespace Umbraco.Tests.Persistence.Repositories public void CreateTestData() { - var relateContent = new RelationType(Constants.ObjectTypes.Document, new Guid("C66BA18E-EAF3-4CFF-8A22-41B16D66A972"), "relateContentOnCopy") { IsBidirectional = true, Name = "Relate Content on Copy" }; - var relateContentType = new RelationType(Constants.ObjectTypes.DocumentType, new Guid("A2CB7800-F571-4787-9638-BC48539A0EFB"), "relateContentTypeOnCopy") { IsBidirectional = true, Name = "Relate ContentType on Copy" }; + var relateContent = new RelationType( + "Relate Content on Copy", "relateContentOnCopy", true, + Constants.ObjectTypes.Document, + new Guid("C66BA18E-EAF3-4CFF-8A22-41B16D66A972")); + + var relateContentType = new RelationType("Relate ContentType on Copy", + "relateContentTypeOnCopy", + true, + Constants.ObjectTypes.DocumentType, + new Guid("A2CB7800-F571-4787-9638-BC48539A0EFB")); var provider = TestObjects.GetScopeProvider(Logger); using (var scope = provider.CreateScope()) diff --git a/src/Umbraco.Tests/Persistence/Repositories/RelationTypeRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/RelationTypeRepositoryTest.cs index e52e2dfcdf..881ea23dc8 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/RelationTypeRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/RelationTypeRepositoryTest.cs @@ -42,9 +42,7 @@ namespace Umbraco.Tests.Persistence.Repositories var repository = CreateRepository(provider); // Act - var relateMemberToContent = new RelationType(Constants.ObjectTypes.Member, - Constants.ObjectTypes.Document, - "relateMemberToContent") { IsBidirectional = true, Name = "Relate Member to Content" }; + var relateMemberToContent = new RelationType("Relate Member to Content", "relateMemberToContent", true, Constants.ObjectTypes.Member, Constants.ObjectTypes.Document); repository.Save(relateMemberToContent); @@ -226,8 +224,8 @@ namespace Umbraco.Tests.Persistence.Repositories public void CreateTestData() { - var relateContent = new RelationType(Constants.ObjectTypes.Document, new Guid("C66BA18E-EAF3-4CFF-8A22-41B16D66A972"), "relateContentOnCopy") { IsBidirectional = true, Name = "Relate Content on Copy" }; - var relateContentType = new RelationType(Constants.ObjectTypes.DocumentType, new Guid("A2CB7800-F571-4787-9638-BC48539A0EFB"), "relateContentTypeOnCopy") { IsBidirectional = true, Name = "Relate ContentType on Copy" }; + var relateContent = new RelationType("Relate Content on Copy", "relateContentOnCopy", true, Constants.ObjectTypes.Document, new Guid("C66BA18E-EAF3-4CFF-8A22-41B16D66A972")); + var relateContentType = new RelationType("Relate ContentType on Copy", "relateContentTypeOnCopy", true, Constants.ObjectTypes.DocumentType, new Guid("A2CB7800-F571-4787-9638-BC48539A0EFB")); var provider = TestObjects.GetScopeProvider(Logger); using (var scope = ScopeProvider.CreateScope()) diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs index 1f53767dbb..7d65513cc0 100644 --- a/src/Umbraco.Tests/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs @@ -1778,7 +1778,7 @@ namespace Umbraco.Tests.Services admin.StartContentIds = new[] {content1.Id}; ServiceContext.UserService.Save(admin); - ServiceContext.RelationService.Save(new RelationType(Constants.ObjectTypes.Document, Constants.ObjectTypes.Document, "test")); + ServiceContext.RelationService.Save(new RelationType("test", "test", false, Constants.ObjectTypes.Document, Constants.ObjectTypes.Document)); Assert.IsNotNull(ServiceContext.RelationService.Relate(content1, content2, "test")); ServiceContext.PublicAccessService.Save(new PublicAccessEntry(content1, content2, content2, new List diff --git a/src/Umbraco.Tests/Services/RelationServiceTests.cs b/src/Umbraco.Tests/Services/RelationServiceTests.cs index ad24d2345a..a39a9c838b 100644 --- a/src/Umbraco.Tests/Services/RelationServiceTests.cs +++ b/src/Umbraco.Tests/Services/RelationServiceTests.cs @@ -4,24 +4,91 @@ using NUnit.Framework; using Umbraco.Core; using Umbraco.Core.Models; using Umbraco.Tests.TestHelpers; +using Umbraco.Tests.TestHelpers.Entities; using Umbraco.Tests.Testing; namespace Umbraco.Tests.Services { [TestFixture] [Apartment(ApartmentState.STA)] - [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerFixture)] + [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] public class RelationServiceTests : TestWithSomeContentBase { [Test] public void Can_Create_RelationType_Without_Name() { var rs = ServiceContext.RelationService; - var rt = new RelationType(Constants.ObjectTypes.Document, Constants.ObjectTypes.Document, "repeatedEventOccurence"); + IRelationType rt = new RelationType("Test", "repeatedEventOccurence", false, Constants.ObjectTypes.Document, Constants.ObjectTypes.Media); Assert.DoesNotThrow(() => rs.Save(rt)); - Assert.AreEqual(rt.Name, "repeatedEventOccurence"); + //re-get + rt = ServiceContext.RelationService.GetRelationTypeById(rt.Id); + + Assert.AreEqual("Test", rt.Name); + Assert.AreEqual("repeatedEventOccurence", rt.Alias); + Assert.AreEqual(false, rt.IsBidirectional); + Assert.AreEqual(Constants.ObjectTypes.Document, rt.ChildObjectType.Value); + Assert.AreEqual(Constants.ObjectTypes.Media, rt.ParentObjectType.Value); + } + + [Test] + public void Create_Relation_Type_Without_Object_Types() + { + var rs = ServiceContext.RelationService; + IRelationType rt = new RelationType("repeatedEventOccurence", "repeatedEventOccurence", false, null, null); + + Assert.DoesNotThrow(() => rs.Save(rt)); + + //re-get + rt = ServiceContext.RelationService.GetRelationTypeById(rt.Id); + + Assert.IsNull(rt.ChildObjectType); + Assert.IsNull(rt.ParentObjectType); + } + + [Test] + public void Relation_Returns_Parent_Child_Object_Types_When_Creating() + { + var r = CreateNewRelation("Test", "test"); + + Assert.AreEqual(Constants.ObjectTypes.Document, r.ParentObjectType); + Assert.AreEqual(Constants.ObjectTypes.Media, r.ChildObjectType); + } + + [Test] + public void Relation_Returns_Parent_Child_Object_Types_When_Getting() + { + var r = CreateNewRelation("Test", "test"); + + // re-get + r = ServiceContext.RelationService.GetById(r.Id); + + Assert.AreEqual(Constants.ObjectTypes.Document, r.ParentObjectType); + Assert.AreEqual(Constants.ObjectTypes.Media, r.ChildObjectType); + } + + private IRelation CreateNewRelation(string name, string alias) + { + var rs = ServiceContext.RelationService; + var rt = new RelationType(name, alias, false, null, null); + rs.Save(rt); + + var ct = MockedContentTypes.CreateBasicContentType(); + ServiceContext.ContentTypeService.Save(ct); + + var mt = MockedContentTypes.CreateImageMediaType("img"); + ServiceContext.MediaTypeService.Save(mt); + + var c1 = MockedContent.CreateBasicContent(ct); + var c2 = MockedMedia.CreateMediaImage(mt, -1); + ServiceContext.ContentService.Save(c1); + ServiceContext.MediaService.Save(c2); + + var r = new Relation(c1.Id, c2.Id, rt); + ServiceContext.RelationService.Save(r); + + return r; } //TODO: Create a relation for entities of the wrong Entity Type (GUID) based on the Relation Type's defined parent/child object types diff --git a/src/Umbraco.Web/Editors/RelationTypeController.cs b/src/Umbraco.Web/Editors/RelationTypeController.cs index faafbb79f1..6fb9108d74 100644 --- a/src/Umbraco.Web/Editors/RelationTypeController.cs +++ b/src/Umbraco.Web/Editors/RelationTypeController.cs @@ -84,11 +84,7 @@ namespace Umbraco.Web.Editors /// A containing the persisted relation type's ID. public HttpResponseMessage PostCreate(RelationTypeSave relationType) { - var relationTypePersisted = new RelationType(relationType.ChildObjectType, relationType.ParentObjectType, relationType.Name.ToSafeAlias(true)) - { - Name = relationType.Name, - IsBidirectional = relationType.IsBidirectional - }; + var relationTypePersisted = new RelationType(relationType.Name, relationType.Name.ToSafeAlias(true), relationType.IsBidirectional, relationType.ChildObjectType, relationType.ParentObjectType); try { diff --git a/src/Umbraco.Web/Models/ContentEditing/RelationTypeDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/RelationTypeDisplay.cs index 55e140d6f3..8a92d085eb 100644 --- a/src/Umbraco.Web/Models/ContentEditing/RelationTypeDisplay.cs +++ b/src/Umbraco.Web/Models/ContentEditing/RelationTypeDisplay.cs @@ -24,7 +24,7 @@ namespace Umbraco.Web.Models.ContentEditing /// /// Corresponds to the NodeObjectType in the umbracoNode table [DataMember(Name = "parentObjectType", IsRequired = true)] - public Guid ParentObjectType { get; set; } + public Guid? ParentObjectType { get; set; } /// /// Gets or sets the Parent's object type name. @@ -38,7 +38,7 @@ namespace Umbraco.Web.Models.ContentEditing /// /// Corresponds to the NodeObjectType in the umbracoNode table [DataMember(Name = "childObjectType", IsRequired = true)] - public Guid ChildObjectType { get; set; } + public Guid? ChildObjectType { get; set; } /// /// Gets or sets the Child's object type name. diff --git a/src/Umbraco.Web/Models/Mapping/RelationMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/RelationMapDefinition.cs index 7787750e54..7f89c78f8a 100644 --- a/src/Umbraco.Web/Models/Mapping/RelationMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/RelationMapDefinition.cs @@ -29,8 +29,8 @@ namespace Umbraco.Web.Models.Mapping target.Path = "-1," + source.Id; // Set the "friendly" names for the parent and child object types - target.ParentObjectTypeName = ObjectTypes.GetUmbracoObjectType(source.ParentObjectType).GetFriendlyName(); - target.ChildObjectTypeName = ObjectTypes.GetUmbracoObjectType(source.ChildObjectType).GetFriendlyName(); + target.ParentObjectTypeName = source.ParentObjectType.HasValue ? ObjectTypes.GetUmbracoObjectType(source.ParentObjectType.Value).GetFriendlyName() : string.Empty; + target.ChildObjectTypeName = source.ChildObjectType.HasValue ? ObjectTypes.GetUmbracoObjectType(source.ChildObjectType.Value).GetFriendlyName() : string.Empty; } // Umbraco.Code.MapAll -ParentName -ChildName From f656f7d0a0cb33e0b6286ae2df15cc76b65df928 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 24 Oct 2019 22:38:11 +1100 Subject: [PATCH 019/202] Fixes relations editor and loading in relations, allows creating relations without object types, fixes migration --- .../V_8_5_0/UpdateRelationTypeTable.cs | 22 +++++++- .../Implement/RelationTypeRepository.cs | 16 +++++- .../Services/Implement/RelationService.cs | 6 ++ .../src/views/relationtypes/create.html | 6 +- .../views/relationtypes/edit.controller.js | 40 +------------ .../Editors/RelationTypeController.cs | 5 +- .../Models/ContentEditing/RelationTypeSave.cs | 4 +- .../Models/Mapping/RelationMapDefinition.cs | 56 ++++++++++++++++--- 8 files changed, 95 insertions(+), 60 deletions(-) diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_5_0/UpdateRelationTypeTable.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_5_0/UpdateRelationTypeTable.cs index ed21935488..30174f8d13 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_5_0/UpdateRelationTypeTable.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_5_0/UpdateRelationTypeTable.cs @@ -11,11 +11,27 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_5_0 public override void Migrate() { - Alter.Table(Constants.DatabaseSchema.Tables.RelationType).AlterColumn("parentObjectType").AsGuid().Nullable(); - Alter.Table(Constants.DatabaseSchema.Tables.RelationType).AlterColumn("childObjectType").AsGuid().Nullable(); + Alter.Table(Constants.DatabaseSchema.Tables.RelationType).AlterColumn("parentObjectType").AsGuid().Nullable().Do(); + Alter.Table(Constants.DatabaseSchema.Tables.RelationType).AlterColumn("childObjectType").AsGuid().Nullable().Do(); //TODO: We have to update this field to ensure it's not null, we can just copy across the name since that is not nullable - Alter.Table(Constants.DatabaseSchema.Tables.RelationType).AlterColumn("alias").AsString(100).NotNullable(); + + //drop index before we can alter the column + if (IndexExists("IX_umbracoRelationType_alias")) + Delete + .Index("IX_umbracoRelationType_alias") + .OnTable(Constants.DatabaseSchema.Tables.RelationType) + .Do(); + //change the column to non nullable + Alter.Table(Constants.DatabaseSchema.Tables.RelationType).AlterColumn("alias").AsString(100).NotNullable().Do(); + //re-create the index + Create + .Index("IX_umbracoRelationType_alias") + .OnTable(Constants.DatabaseSchema.Tables.RelationType) + .OnColumn("alias") + .Ascending() + .WithOptions().Unique().WithOptions().NonClustered() + .Do(); } } } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/RelationTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/RelationTypeRepository.cs index 075d4aa769..623b55b6f8 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/RelationTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/RelationTypeRepository.cs @@ -134,7 +134,9 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override void PersistNewItem(IRelationType entity) { entity.AddingEntity(); - + + CheckNullObjectTypeValues(entity); + var dto = RelationTypeFactory.BuildDto(entity); var id = Convert.ToInt32(Database.Insert(dto)); @@ -146,7 +148,9 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override void PersistUpdatedItem(IRelationType entity) { entity.UpdatingEntity(); - + + CheckNullObjectTypeValues(entity); + var dto = RelationTypeFactory.BuildDto(entity); Database.Update(dto); @@ -154,5 +158,13 @@ namespace Umbraco.Core.Persistence.Repositories.Implement } #endregion + + private void CheckNullObjectTypeValues(IRelationType entity) + { + if (entity.ParentObjectType.HasValue && entity.ParentObjectType == Guid.Empty) + entity.ParentObjectType = null; + if (entity.ChildObjectType.HasValue && entity.ChildObjectType == Guid.Empty) + entity.ChildObjectType = null; + } } } diff --git a/src/Umbraco.Core/Services/Implement/RelationService.cs b/src/Umbraco.Core/Services/Implement/RelationService.cs index cc0a3e23bc..bf1e7bf309 100644 --- a/src/Umbraco.Core/Services/Implement/RelationService.cs +++ b/src/Umbraco.Core/Services/Implement/RelationService.cs @@ -326,6 +326,8 @@ namespace Umbraco.Core.Services.Implement /// An enumerable list of public IEnumerable GetChildEntitiesFromRelations(IEnumerable relations) { + //TODO: Argh! N+1 + foreach (var relation in relations) { var objectType = ObjectTypes.GetUmbracoObjectType(relation.ChildObjectType); @@ -340,6 +342,8 @@ namespace Umbraco.Core.Services.Implement /// An enumerable list of public IEnumerable GetParentEntitiesFromRelations(IEnumerable relations) { + //TODO: Argh! N+1 + foreach (var relation in relations) { var objectType = ObjectTypes.GetUmbracoObjectType(relation.ParentObjectType); @@ -354,6 +358,8 @@ namespace Umbraco.Core.Services.Implement /// An enumerable list of with public IEnumerable> GetEntitiesFromRelations(IEnumerable relations) { + //TODO: Argh! N+1 + foreach (var relation in relations) { var childObjectType = ObjectTypes.GetUmbracoObjectType(relation.ChildObjectType); diff --git a/src/Umbraco.Web.UI.Client/src/views/relationtypes/create.html b/src/Umbraco.Web.UI.Client/src/views/relationtypes/create.html index 67a48e77cd..c854580285 100644 --- a/src/Umbraco.Web.UI.Client/src/views/relationtypes/create.html +++ b/src/Umbraco.Web.UI.Client/src/views/relationtypes/create.html @@ -31,8 +31,7 @@ @@ -41,8 +40,7 @@ diff --git a/src/Umbraco.Web.UI.Client/src/views/relationtypes/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/relationtypes/edit.controller.js index 138e3e90e2..f83829dfba 100644 --- a/src/Umbraco.Web.UI.Client/src/views/relationtypes/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/relationtypes/edit.controller.js @@ -46,7 +46,7 @@ function RelationTypeEditController($scope, $routeParams, relationTypeResource, }); relationTypeResource.getById($routeParams.id) - .then(function(data) { + .then(function (data) { bindRelationType(data); vm.page.loading = false; }); @@ -54,7 +54,6 @@ function RelationTypeEditController($scope, $routeParams, relationTypeResource, function bindRelationType(relationType) { formatDates(relationType.relations); - getRelationNames(relationType); // Convert property value to string, since the umb-radiobutton component at the moment only handle string values. // Sometime later the umb-radiobutton might be able to handle value as boolean. @@ -70,7 +69,7 @@ function RelationTypeEditController($scope, $routeParams, relationTypeResource, } function formatDates(relations) { - if(relations) { + if (relations) { userService.getCurrentUser().then(function (currentUser) { angular.forEach(relations, function (relation) { relation.timestampFormatted = dateHelper.getLocalDate(relation.createDate, currentUser.locale, 'LLL'); @@ -79,41 +78,6 @@ function RelationTypeEditController($scope, $routeParams, relationTypeResource, } } - function getRelationNames(relationType) { - if (relationType.relations) { - // can we grab app entity types in one go? - if (relationType.parentObjectType === relationType.childObjectType) { - // yep, grab the distinct list of parent and child entities - var entityIds = _.uniq(_.union(_.pluck(relationType.relations, "parentId"), _.pluck(relationType.relations, "childId"))); - entityResource.getByIds(entityIds, relationType.parentObjectTypeName).then(function (entities) { - updateRelationNames(relationType, entities); - }); - } else { - // nope, grab the parent and child entities individually - var parentEntityIds = _.uniq(_.pluck(relationType.relations, "parentId")); - var childEntityIds = _.uniq(_.pluck(relationType.relations, "childId")); - entityResource.getByIds(parentEntityIds, relationType.parentObjectTypeName).then(function (entities) { - updateRelationNames(relationType, entities); - }); - entityResource.getByIds(childEntityIds, relationType.childObjectTypeName).then(function (entities) { - updateRelationNames(relationType, entities); - }); - } - } - } - - function updateRelationNames(relationType, entities) { - var entitiesById = _.indexBy(entities, "id"); - _.each(relationType.relations, function(relation) { - if (entitiesById[relation.parentId]) { - relation.parentName = entitiesById[relation.parentId].name; - } - if (entitiesById[relation.childId]) { - relation.childName = entitiesById[relation.childId].name; - } - }); - } - function saveRelationType() { if (formHelper.submitForm({ scope: $scope, statusMessage: "Saving..." })) { diff --git a/src/Umbraco.Web/Editors/RelationTypeController.cs b/src/Umbraco.Web/Editors/RelationTypeController.cs index 6fb9108d74..2845a82aa1 100644 --- a/src/Umbraco.Web/Editors/RelationTypeController.cs +++ b/src/Umbraco.Web/Editors/RelationTypeController.cs @@ -45,11 +45,8 @@ namespace Umbraco.Web.Editors throw new HttpResponseException(HttpStatusCode.NotFound); } - var relations = Services.RelationService.GetByRelationTypeId(relationType.Id); - var display = Mapper.Map(relationType); - display.Relations = Mapper.MapEnumerable(relations); - + return display; } diff --git a/src/Umbraco.Web/Models/ContentEditing/RelationTypeSave.cs b/src/Umbraco.Web/Models/ContentEditing/RelationTypeSave.cs index e7e8d6d2ba..434cf1de89 100644 --- a/src/Umbraco.Web/Models/ContentEditing/RelationTypeSave.cs +++ b/src/Umbraco.Web/Models/ContentEditing/RelationTypeSave.cs @@ -16,12 +16,12 @@ namespace Umbraco.Web.Models.ContentEditing /// Gets or sets the parent object type ID. /// [DataMember(Name = "parentObjectType", IsRequired = false)] - public Guid ParentObjectType { get; set; } + public Guid? ParentObjectType { get; set; } /// /// Gets or sets the child object type ID. /// [DataMember(Name = "childObjectType", IsRequired = false)] - public Guid ChildObjectType { get; set; } + public Guid? ChildObjectType { get; set; } } } diff --git a/src/Umbraco.Web/Models/Mapping/RelationMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/RelationMapDefinition.cs index 7f89c78f8a..8407a7421c 100644 --- a/src/Umbraco.Web/Models/Mapping/RelationMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/RelationMapDefinition.cs @@ -1,12 +1,23 @@ -using Umbraco.Core; +using System.Linq; +using Umbraco.Core; using Umbraco.Core.Mapping; using Umbraco.Core.Models; +using Umbraco.Core.Services; using Umbraco.Web.Models.ContentEditing; namespace Umbraco.Web.Models.Mapping { internal class RelationMapDefinition : IMapDefinition { + private readonly IEntityService _entityService; + private readonly IRelationService _relationService; + + public RelationMapDefinition(IEntityService entityService, IRelationService relationService) + { + _entityService = entityService; + _relationService = relationService; + } + public void DefineMaps(UmbracoMapper mapper) { mapper.Define((source, context) => new RelationTypeDisplay(), Map); @@ -15,8 +26,8 @@ namespace Umbraco.Web.Models.Mapping } // Umbraco.Code.MapAll -Icon -Trashed -AdditionalData - // Umbraco.Code.MapAll -Relations -ParentId -Notifications - private static void Map(IRelationType source, RelationTypeDisplay target, MapperContext context) + // Umbraco.Code.MapAll -ParentId -Notifications + private void Map(IRelationType source, RelationTypeDisplay target, MapperContext context) { target.ChildObjectType = source.ChildObjectType; target.Id = source.Id; @@ -28,13 +39,44 @@ namespace Umbraco.Web.Models.Mapping target.Udi = Udi.Create(Constants.UdiEntityType.RelationType, source.Key); target.Path = "-1," + source.Id; - // Set the "friendly" names for the parent and child object types - target.ParentObjectTypeName = source.ParentObjectType.HasValue ? ObjectTypes.GetUmbracoObjectType(source.ParentObjectType.Value).GetFriendlyName() : string.Empty; - target.ChildObjectTypeName = source.ChildObjectType.HasValue ? ObjectTypes.GetUmbracoObjectType(source.ChildObjectType.Value).GetFriendlyName() : string.Empty; + // Set the "friendly" and entity names for the parent and child object types + if (source.ParentObjectType.HasValue) + { + var objType = ObjectTypes.GetUmbracoObjectType(source.ParentObjectType.Value); + target.ParentObjectTypeName = objType.GetFriendlyName(); + } + + if (source.ChildObjectType.HasValue) + { + var objType = ObjectTypes.GetUmbracoObjectType(source.ChildObjectType.Value); + target.ChildObjectTypeName = objType.GetFriendlyName(); + } + + // Load the relations + + var relations = _relationService.GetByRelationTypeId(source.Id); + var displayRelations = context.MapEnumerable(relations); + + // Load the entities + var entities = _relationService.GetEntitiesFromRelations(relations) + .ToDictionary(x => (x.Item1.Id, x.Item2.Id), x => x); + + foreach(var r in displayRelations) + { + var pair = entities[(r.ParentId, r.ChildId)]; + var parent = pair.Item1; + var child = pair.Item2; + + r.ChildName = child.Name; + r.ParentName = parent.Name; + } + + target.Relations = displayRelations; + } // Umbraco.Code.MapAll -ParentName -ChildName - private static void Map(IRelation source, RelationDisplay target, MapperContext context) + private void Map(IRelation source, RelationDisplay target, MapperContext context) { target.ChildId = source.ChildId; target.Comment = source.Comment; From f3f242b416104d339f2cc62fb5d579c3628db82b Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 24 Oct 2019 22:39:12 +1100 Subject: [PATCH 020/202] fixes test --- .../Persistence/Repositories/RelationTypeRepositoryTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Tests/Persistence/Repositories/RelationTypeRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/RelationTypeRepositoryTest.cs index 881ea23dc8..962737e1dc 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/RelationTypeRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/RelationTypeRepositoryTest.cs @@ -133,7 +133,7 @@ namespace Umbraco.Tests.Persistence.Repositories Assert.That(relationTypes, Is.Not.Null); Assert.That(relationTypes.Any(), Is.True); Assert.That(relationTypes.Any(x => x == null), Is.False); - Assert.That(relationTypes.Count(), Is.EqualTo(5)); + Assert.That(relationTypes.Count(), Is.EqualTo(7)); } } From 15865d566b0549acf651b63cdd97b474e8a362d4 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 25 Oct 2019 14:17:18 +1100 Subject: [PATCH 021/202] Implements the tracking for document/media/member repos --- src/Umbraco.Core/Constants-Conventions.cs | 2 ++ .../Migrations/Install/DatabaseDataCreator.cs | 6 ++-- .../Migrations/Upgrade/UmbracoPlan.cs | 1 + .../Upgrade/V_8_5_0/AddNewRelationTypes.cs | 31 +++++++++++++++++++ .../V_8_5_0/UpdateRelationTypeTable.cs | 1 + .../Implement/ContentRepositoryBase.cs | 3 +- .../Implement/DocumentRepository.cs | 4 +++ .../Repositories/Implement/MediaRepository.cs | 4 +++ .../Implement/MemberRepository.cs | 4 +++ .../PropertyEditors/DataEditor.cs | 3 +- src/Umbraco.Core/Umbraco.Core.csproj | 1 + .../Trees/RelationTypeTreeController.cs | 2 ++ 12 files changed, 56 insertions(+), 6 deletions(-) create mode 100644 src/Umbraco.Core/Migrations/Upgrade/V_8_5_0/AddNewRelationTypes.cs diff --git a/src/Umbraco.Core/Constants-Conventions.cs b/src/Umbraco.Core/Constants-Conventions.cs index 25d259b7d1..e01f0eb0f5 100644 --- a/src/Umbraco.Core/Constants-Conventions.cs +++ b/src/Umbraco.Core/Constants-Conventions.cs @@ -357,6 +357,8 @@ namespace Umbraco.Core /// Alias for default relation type "Relate Parent Media Folder On Delete". /// public const string RelateParentMediaFolderOnDeleteAlias = "relateParentMediaFolderOnDelete"; + + //TODO: return a list of built in types so we can use that to prevent deletion in the uI } } } diff --git a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs index 78e22ef28e..9a96810f23 100644 --- a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs +++ b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs @@ -319,13 +319,11 @@ namespace Umbraco.Core.Migrations.Install relationType.UniqueId = (relationType.Alias + "____" + relationType.Name).ToGuid(); _database.Insert(Constants.DatabaseSchema.Tables.RelationType, "id", false, relationType); - //TODO: We need to decide if we are going to change the relations APIs since it's pretty crappy that we have to explicitly define all relation type object type combinations... - - relationType = new RelationTypeDto { Id = 4, Alias = Constants.Conventions.RelationTypes.RelatedMediaAlias, ChildObjectType = Constants.ObjectTypes.Media, ParentObjectType = Constants.ObjectTypes.Document, Dual = false, Name = Constants.Conventions.RelationTypes.RelatedMediaName }; + relationType = new RelationTypeDto { Id = 4, Alias = Constants.Conventions.RelationTypes.RelatedMediaAlias, ChildObjectType = null, ParentObjectType = null, Dual = false, Name = Constants.Conventions.RelationTypes.RelatedMediaName }; relationType.UniqueId = (relationType.Alias + "____" + relationType.Name).ToGuid(); _database.Insert(Constants.DatabaseSchema.Tables.RelationType, "id", false, relationType); - relationType = new RelationTypeDto { Id = 5, Alias = Constants.Conventions.RelationTypes.RelatedDocumentAlias, ChildObjectType = Constants.ObjectTypes.Document, ParentObjectType = Constants.ObjectTypes.Document, Dual = false, Name = Constants.Conventions.RelationTypes.RelatedDocumentName }; + relationType = new RelationTypeDto { Id = 5, Alias = Constants.Conventions.RelationTypes.RelatedDocumentAlias, ChildObjectType = null, ParentObjectType = null, Dual = false, Name = Constants.Conventions.RelationTypes.RelatedDocumentName }; relationType.UniqueId = (relationType.Alias + "____" + relationType.Name).ToGuid(); _database.Insert(Constants.DatabaseSchema.Tables.RelationType, "id", false, relationType); } diff --git a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs index e5e335d309..45182b17e3 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs @@ -185,6 +185,7 @@ namespace Umbraco.Core.Migrations.Upgrade // to 8.5.0... To("{4759A294-9860-46BC-99F9-B4C975CAE580}"); + To("{0BC866BC-0665-487A-9913-0290BD0169AD}"); //FINAL } diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_5_0/AddNewRelationTypes.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_5_0/AddNewRelationTypes.cs new file mode 100644 index 0000000000..88c6c43c46 --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_5_0/AddNewRelationTypes.cs @@ -0,0 +1,31 @@ +namespace Umbraco.Core.Migrations.Upgrade.V_8_5_0 +{ + /// + /// Ensures the new relation types are created + /// + public class AddNewRelationTypes : MigrationBase + { + public AddNewRelationTypes(IMigrationContext context) + : base(context) + { } + + public override void Migrate() + { + CreateRelation( + Constants.Conventions.RelationTypes.RelatedMediaAlias, + Constants.Conventions.RelationTypes.RelatedMediaName); + + CreateRelation( + Constants.Conventions.RelationTypes.RelatedDocumentAlias, + Constants.Conventions.RelationTypes.RelatedDocumentName); + } + + private void CreateRelation(string alias, string name) + { + var uniqueId = (alias + "____" + name).ToGuid(); //this is the same as how it installs so everything is consistent + Insert.IntoTable(Constants.DatabaseSchema.Tables.RelationType) + .Row(new { typeUniqueId = uniqueId, dual = 0, name, alias }) + .Do(); + } + } +} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_5_0/UpdateRelationTypeTable.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_5_0/UpdateRelationTypeTable.cs index 30174f8d13..b76f5ba3a7 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_5_0/UpdateRelationTypeTable.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_5_0/UpdateRelationTypeTable.cs @@ -2,6 +2,7 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_5_0 { + public class UpdateRelationTypeTable : MigrationBase { public UpdateRelationTypeTable(IMigrationContext context) diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs index 3bb57dca9f..8010d60267 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs @@ -826,7 +826,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement foreach (var p in entity.Properties) { if (!PropertyEditors.TryGet(p.PropertyType.PropertyEditorAlias, out var editor)) continue; - if (!(editor is IDataValueReference reference)) continue; + var valueEditor = editor.GetValueEditor(); + if (!(valueEditor is IDataValueReference reference)) continue; //TODO: Support variants/segments! This is not required for this initial prototype which is why there is a check here if (!p.PropertyType.VariesByNothing()) continue; diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs index ce95875209..5e3e7f05b9 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs @@ -484,6 +484,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement ClearEntityTags(entity, _tagRepository); } + PersistRelations(entity); + entity.ResetDirtyProperties(); // troubleshooting @@ -687,6 +689,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement ClearEntityTags(entity, _tagRepository); } + PersistRelations(entity); + // TODO: note re. tags: explicitly unpublished entities have cleared tags, but masked or trashed entities *still* have tags in the db - so what? entity.ResetDirtyProperties(); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs index 0423ac9125..a3f9e45485 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs @@ -289,6 +289,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // set tags SetEntityTags(entity, _tagRepository); + PersistRelations(entity); + OnUowRefreshedEntity(new ScopedEntityEventArgs(AmbientScope, entity)); entity.ResetDirtyProperties(); @@ -345,6 +347,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement SetEntityTags(entity, _tagRepository); + PersistRelations(entity); + OnUowRefreshedEntity(new ScopedEntityEventArgs(AmbientScope, entity)); entity.ResetDirtyProperties(); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs index c36143d09a..2871bf1dd3 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs @@ -324,6 +324,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement SetEntityTags(entity, _tagRepository); + PersistRelations(entity); + OnUowRefreshedEntity(new ScopedEntityEventArgs(AmbientScope, entity)); entity.ResetDirtyProperties(); @@ -389,6 +391,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement SetEntityTags(entity, _tagRepository); + PersistRelations(entity); + OnUowRefreshedEntity(new ScopedEntityEventArgs(AmbientScope, entity)); entity.ResetDirtyProperties(); diff --git a/src/Umbraco.Core/PropertyEditors/DataEditor.cs b/src/Umbraco.Core/PropertyEditors/DataEditor.cs index dbb2fc467e..a82011edcc 100644 --- a/src/Umbraco.Core/PropertyEditors/DataEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/DataEditor.cs @@ -19,6 +19,7 @@ namespace Umbraco.Core.PropertyEditors public class DataEditor : IDataEditor { private IDictionary _defaultConfiguration; + private IDataValueEditor _nonConfigured; /// /// Initializes a new instance of the class. @@ -90,7 +91,7 @@ namespace Umbraco.Core.PropertyEditors /// simple enough for now. /// // TODO: point of that one? shouldn't we always configure? - public IDataValueEditor GetValueEditor() => ExplicitValueEditor ?? CreateValueEditor(); + public IDataValueEditor GetValueEditor() => ExplicitValueEditor ?? (_nonConfigured ?? (_nonConfigured= CreateValueEditor())); /// /// diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 1f83bd2807..ff3f8bf972 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -245,6 +245,7 @@ + diff --git a/src/Umbraco.Web/Trees/RelationTypeTreeController.cs b/src/Umbraco.Web/Trees/RelationTypeTreeController.cs index ab6dd39820..aa3206b5e4 100644 --- a/src/Umbraco.Web/Trees/RelationTypeTreeController.cs +++ b/src/Umbraco.Web/Trees/RelationTypeTreeController.cs @@ -16,6 +16,8 @@ namespace Umbraco.Web.Trees { protected override MenuItemCollection GetMenuForNode(string id, FormDataCollection queryStrings) { + //TODO: Do not allow deleting built in types + var menu = new MenuItemCollection(); if (id == Constants.System.RootString) From 25c2eed8883813101da1b2371a7d899eae808b9c Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 25 Oct 2019 14:33:40 +1100 Subject: [PATCH 022/202] Ensures the right type is tracked for links ensures relations are removed. --- src/Umbraco.Core/Constants-Conventions.cs | 9 +++++++++ .../Repositories/Implement/ContentRepositoryBase.cs | 7 +++---- .../PropertyEditors/RichTextPropertyEditor.cs | 2 +- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Core/Constants-Conventions.cs b/src/Umbraco.Core/Constants-Conventions.cs index e01f0eb0f5..e2e0f30874 100644 --- a/src/Umbraco.Core/Constants-Conventions.cs +++ b/src/Umbraco.Core/Constants-Conventions.cs @@ -358,6 +358,15 @@ namespace Umbraco.Core /// public const string RelateParentMediaFolderOnDeleteAlias = "relateParentMediaFolderOnDelete"; + /// + /// Returns the types of relations that are automatically tracked + /// + /// + /// Developers should not manually use these relation types since they will all be cleared whenever an entity + /// (content, media or member) is saved since they are auto-populated based on property values. + /// + public static string[] AutomaticRelationTypes = new[] { RelatedMediaAlias, RelatedDocumentAlias }; + //TODO: return a list of built in types so we can use that to prevent deletion in the uI } } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs index 8010d60267..aaa3946494 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs @@ -837,11 +837,10 @@ namespace Umbraco.Core.Persistence.Repositories.Implement trackedRelations.AddRange(refs); } - if (trackedRelations.Count == 0) return; + //First delete all auto-relations for this entity + RelationRepository.DeleteByParent(entity.Id, Constants.Conventions.RelationTypes.AutomaticRelationTypes); - //First delete all relations for this entity - var relationTypes = trackedRelations.Select(x => x.RelationTypeAlias).ToArray(); - RelationRepository.DeleteByParent(entity.Id, relationTypes); + if (trackedRelations.Count == 0) return; var udiToGuids = trackedRelations.Select(x => x.Udi as GuidUdi) .ToDictionary(x => (Udi)x, x => x.Guid); diff --git a/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs index ed3f484a4e..77617f7779 100644 --- a/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs @@ -144,7 +144,7 @@ namespace Umbraco.Web.PropertyEditors yield return new UmbracoEntityReference(udi, Constants.Conventions.RelationTypes.RelatedMediaAlias); foreach (var udi in _localLinkParser.FindUdisFromLocalLinks(asString)) - yield return new UmbracoEntityReference(udi, Constants.Conventions.RelationTypes.RelatedMediaAlias); + yield return new UmbracoEntityReference(udi, Constants.Conventions.RelationTypes.RelatedDocumentAlias); //TODO: Detect Macros too ... but we can save that for a later date, right now need to do media refs } From 70ccee302d186542235e537ed1ea611ae22cc694 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 25 Oct 2019 15:08:56 +1100 Subject: [PATCH 023/202] Renames/moves some stuff and adds test to show auto relation tracking --- ...ntityType.cs => Contants-UdiEntityType.cs} | 0 src/Umbraco.Core/Umbraco.Core.csproj | 2 +- .../Services/ContentServiceTests.cs | 41 +++++++++++++++++++ ...Tests.cs => HtmlImageSourceParserTests.cs} | 2 +- .../HtmlLocalLinkParserTests.cs | 38 ++++++++++++----- .../Templates/LocalLinkParserTests.cs | 34 --------------- src/Umbraco.Tests/Umbraco.Tests.csproj | 5 +-- .../PropertyEditors/RichTextPropertyEditor.cs | 4 ++ .../Templates/HtmlImageSourceParser.cs | 2 +- 9 files changed, 78 insertions(+), 50 deletions(-) rename src/Umbraco.Core/{UdiEntityType.cs => Contants-UdiEntityType.cs} (100%) rename src/Umbraco.Tests/Templates/{ImageSourceParserTests.cs => HtmlImageSourceParserTests.cs} (98%) rename src/Umbraco.Tests/{Web => Templates}/HtmlLocalLinkParserTests.cs (78%) delete mode 100644 src/Umbraco.Tests/Templates/LocalLinkParserTests.cs diff --git a/src/Umbraco.Core/UdiEntityType.cs b/src/Umbraco.Core/Contants-UdiEntityType.cs similarity index 100% rename from src/Umbraco.Core/UdiEntityType.cs rename to src/Umbraco.Core/Contants-UdiEntityType.cs diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index ff3f8bf972..2fd2c38bdd 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -1522,7 +1522,7 @@ - + diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs index 7d65513cc0..89873b5880 100644 --- a/src/Umbraco.Tests/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs @@ -448,6 +448,47 @@ namespace Umbraco.Tests.Services Assert.That(content.HasIdentity, Is.False); } + [Test] + public void Automatically_Track_Relations() + { + var mt = MockedContentTypes.CreateSimpleMediaType("testMediaType", "Test Media Type"); + ServiceContext.MediaTypeService.Save(mt); + var m1 = MockedMedia.CreateSimpleMedia(mt, "hello 1", -1); + var m2 = MockedMedia.CreateSimpleMedia(mt, "hello 1", -1); + ServiceContext.MediaService.Save(m1); + ServiceContext.MediaService.Save(m2); + + var ct = MockedContentTypes.CreateTextPageContentType("richTextTest"); + ct.AllowedTemplates = Enumerable.Empty(); + + ServiceContext.ContentTypeService.Save(ct); + + var c1 = MockedContent.CreateTextpageContent(ct, "my content 1", -1); + ServiceContext.ContentService.Save(c1); + + var c2 = MockedContent.CreateTextpageContent(ct, "my content 2", -1); + + //'bodyText' is a property with a RTE property editor which we knows tracks relations + c2.Properties["bodyText"].SetValue(@"

+ +

+

+

+ hello +

"); + + ServiceContext.ContentService.Save(c2); + + var relations = ServiceContext.RelationService.GetByParentId(c2.Id).ToList(); + Assert.AreEqual(3, relations.Count); + Assert.AreEqual(Constants.Conventions.RelationTypes.RelatedMediaAlias, relations[0].RelationType.Alias); + Assert.AreEqual(m1.Id, relations[0].ChildId); + Assert.AreEqual(Constants.Conventions.RelationTypes.RelatedMediaAlias, relations[1].RelationType.Alias); + Assert.AreEqual(m2.Id, relations[1].ChildId); + Assert.AreEqual(Constants.Conventions.RelationTypes.RelatedDocumentAlias, relations[2].RelationType.Alias); + Assert.AreEqual(c1.Id, relations[2].ChildId); + } + [Test] public void Can_Create_Content_Without_Explicitly_Set_User() { diff --git a/src/Umbraco.Tests/Templates/ImageSourceParserTests.cs b/src/Umbraco.Tests/Templates/HtmlImageSourceParserTests.cs similarity index 98% rename from src/Umbraco.Tests/Templates/ImageSourceParserTests.cs rename to src/Umbraco.Tests/Templates/HtmlImageSourceParserTests.cs index dbe5654117..3bef495507 100644 --- a/src/Umbraco.Tests/Templates/ImageSourceParserTests.cs +++ b/src/Umbraco.Tests/Templates/HtmlImageSourceParserTests.cs @@ -20,7 +20,7 @@ namespace Umbraco.Tests.Templates [TestFixture] - public class ImageSourceParserTests + public class HtmlImageSourceParserTests { [Test] public void Returns_Udis_From_Data_Udi_Html_Attributes() diff --git a/src/Umbraco.Tests/Web/HtmlLocalLinkParserTests.cs b/src/Umbraco.Tests/Templates/HtmlLocalLinkParserTests.cs similarity index 78% rename from src/Umbraco.Tests/Web/HtmlLocalLinkParserTests.cs rename to src/Umbraco.Tests/Templates/HtmlLocalLinkParserTests.cs index e6a0abeb4c..7cd96a32ed 100644 --- a/src/Umbraco.Tests/Web/HtmlLocalLinkParserTests.cs +++ b/src/Umbraco.Tests/Templates/HtmlLocalLinkParserTests.cs @@ -1,26 +1,44 @@ -using System; +using Moq; +using NUnit.Framework; +using System; using System.Linq; using System.Web; -using Moq; -using NUnit.Framework; -using Umbraco.Core.Configuration.UmbracoSettings; +using Umbraco.Core; using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; -using Umbraco.Core.Services; -using Umbraco.Tests.TestHelpers; using Umbraco.Tests.Testing.Objects; using Umbraco.Tests.Testing.Objects.Accessors; using Umbraco.Web; -using Umbraco.Web.PublishedCache; using Umbraco.Web.Routing; using Umbraco.Web.Templates; -namespace Umbraco.Tests.Web +namespace Umbraco.Tests.Templates { - [TestFixture] public class HtmlLocalLinkParserTests { + [Test] + public void Returns_Udis_From_LocalLinks() + { + var input = @"

+

+ + hello +
+

+hello +

"; + + var umbracoContextAccessor = new TestUmbracoContextAccessor(); + var parser = new HtmlLocalLinkParser(umbracoContextAccessor); + + var result = parser.FindUdisFromLocalLinks(input).ToList(); + + Assert.AreEqual(2, result.Count); + Assert.AreEqual(Udi.Parse("umb://document/C093961595094900AAF9170DDE6AD442"), result[0]); + Assert.AreEqual(Udi.Parse("umb://document-type/2D692FCB070B4CDA92FB6883FDBFD6E2"), result[1]); + } + [TestCase("", "")] [TestCase("hello href=\"{localLink:1234}\" world ", "hello href=\"/my-test-url\" world ")] [TestCase("hello href=\"{localLink:umb://document/9931BDE0-AAC3-4BAB-B838-909A7B47570E}\" world ", "hello href=\"/my-test-url\" world ")] @@ -40,7 +58,7 @@ namespace Umbraco.Tests.Web var publishedContent = new Mock(); publishedContent.Setup(x => x.Id).Returns(1234); publishedContent.Setup(x => x.ContentType).Returns(contentType); - + var mediaType = new PublishedContentType(777, "image", PublishedItemType.Media, Enumerable.Empty(), Enumerable.Empty(), ContentVariation.Nothing); var media = new Mock(); media.Setup(x => x.ContentType).Returns(mediaType); diff --git a/src/Umbraco.Tests/Templates/LocalLinkParserTests.cs b/src/Umbraco.Tests/Templates/LocalLinkParserTests.cs deleted file mode 100644 index e09d71196e..0000000000 --- a/src/Umbraco.Tests/Templates/LocalLinkParserTests.cs +++ /dev/null @@ -1,34 +0,0 @@ -using NUnit.Framework; -using System.Linq; -using Umbraco.Core; -using Umbraco.Tests.Testing.Objects.Accessors; -using Umbraco.Web.Templates; - -namespace Umbraco.Tests.Templates -{ - [TestFixture] - public class LocalLinkParserTests - { - [Test] - public void Returns_Udis_From_LocalLinks() - { - var input = @"

-

- - hello -
-

-hello -

"; - - var umbracoContextAccessor = new TestUmbracoContextAccessor(); - var parser = new HtmlLocalLinkParser(umbracoContextAccessor); - - var result = parser.FindUdisFromLocalLinks(input).ToList(); - - Assert.AreEqual(2, result.Count); - Assert.AreEqual(Udi.Parse("umb://document/C093961595094900AAF9170DDE6AD442"), result[0]); - Assert.AreEqual(Udi.Parse("umb://document-type/2D692FCB070B4CDA92FB6883FDBFD6E2"), result[1]); - } - } -} diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index fa654ad4a4..4b035f631e 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -159,7 +159,7 @@ - + @@ -255,8 +255,7 @@ - - + diff --git a/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs index 77617f7779..1c97a2de7a 100644 --- a/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs @@ -140,9 +140,13 @@ namespace Umbraco.Web.PropertyEditors { var asString = value == null ? string.Empty : value is string str ? str : value.ToString(); + //TODO: FindUdisFromDataAttributes will return UDIs of any type found, typically these will always be "media" but + // if the text is modified to be another type it will still be returned but we will always be relating these with the RelatedMediaAlias foreach (var udi in _imageSourceParser.FindUdisFromDataAttributes(asString)) yield return new UmbracoEntityReference(udi, Constants.Conventions.RelationTypes.RelatedMediaAlias); + //TODO: FindUdisFromLocalLinks will return UDIs of any type found, typically these will always be "document" but + // if the text is modified to be another type it will still be returned but we will always be relating these with the RelatedDocumentAlias foreach (var udi in _localLinkParser.FindUdisFromLocalLinks(asString)) yield return new UmbracoEntityReference(udi, Constants.Conventions.RelationTypes.RelatedDocumentAlias); diff --git a/src/Umbraco.Web/Templates/HtmlImageSourceParser.cs b/src/Umbraco.Web/Templates/HtmlImageSourceParser.cs index 66a1cee5bb..b0d6980ef3 100644 --- a/src/Umbraco.Web/Templates/HtmlImageSourceParser.cs +++ b/src/Umbraco.Web/Templates/HtmlImageSourceParser.cs @@ -20,7 +20,7 @@ namespace Umbraco.Web.Templates RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); /// - /// Parses out UDIs from an html string based on 'data-udi' html attributes + /// Parses out media UDIs from an html string based on 'data-udi' html attributes /// /// /// From 1a75d99a6b3c6646842c1777502f7f9058162db8 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 25 Oct 2019 16:14:44 +1100 Subject: [PATCH 024/202] fixes test --- src/Umbraco.Core/PropertyEditors/DataEditor.cs | 8 ++++---- .../Packaging/PackageDataInstallationTests.cs | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Core/PropertyEditors/DataEditor.cs b/src/Umbraco.Core/PropertyEditors/DataEditor.cs index a82011edcc..3f5f6afa4f 100644 --- a/src/Umbraco.Core/PropertyEditors/DataEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/DataEditor.cs @@ -91,7 +91,7 @@ namespace Umbraco.Core.PropertyEditors /// simple enough for now. /// // TODO: point of that one? shouldn't we always configure? - public IDataValueEditor GetValueEditor() => ExplicitValueEditor ?? (_nonConfigured ?? (_nonConfigured= CreateValueEditor())); + public IDataValueEditor GetValueEditor() => ExplicitValueEditor ?? (_nonConfigured ?? (_nonConfigured = CreateValueEditor())); /// /// @@ -114,7 +114,7 @@ namespace Umbraco.Core.PropertyEditors return ExplicitValueEditor; var editor = CreateValueEditor(); - ((DataValueEditor) editor).Configuration = configuration; // TODO: casting is bad + ((DataValueEditor)editor).Configuration = configuration; // TODO: casting is bad return editor; } @@ -164,7 +164,7 @@ namespace Umbraco.Core.PropertyEditors protected virtual IDataValueEditor CreateValueEditor() { if (Attribute == null) - throw new InvalidOperationException("The editor does not specify a view."); + throw new InvalidOperationException($"The editor is not attributed with {nameof(DataEditorAttribute)}"); return new DataValueEditor(Attribute); } @@ -176,7 +176,7 @@ namespace Umbraco.Core.PropertyEditors { var editor = new ConfigurationEditor(); // pass the default configuration if this is not a property value editor - if((Type & EditorType.PropertyValue) == 0) + if ((Type & EditorType.PropertyValue) == 0) { editor.DefaultConfiguration = _defaultConfiguration; } diff --git a/src/Umbraco.Tests/Packaging/PackageDataInstallationTests.cs b/src/Umbraco.Tests/Packaging/PackageDataInstallationTests.cs index 51df7d1f2f..ddfced7c8f 100644 --- a/src/Umbraco.Tests/Packaging/PackageDataInstallationTests.cs +++ b/src/Umbraco.Tests/Packaging/PackageDataInstallationTests.cs @@ -26,22 +26,22 @@ namespace Umbraco.Tests.Packaging public class PackageDataInstallationTests : TestWithSomeContentBase { [HideFromTypeFinder] + [DataEditor("7e062c13-7c41-4ad9-b389-41d88aeef87c", "Editor1", "editor1")] public class Editor1 : DataEditor { public Editor1(ILogger logger) : base(logger) { - Alias = "7e062c13-7c41-4ad9-b389-41d88aeef87c"; } } [HideFromTypeFinder] + [DataEditor("d15e1281-e456-4b24-aa86-1dda3e4299d5", "Editor2", "editor2")] public class Editor2 : DataEditor { public Editor2(ILogger logger) : base(logger) { - Alias = "d15e1281-e456-4b24-aa86-1dda3e4299d5"; } } From 21290fd13430e7dfbf8b75f96b8833beee805461 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Mon, 28 Oct 2019 12:53:08 +0100 Subject: [PATCH 025/202] AB2462 - Fixed issue with multiple references to the same item --- .../Repositories/Implement/ContentRepositoryBase.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs index aaa3946494..d93fd72bfb 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs @@ -25,7 +25,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement internal sealed class ContentRepositoryBase { /// - /// + /// /// This is used for unit tests ONLY /// public static bool ThrowOnWarning = false; @@ -38,7 +38,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement private readonly Lazy _propertyEditors; /// - /// + /// /// /// /// @@ -842,6 +842,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (trackedRelations.Count == 0) return; + trackedRelations = trackedRelations.Distinct().ToList(); var udiToGuids = trackedRelations.Select(x => x.Udi as GuidUdi) .ToDictionary(x => (Udi)x, x => x.Guid); @@ -862,7 +863,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (!keyToIds.TryGetValue(guid, out var id)) continue; // This shouldn't happen! - + //Create new relation //TODO: This is N+1, we could do this all in one operation, just need a new method on the relations repo RelationRepository.Save(new Relation(entity.Id, id, relationType)); From 5699dc32b714564b9c274b51835abd449b3a4a53 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Mon, 28 Oct 2019 12:53:37 +0100 Subject: [PATCH 026/202] AB2462 - Find the relation type from the entity type on the udi --- .../Models/Editors/UmbracoEntityReference.cs | 15 +++++++++++++++ .../PropertyEditors/RichTextPropertyEditor.cs | 8 ++------ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Core/Models/Editors/UmbracoEntityReference.cs b/src/Umbraco.Core/Models/Editors/UmbracoEntityReference.cs index f5121988f5..31d48e60cf 100644 --- a/src/Umbraco.Core/Models/Editors/UmbracoEntityReference.cs +++ b/src/Umbraco.Core/Models/Editors/UmbracoEntityReference.cs @@ -16,6 +16,21 @@ namespace Umbraco.Core.Models.Editors RelationTypeAlias = relationTypeAlias ?? throw new ArgumentNullException(nameof(relationTypeAlias)); } + public UmbracoEntityReference(Udi udi) + { + Udi = udi ?? throw new ArgumentNullException(nameof(udi)); + + switch (udi.EntityType) + { + case Constants.UdiEntityType.Media: + RelationTypeAlias = Constants.Conventions.RelationTypes.RelatedMediaAlias; + break; + default: + RelationTypeAlias = Constants.Conventions.RelationTypes.RelatedDocumentAlias; + break; + } + } + public static UmbracoEntityReference Empty() => _empty; public static bool IsEmpty(UmbracoEntityReference reference) => reference == Empty(); diff --git a/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs index 1c97a2de7a..678b7f2eb8 100644 --- a/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs @@ -140,15 +140,11 @@ namespace Umbraco.Web.PropertyEditors { var asString = value == null ? string.Empty : value is string str ? str : value.ToString(); - //TODO: FindUdisFromDataAttributes will return UDIs of any type found, typically these will always be "media" but - // if the text is modified to be another type it will still be returned but we will always be relating these with the RelatedMediaAlias foreach (var udi in _imageSourceParser.FindUdisFromDataAttributes(asString)) - yield return new UmbracoEntityReference(udi, Constants.Conventions.RelationTypes.RelatedMediaAlias); + yield return new UmbracoEntityReference(udi); - //TODO: FindUdisFromLocalLinks will return UDIs of any type found, typically these will always be "document" but - // if the text is modified to be another type it will still be returned but we will always be relating these with the RelatedDocumentAlias foreach (var udi in _localLinkParser.FindUdisFromLocalLinks(asString)) - yield return new UmbracoEntityReference(udi, Constants.Conventions.RelationTypes.RelatedDocumentAlias); + yield return new UmbracoEntityReference(udi); //TODO: Detect Macros too ... but we can save that for a later date, right now need to do media refs } From c52adf76f7fe5daedd1a51daf9c994b235c838ef Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Mon, 28 Oct 2019 12:56:54 +0100 Subject: [PATCH 027/202] AB3226 - Added MediaPicker references --- .../MediaPickerPropertyEditor.cs | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs index dd755ee0ba..0d0f1a8498 100644 --- a/src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs @@ -1,5 +1,7 @@ -using Umbraco.Core; +using System.Collections.Generic; +using Umbraco.Core; using Umbraco.Core.Logging; +using Umbraco.Core.Models.Editors; using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PropertyEditors @@ -17,14 +19,32 @@ namespace Umbraco.Web.PropertyEditors Icon = Constants.Icons.MediaImage)] public class MediaPickerPropertyEditor : DataEditor { + /// /// Initializes a new instance of the class. /// public MediaPickerPropertyEditor(ILogger logger) : base(logger) - { } + { + } /// protected override IConfigurationEditor CreateConfigurationEditor() => new MediaPickerConfigurationEditor(); + + protected override IDataValueEditor CreateValueEditor() => new MediaPickerPropertyValueEditor(Attribute); + } + + public class MediaPickerPropertyValueEditor : DataValueEditor, IDataValueReference + { + public MediaPickerPropertyValueEditor(DataEditorAttribute attribute) : base(attribute) + { + } + + public IEnumerable GetReferences(object value) + { + var asString = value == null ? string.Empty : value is string str ? str : value.ToString(); + + yield return new UmbracoEntityReference(Udi.Parse(asString)); + } } } From 80643ac6ad8b45c5133e13fcbe9aa4bb31c4a438 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Mon, 28 Oct 2019 13:01:39 +0100 Subject: [PATCH 028/202] AB3325 - Added Grid references --- .../PropertyEditors/GridPropertyEditor.cs | 57 ++++++++++++++++--- 1 file changed, 48 insertions(+), 9 deletions(-) diff --git a/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs index 4eea36300b..e30e661ab1 100644 --- a/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs @@ -29,13 +29,19 @@ namespace Umbraco.Web.PropertyEditors private IUmbracoContextAccessor _umbracoContextAccessor; private readonly HtmlImageSourceParser _imageSourceParser; private readonly RichTextEditorPastedImages _pastedImages; + private readonly HtmlLocalLinkParser _localLinkParser; - public GridPropertyEditor(ILogger logger, IUmbracoContextAccessor umbracoContextAccessor, HtmlImageSourceParser imageSourceParser, RichTextEditorPastedImages pastedImages) + public GridPropertyEditor(ILogger logger, + IUmbracoContextAccessor umbracoContextAccessor, + HtmlImageSourceParser imageSourceParser, + RichTextEditorPastedImages pastedImages, + HtmlLocalLinkParser localLinkParser) : base(logger) { _umbracoContextAccessor = umbracoContextAccessor; _imageSourceParser = imageSourceParser; _pastedImages = pastedImages; + _localLinkParser = localLinkParser; } public override IPropertyIndexValueFactory PropertyIndexValueFactory => new GridPropertyIndexValueFactory(); @@ -44,22 +50,30 @@ namespace Umbraco.Web.PropertyEditors /// Overridden to ensure that the value is validated /// /// - protected override IDataValueEditor CreateValueEditor() => new GridPropertyValueEditor(Attribute, _umbracoContextAccessor, _imageSourceParser, _pastedImages); + protected override IDataValueEditor CreateValueEditor() => new GridPropertyValueEditor(Attribute, _umbracoContextAccessor, _imageSourceParser, _pastedImages, _localLinkParser); protected override IConfigurationEditor CreateConfigurationEditor() => new GridConfigurationEditor(); - internal class GridPropertyValueEditor : DataValueEditor + internal class GridPropertyValueEditor : DataValueEditor, IDataValueReference { - private IUmbracoContextAccessor _umbracoContextAccessor; + private readonly IUmbracoContextAccessor _umbracoContextAccessor; private readonly HtmlImageSourceParser _imageSourceParser; private readonly RichTextEditorPastedImages _pastedImages; + private readonly RichTextPropertyEditor.RichTextPropertyValueEditor _richTextPropertyValueEditor; + private readonly MediaPickerPropertyValueEditor _mediaPickerPropertyValueEditor; - public GridPropertyValueEditor(DataEditorAttribute attribute, IUmbracoContextAccessor umbracoContextAccessor, HtmlImageSourceParser imageSourceParser, RichTextEditorPastedImages pastedImages) + public GridPropertyValueEditor(DataEditorAttribute attribute, + IUmbracoContextAccessor umbracoContextAccessor, + HtmlImageSourceParser imageSourceParser, + RichTextEditorPastedImages pastedImages, + HtmlLocalLinkParser localLinkParser) : base(attribute) { _umbracoContextAccessor = umbracoContextAccessor; _imageSourceParser = imageSourceParser; _pastedImages = pastedImages; + _richTextPropertyValueEditor = new RichTextPropertyEditor.RichTextPropertyValueEditor(attribute, umbracoContextAccessor, imageSourceParser, localLinkParser, pastedImages); + _mediaPickerPropertyValueEditor = new MediaPickerPropertyValueEditor(attribute); } /// @@ -84,7 +98,7 @@ namespace Umbraco.Web.PropertyEditors var mediaParent = config?.MediaParentId; var mediaParentId = mediaParent == null ? Guid.Empty : mediaParent.Guid; - var grid = DeserializeGridValue(rawJson, out var rtes); + var grid = DeserializeGridValue(rawJson, out var rtes, out _); var userId = _umbracoContextAccessor.UmbracoContext?.Security.CurrentUser.Id ?? Constants.Security.SuperUserId; @@ -117,7 +131,7 @@ namespace Umbraco.Web.PropertyEditors var val = property.GetValue(culture, segment); if (val == null) return string.Empty; - var grid = DeserializeGridValue(val.ToString(), out var rtes); + var grid = DeserializeGridValue(val.ToString(), out var rtes, out _); //process the rte values foreach (var rte in rtes.ToList()) @@ -131,16 +145,41 @@ namespace Umbraco.Web.PropertyEditors return grid; } - private GridValue DeserializeGridValue(string rawJson, out IEnumerable richTextValues) + private GridValue DeserializeGridValue(string rawJson, out IEnumerable richTextValues, out IEnumerable mediaValues) { var grid = JsonConvert.DeserializeObject(rawJson); // Find all controls that use the RTE editor - var controls = grid.Sections.SelectMany(x => x.Rows.SelectMany(r => r.Areas).SelectMany(a => a.Controls)); + var controls = grid.Sections.SelectMany(x => x.Rows.SelectMany(r => r.Areas).SelectMany(a => a.Controls)).ToArray(); richTextValues = controls.Where(x => x.Editor.Alias.ToLowerInvariant() == "rte"); + mediaValues = controls.Where(x => x.Editor.Alias.ToLowerInvariant() == "media"); return grid; } + + /// + /// Resolve references from values + /// + /// + /// + public IEnumerable GetReferences(object value) + { + var rawJson = value == null ? string.Empty : value is string str ? str : value.ToString(); + + DeserializeGridValue(rawJson, out var richTextEditorValues, out var mediaValues); + + foreach (var umbracoEntityReference in richTextEditorValues.SelectMany(x => + _richTextPropertyValueEditor.GetReferences(x.Value))) + { + yield return umbracoEntityReference; + } + + foreach (var umbracoEntityReference in mediaValues.SelectMany(x => + _mediaPickerPropertyValueEditor.GetReferences(x.Value["udi"]))) + { + yield return umbracoEntityReference; + } + } } } } From 35ebc13774e07ba5fd62573348c61de291e5d433 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Mon, 28 Oct 2019 13:25:05 +0100 Subject: [PATCH 029/202] AB3331 - Added MNTP references --- .../MultiNodeTreePickerPropertyEditor.cs | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web/PropertyEditors/MultiNodeTreePickerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MultiNodeTreePickerPropertyEditor.cs index 742acbeca2..020ad38c75 100644 --- a/src/Umbraco.Web/PropertyEditors/MultiNodeTreePickerPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MultiNodeTreePickerPropertyEditor.cs @@ -1,5 +1,7 @@ -using Umbraco.Core; +using System.Collections.Generic; +using Umbraco.Core; using Umbraco.Core.Logging; +using Umbraco.Core.Models.Editors; using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PropertyEditors @@ -18,5 +20,29 @@ namespace Umbraco.Web.PropertyEditors { } protected override IConfigurationEditor CreateConfigurationEditor() => new MultiNodePickerConfigurationEditor(); + + protected override IDataValueEditor CreateValueEditor() => new MultiNodeTreePickerPropertyValueEditor(Attribute); + + public class MultiNodeTreePickerPropertyValueEditor : DataValueEditor, IDataValueReference + { + public MultiNodeTreePickerPropertyValueEditor(DataEditorAttribute attribute): base(attribute) + { + + } + + public IEnumerable GetReferences(object value) + { + var asString = value == null ? string.Empty : value is string str ? str : value.ToString(); + + var udiPaths = asString.Split(','); + foreach (var udiPath in udiPaths) + { + yield return new UmbracoEntityReference(Udi.Parse(udiPath)); + } + + } + } } + + } From f2e02ea05eff34f533bb60e6da049eb9f54c8c62 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Mon, 28 Oct 2019 13:57:55 +0100 Subject: [PATCH 030/202] AB3329 - Added Content picker references --- .../ContentPickerPropertyEditor.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/Umbraco.Web/PropertyEditors/ContentPickerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/ContentPickerPropertyEditor.cs index c6de91f560..0e6eaa9254 100644 --- a/src/Umbraco.Web/PropertyEditors/ContentPickerPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/ContentPickerPropertyEditor.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using Umbraco.Core; using Umbraco.Core.Logging; +using Umbraco.Core.Models.Editors; using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PropertyEditors @@ -25,5 +26,23 @@ namespace Umbraco.Web.PropertyEditors { return new ContentPickerConfigurationEditor(); } + + protected override IDataValueEditor CreateValueEditor() => new ContentPickerPropertyValueEditor(Attribute); + + internal class ContentPickerPropertyValueEditor : DataValueEditor, IDataValueReference + { + public ContentPickerPropertyValueEditor(DataEditorAttribute attribute) : base(attribute) + { + } + + public IEnumerable GetReferences(object value) + { + if (value == null) yield break; + + var asString = value is string str ? str : value.ToString(); + + yield return new UmbracoEntityReference(Udi.Parse(asString)); + } + } } } From c8b189d53deb9d17061b3cb2ca84a4a1097f2a30 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Mon, 28 Oct 2019 13:58:23 +0100 Subject: [PATCH 031/202] AB3326 - Made MediaPickerPropertyValueEditor nested and internal and handle null values --- .../PropertyEditors/GridPropertyEditor.cs | 4 ++-- .../MediaPickerPropertyEditor.cs | 22 +++++++++++-------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs index e30e661ab1..0a6d1e2adf 100644 --- a/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs @@ -60,7 +60,7 @@ namespace Umbraco.Web.PropertyEditors private readonly HtmlImageSourceParser _imageSourceParser; private readonly RichTextEditorPastedImages _pastedImages; private readonly RichTextPropertyEditor.RichTextPropertyValueEditor _richTextPropertyValueEditor; - private readonly MediaPickerPropertyValueEditor _mediaPickerPropertyValueEditor; + private readonly MediaPickerPropertyEditor.MediaPickerPropertyValueEditor _mediaPickerPropertyValueEditor; public GridPropertyValueEditor(DataEditorAttribute attribute, IUmbracoContextAccessor umbracoContextAccessor, @@ -73,7 +73,7 @@ namespace Umbraco.Web.PropertyEditors _imageSourceParser = imageSourceParser; _pastedImages = pastedImages; _richTextPropertyValueEditor = new RichTextPropertyEditor.RichTextPropertyValueEditor(attribute, umbracoContextAccessor, imageSourceParser, localLinkParser, pastedImages); - _mediaPickerPropertyValueEditor = new MediaPickerPropertyValueEditor(attribute); + _mediaPickerPropertyValueEditor = new MediaPickerPropertyEditor.MediaPickerPropertyValueEditor(attribute); } /// diff --git a/src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs index 0d0f1a8498..ffa3b8c074 100644 --- a/src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs @@ -32,19 +32,23 @@ namespace Umbraco.Web.PropertyEditors protected override IConfigurationEditor CreateConfigurationEditor() => new MediaPickerConfigurationEditor(); protected override IDataValueEditor CreateValueEditor() => new MediaPickerPropertyValueEditor(Attribute); - } - public class MediaPickerPropertyValueEditor : DataValueEditor, IDataValueReference - { - public MediaPickerPropertyValueEditor(DataEditorAttribute attribute) : base(attribute) + internal class MediaPickerPropertyValueEditor : DataValueEditor, IDataValueReference { - } + public MediaPickerPropertyValueEditor(DataEditorAttribute attribute) : base(attribute) + { + } - public IEnumerable GetReferences(object value) - { - var asString = value == null ? string.Empty : value is string str ? str : value.ToString(); + public IEnumerable GetReferences(object value) + { + if (value == null) yield break; - yield return new UmbracoEntityReference(Udi.Parse(asString)); + var asString = value is string str ? str : value.ToString(); + + yield return new UmbracoEntityReference(Udi.Parse(asString)); + } } } + + } From 0aaee0babab46caabb6fa3e1c545039287b5fe4b Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Mon, 28 Oct 2019 14:11:59 +0100 Subject: [PATCH 032/202] AB3328 - Multi Url Picker references --- .../PropertyEditors/MultiUrlPickerValueEditor.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web/PropertyEditors/MultiUrlPickerValueEditor.cs b/src/Umbraco.Web/PropertyEditors/MultiUrlPickerValueEditor.cs index aa8fa73c7a..1f71f39d96 100644 --- a/src/Umbraco.Web/PropertyEditors/MultiUrlPickerValueEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MultiUrlPickerValueEditor.cs @@ -15,7 +15,7 @@ using Umbraco.Web.PublishedCache; namespace Umbraco.Web.PropertyEditors { - public class MultiUrlPickerValueEditor : DataValueEditor + public class MultiUrlPickerValueEditor : DataValueEditor, IDataValueReference { private readonly IEntityService _entityService; private readonly ILogger _logger; @@ -174,5 +174,17 @@ namespace Umbraco.Web.PropertyEditors [DataMember(Name = "queryString")] public string QueryString { get; set; } } + + public IEnumerable GetReferences(object value) + { + var asString = value == null ? string.Empty : value is string str ? str : value.ToString(); + + var links = JsonConvert.DeserializeObject>(asString); + foreach (var link in links) + { + yield return new UmbracoEntityReference(link.Udi); + } + + } } } From d69356cc1293239709172b892b1541e47e1e0636 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 29 Oct 2019 13:29:22 +1100 Subject: [PATCH 033/202] Adds unit test and couple methods to relation service and improves the relation service lookups for entities. --- src/Umbraco.Core/Services/IRelationService.cs | 16 + .../Services/Implement/RelationService.cs | 370 +++++------------- .../Services/RelationServiceTests.cs | 33 ++ 3 files changed, 155 insertions(+), 264 deletions(-) diff --git a/src/Umbraco.Core/Services/IRelationService.cs b/src/Umbraco.Core/Services/IRelationService.cs index 0f339688de..49fa21af2b 100644 --- a/src/Umbraco.Core/Services/IRelationService.cs +++ b/src/Umbraco.Core/Services/IRelationService.cs @@ -70,6 +70,14 @@ namespace Umbraco.Core.Services /// An enumerable list of objects IEnumerable GetByParentId(int id); + /// + /// Gets a list of objects by their parent Id + /// + /// Id of the parent to retrieve relations for + /// Alias of the type of relation to retrieve + /// An enumerable list of objects + IEnumerable GetByParentId(int id, string relationTypeAlias); + /// /// Gets a list of objects by their parent entity /// @@ -92,6 +100,14 @@ namespace Umbraco.Core.Services /// An enumerable list of objects IEnumerable GetByChildId(int id); + /// + /// Gets a list of objects by their child Id + /// + /// Id of the child to retrieve relations for + /// Alias of the type of relation to retrieve + /// An enumerable list of objects + IEnumerable GetByChildId(int id, string relationTypeAlias); + /// /// Gets a list of objects by their child Entity /// diff --git a/src/Umbraco.Core/Services/Implement/RelationService.cs b/src/Umbraco.Core/Services/Implement/RelationService.cs index bf1e7bf309..490f36e7c7 100644 --- a/src/Umbraco.Core/Services/Implement/RelationService.cs +++ b/src/Umbraco.Core/Services/Implement/RelationService.cs @@ -25,11 +25,7 @@ namespace Umbraco.Core.Services.Implement _entityService = entityService ?? throw new ArgumentNullException(nameof(entityService)); } - /// - /// Gets a by its Id - /// - /// Id of the - /// A object + /// public IRelation GetById(int id) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) @@ -38,11 +34,7 @@ namespace Umbraco.Core.Services.Implement } } - /// - /// Gets a by its Id - /// - /// Id of the - /// A object + /// public IRelationType GetRelationTypeById(int id) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) @@ -51,11 +43,7 @@ namespace Umbraco.Core.Services.Implement } } - /// - /// Gets a by its Id - /// - /// Id of the - /// A object + /// public IRelationType GetRelationTypeById(Guid id) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) @@ -64,25 +52,10 @@ namespace Umbraco.Core.Services.Implement } } - /// - /// Gets a by its Alias - /// - /// Alias of the - /// A object - public IRelationType GetRelationTypeByAlias(string alias) - { - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - var query = Query().Where(x => x.Alias == alias); - return _relationTypeRepository.Get(query).FirstOrDefault(); - } - } + /// + public IRelationType GetRelationTypeByAlias(string alias) => GetRelationType(alias); - /// - /// Gets all objects - /// - /// Optional array of integer ids to return relations for - /// An enumerable list of objects + /// public IEnumerable GetAllRelations(params int[] ids) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) @@ -91,21 +64,13 @@ namespace Umbraco.Core.Services.Implement } } - /// - /// Gets all objects by their - /// - /// to retrieve Relations for - /// An enumerable list of objects + /// public IEnumerable GetAllRelationsByRelationType(IRelationType relationType) { return GetAllRelationsByRelationType(relationType.Id); } - /// - /// Gets all objects by their 's Id - /// - /// Id of the to retrieve Relations for - /// An enumerable list of objects + /// public IEnumerable GetAllRelationsByRelationType(int relationTypeId) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) @@ -115,11 +80,7 @@ namespace Umbraco.Core.Services.Implement } } - /// - /// Gets all objects - /// - /// Optional array of integer ids to return relationtypes for - /// An enumerable list of objects + /// public IEnumerable GetAllRelationTypes(params int[] ids) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) @@ -128,82 +89,65 @@ namespace Umbraco.Core.Services.Implement } } - /// - /// Gets a list of objects by their parent Id - /// - /// Id of the parent to retrieve relations for - /// An enumerable list of objects - public IEnumerable GetByParentId(int id) + /// + public IEnumerable GetByParentId(int id) => GetByParentId(id, null); + + /// + public IEnumerable GetByParentId(int id, string relationTypeAlias) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { - var query = Query().Where(x => x.ParentId == id); - return _relationRepository.Get(query); + if (relationTypeAlias.IsNullOrWhiteSpace()) + { + var qry1 = Query().Where(x => x.ParentId == id); + return _relationRepository.Get(qry1); + } + + var relationType = GetRelationType(relationTypeAlias); + if (relationType == null) + return Enumerable.Empty(); + + var qry2 = Query().Where(x => x.ParentId == id && x.RelationTypeId == relationType.Id); + return _relationRepository.Get(qry2); } } - /// - /// Gets a list of objects by their parent entity - /// - /// Parent Entity to retrieve relations for - /// An enumerable list of objects - public IEnumerable GetByParent(IUmbracoEntity parent) - { - return GetByParentId(parent.Id); - } + /// + public IEnumerable GetByParent(IUmbracoEntity parent) => GetByParentId(parent.Id); - /// - /// Gets a list of objects by their parent entity - /// - /// Parent Entity to retrieve relations for - /// Alias of the type of relation to retrieve - /// An enumerable list of objects - public IEnumerable GetByParent(IUmbracoEntity parent, string relationTypeAlias) - { - return GetByParent(parent).Where(relation => relation.RelationType.Alias == relationTypeAlias); - } + /// + public IEnumerable GetByParent(IUmbracoEntity parent, string relationTypeAlias) => GetByParentId(parent.Id, relationTypeAlias); - /// - /// Gets a list of objects by their child Id - /// - /// Id of the child to retrieve relations for - /// An enumerable list of objects - public IEnumerable GetByChildId(int id) + /// + public IEnumerable GetByChildId(int id) => GetByChildId(id, null); + + /// + public IEnumerable GetByChildId(int id, string relationTypeAlias) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { - var query = Query().Where(x => x.ChildId == id); - return _relationRepository.Get(query); + if (relationTypeAlias.IsNullOrWhiteSpace()) + { + var qry1 = Query().Where(x => x.ChildId == id); + return _relationRepository.Get(qry1); + } + + var relationType = GetRelationType(relationTypeAlias); + if (relationType == null) + return Enumerable.Empty(); + + var qry2 = Query().Where(x => x.ChildId == id && x.RelationTypeId == relationType.Id); + return _relationRepository.Get(qry2); } } - /// - /// Gets a list of objects by their child Entity - /// - /// Child Entity to retrieve relations for - /// An enumerable list of objects - public IEnumerable GetByChild(IUmbracoEntity child) - { - return GetByChildId(child.Id); - } + /// + public IEnumerable GetByChild(IUmbracoEntity child) => GetByChildId(child.Id); - /// - /// Gets a list of objects by their child Entity - /// - /// Child Entity to retrieve relations for - /// Alias of the type of relation to retrieve - /// An enumerable list of objects - public IEnumerable GetByChild(IUmbracoEntity child, string relationTypeAlias) - { - return GetByChild(child).Where(relation => relation.RelationType.Alias == relationTypeAlias); - } + /// + public IEnumerable GetByChild(IUmbracoEntity child, string relationTypeAlias) => GetByChildId(child.Id, relationTypeAlias); - /// - /// Gets a list of objects by their child or parent Id. - /// Using this method will get you all relations regards of it being a child or parent relation. - /// - /// Id of the child or parent to retrieve relations for - /// An enumerable list of objects + /// public IEnumerable GetByParentOrChildId(int id) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) @@ -217,8 +161,7 @@ namespace Umbraco.Core.Services.Implement { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { - var rtQuery = Query().Where(x => x.Alias == relationTypeAlias); - var relationType = _relationTypeRepository.Get(rtQuery).FirstOrDefault(); + var relationType = GetRelationType(relationTypeAlias); if (relationType == null) return Enumerable.Empty(); @@ -227,16 +170,13 @@ namespace Umbraco.Core.Services.Implement } } - /// - /// Gets a list of objects by the Name of the - /// - /// Name of the to retrieve Relations for - /// An enumerable list of objects + /// public IEnumerable GetByRelationTypeName(string relationTypeName) { List relationTypeIds; using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { + //This is a silly query - but i guess it's needed in case someone has more than one relation with the same Name (not alias), odd. var query = Query().Where(x => x.Name == relationTypeName); var relationTypes = _relationTypeRepository.Get(query); relationTypeIds = relationTypes.Select(x => x.Id).ToList(); @@ -247,31 +187,17 @@ namespace Umbraco.Core.Services.Implement : GetRelationsByListOfTypeIds(relationTypeIds); } - /// - /// Gets a list of objects by the Alias of the - /// - /// Alias of the to retrieve Relations for - /// An enumerable list of objects + /// public IEnumerable GetByRelationTypeAlias(string relationTypeAlias) { - List relationTypeIds; - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) - { - var query = Query().Where(x => x.Alias == relationTypeAlias); - var relationTypes = _relationTypeRepository.Get(query); - relationTypeIds = relationTypes.Select(x => x.Id).ToList(); - } - - return relationTypeIds.Count == 0 + var relationType = GetRelationType(relationTypeAlias); + + return relationType == null ? Enumerable.Empty() - : GetRelationsByListOfTypeIds(relationTypeIds); + : GetRelationsByListOfTypeIds(new[] { relationType.Id }); } - /// - /// Gets a list of objects by the Id of the - /// - /// Id of the to retrieve Relations for - /// An enumerable list of objects + /// public IEnumerable GetByRelationTypeId(int relationTypeId) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) @@ -281,33 +207,21 @@ namespace Umbraco.Core.Services.Implement } } - /// - /// Gets the Child object from a Relation as an - /// - /// Relation to retrieve child object from - /// An + /// public IUmbracoEntity GetChildEntityFromRelation(IRelation relation) { var objectType = ObjectTypes.GetUmbracoObjectType(relation.ChildObjectType); return _entityService.Get(relation.ChildId, objectType); } - /// - /// Gets the Parent object from a Relation as an - /// - /// Relation to retrieve parent object from - /// An + /// public IUmbracoEntity GetParentEntityFromRelation(IRelation relation) { var objectType = ObjectTypes.GetUmbracoObjectType(relation.ParentObjectType); return _entityService.Get(relation.ParentId, objectType); } - /// - /// Gets the Parent and Child objects from a Relation as a "/> with . - /// - /// Relation to retrieve parent and child object from - /// Returns a Tuple with Parent (item1) and Child (item2) + /// public Tuple GetEntitiesFromRelation(IRelation relation) { var childObjectType = ObjectTypes.GetUmbracoObjectType(relation.ChildObjectType); @@ -319,43 +233,37 @@ namespace Umbraco.Core.Services.Implement return new Tuple(parent, child); } - /// - /// Gets the Child objects from a list of Relations as a list of objects. - /// - /// List of relations to retrieve child objects from - /// An enumerable list of + /// public IEnumerable GetChildEntitiesFromRelations(IEnumerable relations) { - //TODO: Argh! N+1 + // Trying to avoid full N+1 lookups, so we'll group by the object type and then use the GetAll + // method to lookup batches of entities for each parent object type - foreach (var relation in relations) + foreach (var groupedRelations in relations.GroupBy(x => ObjectTypes.GetUmbracoObjectType(x.ChildObjectType))) { - var objectType = ObjectTypes.GetUmbracoObjectType(relation.ChildObjectType); - yield return _entityService.Get(relation.ChildId, objectType); + var objectType = groupedRelations.Key; + var ids = groupedRelations.Select(x => x.ChildId).ToArray(); + foreach (var e in _entityService.GetAll(objectType, ids)) + yield return e; } } - /// - /// Gets the Parent objects from a list of Relations as a list of objects. - /// - /// List of relations to retrieve parent objects from - /// An enumerable list of + /// public IEnumerable GetParentEntitiesFromRelations(IEnumerable relations) { - //TODO: Argh! N+1 + // Trying to avoid full N+1 lookups, so we'll group by the object type and then use the GetAll + // method to lookup batches of entities for each parent object type - foreach (var relation in relations) + foreach (var groupedRelations in relations.GroupBy(x => ObjectTypes.GetUmbracoObjectType(x.ParentObjectType))) { - var objectType = ObjectTypes.GetUmbracoObjectType(relation.ParentObjectType); - yield return _entityService.Get(relation.ParentId, objectType); + var objectType = groupedRelations.Key; + var ids = groupedRelations.Select(x => x.ParentId).ToArray(); + foreach (var e in _entityService.GetAll(objectType, ids)) + yield return e; } } - /// - /// Gets the Parent and Child objects from a list of Relations as a list of objects. - /// - /// List of relations to retrieve parent and child objects from - /// An enumerable list of with + /// public IEnumerable> GetEntitiesFromRelations(IEnumerable relations) { //TODO: Argh! N+1 @@ -372,13 +280,7 @@ namespace Umbraco.Core.Services.Implement } } - /// - /// Relates two objects by their entity Ids. - /// - /// Id of the parent - /// Id of the child - /// The type of relation to create - /// The created + /// public IRelation Relate(int parentId, int childId, IRelationType relationType) { // Ensure that the RelationType has an identity before using it to relate two entities @@ -406,25 +308,13 @@ namespace Umbraco.Core.Services.Implement } } - /// - /// Relates two objects that are based on the interface. - /// - /// Parent entity - /// Child entity - /// The type of relation to create - /// The created + /// public IRelation Relate(IUmbracoEntity parent, IUmbracoEntity child, IRelationType relationType) { return Relate(parent.Id, child.Id, relationType); } - /// - /// Relates two objects by their entity Ids. - /// - /// Id of the parent - /// Id of the child - /// Alias of the type of relation to create - /// The created + /// public IRelation Relate(int parentId, int childId, string relationTypeAlias) { var relationType = GetRelationTypeByAlias(relationTypeAlias); @@ -434,13 +324,7 @@ namespace Umbraco.Core.Services.Implement return Relate(parentId, childId, relationType); } - /// - /// Relates two objects that are based on the interface. - /// - /// Parent entity - /// Child entity - /// Alias of the type of relation to create - /// The created + /// public IRelation Relate(IUmbracoEntity parent, IUmbracoEntity child, string relationTypeAlias) { var relationType = GetRelationTypeByAlias(relationTypeAlias); @@ -450,11 +334,7 @@ namespace Umbraco.Core.Services.Implement return Relate(parent.Id, child.Id, relationType); } - /// - /// Checks whether any relations exists for the passed in . - /// - /// to check for relations - /// Returns True if any relations exists for the given , otherwise False + /// public bool HasRelations(IRelationType relationType) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) @@ -464,11 +344,7 @@ namespace Umbraco.Core.Services.Implement } } - /// - /// Checks whether any relations exists for the passed in Id. - /// - /// Id of an object to check relations for - /// Returns True if any relations exists with the given Id, otherwise False + /// public bool IsRelated(int id) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) @@ -478,12 +354,7 @@ namespace Umbraco.Core.Services.Implement } } - /// - /// Checks whether two items are related - /// - /// Id of the Parent relation - /// Id of the Child relation - /// Returns True if any relations exists with the given Ids, otherwise False + /// public bool AreRelated(int parentId, int childId) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) @@ -493,13 +364,7 @@ namespace Umbraco.Core.Services.Implement } } - /// - /// Checks whether two items are related with a given relation type alias - /// - /// Id of the Parent relation - /// Id of the Child relation - /// Alias of the relation type - /// Returns True if any relations exists with the given Ids and relation type, otherwise False + /// public bool AreRelated(int parentId, int childId, string relationTypeAlias) { var relType = GetRelationTypeByAlias(relationTypeAlias); @@ -510,13 +375,7 @@ namespace Umbraco.Core.Services.Implement } - /// - /// Checks whether two items are related with a given relation type - /// - /// Id of the Parent relation - /// Id of the Child relation - /// Type of relation - /// Returns True if any relations exists with the given Ids and relation type, otherwise False + /// public bool AreRelated(int parentId, int childId, IRelationType relationType) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) @@ -526,34 +385,20 @@ namespace Umbraco.Core.Services.Implement } } - /// - /// Checks whether two items are related - /// - /// Parent entity - /// Child entity - /// Returns True if any relations exist between the entities, otherwise False + /// public bool AreRelated(IUmbracoEntity parent, IUmbracoEntity child) { return AreRelated(parent.Id, child.Id); } - /// - /// Checks whether two items are related - /// - /// Parent entity - /// Child entity - /// Alias of the type of relation to create - /// Returns True if any relations exist between the entities, otherwise False + /// public bool AreRelated(IUmbracoEntity parent, IUmbracoEntity child, string relationTypeAlias) { return AreRelated(parent.Id, child.Id, relationTypeAlias); } - /// - /// Saves a - /// - /// Relation to save + /// public void Save(IRelation relation) { using (var scope = ScopeProvider.CreateScope()) @@ -572,10 +417,7 @@ namespace Umbraco.Core.Services.Implement } } - /// - /// Saves a - /// - /// RelationType to Save + /// public void Save(IRelationType relationType) { using (var scope = ScopeProvider.CreateScope()) @@ -594,10 +436,7 @@ namespace Umbraco.Core.Services.Implement } } - /// - /// Deletes a - /// - /// Relation to Delete + /// public void Delete(IRelation relation) { using (var scope = ScopeProvider.CreateScope()) @@ -616,10 +455,7 @@ namespace Umbraco.Core.Services.Implement } } - /// - /// Deletes a - /// - /// RelationType to Delete + /// public void Delete(IRelationType relationType) { using (var scope = ScopeProvider.CreateScope()) @@ -638,10 +474,7 @@ namespace Umbraco.Core.Services.Implement } } - /// - /// Deletes all objects based on the passed in - /// - /// to Delete Relations for + /// public void DeleteRelationsOfType(IRelationType relationType) { var relations = new List(); @@ -663,6 +496,15 @@ namespace Umbraco.Core.Services.Implement #region Private Methods + private IRelationType GetRelationType(string relationTypeAlias) + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + var query = Query().Where(x => x.Alias == relationTypeAlias); + return _relationTypeRepository.Get(query).FirstOrDefault(); + } + } + private IEnumerable GetRelationsByListOfTypeIds(IEnumerable relationTypeIds) { var relations = new List(); diff --git a/src/Umbraco.Tests/Services/RelationServiceTests.cs b/src/Umbraco.Tests/Services/RelationServiceTests.cs index a39a9c838b..0357c9e408 100644 --- a/src/Umbraco.Tests/Services/RelationServiceTests.cs +++ b/src/Umbraco.Tests/Services/RelationServiceTests.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Threading; using NUnit.Framework; using Umbraco.Core; @@ -14,6 +15,38 @@ namespace Umbraco.Tests.Services [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] public class RelationServiceTests : TestWithSomeContentBase { + [Test] + public void Return_List_Of_Content_Items_Where_Media_Item_Referenced() + { + var mt = MockedContentTypes.CreateSimpleMediaType("testMediaType", "Test Media Type"); + ServiceContext.MediaTypeService.Save(mt); + var m1 = MockedMedia.CreateSimpleMedia(mt, "hello 1", -1); + ServiceContext.MediaService.Save(m1); + + var ct = MockedContentTypes.CreateTextPageContentType("richTextTest"); + ct.AllowedTemplates = Enumerable.Empty(); + ServiceContext.ContentTypeService.Save(ct); + + void createContentWithMediaRefs() + { + var content = MockedContent.CreateTextpageContent(ct, "my content 2", -1); + //'bodyText' is a property with a RTE property editor which we knows automatically tracks relations + content.Properties["bodyText"].SetValue(@"

+ +

"); + ServiceContext.ContentService.Save(content); + } + + for (var i = 0; i < 6; i++) + createContentWithMediaRefs(); //create 6 content items referencing the same media + + var relations = ServiceContext.RelationService.GetByChildId(m1.Id, Constants.Conventions.RelationTypes.RelatedMediaAlias).ToList(); + Assert.AreEqual(6, relations.Count); + + var entities = ServiceContext.RelationService.GetParentEntitiesFromRelations(relations).ToList(); + Assert.AreEqual(6, entities.Count); + } + [Test] public void Can_Create_RelationType_Without_Name() { From 5ddc961df3cb9a9327c6d4c503c4542ab2637f4f Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Tue, 29 Oct 2019 08:52:02 +0100 Subject: [PATCH 034/202] AB3327 - Nested Content references --- .../NestedContentPropertyEditor.cs | 41 ++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs index 7e91a3af79..ab26290812 100644 --- a/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs @@ -58,7 +58,7 @@ namespace Umbraco.Web.PropertyEditors protected override IDataValueEditor CreateValueEditor() => new NestedContentPropertyValueEditor(Attribute, PropertyEditors); - internal class NestedContentPropertyValueEditor : DataValueEditor + internal class NestedContentPropertyValueEditor : DataValueEditor, IDataValueReference { private readonly PropertyEditorCollection _propertyEditors; @@ -266,6 +266,45 @@ namespace Umbraco.Web.PropertyEditors return JsonConvert.SerializeObject(value); } + public IEnumerable GetReferences(object value) + { + var rawJson = value == null ? string.Empty : value is string str ? str : value.ToString(); + + var result = new List(); + + var list = JsonConvert.DeserializeObject>(rawJson); + if (list == null) + return result; + + foreach (var o in list) + { + var propValues = (JObject) o; + + var contentType = GetElementType(propValues); + if (contentType == null) + continue; + + var propAliases = propValues.Properties().Select(x => x.Name).ToArray(); + foreach (var propAlias in propAliases) + { + var propType = contentType.CompositionPropertyTypes.FirstOrDefault(x => x.Alias == propAlias); + if (propType == null) continue; + + var propEditor = _propertyEditors[propType.PropertyEditorAlias]; + + var valueEditor = propEditor?.GetValueEditor(); + if (!(valueEditor is IDataValueReference reference)) continue; + + var val = propValues[propAlias]?.ToString(); + + var refs = reference.GetReferences(val); + + result.AddRange(refs); + } + } + return result; + } + #endregion } From 8d4de3de55756f8ff2c07c4980beb664009783ee Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Tue, 29 Oct 2019 08:52:47 +0100 Subject: [PATCH 035/202] AB3326 - Handle empty strings from nested content --- src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs index ffa3b8c074..be1a9b2691 100644 --- a/src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs @@ -41,9 +41,9 @@ namespace Umbraco.Web.PropertyEditors public IEnumerable GetReferences(object value) { - if (value == null) yield break; + var asString = value is string str ? str : value?.ToString(); - var asString = value is string str ? str : value.ToString(); + if (string.IsNullOrEmpty(asString)) yield break; yield return new UmbracoEntityReference(Udi.Parse(asString)); } From 34a0210d9da7a38bcfd7eaf93ba197056740dc21 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Tue, 29 Oct 2019 08:53:00 +0100 Subject: [PATCH 036/202] AB3329 - Handle empty strings from nested content --- .../PropertyEditors/ContentPickerPropertyEditor.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web/PropertyEditors/ContentPickerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/ContentPickerPropertyEditor.cs index 0e6eaa9254..c09b3b6930 100644 --- a/src/Umbraco.Web/PropertyEditors/ContentPickerPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/ContentPickerPropertyEditor.cs @@ -37,9 +37,9 @@ namespace Umbraco.Web.PropertyEditors public IEnumerable GetReferences(object value) { - if (value == null) yield break; + var asString = value is string str ? str : value?.ToString(); - var asString = value is string str ? str : value.ToString(); + if (string.IsNullOrEmpty(asString)) yield break; yield return new UmbracoEntityReference(Udi.Parse(asString)); } From da698a9a853eebc109bbbb07f17b4aa3fce4e2c4 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Tue, 29 Oct 2019 09:57:32 +0100 Subject: [PATCH 037/202] AB2462 - Code review fixes --- .../Migrations/Install/DatabaseDataCreator.cs | 15 ++++++++++----- .../Upgrade/V_8_5_0/AddNewRelationTypes.cs | 6 ++++-- src/Umbraco.Core/Models/Relation.cs | 4 +--- .../Repositories/Implement/RelationRepository.cs | 12 ++++++++++-- src/Umbraco.Core/PropertyEditors/DataEditor.cs | 4 ++-- 5 files changed, 27 insertions(+), 14 deletions(-) diff --git a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs index 9a96810f23..888ff9a632 100644 --- a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs +++ b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs @@ -310,24 +310,29 @@ namespace Umbraco.Core.Migrations.Install private void CreateRelationTypeData() { var relationType = new RelationTypeDto { Id = 1, Alias = Constants.Conventions.RelationTypes.RelateDocumentOnCopyAlias, ChildObjectType = Constants.ObjectTypes.Document, ParentObjectType = Constants.ObjectTypes.Document, Dual = true, Name = Constants.Conventions.RelationTypes.RelateDocumentOnCopyName }; - relationType.UniqueId = (relationType.Alias + "____" + relationType.Name).ToGuid(); + relationType.UniqueId = CreateUniqueRelationTypeId(relationType.Alias, relationType.Name); _database.Insert(Constants.DatabaseSchema.Tables.RelationType, "id", false, relationType); relationType = new RelationTypeDto { Id = 2, Alias = Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteAlias, ChildObjectType = Constants.ObjectTypes.Document, ParentObjectType = Constants.ObjectTypes.Document, Dual = false, Name = Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteName }; - relationType.UniqueId = (relationType.Alias + "____" + relationType.Name).ToGuid(); + relationType.UniqueId = CreateUniqueRelationTypeId(relationType.Alias, relationType.Name); _database.Insert(Constants.DatabaseSchema.Tables.RelationType, "id", false, relationType); relationType = new RelationTypeDto { Id = 3, Alias = Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteAlias, ChildObjectType = Constants.ObjectTypes.Media, ParentObjectType = Constants.ObjectTypes.Media, Dual = false, Name = Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteName }; - relationType.UniqueId = (relationType.Alias + "____" + relationType.Name).ToGuid(); + relationType.UniqueId = CreateUniqueRelationTypeId(relationType.Alias, relationType.Name); _database.Insert(Constants.DatabaseSchema.Tables.RelationType, "id", false, relationType); relationType = new RelationTypeDto { Id = 4, Alias = Constants.Conventions.RelationTypes.RelatedMediaAlias, ChildObjectType = null, ParentObjectType = null, Dual = false, Name = Constants.Conventions.RelationTypes.RelatedMediaName }; - relationType.UniqueId = (relationType.Alias + "____" + relationType.Name).ToGuid(); + relationType.UniqueId = CreateUniqueRelationTypeId(relationType.Alias, relationType.Name); _database.Insert(Constants.DatabaseSchema.Tables.RelationType, "id", false, relationType); relationType = new RelationTypeDto { Id = 5, Alias = Constants.Conventions.RelationTypes.RelatedDocumentAlias, ChildObjectType = null, ParentObjectType = null, Dual = false, Name = Constants.Conventions.RelationTypes.RelatedDocumentName }; - relationType.UniqueId = (relationType.Alias + "____" + relationType.Name).ToGuid(); + relationType.UniqueId = CreateUniqueRelationTypeId(relationType.Alias, relationType.Name); _database.Insert(Constants.DatabaseSchema.Tables.RelationType, "id", false, relationType); } + internal static Guid CreateUniqueRelationTypeId(string alias, string name) + { + return (alias + "____" + name).ToGuid(); + } + private void CreateKeyValueData() { // on install, initialize the umbraco migration plan with the final state diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_5_0/AddNewRelationTypes.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_5_0/AddNewRelationTypes.cs index 88c6c43c46..40e541f04c 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_5_0/AddNewRelationTypes.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_5_0/AddNewRelationTypes.cs @@ -1,4 +1,6 @@ -namespace Umbraco.Core.Migrations.Upgrade.V_8_5_0 +using Umbraco.Core.Migrations.Install; + +namespace Umbraco.Core.Migrations.Upgrade.V_8_5_0 { /// /// Ensures the new relation types are created @@ -22,7 +24,7 @@ private void CreateRelation(string alias, string name) { - var uniqueId = (alias + "____" + name).ToGuid(); //this is the same as how it installs so everything is consistent + var uniqueId = DatabaseDataCreator.CreateUniqueRelationTypeId(alias ,name); //this is the same as how it installs so everything is consistent Insert.IntoTable(Constants.DatabaseSchema.Tables.RelationType) .Row(new { typeUniqueId = uniqueId, dual = 0, name, alias }) .Do(); diff --git a/src/Umbraco.Core/Models/Relation.cs b/src/Umbraco.Core/Models/Relation.cs index 8e3a073a96..7afa476226 100644 --- a/src/Umbraco.Core/Models/Relation.cs +++ b/src/Umbraco.Core/Models/Relation.cs @@ -22,8 +22,6 @@ namespace Umbraco.Core.Models /// /// /// - /// - /// /// public Relation(int parentId, int childId, IRelationType relationType) { @@ -48,7 +46,7 @@ namespace Umbraco.Core.Models ParentObjectType = parentObjectType; ChildObjectType = childObjectType; } - + /// /// Gets or sets the Parent Id of the Relation (Source) diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/RelationRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/RelationRepository.cs index ab6b02afb4..e6c4c6fd7e 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/RelationRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/RelationRepository.cs @@ -175,8 +175,16 @@ namespace Umbraco.Core.Persistence.Repositories.Implement { var nodes = Database.Fetch(Sql().Select().From().Where(x => x.NodeId == entity.ChildId || x.NodeId == entity.ParentId)) .ToDictionary(x => x.NodeId, x => x.NodeObjectType); - entity.ParentObjectType = nodes[entity.ParentId].Value; - entity.ChildObjectType = nodes[entity.ChildId].Value; + + if(nodes.TryGetValue(entity.ParentId, out var parentObjectType)) + { + entity.ParentObjectType = parentObjectType.GetValueOrDefault(); + } + + if(nodes.TryGetValue(entity.ChildId, out var childObjectType)) + { + entity.ChildObjectType = childObjectType.GetValueOrDefault(); + } } } } diff --git a/src/Umbraco.Core/PropertyEditors/DataEditor.cs b/src/Umbraco.Core/PropertyEditors/DataEditor.cs index 3f5f6afa4f..7dc260e4c7 100644 --- a/src/Umbraco.Core/PropertyEditors/DataEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/DataEditor.cs @@ -19,7 +19,7 @@ namespace Umbraco.Core.PropertyEditors public class DataEditor : IDataEditor { private IDictionary _defaultConfiguration; - private IDataValueEditor _nonConfigured; + private IDataValueEditor _dataValueEditor; /// /// Initializes a new instance of the class. @@ -91,7 +91,7 @@ namespace Umbraco.Core.PropertyEditors /// simple enough for now. /// // TODO: point of that one? shouldn't we always configure? - public IDataValueEditor GetValueEditor() => ExplicitValueEditor ?? (_nonConfigured ?? (_nonConfigured = CreateValueEditor())); + public IDataValueEditor GetValueEditor() => ExplicitValueEditor ?? (_dataValueEditor ?? (_dataValueEditor = CreateValueEditor())); /// /// From 546a948d0b3632b925e6fb82917e332ad585b3a0 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Tue, 29 Oct 2019 12:13:42 +0100 Subject: [PATCH 038/202] Protect "system media types" from alias changes and deletion --- src/Umbraco.Core/Models/MediaTypeExtensions.cs | 10 ++++++++++ src/Umbraco.Core/Umbraco.Core.csproj | 1 + .../src/views/mediatypes/edit.html | 1 + .../Models/ContentEditing/MediaTypeDisplay.cs | 3 ++- .../Models/Mapping/ContentTypeMapDefinition.cs | 1 + src/Umbraco.Web/Trees/MediaTypeTreeController.cs | 5 ++++- 6 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 src/Umbraco.Core/Models/MediaTypeExtensions.cs diff --git a/src/Umbraco.Core/Models/MediaTypeExtensions.cs b/src/Umbraco.Core/Models/MediaTypeExtensions.cs new file mode 100644 index 0000000000..4e2ae5822a --- /dev/null +++ b/src/Umbraco.Core/Models/MediaTypeExtensions.cs @@ -0,0 +1,10 @@ +namespace Umbraco.Core.Models +{ + internal static class MediaTypeExtensions + { + internal static bool IsSystemMediaType(this IMediaType mediaType) => + mediaType.Alias == Constants.Conventions.MediaTypes.File + || mediaType.Alias == Constants.Conventions.MediaTypes.Folder + || mediaType.Alias == Constants.Conventions.MediaTypes.Image; + } +} diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 309dc97b81..c0fcfe87f8 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -268,6 +268,7 @@ + diff --git a/src/Umbraco.Web.UI.Client/src/views/mediatypes/edit.html b/src/Umbraco.Web.UI.Client/src/views/mediatypes/edit.html index 856089fa32..50975a6d60 100644 --- a/src/Umbraco.Web.UI.Client/src/views/mediatypes/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/mediatypes/edit.html @@ -9,6 +9,7 @@ { - + [DataMember(Name = "isSystemMediaType")] + public bool IsSystemMediaType { get; set; } } } diff --git a/src/Umbraco.Web/Models/Mapping/ContentTypeMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/ContentTypeMapDefinition.cs index 528d5f6de5..0e3af5d0da 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentTypeMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentTypeMapDefinition.cs @@ -145,6 +145,7 @@ namespace Umbraco.Web.Models.Mapping //default listview target.ListViewEditorName = Constants.Conventions.DataTypes.ListViewPrefix + "Media"; + target.IsSystemMediaType = source.IsSystemMediaType(); if (string.IsNullOrEmpty(source.Name)) return; diff --git a/src/Umbraco.Web/Trees/MediaTypeTreeController.cs b/src/Umbraco.Web/Trees/MediaTypeTreeController.cs index f85aefcace..3d0046c319 100644 --- a/src/Umbraco.Web/Trees/MediaTypeTreeController.cs +++ b/src/Umbraco.Web/Trees/MediaTypeTreeController.cs @@ -120,7 +120,10 @@ namespace Umbraco.Web.Trees } menu.Items.Add(Services.TextService, opensDialog: true); - menu.Items.Add(Services.TextService, opensDialog: true); + if(ct.IsSystemMediaType() == false) + { + menu.Items.Add(Services.TextService, opensDialog: true); + } menu.Items.Add(new RefreshNode(Services.TextService, true)); } From 4ee6fdd642628499272634e536470c569378a876 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Tue, 29 Oct 2019 20:39:38 +0100 Subject: [PATCH 039/202] Hide the content picker "max items" help text when it is configured in single picker mode --- .../src/views/propertyeditors/contentpicker/contentpicker.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.html index ab9b078433..ba22ca9d80 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.html @@ -31,7 +31,7 @@ ... -
+
From 12cb37057831908d4148b14306441753b02a51c0 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 30 Oct 2019 17:14:04 +1100 Subject: [PATCH 040/202] Reduces statics, reduces code duplication for iterating over properties in the NC prop editor --- .../Published/NestedContentTests.cs | 3 +- .../NestedContentPropertyEditor.cs | 370 ++++++++---------- 2 files changed, 170 insertions(+), 203 deletions(-) diff --git a/src/Umbraco.Tests/Published/NestedContentTests.cs b/src/Umbraco.Tests/Published/NestedContentTests.cs index 9385b8955a..2bba37c7d3 100644 --- a/src/Umbraco.Tests/Published/NestedContentTests.cs +++ b/src/Umbraco.Tests/Published/NestedContentTests.cs @@ -11,6 +11,7 @@ using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Services; using Umbraco.Tests.PublishedContent; using Umbraco.Tests.TestHelpers; using Umbraco.Web; @@ -33,7 +34,7 @@ namespace Umbraco.Tests.Published var proflog = new ProfilingLogger(logger, profiler); PropertyEditorCollection editors = null; - var editor = new NestedContentPropertyEditor(logger, new Lazy(() => editors)); + var editor = new NestedContentPropertyEditor(logger, new Lazy(() => editors), Mock.Of()); editors = new PropertyEditorCollection(new DataEditorCollection(new DataEditor[] { editor })); var dataType1 = new DataType(editor) diff --git a/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs index ab26290812..7104df0775 100644 --- a/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs @@ -28,13 +28,14 @@ namespace Umbraco.Web.PropertyEditors public class NestedContentPropertyEditor : DataEditor { private readonly Lazy _propertyEditors; - + private readonly IDataTypeService _dataTypeService; internal const string ContentTypeAliasPropertyKey = "ncContentTypeAlias"; - public NestedContentPropertyEditor(ILogger logger, Lazy propertyEditors) + public NestedContentPropertyEditor(ILogger logger, Lazy propertyEditors, IDataTypeService dataTypeService) : base (logger) { _propertyEditors = propertyEditors; + _dataTypeService = dataTypeService; } // has to be lazy else circular dep in ctor @@ -56,21 +57,21 @@ namespace Umbraco.Web.PropertyEditors #region Value Editor - protected override IDataValueEditor CreateValueEditor() => new NestedContentPropertyValueEditor(Attribute, PropertyEditors); + protected override IDataValueEditor CreateValueEditor() => new NestedContentPropertyValueEditor(Attribute, PropertyEditors, _dataTypeService); internal class NestedContentPropertyValueEditor : DataValueEditor, IDataValueReference { private readonly PropertyEditorCollection _propertyEditors; + private readonly IDataTypeService _dataTypeService; - public NestedContentPropertyValueEditor(DataEditorAttribute attribute, PropertyEditorCollection propertyEditors) + public NestedContentPropertyValueEditor(DataEditorAttribute attribute, PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService) : base(attribute) { _propertyEditors = propertyEditors; - Validators.Add(new NestedContentValidator(propertyEditors)); + _dataTypeService = dataTypeService; + Validators.Add(new NestedContentValidator(propertyEditors, dataTypeService)); } - internal ServiceContext Services => Current.Services; - /// public override object Configuration { @@ -87,60 +88,92 @@ namespace Umbraco.Web.PropertyEditors } } - #region DB to String - - public override string ConvertDbToString(PropertyType propertyType, object propertyValue, IDataTypeService dataTypeService) + /// + /// Method used to iterate over the deserialized property values + /// + /// + /// + internal static List IteratePropertyValues(object propertyValue, Action onIteration) { if (propertyValue == null || string.IsNullOrWhiteSpace(propertyValue.ToString())) - return string.Empty; + return null; - var value = JsonConvert.DeserializeObject>(propertyValue.ToString()); - if (value == null) - return string.Empty; + var value = JsonConvert.DeserializeObject>(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 (value == null || value.Count == 0) + return null; + + var index = 0; foreach (var o in value) { - var propValues = (JObject) o; + var propValues = o; + // TODO: This is N+1 (although we cache all doc types, it's still not pretty) var contentType = GetElementType(propValues); if (contentType == null) continue; - var propAliases = propValues.Properties().Select(x => x.Name).ToArray(); + var propertyTypes = contentType.CompositionPropertyTypes.ToDictionary(x => x.Alias, x => x); + var propAliases = propValues.Properties().Select(x => x.Name); foreach (var propAlias in propAliases) { - var propType = contentType.CompositionPropertyTypes.FirstOrDefault(x => x.Alias == propAlias); - if (propType == null) + propertyTypes.TryGetValue(propAlias, out var propType); + onIteration(propAlias, propType, propValues, index); + } + + index++; + } + + return value; + } + + #region DB to String + + public override string ConvertDbToString(PropertyType propertyType, object propertyValue, IDataTypeService dataTypeService) + { + var value = IteratePropertyValues(propertyValue, (string propAlias, PropertyType propType, JObject propValues, int index) => + { + if (propType == null) + { + // type not found, and property is not system: just delete the value + if (IsSystemPropertyKey(propAlias) == false) + propValues[propAlias] = null; + } + else + { + try { - // type not found, and property is not system: just delete the value - if (IsSystemPropertyKey(propAlias) == false) - propValues[propAlias] = null; + // convert the value, and store the converted value + var propEditor = _propertyEditors[propType.PropertyEditorAlias]; + var tempConfig = dataTypeService.GetDataType(propType.DataTypeId).Configuration; + var valEditor = propEditor.GetValueEditor(tempConfig); + var convValue = valEditor.ConvertDbToString(propType, propValues[propAlias]?.ToString(), dataTypeService); + propValues[propAlias] = convValue; } - else + catch (InvalidOperationException) { - try - { - // convert the value, and store the converted value - var propEditor = _propertyEditors[propType.PropertyEditorAlias]; - var tempConfig = dataTypeService.GetDataType(propType.DataTypeId).Configuration; - var valEditor = propEditor.GetValueEditor(tempConfig); - var convValue = valEditor.ConvertDbToString(propType, propValues[propAlias]?.ToString(), dataTypeService); - propValues[propAlias] = convValue; - } - catch (InvalidOperationException) - { - // deal with weird situations by ignoring them (no comment) - propValues[propAlias] = null; - } + // deal with weird situations by ignoring them (no comment) + propValues[propAlias] = null; } } - } + }); + + if (value == null) + return string.Empty; return JsonConvert.SerializeObject(value).ToXmlString(); } #endregion + + #region Convert database // editor // note: there is NO variant support here @@ -148,58 +181,43 @@ namespace Umbraco.Web.PropertyEditors public override object ToEditor(Property property, IDataTypeService dataTypeService, string culture = null, string segment = null) { var val = property.GetValue(culture, segment); - if (val == null || string.IsNullOrWhiteSpace(val.ToString())) - return string.Empty; - var value = JsonConvert.DeserializeObject>(val.ToString()); + var value = IteratePropertyValues(val, (string propAlias, PropertyType propType, JObject propValues, int index) => + { + if (propType == null) + { + // type not found, and property is not system: just delete the value + if (IsSystemPropertyKey(propAlias) == false) + propValues[propAlias] = null; + } + else + { + try + { + // create a temp property with the value + // - force it to be culture invariant as NC can't handle culture variant element properties + propType.Variations = ContentVariation.Nothing; + var tempProp = new Property(propType); + tempProp.SetValue(propValues[propAlias] == null ? null : propValues[propAlias].ToString()); + + // convert that temp property, and store the converted value + var propEditor = _propertyEditors[propType.PropertyEditorAlias]; + var tempConfig = dataTypeService.GetDataType(propType.DataTypeId).Configuration; + var valEditor = propEditor.GetValueEditor(tempConfig); + var convValue = valEditor.ToEditor(tempProp, dataTypeService); + propValues[propAlias] = convValue == null ? null : JToken.FromObject(convValue); + } + catch (InvalidOperationException) + { + // deal with weird situations by ignoring them (no comment) + propValues[propAlias] = null; + } + } + }); + if (value == null) return string.Empty; - foreach (var o in value) - { - var propValues = (JObject) o; - - var contentType = GetElementType(propValues); - if (contentType == null) - continue; - - var propAliases = propValues.Properties().Select(x => x.Name).ToArray(); - foreach (var propAlias in propAliases) - { - var propType = contentType.CompositionPropertyTypes.FirstOrDefault(x => x.Alias == propAlias); - if (propType == null) - { - // type not found, and property is not system: just delete the value - if (IsSystemPropertyKey(propAlias) == false) - propValues[propAlias] = null; - } - else - { - try - { - // create a temp property with the value - // - force it to be culture invariant as NC can't handle culture variant element properties - propType.Variations = ContentVariation.Nothing; - var tempProp = new Property(propType); - tempProp.SetValue(propValues[propAlias] == null ? null : propValues[propAlias].ToString()); - - // convert that temp property, and store the converted value - var propEditor = _propertyEditors[propType.PropertyEditorAlias]; - var tempConfig = dataTypeService.GetDataType(propType.DataTypeId).Configuration; - var valEditor = propEditor.GetValueEditor(tempConfig); - var convValue = valEditor.ToEditor(tempProp, dataTypeService); - propValues[propAlias] = convValue == null ? null : JToken.FromObject(convValue); - } - catch (InvalidOperationException) - { - // deal with weird situations by ignoring them (no comment) - propValues[propAlias] = null; - } - } - - } - } - // return json return value; } @@ -209,60 +227,37 @@ namespace Umbraco.Web.PropertyEditors if (editorValue.Value == null || string.IsNullOrWhiteSpace(editorValue.Value.ToString())) return null; - var value = JsonConvert.DeserializeObject>(editorValue.Value.ToString()); - if (value == null) - return null; - - // Issue #38 - Keep recursive property lookups working - if (!value.Any()) - return null; - - // Process value - for (var i = 0; i < value.Count; i++) + var value = IteratePropertyValues(editorValue.Value, (string propAlias, PropertyType propType, JObject propValues, int index) => { - var o = value[i]; - var propValues = ((JObject)o); - - var contentType = GetElementType(propValues); - if (contentType == null) + if (propType == null) { - continue; + // type not found, and property is not system: just delete the value + if (IsSystemPropertyKey(propAlias) == false) + propValues[propAlias] = null; } - - var propValueKeys = propValues.Properties().Select(x => x.Name).ToArray(); - - foreach (var propKey in propValueKeys) + else { - var propType = contentType.CompositionPropertyTypes.FirstOrDefault(x => x.Alias == propKey); - if (propType == null) - { - if (IsSystemPropertyKey(propKey) == false) - { - // Property missing so just delete the value - propValues[propKey] = null; - } - } - else - { - // Fetch the property types prevalue - var propConfiguration = Services.DataTypeService.GetDataType(propType.DataTypeId).Configuration; + // Fetch the property types prevalue + var propConfiguration = _dataTypeService.GetDataType(propType.DataTypeId).Configuration; - // Lookup the property editor - var propEditor = _propertyEditors[propType.PropertyEditorAlias]; + // Lookup the property editor + var propEditor = _propertyEditors[propType.PropertyEditorAlias]; - // Create a fake content property data object - var contentPropData = new ContentPropertyData(propValues[propKey], propConfiguration); + // Create a fake content property data object + var contentPropData = new ContentPropertyData(propValues[propAlias], propConfiguration); - // Get the property editor to do it's conversion - var newValue = propEditor.GetValueEditor().FromEditor(contentPropData, propValues[propKey]); - - // Store the value back - propValues[propKey] = (newValue == null) ? null : JToken.FromObject(newValue); - } + // Get the property editor to do it's conversion + var newValue = propEditor.GetValueEditor().FromEditor(contentPropData, propValues[propAlias]); + // Store the value back + propValues[propAlias] = (newValue == null) ? null : JToken.FromObject(newValue); } - } + }); + if (value == null) + return string.Empty; + + // return json return JsonConvert.SerializeObject(value); } @@ -272,36 +267,22 @@ namespace Umbraco.Web.PropertyEditors var result = new List(); - var list = JsonConvert.DeserializeObject>(rawJson); - if (list == null) - return result; - - foreach (var o in list) + var json = IteratePropertyValues(rawJson, (string propAlias, PropertyType propType, JObject propValues, int index) => { - var propValues = (JObject) o; + if (propType == null) return; - var contentType = GetElementType(propValues); - if (contentType == null) - continue; + var propEditor = _propertyEditors[propType.PropertyEditorAlias]; - var propAliases = propValues.Properties().Select(x => x.Name).ToArray(); - foreach (var propAlias in propAliases) - { - var propType = contentType.CompositionPropertyTypes.FirstOrDefault(x => x.Alias == propAlias); - if (propType == null) continue; + var valueEditor = propEditor?.GetValueEditor(); + if (!(valueEditor is IDataValueReference reference)) return; - var propEditor = _propertyEditors[propType.PropertyEditorAlias]; + var val = propValues[propAlias]?.ToString(); - var valueEditor = propEditor?.GetValueEditor(); - if (!(valueEditor is IDataValueReference reference)) continue; + var refs = reference.GetReferences(val); - var val = propValues[propAlias]?.ToString(); + result.AddRange(refs); + }); - var refs = reference.GetReferences(val); - - result.AddRange(refs); - } - } return result; } @@ -311,71 +292,56 @@ namespace Umbraco.Web.PropertyEditors internal class NestedContentValidator : IValueValidator { private readonly PropertyEditorCollection _propertyEditors; + private readonly IDataTypeService _dataTypeService; - public NestedContentValidator(PropertyEditorCollection propertyEditors) + public NestedContentValidator(PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService) { _propertyEditors = propertyEditors; + _dataTypeService = dataTypeService; } public IEnumerable Validate(object rawValue, string valueType, object dataTypeConfiguration) { - if (rawValue == null) - yield break; + var validationResults = new List(); - var value = JsonConvert.DeserializeObject>(rawValue.ToString()); - if (value == null) - yield break; - - var dataTypeService = Current.Services.DataTypeService; - for (var i = 0; i < value.Count; i++) + NestedContentPropertyValueEditor.IteratePropertyValues(rawValue, (string propKey, PropertyType propType, JObject propValues, int i) => { - var o = value[i]; - var propValues = (JObject) o; + if (propType == null) return; - var contentType = GetElementType(propValues); - if (contentType == null) continue; + var config = _dataTypeService.GetDataType(propType.DataTypeId).Configuration; + var propertyEditor = _propertyEditors[propType.PropertyEditorAlias]; - var propValueKeys = propValues.Properties().Select(x => x.Name).ToArray(); - - foreach (var propKey in propValueKeys) + foreach (var validator in propertyEditor.GetValueEditor().Validators) { - var propType = contentType.CompositionPropertyTypes.FirstOrDefault(x => x.Alias == propKey); - if (propType != null) + foreach (var result in validator.Validate(propValues[propKey], propertyEditor.GetValueEditor().ValueType, config)) { - var config = dataTypeService.GetDataType(propType.DataTypeId).Configuration; - var propertyEditor = _propertyEditors[propType.PropertyEditorAlias]; - - foreach (var validator in propertyEditor.GetValueEditor().Validators) - { - foreach (var result in validator.Validate(propValues[propKey], propertyEditor.GetValueEditor().ValueType, config)) - { - result.ErrorMessage = "Item " + (i + 1) + " '" + propType.Name + "' " + result.ErrorMessage; - yield return result; - } - } - - // Check mandatory - if (propType.Mandatory) - { - if (propValues[propKey] == null) - yield return new ValidationResult("Item " + (i + 1) + " '" + propType.Name + "' cannot be null", new[] { propKey }); - else if (propValues[propKey].ToString().IsNullOrWhiteSpace() || (propValues[propKey].Type == JTokenType.Array && !propValues[propKey].HasValues)) - yield return new ValidationResult("Item " + (i + 1) + " '" + propType.Name + "' cannot be empty", new[] { propKey }); - } - - // Check regex - if (!propType.ValidationRegExp.IsNullOrWhiteSpace() - && propValues[propKey] != null && !propValues[propKey].ToString().IsNullOrWhiteSpace()) - { - var regex = new Regex(propType.ValidationRegExp); - if (!regex.IsMatch(propValues[propKey].ToString())) - { - yield return new ValidationResult("Item " + (i + 1) + " '" + propType.Name + "' is invalid, it does not match the correct pattern", new[] { propKey }); - } - } + result.ErrorMessage = "Item " + (i + 1) + " '" + propType.Name + "' " + result.ErrorMessage; + validationResults.Add(result); } } - } + + // Check mandatory + if (propType.Mandatory) + { + if (propValues[propKey] == null) + validationResults.Add(new ValidationResult("Item " + (i + 1) + " '" + propType.Name + "' cannot be null", new[] { propKey })); + else if (propValues[propKey].ToString().IsNullOrWhiteSpace() || (propValues[propKey].Type == JTokenType.Array && !propValues[propKey].HasValues)) + validationResults.Add(new ValidationResult("Item " + (i + 1) + " '" + propType.Name + "' cannot be empty", new[] { propKey })); + } + + // Check regex + if (!propType.ValidationRegExp.IsNullOrWhiteSpace() + && propValues[propKey] != null && !propValues[propKey].ToString().IsNullOrWhiteSpace()) + { + var regex = new Regex(propType.ValidationRegExp); + if (!regex.IsMatch(propValues[propKey].ToString())) + { + validationResults.Add(new ValidationResult("Item " + (i + 1) + " '" + propType.Name + "' is invalid, it does not match the correct pattern", new[] { propKey })); + } + } + }); + + return validationResults; } } From f305dd45cbbf0c96855b86c2c3abed58da49f771 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Wed, 30 Oct 2019 08:50:15 +0100 Subject: [PATCH 041/202] AB3328 - Fixed two bugs - One if links was absolute and not references, and one if the value was empty string. --- .../PropertyEditors/MultiUrlPickerValueEditor.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web/PropertyEditors/MultiUrlPickerValueEditor.cs b/src/Umbraco.Web/PropertyEditors/MultiUrlPickerValueEditor.cs index 1f71f39d96..98f41c50b9 100644 --- a/src/Umbraco.Web/PropertyEditors/MultiUrlPickerValueEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MultiUrlPickerValueEditor.cs @@ -179,12 +179,20 @@ namespace Umbraco.Web.PropertyEditors { var asString = value == null ? string.Empty : value is string str ? str : value.ToString(); + if (string.IsNullOrEmpty(asString)) yield break; + var links = JsonConvert.DeserializeObject>(asString); foreach (var link in links) { - yield return new UmbracoEntityReference(link.Udi); + if (link.Udi != null) // Links can be absolute links without a Udi + { + yield return new UmbracoEntityReference(link.Udi); + } + } + + } } } From 3d66c201c2100be22a606432242fdf633bfe6db7 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 31 Oct 2019 14:08:38 +1100 Subject: [PATCH 042/202] Adds safety check of Udi.TryParse --- src/Umbraco.Web/PropertyEditors/ContentPickerPropertyEditor.cs | 3 ++- src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs | 3 ++- .../PropertyEditors/MultiNodeTreePickerPropertyEditor.cs | 3 ++- src/Umbraco.Web/PropertyEditors/MultiUrlPickerValueEditor.cs | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web/PropertyEditors/ContentPickerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/ContentPickerPropertyEditor.cs index c09b3b6930..683f1a05c3 100644 --- a/src/Umbraco.Web/PropertyEditors/ContentPickerPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/ContentPickerPropertyEditor.cs @@ -41,7 +41,8 @@ namespace Umbraco.Web.PropertyEditors if (string.IsNullOrEmpty(asString)) yield break; - yield return new UmbracoEntityReference(Udi.Parse(asString)); + if (Udi.TryParse(asString, out var udi)) + yield return new UmbracoEntityReference(udi); } } } diff --git a/src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs index be1a9b2691..ece210b9d1 100644 --- a/src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs @@ -45,7 +45,8 @@ namespace Umbraco.Web.PropertyEditors if (string.IsNullOrEmpty(asString)) yield break; - yield return new UmbracoEntityReference(Udi.Parse(asString)); + if (Udi.TryParse(asString, out var udi)) + yield return new UmbracoEntityReference(udi); } } } diff --git a/src/Umbraco.Web/PropertyEditors/MultiNodeTreePickerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MultiNodeTreePickerPropertyEditor.cs index 020ad38c75..1da665a622 100644 --- a/src/Umbraco.Web/PropertyEditors/MultiNodeTreePickerPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MultiNodeTreePickerPropertyEditor.cs @@ -37,7 +37,8 @@ namespace Umbraco.Web.PropertyEditors var udiPaths = asString.Split(','); foreach (var udiPath in udiPaths) { - yield return new UmbracoEntityReference(Udi.Parse(udiPath)); + if (Udi.TryParse(udiPath, out var udi)) + yield return new UmbracoEntityReference(udi); } } diff --git a/src/Umbraco.Web/PropertyEditors/MultiUrlPickerValueEditor.cs b/src/Umbraco.Web/PropertyEditors/MultiUrlPickerValueEditor.cs index 98f41c50b9..9c42fe6cbe 100644 --- a/src/Umbraco.Web/PropertyEditors/MultiUrlPickerValueEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MultiUrlPickerValueEditor.cs @@ -181,7 +181,7 @@ namespace Umbraco.Web.PropertyEditors if (string.IsNullOrEmpty(asString)) yield break; - var links = JsonConvert.DeserializeObject>(asString); + var links = JsonConvert.DeserializeObject>(asString); foreach (var link in links) { if (link.Udi != null) // Links can be absolute links without a Udi From 717c185e45636c1898beb1eef8988f93707e000f Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 1 Nov 2019 14:22:49 +1100 Subject: [PATCH 043/202] Manually merges prev changes to nested content from 6768 and 6826 and changes how the prop values are iterated --- .../Published/NestedContentTests.cs | 2 +- .../NestedContentPropertyEditor.cs | 291 ++++++++++-------- 2 files changed, 166 insertions(+), 127 deletions(-) diff --git a/src/Umbraco.Tests/Published/NestedContentTests.cs b/src/Umbraco.Tests/Published/NestedContentTests.cs index 2bba37c7d3..a20053c295 100644 --- a/src/Umbraco.Tests/Published/NestedContentTests.cs +++ b/src/Umbraco.Tests/Published/NestedContentTests.cs @@ -34,7 +34,7 @@ namespace Umbraco.Tests.Published var proflog = new ProfilingLogger(logger, profiler); PropertyEditorCollection editors = null; - var editor = new NestedContentPropertyEditor(logger, new Lazy(() => editors), Mock.Of()); + var editor = new NestedContentPropertyEditor(logger, new Lazy(() => editors), Mock.Of(), Mock.Of()); editors = new PropertyEditorCollection(new DataEditorCollection(new DataEditor[] { editor })); var dataType1 = new DataType(editor) diff --git a/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs index 7104df0775..1f89abdcdd 100644 --- a/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; @@ -29,26 +30,20 @@ namespace Umbraco.Web.PropertyEditors { private readonly Lazy _propertyEditors; private readonly IDataTypeService _dataTypeService; + private readonly IContentTypeService _contentTypeService; internal const string ContentTypeAliasPropertyKey = "ncContentTypeAlias"; - public NestedContentPropertyEditor(ILogger logger, Lazy propertyEditors, IDataTypeService dataTypeService) + public NestedContentPropertyEditor(ILogger logger, Lazy propertyEditors, IDataTypeService dataTypeService, IContentTypeService contentTypeService) : base (logger) { _propertyEditors = propertyEditors; _dataTypeService = dataTypeService; + _contentTypeService = contentTypeService; } // has to be lazy else circular dep in ctor private PropertyEditorCollection PropertyEditors => _propertyEditors.Value; - private static IContentType GetElementType(JObject item) - { - var contentTypeAlias = item[ContentTypeAliasPropertyKey]?.ToObject(); - return string.IsNullOrEmpty(contentTypeAlias) - ? null - : Current.Services.ContentTypeService.Get(contentTypeAlias); - } - #region Pre Value Editor protected override IConfigurationEditor CreateConfigurationEditor() => new NestedContentConfigurationEditor(); @@ -57,19 +52,21 @@ namespace Umbraco.Web.PropertyEditors #region Value Editor - protected override IDataValueEditor CreateValueEditor() => new NestedContentPropertyValueEditor(Attribute, PropertyEditors, _dataTypeService); + protected override IDataValueEditor CreateValueEditor() => new NestedContentPropertyValueEditor(Attribute, PropertyEditors, _dataTypeService, _contentTypeService); internal class NestedContentPropertyValueEditor : DataValueEditor, IDataValueReference { private readonly PropertyEditorCollection _propertyEditors; private readonly IDataTypeService _dataTypeService; - - public NestedContentPropertyValueEditor(DataEditorAttribute attribute, PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService) + private readonly NestedContentValues _nestedContentValues; + + public NestedContentPropertyValueEditor(DataEditorAttribute attribute, PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, IContentTypeService contentTypeService) : base(attribute) { _propertyEditors = propertyEditors; _dataTypeService = dataTypeService; - Validators.Add(new NestedContentValidator(propertyEditors, dataTypeService)); + _nestedContentValues = new NestedContentValues(contentTypeService); + Validators.Add(new NestedContentValidator(propertyEditors, dataTypeService, _nestedContentValues)); } /// @@ -88,86 +85,45 @@ namespace Umbraco.Web.PropertyEditors } } - /// - /// Method used to iterate over the deserialized property values - /// - /// - /// - internal static List IteratePropertyValues(object propertyValue, Action onIteration) - { - if (propertyValue == null || string.IsNullOrWhiteSpace(propertyValue.ToString())) - return null; - - var value = JsonConvert.DeserializeObject>(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 (value == null || value.Count == 0) - return null; - - var index = 0; - - foreach (var o in value) - { - var propValues = o; - - // TODO: This is N+1 (although we cache all doc types, it's still not pretty) - var contentType = GetElementType(propValues); - 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) - { - propertyTypes.TryGetValue(propAlias, out var propType); - onIteration(propAlias, propType, propValues, index); - } - - index++; - } - - return value; - } - #region DB to String public override string ConvertDbToString(PropertyType propertyType, object propertyValue, IDataTypeService dataTypeService) { - var value = IteratePropertyValues(propertyValue, (string propAlias, PropertyType propType, JObject propValues, int index) => + var vals = _nestedContentValues.GetPropertyValues(propertyValue).ToList(); + + if (vals.Count == 0) + return string.Empty; + + foreach (var row in vals) { - if (propType == null) + if (row.PropType == null) { // type not found, and property is not system: just delete the value - if (IsSystemPropertyKey(propAlias) == false) - propValues[propAlias] = null; + if (IsSystemPropertyKey(row.PropKey) == false) + row.PropValues[row.PropKey] = null; } else { try { // convert the value, and store the converted value - var propEditor = _propertyEditors[propType.PropertyEditorAlias]; - var tempConfig = dataTypeService.GetDataType(propType.DataTypeId).Configuration; + var propEditor = _propertyEditors[row.PropType.PropertyEditorAlias]; + if (propEditor == null) continue; + + var tempConfig = dataTypeService.GetDataType(row.PropType.DataTypeId).Configuration; var valEditor = propEditor.GetValueEditor(tempConfig); - var convValue = valEditor.ConvertDbToString(propType, propValues[propAlias]?.ToString(), dataTypeService); - propValues[propAlias] = convValue; + var convValue = valEditor.ConvertDbToString(row.PropType, row.PropValues[row.PropKey]?.ToString(), dataTypeService); + row.PropValues[row.PropKey] = convValue; } catch (InvalidOperationException) { // deal with weird situations by ignoring them (no comment) - propValues[propAlias] = null; + row.PropValues[row.PropKey] = null; } } - }); + } - if (value == null) - return string.Empty; - - return JsonConvert.SerializeObject(value).ToXmlString(); + return JsonConvert.SerializeObject(vals).ToXmlString(); } #endregion @@ -182,13 +138,18 @@ namespace Umbraco.Web.PropertyEditors { var val = property.GetValue(culture, segment); - var value = IteratePropertyValues(val, (string propAlias, PropertyType propType, JObject propValues, int index) => + var vals = _nestedContentValues.GetPropertyValues(val).ToList(); + + if (vals.Count == 0) + return string.Empty; + + foreach (var row in vals) { - if (propType == null) + if (row.PropType == null) { // type not found, and property is not system: just delete the value - if (IsSystemPropertyKey(propAlias) == false) - propValues[propAlias] = null; + if (IsSystemPropertyKey(row.PropKey) == false) + row.PropValues[row.PropKey] = null; } else { @@ -196,30 +157,33 @@ namespace Umbraco.Web.PropertyEditors { // create a temp property with the value // - force it to be culture invariant as NC can't handle culture variant element properties - propType.Variations = ContentVariation.Nothing; - var tempProp = new Property(propType); - tempProp.SetValue(propValues[propAlias] == null ? null : propValues[propAlias].ToString()); + row.PropType.Variations = ContentVariation.Nothing; + var tempProp = new Property(row.PropType); + tempProp.SetValue(row.PropValues[row.PropKey] == null ? null : row.PropValues[row.PropKey].ToString()); // convert that temp property, and store the converted value - var propEditor = _propertyEditors[propType.PropertyEditorAlias]; - var tempConfig = dataTypeService.GetDataType(propType.DataTypeId).Configuration; + var propEditor = _propertyEditors[row.PropType.PropertyEditorAlias]; + if (propEditor == null) + { + row.PropValues[row.PropKey] = tempProp.GetValue()?.ToString(); + continue; + } + + var tempConfig = dataTypeService.GetDataType(row.PropType.DataTypeId).Configuration; var valEditor = propEditor.GetValueEditor(tempConfig); var convValue = valEditor.ToEditor(tempProp, dataTypeService); - propValues[propAlias] = convValue == null ? null : JToken.FromObject(convValue); + row.PropValues[row.PropKey] = convValue == null ? null : JToken.FromObject(convValue); } catch (InvalidOperationException) { // deal with weird situations by ignoring them (no comment) - propValues[propAlias] = null; + row.PropValues[row.PropKey] = null; } } - }); - - if (value == null) - return string.Empty; + } // return json - return value; + return vals; } public override object FromEditor(ContentPropertyData editorValue, object currentValue) @@ -227,38 +191,41 @@ namespace Umbraco.Web.PropertyEditors if (editorValue.Value == null || string.IsNullOrWhiteSpace(editorValue.Value.ToString())) return null; - var value = IteratePropertyValues(editorValue.Value, (string propAlias, PropertyType propType, JObject propValues, int index) => + var vals = _nestedContentValues.GetPropertyValues(editorValue.Value).ToList(); + + if (vals.Count == 0) + return string.Empty; + + foreach (var row in vals) { - if (propType == null) + if (row.PropType == null) { // type not found, and property is not system: just delete the value - if (IsSystemPropertyKey(propAlias) == false) - propValues[propAlias] = null; + if (IsSystemPropertyKey(row.PropKey) == false) + row.PropValues[row.PropKey] = null; } else { // Fetch the property types prevalue - var propConfiguration = _dataTypeService.GetDataType(propType.DataTypeId).Configuration; + var propConfiguration = _dataTypeService.GetDataType(row.PropType.DataTypeId).Configuration; // Lookup the property editor - var propEditor = _propertyEditors[propType.PropertyEditorAlias]; + var propEditor = _propertyEditors[row.PropType.PropertyEditorAlias]; + if (propEditor == null) continue; // Create a fake content property data object - var contentPropData = new ContentPropertyData(propValues[propAlias], propConfiguration); + var contentPropData = new ContentPropertyData(row.PropValues[row.PropKey], propConfiguration); // Get the property editor to do it's conversion - var newValue = propEditor.GetValueEditor().FromEditor(contentPropData, propValues[propAlias]); + var newValue = propEditor.GetValueEditor().FromEditor(contentPropData, row.PropValues[row.PropKey]); // Store the value back - propValues[propAlias] = (newValue == null) ? null : JToken.FromObject(newValue); + row.PropValues[row.PropKey] = (newValue == null) ? null : JToken.FromObject(newValue); } - }); - - if (value == null) - return string.Empty; + } // return json - return JsonConvert.SerializeObject(value); + return JsonConvert.SerializeObject(vals); } public IEnumerable GetReferences(object value) @@ -267,21 +234,21 @@ namespace Umbraco.Web.PropertyEditors var result = new List(); - var json = IteratePropertyValues(rawJson, (string propAlias, PropertyType propType, JObject propValues, int index) => + foreach (var row in _nestedContentValues.GetPropertyValues(rawJson)) { - if (propType == null) return; + if (row.PropType == null) continue; - var propEditor = _propertyEditors[propType.PropertyEditorAlias]; + var propEditor = _propertyEditors[row.PropType.PropertyEditorAlias]; var valueEditor = propEditor?.GetValueEditor(); - if (!(valueEditor is IDataValueReference reference)) return; + if (!(valueEditor is IDataValueReference reference)) continue; - var val = propValues[propAlias]?.ToString(); + var val = row.PropValues[row.PropKey]?.ToString(); var refs = reference.GetReferences(val); result.AddRange(refs); - }); + } return result; } @@ -293,58 +260,130 @@ namespace Umbraco.Web.PropertyEditors { private readonly PropertyEditorCollection _propertyEditors; private readonly IDataTypeService _dataTypeService; + private readonly NestedContentValues _nestedContentValues; - public NestedContentValidator(PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService) + public NestedContentValidator(PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, NestedContentValues nestedContentValues) { _propertyEditors = propertyEditors; _dataTypeService = dataTypeService; + _nestedContentValues = nestedContentValues; } public IEnumerable Validate(object rawValue, string valueType, object dataTypeConfiguration) { var validationResults = new List(); - NestedContentPropertyValueEditor.IteratePropertyValues(rawValue, (string propKey, PropertyType propType, JObject propValues, int i) => + foreach(var row in _nestedContentValues.GetPropertyValues(rawValue)) { - if (propType == null) return; + if (row.PropType == null) continue; - var config = _dataTypeService.GetDataType(propType.DataTypeId).Configuration; - var propertyEditor = _propertyEditors[propType.PropertyEditorAlias]; + 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) { - foreach (var result in validator.Validate(propValues[propKey], propertyEditor.GetValueEditor().ValueType, config)) + foreach (var result in validator.Validate(row.PropValues[row.PropKey], propertyEditor.GetValueEditor().ValueType, config)) { - result.ErrorMessage = "Item " + (i + 1) + " '" + propType.Name + "' " + result.ErrorMessage; + result.ErrorMessage = "Item " + (row.Index + 1) + " '" + row.PropType.Name + "' " + result.ErrorMessage; validationResults.Add(result); } } // Check mandatory - if (propType.Mandatory) + if (row.PropType.Mandatory) { - if (propValues[propKey] == null) - validationResults.Add(new ValidationResult("Item " + (i + 1) + " '" + propType.Name + "' cannot be null", new[] { propKey })); - else if (propValues[propKey].ToString().IsNullOrWhiteSpace() || (propValues[propKey].Type == JTokenType.Array && !propValues[propKey].HasValues)) - validationResults.Add(new ValidationResult("Item " + (i + 1) + " '" + propType.Name + "' cannot be empty", new[] { propKey })); + if (row.PropValues[row.PropKey] == null) + validationResults.Add(new ValidationResult("Item " + (row.Index + 1) + " '" + row.PropType.Name + "' cannot be null", new[] { row.PropKey })); + else if (row.PropValues[row.PropKey].ToString().IsNullOrWhiteSpace() || (row.PropValues[row.PropKey].Type == JTokenType.Array && !row.PropValues[row.PropKey].HasValues)) + validationResults.Add(new ValidationResult("Item " + (row.Index + 1) + " '" + row.PropType.Name + "' cannot be empty", new[] { row.PropKey })); } // Check regex - if (!propType.ValidationRegExp.IsNullOrWhiteSpace() - && propValues[propKey] != null && !propValues[propKey].ToString().IsNullOrWhiteSpace()) + if (!row.PropType.ValidationRegExp.IsNullOrWhiteSpace() + && row.PropValues[row.PropKey] != null && !row.PropValues[row.PropKey].ToString().IsNullOrWhiteSpace()) { - var regex = new Regex(propType.ValidationRegExp); - if (!regex.IsMatch(propValues[propKey].ToString())) + var regex = new Regex(row.PropType.ValidationRegExp); + if (!regex.IsMatch(row.PropValues[row.PropKey].ToString())) { - validationResults.Add(new ValidationResult("Item " + (i + 1) + " '" + propType.Name + "' is invalid, it does not match the correct pattern", new[] { propKey })); + validationResults.Add(new ValidationResult("Item " + (row.Index + 1) + " '" + row.PropType.Name + "' is invalid, it does not match the correct pattern", new[] { row.PropKey })); } } - }); + } return validationResults; } } + internal class NestedContentValues + { + private readonly Lazy> _contentTypes; + + public NestedContentValues(IContentTypeService contentTypeService) + { + _contentTypes = new Lazy>(() => contentTypeService.GetAll().ToDictionary(c => c.Alias)); + } + + private IContentType GetElementType(JObject item) + { + var contentTypeAlias = item[ContentTypeAliasPropertyKey]?.ToObject() ?? string.Empty; + _contentTypes.Value.TryGetValue(contentTypeAlias, out var contentType); + return contentType; + } + + public IEnumerable GetPropertyValues(object propertyValue) + { + if (propertyValue == null || string.IsNullOrWhiteSpace(propertyValue.ToString())) + yield break; + + var value = JsonConvert.DeserializeObject>(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 (value == null || value.Count == 0) + yield break; + + var index = 0; + + foreach (var o in value) + { + var propValues = o; + + var contentType = GetElementType(propValues); + 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) + { + propertyTypes.TryGetValue(propAlias, out var propType); + yield return new RowValue(propAlias, propType, propValues, index); + } + index++; + } + } + + internal class RowValue + { + public RowValue(string propKey, PropertyType propType, JObject propValues, int index) + { + PropKey = propKey ?? throw new ArgumentNullException(nameof(propKey)); + PropType = propType ?? throw new ArgumentNullException(nameof(propType)); + PropValues = propValues ?? throw new ArgumentNullException(nameof(propValues)); + Index = index; + } + + public string PropKey { get; } + public PropertyType PropType { get; } + public JObject PropValues { get; } + public int Index { get; } + } + } + #endregion private static bool IsSystemPropertyKey(string propKey) From d1ea46ff71d7c13025b172292e8817add685c6ab Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 1 Nov 2019 14:45:05 +1100 Subject: [PATCH 044/202] fixes null ref check and returning the correct deserialized val, adds comments --- .../NestedContentPropertyEditor.cs | 109 +++++++++++------- 1 file changed, 65 insertions(+), 44 deletions(-) diff --git a/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs index 1f89abdcdd..3d0605c4f9 100644 --- a/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs @@ -89,7 +89,7 @@ namespace Umbraco.Web.PropertyEditors public override string ConvertDbToString(PropertyType propertyType, object propertyValue, IDataTypeService dataTypeService) { - var vals = _nestedContentValues.GetPropertyValues(propertyValue).ToList(); + var vals = _nestedContentValues.GetPropertyValues(propertyValue, out var deserialized).ToList(); if (vals.Count == 0) return string.Empty; @@ -100,7 +100,7 @@ namespace Umbraco.Web.PropertyEditors { // type not found, and property is not system: just delete the value if (IsSystemPropertyKey(row.PropKey) == false) - row.PropValues[row.PropKey] = null; + row.JsonRowValue[row.PropKey] = null; } else { @@ -112,18 +112,18 @@ namespace Umbraco.Web.PropertyEditors var tempConfig = dataTypeService.GetDataType(row.PropType.DataTypeId).Configuration; var valEditor = propEditor.GetValueEditor(tempConfig); - var convValue = valEditor.ConvertDbToString(row.PropType, row.PropValues[row.PropKey]?.ToString(), dataTypeService); - row.PropValues[row.PropKey] = convValue; + var convValue = valEditor.ConvertDbToString(row.PropType, row.JsonRowValue[row.PropKey]?.ToString(), dataTypeService); + row.JsonRowValue[row.PropKey] = convValue; } catch (InvalidOperationException) { // deal with weird situations by ignoring them (no comment) - row.PropValues[row.PropKey] = null; + row.JsonRowValue[row.PropKey] = null; } } } - return JsonConvert.SerializeObject(vals).ToXmlString(); + return JsonConvert.SerializeObject(deserialized).ToXmlString(); } #endregion @@ -138,7 +138,7 @@ namespace Umbraco.Web.PropertyEditors { var val = property.GetValue(culture, segment); - var vals = _nestedContentValues.GetPropertyValues(val).ToList(); + var vals = _nestedContentValues.GetPropertyValues(val, out var deserialized).ToList(); if (vals.Count == 0) return string.Empty; @@ -149,7 +149,7 @@ namespace Umbraco.Web.PropertyEditors { // type not found, and property is not system: just delete the value if (IsSystemPropertyKey(row.PropKey) == false) - row.PropValues[row.PropKey] = null; + row.JsonRowValue[row.PropKey] = null; } else { @@ -159,31 +159,31 @@ namespace Umbraco.Web.PropertyEditors // - 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.PropValues[row.PropKey] == null ? null : row.PropValues[row.PropKey].ToString()); + tempProp.SetValue(row.JsonRowValue[row.PropKey] == null ? null : row.JsonRowValue[row.PropKey].ToString()); // convert that temp property, and store the converted value var propEditor = _propertyEditors[row.PropType.PropertyEditorAlias]; if (propEditor == null) { - row.PropValues[row.PropKey] = tempProp.GetValue()?.ToString(); + row.JsonRowValue[row.PropKey] = tempProp.GetValue()?.ToString(); continue; } var tempConfig = dataTypeService.GetDataType(row.PropType.DataTypeId).Configuration; var valEditor = propEditor.GetValueEditor(tempConfig); var convValue = valEditor.ToEditor(tempProp, dataTypeService); - row.PropValues[row.PropKey] = convValue == null ? null : JToken.FromObject(convValue); + row.JsonRowValue[row.PropKey] = convValue == null ? null : JToken.FromObject(convValue); } catch (InvalidOperationException) { // deal with weird situations by ignoring them (no comment) - row.PropValues[row.PropKey] = null; + row.JsonRowValue[row.PropKey] = null; } } } // return json - return vals; + return deserialized; } public override object FromEditor(ContentPropertyData editorValue, object currentValue) @@ -191,7 +191,7 @@ namespace Umbraco.Web.PropertyEditors if (editorValue.Value == null || string.IsNullOrWhiteSpace(editorValue.Value.ToString())) return null; - var vals = _nestedContentValues.GetPropertyValues(editorValue.Value).ToList(); + var vals = _nestedContentValues.GetPropertyValues(editorValue.Value, out var deserialized).ToList(); if (vals.Count == 0) return string.Empty; @@ -202,7 +202,7 @@ namespace Umbraco.Web.PropertyEditors { // type not found, and property is not system: just delete the value if (IsSystemPropertyKey(row.PropKey) == false) - row.PropValues[row.PropKey] = null; + row.JsonRowValue[row.PropKey] = null; } else { @@ -214,18 +214,18 @@ namespace Umbraco.Web.PropertyEditors if (propEditor == null) continue; // Create a fake content property data object - var contentPropData = new ContentPropertyData(row.PropValues[row.PropKey], propConfiguration); + var contentPropData = new ContentPropertyData(row.JsonRowValue[row.PropKey], propConfiguration); // Get the property editor to do it's conversion - var newValue = propEditor.GetValueEditor().FromEditor(contentPropData, row.PropValues[row.PropKey]); + var newValue = propEditor.GetValueEditor().FromEditor(contentPropData, row.JsonRowValue[row.PropKey]); // Store the value back - row.PropValues[row.PropKey] = (newValue == null) ? null : JToken.FromObject(newValue); + row.JsonRowValue[row.PropKey] = (newValue == null) ? null : JToken.FromObject(newValue); } } // return json - return JsonConvert.SerializeObject(vals); + return JsonConvert.SerializeObject(deserialized); } public IEnumerable GetReferences(object value) @@ -234,7 +234,7 @@ namespace Umbraco.Web.PropertyEditors var result = new List(); - foreach (var row in _nestedContentValues.GetPropertyValues(rawJson)) + foreach (var row in _nestedContentValues.GetPropertyValues(rawJson, out _)) { if (row.PropType == null) continue; @@ -243,7 +243,7 @@ namespace Umbraco.Web.PropertyEditors var valueEditor = propEditor?.GetValueEditor(); if (!(valueEditor is IDataValueReference reference)) continue; - var val = row.PropValues[row.PropKey]?.ToString(); + var val = row.JsonRowValue[row.PropKey]?.ToString(); var refs = reference.GetReferences(val); @@ -273,7 +273,7 @@ namespace Umbraco.Web.PropertyEditors { var validationResults = new List(); - foreach(var row in _nestedContentValues.GetPropertyValues(rawValue)) + foreach(var row in _nestedContentValues.GetPropertyValues(rawValue, out _)) { if (row.PropType == null) continue; @@ -283,9 +283,9 @@ namespace Umbraco.Web.PropertyEditors foreach (var validator in propertyEditor.GetValueEditor().Validators) { - foreach (var result in validator.Validate(row.PropValues[row.PropKey], propertyEditor.GetValueEditor().ValueType, config)) + foreach (var result in validator.Validate(row.JsonRowValue[row.PropKey], propertyEditor.GetValueEditor().ValueType, config)) { - result.ErrorMessage = "Item " + (row.Index + 1) + " '" + row.PropType.Name + "' " + result.ErrorMessage; + result.ErrorMessage = "Item " + (row.RowIndex + 1) + " '" + row.PropType.Name + "' " + result.ErrorMessage; validationResults.Add(result); } } @@ -293,20 +293,20 @@ namespace Umbraco.Web.PropertyEditors // Check mandatory if (row.PropType.Mandatory) { - if (row.PropValues[row.PropKey] == null) - validationResults.Add(new ValidationResult("Item " + (row.Index + 1) + " '" + row.PropType.Name + "' cannot be null", new[] { row.PropKey })); - else if (row.PropValues[row.PropKey].ToString().IsNullOrWhiteSpace() || (row.PropValues[row.PropKey].Type == JTokenType.Array && !row.PropValues[row.PropKey].HasValues)) - validationResults.Add(new ValidationResult("Item " + (row.Index + 1) + " '" + row.PropType.Name + "' cannot be empty", new[] { row.PropKey })); + if (row.JsonRowValue[row.PropKey] == null) + validationResults.Add(new ValidationResult("Item " + (row.RowIndex + 1) + " '" + row.PropType.Name + "' cannot be null", new[] { row.PropKey })); + else if (row.JsonRowValue[row.PropKey].ToString().IsNullOrWhiteSpace() || (row.JsonRowValue[row.PropKey].Type == JTokenType.Array && !row.JsonRowValue[row.PropKey].HasValues)) + validationResults.Add(new ValidationResult("Item " + (row.RowIndex + 1) + " '" + row.PropType.Name + "' cannot be empty", new[] { row.PropKey })); } // Check regex if (!row.PropType.ValidationRegExp.IsNullOrWhiteSpace() - && row.PropValues[row.PropKey] != null && !row.PropValues[row.PropKey].ToString().IsNullOrWhiteSpace()) + && row.JsonRowValue[row.PropKey] != null && !row.JsonRowValue[row.PropKey].ToString().IsNullOrWhiteSpace()) { var regex = new Regex(row.PropType.ValidationRegExp); - if (!regex.IsMatch(row.PropValues[row.PropKey].ToString())) + if (!regex.IsMatch(row.JsonRowValue[row.PropKey].ToString())) { - validationResults.Add(new ValidationResult("Item " + (row.Index + 1) + " '" + row.PropType.Name + "' is invalid, it does not match the correct pattern", new[] { row.PropKey })); + validationResults.Add(new ValidationResult("Item " + (row.RowIndex + 1) + " '" + row.PropType.Name + "' is invalid, it does not match the correct pattern", new[] { row.PropKey })); } } } @@ -331,24 +331,28 @@ namespace Umbraco.Web.PropertyEditors return contentType; } - public IEnumerable GetPropertyValues(object propertyValue) + public IEnumerable GetPropertyValues(object propertyValue, out List deserialized) { - if (propertyValue == null || string.IsNullOrWhiteSpace(propertyValue.ToString())) - yield break; + var rowValues = new List(); - var value = JsonConvert.DeserializeObject>(propertyValue.ToString()); + deserialized = null; + + if (propertyValue == null || string.IsNullOrWhiteSpace(propertyValue.ToString())) + return Enumerable.Empty(); + + deserialized = JsonConvert.DeserializeObject>(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 (value == null || value.Count == 0) - yield break; + if (deserialized == null || deserialized.Count == 0) + return Enumerable.Empty(); var index = 0; - foreach (var o in value) + foreach (var o in deserialized) { var propValues = o; @@ -361,10 +365,12 @@ namespace Umbraco.Web.PropertyEditors foreach (var propAlias in propAliases) { propertyTypes.TryGetValue(propAlias, out var propType); - yield return new RowValue(propAlias, propType, propValues, index); + rowValues.Add(new RowValue(propAlias, propType, propValues, index)); } index++; } + + return rowValues; } internal class RowValue @@ -372,15 +378,30 @@ namespace Umbraco.Web.PropertyEditors public RowValue(string propKey, PropertyType propType, JObject propValues, int index) { PropKey = propKey ?? throw new ArgumentNullException(nameof(propKey)); - PropType = propType ?? throw new ArgumentNullException(nameof(propType)); - PropValues = propValues ?? throw new ArgumentNullException(nameof(propValues)); - Index = index; + PropType = propType; + JsonRowValue = propValues ?? throw new ArgumentNullException(nameof(propValues)); + RowIndex = index; } + /// + /// The current property key being iterated for the row value + /// public string PropKey { get; } + + /// + /// The of the value (if any), this may be null + /// public PropertyType PropType { get; } - public JObject PropValues { get; } - public int Index { get; } + + /// + /// The json values for the current row + /// + public JObject JsonRowValue { get; } + + /// + /// The Nested Content row index + /// + public int RowIndex { get; } } } From b0d834668d0631d14bd580ae615e08616ed33f07 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Fri, 1 Nov 2019 10:49:45 +0100 Subject: [PATCH 045/202] Fix search not skipping results --- src/Umbraco.Web/PublishedContentQuery.cs | 57 +++++++++++++----------- 1 file changed, 32 insertions(+), 25 deletions(-) diff --git a/src/Umbraco.Web/PublishedContentQuery.cs b/src/Umbraco.Web/PublishedContentQuery.cs index 2dbe4de4c5..72aec94e58 100644 --- a/src/Umbraco.Web/PublishedContentQuery.cs +++ b/src/Umbraco.Web/PublishedContentQuery.cs @@ -191,46 +191,52 @@ namespace Umbraco.Web /// public IEnumerable Search(string term, int skip, int take, out long totalRecords, string culture = "*", string indexName = null) { - indexName = string.IsNullOrEmpty(indexName) - ? Constants.UmbracoIndexes.ExternalIndexName - : indexName; + if (string.IsNullOrEmpty(indexName)) + { + indexName = Constants.UmbracoIndexes.ExternalIndexName; + } if (!_examineManager.TryGetIndex(indexName, out var index) || !(index is IUmbracoIndex umbIndex)) + { throw new InvalidOperationException($"No index found by name {indexName} or is not of type {typeof(IUmbracoIndex)}"); + } var searcher = umbIndex.GetSearcher(); - // default to max 500 results - var count = skip == 0 && take == 0 ? 500 : skip + take; - ISearchResults results; if (culture == "*") { - //search everything - - results = searcher.Search(term, count); - } - else if (culture.IsNullOrWhiteSpace()) - { - //only search invariant - - var qry = searcher.CreateQuery().Field(UmbracoContentIndex.VariesByCultureFieldName, "n"); //must not vary by culture - qry = qry.And().ManagedQuery(term); - results = qry.Execute(count); + // Search everything + results = skip == 0 && take == 0 + ? searcher.Search(term) + : searcher.Search(term, skip + take); } else { - //search only the specified culture + IBooleanOperation query; + if (string.IsNullOrWhiteSpace(culture)) + { + // Only search invariant + query = searcher.CreateQuery() + .Field(UmbracoContentIndex.VariesByCultureFieldName, "n") // Must not vary by culture + .And().ManagedQuery(term); + } + else + { + // Only search the specified culture + var fields = umbIndex.GetCultureAndInvariantFields(culture).ToArray(); // Get all index fields suffixed with the culture name supplied + query = searcher.CreateQuery() + .ManagedQuery(term, fields); + } - //get all index fields suffixed with the culture name supplied - var cultureFields = umbIndex.GetCultureAndInvariantFields(culture).ToArray(); - var qry = searcher.CreateQuery().ManagedQuery(term, cultureFields); - results = qry.Execute(count); + results = skip == 0 && take == 0 + ? query.Execute() + : query.Execute(skip + take); } totalRecords = results.TotalItemCount; - return new CultureContextualSearchResults(results.ToPublishedSearchResults(_publishedSnapshot.Content), _variationContextAccessor, culture); + return new CultureContextualSearchResults(results.Skip(skip).ToPublishedSearchResults(_publishedSnapshot.Content), _variationContextAccessor, culture); } /// @@ -244,10 +250,11 @@ namespace Umbraco.Web { var results = skip == 0 && take == 0 ? query.Execute() - : query.Execute(maxResults: skip + take); + : query.Execute(skip + take); totalRecords = results.TotalItemCount; - return results.ToPublishedSearchResults(_publishedSnapshot.Content); + + return results.Skip(skip).ToPublishedSearchResults(_publishedSnapshot.Content); } /// From dcba53033c4b387c2938127ea64cdf594c41d692 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 5 Nov 2019 10:30:38 +1100 Subject: [PATCH 046/202] Changes namespace for media tracking migrations to 8.6 --- .../Upgrade/{V_8_5_0 => V_8_6_0}/AddNewRelationTypes.cs | 2 +- .../Upgrade/{V_8_5_0 => V_8_6_0}/UpdateRelationTypeTable.cs | 2 +- src/Umbraco.Core/Umbraco.Core.csproj | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) rename src/Umbraco.Core/Migrations/Upgrade/{V_8_5_0 => V_8_6_0}/AddNewRelationTypes.cs (95%) rename src/Umbraco.Core/Migrations/Upgrade/{V_8_5_0 => V_8_6_0}/UpdateRelationTypeTable.cs (96%) diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_5_0/AddNewRelationTypes.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_6_0/AddNewRelationTypes.cs similarity index 95% rename from src/Umbraco.Core/Migrations/Upgrade/V_8_5_0/AddNewRelationTypes.cs rename to src/Umbraco.Core/Migrations/Upgrade/V_8_6_0/AddNewRelationTypes.cs index 40e541f04c..2e2e00a9bc 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_5_0/AddNewRelationTypes.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_6_0/AddNewRelationTypes.cs @@ -1,6 +1,6 @@ using Umbraco.Core.Migrations.Install; -namespace Umbraco.Core.Migrations.Upgrade.V_8_5_0 +namespace Umbraco.Core.Migrations.Upgrade.V_8_6_0 { /// /// Ensures the new relation types are created diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_5_0/UpdateRelationTypeTable.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_6_0/UpdateRelationTypeTable.cs similarity index 96% rename from src/Umbraco.Core/Migrations/Upgrade/V_8_5_0/UpdateRelationTypeTable.cs rename to src/Umbraco.Core/Migrations/Upgrade/V_8_6_0/UpdateRelationTypeTable.cs index b76f5ba3a7..c79f43d20f 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_5_0/UpdateRelationTypeTable.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_6_0/UpdateRelationTypeTable.cs @@ -1,6 +1,6 @@ using Umbraco.Core.Persistence.Dtos; -namespace Umbraco.Core.Migrations.Upgrade.V_8_5_0 +namespace Umbraco.Core.Migrations.Upgrade.V_8_6_0 { public class UpdateRelationTypeTable : MigrationBase diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 15eebe70ba..225317943b 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -251,8 +251,8 @@ - - + + @@ -1575,4 +1575,4 @@ - + \ No newline at end of file From 412eadd9a305a08eedeb679f7519500262e181e1 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 5 Nov 2019 10:48:23 +1100 Subject: [PATCH 047/202] Changes namespace for media tracking migrations to 8.6 --- src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs index 45182b17e3..77dcaa808e 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs @@ -6,7 +6,7 @@ using Umbraco.Core.Migrations.Upgrade.Common; using Umbraco.Core.Migrations.Upgrade.V_8_0_0; using Umbraco.Core.Migrations.Upgrade.V_8_0_1; using Umbraco.Core.Migrations.Upgrade.V_8_1_0; -using Umbraco.Core.Migrations.Upgrade.V_8_5_0; +using Umbraco.Core.Migrations.Upgrade.V_8_6_0; namespace Umbraco.Core.Migrations.Upgrade { From fff3d2648f71b375a6bec6624b140cce08a3e8e2 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 5 Nov 2019 15:05:51 +1100 Subject: [PATCH 048/202] Gets entity repository to be able to return a mix of object types --- src/Umbraco.Core/Models/ContentBase.cs | 3 + src/Umbraco.Core/Models/ContentTypeBase.cs | 3 + src/Umbraco.Core/Models/DataType.cs | 3 + .../Models/Entities/ITreeEntity.cs | 4 +- .../Models/Entities/IUmbracoEntity.cs | 7 +- src/Umbraco.Core/Models/EntityContainer.cs | 2 + src/Umbraco.Core/Models/SimpleContentType.cs | 3 + .../Factories/ContentBaseFactory.cs | 2 +- .../Factories/ContentTypeFactory.cs | 1 + .../Persistence/Factories/DataTypeFactory.cs | 1 + .../Mappers/UmbracoEntityMapper.cs | 1 + .../Repositories/IEntityRepository.cs | 43 ++++- .../Implement/EntityRepository.cs | 157 +++++++++++------- .../Repositories/EntityRepositoryTest.cs | 95 +++++++++++ src/Umbraco.Tests/Umbraco.Tests.csproj | 1 + src/Umbraco.Web/Editors/EntityController.cs | 3 + .../Models/Mapping/EntityMapDefinition.cs | 8 +- .../Models/Mapping/UserMapDefinition.cs | 4 +- .../Trees/ContentBlueprintTreeController.cs | 4 +- 19 files changed, 273 insertions(+), 72 deletions(-) create mode 100644 src/Umbraco.Tests/Persistence/Repositories/EntityRepositoryTest.cs diff --git a/src/Umbraco.Core/Models/ContentBase.cs b/src/Umbraco.Core/Models/ContentBase.cs index fbb68194b7..2ea0fd855b 100644 --- a/src/Umbraco.Core/Models/ContentBase.cs +++ b/src/Umbraco.Core/Models/ContentBase.cs @@ -108,6 +108,9 @@ namespace Umbraco.Core.Models [IgnoreDataMember] public int VersionId { get; set; } + [DataMember] + public Guid NodeObjectType { get; internal set; } + /// /// Integer Id of the default ContentType /// diff --git a/src/Umbraco.Core/Models/ContentTypeBase.cs b/src/Umbraco.Core/Models/ContentTypeBase.cs index 04bcb7424a..1f3f1a5c13 100644 --- a/src/Umbraco.Core/Models/ContentTypeBase.cs +++ b/src/Umbraco.Core/Models/ContentTypeBase.cs @@ -113,6 +113,9 @@ namespace Umbraco.Core.Models OnPropertyChanged(nameof(PropertyTypes)); } + [DataMember] + public Guid NodeObjectType { get; internal set; } + /// /// The Alias of the ContentType /// diff --git a/src/Umbraco.Core/Models/DataType.cs b/src/Umbraco.Core/Models/DataType.cs index c237f6381c..cba53bbd10 100644 --- a/src/Umbraco.Core/Models/DataType.cs +++ b/src/Umbraco.Core/Models/DataType.cs @@ -65,6 +65,9 @@ namespace Umbraco.Core.Models [DataMember] public string EditorAlias => _editor.Alias; + [DataMember] + public Guid NodeObjectType { get; internal set; } + /// [DataMember] public ValueStorageType DatabaseType diff --git a/src/Umbraco.Core/Models/Entities/ITreeEntity.cs b/src/Umbraco.Core/Models/Entities/ITreeEntity.cs index afa3399202..ab63e1e1d8 100644 --- a/src/Umbraco.Core/Models/Entities/ITreeEntity.cs +++ b/src/Umbraco.Core/Models/Entities/ITreeEntity.cs @@ -24,7 +24,7 @@ /// Sets the parent entity. /// /// Use this method to set the parent entity when the parent entity is known, but has not - /// been persistent and does not yet have an identity. The parent identifier will we retrieved + /// been persistent and does not yet have an identity. The parent identifier will be retrieved /// from the parent entity when needed. If the parent entity still does not have an entity by that /// time, an exception will be thrown by getter. void SetParent(ITreeEntity parent); @@ -53,4 +53,4 @@ /// bool Trashed { get; } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Core/Models/Entities/IUmbracoEntity.cs b/src/Umbraco.Core/Models/Entities/IUmbracoEntity.cs index e5f628b098..13087b5e95 100644 --- a/src/Umbraco.Core/Models/Entities/IUmbracoEntity.cs +++ b/src/Umbraco.Core/Models/Entities/IUmbracoEntity.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; namespace Umbraco.Core.Models.Entities { @@ -11,5 +12,7 @@ namespace Umbraco.Core.Models.Entities /// An IUmbracoEntity can participate in notifications. /// public interface IUmbracoEntity : ITreeEntity, IRememberBeingDirty - { } + { + Guid NodeObjectType { get; } + } } diff --git a/src/Umbraco.Core/Models/EntityContainer.cs b/src/Umbraco.Core/Models/EntityContainer.cs index 70f6cbd878..71a477a9d6 100644 --- a/src/Umbraco.Core/Models/EntityContainer.cs +++ b/src/Umbraco.Core/Models/EntityContainer.cs @@ -62,6 +62,8 @@ namespace Umbraco.Core.Models /// public Guid ContainerObjectType => ObjectTypeMap[_containedObjectType]; + Guid IUmbracoEntity.NodeObjectType => ContainerObjectType; + /// /// Gets the container object type corresponding to a contained object type. /// diff --git a/src/Umbraco.Core/Models/SimpleContentType.cs b/src/Umbraco.Core/Models/SimpleContentType.cs index 5c81017ec8..b4e7688eea 100644 --- a/src/Umbraco.Core/Models/SimpleContentType.cs +++ b/src/Umbraco.Core/Models/SimpleContentType.cs @@ -44,11 +44,14 @@ namespace Umbraco.Core.Models Name = contentType.Name; AllowedAsRoot = contentType.AllowedAsRoot; IsElement = contentType.IsElement; + NodeObjectType = contentType.NodeObjectType; } /// public string Alias { get; } + public Guid NodeObjectType { get; } + public int Id { get; } /// diff --git a/src/Umbraco.Core/Persistence/Factories/ContentBaseFactory.cs b/src/Umbraco.Core/Persistence/Factories/ContentBaseFactory.cs index 434e0393cd..25cc6bc358 100644 --- a/src/Umbraco.Core/Persistence/Factories/ContentBaseFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/ContentBaseFactory.cs @@ -34,7 +34,7 @@ namespace Umbraco.Core.Persistence.Factories content.VersionId = contentVersionDto.Id; content.Name = contentVersionDto.Text; - + content.NodeObjectType = nodeDto.NodeObjectType ?? Guid.Empty; content.Path = nodeDto.Path; content.Level = nodeDto.Level; content.ParentId = nodeDto.ParentId; diff --git a/src/Umbraco.Core/Persistence/Factories/ContentTypeFactory.cs b/src/Umbraco.Core/Persistence/Factories/ContentTypeFactory.cs index 54cfee0ffa..46489aa69e 100644 --- a/src/Umbraco.Core/Persistence/Factories/ContentTypeFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/ContentTypeFactory.cs @@ -107,6 +107,7 @@ namespace Umbraco.Core.Persistence.Factories { entity.Id = dto.NodeDto.NodeId; entity.Key = dto.NodeDto.UniqueId; + entity.NodeObjectType = dto.NodeDto.NodeObjectType ?? Guid.Empty; entity.Alias = dto.Alias; entity.Name = dto.NodeDto.Text; entity.Icon = dto.Icon; diff --git a/src/Umbraco.Core/Persistence/Factories/DataTypeFactory.cs b/src/Umbraco.Core/Persistence/Factories/DataTypeFactory.cs index f189d38d05..5d92234ca8 100644 --- a/src/Umbraco.Core/Persistence/Factories/DataTypeFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/DataTypeFactory.cs @@ -28,6 +28,7 @@ namespace Umbraco.Core.Persistence.Factories dataType.DisableChangeTracking(); dataType.CreateDate = dto.NodeDto.CreateDate; + dataType.NodeObjectType = dto.NodeDto.NodeObjectType ?? Guid.Empty; dataType.DatabaseType = dto.DbType.EnumParse(true); dataType.Id = dto.NodeId; dataType.Key = dto.NodeDto.UniqueId; diff --git a/src/Umbraco.Core/Persistence/Mappers/UmbracoEntityMapper.cs b/src/Umbraco.Core/Persistence/Mappers/UmbracoEntityMapper.cs index 16e2e8bcd5..96f664cc25 100644 --- a/src/Umbraco.Core/Persistence/Mappers/UmbracoEntityMapper.cs +++ b/src/Umbraco.Core/Persistence/Mappers/UmbracoEntityMapper.cs @@ -24,6 +24,7 @@ namespace Umbraco.Core.Persistence.Mappers DefineMap(nameof(IUmbracoEntity.Trashed), nameof(NodeDto.Trashed)); DefineMap(nameof(IUmbracoEntity.Key), nameof(NodeDto.UniqueId)); DefineMap(nameof(IUmbracoEntity.CreatorId), nameof(NodeDto.UserId)); + DefineMap(nameof(IUmbracoEntity.NodeObjectType), nameof(NodeDto.NodeObjectType)); } } } diff --git a/src/Umbraco.Core/Persistence/Repositories/IEntityRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IEntityRepository.cs index 69f6ef4c5f..a6a1691347 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IEntityRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IEntityRepository.cs @@ -15,10 +15,22 @@ namespace Umbraco.Core.Persistence.Repositories IEntitySlim Get(int id, Guid objectTypeId); IEntitySlim Get(Guid key, Guid objectTypeId); - IEnumerable GetAll(Guid objectType, params int[] ids); + IEnumerable GetAll(Guid objectType, params int[] ids); IEnumerable GetAll(Guid objectType, params Guid[] keys); + /// + /// Gets entities for a query + /// + /// + /// IEnumerable GetByQuery(IQuery query); + + /// + /// Gets entities for a query and a specific object type allowing the query to be slightly more optimized + /// + /// + /// + /// IEnumerable GetByQuery(IQuery query, Guid objectType); UmbracoObjectTypes GetObjectType(int id); @@ -30,7 +42,36 @@ namespace Umbraco.Core.Persistence.Repositories bool Exists(int id); bool Exists(Guid key); + /// + /// Gets paged entities for a query and a subset of object types + /// + /// + /// + /// + /// + /// + /// + /// + /// A collection of mixed entity types which would be of type , , , + /// + /// + IEnumerable GetPagedResultsByQuery(IQuery query, Guid[] objectTypes, long pageIndex, int pageSize, out long totalRecords, + IQuery filter, Ordering ordering); + + /// + /// Gets paged entities for a query and a specific object type + /// + /// + /// + /// + /// + /// + /// + /// + /// IEnumerable GetPagedResultsByQuery(IQuery query, Guid objectType, long pageIndex, int pageSize, out long totalRecords, IQuery filter, Ordering ordering); + + } } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs index 161db543ba..2b68c8fe19 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs @@ -36,26 +36,70 @@ namespace Umbraco.Core.Persistence.Repositories.Implement #region Repository - // get a page of entities + //public IEnumerable GetPagedResults(IDictionary> typesAndIds, long pageIndex, int pageSize, out long totalRecords, Ordering ordering) + //{ + // var isContent = typesAndIds.Keys.Any(objectType => objectType == Constants.ObjectTypes.Document || objectType == Constants.ObjectTypes.DocumentBlueprint); + // var isMedia = typesAndIds.Keys.Any(objectType => objectType == Constants.ObjectTypes.Media); + // var isMember = typesAndIds.Keys.Any(objectType => objectType == Constants.ObjectTypes.Member); + + // var sql = GetBase(isContent, isMedia, isMember, null, false) + // .WhereIn(x => x.NodeObjectType, typesAndIds.Keys); + + // ordering = ordering ?? Ordering.ByDefault(); + + // sql = AddGroupBy(isContent, isMedia, isMember, sql, ordering.IsEmpty); + + // if (!ordering.IsEmpty) + // { + // // apply ordering + // ApplyOrdering(ref sql, ordering); + // } + + // // TODO: we should be able to do sql = sql.OrderBy(x => Alias(x.NodeId, "NodeId")); but we can't because the OrderBy extension don't support Alias currently + // //no matter what we always must have node id ordered at the end + // sql = ordering.Direction == Direction.Ascending ? sql.OrderBy("NodeId") : sql.OrderByDescending("NodeId"); + + // // for content we must query for ContentEntityDto entities to produce the correct culture variant entity names + // var pageIndexToFetch = pageIndex + 1; + // IEnumerable dtos; + // var page = Database.Page(pageIndexToFetch, pageSize, sql); + // dtos = page.Items; + // totalRecords = page.TotalItems; + + // var entities = dtos.Select(BuildEntity).ToArray(); + + // BuildVariants(entities.OfType()); + + // return entities; + //} + public IEnumerable GetPagedResultsByQuery(IQuery query, Guid objectType, long pageIndex, int pageSize, out long totalRecords, IQuery filter, Ordering ordering) { - var isContent = objectType == Constants.ObjectTypes.Document || objectType == Constants.ObjectTypes.DocumentBlueprint; - var isMedia = objectType == Constants.ObjectTypes.Media; - var isMember = objectType == Constants.ObjectTypes.Member; + query = query.Where(x => x.NodeObjectType == objectType); + return GetPagedResultsByQuery(query, new[] { objectType }, pageIndex, pageSize, out totalRecords, filter, ordering); + } + + // get a page of entities + public IEnumerable GetPagedResultsByQuery(IQuery query, Guid[] objectTypes, long pageIndex, int pageSize, out long totalRecords, + IQuery filter, Ordering ordering) + { + var isContent = objectTypes.Any(objectType => objectType == Constants.ObjectTypes.Document || objectType == Constants.ObjectTypes.DocumentBlueprint); + var isMedia = objectTypes.Any(objectType => objectType == Constants.ObjectTypes.Media); + var isMember = objectTypes.Any(objectType => objectType == Constants.ObjectTypes.Member); var sql = GetBaseWhere(isContent, isMedia, isMember, false, x => { if (filter == null) return; foreach (var filterClause in filter.GetWhereClauses()) x.Where(filterClause.Item1, filterClause.Item2); - }, objectType); + }, objectTypes); ordering = ordering ?? Ordering.ByDefault(); var translator = new SqlTranslator(sql, query); sql = translator.Translate(); - sql = AddGroupBy(isContent, isMedia, isMember, sql, ordering.IsEmpty); + sql = AddGroupBy(true, true, true, sql, ordering.IsEmpty); if (!ordering.IsEmpty) { @@ -70,35 +114,13 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // for content we must query for ContentEntityDto entities to produce the correct culture variant entity names var pageIndexToFetch = pageIndex + 1; IEnumerable dtos; - if(isContent) - { - var page = Database.Page(pageIndexToFetch, pageSize, sql); - dtos = page.Items; - totalRecords = page.TotalItems; - } - else if (isMedia) - { - var page = Database.Page(pageIndexToFetch, pageSize, sql); - dtos = page.Items; - totalRecords = page.TotalItems; - } - else if (isMember) - { - var page = Database.Page(pageIndexToFetch, pageSize, sql); - dtos = page.Items; - totalRecords = page.TotalItems; - } - else - { - var page = Database.Page(pageIndexToFetch, pageSize, sql); - dtos = page.Items; - totalRecords = page.TotalItems; - } + var page = Database.Page(pageIndexToFetch, pageSize, sql); + dtos = page.Items; + totalRecords = page.TotalItems; - var entities = dtos.Select(x => BuildEntity(isContent, isMedia, isMember, x)).ToArray(); + var entities = dtos.Select(BuildEntity).ToArray(); - if (isContent) - BuildVariants(entities.Cast()); + BuildVariants(entities.OfType()); return entities; } @@ -107,7 +129,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement { var sql = GetBaseWhere(false, false, false, false, key); var dto = Database.FirstOrDefault(sql); - return dto == null ? null : BuildEntity(false, false, false, dto); + return dto == null ? null : BuildEntity(dto); } @@ -116,7 +138,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement //isContent is going to return a 1:M result now with the variants so we need to do different things if (isContent) { - var cdtos = Database.Fetch(sql); + var cdtos = Database.Fetch(sql); return cdtos.Count == 0 ? null : BuildVariants(BuildDocumentEntity(cdtos[0])); } @@ -127,7 +149,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (dto == null) return null; - var entity = BuildEntity(false, isMedia, isMember, dto); + var entity = BuildEntity(dto); return entity; } @@ -146,7 +168,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement { var sql = GetBaseWhere(false, false, false, false, id); var dto = Database.FirstOrDefault(sql); - return dto == null ? null : BuildEntity(false, false, false, dto); + return dto == null ? null : BuildEntity(dto); } public IEntitySlim Get(int id, Guid objectTypeId) @@ -178,7 +200,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement //isContent is going to return a 1:M result now with the variants so we need to do different things if (isContent) { - var cdtos = Database.Fetch(sql); + var cdtos = Database.Fetch(sql); return cdtos.Count == 0 ? Enumerable.Empty() @@ -189,7 +211,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement ? (IEnumerable)Database.Fetch(sql) : Database.Fetch(sql); - var entities = dtos.Select(x => BuildEntity(false, isMedia, isMember, x)).ToArray(); + var entities = dtos.Select(BuildEntity).ToArray(); return entities; } @@ -233,7 +255,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var sql = translator.Translate(); sql = AddGroupBy(false, false, false, sql, true); var dtos = Database.Fetch(sql); - return dtos.Select(x => BuildEntity(false, false, false, x)).ToList(); + return dtos.Select(BuildEntity).ToList(); } public IEnumerable GetByQuery(IQuery query, Guid objectType) @@ -242,7 +264,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var isMedia = objectType == Constants.ObjectTypes.Media; var isMember = objectType == Constants.ObjectTypes.Member; - var sql = GetBaseWhere(isContent, isMedia, isMember, false, null, objectType); + var sql = GetBaseWhere(isContent, isMedia, isMember, false, null, new[] { objectType }); var translator = new SqlTranslator(sql, query); sql = translator.Translate(); @@ -356,14 +378,14 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // gets the full sql for a given object type, with a given filter protected Sql GetFullSqlForEntityType(bool isContent, bool isMedia, bool isMember, Guid objectType, Action> filter) { - var sql = GetBaseWhere(isContent, isMedia, isMember, false, filter, objectType); + var sql = GetBaseWhere(isContent, isMedia, isMember, false, filter, new[] { objectType }); return AddGroupBy(isContent, isMedia, isMember, sql, true); } // gets the base SELECT + FROM [+ filter] sql // always from the 'current' content version protected Sql GetBase(bool isContent, bool isMedia, bool isMember, Action> filter, bool isCount = false) - { + { var sql = Sql(); if (isCount) @@ -401,15 +423,15 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (isContent || isMedia || isMember) { sql - .InnerJoin().On((left, right) => left.NodeId == right.NodeId && right.Current) - .InnerJoin().On((left, right) => left.NodeId == right.NodeId) - .InnerJoin().On((left, right) => left.ContentTypeId == right.NodeId); + .LeftJoin().On((left, right) => left.NodeId == right.NodeId && right.Current) + .LeftJoin().On((left, right) => left.NodeId == right.NodeId) + .LeftJoin().On((left, right) => left.ContentTypeId == right.NodeId); } if (isContent) { sql - .InnerJoin().On((left, right) => left.NodeId == right.NodeId); + .LeftJoin().On((left, right) => left.NodeId == right.NodeId); } if (isMedia) @@ -433,10 +455,10 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // gets the base SELECT + FROM [+ filter] + WHERE sql // for a given object type, with a given filter - protected Sql GetBaseWhere(bool isContent, bool isMedia, bool isMember, bool isCount, Action> filter, Guid objectType) + protected Sql GetBaseWhere(bool isContent, bool isMedia, bool isMember, bool isCount, Action> filter, Guid[] objectTypes) { return GetBase(isContent, isMedia, isMember, filter, isCount) - .Where(x => x.NodeObjectType == objectType); + .WhereIn(x => x.NodeObjectType, objectTypes); } // gets the base SELECT + FROM + WHERE sql @@ -510,8 +532,13 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (sql == null) throw new ArgumentNullException(nameof(sql)); if (ordering == null) throw new ArgumentNullException(nameof(ordering)); - // TODO: although this works for name, it probably doesn't work for others without an alias of some sort - var orderBy = ordering.OrderBy; + // TODO: although the default ordering string works for name, it wont work for others without a table or an alias of some sort + // As more things are attempted to be sorted we'll prob have to add more expressions here + var orderBy = ordering.OrderBy.ToUpperInvariant() switch + { + "PATH" => SqlSyntax.GetQuotedColumn(NodeDto.TableName, "path"), + _ => ordering.OrderBy + }; if (ordering.Direction == Direction.Ascending) sql.OrderBy(orderBy); @@ -524,9 +551,17 @@ namespace Umbraco.Core.Persistence.Repositories.Implement #region Classes /// - /// The DTO used to fetch results for a content item with its variation info + /// The DTO used to fetch results for a generic content item which could be either a document, media or a member /// - private class ContentEntityDto : BaseDto + private class GenericContentEntityDto : DocumentEntityDto + { + public string MediaPath { get; set; } + } + + /// + /// The DTO used to fetch results for a document item with its variation info + /// + private class DocumentEntityDto : BaseDto { public ContentVariation Variations { get; set; } @@ -534,11 +569,17 @@ namespace Umbraco.Core.Persistence.Repositories.Implement public bool Edited { get; set; } } + /// + /// The DTO used to fetch results for a media item with its media path info + /// private class MediaEntityDto : BaseDto { public string MediaPath { get; set; } } + /// + /// The DTO used to fetch results for a member item + /// private class MemberEntityDto : BaseDto { } @@ -589,13 +630,13 @@ namespace Umbraco.Core.Persistence.Repositories.Implement #region Factory - private EntitySlim BuildEntity(bool isContent, bool isMedia, bool isMember, BaseDto dto) + private EntitySlim BuildEntity(BaseDto dto) { - if (isContent) + if (dto.NodeObjectType == Constants.ObjectTypes.Document) return BuildDocumentEntity(dto); - if (isMedia) + if (dto.NodeObjectType == Constants.ObjectTypes.Media) return BuildMediaEntity(dto); - if (isMember) + if (dto.NodeObjectType == Constants.ObjectTypes.Member) return BuildMemberEntity(dto); // EntitySlim does not track changes @@ -650,7 +691,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var entity = new DocumentEntitySlim(); BuildContentEntity(entity, dto); - if (dto is ContentEntityDto contentDto) + if (dto is DocumentEntityDto contentDto) { // fill in the invariant info entity.Edited = contentDto.Edited; diff --git a/src/Umbraco.Tests/Persistence/Repositories/EntityRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/EntityRepositoryTest.cs new file mode 100644 index 0000000000..a4df5bcf78 --- /dev/null +++ b/src/Umbraco.Tests/Persistence/Repositories/EntityRepositoryTest.cs @@ -0,0 +1,95 @@ +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Core.Models.Entities; +using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.Repositories.Implement; +using Umbraco.Core.Scoping; +using Umbraco.Tests.TestHelpers; +using Umbraco.Tests.TestHelpers.Entities; +using Umbraco.Tests.Testing; + +namespace Umbraco.Tests.Persistence.Repositories +{ + [TestFixture] + [UmbracoTest(Mapper = true, Database = UmbracoTestOptions.Database.NewSchemaPerTest)] + public class EntityRepositoryTest : TestWithDatabaseBase + { + + private EntityRepository CreateRepository(IScopeAccessor scopeAccessor) + { + var entityRepository = new EntityRepository(scopeAccessor); + return entityRepository; + } + + [Test] + public void Get_Paged_Mixed_Entities_By_Ids() + { + //Create content + + var createdContent = new List(); + var contentType = MockedContentTypes.CreateBasicContentType("blah"); + ServiceContext.ContentTypeService.Save(contentType); + for (int i = 0; i < 10; i++) + { + var c1 = MockedContent.CreateBasicContent(contentType); + ServiceContext.ContentService.Save(c1); + createdContent.Add(c1); + } + + //Create media + + var createdMedia = new List(); + var imageType = MockedContentTypes.CreateImageMediaType("myImage"); + ServiceContext.MediaTypeService.Save(imageType); + for (int i = 0; i < 10; i++) + { + var c1 = MockedMedia.CreateMediaImage(imageType, -1); + ServiceContext.MediaService.Save(c1); + createdMedia.Add(c1); + } + + // Create members + var memberType = MockedContentTypes.CreateSimpleMemberType("simple"); + ServiceContext.MemberTypeService.Save(memberType); + var createdMembers = MockedMember.CreateSimpleMember(memberType, 10).ToList(); + ServiceContext.MemberService.Save(createdMembers); + + var provider = TestObjects.GetScopeProvider(Logger); + using (var scope = provider.CreateScope()) + { + var repo = CreateRepository((IScopeAccessor)provider); + + var ids = createdContent.Select(x => x.Id).Concat(createdMedia.Select(x => x.Id)).Concat(createdMembers.Select(x => x.Id)); + + var objectTypes = new[] { Constants.ObjectTypes.Document, Constants.ObjectTypes.Media, Constants.ObjectTypes.Member }; + + var query = SqlContext.Query() + .WhereIn(e => e.Id, ids); + + var entities = repo.GetPagedResultsByQuery(query, objectTypes, 0, 20, out var totalRecords, null, null).ToList(); + + Assert.AreEqual(20, entities.Count); + Assert.AreEqual(30, totalRecords); + + //add the next page + entities.AddRange(repo.GetPagedResultsByQuery(query, objectTypes, 1, 20, out totalRecords, null, null)); + + Assert.AreEqual(30, entities.Count); + Assert.AreEqual(30, totalRecords); + + var contentEntities = entities.OfType().ToList(); + var mediaEntities = entities.OfType().ToList(); + var memberEntities = entities.OfType().ToList(); + + Assert.AreEqual(10, contentEntities.Count); + Assert.AreEqual(10, mediaEntities.Count); + Assert.AreEqual(10, memberEntities.Count); + } + + } + + } +} diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 4b035f631e..c87e6501f9 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -140,6 +140,7 @@ + diff --git a/src/Umbraco.Web/Editors/EntityController.cs b/src/Umbraco.Web/Editors/EntityController.cs index 0513017b70..11b1260e71 100644 --- a/src/Umbraco.Web/Editors/EntityController.cs +++ b/src/Umbraco.Web/Editors/EntityController.cs @@ -662,6 +662,9 @@ namespace Umbraco.Web.Editors if (pageSize <= 0) throw new HttpResponseException(HttpStatusCode.NotFound); + // re-normalize since NULL can be passed in + filter = filter ?? string.Empty; + var objectType = ConvertToObjectType(type); if (objectType.HasValue) { diff --git a/src/Umbraco.Web/Models/Mapping/EntityMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/EntityMapDefinition.cs index 2e44f1327b..afd1a6b61c 100644 --- a/src/Umbraco.Web/Models/Mapping/EntityMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/EntityMapDefinition.cs @@ -226,11 +226,11 @@ namespace Umbraco.Web.Models.Mapping { switch (entity) { - case ContentEntitySlim contentEntity: - // NOTE: this case covers both content and media entities - return contentEntity.ContentTypeIcon; - case MemberEntitySlim memberEntity: + case IMemberEntitySlim memberEntity: return memberEntity.ContentTypeIcon.IfNullOrWhiteSpace(Constants.Icons.Member); + case IContentEntitySlim contentEntity: + // NOTE: this case covers both content and media entities + return contentEntity.ContentTypeIcon; } return null; diff --git a/src/Umbraco.Web/Models/Mapping/UserMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/UserMapDefinition.cs index 88960fb189..aa158799cb 100644 --- a/src/Umbraco.Web/Models/Mapping/UserMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/UserMapDefinition.cs @@ -380,8 +380,8 @@ namespace Umbraco.Web.Models.Mapping .ToDictionary(x => x.Key, x => (IEnumerable)x.ToArray()); } - private static string MapContentTypeIcon(EntitySlim entity) - => entity is ContentEntitySlim contentEntity ? contentEntity.ContentTypeIcon : null; + private static string MapContentTypeIcon(IEntitySlim entity) + => entity is IContentEntitySlim contentEntity ? contentEntity.ContentTypeIcon : null; private IEnumerable GetStartNodes(int[] startNodeIds, UmbracoObjectTypes objectType, string localizedKey, MapperContext context) { diff --git a/src/Umbraco.Web/Trees/ContentBlueprintTreeController.cs b/src/Umbraco.Web/Trees/ContentBlueprintTreeController.cs index ac75fd831d..fd05f7cfbd 100644 --- a/src/Umbraco.Web/Trees/ContentBlueprintTreeController.cs +++ b/src/Umbraco.Web/Trees/ContentBlueprintTreeController.cs @@ -47,7 +47,7 @@ namespace Umbraco.Web.Trees if (id == Constants.System.RootString) { //get all blueprint content types - var contentTypeAliases = entities.Select(x => ((ContentEntitySlim) x).ContentTypeAlias).Distinct(); + var contentTypeAliases = entities.Select(x => ((IContentEntitySlim) x).ContentTypeAlias).Distinct(); //get the ids var contentTypeIds = Services.ContentTypeService.GetAllContentTypeIds(contentTypeAliases.ToArray()).ToArray(); @@ -75,7 +75,7 @@ namespace Umbraco.Web.Trees var ct = Services.ContentTypeService.Get(intId.Result); if (ct == null) return nodes; - var blueprintsForDocType = entities.Where(x => ct.Alias == ((ContentEntitySlim) x).ContentTypeAlias); + var blueprintsForDocType = entities.Where(x => ct.Alias == ((IContentEntitySlim) x).ContentTypeAlias); nodes.AddRange(blueprintsForDocType .Select(entity => { From 602acce8f49797b870548857df2895836d75f1bd Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 5 Nov 2019 15:05:51 +1100 Subject: [PATCH 049/202] Gets entity repository to be able to return a mix of object types --- src/Umbraco.Core/Models/ContentBase.cs | 3 + src/Umbraco.Core/Models/ContentTypeBase.cs | 3 + src/Umbraco.Core/Models/DataType.cs | 3 + .../Models/Entities/ITreeEntity.cs | 4 +- .../Models/Entities/IUmbracoEntity.cs | 7 +- src/Umbraco.Core/Models/EntityContainer.cs | 2 + src/Umbraco.Core/Models/SimpleContentType.cs | 3 + .../Factories/ContentBaseFactory.cs | 2 +- .../Factories/ContentTypeFactory.cs | 1 + .../Persistence/Factories/DataTypeFactory.cs | 1 + .../Mappers/UmbracoEntityMapper.cs | 1 + .../Repositories/IEntityRepository.cs | 43 ++++- .../Implement/EntityRepository.cs | 157 +++++++++++------- .../Repositories/EntityRepositoryTest.cs | 95 +++++++++++ src/Umbraco.Tests/Umbraco.Tests.csproj | 1 + src/Umbraco.Web/Editors/EntityController.cs | 3 + .../Models/Mapping/EntityMapDefinition.cs | 8 +- .../Models/Mapping/UserMapDefinition.cs | 4 +- .../Trees/ContentBlueprintTreeController.cs | 4 +- 19 files changed, 273 insertions(+), 72 deletions(-) create mode 100644 src/Umbraco.Tests/Persistence/Repositories/EntityRepositoryTest.cs diff --git a/src/Umbraco.Core/Models/ContentBase.cs b/src/Umbraco.Core/Models/ContentBase.cs index fbb68194b7..2ea0fd855b 100644 --- a/src/Umbraco.Core/Models/ContentBase.cs +++ b/src/Umbraco.Core/Models/ContentBase.cs @@ -108,6 +108,9 @@ namespace Umbraco.Core.Models [IgnoreDataMember] public int VersionId { get; set; } + [DataMember] + public Guid NodeObjectType { get; internal set; } + /// /// Integer Id of the default ContentType /// diff --git a/src/Umbraco.Core/Models/ContentTypeBase.cs b/src/Umbraco.Core/Models/ContentTypeBase.cs index 04bcb7424a..1f3f1a5c13 100644 --- a/src/Umbraco.Core/Models/ContentTypeBase.cs +++ b/src/Umbraco.Core/Models/ContentTypeBase.cs @@ -113,6 +113,9 @@ namespace Umbraco.Core.Models OnPropertyChanged(nameof(PropertyTypes)); } + [DataMember] + public Guid NodeObjectType { get; internal set; } + /// /// The Alias of the ContentType /// diff --git a/src/Umbraco.Core/Models/DataType.cs b/src/Umbraco.Core/Models/DataType.cs index c237f6381c..cba53bbd10 100644 --- a/src/Umbraco.Core/Models/DataType.cs +++ b/src/Umbraco.Core/Models/DataType.cs @@ -65,6 +65,9 @@ namespace Umbraco.Core.Models [DataMember] public string EditorAlias => _editor.Alias; + [DataMember] + public Guid NodeObjectType { get; internal set; } + /// [DataMember] public ValueStorageType DatabaseType diff --git a/src/Umbraco.Core/Models/Entities/ITreeEntity.cs b/src/Umbraco.Core/Models/Entities/ITreeEntity.cs index afa3399202..ab63e1e1d8 100644 --- a/src/Umbraco.Core/Models/Entities/ITreeEntity.cs +++ b/src/Umbraco.Core/Models/Entities/ITreeEntity.cs @@ -24,7 +24,7 @@ /// Sets the parent entity. ///
/// Use this method to set the parent entity when the parent entity is known, but has not - /// been persistent and does not yet have an identity. The parent identifier will we retrieved + /// been persistent and does not yet have an identity. The parent identifier will be retrieved /// from the parent entity when needed. If the parent entity still does not have an entity by that /// time, an exception will be thrown by getter. void SetParent(ITreeEntity parent); @@ -53,4 +53,4 @@ /// bool Trashed { get; } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Core/Models/Entities/IUmbracoEntity.cs b/src/Umbraco.Core/Models/Entities/IUmbracoEntity.cs index e5f628b098..13087b5e95 100644 --- a/src/Umbraco.Core/Models/Entities/IUmbracoEntity.cs +++ b/src/Umbraco.Core/Models/Entities/IUmbracoEntity.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; namespace Umbraco.Core.Models.Entities { @@ -11,5 +12,7 @@ namespace Umbraco.Core.Models.Entities /// An IUmbracoEntity can participate in notifications. /// public interface IUmbracoEntity : ITreeEntity, IRememberBeingDirty - { } + { + Guid NodeObjectType { get; } + } } diff --git a/src/Umbraco.Core/Models/EntityContainer.cs b/src/Umbraco.Core/Models/EntityContainer.cs index 70f6cbd878..71a477a9d6 100644 --- a/src/Umbraco.Core/Models/EntityContainer.cs +++ b/src/Umbraco.Core/Models/EntityContainer.cs @@ -62,6 +62,8 @@ namespace Umbraco.Core.Models ///
public Guid ContainerObjectType => ObjectTypeMap[_containedObjectType]; + Guid IUmbracoEntity.NodeObjectType => ContainerObjectType; + /// /// Gets the container object type corresponding to a contained object type. /// diff --git a/src/Umbraco.Core/Models/SimpleContentType.cs b/src/Umbraco.Core/Models/SimpleContentType.cs index 5c81017ec8..b4e7688eea 100644 --- a/src/Umbraco.Core/Models/SimpleContentType.cs +++ b/src/Umbraco.Core/Models/SimpleContentType.cs @@ -44,11 +44,14 @@ namespace Umbraco.Core.Models Name = contentType.Name; AllowedAsRoot = contentType.AllowedAsRoot; IsElement = contentType.IsElement; + NodeObjectType = contentType.NodeObjectType; } /// public string Alias { get; } + public Guid NodeObjectType { get; } + public int Id { get; } /// diff --git a/src/Umbraco.Core/Persistence/Factories/ContentBaseFactory.cs b/src/Umbraco.Core/Persistence/Factories/ContentBaseFactory.cs index 434e0393cd..25cc6bc358 100644 --- a/src/Umbraco.Core/Persistence/Factories/ContentBaseFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/ContentBaseFactory.cs @@ -34,7 +34,7 @@ namespace Umbraco.Core.Persistence.Factories content.VersionId = contentVersionDto.Id; content.Name = contentVersionDto.Text; - + content.NodeObjectType = nodeDto.NodeObjectType ?? Guid.Empty; content.Path = nodeDto.Path; content.Level = nodeDto.Level; content.ParentId = nodeDto.ParentId; diff --git a/src/Umbraco.Core/Persistence/Factories/ContentTypeFactory.cs b/src/Umbraco.Core/Persistence/Factories/ContentTypeFactory.cs index 54cfee0ffa..46489aa69e 100644 --- a/src/Umbraco.Core/Persistence/Factories/ContentTypeFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/ContentTypeFactory.cs @@ -107,6 +107,7 @@ namespace Umbraco.Core.Persistence.Factories { entity.Id = dto.NodeDto.NodeId; entity.Key = dto.NodeDto.UniqueId; + entity.NodeObjectType = dto.NodeDto.NodeObjectType ?? Guid.Empty; entity.Alias = dto.Alias; entity.Name = dto.NodeDto.Text; entity.Icon = dto.Icon; diff --git a/src/Umbraco.Core/Persistence/Factories/DataTypeFactory.cs b/src/Umbraco.Core/Persistence/Factories/DataTypeFactory.cs index f189d38d05..5d92234ca8 100644 --- a/src/Umbraco.Core/Persistence/Factories/DataTypeFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/DataTypeFactory.cs @@ -28,6 +28,7 @@ namespace Umbraco.Core.Persistence.Factories dataType.DisableChangeTracking(); dataType.CreateDate = dto.NodeDto.CreateDate; + dataType.NodeObjectType = dto.NodeDto.NodeObjectType ?? Guid.Empty; dataType.DatabaseType = dto.DbType.EnumParse(true); dataType.Id = dto.NodeId; dataType.Key = dto.NodeDto.UniqueId; diff --git a/src/Umbraco.Core/Persistence/Mappers/UmbracoEntityMapper.cs b/src/Umbraco.Core/Persistence/Mappers/UmbracoEntityMapper.cs index 16e2e8bcd5..96f664cc25 100644 --- a/src/Umbraco.Core/Persistence/Mappers/UmbracoEntityMapper.cs +++ b/src/Umbraco.Core/Persistence/Mappers/UmbracoEntityMapper.cs @@ -24,6 +24,7 @@ namespace Umbraco.Core.Persistence.Mappers DefineMap(nameof(IUmbracoEntity.Trashed), nameof(NodeDto.Trashed)); DefineMap(nameof(IUmbracoEntity.Key), nameof(NodeDto.UniqueId)); DefineMap(nameof(IUmbracoEntity.CreatorId), nameof(NodeDto.UserId)); + DefineMap(nameof(IUmbracoEntity.NodeObjectType), nameof(NodeDto.NodeObjectType)); } } } diff --git a/src/Umbraco.Core/Persistence/Repositories/IEntityRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IEntityRepository.cs index 69f6ef4c5f..a6a1691347 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IEntityRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IEntityRepository.cs @@ -15,10 +15,22 @@ namespace Umbraco.Core.Persistence.Repositories IEntitySlim Get(int id, Guid objectTypeId); IEntitySlim Get(Guid key, Guid objectTypeId); - IEnumerable GetAll(Guid objectType, params int[] ids); + IEnumerable GetAll(Guid objectType, params int[] ids); IEnumerable GetAll(Guid objectType, params Guid[] keys); + /// + /// Gets entities for a query + /// + /// + /// IEnumerable GetByQuery(IQuery query); + + /// + /// Gets entities for a query and a specific object type allowing the query to be slightly more optimized + /// + /// + /// + /// IEnumerable GetByQuery(IQuery query, Guid objectType); UmbracoObjectTypes GetObjectType(int id); @@ -30,7 +42,36 @@ namespace Umbraco.Core.Persistence.Repositories bool Exists(int id); bool Exists(Guid key); + /// + /// Gets paged entities for a query and a subset of object types + /// + /// + /// + /// + /// + /// + /// + /// + /// A collection of mixed entity types which would be of type , , , + /// + /// + IEnumerable GetPagedResultsByQuery(IQuery query, Guid[] objectTypes, long pageIndex, int pageSize, out long totalRecords, + IQuery filter, Ordering ordering); + + /// + /// Gets paged entities for a query and a specific object type + /// + /// + /// + /// + /// + /// + /// + /// + /// IEnumerable GetPagedResultsByQuery(IQuery query, Guid objectType, long pageIndex, int pageSize, out long totalRecords, IQuery filter, Ordering ordering); + + } } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs index 161db543ba..2b68c8fe19 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs @@ -36,26 +36,70 @@ namespace Umbraco.Core.Persistence.Repositories.Implement #region Repository - // get a page of entities + //public IEnumerable GetPagedResults(IDictionary> typesAndIds, long pageIndex, int pageSize, out long totalRecords, Ordering ordering) + //{ + // var isContent = typesAndIds.Keys.Any(objectType => objectType == Constants.ObjectTypes.Document || objectType == Constants.ObjectTypes.DocumentBlueprint); + // var isMedia = typesAndIds.Keys.Any(objectType => objectType == Constants.ObjectTypes.Media); + // var isMember = typesAndIds.Keys.Any(objectType => objectType == Constants.ObjectTypes.Member); + + // var sql = GetBase(isContent, isMedia, isMember, null, false) + // .WhereIn(x => x.NodeObjectType, typesAndIds.Keys); + + // ordering = ordering ?? Ordering.ByDefault(); + + // sql = AddGroupBy(isContent, isMedia, isMember, sql, ordering.IsEmpty); + + // if (!ordering.IsEmpty) + // { + // // apply ordering + // ApplyOrdering(ref sql, ordering); + // } + + // // TODO: we should be able to do sql = sql.OrderBy(x => Alias(x.NodeId, "NodeId")); but we can't because the OrderBy extension don't support Alias currently + // //no matter what we always must have node id ordered at the end + // sql = ordering.Direction == Direction.Ascending ? sql.OrderBy("NodeId") : sql.OrderByDescending("NodeId"); + + // // for content we must query for ContentEntityDto entities to produce the correct culture variant entity names + // var pageIndexToFetch = pageIndex + 1; + // IEnumerable dtos; + // var page = Database.Page(pageIndexToFetch, pageSize, sql); + // dtos = page.Items; + // totalRecords = page.TotalItems; + + // var entities = dtos.Select(BuildEntity).ToArray(); + + // BuildVariants(entities.OfType()); + + // return entities; + //} + public IEnumerable GetPagedResultsByQuery(IQuery query, Guid objectType, long pageIndex, int pageSize, out long totalRecords, IQuery filter, Ordering ordering) { - var isContent = objectType == Constants.ObjectTypes.Document || objectType == Constants.ObjectTypes.DocumentBlueprint; - var isMedia = objectType == Constants.ObjectTypes.Media; - var isMember = objectType == Constants.ObjectTypes.Member; + query = query.Where(x => x.NodeObjectType == objectType); + return GetPagedResultsByQuery(query, new[] { objectType }, pageIndex, pageSize, out totalRecords, filter, ordering); + } + + // get a page of entities + public IEnumerable GetPagedResultsByQuery(IQuery query, Guid[] objectTypes, long pageIndex, int pageSize, out long totalRecords, + IQuery filter, Ordering ordering) + { + var isContent = objectTypes.Any(objectType => objectType == Constants.ObjectTypes.Document || objectType == Constants.ObjectTypes.DocumentBlueprint); + var isMedia = objectTypes.Any(objectType => objectType == Constants.ObjectTypes.Media); + var isMember = objectTypes.Any(objectType => objectType == Constants.ObjectTypes.Member); var sql = GetBaseWhere(isContent, isMedia, isMember, false, x => { if (filter == null) return; foreach (var filterClause in filter.GetWhereClauses()) x.Where(filterClause.Item1, filterClause.Item2); - }, objectType); + }, objectTypes); ordering = ordering ?? Ordering.ByDefault(); var translator = new SqlTranslator(sql, query); sql = translator.Translate(); - sql = AddGroupBy(isContent, isMedia, isMember, sql, ordering.IsEmpty); + sql = AddGroupBy(true, true, true, sql, ordering.IsEmpty); if (!ordering.IsEmpty) { @@ -70,35 +114,13 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // for content we must query for ContentEntityDto entities to produce the correct culture variant entity names var pageIndexToFetch = pageIndex + 1; IEnumerable dtos; - if(isContent) - { - var page = Database.Page(pageIndexToFetch, pageSize, sql); - dtos = page.Items; - totalRecords = page.TotalItems; - } - else if (isMedia) - { - var page = Database.Page(pageIndexToFetch, pageSize, sql); - dtos = page.Items; - totalRecords = page.TotalItems; - } - else if (isMember) - { - var page = Database.Page(pageIndexToFetch, pageSize, sql); - dtos = page.Items; - totalRecords = page.TotalItems; - } - else - { - var page = Database.Page(pageIndexToFetch, pageSize, sql); - dtos = page.Items; - totalRecords = page.TotalItems; - } + var page = Database.Page(pageIndexToFetch, pageSize, sql); + dtos = page.Items; + totalRecords = page.TotalItems; - var entities = dtos.Select(x => BuildEntity(isContent, isMedia, isMember, x)).ToArray(); + var entities = dtos.Select(BuildEntity).ToArray(); - if (isContent) - BuildVariants(entities.Cast()); + BuildVariants(entities.OfType()); return entities; } @@ -107,7 +129,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement { var sql = GetBaseWhere(false, false, false, false, key); var dto = Database.FirstOrDefault(sql); - return dto == null ? null : BuildEntity(false, false, false, dto); + return dto == null ? null : BuildEntity(dto); } @@ -116,7 +138,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement //isContent is going to return a 1:M result now with the variants so we need to do different things if (isContent) { - var cdtos = Database.Fetch(sql); + var cdtos = Database.Fetch(sql); return cdtos.Count == 0 ? null : BuildVariants(BuildDocumentEntity(cdtos[0])); } @@ -127,7 +149,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (dto == null) return null; - var entity = BuildEntity(false, isMedia, isMember, dto); + var entity = BuildEntity(dto); return entity; } @@ -146,7 +168,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement { var sql = GetBaseWhere(false, false, false, false, id); var dto = Database.FirstOrDefault(sql); - return dto == null ? null : BuildEntity(false, false, false, dto); + return dto == null ? null : BuildEntity(dto); } public IEntitySlim Get(int id, Guid objectTypeId) @@ -178,7 +200,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement //isContent is going to return a 1:M result now with the variants so we need to do different things if (isContent) { - var cdtos = Database.Fetch(sql); + var cdtos = Database.Fetch(sql); return cdtos.Count == 0 ? Enumerable.Empty() @@ -189,7 +211,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement ? (IEnumerable)Database.Fetch(sql) : Database.Fetch(sql); - var entities = dtos.Select(x => BuildEntity(false, isMedia, isMember, x)).ToArray(); + var entities = dtos.Select(BuildEntity).ToArray(); return entities; } @@ -233,7 +255,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var sql = translator.Translate(); sql = AddGroupBy(false, false, false, sql, true); var dtos = Database.Fetch(sql); - return dtos.Select(x => BuildEntity(false, false, false, x)).ToList(); + return dtos.Select(BuildEntity).ToList(); } public IEnumerable GetByQuery(IQuery query, Guid objectType) @@ -242,7 +264,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var isMedia = objectType == Constants.ObjectTypes.Media; var isMember = objectType == Constants.ObjectTypes.Member; - var sql = GetBaseWhere(isContent, isMedia, isMember, false, null, objectType); + var sql = GetBaseWhere(isContent, isMedia, isMember, false, null, new[] { objectType }); var translator = new SqlTranslator(sql, query); sql = translator.Translate(); @@ -356,14 +378,14 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // gets the full sql for a given object type, with a given filter protected Sql GetFullSqlForEntityType(bool isContent, bool isMedia, bool isMember, Guid objectType, Action> filter) { - var sql = GetBaseWhere(isContent, isMedia, isMember, false, filter, objectType); + var sql = GetBaseWhere(isContent, isMedia, isMember, false, filter, new[] { objectType }); return AddGroupBy(isContent, isMedia, isMember, sql, true); } // gets the base SELECT + FROM [+ filter] sql // always from the 'current' content version protected Sql GetBase(bool isContent, bool isMedia, bool isMember, Action> filter, bool isCount = false) - { + { var sql = Sql(); if (isCount) @@ -401,15 +423,15 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (isContent || isMedia || isMember) { sql - .InnerJoin().On((left, right) => left.NodeId == right.NodeId && right.Current) - .InnerJoin().On((left, right) => left.NodeId == right.NodeId) - .InnerJoin().On((left, right) => left.ContentTypeId == right.NodeId); + .LeftJoin().On((left, right) => left.NodeId == right.NodeId && right.Current) + .LeftJoin().On((left, right) => left.NodeId == right.NodeId) + .LeftJoin().On((left, right) => left.ContentTypeId == right.NodeId); } if (isContent) { sql - .InnerJoin().On((left, right) => left.NodeId == right.NodeId); + .LeftJoin().On((left, right) => left.NodeId == right.NodeId); } if (isMedia) @@ -433,10 +455,10 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // gets the base SELECT + FROM [+ filter] + WHERE sql // for a given object type, with a given filter - protected Sql GetBaseWhere(bool isContent, bool isMedia, bool isMember, bool isCount, Action> filter, Guid objectType) + protected Sql GetBaseWhere(bool isContent, bool isMedia, bool isMember, bool isCount, Action> filter, Guid[] objectTypes) { return GetBase(isContent, isMedia, isMember, filter, isCount) - .Where(x => x.NodeObjectType == objectType); + .WhereIn(x => x.NodeObjectType, objectTypes); } // gets the base SELECT + FROM + WHERE sql @@ -510,8 +532,13 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (sql == null) throw new ArgumentNullException(nameof(sql)); if (ordering == null) throw new ArgumentNullException(nameof(ordering)); - // TODO: although this works for name, it probably doesn't work for others without an alias of some sort - var orderBy = ordering.OrderBy; + // TODO: although the default ordering string works for name, it wont work for others without a table or an alias of some sort + // As more things are attempted to be sorted we'll prob have to add more expressions here + var orderBy = ordering.OrderBy.ToUpperInvariant() switch + { + "PATH" => SqlSyntax.GetQuotedColumn(NodeDto.TableName, "path"), + _ => ordering.OrderBy + }; if (ordering.Direction == Direction.Ascending) sql.OrderBy(orderBy); @@ -524,9 +551,17 @@ namespace Umbraco.Core.Persistence.Repositories.Implement #region Classes /// - /// The DTO used to fetch results for a content item with its variation info + /// The DTO used to fetch results for a generic content item which could be either a document, media or a member /// - private class ContentEntityDto : BaseDto + private class GenericContentEntityDto : DocumentEntityDto + { + public string MediaPath { get; set; } + } + + /// + /// The DTO used to fetch results for a document item with its variation info + /// + private class DocumentEntityDto : BaseDto { public ContentVariation Variations { get; set; } @@ -534,11 +569,17 @@ namespace Umbraco.Core.Persistence.Repositories.Implement public bool Edited { get; set; } } + /// + /// The DTO used to fetch results for a media item with its media path info + /// private class MediaEntityDto : BaseDto { public string MediaPath { get; set; } } + /// + /// The DTO used to fetch results for a member item + /// private class MemberEntityDto : BaseDto { } @@ -589,13 +630,13 @@ namespace Umbraco.Core.Persistence.Repositories.Implement #region Factory - private EntitySlim BuildEntity(bool isContent, bool isMedia, bool isMember, BaseDto dto) + private EntitySlim BuildEntity(BaseDto dto) { - if (isContent) + if (dto.NodeObjectType == Constants.ObjectTypes.Document) return BuildDocumentEntity(dto); - if (isMedia) + if (dto.NodeObjectType == Constants.ObjectTypes.Media) return BuildMediaEntity(dto); - if (isMember) + if (dto.NodeObjectType == Constants.ObjectTypes.Member) return BuildMemberEntity(dto); // EntitySlim does not track changes @@ -650,7 +691,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var entity = new DocumentEntitySlim(); BuildContentEntity(entity, dto); - if (dto is ContentEntityDto contentDto) + if (dto is DocumentEntityDto contentDto) { // fill in the invariant info entity.Edited = contentDto.Edited; diff --git a/src/Umbraco.Tests/Persistence/Repositories/EntityRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/EntityRepositoryTest.cs new file mode 100644 index 0000000000..a4df5bcf78 --- /dev/null +++ b/src/Umbraco.Tests/Persistence/Repositories/EntityRepositoryTest.cs @@ -0,0 +1,95 @@ +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Core.Models.Entities; +using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.Repositories.Implement; +using Umbraco.Core.Scoping; +using Umbraco.Tests.TestHelpers; +using Umbraco.Tests.TestHelpers.Entities; +using Umbraco.Tests.Testing; + +namespace Umbraco.Tests.Persistence.Repositories +{ + [TestFixture] + [UmbracoTest(Mapper = true, Database = UmbracoTestOptions.Database.NewSchemaPerTest)] + public class EntityRepositoryTest : TestWithDatabaseBase + { + + private EntityRepository CreateRepository(IScopeAccessor scopeAccessor) + { + var entityRepository = new EntityRepository(scopeAccessor); + return entityRepository; + } + + [Test] + public void Get_Paged_Mixed_Entities_By_Ids() + { + //Create content + + var createdContent = new List(); + var contentType = MockedContentTypes.CreateBasicContentType("blah"); + ServiceContext.ContentTypeService.Save(contentType); + for (int i = 0; i < 10; i++) + { + var c1 = MockedContent.CreateBasicContent(contentType); + ServiceContext.ContentService.Save(c1); + createdContent.Add(c1); + } + + //Create media + + var createdMedia = new List(); + var imageType = MockedContentTypes.CreateImageMediaType("myImage"); + ServiceContext.MediaTypeService.Save(imageType); + for (int i = 0; i < 10; i++) + { + var c1 = MockedMedia.CreateMediaImage(imageType, -1); + ServiceContext.MediaService.Save(c1); + createdMedia.Add(c1); + } + + // Create members + var memberType = MockedContentTypes.CreateSimpleMemberType("simple"); + ServiceContext.MemberTypeService.Save(memberType); + var createdMembers = MockedMember.CreateSimpleMember(memberType, 10).ToList(); + ServiceContext.MemberService.Save(createdMembers); + + var provider = TestObjects.GetScopeProvider(Logger); + using (var scope = provider.CreateScope()) + { + var repo = CreateRepository((IScopeAccessor)provider); + + var ids = createdContent.Select(x => x.Id).Concat(createdMedia.Select(x => x.Id)).Concat(createdMembers.Select(x => x.Id)); + + var objectTypes = new[] { Constants.ObjectTypes.Document, Constants.ObjectTypes.Media, Constants.ObjectTypes.Member }; + + var query = SqlContext.Query() + .WhereIn(e => e.Id, ids); + + var entities = repo.GetPagedResultsByQuery(query, objectTypes, 0, 20, out var totalRecords, null, null).ToList(); + + Assert.AreEqual(20, entities.Count); + Assert.AreEqual(30, totalRecords); + + //add the next page + entities.AddRange(repo.GetPagedResultsByQuery(query, objectTypes, 1, 20, out totalRecords, null, null)); + + Assert.AreEqual(30, entities.Count); + Assert.AreEqual(30, totalRecords); + + var contentEntities = entities.OfType().ToList(); + var mediaEntities = entities.OfType().ToList(); + var memberEntities = entities.OfType().ToList(); + + Assert.AreEqual(10, contentEntities.Count); + Assert.AreEqual(10, mediaEntities.Count); + Assert.AreEqual(10, memberEntities.Count); + } + + } + + } +} diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 4b035f631e..c87e6501f9 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -140,6 +140,7 @@ + diff --git a/src/Umbraco.Web/Editors/EntityController.cs b/src/Umbraco.Web/Editors/EntityController.cs index 0513017b70..11b1260e71 100644 --- a/src/Umbraco.Web/Editors/EntityController.cs +++ b/src/Umbraco.Web/Editors/EntityController.cs @@ -662,6 +662,9 @@ namespace Umbraco.Web.Editors if (pageSize <= 0) throw new HttpResponseException(HttpStatusCode.NotFound); + // re-normalize since NULL can be passed in + filter = filter ?? string.Empty; + var objectType = ConvertToObjectType(type); if (objectType.HasValue) { diff --git a/src/Umbraco.Web/Models/Mapping/EntityMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/EntityMapDefinition.cs index 2e44f1327b..afd1a6b61c 100644 --- a/src/Umbraco.Web/Models/Mapping/EntityMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/EntityMapDefinition.cs @@ -226,11 +226,11 @@ namespace Umbraco.Web.Models.Mapping { switch (entity) { - case ContentEntitySlim contentEntity: - // NOTE: this case covers both content and media entities - return contentEntity.ContentTypeIcon; - case MemberEntitySlim memberEntity: + case IMemberEntitySlim memberEntity: return memberEntity.ContentTypeIcon.IfNullOrWhiteSpace(Constants.Icons.Member); + case IContentEntitySlim contentEntity: + // NOTE: this case covers both content and media entities + return contentEntity.ContentTypeIcon; } return null; diff --git a/src/Umbraco.Web/Models/Mapping/UserMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/UserMapDefinition.cs index 88960fb189..aa158799cb 100644 --- a/src/Umbraco.Web/Models/Mapping/UserMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/UserMapDefinition.cs @@ -380,8 +380,8 @@ namespace Umbraco.Web.Models.Mapping .ToDictionary(x => x.Key, x => (IEnumerable)x.ToArray()); } - private static string MapContentTypeIcon(EntitySlim entity) - => entity is ContentEntitySlim contentEntity ? contentEntity.ContentTypeIcon : null; + private static string MapContentTypeIcon(IEntitySlim entity) + => entity is IContentEntitySlim contentEntity ? contentEntity.ContentTypeIcon : null; private IEnumerable GetStartNodes(int[] startNodeIds, UmbracoObjectTypes objectType, string localizedKey, MapperContext context) { diff --git a/src/Umbraco.Web/Trees/ContentBlueprintTreeController.cs b/src/Umbraco.Web/Trees/ContentBlueprintTreeController.cs index ac75fd831d..fd05f7cfbd 100644 --- a/src/Umbraco.Web/Trees/ContentBlueprintTreeController.cs +++ b/src/Umbraco.Web/Trees/ContentBlueprintTreeController.cs @@ -47,7 +47,7 @@ namespace Umbraco.Web.Trees if (id == Constants.System.RootString) { //get all blueprint content types - var contentTypeAliases = entities.Select(x => ((ContentEntitySlim) x).ContentTypeAlias).Distinct(); + var contentTypeAliases = entities.Select(x => ((IContentEntitySlim) x).ContentTypeAlias).Distinct(); //get the ids var contentTypeIds = Services.ContentTypeService.GetAllContentTypeIds(contentTypeAliases.ToArray()).ToArray(); @@ -75,7 +75,7 @@ namespace Umbraco.Web.Trees var ct = Services.ContentTypeService.Get(intId.Result); if (ct == null) return nodes; - var blueprintsForDocType = entities.Where(x => ct.Alias == ((ContentEntitySlim) x).ContentTypeAlias); + var blueprintsForDocType = entities.Where(x => ct.Alias == ((IContentEntitySlim) x).ContentTypeAlias); nodes.AddRange(blueprintsForDocType .Select(entity => { From f7e8a53922f074581b2106e108b3d10a20a0a30d Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 5 Nov 2019 15:53:50 +1100 Subject: [PATCH 050/202] Revert "Gets entity repository to be able to return a mix of object types" This reverts commit fff3d2648f71b375a6bec6624b140cce08a3e8e2. --- src/Umbraco.Core/Models/ContentBase.cs | 3 - src/Umbraco.Core/Models/ContentTypeBase.cs | 3 - src/Umbraco.Core/Models/DataType.cs | 3 - .../Models/Entities/ITreeEntity.cs | 4 +- .../Models/Entities/IUmbracoEntity.cs | 7 +- src/Umbraco.Core/Models/EntityContainer.cs | 2 - src/Umbraco.Core/Models/SimpleContentType.cs | 3 - .../Factories/ContentBaseFactory.cs | 2 +- .../Factories/ContentTypeFactory.cs | 1 - .../Persistence/Factories/DataTypeFactory.cs | 1 - .../Mappers/UmbracoEntityMapper.cs | 1 - .../Repositories/IEntityRepository.cs | 43 +---- .../Implement/EntityRepository.cs | 157 +++++++----------- .../Repositories/EntityRepositoryTest.cs | 95 ----------- src/Umbraco.Tests/Umbraco.Tests.csproj | 1 - src/Umbraco.Web/Editors/EntityController.cs | 3 - .../Models/Mapping/EntityMapDefinition.cs | 8 +- .../Models/Mapping/UserMapDefinition.cs | 4 +- .../Trees/ContentBlueprintTreeController.cs | 4 +- 19 files changed, 72 insertions(+), 273 deletions(-) delete mode 100644 src/Umbraco.Tests/Persistence/Repositories/EntityRepositoryTest.cs diff --git a/src/Umbraco.Core/Models/ContentBase.cs b/src/Umbraco.Core/Models/ContentBase.cs index 2ea0fd855b..fbb68194b7 100644 --- a/src/Umbraco.Core/Models/ContentBase.cs +++ b/src/Umbraco.Core/Models/ContentBase.cs @@ -108,9 +108,6 @@ namespace Umbraco.Core.Models [IgnoreDataMember] public int VersionId { get; set; } - [DataMember] - public Guid NodeObjectType { get; internal set; } - /// /// Integer Id of the default ContentType /// diff --git a/src/Umbraco.Core/Models/ContentTypeBase.cs b/src/Umbraco.Core/Models/ContentTypeBase.cs index 1f3f1a5c13..04bcb7424a 100644 --- a/src/Umbraco.Core/Models/ContentTypeBase.cs +++ b/src/Umbraco.Core/Models/ContentTypeBase.cs @@ -113,9 +113,6 @@ namespace Umbraco.Core.Models OnPropertyChanged(nameof(PropertyTypes)); } - [DataMember] - public Guid NodeObjectType { get; internal set; } - /// /// The Alias of the ContentType /// diff --git a/src/Umbraco.Core/Models/DataType.cs b/src/Umbraco.Core/Models/DataType.cs index cba53bbd10..c237f6381c 100644 --- a/src/Umbraco.Core/Models/DataType.cs +++ b/src/Umbraco.Core/Models/DataType.cs @@ -65,9 +65,6 @@ namespace Umbraco.Core.Models [DataMember] public string EditorAlias => _editor.Alias; - [DataMember] - public Guid NodeObjectType { get; internal set; } - /// [DataMember] public ValueStorageType DatabaseType diff --git a/src/Umbraco.Core/Models/Entities/ITreeEntity.cs b/src/Umbraco.Core/Models/Entities/ITreeEntity.cs index ab63e1e1d8..afa3399202 100644 --- a/src/Umbraco.Core/Models/Entities/ITreeEntity.cs +++ b/src/Umbraco.Core/Models/Entities/ITreeEntity.cs @@ -24,7 +24,7 @@ /// Sets the parent entity. ///
/// Use this method to set the parent entity when the parent entity is known, but has not - /// been persistent and does not yet have an identity. The parent identifier will be retrieved + /// been persistent and does not yet have an identity. The parent identifier will we retrieved /// from the parent entity when needed. If the parent entity still does not have an entity by that /// time, an exception will be thrown by getter. void SetParent(ITreeEntity parent); @@ -53,4 +53,4 @@ /// bool Trashed { get; } } -} +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Entities/IUmbracoEntity.cs b/src/Umbraco.Core/Models/Entities/IUmbracoEntity.cs index 13087b5e95..e5f628b098 100644 --- a/src/Umbraco.Core/Models/Entities/IUmbracoEntity.cs +++ b/src/Umbraco.Core/Models/Entities/IUmbracoEntity.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; namespace Umbraco.Core.Models.Entities { @@ -12,7 +11,5 @@ namespace Umbraco.Core.Models.Entities /// An IUmbracoEntity can participate in notifications. /// public interface IUmbracoEntity : ITreeEntity, IRememberBeingDirty - { - Guid NodeObjectType { get; } - } + { } } diff --git a/src/Umbraco.Core/Models/EntityContainer.cs b/src/Umbraco.Core/Models/EntityContainer.cs index 71a477a9d6..70f6cbd878 100644 --- a/src/Umbraco.Core/Models/EntityContainer.cs +++ b/src/Umbraco.Core/Models/EntityContainer.cs @@ -62,8 +62,6 @@ namespace Umbraco.Core.Models ///
public Guid ContainerObjectType => ObjectTypeMap[_containedObjectType]; - Guid IUmbracoEntity.NodeObjectType => ContainerObjectType; - /// /// Gets the container object type corresponding to a contained object type. /// diff --git a/src/Umbraco.Core/Models/SimpleContentType.cs b/src/Umbraco.Core/Models/SimpleContentType.cs index b4e7688eea..5c81017ec8 100644 --- a/src/Umbraco.Core/Models/SimpleContentType.cs +++ b/src/Umbraco.Core/Models/SimpleContentType.cs @@ -44,14 +44,11 @@ namespace Umbraco.Core.Models Name = contentType.Name; AllowedAsRoot = contentType.AllowedAsRoot; IsElement = contentType.IsElement; - NodeObjectType = contentType.NodeObjectType; } /// public string Alias { get; } - public Guid NodeObjectType { get; } - public int Id { get; } /// diff --git a/src/Umbraco.Core/Persistence/Factories/ContentBaseFactory.cs b/src/Umbraco.Core/Persistence/Factories/ContentBaseFactory.cs index 25cc6bc358..434e0393cd 100644 --- a/src/Umbraco.Core/Persistence/Factories/ContentBaseFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/ContentBaseFactory.cs @@ -34,7 +34,7 @@ namespace Umbraco.Core.Persistence.Factories content.VersionId = contentVersionDto.Id; content.Name = contentVersionDto.Text; - content.NodeObjectType = nodeDto.NodeObjectType ?? Guid.Empty; + content.Path = nodeDto.Path; content.Level = nodeDto.Level; content.ParentId = nodeDto.ParentId; diff --git a/src/Umbraco.Core/Persistence/Factories/ContentTypeFactory.cs b/src/Umbraco.Core/Persistence/Factories/ContentTypeFactory.cs index 46489aa69e..54cfee0ffa 100644 --- a/src/Umbraco.Core/Persistence/Factories/ContentTypeFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/ContentTypeFactory.cs @@ -107,7 +107,6 @@ namespace Umbraco.Core.Persistence.Factories { entity.Id = dto.NodeDto.NodeId; entity.Key = dto.NodeDto.UniqueId; - entity.NodeObjectType = dto.NodeDto.NodeObjectType ?? Guid.Empty; entity.Alias = dto.Alias; entity.Name = dto.NodeDto.Text; entity.Icon = dto.Icon; diff --git a/src/Umbraco.Core/Persistence/Factories/DataTypeFactory.cs b/src/Umbraco.Core/Persistence/Factories/DataTypeFactory.cs index 5d92234ca8..f189d38d05 100644 --- a/src/Umbraco.Core/Persistence/Factories/DataTypeFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/DataTypeFactory.cs @@ -28,7 +28,6 @@ namespace Umbraco.Core.Persistence.Factories dataType.DisableChangeTracking(); dataType.CreateDate = dto.NodeDto.CreateDate; - dataType.NodeObjectType = dto.NodeDto.NodeObjectType ?? Guid.Empty; dataType.DatabaseType = dto.DbType.EnumParse(true); dataType.Id = dto.NodeId; dataType.Key = dto.NodeDto.UniqueId; diff --git a/src/Umbraco.Core/Persistence/Mappers/UmbracoEntityMapper.cs b/src/Umbraco.Core/Persistence/Mappers/UmbracoEntityMapper.cs index 96f664cc25..16e2e8bcd5 100644 --- a/src/Umbraco.Core/Persistence/Mappers/UmbracoEntityMapper.cs +++ b/src/Umbraco.Core/Persistence/Mappers/UmbracoEntityMapper.cs @@ -24,7 +24,6 @@ namespace Umbraco.Core.Persistence.Mappers DefineMap(nameof(IUmbracoEntity.Trashed), nameof(NodeDto.Trashed)); DefineMap(nameof(IUmbracoEntity.Key), nameof(NodeDto.UniqueId)); DefineMap(nameof(IUmbracoEntity.CreatorId), nameof(NodeDto.UserId)); - DefineMap(nameof(IUmbracoEntity.NodeObjectType), nameof(NodeDto.NodeObjectType)); } } } diff --git a/src/Umbraco.Core/Persistence/Repositories/IEntityRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IEntityRepository.cs index a6a1691347..69f6ef4c5f 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IEntityRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IEntityRepository.cs @@ -15,22 +15,10 @@ namespace Umbraco.Core.Persistence.Repositories IEntitySlim Get(int id, Guid objectTypeId); IEntitySlim Get(Guid key, Guid objectTypeId); - IEnumerable GetAll(Guid objectType, params int[] ids); + IEnumerable GetAll(Guid objectType, params int[] ids); IEnumerable GetAll(Guid objectType, params Guid[] keys); - /// - /// Gets entities for a query - /// - /// - /// IEnumerable GetByQuery(IQuery query); - - /// - /// Gets entities for a query and a specific object type allowing the query to be slightly more optimized - /// - /// - /// - /// IEnumerable GetByQuery(IQuery query, Guid objectType); UmbracoObjectTypes GetObjectType(int id); @@ -42,36 +30,7 @@ namespace Umbraco.Core.Persistence.Repositories bool Exists(int id); bool Exists(Guid key); - /// - /// Gets paged entities for a query and a subset of object types - /// - /// - /// - /// - /// - /// - /// - /// - /// A collection of mixed entity types which would be of type , , , - /// - /// - IEnumerable GetPagedResultsByQuery(IQuery query, Guid[] objectTypes, long pageIndex, int pageSize, out long totalRecords, - IQuery filter, Ordering ordering); - - /// - /// Gets paged entities for a query and a specific object type - /// - /// - /// - /// - /// - /// - /// - /// - /// IEnumerable GetPagedResultsByQuery(IQuery query, Guid objectType, long pageIndex, int pageSize, out long totalRecords, IQuery filter, Ordering ordering); - - } } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs index 2b68c8fe19..161db543ba 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs @@ -36,70 +36,26 @@ namespace Umbraco.Core.Persistence.Repositories.Implement #region Repository - //public IEnumerable GetPagedResults(IDictionary> typesAndIds, long pageIndex, int pageSize, out long totalRecords, Ordering ordering) - //{ - // var isContent = typesAndIds.Keys.Any(objectType => objectType == Constants.ObjectTypes.Document || objectType == Constants.ObjectTypes.DocumentBlueprint); - // var isMedia = typesAndIds.Keys.Any(objectType => objectType == Constants.ObjectTypes.Media); - // var isMember = typesAndIds.Keys.Any(objectType => objectType == Constants.ObjectTypes.Member); - - // var sql = GetBase(isContent, isMedia, isMember, null, false) - // .WhereIn(x => x.NodeObjectType, typesAndIds.Keys); - - // ordering = ordering ?? Ordering.ByDefault(); - - // sql = AddGroupBy(isContent, isMedia, isMember, sql, ordering.IsEmpty); - - // if (!ordering.IsEmpty) - // { - // // apply ordering - // ApplyOrdering(ref sql, ordering); - // } - - // // TODO: we should be able to do sql = sql.OrderBy(x => Alias(x.NodeId, "NodeId")); but we can't because the OrderBy extension don't support Alias currently - // //no matter what we always must have node id ordered at the end - // sql = ordering.Direction == Direction.Ascending ? sql.OrderBy("NodeId") : sql.OrderByDescending("NodeId"); - - // // for content we must query for ContentEntityDto entities to produce the correct culture variant entity names - // var pageIndexToFetch = pageIndex + 1; - // IEnumerable dtos; - // var page = Database.Page(pageIndexToFetch, pageSize, sql); - // dtos = page.Items; - // totalRecords = page.TotalItems; - - // var entities = dtos.Select(BuildEntity).ToArray(); - - // BuildVariants(entities.OfType()); - - // return entities; - //} - + // get a page of entities public IEnumerable GetPagedResultsByQuery(IQuery query, Guid objectType, long pageIndex, int pageSize, out long totalRecords, IQuery filter, Ordering ordering) { - query = query.Where(x => x.NodeObjectType == objectType); - return GetPagedResultsByQuery(query, new[] { objectType }, pageIndex, pageSize, out totalRecords, filter, ordering); - } - - // get a page of entities - public IEnumerable GetPagedResultsByQuery(IQuery query, Guid[] objectTypes, long pageIndex, int pageSize, out long totalRecords, - IQuery filter, Ordering ordering) - { - var isContent = objectTypes.Any(objectType => objectType == Constants.ObjectTypes.Document || objectType == Constants.ObjectTypes.DocumentBlueprint); - var isMedia = objectTypes.Any(objectType => objectType == Constants.ObjectTypes.Media); - var isMember = objectTypes.Any(objectType => objectType == Constants.ObjectTypes.Member); + var isContent = objectType == Constants.ObjectTypes.Document || objectType == Constants.ObjectTypes.DocumentBlueprint; + var isMedia = objectType == Constants.ObjectTypes.Media; + var isMember = objectType == Constants.ObjectTypes.Member; var sql = GetBaseWhere(isContent, isMedia, isMember, false, x => { if (filter == null) return; foreach (var filterClause in filter.GetWhereClauses()) x.Where(filterClause.Item1, filterClause.Item2); - }, objectTypes); + }, objectType); ordering = ordering ?? Ordering.ByDefault(); var translator = new SqlTranslator(sql, query); sql = translator.Translate(); - sql = AddGroupBy(true, true, true, sql, ordering.IsEmpty); + sql = AddGroupBy(isContent, isMedia, isMember, sql, ordering.IsEmpty); if (!ordering.IsEmpty) { @@ -114,13 +70,35 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // for content we must query for ContentEntityDto entities to produce the correct culture variant entity names var pageIndexToFetch = pageIndex + 1; IEnumerable dtos; - var page = Database.Page(pageIndexToFetch, pageSize, sql); - dtos = page.Items; - totalRecords = page.TotalItems; + if(isContent) + { + var page = Database.Page(pageIndexToFetch, pageSize, sql); + dtos = page.Items; + totalRecords = page.TotalItems; + } + else if (isMedia) + { + var page = Database.Page(pageIndexToFetch, pageSize, sql); + dtos = page.Items; + totalRecords = page.TotalItems; + } + else if (isMember) + { + var page = Database.Page(pageIndexToFetch, pageSize, sql); + dtos = page.Items; + totalRecords = page.TotalItems; + } + else + { + var page = Database.Page(pageIndexToFetch, pageSize, sql); + dtos = page.Items; + totalRecords = page.TotalItems; + } - var entities = dtos.Select(BuildEntity).ToArray(); + var entities = dtos.Select(x => BuildEntity(isContent, isMedia, isMember, x)).ToArray(); - BuildVariants(entities.OfType()); + if (isContent) + BuildVariants(entities.Cast()); return entities; } @@ -129,7 +107,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement { var sql = GetBaseWhere(false, false, false, false, key); var dto = Database.FirstOrDefault(sql); - return dto == null ? null : BuildEntity(dto); + return dto == null ? null : BuildEntity(false, false, false, dto); } @@ -138,7 +116,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement //isContent is going to return a 1:M result now with the variants so we need to do different things if (isContent) { - var cdtos = Database.Fetch(sql); + var cdtos = Database.Fetch(sql); return cdtos.Count == 0 ? null : BuildVariants(BuildDocumentEntity(cdtos[0])); } @@ -149,7 +127,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (dto == null) return null; - var entity = BuildEntity(dto); + var entity = BuildEntity(false, isMedia, isMember, dto); return entity; } @@ -168,7 +146,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement { var sql = GetBaseWhere(false, false, false, false, id); var dto = Database.FirstOrDefault(sql); - return dto == null ? null : BuildEntity(dto); + return dto == null ? null : BuildEntity(false, false, false, dto); } public IEntitySlim Get(int id, Guid objectTypeId) @@ -200,7 +178,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement //isContent is going to return a 1:M result now with the variants so we need to do different things if (isContent) { - var cdtos = Database.Fetch(sql); + var cdtos = Database.Fetch(sql); return cdtos.Count == 0 ? Enumerable.Empty() @@ -211,7 +189,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement ? (IEnumerable)Database.Fetch(sql) : Database.Fetch(sql); - var entities = dtos.Select(BuildEntity).ToArray(); + var entities = dtos.Select(x => BuildEntity(false, isMedia, isMember, x)).ToArray(); return entities; } @@ -255,7 +233,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var sql = translator.Translate(); sql = AddGroupBy(false, false, false, sql, true); var dtos = Database.Fetch(sql); - return dtos.Select(BuildEntity).ToList(); + return dtos.Select(x => BuildEntity(false, false, false, x)).ToList(); } public IEnumerable GetByQuery(IQuery query, Guid objectType) @@ -264,7 +242,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var isMedia = objectType == Constants.ObjectTypes.Media; var isMember = objectType == Constants.ObjectTypes.Member; - var sql = GetBaseWhere(isContent, isMedia, isMember, false, null, new[] { objectType }); + var sql = GetBaseWhere(isContent, isMedia, isMember, false, null, objectType); var translator = new SqlTranslator(sql, query); sql = translator.Translate(); @@ -378,14 +356,14 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // gets the full sql for a given object type, with a given filter protected Sql GetFullSqlForEntityType(bool isContent, bool isMedia, bool isMember, Guid objectType, Action> filter) { - var sql = GetBaseWhere(isContent, isMedia, isMember, false, filter, new[] { objectType }); + var sql = GetBaseWhere(isContent, isMedia, isMember, false, filter, objectType); return AddGroupBy(isContent, isMedia, isMember, sql, true); } // gets the base SELECT + FROM [+ filter] sql // always from the 'current' content version protected Sql GetBase(bool isContent, bool isMedia, bool isMember, Action> filter, bool isCount = false) - { + { var sql = Sql(); if (isCount) @@ -423,15 +401,15 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (isContent || isMedia || isMember) { sql - .LeftJoin().On((left, right) => left.NodeId == right.NodeId && right.Current) - .LeftJoin().On((left, right) => left.NodeId == right.NodeId) - .LeftJoin().On((left, right) => left.ContentTypeId == right.NodeId); + .InnerJoin().On((left, right) => left.NodeId == right.NodeId && right.Current) + .InnerJoin().On((left, right) => left.NodeId == right.NodeId) + .InnerJoin().On((left, right) => left.ContentTypeId == right.NodeId); } if (isContent) { sql - .LeftJoin().On((left, right) => left.NodeId == right.NodeId); + .InnerJoin().On((left, right) => left.NodeId == right.NodeId); } if (isMedia) @@ -455,10 +433,10 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // gets the base SELECT + FROM [+ filter] + WHERE sql // for a given object type, with a given filter - protected Sql GetBaseWhere(bool isContent, bool isMedia, bool isMember, bool isCount, Action> filter, Guid[] objectTypes) + protected Sql GetBaseWhere(bool isContent, bool isMedia, bool isMember, bool isCount, Action> filter, Guid objectType) { return GetBase(isContent, isMedia, isMember, filter, isCount) - .WhereIn(x => x.NodeObjectType, objectTypes); + .Where(x => x.NodeObjectType == objectType); } // gets the base SELECT + FROM + WHERE sql @@ -532,13 +510,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (sql == null) throw new ArgumentNullException(nameof(sql)); if (ordering == null) throw new ArgumentNullException(nameof(ordering)); - // TODO: although the default ordering string works for name, it wont work for others without a table or an alias of some sort - // As more things are attempted to be sorted we'll prob have to add more expressions here - var orderBy = ordering.OrderBy.ToUpperInvariant() switch - { - "PATH" => SqlSyntax.GetQuotedColumn(NodeDto.TableName, "path"), - _ => ordering.OrderBy - }; + // TODO: although this works for name, it probably doesn't work for others without an alias of some sort + var orderBy = ordering.OrderBy; if (ordering.Direction == Direction.Ascending) sql.OrderBy(orderBy); @@ -551,17 +524,9 @@ namespace Umbraco.Core.Persistence.Repositories.Implement #region Classes /// - /// The DTO used to fetch results for a generic content item which could be either a document, media or a member + /// The DTO used to fetch results for a content item with its variation info /// - private class GenericContentEntityDto : DocumentEntityDto - { - public string MediaPath { get; set; } - } - - /// - /// The DTO used to fetch results for a document item with its variation info - /// - private class DocumentEntityDto : BaseDto + private class ContentEntityDto : BaseDto { public ContentVariation Variations { get; set; } @@ -569,17 +534,11 @@ namespace Umbraco.Core.Persistence.Repositories.Implement public bool Edited { get; set; } } - /// - /// The DTO used to fetch results for a media item with its media path info - /// private class MediaEntityDto : BaseDto { public string MediaPath { get; set; } } - /// - /// The DTO used to fetch results for a member item - /// private class MemberEntityDto : BaseDto { } @@ -630,13 +589,13 @@ namespace Umbraco.Core.Persistence.Repositories.Implement #region Factory - private EntitySlim BuildEntity(BaseDto dto) + private EntitySlim BuildEntity(bool isContent, bool isMedia, bool isMember, BaseDto dto) { - if (dto.NodeObjectType == Constants.ObjectTypes.Document) + if (isContent) return BuildDocumentEntity(dto); - if (dto.NodeObjectType == Constants.ObjectTypes.Media) + if (isMedia) return BuildMediaEntity(dto); - if (dto.NodeObjectType == Constants.ObjectTypes.Member) + if (isMember) return BuildMemberEntity(dto); // EntitySlim does not track changes @@ -691,7 +650,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var entity = new DocumentEntitySlim(); BuildContentEntity(entity, dto); - if (dto is DocumentEntityDto contentDto) + if (dto is ContentEntityDto contentDto) { // fill in the invariant info entity.Edited = contentDto.Edited; diff --git a/src/Umbraco.Tests/Persistence/Repositories/EntityRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/EntityRepositoryTest.cs deleted file mode 100644 index a4df5bcf78..0000000000 --- a/src/Umbraco.Tests/Persistence/Repositories/EntityRepositoryTest.cs +++ /dev/null @@ -1,95 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using NUnit.Framework; -using Umbraco.Core; -using Umbraco.Core.Models; -using Umbraco.Core.Models.Entities; -using Umbraco.Core.Persistence; -using Umbraco.Core.Persistence.Repositories.Implement; -using Umbraco.Core.Scoping; -using Umbraco.Tests.TestHelpers; -using Umbraco.Tests.TestHelpers.Entities; -using Umbraco.Tests.Testing; - -namespace Umbraco.Tests.Persistence.Repositories -{ - [TestFixture] - [UmbracoTest(Mapper = true, Database = UmbracoTestOptions.Database.NewSchemaPerTest)] - public class EntityRepositoryTest : TestWithDatabaseBase - { - - private EntityRepository CreateRepository(IScopeAccessor scopeAccessor) - { - var entityRepository = new EntityRepository(scopeAccessor); - return entityRepository; - } - - [Test] - public void Get_Paged_Mixed_Entities_By_Ids() - { - //Create content - - var createdContent = new List(); - var contentType = MockedContentTypes.CreateBasicContentType("blah"); - ServiceContext.ContentTypeService.Save(contentType); - for (int i = 0; i < 10; i++) - { - var c1 = MockedContent.CreateBasicContent(contentType); - ServiceContext.ContentService.Save(c1); - createdContent.Add(c1); - } - - //Create media - - var createdMedia = new List(); - var imageType = MockedContentTypes.CreateImageMediaType("myImage"); - ServiceContext.MediaTypeService.Save(imageType); - for (int i = 0; i < 10; i++) - { - var c1 = MockedMedia.CreateMediaImage(imageType, -1); - ServiceContext.MediaService.Save(c1); - createdMedia.Add(c1); - } - - // Create members - var memberType = MockedContentTypes.CreateSimpleMemberType("simple"); - ServiceContext.MemberTypeService.Save(memberType); - var createdMembers = MockedMember.CreateSimpleMember(memberType, 10).ToList(); - ServiceContext.MemberService.Save(createdMembers); - - var provider = TestObjects.GetScopeProvider(Logger); - using (var scope = provider.CreateScope()) - { - var repo = CreateRepository((IScopeAccessor)provider); - - var ids = createdContent.Select(x => x.Id).Concat(createdMedia.Select(x => x.Id)).Concat(createdMembers.Select(x => x.Id)); - - var objectTypes = new[] { Constants.ObjectTypes.Document, Constants.ObjectTypes.Media, Constants.ObjectTypes.Member }; - - var query = SqlContext.Query() - .WhereIn(e => e.Id, ids); - - var entities = repo.GetPagedResultsByQuery(query, objectTypes, 0, 20, out var totalRecords, null, null).ToList(); - - Assert.AreEqual(20, entities.Count); - Assert.AreEqual(30, totalRecords); - - //add the next page - entities.AddRange(repo.GetPagedResultsByQuery(query, objectTypes, 1, 20, out totalRecords, null, null)); - - Assert.AreEqual(30, entities.Count); - Assert.AreEqual(30, totalRecords); - - var contentEntities = entities.OfType().ToList(); - var mediaEntities = entities.OfType().ToList(); - var memberEntities = entities.OfType().ToList(); - - Assert.AreEqual(10, contentEntities.Count); - Assert.AreEqual(10, mediaEntities.Count); - Assert.AreEqual(10, memberEntities.Count); - } - - } - - } -} diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index c87e6501f9..4b035f631e 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -140,7 +140,6 @@ - diff --git a/src/Umbraco.Web/Editors/EntityController.cs b/src/Umbraco.Web/Editors/EntityController.cs index 11b1260e71..0513017b70 100644 --- a/src/Umbraco.Web/Editors/EntityController.cs +++ b/src/Umbraco.Web/Editors/EntityController.cs @@ -662,9 +662,6 @@ namespace Umbraco.Web.Editors if (pageSize <= 0) throw new HttpResponseException(HttpStatusCode.NotFound); - // re-normalize since NULL can be passed in - filter = filter ?? string.Empty; - var objectType = ConvertToObjectType(type); if (objectType.HasValue) { diff --git a/src/Umbraco.Web/Models/Mapping/EntityMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/EntityMapDefinition.cs index afd1a6b61c..2e44f1327b 100644 --- a/src/Umbraco.Web/Models/Mapping/EntityMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/EntityMapDefinition.cs @@ -226,11 +226,11 @@ namespace Umbraco.Web.Models.Mapping { switch (entity) { - case IMemberEntitySlim memberEntity: - return memberEntity.ContentTypeIcon.IfNullOrWhiteSpace(Constants.Icons.Member); - case IContentEntitySlim contentEntity: + case ContentEntitySlim contentEntity: // NOTE: this case covers both content and media entities - return contentEntity.ContentTypeIcon; + return contentEntity.ContentTypeIcon; + case MemberEntitySlim memberEntity: + return memberEntity.ContentTypeIcon.IfNullOrWhiteSpace(Constants.Icons.Member); } return null; diff --git a/src/Umbraco.Web/Models/Mapping/UserMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/UserMapDefinition.cs index aa158799cb..88960fb189 100644 --- a/src/Umbraco.Web/Models/Mapping/UserMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/UserMapDefinition.cs @@ -380,8 +380,8 @@ namespace Umbraco.Web.Models.Mapping .ToDictionary(x => x.Key, x => (IEnumerable)x.ToArray()); } - private static string MapContentTypeIcon(IEntitySlim entity) - => entity is IContentEntitySlim contentEntity ? contentEntity.ContentTypeIcon : null; + private static string MapContentTypeIcon(EntitySlim entity) + => entity is ContentEntitySlim contentEntity ? contentEntity.ContentTypeIcon : null; private IEnumerable GetStartNodes(int[] startNodeIds, UmbracoObjectTypes objectType, string localizedKey, MapperContext context) { diff --git a/src/Umbraco.Web/Trees/ContentBlueprintTreeController.cs b/src/Umbraco.Web/Trees/ContentBlueprintTreeController.cs index fd05f7cfbd..ac75fd831d 100644 --- a/src/Umbraco.Web/Trees/ContentBlueprintTreeController.cs +++ b/src/Umbraco.Web/Trees/ContentBlueprintTreeController.cs @@ -47,7 +47,7 @@ namespace Umbraco.Web.Trees if (id == Constants.System.RootString) { //get all blueprint content types - var contentTypeAliases = entities.Select(x => ((IContentEntitySlim) x).ContentTypeAlias).Distinct(); + var contentTypeAliases = entities.Select(x => ((ContentEntitySlim) x).ContentTypeAlias).Distinct(); //get the ids var contentTypeIds = Services.ContentTypeService.GetAllContentTypeIds(contentTypeAliases.ToArray()).ToArray(); @@ -75,7 +75,7 @@ namespace Umbraco.Web.Trees var ct = Services.ContentTypeService.Get(intId.Result); if (ct == null) return nodes; - var blueprintsForDocType = entities.Where(x => ct.Alias == ((IContentEntitySlim) x).ContentTypeAlias); + var blueprintsForDocType = entities.Where(x => ct.Alias == ((ContentEntitySlim) x).ContentTypeAlias); nodes.AddRange(blueprintsForDocType .Select(entity => { From 6b7a48d00b83f681763b4a7bada9f897bb9bb781 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 6 Nov 2019 12:43:10 +1100 Subject: [PATCH 051/202] Gets entity repository working with relation queries and adds test --- .../Repositories/IEntityRepository.cs | 5 +- .../Repositories/IRelationRepository.cs | 4 + .../Implement/EntityRepository.cs | 36 +++++++-- .../Implement/RelationRepository.cs | 18 ++++- src/Umbraco.Core/Services/IRelationService.cs | 10 +++ .../Services/Implement/RelationService.cs | 11 +++ .../Repositories/ContentTypeRepositoryTest.cs | 3 +- .../Repositories/DocumentRepositoryTest.cs | 3 +- .../Repositories/DomainRepositoryTest.cs | 3 +- .../Repositories/MediaRepositoryTest.cs | 3 +- .../Repositories/MemberRepositoryTest.cs | 3 +- .../PublicAccessRepositoryTest.cs | 3 +- .../Repositories/RelationRepositoryTest.cs | 78 ++++++++++++++++++- .../Repositories/TagRepositoryTest.cs | 6 +- .../Repositories/TemplateRepositoryTest.cs | 3 +- .../Repositories/UserRepositoryTest.cs | 6 +- .../Services/ContentServicePerformanceTest.cs | 18 +++-- .../Services/ContentServiceTests.cs | 3 +- 18 files changed, 182 insertions(+), 34 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/IEntityRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IEntityRepository.cs index a6a1691347..102dc7c81a 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IEntityRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IEntityRepository.cs @@ -55,8 +55,9 @@ namespace Umbraco.Core.Persistence.Repositories /// A collection of mixed entity types which would be of type , , , /// /// - IEnumerable GetPagedResultsByQuery(IQuery query, Guid[] objectTypes, long pageIndex, int pageSize, out long totalRecords, - IQuery filter, Ordering ordering); + IEnumerable GetPagedResultsByQuery( + IQuery query, Guid[] objectTypes, long pageIndex, int pageSize, out long totalRecords, + IQuery filter, Ordering ordering, IQuery relationQuery = null); /// /// Gets paged entities for a query and a specific object type diff --git a/src/Umbraco.Core/Persistence/Repositories/IRelationRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IRelationRepository.cs index 9a2e42568e..9a99cb2a77 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IRelationRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IRelationRepository.cs @@ -1,5 +1,7 @@ using System.Collections.Generic; using Umbraco.Core.Models; +using Umbraco.Core.Models.Entities; +using Umbraco.Core.Persistence.Querying; namespace Umbraco.Core.Persistence.Repositories { @@ -13,5 +15,7 @@ namespace Umbraco.Core.Persistence.Repositories /// A list of relation types to match for deletion, if none are specified then all relations for this parent id are deleted /// void DeleteByParent(int parentId, params string[] relationTypeAliases); + + IEnumerable GetPagedParentEntitiesByChildId(int childId, long pageIndex, int pageSize, out long totalRecords); } } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs index 2b68c8fe19..9b05e95a17 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs @@ -82,24 +82,40 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // get a page of entities public IEnumerable GetPagedResultsByQuery(IQuery query, Guid[] objectTypes, long pageIndex, int pageSize, out long totalRecords, - IQuery filter, Ordering ordering) + IQuery filter, Ordering ordering, IQuery relationQuery = null) { var isContent = objectTypes.Any(objectType => objectType == Constants.ObjectTypes.Document || objectType == Constants.ObjectTypes.DocumentBlueprint); var isMedia = objectTypes.Any(objectType => objectType == Constants.ObjectTypes.Media); var isMember = objectTypes.Any(objectType => objectType == Constants.ObjectTypes.Member); - var sql = GetBaseWhere(isContent, isMedia, isMember, false, x => + var sql = GetBaseWhere(isContent, isMedia, isMember, false, s => { - if (filter == null) return; - foreach (var filterClause in filter.GetWhereClauses()) - x.Where(filterClause.Item1, filterClause.Item2); + if (relationQuery != null) + { + // add left joins for relation tables (this joins on both child or parent, so beware that this will normally return entities for + // both sides of the relation type unless the IUmbracoEntity query passed in filters one side out). + s.LeftJoin().On((left, right) => left.NodeId == right.ChildId || left.NodeId == right.ParentId); + s.LeftJoin().On((left, right) => left.RelationType == right.Id); + + // append the relations wheres + foreach (var relClause in relationQuery.GetWhereClauses()) + s.Where(relClause.Item1, relClause.Item2); + } + + if (filter != null) + { + foreach (var filterClause in filter.GetWhereClauses()) + s.Where(filterClause.Item1, filterClause.Item2); + } + + }, objectTypes); ordering = ordering ?? Ordering.ByDefault(); var translator = new SqlTranslator(sql, query); sql = translator.Translate(); - sql = AddGroupBy(true, true, true, sql, ordering.IsEmpty); + sql = AddGroupBy(isContent, isMedia, isMember, sql, ordering.IsEmpty); if (!ordering.IsEmpty) { @@ -457,8 +473,12 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // for a given object type, with a given filter protected Sql GetBaseWhere(bool isContent, bool isMedia, bool isMember, bool isCount, Action> filter, Guid[] objectTypes) { - return GetBase(isContent, isMedia, isMember, filter, isCount) - .WhereIn(x => x.NodeObjectType, objectTypes); + var sql = GetBase(isContent, isMedia, isMember, filter, isCount); + if (objectTypes.Length > 0) + { + sql.WhereIn(x => x.NodeObjectType, objectTypes); + } + return sql; } // gets the base SELECT + FROM + WHERE sql diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/RelationRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/RelationRepository.cs index e6c4c6fd7e..530872203c 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/RelationRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/RelationRepository.cs @@ -11,6 +11,7 @@ using Umbraco.Core.Persistence.Factories; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.SqlSyntax; using Umbraco.Core.Scoping; +using Umbraco.Core.Services; using static Umbraco.Core.Persistence.NPocoSqlExtensions.Statics; namespace Umbraco.Core.Persistence.Repositories.Implement @@ -21,11 +22,13 @@ namespace Umbraco.Core.Persistence.Repositories.Implement internal class RelationRepository : NPocoRepositoryBase, IRelationRepository { private readonly IRelationTypeRepository _relationTypeRepository; + private readonly IEntityRepository _entityRepository; - public RelationRepository(IScopeAccessor scopeAccessor, ILogger logger, IRelationTypeRepository relationTypeRepository) + public RelationRepository(IScopeAccessor scopeAccessor, ILogger logger, IRelationTypeRepository relationTypeRepository, IEntityRepository entityRepository) : base(scopeAccessor, AppCaches.NoCache, logger) { _relationTypeRepository = relationTypeRepository; + _entityRepository = entityRepository; } #region Overrides of RepositoryBase @@ -156,6 +159,19 @@ namespace Umbraco.Core.Persistence.Repositories.Implement #endregion + public IEnumerable GetPagedParentEntitiesByChildId(int childId, long pageIndex, int pageSize, out long totalRecords) + { + // Create a query to match the child id + var relQuery = Query().Where(r => r.ChildId == childId); + + // Because of the way that the entity repository joins relations (on both child or parent) we need to add + // a clause to filter out the child entity from being returned from the results + var entityQuery = Query().Where(e => e.Id != childId); + + return _entityRepository.GetPagedResultsByQuery(entityQuery, Array.Empty(), pageIndex, pageSize, out totalRecords, null, null, relQuery); + + } + public void DeleteByParent(int parentId, params string[] relationTypeAliases) { var subQuery = Sql().Select(x => x.Id) diff --git a/src/Umbraco.Core/Services/IRelationService.cs b/src/Umbraco.Core/Services/IRelationService.cs index 49fa21af2b..95f04b6df2 100644 --- a/src/Umbraco.Core/Services/IRelationService.cs +++ b/src/Umbraco.Core/Services/IRelationService.cs @@ -189,6 +189,16 @@ namespace Umbraco.Core.Services /// An enumerable list of IEnumerable GetParentEntitiesFromRelations(IEnumerable relations); + /// + /// Returns paged parent entities for a related child id + /// + /// + /// + /// + /// + /// + IEnumerable GetPagedParentEntitiesByChildId(int id, long pageIndex, int pageSize, out long totalChildren); + /// /// Gets the Parent and Child objects from a list of Relations as a list of objects. /// diff --git a/src/Umbraco.Core/Services/Implement/RelationService.cs b/src/Umbraco.Core/Services/Implement/RelationService.cs index 490f36e7c7..56bf2bba06 100644 --- a/src/Umbraco.Core/Services/Implement/RelationService.cs +++ b/src/Umbraco.Core/Services/Implement/RelationService.cs @@ -263,6 +263,17 @@ namespace Umbraco.Core.Services.Implement } } + /// + public IEnumerable GetPagedParentEntitiesByChildId(int id, long pageIndex, int pageSize, out long totalChildren) + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + //var query = Query().Where(x => x.ChildId == id); + return _relationRepository.GetPagedParentEntitiesByChildId(id, pageIndex, pageSize, out totalChildren); + } + throw new NotImplementedException(); + } + /// public IEnumerable> GetEntitiesFromRelations(IEnumerable relations) { diff --git a/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs index 07ca7d238d..8ed935795d 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs @@ -37,7 +37,8 @@ namespace Umbraco.Tests.Persistence.Repositories contentTypeRepository = new ContentTypeRepository(scopeAccessor, AppCaches.Disabled, Logger, commonRepository, langRepository); var languageRepository = new LanguageRepository(scopeAccessor, AppCaches.Disabled, Logger); var relationTypeRepository = new RelationTypeRepository(scopeAccessor, AppCaches.Disabled, Logger); - var relationRepository = new RelationRepository(scopeAccessor, Logger, relationTypeRepository); + var entityRepository = new EntityRepository(scopeAccessor); + var relationRepository = new RelationRepository(scopeAccessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); var repository = new DocumentRepository(scopeAccessor, AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors); return repository; diff --git a/src/Umbraco.Tests/Persistence/Repositories/DocumentRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/DocumentRepositoryTest.cs index 45dc3de2e3..3a36a647f0 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/DocumentRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/DocumentRepositoryTest.cs @@ -70,7 +70,8 @@ namespace Umbraco.Tests.Persistence.Repositories var languageRepository = new LanguageRepository(scopeAccessor, appCaches, Logger); contentTypeRepository = new ContentTypeRepository(scopeAccessor, appCaches, Logger, commonRepository, languageRepository); var relationTypeRepository = new RelationTypeRepository(scopeAccessor, AppCaches.Disabled, Logger); - var relationRepository = new RelationRepository(scopeAccessor, Logger, relationTypeRepository); + var entityRepository = new EntityRepository(scopeAccessor); + var relationRepository = new RelationRepository(scopeAccessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); var repository = new DocumentRepository(scopeAccessor, appCaches, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors); return repository; diff --git a/src/Umbraco.Tests/Persistence/Repositories/DomainRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/DomainRepositoryTest.cs index 27ea92fed6..ad27aed309 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/DomainRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/DomainRepositoryTest.cs @@ -28,7 +28,8 @@ namespace Umbraco.Tests.Persistence.Repositories languageRepository = new LanguageRepository(accessor, Core.Cache.AppCaches.Disabled, Logger); contentTypeRepository = new ContentTypeRepository(accessor, Core.Cache.AppCaches.Disabled, Logger, commonRepository, languageRepository); var relationTypeRepository = new RelationTypeRepository(accessor, Core.Cache.AppCaches.Disabled, Logger); - var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository); + var entityRepository = new EntityRepository(accessor); + var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); documentRepository = new DocumentRepository(accessor, Core.Cache.AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors); var domainRepository = new DomainRepository(accessor, Core.Cache.AppCaches.Disabled, Logger); diff --git a/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs index 93587506e8..ced0ee75e9 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs @@ -43,7 +43,8 @@ namespace Umbraco.Tests.Persistence.Repositories mediaTypeRepository = new MediaTypeRepository(scopeAccessor, appCaches, Logger, commonRepository, languageRepository); var tagRepository = new TagRepository(scopeAccessor, appCaches, Logger); var relationTypeRepository = new RelationTypeRepository(scopeAccessor, AppCaches.Disabled, Logger); - var relationRepository = new RelationRepository(scopeAccessor, Logger, relationTypeRepository); + var entityRepository = new EntityRepository(scopeAccessor); + var relationRepository = new RelationRepository(scopeAccessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); var repository = new MediaRepository(scopeAccessor, appCaches, Logger, mediaTypeRepository, tagRepository, Mock.Of(), relationRepository, relationTypeRepository, propertyEditors); return repository; diff --git a/src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs index 72b4691639..7531d92b49 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs @@ -37,7 +37,8 @@ namespace Umbraco.Tests.Persistence.Repositories memberGroupRepository = new MemberGroupRepository(accessor, AppCaches.Disabled, Logger); var tagRepo = new TagRepository(accessor, AppCaches.Disabled, Logger); var relationTypeRepository = new RelationTypeRepository(accessor, AppCaches.Disabled, Logger); - var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository); + var entityRepository = new EntityRepository(accessor); + var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); var repository = new MemberRepository(accessor, AppCaches.Disabled, Logger, memberTypeRepository, memberGroupRepository, tagRepo, Mock.Of(), relationRepository, relationTypeRepository, propertyEditors); return repository; diff --git a/src/Umbraco.Tests/Persistence/Repositories/PublicAccessRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/PublicAccessRepositoryTest.cs index 84a0f608f7..9a65bde41f 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/PublicAccessRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/PublicAccessRepositoryTest.cs @@ -312,7 +312,8 @@ namespace Umbraco.Tests.Persistence.Repositories var languageRepository = new LanguageRepository(accessor, AppCaches, Logger); contentTypeRepository = new ContentTypeRepository(accessor, AppCaches, Logger, commonRepository, languageRepository); var relationTypeRepository = new RelationTypeRepository(accessor, AppCaches, Logger); - var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository); + var entityRepository = new EntityRepository(accessor); + var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); var repository = new DocumentRepository(accessor, AppCaches, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors); return repository; diff --git a/src/Umbraco.Tests/Persistence/Repositories/RelationRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/RelationRepositoryTest.cs index 8d9f82a776..b67a662123 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/RelationRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/RelationRepositoryTest.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using Moq; using NUnit.Framework; @@ -6,6 +7,7 @@ using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Logging; using Umbraco.Core.Models; +using Umbraco.Core.Models.Entities; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Persistence.Repositories.Implement; @@ -31,7 +33,8 @@ namespace Umbraco.Tests.Persistence.Repositories { var accessor = (IScopeAccessor) provider; relationTypeRepository = new RelationTypeRepository(accessor, AppCaches.Disabled, Mock.Of()); - var repository = new RelationRepository(accessor, Mock.Of(), relationTypeRepository); + var entityRepository = new EntityRepository(accessor); + var repository = new RelationRepository(accessor, Mock.Of(), relationTypeRepository, entityRepository); return repository; } @@ -168,6 +171,73 @@ namespace Umbraco.Tests.Persistence.Repositories } } + [Test] + public void Get_Paged_Parent_Entities_By_Child_Id() + { + //Create content + var createdContent = new List(); + var contentType = MockedContentTypes.CreateBasicContentType("blah"); + ServiceContext.ContentTypeService.Save(contentType); + for (int i = 0; i < 10; i++) + { + var c1 = MockedContent.CreateBasicContent(contentType); + ServiceContext.ContentService.Save(c1); + createdContent.Add(c1); + } + + //Create media + var createdMedia = new List(); + var imageType = MockedContentTypes.CreateImageMediaType("myImage"); + ServiceContext.MediaTypeService.Save(imageType); + for (int i = 0; i < 10; i++) + { + var c1 = MockedMedia.CreateMediaImage(imageType, -1); + ServiceContext.MediaService.Save(c1); + createdMedia.Add(c1); + } + + // Create members + var memberType = MockedContentTypes.CreateSimpleMemberType("simple"); + ServiceContext.MemberTypeService.Save(memberType); + var createdMembers = MockedMember.CreateSimpleMember(memberType, 10).ToList(); + ServiceContext.MemberService.Save(createdMembers); + + var relType = ServiceContext.RelationService.GetRelationTypeByAlias(Constants.Conventions.RelationTypes.RelatedMediaAlias); + + // Relate content to media + foreach(var content in createdContent) + foreach(var media in createdMedia) + ServiceContext.RelationService.Relate(content.Id, media.Id, relType); + // Relate members to media + foreach (var member in createdMembers) + foreach (var media in createdMedia) + ServiceContext.RelationService.Relate(member.Id, media.Id, relType); + + var provider = TestObjects.GetScopeProvider(Logger); + using (var scope = provider.CreateScope()) + { + var repository = CreateRepository(provider, out var relationTypeRepository); + + // Get parent entities for child id + var parents = repository.GetPagedParentEntitiesByChildId(createdMedia[0].Id, 0, 11, out var totalRecords).ToList(); + Assert.AreEqual(20, totalRecords); + Assert.AreEqual(11, parents.Count); + + //add the next page + parents.AddRange(repository.GetPagedParentEntitiesByChildId(createdMedia[0].Id, 1, 11, out totalRecords)); + Assert.AreEqual(20, totalRecords); + Assert.AreEqual(20, parents.Count); + + var contentEntities = parents.OfType().ToList(); + var mediaEntities = parents.OfType().ToList(); + var memberEntities = parents.OfType().ToList(); + + Assert.AreEqual(10, contentEntities.Count); + Assert.AreEqual(0, mediaEntities.Count); + Assert.AreEqual(10, memberEntities.Count); + } + } + [Test] public void Can_Perform_Exists_On_RelationRepository() { @@ -274,8 +344,10 @@ namespace Umbraco.Tests.Persistence.Repositories var provider = TestObjects.GetScopeProvider(Logger); using (var scope = provider.CreateScope()) { - var relationTypeRepository = new RelationTypeRepository((IScopeAccessor) provider, AppCaches.Disabled, Mock.Of()); - var relationRepository = new RelationRepository((IScopeAccessor) provider, Mock.Of(), relationTypeRepository); + var accessor = (IScopeAccessor)provider; + var relationTypeRepository = new RelationTypeRepository(accessor, AppCaches.Disabled, Mock.Of()); + var entityRepository = new EntityRepository(accessor); + var relationRepository = new RelationRepository(accessor, Mock.Of(), relationTypeRepository, entityRepository); relationTypeRepository.Save(relateContent); relationTypeRepository.Save(relateContentType); diff --git a/src/Umbraco.Tests/Persistence/Repositories/TagRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/TagRepositoryTest.cs index a4155639be..fe0db4563a 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/TagRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/TagRepositoryTest.cs @@ -961,7 +961,8 @@ namespace Umbraco.Tests.Persistence.Repositories var languageRepository = new LanguageRepository(accessor, AppCaches.Disabled, Logger); contentTypeRepository = new ContentTypeRepository(accessor, AppCaches.Disabled, Logger, commonRepository, languageRepository); var relationTypeRepository = new RelationTypeRepository(accessor, AppCaches.Disabled, Logger); - var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository); + var entityRepository = new EntityRepository(accessor); + var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); var repository = new DocumentRepository(accessor, AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors); return repository; @@ -976,7 +977,8 @@ namespace Umbraco.Tests.Persistence.Repositories var languageRepository = new LanguageRepository(accessor, AppCaches.Disabled, Logger); mediaTypeRepository = new MediaTypeRepository(accessor, AppCaches.Disabled, Logger, commonRepository, languageRepository); var relationTypeRepository = new RelationTypeRepository(accessor, AppCaches.Disabled, Logger); - var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository); + var entityRepository = new EntityRepository(accessor); + var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); var repository = new MediaRepository(accessor, AppCaches.Disabled, Logger, mediaTypeRepository, tagRepository, Mock.Of(), relationRepository, relationTypeRepository, propertyEditors); return repository; diff --git a/src/Umbraco.Tests/Persistence/Repositories/TemplateRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/TemplateRepositoryTest.cs index e996c4f6a1..4bf50c6ffd 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/TemplateRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/TemplateRepositoryTest.cs @@ -243,7 +243,8 @@ namespace Umbraco.Tests.Persistence.Repositories var languageRepository = new LanguageRepository(ScopeProvider, AppCaches.Disabled, Logger); var contentTypeRepository = new ContentTypeRepository(ScopeProvider, AppCaches.Disabled, Logger, commonRepository, languageRepository); var relationTypeRepository = new RelationTypeRepository(ScopeProvider, AppCaches.Disabled, Logger); - var relationRepository = new RelationRepository(ScopeProvider, Logger, relationTypeRepository); + var entityRepository = new EntityRepository(ScopeProvider); + var relationRepository = new RelationRepository(ScopeProvider, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); var contentRepo = new DocumentRepository(ScopeProvider, AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors); diff --git a/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs index 0438e2193b..f4ab387683 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs @@ -32,7 +32,8 @@ namespace Umbraco.Tests.Persistence.Repositories mediaTypeRepository = new MediaTypeRepository(accessor, AppCaches, Mock.Of(), commonRepository, languageRepository); var tagRepository = new TagRepository(accessor, AppCaches, Mock.Of()); var relationTypeRepository = new RelationTypeRepository(accessor, AppCaches.Disabled, Logger); - var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository); + var entityRepository = new EntityRepository(accessor); + var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); var repository = new MediaRepository(accessor, AppCaches, Mock.Of(), mediaTypeRepository, tagRepository, Mock.Of(), relationRepository, relationTypeRepository, propertyEditors); return repository; @@ -53,7 +54,8 @@ namespace Umbraco.Tests.Persistence.Repositories var languageRepository = new LanguageRepository(accessor, AppCaches, Logger); contentTypeRepository = new ContentTypeRepository(accessor, AppCaches, Logger, commonRepository, languageRepository); var relationTypeRepository = new RelationTypeRepository(accessor, AppCaches.Disabled, Logger); - var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository); + var entityRepository = new EntityRepository(accessor); + var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); var repository = new DocumentRepository(accessor, AppCaches, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors); return repository; diff --git a/src/Umbraco.Tests/Services/ContentServicePerformanceTest.cs b/src/Umbraco.Tests/Services/ContentServicePerformanceTest.cs index d7729c19c1..763635c393 100644 --- a/src/Umbraco.Tests/Services/ContentServicePerformanceTest.cs +++ b/src/Umbraco.Tests/Services/ContentServicePerformanceTest.cs @@ -42,15 +42,17 @@ namespace Umbraco.Tests.Services private DocumentRepository CreateDocumentRepository(IScopeProvider provider) { - var tRepository = new TemplateRepository((IScopeAccessor)provider, AppCaches.Disabled, Logger, TestObjects.GetFileSystemsMock()); - var tagRepo = new TagRepository((IScopeAccessor)provider, AppCaches.Disabled, Logger); - var commonRepository = new ContentTypeCommonRepository((IScopeAccessor)provider, tRepository, AppCaches); - var languageRepository = new LanguageRepository((IScopeAccessor)provider, AppCaches.Disabled, Logger); - var ctRepository = new ContentTypeRepository((IScopeAccessor)provider, AppCaches.Disabled, Logger, commonRepository, languageRepository); - var relationTypeRepository = new RelationTypeRepository((IScopeAccessor)provider, AppCaches.Disabled, Logger); - var relationRepository = new RelationRepository((IScopeAccessor)provider, Logger, relationTypeRepository); + var accessor = (IScopeAccessor)provider; + var tRepository = new TemplateRepository(accessor, AppCaches.Disabled, Logger, TestObjects.GetFileSystemsMock()); + var tagRepo = new TagRepository(accessor, AppCaches.Disabled, Logger); + var commonRepository = new ContentTypeCommonRepository(accessor, tRepository, AppCaches); + var languageRepository = new LanguageRepository(accessor, AppCaches.Disabled, Logger); + var ctRepository = new ContentTypeRepository(accessor, AppCaches.Disabled, Logger, commonRepository, languageRepository); + var relationTypeRepository = new RelationTypeRepository(accessor, AppCaches.Disabled, Logger); + var entityRepository = new EntityRepository(accessor); + var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var repository = new DocumentRepository((IScopeAccessor)provider, AppCaches.Disabled, Logger, ctRepository, tRepository, tagRepo, languageRepository, relationRepository, relationTypeRepository, propertyEditors); + var repository = new DocumentRepository(accessor, AppCaches.Disabled, Logger, ctRepository, tRepository, tagRepo, languageRepository, relationRepository, relationTypeRepository, propertyEditors); return repository; } diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs index 89873b5880..88b5cb5c34 100644 --- a/src/Umbraco.Tests/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs @@ -3208,7 +3208,8 @@ namespace Umbraco.Tests.Services var languageRepository = new LanguageRepository(accessor, AppCaches.Disabled, Logger); contentTypeRepository = new ContentTypeRepository(accessor, AppCaches.Disabled, Logger, commonRepository, languageRepository); var relationTypeRepository = new RelationTypeRepository(accessor, AppCaches.Disabled, Logger); - var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository); + var entityRepository = new EntityRepository(accessor); + var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); var repository = new DocumentRepository(accessor, AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors); return repository; From d1948b1543590d670fbe5689d91a304a16d063d4 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 6 Nov 2019 13:08:03 +1100 Subject: [PATCH 052/202] Adds relation service tests for both paged children and parents and tests the repo --- .../Repositories/IRelationRepository.cs | 2 + .../Implement/RelationRepository.cs | 11 ++ src/Umbraco.Core/Services/IRelationService.cs | 10 ++ .../Services/Implement/RelationService.cs | 11 +- .../Repositories/RelationRepositoryTest.cs | 111 ++++++++++++------ 5 files changed, 105 insertions(+), 40 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/IRelationRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IRelationRepository.cs index 9a99cb2a77..23271b34ee 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IRelationRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IRelationRepository.cs @@ -17,5 +17,7 @@ namespace Umbraco.Core.Persistence.Repositories void DeleteByParent(int parentId, params string[] relationTypeAliases); IEnumerable GetPagedParentEntitiesByChildId(int childId, long pageIndex, int pageSize, out long totalRecords); + + IEnumerable GetPagedChildEntitiesByParentId(int parentId, long pageIndex, int pageSize, out long totalRecords); } } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/RelationRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/RelationRepository.cs index 530872203c..71ca737fbb 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/RelationRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/RelationRepository.cs @@ -169,7 +169,18 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var entityQuery = Query().Where(e => e.Id != childId); return _entityRepository.GetPagedResultsByQuery(entityQuery, Array.Empty(), pageIndex, pageSize, out totalRecords, null, null, relQuery); + } + public IEnumerable GetPagedChildEntitiesByParentId(int parentId, long pageIndex, int pageSize, out long totalRecords) + { + // Create a query to match the parent id + var relQuery = Query().Where(r => r.ParentId == parentId); + + // Because of the way that the entity repository joins relations (on both child or parent) we need to add + // a clause to filter out the child entity from being returned from the results + var entityQuery = Query().Where(e => e.Id != parentId); + + return _entityRepository.GetPagedResultsByQuery(entityQuery, Array.Empty(), pageIndex, pageSize, out totalRecords, null, null, relQuery); } public void DeleteByParent(int parentId, params string[] relationTypeAliases) diff --git a/src/Umbraco.Core/Services/IRelationService.cs b/src/Umbraco.Core/Services/IRelationService.cs index 95f04b6df2..6bfe1f0be3 100644 --- a/src/Umbraco.Core/Services/IRelationService.cs +++ b/src/Umbraco.Core/Services/IRelationService.cs @@ -199,6 +199,16 @@ namespace Umbraco.Core.Services /// IEnumerable GetPagedParentEntitiesByChildId(int id, long pageIndex, int pageSize, out long totalChildren); + /// + /// Returns paged child entities for a related parent id + /// + /// + /// + /// + /// + /// + IEnumerable GetPagedChildEntitiesByParentId(int id, long pageIndex, int pageSize, out long totalChildren); + /// /// Gets the Parent and Child objects from a list of Relations as a list of objects. /// diff --git a/src/Umbraco.Core/Services/Implement/RelationService.cs b/src/Umbraco.Core/Services/Implement/RelationService.cs index 56bf2bba06..c6d541c48b 100644 --- a/src/Umbraco.Core/Services/Implement/RelationService.cs +++ b/src/Umbraco.Core/Services/Implement/RelationService.cs @@ -268,10 +268,17 @@ namespace Umbraco.Core.Services.Implement { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { - //var query = Query().Where(x => x.ChildId == id); return _relationRepository.GetPagedParentEntitiesByChildId(id, pageIndex, pageSize, out totalChildren); } - throw new NotImplementedException(); + } + + /// + public IEnumerable GetPagedChildEntitiesByParentId(int id, long pageIndex, int pageSize, out long totalChildren) + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + return _relationRepository.GetPagedChildEntitiesByParentId(id, pageIndex, pageSize, out totalChildren); + } } /// diff --git a/src/Umbraco.Tests/Persistence/Repositories/RelationRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/RelationRepositoryTest.cs index b67a662123..4c8a7c1b16 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/RelationRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/RelationRepositoryTest.cs @@ -174,44 +174,7 @@ namespace Umbraco.Tests.Persistence.Repositories [Test] public void Get_Paged_Parent_Entities_By_Child_Id() { - //Create content - var createdContent = new List(); - var contentType = MockedContentTypes.CreateBasicContentType("blah"); - ServiceContext.ContentTypeService.Save(contentType); - for (int i = 0; i < 10; i++) - { - var c1 = MockedContent.CreateBasicContent(contentType); - ServiceContext.ContentService.Save(c1); - createdContent.Add(c1); - } - - //Create media - var createdMedia = new List(); - var imageType = MockedContentTypes.CreateImageMediaType("myImage"); - ServiceContext.MediaTypeService.Save(imageType); - for (int i = 0; i < 10; i++) - { - var c1 = MockedMedia.CreateMediaImage(imageType, -1); - ServiceContext.MediaService.Save(c1); - createdMedia.Add(c1); - } - - // Create members - var memberType = MockedContentTypes.CreateSimpleMemberType("simple"); - ServiceContext.MemberTypeService.Save(memberType); - var createdMembers = MockedMember.CreateSimpleMember(memberType, 10).ToList(); - ServiceContext.MemberService.Save(createdMembers); - - var relType = ServiceContext.RelationService.GetRelationTypeByAlias(Constants.Conventions.RelationTypes.RelatedMediaAlias); - - // Relate content to media - foreach(var content in createdContent) - foreach(var media in createdMedia) - ServiceContext.RelationService.Relate(content.Id, media.Id, relType); - // Relate members to media - foreach (var member in createdMembers) - foreach (var media in createdMedia) - ServiceContext.RelationService.Relate(member.Id, media.Id, relType); + CreateTestDataForPagingTests(out var createdContent, out var createdMembers, out var createdMedia); var provider = TestObjects.GetScopeProvider(Logger); using (var scope = provider.CreateScope()) @@ -238,6 +201,78 @@ namespace Umbraco.Tests.Persistence.Repositories } } + [Test] + public void Get_Paged_Child_Entities_By_Parent_Id() + { + CreateTestDataForPagingTests(out var createdContent, out var createdMembers, out var createdMedia); + + var provider = TestObjects.GetScopeProvider(Logger); + using (var scope = provider.CreateScope()) + { + var repository = CreateRepository(provider, out var relationTypeRepository); + + // Get parent entities for child id + var parents = repository.GetPagedChildEntitiesByParentId(createdContent[0].Id, 0, 6, out var totalRecords).ToList(); + Assert.AreEqual(10, totalRecords); + Assert.AreEqual(6, parents.Count); + + //add the next page + parents.AddRange(repository.GetPagedChildEntitiesByParentId(createdContent[0].Id, 1, 6, out totalRecords)); + Assert.AreEqual(10, totalRecords); + Assert.AreEqual(10, parents.Count); + + var contentEntities = parents.OfType().ToList(); + var mediaEntities = parents.OfType().ToList(); + var memberEntities = parents.OfType().ToList(); + + Assert.AreEqual(0, contentEntities.Count); + Assert.AreEqual(10, mediaEntities.Count); + Assert.AreEqual(0, memberEntities.Count); + } + } + + private void CreateTestDataForPagingTests(out List createdContent, out List createdMembers, out List createdMedia) + { + //Create content + createdContent = new List(); + var contentType = MockedContentTypes.CreateBasicContentType("blah"); + ServiceContext.ContentTypeService.Save(contentType); + for (int i = 0; i < 10; i++) + { + var c1 = MockedContent.CreateBasicContent(contentType); + ServiceContext.ContentService.Save(c1); + createdContent.Add(c1); + } + + //Create media + createdMedia = new List(); + var imageType = MockedContentTypes.CreateImageMediaType("myImage"); + ServiceContext.MediaTypeService.Save(imageType); + for (int i = 0; i < 10; i++) + { + var c1 = MockedMedia.CreateMediaImage(imageType, -1); + ServiceContext.MediaService.Save(c1); + createdMedia.Add(c1); + } + + // Create members + var memberType = MockedContentTypes.CreateSimpleMemberType("simple"); + ServiceContext.MemberTypeService.Save(memberType); + createdMembers = MockedMember.CreateSimpleMember(memberType, 10).ToList(); + ServiceContext.MemberService.Save(createdMembers); + + var relType = ServiceContext.RelationService.GetRelationTypeByAlias(Constants.Conventions.RelationTypes.RelatedMediaAlias); + + // Relate content to media + foreach (var content in createdContent) + foreach (var media in createdMedia) + ServiceContext.RelationService.Relate(content.Id, media.Id, relType); + // Relate members to media + foreach (var member in createdMembers) + foreach (var media in createdMedia) + ServiceContext.RelationService.Relate(member.Id, media.Id, relType); + } + [Test] public void Can_Perform_Exists_On_RelationRepository() { From 9ef40fb4844c32f06a4fd9fbaebd192fa4e60499 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 6 Nov 2019 13:16:28 +1100 Subject: [PATCH 053/202] adds notes --- .../Repositories/Implement/RelationRepository.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/RelationRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/RelationRepository.cs index 71ca737fbb..235bcd8484 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/RelationRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/RelationRepository.cs @@ -168,6 +168,12 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // a clause to filter out the child entity from being returned from the results var entityQuery = Query().Where(e => e.Id != childId); + // var contentObjectTypes = new[] { Constants.ObjectTypes.Document, Constants.ObjectTypes.Media, Constants.ObjectTypes.Member } + // we could pass in the contentObjectTypes so that the entity repository sql is configured to do full entity lookups so that we get the full data + // required to populate content, media or members, else we get the bare minimum data needed to populate an entity. BUT if we do this it + // means that the SQL is less efficient and returns data that is probably not needed for what we need this lookup for. For the time being we + // will just return the bare minimum entity data. + return _entityRepository.GetPagedResultsByQuery(entityQuery, Array.Empty(), pageIndex, pageSize, out totalRecords, null, null, relQuery); } @@ -180,6 +186,12 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // a clause to filter out the child entity from being returned from the results var entityQuery = Query().Where(e => e.Id != parentId); + // var contentObjectTypes = new[] { Constants.ObjectTypes.Document, Constants.ObjectTypes.Media, Constants.ObjectTypes.Member } + // we could pass in the contentObjectTypes so that the entity repository sql is configured to do full entity lookups so that we get the full data + // required to populate content, media or members, else we get the bare minimum data needed to populate an entity. BUT if we do this it + // means that the SQL is less efficient and returns data that is probably not needed for what we need this lookup for. For the time being we + // will just return the bare minimum entity data. + return _entityRepository.GetPagedResultsByQuery(entityQuery, Array.Empty(), pageIndex, pageSize, out totalRecords, null, null, relQuery); } From 12541e7e8f41431ed05bfb02238862f337506d97 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 6 Nov 2019 13:22:12 +1100 Subject: [PATCH 054/202] reverts changes to IUmbracoEntity, we don't need to query on the object type property --- src/Umbraco.Core/Models/ContentBase.cs | 3 --- src/Umbraco.Core/Models/ContentTypeBase.cs | 3 --- src/Umbraco.Core/Models/DataType.cs | 3 --- src/Umbraco.Core/Models/Entities/IUmbracoEntity.cs | 7 ++----- src/Umbraco.Core/Models/EntityContainer.cs | 2 -- src/Umbraco.Core/Models/SimpleContentType.cs | 3 --- .../Persistence/Factories/ContentBaseFactory.cs | 2 +- .../Persistence/Factories/ContentTypeFactory.cs | 1 - src/Umbraco.Core/Persistence/Factories/DataTypeFactory.cs | 1 - .../Persistence/Mappers/UmbracoEntityMapper.cs | 1 - .../Persistence/Repositories/Implement/EntityRepository.cs | 1 - 11 files changed, 3 insertions(+), 24 deletions(-) diff --git a/src/Umbraco.Core/Models/ContentBase.cs b/src/Umbraco.Core/Models/ContentBase.cs index 2ea0fd855b..fbb68194b7 100644 --- a/src/Umbraco.Core/Models/ContentBase.cs +++ b/src/Umbraco.Core/Models/ContentBase.cs @@ -108,9 +108,6 @@ namespace Umbraco.Core.Models [IgnoreDataMember] public int VersionId { get; set; } - [DataMember] - public Guid NodeObjectType { get; internal set; } - /// /// Integer Id of the default ContentType /// diff --git a/src/Umbraco.Core/Models/ContentTypeBase.cs b/src/Umbraco.Core/Models/ContentTypeBase.cs index 1f3f1a5c13..04bcb7424a 100644 --- a/src/Umbraco.Core/Models/ContentTypeBase.cs +++ b/src/Umbraco.Core/Models/ContentTypeBase.cs @@ -113,9 +113,6 @@ namespace Umbraco.Core.Models OnPropertyChanged(nameof(PropertyTypes)); } - [DataMember] - public Guid NodeObjectType { get; internal set; } - /// /// The Alias of the ContentType /// diff --git a/src/Umbraco.Core/Models/DataType.cs b/src/Umbraco.Core/Models/DataType.cs index cba53bbd10..c237f6381c 100644 --- a/src/Umbraco.Core/Models/DataType.cs +++ b/src/Umbraco.Core/Models/DataType.cs @@ -65,9 +65,6 @@ namespace Umbraco.Core.Models [DataMember] public string EditorAlias => _editor.Alias; - [DataMember] - public Guid NodeObjectType { get; internal set; } - /// [DataMember] public ValueStorageType DatabaseType diff --git a/src/Umbraco.Core/Models/Entities/IUmbracoEntity.cs b/src/Umbraco.Core/Models/Entities/IUmbracoEntity.cs index 13087b5e95..e5f628b098 100644 --- a/src/Umbraco.Core/Models/Entities/IUmbracoEntity.cs +++ b/src/Umbraco.Core/Models/Entities/IUmbracoEntity.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; namespace Umbraco.Core.Models.Entities { @@ -12,7 +11,5 @@ namespace Umbraco.Core.Models.Entities /// An IUmbracoEntity can participate in notifications. /// public interface IUmbracoEntity : ITreeEntity, IRememberBeingDirty - { - Guid NodeObjectType { get; } - } + { } } diff --git a/src/Umbraco.Core/Models/EntityContainer.cs b/src/Umbraco.Core/Models/EntityContainer.cs index 71a477a9d6..70f6cbd878 100644 --- a/src/Umbraco.Core/Models/EntityContainer.cs +++ b/src/Umbraco.Core/Models/EntityContainer.cs @@ -62,8 +62,6 @@ namespace Umbraco.Core.Models /// public Guid ContainerObjectType => ObjectTypeMap[_containedObjectType]; - Guid IUmbracoEntity.NodeObjectType => ContainerObjectType; - /// /// Gets the container object type corresponding to a contained object type. /// diff --git a/src/Umbraco.Core/Models/SimpleContentType.cs b/src/Umbraco.Core/Models/SimpleContentType.cs index b4e7688eea..5c81017ec8 100644 --- a/src/Umbraco.Core/Models/SimpleContentType.cs +++ b/src/Umbraco.Core/Models/SimpleContentType.cs @@ -44,14 +44,11 @@ namespace Umbraco.Core.Models Name = contentType.Name; AllowedAsRoot = contentType.AllowedAsRoot; IsElement = contentType.IsElement; - NodeObjectType = contentType.NodeObjectType; } /// public string Alias { get; } - public Guid NodeObjectType { get; } - public int Id { get; } /// diff --git a/src/Umbraco.Core/Persistence/Factories/ContentBaseFactory.cs b/src/Umbraco.Core/Persistence/Factories/ContentBaseFactory.cs index 25cc6bc358..434e0393cd 100644 --- a/src/Umbraco.Core/Persistence/Factories/ContentBaseFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/ContentBaseFactory.cs @@ -34,7 +34,7 @@ namespace Umbraco.Core.Persistence.Factories content.VersionId = contentVersionDto.Id; content.Name = contentVersionDto.Text; - content.NodeObjectType = nodeDto.NodeObjectType ?? Guid.Empty; + content.Path = nodeDto.Path; content.Level = nodeDto.Level; content.ParentId = nodeDto.ParentId; diff --git a/src/Umbraco.Core/Persistence/Factories/ContentTypeFactory.cs b/src/Umbraco.Core/Persistence/Factories/ContentTypeFactory.cs index 46489aa69e..54cfee0ffa 100644 --- a/src/Umbraco.Core/Persistence/Factories/ContentTypeFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/ContentTypeFactory.cs @@ -107,7 +107,6 @@ namespace Umbraco.Core.Persistence.Factories { entity.Id = dto.NodeDto.NodeId; entity.Key = dto.NodeDto.UniqueId; - entity.NodeObjectType = dto.NodeDto.NodeObjectType ?? Guid.Empty; entity.Alias = dto.Alias; entity.Name = dto.NodeDto.Text; entity.Icon = dto.Icon; diff --git a/src/Umbraco.Core/Persistence/Factories/DataTypeFactory.cs b/src/Umbraco.Core/Persistence/Factories/DataTypeFactory.cs index 5d92234ca8..f189d38d05 100644 --- a/src/Umbraco.Core/Persistence/Factories/DataTypeFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/DataTypeFactory.cs @@ -28,7 +28,6 @@ namespace Umbraco.Core.Persistence.Factories dataType.DisableChangeTracking(); dataType.CreateDate = dto.NodeDto.CreateDate; - dataType.NodeObjectType = dto.NodeDto.NodeObjectType ?? Guid.Empty; dataType.DatabaseType = dto.DbType.EnumParse(true); dataType.Id = dto.NodeId; dataType.Key = dto.NodeDto.UniqueId; diff --git a/src/Umbraco.Core/Persistence/Mappers/UmbracoEntityMapper.cs b/src/Umbraco.Core/Persistence/Mappers/UmbracoEntityMapper.cs index 96f664cc25..16e2e8bcd5 100644 --- a/src/Umbraco.Core/Persistence/Mappers/UmbracoEntityMapper.cs +++ b/src/Umbraco.Core/Persistence/Mappers/UmbracoEntityMapper.cs @@ -24,7 +24,6 @@ namespace Umbraco.Core.Persistence.Mappers DefineMap(nameof(IUmbracoEntity.Trashed), nameof(NodeDto.Trashed)); DefineMap(nameof(IUmbracoEntity.Key), nameof(NodeDto.UniqueId)); DefineMap(nameof(IUmbracoEntity.CreatorId), nameof(NodeDto.UserId)); - DefineMap(nameof(IUmbracoEntity.NodeObjectType), nameof(NodeDto.NodeObjectType)); } } } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs index 9b05e95a17..8aa912dc0d 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs @@ -76,7 +76,6 @@ namespace Umbraco.Core.Persistence.Repositories.Implement public IEnumerable GetPagedResultsByQuery(IQuery query, Guid objectType, long pageIndex, int pageSize, out long totalRecords, IQuery filter, Ordering ordering) { - query = query.Where(x => x.NodeObjectType == objectType); return GetPagedResultsByQuery(query, new[] { objectType }, pageIndex, pageSize, out totalRecords, filter, ordering); } From b03bca409363fbcdd14fec140fadd1234b96b36f Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 6 Nov 2019 13:29:50 +1100 Subject: [PATCH 055/202] removes test code --- .../Implement/EntityRepository.cs | 39 +------------------ 1 file changed, 1 insertion(+), 38 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs index 8aa912dc0d..3ca28b6b44 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs @@ -35,44 +35,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected ISqlSyntaxProvider SqlSyntax => _scopeAccessor.AmbientScope.SqlContext.SqlSyntax; #region Repository - - //public IEnumerable GetPagedResults(IDictionary> typesAndIds, long pageIndex, int pageSize, out long totalRecords, Ordering ordering) - //{ - // var isContent = typesAndIds.Keys.Any(objectType => objectType == Constants.ObjectTypes.Document || objectType == Constants.ObjectTypes.DocumentBlueprint); - // var isMedia = typesAndIds.Keys.Any(objectType => objectType == Constants.ObjectTypes.Media); - // var isMember = typesAndIds.Keys.Any(objectType => objectType == Constants.ObjectTypes.Member); - - // var sql = GetBase(isContent, isMedia, isMember, null, false) - // .WhereIn(x => x.NodeObjectType, typesAndIds.Keys); - - // ordering = ordering ?? Ordering.ByDefault(); - - // sql = AddGroupBy(isContent, isMedia, isMember, sql, ordering.IsEmpty); - - // if (!ordering.IsEmpty) - // { - // // apply ordering - // ApplyOrdering(ref sql, ordering); - // } - - // // TODO: we should be able to do sql = sql.OrderBy(x => Alias(x.NodeId, "NodeId")); but we can't because the OrderBy extension don't support Alias currently - // //no matter what we always must have node id ordered at the end - // sql = ordering.Direction == Direction.Ascending ? sql.OrderBy("NodeId") : sql.OrderByDescending("NodeId"); - - // // for content we must query for ContentEntityDto entities to produce the correct culture variant entity names - // var pageIndexToFetch = pageIndex + 1; - // IEnumerable dtos; - // var page = Database.Page(pageIndexToFetch, pageSize, sql); - // dtos = page.Items; - // totalRecords = page.TotalItems; - - // var entities = dtos.Select(BuildEntity).ToArray(); - - // BuildVariants(entities.OfType()); - - // return entities; - //} - + public IEnumerable GetPagedResultsByQuery(IQuery query, Guid objectType, long pageIndex, int pageSize, out long totalRecords, IQuery filter, Ordering ordering) { From 90b6a09013b349ffc656b80737f9006ebe3313be Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 6 Nov 2019 14:35:15 +1100 Subject: [PATCH 056/202] Adds ability to get paged relations by type --- .../Repositories/IRelationRepository.cs | 4 + .../Implement/RelationRepository.cs | 47 ++++++ src/Umbraco.Core/Services/IRelationService.cs | 134 ++++++++++-------- .../Services/Implement/RelationService.cs | 10 ++ .../Services/RelationServiceTests.cs | 49 +++++++ 5 files changed, 182 insertions(+), 62 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/IRelationRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IRelationRepository.cs index 9a2e42568e..3b67c25856 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IRelationRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IRelationRepository.cs @@ -1,10 +1,14 @@ using System.Collections.Generic; using Umbraco.Core.Models; +using Umbraco.Core.Persistence.Querying; +using Umbraco.Core.Services; namespace Umbraco.Core.Persistence.Repositories { public interface IRelationRepository : IReadWriteQueryRepository { + IEnumerable GetPagedRelationsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, Ordering ordering); + /// /// Deletes all relations for a parent for any specified relation type alias /// diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/RelationRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/RelationRepository.cs index e6c4c6fd7e..7765086079 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/RelationRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/RelationRepository.cs @@ -6,11 +6,13 @@ using Umbraco.Core.Cache; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.Entities; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Factories; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.SqlSyntax; using Umbraco.Core.Scoping; +using Umbraco.Core.Services; using static Umbraco.Core.Persistence.NPocoSqlExtensions.Statics; namespace Umbraco.Core.Persistence.Repositories.Implement @@ -156,6 +158,37 @@ namespace Umbraco.Core.Persistence.Repositories.Implement #endregion + public IEnumerable GetPagedRelationsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, Ordering ordering) + { + var sql = GetBaseQuery(false); + + if (ordering == null || ordering.IsEmpty) + ordering = Ordering.By(SqlSyntax.GetQuotedColumn(Constants.DatabaseSchema.Tables.Relation, "id")); + + var translator = new SqlTranslator(sql, query); + sql = translator.Translate(); + + // apply ordering + ApplyOrdering(ref sql, ordering); + + var pageIndexToFetch = pageIndex + 1; + var page = Database.Page(pageIndexToFetch, pageSize, sql); + var dtos = page.Items; + totalRecords = page.TotalItems; + + var relTypes = _relationTypeRepository.GetMany(dtos.Select(x => x.RelationType).Distinct().ToArray()) + .ToDictionary(x => x.Id, x => x); + + var result = dtos.Select(r => + { + if (!relTypes.TryGetValue(r.RelationType, out var relType)) + throw new InvalidOperationException(string.Format("RelationType with Id: {0} doesn't exist", r.RelationType)); + return DtoToEntity(r, relType); + }).ToList(); + + return result; + } + public void DeleteByParent(int parentId, params string[] relationTypeAliases) { var subQuery = Sql().Select(x => x.Id) @@ -186,5 +219,19 @@ namespace Umbraco.Core.Persistence.Repositories.Implement entity.ChildObjectType = childObjectType.GetValueOrDefault(); } } + + private void ApplyOrdering(ref Sql sql, Ordering ordering) + { + if (sql == null) throw new ArgumentNullException(nameof(sql)); + if (ordering == null) throw new ArgumentNullException(nameof(ordering)); + + // TODO: although this works for name, it probably doesn't work for others without an alias of some sort + var orderBy = ordering.OrderBy; + + if (ordering.Direction == Direction.Ascending) + sql.OrderBy(orderBy); + else + sql.OrderByDescending(orderBy); + } } } diff --git a/src/Umbraco.Core/Services/IRelationService.cs b/src/Umbraco.Core/Services/IRelationService.cs index 49fa21af2b..1410b05531 100644 --- a/src/Umbraco.Core/Services/IRelationService.cs +++ b/src/Umbraco.Core/Services/IRelationService.cs @@ -8,152 +8,162 @@ namespace Umbraco.Core.Services public interface IRelationService : IService { /// - /// Gets a by its Id + /// Gets a by its Id /// - /// Id of the - /// A object + /// Id of the + /// A object IRelation GetById(int id); /// - /// Gets a by its Id + /// Gets a by its Id /// - /// Id of the - /// A object + /// Id of the + /// A object IRelationType GetRelationTypeById(int id); /// - /// Gets a by its Id + /// Gets a by its Id /// - /// Id of the - /// A object + /// Id of the + /// A object IRelationType GetRelationTypeById(Guid id); /// - /// Gets a by its Alias + /// Gets a by its Alias /// - /// Alias of the - /// A object + /// Alias of the + /// A object IRelationType GetRelationTypeByAlias(string alias); /// - /// Gets all objects + /// Gets all objects /// /// Optional array of integer ids to return relations for - /// An enumerable list of objects + /// An enumerable list of objects IEnumerable GetAllRelations(params int[] ids); /// - /// Gets all objects by their + /// Gets all objects by their /// - /// to retrieve Relations for - /// An enumerable list of objects + /// to retrieve Relations for + /// An enumerable list of objects IEnumerable GetAllRelationsByRelationType(IRelationType relationType); /// - /// Gets all objects by their 's Id + /// Gets all objects by their 's Id /// - /// Id of the to retrieve Relations for - /// An enumerable list of objects + /// Id of the to retrieve Relations for + /// An enumerable list of objects IEnumerable GetAllRelationsByRelationType(int relationTypeId); /// - /// Gets all objects + /// Gets all objects /// /// Optional array of integer ids to return relationtypes for - /// An enumerable list of objects + /// An enumerable list of objects IEnumerable GetAllRelationTypes(params int[] ids); /// - /// Gets a list of objects by their parent Id + /// Gets a list of objects by their parent Id /// /// Id of the parent to retrieve relations for - /// An enumerable list of objects + /// An enumerable list of objects IEnumerable GetByParentId(int id); /// - /// Gets a list of objects by their parent Id + /// Gets a list of objects by their parent Id /// /// Id of the parent to retrieve relations for /// Alias of the type of relation to retrieve - /// An enumerable list of objects + /// An enumerable list of objects IEnumerable GetByParentId(int id, string relationTypeAlias); /// - /// Gets a list of objects by their parent entity + /// Gets a list of objects by their parent entity /// /// Parent Entity to retrieve relations for - /// An enumerable list of objects + /// An enumerable list of objects IEnumerable GetByParent(IUmbracoEntity parent); /// - /// Gets a list of objects by their parent entity + /// Gets a list of objects by their parent entity /// /// Parent Entity to retrieve relations for /// Alias of the type of relation to retrieve - /// An enumerable list of objects + /// An enumerable list of objects IEnumerable GetByParent(IUmbracoEntity parent, string relationTypeAlias); /// - /// Gets a list of objects by their child Id + /// Gets a list of objects by their child Id /// /// Id of the child to retrieve relations for - /// An enumerable list of objects + /// An enumerable list of objects IEnumerable GetByChildId(int id); /// - /// Gets a list of objects by their child Id + /// Gets a list of objects by their child Id /// /// Id of the child to retrieve relations for /// Alias of the type of relation to retrieve - /// An enumerable list of objects + /// An enumerable list of objects IEnumerable GetByChildId(int id, string relationTypeAlias); /// - /// Gets a list of objects by their child Entity + /// Gets a list of objects by their child Entity /// /// Child Entity to retrieve relations for - /// An enumerable list of objects + /// An enumerable list of objects IEnumerable GetByChild(IUmbracoEntity child); /// - /// Gets a list of objects by their child Entity + /// Gets a list of objects by their child Entity /// /// Child Entity to retrieve relations for /// Alias of the type of relation to retrieve - /// An enumerable list of objects + /// An enumerable list of objects IEnumerable GetByChild(IUmbracoEntity child, string relationTypeAlias); /// - /// Gets a list of objects by their child or parent Id. + /// Gets a list of objects by their child or parent Id. /// Using this method will get you all relations regards of it being a child or parent relation. /// /// Id of the child or parent to retrieve relations for - /// An enumerable list of objects + /// An enumerable list of objects IEnumerable GetByParentOrChildId(int id); IEnumerable GetByParentOrChildId(int id, string relationTypeAlias); /// - /// Gets a list of objects by the Name of the + /// Gets a list of objects by the Name of the /// - /// Name of the to retrieve Relations for - /// An enumerable list of objects + /// Name of the to retrieve Relations for + /// An enumerable list of objects IEnumerable GetByRelationTypeName(string relationTypeName); /// - /// Gets a list of objects by the Alias of the + /// Gets a list of objects by the Alias of the /// - /// Alias of the to retrieve Relations for - /// An enumerable list of objects + /// Alias of the to retrieve Relations for + /// An enumerable list of objects IEnumerable GetByRelationTypeAlias(string relationTypeAlias); /// - /// Gets a list of objects by the Id of the + /// Gets a list of objects by the Id of the /// - /// Id of the to retrieve Relations for - /// An enumerable list of objects + /// Id of the to retrieve Relations for + /// An enumerable list of objects IEnumerable GetByRelationTypeId(int relationTypeId); + /// + /// Gets a paged result of + /// + /// + /// + /// + /// + /// + IEnumerable GetPagedByRelationTypeId(int relationTypeId, long pageIndex, int pageSize, out long totalRecords, Ordering ordering = null); + /// /// Gets the Child object from a Relation as an /// @@ -202,7 +212,7 @@ namespace Umbraco.Core.Services /// Id of the parent /// Id of the child /// The type of relation to create - /// The created + /// The created IRelation Relate(int parentId, int childId, IRelationType relationType); /// @@ -211,7 +221,7 @@ namespace Umbraco.Core.Services /// Parent entity /// Child entity /// The type of relation to create - /// The created + /// The created IRelation Relate(IUmbracoEntity parent, IUmbracoEntity child, IRelationType relationType); /// @@ -220,7 +230,7 @@ namespace Umbraco.Core.Services /// Id of the parent /// Id of the child /// Alias of the type of relation to create - /// The created + /// The created IRelation Relate(int parentId, int childId, string relationTypeAlias); /// @@ -229,14 +239,14 @@ namespace Umbraco.Core.Services /// Parent entity /// Child entity /// Alias of the type of relation to create - /// The created + /// The created IRelation Relate(IUmbracoEntity parent, IUmbracoEntity child, string relationTypeAlias); /// - /// Checks whether any relations exists for the passed in . + /// Checks whether any relations exists for the passed in . /// - /// to check for relations - /// Returns True if any relations exists for the given , otherwise False + /// to check for relations + /// Returns True if any relations exists for the given , otherwise False bool HasRelations(IRelationType relationType); /// @@ -281,33 +291,33 @@ namespace Umbraco.Core.Services bool AreRelated(int parentId, int childId, string relationTypeAlias); /// - /// Saves a + /// Saves a /// /// Relation to save void Save(IRelation relation); /// - /// Saves a + /// Saves a /// /// RelationType to Save void Save(IRelationType relationType); /// - /// Deletes a + /// Deletes a /// /// Relation to Delete void Delete(IRelation relation); /// - /// Deletes a + /// Deletes a /// /// RelationType to Delete void Delete(IRelationType relationType); /// - /// Deletes all objects based on the passed in + /// Deletes all objects based on the passed in /// - /// to Delete Relations for + /// to Delete Relations for void DeleteRelationsOfType(IRelationType relationType); } } diff --git a/src/Umbraco.Core/Services/Implement/RelationService.cs b/src/Umbraco.Core/Services/Implement/RelationService.cs index 490f36e7c7..a9d98383c7 100644 --- a/src/Umbraco.Core/Services/Implement/RelationService.cs +++ b/src/Umbraco.Core/Services/Implement/RelationService.cs @@ -207,6 +207,16 @@ namespace Umbraco.Core.Services.Implement } } + /// + public IEnumerable GetPagedByRelationTypeId(int relationTypeId, long pageIndex, int pageSize, out long totalRecords, Ordering ordering = null) + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + var query = Query().Where(x => x.RelationTypeId == relationTypeId); + return _relationRepository.GetPagedRelationsByQuery(query, pageIndex, pageSize, out totalRecords, ordering); + } + } + /// public IUmbracoEntity GetChildEntityFromRelation(IRelation relation) { diff --git a/src/Umbraco.Tests/Services/RelationServiceTests.cs b/src/Umbraco.Tests/Services/RelationServiceTests.cs index 0357c9e408..8406179d9a 100644 --- a/src/Umbraco.Tests/Services/RelationServiceTests.cs +++ b/src/Umbraco.Tests/Services/RelationServiceTests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Threading; using NUnit.Framework; @@ -15,6 +16,54 @@ namespace Umbraco.Tests.Services [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] public class RelationServiceTests : TestWithSomeContentBase { + + [Test] + public void Get_Paged_Relations_By_Relation_Type() + { + //Create content + var createdContent = new List(); + var contentType = MockedContentTypes.CreateBasicContentType("blah"); + ServiceContext.ContentTypeService.Save(contentType); + for (int i = 0; i < 10; i++) + { + var c1 = MockedContent.CreateBasicContent(contentType); + ServiceContext.ContentService.Save(c1); + createdContent.Add(c1); + } + + //Create media + var createdMedia = new List(); + var imageType = MockedContentTypes.CreateImageMediaType("myImage"); + ServiceContext.MediaTypeService.Save(imageType); + for (int i = 0; i < 10; i++) + { + var c1 = MockedMedia.CreateMediaImage(imageType, -1); + ServiceContext.MediaService.Save(c1); + createdMedia.Add(c1); + } + + var relType = ServiceContext.RelationService.GetRelationTypeByAlias(Constants.Conventions.RelationTypes.RelatedMediaAlias); + + // Relate content to media + foreach (var content in createdContent) + foreach (var media in createdMedia) + ServiceContext.RelationService.Relate(content.Id, media.Id, relType); + + var paged = ServiceContext.RelationService.GetPagedByRelationTypeId(relType.Id, 0, 51, out var totalRecs).ToList(); + + Assert.AreEqual(100, totalRecs); + Assert.AreEqual(51, paged.Count); + + //next page + paged.AddRange(ServiceContext.RelationService.GetPagedByRelationTypeId(relType.Id, 1, 51, out totalRecs)); + + Assert.AreEqual(100, totalRecs); + Assert.AreEqual(100, paged.Count); + + Assert.IsTrue(createdContent.Select(x => x.Id).ContainsAll(paged.Select(x => x.ParentId))); + Assert.IsTrue(createdMedia.Select(x => x.Id).ContainsAll(paged.Select(x => x.ChildId))); + } + [Test] public void Return_List_Of_Content_Items_Where_Media_Item_Referenced() { From 049d51e466e9fec0767493dfd52626845c8e789a Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 6 Nov 2019 16:45:28 +1100 Subject: [PATCH 057/202] Adds ability to bulk insert relations and adds tests --- .../Repositories/IRelationRepository.cs | 6 ++ .../Implement/ContentRepositoryBase.cs | 25 +++--- .../Implement/RelationRepository.cs | 71 ++++++++++++++--- src/Umbraco.Core/Services/IRelationService.cs | 2 + .../Services/Implement/RelationService.cs | 18 +++++ .../Services/RelationServiceTests.cs | 78 ++++++++++++++++++- 6 files changed, 175 insertions(+), 25 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/IRelationRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IRelationRepository.cs index 9a2e42568e..885d042d83 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IRelationRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IRelationRepository.cs @@ -5,6 +5,12 @@ namespace Umbraco.Core.Persistence.Repositories { public interface IRelationRepository : IReadWriteQueryRepository { + /// + /// Persist multiple at once + /// + /// + void Save(IEnumerable relations); + /// /// Deletes all relations for a parent for any specified relation type alias /// diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs index d93fd72bfb..65f6dc0472 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs @@ -853,21 +853,22 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var allRelationTypes = RelationTypeRepository.GetMany(Array.Empty()) .ToDictionary(x => x.Alias, x => x); - foreach(var rel in trackedRelations) - { - if (!allRelationTypes.TryGetValue(rel.RelationTypeAlias, out var relationType)) - throw new InvalidOperationException($"The relation type {rel.RelationTypeAlias} does not exist"); + var toSave = trackedRelations.Select(rel => + { + if (!allRelationTypes.TryGetValue(rel.RelationTypeAlias, out var relationType)) + throw new InvalidOperationException($"The relation type {rel.RelationTypeAlias} does not exist"); - if (!udiToGuids.TryGetValue(rel.Udi, out var guid)) - continue; // This shouldn't happen! + if (!udiToGuids.TryGetValue(rel.Udi, out var guid)) + return null; // This shouldn't happen! - if (!keyToIds.TryGetValue(guid, out var id)) - continue; // This shouldn't happen! + if (!keyToIds.TryGetValue(guid, out var id)) + return null; // This shouldn't happen! - //Create new relation - //TODO: This is N+1, we could do this all in one operation, just need a new method on the relations repo - RelationRepository.Save(new Relation(entity.Id, id, relationType)); - } + return new Relation(entity.Id, id, relationType); + }).WhereNotNull(); + + // Save bulk relations + RelationRepository.Save(toSave); } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/RelationRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/RelationRepository.cs index e6c4c6fd7e..b4fd0a349e 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/RelationRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/RelationRepository.cs @@ -156,6 +156,50 @@ namespace Umbraco.Core.Persistence.Repositories.Implement #endregion + public void Save(IEnumerable relations) + { + foreach (var hasIdentityGroup in relations.GroupBy(r => r.HasIdentity)) + { + if (hasIdentityGroup.Key) + { + // Do updates, we can't really do a bulk update so this is still a 1 by 1 operation + // however we can bulk populate the object types. It might be possible to bulk update + // with SQL but would be pretty ugly and we're not really too worried about that for perf, + // it's the bulk inserts we care about. + var asArray = hasIdentityGroup.ToArray(); + foreach (var relation in hasIdentityGroup) + { + relation.UpdatingEntity(); + var dto = RelationFactory.BuildDto(relation); + Database.Update(dto); + } + PopulateObjectTypes(asArray); + } + else + { + // Do bulk inserts + var entitiesAndDtos = hasIdentityGroup.ToDictionary( + r => // key = entity + { + r.AddingEntity(); + return r; + }, + RelationFactory.BuildDto); // value = DTO + + Database.InsertBulk(entitiesAndDtos.Values); + + // All dtos now have IDs assigned + foreach (var de in entitiesAndDtos) + { + // re-assign ID to the entity + de.Key.Id = de.Value.Id; + } + + PopulateObjectTypes(entitiesAndDtos.Keys.ToArray()); + } + } + } + public void DeleteByParent(int parentId, params string[] relationTypeAliases) { var subQuery = Sql().Select(x => x.Id) @@ -171,19 +215,28 @@ namespace Umbraco.Core.Persistence.Repositories.Implement Database.Execute(Sql().Delete().WhereIn(x => x.Id, subQuery)); } - private void PopulateObjectTypes(IRelation entity) + /// + /// Used to populate the object types after insert/update + /// + /// + private void PopulateObjectTypes(params IRelation[] entities) { - var nodes = Database.Fetch(Sql().Select().From().Where(x => x.NodeId == entity.ChildId || x.NodeId == entity.ParentId)) + var entityIds = entities.Select(x => x.ParentId).Concat(entities.Select(y => y.ChildId)).Distinct(); + + var nodes = Database.Fetch(Sql().Select().From() + .WhereIn(x => x.NodeId, entityIds)) .ToDictionary(x => x.NodeId, x => x.NodeObjectType); - if(nodes.TryGetValue(entity.ParentId, out var parentObjectType)) + foreach (var e in entities) { - entity.ParentObjectType = parentObjectType.GetValueOrDefault(); - } - - if(nodes.TryGetValue(entity.ChildId, out var childObjectType)) - { - entity.ChildObjectType = childObjectType.GetValueOrDefault(); + if (nodes.TryGetValue(e.ParentId, out var parentObjectType)) + { + e.ParentObjectType = parentObjectType.GetValueOrDefault(); + } + if (nodes.TryGetValue(e.ChildId, out var childObjectType)) + { + e.ChildObjectType = childObjectType.GetValueOrDefault(); + } } } } diff --git a/src/Umbraco.Core/Services/IRelationService.cs b/src/Umbraco.Core/Services/IRelationService.cs index 49fa21af2b..6660ca5d1f 100644 --- a/src/Umbraco.Core/Services/IRelationService.cs +++ b/src/Umbraco.Core/Services/IRelationService.cs @@ -286,6 +286,8 @@ namespace Umbraco.Core.Services /// Relation to save void Save(IRelation relation); + void Save(IEnumerable relations); + /// /// Saves a /// diff --git a/src/Umbraco.Core/Services/Implement/RelationService.cs b/src/Umbraco.Core/Services/Implement/RelationService.cs index 490f36e7c7..4a5ca3d8d8 100644 --- a/src/Umbraco.Core/Services/Implement/RelationService.cs +++ b/src/Umbraco.Core/Services/Implement/RelationService.cs @@ -417,6 +417,24 @@ namespace Umbraco.Core.Services.Implement } } + public void Save(IEnumerable relations) + { + using (var scope = ScopeProvider.CreateScope()) + { + var saveEventArgs = new SaveEventArgs(relations); + if (scope.Events.DispatchCancelable(SavingRelation, this, saveEventArgs)) + { + scope.Complete(); + return; + } + + _relationRepository.Save(relations); + scope.Complete(); + saveEventArgs.CanCancel = false; + scope.Events.Dispatch(SavedRelation, this, saveEventArgs); + } + } + /// public void Save(IRelationType relationType) { diff --git a/src/Umbraco.Tests/Services/RelationServiceTests.cs b/src/Umbraco.Tests/Services/RelationServiceTests.cs index 0357c9e408..42b6f45047 100644 --- a/src/Umbraco.Tests/Services/RelationServiceTests.cs +++ b/src/Umbraco.Tests/Services/RelationServiceTests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Threading; using NUnit.Framework; @@ -37,7 +38,7 @@ namespace Umbraco.Tests.Services ServiceContext.ContentService.Save(content); } - for (var i = 0; i < 6; i++) + for (var i = 0; i < 6; i++) createContentWithMediaRefs(); //create 6 content items referencing the same media var relations = ServiceContext.RelationService.GetByChildId(m1.Id, Constants.Conventions.RelationTypes.RelatedMediaAlias).ToList(); @@ -83,7 +84,7 @@ namespace Umbraco.Tests.Services [Test] public void Relation_Returns_Parent_Child_Object_Types_When_Creating() { - var r = CreateNewRelation("Test", "test"); + var r = CreateAndSaveRelation("Test", "test"); Assert.AreEqual(Constants.ObjectTypes.Document, r.ParentObjectType); Assert.AreEqual(Constants.ObjectTypes.Media, r.ChildObjectType); @@ -92,7 +93,7 @@ namespace Umbraco.Tests.Services [Test] public void Relation_Returns_Parent_Child_Object_Types_When_Getting() { - var r = CreateNewRelation("Test", "test"); + var r = CreateAndSaveRelation("Test", "test"); // re-get r = ServiceContext.RelationService.GetById(r.Id); @@ -101,7 +102,47 @@ namespace Umbraco.Tests.Services Assert.AreEqual(Constants.ObjectTypes.Media, r.ChildObjectType); } - private IRelation CreateNewRelation(string name, string alias) + [Test] + public void Insert_Bulk_Relations() + { + var rs = ServiceContext.RelationService; + + var newRelations = CreateRelations(10); + + Assert.IsTrue(newRelations.All(x => !x.HasIdentity)); + + ServiceContext.RelationService.Save(newRelations); + + Assert.IsTrue(newRelations.All(x => x.HasIdentity)); + } + + [Test] + public void Update_Bulk_Relations() + { + var rs = ServiceContext.RelationService; + + var date = DateTime.Now.AddDays(-10); + var newRelations = CreateRelations(10); + foreach (var r in newRelations) + { + r.CreateDate = date; + r.UpdateDate = date; + } + + //insert + ServiceContext.RelationService.Save(newRelations); + Assert.IsTrue(newRelations.All(x => x.UpdateDate == date)); + + var newDate = DateTime.Now.AddDays(-5); + foreach (var r in newRelations) + r.UpdateDate = newDate; + + //update + ServiceContext.RelationService.Save(newRelations); + Assert.IsTrue(newRelations.All(x => x.UpdateDate == newDate)); + } + + private IRelation CreateAndSaveRelation(string name, string alias) { var rs = ServiceContext.RelationService; var rt = new RelationType(name, alias, false, null, null); @@ -124,6 +165,35 @@ namespace Umbraco.Tests.Services return r; } + /// + /// Creates a bunch of content/media items return relation objects for them (unsaved) + /// + /// + /// + private IEnumerable CreateRelations(int count) + { + var rs = ServiceContext.RelationService; + var rtName = Guid.NewGuid().ToString(); + var rt = new RelationType(rtName, rtName, false, null, null); + rs.Save(rt); + + var ct = MockedContentTypes.CreateBasicContentType(); + ServiceContext.ContentTypeService.Save(ct); + + var mt = MockedContentTypes.CreateImageMediaType("img"); + ServiceContext.MediaTypeService.Save(mt); + + return Enumerable.Range(1, count).Select(index => + { + var c1 = MockedContent.CreateBasicContent(ct); + var c2 = MockedMedia.CreateMediaImage(mt, -1); + ServiceContext.ContentService.Save(c1); + ServiceContext.MediaService.Save(c2); + + return new Relation(c1.Id, c2.Id, rt); + }).ToList(); + } + //TODO: Create a relation for entities of the wrong Entity Type (GUID) based on the Relation Type's defined parent/child object types } } From bf2727a1ebc4a1ab0aed5a2321f2ea501230ab6f Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Wed, 6 Nov 2019 13:40:00 +0000 Subject: [PATCH 058/202] New model to return from WebAPI --- .../Models/ContentEditing/MediaReferences.cs | 24 +++++++++++++++++++ src/Umbraco.Web/Umbraco.Web.csproj | 3 ++- 2 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 src/Umbraco.Web/Models/ContentEditing/MediaReferences.cs diff --git a/src/Umbraco.Web/Models/ContentEditing/MediaReferences.cs b/src/Umbraco.Web/Models/ContentEditing/MediaReferences.cs new file mode 100644 index 0000000000..b10022b105 --- /dev/null +++ b/src/Umbraco.Web/Models/ContentEditing/MediaReferences.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using System.Linq; +using System.Runtime.Serialization; + +namespace Umbraco.Web.Models.ContentEditing +{ + [DataContract(Name = "mediaReferences", Namespace = "")] + public class MediaReferences + { + [DataMember(Name = "content")] + public IEnumerable Content { get; set; } = Enumerable.Empty(); + + [DataMember(Name = "members")] + public IEnumerable Members { get; set; } = Enumerable.Empty(); + + [DataMember(Name = "media")] + public IEnumerable Media { get; set; } = Enumerable.Empty(); + + [DataContract(Name = "entityType", Namespace = "")] + public class EntityTypeReferences : EntityBasic + { + } + } +} diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 60b70ed9a8..361a548123 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -222,6 +222,7 @@ + @@ -1283,4 +1284,4 @@ - + \ No newline at end of file From 1c7e2367f58cd2f8a32a594a48271218c897334a Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Wed, 6 Nov 2019 13:41:21 +0000 Subject: [PATCH 059/202] New WebAPI method to return references/relations for Media items --- src/Umbraco.Web/Editors/MediaController.cs | 67 ++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/src/Umbraco.Web/Editors/MediaController.cs b/src/Umbraco.Web/Editors/MediaController.cs index a673f06e1d..36750d74ec 100644 --- a/src/Umbraco.Web/Editors/MediaController.cs +++ b/src/Umbraco.Web/Editors/MediaController.cs @@ -36,6 +36,7 @@ using Umbraco.Core.PropertyEditors; using Umbraco.Web.ContentApps; using Umbraco.Web.Editors.Binders; using Umbraco.Web.Editors.Filters; +using Umbraco.Core.Models.Entities; namespace Umbraco.Web.Editors { @@ -943,5 +944,71 @@ namespace Umbraco.Web.Editors return hasPathAccess; } + + /// + /// Returns the references (usages) for the media item + /// + /// + /// + public MediaReferences GetReferences(int id) + { + var result = new MediaReferences(); + + var relations = Services.RelationService.GetByChildId(id, Constants.Conventions.RelationTypes.RelatedMediaAlias).ToList(); + var relationEntities = Services.RelationService.GetParentEntitiesFromRelations(relations).ToList(); + + var documents = new List(); + var members = new List(); + var media = new List(); + + foreach (var item in relationEntities) + { + switch (item) + { + case DocumentEntitySlim doc: + documents.Add(new MediaReferences.EntityTypeReferences { + Id = doc.Id, + Key = doc.Key, + Udi = Udi.Create(Constants.UdiEntityType.Document, doc.Key), + Icon = doc.ContentTypeIcon, + Name = doc.Name, + Alias = doc.ContentTypeAlias + }); + break; + + case MemberEntitySlim memb: + members.Add(new MediaReferences.EntityTypeReferences + { + Id = memb.Id, + Key = memb.Key, + Udi = Udi.Create(Constants.UdiEntityType.Member, memb.Key), + Icon = memb.ContentTypeIcon, + Name = memb.Name, + Alias = memb.ContentTypeAlias + }); + break; + + case MediaEntitySlim med: + media.Add(new MediaReferences.EntityTypeReferences + { + Id = med.Id, + Key = med.Key, + Udi = Udi.Create(Constants.UdiEntityType.Media, med.Key), + Icon = med.ContentTypeIcon, + Name = med.Name, + Alias = med.ContentTypeAlias + }); + break; + + default: + break; + } + } + + result.Content = documents; + result.Members = members; + result.Media = media; + return result; + } } } From 642247799a778c50d05e3399c06f192c28d8c55b Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Wed, 6 Nov 2019 13:42:00 +0000 Subject: [PATCH 060/202] Adds new AngularJS reference to call the new WebAPI endpoint --- .../src/common/resources/media.resource.js | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js index ca7700c188..58c02b55df 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js @@ -552,8 +552,31 @@ function mediaResource($q, $http, umbDataFormatter, umbRequestHelper) { "Search", args)), 'Failed to retrieve media items for search: ' + query); - } + }, + /** + * @ngdoc method + * @name umbraco.resources.mediaResource#getReferences + * @methodOf umbraco.resources.mediaResource + * + * @description + * Retrieves references of a given media item. + * + * @param {Int} id id of media node to retrieve references for + * @returns {Promise} resourcePromise object. + * + */ + getReferences: function (id) { + + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "mediaApiBaseUrl", + "GetReferences", + { id: id })), + "Failed to retrieve usages for media of id " + id); + + } }; } From 45392603bdf2d1e553f3a5eaca509dd005cde78b Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Wed, 6 Nov 2019 13:45:03 +0000 Subject: [PATCH 061/202] Adds in the new Media Relations tables to the directive view & JS to call the resource/WebAPI --- .../media/umbmedianodeinfo.directive.js | 26 +++- .../components/media/umb-media-node-info.html | 127 ++++++++++++++++-- 2 files changed, 139 insertions(+), 14 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/media/umbmedianodeinfo.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/media/umbmedianodeinfo.directive.js index 4993b013c7..18aa457862 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/media/umbmedianodeinfo.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/media/umbmedianodeinfo.directive.js @@ -1,13 +1,15 @@ (function () { 'use strict'; - function MediaNodeInfoDirective($timeout, $location, eventsService, userService, dateHelper, editorService, mediaHelper) { + function MediaNodeInfoDirective($timeout, $location, eventsService, userService, dateHelper, editorService, mediaHelper, mediaResource, $routeParams) { function link(scope, element, attrs, ctrl) { var evts = []; + var referencesLoaded = false; scope.allowChangeMediaType = false; + scope.loading = true; function onInit() { @@ -94,6 +96,19 @@ setMediaExtension(); }); + /** Loads in the media references one time */ + function loadRelations() { + if (!referencesLoaded) { + referencesLoaded = true; + mediaResource.getReferences($routeParams.id) + .then(function (data) { + scope.loading = false; + scope.references = data; + scope.hasReferences = data.content.length > 0 || data.members.length > 0; + }); + } + } + //ensure to unregister from all events! scope.$on('$destroy', function () { for (var e in evts) { @@ -102,6 +117,15 @@ }); onInit(); + + // load media type references when the 'info' tab is first activated/switched to + evts.push(eventsService.on("app.tabChange", function (event, args) { + $timeout(function () { + if (args.alias === "umbInfo") { + loadRelations(); + } + }); + })); } var directive = { diff --git a/src/Umbraco.Web.UI.Client/src/views/components/media/umb-media-node-info.html b/src/Umbraco.Web.UI.Client/src/views/components/media/umb-media-node-info.html index 4f7141559c..a82e1897a1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/media/umb-media-node-info.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/media/umb-media-node-info.html @@ -1,20 +1,25 @@
-
+ + + + +
+ + - +
-
+ +
+ @@ -39,12 +141,11 @@ - + From ab87bd200c68e09199b93ba6696a0605502abd52 Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Wed, 6 Nov 2019 13:45:21 +0000 Subject: [PATCH 062/202] Adds in lang backoffice keys to EN & EN_US --- src/Umbraco.Web.UI/Umbraco/config/lang/en.xml | 3 +++ src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml | 3 +++ 2 files changed, 6 insertions(+) diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index 19c8642dc5..c95f059934 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -2181,6 +2181,9 @@ To manage your website, simply open the Umbraco back office and start adding con Used in Member Types No references to Member Types. Used by + Used in Documents + Used in Members + Used in Media Log Levels diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml index 0c572fba26..e4cd5765c1 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -2197,6 +2197,9 @@ To manage your website, simply open the Umbraco back office and start adding con Used in Member Types No references to Member Types. Used by + Used in Documents + Used in Members + Used in Media Log Levels From eee33696e82e81a54ef35dbfea1ad2eb384380f3 Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Mon, 11 Nov 2019 11:05:57 +0000 Subject: [PATCH 063/202] Adss in missing XML params from signature --- src/Umbraco.Core/Persistence/Repositories/IEntityRepository.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Umbraco.Core/Persistence/Repositories/IEntityRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IEntityRepository.cs index 102dc7c81a..be626b8d8b 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IEntityRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IEntityRepository.cs @@ -46,11 +46,13 @@ namespace Umbraco.Core.Persistence.Repositories /// Gets paged entities for a query and a subset of object types ///
/// + /// /// /// /// /// /// + /// /// /// A collection of mixed entity types which would be of type , , , /// From dd1c8ec6a3e699b66706b48821fc679bf92f8f63 Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Mon, 11 Nov 2019 12:05:26 +0000 Subject: [PATCH 064/202] Revert a C#8 fancy new switch as AzureDevOps does not like that --- .../Repositories/Implement/EntityRepository.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs index 3ca28b6b44..a0d2baa907 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs @@ -516,11 +516,17 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // TODO: although the default ordering string works for name, it wont work for others without a table or an alias of some sort // As more things are attempted to be sorted we'll prob have to add more expressions here - var orderBy = ordering.OrderBy.ToUpperInvariant() switch + string orderBy; + switch (ordering.OrderBy.ToUpperInvariant()) { - "PATH" => SqlSyntax.GetQuotedColumn(NodeDto.TableName, "path"), - _ => ordering.OrderBy - }; + case "PATH": + orderBy = SqlSyntax.GetQuotedColumn(NodeDto.TableName, "path"); + break; + + default: + orderBy = ordering.OrderBy; + break; + } if (ordering.Direction == Direction.Ascending) sql.OrderBy(orderBy); From 8dcec429d359e68554d5f653dc987b2ac14501a2 Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Mon, 11 Nov 2019 15:16:19 +0000 Subject: [PATCH 065/202] Remove relations for the RelationType overview --- .../ContentEditing/RelationTypeDisplay.cs | 7 ------ .../Models/Mapping/RelationMapDefinition.cs | 22 ------------------- 2 files changed, 29 deletions(-) diff --git a/src/Umbraco.Web/Models/ContentEditing/RelationTypeDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/RelationTypeDisplay.cs index 8a92d085eb..b7f209cd20 100644 --- a/src/Umbraco.Web/Models/ContentEditing/RelationTypeDisplay.cs +++ b/src/Umbraco.Web/Models/ContentEditing/RelationTypeDisplay.cs @@ -47,13 +47,6 @@ namespace Umbraco.Web.Models.ContentEditing [ReadOnly(true)] public string ChildObjectTypeName { get; set; } - /// - /// Gets or sets the relations associated with this relation type. - /// - [DataMember(Name = "relations")] - [ReadOnly(true)] - public IEnumerable Relations { get; set; } - /// /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes. /// diff --git a/src/Umbraco.Web/Models/Mapping/RelationMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/RelationMapDefinition.cs index 8407a7421c..2f111ffb5a 100644 --- a/src/Umbraco.Web/Models/Mapping/RelationMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/RelationMapDefinition.cs @@ -51,28 +51,6 @@ namespace Umbraco.Web.Models.Mapping var objType = ObjectTypes.GetUmbracoObjectType(source.ChildObjectType.Value); target.ChildObjectTypeName = objType.GetFriendlyName(); } - - // Load the relations - - var relations = _relationService.GetByRelationTypeId(source.Id); - var displayRelations = context.MapEnumerable(relations); - - // Load the entities - var entities = _relationService.GetEntitiesFromRelations(relations) - .ToDictionary(x => (x.Item1.Id, x.Item2.Id), x => x); - - foreach(var r in displayRelations) - { - var pair = entities[(r.ParentId, r.ChildId)]; - var parent = pair.Item1; - var child = pair.Item2; - - r.ChildName = child.Name; - r.ParentName = parent.Name; - } - - target.Relations = displayRelations; - } // Umbraco.Code.MapAll -ParentName -ChildName From bbcdcbdde0ea25f543d68fc9732212ad1c34af65 Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Tue, 12 Nov 2019 14:09:36 +0000 Subject: [PATCH 066/202] Start adding in pagination to relations --- .../Implement/RelationRepository.cs | 2 +- .../common/resources/relationtype.resource.js | 43 ++++++++++++++++ .../views/relationtypes/edit.controller.js | 38 ++++++++++++-- .../views/relationtypes/views/relations.html | 51 +++++++++++-------- .../Editors/RelationTypeController.cs | 19 +++++++ 5 files changed, 129 insertions(+), 24 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/RelationRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/RelationRepository.cs index 68575b2bab..9b04cd3d76 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/RelationRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/RelationRepository.cs @@ -253,7 +253,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // apply ordering ApplyOrdering(ref sql, ordering); - var pageIndexToFetch = pageIndex + 1; + var pageIndexToFetch = pageIndex; var page = Database.Page(pageIndexToFetch, pageSize, sql); var dtos = page.Items; totalRecords = page.TotalItems; diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/relationtype.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/relationtype.resource.js index 7f13a46d2f..5a7616dd1d 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/relationtype.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/relationtype.resource.js @@ -114,6 +114,49 @@ function relationTypeResource($q, $http, umbRequestHelper, umbDataFormatter) { $http.post(umbRequestHelper.getApiUrl("relationTypeApiBaseUrl", "DeleteById", [{ id: id }])), "Failed to delete item " + id ); + }, + + getPagedResults: function (id, options) { + + console.log('options in', options); + + var defaults = { + pageSize: 100, + pageNumber: 1, + orderDirection: "Ascending", + orderBy: "SortOrder" + }; + if (options === undefined) { + options = {}; + } + //overwrite the defaults if there are any specified + angular.extend(defaults, options); + //now copy back to the options we will use + options = defaults; + //change asc/desct + if (options.orderDirection === "asc") { + options.orderDirection = "Ascending"; + } + else if (options.orderDirection === "desc") { + options.orderDirection = "Descending"; + } + + console.log('options before HTTP', options); + + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "relationTypeApiBaseUrl", + "GetPagedResults", + { + id: id, + pageNumber: options.pageNumber, + pageSize: options.pageSize, + orderBy: options.orderBy, + orderDirection: options.orderDirection + } + )), + 'Failed to get paged relations for id ' + id); } }; diff --git a/src/Umbraco.Web.UI.Client/src/views/relationtypes/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/relationtypes/edit.controller.js index f83829dfba..d2bb3ef41d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/relationtypes/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/relationtypes/edit.controller.js @@ -6,7 +6,7 @@ * @description * The controller for editing relation types. */ -function RelationTypeEditController($scope, $routeParams, relationTypeResource, editorState, navigationService, dateHelper, userService, entityResource, formHelper, contentEditingHelper, localizationService) { +function RelationTypeEditController($scope, $routeParams, relationTypeResource, editorState, navigationService, dateHelper, userService, entityResource, formHelper, contentEditingHelper, localizationService, eventsService) { var vm = this; @@ -15,12 +15,19 @@ function RelationTypeEditController($scope, $routeParams, relationTypeResource, vm.page.saveButtonState = "init"; vm.page.menu = {} + //var referencesLoaded = false; + vm.save = saveRelationType; init(); function init() { vm.page.loading = true; + vm.relationsLoading = true; + + vm.changePageNumber = changePageNumber; + vm.options = {}; + vm.options.pageSize = 3; var labelKeys = [ "relationType_tabRelationType", @@ -45,6 +52,13 @@ function RelationTypeEditController($scope, $routeParams, relationTypeResource, ]; }); + // load media type references when the 'info' tab is first activated/switched to + eventsService.on("app.tabChange", function (event, args) { + if (args.alias === "relations") { + loadRelations(); + } + }); + relationTypeResource.getById($routeParams.id) .then(function (data) { bindRelationType(data); @@ -52,9 +66,27 @@ function RelationTypeEditController($scope, $routeParams, relationTypeResource, }); } - function bindRelationType(relationType) { - formatDates(relationType.relations); + function changePageNumber(pageNumber) { + alert('pagenumber' + pageNumber); + vm.options.pageNumber = pageNumber; + loadRelations(); + } + + /** Loads in the references one time when content app changed */ + function loadRelations() { + relationTypeResource.getPagedResults($routeParams.id, vm.options) + .then(function (data) { + console.log('paged data', data); + formatDates(data.items); + + vm.relationsLoading = false; + vm.relations = data; + }); + } + + + function bindRelationType(relationType) { // Convert property value to string, since the umb-radiobutton component at the moment only handle string values. // Sometime later the umb-radiobutton might be able to handle value as boolean. relationType.isBidirectional = (relationType.isBidirectional || false).toString(); diff --git a/src/Umbraco.Web.UI.Client/src/views/relationtypes/views/relations.html b/src/Umbraco.Web.UI.Client/src/views/relationtypes/views/relations.html index 96e86c2303..b525fbcba3 100644 --- a/src/Umbraco.Web.UI.Client/src/views/relationtypes/views/relations.html +++ b/src/Umbraco.Web.UI.Client/src/views/relationtypes/views/relations.html @@ -1,28 +1,39 @@ - + + + - + No relations for this relation type. - -
- - - - - - - - - - - - - -
ParentChildCreatedComment
{{relation.parentName}}{{relation.childName}}{{relation.timestampFormatted}}{{relation.comment}}
-
-
+ +
+ + + + + + + + + + + + + +
ParentChildCreatedComment
{{relation.parentName}}{{relation.childName}}{{relation.timestampFormatted}}{{relation.comment}}
+
+ + +
+ + +
+
diff --git a/src/Umbraco.Web/Editors/RelationTypeController.cs b/src/Umbraco.Web/Editors/RelationTypeController.cs index 2845a82aa1..14726b1074 100644 --- a/src/Umbraco.Web/Editors/RelationTypeController.cs +++ b/src/Umbraco.Web/Editors/RelationTypeController.cs @@ -3,12 +3,14 @@ using System.Collections.Generic; using System.Net; using System.Net.Http; using System.Web.Http; +using System.Linq; using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Configuration; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Services; using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Mvc; @@ -50,6 +52,23 @@ namespace Umbraco.Web.Editors return display; } + public PagedResult GetPagedResults(int id, int pageNumber = 1, int pageSize = 100) + { + + if (pageNumber <= 0 || pageSize <= 0) + { + throw new NotSupportedException("Both pageNumber and pageSize must be greater than zero"); + } + + // Ordering do we need to pass through? + var relations = Services.RelationService.GetPagedByRelationTypeId(id, pageNumber, pageSize, out long totalRecords); + + return new PagedResult(totalRecords, pageNumber, pageSize) + { + Items = relations.Select(x => Mapper.Map(x)) + }; + } + /// /// Gets a list of object types which can be associated via relations. /// From 05c1f0f38903f364bf86fc2a9332b3012f705fce Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Tue, 12 Nov 2019 14:50:43 +0000 Subject: [PATCH 067/202] Fix up the missing nulls & ammend the mapping --- src/Umbraco.Web/Models/Mapping/RelationMapDefinition.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Umbraco.Web/Models/Mapping/RelationMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/RelationMapDefinition.cs index 2f111ffb5a..d26a867858 100644 --- a/src/Umbraco.Web/Models/Mapping/RelationMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/RelationMapDefinition.cs @@ -60,6 +60,11 @@ namespace Umbraco.Web.Models.Mapping target.Comment = source.Comment; target.CreateDate = source.CreateDate; target.ParentId = source.ParentId; + + var entities = _relationService.GetEntitiesFromRelation(source); + + target.ParentName = entities.Item1.Name; + target.ChildName = entities.Item2.Name; } // Umbraco.Code.MapAll -CreateDate -UpdateDate -DeleteDate From 411bf067e5e2bf635f2a3345724272d82350f60b Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Tue, 12 Nov 2019 19:11:16 +0000 Subject: [PATCH 068/202] Bit of tidying up --- .../src/views/relationtypes/edit.controller.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/relationtypes/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/relationtypes/edit.controller.js index d2bb3ef41d..65b581af3d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/relationtypes/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/relationtypes/edit.controller.js @@ -15,8 +15,6 @@ function RelationTypeEditController($scope, $routeParams, relationTypeResource, vm.page.saveButtonState = "init"; vm.page.menu = {} - //var referencesLoaded = false; - vm.save = saveRelationType; init(); @@ -52,13 +50,14 @@ function RelationTypeEditController($scope, $routeParams, relationTypeResource, ]; }); - // load media type references when the 'info' tab is first activated/switched to + // load references when the 'relations' tab is first activated/switched to eventsService.on("app.tabChange", function (event, args) { if (args.alias === "relations") { loadRelations(); } }); + // Inital page/overview API call of relation type relationTypeResource.getById($routeParams.id) .then(function (data) { bindRelationType(data); @@ -67,7 +66,6 @@ function RelationTypeEditController($scope, $routeParams, relationTypeResource, } function changePageNumber(pageNumber) { - alert('pagenumber' + pageNumber); vm.options.pageNumber = pageNumber; loadRelations(); } @@ -77,9 +75,7 @@ function RelationTypeEditController($scope, $routeParams, relationTypeResource, function loadRelations() { relationTypeResource.getPagedResults($routeParams.id, vm.options) .then(function (data) { - console.log('paged data', data); formatDates(data.items); - vm.relationsLoading = false; vm.relations = data; }); From 9b87b65537d92ca954db116f9948561fd55ea411 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Wed, 13 Nov 2019 14:39:11 +0100 Subject: [PATCH 069/202] Create Examine query that only searches content --- src/Umbraco.Web/PublishedContentQuery.cs | 40 ++++++++++-------------- 1 file changed, 16 insertions(+), 24 deletions(-) diff --git a/src/Umbraco.Web/PublishedContentQuery.cs b/src/Umbraco.Web/PublishedContentQuery.cs index 72aec94e58..8f4242c180 100644 --- a/src/Umbraco.Web/PublishedContentQuery.cs +++ b/src/Umbraco.Web/PublishedContentQuery.cs @@ -201,39 +201,31 @@ namespace Umbraco.Web throw new InvalidOperationException($"No index found by name {indexName} or is not of type {typeof(IUmbracoIndex)}"); } - var searcher = umbIndex.GetSearcher(); + var query = umbIndex.GetSearcher().CreateQuery(IndexTypes.Content); - ISearchResults results; + IQueryExecutor queryExecutor; if (culture == "*") { // Search everything - results = skip == 0 && take == 0 - ? searcher.Search(term) - : searcher.Search(term, skip + take); + queryExecutor = query.ManagedQuery(term); + } + else if (string.IsNullOrWhiteSpace(culture)) + { + // Only search invariant + queryExecutor = query.Field(UmbracoContentIndex.VariesByCultureFieldName, "n") // Must not vary by culture + .And().ManagedQuery(term); } else { - IBooleanOperation query; - if (string.IsNullOrWhiteSpace(culture)) - { - // Only search invariant - query = searcher.CreateQuery() - .Field(UmbracoContentIndex.VariesByCultureFieldName, "n") // Must not vary by culture - .And().ManagedQuery(term); - } - else - { - // Only search the specified culture - var fields = umbIndex.GetCultureAndInvariantFields(culture).ToArray(); // Get all index fields suffixed with the culture name supplied - query = searcher.CreateQuery() - .ManagedQuery(term, fields); - } - - results = skip == 0 && take == 0 - ? query.Execute() - : query.Execute(skip + take); + // Only search the specified culture + var fields = umbIndex.GetCultureAndInvariantFields(culture).ToArray(); // Get all index fields suffixed with the culture name supplied + queryExecutor = query.ManagedQuery(term, fields); } + var results = skip == 0 && take == 0 + ? queryExecutor.Execute() + : queryExecutor.Execute(skip + take); + totalRecords = results.TotalItemCount; return new CultureContextualSearchResults(results.Skip(skip).ToPublishedSearchResults(_publishedSnapshot.Content), _variationContextAccessor, culture); From b64eb05e792abb9a3c44dda14f173541644deac1 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Wed, 13 Nov 2019 14:39:52 +0100 Subject: [PATCH 070/202] Update documentation of Search overloads --- src/Umbraco.Web/IPublishedContentQuery.cs | 48 +++++++++++++++-------- 1 file changed, 31 insertions(+), 17 deletions(-) diff --git a/src/Umbraco.Web/IPublishedContentQuery.cs b/src/Umbraco.Web/IPublishedContentQuery.cs index 8a8d678aba..351847da73 100644 --- a/src/Umbraco.Web/IPublishedContentQuery.cs +++ b/src/Umbraco.Web/IPublishedContentQuery.cs @@ -35,12 +35,15 @@ namespace Umbraco.Web /// /// Searches content. /// - /// Term to search. - /// Optional culture. - /// Optional index name. + /// The term to search. + /// The culture (defaults to a culture insensitive search). + /// The name of the index to search (defaults to ). + /// + /// The search results. + /// /// /// - /// When the is not specified or is *, all cultures are searched. + /// When the is not specified or is *, all cultures are searched. /// To search for only invariant documents and fields use null. /// When searching on a specific culture, all culture specific fields are searched for the provided culture and all invariant fields for all documents. /// @@ -51,15 +54,18 @@ namespace Umbraco.Web /// /// Searches content. /// - /// Term to search. - /// Numbers of items to skip. - /// Numbers of items to return. - /// Total number of matching items. - /// Optional culture. - /// Optional index name. + /// The term to search. + /// The amount of results to skip. + /// The amount of results to take/return. + /// The total amount of records. + /// The culture (defaults to a culture insensitive search). + /// The name of the index to search (defaults to ). + /// + /// The search results. + /// /// /// - /// When the is not specified or is *, all cultures are searched. + /// When the is not specified or is *, all cultures are searched. /// To search for only invariant documents and fields use null. /// When searching on a specific culture, all culture specific fields are searched for the provided culture and all invariant fields for all documents. /// @@ -68,18 +74,26 @@ namespace Umbraco.Web IEnumerable Search(string term, int skip, int take, out long totalRecords, string culture = "*", string indexName = null); /// - /// Executes the query and converts the results to PublishedSearchResult. + /// Executes the query and converts the results to using the . /// - /// - /// While enumerating results, the ambient culture is changed to be the searched culture. - /// + /// The query. + /// + /// The search results. + /// IEnumerable Search(IQueryExecutor query); /// - /// Executes the query and converts the results to PublishedSearchResult. + /// Executes the query and converts the results to using the . /// + /// The query. + /// The amount of results to skip. + /// The amount of results to take/return. + /// The total amount of records. + /// + /// The search results. + /// /// - /// While enumerating results, the ambient culture is changed to be the searched culture. + /// Make sure only content is searched (searcher.CreateQuery(IndexTypes.Content)), as this only returns content, but the could otherwise also include media records. /// IEnumerable Search(IQueryExecutor query, int skip, int take, out long totalRecords); } From 2314b4de5e25de35f0aef4c9fcb28b51281a87af Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Wed, 13 Nov 2019 14:35:13 +0000 Subject: [PATCH 071/202] Make fixes based on Shan's review notes --- .../Implement/RelationRepository.cs | 2 +- .../common/resources/relationtype.resource.js | 23 ++++--------------- .../views/relationtypes/edit.controller.js | 1 - .../Editors/RelationTypeController.cs | 2 +- 4 files changed, 6 insertions(+), 22 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/RelationRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/RelationRepository.cs index 9b04cd3d76..68575b2bab 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/RelationRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/RelationRepository.cs @@ -253,7 +253,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // apply ordering ApplyOrdering(ref sql, ordering); - var pageIndexToFetch = pageIndex; + var pageIndexToFetch = pageIndex + 1; var page = Database.Page(pageIndexToFetch, pageSize, sql); var dtos = page.Items; totalRecords = page.TotalItems; diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/relationtype.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/relationtype.resource.js index 5a7616dd1d..7c542c5e7b 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/relationtype.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/relationtype.resource.js @@ -118,13 +118,9 @@ function relationTypeResource($q, $http, umbRequestHelper, umbDataFormatter) { getPagedResults: function (id, options) { - console.log('options in', options); - var defaults = { - pageSize: 100, - pageNumber: 1, - orderDirection: "Ascending", - orderBy: "SortOrder" + pageSize: 25, + pageNumber: 1 }; if (options === undefined) { options = {}; @@ -133,16 +129,7 @@ function relationTypeResource($q, $http, umbRequestHelper, umbDataFormatter) { angular.extend(defaults, options); //now copy back to the options we will use options = defaults; - //change asc/desct - if (options.orderDirection === "asc") { - options.orderDirection = "Ascending"; - } - else if (options.orderDirection === "desc") { - options.orderDirection = "Descending"; - } - - console.log('options before HTTP', options); - + return umbRequestHelper.resourcePromise( $http.get( umbRequestHelper.getApiUrl( @@ -151,9 +138,7 @@ function relationTypeResource($q, $http, umbRequestHelper, umbDataFormatter) { { id: id, pageNumber: options.pageNumber, - pageSize: options.pageSize, - orderBy: options.orderBy, - orderDirection: options.orderDirection + pageSize: options.pageSize } )), 'Failed to get paged relations for id ' + id); diff --git a/src/Umbraco.Web.UI.Client/src/views/relationtypes/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/relationtypes/edit.controller.js index 65b581af3d..44fbf6ffe9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/relationtypes/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/relationtypes/edit.controller.js @@ -25,7 +25,6 @@ function RelationTypeEditController($scope, $routeParams, relationTypeResource, vm.changePageNumber = changePageNumber; vm.options = {}; - vm.options.pageSize = 3; var labelKeys = [ "relationType_tabRelationType", diff --git a/src/Umbraco.Web/Editors/RelationTypeController.cs b/src/Umbraco.Web/Editors/RelationTypeController.cs index 14726b1074..f12faf77cc 100644 --- a/src/Umbraco.Web/Editors/RelationTypeController.cs +++ b/src/Umbraco.Web/Editors/RelationTypeController.cs @@ -61,7 +61,7 @@ namespace Umbraco.Web.Editors } // Ordering do we need to pass through? - var relations = Services.RelationService.GetPagedByRelationTypeId(id, pageNumber, pageSize, out long totalRecords); + var relations = Services.RelationService.GetPagedByRelationTypeId(id, pageNumber -1, pageSize, out long totalRecords); return new PagedResult(totalRecords, pageNumber, pageSize) { From 634f64851753f612b938475238aa687a0dc3c749 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Thu, 14 Nov 2019 09:20:18 +0100 Subject: [PATCH 072/202] Require Examine 1.0.2 --- build/NuSpecs/UmbracoCms.Web.nuspec | 2 +- src/Umbraco.Examine/Umbraco.Examine.csproj | 4 ++-- src/Umbraco.Tests/Umbraco.Tests.csproj | 2 +- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 4 ++-- src/Umbraco.Web/Umbraco.Web.csproj | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/build/NuSpecs/UmbracoCms.Web.nuspec b/build/NuSpecs/UmbracoCms.Web.nuspec index c8374bc2f7..72b4df2ecb 100644 --- a/build/NuSpecs/UmbracoCms.Web.nuspec +++ b/build/NuSpecs/UmbracoCms.Web.nuspec @@ -28,7 +28,7 @@ - + diff --git a/src/Umbraco.Examine/Umbraco.Examine.csproj b/src/Umbraco.Examine/Umbraco.Examine.csproj index db623ecddd..0e0ee62139 100644 --- a/src/Umbraco.Examine/Umbraco.Examine.csproj +++ b/src/Umbraco.Examine/Umbraco.Examine.csproj @@ -49,7 +49,7 @@ - + 1.0.0-beta2-19324-01 runtime; build; native; contentfiles; analyzers; buildtransitive @@ -112,4 +112,4 @@ - + \ No newline at end of file diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 39826fcc38..47542484d8 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -79,7 +79,7 @@ - + 1.8.14 diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 6e766f578e..70039488ed 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -87,7 +87,7 @@ - + @@ -431,4 +431,4 @@ - + \ No newline at end of file diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 56feaf52e7..9947a5e1de 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -63,7 +63,7 @@ - + 2.7.0.100 @@ -1279,4 +1279,4 @@ - + \ No newline at end of file From c206382fa105ebe4140fc724c5c987a4bf5bba22 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Thu, 14 Nov 2019 09:23:18 +0100 Subject: [PATCH 073/202] Use snapshot to convert search results into content, media or members --- src/Umbraco.Web/ExamineExtensions.cs | 83 ++++++++++++++++++++--- src/Umbraco.Web/IPublishedContentQuery.cs | 7 +- src/Umbraco.Web/PublishedContentQuery.cs | 2 +- 3 files changed, 76 insertions(+), 16 deletions(-) diff --git a/src/Umbraco.Web/ExamineExtensions.cs b/src/Umbraco.Web/ExamineExtensions.cs index 9a9fa98d95..8fead91194 100644 --- a/src/Umbraco.Web/ExamineExtensions.cs +++ b/src/Umbraco.Web/ExamineExtensions.cs @@ -2,32 +2,95 @@ using System.Collections.Generic; using System.Linq; using Examine; -using Umbraco.Core; +using Examine.LuceneEngine.Providers; using Umbraco.Core.Models.PublishedContent; +using Umbraco.Examine; using Umbraco.Web.PublishedCache; namespace Umbraco.Web { /// - /// Extension methods for Examine + /// Extension methods for Examine. /// public static class ExamineExtensions { + /// + /// Creates an containing all content from the . + /// + /// The search results. + /// The cache to fetch the content from. + /// + /// An containing all content. + /// + /// cache + /// + /// Search results are skipped if it can't be fetched from the by its integer id. + /// public static IEnumerable ToPublishedSearchResults(this IEnumerable results, IPublishedCache cache) { - var list = new List(); + if (cache == null) throw new ArgumentNullException(nameof(cache)); + + var publishedSearchResults = new List(); foreach (var result in results.OrderByDescending(x => x.Score)) { - if (!int.TryParse(result.Id, out var intId)) continue; //invalid - var content = cache.GetById(intId); - if (content == null) continue; // skip if this doesn't exist in the cache - - list.Add(new PublishedSearchResult(content, result.Score)); - + if (int.TryParse(result.Id, out var contentId) && + cache.GetById(contentId) is IPublishedContent content) + { + publishedSearchResults.Add(new PublishedSearchResult(content, result.Score)); + } } - return list; + return publishedSearchResults; + } + + /// + /// Creates an containing all content, media or members from the . + /// + /// The search results. + /// The snapshot. + /// + /// An containing all content, media or members. + /// + /// snapshot + /// + /// Search results are skipped if it can't be fetched from the respective cache by its integer id. + /// + public static IEnumerable ToPublishedSearchResults(this IEnumerable results, IPublishedSnapshot snapshot) + { + if (snapshot == null) throw new ArgumentNullException(nameof(snapshot)); + + var publishedSearchResults = new List(); + + foreach (var result in results.OrderByDescending(x => x.Score)) + { + if (int.TryParse(result.Id, out var contentId) && + result.Values.TryGetValue(LuceneIndex.CategoryFieldName, out var indexType)) + { + IPublishedContent content; + switch (indexType) + { + case IndexTypes.Content: + content = snapshot.Content.GetById(contentId); + break; + case IndexTypes.Media: + content = snapshot.Media.GetById(contentId); + break; + case IndexTypes.Member: + content = snapshot.Members.GetById(contentId); + break; + default: + continue; + } + + if (content != null) + { + publishedSearchResults.Add(new PublishedSearchResult(content, result.Score)); + } + } + } + + return publishedSearchResults; } } } diff --git a/src/Umbraco.Web/IPublishedContentQuery.cs b/src/Umbraco.Web/IPublishedContentQuery.cs index 351847da73..c4d829968f 100644 --- a/src/Umbraco.Web/IPublishedContentQuery.cs +++ b/src/Umbraco.Web/IPublishedContentQuery.cs @@ -74,7 +74,7 @@ namespace Umbraco.Web IEnumerable Search(string term, int skip, int take, out long totalRecords, string culture = "*", string indexName = null); /// - /// Executes the query and converts the results to using the . + /// Executes the query and converts the results to . /// /// The query. /// @@ -83,7 +83,7 @@ namespace Umbraco.Web IEnumerable Search(IQueryExecutor query); /// - /// Executes the query and converts the results to using the . + /// Executes the query and converts the results to . /// /// The query. /// The amount of results to skip. @@ -92,9 +92,6 @@ namespace Umbraco.Web /// /// The search results. /// - /// - /// Make sure only content is searched (searcher.CreateQuery(IndexTypes.Content)), as this only returns content, but the could otherwise also include media records. - /// IEnumerable Search(IQueryExecutor query, int skip, int take, out long totalRecords); } } diff --git a/src/Umbraco.Web/PublishedContentQuery.cs b/src/Umbraco.Web/PublishedContentQuery.cs index 8f4242c180..833df9971b 100644 --- a/src/Umbraco.Web/PublishedContentQuery.cs +++ b/src/Umbraco.Web/PublishedContentQuery.cs @@ -246,7 +246,7 @@ namespace Umbraco.Web totalRecords = results.TotalItemCount; - return results.Skip(skip).ToPublishedSearchResults(_publishedSnapshot.Content); + return results.Skip(skip).ToPublishedSearchResults(_publishedSnapshot); } /// From ca91bf0f94332555495281d78012d08618e2396c Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 15 Nov 2019 14:14:09 +1100 Subject: [PATCH 074/202] Fixes issue with getting paged entity relations when the relation is between the same entity and itself --- .../Repositories/IEntityRepository.cs | 9 ++-- .../Implement/EntityRepository.cs | 14 +------ .../Implement/RelationRepository.cs | 42 ++++++++++++------- .../Repositories/RelationRepositoryTest.cs | 28 +++++++++++++ 4 files changed, 62 insertions(+), 31 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/IEntityRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IEntityRepository.cs index be626b8d8b..a0ddcac8e6 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IEntityRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IEntityRepository.cs @@ -1,4 +1,5 @@ -using System; +using NPoco; +using System; using System.Collections.Generic; using Umbraco.Core.Models; using Umbraco.Core.Models.Entities; @@ -52,14 +53,16 @@ namespace Umbraco.Core.Persistence.Repositories /// /// /// - /// + /// + /// A callback providing the ability to customize the generated SQL used to retrieve entities + /// /// /// A collection of mixed entity types which would be of type , , , /// /// IEnumerable GetPagedResultsByQuery( IQuery query, Guid[] objectTypes, long pageIndex, int pageSize, out long totalRecords, - IQuery filter, Ordering ordering, IQuery relationQuery = null); + IQuery filter, Ordering ordering, Action> sqlCustomization = null); /// /// Gets paged entities for a query and a specific object type diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs index a0d2baa907..0eafebbfde 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs @@ -44,7 +44,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // get a page of entities public IEnumerable GetPagedResultsByQuery(IQuery query, Guid[] objectTypes, long pageIndex, int pageSize, out long totalRecords, - IQuery filter, Ordering ordering, IQuery relationQuery = null) + IQuery filter, Ordering ordering, Action> sqlCustomization = null) { var isContent = objectTypes.Any(objectType => objectType == Constants.ObjectTypes.Document || objectType == Constants.ObjectTypes.DocumentBlueprint); var isMedia = objectTypes.Any(objectType => objectType == Constants.ObjectTypes.Media); @@ -52,17 +52,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var sql = GetBaseWhere(isContent, isMedia, isMember, false, s => { - if (relationQuery != null) - { - // add left joins for relation tables (this joins on both child or parent, so beware that this will normally return entities for - // both sides of the relation type unless the IUmbracoEntity query passed in filters one side out). - s.LeftJoin().On((left, right) => left.NodeId == right.ChildId || left.NodeId == right.ParentId); - s.LeftJoin().On((left, right) => left.RelationType == right.Id); - - // append the relations wheres - foreach (var relClause in relationQuery.GetWhereClauses()) - s.Where(relClause.Item1, relClause.Item2); - } + sqlCustomization?.Invoke(s); if (filter != null) { diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/RelationRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/RelationRepository.cs index 68575b2bab..b03928996a 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/RelationRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/RelationRepository.cs @@ -160,40 +160,50 @@ namespace Umbraco.Core.Persistence.Repositories.Implement #endregion + /// + /// Used for joining the entity query with relations for the paging methods + /// + /// + private void SqlJoinRelations(Sql sql) + { + // add left joins for relation tables (this joins on both child or parent, so beware that this will normally return entities for + // both sides of the relation type unless the IUmbracoEntity query passed in filters one side out). + sql.LeftJoin().On((left, right) => left.NodeId == right.ChildId || left.NodeId == right.ParentId); + sql.LeftJoin().On((left, right) => left.RelationType == right.Id); + } + public IEnumerable GetPagedParentEntitiesByChildId(int childId, long pageIndex, int pageSize, out long totalRecords) { - // Create a query to match the child id - var relQuery = Query().Where(r => r.ChildId == childId); - - // Because of the way that the entity repository joins relations (on both child or parent) we need to add - // a clause to filter out the child entity from being returned from the results - var entityQuery = Query().Where(e => e.Id != childId); - // var contentObjectTypes = new[] { Constants.ObjectTypes.Document, Constants.ObjectTypes.Media, Constants.ObjectTypes.Member } // we could pass in the contentObjectTypes so that the entity repository sql is configured to do full entity lookups so that we get the full data // required to populate content, media or members, else we get the bare minimum data needed to populate an entity. BUT if we do this it // means that the SQL is less efficient and returns data that is probably not needed for what we need this lookup for. For the time being we // will just return the bare minimum entity data. - return _entityRepository.GetPagedResultsByQuery(entityQuery, Array.Empty(), pageIndex, pageSize, out totalRecords, null, null, relQuery); + return _entityRepository.GetPagedResultsByQuery(Query(), Array.Empty(), pageIndex, pageSize, out totalRecords, null, null, sql => + { + SqlJoinRelations(sql); + + sql.Where(r => r.ChildId == childId); + sql.Where((rel, node) => rel.ParentId == childId || node.NodeId != childId); + }); } public IEnumerable GetPagedChildEntitiesByParentId(int parentId, long pageIndex, int pageSize, out long totalRecords) { - // Create a query to match the parent id - var relQuery = Query().Where(r => r.ParentId == parentId); - - // Because of the way that the entity repository joins relations (on both child or parent) we need to add - // a clause to filter out the child entity from being returned from the results - var entityQuery = Query().Where(e => e.Id != parentId); - // var contentObjectTypes = new[] { Constants.ObjectTypes.Document, Constants.ObjectTypes.Media, Constants.ObjectTypes.Member } // we could pass in the contentObjectTypes so that the entity repository sql is configured to do full entity lookups so that we get the full data // required to populate content, media or members, else we get the bare minimum data needed to populate an entity. BUT if we do this it // means that the SQL is less efficient and returns data that is probably not needed for what we need this lookup for. For the time being we // will just return the bare minimum entity data. - return _entityRepository.GetPagedResultsByQuery(entityQuery, Array.Empty(), pageIndex, pageSize, out totalRecords, null, null, relQuery); + return _entityRepository.GetPagedResultsByQuery(Query(), Array.Empty(), pageIndex, pageSize, out totalRecords, null, null, sql => + { + SqlJoinRelations(sql); + + sql.Where(r => r.ParentId == parentId); + sql.Where((rel, node) => rel.ChildId == parentId || node.NodeId != parentId); + }); } public void Save(IEnumerable relations) diff --git a/src/Umbraco.Tests/Persistence/Repositories/RelationRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/RelationRepositoryTest.cs index 4c8a7c1b16..9622760792 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/RelationRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/RelationRepositoryTest.cs @@ -201,6 +201,34 @@ namespace Umbraco.Tests.Persistence.Repositories } } + [Test] + public void Get_Paged_Parent_Child_Entities_With_Same_Entity_Relation() + { + //Create a media item and create a relationship between itself (parent -> child) + var imageType = MockedContentTypes.CreateImageMediaType("myImage"); + ServiceContext.MediaTypeService.Save(imageType); + var media = MockedMedia.CreateMediaImage(imageType, -1); + ServiceContext.MediaService.Save(media); + var relType = ServiceContext.RelationService.GetRelationTypeByAlias(Constants.Conventions.RelationTypes.RelatedMediaAlias); + ServiceContext.RelationService.Relate(media.Id, media.Id, relType); + + var provider = TestObjects.GetScopeProvider(Logger); + using (var scope = provider.CreateScope()) + { + var repository = CreateRepository(provider, out var relationTypeRepository); + + // Get parent entities for child id + var parents = repository.GetPagedParentEntitiesByChildId(media.Id, 0, 10, out var totalRecords).ToList(); + Assert.AreEqual(1, totalRecords); + Assert.AreEqual(1, parents.Count); + + // Get child entities for parent id + var children = repository.GetPagedChildEntitiesByParentId(media.Id, 0, 10, out totalRecords).ToList(); + Assert.AreEqual(1, totalRecords); + Assert.AreEqual(1, children.Count); + } + } + [Test] public void Get_Paged_Child_Entities_By_Parent_Id() { From 85c22696451ad20655bb76828b05f8f1b80e288d Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 15 Nov 2019 14:49:46 +1100 Subject: [PATCH 075/202] Adds ability to filter out paged relations by entity type --- .../Repositories/IRelationRepository.cs | 7 ++++--- .../Implement/RelationRepository.cs | 12 +++++------ src/Umbraco.Core/Services/IRelationService.cs | 4 ++-- .../Services/Implement/RelationService.cs | 8 ++++---- .../Repositories/RelationRepositoryTest.cs | 20 +++++++++++++++++++ 5 files changed, 36 insertions(+), 15 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/IRelationRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IRelationRepository.cs index 6c69e16edf..fc1be20e6f 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IRelationRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IRelationRepository.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Umbraco.Core.Models; using Umbraco.Core.Models.Entities; using Umbraco.Core.Persistence.Querying; @@ -25,8 +26,8 @@ namespace Umbraco.Core.Persistence.Repositories /// void DeleteByParent(int parentId, params string[] relationTypeAliases); - IEnumerable GetPagedParentEntitiesByChildId(int childId, long pageIndex, int pageSize, out long totalRecords); + IEnumerable GetPagedParentEntitiesByChildId(int childId, long pageIndex, int pageSize, out long totalRecords, params Guid[] entityTypes); - IEnumerable GetPagedChildEntitiesByParentId(int parentId, long pageIndex, int pageSize, out long totalRecords); + IEnumerable GetPagedChildEntitiesByParentId(int parentId, long pageIndex, int pageSize, out long totalRecords, params Guid[] entityTypes); } } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/RelationRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/RelationRepository.cs index b03928996a..cff5e48854 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/RelationRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/RelationRepository.cs @@ -172,7 +172,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement sql.LeftJoin().On((left, right) => left.RelationType == right.Id); } - public IEnumerable GetPagedParentEntitiesByChildId(int childId, long pageIndex, int pageSize, out long totalRecords) + public IEnumerable GetPagedParentEntitiesByChildId(int childId, long pageIndex, int pageSize, out long totalRecords, params Guid[] entityTypes) { // var contentObjectTypes = new[] { Constants.ObjectTypes.Document, Constants.ObjectTypes.Media, Constants.ObjectTypes.Member } // we could pass in the contentObjectTypes so that the entity repository sql is configured to do full entity lookups so that we get the full data @@ -180,16 +180,16 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // means that the SQL is less efficient and returns data that is probably not needed for what we need this lookup for. For the time being we // will just return the bare minimum entity data. - return _entityRepository.GetPagedResultsByQuery(Query(), Array.Empty(), pageIndex, pageSize, out totalRecords, null, null, sql => + return _entityRepository.GetPagedResultsByQuery(Query(), entityTypes, pageIndex, pageSize, out totalRecords, null, null, sql => { SqlJoinRelations(sql); - sql.Where(r => r.ChildId == childId); + sql.Where(rel => rel.ChildId == childId); sql.Where((rel, node) => rel.ParentId == childId || node.NodeId != childId); }); } - public IEnumerable GetPagedChildEntitiesByParentId(int parentId, long pageIndex, int pageSize, out long totalRecords) + public IEnumerable GetPagedChildEntitiesByParentId(int parentId, long pageIndex, int pageSize, out long totalRecords, params Guid[] entityTypes) { // var contentObjectTypes = new[] { Constants.ObjectTypes.Document, Constants.ObjectTypes.Media, Constants.ObjectTypes.Member } // we could pass in the contentObjectTypes so that the entity repository sql is configured to do full entity lookups so that we get the full data @@ -197,11 +197,11 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // means that the SQL is less efficient and returns data that is probably not needed for what we need this lookup for. For the time being we // will just return the bare minimum entity data. - return _entityRepository.GetPagedResultsByQuery(Query(), Array.Empty(), pageIndex, pageSize, out totalRecords, null, null, sql => + return _entityRepository.GetPagedResultsByQuery(Query(), entityTypes, pageIndex, pageSize, out totalRecords, null, null, sql => { SqlJoinRelations(sql); - sql.Where(r => r.ParentId == parentId); + sql.Where(rel => rel.ParentId == parentId); sql.Where((rel, node) => rel.ChildId == parentId || node.NodeId != parentId); }); } diff --git a/src/Umbraco.Core/Services/IRelationService.cs b/src/Umbraco.Core/Services/IRelationService.cs index 4d94ddb48c..bf8bcd5b2a 100644 --- a/src/Umbraco.Core/Services/IRelationService.cs +++ b/src/Umbraco.Core/Services/IRelationService.cs @@ -207,7 +207,7 @@ namespace Umbraco.Core.Services /// /// /// - IEnumerable GetPagedParentEntitiesByChildId(int id, long pageIndex, int pageSize, out long totalChildren); + IEnumerable GetPagedParentEntitiesByChildId(int id, long pageIndex, int pageSize, out long totalChildren, params UmbracoObjectTypes[] entityTypes); /// /// Returns paged child entities for a related parent id @@ -217,7 +217,7 @@ namespace Umbraco.Core.Services /// /// /// - IEnumerable GetPagedChildEntitiesByParentId(int id, long pageIndex, int pageSize, out long totalChildren); + IEnumerable GetPagedChildEntitiesByParentId(int id, long pageIndex, int pageSize, out long totalChildren, params UmbracoObjectTypes[] entityTypes); /// /// Gets the Parent and Child objects from a list of Relations as a list of objects. diff --git a/src/Umbraco.Core/Services/Implement/RelationService.cs b/src/Umbraco.Core/Services/Implement/RelationService.cs index 11572f09ff..4b53709de9 100644 --- a/src/Umbraco.Core/Services/Implement/RelationService.cs +++ b/src/Umbraco.Core/Services/Implement/RelationService.cs @@ -274,20 +274,20 @@ namespace Umbraco.Core.Services.Implement } /// - public IEnumerable GetPagedParentEntitiesByChildId(int id, long pageIndex, int pageSize, out long totalChildren) + public IEnumerable GetPagedParentEntitiesByChildId(int id, long pageIndex, int pageSize, out long totalChildren, params UmbracoObjectTypes[] entityTypes) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { - return _relationRepository.GetPagedParentEntitiesByChildId(id, pageIndex, pageSize, out totalChildren); + return _relationRepository.GetPagedParentEntitiesByChildId(id, pageIndex, pageSize, out totalChildren, entityTypes.Select(x => x.GetGuid()).ToArray()); } } /// - public IEnumerable GetPagedChildEntitiesByParentId(int id, long pageIndex, int pageSize, out long totalChildren) + public IEnumerable GetPagedChildEntitiesByParentId(int id, long pageIndex, int pageSize, out long totalChildren, params UmbracoObjectTypes[] entityTypes) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { - return _relationRepository.GetPagedChildEntitiesByParentId(id, pageIndex, pageSize, out totalChildren); + return _relationRepository.GetPagedChildEntitiesByParentId(id, pageIndex, pageSize, out totalChildren, entityTypes.Select(x => x.GetGuid()).ToArray()); } } diff --git a/src/Umbraco.Tests/Persistence/Repositories/RelationRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/RelationRepositoryTest.cs index 9622760792..364e4e2b3f 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/RelationRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/RelationRepositoryTest.cs @@ -198,6 +198,16 @@ namespace Umbraco.Tests.Persistence.Repositories Assert.AreEqual(10, contentEntities.Count); Assert.AreEqual(0, mediaEntities.Count); Assert.AreEqual(10, memberEntities.Count); + + //only of a certain type + parents.AddRange(repository.GetPagedParentEntitiesByChildId(createdMedia[0].Id, 0, 100, out totalRecords, UmbracoObjectTypes.Document.GetGuid())); + Assert.AreEqual(10, totalRecords); + + parents.AddRange(repository.GetPagedParentEntitiesByChildId(createdMedia[0].Id, 0, 100, out totalRecords, UmbracoObjectTypes.Member.GetGuid())); + Assert.AreEqual(10, totalRecords); + + parents.AddRange(repository.GetPagedParentEntitiesByChildId(createdMedia[0].Id, 0, 100, out totalRecords, UmbracoObjectTypes.Media.GetGuid())); + Assert.AreEqual(0, totalRecords); } } @@ -256,6 +266,16 @@ namespace Umbraco.Tests.Persistence.Repositories Assert.AreEqual(0, contentEntities.Count); Assert.AreEqual(10, mediaEntities.Count); Assert.AreEqual(0, memberEntities.Count); + + //only of a certain type + parents.AddRange(repository.GetPagedChildEntitiesByParentId(createdContent[0].Id, 0, 100, out totalRecords, UmbracoObjectTypes.Media.GetGuid())); + Assert.AreEqual(10, totalRecords); + + parents.AddRange(repository.GetPagedChildEntitiesByParentId(createdMembers[0].Id, 0, 100, out totalRecords, UmbracoObjectTypes.Media.GetGuid())); + Assert.AreEqual(10, totalRecords); + + parents.AddRange(repository.GetPagedChildEntitiesByParentId(createdContent[0].Id, 0, 100, out totalRecords, UmbracoObjectTypes.Member.GetGuid())); + Assert.AreEqual(0, totalRecords); } } From 8773d644aade2a8b488a10eccd081843e6eacaf6 Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Fri, 15 Nov 2019 12:50:44 +0000 Subject: [PATCH 076/202] Implement paging for the three entity types that media can be related to --- .../media/umbmedianodeinfo.directive.js | 72 ++++++++-- .../src/common/resources/media.resource.js | 90 ++++++++++--- .../components/media/umb-media-node-info.html | 44 +++++-- src/Umbraco.Web/Editors/MediaController.cs | 123 +++++++++--------- .../Models/ContentEditing/MediaReferences.cs | 21 +-- 5 files changed, 234 insertions(+), 116 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/media/umbmedianodeinfo.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/media/umbmedianodeinfo.directive.js index 18aa457862..6f1dde18a0 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/media/umbmedianodeinfo.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/media/umbmedianodeinfo.directive.js @@ -1,16 +1,31 @@ (function () { 'use strict'; - function MediaNodeInfoDirective($timeout, $location, eventsService, userService, dateHelper, editorService, mediaHelper, mediaResource, $routeParams) { + function MediaNodeInfoDirective($timeout, $location, eventsService, userService, dateHelper, editorService, mediaHelper, mediaResource, $routeParams, $q) { function link(scope, element, attrs, ctrl) { var evts = []; - var referencesLoaded = false; scope.allowChangeMediaType = false; scope.loading = true; + scope.changeContentPageNumber = changeContentPageNumber; + scope.contentOptions = {}; + scope.contentOptions.pageSize = 1; + scope.hasContentReferences = false; + + scope.changeMediaPageNumber = changeMediaPageNumber; + scope.mediaOptions = {}; + scope.mediaOptions.pageSize = 1; + scope.hasMediaReferences = false; + + scope.changeMemberPageNumber = changeMemberPageNumber; + scope.memberOptions = {}; + scope.memberOptions.pageSize = 1; + scope.hasMemberReferences = false; + + function onInit() { userService.getCurrentUser().then(function(user){ @@ -96,17 +111,43 @@ setMediaExtension(); }); - /** Loads in the media references one time */ - function loadRelations() { - if (!referencesLoaded) { - referencesLoaded = true; - mediaResource.getReferences($routeParams.id) - .then(function (data) { - scope.loading = false; - scope.references = data; - scope.hasReferences = data.content.length > 0 || data.members.length > 0; - }); - } + function changeContentPageNumber(pageNumber) { + scope.contentOptions.pageNumber = pageNumber; + loadContentRelations(); + } + + function changeMediaPageNumber(pageNumber) { + scope.mediaOptions.pageNumber = pageNumber; + loadMediaRelations(); + } + + function changeMemberPageNumber(pageNumber) { + scope.memberOptions.pageNumber = pageNumber; + loadMemberRelations(); + } + + function loadContentRelations() { + return mediaResource.getPagedContentReferences($routeParams.id, scope.contentOptions) + .then(function (data) { + scope.contentReferences = data; + scope.hasContentReferences = data.items.length > 0; + }); + } + + function loadMediaRelations() { + return mediaResource.getPagedMediaReferences($routeParams.id, scope.mediaOptions) + .then(function (data) { + scope.mediaReferences = data; + scope.hasMediaReferences = data.items.length > 0; + }); + } + + function loadMemberRelations() { + return mediaResource.getPagedMemberReferences($routeParams.id, scope.memberOptions) + .then(function (data) { + scope.memberReferences = data; + scope.hasMemberReferences = data.items.length > 0; + }); } //ensure to unregister from all events! @@ -122,7 +163,10 @@ evts.push(eventsService.on("app.tabChange", function (event, args) { $timeout(function () { if (args.alias === "umbInfo") { - loadRelations(); + + $q.all([loadContentRelations(), loadMediaRelations(), loadMemberRelations()]).then(function () { + scope.loading = false; + }); } }); })); diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js index 58c02b55df..cb8c883bb9 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js @@ -554,28 +554,88 @@ function mediaResource($q, $http, umbDataFormatter, umbRequestHelper) { 'Failed to retrieve media items for search: ' + query); }, - /** - * @ngdoc method - * @name umbraco.resources.mediaResource#getReferences - * @methodOf umbraco.resources.mediaResource - * - * @description - * Retrieves references of a given media item. - * - * @param {Int} id id of media node to retrieve references for - * @returns {Promise} resourcePromise object. - * - */ - getReferences: function (id) { + getPagedContentReferences: function (id, options) { + + var defaults = { + pageSize: 25, + pageNumber: 1 + }; + if (options === undefined) { + options = {}; + } + //overwrite the defaults if there are any specified + angular.extend(defaults, options); + //now copy back to the options we will use + options = defaults; return umbRequestHelper.resourcePromise( $http.get( umbRequestHelper.getApiUrl( "mediaApiBaseUrl", - "GetReferences", - { id: id })), + "GetPagedContentReferences", + { + id: id, + pageNumber: options.pageNumber, + pageSize: options.pageSize + } + )), "Failed to retrieve usages for media of id " + id); + }, + getPagedMemberReferences: function (id, options) { + + var defaults = { + pageSize: 25, + pageNumber: 1 + }; + if (options === undefined) { + options = {}; + } + //overwrite the defaults if there are any specified + angular.extend(defaults, options); + //now copy back to the options we will use + options = defaults; + + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "mediaApiBaseUrl", + "GetPagedMemberReferences", + { + id: id, + pageNumber: options.pageNumber, + pageSize: options.pageSize + } + )), + "Failed to retrieve usages for media of id " + id); + }, + + getPagedMediaReferences: function (id, options) { + + var defaults = { + pageSize: 25, + pageNumber: 1 + }; + if (options === undefined) { + options = {}; + } + //overwrite the defaults if there are any specified + angular.extend(defaults, options); + //now copy back to the options we will use + options = defaults; + + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "mediaApiBaseUrl", + "GetPagedMediaReferences", + { + id: id, + pageNumber: options.pageNumber, + pageSize: options.pageSize + } + )), + "Failed to retrieve usages for media of id " + id); } }; } diff --git a/src/Umbraco.Web.UI.Client/src/views/components/media/umb-media-node-info.html b/src/Umbraco.Web.UI.Client/src/views/components/media/umb-media-node-info.html index a82e1897a1..21a28a905f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/media/umb-media-node-info.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/media/umb-media-node-info.html @@ -29,7 +29,7 @@ - + @@ -39,10 +39,9 @@ -
- +
-
+
Used in Documents @@ -58,7 +57,7 @@
-
+
{{::reference.name}}
{{::reference.alias}}
@@ -66,10 +65,19 @@
+ + +
+ + +
-
+
Used in Members @@ -85,7 +93,7 @@
-
+
{{::reference.name}}
{{::reference.alias}}
@@ -93,10 +101,19 @@
+ + +
+ + +
-
+
Used in Media @@ -112,7 +129,7 @@
-
+
{{::reference.name}}
{{::reference.alias}}
@@ -120,6 +137,15 @@
+ + +
+ + +
diff --git a/src/Umbraco.Web/Editors/MediaController.cs b/src/Umbraco.Web/Editors/MediaController.cs index 36750d74ec..22a546ad7d 100644 --- a/src/Umbraco.Web/Editors/MediaController.cs +++ b/src/Umbraco.Web/Editors/MediaController.cs @@ -945,70 +945,73 @@ namespace Umbraco.Web.Editors return hasPathAccess; } - /// - /// Returns the references (usages) for the media item - /// - /// - /// - public MediaReferences GetReferences(int id) + public PagedResult GetPagedContentReferences(int id, int pageNumber = 1, int pageSize = 100) { - var result = new MediaReferences(); - - var relations = Services.RelationService.GetByChildId(id, Constants.Conventions.RelationTypes.RelatedMediaAlias).ToList(); - var relationEntities = Services.RelationService.GetParentEntitiesFromRelations(relations).ToList(); - - var documents = new List(); - var members = new List(); - var media = new List(); - - foreach (var item in relationEntities) + if (pageNumber <= 0 || pageSize <= 0) { - switch (item) - { - case DocumentEntitySlim doc: - documents.Add(new MediaReferences.EntityTypeReferences { - Id = doc.Id, - Key = doc.Key, - Udi = Udi.Create(Constants.UdiEntityType.Document, doc.Key), - Icon = doc.ContentTypeIcon, - Name = doc.Name, - Alias = doc.ContentTypeAlias - }); - break; - - case MemberEntitySlim memb: - members.Add(new MediaReferences.EntityTypeReferences - { - Id = memb.Id, - Key = memb.Key, - Udi = Udi.Create(Constants.UdiEntityType.Member, memb.Key), - Icon = memb.ContentTypeIcon, - Name = memb.Name, - Alias = memb.ContentTypeAlias - }); - break; - - case MediaEntitySlim med: - media.Add(new MediaReferences.EntityTypeReferences - { - Id = med.Id, - Key = med.Key, - Udi = Udi.Create(Constants.UdiEntityType.Media, med.Key), - Icon = med.ContentTypeIcon, - Name = med.Name, - Alias = med.ContentTypeAlias - }); - break; - - default: - break; - } + throw new NotSupportedException("Both pageNumber and pageSize must be greater than zero"); } - result.Content = documents; - result.Members = members; - result.Media = media; - return result; + var relations = Services.RelationService.GetPagedParentEntitiesByChildId(id, pageNumber - 1, pageSize, out long totalRecords, UmbracoObjectTypes.Document); + + return new PagedResult(totalRecords, pageNumber, pageSize) + { + Items = relations.Cast().Select(doc => new EntityTypeReferences + { + Id = doc.Id, + Key = doc.Key, + Udi = Udi.Create(Constants.UdiEntityType.Document, doc.Key), + Icon = doc.ContentTypeIcon, + Name = doc.Name, + Alias = doc.ContentTypeAlias + }) + }; + } + + public PagedResult GetPagedMemberReferences(int id, int pageNumber = 1, int pageSize = 100) + { + if (pageNumber <= 0 || pageSize <= 0) + { + throw new NotSupportedException("Both pageNumber and pageSize must be greater than zero"); + } + + var relations = Services.RelationService.GetPagedParentEntitiesByChildId(id, pageNumber - 1, pageSize, out long totalRecords, UmbracoObjectTypes.Member); + + return new PagedResult(totalRecords, pageNumber, pageSize) + { + Items = relations.Cast().Select(memb => new EntityTypeReferences + { + Id = memb.Id, + Key = memb.Key, + Udi = Udi.Create(Constants.UdiEntityType.Member, memb.Key), + Icon = memb.ContentTypeIcon, + Name = memb.Name, + Alias = memb.ContentTypeAlias + }) + }; + } + + public PagedResult GetPagedMediaReferences(int id, int pageNumber = 1, int pageSize = 100) + { + if (pageNumber <= 0 || pageSize <= 0) + { + throw new NotSupportedException("Both pageNumber and pageSize must be greater than zero"); + } + + var relations = Services.RelationService.GetPagedParentEntitiesByChildId(id, pageNumber - 1, pageSize, out long totalRecords, UmbracoObjectTypes.Media); + + return new PagedResult(totalRecords, pageNumber, pageSize) + { + Items = relations.Cast().Select(med => new EntityTypeReferences + { + Id = med.Id, + Key = med.Key, + Udi = Udi.Create(Constants.UdiEntityType.Media, med.Key), + Icon = med.ContentTypeIcon, + Name = med.Name, + Alias = med.ContentTypeAlias + }) + }; } } } diff --git a/src/Umbraco.Web/Models/ContentEditing/MediaReferences.cs b/src/Umbraco.Web/Models/ContentEditing/MediaReferences.cs index b10022b105..a1fbdfa1e1 100644 --- a/src/Umbraco.Web/Models/ContentEditing/MediaReferences.cs +++ b/src/Umbraco.Web/Models/ContentEditing/MediaReferences.cs @@ -1,24 +1,9 @@ -using System.Collections.Generic; -using System.Linq; -using System.Runtime.Serialization; +using System.Runtime.Serialization; namespace Umbraco.Web.Models.ContentEditing { - [DataContract(Name = "mediaReferences", Namespace = "")] - public class MediaReferences + [DataContract(Name = "entityType", Namespace = "")] + public class EntityTypeReferences : EntityBasic { - [DataMember(Name = "content")] - public IEnumerable Content { get; set; } = Enumerable.Empty(); - - [DataMember(Name = "members")] - public IEnumerable Members { get; set; } = Enumerable.Empty(); - - [DataMember(Name = "media")] - public IEnumerable Media { get; set; } = Enumerable.Empty(); - - [DataContract(Name = "entityType", Namespace = "")] - public class EntityTypeReferences : EntityBasic - { - } } } From 675b6e7b7c8c009d5b75adcabdd19e020d8eccb9 Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Fri, 15 Nov 2019 13:53:56 +0000 Subject: [PATCH 077/202] Remove test page size of 1 to check if each pager was working fine --- .../directives/components/media/umbmedianodeinfo.directive.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/media/umbmedianodeinfo.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/media/umbmedianodeinfo.directive.js index 6f1dde18a0..d21a31e2a8 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/media/umbmedianodeinfo.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/media/umbmedianodeinfo.directive.js @@ -12,17 +12,14 @@ scope.changeContentPageNumber = changeContentPageNumber; scope.contentOptions = {}; - scope.contentOptions.pageSize = 1; scope.hasContentReferences = false; scope.changeMediaPageNumber = changeMediaPageNumber; scope.mediaOptions = {}; - scope.mediaOptions.pageSize = 1; scope.hasMediaReferences = false; scope.changeMemberPageNumber = changeMemberPageNumber; scope.memberOptions = {}; - scope.memberOptions.pageSize = 1; scope.hasMemberReferences = false; From 31b85a2cd6605b4478fe50a93406369b8381f045 Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Mon, 18 Nov 2019 11:43:31 +0000 Subject: [PATCH 078/202] Remove code duplication & do some cleanup - need to work on passing the type variable to Cast --- .../media/umbmedianodeinfo.directive.js | 9 +- .../src/common/resources/media.resource.js | 65 ++------------ src/Umbraco.Web/Editors/MediaController.cs | 88 ++++++++----------- .../Models/ContentEditing/MediaReferences.cs | 9 -- src/Umbraco.Web/Umbraco.Web.csproj | 1 - 5 files changed, 49 insertions(+), 123 deletions(-) delete mode 100644 src/Umbraco.Web/Models/ContentEditing/MediaReferences.cs diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/media/umbmedianodeinfo.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/media/umbmedianodeinfo.directive.js index d21a31e2a8..a4c0e29fe4 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/media/umbmedianodeinfo.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/media/umbmedianodeinfo.directive.js @@ -12,14 +12,17 @@ scope.changeContentPageNumber = changeContentPageNumber; scope.contentOptions = {}; + scope.contentOptions.entityType = "DOCUMENT"; scope.hasContentReferences = false; scope.changeMediaPageNumber = changeMediaPageNumber; scope.mediaOptions = {}; + scope.mediaOptions.entityType = "MEDIA"; scope.hasMediaReferences = false; scope.changeMemberPageNumber = changeMemberPageNumber; scope.memberOptions = {}; + scope.memberOptions.entityType = "MEMBER"; scope.hasMemberReferences = false; @@ -124,7 +127,7 @@ } function loadContentRelations() { - return mediaResource.getPagedContentReferences($routeParams.id, scope.contentOptions) + return mediaResource.getPagedReferences($routeParams.id, scope.contentOptions) .then(function (data) { scope.contentReferences = data; scope.hasContentReferences = data.items.length > 0; @@ -132,7 +135,7 @@ } function loadMediaRelations() { - return mediaResource.getPagedMediaReferences($routeParams.id, scope.mediaOptions) + return mediaResource.getPagedReferences($routeParams.id, scope.mediaOptions) .then(function (data) { scope.mediaReferences = data; scope.hasMediaReferences = data.items.length > 0; @@ -140,7 +143,7 @@ } function loadMemberRelations() { - return mediaResource.getPagedMemberReferences($routeParams.id, scope.memberOptions) + return mediaResource.getPagedReferences($routeParams.id, scope.memberOptions) .then(function (data) { scope.memberReferences = data; scope.hasMemberReferences = data.items.length > 0; diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js index cb8c883bb9..e4e3cc6f3f 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js @@ -554,11 +554,12 @@ function mediaResource($q, $http, umbDataFormatter, umbRequestHelper) { 'Failed to retrieve media items for search: ' + query); }, - getPagedContentReferences: function (id, options) { + getPagedReferences: function (id, options) { var defaults = { pageSize: 25, - pageNumber: 1 + pageNumber: 1, + entityType: "DOCUMENT" }; if (options === undefined) { options = {}; @@ -572,71 +573,17 @@ function mediaResource($q, $http, umbDataFormatter, umbRequestHelper) { $http.get( umbRequestHelper.getApiUrl( "mediaApiBaseUrl", - "GetPagedContentReferences", - { - id: id, - pageNumber: options.pageNumber, - pageSize: options.pageSize - } - )), - "Failed to retrieve usages for media of id " + id); - }, - - getPagedMemberReferences: function (id, options) { - - var defaults = { - pageSize: 25, - pageNumber: 1 - }; - if (options === undefined) { - options = {}; - } - //overwrite the defaults if there are any specified - angular.extend(defaults, options); - //now copy back to the options we will use - options = defaults; - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "mediaApiBaseUrl", - "GetPagedMemberReferences", - { - id: id, - pageNumber: options.pageNumber, - pageSize: options.pageSize - } - )), - "Failed to retrieve usages for media of id " + id); - }, - - getPagedMediaReferences: function (id, options) { - - var defaults = { - pageSize: 25, - pageNumber: 1 - }; - if (options === undefined) { - options = {}; - } - //overwrite the defaults if there are any specified - angular.extend(defaults, options); - //now copy back to the options we will use - options = defaults; - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "mediaApiBaseUrl", - "GetPagedMediaReferences", + "GetPagedReferences", { id: id, + entityType: options.entityType, pageNumber: options.pageNumber, pageSize: options.pageSize } )), "Failed to retrieve usages for media of id " + id); } + }; } diff --git a/src/Umbraco.Web/Editors/MediaController.cs b/src/Umbraco.Web/Editors/MediaController.cs index 22a546ad7d..2d64ae252a 100644 --- a/src/Umbraco.Web/Editors/MediaController.cs +++ b/src/Umbraco.Web/Editors/MediaController.cs @@ -945,71 +945,57 @@ namespace Umbraco.Web.Editors return hasPathAccess; } - public PagedResult GetPagedContentReferences(int id, int pageNumber = 1, int pageSize = 100) + public PagedResult GetPagedReferences(int id, string entityType, int pageNumber = 1, int pageSize = 100) { if (pageNumber <= 0 || pageSize <= 0) { throw new NotSupportedException("Both pageNumber and pageSize must be greater than zero"); } - var relations = Services.RelationService.GetPagedParentEntitiesByChildId(id, pageNumber - 1, pageSize, out long totalRecords, UmbracoObjectTypes.Document); + UmbracoObjectTypes entity; + string udiEntity; + Type castType; - return new PagedResult(totalRecords, pageNumber, pageSize) + switch (entityType.ToUpperInvariant()) { - Items = relations.Cast().Select(doc => new EntityTypeReferences - { - Id = doc.Id, - Key = doc.Key, - Udi = Udi.Create(Constants.UdiEntityType.Document, doc.Key), - Icon = doc.ContentTypeIcon, - Name = doc.Name, - Alias = doc.ContentTypeAlias - }) - }; - } + case "DOCUMENT": + entity = UmbracoObjectTypes.Document; + udiEntity = Constants.UdiEntityType.Document; + castType = typeof(DocumentEntitySlim); + break; - public PagedResult GetPagedMemberReferences(int id, int pageNumber = 1, int pageSize = 100) - { - if (pageNumber <= 0 || pageSize <= 0) - { - throw new NotSupportedException("Both pageNumber and pageSize must be greater than zero"); + case "MEDIA": + entity = UmbracoObjectTypes.Media; + udiEntity = Constants.UdiEntityType.Media; + castType = typeof(MediaEntitySlim); + break; + + case "MEMBER": + entity = UmbracoObjectTypes.Member; + udiEntity = Constants.UdiEntityType.Member; + castType = typeof(MemberEntitySlim); + break; + + default: + entity = UmbracoObjectTypes.Document; + udiEntity = Constants.UdiEntityType.Document; + castType = typeof(DocumentEntitySlim); + break; } - var relations = Services.RelationService.GetPagedParentEntitiesByChildId(id, pageNumber - 1, pageSize, out long totalRecords, UmbracoObjectTypes.Member); + var relations = Services.RelationService.GetPagedParentEntitiesByChildId(id, pageNumber - 1, pageSize, out long totalRecords, entity); - return new PagedResult(totalRecords, pageNumber, pageSize) + + return new PagedResult(totalRecords, pageNumber, pageSize) { - Items = relations.Cast().Select(memb => new EntityTypeReferences + Items = relations.Cast().Select(rel => new EntityBasic { - Id = memb.Id, - Key = memb.Key, - Udi = Udi.Create(Constants.UdiEntityType.Member, memb.Key), - Icon = memb.ContentTypeIcon, - Name = memb.Name, - Alias = memb.ContentTypeAlias - }) - }; - } - - public PagedResult GetPagedMediaReferences(int id, int pageNumber = 1, int pageSize = 100) - { - if (pageNumber <= 0 || pageSize <= 0) - { - throw new NotSupportedException("Both pageNumber and pageSize must be greater than zero"); - } - - var relations = Services.RelationService.GetPagedParentEntitiesByChildId(id, pageNumber - 1, pageSize, out long totalRecords, UmbracoObjectTypes.Media); - - return new PagedResult(totalRecords, pageNumber, pageSize) - { - Items = relations.Cast().Select(med => new EntityTypeReferences - { - Id = med.Id, - Key = med.Key, - Udi = Udi.Create(Constants.UdiEntityType.Media, med.Key), - Icon = med.ContentTypeIcon, - Name = med.Name, - Alias = med.ContentTypeAlias + Id = rel.Id, + Key = rel.Key, + Udi = Udi.Create(udiEntity, rel.Key), + Icon = rel.ContentTypeIcon, + Name = rel.Name, + Alias = rel.ContentTypeAlias }) }; } diff --git a/src/Umbraco.Web/Models/ContentEditing/MediaReferences.cs b/src/Umbraco.Web/Models/ContentEditing/MediaReferences.cs deleted file mode 100644 index a1fbdfa1e1..0000000000 --- a/src/Umbraco.Web/Models/ContentEditing/MediaReferences.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Runtime.Serialization; - -namespace Umbraco.Web.Models.ContentEditing -{ - [DataContract(Name = "entityType", Namespace = "")] - public class EntityTypeReferences : EntityBasic - { - } -} diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 361a548123..5eca5ad7fe 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -222,7 +222,6 @@ - From dc494ff52587d3e29530fde75ac2b01ad58ae3da Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Mon, 18 Nov 2019 12:18:50 +0000 Subject: [PATCH 079/202] MemberEntitySlim has the same properties as ContentEntitySlim so lets inherit it --- src/Umbraco.Core/Models/Entities/MemberEntitySlim.cs | 11 ++--------- src/Umbraco.Web/Editors/MediaController.cs | 9 ++------- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/src/Umbraco.Core/Models/Entities/MemberEntitySlim.cs b/src/Umbraco.Core/Models/Entities/MemberEntitySlim.cs index 335e269467..338f363856 100644 --- a/src/Umbraco.Core/Models/Entities/MemberEntitySlim.cs +++ b/src/Umbraco.Core/Models/Entities/MemberEntitySlim.cs @@ -1,13 +1,6 @@ namespace Umbraco.Core.Models.Entities { - public class MemberEntitySlim : EntitySlim, IMemberEntitySlim + public class MemberEntitySlim : ContentEntitySlim, IMemberEntitySlim { - public string ContentTypeAlias { get; set; } - - /// - public string ContentTypeIcon { get; set; } - - /// - public string ContentTypeThumbnail { get; set; } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Web/Editors/MediaController.cs b/src/Umbraco.Web/Editors/MediaController.cs index 2d64ae252a..751e386edb 100644 --- a/src/Umbraco.Web/Editors/MediaController.cs +++ b/src/Umbraco.Web/Editors/MediaController.cs @@ -954,32 +954,27 @@ namespace Umbraco.Web.Editors UmbracoObjectTypes entity; string udiEntity; - Type castType; switch (entityType.ToUpperInvariant()) { - case "DOCUMENT": + case "DOCUMENT": entity = UmbracoObjectTypes.Document; udiEntity = Constants.UdiEntityType.Document; - castType = typeof(DocumentEntitySlim); break; case "MEDIA": entity = UmbracoObjectTypes.Media; udiEntity = Constants.UdiEntityType.Media; - castType = typeof(MediaEntitySlim); break; case "MEMBER": entity = UmbracoObjectTypes.Member; udiEntity = Constants.UdiEntityType.Member; - castType = typeof(MemberEntitySlim); break; default: entity = UmbracoObjectTypes.Document; udiEntity = Constants.UdiEntityType.Document; - castType = typeof(DocumentEntitySlim); break; } @@ -988,7 +983,7 @@ namespace Umbraco.Web.Editors return new PagedResult(totalRecords, pageNumber, pageSize) { - Items = relations.Cast().Select(rel => new EntityBasic + Items = relations.Cast().Select(rel => new EntityBasic { Id = rel.Id, Key = rel.Key, From 895f68d9e2c6283bce0bd1dd3ef838f577467f2b Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 20 Nov 2019 12:15:27 +1100 Subject: [PATCH 080/202] Fixes bulk insert records, adjusts parsing of object types --- src/Umbraco.Core/Models/ObjectTypes.cs | 2 +- .../NPocoDatabaseExtensions-Bulk.cs | 18 +++++++++-- .../Implement/RelationRepository.cs | 2 ++ .../Persistence/UmbracoDatabase.cs | 4 +++ src/Umbraco.Web/Editors/MediaController.cs | 32 +++---------------- 5 files changed, 27 insertions(+), 31 deletions(-) diff --git a/src/Umbraco.Core/Models/ObjectTypes.cs b/src/Umbraco.Core/Models/ObjectTypes.cs index dd943ee02b..2ddbdca77a 100644 --- a/src/Umbraco.Core/Models/ObjectTypes.cs +++ b/src/Umbraco.Core/Models/ObjectTypes.cs @@ -41,7 +41,7 @@ namespace Umbraco.Core.Models ///
public static UmbracoObjectTypes GetUmbracoObjectType(string name) { - return (UmbracoObjectTypes) Enum.Parse(typeof (UmbracoObjectTypes), name, false); + return (UmbracoObjectTypes) Enum.Parse(typeof (UmbracoObjectTypes), name, true); } #region Guid object type utilities diff --git a/src/Umbraco.Core/Persistence/NPocoDatabaseExtensions-Bulk.cs b/src/Umbraco.Core/Persistence/NPocoDatabaseExtensions-Bulk.cs index 0574e37c4c..10db1ca18e 100644 --- a/src/Umbraco.Core/Persistence/NPocoDatabaseExtensions-Bulk.cs +++ b/src/Umbraco.Core/Persistence/NPocoDatabaseExtensions-Bulk.cs @@ -14,7 +14,21 @@ namespace Umbraco.Core.Persistence ///
public static partial class NPocoDatabaseExtensions { - // TODO: review NPoco native InsertBulk to replace the code below + /// + /// Configures NPoco's SqlBulkCopyHelper to use the correct SqlConnection and SqlTransaction instances from the underlying RetryDbConnection and ProfiledDbTransaction + /// + /// + /// This is required to use NPoco's own method because we use wrapped DbConnection and DbTransaction instances. + /// NPoco's InsertBulk method only caters for efficient bulk inserting records for Sql Server, it does not cater for bulk inserting of records for + /// any other database type and in which case will just insert records one at a time. + /// NPoco's InsertBulk method also deals with updating the passed in entity's PK/ID once it's inserted whereas our own BulkInsertRecords methods + /// do not handle this scenario. + /// + public static void ConfigureNPocoBulkExtensions() + { + SqlBulkCopyHelper.SqlConnectionResolver = dbConn => GetTypedConnection(dbConn); + SqlBulkCopyHelper.SqlTransactionResolver = dbTran => GetTypedTransaction(dbTran); + } /// /// Bulk-inserts records within a transaction. @@ -235,7 +249,7 @@ namespace Umbraco.Core.Persistence //we need to add column mappings here because otherwise columns will be matched by their order and if the order of them are different in the DB compared //to the order in which they are declared in the model then this will not work, so instead we will add column mappings by name so that this explicitly uses //the names instead of their ordering. - foreach(var col in bulkReader.ColumnMappings) + foreach (var col in bulkReader.ColumnMappings) { copy.ColumnMappings.Add(col.DestinationColumn, col.DestinationColumn); } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/RelationRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/RelationRepository.cs index cff5e48854..56a6336f75 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/RelationRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/RelationRepository.cs @@ -236,6 +236,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement }, RelationFactory.BuildDto); // value = DTO + // Use NPoco's own InsertBulk command which will automatically re-populate the new Ids on the entities, our own + // BulkInsertRecords does not cater for this. Database.InsertBulk(entitiesAndDtos.Values); // All dtos now have IDs assigned diff --git a/src/Umbraco.Core/Persistence/UmbracoDatabase.cs b/src/Umbraco.Core/Persistence/UmbracoDatabase.cs index 072813b4e6..a95d95ea08 100644 --- a/src/Umbraco.Core/Persistence/UmbracoDatabase.cs +++ b/src/Umbraco.Core/Persistence/UmbracoDatabase.cs @@ -44,6 +44,8 @@ namespace Umbraco.Core.Persistence _commandRetryPolicy = commandRetryPolicy; EnableSqlTrace = EnableSqlTraceDefault; + + NPocoDatabaseExtensions.ConfigureNPocoBulkExtensions(); } /// @@ -57,6 +59,8 @@ namespace Umbraco.Core.Persistence _logger = logger; EnableSqlTrace = EnableSqlTraceDefault; + + NPocoDatabaseExtensions.ConfigureNPocoBulkExtensions(); } #endregion diff --git a/src/Umbraco.Web/Editors/MediaController.cs b/src/Umbraco.Web/Editors/MediaController.cs index 751e386edb..e9b879c48b 100644 --- a/src/Umbraco.Web/Editors/MediaController.cs +++ b/src/Umbraco.Web/Editors/MediaController.cs @@ -952,34 +952,10 @@ namespace Umbraco.Web.Editors throw new NotSupportedException("Both pageNumber and pageSize must be greater than zero"); } - UmbracoObjectTypes entity; - string udiEntity; - - switch (entityType.ToUpperInvariant()) - { - case "DOCUMENT": - entity = UmbracoObjectTypes.Document; - udiEntity = Constants.UdiEntityType.Document; - break; - - case "MEDIA": - entity = UmbracoObjectTypes.Media; - udiEntity = Constants.UdiEntityType.Media; - break; - - case "MEMBER": - entity = UmbracoObjectTypes.Member; - udiEntity = Constants.UdiEntityType.Member; - break; - - default: - entity = UmbracoObjectTypes.Document; - udiEntity = Constants.UdiEntityType.Document; - break; - } - - var relations = Services.RelationService.GetPagedParentEntitiesByChildId(id, pageNumber - 1, pageSize, out long totalRecords, entity); + var objectType = ObjectTypes.GetUmbracoObjectType(entityType); + var udiType = ObjectTypes.GetUdiType(objectType); + var relations = Services.RelationService.GetPagedParentEntitiesByChildId(id, pageNumber - 1, pageSize, out var totalRecords, objectType); return new PagedResult(totalRecords, pageNumber, pageSize) { @@ -987,7 +963,7 @@ namespace Umbraco.Web.Editors { Id = rel.Id, Key = rel.Key, - Udi = Udi.Create(udiEntity, rel.Key), + Udi = Udi.Create(udiType, rel.Key), Icon = rel.ContentTypeIcon, Name = rel.Name, Alias = rel.ContentTypeAlias From beebdce0a2f1b1eceade48d283369ed30def73c9 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 20 Nov 2019 12:18:14 +1100 Subject: [PATCH 081/202] Fixes $routeParams issue --- .../components/media/umbmedianodeinfo.directive.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/media/umbmedianodeinfo.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/media/umbmedianodeinfo.directive.js index a4c0e29fe4..dfa1afc247 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/media/umbmedianodeinfo.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/media/umbmedianodeinfo.directive.js @@ -1,7 +1,7 @@ (function () { 'use strict'; - function MediaNodeInfoDirective($timeout, $location, eventsService, userService, dateHelper, editorService, mediaHelper, mediaResource, $routeParams, $q) { + function MediaNodeInfoDirective($timeout, $location, eventsService, userService, dateHelper, editorService, mediaHelper, mediaResource, $q) { function link(scope, element, attrs, ctrl) { @@ -127,7 +127,7 @@ } function loadContentRelations() { - return mediaResource.getPagedReferences($routeParams.id, scope.contentOptions) + return mediaResource.getPagedReferences(scope.node.id, scope.contentOptions) .then(function (data) { scope.contentReferences = data; scope.hasContentReferences = data.items.length > 0; @@ -135,7 +135,7 @@ } function loadMediaRelations() { - return mediaResource.getPagedReferences($routeParams.id, scope.mediaOptions) + return mediaResource.getPagedReferences(scope.node.id, scope.mediaOptions) .then(function (data) { scope.mediaReferences = data; scope.hasMediaReferences = data.items.length > 0; @@ -143,7 +143,7 @@ } function loadMemberRelations() { - return mediaResource.getPagedReferences($routeParams.id, scope.memberOptions) + return mediaResource.getPagedReferences(scope.node.id, scope.memberOptions) .then(function (data) { scope.memberReferences = data; scope.hasMemberReferences = data.items.length > 0; From b13120f0a11c25968d554f3064e485f8d651070e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Fri, 22 Nov 2019 11:32:11 +0100 Subject: [PATCH 082/202] minor style adjustment for media references --- .../src/views/components/media/umb-media-node-info.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/components/media/umb-media-node-info.html b/src/Umbraco.Web.UI.Client/src/views/components/media/umb-media-node-info.html index 21a28a905f..a606aa5588 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/media/umb-media-node-info.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/media/umb-media-node-info.html @@ -6,7 +6,7 @@
- + @@ -43,7 +43,7 @@
-
+
Used in Documents
@@ -79,7 +79,7 @@
-
+
Used in Members
@@ -115,7 +115,7 @@
-
+
Used in Media
From 2420b9c25362dc1bf55314edbe6a864c34f1bb64 Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Tue, 26 Nov 2019 10:28:56 +0000 Subject: [PATCH 083/202] Update CoreComposer to use the easier to read extension method on composition as opposed to declaring WithCollectionBuilder --- src/Umbraco.Core/Runtime/CoreInitialComposer.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Core/Runtime/CoreInitialComposer.cs b/src/Umbraco.Core/Runtime/CoreInitialComposer.cs index 1f004846d0..86e61aeb90 100644 --- a/src/Umbraco.Core/Runtime/CoreInitialComposer.cs +++ b/src/Umbraco.Core/Runtime/CoreInitialComposer.cs @@ -43,7 +43,7 @@ namespace Umbraco.Core.Runtime // register persistence mappers - required by database factory so needs to be done here // means the only place the collection can be modified is in a runtime - afterwards it // has been frozen and it is too late - composition.WithCollectionBuilder().AddCoreMappers(); + composition.Mappers().AddCoreMappers(); // register the scope provider composition.RegisterUnique(); // implements both IScopeProvider and IScopeAccessor @@ -70,7 +70,7 @@ namespace Umbraco.Core.Runtime composition.ManifestFilters(); // properties and parameters derive from data editors - composition.WithCollectionBuilder() + composition.DataEditors() .Add(() => composition.TypeLoader.GetDataEditors()); composition.RegisterUnique(); composition.RegisterUnique(); @@ -101,13 +101,13 @@ namespace Umbraco.Core.Runtime factory.GetInstance(), true, new DatabaseServerMessengerOptions())); - composition.WithCollectionBuilder() + composition.CacheRefreshers() .Add(() => composition.TypeLoader.GetCacheRefreshers()); - composition.WithCollectionBuilder() + composition.PackageActions() .Add(() => composition.TypeLoader.GetPackageActions()); - composition.WithCollectionBuilder() + composition.PropertyValueConverters() .Append(composition.TypeLoader.GetTypes()); composition.RegisterUnique(); @@ -115,7 +115,7 @@ namespace Umbraco.Core.Runtime composition.RegisterUnique(factory => new DefaultShortStringHelper(new DefaultShortStringHelperConfig().WithDefault(factory.GetInstance()))); - composition.WithCollectionBuilder() + composition.UrlSegmentProviders() .Append(); composition.RegisterUnique(factory => new MigrationBuilder(factory)); From 3ead51ff64c13e8ff066181fcd0d830e8bde1048 Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Tue, 26 Nov 2019 11:05:30 +0000 Subject: [PATCH 084/202] Update WebInitComposer to use the easier to read extension method on composition as opposed to declaring WithCollectionBuilder --- src/Umbraco.Web/Runtime/WebInitialComposer.cs | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/Umbraco.Web/Runtime/WebInitialComposer.cs b/src/Umbraco.Web/Runtime/WebInitialComposer.cs index 4de5e8627a..27d0b5a1ce 100644 --- a/src/Umbraco.Web/Runtime/WebInitialComposer.cs +++ b/src/Umbraco.Web/Runtime/WebInitialComposer.cs @@ -150,19 +150,19 @@ namespace Umbraco.Web.Runtime .ComposeUmbracoControllers(GetType().Assembly) .SetDefaultRenderMvcController(); // default controller for template views - composition.WithCollectionBuilder() + composition.SearchableTrees() .Add(() => composition.TypeLoader.GetTypes()); composition.Register(Lifetime.Request); - composition.WithCollectionBuilder() + composition.EditorValidators() .Add(() => composition.TypeLoader.GetTypes()); - composition.WithCollectionBuilder(); + composition.TourFilters(); composition.RegisterUnique(); - composition.WithCollectionBuilder() + composition.Actions() .Add(() => composition.TypeLoader.GetTypes()); //we need to eagerly scan controller types since they will need to be routed @@ -177,26 +177,26 @@ namespace Umbraco.Web.Runtime // here because there cannot be two converters for one property editor - and we want the full // RteMacroRenderingValueConverter that converts macros, etc. So remove TinyMceValueConverter. // (the limited one, defined in Core, is there for tests) - same for others - composition.WithCollectionBuilder() + composition.PropertyValueConverters() .Remove() .Remove() .Remove(); // add all known factories, devs can then modify this list on application // startup either by binding to events or in their own global.asax - composition.WithCollectionBuilder() + composition.FilteredControllerFactory() .Append(); - composition.WithCollectionBuilder() + composition.UrlProviders() .Append() .Append(); - composition.WithCollectionBuilder() + composition.MediaUrlProviders() .Append(); composition.RegisterUnique(); - composition.WithCollectionBuilder() + composition.ContentFinders() // all built-in finders in the correct order, // devs can then modify this list on application startup .Append() @@ -211,7 +211,7 @@ namespace Umbraco.Web.Runtime composition.RegisterUnique(); // register *all* checks, except those marked [HideFromTypeFinder] of course - composition.WithCollectionBuilder() + composition.HealthChecks() .Add(() => composition.TypeLoader.GetTypes()); composition.WithCollectionBuilder() @@ -231,13 +231,13 @@ namespace Umbraco.Web.Runtime composition.RegisterUnique(); // register known content apps - composition.WithCollectionBuilder() + composition.ContentApps() .Append() .Append() .Append(); // register back office sections in the order we want them rendered - composition.WithCollectionBuilder() + composition.Sections() .Append() .Append() .Append() @@ -248,18 +248,18 @@ namespace Umbraco.Web.Runtime .Append(); // register core CMS dashboards and 3rd party types - will be ordered by weight attribute & merged with package.manifest dashboards - composition.WithCollectionBuilder() + composition.Dashboards() .Add(composition.TypeLoader.GetTypes()); // register back office trees // the collection builder only accepts types inheriting from TreeControllerBase // and will filter out those that are not attributed with TreeAttribute - composition.WithCollectionBuilder() + composition.Trees() .AddTreeControllers(umbracoApiControllerTypes.Where(x => typeof(TreeControllerBase).IsAssignableFrom(x))); // register OEmbed providers - no type scanning - all explicit opt-in of adding types // note: IEmbedProvider is not IDiscoverable - think about it if going for type scanning - composition.WithCollectionBuilder() + composition.OEmbedProviders() .Append() .Append() .Append() From 7573d528074871e980341b171fcb61b42b02c54a Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Tue, 26 Nov 2019 11:09:33 +0000 Subject: [PATCH 085/202] Initial plumbing, extension methods etc for collection to store IDataValueReferences --- src/Umbraco.Core/Composing/Current.cs | 3 +++ src/Umbraco.Core/CompositionExtensions.cs | 7 +++++++ .../PropertyEditors/DataValueReferenceCollection.cs | 12 ++++++++++++ .../DataValueReferenceCollectionBuilder.cs | 9 +++++++++ src/Umbraco.Core/Runtime/CoreInitialComposer.cs | 4 ++++ src/Umbraco.Core/Umbraco.Core.csproj | 2 ++ src/Umbraco.Web/Composing/Current.cs | 2 ++ 7 files changed, 39 insertions(+) create mode 100644 src/Umbraco.Core/PropertyEditors/DataValueReferenceCollection.cs create mode 100644 src/Umbraco.Core/PropertyEditors/DataValueReferenceCollectionBuilder.cs diff --git a/src/Umbraco.Core/Composing/Current.cs b/src/Umbraco.Core/Composing/Current.cs index f12bf0dd63..1bd56ed727 100644 --- a/src/Umbraco.Core/Composing/Current.cs +++ b/src/Umbraco.Core/Composing/Current.cs @@ -154,6 +154,9 @@ namespace Umbraco.Core.Composing public static DataEditorCollection DataEditors => Factory.GetInstance(); + public static DataValueReferenceCollection DataValueReferences + => Factory.GetInstance(); + public static PropertyEditorCollection PropertyEditors => Factory.GetInstance(); diff --git a/src/Umbraco.Core/CompositionExtensions.cs b/src/Umbraco.Core/CompositionExtensions.cs index 5dd33c2a60..d29f251edc 100644 --- a/src/Umbraco.Core/CompositionExtensions.cs +++ b/src/Umbraco.Core/CompositionExtensions.cs @@ -49,6 +49,13 @@ namespace Umbraco.Core public static DataEditorCollectionBuilder DataEditors(this Composition composition) => composition.WithCollectionBuilder(); + /// + /// Gets the data value reference collection builder. + /// + /// The composition. + public static DataValueReferenceCollectionBuilder DataValueReferences(this Composition composition) + => composition.WithCollectionBuilder(); + /// /// Gets the property value converters collection builder. /// diff --git a/src/Umbraco.Core/PropertyEditors/DataValueReferenceCollection.cs b/src/Umbraco.Core/PropertyEditors/DataValueReferenceCollection.cs new file mode 100644 index 0000000000..6d0e3e62df --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/DataValueReferenceCollection.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using Umbraco.Core.Composing; + +namespace Umbraco.Core.PropertyEditors +{ + public class DataValueReferenceCollection : BuilderCollectionBase + { + public DataValueReferenceCollection(IEnumerable items) + : base(items) + { } + } +} diff --git a/src/Umbraco.Core/PropertyEditors/DataValueReferenceCollectionBuilder.cs b/src/Umbraco.Core/PropertyEditors/DataValueReferenceCollectionBuilder.cs new file mode 100644 index 0000000000..0f8ee1bd8e --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/DataValueReferenceCollectionBuilder.cs @@ -0,0 +1,9 @@ +using Umbraco.Core.Composing; + +namespace Umbraco.Core.PropertyEditors +{ + public class DataValueReferenceCollectionBuilder : LazyCollectionBuilderBase + { + protected override DataValueReferenceCollectionBuilder This => this; + } +} diff --git a/src/Umbraco.Core/Runtime/CoreInitialComposer.cs b/src/Umbraco.Core/Runtime/CoreInitialComposer.cs index 86e61aeb90..db308d720c 100644 --- a/src/Umbraco.Core/Runtime/CoreInitialComposer.cs +++ b/src/Umbraco.Core/Runtime/CoreInitialComposer.cs @@ -75,6 +75,10 @@ namespace Umbraco.Core.Runtime composition.RegisterUnique(); composition.RegisterUnique(); + // TODO: WB Add our collection + // Manually register stuff in this collection + composition.DataValueReferences(); + // register a server registrar, by default it's the db registrar composition.RegisterUnique(f => { diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 225317943b..c6545a4f8b 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -281,6 +281,8 @@ + + diff --git a/src/Umbraco.Web/Composing/Current.cs b/src/Umbraco.Web/Composing/Current.cs index 5be5e45ecd..1363d60b8a 100644 --- a/src/Umbraco.Web/Composing/Current.cs +++ b/src/Umbraco.Web/Composing/Current.cs @@ -182,6 +182,8 @@ namespace Umbraco.Web.Composing public static DataEditorCollection DataEditors => CoreCurrent.DataEditors; + public static DataValueReferenceCollection DataValueReferences => CoreCurrent.DataValueReferences; + public static PropertyEditorCollection PropertyEditors => CoreCurrent.PropertyEditors; public static ParameterEditorCollection ParameterEditors => CoreCurrent.ParameterEditors; From bd2c3bdcb3aedd474c2232f8509f9515158c3852 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 27 Nov 2019 14:28:53 +1100 Subject: [PATCH 086/202] Creates test to assert the problem --- .../PublishedContent/NuCacheChildrenTests.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs b/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs index 5d32606ee7..cc9ffdba3c 100644 --- a/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs +++ b/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs @@ -918,6 +918,14 @@ namespace Umbraco.Tests.PublishedContent var snapshot = _snapshotService.CreatePublishedSnapshot(previewToken: null); _snapshotAccessor.PublishedSnapshot = snapshot; + var snapshotService = (PublishedSnapshotService)_snapshotService; + var contentStore = snapshotService.GetContentStore(); + + var parentNodes = contentStore.Test.GetValues(1); + var parentNode = parentNodes[0]; + AssertLinkedNode(parentNode.contentNode, -1, -1, 2, 4, 6); + Assert.AreEqual(1, parentNode.gen); + var documents = snapshot.Content.GetAtRoot().ToArray(); AssertDocuments(documents, "N1", "N2", "N3"); @@ -934,6 +942,15 @@ namespace Umbraco.Tests.PublishedContent new ContentCacheRefresher.JsonPayload(2, Guid.Empty, TreeChangeTypes.RefreshNode), }, out _, out _); + parentNodes = contentStore.Test.GetValues(1); + Assert.AreEqual(2, parentNodes.Length); + parentNode = parentNodes[1]; // get the first gen + AssertLinkedNode(parentNode.contentNode, -1, -1, 2, 4, 6); // the structure should have remained the same + Assert.AreEqual(1, parentNode.gen); + parentNode = parentNodes[0]; // get the latest gen + AssertLinkedNode(parentNode.contentNode, -1, -1, 2, 4, 6); // the structure should have remained the same + Assert.AreEqual(2, parentNode.gen); + documents = snapshot.Content.GetAtRoot().ToArray(); AssertDocuments(documents, "N1", "N2", "N3"); @@ -942,6 +959,8 @@ namespace Umbraco.Tests.PublishedContent documents = snapshot.Content.GetById(2).Children().ToArray(); AssertDocuments(documents, "N9", "N8", "N7"); + + } [Test] From 645a30a8aa0027fd49b85698bd26f4e1ee47a5e0 Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Wed, 27 Nov 2019 20:24:16 +0000 Subject: [PATCH 087/202] Adds new ValueReference classes and update to Interface for IsForEditor method to check if we should get references for a specific dataeditor based on its alias --- .../Implement/ContentRepositoryBase.cs | 18 +++++-- .../Implement/DocumentBlueprintRepository.cs | 4 +- .../Implement/DocumentRepository.cs | 4 +- .../Repositories/Implement/MediaRepository.cs | 4 +- .../Implement/MemberRepository.cs | 4 +- .../DataValueReferenceCollectionBuilder.cs | 4 +- .../PropertyEditors/IDataValueReference.cs | 7 +++ .../Runtime/CoreInitialComposer.cs | 3 -- .../ContentPickerPropertyEditor.cs | 12 +---- .../PropertyEditors/GridPropertyEditor.cs | 26 +--------- .../MediaPickerPropertyEditor.cs | 12 +---- .../MultiNodeTreePickerPropertyEditor.cs | 15 +----- .../MultiUrlPickerValueEditor.cs | 5 +- .../NestedContentPropertyEditor.cs | 28 +---------- .../PropertyEditors/RichTextPropertyEditor.cs | 20 +------- .../ContentPickerPropertyValueReferences.cs | 22 +++++++++ .../GridPropertyValueReferences.cs | 37 ++++++++++++++ .../MediaPickerPropertyValueReferences.cs | 22 +++++++++ ...tiNodeTreePickerPropertyValueReferences.cs | 23 +++++++++ .../MultiUrlPickerValueReferences.cs | 26 ++++++++++ .../NestedContentPropertyValueReferences.cs | 49 +++++++++++++++++++ .../RichTextPropertyValueReferences.cs | 40 +++++++++++++++ src/Umbraco.Web/Runtime/WebInitialComposer.cs | 19 ++++++- src/Umbraco.Web/Umbraco.Web.csproj | 7 +++ 24 files changed, 281 insertions(+), 130 deletions(-) create mode 100644 src/Umbraco.Web/PropertyEditors/ValueReferences/ContentPickerPropertyValueReferences.cs create mode 100644 src/Umbraco.Web/PropertyEditors/ValueReferences/GridPropertyValueReferences.cs create mode 100644 src/Umbraco.Web/PropertyEditors/ValueReferences/MediaPickerPropertyValueReferences.cs create mode 100644 src/Umbraco.Web/PropertyEditors/ValueReferences/MultiNodeTreePickerPropertyValueReferences.cs create mode 100644 src/Umbraco.Web/PropertyEditors/ValueReferences/MultiUrlPickerValueReferences.cs create mode 100644 src/Umbraco.Web/PropertyEditors/ValueReferences/NestedContentPropertyValueReferences.cs create mode 100644 src/Umbraco.Web/PropertyEditors/ValueReferences/RichTextPropertyValueReferences.cs diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs index 65f6dc0472..c91a7e20e1 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs @@ -36,6 +36,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement where TRepository : class, IRepository { private readonly Lazy _propertyEditors; + private readonly DataValueReferenceCollection _dataValueReferences; /// /// @@ -49,13 +50,14 @@ namespace Umbraco.Core.Persistence.Repositories.Implement /// protected ContentRepositoryBase(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, ILanguageRepository languageRepository, IRelationRepository relationRepository, IRelationTypeRepository relationTypeRepository, - Lazy propertyEditors) + Lazy propertyEditors, DataValueReferenceCollection dataValueReferences) : base(scopeAccessor, cache, logger) { LanguageRepository = languageRepository; RelationRepository = relationRepository; RelationTypeRepository = relationTypeRepository; _propertyEditors = propertyEditors; + _dataValueReferences = dataValueReferences; } protected abstract TRepository This { get; } @@ -826,15 +828,21 @@ namespace Umbraco.Core.Persistence.Repositories.Implement foreach (var p in entity.Properties) { if (!PropertyEditors.TryGet(p.PropertyType.PropertyEditorAlias, out var editor)) continue; - var valueEditor = editor.GetValueEditor(); - if (!(valueEditor is IDataValueReference reference)) continue; //TODO: Support variants/segments! This is not required for this initial prototype which is why there is a check here if (!p.PropertyType.VariesByNothing()) continue; var val = p.GetValue(); // get the invariant value - var refs = reference.GetReferences(val); - trackedRelations.AddRange(refs); + + // WB: Loop over our collection of references registered and add references + foreach (var item in _dataValueReferences) + { + // Check if this value reference is for this datatype/editor + // Then call it's GetReferences method - to see if the value stored + // in the dataeditor/property has referecnes to media items + if (item.IsForEditor(editor)) + trackedRelations.AddRange(item.GetReferences(val)); + } } //First delete all auto-relations for this entity diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentBlueprintRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentBlueprintRepository.cs index 60d4026ad5..5c2e73de9d 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentBlueprintRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentBlueprintRepository.cs @@ -20,8 +20,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement { public DocumentBlueprintRepository(IScopeAccessor scopeAccessor, AppCaches appCaches, ILogger logger, IContentTypeRepository contentTypeRepository, ITemplateRepository templateRepository, ITagRepository tagRepository, ILanguageRepository languageRepository, IRelationRepository relationRepository, IRelationTypeRepository relationTypeRepository, - Lazy propertyEditorCollection) - : base(scopeAccessor, appCaches, logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditorCollection) + Lazy propertyEditorCollection, DataValueReferenceCollection dataValueReferences) + : base(scopeAccessor, appCaches, logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditorCollection, dataValueReferences) { } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs index 5e3e7f05b9..e1ea955972 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs @@ -46,8 +46,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement /// public DocumentRepository(IScopeAccessor scopeAccessor, AppCaches appCaches, ILogger logger, IContentTypeRepository contentTypeRepository, ITemplateRepository templateRepository, ITagRepository tagRepository, ILanguageRepository languageRepository, IRelationRepository relationRepository, IRelationTypeRepository relationTypeRepository, - Lazy propertyEditors) - : base(scopeAccessor, appCaches, logger, languageRepository, relationRepository, relationTypeRepository, propertyEditors) + Lazy propertyEditors, DataValueReferenceCollection dataValueReferences) + : base(scopeAccessor, appCaches, logger, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferences) { _contentTypeRepository = contentTypeRepository ?? throw new ArgumentNullException(nameof(contentTypeRepository)); _templateRepository = templateRepository ?? throw new ArgumentNullException(nameof(templateRepository)); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs index a3f9e45485..2297bbed2c 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs @@ -29,8 +29,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement private readonly MediaByGuidReadRepository _mediaByGuidReadRepository; public MediaRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IMediaTypeRepository mediaTypeRepository, ITagRepository tagRepository, ILanguageRepository languageRepository, IRelationRepository relationRepository, IRelationTypeRepository relationTypeRepository, - Lazy propertyEditorCollection) - : base(scopeAccessor, cache, logger, languageRepository, relationRepository, relationTypeRepository, propertyEditorCollection) + Lazy propertyEditorCollection, DataValueReferenceCollection dataValueReferences) + : base(scopeAccessor, cache, logger, languageRepository, relationRepository, relationTypeRepository, propertyEditorCollection, dataValueReferences) { _mediaTypeRepository = mediaTypeRepository ?? throw new ArgumentNullException(nameof(mediaTypeRepository)); _tagRepository = tagRepository ?? throw new ArgumentNullException(nameof(tagRepository)); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs index 2871bf1dd3..c20e990a2b 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs @@ -28,8 +28,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement public MemberRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IMemberTypeRepository memberTypeRepository, IMemberGroupRepository memberGroupRepository, ITagRepository tagRepository, ILanguageRepository languageRepository, IRelationRepository relationRepository, IRelationTypeRepository relationTypeRepository, - Lazy propertyEditors) - : base(scopeAccessor, cache, logger, languageRepository, relationRepository, relationTypeRepository, propertyEditors) + Lazy propertyEditors, DataValueReferenceCollection dataValueReferences) + : base(scopeAccessor, cache, logger, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferences) { _memberTypeRepository = memberTypeRepository ?? throw new ArgumentNullException(nameof(memberTypeRepository)); _tagRepository = tagRepository ?? throw new ArgumentNullException(nameof(tagRepository)); diff --git a/src/Umbraco.Core/PropertyEditors/DataValueReferenceCollectionBuilder.cs b/src/Umbraco.Core/PropertyEditors/DataValueReferenceCollectionBuilder.cs index 0f8ee1bd8e..c6442275b0 100644 --- a/src/Umbraco.Core/PropertyEditors/DataValueReferenceCollectionBuilder.cs +++ b/src/Umbraco.Core/PropertyEditors/DataValueReferenceCollectionBuilder.cs @@ -2,8 +2,8 @@ namespace Umbraco.Core.PropertyEditors { - public class DataValueReferenceCollectionBuilder : LazyCollectionBuilderBase + public class DataValueReferenceCollectionBuilder : OrderedCollectionBuilderBase { - protected override DataValueReferenceCollectionBuilder This => this; + protected override DataValueReferenceCollectionBuilder This => this; } } diff --git a/src/Umbraco.Core/PropertyEditors/IDataValueReference.cs b/src/Umbraco.Core/PropertyEditors/IDataValueReference.cs index 8c0806a4a4..7d46704938 100644 --- a/src/Umbraco.Core/PropertyEditors/IDataValueReference.cs +++ b/src/Umbraco.Core/PropertyEditors/IDataValueReference.cs @@ -14,5 +14,12 @@ namespace Umbraco.Core.PropertyEditors /// /// IEnumerable GetReferences(object value); + + /// + /// Gets a value indicating whether the DataValueReference lookup supports a datatype (data editor). + /// + /// The datatype. + /// A value indicating whether the converter supports a datatype. + bool IsForEditor(IDataEditor dataEditor); } } diff --git a/src/Umbraco.Core/Runtime/CoreInitialComposer.cs b/src/Umbraco.Core/Runtime/CoreInitialComposer.cs index db308d720c..94d2b68121 100644 --- a/src/Umbraco.Core/Runtime/CoreInitialComposer.cs +++ b/src/Umbraco.Core/Runtime/CoreInitialComposer.cs @@ -75,9 +75,6 @@ namespace Umbraco.Core.Runtime composition.RegisterUnique(); composition.RegisterUnique(); - // TODO: WB Add our collection - // Manually register stuff in this collection - composition.DataValueReferences(); // register a server registrar, by default it's the db registrar composition.RegisterUnique(f => diff --git a/src/Umbraco.Web/PropertyEditors/ContentPickerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/ContentPickerPropertyEditor.cs index 683f1a05c3..2f45d98fa1 100644 --- a/src/Umbraco.Web/PropertyEditors/ContentPickerPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/ContentPickerPropertyEditor.cs @@ -29,21 +29,11 @@ namespace Umbraco.Web.PropertyEditors protected override IDataValueEditor CreateValueEditor() => new ContentPickerPropertyValueEditor(Attribute); - internal class ContentPickerPropertyValueEditor : DataValueEditor, IDataValueReference + internal class ContentPickerPropertyValueEditor : DataValueEditor { public ContentPickerPropertyValueEditor(DataEditorAttribute attribute) : base(attribute) { } - - public IEnumerable GetReferences(object value) - { - var asString = value is string str ? str : value?.ToString(); - - if (string.IsNullOrEmpty(asString)) yield break; - - if (Udi.TryParse(asString, out var udi)) - yield return new UmbracoEntityReference(udi); - } } } } diff --git a/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs index 228e058ee7..16dffb3b10 100644 --- a/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs @@ -54,7 +54,7 @@ namespace Umbraco.Web.PropertyEditors protected override IConfigurationEditor CreateConfigurationEditor() => new GridConfigurationEditor(); - internal class GridPropertyValueEditor : DataValueEditor, IDataValueReference + internal class GridPropertyValueEditor : DataValueEditor { private readonly IUmbracoContextAccessor _umbracoContextAccessor; private readonly HtmlImageSourceParser _imageSourceParser; @@ -156,30 +156,6 @@ namespace Umbraco.Web.PropertyEditors return grid; } - - /// - /// Resolve references from values - /// - /// - /// - public IEnumerable GetReferences(object value) - { - var rawJson = value == null ? string.Empty : value is string str ? str : value.ToString(); - - DeserializeGridValue(rawJson, out var richTextEditorValues, out var mediaValues); - - foreach (var umbracoEntityReference in richTextEditorValues.SelectMany(x => - _richTextPropertyValueEditor.GetReferences(x.Value))) - { - yield return umbracoEntityReference; - } - - foreach (var umbracoEntityReference in mediaValues.SelectMany(x => - _mediaPickerPropertyValueEditor.GetReferences(x.Value["udi"]))) - { - yield return umbracoEntityReference; - } - } } } } diff --git a/src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs index ece210b9d1..6416fa3342 100644 --- a/src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs @@ -33,21 +33,11 @@ namespace Umbraco.Web.PropertyEditors protected override IDataValueEditor CreateValueEditor() => new MediaPickerPropertyValueEditor(Attribute); - internal class MediaPickerPropertyValueEditor : DataValueEditor, IDataValueReference + internal class MediaPickerPropertyValueEditor : DataValueEditor { public MediaPickerPropertyValueEditor(DataEditorAttribute attribute) : base(attribute) { } - - public IEnumerable GetReferences(object value) - { - var asString = value is string str ? str : value?.ToString(); - - if (string.IsNullOrEmpty(asString)) yield break; - - if (Udi.TryParse(asString, out var udi)) - yield return new UmbracoEntityReference(udi); - } } } diff --git a/src/Umbraco.Web/PropertyEditors/MultiNodeTreePickerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MultiNodeTreePickerPropertyEditor.cs index 1da665a622..d7a7a2ed59 100644 --- a/src/Umbraco.Web/PropertyEditors/MultiNodeTreePickerPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MultiNodeTreePickerPropertyEditor.cs @@ -23,25 +23,12 @@ namespace Umbraco.Web.PropertyEditors protected override IDataValueEditor CreateValueEditor() => new MultiNodeTreePickerPropertyValueEditor(Attribute); - public class MultiNodeTreePickerPropertyValueEditor : DataValueEditor, IDataValueReference + public class MultiNodeTreePickerPropertyValueEditor : DataValueEditor { public MultiNodeTreePickerPropertyValueEditor(DataEditorAttribute attribute): base(attribute) { } - - public IEnumerable GetReferences(object value) - { - var asString = value == null ? string.Empty : value is string str ? str : value.ToString(); - - var udiPaths = asString.Split(','); - foreach (var udiPath in udiPaths) - { - if (Udi.TryParse(udiPath, out var udi)) - yield return new UmbracoEntityReference(udi); - } - - } } } diff --git a/src/Umbraco.Web/PropertyEditors/MultiUrlPickerValueEditor.cs b/src/Umbraco.Web/PropertyEditors/MultiUrlPickerValueEditor.cs index 9c42fe6cbe..de641f69a3 100644 --- a/src/Umbraco.Web/PropertyEditors/MultiUrlPickerValueEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MultiUrlPickerValueEditor.cs @@ -15,7 +15,7 @@ using Umbraco.Web.PublishedCache; namespace Umbraco.Web.PropertyEditors { - public class MultiUrlPickerValueEditor : DataValueEditor, IDataValueReference + public class MultiUrlPickerValueEditor : DataValueEditor { private readonly IEntityService _entityService; private readonly ILogger _logger; @@ -190,9 +190,6 @@ namespace Umbraco.Web.PropertyEditors } } - - - } } } diff --git a/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs index 3d0605c4f9..865b583624 100644 --- a/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs @@ -54,7 +54,7 @@ namespace Umbraco.Web.PropertyEditors protected override IDataValueEditor CreateValueEditor() => new NestedContentPropertyValueEditor(Attribute, PropertyEditors, _dataTypeService, _contentTypeService); - internal class NestedContentPropertyValueEditor : DataValueEditor, IDataValueReference + internal class NestedContentPropertyValueEditor : DataValueEditor { private readonly PropertyEditorCollection _propertyEditors; private readonly IDataTypeService _dataTypeService; @@ -227,32 +227,6 @@ namespace Umbraco.Web.PropertyEditors // return json return JsonConvert.SerializeObject(deserialized); } - - public IEnumerable GetReferences(object value) - { - var rawJson = value == null ? string.Empty : value is string str ? str : value.ToString(); - - var result = new List(); - - foreach (var row in _nestedContentValues.GetPropertyValues(rawJson, out _)) - { - if (row.PropType == null) continue; - - var propEditor = _propertyEditors[row.PropType.PropertyEditorAlias]; - - var valueEditor = propEditor?.GetValueEditor(); - if (!(valueEditor is IDataValueReference reference)) continue; - - var val = row.JsonRowValue[row.PropKey]?.ToString(); - - var refs = reference.GetReferences(val); - - result.AddRange(refs); - } - - return result; - } - #endregion } diff --git a/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs index 427e36b37a..96b1d87205 100644 --- a/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs @@ -57,7 +57,7 @@ namespace Umbraco.Web.PropertyEditors /// /// A custom value editor to ensure that macro syntax is parsed when being persisted and formatted correctly for display in the editor /// - internal class RichTextPropertyValueEditor : DataValueEditor, IDataValueReference + internal class RichTextPropertyValueEditor : DataValueEditor { private IUmbracoContextAccessor _umbracoContextAccessor; private readonly HtmlImageSourceParser _imageSourceParser; @@ -130,24 +130,6 @@ namespace Umbraco.Web.PropertyEditors return parsed; } - - /// - /// Resolve references from values - /// - /// - /// - public IEnumerable GetReferences(object value) - { - var asString = value == null ? string.Empty : value is string str ? str : value.ToString(); - - foreach (var udi in _imageSourceParser.FindUdisFromDataAttributes(asString)) - yield return new UmbracoEntityReference(udi); - - foreach (var udi in _localLinkParser.FindUdisFromLocalLinks(asString)) - yield return new UmbracoEntityReference(udi); - - //TODO: Detect Macros too ... but we can save that for a later date, right now need to do media refs - } } internal class RichTextPropertyIndexValueFactory : IPropertyIndexValueFactory diff --git a/src/Umbraco.Web/PropertyEditors/ValueReferences/ContentPickerPropertyValueReferences.cs b/src/Umbraco.Web/PropertyEditors/ValueReferences/ContentPickerPropertyValueReferences.cs new file mode 100644 index 0000000000..08405b6d66 --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/ValueReferences/ContentPickerPropertyValueReferences.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using Umbraco.Core; +using Umbraco.Core.Models.Editors; +using Umbraco.Core.PropertyEditors; + +namespace Umbraco.Web.PropertyEditors.ValueReferences +{ + public class ContentPickerPropertyValueReferences : IDataValueReference + { + public IEnumerable GetReferences(object value) + { + var asString = value is string str ? str : value?.ToString(); + + if (string.IsNullOrEmpty(asString)) yield break; + + if (Udi.TryParse(asString, out var udi)) + yield return new UmbracoEntityReference(udi); + } + + public bool IsForEditor(IDataEditor dataEditor) => dataEditor.Alias.InvariantEquals(Constants.PropertyEditors.Aliases.ContentPicker); + } +} diff --git a/src/Umbraco.Web/PropertyEditors/ValueReferences/GridPropertyValueReferences.cs b/src/Umbraco.Web/PropertyEditors/ValueReferences/GridPropertyValueReferences.cs new file mode 100644 index 0000000000..85acff7ca3 --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/ValueReferences/GridPropertyValueReferences.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core; +using Umbraco.Core.Models.Editors; +using Umbraco.Core.PropertyEditors; + +namespace Umbraco.Web.PropertyEditors.ValueReferences +{ + public class GridPropertyValueReferences : IDataValueReference + { + + /// + /// Resolve references from values + /// + /// + /// + public IEnumerable GetReferences(object value) + { + return Enumerable.Empty(); + + //var rawJson = value == null ? string.Empty : value is string str ? str : value.ToString(); + + ////TODO FIX SQUIGLES + //GridPropertyEditor.GridPropertyValueEditor.DeserializeGridValue(rawJson, out var richTextEditorValues, out var mediaValues); + + //foreach (var umbracoEntityReference in richTextEditorValues.SelectMany(x => + // _richTextPropertyValueEditor.GetReferences(x.Value))) + // yield return umbracoEntityReference; + + //foreach (var umbracoEntityReference in mediaValues.SelectMany(x => + // _mediaPickerPropertyValueEditor.GetReferences(x.Value["udi"]))) + // yield return umbracoEntityReference; + } + + public bool IsForEditor(IDataEditor dataEditor) => dataEditor.Alias.InvariantEquals(Constants.PropertyEditors.Aliases.Grid); + } +} diff --git a/src/Umbraco.Web/PropertyEditors/ValueReferences/MediaPickerPropertyValueReferences.cs b/src/Umbraco.Web/PropertyEditors/ValueReferences/MediaPickerPropertyValueReferences.cs new file mode 100644 index 0000000000..ecd46b7ea5 --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/ValueReferences/MediaPickerPropertyValueReferences.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using Umbraco.Core; +using Umbraco.Core.Models.Editors; +using Umbraco.Core.PropertyEditors; + +namespace Umbraco.Web.PropertyEditors.ValueReferences +{ + public class MediaPickerPropertyValueReferences : IDataValueReference + { + public IEnumerable GetReferences(object value) + { + var asString = value is string str ? str : value?.ToString(); + + if (string.IsNullOrEmpty(asString)) yield break; + + if (Udi.TryParse(asString, out var udi)) + yield return new UmbracoEntityReference(udi); + } + + public bool IsForEditor(IDataEditor dataEditor) => dataEditor.Alias.InvariantEquals(Constants.PropertyEditors.Aliases.MediaPicker); + } +} diff --git a/src/Umbraco.Web/PropertyEditors/ValueReferences/MultiNodeTreePickerPropertyValueReferences.cs b/src/Umbraco.Web/PropertyEditors/ValueReferences/MultiNodeTreePickerPropertyValueReferences.cs new file mode 100644 index 0000000000..4413f75080 --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/ValueReferences/MultiNodeTreePickerPropertyValueReferences.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using Umbraco.Core; +using Umbraco.Core.Models.Editors; +using Umbraco.Core.PropertyEditors; + +namespace Umbraco.Web.PropertyEditors.ValueReferences +{ + public class MultiNodeTreePickerPropertyValueReferences : IDataValueReference + { + public IEnumerable GetReferences(object value) + { + var asString = value == null ? string.Empty : value is string str ? str : value.ToString(); + + var udiPaths = asString.Split(','); + foreach (var udiPath in udiPaths) + if (Udi.TryParse(udiPath, out var udi)) + yield return new UmbracoEntityReference(udi); + + } + + public bool IsForEditor(IDataEditor dataEditor) => dataEditor.Alias.InvariantEquals(Constants.PropertyEditors.Aliases.MultiNodeTreePicker); + } +} diff --git a/src/Umbraco.Web/PropertyEditors/ValueReferences/MultiUrlPickerValueReferences.cs b/src/Umbraco.Web/PropertyEditors/ValueReferences/MultiUrlPickerValueReferences.cs new file mode 100644 index 0000000000..5d8dfd4f39 --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/ValueReferences/MultiUrlPickerValueReferences.cs @@ -0,0 +1,26 @@ +using Newtonsoft.Json; +using System.Collections.Generic; +using Umbraco.Core; +using Umbraco.Core.Models.Editors; +using Umbraco.Core.PropertyEditors; + +namespace Umbraco.Web.PropertyEditors.ValueReferences +{ + public class MultiUrlPickerValueReferences : IDataValueReference + { + public IEnumerable GetReferences(object value) + { + var asString = value == null ? string.Empty : value is string str ? str : value.ToString(); + + if (string.IsNullOrEmpty(asString)) yield break; + + var links = JsonConvert.DeserializeObject>(asString); + foreach (var link in links) + if (link.Udi != null) // Links can be absolute links without a Udi + yield return new UmbracoEntityReference(link.Udi); + } + + public bool IsForEditor(IDataEditor dataEditor) => dataEditor.Alias.InvariantEquals(Constants.PropertyEditors.Aliases.MultiUrlPicker); + + } +} diff --git a/src/Umbraco.Web/PropertyEditors/ValueReferences/NestedContentPropertyValueReferences.cs b/src/Umbraco.Web/PropertyEditors/ValueReferences/NestedContentPropertyValueReferences.cs new file mode 100644 index 0000000000..a872dde1c7 --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/ValueReferences/NestedContentPropertyValueReferences.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using Umbraco.Core; +using Umbraco.Core.Models.Editors; +using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Services; +using static Umbraco.Web.PropertyEditors.NestedContentPropertyEditor; + +namespace Umbraco.Web.PropertyEditors.ValueReferences +{ + public class NestedContentPropertyValueReferences : IDataValueReference + { + private PropertyEditorCollection _propertyEditors; + private NestedContentValues _nestedContentValues; + + //ARGH LightInject moaning at me + public NestedContentPropertyValueReferences(PropertyEditorCollection propertyEditors, IContentTypeService contentTypeService) + { + _propertyEditors = propertyEditors; + _nestedContentValues = new NestedContentValues(contentTypeService); + } + + public IEnumerable GetReferences(object value) + { + var rawJson = value == null ? string.Empty : value is string str ? str : value.ToString(); + + var result = new List(); + + foreach (var row in _nestedContentValues.GetPropertyValues(rawJson, out _)) + { + if (row.PropType == null) continue; + + var propEditor = _propertyEditors[row.PropType.PropertyEditorAlias]; + + var valueEditor = propEditor?.GetValueEditor(); + if (!(valueEditor is IDataValueReference reference)) continue; + + var val = row.JsonRowValue[row.PropKey]?.ToString(); + + var refs = reference.GetReferences(val); + + result.AddRange(refs); + } + + return result; + } + + public bool IsForEditor(IDataEditor dataEditor) => dataEditor.Alias.InvariantEquals(Constants.PropertyEditors.Aliases.NestedContent); + } +} diff --git a/src/Umbraco.Web/PropertyEditors/ValueReferences/RichTextPropertyValueReferences.cs b/src/Umbraco.Web/PropertyEditors/ValueReferences/RichTextPropertyValueReferences.cs new file mode 100644 index 0000000000..744b0dc27c --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/ValueReferences/RichTextPropertyValueReferences.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; +using Umbraco.Core; +using Umbraco.Core.Models.Editors; +using Umbraco.Core.PropertyEditors; +using Umbraco.Web.Templates; + +namespace Umbraco.Web.PropertyEditors.ValueReferences +{ + public class RichTextPropertyValueReferences : IDataValueReference + { + private HtmlImageSourceParser _imageSourceParser; + private HtmlLocalLinkParser _localLinkParser; + + public RichTextPropertyValueReferences(HtmlImageSourceParser imageSourceParser, HtmlLocalLinkParser localLinkParser) + { + _imageSourceParser = imageSourceParser; + _localLinkParser = localLinkParser; + } + + /// + /// Resolve references from values + /// + /// + /// + public IEnumerable GetReferences(object value) + { + var asString = value == null ? string.Empty : value is string str ? str : value.ToString(); + + foreach (var udi in _imageSourceParser.FindUdisFromDataAttributes(asString)) + yield return new UmbracoEntityReference(udi); + + foreach (var udi in _localLinkParser.FindUdisFromLocalLinks(asString)) + yield return new UmbracoEntityReference(udi); + + //TODO: Detect Macros too ... but we can save that for a later date, right now need to do media refs + } + + public bool IsForEditor(IDataEditor dataEditor) => dataEditor.Alias.InvariantEquals(Constants.PropertyEditors.Aliases.TinyMce); + } +} diff --git a/src/Umbraco.Web/Runtime/WebInitialComposer.cs b/src/Umbraco.Web/Runtime/WebInitialComposer.cs index 27d0b5a1ce..295a31c461 100644 --- a/src/Umbraco.Web/Runtime/WebInitialComposer.cs +++ b/src/Umbraco.Web/Runtime/WebInitialComposer.cs @@ -42,6 +42,8 @@ using Umbraco.Web.Trees; using Umbraco.Web.WebApi; using Current = Umbraco.Web.Composing.Current; using Umbraco.Web.PropertyEditors; +using static Umbraco.Web.PropertyEditors.GridPropertyEditor; +using Umbraco.Web.PropertyEditors.ValueReferences; namespace Umbraco.Web.Runtime { @@ -274,7 +276,22 @@ namespace Umbraco.Web.Runtime .Append() .Append() .Append(); - + + // Used to determine if a datatype/editor should be storing/tracking + // references to media item/s + composition.DataValueReferences() + .Append() + + // TODO: Unsure how to call other ValueReference in collection + // When looking at grid item cells for RTE or media found + //.Append() + .Append() + .Append() + .Append() + + // TODO: LightInject problem + //.Append() + .Append(); // replace with web implementation composition.RegisterUnique(); diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 5eca5ad7fe..22680898af 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -235,6 +235,13 @@ + + + + + + + From 69faaa8797d282eb5bfc5bd33eed1db32ab89e61 Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Wed, 27 Nov 2019 20:29:38 +0000 Subject: [PATCH 088/202] Update tests to pass in an empty collection for the repository layers expecting the new dataValueReferencesCollection --- .../Persistence/Repositories/ContentTypeRepositoryTest.cs | 3 ++- .../Persistence/Repositories/DocumentRepositoryTest.cs | 3 ++- .../Persistence/Repositories/DomainRepositoryTest.cs | 3 ++- .../Persistence/Repositories/MediaRepositoryTest.cs | 3 ++- .../Persistence/Repositories/MemberRepositoryTest.cs | 3 ++- .../Persistence/Repositories/PublicAccessRepositoryTest.cs | 3 ++- .../Persistence/Repositories/TagRepositoryTest.cs | 6 ++++-- .../Persistence/Repositories/TemplateRepositoryTest.cs | 3 ++- .../Persistence/Repositories/UserRepositoryTest.cs | 6 ++++-- src/Umbraco.Tests/Services/ContentServicePerformanceTest.cs | 3 ++- src/Umbraco.Tests/Services/ContentServiceTests.cs | 3 ++- 11 files changed, 26 insertions(+), 13 deletions(-) diff --git a/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs index 8ed935795d..141331d4f4 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs @@ -40,7 +40,8 @@ namespace Umbraco.Tests.Persistence.Repositories var entityRepository = new EntityRepository(scopeAccessor); var relationRepository = new RelationRepository(scopeAccessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var repository = new DocumentRepository(scopeAccessor, AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors); + var dataValueReferences = new DataValueReferenceCollection(Enumerable.Empty()); + var repository = new DocumentRepository(scopeAccessor, AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferences); return repository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/DocumentRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/DocumentRepositoryTest.cs index 3a36a647f0..76d52fb844 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/DocumentRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/DocumentRepositoryTest.cs @@ -73,7 +73,8 @@ namespace Umbraco.Tests.Persistence.Repositories var entityRepository = new EntityRepository(scopeAccessor); var relationRepository = new RelationRepository(scopeAccessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var repository = new DocumentRepository(scopeAccessor, appCaches, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors); + var dataValueReferences = new DataValueReferenceCollection(Enumerable.Empty()); + var repository = new DocumentRepository(scopeAccessor, appCaches, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferences); return repository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/DomainRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/DomainRepositoryTest.cs index ad27aed309..99adcd63e3 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/DomainRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/DomainRepositoryTest.cs @@ -31,7 +31,8 @@ namespace Umbraco.Tests.Persistence.Repositories var entityRepository = new EntityRepository(accessor); var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - documentRepository = new DocumentRepository(accessor, Core.Cache.AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors); + var dataValueReferences = new DataValueReferenceCollection(Enumerable.Empty()); + documentRepository = new DocumentRepository(accessor, Core.Cache.AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferences); var domainRepository = new DomainRepository(accessor, Core.Cache.AppCaches.Disabled, Logger); return domainRepository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs index ced0ee75e9..c7503671b7 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs @@ -46,7 +46,8 @@ namespace Umbraco.Tests.Persistence.Repositories var entityRepository = new EntityRepository(scopeAccessor); var relationRepository = new RelationRepository(scopeAccessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var repository = new MediaRepository(scopeAccessor, appCaches, Logger, mediaTypeRepository, tagRepository, Mock.Of(), relationRepository, relationTypeRepository, propertyEditors); + var dataValueReferences = new DataValueReferenceCollection(Enumerable.Empty()); + var repository = new MediaRepository(scopeAccessor, appCaches, Logger, mediaTypeRepository, tagRepository, Mock.Of(), relationRepository, relationTypeRepository, propertyEditors, dataValueReferences); return repository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs index 7531d92b49..b9d034bd12 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs @@ -40,7 +40,8 @@ namespace Umbraco.Tests.Persistence.Repositories var entityRepository = new EntityRepository(accessor); var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var repository = new MemberRepository(accessor, AppCaches.Disabled, Logger, memberTypeRepository, memberGroupRepository, tagRepo, Mock.Of(), relationRepository, relationTypeRepository, propertyEditors); + var dataValueReferences = new DataValueReferenceCollection(Enumerable.Empty()); + var repository = new MemberRepository(accessor, AppCaches.Disabled, Logger, memberTypeRepository, memberGroupRepository, tagRepo, Mock.Of(), relationRepository, relationTypeRepository, propertyEditors, dataValueReferences); return repository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/PublicAccessRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/PublicAccessRepositoryTest.cs index 9a65bde41f..b69adcbb67 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/PublicAccessRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/PublicAccessRepositoryTest.cs @@ -315,7 +315,8 @@ namespace Umbraco.Tests.Persistence.Repositories var entityRepository = new EntityRepository(accessor); var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var repository = new DocumentRepository(accessor, AppCaches, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors); + var dataValueReferences = new DataValueReferenceCollection(Enumerable.Empty()); + var repository = new DocumentRepository(accessor, AppCaches, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferences); return repository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/TagRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/TagRepositoryTest.cs index fe0db4563a..7f356897af 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/TagRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/TagRepositoryTest.cs @@ -964,7 +964,8 @@ namespace Umbraco.Tests.Persistence.Repositories var entityRepository = new EntityRepository(accessor); var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var repository = new DocumentRepository(accessor, AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors); + var dataValueReferences = new DataValueReferenceCollection(Enumerable.Empty()); + var repository = new DocumentRepository(accessor, AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferences); return repository; } @@ -980,7 +981,8 @@ namespace Umbraco.Tests.Persistence.Repositories var entityRepository = new EntityRepository(accessor); var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var repository = new MediaRepository(accessor, AppCaches.Disabled, Logger, mediaTypeRepository, tagRepository, Mock.Of(), relationRepository, relationTypeRepository, propertyEditors); + var dataValueReferences = new DataValueReferenceCollection(Enumerable.Empty()); + var repository = new MediaRepository(accessor, AppCaches.Disabled, Logger, mediaTypeRepository, tagRepository, Mock.Of(), relationRepository, relationTypeRepository, propertyEditors, dataValueReferences); return repository; } } diff --git a/src/Umbraco.Tests/Persistence/Repositories/TemplateRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/TemplateRepositoryTest.cs index 4bf50c6ffd..14aa9d8cf2 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/TemplateRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/TemplateRepositoryTest.cs @@ -246,7 +246,8 @@ namespace Umbraco.Tests.Persistence.Repositories var entityRepository = new EntityRepository(ScopeProvider); var relationRepository = new RelationRepository(ScopeProvider, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var contentRepo = new DocumentRepository(ScopeProvider, AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors); + var dataValueReferences = new DataValueReferenceCollection(Enumerable.Empty()); + var contentRepo = new DocumentRepository(ScopeProvider, AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferences); var contentType = MockedContentTypes.CreateSimpleContentType("umbTextpage2", "Textpage"); ServiceContext.FileService.SaveTemplate(contentType.DefaultTemplate); // else, FK violation on contentType! diff --git a/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs index f4ab387683..fead39b6cb 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs @@ -35,7 +35,8 @@ namespace Umbraco.Tests.Persistence.Repositories var entityRepository = new EntityRepository(accessor); var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var repository = new MediaRepository(accessor, AppCaches, Mock.Of(), mediaTypeRepository, tagRepository, Mock.Of(), relationRepository, relationTypeRepository, propertyEditors); + var dataValueReferences = new DataValueReferenceCollection(Enumerable.Empty()); + var repository = new MediaRepository(accessor, AppCaches, Mock.Of(), mediaTypeRepository, tagRepository, Mock.Of(), relationRepository, relationTypeRepository, propertyEditors, dataValueReferences); return repository; } @@ -57,7 +58,8 @@ namespace Umbraco.Tests.Persistence.Repositories var entityRepository = new EntityRepository(accessor); var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var repository = new DocumentRepository(accessor, AppCaches, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors); + var dataValueReferences = new DataValueReferenceCollection(Enumerable.Empty()); + var repository = new DocumentRepository(accessor, AppCaches, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferences); return repository; } diff --git a/src/Umbraco.Tests/Services/ContentServicePerformanceTest.cs b/src/Umbraco.Tests/Services/ContentServicePerformanceTest.cs index 763635c393..d0f4ff95b3 100644 --- a/src/Umbraco.Tests/Services/ContentServicePerformanceTest.cs +++ b/src/Umbraco.Tests/Services/ContentServicePerformanceTest.cs @@ -52,7 +52,8 @@ namespace Umbraco.Tests.Services var entityRepository = new EntityRepository(accessor); var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var repository = new DocumentRepository(accessor, AppCaches.Disabled, Logger, ctRepository, tRepository, tagRepo, languageRepository, relationRepository, relationTypeRepository, propertyEditors); + var dataValueReferences = new DataValueReferenceCollection(Enumerable.Empty()); + var repository = new DocumentRepository(accessor, AppCaches.Disabled, Logger, ctRepository, tRepository, tagRepo, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferences); return repository; } diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs index 88b5cb5c34..cb097af2ef 100644 --- a/src/Umbraco.Tests/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs @@ -3211,7 +3211,8 @@ namespace Umbraco.Tests.Services var entityRepository = new EntityRepository(accessor); var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var repository = new DocumentRepository(accessor, AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors); + var dataValueReferences = new DataValueReferenceCollection(Enumerable.Empty()); + var repository = new DocumentRepository(accessor, AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferences); return repository; } From 1471bffeffd1a9a9f68fc8b92b741b35bc227491 Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Mon, 2 Dec 2019 15:00:56 +0000 Subject: [PATCH 089/202] Adds DataValueReferenceFor & its collection --- src/Umbraco.Core/Composing/Current.cs | 4 +- src/Umbraco.Core/CompositionExtensions.cs | 4 +- .../Implement/ContentRepositoryBase.cs | 20 +++++--- .../Implement/DocumentBlueprintRepository.cs | 4 +- .../Implement/DocumentRepository.cs | 4 +- .../Repositories/Implement/MediaRepository.cs | 4 +- .../Implement/MemberRepository.cs | 4 +- .../DataValueReferenceCollection.cs | 12 ----- .../DataValueReferenceCollectionBuilder.cs | 9 ---- .../DataValueReferenceForCollection.cs | 12 +++++ .../DataValueReferenceForCollectionBuilder.cs | 9 ++++ .../PropertyEditors/IDataValueReference.cs | 9 +--- .../PropertyEditors/IDataValueReferenceFor.cs | 18 +++++++ src/Umbraco.Core/Umbraco.Core.csproj | 5 +- .../Repositories/ContentTypeRepositoryTest.cs | 2 +- .../Repositories/DocumentRepositoryTest.cs | 2 +- .../Repositories/DomainRepositoryTest.cs | 2 +- .../Repositories/MediaRepositoryTest.cs | 2 +- .../Repositories/MemberRepositoryTest.cs | 2 +- .../PublicAccessRepositoryTest.cs | 2 +- .../Repositories/TagRepositoryTest.cs | 4 +- .../Repositories/TemplateRepositoryTest.cs | 2 +- .../Repositories/UserRepositoryTest.cs | 4 +- .../Services/ContentServicePerformanceTest.cs | 2 +- .../Services/ContentServiceTests.cs | 2 +- src/Umbraco.Web/Composing/Current.cs | 2 +- .../ContentPickerPropertyEditor.cs | 12 ++++- .../PropertyEditors/GridPropertyEditor.cs | 21 +++++++- .../MediaPickerPropertyEditor.cs | 12 ++++- .../MultiNodeTreePickerPropertyEditor.cs | 12 ++++- .../MultiUrlPickerPropertyEditor.cs | 17 ++++++- .../NestedContentPropertyEditor.cs | 27 +++++++++- .../PropertyEditors/RichTextPropertyEditor.cs | 20 +++++++- .../ContentPickerPropertyValueReferences.cs | 22 --------- .../GridPropertyValueReferences.cs | 37 -------------- .../MediaPickerPropertyValueReferences.cs | 22 --------- ...tiNodeTreePickerPropertyValueReferences.cs | 23 --------- .../MultiUrlPickerValueReferences.cs | 26 ---------- .../NestedContentPropertyValueReferences.cs | 49 ------------------- .../RichTextPropertyValueReferences.cs | 40 --------------- src/Umbraco.Web/Runtime/WebInitialComposer.cs | 21 +------- src/Umbraco.Web/Umbraco.Web.csproj | 7 --- 42 files changed, 199 insertions(+), 315 deletions(-) delete mode 100644 src/Umbraco.Core/PropertyEditors/DataValueReferenceCollection.cs delete mode 100644 src/Umbraco.Core/PropertyEditors/DataValueReferenceCollectionBuilder.cs create mode 100644 src/Umbraco.Core/PropertyEditors/DataValueReferenceForCollection.cs create mode 100644 src/Umbraco.Core/PropertyEditors/DataValueReferenceForCollectionBuilder.cs create mode 100644 src/Umbraco.Core/PropertyEditors/IDataValueReferenceFor.cs delete mode 100644 src/Umbraco.Web/PropertyEditors/ValueReferences/ContentPickerPropertyValueReferences.cs delete mode 100644 src/Umbraco.Web/PropertyEditors/ValueReferences/GridPropertyValueReferences.cs delete mode 100644 src/Umbraco.Web/PropertyEditors/ValueReferences/MediaPickerPropertyValueReferences.cs delete mode 100644 src/Umbraco.Web/PropertyEditors/ValueReferences/MultiNodeTreePickerPropertyValueReferences.cs delete mode 100644 src/Umbraco.Web/PropertyEditors/ValueReferences/MultiUrlPickerValueReferences.cs delete mode 100644 src/Umbraco.Web/PropertyEditors/ValueReferences/NestedContentPropertyValueReferences.cs delete mode 100644 src/Umbraco.Web/PropertyEditors/ValueReferences/RichTextPropertyValueReferences.cs diff --git a/src/Umbraco.Core/Composing/Current.cs b/src/Umbraco.Core/Composing/Current.cs index 1bd56ed727..899c465ca7 100644 --- a/src/Umbraco.Core/Composing/Current.cs +++ b/src/Umbraco.Core/Composing/Current.cs @@ -154,8 +154,8 @@ namespace Umbraco.Core.Composing public static DataEditorCollection DataEditors => Factory.GetInstance(); - public static DataValueReferenceCollection DataValueReferences - => Factory.GetInstance(); + public static DataValueReferenceForCollection DataValueReferenceFors + => Factory.GetInstance(); public static PropertyEditorCollection PropertyEditors => Factory.GetInstance(); diff --git a/src/Umbraco.Core/CompositionExtensions.cs b/src/Umbraco.Core/CompositionExtensions.cs index d29f251edc..aee4b41be8 100644 --- a/src/Umbraco.Core/CompositionExtensions.cs +++ b/src/Umbraco.Core/CompositionExtensions.cs @@ -53,8 +53,8 @@ namespace Umbraco.Core /// Gets the data value reference collection builder. /// /// The composition. - public static DataValueReferenceCollectionBuilder DataValueReferences(this Composition composition) - => composition.WithCollectionBuilder(); + public static DataValueReferenceForCollectionBuilder DataValueReferenceFors(this Composition composition) + => composition.WithCollectionBuilder(); /// /// Gets the property value converters collection builder. diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs index c91a7e20e1..77d36b8d43 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs @@ -36,7 +36,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement where TRepository : class, IRepository { private readonly Lazy _propertyEditors; - private readonly DataValueReferenceCollection _dataValueReferences; + private readonly DataValueReferenceForCollection _dataValueReferenceFors; /// /// @@ -50,14 +50,14 @@ namespace Umbraco.Core.Persistence.Repositories.Implement /// protected ContentRepositoryBase(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, ILanguageRepository languageRepository, IRelationRepository relationRepository, IRelationTypeRepository relationTypeRepository, - Lazy propertyEditors, DataValueReferenceCollection dataValueReferences) + Lazy propertyEditors, DataValueReferenceForCollection dataValueReferenceFors) : base(scopeAccessor, cache, logger) { LanguageRepository = languageRepository; RelationRepository = relationRepository; RelationTypeRepository = relationTypeRepository; _propertyEditors = propertyEditors; - _dataValueReferences = dataValueReferences; + _dataValueReferenceFors = dataValueReferenceFors; } protected abstract TRepository This { get; } @@ -828,20 +828,28 @@ namespace Umbraco.Core.Persistence.Repositories.Implement foreach (var p in entity.Properties) { if (!PropertyEditors.TryGet(p.PropertyType.PropertyEditorAlias, out var editor)) continue; + var valueEditor = editor.GetValueEditor(); + if (!(valueEditor is IDataValueReference reference)) continue; //TODO: Support variants/segments! This is not required for this initial prototype which is why there is a check here if (!p.PropertyType.VariesByNothing()) continue; var val = p.GetValue(); // get the invariant value + var refs = reference.GetReferences(val); + trackedRelations.AddRange(refs); - // WB: Loop over our collection of references registered and add references - foreach (var item in _dataValueReferences) + + // Loop over collection that may be add to existing property editors + // implementation of GetReferences in IDataValueReference. + // Allows developers to add support for references by a + // package /property editor that did not implement IDataValueReference themselves + foreach (var item in _dataValueReferenceFors) { // Check if this value reference is for this datatype/editor // Then call it's GetReferences method - to see if the value stored // in the dataeditor/property has referecnes to media items if (item.IsForEditor(editor)) - trackedRelations.AddRange(item.GetReferences(val)); + trackedRelations.AddRange(item.GetDataValueReference().GetReferences(val)); } } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentBlueprintRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentBlueprintRepository.cs index 5c2e73de9d..e3fba3db01 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentBlueprintRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentBlueprintRepository.cs @@ -20,8 +20,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement { public DocumentBlueprintRepository(IScopeAccessor scopeAccessor, AppCaches appCaches, ILogger logger, IContentTypeRepository contentTypeRepository, ITemplateRepository templateRepository, ITagRepository tagRepository, ILanguageRepository languageRepository, IRelationRepository relationRepository, IRelationTypeRepository relationTypeRepository, - Lazy propertyEditorCollection, DataValueReferenceCollection dataValueReferences) - : base(scopeAccessor, appCaches, logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditorCollection, dataValueReferences) + Lazy propertyEditorCollection, DataValueReferenceForCollection dataValueReferenceFors) + : base(scopeAccessor, appCaches, logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditorCollection, dataValueReferenceFors) { } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs index e1ea955972..8b63b93f16 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs @@ -46,8 +46,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement /// public DocumentRepository(IScopeAccessor scopeAccessor, AppCaches appCaches, ILogger logger, IContentTypeRepository contentTypeRepository, ITemplateRepository templateRepository, ITagRepository tagRepository, ILanguageRepository languageRepository, IRelationRepository relationRepository, IRelationTypeRepository relationTypeRepository, - Lazy propertyEditors, DataValueReferenceCollection dataValueReferences) - : base(scopeAccessor, appCaches, logger, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferences) + Lazy propertyEditors, DataValueReferenceForCollection dataValueReferenceFors) + : base(scopeAccessor, appCaches, logger, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferenceFors) { _contentTypeRepository = contentTypeRepository ?? throw new ArgumentNullException(nameof(contentTypeRepository)); _templateRepository = templateRepository ?? throw new ArgumentNullException(nameof(templateRepository)); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs index 2297bbed2c..da124db77f 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs @@ -29,8 +29,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement private readonly MediaByGuidReadRepository _mediaByGuidReadRepository; public MediaRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IMediaTypeRepository mediaTypeRepository, ITagRepository tagRepository, ILanguageRepository languageRepository, IRelationRepository relationRepository, IRelationTypeRepository relationTypeRepository, - Lazy propertyEditorCollection, DataValueReferenceCollection dataValueReferences) - : base(scopeAccessor, cache, logger, languageRepository, relationRepository, relationTypeRepository, propertyEditorCollection, dataValueReferences) + Lazy propertyEditorCollection, DataValueReferenceForCollection dataValueReferenceFors) + : base(scopeAccessor, cache, logger, languageRepository, relationRepository, relationTypeRepository, propertyEditorCollection, dataValueReferenceFors) { _mediaTypeRepository = mediaTypeRepository ?? throw new ArgumentNullException(nameof(mediaTypeRepository)); _tagRepository = tagRepository ?? throw new ArgumentNullException(nameof(tagRepository)); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs index c20e990a2b..06d93dfe50 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs @@ -28,8 +28,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement public MemberRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IMemberTypeRepository memberTypeRepository, IMemberGroupRepository memberGroupRepository, ITagRepository tagRepository, ILanguageRepository languageRepository, IRelationRepository relationRepository, IRelationTypeRepository relationTypeRepository, - Lazy propertyEditors, DataValueReferenceCollection dataValueReferences) - : base(scopeAccessor, cache, logger, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferences) + Lazy propertyEditors, DataValueReferenceForCollection dataValueReferenceFors) + : base(scopeAccessor, cache, logger, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferenceFors) { _memberTypeRepository = memberTypeRepository ?? throw new ArgumentNullException(nameof(memberTypeRepository)); _tagRepository = tagRepository ?? throw new ArgumentNullException(nameof(tagRepository)); diff --git a/src/Umbraco.Core/PropertyEditors/DataValueReferenceCollection.cs b/src/Umbraco.Core/PropertyEditors/DataValueReferenceCollection.cs deleted file mode 100644 index 6d0e3e62df..0000000000 --- a/src/Umbraco.Core/PropertyEditors/DataValueReferenceCollection.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Collections.Generic; -using Umbraco.Core.Composing; - -namespace Umbraco.Core.PropertyEditors -{ - public class DataValueReferenceCollection : BuilderCollectionBase - { - public DataValueReferenceCollection(IEnumerable items) - : base(items) - { } - } -} diff --git a/src/Umbraco.Core/PropertyEditors/DataValueReferenceCollectionBuilder.cs b/src/Umbraco.Core/PropertyEditors/DataValueReferenceCollectionBuilder.cs deleted file mode 100644 index c6442275b0..0000000000 --- a/src/Umbraco.Core/PropertyEditors/DataValueReferenceCollectionBuilder.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Umbraco.Core.Composing; - -namespace Umbraco.Core.PropertyEditors -{ - public class DataValueReferenceCollectionBuilder : OrderedCollectionBuilderBase - { - protected override DataValueReferenceCollectionBuilder This => this; - } -} diff --git a/src/Umbraco.Core/PropertyEditors/DataValueReferenceForCollection.cs b/src/Umbraco.Core/PropertyEditors/DataValueReferenceForCollection.cs new file mode 100644 index 0000000000..c5b9470868 --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/DataValueReferenceForCollection.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using Umbraco.Core.Composing; + +namespace Umbraco.Core.PropertyEditors +{ + public class DataValueReferenceForCollection : BuilderCollectionBase + { + public DataValueReferenceForCollection(IEnumerable items) + : base(items) + { } + } +} diff --git a/src/Umbraco.Core/PropertyEditors/DataValueReferenceForCollectionBuilder.cs b/src/Umbraco.Core/PropertyEditors/DataValueReferenceForCollectionBuilder.cs new file mode 100644 index 0000000000..a7b03ea482 --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/DataValueReferenceForCollectionBuilder.cs @@ -0,0 +1,9 @@ +using Umbraco.Core.Composing; + +namespace Umbraco.Core.PropertyEditors +{ + public class DataValueReferenceForCollectionBuilder : OrderedCollectionBuilderBase + { + protected override DataValueReferenceForCollectionBuilder This => this; + } +} diff --git a/src/Umbraco.Core/PropertyEditors/IDataValueReference.cs b/src/Umbraco.Core/PropertyEditors/IDataValueReference.cs index 7d46704938..6377098bfc 100644 --- a/src/Umbraco.Core/PropertyEditors/IDataValueReference.cs +++ b/src/Umbraco.Core/PropertyEditors/IDataValueReference.cs @@ -13,13 +13,6 @@ namespace Umbraco.Core.PropertyEditors /// /// /// - IEnumerable GetReferences(object value); - - /// - /// Gets a value indicating whether the DataValueReference lookup supports a datatype (data editor). - /// - /// The datatype. - /// A value indicating whether the converter supports a datatype. - bool IsForEditor(IDataEditor dataEditor); + IEnumerable GetReferences(object value); } } diff --git a/src/Umbraco.Core/PropertyEditors/IDataValueReferenceFor.cs b/src/Umbraco.Core/PropertyEditors/IDataValueReferenceFor.cs new file mode 100644 index 0000000000..e0d5e4bad1 --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/IDataValueReferenceFor.cs @@ -0,0 +1,18 @@ +namespace Umbraco.Core.PropertyEditors +{ + public interface IDataValueReferenceFor + { + /// + /// Gets a value indicating whether the DataValueReference lookup supports a datatype (data editor). + /// + /// The datatype. + /// A value indicating whether the converter supports a datatype. + bool IsForEditor(IDataEditor dataEditor); + + /// + /// + /// + /// + IDataValueReference GetDataValueReference(); + } +} diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index c6545a4f8b..98871fbf5c 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -281,9 +281,10 @@ - - + + + diff --git a/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs index 141331d4f4..9b99aa9b4c 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs @@ -40,7 +40,7 @@ namespace Umbraco.Tests.Persistence.Repositories var entityRepository = new EntityRepository(scopeAccessor); var relationRepository = new RelationRepository(scopeAccessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var dataValueReferences = new DataValueReferenceCollection(Enumerable.Empty()); + var dataValueReferences = new DataValueReferenceForCollection(Enumerable.Empty()); var repository = new DocumentRepository(scopeAccessor, AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferences); return repository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/DocumentRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/DocumentRepositoryTest.cs index 76d52fb844..42d2d13c6e 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/DocumentRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/DocumentRepositoryTest.cs @@ -73,7 +73,7 @@ namespace Umbraco.Tests.Persistence.Repositories var entityRepository = new EntityRepository(scopeAccessor); var relationRepository = new RelationRepository(scopeAccessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var dataValueReferences = new DataValueReferenceCollection(Enumerable.Empty()); + var dataValueReferences = new DataValueReferenceForCollection(Enumerable.Empty()); var repository = new DocumentRepository(scopeAccessor, appCaches, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferences); return repository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/DomainRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/DomainRepositoryTest.cs index 99adcd63e3..6d78601bba 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/DomainRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/DomainRepositoryTest.cs @@ -31,7 +31,7 @@ namespace Umbraco.Tests.Persistence.Repositories var entityRepository = new EntityRepository(accessor); var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var dataValueReferences = new DataValueReferenceCollection(Enumerable.Empty()); + var dataValueReferences = new DataValueReferenceForCollection(Enumerable.Empty()); documentRepository = new DocumentRepository(accessor, Core.Cache.AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferences); var domainRepository = new DomainRepository(accessor, Core.Cache.AppCaches.Disabled, Logger); return domainRepository; diff --git a/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs index c7503671b7..886d0c5ecf 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs @@ -46,7 +46,7 @@ namespace Umbraco.Tests.Persistence.Repositories var entityRepository = new EntityRepository(scopeAccessor); var relationRepository = new RelationRepository(scopeAccessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var dataValueReferences = new DataValueReferenceCollection(Enumerable.Empty()); + var dataValueReferences = new DataValueReferenceForCollection(Enumerable.Empty()); var repository = new MediaRepository(scopeAccessor, appCaches, Logger, mediaTypeRepository, tagRepository, Mock.Of(), relationRepository, relationTypeRepository, propertyEditors, dataValueReferences); return repository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs index b9d034bd12..d3150623c2 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs @@ -40,7 +40,7 @@ namespace Umbraco.Tests.Persistence.Repositories var entityRepository = new EntityRepository(accessor); var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var dataValueReferences = new DataValueReferenceCollection(Enumerable.Empty()); + var dataValueReferences = new DataValueReferenceForCollection(Enumerable.Empty()); var repository = new MemberRepository(accessor, AppCaches.Disabled, Logger, memberTypeRepository, memberGroupRepository, tagRepo, Mock.Of(), relationRepository, relationTypeRepository, propertyEditors, dataValueReferences); return repository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/PublicAccessRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/PublicAccessRepositoryTest.cs index b69adcbb67..89f7c39abc 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/PublicAccessRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/PublicAccessRepositoryTest.cs @@ -315,7 +315,7 @@ namespace Umbraco.Tests.Persistence.Repositories var entityRepository = new EntityRepository(accessor); var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var dataValueReferences = new DataValueReferenceCollection(Enumerable.Empty()); + var dataValueReferences = new DataValueReferenceForCollection(Enumerable.Empty()); var repository = new DocumentRepository(accessor, AppCaches, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferences); return repository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/TagRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/TagRepositoryTest.cs index 7f356897af..a8a263043d 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/TagRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/TagRepositoryTest.cs @@ -964,7 +964,7 @@ namespace Umbraco.Tests.Persistence.Repositories var entityRepository = new EntityRepository(accessor); var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var dataValueReferences = new DataValueReferenceCollection(Enumerable.Empty()); + var dataValueReferences = new DataValueReferenceForCollection(Enumerable.Empty()); var repository = new DocumentRepository(accessor, AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferences); return repository; } @@ -981,7 +981,7 @@ namespace Umbraco.Tests.Persistence.Repositories var entityRepository = new EntityRepository(accessor); var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var dataValueReferences = new DataValueReferenceCollection(Enumerable.Empty()); + var dataValueReferences = new DataValueReferenceForCollection(Enumerable.Empty()); var repository = new MediaRepository(accessor, AppCaches.Disabled, Logger, mediaTypeRepository, tagRepository, Mock.Of(), relationRepository, relationTypeRepository, propertyEditors, dataValueReferences); return repository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/TemplateRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/TemplateRepositoryTest.cs index 14aa9d8cf2..7caf0ef934 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/TemplateRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/TemplateRepositoryTest.cs @@ -246,7 +246,7 @@ namespace Umbraco.Tests.Persistence.Repositories var entityRepository = new EntityRepository(ScopeProvider); var relationRepository = new RelationRepository(ScopeProvider, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var dataValueReferences = new DataValueReferenceCollection(Enumerable.Empty()); + var dataValueReferences = new DataValueReferenceForCollection(Enumerable.Empty()); var contentRepo = new DocumentRepository(ScopeProvider, AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferences); var contentType = MockedContentTypes.CreateSimpleContentType("umbTextpage2", "Textpage"); diff --git a/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs index fead39b6cb..b632699bfd 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs @@ -35,7 +35,7 @@ namespace Umbraco.Tests.Persistence.Repositories var entityRepository = new EntityRepository(accessor); var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var dataValueReferences = new DataValueReferenceCollection(Enumerable.Empty()); + var dataValueReferences = new DataValueReferenceForCollection(Enumerable.Empty()); var repository = new MediaRepository(accessor, AppCaches, Mock.Of(), mediaTypeRepository, tagRepository, Mock.Of(), relationRepository, relationTypeRepository, propertyEditors, dataValueReferences); return repository; } @@ -58,7 +58,7 @@ namespace Umbraco.Tests.Persistence.Repositories var entityRepository = new EntityRepository(accessor); var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var dataValueReferences = new DataValueReferenceCollection(Enumerable.Empty()); + var dataValueReferences = new DataValueReferenceForCollection(Enumerable.Empty()); var repository = new DocumentRepository(accessor, AppCaches, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferences); return repository; } diff --git a/src/Umbraco.Tests/Services/ContentServicePerformanceTest.cs b/src/Umbraco.Tests/Services/ContentServicePerformanceTest.cs index d0f4ff95b3..dd561cb3a8 100644 --- a/src/Umbraco.Tests/Services/ContentServicePerformanceTest.cs +++ b/src/Umbraco.Tests/Services/ContentServicePerformanceTest.cs @@ -52,7 +52,7 @@ namespace Umbraco.Tests.Services var entityRepository = new EntityRepository(accessor); var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var dataValueReferences = new DataValueReferenceCollection(Enumerable.Empty()); + var dataValueReferences = new DataValueReferenceForCollection(Enumerable.Empty()); var repository = new DocumentRepository(accessor, AppCaches.Disabled, Logger, ctRepository, tRepository, tagRepo, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferences); return repository; } diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs index cb097af2ef..70c51c5baa 100644 --- a/src/Umbraco.Tests/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs @@ -3211,7 +3211,7 @@ namespace Umbraco.Tests.Services var entityRepository = new EntityRepository(accessor); var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var dataValueReferences = new DataValueReferenceCollection(Enumerable.Empty()); + var dataValueReferences = new DataValueReferenceForCollection(Enumerable.Empty()); var repository = new DocumentRepository(accessor, AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferences); return repository; } diff --git a/src/Umbraco.Web/Composing/Current.cs b/src/Umbraco.Web/Composing/Current.cs index 1363d60b8a..2dd82d9a4a 100644 --- a/src/Umbraco.Web/Composing/Current.cs +++ b/src/Umbraco.Web/Composing/Current.cs @@ -182,7 +182,7 @@ namespace Umbraco.Web.Composing public static DataEditorCollection DataEditors => CoreCurrent.DataEditors; - public static DataValueReferenceCollection DataValueReferences => CoreCurrent.DataValueReferences; + public static DataValueReferenceForCollection DataValueReferenceFors => CoreCurrent.DataValueReferenceFors; public static PropertyEditorCollection PropertyEditors => CoreCurrent.PropertyEditors; diff --git a/src/Umbraco.Web/PropertyEditors/ContentPickerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/ContentPickerPropertyEditor.cs index 2f45d98fa1..683f1a05c3 100644 --- a/src/Umbraco.Web/PropertyEditors/ContentPickerPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/ContentPickerPropertyEditor.cs @@ -29,11 +29,21 @@ namespace Umbraco.Web.PropertyEditors protected override IDataValueEditor CreateValueEditor() => new ContentPickerPropertyValueEditor(Attribute); - internal class ContentPickerPropertyValueEditor : DataValueEditor + internal class ContentPickerPropertyValueEditor : DataValueEditor, IDataValueReference { public ContentPickerPropertyValueEditor(DataEditorAttribute attribute) : base(attribute) { } + + public IEnumerable GetReferences(object value) + { + var asString = value is string str ? str : value?.ToString(); + + if (string.IsNullOrEmpty(asString)) yield break; + + if (Udi.TryParse(asString, out var udi)) + yield return new UmbracoEntityReference(udi); + } } } } diff --git a/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs index 16dffb3b10..da4264be28 100644 --- a/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs @@ -54,7 +54,7 @@ namespace Umbraco.Web.PropertyEditors protected override IConfigurationEditor CreateConfigurationEditor() => new GridConfigurationEditor(); - internal class GridPropertyValueEditor : DataValueEditor + internal class GridPropertyValueEditor : DataValueEditor, IDataValueReference { private readonly IUmbracoContextAccessor _umbracoContextAccessor; private readonly HtmlImageSourceParser _imageSourceParser; @@ -156,6 +156,25 @@ namespace Umbraco.Web.PropertyEditors return grid; } + + /// + /// Resolve references from values + /// + /// + /// + public IEnumerable GetReferences(object value) + { + var rawJson = value == null ? string.Empty : value is string str ? str : value.ToString(); + DeserializeGridValue(rawJson, out var richTextEditorValues, out var mediaValues); + + foreach (var umbracoEntityReference in richTextEditorValues.SelectMany(x => + _richTextPropertyValueEditor.GetReferences(x.Value))) + yield return umbracoEntityReference; + + foreach (var umbracoEntityReference in mediaValues.SelectMany(x => + _mediaPickerPropertyValueEditor.GetReferences(x.Value["udi"]))) + yield return umbracoEntityReference; + } } } } diff --git a/src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs index 6416fa3342..ece210b9d1 100644 --- a/src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs @@ -33,11 +33,21 @@ namespace Umbraco.Web.PropertyEditors protected override IDataValueEditor CreateValueEditor() => new MediaPickerPropertyValueEditor(Attribute); - internal class MediaPickerPropertyValueEditor : DataValueEditor + internal class MediaPickerPropertyValueEditor : DataValueEditor, IDataValueReference { public MediaPickerPropertyValueEditor(DataEditorAttribute attribute) : base(attribute) { } + + public IEnumerable GetReferences(object value) + { + var asString = value is string str ? str : value?.ToString(); + + if (string.IsNullOrEmpty(asString)) yield break; + + if (Udi.TryParse(asString, out var udi)) + yield return new UmbracoEntityReference(udi); + } } } diff --git a/src/Umbraco.Web/PropertyEditors/MultiNodeTreePickerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MultiNodeTreePickerPropertyEditor.cs index d7a7a2ed59..fd7f735e68 100644 --- a/src/Umbraco.Web/PropertyEditors/MultiNodeTreePickerPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MultiNodeTreePickerPropertyEditor.cs @@ -23,12 +23,22 @@ namespace Umbraco.Web.PropertyEditors protected override IDataValueEditor CreateValueEditor() => new MultiNodeTreePickerPropertyValueEditor(Attribute); - public class MultiNodeTreePickerPropertyValueEditor : DataValueEditor + public class MultiNodeTreePickerPropertyValueEditor : DataValueEditor, IDataValueReference { public MultiNodeTreePickerPropertyValueEditor(DataEditorAttribute attribute): base(attribute) { } + + public IEnumerable GetReferences(object value) + { + var asString = value == null ? string.Empty : value is string str ? str : value.ToString(); + + var udiPaths = asString.Split(','); + foreach (var udiPath in udiPaths) + if (Udi.TryParse(udiPath, out var udi)) + yield return new UmbracoEntityReference(udi); + } } } diff --git a/src/Umbraco.Web/PropertyEditors/MultiUrlPickerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MultiUrlPickerPropertyEditor.cs index 95ac809576..e77ce3a993 100644 --- a/src/Umbraco.Web/PropertyEditors/MultiUrlPickerPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MultiUrlPickerPropertyEditor.cs @@ -4,6 +4,9 @@ using Umbraco.Core.PropertyEditors; using Umbraco.Core.Logging; using Umbraco.Core.Services; using Umbraco.Web.PublishedCache; +using System.Collections.Generic; +using Umbraco.Core.Models.Editors; +using Newtonsoft.Json; namespace Umbraco.Web.PropertyEditors { @@ -15,7 +18,7 @@ namespace Umbraco.Web.PropertyEditors ValueType = ValueTypes.Json, Group = Constants.PropertyEditors.Groups.Pickers, Icon = "icon-link")] - public class MultiUrlPickerPropertyEditor : DataEditor + public class MultiUrlPickerPropertyEditor : DataEditor, IDataValueReference { private readonly IEntityService _entityService; private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; @@ -25,6 +28,18 @@ namespace Umbraco.Web.PropertyEditors _entityService = entityService ?? throw new ArgumentNullException(nameof(entityService)); _publishedSnapshotAccessor = publishedSnapshotAccessor ?? throw new ArgumentNullException(nameof(publishedSnapshotAccessor)); } + + public IEnumerable GetReferences(object value) + { + var asString = value == null ? string.Empty : value is string str ? str : value.ToString(); + + if (string.IsNullOrEmpty(asString)) yield break; + + var links = JsonConvert.DeserializeObject>(asString); + foreach (var link in links) + if (link.Udi != null) // Links can be absolute links without a Udi + yield return new UmbracoEntityReference(link.Udi); + } protected override IConfigurationEditor CreateConfigurationEditor() => new MultiUrlPickerConfigurationEditor(); diff --git a/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs index 865b583624..f3e391aeab 100644 --- a/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs @@ -54,7 +54,7 @@ namespace Umbraco.Web.PropertyEditors protected override IDataValueEditor CreateValueEditor() => new NestedContentPropertyValueEditor(Attribute, PropertyEditors, _dataTypeService, _contentTypeService); - internal class NestedContentPropertyValueEditor : DataValueEditor + internal class NestedContentPropertyValueEditor : DataValueEditor, IDataValueReference { private readonly PropertyEditorCollection _propertyEditors; private readonly IDataTypeService _dataTypeService; @@ -228,6 +228,31 @@ namespace Umbraco.Web.PropertyEditors return JsonConvert.SerializeObject(deserialized); } #endregion + + public IEnumerable GetReferences(object value) + { + var rawJson = value == null ? string.Empty : value is string str ? str : value.ToString(); + + var result = new List(); + + foreach (var row in _nestedContentValues.GetPropertyValues(rawJson, out _)) + { + if (row.PropType == null) continue; + + var propEditor = _propertyEditors[row.PropType.PropertyEditorAlias]; + + var valueEditor = propEditor?.GetValueEditor(); + if (!(valueEditor is IDataValueReference reference)) continue; + + var val = row.JsonRowValue[row.PropKey]?.ToString(); + + var refs = reference.GetReferences(val); + + result.AddRange(refs); + } + + return result; + } } internal class NestedContentValidator : IValueValidator diff --git a/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs index 96b1d87205..427e36b37a 100644 --- a/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs @@ -57,7 +57,7 @@ namespace Umbraco.Web.PropertyEditors /// /// A custom value editor to ensure that macro syntax is parsed when being persisted and formatted correctly for display in the editor /// - internal class RichTextPropertyValueEditor : DataValueEditor + internal class RichTextPropertyValueEditor : DataValueEditor, IDataValueReference { private IUmbracoContextAccessor _umbracoContextAccessor; private readonly HtmlImageSourceParser _imageSourceParser; @@ -130,6 +130,24 @@ namespace Umbraco.Web.PropertyEditors return parsed; } + + /// + /// Resolve references from values + /// + /// + /// + public IEnumerable GetReferences(object value) + { + var asString = value == null ? string.Empty : value is string str ? str : value.ToString(); + + foreach (var udi in _imageSourceParser.FindUdisFromDataAttributes(asString)) + yield return new UmbracoEntityReference(udi); + + foreach (var udi in _localLinkParser.FindUdisFromLocalLinks(asString)) + yield return new UmbracoEntityReference(udi); + + //TODO: Detect Macros too ... but we can save that for a later date, right now need to do media refs + } } internal class RichTextPropertyIndexValueFactory : IPropertyIndexValueFactory diff --git a/src/Umbraco.Web/PropertyEditors/ValueReferences/ContentPickerPropertyValueReferences.cs b/src/Umbraco.Web/PropertyEditors/ValueReferences/ContentPickerPropertyValueReferences.cs deleted file mode 100644 index 08405b6d66..0000000000 --- a/src/Umbraco.Web/PropertyEditors/ValueReferences/ContentPickerPropertyValueReferences.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Collections.Generic; -using Umbraco.Core; -using Umbraco.Core.Models.Editors; -using Umbraco.Core.PropertyEditors; - -namespace Umbraco.Web.PropertyEditors.ValueReferences -{ - public class ContentPickerPropertyValueReferences : IDataValueReference - { - public IEnumerable GetReferences(object value) - { - var asString = value is string str ? str : value?.ToString(); - - if (string.IsNullOrEmpty(asString)) yield break; - - if (Udi.TryParse(asString, out var udi)) - yield return new UmbracoEntityReference(udi); - } - - public bool IsForEditor(IDataEditor dataEditor) => dataEditor.Alias.InvariantEquals(Constants.PropertyEditors.Aliases.ContentPicker); - } -} diff --git a/src/Umbraco.Web/PropertyEditors/ValueReferences/GridPropertyValueReferences.cs b/src/Umbraco.Web/PropertyEditors/ValueReferences/GridPropertyValueReferences.cs deleted file mode 100644 index 85acff7ca3..0000000000 --- a/src/Umbraco.Web/PropertyEditors/ValueReferences/GridPropertyValueReferences.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Umbraco.Core; -using Umbraco.Core.Models.Editors; -using Umbraco.Core.PropertyEditors; - -namespace Umbraco.Web.PropertyEditors.ValueReferences -{ - public class GridPropertyValueReferences : IDataValueReference - { - - /// - /// Resolve references from values - /// - /// - /// - public IEnumerable GetReferences(object value) - { - return Enumerable.Empty(); - - //var rawJson = value == null ? string.Empty : value is string str ? str : value.ToString(); - - ////TODO FIX SQUIGLES - //GridPropertyEditor.GridPropertyValueEditor.DeserializeGridValue(rawJson, out var richTextEditorValues, out var mediaValues); - - //foreach (var umbracoEntityReference in richTextEditorValues.SelectMany(x => - // _richTextPropertyValueEditor.GetReferences(x.Value))) - // yield return umbracoEntityReference; - - //foreach (var umbracoEntityReference in mediaValues.SelectMany(x => - // _mediaPickerPropertyValueEditor.GetReferences(x.Value["udi"]))) - // yield return umbracoEntityReference; - } - - public bool IsForEditor(IDataEditor dataEditor) => dataEditor.Alias.InvariantEquals(Constants.PropertyEditors.Aliases.Grid); - } -} diff --git a/src/Umbraco.Web/PropertyEditors/ValueReferences/MediaPickerPropertyValueReferences.cs b/src/Umbraco.Web/PropertyEditors/ValueReferences/MediaPickerPropertyValueReferences.cs deleted file mode 100644 index ecd46b7ea5..0000000000 --- a/src/Umbraco.Web/PropertyEditors/ValueReferences/MediaPickerPropertyValueReferences.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Collections.Generic; -using Umbraco.Core; -using Umbraco.Core.Models.Editors; -using Umbraco.Core.PropertyEditors; - -namespace Umbraco.Web.PropertyEditors.ValueReferences -{ - public class MediaPickerPropertyValueReferences : IDataValueReference - { - public IEnumerable GetReferences(object value) - { - var asString = value is string str ? str : value?.ToString(); - - if (string.IsNullOrEmpty(asString)) yield break; - - if (Udi.TryParse(asString, out var udi)) - yield return new UmbracoEntityReference(udi); - } - - public bool IsForEditor(IDataEditor dataEditor) => dataEditor.Alias.InvariantEquals(Constants.PropertyEditors.Aliases.MediaPicker); - } -} diff --git a/src/Umbraco.Web/PropertyEditors/ValueReferences/MultiNodeTreePickerPropertyValueReferences.cs b/src/Umbraco.Web/PropertyEditors/ValueReferences/MultiNodeTreePickerPropertyValueReferences.cs deleted file mode 100644 index 4413f75080..0000000000 --- a/src/Umbraco.Web/PropertyEditors/ValueReferences/MultiNodeTreePickerPropertyValueReferences.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Collections.Generic; -using Umbraco.Core; -using Umbraco.Core.Models.Editors; -using Umbraco.Core.PropertyEditors; - -namespace Umbraco.Web.PropertyEditors.ValueReferences -{ - public class MultiNodeTreePickerPropertyValueReferences : IDataValueReference - { - public IEnumerable GetReferences(object value) - { - var asString = value == null ? string.Empty : value is string str ? str : value.ToString(); - - var udiPaths = asString.Split(','); - foreach (var udiPath in udiPaths) - if (Udi.TryParse(udiPath, out var udi)) - yield return new UmbracoEntityReference(udi); - - } - - public bool IsForEditor(IDataEditor dataEditor) => dataEditor.Alias.InvariantEquals(Constants.PropertyEditors.Aliases.MultiNodeTreePicker); - } -} diff --git a/src/Umbraco.Web/PropertyEditors/ValueReferences/MultiUrlPickerValueReferences.cs b/src/Umbraco.Web/PropertyEditors/ValueReferences/MultiUrlPickerValueReferences.cs deleted file mode 100644 index 5d8dfd4f39..0000000000 --- a/src/Umbraco.Web/PropertyEditors/ValueReferences/MultiUrlPickerValueReferences.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Newtonsoft.Json; -using System.Collections.Generic; -using Umbraco.Core; -using Umbraco.Core.Models.Editors; -using Umbraco.Core.PropertyEditors; - -namespace Umbraco.Web.PropertyEditors.ValueReferences -{ - public class MultiUrlPickerValueReferences : IDataValueReference - { - public IEnumerable GetReferences(object value) - { - var asString = value == null ? string.Empty : value is string str ? str : value.ToString(); - - if (string.IsNullOrEmpty(asString)) yield break; - - var links = JsonConvert.DeserializeObject>(asString); - foreach (var link in links) - if (link.Udi != null) // Links can be absolute links without a Udi - yield return new UmbracoEntityReference(link.Udi); - } - - public bool IsForEditor(IDataEditor dataEditor) => dataEditor.Alias.InvariantEquals(Constants.PropertyEditors.Aliases.MultiUrlPicker); - - } -} diff --git a/src/Umbraco.Web/PropertyEditors/ValueReferences/NestedContentPropertyValueReferences.cs b/src/Umbraco.Web/PropertyEditors/ValueReferences/NestedContentPropertyValueReferences.cs deleted file mode 100644 index a872dde1c7..0000000000 --- a/src/Umbraco.Web/PropertyEditors/ValueReferences/NestedContentPropertyValueReferences.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System.Collections.Generic; -using Umbraco.Core; -using Umbraco.Core.Models.Editors; -using Umbraco.Core.PropertyEditors; -using Umbraco.Core.Services; -using static Umbraco.Web.PropertyEditors.NestedContentPropertyEditor; - -namespace Umbraco.Web.PropertyEditors.ValueReferences -{ - public class NestedContentPropertyValueReferences : IDataValueReference - { - private PropertyEditorCollection _propertyEditors; - private NestedContentValues _nestedContentValues; - - //ARGH LightInject moaning at me - public NestedContentPropertyValueReferences(PropertyEditorCollection propertyEditors, IContentTypeService contentTypeService) - { - _propertyEditors = propertyEditors; - _nestedContentValues = new NestedContentValues(contentTypeService); - } - - public IEnumerable GetReferences(object value) - { - var rawJson = value == null ? string.Empty : value is string str ? str : value.ToString(); - - var result = new List(); - - foreach (var row in _nestedContentValues.GetPropertyValues(rawJson, out _)) - { - if (row.PropType == null) continue; - - var propEditor = _propertyEditors[row.PropType.PropertyEditorAlias]; - - var valueEditor = propEditor?.GetValueEditor(); - if (!(valueEditor is IDataValueReference reference)) continue; - - var val = row.JsonRowValue[row.PropKey]?.ToString(); - - var refs = reference.GetReferences(val); - - result.AddRange(refs); - } - - return result; - } - - public bool IsForEditor(IDataEditor dataEditor) => dataEditor.Alias.InvariantEquals(Constants.PropertyEditors.Aliases.NestedContent); - } -} diff --git a/src/Umbraco.Web/PropertyEditors/ValueReferences/RichTextPropertyValueReferences.cs b/src/Umbraco.Web/PropertyEditors/ValueReferences/RichTextPropertyValueReferences.cs deleted file mode 100644 index 744b0dc27c..0000000000 --- a/src/Umbraco.Web/PropertyEditors/ValueReferences/RichTextPropertyValueReferences.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Collections.Generic; -using Umbraco.Core; -using Umbraco.Core.Models.Editors; -using Umbraco.Core.PropertyEditors; -using Umbraco.Web.Templates; - -namespace Umbraco.Web.PropertyEditors.ValueReferences -{ - public class RichTextPropertyValueReferences : IDataValueReference - { - private HtmlImageSourceParser _imageSourceParser; - private HtmlLocalLinkParser _localLinkParser; - - public RichTextPropertyValueReferences(HtmlImageSourceParser imageSourceParser, HtmlLocalLinkParser localLinkParser) - { - _imageSourceParser = imageSourceParser; - _localLinkParser = localLinkParser; - } - - /// - /// Resolve references from values - /// - /// - /// - public IEnumerable GetReferences(object value) - { - var asString = value == null ? string.Empty : value is string str ? str : value.ToString(); - - foreach (var udi in _imageSourceParser.FindUdisFromDataAttributes(asString)) - yield return new UmbracoEntityReference(udi); - - foreach (var udi in _localLinkParser.FindUdisFromLocalLinks(asString)) - yield return new UmbracoEntityReference(udi); - - //TODO: Detect Macros too ... but we can save that for a later date, right now need to do media refs - } - - public bool IsForEditor(IDataEditor dataEditor) => dataEditor.Alias.InvariantEquals(Constants.PropertyEditors.Aliases.TinyMce); - } -} diff --git a/src/Umbraco.Web/Runtime/WebInitialComposer.cs b/src/Umbraco.Web/Runtime/WebInitialComposer.cs index 295a31c461..c1eea60517 100644 --- a/src/Umbraco.Web/Runtime/WebInitialComposer.cs +++ b/src/Umbraco.Web/Runtime/WebInitialComposer.cs @@ -9,9 +9,7 @@ using Umbraco.Core.Dashboards; using Umbraco.Core.Dictionary; using Umbraco.Core.Events; using Umbraco.Core.Migrations.PostMigrations; -using Umbraco.Web.Migrations.PostMigrations; using Umbraco.Core.Models.PublishedContent; -using Umbraco.Core.PropertyEditors; using Umbraco.Core.PropertyEditors.ValueConverters; using Umbraco.Core.Runtime; using Umbraco.Core.Services; @@ -19,7 +17,6 @@ using Umbraco.Web.Actions; using Umbraco.Web.Cache; using Umbraco.Web.Composing.CompositionExtensions; using Umbraco.Web.ContentApps; -using Umbraco.Web.Dashboards; using Umbraco.Web.Dictionary; using Umbraco.Web.Editors; using Umbraco.Web.Features; @@ -37,13 +34,10 @@ using Umbraco.Web.Security.Providers; using Umbraco.Web.Services; using Umbraco.Web.SignalR; using Umbraco.Web.Templates; -using Umbraco.Web.Tour; using Umbraco.Web.Trees; using Umbraco.Web.WebApi; using Current = Umbraco.Web.Composing.Current; using Umbraco.Web.PropertyEditors; -using static Umbraco.Web.PropertyEditors.GridPropertyEditor; -using Umbraco.Web.PropertyEditors.ValueReferences; namespace Umbraco.Web.Runtime { @@ -279,19 +273,8 @@ namespace Umbraco.Web.Runtime // Used to determine if a datatype/editor should be storing/tracking // references to media item/s - composition.DataValueReferences() - .Append() - - // TODO: Unsure how to call other ValueReference in collection - // When looking at grid item cells for RTE or media found - //.Append() - .Append() - .Append() - .Append() - - // TODO: LightInject problem - //.Append() - .Append(); + composition.DataValueReferenceFors(); + //WB:TODO Try me out // replace with web implementation composition.RegisterUnique(); diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 22680898af..5eca5ad7fe 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -235,13 +235,6 @@ - - - - - - - From 958eb822137df9752a0ff49ed46a74310b66ee53 Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Mon, 2 Dec 2019 15:23:56 +0000 Subject: [PATCH 090/202] Move the GetReferences about (back to where it was) --- .../MultiUrlPickerPropertyEditor.cs | 14 +------------- .../PropertyEditors/MultiUrlPickerValueEditor.cs | 14 +++++++++++++- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/Umbraco.Web/PropertyEditors/MultiUrlPickerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MultiUrlPickerPropertyEditor.cs index e77ce3a993..8af2d98018 100644 --- a/src/Umbraco.Web/PropertyEditors/MultiUrlPickerPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MultiUrlPickerPropertyEditor.cs @@ -18,7 +18,7 @@ namespace Umbraco.Web.PropertyEditors ValueType = ValueTypes.Json, Group = Constants.PropertyEditors.Groups.Pickers, Icon = "icon-link")] - public class MultiUrlPickerPropertyEditor : DataEditor, IDataValueReference + public class MultiUrlPickerPropertyEditor : DataEditor { private readonly IEntityService _entityService; private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; @@ -29,18 +29,6 @@ namespace Umbraco.Web.PropertyEditors _publishedSnapshotAccessor = publishedSnapshotAccessor ?? throw new ArgumentNullException(nameof(publishedSnapshotAccessor)); } - public IEnumerable GetReferences(object value) - { - var asString = value == null ? string.Empty : value is string str ? str : value.ToString(); - - if (string.IsNullOrEmpty(asString)) yield break; - - var links = JsonConvert.DeserializeObject>(asString); - foreach (var link in links) - if (link.Udi != null) // Links can be absolute links without a Udi - yield return new UmbracoEntityReference(link.Udi); - } - protected override IConfigurationEditor CreateConfigurationEditor() => new MultiUrlPickerConfigurationEditor(); protected override IDataValueEditor CreateValueEditor() => new MultiUrlPickerValueEditor(_entityService, _publishedSnapshotAccessor, Logger, Attribute); diff --git a/src/Umbraco.Web/PropertyEditors/MultiUrlPickerValueEditor.cs b/src/Umbraco.Web/PropertyEditors/MultiUrlPickerValueEditor.cs index de641f69a3..ff2ac28986 100644 --- a/src/Umbraco.Web/PropertyEditors/MultiUrlPickerValueEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MultiUrlPickerValueEditor.cs @@ -15,7 +15,7 @@ using Umbraco.Web.PublishedCache; namespace Umbraco.Web.PropertyEditors { - public class MultiUrlPickerValueEditor : DataValueEditor + public class MultiUrlPickerValueEditor : DataValueEditor, IDataValueReference { private readonly IEntityService _entityService; private readonly ILogger _logger; @@ -156,6 +156,18 @@ namespace Umbraco.Web.PropertyEditors return base.FromEditor(editorValue, currentValue); } + public IEnumerable GetReferences(object value) + { + var asString = value == null ? string.Empty : value is string str ? str : value.ToString(); + + if (string.IsNullOrEmpty(asString)) yield break; + + var links = JsonConvert.DeserializeObject>(asString); + foreach (var link in links) + if (link.Udi != null) // Links can be absolute links without a Udi + yield return new UmbracoEntityReference(link.Udi); + } + [DataContract] internal class LinkDto { From 1b84051e8f19bc0135a2d74f8c12ebee534114aa Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Mon, 2 Dec 2019 15:24:50 +0000 Subject: [PATCH 091/202] Move DataValueReferceFors collection from Umbraco.Web to Umbraco.Core composer in order to see if that helps the Unit Test LightInject issue --- src/Umbraco.Core/Runtime/CoreInitialComposer.cs | 4 ++++ src/Umbraco.Web/Runtime/WebInitialComposer.cs | 6 +----- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Core/Runtime/CoreInitialComposer.cs b/src/Umbraco.Core/Runtime/CoreInitialComposer.cs index 94d2b68121..be8a920988 100644 --- a/src/Umbraco.Core/Runtime/CoreInitialComposer.cs +++ b/src/Umbraco.Core/Runtime/CoreInitialComposer.cs @@ -75,6 +75,10 @@ namespace Umbraco.Core.Runtime composition.RegisterUnique(); composition.RegisterUnique(); + // Used to determine if a datatype/editor should be storing/tracking + // references to media item/s + composition.DataValueReferenceFors(); + //WB:TODO Try me out & overide/add to an existing list // register a server registrar, by default it's the db registrar composition.RegisterUnique(f => diff --git a/src/Umbraco.Web/Runtime/WebInitialComposer.cs b/src/Umbraco.Web/Runtime/WebInitialComposer.cs index c1eea60517..28f365fc60 100644 --- a/src/Umbraco.Web/Runtime/WebInitialComposer.cs +++ b/src/Umbraco.Web/Runtime/WebInitialComposer.cs @@ -177,7 +177,7 @@ namespace Umbraco.Web.Runtime .Remove() .Remove() .Remove(); - + // add all known factories, devs can then modify this list on application // startup either by binding to events or in their own global.asax composition.FilteredControllerFactory() @@ -271,10 +271,6 @@ namespace Umbraco.Web.Runtime .Append() .Append(); - // Used to determine if a datatype/editor should be storing/tracking - // references to media item/s - composition.DataValueReferenceFors(); - //WB:TODO Try me out // replace with web implementation composition.RegisterUnique(); From e1e5ac44cc8d2e147d971da7176b514dd70bd38f Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Mon, 2 Dec 2019 15:35:13 +0000 Subject: [PATCH 092/202] Something gone funky/awol with my commits - remove dupe code :S --- .../PropertyEditors/MultiUrlPickerValueEditor.cs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/Umbraco.Web/PropertyEditors/MultiUrlPickerValueEditor.cs b/src/Umbraco.Web/PropertyEditors/MultiUrlPickerValueEditor.cs index ff2ac28986..853e995ed8 100644 --- a/src/Umbraco.Web/PropertyEditors/MultiUrlPickerValueEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MultiUrlPickerValueEditor.cs @@ -156,18 +156,6 @@ namespace Umbraco.Web.PropertyEditors return base.FromEditor(editorValue, currentValue); } - public IEnumerable GetReferences(object value) - { - var asString = value == null ? string.Empty : value is string str ? str : value.ToString(); - - if (string.IsNullOrEmpty(asString)) yield break; - - var links = JsonConvert.DeserializeObject>(asString); - foreach (var link in links) - if (link.Udi != null) // Links can be absolute links without a Udi - yield return new UmbracoEntityReference(link.Udi); - } - [DataContract] internal class LinkDto { From 19497ee7857d544552d57068d4431328f882d4f8 Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Tue, 3 Dec 2019 11:52:46 +0000 Subject: [PATCH 093/202] Fix up unit test - needed to add the collection to the Compositon for all tests inheriting from UmbracoTestBase --- src/Umbraco.Tests/Testing/UmbracoTestBase.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Umbraco.Tests/Testing/UmbracoTestBase.cs b/src/Umbraco.Tests/Testing/UmbracoTestBase.cs index ffc49b47ac..a444e4b81d 100644 --- a/src/Umbraco.Tests/Testing/UmbracoTestBase.cs +++ b/src/Umbraco.Tests/Testing/UmbracoTestBase.cs @@ -217,6 +217,9 @@ namespace Umbraco.Tests.Testing Composition.RegisterUnique(_ => Umbraco.Web.Composing.Current.UmbracoContextAccessor); Composition.RegisterUnique(); Composition.WithCollectionBuilder(); + + Composition.DataValueReferenceFors(); + Composition.RegisterUnique(); Composition.RegisterUnique(); Composition.RegisterUnique(); From 9e1a56eba59a2650905e955fbbb5f9d976ebacf6 Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Wed, 4 Dec 2019 16:14:33 +0000 Subject: [PATCH 094/202] Rename from DataValueReferenceFor to DataValyeReferenceFactory & Factories for the plural collection --- src/Umbraco.Core/Composing/Current.cs | 4 ++-- src/Umbraco.Core/CompositionExtensions.cs | 6 +++--- .../Repositories/Implement/ContentRepositoryBase.cs | 8 ++++---- .../Implement/DocumentBlueprintRepository.cs | 4 ++-- .../Repositories/Implement/DocumentRepository.cs | 4 ++-- .../Repositories/Implement/MediaRepository.cs | 4 ++-- .../Repositories/Implement/MemberRepository.cs | 4 ++-- .../DataValueReferenceFactoryCollection.cs | 12 ++++++++++++ .../DataValueReferenceFactoryCollectionBuilder.cs | 9 +++++++++ .../DataValueReferenceForCollection.cs | 12 ------------ .../DataValueReferenceForCollectionBuilder.cs | 9 --------- ...ReferenceFor.cs => IDataValueReferenceFactory.cs} | 2 +- src/Umbraco.Core/Runtime/CoreInitialComposer.cs | 3 +-- src/Umbraco.Core/Umbraco.Core.csproj | 6 +++--- .../Repositories/ContentTypeRepositoryTest.cs | 2 +- .../Repositories/DocumentRepositoryTest.cs | 2 +- .../Persistence/Repositories/DomainRepositoryTest.cs | 2 +- .../Persistence/Repositories/MediaRepositoryTest.cs | 2 +- .../Persistence/Repositories/MemberRepositoryTest.cs | 2 +- .../Repositories/PublicAccessRepositoryTest.cs | 2 +- .../Persistence/Repositories/TagRepositoryTest.cs | 4 ++-- .../Repositories/TemplateRepositoryTest.cs | 2 +- .../Persistence/Repositories/UserRepositoryTest.cs | 4 ++-- .../Services/ContentServicePerformanceTest.cs | 2 +- src/Umbraco.Tests/Services/ContentServiceTests.cs | 2 +- src/Umbraco.Tests/Testing/UmbracoTestBase.cs | 2 +- src/Umbraco.Web/Composing/Current.cs | 2 +- 27 files changed, 58 insertions(+), 59 deletions(-) create mode 100644 src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollection.cs create mode 100644 src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollectionBuilder.cs delete mode 100644 src/Umbraco.Core/PropertyEditors/DataValueReferenceForCollection.cs delete mode 100644 src/Umbraco.Core/PropertyEditors/DataValueReferenceForCollectionBuilder.cs rename src/Umbraco.Core/PropertyEditors/{IDataValueReferenceFor.cs => IDataValueReferenceFactory.cs} (92%) diff --git a/src/Umbraco.Core/Composing/Current.cs b/src/Umbraco.Core/Composing/Current.cs index 899c465ca7..846331a16e 100644 --- a/src/Umbraco.Core/Composing/Current.cs +++ b/src/Umbraco.Core/Composing/Current.cs @@ -154,8 +154,8 @@ namespace Umbraco.Core.Composing public static DataEditorCollection DataEditors => Factory.GetInstance(); - public static DataValueReferenceForCollection DataValueReferenceFors - => Factory.GetInstance(); + public static DataValueReferenceFactoryCollection DataValueReferenceFactories + => Factory.GetInstance(); public static PropertyEditorCollection PropertyEditors => Factory.GetInstance(); diff --git a/src/Umbraco.Core/CompositionExtensions.cs b/src/Umbraco.Core/CompositionExtensions.cs index aee4b41be8..ced9a9386a 100644 --- a/src/Umbraco.Core/CompositionExtensions.cs +++ b/src/Umbraco.Core/CompositionExtensions.cs @@ -50,11 +50,11 @@ namespace Umbraco.Core => composition.WithCollectionBuilder(); /// - /// Gets the data value reference collection builder. + /// Gets the data value reference factory collection builder. /// /// The composition. - public static DataValueReferenceForCollectionBuilder DataValueReferenceFors(this Composition composition) - => composition.WithCollectionBuilder(); + public static DataValueReferenceFactoryCollectionBuilder DataValueReferenceFactories(this Composition composition) + => composition.WithCollectionBuilder(); /// /// Gets the property value converters collection builder. diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs index 77d36b8d43..9bd1520b51 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs @@ -36,7 +36,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement where TRepository : class, IRepository { private readonly Lazy _propertyEditors; - private readonly DataValueReferenceForCollection _dataValueReferenceFors; + private readonly DataValueReferenceFactoryCollection _dataValueReferenceFactories; /// /// @@ -50,14 +50,14 @@ namespace Umbraco.Core.Persistence.Repositories.Implement /// protected ContentRepositoryBase(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, ILanguageRepository languageRepository, IRelationRepository relationRepository, IRelationTypeRepository relationTypeRepository, - Lazy propertyEditors, DataValueReferenceForCollection dataValueReferenceFors) + Lazy propertyEditors, DataValueReferenceFactoryCollection dataValueReferenceFactories) : base(scopeAccessor, cache, logger) { LanguageRepository = languageRepository; RelationRepository = relationRepository; RelationTypeRepository = relationTypeRepository; _propertyEditors = propertyEditors; - _dataValueReferenceFors = dataValueReferenceFors; + _dataValueReferenceFactories = dataValueReferenceFactories; } protected abstract TRepository This { get; } @@ -843,7 +843,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // implementation of GetReferences in IDataValueReference. // Allows developers to add support for references by a // package /property editor that did not implement IDataValueReference themselves - foreach (var item in _dataValueReferenceFors) + foreach (var item in _dataValueReferenceFactories) { // Check if this value reference is for this datatype/editor // Then call it's GetReferences method - to see if the value stored diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentBlueprintRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentBlueprintRepository.cs index e3fba3db01..e150b2e632 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentBlueprintRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentBlueprintRepository.cs @@ -20,8 +20,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement { public DocumentBlueprintRepository(IScopeAccessor scopeAccessor, AppCaches appCaches, ILogger logger, IContentTypeRepository contentTypeRepository, ITemplateRepository templateRepository, ITagRepository tagRepository, ILanguageRepository languageRepository, IRelationRepository relationRepository, IRelationTypeRepository relationTypeRepository, - Lazy propertyEditorCollection, DataValueReferenceForCollection dataValueReferenceFors) - : base(scopeAccessor, appCaches, logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditorCollection, dataValueReferenceFors) + Lazy propertyEditorCollection, DataValueReferenceFactoryCollection dataValueReferenceFactories) + : base(scopeAccessor, appCaches, logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditorCollection, dataValueReferenceFactories) { } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs index 8b63b93f16..35e54a39e0 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs @@ -46,8 +46,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement /// public DocumentRepository(IScopeAccessor scopeAccessor, AppCaches appCaches, ILogger logger, IContentTypeRepository contentTypeRepository, ITemplateRepository templateRepository, ITagRepository tagRepository, ILanguageRepository languageRepository, IRelationRepository relationRepository, IRelationTypeRepository relationTypeRepository, - Lazy propertyEditors, DataValueReferenceForCollection dataValueReferenceFors) - : base(scopeAccessor, appCaches, logger, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferenceFors) + Lazy propertyEditors, DataValueReferenceFactoryCollection dataValueReferenceFactories) + : base(scopeAccessor, appCaches, logger, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferenceFactories) { _contentTypeRepository = contentTypeRepository ?? throw new ArgumentNullException(nameof(contentTypeRepository)); _templateRepository = templateRepository ?? throw new ArgumentNullException(nameof(templateRepository)); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs index da124db77f..ad47c7af6b 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs @@ -29,8 +29,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement private readonly MediaByGuidReadRepository _mediaByGuidReadRepository; public MediaRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IMediaTypeRepository mediaTypeRepository, ITagRepository tagRepository, ILanguageRepository languageRepository, IRelationRepository relationRepository, IRelationTypeRepository relationTypeRepository, - Lazy propertyEditorCollection, DataValueReferenceForCollection dataValueReferenceFors) - : base(scopeAccessor, cache, logger, languageRepository, relationRepository, relationTypeRepository, propertyEditorCollection, dataValueReferenceFors) + Lazy propertyEditorCollection, DataValueReferenceFactoryCollection dataValueReferenceFactories) + : base(scopeAccessor, cache, logger, languageRepository, relationRepository, relationTypeRepository, propertyEditorCollection, dataValueReferenceFactories) { _mediaTypeRepository = mediaTypeRepository ?? throw new ArgumentNullException(nameof(mediaTypeRepository)); _tagRepository = tagRepository ?? throw new ArgumentNullException(nameof(tagRepository)); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs index 06d93dfe50..42e7d1c32f 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs @@ -28,8 +28,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement public MemberRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IMemberTypeRepository memberTypeRepository, IMemberGroupRepository memberGroupRepository, ITagRepository tagRepository, ILanguageRepository languageRepository, IRelationRepository relationRepository, IRelationTypeRepository relationTypeRepository, - Lazy propertyEditors, DataValueReferenceForCollection dataValueReferenceFors) - : base(scopeAccessor, cache, logger, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferenceFors) + Lazy propertyEditors, DataValueReferenceFactoryCollection dataValueReferenceFactories) + : base(scopeAccessor, cache, logger, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferenceFactories) { _memberTypeRepository = memberTypeRepository ?? throw new ArgumentNullException(nameof(memberTypeRepository)); _tagRepository = tagRepository ?? throw new ArgumentNullException(nameof(tagRepository)); diff --git a/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollection.cs b/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollection.cs new file mode 100644 index 0000000000..6b6607e1f9 --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollection.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using Umbraco.Core.Composing; + +namespace Umbraco.Core.PropertyEditors +{ + public class DataValueReferenceFactoryCollection : BuilderCollectionBase + { + public DataValueReferenceFactoryCollection(IEnumerable items) + : base(items) + { } + } +} diff --git a/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollectionBuilder.cs b/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollectionBuilder.cs new file mode 100644 index 0000000000..f12071c492 --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollectionBuilder.cs @@ -0,0 +1,9 @@ +using Umbraco.Core.Composing; + +namespace Umbraco.Core.PropertyEditors +{ + public class DataValueReferenceFactoryCollectionBuilder : OrderedCollectionBuilderBase + { + protected override DataValueReferenceFactoryCollectionBuilder This => this; + } +} diff --git a/src/Umbraco.Core/PropertyEditors/DataValueReferenceForCollection.cs b/src/Umbraco.Core/PropertyEditors/DataValueReferenceForCollection.cs deleted file mode 100644 index c5b9470868..0000000000 --- a/src/Umbraco.Core/PropertyEditors/DataValueReferenceForCollection.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Collections.Generic; -using Umbraco.Core.Composing; - -namespace Umbraco.Core.PropertyEditors -{ - public class DataValueReferenceForCollection : BuilderCollectionBase - { - public DataValueReferenceForCollection(IEnumerable items) - : base(items) - { } - } -} diff --git a/src/Umbraco.Core/PropertyEditors/DataValueReferenceForCollectionBuilder.cs b/src/Umbraco.Core/PropertyEditors/DataValueReferenceForCollectionBuilder.cs deleted file mode 100644 index a7b03ea482..0000000000 --- a/src/Umbraco.Core/PropertyEditors/DataValueReferenceForCollectionBuilder.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Umbraco.Core.Composing; - -namespace Umbraco.Core.PropertyEditors -{ - public class DataValueReferenceForCollectionBuilder : OrderedCollectionBuilderBase - { - protected override DataValueReferenceForCollectionBuilder This => this; - } -} diff --git a/src/Umbraco.Core/PropertyEditors/IDataValueReferenceFor.cs b/src/Umbraco.Core/PropertyEditors/IDataValueReferenceFactory.cs similarity index 92% rename from src/Umbraco.Core/PropertyEditors/IDataValueReferenceFor.cs rename to src/Umbraco.Core/PropertyEditors/IDataValueReferenceFactory.cs index e0d5e4bad1..6587e071bf 100644 --- a/src/Umbraco.Core/PropertyEditors/IDataValueReferenceFor.cs +++ b/src/Umbraco.Core/PropertyEditors/IDataValueReferenceFactory.cs @@ -1,6 +1,6 @@ namespace Umbraco.Core.PropertyEditors { - public interface IDataValueReferenceFor + public interface IDataValueReferenceFactory { /// /// Gets a value indicating whether the DataValueReference lookup supports a datatype (data editor). diff --git a/src/Umbraco.Core/Runtime/CoreInitialComposer.cs b/src/Umbraco.Core/Runtime/CoreInitialComposer.cs index be8a920988..d95ada499b 100644 --- a/src/Umbraco.Core/Runtime/CoreInitialComposer.cs +++ b/src/Umbraco.Core/Runtime/CoreInitialComposer.cs @@ -77,8 +77,7 @@ namespace Umbraco.Core.Runtime // Used to determine if a datatype/editor should be storing/tracking // references to media item/s - composition.DataValueReferenceFors(); - //WB:TODO Try me out & overide/add to an existing list + composition.DataValueReferenceFactories(); // register a server registrar, by default it's the db registrar composition.RegisterUnique(f => diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 98871fbf5c..306b764787 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -281,10 +281,10 @@ - - + + - + diff --git a/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs index 9b99aa9b4c..f7f3adf0a0 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs @@ -40,7 +40,7 @@ namespace Umbraco.Tests.Persistence.Repositories var entityRepository = new EntityRepository(scopeAccessor); var relationRepository = new RelationRepository(scopeAccessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var dataValueReferences = new DataValueReferenceForCollection(Enumerable.Empty()); + var dataValueReferences = new DataValueReferenceFactoryCollection(Enumerable.Empty()); var repository = new DocumentRepository(scopeAccessor, AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferences); return repository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/DocumentRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/DocumentRepositoryTest.cs index 42d2d13c6e..291525ba13 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/DocumentRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/DocumentRepositoryTest.cs @@ -73,7 +73,7 @@ namespace Umbraco.Tests.Persistence.Repositories var entityRepository = new EntityRepository(scopeAccessor); var relationRepository = new RelationRepository(scopeAccessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var dataValueReferences = new DataValueReferenceForCollection(Enumerable.Empty()); + var dataValueReferences = new DataValueReferenceFactoryCollection(Enumerable.Empty()); var repository = new DocumentRepository(scopeAccessor, appCaches, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferences); return repository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/DomainRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/DomainRepositoryTest.cs index 6d78601bba..56138faea9 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/DomainRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/DomainRepositoryTest.cs @@ -31,7 +31,7 @@ namespace Umbraco.Tests.Persistence.Repositories var entityRepository = new EntityRepository(accessor); var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var dataValueReferences = new DataValueReferenceForCollection(Enumerable.Empty()); + var dataValueReferences = new DataValueReferenceFactoryCollection(Enumerable.Empty()); documentRepository = new DocumentRepository(accessor, Core.Cache.AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferences); var domainRepository = new DomainRepository(accessor, Core.Cache.AppCaches.Disabled, Logger); return domainRepository; diff --git a/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs index 886d0c5ecf..ef436a3489 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs @@ -46,7 +46,7 @@ namespace Umbraco.Tests.Persistence.Repositories var entityRepository = new EntityRepository(scopeAccessor); var relationRepository = new RelationRepository(scopeAccessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var dataValueReferences = new DataValueReferenceForCollection(Enumerable.Empty()); + var dataValueReferences = new DataValueReferenceFactoryCollection(Enumerable.Empty()); var repository = new MediaRepository(scopeAccessor, appCaches, Logger, mediaTypeRepository, tagRepository, Mock.Of(), relationRepository, relationTypeRepository, propertyEditors, dataValueReferences); return repository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs index d3150623c2..060478d64b 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs @@ -40,7 +40,7 @@ namespace Umbraco.Tests.Persistence.Repositories var entityRepository = new EntityRepository(accessor); var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var dataValueReferences = new DataValueReferenceForCollection(Enumerable.Empty()); + var dataValueReferences = new DataValueReferenceFactoryCollection(Enumerable.Empty()); var repository = new MemberRepository(accessor, AppCaches.Disabled, Logger, memberTypeRepository, memberGroupRepository, tagRepo, Mock.Of(), relationRepository, relationTypeRepository, propertyEditors, dataValueReferences); return repository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/PublicAccessRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/PublicAccessRepositoryTest.cs index 89f7c39abc..4cf440c369 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/PublicAccessRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/PublicAccessRepositoryTest.cs @@ -315,7 +315,7 @@ namespace Umbraco.Tests.Persistence.Repositories var entityRepository = new EntityRepository(accessor); var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var dataValueReferences = new DataValueReferenceForCollection(Enumerable.Empty()); + var dataValueReferences = new DataValueReferenceFactoryCollection(Enumerable.Empty()); var repository = new DocumentRepository(accessor, AppCaches, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferences); return repository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/TagRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/TagRepositoryTest.cs index a8a263043d..2c15e91e61 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/TagRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/TagRepositoryTest.cs @@ -964,7 +964,7 @@ namespace Umbraco.Tests.Persistence.Repositories var entityRepository = new EntityRepository(accessor); var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var dataValueReferences = new DataValueReferenceForCollection(Enumerable.Empty()); + var dataValueReferences = new DataValueReferenceFactoryCollection(Enumerable.Empty()); var repository = new DocumentRepository(accessor, AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferences); return repository; } @@ -981,7 +981,7 @@ namespace Umbraco.Tests.Persistence.Repositories var entityRepository = new EntityRepository(accessor); var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var dataValueReferences = new DataValueReferenceForCollection(Enumerable.Empty()); + var dataValueReferences = new DataValueReferenceFactoryCollection(Enumerable.Empty()); var repository = new MediaRepository(accessor, AppCaches.Disabled, Logger, mediaTypeRepository, tagRepository, Mock.Of(), relationRepository, relationTypeRepository, propertyEditors, dataValueReferences); return repository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/TemplateRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/TemplateRepositoryTest.cs index 7caf0ef934..200447e30a 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/TemplateRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/TemplateRepositoryTest.cs @@ -246,7 +246,7 @@ namespace Umbraco.Tests.Persistence.Repositories var entityRepository = new EntityRepository(ScopeProvider); var relationRepository = new RelationRepository(ScopeProvider, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var dataValueReferences = new DataValueReferenceForCollection(Enumerable.Empty()); + var dataValueReferences = new DataValueReferenceFactoryCollection(Enumerable.Empty()); var contentRepo = new DocumentRepository(ScopeProvider, AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferences); var contentType = MockedContentTypes.CreateSimpleContentType("umbTextpage2", "Textpage"); diff --git a/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs index b632699bfd..3ba00e54cf 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs @@ -35,7 +35,7 @@ namespace Umbraco.Tests.Persistence.Repositories var entityRepository = new EntityRepository(accessor); var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var dataValueReferences = new DataValueReferenceForCollection(Enumerable.Empty()); + var dataValueReferences = new DataValueReferenceFactoryCollection(Enumerable.Empty()); var repository = new MediaRepository(accessor, AppCaches, Mock.Of(), mediaTypeRepository, tagRepository, Mock.Of(), relationRepository, relationTypeRepository, propertyEditors, dataValueReferences); return repository; } @@ -58,7 +58,7 @@ namespace Umbraco.Tests.Persistence.Repositories var entityRepository = new EntityRepository(accessor); var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var dataValueReferences = new DataValueReferenceForCollection(Enumerable.Empty()); + var dataValueReferences = new DataValueReferenceFactoryCollection(Enumerable.Empty()); var repository = new DocumentRepository(accessor, AppCaches, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferences); return repository; } diff --git a/src/Umbraco.Tests/Services/ContentServicePerformanceTest.cs b/src/Umbraco.Tests/Services/ContentServicePerformanceTest.cs index dd561cb3a8..9f4304ebee 100644 --- a/src/Umbraco.Tests/Services/ContentServicePerformanceTest.cs +++ b/src/Umbraco.Tests/Services/ContentServicePerformanceTest.cs @@ -52,7 +52,7 @@ namespace Umbraco.Tests.Services var entityRepository = new EntityRepository(accessor); var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var dataValueReferences = new DataValueReferenceForCollection(Enumerable.Empty()); + var dataValueReferences = new DataValueReferenceFactoryCollection(Enumerable.Empty()); var repository = new DocumentRepository(accessor, AppCaches.Disabled, Logger, ctRepository, tRepository, tagRepo, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferences); return repository; } diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs index 70c51c5baa..92d2a68472 100644 --- a/src/Umbraco.Tests/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs @@ -3211,7 +3211,7 @@ namespace Umbraco.Tests.Services var entityRepository = new EntityRepository(accessor); var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var dataValueReferences = new DataValueReferenceForCollection(Enumerable.Empty()); + var dataValueReferences = new DataValueReferenceFactoryCollection(Enumerable.Empty()); var repository = new DocumentRepository(accessor, AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferences); return repository; } diff --git a/src/Umbraco.Tests/Testing/UmbracoTestBase.cs b/src/Umbraco.Tests/Testing/UmbracoTestBase.cs index a444e4b81d..4f7de2d230 100644 --- a/src/Umbraco.Tests/Testing/UmbracoTestBase.cs +++ b/src/Umbraco.Tests/Testing/UmbracoTestBase.cs @@ -218,7 +218,7 @@ namespace Umbraco.Tests.Testing Composition.RegisterUnique(); Composition.WithCollectionBuilder(); - Composition.DataValueReferenceFors(); + Composition.DataValueReferenceFactories(); Composition.RegisterUnique(); Composition.RegisterUnique(); diff --git a/src/Umbraco.Web/Composing/Current.cs b/src/Umbraco.Web/Composing/Current.cs index 2dd82d9a4a..ec65046e84 100644 --- a/src/Umbraco.Web/Composing/Current.cs +++ b/src/Umbraco.Web/Composing/Current.cs @@ -182,7 +182,7 @@ namespace Umbraco.Web.Composing public static DataEditorCollection DataEditors => CoreCurrent.DataEditors; - public static DataValueReferenceForCollection DataValueReferenceFors => CoreCurrent.DataValueReferenceFors; + public static DataValueReferenceFactoryCollection DataValueReferenceFactories => CoreCurrent.DataValueReferenceFactories; public static PropertyEditorCollection PropertyEditors => CoreCurrent.PropertyEditors; From cad384a0a6b5e1c8e0bb967d3560663ddc0f43c9 Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Thu, 5 Dec 2019 11:18:18 +0000 Subject: [PATCH 095/202] Moves logic out from ContentRepositoryBase & into a method on the Collection itself --- .../Implement/ContentRepositoryBase.cs | 31 ++------------ .../DataValueReferenceFactoryCollection.cs | 40 +++++++++++++++++++ ...aValueReferenceFactoryCollectionBuilder.cs | 2 +- 3 files changed, 44 insertions(+), 29 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs index 9bd1520b51..13b687eb4e 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs @@ -823,35 +823,10 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected void PersistRelations(TEntity entity) { + // Get all references from our core built in DataEditors/Property Editors + // Along with seeing if deverlopers want to collect additional references from the DataValueReferenceFactories collection var trackedRelations = new List(); - - foreach (var p in entity.Properties) - { - if (!PropertyEditors.TryGet(p.PropertyType.PropertyEditorAlias, out var editor)) continue; - var valueEditor = editor.GetValueEditor(); - if (!(valueEditor is IDataValueReference reference)) continue; - - //TODO: Support variants/segments! This is not required for this initial prototype which is why there is a check here - if (!p.PropertyType.VariesByNothing()) continue; - - var val = p.GetValue(); // get the invariant value - var refs = reference.GetReferences(val); - trackedRelations.AddRange(refs); - - - // Loop over collection that may be add to existing property editors - // implementation of GetReferences in IDataValueReference. - // Allows developers to add support for references by a - // package /property editor that did not implement IDataValueReference themselves - foreach (var item in _dataValueReferenceFactories) - { - // Check if this value reference is for this datatype/editor - // Then call it's GetReferences method - to see if the value stored - // in the dataeditor/property has referecnes to media items - if (item.IsForEditor(editor)) - trackedRelations.AddRange(item.GetDataValueReference().GetReferences(val)); - } - } + trackedRelations.AddRange(_dataValueReferenceFactories.GetAllReferences(entity.Properties, PropertyEditors)); //First delete all auto-relations for this entity RelationRepository.DeleteByParent(entity.Id, Constants.Conventions.RelationTypes.AutomaticRelationTypes); diff --git a/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollection.cs b/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollection.cs index 6b6607e1f9..c1dbfbaca4 100644 --- a/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollection.cs +++ b/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollection.cs @@ -1,5 +1,7 @@ using System.Collections.Generic; using Umbraco.Core.Composing; +using Umbraco.Core.Models; +using Umbraco.Core.Models.Editors; namespace Umbraco.Core.PropertyEditors { @@ -8,5 +10,43 @@ namespace Umbraco.Core.PropertyEditors public DataValueReferenceFactoryCollection(IEnumerable items) : base(items) { } + + public IEnumerable GetAllReferences(PropertyCollection properties, PropertyEditorCollection propertyEditors) + { + var trackedRelations = new List(); + + foreach (var p in properties) + { + // Injecting propertyEditorCollection is causing LightInject to throw a Recursive Dependancy error + // Hence using Current.PropertyEditors + if (!propertyEditors.TryGet(p.PropertyType.PropertyEditorAlias, out var editor)) continue; + + //TODO: Support variants/segments! This is not required for this initial prototype which is why there is a check here + if (!p.PropertyType.VariesByNothing()) continue; + var val = p.GetValue(); // get the invariant value + + var valueEditor = editor.GetValueEditor(); + if (valueEditor is IDataValueReference reference) + { + var refs = reference.GetReferences(val); + trackedRelations.AddRange(refs); + } + + // Loop over collection that may be add to existing property editors + // implementation of GetReferences in IDataValueReference. + // Allows developers to add support for references by a + // package /property editor that did not implement IDataValueReference themselves + foreach (var item in this) + { + // Check if this value reference is for this datatype/editor + // Then call it's GetReferences method - to see if the value stored + // in the dataeditor/property has referecnes to media/content items + if (item.IsForEditor(editor)) + trackedRelations.AddRange(item.GetDataValueReference().GetReferences(val)); + } + } + + return trackedRelations; + } } } diff --git a/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollectionBuilder.cs b/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollectionBuilder.cs index f12071c492..2cf76712c8 100644 --- a/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollectionBuilder.cs +++ b/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollectionBuilder.cs @@ -4,6 +4,6 @@ namespace Umbraco.Core.PropertyEditors { public class DataValueReferenceFactoryCollectionBuilder : OrderedCollectionBuilderBase { - protected override DataValueReferenceFactoryCollectionBuilder This => this; + protected override DataValueReferenceFactoryCollectionBuilder This => this; } } From 4d78a2c848d7399ddaa13257a7d13526205f5b31 Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Thu, 5 Dec 2019 12:48:41 +0000 Subject: [PATCH 096/202] Remove old & unecessary comment --- .../PropertyEditors/DataValueReferenceFactoryCollection.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollection.cs b/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollection.cs index c1dbfbaca4..386ab6a8f3 100644 --- a/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollection.cs +++ b/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollection.cs @@ -17,8 +17,6 @@ namespace Umbraco.Core.PropertyEditors foreach (var p in properties) { - // Injecting propertyEditorCollection is causing LightInject to throw a Recursive Dependancy error - // Hence using Current.PropertyEditors if (!propertyEditors.TryGet(p.PropertyType.PropertyEditorAlias, out var editor)) continue; //TODO: Support variants/segments! This is not required for this initial prototype which is why there is a check here From 1513a1254902dd755a2876321b8d50bbcf5ced48 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 10 Dec 2019 13:33:59 +0100 Subject: [PATCH 097/202] Introduce a new IMainDomLock and both default and sql implementations --- .../Migrations/Install/DatabaseDataCreator.cs | 2 + .../Migrations/Upgrade/UmbracoPlan.cs | 1 + .../Upgrade/V_8_6_0/AddMainDomLock.cs | 16 ++ ...AddPropertyTypeValidationMessageColumns.cs | 1 + .../Persistence/Constants-Locks.cs | 5 + .../SqlSyntax/SqlServerSyntaxProvider.cs | 7 +- src/Umbraco.Core/Runtime/CoreRuntime.cs | 2 +- src/Umbraco.Core/{ => Runtime}/IMainDom.cs | 1 + src/Umbraco.Core/Runtime/IMainDomLock.cs | 30 +++ src/Umbraco.Core/{ => Runtime}/MainDom.cs | 97 ++++------ .../Runtime/MainDomSemaphoreLock.cs | 92 +++++++++ src/Umbraco.Core/Runtime/SqlMainDomLock.cs | 178 ++++++++++++++++++ src/Umbraco.Core/Scoping/IScopeProvider.cs | 2 +- src/Umbraco.Core/Umbraco.Core.csproj | 8 +- .../XmlPublishedSnapshotService.cs | 1 + .../LegacyXmlPublishedCache/XmlStore.cs | 1 + 16 files changed, 375 insertions(+), 69 deletions(-) create mode 100644 src/Umbraco.Core/Migrations/Upgrade/V_8_6_0/AddMainDomLock.cs rename src/Umbraco.Core/{ => Runtime}/IMainDom.cs (96%) create mode 100644 src/Umbraco.Core/Runtime/IMainDomLock.cs rename src/Umbraco.Core/{ => Runtime}/MainDom.cs (77%) create mode 100644 src/Umbraco.Core/Runtime/MainDomSemaphoreLock.cs create mode 100644 src/Umbraco.Core/Runtime/SqlMainDomLock.cs diff --git a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs index 94d8cfbc62..f6daf180b7 100644 --- a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs +++ b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs @@ -151,6 +151,8 @@ namespace Umbraco.Core.Migrations.Install _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Constants.Locks.Domains, Name = "Domains" }); _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Constants.Locks.KeyValues, Name = "KeyValues" }); _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Constants.Locks.Languages, Name = "Languages" }); + + _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Constants.Locks.MainDom, Name = "MainDom" }); } private void CreateContentTypeData() diff --git a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs index 223603be14..6164f828f0 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs @@ -185,6 +185,7 @@ namespace Umbraco.Core.Migrations.Upgrade // to 8.6.0 To("{3D67D2C8-5E65-47D0-A9E1-DC2EE0779D6B}"); + To("{2AB29964-02A1-474D-BD6B-72148D2A53A2}"); //FINAL } diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_6_0/AddMainDomLock.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_6_0/AddMainDomLock.cs new file mode 100644 index 0000000000..6ca493ac7e --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_6_0/AddMainDomLock.cs @@ -0,0 +1,16 @@ +using Umbraco.Core.Persistence.Dtos; + +namespace Umbraco.Core.Migrations.Upgrade.V_8_6_0 +{ + public class AddMainDomLock : MigrationBase + { + public AddMainDomLock(IMigrationContext context) + : base(context) + { } + + public override void Migrate() + { + Database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Constants.Locks.MainDom, Name = "MainDom" }); + } + } +} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_6_0/AddPropertyTypeValidationMessageColumns.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_6_0/AddPropertyTypeValidationMessageColumns.cs index 30eb30109e..f44695da69 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_6_0/AddPropertyTypeValidationMessageColumns.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_6_0/AddPropertyTypeValidationMessageColumns.cs @@ -3,6 +3,7 @@ using Umbraco.Core.Persistence.Dtos; namespace Umbraco.Core.Migrations.Upgrade.V_8_6_0 { + public class AddPropertyTypeValidationMessageColumns : MigrationBase { public AddPropertyTypeValidationMessageColumns(IMigrationContext context) diff --git a/src/Umbraco.Core/Persistence/Constants-Locks.cs b/src/Umbraco.Core/Persistence/Constants-Locks.cs index 1dcd2408e7..e64f40ced7 100644 --- a/src/Umbraco.Core/Persistence/Constants-Locks.cs +++ b/src/Umbraco.Core/Persistence/Constants-Locks.cs @@ -8,6 +8,11 @@ namespace Umbraco.Core /// public static class Locks { + /// + /// The lock + /// + public const int MainDom = -1000; + /// /// All servers. /// diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs b/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs index 3d0adf175e..6dda49cd5e 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs @@ -250,6 +250,11 @@ where tbl.[name]=@0 and col.[name]=@1;", tableName, columnName) } public override void WriteLock(IDatabase db, params int[] lockIds) + { + WriteLock(db, 1800, lockIds); + } + + public void WriteLock(IDatabase db, int millisecondsTimeout, params int[] lockIds) { // soon as we get Database, a transaction is started @@ -260,7 +265,7 @@ where tbl.[name]=@0 and col.[name]=@1;", tableName, columnName) // *not* using a unique 'WHERE IN' query here because the *order* of lockIds is important to avoid deadlocks foreach (var lockId in lockIds) { - db.Execute(@"SET LOCK_TIMEOUT 1800;"); + db.Execute($"SET LOCK_TIMEOUT {millisecondsTimeout};"); var i = db.Execute(@"UPDATE umbracoLock WITH (REPEATABLEREAD) SET value = (CASE WHEN (value=1) THEN -1 ELSE 1 END) WHERE id=@id", new { id = lockId }); if (i == 0) // ensure we are actually locking! throw new ArgumentException($"LockObject with id={lockId} does not exist."); diff --git a/src/Umbraco.Core/Runtime/CoreRuntime.cs b/src/Umbraco.Core/Runtime/CoreRuntime.cs index 50653edc7c..49d7658647 100644 --- a/src/Umbraco.Core/Runtime/CoreRuntime.cs +++ b/src/Umbraco.Core/Runtime/CoreRuntime.cs @@ -147,7 +147,7 @@ namespace Umbraco.Core.Runtime // TODO: remove this in netcore, this is purely backwards compat hacks with the empty ctor if (MainDom == null) { - MainDom = new MainDom(Logger); + MainDom = new MainDom(Logger, new MainDomSemaphoreLock()); } diff --git a/src/Umbraco.Core/IMainDom.cs b/src/Umbraco.Core/Runtime/IMainDom.cs similarity index 96% rename from src/Umbraco.Core/IMainDom.cs rename to src/Umbraco.Core/Runtime/IMainDom.cs index 31b2e2eee0..444fc1c7d0 100644 --- a/src/Umbraco.Core/IMainDom.cs +++ b/src/Umbraco.Core/Runtime/IMainDom.cs @@ -1,5 +1,6 @@ using System; +// TODO: Can't change namespace due to breaking changes, change in netcore namespace Umbraco.Core { /// diff --git a/src/Umbraco.Core/Runtime/IMainDomLock.cs b/src/Umbraco.Core/Runtime/IMainDomLock.cs new file mode 100644 index 0000000000..c32e990114 --- /dev/null +++ b/src/Umbraco.Core/Runtime/IMainDomLock.cs @@ -0,0 +1,30 @@ +using System; +using System.Threading.Tasks; + +namespace Umbraco.Core.Runtime +{ + /// + /// An application-wide distributed lock + /// + /// + /// Disposing releases the lock + /// + public interface IMainDomLock : IDisposable + { + /// + /// Acquires an application-wide distributed lock + /// + /// + /// + /// A disposable object which will be disposed in order to release the lock + /// + /// Throws a if the elapsed millsecondsTimeout value is exceeded + Task AcquireLockAsync(int millisecondsTimeout); + + /// + /// Wait on a background thread to receive a signal from another AppDomain + /// + /// + Task ListenAsync(); + } +} diff --git a/src/Umbraco.Core/MainDom.cs b/src/Umbraco.Core/Runtime/MainDom.cs similarity index 77% rename from src/Umbraco.Core/MainDom.cs rename to src/Umbraco.Core/Runtime/MainDom.cs index e2049c0190..406fb9b731 100644 --- a/src/Umbraco.Core/MainDom.cs +++ b/src/Umbraco.Core/Runtime/MainDom.cs @@ -4,10 +4,13 @@ using System.Linq; using System.Security.Cryptography; using System.Threading; using System.Web.Hosting; +using Umbraco.Core; using Umbraco.Core.Logging; +using Umbraco.Core.Persistence; -namespace Umbraco.Core +namespace Umbraco.Core.Runtime { + /// /// Provides the full implementation of . /// @@ -20,18 +23,11 @@ namespace Umbraco.Core #region Vars private readonly ILogger _logger; + private readonly IMainDomLock _mainDomLock; // our own lock for local consistency private object _locko = new object(); - // async lock representing the main domain lock - private readonly SystemLock _systemLock; - private IDisposable _systemLocker; - - // event wait handle used to notify current main domain that it should - // release the lock because a new domain wants to be the main domain - private readonly EventWaitHandle _signal; - private bool _isInitialized; // indicates whether... private bool _isMainDom; // we are the main domain @@ -47,32 +43,12 @@ namespace Umbraco.Core #region Ctor // initializes a new instance of MainDom - public MainDom(ILogger logger) + public MainDom(ILogger logger, IMainDomLock systemLock) { HostingEnvironment.RegisterObject(this); _logger = logger; - - // HostingEnvironment.ApplicationID is null in unit tests, making ReplaceNonAlphanumericChars fail - var appId = HostingEnvironment.ApplicationID?.ReplaceNonAlphanumericChars(string.Empty) ?? string.Empty; - - // combining with the physical path because if running on eg IIS Express, - // two sites could have the same appId even though they are different. - // - // now what could still collide is... two sites, running in two different processes - // and having the same appId, and running on the same app physical path - // - // we *cannot* use the process ID here because when an AppPool restarts it is - // a new process for the same application path - - var appPath = HostingEnvironment.ApplicationPhysicalPath?.ToLowerInvariant() ?? string.Empty; - var hash = (appId + ":::" + appPath).GenerateHash(); - - var lockName = "UMBRACO-" + hash + "-MAINDOM-LCK"; - _systemLock = new SystemLock(lockName); - - var eventName = "UMBRACO-" + hash + "-MAINDOM-EVT"; - _signal = new EventWaitHandle(false, EventResetMode.AutoReset, eventName); + _mainDomLock = systemLock; } #endregion @@ -130,7 +106,6 @@ namespace Umbraco.Core { _logger.Info("Stopping ({SignalSource})", source); foreach (var callback in _callbacks.OrderBy(x => x.Key).Select(x => x.Value)) - { try { callback(); // no timeout on callbacks @@ -140,14 +115,13 @@ namespace Umbraco.Core _logger.Error(e, "Error while running callback"); continue; } - } _logger.Debug("Stopped ({SignalSource})", source); } finally { // in any case... _isMainDom = false; - _systemLocker?.Dispose(); + _mainDomLock.Dispose(); _logger.Info("Released ({SignalSource})", source); } @@ -167,35 +141,11 @@ namespace Umbraco.Core _logger.Info("Acquiring."); - // signal other instances that we want the lock, then wait one the lock, - // which may timeout, and this is accepted - see comments below + // Get the lock + _mainDomLock.AcquireLockAsync(LockTimeoutMilliseconds).Wait(); - // signal, then wait for the lock, then make sure the event is - // reset (maybe there was noone listening..) - _signal.Set(); - - // if more than 1 instance reach that point, one will get the lock - // and the other one will timeout, which is accepted - - //This can throw a TimeoutException - in which case should this be in a try/finally to ensure the signal is always reset. - try - { - _systemLocker = _systemLock.Lock(LockTimeoutMilliseconds); - } - finally - { - // we need to reset the event, because otherwise we would end up - // signaling ourselves and committing suicide immediately. - // only 1 instance can reach that point, but other instances may - // have started and be trying to get the lock - they will timeout, - // which is accepted - - _signal.Reset(); - } - - //WaitOneAsync (ext method) will wait for a signal without blocking the main thread, the waiting is done on a background thread - - _signal.WaitOneAsync() + // Listen for the signal from another AppDomain coming online to release the lock + _mainDomLock.ListenAsync() .ContinueWith(_ => OnSignal("signal")); _logger.Info("Acquired."); @@ -230,8 +180,7 @@ namespace Umbraco.Core { if (disposing) { - _signal?.Close(); - _signal?.Dispose(); + _mainDomLock.Dispose(); } disposedValue = true; @@ -244,5 +193,25 @@ namespace Umbraco.Core } #endregion + + public static string GetMainDomId() + { + // HostingEnvironment.ApplicationID is null in unit tests, making ReplaceNonAlphanumericChars fail + var appId = HostingEnvironment.ApplicationID?.ReplaceNonAlphanumericChars(string.Empty) ?? string.Empty; + + // combining with the physical path because if running on eg IIS Express, + // two sites could have the same appId even though they are different. + // + // now what could still collide is... two sites, running in two different processes + // and having the same appId, and running on the same app physical path + // + // we *cannot* use the process ID here because when an AppPool restarts it is + // a new process for the same application path + + var appPath = HostingEnvironment.ApplicationPhysicalPath?.ToLowerInvariant() ?? string.Empty; + var hash = (appId + ":::" + appPath).GenerateHash(); + + return hash; + } } } diff --git a/src/Umbraco.Core/Runtime/MainDomSemaphoreLock.cs b/src/Umbraco.Core/Runtime/MainDomSemaphoreLock.cs new file mode 100644 index 0000000000..89eef8a658 --- /dev/null +++ b/src/Umbraco.Core/Runtime/MainDomSemaphoreLock.cs @@ -0,0 +1,92 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Umbraco.Core.Runtime +{ + /// + /// Uses a system-wide Semaphore and EventWaitHandle to synchronize the current AppDomain + /// + internal class MainDomSemaphoreLock : IMainDomLock + { + private readonly SystemLock _systemLock; + + // event wait handle used to notify current main domain that it should + // release the lock because a new domain wants to be the main domain + private readonly EventWaitHandle _signal; + + private IDisposable _lockRelease; + + public MainDomSemaphoreLock() + { + var lockName = "UMBRACO-" + MainDom.GetMainDomId() + "-MAINDOM-LCK"; + _systemLock = new SystemLock(lockName); + + var eventName = "UMBRACO-" + MainDom.GetMainDomId() + "-MAINDOM-EVT"; + _signal = new EventWaitHandle(false, EventResetMode.AutoReset, eventName); + } + + //WaitOneAsync (ext method) will wait for a signal without blocking the main thread, the waiting is done on a background thread + public Task ListenAsync() => _signal.WaitOneAsync(); + + public Task AcquireLockAsync(int millisecondsTimeout) + { + // signal other instances that we want the lock, then wait on the lock, + // which may timeout, and this is accepted - see comments below + + // signal, then wait for the lock, then make sure the event is + // reset (maybe there was noone listening..) + _signal.Set(); + + // if more than 1 instance reach that point, one will get the lock + // and the other one will timeout, which is accepted + + //This can throw a TimeoutException - in which case should this be in a try/finally to ensure the signal is always reset. + try + { + _lockRelease = _systemLock.Lock(millisecondsTimeout); + return Task.FromResult(true); + } + catch (TimeoutException) + { + return Task.FromResult(false); + } + finally + { + // we need to reset the event, because otherwise we would end up + // signaling ourselves and committing suicide immediately. + // only 1 instance can reach that point, but other instances may + // have started and be trying to get the lock - they will timeout, + // which is accepted + + _signal.Reset(); + } + } + + #region IDisposable Support + private bool disposedValue = false; // To detect redundant calls + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + _lockRelease?.Dispose(); + _signal.Close(); + _signal.Dispose(); + } + + disposedValue = true; + } + } + + // This code added to correctly implement the disposable pattern. + public void Dispose() + { + // Do not change this code. Put cleanup code in Dispose(bool disposing) above. + Dispose(true); + } + #endregion + } +} diff --git a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs new file mode 100644 index 0000000000..28449fe889 --- /dev/null +++ b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs @@ -0,0 +1,178 @@ +using System; +using System.Data; +using System.Data.SqlClient; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Umbraco.Core.Logging; +using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.Dtos; +using Umbraco.Core.Persistence.Mappers; +using Umbraco.Core.Persistence.SqlSyntax; + +namespace Umbraco.Core.Runtime +{ + internal class SqlMainDomLock : IMainDomLock + { + private string _appDomainId; + private const string MainDomKey = "Umbraco.Core.Runtime.SqlMainDom"; + private readonly ILogger _logger; + private IUmbracoDatabase _db; + private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); + private SqlServerSyntaxProvider _sqlServerSyntax = new SqlServerSyntaxProvider(); + + public SqlMainDomLock(ILogger logger) + { + _appDomainId = AppDomain.CurrentDomain.Id.ToString(); + _logger = logger; + } + + + + public Task AcquireLockAsync(int millisecondsTimeout) + { + var factory = new UmbracoDatabaseFactory( + Constants.System.UmbracoConnectionName, + _logger, + new Lazy(() => new Persistence.Mappers.MapperCollection(Enumerable.Empty()))); + + _db = factory.CreateDatabase(); + + try + { + _db.BeginTransaction(IsolationLevel.ReadCommitted); + + try + { + // wait to get a write lock + _sqlServerSyntax.WriteLock(_db, millisecondsTimeout, Constants.Locks.MainDom); + } + catch (Exception ex) + { + if (IsLockTimeoutException(ex)) + { + return Task.FromResult(false); + } + + // unexpected + throw; + } + + InsertLockRecord(); + + return Task.FromResult(true); + } + catch(Exception) + { + _db.AbortTransaction(); + + // unexpected + throw; + } + finally + { + _db.CompleteTransaction(); + } + } + + public Task ListenAsync() + { + // Create a long running task (dedicated thread) + // to poll to check if we are still the MainDom registered in the DB + return Task.Factory.StartNew(() => + { + while(true) + { + if (_cancellationTokenSource.IsCancellationRequested) + break; + + // poll every 1 second + Thread.Sleep(1000); + + try + { + _db.BeginTransaction(IsolationLevel.ReadCommitted); + + // get a read lock + _sqlServerSyntax.ReadLock(_db, Constants.Locks.MainDom); + + if (!IsStillMainDom()) + { + // we are no longer main dom, exit + return; + } + } + catch (Exception) + { + _db.AbortTransaction(); + throw; + } + finally + { + _db.CompleteTransaction(); + } + } + + + }, _cancellationTokenSource.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default); + + } + + /// + /// Inserts or updates the key/value row to check if the current appdomain is registerd as the maindom + /// + private void InsertLockRecord() + { + _db.InsertOrUpdate(new KeyValueDto + { + Key = MainDomKey, + Value = _appDomainId, + Updated = DateTime.Now + }); + } + + /// + /// Checks if the DB row value is our current appdomain value + /// + /// + private bool IsStillMainDom() + { + return _db.ExecuteScalar("SELECT COUNT(*) FROM umbracoKeyValue WHERE [key] = @key AND [value] = @val", + new { key = MainDomKey, val = _appDomainId }) == 1; + } + + /// + /// Checks if the exception is an SQL timeout + /// + /// + /// + private bool IsLockTimeoutException(Exception exception) => exception is SqlException sqlException && sqlException.Number == 1222; + + #region IDisposable Support + private bool disposedValue = false; // To detect redundant calls + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + _db.Dispose(); + _cancellationTokenSource.Cancel(); + _cancellationTokenSource.Dispose(); + } + + disposedValue = true; + } + } + + // This code added to correctly implement the disposable pattern. + public void Dispose() + { + // Do not change this code. Put cleanup code in Dispose(bool disposing) above. + Dispose(true); + } + #endregion + + } +} diff --git a/src/Umbraco.Core/Scoping/IScopeProvider.cs b/src/Umbraco.Core/Scoping/IScopeProvider.cs index 6c9eb63ba0..96bb939f8e 100644 --- a/src/Umbraco.Core/Scoping/IScopeProvider.cs +++ b/src/Umbraco.Core/Scoping/IScopeProvider.cs @@ -13,7 +13,7 @@ namespace Umbraco.Core.Scoping /// Provides scopes. /// public interface IScopeProvider - { + { /// /// Creates an ambient scope. /// diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 6f9ff1e783..8d278f5630 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -128,6 +128,10 @@ --> + + + + @@ -398,7 +402,7 @@ - + @@ -729,7 +733,7 @@ - + diff --git a/src/Umbraco.Tests/LegacyXmlPublishedCache/XmlPublishedSnapshotService.cs b/src/Umbraco.Tests/LegacyXmlPublishedCache/XmlPublishedSnapshotService.cs index ec6b854a46..97fe9057bb 100644 --- a/src/Umbraco.Tests/LegacyXmlPublishedCache/XmlPublishedSnapshotService.cs +++ b/src/Umbraco.Tests/LegacyXmlPublishedCache/XmlPublishedSnapshotService.cs @@ -9,6 +9,7 @@ using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Persistence.Repositories; +using Umbraco.Core.Runtime; using Umbraco.Core.Scoping; using Umbraco.Core.Services; using Umbraco.Web; diff --git a/src/Umbraco.Tests/LegacyXmlPublishedCache/XmlStore.cs b/src/Umbraco.Tests/LegacyXmlPublishedCache/XmlStore.cs index c452c4792a..803b86aec5 100644 --- a/src/Umbraco.Tests/LegacyXmlPublishedCache/XmlStore.cs +++ b/src/Umbraco.Tests/LegacyXmlPublishedCache/XmlStore.cs @@ -14,6 +14,7 @@ using Umbraco.Core.Models; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Persistence.Repositories.Implement; +using Umbraco.Core.Runtime; using Umbraco.Core.Scoping; using Umbraco.Core.Services; using Umbraco.Core.Services.Changes; From 654511a6564af56e3d50c527cd7b35921ce120cc Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 10 Dec 2019 13:42:53 +0100 Subject: [PATCH 098/202] adds appSetting to switch maindom lock --- src/Umbraco.Core/Constants-AppSettings.cs | 2 ++ src/Umbraco.Web/UmbracoApplication.cs | 16 ++++++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Core/Constants-AppSettings.cs b/src/Umbraco.Core/Constants-AppSettings.cs index 509be46b61..85d6b24ae0 100644 --- a/src/Umbraco.Core/Constants-AppSettings.cs +++ b/src/Umbraco.Core/Constants-AppSettings.cs @@ -9,6 +9,8 @@ namespace Umbraco.Core /// public static class AppSettings { + public const string MainDomLock = "Umbraco.Core.MainDom.Lock"; + // TODO: Kill me - still used in Umbraco.Core.IO.SystemFiles:27 [Obsolete("We need to kill this appsetting as we do not use XML content cache umbraco.config anymore due to NuCache")] public const string ContentXML = "Umbraco.Core.ContentXML"; //umbracoContentXML diff --git a/src/Umbraco.Web/UmbracoApplication.cs b/src/Umbraco.Web/UmbracoApplication.cs index 94403bc1be..ad8dcb7e8b 100644 --- a/src/Umbraco.Web/UmbracoApplication.cs +++ b/src/Umbraco.Web/UmbracoApplication.cs @@ -1,7 +1,9 @@ -using System.Threading; +using System.Configuration; +using System.Threading; using System.Web; using Umbraco.Core; using Umbraco.Core.Logging.Serilog; +using Umbraco.Core.Runtime; using Umbraco.Web.Runtime; namespace Umbraco.Web @@ -14,7 +16,17 @@ namespace Umbraco.Web protected override IRuntime GetRuntime() { var logger = SerilogLogger.CreateWithDefaultConfiguration(); - return new WebRuntime(this, logger, new MainDom(logger)); + + // Determine if we should use the sql main dom or the default + var appSettingMainDomLock = ConfigurationManager.AppSettings[Constants.AppSettings.MainDomLock]; + + var mainDomLock = appSettingMainDomLock == "SqlMainDomLock" + ? (IMainDomLock)new SqlMainDomLock(logger) + : new MainDomSemaphoreLock(); + + var runtime = new WebRuntime(this, logger, new MainDom(logger, mainDomLock)); + + return runtime; } /// From e919af14d33827de2415f332907cdfc12766ed91 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 10 Dec 2019 13:50:10 +0100 Subject: [PATCH 099/202] adds comments --- src/Umbraco.Core/Runtime/MainDom.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Umbraco.Core/Runtime/MainDom.cs b/src/Umbraco.Core/Runtime/MainDom.cs index 406fb9b731..c6ee819cda 100644 --- a/src/Umbraco.Core/Runtime/MainDom.cs +++ b/src/Umbraco.Core/Runtime/MainDom.cs @@ -155,6 +155,10 @@ namespace Umbraco.Core.Runtime /// /// Gets a value indicating whether the current domain is the main domain. /// + /// + /// The lazy initializer call will only call the Acquire callback when it's not been initialized, else it will just return + /// the value from _isMainDom which means when we set _isMainDom to false again after being signaled, this will return false; + /// public bool IsMainDom => LazyInitializer.EnsureInitialized(ref _isMainDom, ref _isInitialized, ref _locko, () => Acquire()); // IRegisteredObject From aae8dbdc15ec0f10efaa98c87384d2c2a069b936 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 10 Dec 2019 15:44:47 +0100 Subject: [PATCH 100/202] Updates to allow the sql main dom lock to be able to wait until the previous maindom has fully unwound before it can be taken over. --- src/Umbraco.Core/Runtime/MainDom.cs | 13 +- src/Umbraco.Core/Runtime/SqlMainDomLock.cs | 204 +++++++++++++++++++-- 2 files changed, 195 insertions(+), 22 deletions(-) diff --git a/src/Umbraco.Core/Runtime/MainDom.cs b/src/Umbraco.Core/Runtime/MainDom.cs index c6ee819cda..85cf5ad6a9 100644 --- a/src/Umbraco.Core/Runtime/MainDom.cs +++ b/src/Umbraco.Core/Runtime/MainDom.cs @@ -144,9 +144,16 @@ namespace Umbraco.Core.Runtime // Get the lock _mainDomLock.AcquireLockAsync(LockTimeoutMilliseconds).Wait(); - // Listen for the signal from another AppDomain coming online to release the lock - _mainDomLock.ListenAsync() - .ContinueWith(_ => OnSignal("signal")); + try + { + // Listen for the signal from another AppDomain coming online to release the lock + _mainDomLock.ListenAsync() + .ContinueWith(_ => OnSignal("signal")); + } + catch (OperationCanceledException) + { + // the waiting task could be canceled if this appdomain is naturally shutting down, we'll just swallow this exception + } _logger.Info("Acquired."); return true; diff --git a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs index 28449fe889..c077572f85 100644 --- a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs +++ b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs @@ -1,6 +1,7 @@ using System; using System.Data; using System.Data.SqlClient; +using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -14,22 +15,22 @@ namespace Umbraco.Core.Runtime { internal class SqlMainDomLock : IMainDomLock { - private string _appDomainId; + private string _lockId; private const string MainDomKey = "Umbraco.Core.Runtime.SqlMainDom"; private readonly ILogger _logger; private IUmbracoDatabase _db; private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); private SqlServerSyntaxProvider _sqlServerSyntax = new SqlServerSyntaxProvider(); + private bool _mainDomChanging = false; public SqlMainDomLock(ILogger logger) { - _appDomainId = AppDomain.CurrentDomain.Id.ToString(); + // unique id for our appdomain, this is more unique than the appdomain id which is just an INT counter to its safer + _lockId = Guid.NewGuid().ToString(); _logger = logger; } - - - public Task AcquireLockAsync(int millisecondsTimeout) + public async Task AcquireLockAsync(int millisecondsTimeout) { var factory = new UmbracoDatabaseFactory( Constants.System.UmbracoConnectionName, @@ -38,6 +39,8 @@ namespace Umbraco.Core.Runtime _db = factory.CreateDatabase(); + var tempId = Guid.NewGuid().ToString(); + try { _db.BeginTransaction(IsolationLevel.ReadCommitted); @@ -51,18 +54,29 @@ namespace Umbraco.Core.Runtime { if (IsLockTimeoutException(ex)) { - return Task.FromResult(false); + return false; } // unexpected throw; } - InsertLockRecord(); + var result = InsertLockRecord(tempId); //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 - return Task.FromResult(true); + // 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 + return true; + } + + // if we've updated, this means there is an active MainDom, now we need to wait to + // for the current MainDom to shutdown which also requires releasing our write lock } - catch(Exception) + catch (Exception) { _db.AbortTransaction(); @@ -73,6 +87,8 @@ namespace Umbraco.Core.Runtime { _db.CompleteTransaction(); } + + return await WaitForExistingAsync(tempId, millisecondsTimeout); } public Task ListenAsync() @@ -96,9 +112,14 @@ namespace Umbraco.Core.Runtime // get a read lock _sqlServerSyntax.ReadLock(_db, Constants.Locks.MainDom); - if (!IsStillMainDom()) + // 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)) { - // we are no longer main dom, exit + // we are no longer main dom, another one has come online, exit + _mainDomChanging = true; return; } } @@ -119,26 +140,134 @@ namespace Umbraco.Core.Runtime } /// - /// Inserts or updates the key/value row to check if the current appdomain is registerd as the maindom + /// Wait for any existing MainDom to release so we can continue booting /// - private void InsertLockRecord() + /// + /// + /// + private Task WaitForExistingAsync(string tempId, int millisecondsTimeout) { - _db.InsertOrUpdate(new KeyValueDto + var updatedTempId = tempId + "_updated"; + + return Task.Run(() => + { + var watch = new Stopwatch(); + watch.Start(); + while(true) + { + // poll very often, we need to take over as fast as we can + Thread.Sleep(100); + + try + { + _db.BeginTransaction(IsolationLevel.ReadCommitted); + + // get a read lock + _sqlServerSyntax.ReadLock(_db, Constants.Locks.MainDom); + + var mainDomRows = _db.Fetch("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. + + _sqlServerSyntax.WriteLock(_db, Constants.Locks.MainDom); + + // so now we update the row with our appdomain id + InsertLockRecord(_lockId); + + return true; + } + else if (mainDomRows.Count == 1 && mainDomRows[0].Value.EndsWith("_updated")) + { + // in this case, there is a suffixed _updated value but it's not for our ID which means + // another new AppDomain has come online and is wanting to take over. In that case, we will not + // acquire. + + return false; + } + } + catch (Exception ex) + { + _db.AbortTransaction(); + + if (IsLockTimeoutException(ex)) + { + return false; + } + + throw; + } + 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 + + + try + { + _db.BeginTransaction(IsolationLevel.ReadCommitted); + + _sqlServerSyntax.WriteLock(_db, Constants.Locks.MainDom); + + // so now we update the row with our appdomain id + InsertLockRecord(_lockId); + return true; + } + catch (Exception ex) + { + _db.AbortTransaction(); + + if (IsLockTimeoutException(ex)) + { + // something is wrong, we cannot acquire, not much we can do + return false; + } + + throw; + } + finally + { + _db.CompleteTransaction(); + } + } + } + }, _cancellationTokenSource.Token); + } + + /// + /// Inserts or updates the key/value row + /// + private RecordPersistenceType InsertLockRecord(string id) + { + return _db.InsertOrUpdate(new KeyValueDto { Key = MainDomKey, - Value = _appDomainId, + Value = id, Updated = DateTime.Now }); } /// - /// Checks if the DB row value is our current appdomain value + /// Checks if the DB row value is equals the value /// /// - private bool IsStillMainDom() + private bool IsMainDomValue(string val) { return _db.ExecuteScalar("SELECT COUNT(*) FROM umbracoKeyValue WHERE [key] = @key AND [value] = @val", - new { key = MainDomKey, val = _appDomainId }) == 1; + new { key = MainDomKey, val = val }) == 1; } /// @@ -157,9 +286,46 @@ namespace Umbraco.Core.Runtime { if (disposing) { - _db.Dispose(); + // capture locally just in case in some strange way a sub task somehow updates this + var mainDomChanging = _mainDomChanging; + + // immediately cancel all sub-tasks, we don't want them to keep querying _cancellationTokenSource.Cancel(); _cancellationTokenSource.Dispose(); + + try + { + _db.BeginTransaction(IsolationLevel.ReadCommitted); + + // get a write lock + _sqlServerSyntax.WriteLock(_db, Constants.Locks.MainDom); + + // When we are disposed, it means we have released the MainDom lock + // and called all MainDom release callbacks, in this case + // if another maindom is actually coming online we need + // to signal to the MainDom coming online that we have shutdown. + // To do that, we update the existing main dom DB record with a suffixed "_updated" string. + // Otherwise, if we are just shutting down, we want to just delete the row. + if (mainDomChanging) + { + _db.Execute("UPDATE umbracoKeyValue SET [value] = [value] + '_updated' WHERE [key] = @key", new { key = MainDomKey }); + } + else + { + _db.Execute("DELETE FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey }); + } + } + catch (Exception) + { + _db.AbortTransaction(); + throw; + } + finally + { + _db.CompleteTransaction(); + + _db.Dispose(); + } } disposedValue = true; From a63de7672bd891a306d5c1a8bc2cc682fd3cc784 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 10 Dec 2019 15:53:57 +0100 Subject: [PATCH 101/202] updates a bool check --- src/Umbraco.Core/Runtime/SqlMainDomLock.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs index c077572f85..edd5af7ea8 100644 --- a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs +++ b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs @@ -181,9 +181,9 @@ namespace Umbraco.Core.Runtime return true; } - else if (mainDomRows.Count == 1 && mainDomRows[0].Value.EndsWith("_updated")) + else if (mainDomRows.Count == 1 && !mainDomRows[0].Value.StartsWith(tempId)) { - // in this case, there is a suffixed _updated value but it's not for our ID which means + // in this case, the prefixed ID is different which means // another new AppDomain has come online and is wanting to take over. In that case, we will not // acquire. From 0ef08db7658f1eae04c1d3602200b0c8420e4a88 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 11 Dec 2019 08:55:07 +0100 Subject: [PATCH 102/202] adds error logging and ensure we don't throw exceptions on a background task which would destroy the app domain --- src/Umbraco.Core/Runtime/SqlMainDomLock.cs | 138 ++++++++++++++------- 1 file changed, 91 insertions(+), 47 deletions(-) diff --git a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs index edd5af7ea8..c567b76403 100644 --- a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs +++ b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs @@ -22,42 +22,45 @@ namespace Umbraco.Core.Runtime private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); private SqlServerSyntaxProvider _sqlServerSyntax = new SqlServerSyntaxProvider(); private bool _mainDomChanging = false; + private readonly UmbracoDatabaseFactory _dbFactory; + private bool _hasError; public SqlMainDomLock(ILogger logger) { // unique id for our appdomain, this is more unique than the appdomain id which is just an INT counter to its safer _lockId = Guid.NewGuid().ToString(); _logger = logger; + _dbFactory = new UmbracoDatabaseFactory( + Constants.System.UmbracoConnectionName, + _logger, + new Lazy(() => new Persistence.Mappers.MapperCollection(Enumerable.Empty()))); } public async Task AcquireLockAsync(int millisecondsTimeout) { - var factory = new UmbracoDatabaseFactory( - Constants.System.UmbracoConnectionName, - _logger, - new Lazy(() => new Persistence.Mappers.MapperCollection(Enumerable.Empty()))); - - _db = factory.CreateDatabase(); + var db = GetDatabase(); var tempId = Guid.NewGuid().ToString(); try { - _db.BeginTransaction(IsolationLevel.ReadCommitted); + db.BeginTransaction(IsolationLevel.ReadCommitted); try { // wait to get a write lock - _sqlServerSyntax.WriteLock(_db, millisecondsTimeout, Constants.Locks.MainDom); + _sqlServerSyntax.WriteLock(db, millisecondsTimeout, Constants.Locks.MainDom); } catch (Exception ex) { if (IsLockTimeoutException(ex)) { + _logger.Error(ex, "Sql timeout occurred, could not acquire MainDom."); + _hasError = true; return false; } - // unexpected + // unexpected (will be caught below) throw; } @@ -76,16 +79,17 @@ namespace Umbraco.Core.Runtime // if we've updated, this means there is an active MainDom, now we need to wait to // for the current MainDom to shutdown which also requires releasing our write lock } - catch (Exception) + catch (Exception ex) { - _db.AbortTransaction(); - + ResetDatabase(); // unexpected - throw; + _logger.Error(ex, "Unexpected error, cannot acquire MainDom"); + _hasError = true; + return false; } finally { - _db.CompleteTransaction(); + db?.CompleteTransaction(); } return await WaitForExistingAsync(tempId, millisecondsTimeout); @@ -93,6 +97,12 @@ namespace Umbraco.Core.Runtime public Task ListenAsync() { + if (_hasError) + { + _logger.Warn("Could not acquire MainDom, listening is canceled."); + return Task.CompletedTask; + } + // Create a long running task (dedicated thread) // to poll to check if we are still the MainDom registered in the DB return Task.Factory.StartNew(() => @@ -105,12 +115,14 @@ namespace Umbraco.Core.Runtime // poll every 1 second Thread.Sleep(1000); + var db = GetDatabase(); + try { - _db.BeginTransaction(IsolationLevel.ReadCommitted); + db.BeginTransaction(IsolationLevel.ReadCommitted); // get a read lock - _sqlServerSyntax.ReadLock(_db, Constants.Locks.MainDom); + _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 @@ -123,14 +135,17 @@ namespace Umbraco.Core.Runtime return; } } - catch (Exception) + catch (Exception ex) { - _db.AbortTransaction(); - throw; + ResetDatabase(); + // unexpected + _logger.Error(ex, "Unexpected error, listening is canceled."); + _hasError = true; + return; } finally { - _db.CompleteTransaction(); + db?.CompleteTransaction(); } } @@ -139,6 +154,23 @@ namespace Umbraco.Core.Runtime } + 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; + } + /// /// Wait for any existing MainDom to release so we can continue booting /// @@ -151,6 +183,7 @@ namespace Umbraco.Core.Runtime return Task.Run(() => { + var db = GetDatabase(); var watch = new Stopwatch(); watch.Start(); while(true) @@ -160,12 +193,13 @@ namespace Umbraco.Core.Runtime try { - _db.BeginTransaction(IsolationLevel.ReadCommitted); + db.BeginTransaction(IsolationLevel.ReadCommitted); // get a read lock - _sqlServerSyntax.ReadLock(_db, Constants.Locks.MainDom); + _sqlServerSyntax.ReadLock(db, Constants.Locks.MainDom); - var mainDomRows = _db.Fetch("SELECT * FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey }); + // the row + var mainDomRows = db.Fetch("SELECT * FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey }); if (mainDomRows.Count == 0 || mainDomRows[0].Value == updatedTempId) { @@ -174,7 +208,7 @@ namespace Umbraco.Core.Runtime // 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); @@ -192,18 +226,22 @@ namespace Umbraco.Core.Runtime } catch (Exception ex) { - _db.AbortTransaction(); + ResetDatabase(); if (IsLockTimeoutException(ex)) { + _logger.Error(ex, "Sql timeout occurred, waiting for existing MainDom is canceled."); + _hasError = true; return false; } - - throw; + // unexpected + _logger.Error(ex, "Unexpected error, waiting for existing MainDom is canceled."); + _hasError = true; + return false; } finally { - _db.CompleteTransaction(); + db?.CompleteTransaction(); } if (watch.ElapsedMilliseconds >= millisecondsTimeout) @@ -218,9 +256,9 @@ namespace Umbraco.Core.Runtime try { - _db.BeginTransaction(IsolationLevel.ReadCommitted); + db.BeginTransaction(IsolationLevel.ReadCommitted); - _sqlServerSyntax.WriteLock(_db, Constants.Locks.MainDom); + _sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom); // so now we update the row with our appdomain id InsertLockRecord(_lockId); @@ -228,19 +266,22 @@ namespace Umbraco.Core.Runtime } catch (Exception ex) { - _db.AbortTransaction(); + ResetDatabase(); if (IsLockTimeoutException(ex)) { - // something is wrong, we cannot acquire, not much we can do + // something is wrong, we cannot acquire, not much we can do + _logger.Error(ex, "Sql timeout occurred, could not forcibly acquire MainDom."); + _hasError = true; return false; } - - throw; + _logger.Error(ex, "Unexpected error, could not forcibly acquire MainDom."); + _hasError = true; + return false; } finally { - _db.CompleteTransaction(); + db?.CompleteTransaction(); } } } @@ -252,7 +293,8 @@ namespace Umbraco.Core.Runtime /// private RecordPersistenceType InsertLockRecord(string id) { - return _db.InsertOrUpdate(new KeyValueDto + var db = GetDatabase(); + return db.InsertOrUpdate(new KeyValueDto { Key = MainDomKey, Value = id, @@ -266,7 +308,8 @@ namespace Umbraco.Core.Runtime /// private bool IsMainDomValue(string val) { - return _db.ExecuteScalar("SELECT COUNT(*) FROM umbracoKeyValue WHERE [key] = @key AND [value] = @val", + var db = GetDatabase(); + return db.ExecuteScalar("SELECT COUNT(*) FROM umbracoKeyValue WHERE [key] = @key AND [value] = @val", new { key = MainDomKey, val = val }) == 1; } @@ -293,12 +336,13 @@ namespace Umbraco.Core.Runtime _cancellationTokenSource.Cancel(); _cancellationTokenSource.Dispose(); + var db = GetDatabase(); try { - _db.BeginTransaction(IsolationLevel.ReadCommitted); + db.BeginTransaction(IsolationLevel.ReadCommitted); // get a write lock - _sqlServerSyntax.WriteLock(_db, Constants.Locks.MainDom); + _sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom); // When we are disposed, it means we have released the MainDom lock // and called all MainDom release callbacks, in this case @@ -308,23 +352,23 @@ namespace Umbraco.Core.Runtime // Otherwise, if we are just shutting down, we want to just delete the row. if (mainDomChanging) { - _db.Execute("UPDATE umbracoKeyValue SET [value] = [value] + '_updated' WHERE [key] = @key", new { key = MainDomKey }); + db.Execute("UPDATE umbracoKeyValue SET [value] = [value] + '_updated' WHERE [key] = @key", new { key = MainDomKey }); } else { - _db.Execute("DELETE FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey }); + db.Execute("DELETE FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey }); } } - catch (Exception) + catch (Exception ex) { - _db.AbortTransaction(); - throw; + ResetDatabase(); + _logger.Error(ex, "Unexpected error during dipsose."); + _hasError = true; } finally { - _db.CompleteTransaction(); - - _db.Dispose(); + db?.CompleteTransaction(); + ResetDatabase(); } } From 9e9a2d8379877961e2c0563b91286a4ca4f3f3d3 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 17 Dec 2019 10:38:03 +1100 Subject: [PATCH 103/202] Adds TestData project with an endpoint to create content/media hieararchy for testing --- .../Properties/AssemblyInfo.cs | 36 +++ src/Umbraco.TestData/Umbraco.TestData.csproj | 69 +++++ .../UmbracoTestDataController.cs | 269 ++++++++++++++++++ src/Umbraco.TestData/readme.md | 4 + src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 6 + src/umbraco.sln | 7 + 6 files changed, 391 insertions(+) create mode 100644 src/Umbraco.TestData/Properties/AssemblyInfo.cs create mode 100644 src/Umbraco.TestData/Umbraco.TestData.csproj create mode 100644 src/Umbraco.TestData/UmbracoTestDataController.cs create mode 100644 src/Umbraco.TestData/readme.md diff --git a/src/Umbraco.TestData/Properties/AssemblyInfo.cs b/src/Umbraco.TestData/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..3c4251cdf6 --- /dev/null +++ b/src/Umbraco.TestData/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Umbraco.TestData")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Umbraco.TestData")] +[assembly: AssemblyCopyright("Copyright © 2019")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("fb5676ed-7a69-492c-b802-e7b24144c0fc")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/Umbraco.TestData/Umbraco.TestData.csproj b/src/Umbraco.TestData/Umbraco.TestData.csproj new file mode 100644 index 0000000000..b0d6b5677b --- /dev/null +++ b/src/Umbraco.TestData/Umbraco.TestData.csproj @@ -0,0 +1,69 @@ + + + + + Debug + AnyCPU + {FB5676ED-7A69-492C-B802-E7B24144C0FC} + Library + Properties + Umbraco.TestData + Umbraco.TestData + v4.7.2 + 512 + true + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + {31785bc3-256c-4613-b2f5-a1b0bdded8c1} + Umbraco.Core + + + {651e1350-91b6-44b7-bd60-7207006d7003} + Umbraco.Web + + + + + 28.4.4 + + + 5.2.7 + + + + \ No newline at end of file diff --git a/src/Umbraco.TestData/UmbracoTestDataController.cs b/src/Umbraco.TestData/UmbracoTestDataController.cs new file mode 100644 index 0000000000..8912a28940 --- /dev/null +++ b/src/Umbraco.TestData/UmbracoTestDataController.cs @@ -0,0 +1,269 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Umbraco.Core; +using System.Web.Mvc; +using Umbraco.Core.Cache; +using Umbraco.Core.Logging; +using Umbraco.Core.Models; +using Umbraco.Core.Persistence; +using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Services; +using Umbraco.Web; +using Umbraco.Web.Mvc; +using System.Configuration; +using Bogus; +using Umbraco.Core.Scoping; + +namespace Umbraco.TestData +{ + /// + /// Creates test data + /// + public class UmbracoTestDataController : SurfaceController + { + private const string RichTextDataTypeName = "UmbracoTestDataContent.RTE"; + private const string MediaPickerDataTypeName = "UmbracoTestDataContent.MediaPicker"; + private const string TextDataTypeName = "UmbracoTestDataContent.Text"; + private const string TestDataContentTypeAlias = "umbTestDataContent"; + private const int IdealPerBranch = 5; + private readonly IScopeProvider _scopeProvider; + private readonly PropertyEditorCollection _propertyEditors; + + public UmbracoTestDataController(IScopeProvider scopeProvider, PropertyEditorCollection propertyEditors, IUmbracoContextAccessor umbracoContextAccessor, IUmbracoDatabaseFactory databaseFactory, ServiceContext services, AppCaches appCaches, ILogger logger, IProfilingLogger profilingLogger, UmbracoHelper umbracoHelper) : base(umbracoContextAccessor, databaseFactory, services, appCaches, logger, profilingLogger, umbracoHelper) + { + _scopeProvider = scopeProvider; + _propertyEditors = propertyEditors; + } + + /// + /// Creates a content and associated media tree (hierarchy) + /// + /// + /// + /// + /// + /// + /// Each content item created is associated to a media item via a media picker and therefore a relation is created between the two + /// + public ActionResult CreateTree(int count, int depth, string locale = "en") + { + if (ConfigurationManager.AppSettings["Umbraco.TestData.Enabled"] != "true") + return HttpNotFound(); + + if (!Validate(count, depth, out var message, out var perLevel)) + throw new InvalidOperationException(message); + + var faker = new Faker(locale); + var company = faker.Company.CompanyName(); + + using (var scope = _scopeProvider.CreateScope()) + { + var imageIds = CreateMediaTree(company, faker, count, depth).ToList(); + var contentIds = CreateContentTree(company, faker, count, depth, imageIds, out var root).ToList(); + + Services.ContentService.SaveAndPublishBranch(root, true); + + scope.Complete(); + } + + + return Content("Done"); + } + + private bool Validate(int count, int depth, out string message, out int perLevel) + { + perLevel = 0; + message = null; + + if (count <= 0) + { + message = "Count must be more than 0"; + return false; + } + + perLevel = count / depth; + if (perLevel < 1) + { + message = "Count not high enough for specified for number of levels required"; + return false; + } + + return true; + } + + /// + /// Utility to create a tree hierarchy + /// + /// + /// + /// + /// + /// + /// A callback that returns a tuple of Content and another callback to produce a Container. + /// For media, a container will be another folder, for content the container will be the Content itself. + /// + /// + private IEnumerable CreateHierarchy( + T parent, int count, int depth, + Func container)> creator) + where T: class, IContentBase + { + yield return parent.GetUdi(); + + var totalDescendants = count - 1; + int perLevel = totalDescendants / depth; + var branchCount = perLevel / IdealPerBranch; + var perBranch = perLevel / branchCount; + + var currentPerBranchCount = 0; + + T lastParent = null; + + for (int i = 0; i < count; i++) + { + var created = creator(parent); + var contentItem = created.content; + + yield return contentItem.GetUdi(); + + currentPerBranchCount++; + + if (contentItem.Level < depth) + { + // not at max depth, create below + lastParent = parent; + parent = created.container(); + currentPerBranchCount = 0; + } + else if (currentPerBranchCount == perBranch) + { + parent = lastParent; + currentPerBranchCount = 0; + } + } + } + + /// + /// Creates the media tree hiearachy + /// + /// + /// + /// + /// + /// + private IEnumerable CreateMediaTree(string company, Faker faker, int count, int depth) + { + var parent = Services.MediaService.CreateMediaWithIdentity(company, -1, Constants.Conventions.MediaTypes.Folder); + + return CreateHierarchy(parent, count, depth, currParent => + { + var imageUrl = faker.Image.PicsumUrl(); + var media = Services.MediaService.CreateMedia(faker.Commerce.ProductName(), currParent, Constants.Conventions.MediaTypes.Image); + media.SetValue(Constants.Conventions.Media.File, imageUrl); + Services.MediaService.Save(media); + return (media, () => + { + // create a folder to contain child media + var container = Services.MediaService.CreateMediaWithIdentity(faker.Commerce.Department(), parent, Constants.Conventions.MediaTypes.Folder); + return container; + }); + }); + } + + /// + /// Creates the content tree hiearachy + /// + /// + /// + /// + /// + /// + /// + private IEnumerable CreateContentTree(string company, Faker faker, int count, int depth, List imageIds, out IContent root) + { + var random = new Random(company.GetHashCode()); + + var docType = GetOrCreateContentType(); + + var parent = Services.ContentService.Create(company, -1, docType.Alias); + parent.SetValue("review", faker.Rant.Review()); + parent.SetValue("desc", company); + parent.SetValue("media", imageIds[random.Next(0, imageIds.Count - 1)]); + Services.ContentService.Save(parent); + + root = parent; + + return CreateHierarchy(parent, count, depth, currParent => + { + var content = Services.ContentService.Create(faker.Commerce.ProductName(), currParent, docType.Alias); + content.SetValue("review", faker.Rant.Review()); + content.SetValue("desc", string.Join(", ", Enumerable.Range(0, 5).Select(x => faker.Commerce.ProductAdjective()))); ; + content.SetValue("media", imageIds[random.Next(0, imageIds.Count - 1)]); + + Services.ContentService.Save(content); + return (content, () => content); + }); + + } + + private IContentType GetOrCreateContentType() + { + var docType = Services.ContentTypeService.Get(TestDataContentTypeAlias); + if (docType != null) + return docType; + + docType = new ContentType(-1) + { + Alias = TestDataContentTypeAlias, + Name = "Umbraco Test Data Content", + Icon = "icon-science color-green" + }; + docType.AddPropertyGroup("Content"); + docType.AddPropertyType(new PropertyType(GetOrCreateRichText(), "review") + { + Name = "Review" + }); + docType.AddPropertyType(new PropertyType(GetOrCreateMediaPicker(), "media") + { + Name = "Media" + }); + docType.AddPropertyType(new PropertyType(GetOrCreateText(), "desc") + { + Name = "Description" + }); + Services.ContentTypeService.Save(docType); + docType.AllowedContentTypes = new[] { new ContentTypeSort(docType.Id, 0) }; + Services.ContentTypeService.Save(docType); + return docType; + } + + private IDataType GetOrCreateRichText() => GetOrCreateDataType(RichTextDataTypeName, Constants.PropertyEditors.Aliases.TinyMce); + + private IDataType GetOrCreateMediaPicker() => GetOrCreateDataType(MediaPickerDataTypeName, Constants.PropertyEditors.Aliases.MediaPicker); + + private IDataType GetOrCreateText() => GetOrCreateDataType(TextDataTypeName, Constants.PropertyEditors.Aliases.TextBox); + + private IDataType GetOrCreateDataType(string name, string editorAlias) + { + var dt = Services.DataTypeService.GetDataType(name); + if (dt != null) return dt; + + var editor = _propertyEditors.FirstOrDefault(x => x.Alias == editorAlias); + if (editor == null) + throw new InvalidOperationException($"No {editorAlias} editor found"); + + dt = new DataType(editor) + { + Name = name, + Configuration = editor.GetConfigurationEditor().DefaultConfigurationObject, + DatabaseType = ValueStorageType.Ntext + }; + + Services.DataTypeService.Save(dt); + return dt; + } + } +} diff --git a/src/Umbraco.TestData/readme.md b/src/Umbraco.TestData/readme.md new file mode 100644 index 0000000000..78c08e0957 --- /dev/null +++ b/src/Umbraco.TestData/readme.md @@ -0,0 +1,4 @@ +## Umbraco Test Data + +This project is a utility to be able to generate large amounts of content and media in an +Umbraco installation for testing. diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 43130f34a3..b18856ad2d 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -115,6 +115,12 @@ + + + {fb5676ed-7a69-492c-b802-e7b24144c0fc} + Umbraco.TestData + + {31785bc3-256c-4613-b2f5-a1b0bdded8c1} diff --git a/src/umbraco.sln b/src/umbraco.sln index 938532beb0..0c5f02c7bd 100644 --- a/src/umbraco.sln +++ b/src/umbraco.sln @@ -102,6 +102,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "IssueTemplates", "IssueTemp ..\.github\ISSUE_TEMPLATE\5_Security_issue.md = ..\.github\ISSUE_TEMPLATE\5_Security_issue.md EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Umbraco.TestData", "Umbraco.TestData\Umbraco.TestData.csproj", "{FB5676ED-7A69-492C-B802-E7B24144C0FC}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -134,6 +136,10 @@ Global {3A33ADC9-C6C0-4DB1-A613-A9AF0210DF3D}.Debug|Any CPU.Build.0 = Debug|Any CPU {3A33ADC9-C6C0-4DB1-A613-A9AF0210DF3D}.Release|Any CPU.ActiveCfg = Release|Any CPU {3A33ADC9-C6C0-4DB1-A613-A9AF0210DF3D}.Release|Any CPU.Build.0 = Release|Any CPU + {FB5676ED-7A69-492C-B802-E7B24144C0FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FB5676ED-7A69-492C-B802-E7B24144C0FC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FB5676ED-7A69-492C-B802-E7B24144C0FC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FB5676ED-7A69-492C-B802-E7B24144C0FC}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -146,6 +152,7 @@ Global {53594E5B-64A2-4545-8367-E3627D266AE8} = {FD962632-184C-4005-A5F3-E705D92FC645} {3A33ADC9-C6C0-4DB1-A613-A9AF0210DF3D} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} {C7311C00-2184-409B-B506-52A5FAEA8736} = {FD962632-184C-4005-A5F3-E705D92FC645} + {FB5676ED-7A69-492C-B802-E7B24144C0FC} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {7A0F2E34-D2AF-4DAB-86A0-7D7764B3D0EC} From 292a40194fccd8e3cfef04213f8cf49321013cea Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 17 Dec 2019 16:46:02 +1100 Subject: [PATCH 104/202] updates readme, adjusts controller --- .../UmbracoTestDataController.cs | 48 +++++++++++-------- src/Umbraco.TestData/readme.md | 45 +++++++++++++++++ 2 files changed, 74 insertions(+), 19 deletions(-) diff --git a/src/Umbraco.TestData/UmbracoTestDataController.cs b/src/Umbraco.TestData/UmbracoTestDataController.cs index 8912a28940..f5ded2a754 100644 --- a/src/Umbraco.TestData/UmbracoTestDataController.cs +++ b/src/Umbraco.TestData/UmbracoTestDataController.cs @@ -28,7 +28,6 @@ namespace Umbraco.TestData private const string MediaPickerDataTypeName = "UmbracoTestDataContent.MediaPicker"; private const string TextDataTypeName = "UmbracoTestDataContent.Text"; private const string TestDataContentTypeAlias = "umbTestDataContent"; - private const int IdealPerBranch = 5; private readonly IScopeProvider _scopeProvider; private readonly PropertyEditorCollection _propertyEditors; @@ -101,48 +100,59 @@ namespace Umbraco.TestData /// /// /// - /// + /// /// A callback that returns a tuple of Content and another callback to produce a Container. /// For media, a container will be another folder, for content the container will be the Content itself. /// /// private IEnumerable CreateHierarchy( T parent, int count, int depth, - Func container)> creator) + Func container)> create) where T: class, IContentBase { yield return parent.GetUdi(); + // This will not calculate a balanced tree but it will ensure that there will be enough nodes deep enough to not fill up the tree. var totalDescendants = count - 1; - int perLevel = totalDescendants / depth; - var branchCount = perLevel / IdealPerBranch; - var perBranch = perLevel / branchCount; + var perLevel = Math.Ceiling(totalDescendants / (double)depth); + var perBranch = Math.Ceiling(perLevel / depth); - var currentPerBranchCount = 0; + var tracked = new Stack<(T parent, int childCount)>(); - T lastParent = null; + var currChildCount = 0; for (int i = 0; i < count; i++) { - var created = creator(parent); + var created = create(parent); var contentItem = created.content; yield return contentItem.GetUdi(); - currentPerBranchCount++; + currChildCount++; - if (contentItem.Level < depth) + if (currChildCount == perBranch) { + // move back up... + + var prev = tracked.Pop(); + + // restore child count + currChildCount = prev.childCount; + // restore the parent + parent = prev.parent; + + } + else if (contentItem.Level < depth) + { + // track the current parent and it's current child count + tracked.Push((parent, currChildCount)); + // not at max depth, create below - lastParent = parent; parent = created.container(); - currentPerBranchCount = 0; - } - else if (currentPerBranchCount == perBranch) - { - parent = lastParent; - currentPerBranchCount = 0; + + currChildCount = 0; } + } } @@ -167,7 +177,7 @@ namespace Umbraco.TestData return (media, () => { // create a folder to contain child media - var container = Services.MediaService.CreateMediaWithIdentity(faker.Commerce.Department(), parent, Constants.Conventions.MediaTypes.Folder); + var container = Services.MediaService.CreateMediaWithIdentity(faker.Commerce.Department(), currParent, Constants.Conventions.MediaTypes.Folder); return container; }); }); diff --git a/src/Umbraco.TestData/readme.md b/src/Umbraco.TestData/readme.md index 78c08e0957..717b6aac57 100644 --- a/src/Umbraco.TestData/readme.md +++ b/src/Umbraco.TestData/readme.md @@ -2,3 +2,48 @@ This project is a utility to be able to generate large amounts of content and media in an Umbraco installation for testing. + +Currently this project is referenced in the Umbraco.Web.UI project but only when it's being built +in Debug mode (i.e. when testing within Visual Studio). + +## Usage + +It has to be enabled by an appSetting: + +```xml + +``` + +Once this is enabled this endpoint can be executed: + +`/umbraco/surface/umbracotestdata/CreateTree?count=100&depth=5` + +The query string options are: + +* `count` = the number of content and media nodes to create +* `depth` = how deep the trees created will be +* `locale` (optional, default = "en") = the language that the data will be generated in + +This creates a content and associated media tree (hierarchy). Each content item created is associated +to a media item via a media picker and therefore a relation is created between the two. Each content and +media tree created have the same root node name so it's easy to know which content branch relates to +which media branch. + +All values are generated using the very handy `Bogus` package. + +## Schema + +This will install some schema items: + +* `umbTestDataContent` Document Type. __TIP__: If you want to delete all of the content data generated with this tool, just delete this content type +* `UmbracoTestDataContent.RTE` Data Type +* `UmbracoTestDataContent.MediaPicker` Data Type +* `UmbracoTestDataContent.Text` Data Type + +For media, the normal folder and image is used + +## Media + +This does not upload physical files, it just uses a randomized online image as the `umbracoFile` value. +This works when viewing the media item in the media section and the image will show up, however when viewing a content item +that has these media items picked, the thumbnail will not show up (which is due to a bug in the CMS that will be fixed in 8.6). From 25d1c454dc10ce6bb6e9e3e9b0b5aad386492f04 Mon Sep 17 00:00:00 2001 From: Shannon Deminick Date: Tue, 17 Dec 2019 16:50:54 +1100 Subject: [PATCH 105/202] Update readme.md --- src/Umbraco.TestData/readme.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Umbraco.TestData/readme.md b/src/Umbraco.TestData/readme.md index 717b6aac57..5475da1ff5 100644 --- a/src/Umbraco.TestData/readme.md +++ b/src/Umbraco.TestData/readme.md @@ -8,6 +8,8 @@ in Debug mode (i.e. when testing within Visual Studio). ## Usage +You must use SQL Server for this, using SQLCE will die if you try to bulk create huge amounts of data. + It has to be enabled by an appSetting: ```xml From 000a8d2c946ef64e7e9b025b4f0e1efa7f3cc6b3 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 18 Dec 2019 15:37:00 +1100 Subject: [PATCH 106/202] manually merges changes for the NC validation stuff since there was conflicts --- .../NestedContentPropertyEditor.cs | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs index f3e391aeab..564630c574 100644 --- a/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs @@ -293,9 +293,19 @@ namespace Umbraco.Web.PropertyEditors if (row.PropType.Mandatory) { if (row.JsonRowValue[row.PropKey] == null) - validationResults.Add(new ValidationResult("Item " + (row.RowIndex + 1) + " '" + row.PropType.Name + "' cannot be null", new[] { row.PropKey })); + { + 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)) - validationResults.Add(new ValidationResult("Item " + (row.RowIndex + 1) + " '" + row.PropType.Name + "' cannot be empty", new[] { row.PropKey })); + { + 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 @@ -305,7 +315,10 @@ namespace Umbraco.Web.PropertyEditors var regex = new Regex(row.PropType.ValidationRegExp); if (!regex.IsMatch(row.JsonRowValue[row.PropKey].ToString())) { - validationResults.Add(new ValidationResult("Item " + (row.RowIndex + 1) + " '" + row.PropType.Name + "' is invalid, it does not match the correct pattern", new[] { row.PropKey })); + 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 })); } } } From 1e35aeb9cf3ef100eb7a2e25d5aca84f9e5bb439 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 18 Dec 2019 16:01:27 +1100 Subject: [PATCH 107/202] ensures .jpg ext is added to test data --- src/Umbraco.TestData/UmbracoTestDataController.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Umbraco.TestData/UmbracoTestDataController.cs b/src/Umbraco.TestData/UmbracoTestDataController.cs index f5ded2a754..402c05cc1c 100644 --- a/src/Umbraco.TestData/UmbracoTestDataController.cs +++ b/src/Umbraco.TestData/UmbracoTestDataController.cs @@ -171,6 +171,15 @@ namespace Umbraco.TestData return CreateHierarchy(parent, count, depth, currParent => { var imageUrl = faker.Image.PicsumUrl(); + + // we are appending a &ext=.jpg to the end of this for a reason. The result of this url will be something like: + // https://picsum.photos/640/480/?image=106 + // and due to the way that we detect images there must be an extension so we'll change it to + // https://picsum.photos/640/480/?image=106&ext=.jpg + // which will trick our app into parsing this and thinking it's an image ... which it is so that's good. + // if we don't do this we don't get thumbnails in the back office. + imageUrl += "&ext=.jpg"; + var media = Services.MediaService.CreateMedia(faker.Commerce.ProductName(), currParent, Constants.Conventions.MediaTypes.Image); media.SetValue(Constants.Conventions.Media.File, imageUrl); Services.MediaService.Save(media); From e0531f1429cc2e1f7c812c7737653f2bc9ed58d0 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 18 Dec 2019 16:10:33 +1100 Subject: [PATCH 108/202] allows ImagesController to still return valid urls even if the uri isn't resolved to a local on-disk file --- src/Umbraco.Web/Editors/ImagesController.cs | 23 +++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web/Editors/ImagesController.cs b/src/Umbraco.Web/Editors/ImagesController.cs index b29c166765..db6706d1bb 100644 --- a/src/Umbraco.Web/Editors/ImagesController.cs +++ b/src/Umbraco.Web/Editors/ImagesController.cs @@ -60,8 +60,27 @@ namespace Umbraco.Web.Editors //redirect to ImageProcessor thumbnail with rnd generated from last modified time of original media file var response = Request.CreateResponse(HttpStatusCode.Found); - var imageLastModified = _mediaFileSystem.GetLastModified(imagePath); - response.Headers.Location = new Uri($"{imagePath}?rnd={imageLastModified:yyyyMMddHHmmss}&upscale=false&width={width}&animationprocessmode=first&mode=max", UriKind.Relative); + + DateTimeOffset? imageLastModified = null; + try + { + imageLastModified = _mediaFileSystem.GetLastModified(imagePath); + + } + catch (Exception) + { + // if we get an exception here it's probably because the image path being requested is an image that doesn't exist + // in the local media file system. This can happen if someone is storing an absolute path to an image online, which + // is perfectly legal but in that case the media file system isn't going to resolve it. + // so ignore and we won't set a last modified date. + } + + // TODO: When we abstract imaging for netcore, we are actually just going to be abstracting a URI builder for images, this + // is one of those places where this can be used. + + var rnd = imageLastModified.HasValue ? $"&rnd={imageLastModified:yyyyMMddHHmmss}" : string.Empty; + + response.Headers.Location = new Uri($"{imagePath}?upscale=false&width={width}&animationprocessmode=first&mode=max{rnd}", UriKind.RelativeOrAbsolute); return response; } From ddaafa2abe1c0fb1f65daf7aa692835c604eea2c Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 18 Dec 2019 16:11:31 +1100 Subject: [PATCH 109/202] updates readme --- src/Umbraco.TestData/readme.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.TestData/readme.md b/src/Umbraco.TestData/readme.md index 5475da1ff5..f943326303 100644 --- a/src/Umbraco.TestData/readme.md +++ b/src/Umbraco.TestData/readme.md @@ -47,5 +47,5 @@ For media, the normal folder and image is used ## Media This does not upload physical files, it just uses a randomized online image as the `umbracoFile` value. -This works when viewing the media item in the media section and the image will show up, however when viewing a content item -that has these media items picked, the thumbnail will not show up (which is due to a bug in the CMS that will be fixed in 8.6). +This works when viewing the media item in the media section and the image will show up and with recent changes this will also work +when editing content to view the thumbnail for the picked media. From 7501629c541633c8bb33077b473514bbe288f359 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 30 Dec 2019 17:15:57 +1100 Subject: [PATCH 110/202] Adds logging to SqlMainDomLock --- src/Umbraco.Core/Runtime/SqlMainDomLock.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs index c567b76403..a2560b71a3 100644 --- a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs +++ b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs @@ -38,6 +38,8 @@ namespace Umbraco.Core.Runtime public async Task AcquireLockAsync(int millisecondsTimeout) { + _logger.Info("Acquiring lock..."); + var db = GetDatabase(); var tempId = Guid.NewGuid().ToString(); @@ -73,6 +75,7 @@ namespace Umbraco.Core.Runtime // are MainDom? then we don't leave any orphan rows behind. InsertLockRecord(_lockId); // so update with our appdomain id + _logger.Info("Acquired with ID {LockId}", _lockId); return true; } @@ -212,7 +215,7 @@ namespace Umbraco.Core.Runtime // so now we update the row with our appdomain id InsertLockRecord(_lockId); - + _logger.Info("Acquired with ID {LockId}", _lockId); return true; } else if (mainDomRows.Count == 1 && !mainDomRows[0].Value.StartsWith(tempId)) @@ -221,6 +224,8 @@ namespace Umbraco.Core.Runtime // another new AppDomain has come online and is wanting to take over. In that case, we will not // acquire. + _logger.Info("Cannot acquire, another booting application detected."); + return false; } } @@ -253,6 +258,7 @@ namespace Umbraco.Core.Runtime // which isn't ideal. // So... we're going to 'just' take over, if the writelock works then we'll assume we're ok + _logger.Info("Timeout elapsed, assuming orphan row, acquiring MainDom."); try { @@ -262,6 +268,7 @@ namespace Umbraco.Core.Runtime // so now we update the row with our appdomain id InsertLockRecord(_lockId); + _logger.Info("Acquired with ID {LockId}", _lockId); return true; } catch (Exception ex) From 0d1101eaa748180d350662beee6040d203bda37f Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 30 Dec 2019 18:14:18 +1100 Subject: [PATCH 111/202] adds notes/logging --- src/Umbraco.Core/Runtime/SqlMainDomLock.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs index a2560b71a3..d7b800c364 100644 --- a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs +++ b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs @@ -112,6 +112,10 @@ namespace Umbraco.Core.Runtime { while(true) { + // If cancellation has been requested we will just exit. Depending on timing of the shutdown, + // we will have already flagged _mainDomChanging = true, or we're shutting down faster than + // the other MainDom is taking to startup. In this case the db row will just be deleted and the + // new MainDom will just take over. if (_cancellationTokenSource.IsCancellationRequested) break; @@ -135,6 +139,7 @@ namespace Umbraco.Core.Runtime { // we are no longer main dom, another one has come online, exit _mainDomChanging = true; + _logger.Info("Detected new booting application, releasing MainDom."); return; } } @@ -359,10 +364,12 @@ namespace Umbraco.Core.Runtime // Otherwise, if we are just shutting down, we want to just delete the row. if (mainDomChanging) { + _logger.Info("Releasing MainDom, updating row, new application is booting."); db.Execute("UPDATE umbracoKeyValue SET [value] = [value] + '_updated' WHERE [key] = @key", new { key = MainDomKey }); } else { + _logger.Info("Releasing MainDom, deleting row, application is shutting down."); db.Execute("DELETE FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey }); } } From 5476388e787c778852802042267f1f20ceb3aae6 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 30 Dec 2019 18:49:33 +1100 Subject: [PATCH 112/202] adds lock else we end up detecting when not needed --- src/Umbraco.Core/Runtime/SqlMainDomLock.cs | 143 +++++++++++---------- 1 file changed, 74 insertions(+), 69 deletions(-) diff --git a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs index d7b800c364..bb44bbbfbf 100644 --- a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs +++ b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs @@ -24,6 +24,7 @@ namespace Umbraco.Core.Runtime private bool _mainDomChanging = false; private readonly UmbracoDatabaseFactory _dbFactory; private bool _hasError; + private object _locker = new object(); public SqlMainDomLock(ILogger logger) { @@ -112,49 +113,53 @@ namespace Umbraco.Core.Runtime { while(true) { - // If cancellation has been requested we will just exit. Depending on timing of the shutdown, - // we will have already flagged _mainDomChanging = true, or we're shutting down faster than - // the other MainDom is taking to startup. In this case the db row will just be deleted and the - // new MainDom will just take over. - if (_cancellationTokenSource.IsCancellationRequested) - break; - // poll every 1 second Thread.Sleep(1000); - var db = GetDatabase(); - - try + lock(_locker) { - db.BeginTransaction(IsolationLevel.ReadCommitted); + // If cancellation has been requested we will just exit. Depending on timing of the shutdown, + // we will have already flagged _mainDomChanging = true, or we're shutting down faster than + // the other MainDom is taking to startup. In this case the db row will just be deleted and the + // new MainDom will just take over. + if (_cancellationTokenSource.IsCancellationRequested) + break; - // get a read lock - _sqlServerSyntax.ReadLock(db, Constants.Locks.MainDom); + var db = GetDatabase(); - // 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)) + try { - // we are no longer main dom, another one has come online, exit - _mainDomChanging = true; - _logger.Info("Detected new booting application, releasing MainDom."); + 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)) + { + // we are no longer main dom, another one has come online, exit + _mainDomChanging = true; + _logger.Info("Detected new booting application, releasing MainDom."); + return; + } + } + catch (Exception ex) + { + ResetDatabase(); + // unexpected + _logger.Error(ex, "Unexpected error, listening is canceled."); + _hasError = true; return; } + finally + { + db?.CompleteTransaction(); + } } - catch (Exception ex) - { - ResetDatabase(); - // unexpected - _logger.Error(ex, "Unexpected error, listening is canceled."); - _hasError = true; - return; - } - finally - { - db?.CompleteTransaction(); - } + } @@ -341,48 +346,48 @@ namespace Umbraco.Core.Runtime { if (disposing) { - // capture locally just in case in some strange way a sub task somehow updates this - var mainDomChanging = _mainDomChanging; - - // immediately cancel all sub-tasks, we don't want them to keep querying - _cancellationTokenSource.Cancel(); - _cancellationTokenSource.Dispose(); - - var db = GetDatabase(); - try + lock (_locker) { - db.BeginTransaction(IsolationLevel.ReadCommitted); + // immediately cancel all sub-tasks, we don't want them to keep querying + _cancellationTokenSource.Cancel(); + _cancellationTokenSource.Dispose(); - // get a write lock - _sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom); - - // When we are disposed, it means we have released the MainDom lock - // and called all MainDom release callbacks, in this case - // if another maindom is actually coming online we need - // to signal to the MainDom coming online that we have shutdown. - // To do that, we update the existing main dom DB record with a suffixed "_updated" string. - // Otherwise, if we are just shutting down, we want to just delete the row. - if (mainDomChanging) + var db = GetDatabase(); + try { - _logger.Info("Releasing MainDom, updating row, new application is booting."); - db.Execute("UPDATE umbracoKeyValue SET [value] = [value] + '_updated' WHERE [key] = @key", new { key = MainDomKey }); + db.BeginTransaction(IsolationLevel.ReadCommitted); + + // get a write lock + _sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom); + + // When we are disposed, it means we have released the MainDom lock + // and called all MainDom release callbacks, in this case + // if another maindom is actually coming online we need + // to signal to the MainDom coming online that we have shutdown. + // To do that, we update the existing main dom DB record with a suffixed "_updated" string. + // Otherwise, if we are just shutting down, we want to just delete the row. + if (_mainDomChanging) + { + _logger.Info("Releasing MainDom, updating row, new application is booting."); + db.Execute("UPDATE umbracoKeyValue SET [value] = [value] + '_updated' WHERE [key] = @key", new { key = MainDomKey }); + } + else + { + _logger.Info("Releasing MainDom, deleting row, application is shutting down."); + db.Execute("DELETE FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey }); + } } - else + catch (Exception ex) { - _logger.Info("Releasing MainDom, deleting row, application is shutting down."); - db.Execute("DELETE FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey }); + ResetDatabase(); + _logger.Error(ex, "Unexpected error during dipsose."); + _hasError = true; + } + finally + { + db?.CompleteTransaction(); + ResetDatabase(); } - } - catch (Exception ex) - { - ResetDatabase(); - _logger.Error(ex, "Unexpected error during dipsose."); - _hasError = true; - } - finally - { - db?.CompleteTransaction(); - ResetDatabase(); } } From b24a87d9868d70bcbc164559f74caab744785b5c Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 30 Dec 2019 18:51:38 +1100 Subject: [PATCH 113/202] Changes logging to debug --- src/Umbraco.Core/Runtime/SqlMainDomLock.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs index bb44bbbfbf..847a0c25df 100644 --- a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs +++ b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs @@ -39,7 +39,7 @@ namespace Umbraco.Core.Runtime public async Task AcquireLockAsync(int millisecondsTimeout) { - _logger.Info("Acquiring lock..."); + _logger.Debug("Acquiring lock..."); var db = GetDatabase(); @@ -76,7 +76,7 @@ namespace Umbraco.Core.Runtime // are MainDom? then we don't leave any orphan rows behind. InsertLockRecord(_lockId); // so update with our appdomain id - _logger.Info("Acquired with ID {LockId}", _lockId); + _logger.Debug("Acquired with ID {LockId}", _lockId); return true; } @@ -142,7 +142,7 @@ namespace Umbraco.Core.Runtime { // we are no longer main dom, another one has come online, exit _mainDomChanging = true; - _logger.Info("Detected new booting application, releasing MainDom."); + _logger.Debug("Detected new booting application, releasing MainDom."); return; } } @@ -225,7 +225,7 @@ namespace Umbraco.Core.Runtime // so now we update the row with our appdomain id InsertLockRecord(_lockId); - _logger.Info("Acquired with ID {LockId}", _lockId); + _logger.Debug("Acquired with ID {LockId}", _lockId); return true; } else if (mainDomRows.Count == 1 && !mainDomRows[0].Value.StartsWith(tempId)) @@ -234,7 +234,7 @@ namespace Umbraco.Core.Runtime // another new AppDomain has come online and is wanting to take over. In that case, we will not // acquire. - _logger.Info("Cannot acquire, another booting application detected."); + _logger.Debug("Cannot acquire, another booting application detected."); return false; } @@ -268,7 +268,7 @@ namespace Umbraco.Core.Runtime // which isn't ideal. // So... we're going to 'just' take over, if the writelock works then we'll assume we're ok - _logger.Info("Timeout elapsed, assuming orphan row, acquiring MainDom."); + _logger.Debug("Timeout elapsed, assuming orphan row, acquiring MainDom."); try { @@ -278,7 +278,7 @@ namespace Umbraco.Core.Runtime // so now we update the row with our appdomain id InsertLockRecord(_lockId); - _logger.Info("Acquired with ID {LockId}", _lockId); + _logger.Debug("Acquired with ID {LockId}", _lockId); return true; } catch (Exception ex) @@ -368,12 +368,12 @@ namespace Umbraco.Core.Runtime // Otherwise, if we are just shutting down, we want to just delete the row. if (_mainDomChanging) { - _logger.Info("Releasing MainDom, updating row, new application is booting."); + _logger.Debug("Releasing MainDom, updating row, new application is booting."); db.Execute("UPDATE umbracoKeyValue SET [value] = [value] + '_updated' WHERE [key] = @key", new { key = MainDomKey }); } else { - _logger.Info("Releasing MainDom, deleting row, application is shutting down."); + _logger.Debug("Releasing MainDom, deleting row, application is shutting down."); db.Execute("DELETE FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey }); } } From 00c913d6217ecd9dfe16454d0f883cf034e328f4 Mon Sep 17 00:00:00 2001 From: abi Date: Thu, 2 Jan 2020 12:02:48 +0000 Subject: [PATCH 114/202] add InternalSearchConstants --- src/Umbraco.Web/InternalSearchConstants.cs | 32 +++++++++++++++++++ src/Umbraco.Web/Runtime/WebInitialComposer.cs | 2 +- .../Search/IInternalSearchConstants.cs | 13 ++++++++ src/Umbraco.Web/Search/UmbracoTreeSearcher.cs | 12 ++++--- src/Umbraco.Web/Umbraco.Web.csproj | 2 ++ 5 files changed, 56 insertions(+), 5 deletions(-) create mode 100644 src/Umbraco.Web/InternalSearchConstants.cs create mode 100644 src/Umbraco.Web/Search/IInternalSearchConstants.cs diff --git a/src/Umbraco.Web/InternalSearchConstants.cs b/src/Umbraco.Web/InternalSearchConstants.cs new file mode 100644 index 0000000000..012cd98f1d --- /dev/null +++ b/src/Umbraco.Web/InternalSearchConstants.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using Umbraco.Examine; +using Umbraco.Web.Search; + +namespace Umbraco.Web +{ + public class InternalSearchConstants : IInternalSearchConstants + { + private List _backOfficeFields = new List {"id", "__NodeId", "__Key"}; + public List GetBackOfficeFields() + { + return _backOfficeFields; + } + + + private List _backOfficeMembersFields = new List {"email", "loginName"}; + public List GetBackOfficeMembersFields() + { + return _backOfficeMembersFields; + } + private List _backOfficeMediaFields = new List {UmbracoExamineIndex.UmbracoFileFieldName }; + public List GetBackOfficeMediaFields() + { + return _backOfficeMediaFields; + } + private List _backOfficeDocumentFields = new List (); + public List GetBackOfficeDocumentFields() + { + return _backOfficeDocumentFields; + } + } +} diff --git a/src/Umbraco.Web/Runtime/WebInitialComposer.cs b/src/Umbraco.Web/Runtime/WebInitialComposer.cs index 87c0f46fba..b8be2319d8 100644 --- a/src/Umbraco.Web/Runtime/WebInitialComposer.cs +++ b/src/Umbraco.Web/Runtime/WebInitialComposer.cs @@ -95,7 +95,7 @@ namespace Umbraco.Web.Runtime // a way to inject the UmbracoContext - DO NOT register this as Lifetime.Request since LI will dispose the context // in it's own way but we don't want that to happen, we manage its lifetime ourselves. composition.Register(factory => factory.GetInstance().UmbracoContext); - + composition.RegisterUnique(); composition.Register(factory => { var umbCtx = factory.GetInstance(); diff --git a/src/Umbraco.Web/Search/IInternalSearchConstants.cs b/src/Umbraco.Web/Search/IInternalSearchConstants.cs new file mode 100644 index 0000000000..72a37deaa1 --- /dev/null +++ b/src/Umbraco.Web/Search/IInternalSearchConstants.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +namespace Umbraco.Web.Search +{ + public interface IInternalSearchConstants + { + List GetBackOfficeFields(); + List GetBackOfficeMembersFields(); + + List GetBackOfficeMediaFields(); + List GetBackOfficeDocumentFields(); + } +} diff --git a/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs b/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs index 463f4b09df..1345ffd4cc 100644 --- a/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs +++ b/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs @@ -28,13 +28,15 @@ namespace Umbraco.Web.Search private readonly IEntityService _entityService; private readonly UmbracoMapper _mapper; private readonly ISqlContext _sqlContext; + private readonly IInternalSearchConstants _internalSearchConstants; + public UmbracoTreeSearcher(IExamineManager examineManager, UmbracoContext umbracoContext, ILocalizationService languageService, IEntityService entityService, UmbracoMapper mapper, - ISqlContext sqlContext) + ISqlContext sqlContext,IInternalSearchConstants internalSearchConstants) { _examineManager = examineManager ?? throw new ArgumentNullException(nameof(examineManager)); _umbracoContext = umbracoContext; @@ -42,6 +44,7 @@ namespace Umbraco.Web.Search _entityService = entityService; _mapper = mapper; _sqlContext = sqlContext; + _internalSearchConstants = internalSearchConstants; } /// @@ -67,7 +70,7 @@ namespace Umbraco.Web.Search string type; var indexName = Constants.UmbracoIndexes.InternalIndexName; - var fields = new List { "id", "__NodeId", "__Key" }; + var fields = _internalSearchConstants.GetBackOfficeFields(); // TODO: WE should try to allow passing in a lucene raw query, however we will still need to do some manual string // manipulation for things like start paths, member types, etc... @@ -87,7 +90,7 @@ namespace Umbraco.Web.Search case UmbracoEntityTypes.Member: indexName = Constants.UmbracoIndexes.MembersIndexName; type = "member"; - fields.AddRange(new[]{ "email", "loginName"}); + fields.AddRange(_internalSearchConstants.GetBackOfficeMembersFields()); if (searchFrom != null && searchFrom != Constants.Conventions.MemberTypes.AllMembersListId && searchFrom.Trim() != "-1") { sb.Append("+__NodeTypeAlias:"); @@ -97,12 +100,13 @@ namespace Umbraco.Web.Search break; case UmbracoEntityTypes.Media: type = "media"; - fields.AddRange(new[] { UmbracoExamineIndex.UmbracoFileFieldName }); + fields.AddRange(_internalSearchConstants.GetBackOfficeMediaFields()); var allMediaStartNodes = _umbracoContext.Security.CurrentUser.CalculateMediaStartNodeIds(_entityService); AppendPath(sb, UmbracoObjectTypes.Media, allMediaStartNodes, searchFrom, ignoreUserStartNodes, _entityService); break; case UmbracoEntityTypes.Document: type = "content"; + fields.AddRange(_internalSearchConstants.GetBackOfficeDocumentFields()); var allContentStartNodes = _umbracoContext.Security.CurrentUser.CalculateContentStartNodeIds(_entityService); AppendPath(sb, UmbracoObjectTypes.Document, allContentStartNodes, searchFrom, ignoreUserStartNodes, _entityService); break; diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 5d29e53d4a..159abd435b 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -155,6 +155,7 @@ + @@ -249,6 +250,7 @@ + From 9614dcf0b08bcca001e423733e2dc7f2826a2de8 Mon Sep 17 00:00:00 2001 From: abi Date: Thu, 2 Jan 2020 13:20:32 +0000 Subject: [PATCH 115/202] move InternalSearchConstants to correct place in source code --- src/Umbraco.Web/{ => Search}/InternalSearchConstants.cs | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/Umbraco.Web/{ => Search}/InternalSearchConstants.cs (100%) diff --git a/src/Umbraco.Web/InternalSearchConstants.cs b/src/Umbraco.Web/Search/InternalSearchConstants.cs similarity index 100% rename from src/Umbraco.Web/InternalSearchConstants.cs rename to src/Umbraco.Web/Search/InternalSearchConstants.cs From 575ec6fec4e5c0f2aa94a2d4efb216fddef54c18 Mon Sep 17 00:00:00 2001 From: abi Date: Thu, 2 Jan 2020 13:39:23 +0000 Subject: [PATCH 116/202] Correct project references --- src/Umbraco.Web/Umbraco.Web.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 159abd435b..0980b1b3d6 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -155,7 +155,6 @@ - @@ -251,6 +250,7 @@ + From 12f51dec4e6ee3c4b64719799066749feae04cd5 Mon Sep 17 00:00:00 2001 From: abi Date: Thu, 2 Jan 2020 23:00:53 +0000 Subject: [PATCH 117/202] Use immutable types --- .../Search/IInternalSearchConstants.cs | 10 +++++----- .../Search/InternalSearchConstants.cs | 16 ++++++++-------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/Umbraco.Web/Search/IInternalSearchConstants.cs b/src/Umbraco.Web/Search/IInternalSearchConstants.cs index 72a37deaa1..06222e2ec7 100644 --- a/src/Umbraco.Web/Search/IInternalSearchConstants.cs +++ b/src/Umbraco.Web/Search/IInternalSearchConstants.cs @@ -4,10 +4,10 @@ namespace Umbraco.Web.Search { public interface IInternalSearchConstants { - List GetBackOfficeFields(); - List GetBackOfficeMembersFields(); - - List GetBackOfficeMediaFields(); - List GetBackOfficeDocumentFields(); + IEnumerable GetBackOfficeFields(); + IEnumerable GetBackOfficeMembersFields(); + + IEnumerable GetBackOfficeMediaFields(); + IEnumerable GetBackOfficeDocumentFields(); } } diff --git a/src/Umbraco.Web/Search/InternalSearchConstants.cs b/src/Umbraco.Web/Search/InternalSearchConstants.cs index 012cd98f1d..51e5df4c4e 100644 --- a/src/Umbraco.Web/Search/InternalSearchConstants.cs +++ b/src/Umbraco.Web/Search/InternalSearchConstants.cs @@ -6,25 +6,25 @@ namespace Umbraco.Web { public class InternalSearchConstants : IInternalSearchConstants { - private List _backOfficeFields = new List {"id", "__NodeId", "__Key"}; - public List GetBackOfficeFields() + private IReadOnlyList _backOfficeFields = new List {"id", "__NodeId", "__Key"}; + public IEnumerable GetBackOfficeFields() { return _backOfficeFields; } - private List _backOfficeMembersFields = new List {"email", "loginName"}; - public List GetBackOfficeMembersFields() + private IReadOnlyList _backOfficeMembersFields = new List {"email", "loginName"}; + public IEnumerable GetBackOfficeMembersFields() { return _backOfficeMembersFields; } - private List _backOfficeMediaFields = new List {UmbracoExamineIndex.UmbracoFileFieldName }; - public List GetBackOfficeMediaFields() + private IReadOnlyList _backOfficeMediaFields = new List {UmbracoExamineIndex.UmbracoFileFieldName }; + public IEnumerable GetBackOfficeMediaFields() { return _backOfficeMediaFields; } - private List _backOfficeDocumentFields = new List (); - public List GetBackOfficeDocumentFields() + private IReadOnlyList _backOfficeDocumentFields = new List (); + public IEnumerable GetBackOfficeDocumentFields() { return _backOfficeDocumentFields; } From 3b8e6d3cc05621c0c402b807902cf0e549e2d6e9 Mon Sep 17 00:00:00 2001 From: abi Date: Thu, 2 Jan 2020 23:05:08 +0000 Subject: [PATCH 118/202] rename to UmbracoTreeSearcherFields --- src/Umbraco.Web/Runtime/WebInitialComposer.cs | 6 +++--- ...hConstants.cs => IUmbracoTreeSearcherFields.cs} | 2 +- src/Umbraco.Web/Search/UmbracoTreeSearcher.cs | 14 +++++++------- ...chConstants.cs => UmbracoTreeSearcherFields.cs} | 2 +- 4 files changed, 12 insertions(+), 12 deletions(-) rename src/Umbraco.Web/Search/{IInternalSearchConstants.cs => IUmbracoTreeSearcherFields.cs} (86%) rename src/Umbraco.Web/Search/{InternalSearchConstants.cs => UmbracoTreeSearcherFields.cs} (93%) diff --git a/src/Umbraco.Web/Runtime/WebInitialComposer.cs b/src/Umbraco.Web/Runtime/WebInitialComposer.cs index b8be2319d8..cc6cdc1f60 100644 --- a/src/Umbraco.Web/Runtime/WebInitialComposer.cs +++ b/src/Umbraco.Web/Runtime/WebInitialComposer.cs @@ -73,7 +73,7 @@ namespace Umbraco.Web.Runtime // register accessors for cultures composition.RegisterUnique(); composition.RegisterUnique(); - + // register the http context and umbraco context accessors // we *should* use the HttpContextUmbracoContextAccessor, however there are cases when // we have no http context, eg when booting Umbraco or in background threads, so instead @@ -95,7 +95,7 @@ namespace Umbraco.Web.Runtime // a way to inject the UmbracoContext - DO NOT register this as Lifetime.Request since LI will dispose the context // in it's own way but we don't want that to happen, we manage its lifetime ourselves. composition.Register(factory => factory.GetInstance().UmbracoContext); - composition.RegisterUnique(); + composition.RegisterUnique(); composition.Register(factory => { var umbCtx = factory.GetInstance(); @@ -268,7 +268,7 @@ namespace Umbraco.Web.Runtime .Append() .Append() .Append(); - + // replace with web implementation composition.RegisterUnique(); diff --git a/src/Umbraco.Web/Search/IInternalSearchConstants.cs b/src/Umbraco.Web/Search/IUmbracoTreeSearcherFields.cs similarity index 86% rename from src/Umbraco.Web/Search/IInternalSearchConstants.cs rename to src/Umbraco.Web/Search/IUmbracoTreeSearcherFields.cs index 06222e2ec7..eaa5d743a1 100644 --- a/src/Umbraco.Web/Search/IInternalSearchConstants.cs +++ b/src/Umbraco.Web/Search/IUmbracoTreeSearcherFields.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; namespace Umbraco.Web.Search { - public interface IInternalSearchConstants + public interface IUmbracoTreeSearcherFields { IEnumerable GetBackOfficeFields(); IEnumerable GetBackOfficeMembersFields(); diff --git a/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs b/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs index 1345ffd4cc..56c7c6fbf3 100644 --- a/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs +++ b/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs @@ -28,7 +28,7 @@ namespace Umbraco.Web.Search private readonly IEntityService _entityService; private readonly UmbracoMapper _mapper; private readonly ISqlContext _sqlContext; - private readonly IInternalSearchConstants _internalSearchConstants; + private readonly IUmbracoTreeSearcherFields _umbracoTreeSearcherFields; public UmbracoTreeSearcher(IExamineManager examineManager, @@ -36,7 +36,7 @@ namespace Umbraco.Web.Search ILocalizationService languageService, IEntityService entityService, UmbracoMapper mapper, - ISqlContext sqlContext,IInternalSearchConstants internalSearchConstants) + ISqlContext sqlContext,IUmbracoTreeSearcherFields umbracoTreeSearcherFields) { _examineManager = examineManager ?? throw new ArgumentNullException(nameof(examineManager)); _umbracoContext = umbracoContext; @@ -44,7 +44,7 @@ namespace Umbraco.Web.Search _entityService = entityService; _mapper = mapper; _sqlContext = sqlContext; - _internalSearchConstants = internalSearchConstants; + _umbracoTreeSearcherFields = umbracoTreeSearcherFields; } /// @@ -70,7 +70,7 @@ namespace Umbraco.Web.Search string type; var indexName = Constants.UmbracoIndexes.InternalIndexName; - var fields = _internalSearchConstants.GetBackOfficeFields(); + var fields = _umbracoTreeSearcherFields.GetBackOfficeFields(); // TODO: WE should try to allow passing in a lucene raw query, however we will still need to do some manual string // manipulation for things like start paths, member types, etc... @@ -90,7 +90,7 @@ namespace Umbraco.Web.Search case UmbracoEntityTypes.Member: indexName = Constants.UmbracoIndexes.MembersIndexName; type = "member"; - fields.AddRange(_internalSearchConstants.GetBackOfficeMembersFields()); + fields.AddRange(_umbracoTreeSearcherFields.GetBackOfficeMembersFields()); if (searchFrom != null && searchFrom != Constants.Conventions.MemberTypes.AllMembersListId && searchFrom.Trim() != "-1") { sb.Append("+__NodeTypeAlias:"); @@ -100,13 +100,13 @@ namespace Umbraco.Web.Search break; case UmbracoEntityTypes.Media: type = "media"; - fields.AddRange(_internalSearchConstants.GetBackOfficeMediaFields()); + fields.AddRange(_umbracoTreeSearcherFields.GetBackOfficeMediaFields()); var allMediaStartNodes = _umbracoContext.Security.CurrentUser.CalculateMediaStartNodeIds(_entityService); AppendPath(sb, UmbracoObjectTypes.Media, allMediaStartNodes, searchFrom, ignoreUserStartNodes, _entityService); break; case UmbracoEntityTypes.Document: type = "content"; - fields.AddRange(_internalSearchConstants.GetBackOfficeDocumentFields()); + fields.AddRange(_umbracoTreeSearcherFields.GetBackOfficeDocumentFields()); var allContentStartNodes = _umbracoContext.Security.CurrentUser.CalculateContentStartNodeIds(_entityService); AppendPath(sb, UmbracoObjectTypes.Document, allContentStartNodes, searchFrom, ignoreUserStartNodes, _entityService); break; diff --git a/src/Umbraco.Web/Search/InternalSearchConstants.cs b/src/Umbraco.Web/Search/UmbracoTreeSearcherFields.cs similarity index 93% rename from src/Umbraco.Web/Search/InternalSearchConstants.cs rename to src/Umbraco.Web/Search/UmbracoTreeSearcherFields.cs index 51e5df4c4e..d1c663024b 100644 --- a/src/Umbraco.Web/Search/InternalSearchConstants.cs +++ b/src/Umbraco.Web/Search/UmbracoTreeSearcherFields.cs @@ -4,7 +4,7 @@ using Umbraco.Web.Search; namespace Umbraco.Web { - public class InternalSearchConstants : IInternalSearchConstants + public class UmbracoTreeSearcherFields : IUmbracoTreeSearcherFields { private IReadOnlyList _backOfficeFields = new List {"id", "__NodeId", "__Key"}; public IEnumerable GetBackOfficeFields() From 358b4c9133ac2f4a2cc45cff21cfbe325c3a57fd Mon Sep 17 00:00:00 2001 From: abi Date: Thu, 2 Jan 2020 23:08:53 +0000 Subject: [PATCH 119/202] Make fields list to allow to add fields --- src/Umbraco.Web/Search/UmbracoTreeSearcher.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs b/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs index 56c7c6fbf3..146177f86f 100644 --- a/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs +++ b/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs @@ -70,7 +70,7 @@ namespace Umbraco.Web.Search string type; var indexName = Constants.UmbracoIndexes.InternalIndexName; - var fields = _umbracoTreeSearcherFields.GetBackOfficeFields(); + var fields = _umbracoTreeSearcherFields.GetBackOfficeFields().ToList(); // TODO: WE should try to allow passing in a lucene raw query, however we will still need to do some manual string // manipulation for things like start paths, member types, etc... From a46e9124d28799067377da5d969d2730425f57bb Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 3 Jan 2020 10:38:48 +1100 Subject: [PATCH 120/202] First commit in fixing deadlock - committing my notes, etc... --- src/Umbraco.Core/Runtime/CoreRuntime.cs | 2 +- src/Umbraco.Core/Runtime/MainDom.cs | 27 ++++- .../Runtime/MainDomSemaphoreLock.cs | 9 +- src/Umbraco.Core/Runtime/SqlMainDomLock.cs | 88 +++++++-------- .../PublishedCache/NuCache/ContentStore.cs | 101 ++++++++++++++---- .../NuCache/PublishedSnapshot.cs | 8 ++ .../NuCache/PublishedSnapshotService.cs | 25 ++++- .../NuCache/{notes.txt => readme.md} | 44 ++++---- src/Umbraco.Web/Umbraco.Web.csproj | 4 +- src/Umbraco.Web/UmbracoApplication.cs | 2 +- 10 files changed, 207 insertions(+), 103 deletions(-) rename src/Umbraco.Web/PublishedCache/NuCache/{notes.txt => readme.md} (70%) diff --git a/src/Umbraco.Core/Runtime/CoreRuntime.cs b/src/Umbraco.Core/Runtime/CoreRuntime.cs index b46058fa82..b852aff2ff 100644 --- a/src/Umbraco.Core/Runtime/CoreRuntime.cs +++ b/src/Umbraco.Core/Runtime/CoreRuntime.cs @@ -147,7 +147,7 @@ namespace Umbraco.Core.Runtime // TODO: remove this in netcore, this is purely backwards compat hacks with the empty ctor if (MainDom == null) { - MainDom = new MainDom(Logger, new MainDomSemaphoreLock()); + MainDom = new MainDom(Logger, new MainDomSemaphoreLock(Logger)); } diff --git a/src/Umbraco.Core/Runtime/MainDom.cs b/src/Umbraco.Core/Runtime/MainDom.cs index 85cf5ad6a9..883b69b2a7 100644 --- a/src/Umbraco.Core/Runtime/MainDom.cs +++ b/src/Umbraco.Core/Runtime/MainDom.cs @@ -36,7 +36,7 @@ namespace Umbraco.Core.Runtime // actions to run before releasing the main domain private readonly List> _callbacks = new List>(); - private const int LockTimeoutMilliseconds = 90000; // (1.5 * 60 * 1000) == 1 min 30 seconds + private const int LockTimeoutMilliseconds = 10000; // 10 seconds #endregion @@ -106,6 +106,7 @@ namespace Umbraco.Core.Runtime { _logger.Info("Stopping ({SignalSource})", source); foreach (var callback in _callbacks.OrderBy(x => x.Key).Select(x => x.Value)) + { try { callback(); // no timeout on callbacks @@ -115,6 +116,8 @@ namespace Umbraco.Core.Runtime _logger.Error(e, "Error while running callback"); continue; } + } + _logger.Debug("Stopped ({SignalSource})", source); } finally @@ -142,17 +145,33 @@ namespace Umbraco.Core.Runtime _logger.Info("Acquiring."); // Get the lock - _mainDomLock.AcquireLockAsync(LockTimeoutMilliseconds).Wait(); + var acquired = _mainDomLock.AcquireLockAsync(LockTimeoutMilliseconds).Result; + + if (!acquired) + { + _logger.Info("Cannot acquire (timeout)."); + + // TODO: Previously we'd throw an exception and the appdomain would not start, what do we want to do? + + // return false; + + throw new TimeoutException("Cannot acquire MainDom"); + } try { // Listen for the signal from another AppDomain coming online to release the lock _mainDomLock.ListenAsync() - .ContinueWith(_ => OnSignal("signal")); + .ContinueWith(_ => + { + _logger.Debug("Signal heard from other appdomain."); + OnSignal("signal"); + }); } - catch (OperationCanceledException) + catch (OperationCanceledException ex) { // the waiting task could be canceled if this appdomain is naturally shutting down, we'll just swallow this exception + _logger.Warn(ex, ex.Message); } _logger.Info("Acquired."); diff --git a/src/Umbraco.Core/Runtime/MainDomSemaphoreLock.cs b/src/Umbraco.Core/Runtime/MainDomSemaphoreLock.cs index 89eef8a658..2dcc37e25f 100644 --- a/src/Umbraco.Core/Runtime/MainDomSemaphoreLock.cs +++ b/src/Umbraco.Core/Runtime/MainDomSemaphoreLock.cs @@ -1,6 +1,7 @@ using System; using System.Threading; using System.Threading.Tasks; +using Umbraco.Core.Logging; namespace Umbraco.Core.Runtime { @@ -14,16 +15,17 @@ namespace Umbraco.Core.Runtime // event wait handle used to notify current main domain that it should // release the lock because a new domain wants to be the main domain private readonly EventWaitHandle _signal; - + private readonly ILogger _logger; private IDisposable _lockRelease; - public MainDomSemaphoreLock() + public MainDomSemaphoreLock(ILogger logger) { var lockName = "UMBRACO-" + MainDom.GetMainDomId() + "-MAINDOM-LCK"; _systemLock = new SystemLock(lockName); var eventName = "UMBRACO-" + MainDom.GetMainDomId() + "-MAINDOM-EVT"; _signal = new EventWaitHandle(false, EventResetMode.AutoReset, eventName); + _logger = logger; } //WaitOneAsync (ext method) will wait for a signal without blocking the main thread, the waiting is done on a background thread @@ -47,8 +49,9 @@ namespace Umbraco.Core.Runtime _lockRelease = _systemLock.Lock(millisecondsTimeout); return Task.FromResult(true); } - catch (TimeoutException) + catch (TimeoutException ex) { + _logger.Error(ex); return Task.FromResult(false); } finally diff --git a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs index 847a0c25df..31f8b36ed0 100644 --- a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs +++ b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs @@ -109,62 +109,62 @@ namespace Umbraco.Core.Runtime // Create a long running task (dedicated thread) // to poll to check if we are still the MainDom registered in the DB - return Task.Factory.StartNew(() => + return Task.Factory.StartNew(ListeningLoop, _cancellationTokenSource.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default); + + } + + private void ListeningLoop() + { + while (true) { - while(true) + // poll every 1 second + Thread.Sleep(1000); + + lock (_locker) { - // poll every 1 second - Thread.Sleep(1000); + // If cancellation has been requested we will just exit. Depending on timing of the shutdown, + // we will have already flagged _mainDomChanging = true, or we're shutting down faster than + // the other MainDom is taking to startup. In this case the db row will just be deleted and the + // new MainDom will just take over. + if (_cancellationTokenSource.IsCancellationRequested) + return; - lock(_locker) + var db = GetDatabase(); + + try { - // If cancellation has been requested we will just exit. Depending on timing of the shutdown, - // we will have already flagged _mainDomChanging = true, or we're shutting down faster than - // the other MainDom is taking to startup. In this case the db row will just be deleted and the - // new MainDom will just take over. - if (_cancellationTokenSource.IsCancellationRequested) - break; + db.BeginTransaction(IsolationLevel.ReadCommitted); - var db = GetDatabase(); + // get a read lock + _sqlServerSyntax.ReadLock(db, Constants.Locks.MainDom); - try + // 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)) { - 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)) - { - // we are no longer main dom, another one has come online, exit - _mainDomChanging = true; - _logger.Debug("Detected new booting application, releasing MainDom."); - return; - } - } - catch (Exception ex) - { - ResetDatabase(); - // unexpected - _logger.Error(ex, "Unexpected error, listening is canceled."); - _hasError = true; + // we are no longer main dom, another one has come online, exit + _mainDomChanging = true; + _logger.Debug("Detected new booting application, releasing MainDom lock."); return; } - finally - { - db?.CompleteTransaction(); - } } - + catch (Exception ex) + { + ResetDatabase(); + // unexpected + _logger.Error(ex, "Unexpected error, listening is canceled."); + _hasError = true; + return; + } + finally + { + db?.CompleteTransaction(); + } } - - - }, _cancellationTokenSource.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default); + } } private void ResetDatabase() diff --git a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs index 550fd507d5..81229af9e1 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs @@ -115,11 +115,14 @@ namespace Umbraco.Web.PublishedCache.NuCache // TODO: GetScopedWriter? should the dict have a ref onto the scope provider? public IDisposable GetScopedWriteLock(IScopeProvider scopeProvider) { + _logger.Debug("GetScopedWriteLock"); return ScopeContextualBase.Get(scopeProvider, _instanceId, scoped => new ScopedWriteLock(this, scoped)); } private void Lock(WriteLockInfo lockInfo, bool forceGen = false) { + // TODO: We are in a deadlock here somehow?? + Monitor.Enter(_wlocko, ref lockInfo.Taken); var rtaken = false; @@ -193,8 +196,15 @@ namespace Umbraco.Web.PublishedCache.NuCache _localDb.Commit(); } - if (lockInfo.Count) _wlocked--; - if (lockInfo.Taken) Monitor.Exit(_wlocko); + // TODO: Shouldn't this be in a finally block? + // TODO: Shouldn't this be decremented after we exit?? + // TODO: Shouldn't the locked flag never exceed 1? + if (lockInfo.Count) + _wlocked--; + + // TODO: Shouldn't this be in a finally block? + if (lockInfo.Taken) + Monitor.Exit(_wlocko); } private void Release(ReadLockInfo lockInfo) @@ -232,21 +242,29 @@ namespace Umbraco.Web.PublishedCache.NuCache public void ReleaseLocalDb() { + _logger.Info("Releasing DB..."); var lockInfo = new WriteLockInfo(); try { try { // Trying to lock could throw exceptions so always make sure to clean up. + _logger.Info("Trying to lock before releasing DB (lock count = {LockCount}) ...", _wlocked); + Lock(lockInfo); } finally { try { + _logger.Info("Disposing local DB..."); _localDb?.Dispose(); } - catch { /* TBD: May already be throwing so don't throw again */} + catch (Exception ex) + { + /* TBD: May already be throwing so don't throw again */ + _logger.Error(ex, "Error trying to release DB"); + } finally { _localDb = null; @@ -254,8 +272,17 @@ namespace Umbraco.Web.PublishedCache.NuCache } } + catch (Exception ex) + { + _logger.Error(ex, "Error trying to lock"); + throw; + } finally { + _logger.Info("Releasing ContentStore..."); + + // TODO: I don't understand this, we would have already closed the BPlusTree store above?? + // I guess it's 'safe' and will just decrement the write lock counter? Release(lockInfo); } } @@ -275,6 +302,7 @@ namespace Umbraco.Web.PublishedCache.NuCache var lockInfo = new WriteLockInfo(); try { + _logger.Debug("NewContentTypes"); Lock(lockInfo); foreach (var type in types) @@ -297,6 +325,7 @@ namespace Umbraco.Web.PublishedCache.NuCache var lockInfo = new WriteLockInfo(); try { + _logger.Debug("UpdateContentTypes"); Lock(lockInfo); var index = types.ToDictionary(x => x.Id, x => x); @@ -324,10 +353,13 @@ namespace Umbraco.Web.PublishedCache.NuCache public void SetAllContentTypes(IEnumerable types) { - var lockInfo = new WriteLockInfo(); + // TODO: There should be NO lock here! All calls made to this are wrapped in GetScopedWriteLock + + //var lockInfo = new WriteLockInfo(); try { - Lock(lockInfo); + _logger.Debug("SetAllContentTypes"); + //Lock(lockInfo); // clear all existing content types ClearLocked(_contentTypesById); @@ -345,7 +377,7 @@ namespace Umbraco.Web.PublishedCache.NuCache } finally { - Release(lockInfo); + //Release(lockInfo); } } @@ -362,6 +394,7 @@ namespace Umbraco.Web.PublishedCache.NuCache var lockInfo = new WriteLockInfo(); try { + _logger.Debug("UpdateContentTypes"); Lock(lockInfo); var removedContentTypeNodes = new List(); @@ -435,6 +468,7 @@ namespace Umbraco.Web.PublishedCache.NuCache var lockInfo = new WriteLockInfo(); try { + _logger.Debug("UpdateDataTypes"); Lock(lockInfo); var contentTypes = _contentTypesById @@ -545,10 +579,11 @@ namespace Umbraco.Web.PublishedCache.NuCache _logger.Debug("Set content ID: {KitNodeId}", kit.Node.Id); - var lockInfo = new WriteLockInfo(); + // TODO: There should be NO locks here, all calls made to this are done within GetScopedWriteLock + //var lockInfo = new WriteLockInfo(); try { - Lock(lockInfo); + //Lock(lockInfo); // get existing _contentNodes.TryGetValue(kit.Node.Id, out var link); @@ -594,7 +629,7 @@ namespace Umbraco.Web.PublishedCache.NuCache } finally { - Release(lockInfo); + //Release(lockInfo); } return true; @@ -623,11 +658,16 @@ namespace Umbraco.Web.PublishedCache.NuCache /// internal bool SetAllFastSorted(IEnumerable kits, bool fromDb) { - var lockInfo = new WriteLockInfo(); + + //TODO: There should be NO locks here all calls made to this are done within GetScopedWriteLock + + //var lockInfo = new WriteLockInfo(); var ok = true; try { - Lock(lockInfo); + _logger.Debug("SetAllFastSorted"); + + //Lock(lockInfo); ClearLocked(_contentNodes); ClearRootLocked(); @@ -665,7 +705,7 @@ namespace Umbraco.Web.PublishedCache.NuCache previousNode = null; // there is no previous sibling } - _logger.Debug($"Set {thisNode.Id} with parent {thisNode.ParentContentId}"); + _logger.Verbose($"Set {thisNode.Id} with parent {thisNode.ParentContentId}"); SetValueLocked(_contentNodes, thisNode.Id, thisNode); // if we are initializing from the database source ensure the local db is updated @@ -689,7 +729,7 @@ namespace Umbraco.Web.PublishedCache.NuCache } finally { - Release(lockInfo); + //Release(lockInfo); } return ok; @@ -697,11 +737,15 @@ namespace Umbraco.Web.PublishedCache.NuCache public bool SetAll(IEnumerable kits) { - var lockInfo = new WriteLockInfo(); + //TODO: There should be NO locks here all calls made to this are done within GetScopedWriteLock + + //var lockInfo = new WriteLockInfo(); var ok = true; try { - Lock(lockInfo); + _logger.Debug("SetAll"); + + //Lock(lockInfo); ClearLocked(_contentNodes); ClearRootLocked(); @@ -717,7 +761,7 @@ namespace Umbraco.Web.PublishedCache.NuCache ok = false; continue; // skip that one } - _logger.Debug($"Set {kit.Node.Id} with parent {kit.Node.ParentContentId}"); + _logger.Verbose($"Set {kit.Node.Id} with parent {kit.Node.ParentContentId}"); SetValueLocked(_contentNodes, kit.Node.Id, kit.Node); if (_localDb != null) RegisterChange(kit.Node.Id, kit); @@ -728,7 +772,7 @@ namespace Umbraco.Web.PublishedCache.NuCache } finally { - Release(lockInfo); + //Release(lockInfo); } return ok; @@ -737,11 +781,15 @@ namespace Umbraco.Web.PublishedCache.NuCache // IMPORTANT kits must be sorted out by LEVEL and by SORT ORDER public bool SetBranch(int rootContentId, IEnumerable kits) { - var lockInfo = new WriteLockInfo(); + //TODO: There should be NO locks here all calls made to this are done within GetScopedWriteLock + + //var lockInfo = new WriteLockInfo(); var ok = true; try { - Lock(lockInfo); + _logger.Debug("SetBranch"); + + //Lock(lockInfo); // get existing _contentNodes.TryGetValue(rootContentId, out var link); @@ -773,7 +821,7 @@ namespace Umbraco.Web.PublishedCache.NuCache } finally { - Release(lockInfo); + //Release(lockInfo); } return ok; @@ -781,10 +829,13 @@ namespace Umbraco.Web.PublishedCache.NuCache public bool Clear(int id) { - var lockInfo = new WriteLockInfo(); + // TODO: There should be NO locks here! All calls to this are made within GetScopedWriteLock + //var lockInfo = new WriteLockInfo(); try { - Lock(lockInfo); + _logger.Debug("Clear"); + + //Lock(lockInfo); // try to find the content // if it is not there, nothing to do @@ -804,7 +855,7 @@ namespace Umbraco.Web.PublishedCache.NuCache } finally { - Release(lockInfo); + //Release(lockInfo); } } @@ -1200,6 +1251,8 @@ namespace Umbraco.Web.PublishedCache.NuCache var lockInfo = new ReadLockInfo(); try { + // TODO: This would be much simpler with just a lock(_rlocko) { } + // in this case I see no reason why we are using this syntax?! Lock(lockInfo); // if no next generation is required, and we already have one, @@ -1374,6 +1427,8 @@ namespace Umbraco.Web.PublishedCache.NuCache } } + // TODO: This is never used? Should it be? + public async Task WaitForPendingCollect() { Task task; diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshot.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshot.cs index 3f5c1aa4d5..bbb84fc650 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshot.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshot.cs @@ -39,7 +39,15 @@ namespace Umbraco.Web.PublishedCache.NuCache public void Resync() { + // This is annoying since this can cause deadlocks. Since this will be cleared if something happens in the same + // thread after that calls Elements, it will re-lock the _storesLock but that might already be locked again + // and since we're most likely in a ContentStore write lock, the other thread is probably wanting that one too. + // no lock - published snapshots are single-thread + + // TODO: Instead of clearing, we could hold this value in another var in case the above call to GetElements() Or potentially + // a new TryGetElements() fails (since we might be shutting down). In that case we can use the old value. + _elements?.Dispose(); _elements = null; } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs index a866297d72..2439633005 100755 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs @@ -190,10 +190,15 @@ namespace Umbraco.Web.PublishedCache.NuCache /// private void MainDomRelease() { + _logger.Debug("Releasing from MainDom..."); + lock (_storesLock) { + _logger.Debug("Releasing content store..."); _contentStore?.ReleaseLocalDb(); //null check because we could shut down before being assigned _localContentDb = null; + + _logger.Debug("Releasing media store..."); _mediaStore?.ReleaseLocalDb(); //null check because we could shut down before being assigned _localMediaDb = null; @@ -663,10 +668,20 @@ namespace Umbraco.Web.PublishedCache.NuCache publishedChanged = publishedChanged2; } + // TODO: These resync's are a problem, they cause deadlocks because when this is called, it's generally called within a writelock + // and then we clear out the snapshot and then if there's some event handler that needs the content cache, it tries to re-get it + // which first tries locking on the _storesLock which may have already been acquired and in this case we deadlock because + // we're still holding the write lock. + // We resync at the end of a ScopedWriteLock + // BUT if we don't resync here then the current snapshot is out of date for any event handlers that wish to use the most up to date + // data... + // so we need to figure out how to deal with the _storesLock + if (draftChanged || publishedChanged) ((PublishedSnapshot)CurrentPublishedSnapshot)?.Resync(); } + // Calling this method means we have a lock on the contentStore (i.e. GetScopedWriteLock) private void NotifyLocked(IEnumerable payloads, out bool draftChanged, out bool publishedChanged) { publishedChanged = false; @@ -676,7 +691,7 @@ namespace Umbraco.Web.PublishedCache.NuCache // content (and content types) are read-locked while reading content // contentStore is wlocked (so readable, only no new views) // and it can be wlocked by 1 thread only at a time - // contentStore is write-locked during changes + // contentStore is write-locked during changes - see note above, calls to this method are wrapped in contentStore.GetScopedWriteLock foreach (var payload in payloads) { @@ -1131,6 +1146,12 @@ namespace Umbraco.Web.PublishedCache.NuCache ContentStore.Snapshot contentSnap, mediaSnap; SnapDictionary.Snapshot domainSnap; IAppCache elementsCache; + + // TODO: Idea... TryGetElements? Might check if we are shutting down and return false and callers to this could handle it? + // Else does a readerwriterlockslim work here? (i don't think so) + // Else we have 2x locks, one for startup/shutdown, the other for getting elements and then we can maybe do a Monitor.Try? + // That is sort of the same as a TryGetElements + lock (_storesLock) { var scopeContext = _scopeProvider.Context; @@ -1156,6 +1177,8 @@ namespace Umbraco.Web.PublishedCache.NuCache // elements // just need to make sure nothing gets elements in another enlisted action... so using // a MaxValue to make sure this one runs last, and it should be ok + // ... else there is potential to deadlock since this would recursively go back into trying + // lock on _storesLock but another thread may have already tried that scopeContext.Enlist("Umbraco.Web.PublishedCache.NuCache.PublishedSnapshotService.Resync", () => this, (completed, svc) => { ((PublishedSnapshot)svc.CurrentPublishedSnapshot)?.Resync(); diff --git a/src/Umbraco.Web/PublishedCache/NuCache/notes.txt b/src/Umbraco.Web/PublishedCache/NuCache/readme.md similarity index 70% rename from src/Umbraco.Web/PublishedCache/NuCache/notes.txt rename to src/Umbraco.Web/PublishedCache/NuCache/readme.md index ff2d8dd48b..c8e8a363e9 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/notes.txt +++ b/src/Umbraco.Web/PublishedCache/NuCache/readme.md @@ -1,10 +1,6 @@ NuCache Documentation ====================== -NOTE - RENAMING -Facade = PublishedSnapshot -and everything needs to be renamed accordingly - HOW IT WORKS ------------- @@ -22,12 +18,12 @@ When reading the cache, we read views up the chain until we find a value (which null) for the given id, and finally we read the store itself. -The FacadeService manages a ContentStore for content, and another for media. -When a Facade is created, the FacadeService gets ContentView objects from the stores. +The PublishedSnapshotService manages a ContentStore for content, and another for media. +When a PublishedSnapshot is created, the PublishedSnapshotService gets ContentView objects from the stores. Views provide an immutable snapshot of the content and media. -When the FacadeService is notified of changes, it notifies the stores. -Then it resync the current Facade, so that it requires new views, etc. +When the PublishedSnapshotService is notified of changes, it notifies the stores. +Then it resync the current PublishedSnapshot, so that it requires new views, etc. Whenever a content, media or member is modified or removed, the cmsContentNu table is updated with a json dictionary of alias => property value, so that a content, @@ -50,32 +46,32 @@ Each ContentStore has a _freezeLock object used to protect the 'Frozen' state of the store. It's a disposable object that releases the lock when disposed, so usage would be: using (store.Frozen) { ... }. -The FacadeService has a _storesLock object used to guarantee atomic access to the +The PublishedSnapshotService has a _storesLock object used to guarantee atomic access to the set of content, media stores. CACHE ------ -For each set of views, the FacadeService creates a SnapshotCache. So a SnapshotCache +For each set of views, the PublishedSnapshotService creates a SnapshotCache. So a SnapshotCache is valid until anything changes in the content or media trees. In other words, things that go in the SnapshotCache stay until a content or media is modified. -For each Facade, the FacadeService creates a FacadeCache. So a FacadeCache is valid -for the duration of the Facade (usually, the request). In other words, things that go -in the FacadeCache stay (and are visible to) for the duration of the request only. +For each PublishedSnapshot, the PublishedSnapshotService creates a PublishedSnapshotCache. So a PublishedSnapshotCache is valid +for the duration of the PublishedSnapshot (usually, the request). In other words, things that go +in the PublishedSnapshotCache stay (and are visible to) for the duration of the request only. -The FacadeService defines a static constant FullCacheWhenPreviewing, that defines +The PublishedSnapshotService defines a static constant FullCacheWhenPreviewing, that defines how caches operate when previewing: - when true, the caches in preview mode work normally. -- when false, everything that would go to the SnapshotCache goes to the FacadeCache. +- when false, everything that would go to the SnapshotCache goes to the PublishedSnapshotCache. At the moment it is true in the code, which means that eg converted values for previewed content will go in the SnapshotCache. Makes for faster preview, but uses more memory on the long term... would need some benchmarking to figure out what is best. -Members only live for the duration of the Facade. So, for members SnapshotCache is -never used, and everything goes to the FacadeCache. +Members only live for the duration of the PublishedSnapshot. So, for members SnapshotCache is +never used, and everything goes to the PublishedSnapshotCache. All cache keys are computed in the CacheKeys static class. @@ -85,15 +81,15 @@ TESTS For testing purposes the following mechanisms exist: -The Facade type has a static Current property that is used to obtain the 'current' -facade in many places, going through the PublishedCachesServiceResolver to get the -current service, and asking the current service for the current facade, which by +The PublishedSnapshot type has a static Current property that is used to obtain the 'current' +PublishedSnapshot in many places, going through the PublishedCachesServiceResolver to get the +current service, and asking the current service for the current PublishedSnapshot, which by default relies on UmbracoContext. For test purposes, it is possible to override the -entire mechanism by defining Facade.GetCurrentFacadeFunc which should return a facade. +entire mechanism by defining PublishedSnapshot.GetCurrentPublishedSnapshotFunc which should return a PublishedSnapshot. A PublishedContent keeps only id-references to its parent and children, and needs a way to retrieve the actual objects from the cache - which depends on the current -facade. It is possible to override the entire mechanism by defining PublishedContent. +PublishedSnapshot. It is possible to override the entire mechanism by defining PublishedContent. GetContentByIdFunc or .GetMediaByIdFunc. Setting these functions must be done before Resolution is frozen. @@ -110,7 +106,7 @@ possible to support detached contents & properties, even those that do not have int id, but again this should be refactored entirely anyway. Not doing any row-version checks (see XmlStore) when reloading from database, though it -is maintained in the database. Two FIXME in FacadeService. Should we do it? +is maintained in the database. Two FIXME in PublishedSnapshotService. Should we do it? There is no on-disk cache at all so everything is reloaded from the cmsContentNu table when the site restarts. This is pretty fast, but we should experiment with solutions to @@ -121,4 +117,4 @@ PublishedMember exposes properties that IPublishedContent does not, and that are to be lost soon as the member is wrapped (content set, model...) - so we probably need some sort of IPublishedMember. -/ \ No newline at end of file +/ diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 5d29e53d4a..861b95691b 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -1216,7 +1216,7 @@ - + @@ -1279,4 +1279,4 @@ - + \ No newline at end of file diff --git a/src/Umbraco.Web/UmbracoApplication.cs b/src/Umbraco.Web/UmbracoApplication.cs index ad8dcb7e8b..f8ee238da7 100644 --- a/src/Umbraco.Web/UmbracoApplication.cs +++ b/src/Umbraco.Web/UmbracoApplication.cs @@ -22,7 +22,7 @@ namespace Umbraco.Web var mainDomLock = appSettingMainDomLock == "SqlMainDomLock" ? (IMainDomLock)new SqlMainDomLock(logger) - : new MainDomSemaphoreLock(); + : new MainDomSemaphoreLock(logger); var runtime = new WebRuntime(this, logger, new MainDom(logger, mainDomLock)); From d33928f4259b01e4045a46adf92c2356d7297406 Mon Sep 17 00:00:00 2001 From: abi Date: Fri, 3 Jan 2020 01:43:37 +0100 Subject: [PATCH 121/202] Correct project references --- src/Umbraco.Web/Umbraco.Web.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 0980b1b3d6..99eadef2bd 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -249,8 +249,8 @@ - - + + From 3d8e9a78e3da9bd8d64abd993f69c1374991c20f Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 3 Jan 2020 12:39:17 +1100 Subject: [PATCH 122/202] Fixes deadlock --- .../NuCache/PublishedSnapshotService.cs | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs index 2439633005..97e546e603 100755 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; +using System.Threading; using CSharpTest.Net.Collections; using Newtonsoft.Json; using Umbraco.Core; @@ -54,6 +55,7 @@ namespace Umbraco.Web.PublishedCache.NuCache private readonly ContentStore _mediaStore; private readonly SnapDictionary _domainStore; private readonly object _storesLock = new object(); + private readonly object _elementsLock = new object(); private BPlusTree _localContentDb; private BPlusTree _localMediaDb; @@ -143,7 +145,7 @@ namespace Umbraco.Web.PublishedCache.NuCache _domainStore = new SnapDictionary(); - LoadCachesOnStartup(); + LoadCachesOnStartup(); } Guid GetUid(ContentStore store, int id) => store.LiveSnapshot.Get(id)?.Uid ?? default; @@ -171,7 +173,7 @@ namespace Umbraco.Web.PublishedCache.NuCache var path = GetLocalFilesPath(); var localContentDbPath = Path.Combine(path, "NuCache.Content.db"); var localMediaDbPath = Path.Combine(path, "NuCache.Media.db"); - + _localContentDbExists = File.Exists(localContentDbPath); _localMediaDbExists = File.Exists(localMediaDbPath); @@ -221,7 +223,7 @@ namespace Umbraco.Web.PublishedCache.NuCache { okContent = LockAndLoadContent(scope => LoadContentFromLocalDbLocked(true)); if (!okContent) - _logger.Warn("Loading content from local db raised warnings, will reload from database."); + _logger.Warn("Loading content from local db raised warnings, will reload from database."); } if (_localMediaDbExists) @@ -1147,12 +1149,12 @@ namespace Umbraco.Web.PublishedCache.NuCache SnapDictionary.Snapshot domainSnap; IAppCache elementsCache; - // TODO: Idea... TryGetElements? Might check if we are shutting down and return false and callers to this could handle it? - // Else does a readerwriterlockslim work here? (i don't think so) - // Else we have 2x locks, one for startup/shutdown, the other for getting elements and then we can maybe do a Monitor.Try? - // That is sort of the same as a TryGetElements + // Here we are reading/writing to shared objects so we need to lock (can't be _storesLock which manages the actual nucache files + // and would result in a deadlock). Even though we are locking around underlying readlocks (within CreateSnapshot) it's because + // we need to ensure that the result of contentSnap.Gen (etc) and the re-assignment of these values and _elements cache + // are done atomically. - lock (_storesLock) + lock (_elementsLock) { var scopeContext = _scopeProvider.Context; @@ -1177,14 +1179,14 @@ namespace Umbraco.Web.PublishedCache.NuCache // elements // just need to make sure nothing gets elements in another enlisted action... so using // a MaxValue to make sure this one runs last, and it should be ok - // ... else there is potential to deadlock since this would recursively go back into trying - // lock on _storesLock but another thread may have already tried that + scopeContext.Enlist("Umbraco.Web.PublishedCache.NuCache.PublishedSnapshotService.Resync", () => this, (completed, svc) => { ((PublishedSnapshot)svc.CurrentPublishedSnapshot)?.Resync(); }, int.MaxValue); } + // create a new snapshot cache if snapshots are different gens if (contentSnap.Gen != _contentGen || mediaSnap.Gen != _mediaGen || domainSnap.Gen != _domainGen || _elementsCache == null) { @@ -1351,7 +1353,7 @@ namespace Umbraco.Web.PublishedCache.NuCache { //culture changed on an existing language var cultureChanged = e.SavedEntities.Any(x => !x.WasPropertyDirty(nameof(ILanguage.Id)) && x.WasPropertyDirty(nameof(ILanguage.IsoCode))); - if(cultureChanged) + if (cultureChanged) { RebuildContentDbCache(); } From e6b333a3ec2f5234a081bb7f1ab821e42f332a3e Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 3 Jan 2020 12:39:56 +1100 Subject: [PATCH 123/202] Changes readlocks to normal locks, no need to have extra objects allocated and complex code. --- .../PublishedCache/NuCache/ContentStore.cs | 72 +++++-------------- .../PublishedCache/NuCache/SnapDictionary.cs | 58 ++++----------- 2 files changed, 32 insertions(+), 98 deletions(-) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs index 81229af9e1..450dc2f283 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs @@ -20,6 +20,7 @@ namespace Umbraco.Web.PublishedCache.NuCache // this class is an extended version of SnapDictionary // most of the snapshots management code, etc is an exact copy // SnapDictionary has unit tests to ensure it all works correctly + // For locking information, see SnapDictionary private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; private readonly IVariationContextAccessor _variationContextAccessor; @@ -79,11 +80,6 @@ namespace Umbraco.Web.PublishedCache.NuCache private readonly string _instanceId = Guid.NewGuid().ToString("N"); - private class ReadLockInfo - { - public bool Taken; - } - private class WriteLockInfo { public bool Taken; @@ -121,15 +117,10 @@ namespace Umbraco.Web.PublishedCache.NuCache private void Lock(WriteLockInfo lockInfo, bool forceGen = false) { - // TODO: We are in a deadlock here somehow?? - Monitor.Enter(_wlocko, ref lockInfo.Taken); - var rtaken = false; - try + lock(_rlocko) { - Monitor.Enter(_rlocko, ref rtaken); - // see SnapDictionary try { } finally @@ -146,26 +137,16 @@ namespace Umbraco.Web.PublishedCache.NuCache _nextGen = true; } } - } - finally - { - if (rtaken) Monitor.Exit(_rlocko); - } - } - - private void Lock(ReadLockInfo lockInfo) - { - Monitor.Enter(_rlocko, ref lockInfo.Taken); + } } private void Release(WriteLockInfo lockInfo, bool commit = true) { if (commit == false) { - var rtaken = false; - try + lock(_rlocko) { - Monitor.Enter(_rlocko, ref rtaken); + // see SnapDictionary try { } finally { @@ -173,10 +154,6 @@ namespace Umbraco.Web.PublishedCache.NuCache _liveGen -= 1; } } - finally - { - if (rtaken) Monitor.Exit(_rlocko); - } Rollback(_contentNodes); RollbackRoot(); @@ -207,11 +184,6 @@ namespace Umbraco.Web.PublishedCache.NuCache Monitor.Exit(_wlocko); } - private void Release(ReadLockInfo lockInfo) - { - if (lockInfo.Taken) Monitor.Exit(_rlocko); - } - private void RollbackRoot() { if (_root.Gen <= _liveGen) return; @@ -1248,13 +1220,8 @@ namespace Umbraco.Web.PublishedCache.NuCache public Snapshot CreateSnapshot() { - var lockInfo = new ReadLockInfo(); - try + lock(_rlocko) { - // TODO: This would be much simpler with just a lock(_rlocko) { } - // in this case I see no reason why we are using this syntax?! - Lock(lockInfo); - // if no next generation is required, and we already have one, // use it and create a new snapshot if (_nextGen == false && _genObj != null) @@ -1306,10 +1273,6 @@ namespace Umbraco.Web.PublishedCache.NuCache return snapshot; } - finally - { - Release(lockInfo); - } } public Snapshot LiveSnapshot => new Snapshot(this, _liveGen @@ -1427,18 +1390,17 @@ namespace Umbraco.Web.PublishedCache.NuCache } } - // TODO: This is never used? Should it be? - - public async Task WaitForPendingCollect() - { - Task task; - lock (_rlocko) - { - task = _collectTask; - } - if (task != null) - await task; - } + // TODO: This is never used? Should it be? Maybe move to TestHelper below? + //public async Task WaitForPendingCollect() + //{ + // Task task; + // lock (_rlocko) + // { + // task = _collectTask; + // } + // if (task != null) + // await task; + //} public long GenCount => _genObjs.Count; diff --git a/src/Umbraco.Web/PublishedCache/NuCache/SnapDictionary.cs b/src/Umbraco.Web/PublishedCache/NuCache/SnapDictionary.cs index 9671949ff0..e0ad26eb81 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/SnapDictionary.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/SnapDictionary.cs @@ -22,6 +22,11 @@ namespace Umbraco.Web.PublishedCache.NuCache // This class is optimized for many readers, few writers // Readers are lock-free + // NOTE - we used to lock _rlocko the long hand way with Monitor.Enter(_rlocko, ref lockTaken) but this has + // been replaced with a normal c# lock because that's exactly how the normal c# lock works, + // see https://blogs.msdn.microsoft.com/ericlippert/2009/03/06/locks-and-exceptions-do-not-mix/ + // for the readlock, there's no reason here to use the long hand way. + private readonly ConcurrentDictionary> _items; private readonly ConcurrentQueue _genObjs; private GenObj _genObj; @@ -71,16 +76,11 @@ namespace Umbraco.Web.PublishedCache.NuCache // are all ignored - Release is private and meant to be invoked with 'commit' being false only // only on the outermost lock (by SnapDictionaryWriter) - // using (...) {} for locking is prone to nasty leaks in case of weird exceptions - // such as thread-abort or out-of-memory, but let's not worry about it now + // side note - using (...) {} for locking is prone to nasty leaks in case of weird exceptions + // such as thread-abort or out-of-memory, which is why we've moved away from the old using wrapper we had on locking. private readonly string _instanceId = Guid.NewGuid().ToString("N"); - private class ReadLockInfo - { - public bool Taken; - } - private class WriteLockInfo { public bool Taken; @@ -121,18 +121,16 @@ namespace Umbraco.Web.PublishedCache.NuCache { Monitor.Enter(_wlocko, ref lockInfo.Taken); - var rtaken = false; - try + lock(_rlocko) { - Monitor.Enter(_rlocko, ref rtaken); - // assume everything in finally runs atomically // http://stackoverflow.com/questions/18501678/can-this-unexpected-behavior-of-prepareconstrainedregions-and-thread-abort-be-ex // http://joeduffyblog.com/2005/03/18/atomicity-and-asynchronous-exception-failures/ // http://joeduffyblog.com/2007/02/07/introducing-the-new-readerwriterlockslim-in-orcas/ // http://chabster.blogspot.fr/2013/12/readerwriterlockslim-fails-on-dual.html //RuntimeHelpers.PrepareConstrainedRegions(); - try { } finally + try { } + finally { // increment the lock count, and register that this lock is counting _wlocked++; @@ -149,15 +147,6 @@ namespace Umbraco.Web.PublishedCache.NuCache } } } - finally - { - if (rtaken) Monitor.Exit(_rlocko); - } - } - - private void Lock(ReadLockInfo lockInfo) - { - Monitor.Enter(_rlocko, ref lockInfo.Taken); } private void Release(WriteLockInfo lockInfo, bool commit = true) @@ -168,22 +157,17 @@ namespace Umbraco.Web.PublishedCache.NuCache if (commit == false) { - var rtaken = false; - try + lock(_rlocko) { - Monitor.Enter(_rlocko, ref rtaken); - try { } finally + try { } + finally { // forget about the temp. liveGen _nextGen = false; _liveGen -= 1; } } - finally - { - if (rtaken) Monitor.Exit(_rlocko); - } - + foreach (var item in _items) { var link = item.Value; @@ -202,11 +186,6 @@ namespace Umbraco.Web.PublishedCache.NuCache Monitor.Exit(_wlocko); } - private void Release(ReadLockInfo lockInfo) - { - if (lockInfo.Taken) Monitor.Exit(_rlocko); - } - #endregion #region Set, Clear, Get, Has @@ -347,11 +326,8 @@ namespace Umbraco.Web.PublishedCache.NuCache public Snapshot CreateSnapshot() { - var lockInfo = new ReadLockInfo(); - try + lock(_rlocko) { - Lock(lockInfo); - // if no next generation is required, and we already have a gen object, // use it to create a new snapshot if (_nextGen == false && _genObj != null) @@ -398,10 +374,6 @@ namespace Umbraco.Web.PublishedCache.NuCache return snapshot; } - finally - { - Release(lockInfo); - } } public Task CollectAsync() From 8e3b3c83261e5272fefb219aaceaa645775ad164 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 3 Jan 2020 13:21:49 +1100 Subject: [PATCH 124/202] Changes methods that should already be locked to check that they are and changes their names/adds docs --- .../PublishedCache/NuCache/ContentStore.cs | 488 +++++++++--------- .../NuCache/PublishedSnapshotService.cs | 30 +- 2 files changed, 270 insertions(+), 248 deletions(-) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs index 450dc2f283..82fae0d32a 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs @@ -115,6 +115,12 @@ namespace Umbraco.Web.PublishedCache.NuCache return ScopeContextualBase.Get(scopeProvider, _instanceId, scoped => new ScopedWriteLock(this, scoped)); } + private void EnsureLocked() + { + if (!Monitor.IsEntered(_wlocko)) + throw new InvalidOperationException("Write lock must be acquried."); + } + private void Lock(WriteLockInfo lockInfo, bool forceGen = false) { Monitor.Enter(_wlocko, ref lockInfo.Taken); @@ -323,34 +329,36 @@ namespace Umbraco.Web.PublishedCache.NuCache } } - public void SetAllContentTypes(IEnumerable types) + /// + /// Updates/sets data for all content types + /// + /// + /// + /// This methods MUST be called from within a write lock, normally wrapped within GetScopedWriteLock + /// otherwise an exception will occur. + /// + /// + /// Thrown if this method is not called within a write lock + /// + public void SetAllContentTypesLocked(IEnumerable types) { - // TODO: There should be NO lock here! All calls made to this are wrapped in GetScopedWriteLock + EnsureLocked(); - //var lockInfo = new WriteLockInfo(); - try + _logger.Debug("SetAllContentTypes"); + + // clear all existing content types + ClearLocked(_contentTypesById); + ClearLocked(_contentTypesByAlias); + + // set all new content types + foreach (var type in types) { - _logger.Debug("SetAllContentTypes"); - //Lock(lockInfo); - - // clear all existing content types - ClearLocked(_contentTypesById); - ClearLocked(_contentTypesByAlias); - - // set all new content types - foreach (var type in types) - { - SetValueLocked(_contentTypesById, type.Id, type); - SetValueLocked(_contentTypesByAlias, type.Alias, type); - } - - // beware! at that point the cache is inconsistent, - // assuming we are going to SetAll content items! - } - finally - { - //Release(lockInfo); + SetValueLocked(_contentTypesById, type.Id, type); + SetValueLocked(_contentTypesByAlias, type.Alias, type); } + + // beware! at that point the cache is inconsistent, + // assuming we are going to SetAll content items! } public void UpdateContentTypes(IReadOnlyCollection removedIds, IReadOnlyCollection refreshedTypes, IReadOnlyCollection kits) @@ -540,8 +548,22 @@ namespace Umbraco.Web.PublishedCache.NuCache return link; } - public bool Set(ContentNodeKit kit) + /// + /// Sets the data for a + /// + /// + /// + /// + /// This methods MUST be called from within a write lock, normally wrapped within GetScopedWriteLock + /// otherwise an exception will occur. + /// + /// + /// Thrown if this method is not called within a write lock + /// + public bool SetLocked(ContentNodeKit kit) { + EnsureLocked(); + // ReSharper disable LocalizableElement if (kit.IsEmpty) throw new ArgumentException("Kit is empty.", nameof(kit)); @@ -551,58 +573,47 @@ namespace Umbraco.Web.PublishedCache.NuCache _logger.Debug("Set content ID: {KitNodeId}", kit.Node.Id); - // TODO: There should be NO locks here, all calls made to this are done within GetScopedWriteLock - //var lockInfo = new WriteLockInfo(); - try + // get existing + _contentNodes.TryGetValue(kit.Node.Id, out var link); + var existing = link?.Value; + + if (!BuildKit(kit, out var parent)) + return false; + + // moving? + var moving = existing != null && existing.ParentContentId != kit.Node.ParentContentId; + + // manage children + if (existing != null) { - //Lock(lockInfo); - - // get existing - _contentNodes.TryGetValue(kit.Node.Id, out var link); - var existing = link?.Value; - - if (!BuildKit(kit, out var parent)) - return false; - - // moving? - var moving = existing != null && existing.ParentContentId != kit.Node.ParentContentId; - - // manage children - if (existing != null) - { - kit.Node.FirstChildContentId = existing.FirstChildContentId; - kit.Node.LastChildContentId = existing.LastChildContentId; - } - - // set - SetValueLocked(_contentNodes, kit.Node.Id, kit.Node); - if (_localDb != null) RegisterChange(kit.Node.Id, kit); - - // manage the tree - if (existing == null) - { - // new, add to parent - AddTreeNodeLocked(kit.Node, parent); - } - else if (moving || existing.SortOrder != kit.Node.SortOrder) - { - // moved, remove existing from its parent, add content to its parent - RemoveTreeNodeLocked(existing); - AddTreeNodeLocked(kit.Node); - } - else - { - // replacing existing, handle siblings - kit.Node.NextSiblingContentId = existing.NextSiblingContentId; - kit.Node.PreviousSiblingContentId = existing.PreviousSiblingContentId; - } - - _xmap[kit.Node.Uid] = kit.Node.Id; + kit.Node.FirstChildContentId = existing.FirstChildContentId; + kit.Node.LastChildContentId = existing.LastChildContentId; } - finally + + // set + SetValueLocked(_contentNodes, kit.Node.Id, kit.Node); + if (_localDb != null) RegisterChange(kit.Node.Id, kit); + + // manage the tree + if (existing == null) { - //Release(lockInfo); + // new, add to parent + AddTreeNodeLocked(kit.Node, parent); } + else if (moving || existing.SortOrder != kit.Node.SortOrder) + { + // moved, remove existing from its parent, add content to its parent + RemoveTreeNodeLocked(existing); + AddTreeNodeLocked(kit.Node); + } + else + { + // replacing existing, handle siblings + kit.Node.NextSiblingContentId = existing.NextSiblingContentId; + kit.Node.PreviousSiblingContentId = existing.PreviousSiblingContentId; + } + + _xmap[kit.Node.Uid] = kit.Node.Id; return true; } @@ -624,211 +635,222 @@ namespace Umbraco.Web.PublishedCache.NuCache /// True if the data is coming from the database (not the local cache db) /// /// + /// /// This requires that the collection is sorted by Level + ParentId + Sort Order. /// This should be used only on a site startup as the first generations. /// This CANNOT be used after startup since it bypasses all checks for Generations. + /// + /// + /// This methods MUST be called from within a write lock, normally wrapped within GetScopedWriteLock + /// otherwise an exception will occur. + /// /// - internal bool SetAllFastSorted(IEnumerable kits, bool fromDb) + /// + /// Thrown if this method is not called within a write lock + /// + public bool SetAllFastSortedLocked(IEnumerable kits, bool fromDb) { + EnsureLocked(); - //TODO: There should be NO locks here all calls made to this are done within GetScopedWriteLock + _logger.Debug("SetAllFastSorted"); - //var lockInfo = new WriteLockInfo(); var ok = true; - try + + ClearLocked(_contentNodes); + ClearRootLocked(); + + // The name of the game here is to populate each kit's + // FirstChildContentId + // LastChildContentId + // NextSiblingContentId + // PreviousSiblingContentId + + ContentNode previousNode = null; + ContentNode parent = null; + + foreach (var kit in kits) { - _logger.Debug("SetAllFastSorted"); - - //Lock(lockInfo); - - ClearLocked(_contentNodes); - ClearRootLocked(); - - // The name of the game here is to populate each kit's - // FirstChildContentId - // LastChildContentId - // NextSiblingContentId - // PreviousSiblingContentId - - ContentNode previousNode = null; - ContentNode parent = null; - - foreach (var kit in kits) + if (!BuildKit(kit, out var parentLink)) { - if (!BuildKit(kit, out var parentLink)) - { - ok = false; - continue; // skip that one - } - - var thisNode = kit.Node; - - if (parent == null) - { - // first parent - parent = parentLink.Value; - parent.FirstChildContentId = thisNode.Id; // this node is the first node - } - else if (parent.Id != parentLink.Value.Id) - { - // new parent - parent = parentLink.Value; - parent.FirstChildContentId = thisNode.Id; // this node is the first node - previousNode = null; // there is no previous sibling - } - - _logger.Verbose($"Set {thisNode.Id} with parent {thisNode.ParentContentId}"); - SetValueLocked(_contentNodes, thisNode.Id, thisNode); - - // if we are initializing from the database source ensure the local db is updated - if (fromDb && _localDb != null) RegisterChange(thisNode.Id, kit); - - // this node is always the last child - parent.LastChildContentId = thisNode.Id; - - // wire previous node as previous sibling - if (previousNode != null) - { - previousNode.NextSiblingContentId = thisNode.Id; - thisNode.PreviousSiblingContentId = previousNode.Id; - } - - // this node becomes the previous node - previousNode = thisNode; - - _xmap[kit.Node.Uid] = kit.Node.Id; + ok = false; + continue; // skip that one } - } - finally - { - //Release(lockInfo); + + var thisNode = kit.Node; + + if (parent == null) + { + // first parent + parent = parentLink.Value; + parent.FirstChildContentId = thisNode.Id; // this node is the first node + } + else if (parent.Id != parentLink.Value.Id) + { + // new parent + parent = parentLink.Value; + parent.FirstChildContentId = thisNode.Id; // this node is the first node + previousNode = null; // there is no previous sibling + } + + _logger.Verbose($"Set {thisNode.Id} with parent {thisNode.ParentContentId}"); + SetValueLocked(_contentNodes, thisNode.Id, thisNode); + + // if we are initializing from the database source ensure the local db is updated + if (fromDb && _localDb != null) RegisterChange(thisNode.Id, kit); + + // this node is always the last child + parent.LastChildContentId = thisNode.Id; + + // wire previous node as previous sibling + if (previousNode != null) + { + previousNode.NextSiblingContentId = thisNode.Id; + thisNode.PreviousSiblingContentId = previousNode.Id; + } + + // this node becomes the previous node + previousNode = thisNode; + + _xmap[kit.Node.Uid] = kit.Node.Id; } return ok; } - public bool SetAll(IEnumerable kits) + /// + /// Set all data for a collection of + /// + /// + /// + /// + /// This methods MUST be called from within a write lock, normally wrapped within GetScopedWriteLock + /// otherwise an exception will occur. + /// + /// + /// Thrown if this method is not called within a write lock + /// + public bool SetAllLocked(IEnumerable kits) { - //TODO: There should be NO locks here all calls made to this are done within GetScopedWriteLock + EnsureLocked(); - //var lockInfo = new WriteLockInfo(); var ok = true; - try + _logger.Debug("SetAll"); + + ClearLocked(_contentNodes); + ClearRootLocked(); + + // do NOT clear types else they are gone! + //ClearLocked(_contentTypesById); + //ClearLocked(_contentTypesByAlias); + + foreach (var kit in kits) { - _logger.Debug("SetAll"); - - //Lock(lockInfo); - - ClearLocked(_contentNodes); - ClearRootLocked(); - - // do NOT clear types else they are gone! - //ClearLocked(_contentTypesById); - //ClearLocked(_contentTypesByAlias); - - foreach (var kit in kits) + if (!BuildKit(kit, out var parent)) { - if (!BuildKit(kit, out var parent)) - { - ok = false; - continue; // skip that one - } - _logger.Verbose($"Set {kit.Node.Id} with parent {kit.Node.ParentContentId}"); - SetValueLocked(_contentNodes, kit.Node.Id, kit.Node); - - if (_localDb != null) RegisterChange(kit.Node.Id, kit); - AddTreeNodeLocked(kit.Node, parent); - - _xmap[kit.Node.Uid] = kit.Node.Id; + ok = false; + continue; // skip that one } - } - finally - { - //Release(lockInfo); + _logger.Verbose($"Set {kit.Node.Id} with parent {kit.Node.ParentContentId}"); + SetValueLocked(_contentNodes, kit.Node.Id, kit.Node); + + if (_localDb != null) RegisterChange(kit.Node.Id, kit); + AddTreeNodeLocked(kit.Node, parent); + + _xmap[kit.Node.Uid] = kit.Node.Id; } return ok; } - // IMPORTANT kits must be sorted out by LEVEL and by SORT ORDER - public bool SetBranch(int rootContentId, IEnumerable kits) + /// + /// Sets data for a branch of + /// + /// + /// + /// + /// + /// + /// IMPORTANT kits must be sorted out by LEVEL and by SORT ORDER + /// + /// + /// This methods MUST be called from within a write lock, normally wrapped within GetScopedWriteLock + /// otherwise an exception will occur. + /// + /// + /// + /// Thrown if this method is not called within a write lock + /// + public bool SetBranchLocked(int rootContentId, IEnumerable kits) { - //TODO: There should be NO locks here all calls made to this are done within GetScopedWriteLock + EnsureLocked(); - //var lockInfo = new WriteLockInfo(); var ok = true; - try + _logger.Debug("SetBranch"); + + // get existing + _contentNodes.TryGetValue(rootContentId, out var link); + var existing = link?.Value; + + // clear + if (existing != null) { - _logger.Debug("SetBranch"); - - //Lock(lockInfo); - - // get existing - _contentNodes.TryGetValue(rootContentId, out var link); - var existing = link?.Value; - - // clear - if (existing != null) - { - //this zero's out the branch (recursively), if we're in a new gen this will add a NULL placeholder for the gen - ClearBranchLocked(existing); - //TODO: This removes the current GEN from the tree - do we really want to do that? - RemoveTreeNodeLocked(existing); - } - - // now add them all back - foreach (var kit in kits) - { - if (!BuildKit(kit, out var parent)) - { - ok = false; - continue; // skip that one - } - SetValueLocked(_contentNodes, kit.Node.Id, kit.Node); - if (_localDb != null) RegisterChange(kit.Node.Id, kit); - AddTreeNodeLocked(kit.Node, parent); - - _xmap[kit.Node.Uid] = kit.Node.Id; - } + //this zero's out the branch (recursively), if we're in a new gen this will add a NULL placeholder for the gen + ClearBranchLocked(existing); + //TODO: This removes the current GEN from the tree - do we really want to do that? + RemoveTreeNodeLocked(existing); } - finally + + // now add them all back + foreach (var kit in kits) { - //Release(lockInfo); + if (!BuildKit(kit, out var parent)) + { + ok = false; + continue; // skip that one + } + SetValueLocked(_contentNodes, kit.Node.Id, kit.Node); + if (_localDb != null) RegisterChange(kit.Node.Id, kit); + AddTreeNodeLocked(kit.Node, parent); + + _xmap[kit.Node.Uid] = kit.Node.Id; } return ok; } - public bool Clear(int id) + /// + /// Clears data for a given node id + /// + /// + /// + /// + /// This methods MUST be called from within a write lock, normally wrapped within GetScopedWriteLock + /// otherwise an exception will occur. + /// + /// + /// Thrown if this method is not called within a write lock + /// + public bool ClearLocked(int id) { - // TODO: There should be NO locks here! All calls to this are made within GetScopedWriteLock - //var lockInfo = new WriteLockInfo(); - try - { - _logger.Debug("Clear"); + EnsureLocked(); - //Lock(lockInfo); + _logger.Debug("Clear"); - // try to find the content - // if it is not there, nothing to do - _contentNodes.TryGetValue(id, out var link); // else null - if (link?.Value == null) return false; + // try to find the content + // if it is not there, nothing to do + _contentNodes.TryGetValue(id, out var link); // else null + if (link?.Value == null) return false; - var content = link.Value; - _logger.Debug("Clear content ID: {ContentId}", content.Id); + var content = link.Value; + _logger.Debug("Clear content ID: {ContentId}", content.Id); - // clear the entire branch - ClearBranchLocked(content); + // clear the entire branch + ClearBranchLocked(content); - // manage the tree - RemoveTreeNodeLocked(content); + // manage the tree + RemoveTreeNodeLocked(content); - return true; - } - finally - { - //Release(lockInfo); - } + return true; } private void ClearBranchLocked(int id) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs index 97e546e603..b761fe0fa4 100755 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs @@ -384,7 +384,7 @@ namespace Umbraco.Web.PublishedCache.NuCache var contentTypes = _serviceContext.ContentTypeService.GetAll() .Select(x => _publishedContentTypeFactory.CreateContentType(x)); - _contentStore.SetAllContentTypes(contentTypes); + _contentStore.SetAllContentTypesLocked(contentTypes); using (_logger.TraceDuration("Loading content from database")) { @@ -395,7 +395,7 @@ namespace Umbraco.Web.PublishedCache.NuCache // IMPORTANT GetAllContentSources sorts kits by level + parentId + sortOrder var kits = _dataSource.GetAllContentSources(scope); - return onStartup ? _contentStore.SetAllFastSorted(kits, true) : _contentStore.SetAll(kits); + return onStartup ? _contentStore.SetAllFastSortedLocked(kits, true) : _contentStore.SetAllLocked(kits); } } @@ -403,7 +403,7 @@ namespace Umbraco.Web.PublishedCache.NuCache { var contentTypes = _serviceContext.ContentTypeService.GetAll() .Select(x => _publishedContentTypeFactory.CreateContentType(x)); - _contentStore.SetAllContentTypes(contentTypes); + _contentStore.SetAllContentTypesLocked(contentTypes); using (_logger.TraceDuration("Loading content from local cache file")) { @@ -455,7 +455,7 @@ namespace Umbraco.Web.PublishedCache.NuCache var mediaTypes = _serviceContext.MediaTypeService.GetAll() .Select(x => _publishedContentTypeFactory.CreateContentType(x)); - _mediaStore.SetAllContentTypes(mediaTypes); + _mediaStore.SetAllContentTypesLocked(mediaTypes); using (_logger.TraceDuration("Loading media from database")) { @@ -467,7 +467,7 @@ namespace Umbraco.Web.PublishedCache.NuCache _logger.Debug("Loading media from database..."); // IMPORTANT GetAllMediaSources sorts kits by level + parentId + sortOrder var kits = _dataSource.GetAllMediaSources(scope); - return onStartup ? _mediaStore.SetAllFastSorted(kits, true) : _mediaStore.SetAll(kits); + return onStartup ? _mediaStore.SetAllFastSortedLocked(kits, true) : _mediaStore.SetAllLocked(kits); } } @@ -475,7 +475,7 @@ namespace Umbraco.Web.PublishedCache.NuCache { var mediaTypes = _serviceContext.MediaTypeService.GetAll() .Select(x => _publishedContentTypeFactory.CreateContentType(x)); - _mediaStore.SetAllContentTypes(mediaTypes); + _mediaStore.SetAllContentTypesLocked(mediaTypes); using (_logger.TraceDuration("Loading media from local cache file")) { @@ -516,7 +516,7 @@ namespace Umbraco.Web.PublishedCache.NuCache return false; } - return onStartup ? store.SetAllFastSorted(kits, false) : store.SetAll(kits); + return onStartup ? store.SetAllFastSortedLocked(kits, false) : store.SetAllLocked(kits); } // keep these around - might be useful @@ -713,7 +713,7 @@ namespace Umbraco.Web.PublishedCache.NuCache if (payload.ChangeTypes.HasType(TreeChangeTypes.Remove)) { - if (_contentStore.Clear(payload.Id)) + if (_contentStore.ClearLocked(payload.Id)) draftChanged = publishedChanged = true; continue; } @@ -736,7 +736,7 @@ namespace Umbraco.Web.PublishedCache.NuCache // ?? should we do some RV check here? // IMPORTANT GetbranchContentSources sorts kits by level and by sort order var kits = _dataSource.GetBranchContentSources(scope, capture.Id); - _contentStore.SetBranch(capture.Id, kits); + _contentStore.SetBranchLocked(capture.Id, kits); } else { @@ -744,11 +744,11 @@ namespace Umbraco.Web.PublishedCache.NuCache var kit = _dataSource.GetContentSource(scope, capture.Id); if (kit.IsEmpty) { - _contentStore.Clear(capture.Id); + _contentStore.ClearLocked(capture.Id); } else { - _contentStore.Set(kit); + _contentStore.SetLocked(kit); } } @@ -806,7 +806,7 @@ namespace Umbraco.Web.PublishedCache.NuCache if (payload.ChangeTypes.HasType(TreeChangeTypes.Remove)) { - if (_mediaStore.Clear(payload.Id)) + if (_mediaStore.ClearLocked(payload.Id)) anythingChanged = true; continue; } @@ -829,7 +829,7 @@ namespace Umbraco.Web.PublishedCache.NuCache // ?? should we do some RV check here? // IMPORTANT GetbranchContentSources sorts kits by level and by sort order var kits = _dataSource.GetBranchMediaSources(scope, capture.Id); - _mediaStore.SetBranch(capture.Id, kits); + _mediaStore.SetBranchLocked(capture.Id, kits); } else { @@ -837,11 +837,11 @@ namespace Umbraco.Web.PublishedCache.NuCache var kit = _dataSource.GetMediaSource(scope, capture.Id); if (kit.IsEmpty) { - _mediaStore.Clear(capture.Id); + _mediaStore.ClearLocked(capture.Id); } else { - _mediaStore.Set(kit); + _mediaStore.SetLocked(kit); } } From 243e76b3cc3e5fccdf9c021354d4a2d88b6bb707 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 3 Jan 2020 15:04:39 +1100 Subject: [PATCH 125/202] Removes ability to have recursive locks in SnapDictionary, changes logic to require locking around the methods just like ContentStore, updates tests --- .../Cache/SnapDictionaryTests.cs | 126 ++++++++------ .../PublishedCache/NuCache/ContentStore.cs | 104 ++++++------ .../NuCache/PublishedSnapshotService.cs | 14 +- .../PublishedCache/NuCache/SnapDictionary.cs | 154 ++++++++---------- 4 files changed, 204 insertions(+), 194 deletions(-) diff --git a/src/Umbraco.Tests/Cache/SnapDictionaryTests.cs b/src/Umbraco.Tests/Cache/SnapDictionaryTests.cs index b435af9e77..86426d196c 100644 --- a/src/Umbraco.Tests/Cache/SnapDictionaryTests.cs +++ b/src/Umbraco.Tests/Cache/SnapDictionaryTests.cs @@ -9,6 +9,47 @@ using Umbraco.Web.PublishedCache.NuCache.Snap; namespace Umbraco.Tests.Cache { + /// + /// Used for tests + /// + public static class SnapDictionaryExtensions + { + internal static void Set(this SnapDictionary d, TKey key, TValue value) + where TValue : class + { + using (d.GetScopedWriteLock(GetScopeProvider())) + { + d.SetLocked(key, value); + } + } + + internal static void Clear(this SnapDictionary d) + where TValue : class + { + using (d.GetScopedWriteLock(GetScopeProvider())) + { + d.ClearLocked(); + } + } + + internal static void Clear(this SnapDictionary d, TKey key) + where TValue : class + { + using (d.GetScopedWriteLock(GetScopeProvider())) + { + d.ClearLocked(key); + } + } + + private static IScopeProvider GetScopeProvider() + { + var scopeProvider = Mock.Of(); + Mock.Get(scopeProvider) + .Setup(x => x.Context).Returns(() => null); + return scopeProvider; + } + } + [TestFixture] public class SnapDictionaryTests { @@ -223,7 +264,7 @@ namespace Umbraco.Tests.Cache { var d = new SnapDictionary(); d.Test.CollectAuto = false; - + // gen 1 d.Set(1, "one"); Assert.AreEqual(1, d.Test.GetValues(1).Length); @@ -321,7 +362,7 @@ namespace Umbraco.Tests.Cache { var d = new SnapDictionary(); d.Test.CollectAuto = false; - + Assert.AreEqual(0, d.Test.GetValues(1).Length); // gen 1 @@ -416,7 +457,7 @@ namespace Umbraco.Tests.Cache { var d = new SnapDictionary(); d.Test.CollectAuto = false; - + // gen 1 d.Set(1, "one"); Assert.AreEqual(1, d.Test.GetValues(1).Length); @@ -578,7 +619,7 @@ namespace Umbraco.Tests.Cache { var d = new SnapDictionary(); d.Test.CollectAuto = false; - + d.Set(1, "one"); d.Set(2, "two"); @@ -689,7 +730,7 @@ namespace Umbraco.Tests.Cache { // gen 3 Assert.AreEqual(2, d.Test.GetValues(1).Length); - d.Set(1, "ein"); + d.SetLocked(1, "ein"); Assert.AreEqual(3, d.Test.GetValues(1).Length); Assert.AreEqual(3, d.Test.LiveGen); @@ -727,31 +768,25 @@ namespace Umbraco.Tests.Cache using (var w1 = d.GetScopedWriteLock(scopeProvider)) { Assert.AreEqual(1, t.LiveGen); - Assert.AreEqual(1, t.WLocked); + Assert.IsTrue(t.IsLocked); Assert.IsTrue(t.NextGen); - using (var w2 = d.GetScopedWriteLock(scopeProvider)) + Assert.Throws(() => { - Assert.AreEqual(1, t.LiveGen); - Assert.AreEqual(2, t.WLocked); - Assert.IsTrue(t.NextGen); - - Assert.AreNotSame(w1, w2); // get a new writer each time - - d.Set(1, "one"); - - Assert.AreEqual(0, d.CreateSnapshot().Gen); - } + using (var w2 = d.GetScopedWriteLock(scopeProvider)) + { + } + }); Assert.AreEqual(1, t.LiveGen); - Assert.AreEqual(1, t.WLocked); + Assert.IsTrue(t.IsLocked); Assert.IsTrue(t.NextGen); Assert.AreEqual(0, d.CreateSnapshot().Gen); } Assert.AreEqual(1, t.LiveGen); - Assert.AreEqual(0, t.WLocked); + Assert.IsFalse(t.IsLocked); Assert.IsTrue(t.NextGen); Assert.AreEqual(1, d.CreateSnapshot().Gen); @@ -772,11 +807,14 @@ namespace Umbraco.Tests.Cache using (var w1 = d.GetScopedWriteLock(scopeProvider)) { + // This one is interesting, although we don't allow recursive locks, since this is + // using the same ScopeContext/key, the lock acquisition is only done once + using (var w2 = d.GetScopedWriteLock(scopeProvider)) { Assert.AreSame(w1, w2); - d.Set(1, "one"); + d.SetLocked(1, "one"); } } } @@ -797,19 +835,16 @@ namespace Umbraco.Tests.Cache using (var w1 = d.GetScopedWriteLock(scopeProvider1)) { Assert.AreEqual(1, t.LiveGen); - Assert.AreEqual(1, t.WLocked); + Assert.IsTrue(t.IsLocked); Assert.IsTrue(t.NextGen); - using (var w2 = d.GetScopedWriteLock(scopeProvider2)) + Assert.Throws(() => { - Assert.AreEqual(1, t.LiveGen); - Assert.AreEqual(2, t.WLocked); - Assert.IsTrue(t.NextGen); + using (var w2 = d.GetScopedWriteLock(scopeProvider2)) + { + } + }); - Assert.AreNotSame(w1, w2); - - d.Set(1, "one"); - } } } @@ -848,13 +883,13 @@ namespace Umbraco.Tests.Cache Assert.IsFalse(d.Test.NextGen); Assert.AreEqual("uno", s2.Get(1)); - var scopeProvider = GetScopeProvider(); + var scopeProvider = GetScopeProvider(); using (d.GetScopedWriteLock(scopeProvider)) { // gen 3 Assert.AreEqual(2, d.Test.GetValues(1).Length); - d.Set(1, "ein"); + d.SetLocked(1, "ein"); Assert.AreEqual(3, d.Test.GetValues(1).Length); Assert.AreEqual(3, d.Test.LiveGen); @@ -881,6 +916,7 @@ namespace Umbraco.Tests.Cache { var d = new SnapDictionary(); d.Test.CollectAuto = false; + // gen 1 d.Set(1, "one"); @@ -894,12 +930,11 @@ namespace Umbraco.Tests.Cache Assert.AreEqual("uno", s2.Get(1)); var scopeProvider = GetScopeProvider(); - using (d.GetScopedWriteLock(scopeProvider)) { // creating a snapshot in a write-lock does NOT return the "current" content // it uses the previous snapshot, so new snapshot created only on release - d.Set(1, "ein"); + d.SetLocked(1, "ein"); var s3 = d.CreateSnapshot(); Assert.AreEqual(2, s3.Gen); Assert.AreEqual("uno", s3.Get(1)); @@ -934,12 +969,11 @@ namespace Umbraco.Tests.Cache var scopeContext = new ScopeContext(); var scopeProvider = GetScopeProvider(scopeContext); - using (d.GetScopedWriteLock(scopeProvider)) { // creating a snapshot in a write-lock does NOT return the "current" content // it uses the previous snapshot, so new snapshot created only on release - d.Set(1, "ein"); + d.SetLocked(1, "ein"); var s3 = d.CreateSnapshot(); Assert.AreEqual(2, s3.Gen); Assert.AreEqual("uno", s3.Get(1)); @@ -967,7 +1001,7 @@ namespace Umbraco.Tests.Cache var d = new SnapDictionary(); var t = d.Test; t.CollectAuto = false; - + // gen 1 d.Set(1, "one"); var s1 = d.CreateSnapshot(); @@ -984,12 +1018,11 @@ namespace Umbraco.Tests.Cache var scopeContext = new ScopeContext(); var scopeProvider = GetScopeProvider(scopeContext); - using (d.GetScopedWriteLock(scopeProvider)) { // creating a snapshot in a write-lock does NOT return the "current" content // it uses the previous snapshot, so new snapshot created only on release - d.Set(1, "ein"); + d.SetLocked(1, "ein"); var s3 = d.CreateSnapshot(); Assert.AreEqual(2, s3.Gen); Assert.AreEqual("uno", s3.Get(1)); @@ -997,7 +1030,7 @@ namespace Umbraco.Tests.Cache // we made some changes, so a next gen is required Assert.AreEqual(3, t.LiveGen); Assert.IsTrue(t.NextGen); - Assert.AreEqual(1, t.WLocked); + Assert.IsTrue(t.IsLocked); // but live snapshot contains changes var ls = t.LiveSnapshot; @@ -1008,7 +1041,7 @@ namespace Umbraco.Tests.Cache // nothing is committed until scope exits Assert.AreEqual(3, t.LiveGen); Assert.IsTrue(t.NextGen); - Assert.AreEqual(1, t.WLocked); + Assert.IsTrue(t.IsLocked); // no changes until exit var s4 = d.CreateSnapshot(); @@ -1020,7 +1053,7 @@ namespace Umbraco.Tests.Cache // now things have changed Assert.AreEqual(2, t.LiveGen); Assert.IsFalse(t.NextGen); - Assert.AreEqual(0, t.WLocked); + Assert.IsFalse(t.IsLocked); // no changes since not completed var s5 = d.CreateSnapshot(); @@ -1097,9 +1130,10 @@ namespace Umbraco.Tests.Cache // writer is scope contextual and scoped // when disposed, nothing happens // when the context exists, the writer is released + using (d.GetScopedWriteLock(scopeProvider)) { - d.Set(1, "ein"); + d.SetLocked(1, "ein"); Assert.IsTrue(d.Test.NextGen); Assert.AreEqual(3, d.Test.LiveGen); Assert.IsNotNull(d.Test.GenObj); @@ -1107,7 +1141,7 @@ namespace Umbraco.Tests.Cache } // writer has not released - Assert.AreEqual(1, d.Test.WLocked); + Assert.IsTrue(d.Test.IsLocked); Assert.IsNotNull(d.Test.GenObj); Assert.AreEqual(2, d.Test.GenObj.Gen); @@ -1118,7 +1152,7 @@ namespace Umbraco.Tests.Cache // panic! var s2 = d.CreateSnapshot(); - Assert.AreEqual(1, d.Test.WLocked); + Assert.IsTrue(d.Test.IsLocked); Assert.IsNotNull(d.Test.GenObj); Assert.AreEqual(2, d.Test.GenObj.Gen); Assert.AreEqual(3, d.Test.LiveGen); @@ -1127,7 +1161,7 @@ namespace Umbraco.Tests.Cache // release writer scopeContext.ScopeExit(true); - Assert.AreEqual(0, d.Test.WLocked); + Assert.IsFalse(d.Test.IsLocked); Assert.IsNotNull(d.Test.GenObj); Assert.AreEqual(2, d.Test.GenObj.Gen); Assert.AreEqual(3, d.Test.LiveGen); @@ -1135,7 +1169,7 @@ namespace Umbraco.Tests.Cache var s3 = d.CreateSnapshot(); - Assert.AreEqual(0, d.Test.WLocked); + Assert.IsFalse(d.Test.IsLocked); Assert.IsNotNull(d.Test.GenObj); Assert.AreEqual(3, d.Test.GenObj.Gen); Assert.AreEqual(3, d.Test.LiveGen); diff --git a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs index 82fae0d32a..17eb2b6457 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs @@ -39,7 +39,6 @@ namespace Umbraco.Web.PublishedCache.NuCache private long _liveGen, _floorGen; private bool _nextGen, _collectAuto; private Task _collectTask; - private volatile int _wlocked; private List> _wchanges; // TODO: collection trigger (ok for now) @@ -83,7 +82,6 @@ namespace Umbraco.Web.PublishedCache.NuCache private class WriteLockInfo { public bool Taken; - public bool Count; } // a scope contextual that represents a locked writer to the dictionary @@ -111,7 +109,6 @@ namespace Umbraco.Web.PublishedCache.NuCache // TODO: GetScopedWriter? should the dict have a ref onto the scope provider? public IDisposable GetScopedWriteLock(IScopeProvider scopeProvider) { - _logger.Debug("GetScopedWriteLock"); return ScopeContextualBase.Get(scopeProvider, _instanceId, scoped => new ScopedWriteLock(this, scoped)); } @@ -123,6 +120,9 @@ namespace Umbraco.Web.PublishedCache.NuCache private void Lock(WriteLockInfo lockInfo, bool forceGen = false) { + if (Monitor.IsEntered(_wlocko)) + throw new InvalidOperationException("Recursive locks not allowed"); + Monitor.Enter(_wlocko, ref lockInfo.Taken); lock(_rlocko) @@ -131,9 +131,7 @@ namespace Umbraco.Web.PublishedCache.NuCache try { } finally { - _wlocked++; - lockInfo.Count = true; - if (_nextGen == false || (forceGen && _wlocked == 1)) + if (_nextGen == false || (forceGen)) { // because we are changing things, a new generation // is created, which will trigger a new snapshot @@ -179,12 +177,6 @@ namespace Umbraco.Web.PublishedCache.NuCache _localDb.Commit(); } - // TODO: Shouldn't this be in a finally block? - // TODO: Shouldn't this be decremented after we exit?? - // TODO: Shouldn't the locked flag never exceed 1? - if (lockInfo.Count) - _wlocked--; - // TODO: Shouldn't this be in a finally block? if (lockInfo.Taken) Monitor.Exit(_wlocko); @@ -220,22 +212,18 @@ namespace Umbraco.Web.PublishedCache.NuCache public void ReleaseLocalDb() { - _logger.Info("Releasing DB..."); var lockInfo = new WriteLockInfo(); try { try { - // Trying to lock could throw exceptions so always make sure to clean up. - _logger.Info("Trying to lock before releasing DB (lock count = {LockCount}) ...", _wlocked); - + // Trying to lock could throw exceptions so always make sure to clean up. Lock(lockInfo); } finally { try { - _logger.Info("Disposing local DB..."); _localDb?.Dispose(); } catch (Exception ex) @@ -275,57 +263,61 @@ namespace Umbraco.Web.PublishedCache.NuCache #region Content types - public void NewContentTypes(IEnumerable types) + /// + /// Sets data for new content types + /// + /// + /// + /// This methods MUST be called from within a write lock, normally wrapped within GetScopedWriteLock + /// otherwise an exception will occur. + /// + /// + /// Thrown if this method is not called within a write lock + /// + public void NewContentTypesLocked(IEnumerable types) { - var lockInfo = new WriteLockInfo(); - try - { - _logger.Debug("NewContentTypes"); - Lock(lockInfo); + EnsureLocked(); - foreach (var type in types) - { - SetValueLocked(_contentTypesById, type.Id, type); - SetValueLocked(_contentTypesByAlias, type.Alias, type); - } - } - finally + foreach (var type in types) { - Release(lockInfo); + SetValueLocked(_contentTypesById, type.Id, type); + SetValueLocked(_contentTypesByAlias, type.Alias, type); } } - public void UpdateContentTypes(IEnumerable types) + /// + /// Sets data for updated content types + /// + /// + /// + /// This methods MUST be called from within a write lock, normally wrapped within GetScopedWriteLock + /// otherwise an exception will occur. + /// + /// + /// Thrown if this method is not called within a write lock + /// + public void UpdateContentTypesLocked(IEnumerable types) { //nothing to do if this is empty, no need to lock/allocate/iterate/etc... if (!types.Any()) return; - var lockInfo = new WriteLockInfo(); - try + EnsureLocked(); + + var index = types.ToDictionary(x => x.Id, x => x); + + foreach (var type in index.Values) { - _logger.Debug("UpdateContentTypes"); - Lock(lockInfo); - - var index = types.ToDictionary(x => x.Id, x => x); - - foreach (var type in index.Values) - { - SetValueLocked(_contentTypesById, type.Id, type); - SetValueLocked(_contentTypesByAlias, type.Alias, type); - } - - foreach (var link in _contentNodes.Values) - { - var node = link.Value; - if (node == null) continue; - var contentTypeId = node.ContentType.Id; - if (index.TryGetValue(contentTypeId, out var contentType) == false) continue; - SetValueLocked(_contentNodes, node.Id, new ContentNode(node, contentType)); - } + SetValueLocked(_contentTypesById, type.Id, type); + SetValueLocked(_contentTypesByAlias, type.Alias, type); } - finally + + foreach (var link in _contentNodes.Values) { - Release(lockInfo); + var node = link.Value; + if (node == null) continue; + var contentTypeId = node.ContentType.Id; + if (index.TryGetValue(contentTypeId, out var contentType) == false) continue; + SetValueLocked(_contentNodes, node.Id, new ContentNode(node, contentType)); } } @@ -1256,7 +1248,7 @@ namespace Umbraco.Web.PublishedCache.NuCache // else we need to try to create a new gen ref // whether we are wlocked or not, noone can rlock while we do, // so _liveGen and _nextGen are safe - if (_wlocked > 0) // volatile, cannot ++ but could -- + if (Monitor.IsEntered(_wlocko)) { // write-locked, cannot use latest gen (at least 1) so use previous var snapGen = _nextGen ? _liveGen - 1 : _liveGen; diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs index b761fe0fa4..249eb882f9 100755 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs @@ -622,7 +622,7 @@ namespace Umbraco.Web.PublishedCache.NuCache .Where(x => x.RootContentId.HasValue && x.LanguageIsoCode.IsNullOrWhiteSpace() == false) .Select(x => new Domain(x.Id, x.DomainName, x.RootContentId.Value, CultureInfo.GetCultureInfo(x.LanguageIsoCode), x.IsWildcard))) { - _domainStore.Set(domain.Id, domain); + _domainStore.SetLocked(domain.Id, domain); } } @@ -980,7 +980,7 @@ namespace Umbraco.Web.PublishedCache.NuCache } break; case DomainChangeTypes.Remove: - _domainStore.Clear(payload.Id); + _domainStore.ClearLocked(payload.Id); break; case DomainChangeTypes.Refresh: var domain = _serviceContext.DomainService.GetById(payload.Id); @@ -988,7 +988,7 @@ namespace Umbraco.Web.PublishedCache.NuCache if (domain.RootContentId.HasValue == false) continue; // anomaly if (domain.LanguageIsoCode.IsNullOrWhiteSpace()) continue; // anomaly var culture = CultureInfo.GetCultureInfo(domain.LanguageIsoCode); - _domainStore.Set(domain.Id, new Domain(domain.Id, domain.DomainName, domain.RootContentId.Value, culture, domain.IsWildcard)); + _domainStore.SetLocked(domain.Id, new Domain(domain.Id, domain.DomainName, domain.RootContentId.Value, culture, domain.IsWildcard)); break; } } @@ -1075,9 +1075,9 @@ namespace Umbraco.Web.PublishedCache.NuCache _contentStore.UpdateContentTypes(removedIds, typesA, kits); if (!otherIds.IsCollectionEmpty()) - _contentStore.UpdateContentTypes(CreateContentTypes(PublishedItemType.Content, otherIds.ToArray())); + _contentStore.UpdateContentTypesLocked(CreateContentTypes(PublishedItemType.Content, otherIds.ToArray())); if (!newIds.IsCollectionEmpty()) - _contentStore.NewContentTypes(CreateContentTypes(PublishedItemType.Content, newIds.ToArray())); + _contentStore.NewContentTypesLocked(CreateContentTypes(PublishedItemType.Content, newIds.ToArray())); scope.Complete(); } } @@ -1106,9 +1106,9 @@ namespace Umbraco.Web.PublishedCache.NuCache _mediaStore.UpdateContentTypes(removedIds, typesA, kits); if (!otherIds.IsCollectionEmpty()) - _mediaStore.UpdateContentTypes(CreateContentTypes(PublishedItemType.Media, otherIds.ToArray()).ToArray()); + _mediaStore.UpdateContentTypesLocked(CreateContentTypes(PublishedItemType.Media, otherIds.ToArray()).ToArray()); if (!newIds.IsCollectionEmpty()) - _mediaStore.NewContentTypes(CreateContentTypes(PublishedItemType.Media, newIds.ToArray()).ToArray()); + _mediaStore.NewContentTypesLocked(CreateContentTypes(PublishedItemType.Media, newIds.ToArray()).ToArray()); scope.Complete(); } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/SnapDictionary.cs b/src/Umbraco.Web/PublishedCache/NuCache/SnapDictionary.cs index e0ad26eb81..c38940da25 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/SnapDictionary.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/SnapDictionary.cs @@ -35,7 +35,6 @@ namespace Umbraco.Web.PublishedCache.NuCache private long _liveGen, _floorGen; private bool _nextGen, _collectAuto; private Task _collectTask; - private volatile int _wlocked; // minGenDelta to be adjusted // we may want to throttle collects even if delta is reached @@ -84,7 +83,6 @@ namespace Umbraco.Web.PublishedCache.NuCache private class WriteLockInfo { public bool Taken; - public bool Count; } // a scope contextual that represents a locked writer to the dictionary @@ -92,8 +90,7 @@ namespace Umbraco.Web.PublishedCache.NuCache { private readonly WriteLockInfo _lockinfo = new WriteLockInfo(); private readonly SnapDictionary _dictionary; - private int _released; - + public ScopedWriteLock(SnapDictionary dictionary, bool scoped) { _dictionary = dictionary; @@ -102,8 +99,6 @@ namespace Umbraco.Web.PublishedCache.NuCache public override void Release(bool completed) { - if (Interlocked.CompareExchange(ref _released, 1, 0) != 0) - return; _dictionary.Release(_lockinfo, completed); } } @@ -117,8 +112,17 @@ namespace Umbraco.Web.PublishedCache.NuCache return ScopeContextualBase.Get(scopeProvider, _instanceId, scoped => new ScopedWriteLock(this, scoped)); } + private void EnsureLocked() + { + if (!Monitor.IsEntered(_wlocko)) + throw new InvalidOperationException("Write lock must be acquried."); + } + private void Lock(WriteLockInfo lockInfo, bool forceGen = false) { + if (Monitor.IsEntered(_wlocko)) + throw new InvalidOperationException("Recursive locks not allowed"); + Monitor.Enter(_wlocko, ref lockInfo.Taken); lock(_rlocko) @@ -132,11 +136,7 @@ namespace Umbraco.Web.PublishedCache.NuCache try { } finally { - // increment the lock count, and register that this lock is counting - _wlocked++; - lockInfo.Count = true; - - if (_nextGen == false || (forceGen && _wlocked == 1)) + if (_nextGen == false || (forceGen)) { // because we are changing things, a new generation // is created, which will trigger a new snapshot @@ -181,8 +181,7 @@ namespace Umbraco.Web.PublishedCache.NuCache } } - // decrement the lock count, if counting, then exit the lock - if (lockInfo.Count) _wlocked--; + // TODO: Shouldn't this be in a finally block? Monitor.Exit(_wlocko); } @@ -198,75 +197,59 @@ namespace Umbraco.Web.PublishedCache.NuCache return link; } - public void Set(TKey key, TValue value) + public void SetLocked(TKey key, TValue value) { - var lockInfo = new WriteLockInfo(); - try - { - Lock(lockInfo); + EnsureLocked(); - // this is safe only because we're write-locked - var link = GetHead(key); - if (link != null) + // this is safe only because we're write-locked + var link = GetHead(key); + if (link != null) + { + // already in the dict + if (link.Gen != _liveGen) { - // already in the dict - if (link.Gen != _liveGen) - { - // for an older gen - if value is different then insert a new - // link for the new gen, with the new value - if (link.Value != value) - _items.TryUpdate(key, new LinkedNode(value, _liveGen, link), link); - } - else - { - // for the live gen - we can fix the live gen - and remove it - // if value is null and there's no next gen - if (value == null && link.Next == null) - _items.TryRemove(key, out link); - else - link.Value = value; - } + // for an older gen - if value is different then insert a new + // link for the new gen, with the new value + if (link.Value != value) + _items.TryUpdate(key, new LinkedNode(value, _liveGen, link), link); } else { - _items.TryAdd(key, new LinkedNode(value, _liveGen)); - } - } - finally - { - Release(lockInfo); - } - } - - public void Clear(TKey key) - { - Set(key, null); - } - - public void Clear() - { - var lockInfo = new WriteLockInfo(); - try - { - Lock(lockInfo); - - // this is safe only because we're write-locked - foreach (var kvp in _items.Where(x => x.Value != null)) - { - if (kvp.Value.Gen < _liveGen) - { - var link = new LinkedNode(null, _liveGen, kvp.Value); - _items.TryUpdate(kvp.Key, link, kvp.Value); - } + // for the live gen - we can fix the live gen - and remove it + // if value is null and there's no next gen + if (value == null && link.Next == null) + _items.TryRemove(key, out link); else - { - kvp.Value.Value = null; - } + link.Value = value; } } - finally + else { - Release(lockInfo); + _items.TryAdd(key, new LinkedNode(value, _liveGen)); + } + } + + public void ClearLocked(TKey key) + { + SetLocked(key, null); + } + + public void ClearLocked() + { + EnsureLocked(); + + // this is safe only because we're write-locked + foreach (var kvp in _items.Where(x => x.Value != null)) + { + if (kvp.Value.Gen < _liveGen) + { + var link = new LinkedNode(null, _liveGen, kvp.Value); + _items.TryUpdate(kvp.Key, link, kvp.Value); + } + else + { + kvp.Value.Value = null; + } } } @@ -336,7 +319,7 @@ namespace Umbraco.Web.PublishedCache.NuCache // else we need to try to create a new gen object // whether we are wlocked or not, noone can rlock while we do, // so _liveGen and _nextGen are safe - if (_wlocked > 0) // volatile, cannot ++ but could -- + if (Monitor.IsEntered(_wlocko)) { // write-locked, cannot use latest gen (at least 1) so use previous var snapGen = _nextGen ? _liveGen - 1 : _liveGen; @@ -468,17 +451,18 @@ namespace Umbraco.Web.PublishedCache.NuCache } } - public /*async*/ Task PendingCollect() - { - Task task; - lock (_rlocko) - { - task = _collectTask; - } - return task ?? Task.CompletedTask; - //if (task != null) - // await task; - } + // TODO: This is never used? Should it be? Maybe move to TestHelper below? + //public /*async*/ Task PendingCollect() + //{ + // Task task; + // lock (_rlocko) + // { + // task = _collectTask; + // } + // return task ?? Task.CompletedTask; + // //if (task != null) + // // await task; + //} public long GenCount => _genObjs.Count; @@ -503,7 +487,7 @@ namespace Umbraco.Web.PublishedCache.NuCache public long LiveGen => _dict._liveGen; public long FloorGen => _dict._floorGen; public bool NextGen => _dict._nextGen; - public int WLocked => _dict._wlocked; + public bool IsLocked => Monitor.IsEntered(_dict._wlocko); public bool CollectAuto { From 7a129f890dc5b87094901d2ac3bbe953486be04b Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 3 Jan 2020 15:07:21 +1100 Subject: [PATCH 126/202] Changes MainDom testing timeout to be larger... but not 1.5 mins! --- src/Umbraco.Core/Runtime/MainDom.cs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/Umbraco.Core/Runtime/MainDom.cs b/src/Umbraco.Core/Runtime/MainDom.cs index 883b69b2a7..d7bd407015 100644 --- a/src/Umbraco.Core/Runtime/MainDom.cs +++ b/src/Umbraco.Core/Runtime/MainDom.cs @@ -36,7 +36,7 @@ namespace Umbraco.Core.Runtime // actions to run before releasing the main domain private readonly List> _callbacks = new List>(); - private const int LockTimeoutMilliseconds = 10000; // 10 seconds + private const int LockTimeoutMilliseconds = 40000; // 40 seconds #endregion @@ -151,22 +151,20 @@ namespace Umbraco.Core.Runtime { _logger.Info("Cannot acquire (timeout)."); - // TODO: Previously we'd throw an exception and the appdomain would not start, what do we want to do? - - // return false; + // TODO: In previous versions we'd let a TimeoutException be thrown + // and the appdomain would not start. We have the opportunity to allow it to + // start without having MainDom? This would mean that it couldn't write + // to nucache/examine and would only be ok if this was a super short lived appdomain. + // maybe safer to just keep throwing in this case. throw new TimeoutException("Cannot acquire MainDom"); + // return false; } try { // Listen for the signal from another AppDomain coming online to release the lock - _mainDomLock.ListenAsync() - .ContinueWith(_ => - { - _logger.Debug("Signal heard from other appdomain."); - OnSignal("signal"); - }); + _mainDomLock.ListenAsync().ContinueWith(_ => OnSignal("signal")); } catch (OperationCanceledException ex) { From c355e8f5f0938b2ef9eb5a7247df43728a7f61f0 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Sun, 5 Jan 2020 22:26:13 +0100 Subject: [PATCH 127/202] Use ExternalIndexName as default parameter value --- src/Umbraco.Web/IPublishedContentQuery.cs | 4 ++-- src/Umbraco.Web/PublishedContentQuery.cs | 7 ++----- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Web/IPublishedContentQuery.cs b/src/Umbraco.Web/IPublishedContentQuery.cs index c4d829968f..7066475dc9 100644 --- a/src/Umbraco.Web/IPublishedContentQuery.cs +++ b/src/Umbraco.Web/IPublishedContentQuery.cs @@ -49,7 +49,7 @@ namespace Umbraco.Web /// /// While enumerating results, the ambient culture is changed to be the searched culture. /// - IEnumerable Search(string term, string culture = "*", string indexName = null); + IEnumerable Search(string term, string culture = "*", string indexName = Constants.UmbracoIndexes.ExternalIndexName); /// /// Searches content. @@ -71,7 +71,7 @@ namespace Umbraco.Web /// /// While enumerating results, the ambient culture is changed to be the searched culture. /// - IEnumerable Search(string term, int skip, int take, out long totalRecords, string culture = "*", string indexName = null); + IEnumerable Search(string term, int skip, int take, out long totalRecords, string culture = "*", string indexName = Constants.UmbracoIndexes.ExternalIndexName); /// /// Executes the query and converts the results to . diff --git a/src/Umbraco.Web/PublishedContentQuery.cs b/src/Umbraco.Web/PublishedContentQuery.cs index 833df9971b..44a3208956 100644 --- a/src/Umbraco.Web/PublishedContentQuery.cs +++ b/src/Umbraco.Web/PublishedContentQuery.cs @@ -183,13 +183,13 @@ namespace Umbraco.Web #region Search /// - public IEnumerable Search(string term, string culture = "*", string indexName = null) + public IEnumerable Search(string term, string culture = "*", string indexName = Constants.UmbracoIndexes.ExternalIndexName) { return Search(term, 0, 0, out _, culture, indexName); } /// - public IEnumerable Search(string term, int skip, int take, out long totalRecords, string culture = "*", string indexName = null) + public IEnumerable Search(string term, int skip, int take, out long totalRecords, string culture = "*", string indexName = Constants.UmbracoIndexes.ExternalIndexName) { if (string.IsNullOrEmpty(indexName)) { @@ -319,9 +319,6 @@ namespace Umbraco.Web } } - - - #endregion } } From 3bd7718c38e03212a7d9061be7b40e1061611786 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Sun, 5 Jan 2020 22:27:56 +0100 Subject: [PATCH 128/202] Remove ordering by score when converting to PublishedSearchResult --- src/Umbraco.Web/ExamineExtensions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web/ExamineExtensions.cs b/src/Umbraco.Web/ExamineExtensions.cs index 8fead91194..421993f8fd 100644 --- a/src/Umbraco.Web/ExamineExtensions.cs +++ b/src/Umbraco.Web/ExamineExtensions.cs @@ -32,7 +32,7 @@ namespace Umbraco.Web var publishedSearchResults = new List(); - foreach (var result in results.OrderByDescending(x => x.Score)) + foreach (var result in results) { if (int.TryParse(result.Id, out var contentId) && cache.GetById(contentId) is IPublishedContent content) @@ -62,7 +62,7 @@ namespace Umbraco.Web var publishedSearchResults = new List(); - foreach (var result in results.OrderByDescending(x => x.Score)) + foreach (var result in results) { if (int.TryParse(result.Id, out var contentId) && result.Values.TryGetValue(LuceneIndex.CategoryFieldName, out var indexType)) From fa23b50d1ee3248499591ffb781d2486d6fa06fe Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Sun, 5 Jan 2020 22:55:55 +0100 Subject: [PATCH 129/202] Add skip and take argument validation --- src/Umbraco.Web/PublishedContentQuery.cs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/Umbraco.Web/PublishedContentQuery.cs b/src/Umbraco.Web/PublishedContentQuery.cs index 44a3208956..d697898f33 100644 --- a/src/Umbraco.Web/PublishedContentQuery.cs +++ b/src/Umbraco.Web/PublishedContentQuery.cs @@ -191,6 +191,16 @@ namespace Umbraco.Web /// public IEnumerable Search(string term, int skip, int take, out long totalRecords, string culture = "*", string indexName = Constants.UmbracoIndexes.ExternalIndexName) { + if (skip < 0) + { + throw new ArgumentOutOfRangeException(nameof(skip), skip, "The value must be greater than or equal to zero."); + } + + if (take < 0) + { + throw new ArgumentOutOfRangeException(nameof(take), take, "The value must be greater than or equal to zero."); + } + if (string.IsNullOrEmpty(indexName)) { indexName = Constants.UmbracoIndexes.ExternalIndexName; @@ -240,6 +250,16 @@ namespace Umbraco.Web /// public IEnumerable Search(IQueryExecutor query, int skip, int take, out long totalRecords) { + if (skip < 0) + { + throw new ArgumentOutOfRangeException(nameof(skip), skip, "The value must be greater than or equal to zero."); + } + + if (take < 0) + { + throw new ArgumentOutOfRangeException(nameof(take), take, "The value must be greater than or equal to zero."); + } + var results = skip == 0 && take == 0 ? query.Execute() : query.Execute(skip + take); From c2ac5e85311b39ee0d9c8734659a39fb6be55ab9 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 6 Jan 2020 18:34:04 +1100 Subject: [PATCH 130/202] Fixes a couple more issues with recursive locks --- .../PublishedCache/NuCache/ContentStore.cs | 244 +++++++++--------- .../NuCache/PublishedSnapshotService.cs | 8 +- 2 files changed, 130 insertions(+), 122 deletions(-) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs index 17eb2b6457..8ce299932c 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs @@ -14,7 +14,18 @@ using Umbraco.Web.PublishedCache.NuCache.Snap; namespace Umbraco.Web.PublishedCache.NuCache { - // stores content + /// + /// Stores content in memory and persists it back to disk + /// + /// + /// + /// Methods in this class suffixed with the term "Locked" means that those methods can only be called within a WriteLock. A WriteLock + /// is acquired by the GetScopedWriteLock method. Locks are not allowed to be recursive. + /// + /// + /// This class's logic is based on the class but has been slightly modified to suit these purposes. + /// + /// internal class ContentStore { // this class is an extended version of SnapDictionary @@ -336,8 +347,6 @@ namespace Umbraco.Web.PublishedCache.NuCache { EnsureLocked(); - _logger.Debug("SetAllContentTypes"); - // clear all existing content types ClearLocked(_contentTypesById); ClearLocked(_contentTypesByAlias); @@ -353,8 +362,23 @@ namespace Umbraco.Web.PublishedCache.NuCache // assuming we are going to SetAll content items! } - public void UpdateContentTypes(IReadOnlyCollection removedIds, IReadOnlyCollection refreshedTypes, IReadOnlyCollection kits) + /// + /// Updates/sets/removes data for content types + /// + /// + /// + /// + /// + /// This methods MUST be called from within a write lock, normally wrapped within GetScopedWriteLock + /// otherwise an exception will occur. + /// + /// + /// Thrown if this method is not called within a write lock + /// + public void UpdateContentTypesLocked(IReadOnlyCollection removedIds, IReadOnlyCollection refreshedTypes, IReadOnlyCollection kits) { + EnsureLocked(); + var removedIdsA = removedIds ?? Array.Empty(); var refreshedTypesA = refreshedTypes ?? Array.Empty(); var refreshedIdsA = refreshedTypesA.Select(x => x.Id).ToList(); @@ -363,87 +387,82 @@ namespace Umbraco.Web.PublishedCache.NuCache if (kits.Count == 0 && refreshedIdsA.Count == 0 && removedIdsA.Count == 0) return; //exit - there is nothing to do here - var lockInfo = new WriteLockInfo(); - try + var removedContentTypeNodes = new List(); + var refreshedContentTypeNodes = new List(); + + // find all the nodes that are either refreshed or removed, + // because of their content type being either refreshed or removed + foreach (var link in _contentNodes.Values) { - _logger.Debug("UpdateContentTypes"); - Lock(lockInfo); - - var removedContentTypeNodes = new List(); - var refreshedContentTypeNodes = new List(); - - // find all the nodes that are either refreshed or removed, - // because of their content type being either refreshed or removed - foreach (var link in _contentNodes.Values) - { - var node = link.Value; - if (node == null) continue; - var contentTypeId = node.ContentType.Id; - if (removedIdsA.Contains(contentTypeId)) removedContentTypeNodes.Add(node.Id); - if (refreshedIdsA.Contains(contentTypeId)) refreshedContentTypeNodes.Add(node.Id); - } - - // perform deletion of content with removed content type - // removing content types should have removed their content already - // but just to be 100% sure, clear again here - foreach (var node in removedContentTypeNodes) - ClearBranchLocked(node); - - // perform deletion of removed content types - foreach (var id in removedIdsA) - { - if (_contentTypesById.TryGetValue(id, out var link) == false || link.Value == null) - continue; - SetValueLocked(_contentTypesById, id, null); - SetValueLocked(_contentTypesByAlias, link.Value.Alias, null); - } - - // perform update of refreshed content types - foreach (var type in refreshedTypesA) - { - SetValueLocked(_contentTypesById, type.Id, type); - SetValueLocked(_contentTypesByAlias, type.Alias, type); - } - - // perform update of content with refreshed content type - from the kits - // skip missing type, skip missing parents & un-buildable kits - what else could we do? - // kits are ordered by level, so ParentExists is ok here - var visited = new List(); - foreach (var kit in kits.Where(x => - refreshedIdsA.Contains(x.ContentTypeId) && - BuildKit(x, out _))) - { - // replacing the node: must preserve the parents - var node = GetHead(_contentNodes, kit.Node.Id)?.Value; - if (node != null) - kit.Node.FirstChildContentId = node.FirstChildContentId; - - SetValueLocked(_contentNodes, kit.Node.Id, kit.Node); - - visited.Add(kit.Node.Id); - if (_localDb != null) RegisterChange(kit.Node.Id, kit); - } - - // all content should have been refreshed - but... - var orphans = refreshedContentTypeNodes.Except(visited); - foreach (var id in orphans) - ClearBranchLocked(id); + var node = link.Value; + if (node == null) continue; + var contentTypeId = node.ContentType.Id; + if (removedIdsA.Contains(contentTypeId)) removedContentTypeNodes.Add(node.Id); + if (refreshedIdsA.Contains(contentTypeId)) refreshedContentTypeNodes.Add(node.Id); } - finally + + // perform deletion of content with removed content type + // removing content types should have removed their content already + // but just to be 100% sure, clear again here + foreach (var node in removedContentTypeNodes) + ClearBranchLocked(node); + + // perform deletion of removed content types + foreach (var id in removedIdsA) { - Release(lockInfo); + if (_contentTypesById.TryGetValue(id, out var link) == false || link.Value == null) + continue; + SetValueLocked(_contentTypesById, id, null); + SetValueLocked(_contentTypesByAlias, link.Value.Alias, null); } + + // perform update of refreshed content types + foreach (var type in refreshedTypesA) + { + SetValueLocked(_contentTypesById, type.Id, type); + SetValueLocked(_contentTypesByAlias, type.Alias, type); + } + + // perform update of content with refreshed content type - from the kits + // skip missing type, skip missing parents & un-buildable kits - what else could we do? + // kits are ordered by level, so ParentExists is ok here + var visited = new List(); + foreach (var kit in kits.Where(x => + refreshedIdsA.Contains(x.ContentTypeId) && + BuildKit(x, out _))) + { + // replacing the node: must preserve the parents + var node = GetHead(_contentNodes, kit.Node.Id)?.Value; + if (node != null) + kit.Node.FirstChildContentId = node.FirstChildContentId; + + SetValueLocked(_contentNodes, kit.Node.Id, kit.Node); + + visited.Add(kit.Node.Id); + if (_localDb != null) RegisterChange(kit.Node.Id, kit); + } + + // all content should have been refreshed - but... + var orphans = refreshedContentTypeNodes.Except(visited); + foreach (var id in orphans) + ClearBranchLocked(id); } - public void UpdateDataTypes(IEnumerable dataTypeIds, Func getContentType) + /// + /// Updates data types + /// + /// + /// + /// + /// This methods MUST be called from within a write lock, normally wrapped within GetScopedWriteLock + /// otherwise an exception will occur. + /// + /// + /// Thrown if this method is not called within a write lock + /// + public void UpdateDataTypesLocked(IEnumerable dataTypeIds, Func getContentType) { - var lockInfo = new WriteLockInfo(); - try - { - _logger.Debug("UpdateDataTypes"); - Lock(lockInfo); - - var contentTypes = _contentTypesById + var contentTypes = _contentTypesById .Where(kvp => kvp.Value.Value != null && kvp.Value.Value.PropertyTypes.Any(p => dataTypeIds.Contains(p.DataType.Id))) @@ -452,37 +471,32 @@ namespace Umbraco.Web.PublishedCache.NuCache .Where(x => x != null) // poof, gone, very unlikely and probably an anomaly .ToArray(); - var contentTypeIdsA = contentTypes.Select(x => x.Id).ToArray(); - var contentTypeNodes = new Dictionary>(); - foreach (var id in contentTypeIdsA) - contentTypeNodes[id] = new List(); - foreach (var link in _contentNodes.Values) - { - var node = link.Value; - if (node != null && contentTypeIdsA.Contains(node.ContentType.Id)) - contentTypeNodes[node.ContentType.Id].Add(node.Id); - } - - foreach (var contentType in contentTypes) - { - // again, weird situation - if (contentTypeNodes.ContainsKey(contentType.Id) == false) - continue; - - foreach (var id in contentTypeNodes[contentType.Id]) - { - _contentNodes.TryGetValue(id, out var link); - if (link?.Value == null) - continue; - var node = new ContentNode(link.Value, contentType); - SetValueLocked(_contentNodes, id, node); - if (_localDb != null) RegisterChange(id, node.ToKit()); - } - } - } - finally + var contentTypeIdsA = contentTypes.Select(x => x.Id).ToArray(); + var contentTypeNodes = new Dictionary>(); + foreach (var id in contentTypeIdsA) + contentTypeNodes[id] = new List(); + foreach (var link in _contentNodes.Values) { - Release(lockInfo); + var node = link.Value; + if (node != null && contentTypeIdsA.Contains(node.ContentType.Id)) + contentTypeNodes[node.ContentType.Id].Add(node.Id); + } + + foreach (var contentType in contentTypes) + { + // again, weird situation + if (contentTypeNodes.ContainsKey(contentType.Id) == false) + continue; + + foreach (var id in contentTypeNodes[contentType.Id]) + { + _contentNodes.TryGetValue(id, out var link); + if (link?.Value == null) + continue; + var node = new ContentNode(link.Value, contentType); + SetValueLocked(_contentNodes, id, node); + if (_localDb != null) RegisterChange(id, node.ToKit()); + } } } @@ -644,8 +658,6 @@ namespace Umbraco.Web.PublishedCache.NuCache { EnsureLocked(); - _logger.Debug("SetAllFastSorted"); - var ok = true; ClearLocked(_contentNodes); @@ -684,7 +696,7 @@ namespace Umbraco.Web.PublishedCache.NuCache previousNode = null; // there is no previous sibling } - _logger.Verbose($"Set {thisNode.Id} with parent {thisNode.ParentContentId}"); + _logger.Debug($"Set {thisNode.Id} with parent {thisNode.ParentContentId}"); SetValueLocked(_contentNodes, thisNode.Id, thisNode); // if we are initializing from the database source ensure the local db is updated @@ -726,8 +738,7 @@ namespace Umbraco.Web.PublishedCache.NuCache EnsureLocked(); var ok = true; - _logger.Debug("SetAll"); - + ClearLocked(_contentNodes); ClearRootLocked(); @@ -742,7 +753,7 @@ namespace Umbraco.Web.PublishedCache.NuCache ok = false; continue; // skip that one } - _logger.Verbose($"Set {kit.Node.Id} with parent {kit.Node.ParentContentId}"); + _logger.Debug($"Set {kit.Node.Id} with parent {kit.Node.ParentContentId}"); SetValueLocked(_contentNodes, kit.Node.Id, kit.Node); if (_localDb != null) RegisterChange(kit.Node.Id, kit); @@ -777,8 +788,7 @@ namespace Umbraco.Web.PublishedCache.NuCache EnsureLocked(); var ok = true; - _logger.Debug("SetBranch"); - + // get existing _contentNodes.TryGetValue(rootContentId, out var link); var existing = link?.Value; @@ -826,8 +836,6 @@ namespace Umbraco.Web.PublishedCache.NuCache { EnsureLocked(); - _logger.Debug("Clear"); - // try to find the content // if it is not there, nothing to do _contentNodes.TryGetValue(id, out var link); // else null diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs index 249eb882f9..c37e5731e4 100755 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs @@ -943,14 +943,14 @@ namespace Umbraco.Web.PublishedCache.NuCache using (var scope = _scopeProvider.CreateScope()) { scope.ReadLock(Constants.Locks.ContentTree); - _contentStore.UpdateDataTypes(idsA, id => CreateContentType(PublishedItemType.Content, id)); + _contentStore.UpdateDataTypesLocked(idsA, id => CreateContentType(PublishedItemType.Content, id)); scope.Complete(); } using (var scope = _scopeProvider.CreateScope()) { scope.ReadLock(Constants.Locks.MediaTree); - _mediaStore.UpdateDataTypes(idsA, id => CreateContentType(PublishedItemType.Media, id)); + _mediaStore.UpdateDataTypesLocked(idsA, id => CreateContentType(PublishedItemType.Media, id)); scope.Complete(); } } @@ -1073,7 +1073,7 @@ namespace Umbraco.Web.PublishedCache.NuCache ? Array.Empty() : _dataSource.GetTypeContentSources(scope, refreshedIds).ToArray(); - _contentStore.UpdateContentTypes(removedIds, typesA, kits); + _contentStore.UpdateContentTypesLocked(removedIds, typesA, kits); if (!otherIds.IsCollectionEmpty()) _contentStore.UpdateContentTypesLocked(CreateContentTypes(PublishedItemType.Content, otherIds.ToArray())); if (!newIds.IsCollectionEmpty()) @@ -1104,7 +1104,7 @@ namespace Umbraco.Web.PublishedCache.NuCache ? Array.Empty() : _dataSource.GetTypeMediaSources(scope, refreshedIds).ToArray(); - _mediaStore.UpdateContentTypes(removedIds, typesA, kits); + _mediaStore.UpdateContentTypesLocked(removedIds, typesA, kits); if (!otherIds.IsCollectionEmpty()) _mediaStore.UpdateContentTypesLocked(CreateContentTypes(PublishedItemType.Media, otherIds.ToArray()).ToArray()); if (!newIds.IsCollectionEmpty()) From 94d05b1768bfc2fdc0b6fd8dc11d885d3281d63e Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 6 Jan 2020 21:14:46 +1100 Subject: [PATCH 131/202] Fixes #7404 - Deadlock issue --- .../PublishedCache/NuCache/PublishedSnapshotService.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs index 80662f5db0..6e6ad7b79f 100755 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs @@ -54,6 +54,7 @@ namespace Umbraco.Web.PublishedCache.NuCache private readonly ContentStore _mediaStore; private readonly SnapDictionary _domainStore; private readonly object _storesLock = new object(); + private readonly object _elementsLock = new object(); private BPlusTree _localContentDb; private BPlusTree _localMediaDb; @@ -1125,7 +1126,13 @@ namespace Umbraco.Web.PublishedCache.NuCache ContentStore.Snapshot contentSnap, mediaSnap; SnapDictionary.Snapshot domainSnap; IAppCache elementsCache; - lock (_storesLock) + + // Here we are reading/writing to shared objects so we need to lock (can't be _storesLock which manages the actual nucache files + // and would result in a deadlock). Even though we are locking around underlying readlocks (within CreateSnapshot) it's because + // we need to ensure that the result of contentSnap.Gen (etc) and the re-assignment of these values and _elements cache + // are done atomically. + + lock (_elementsLock) { var scopeContext = _scopeProvider.Context; From 95337d5a7008469a1cfc84215b9c98e461798fe1 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 6 Jan 2020 21:39:26 +1100 Subject: [PATCH 132/202] Cleans up old notes --- src/Umbraco.Core/Runtime/MainDom.cs | 2 +- .../Cache/SnapDictionaryTests.cs | 82 +++++++++---------- .../PublishedCache/NuCache/ContentStore.cs | 60 +++++++------- .../NuCache/PublishedSnapshot.cs | 8 -- .../NuCache/PublishedSnapshotService.cs | 10 +-- 5 files changed, 73 insertions(+), 89 deletions(-) diff --git a/src/Umbraco.Core/Runtime/MainDom.cs b/src/Umbraco.Core/Runtime/MainDom.cs index d7bd407015..67c7be3d21 100644 --- a/src/Umbraco.Core/Runtime/MainDom.cs +++ b/src/Umbraco.Core/Runtime/MainDom.cs @@ -151,7 +151,7 @@ namespace Umbraco.Core.Runtime { _logger.Info("Cannot acquire (timeout)."); - // TODO: In previous versions we'd let a TimeoutException be thrown + // In previous versions we'd let a TimeoutException be thrown // and the appdomain would not start. We have the opportunity to allow it to // start without having MainDom? This would mean that it couldn't write // to nucache/examine and would only be ok if this was a super short lived appdomain. diff --git a/src/Umbraco.Tests/Cache/SnapDictionaryTests.cs b/src/Umbraco.Tests/Cache/SnapDictionaryTests.cs index 86426d196c..00ba721bdb 100644 --- a/src/Umbraco.Tests/Cache/SnapDictionaryTests.cs +++ b/src/Umbraco.Tests/Cache/SnapDictionaryTests.cs @@ -9,47 +9,6 @@ using Umbraco.Web.PublishedCache.NuCache.Snap; namespace Umbraco.Tests.Cache { - /// - /// Used for tests - /// - public static class SnapDictionaryExtensions - { - internal static void Set(this SnapDictionary d, TKey key, TValue value) - where TValue : class - { - using (d.GetScopedWriteLock(GetScopeProvider())) - { - d.SetLocked(key, value); - } - } - - internal static void Clear(this SnapDictionary d) - where TValue : class - { - using (d.GetScopedWriteLock(GetScopeProvider())) - { - d.ClearLocked(); - } - } - - internal static void Clear(this SnapDictionary d, TKey key) - where TValue : class - { - using (d.GetScopedWriteLock(GetScopeProvider())) - { - d.ClearLocked(key); - } - } - - private static IScopeProvider GetScopeProvider() - { - var scopeProvider = Mock.Of(); - Mock.Get(scopeProvider) - .Setup(x => x.Context).Returns(() => null); - return scopeProvider; - } - } - [TestFixture] public class SnapDictionaryTests { @@ -1184,4 +1143,45 @@ namespace Umbraco.Tests.Cache return scopeProvider; } } + + /// + /// Used for tests so that we don't have to wrap every Set/Clear call in locks + /// + public static class SnapDictionaryExtensions + { + internal static void Set(this SnapDictionary d, TKey key, TValue value) + where TValue : class + { + using (d.GetScopedWriteLock(GetScopeProvider())) + { + d.SetLocked(key, value); + } + } + + internal static void Clear(this SnapDictionary d) + where TValue : class + { + using (d.GetScopedWriteLock(GetScopeProvider())) + { + d.ClearLocked(); + } + } + + internal static void Clear(this SnapDictionary d, TKey key) + where TValue : class + { + using (d.GetScopedWriteLock(GetScopeProvider())) + { + d.ClearLocked(key); + } + } + + private static IScopeProvider GetScopeProvider() + { + var scopeProvider = Mock.Of(); + Mock.Get(scopeProvider) + .Setup(x => x.Context).Returns(() => null); + return scopeProvider; + } + } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs index 8ce299932c..c959d9f67a 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs @@ -157,40 +157,44 @@ namespace Umbraco.Web.PublishedCache.NuCache private void Release(WriteLockInfo lockInfo, bool commit = true) { - if (commit == false) + try { - lock(_rlocko) + if (commit == false) { - // see SnapDictionary - try { } - finally + lock (_rlocko) { - _nextGen = false; - _liveGen -= 1; + // see SnapDictionary + try { } + finally + { + _nextGen = false; + _liveGen -= 1; + } } - } - Rollback(_contentNodes); - RollbackRoot(); - Rollback(_contentTypesById); - Rollback(_contentTypesByAlias); - } - else if (_localDb != null && _wchanges != null) - { - foreach (var change in _wchanges) + Rollback(_contentNodes); + RollbackRoot(); + Rollback(_contentTypesById); + Rollback(_contentTypesByAlias); + } + else if (_localDb != null && _wchanges != null) { - if (change.Value.IsNull) - _localDb.TryRemove(change.Key, out ContentNodeKit unused); - else - _localDb[change.Key] = change.Value; + foreach (var change in _wchanges) + { + if (change.Value.IsNull) + _localDb.TryRemove(change.Key, out ContentNodeKit unused); + else + _localDb[change.Key] = change.Value; + } + _wchanges = null; + _localDb.Commit(); } - _wchanges = null; - _localDb.Commit(); } - - // TODO: Shouldn't this be in a finally block? - if (lockInfo.Taken) - Monitor.Exit(_wlocko); + finally + { + if (lockInfo.Taken) + Monitor.Exit(_wlocko); + } } private void RollbackRoot() @@ -256,10 +260,6 @@ namespace Umbraco.Web.PublishedCache.NuCache } finally { - _logger.Info("Releasing ContentStore..."); - - // TODO: I don't understand this, we would have already closed the BPlusTree store above?? - // I guess it's 'safe' and will just decrement the write lock counter? Release(lockInfo); } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshot.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshot.cs index bbb84fc650..3f5c1aa4d5 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshot.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshot.cs @@ -39,15 +39,7 @@ namespace Umbraco.Web.PublishedCache.NuCache public void Resync() { - // This is annoying since this can cause deadlocks. Since this will be cleared if something happens in the same - // thread after that calls Elements, it will re-lock the _storesLock but that might already be locked again - // and since we're most likely in a ContentStore write lock, the other thread is probably wanting that one too. - // no lock - published snapshots are single-thread - - // TODO: Instead of clearing, we could hold this value in another var in case the above call to GetElements() Or potentially - // a new TryGetElements() fails (since we might be shutting down). In that case we can use the old value. - _elements?.Dispose(); _elements = null; } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs index c37e5731e4..aee588d567 100755 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs @@ -670,15 +670,7 @@ namespace Umbraco.Web.PublishedCache.NuCache publishedChanged = publishedChanged2; } - // TODO: These resync's are a problem, they cause deadlocks because when this is called, it's generally called within a writelock - // and then we clear out the snapshot and then if there's some event handler that needs the content cache, it tries to re-get it - // which first tries locking on the _storesLock which may have already been acquired and in this case we deadlock because - // we're still holding the write lock. - // We resync at the end of a ScopedWriteLock - // BUT if we don't resync here then the current snapshot is out of date for any event handlers that wish to use the most up to date - // data... - // so we need to figure out how to deal with the _storesLock - + if (draftChanged || publishedChanged) ((PublishedSnapshot)CurrentPublishedSnapshot)?.Resync(); } From 838b26cedb4cd75d6e6cf25bf8926699142b1742 Mon Sep 17 00:00:00 2001 From: Claus Date: Tue, 7 Jan 2020 13:04:52 +0100 Subject: [PATCH 133/202] bump version to 8.4.1 --- src/SolutionInfo.cs | 4 ++-- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/SolutionInfo.cs b/src/SolutionInfo.cs index 5614cb8c41..0da90a03fd 100644 --- a/src/SolutionInfo.cs +++ b/src/SolutionInfo.cs @@ -18,5 +18,5 @@ using System.Resources; [assembly: AssemblyVersion("8.0.0")] // these are FYI and changed automatically -[assembly: AssemblyFileVersion("8.4.0")] -[assembly: AssemblyInformationalVersion("8.4.0")] +[assembly: AssemblyFileVersion("8.4.1")] +[assembly: AssemblyInformationalVersion("8.4.1")] diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 65055bd8e4..3bdf9b5618 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -347,9 +347,9 @@ False True - 8400 + 8410 / - http://localhost:8400/ + http://localhost:8410 False False @@ -431,4 +431,4 @@ - + \ No newline at end of file From cae2c3217296157d83a559fc1427ac99112a1520 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Tue, 7 Jan 2020 20:32:04 +0100 Subject: [PATCH 134/202] Remove SetFlag and UnsetFlag extension methods --- src/Umbraco.Core/EnumExtensions.cs | 38 ----------------- .../CoreThings/EnumExtensionsTests.cs | 42 ------------------- .../Mapping/ContentTypeMapDefinition.cs | 10 ++--- 3 files changed, 5 insertions(+), 85 deletions(-) diff --git a/src/Umbraco.Core/EnumExtensions.cs b/src/Umbraco.Core/EnumExtensions.cs index b2e5f32c9a..1443a27876 100644 --- a/src/Umbraco.Core/EnumExtensions.cs +++ b/src/Umbraco.Core/EnumExtensions.cs @@ -41,43 +41,5 @@ namespace Umbraco.Core return (num & nums) > 0; } - - /// - /// Sets a flag of the given input enum - /// - /// - /// Enum to set flag of - /// Flag to set - /// A new enum with the flag set - public static T SetFlag(this T input, T flag) - where T : Enum - { - var i = Convert.ToUInt64(input); - var f = Convert.ToUInt64(flag); - - // bitwise OR to set flag f of enum i - var result = i | f; - - return (T)Enum.ToObject(typeof(T), result); - } - - /// - /// Unsets a flag of the given input enum - /// - /// - /// Enum to unset flag of - /// Flag to unset - /// A new enum with the flag unset - public static T UnsetFlag(this T input, T flag) - where T : Enum - { - var i = Convert.ToUInt64(input); - var f = Convert.ToUInt64(flag); - - // bitwise AND combined with bitwise complement to unset flag f of enum i - var result = i & ~f; - - return (T)Enum.ToObject(typeof(T), result); - } } } diff --git a/src/Umbraco.Tests/CoreThings/EnumExtensionsTests.cs b/src/Umbraco.Tests/CoreThings/EnumExtensionsTests.cs index 4a0c1d0f41..72a55cee85 100644 --- a/src/Umbraco.Tests/CoreThings/EnumExtensionsTests.cs +++ b/src/Umbraco.Tests/CoreThings/EnumExtensionsTests.cs @@ -51,47 +51,5 @@ namespace Umbraco.Tests.CoreThings else Assert.IsFalse(value.HasFlagAny(test)); } - - [TestCase(TreeUse.None, TreeUse.None, TreeUse.None)] - [TestCase(TreeUse.None, TreeUse.Main, TreeUse.Main)] - [TestCase(TreeUse.None, TreeUse.Dialog, TreeUse.Dialog)] - [TestCase(TreeUse.None, TreeUse.Main | TreeUse.Dialog, TreeUse.Main | TreeUse.Dialog)] - [TestCase(TreeUse.Main, TreeUse.None, TreeUse.Main)] - [TestCase(TreeUse.Main, TreeUse.Main, TreeUse.Main)] - [TestCase(TreeUse.Main, TreeUse.Dialog, TreeUse.Main | TreeUse.Dialog)] - [TestCase(TreeUse.Main, TreeUse.Main | TreeUse.Dialog, TreeUse.Main | TreeUse.Dialog)] - [TestCase(TreeUse.Dialog, TreeUse.None, TreeUse.Dialog)] - [TestCase(TreeUse.Dialog, TreeUse.Main, TreeUse.Main | TreeUse.Dialog)] - [TestCase(TreeUse.Dialog, TreeUse.Dialog, TreeUse.Dialog)] - [TestCase(TreeUse.Dialog, TreeUse.Main | TreeUse.Dialog, TreeUse.Main | TreeUse.Dialog)] - [TestCase(TreeUse.Main | TreeUse.Dialog, TreeUse.None, TreeUse.Main | TreeUse.Dialog)] - [TestCase(TreeUse.Main | TreeUse.Dialog, TreeUse.Main, TreeUse.Main | TreeUse.Dialog)] - [TestCase(TreeUse.Main | TreeUse.Dialog, TreeUse.Dialog, TreeUse.Main | TreeUse.Dialog)] - [TestCase(TreeUse.Main | TreeUse.Dialog, TreeUse.Main | TreeUse.Dialog, TreeUse.Main | TreeUse.Dialog)] - public void SetFlagTests(TreeUse value, TreeUse flag, TreeUse expected) - { - Assert.AreEqual(expected, value.SetFlag(flag)); - } - - [TestCase(TreeUse.None, TreeUse.None, TreeUse.None)] - [TestCase(TreeUse.None, TreeUse.Main, TreeUse.None)] - [TestCase(TreeUse.None, TreeUse.Dialog, TreeUse.None)] - [TestCase(TreeUse.None, TreeUse.Main | TreeUse.Dialog, TreeUse.None)] - [TestCase(TreeUse.Main, TreeUse.None, TreeUse.Main)] - [TestCase(TreeUse.Main, TreeUse.Main, TreeUse.None)] - [TestCase(TreeUse.Main, TreeUse.Dialog, TreeUse.Main)] - [TestCase(TreeUse.Main, TreeUse.Main | TreeUse.Dialog, TreeUse.None)] - [TestCase(TreeUse.Dialog, TreeUse.None, TreeUse.Dialog)] - [TestCase(TreeUse.Dialog, TreeUse.Main, TreeUse.Dialog)] - [TestCase(TreeUse.Dialog, TreeUse.Dialog, TreeUse.None)] - [TestCase(TreeUse.Dialog, TreeUse.Main | TreeUse.Dialog, TreeUse.None)] - [TestCase(TreeUse.Main | TreeUse.Dialog, TreeUse.None, TreeUse.Main | TreeUse.Dialog)] - [TestCase(TreeUse.Main | TreeUse.Dialog, TreeUse.Main, TreeUse.Dialog)] - [TestCase(TreeUse.Main | TreeUse.Dialog, TreeUse.Dialog, TreeUse.Main)] - [TestCase(TreeUse.Main | TreeUse.Dialog, TreeUse.Main | TreeUse.Dialog, TreeUse.None)] - public void UnsetFlagTests(TreeUse value, TreeUse flag, TreeUse expected) - { - Assert.AreEqual(expected, value.UnsetFlag(flag)); - } } } diff --git a/src/Umbraco.Web/Models/Mapping/ContentTypeMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/ContentTypeMapDefinition.cs index 95101da4e3..cdd534a14f 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentTypeMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentTypeMapDefinition.cs @@ -226,8 +226,8 @@ namespace Umbraco.Web.Models.Mapping target.ValidationRegExp = source.Validation.Pattern; target.ValidationRegExpMessage = source.Validation.PatternMessage; target.Variations = source.AllowCultureVariant - ? target.Variations.SetFlag(ContentVariation.Culture) - : target.Variations.UnsetFlag(ContentVariation.Culture); + ? target.Variations | ContentVariation.Culture // Set flag using bitwise logical OR + : target.Variations & ~ContentVariation.Culture; // Remove flag using bitwise logical AND with bitwise complement (reversing the bit) if (source.Id > 0) target.Id = source.Id; @@ -399,9 +399,9 @@ namespace Umbraco.Web.Models.Mapping if (!(target is IMemberType)) { - target.Variations = source.AllowCultureVariant - ? target.Variations.SetFlag(ContentVariation.Culture) - : target.Variations.UnsetFlag(ContentVariation.Culture); + target.Variations = source.AllowCultureVariant + ? target.Variations | ContentVariation.Culture // Set flag using bitwise logical OR + : target.Variations & ~ContentVariation.Culture; // Remove flag using bitwise logical AND with bitwise complement (reversing the bit) } // handle property groups and property types From ec4bc11c1b1163790d0ef705ae0ef2e573371948 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Tue, 7 Jan 2020 20:35:00 +0100 Subject: [PATCH 135/202] Obsolete HasFlagAll extension method --- src/Umbraco.Core/EnumExtensions.cs | 45 +++++++++---------- .../CoreThings/EnumExtensionsTests.cs | 6 ++- 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/src/Umbraco.Core/EnumExtensions.cs b/src/Umbraco.Core/EnumExtensions.cs index 1443a27876..9097432f64 100644 --- a/src/Umbraco.Core/EnumExtensions.cs +++ b/src/Umbraco.Core/EnumExtensions.cs @@ -3,43 +3,42 @@ namespace Umbraco.Core { /// - /// Provides extension methods to enums. + /// Provides extension methods to . /// public static class EnumExtensions { - // note: - // - no need to HasFlagExact, that's basically an == test - // - HasFlagAll cannot be named HasFlag because ext. methods never take priority over instance methods - /// - /// Determines whether a flag enum has all the specified values. + /// Determines whether all the flags/bits are set within the enum value. /// - /// - /// True when all bits set in are set in , though other bits may be set too. - /// This is the behavior of the original method. - /// - public static bool HasFlagAll(this T use, T uses) + /// The enum type. + /// The enum value. + /// The flags. + /// + /// true if all the flags/bits are set within the enum value; otherwise, false. + /// + [Obsolete("Use Enum.HasFlag() or bitwise operations (if performance is important) instead.")] + public static bool HasFlagAll(this T value, T flags) where T : Enum { - var num = Convert.ToUInt64(use); - var nums = Convert.ToUInt64(uses); - - return (num & nums) == nums; + return value.HasFlag(flags); } /// - /// Determines whether a flag enum has any of the specified values. + /// Determines whether any of the flags/bits are set within the enum value. /// - /// - /// True when at least one of the bits set in is set in . - /// - public static bool HasFlagAny(this T use, T uses) + /// The enum type. + /// The value. + /// The flags. + /// + /// true if any of the flags/bits are set within the enum value; otherwise, false. + /// + public static bool HasFlagAny(this T value, T flags) where T : Enum { - var num = Convert.ToUInt64(use); - var nums = Convert.ToUInt64(uses); + var v = Convert.ToUInt64(value); + var f = Convert.ToUInt64(flags); - return (num & nums) > 0; + return (v & f) > 0; } } } diff --git a/src/Umbraco.Tests/CoreThings/EnumExtensionsTests.cs b/src/Umbraco.Tests/CoreThings/EnumExtensionsTests.cs index 72a55cee85..faa15b0077 100644 --- a/src/Umbraco.Tests/CoreThings/EnumExtensionsTests.cs +++ b/src/Umbraco.Tests/CoreThings/EnumExtensionsTests.cs @@ -1,6 +1,7 @@ -using NUnit.Framework; -using Umbraco.Web.Trees; +using System; +using NUnit.Framework; using Umbraco.Core; +using Umbraco.Web.Trees; namespace Umbraco.Tests.CoreThings { @@ -22,6 +23,7 @@ namespace Umbraco.Tests.CoreThings Assert.IsFalse(value.HasFlag(test)); } + [Obsolete] [TestCase(TreeUse.Dialog, TreeUse.Dialog, true)] [TestCase(TreeUse.Dialog, TreeUse.Main, false)] [TestCase(TreeUse.Dialog | TreeUse.Main, TreeUse.Dialog, true)] From c38240c872428b366cd716c34c8ce468ced91271 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Thu, 9 Jan 2020 09:35:37 +0100 Subject: [PATCH 136/202] Add SetVariesBy extension methods for ContentVariation --- .../ContentVariationExtensions.cs | 32 +++++++++++++++++++ .../Mapping/ContentTypeMapDefinition.cs | 12 +++---- 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Core/ContentVariationExtensions.cs b/src/Umbraco.Core/ContentVariationExtensions.cs index 5b157307ab..0b6b3b5c12 100644 --- a/src/Umbraco.Core/ContentVariationExtensions.cs +++ b/src/Umbraco.Core/ContentVariationExtensions.cs @@ -195,5 +195,37 @@ namespace Umbraco.Core return true; } + + /// + /// Sets or removes the content type variation depending on the specified value. + /// + /// The content type. + /// The variation to set or remove. + /// If set to true sets the variation; otherwise, removes the variation. + public static void SetVariesBy(this IContentTypeBase contentType, ContentVariation variation, bool value = true) => contentType.Variations = contentType.Variations.SetVariesBy(variation, value); + + /// + /// Sets or removes the property type variation depending on the specified value. + /// + /// The property type. + /// The variation to set or remove. + /// If set to true sets the variation; otherwise, removes the variation. + public static void SetVariesBy(this PropertyType propertyType, ContentVariation variation, bool value = true) => propertyType.Variations = propertyType.Variations.SetVariesBy(variation, value); + + /// + /// Sets or removes the variation depending on the specified value. + /// + /// The existing variations. + /// The variation to set or remove. + /// If set to true sets the variation; otherwise, removes the variation. + /// + /// The variations with the specified variation set or removed. + /// + public static ContentVariation SetVariesBy(this ContentVariation variations, ContentVariation variation, bool value = true) + { + return value + ? variations | variation // Set flag using bitwise logical OR + : variations & ~variation; // Remove flag using bitwise logical AND with bitwise complement (reversing the bit) + } } } diff --git a/src/Umbraco.Web/Models/Mapping/ContentTypeMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/ContentTypeMapDefinition.cs index cdd534a14f..06155d6888 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentTypeMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentTypeMapDefinition.cs @@ -215,7 +215,7 @@ namespace Umbraco.Web.Models.Mapping } // Umbraco.Code.MapAll -CreateDate -DeleteDate -UpdateDate - // Umbraco.Code.MapAll -SupportsPublishing -Key -PropertyEditorAlias -ValueStorageType + // Umbraco.Code.MapAll -SupportsPublishing -Key -PropertyEditorAlias -ValueStorageType -Variations private static void Map(PropertyTypeBasic source, PropertyType target, MapperContext context) { target.Name = source.Label; @@ -225,9 +225,7 @@ namespace Umbraco.Web.Models.Mapping target.MandatoryMessage = source.Validation.MandatoryMessage; target.ValidationRegExp = source.Validation.Pattern; target.ValidationRegExpMessage = source.Validation.PatternMessage; - target.Variations = source.AllowCultureVariant - ? target.Variations | ContentVariation.Culture // Set flag using bitwise logical OR - : target.Variations & ~ContentVariation.Culture; // Remove flag using bitwise logical AND with bitwise complement (reversing the bit) + target.SetVariesBy(ContentVariation.Culture, source.AllowCultureVariant); if (source.Id > 0) target.Id = source.Id; @@ -369,7 +367,7 @@ namespace Umbraco.Web.Models.Mapping target.Validation = source.Validation; } - // Umbraco.Code.MapAll -CreatorId -Level -SortOrder + // Umbraco.Code.MapAll -CreatorId -Level -SortOrder -Variations // Umbraco.Code.MapAll -CreateDate -UpdateDate -DeleteDate // Umbraco.Code.MapAll -ContentTypeComposition (done by AfterMapSaveToType) private static void MapSaveToTypeBase(TSource source, IContentTypeComposition target, MapperContext context) @@ -399,9 +397,7 @@ namespace Umbraco.Web.Models.Mapping if (!(target is IMemberType)) { - target.Variations = source.AllowCultureVariant - ? target.Variations | ContentVariation.Culture // Set flag using bitwise logical OR - : target.Variations & ~ContentVariation.Culture; // Remove flag using bitwise logical AND with bitwise complement (reversing the bit) + target.SetVariesBy(ContentVariation.Culture, source.AllowCultureVariant); } // handle property groups and property types From babe1aa60aa5518967b7be7cc60a00e1517eb9aa Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Thu, 9 Jan 2020 09:50:33 +0100 Subject: [PATCH 137/202] Add documentation and clean code --- .../ContentVariationExtensions.cs | 115 +++++++++++++++--- 1 file changed, 100 insertions(+), 15 deletions(-) diff --git a/src/Umbraco.Core/ContentVariationExtensions.cs b/src/Umbraco.Core/ContentVariationExtensions.cs index 0b6b3b5c12..b3688b08b4 100644 --- a/src/Umbraco.Core/ContentVariationExtensions.cs +++ b/src/Umbraco.Core/ContentVariationExtensions.cs @@ -12,119 +12,199 @@ namespace Umbraco.Core /// /// Determines whether the content type is invariant. /// + /// The content type. + /// + /// A value indicating whether the content type is invariant. + /// public static bool VariesByNothing(this ISimpleContentType contentType) => contentType.Variations.VariesByNothing(); /// /// Determines whether the content type varies by culture. /// + /// The content type. + /// + /// A value indicating whether the content type varies by culture. + /// public static bool VariesByCulture(this ISimpleContentType contentType) => contentType.Variations.VariesByCulture(); /// /// Determines whether the content type is invariant. /// + /// The content type. + /// + /// A value indicating whether the content type is invariant. + /// public static bool VariesByNothing(this IContentTypeBase contentType) => contentType.Variations.VariesByNothing(); /// /// Determines whether the content type varies by culture. /// - /// And then it could also vary by segment. + /// The content type. + /// + /// A value indicating whether the content type varies by culture. + /// public static bool VariesByCulture(this IContentTypeBase contentType) => contentType.Variations.VariesByCulture(); /// /// Determines whether the content type varies by segment. /// - /// And then it could also vary by culture. + /// The content type. + /// + /// A value indicating whether the content type varies by segment. + /// public static bool VariesBySegment(this IContentTypeBase contentType) => contentType.Variations.VariesBySegment(); /// /// Determines whether the content type varies by culture and segment. /// + /// The content type. + /// + /// A value indicating whether the content type varies by culture and segment. + /// public static bool VariesByCultureAndSegment(this IContentTypeBase contentType) => contentType.Variations.VariesByCultureAndSegment(); /// /// Determines whether the property type is invariant. /// + /// The property type. + /// + /// A value indicating whether the property type is invariant. + /// public static bool VariesByNothing(this PropertyType propertyType) => propertyType.Variations.VariesByNothing(); /// /// Determines whether the property type varies by culture. /// - /// And then it could also vary by segment. + /// The property type. + /// + /// A value indicating whether the property type varies by culture. + /// public static bool VariesByCulture(this PropertyType propertyType) => propertyType.Variations.VariesByCulture(); /// /// Determines whether the property type varies by segment. /// - /// And then it could also vary by culture. + /// The property type. + /// + /// A value indicating whether the property type varies by segment. + /// public static bool VariesBySegment(this PropertyType propertyType) => propertyType.Variations.VariesBySegment(); /// /// Determines whether the property type varies by culture and segment. /// + /// The property type. + /// + /// A value indicating whether the property type varies by culture and segment. + /// public static bool VariesByCultureAndSegment(this PropertyType propertyType) => propertyType.Variations.VariesByCultureAndSegment(); /// /// Determines whether the content type is invariant. /// + /// The content type. + /// + /// A value indicating whether the content type is invariant. + /// public static bool VariesByNothing(this IPublishedContentType contentType) => contentType.Variations.VariesByNothing(); /// /// Determines whether the content type varies by culture. /// - /// And then it could also vary by segment. + /// The content type. + /// + /// A value indicating whether the content type varies by culture. + /// public static bool VariesByCulture(this IPublishedContentType contentType) => contentType.Variations.VariesByCulture(); /// /// Determines whether the content type varies by segment. /// - /// And then it could also vary by culture. + /// The content type. + /// + /// A value indicating whether the content type varies by segment. + /// public static bool VariesBySegment(this IPublishedContentType contentType) => contentType.Variations.VariesBySegment(); /// /// Determines whether the content type varies by culture and segment. /// + /// The content type. + /// + /// A value indicating whether the content type varies by culture and segment. + /// public static bool VariesByCultureAndSegment(this IPublishedContentType contentType) => contentType.Variations.VariesByCultureAndSegment(); /// /// Determines whether the property type is invariant. /// + /// The property type. + /// + /// A value indicating whether the property type is invariant. + /// public static bool VariesByNothing(this IPublishedPropertyType propertyType) => propertyType.Variations.VariesByNothing(); /// /// Determines whether the property type varies by culture. /// + /// The property type. + /// + /// A value indicating whether the property type varies by culture. + /// public static bool VariesByCulture(this IPublishedPropertyType propertyType) => propertyType.Variations.VariesByCulture(); /// /// Determines whether the property type varies by segment. /// + /// The property type. + /// + /// A value indicating whether the property type varies by segment. + /// public static bool VariesBySegment(this IPublishedPropertyType propertyType) => propertyType.Variations.VariesBySegment(); /// /// Determines whether the property type varies by culture and segment. /// + /// The property type. + /// + /// A value indicating whether the property type varies by culture and segment. + /// public static bool VariesByCultureAndSegment(this IPublishedPropertyType propertyType) => propertyType.Variations.VariesByCultureAndSegment(); /// /// Determines whether a variation is invariant. /// + /// The variation. + /// + /// A value indicating whether the variation is invariant. + /// public static bool VariesByNothing(this ContentVariation variation) => variation == ContentVariation.Nothing; /// /// Determines whether a variation varies by culture. /// - /// And then it could also vary by segment. + /// The variation. + /// + /// A value indicating whether the variation varies by culture. + /// public static bool VariesByCulture(this ContentVariation variation) => (variation & ContentVariation.Culture) > 0; /// /// Determines whether a variation varies by segment. /// - /// And then it could also vary by culture. + /// The variation. + /// + /// A value indicating whether the variation varies by segment. + /// public static bool VariesBySegment(this ContentVariation variation) => (variation & ContentVariation.Segment) > 0; /// /// Determines whether a variation varies by culture and segment. /// + /// The variation. + /// + /// A value indicating whether the variation varies by culture and segment. + /// public static bool VariesByCultureAndSegment(this ContentVariation variation) => (variation & ContentVariation.CultureAndSegment) == ContentVariation.CultureAndSegment; /// @@ -135,16 +215,18 @@ namespace Umbraco.Core /// The segment. /// A value indicating whether to perform exact validation. /// A value indicating whether to support wildcards. - /// A value indicating whether to throw a when the combination is invalid. - /// True if the combination is valid; otherwise false. + /// A value indicating whether to throw a when the combination is invalid. + /// + /// true if the combination is valid; otherwise false. + /// + /// Occurs when the combination is invalid, and is true. /// /// When validation is exact, the combination must match the variation exactly. For instance, if the variation is Culture, then /// a culture is required. When validation is not strict, the combination must be equivalent, or more restrictive: if the variation is /// Culture, an invariant combination is ok. /// Basically, exact is for one content type, or one property type, and !exact is for "all property types" of one content type. - /// Both and can be "*" to indicate "all of them". + /// Both and can be "*" to indicate "all of them". /// - /// Occurs when the combination is invalid, and is true. public static bool ValidateVariation(this ContentVariation variation, string culture, string segment, bool exact, bool wildcards, bool throwIfInvalid) { culture = culture.NullOrWhiteSpaceAsNull(); @@ -161,13 +243,14 @@ namespace Umbraco.Core if (variation.VariesByCulture()) { // varies by culture - // in exact mode, the culture cannot be null + // in exact mode, the culture cannot be null if (exact && culture == null) { if (throwIfInvalid) throw new NotSupportedException($"Culture may not be null because culture variation is enabled."); + return false; - } + } } else { @@ -178,9 +261,10 @@ namespace Umbraco.Core { if (throwIfInvalid) throw new NotSupportedException($"Culture \"{culture}\" is invalid because culture variation is disabled."); + return false; } - } + } // if it does not vary by segment // the segment cannot have a value @@ -190,6 +274,7 @@ namespace Umbraco.Core { if (throwIfInvalid) throw new NotSupportedException($"Segment \"{segment}\" is invalid because segment variation is disabled."); + return false; } From a10c4403abda8ca1817817061c2042b8373a1284 Mon Sep 17 00:00:00 2001 From: Bjarne Fyrstenborg Date: Thu, 9 Jan 2020 14:26:18 +0100 Subject: [PATCH 138/202] Remove konamiCode directive since it isn't used anywhere in core (#7437) --- .../directives/util/konami.directive.js | 62 ------------------- 1 file changed, 62 deletions(-) delete mode 100644 src/Umbraco.Web.UI.Client/src/common/directives/util/konami.directive.js diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/util/konami.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/util/konami.directive.js deleted file mode 100644 index 7914dfc3f0..0000000000 --- a/src/Umbraco.Web.UI.Client/src/common/directives/util/konami.directive.js +++ /dev/null @@ -1,62 +0,0 @@ -/** - * Konami Code directive for AngularJS - * @version v0.0.1 - * @license MIT License, https://www.opensource.org/licenses/MIT - */ - -angular.module('umbraco.directives') - .directive('konamiCode', ['$document', function ($document) { - var konamiKeysDefault = [38, 38, 40, 40, 37, 39, 37, 39, 66, 65]; - - return { - restrict: 'A', - link: function (scope, element, attr) { - - if (!attr.konamiCode) { - throw ('Konami directive must receive an expression as value.'); - } - - // Let user define a custom code. - var konamiKeys = attr.konamiKeys || konamiKeysDefault; - var keyIndex = 0; - - /** - * Fired when konami code is type. - */ - function activated() { - if ('konamiOnce' in attr) { - stopListening(); - } - // Execute expression. - scope.$eval(attr.konamiCode); - } - - /** - * Handle keydown events. - */ - function keydown(e) { - if (e.keyCode === konamiKeys[keyIndex++]) { - if (keyIndex === konamiKeys.length) { - keyIndex = 0; - activated(); - } - } else { - keyIndex = 0; - } - } - - /** - * Stop to listen typing. - */ - function stopListening() { - $document.off('keydown', keydown); - } - - // Start listening to key typing. - $document.on('keydown', keydown); - - // Stop listening when scope is destroyed. - scope.$on('$destroy', stopListening); - } - }; - }]); From 16faeb1886276b0843087230d3693f721db62013 Mon Sep 17 00:00:00 2001 From: Steve Megson Date: Fri, 10 Jan 2020 08:34:23 +0000 Subject: [PATCH 139/202] V8: Date pickers - parsing non-standard formats (#6001) --- .../datepicker/datepicker.controller.js | 32 +++++++++++++------ 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.controller.js index 0f19bf3b1a..24affc6ac1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.controller.js @@ -85,13 +85,27 @@ function dateTimePickerController($scope, angularHelper, dateHelper, validationM }; $scope.datePickerChange = function(date) { - setDate(date); + const momentDate = moment(date); + setDate(momentDate); setDatePickerVal(); }; - $scope.inputChanged = function() { - setDate($scope.model.datetimePickerValue); - setDatePickerVal(); + $scope.inputChanged = function () { + if ($scope.model.datetimePickerValue === "" && $scope.hasDatetimePickerValue) { + // $scope.hasDatetimePickerValue indicates that we had a value before the input was changed, + // but now the input is empty. + $scope.clearDate(); + } else if ($scope.model.datetimePickerValue) { + var momentDate = moment($scope.model.datetimePickerValue, $scope.model.config.format, true); + if (!momentDate || !momentDate.isValid()) { + momentDate = moment(new Date($scope.model.datetimePickerValue)); + } + if (momentDate && momentDate.isValid()) { + setDate(momentDate); + } + setDatePickerVal(); + flatPickr.setDate($scope.model.value, false); + } } //here we declare a special method which will be called whenever the value has changed from the server @@ -103,15 +117,14 @@ function dateTimePickerController($scope, angularHelper, dateHelper, validationM var newDate = moment(newVal); if (newDate.isAfter(minDate)) { - setDate(newVal); + setDate(newDate); } else { $scope.clearDate(); } } }; - function setDate(date) { - const momentDate = moment(date); + function setDate(momentDate) { angularHelper.safeApply($scope, function() { // when a date is changed, update the model if (momentDate && momentDate.isValid()) { @@ -123,12 +136,11 @@ function dateTimePickerController($scope, angularHelper, dateHelper, validationM $scope.hasDatetimePickerValue = false; $scope.model.datetimePickerValue = null; } - updateModelValue(date); + updateModelValue(momentDate); }); } - function updateModelValue(date) { - const momentDate = moment(date); + function updateModelValue(momentDate) { if ($scope.hasDatetimePickerValue) { if ($scope.model.config.pickTime) { //check if we are supposed to offset the time From 2b4279315a1d9619455509a29d83aecef56f634c Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Fri, 10 Jan 2020 10:05:52 +0100 Subject: [PATCH 140/202] V8: Make the markdown editor use the Umbraco link picker (#5745) --- .../lib/markdown/markdown.editor.js | 10 ++++++---- .../markdowneditor.controller.js | 20 +++++++++++++++++++ 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/lib/markdown/markdown.editor.js b/src/Umbraco.Web.UI.Client/lib/markdown/markdown.editor.js index a75a7f1f3c..60118dbdb3 100644 --- a/src/Umbraco.Web.UI.Client/lib/markdown/markdown.editor.js +++ b/src/Umbraco.Web.UI.Client/lib/markdown/markdown.editor.js @@ -60,6 +60,7 @@ * its own image insertion dialog, this hook should return true, and the callback should be called with the chosen * image url (or null if the user cancelled). If this hook returns false, the default dialog will be used. */ + hooks.addFalse("insertLinkDialog"); this.getConverter = function () { return markdownConverter; } @@ -1636,7 +1637,7 @@ var that = this; // The function to be executed when you enter a link and press OK or Cancel. // Marks up the link and adds the ref. - var linkEnteredCallback = function (link) { + var linkEnteredCallback = function (link, title) { if (link !== null) { // ( $1 @@ -1667,10 +1668,10 @@ if (!chunk.selection) { if (isImage) { - chunk.selection = "enter image description here"; + chunk.selection = title || "enter image description here"; } else { - chunk.selection = "enter link description here"; + chunk.selection = title || "enter link description here"; } } } @@ -1683,7 +1684,8 @@ ui.prompt('Insert Image', imageDialogText, imageDefaultText, linkEnteredCallback); } else { - ui.prompt('Insert Link', linkDialogText, linkDefaultText, linkEnteredCallback); + if (!this.hooks.insertLinkDialog(linkEnteredCallback)) + ui.prompt('Insert Link', linkDialogText, linkDefaultText, linkEnteredCallback); } return true; } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/markdowneditor/markdowneditor.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/markdowneditor/markdowneditor.controller.js index f05b1e31d8..bb87f0463d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/markdowneditor/markdowneditor.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/markdowneditor/markdowneditor.controller.js @@ -27,6 +27,20 @@ function MarkdownEditorController($scope, $element, assetsService, editorService editorService.mediaPicker(mediaPicker); } + function openLinkPicker(callback) { + var linkPicker = { + hideTarget: true, + submit: function(model) { + callback(model.target.url, model.target.name); + editorService.close(); + }, + close: function() { + editorService.close(); + } + }; + editorService.linkPicker(linkPicker); + } + assetsService .load([ "lib/markdown/markdown.converter.js", @@ -53,6 +67,12 @@ function MarkdownEditorController($scope, $element, assetsService, editorService return true; // tell the editor that we'll take care of getting the image url }); + //subscribe to the link dialog clicks + editor2.hooks.set("insertLinkDialog", function (callback) { + openLinkPicker(callback); + return true; // tell the editor that we'll take care of getting the link url + }); + editor2.hooks.set("onPreviewRefresh", function () { // We must manually update the model as there is no way to hook into the markdown editor events without exstensive edits to the library. if ($scope.model.value !== $("textarea", $element).val()) { From eeaa5a82d46ebbae5616aafa747a452d36bd05c8 Mon Sep 17 00:00:00 2001 From: Dave Woestenborghs Date: Fri, 10 Jan 2020 10:39:22 +0100 Subject: [PATCH 141/202] V8/doctype tours (#6980) --- .../src/common/resources/tour.resource.js | 13 +++- .../common/services/editorstate.service.js | 3 +- .../src/common/services/tour.service.js | 65 ++++++++++------ .../common/drawers/help/help.controller.js | 35 ++++++++- .../src/views/common/drawers/help/help.html | 74 ++++++++++++------- src/Umbraco.Web/Editors/TourController.cs | 33 +++++++++ src/Umbraco.Web/Models/BackOfficeTour.cs | 3 + 7 files changed, 174 insertions(+), 52 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/tour.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/tour.resource.js index 40baf0f389..485b0d299a 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/tour.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/tour.resource.js @@ -20,10 +20,21 @@ "GetTours")), 'Failed to get tours'); } + + function getToursForDoctype(doctypeAlias) { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "tourApiBaseUrl", + "GetToursForDoctype", + [{ doctypeAlias: doctypeAlias }])), + 'Failed to get tours'); + } var resource = { - getTours: getTours + getTours: getTours, + getToursForDoctype: getToursForDoctype }; return resource; diff --git a/src/Umbraco.Web.UI.Client/src/common/services/editorstate.service.js b/src/Umbraco.Web.UI.Client/src/common/services/editorstate.service.js index 97a9ac5c4b..28daa3f245 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/editorstate.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/editorstate.service.js @@ -10,7 +10,7 @@ * * it is possible to modify this object, so should be used with care */ -angular.module('umbraco.services').factory("editorState", function ($rootScope) { +angular.module('umbraco.services').factory("editorState", function ($rootScope, eventsService) { var current = null; @@ -30,6 +30,7 @@ angular.module('umbraco.services').factory("editorState", function ($rootScope) */ set: function (entity) { current = entity; + eventsService.emit("editorState.changed", { entity: entity }); }, /** diff --git a/src/Umbraco.Web.UI.Client/src/common/services/tour.service.js b/src/Umbraco.Web.UI.Client/src/common/services/tour.service.js index e102da5d34..7219395fd6 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/tour.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/tour.service.js @@ -134,31 +134,33 @@ var groupedTours = []; tours.forEach(function (item) { - var groupExists = false; - var newGroup = { - "group": "", - "tours": [] - }; + if (item.contentType === null || item.contentType === '') { + var groupExists = false; + var newGroup = { + "group": "", + "tours": [] + }; - groupedTours.forEach(function(group){ - // extend existing group if it is already added - if(group.group === item.group) { - if(item.groupOrder) { - group.groupOrder = item.groupOrder + groupedTours.forEach(function (group) { + // extend existing group if it is already added + if (group.group === item.group) { + if (item.groupOrder) { + group.groupOrder = item.groupOrder; + } + groupExists = true; + group.tours.push(item); } - groupExists = true; - group.tours.push(item) - } - }); + }); - // push new group to array if it doesn't exist - if(!groupExists) { - newGroup.group = item.group; - if(item.groupOrder) { - newGroup.groupOrder = item.groupOrder + // push new group to array if it doesn't exist + if (!groupExists) { + newGroup.group = item.group; + if (item.groupOrder) { + newGroup.groupOrder = item.groupOrder; + } + newGroup.tours.push(item); + groupedTours.push(newGroup); } - newGroup.tours.push(item); - groupedTours.push(newGroup); } }); @@ -188,6 +190,24 @@ return deferred.promise; } + /** + * @ngdoc method + * @name umbraco.services.tourService#getToursForDoctype + * @methodOf umbraco.services.tourService + * + * @description + * Returns a promise of the tours found by documenttype alias. + * @param {Object} doctypeAlias The doctype alias for which the tours which should be returned + * @returns {Array} An array of tour objects for the doctype + */ + function getToursForDoctype(doctypeAlias) { + var deferred = $q.defer(); + tourResource.getToursForDoctype(doctypeAlias).then(function (tours) { + deferred.resolve(tours); + }); + return deferred.promise; + } + /////////// /** @@ -269,7 +289,8 @@ completeTour: completeTour, getCurrentTour: getCurrentTour, getGroupedTours: getGroupedTours, - getTourByAlias: getTourByAlias + getTourByAlias: getTourByAlias, + getToursForDoctype : getToursForDoctype }; return service; diff --git a/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.controller.js index 3323f2bfb3..268bfb3a8c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.controller.js @@ -1,7 +1,7 @@ (function () { "use strict"; - function HelpDrawerController($scope, $routeParams, $timeout, dashboardResource, localizationService, userService, eventsService, helpService, appState, tourService, $filter) { + function HelpDrawerController($scope, $routeParams, $timeout, dashboardResource, localizationService, userService, eventsService, helpService, appState, tourService, $filter, editorState) { var vm = this; var evts = []; @@ -18,6 +18,10 @@ vm.startTour = startTour; vm.getTourGroupCompletedPercentage = getTourGroupCompletedPercentage; vm.showTourButton = showTourButton; + + vm.showDocTypeTour = false; + vm.docTypeTours = []; + vm.nodeName = ''; function startTour(tour) { tourService.startTour(tour); @@ -58,9 +62,16 @@ handleSectionChange(); })); + evts.push(eventsService.on("editorState.changed", + function (e, args) { + setDocTypeTour(args.entity); + })); + findHelp(vm.section, vm.tree, vm.userType, vm.userLang); }); + + setDocTypeTour(editorState.getCurrent()); // check if a tour is running - if it is open the matching group var currentTour = tourService.getCurrentTour(); @@ -84,7 +95,7 @@ setSectionName(); findHelp(vm.section, vm.tree, vm.userType, vm.userLang); - + setDocTypeTour(); } }); } @@ -168,6 +179,26 @@ }); } + function setDocTypeTour(node) { + vm.showDocTypeTour = false; + vm.docTypeTours = []; + vm.nodeName = ''; + + if (vm.section === 'content' && vm.tree === 'content') { + + if (node) { + tourService.getToursForDoctype(node.contentTypeAlias).then(function (data) { + if (data && data.length > 0) { + vm.docTypeTours = data; + var currentVariant = _.find(node.variants, (x) => x.active); + vm.nodeName = currentVariant.name; + vm.showDocTypeTour = true; + } + }); + } + } + } + evts.push(eventsService.on("appState.tour.complete", function (event, tour) { tourService.getGroupedTours().then(function(groupedTours) { vm.tours = groupedTours; diff --git a/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.html b/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.html index 96f4a404bc..822b5bdb19 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.html @@ -8,12 +8,34 @@ - -
+ +
+
Need help editing current item '{{vm.nodeName}}' ?
-
Tours
+
-
+ +
+
+
+
+ {{ tour.name }} +
+
+ +
+
+
+
+
+
+ + +
+ +
Tours
+ +
@@ -51,33 +73,33 @@
-
+
- -
-
-
{{dashboard.label}}
-
-
-
+ +
+
+
{{dashboard.label}}
+
+
+
+
-
- -
-
Articles
-
    -
  • - - - - {{topic.name}} - - - {{topic.description}} - + +
    +
    Articles
    +
    diff --git a/src/Umbraco.Web/Editors/TourController.cs b/src/Umbraco.Web/Editors/TourController.cs index 25b4f7e9fc..8991bcdd6a 100644 --- a/src/Umbraco.Web/Editors/TourController.cs +++ b/src/Umbraco.Web/Editors/TourController.cs @@ -110,6 +110,39 @@ namespace Umbraco.Web.Editors return result.Except(toursToBeRemoved).OrderBy(x => x.FileName, StringComparer.InvariantCultureIgnoreCase); } + /// + /// Gets a tours for a specific doctype + /// + /// The documenttype alias + /// A + public IEnumerable GetToursForDoctype(string doctypeAlias) + { + var tourFiles = this.GetTours(); + + var doctypeAliasWithCompositions = new List + { + doctypeAlias + }; + + var contentType = this.Services.ContentTypeService.Get(doctypeAlias); + + if (contentType != null) + { + doctypeAliasWithCompositions.AddRange(contentType.CompositionAliases()); + } + + return tourFiles.SelectMany(x => x.Tours) + .Where(x => + { + if (string.IsNullOrEmpty(x.ContentType)) + { + return false; + } + var contentTypes = x.ContentType.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(ct => ct.Trim()); + return contentTypes.Intersect(doctypeAliasWithCompositions).Any(); + }); + } + private void TryParseTourFile(string tourFile, ICollection result, List filters, diff --git a/src/Umbraco.Web/Models/BackOfficeTour.cs b/src/Umbraco.Web/Models/BackOfficeTour.cs index d5987ec5bc..3e14f278e2 100644 --- a/src/Umbraco.Web/Models/BackOfficeTour.cs +++ b/src/Umbraco.Web/Models/BackOfficeTour.cs @@ -31,5 +31,8 @@ namespace Umbraco.Web.Models [DataMember(Name = "culture")] public string Culture { get; set; } + + [DataMember(Name = "contentType")] + public string ContentType { get; set; } } } From f92d0b59bd64b0b751e2e329499a2785cf613637 Mon Sep 17 00:00:00 2001 From: Dave Woestenborghs Date: Fri, 10 Jan 2020 10:41:37 +0100 Subject: [PATCH 142/202] Make sure the configured url provider mode is used when getting a url (#7189) --- src/Umbraco.Web/PublishedContentExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web/PublishedContentExtensions.cs b/src/Umbraco.Web/PublishedContentExtensions.cs index 75eb6adbcb..061422859c 100644 --- a/src/Umbraco.Web/PublishedContentExtensions.cs +++ b/src/Umbraco.Web/PublishedContentExtensions.cs @@ -1193,7 +1193,7 @@ namespace Umbraco.Web /// if any. In addition, when the content type is multi-lingual, this is the url for the /// specified culture. Otherwise, it is the invariant url. /// - public static string Url(this IPublishedContent content, string culture = null, UrlMode mode = UrlMode.Auto) + public static string Url(this IPublishedContent content, string culture = null, UrlMode mode = UrlMode.Default) { var umbracoContext = Composing.Current.UmbracoContext; From 4a44cd3a5823454f3e0c9cb5b44267023a8c98df Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Fri, 10 Jan 2020 20:02:39 +0100 Subject: [PATCH 143/202] =?UTF-8?q?V8:=20If=20Nested=20Content=20has=20mul?= =?UTF-8?q?tiple=20item=20types,=20always=20let=20the=20e=E2=80=A6=20(#542?= =?UTF-8?q?9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lovely work Kenn. Thanks, as ever --- .../nestedcontent/nestedcontent.controller.js | 50 ++++++++++++------- .../nestedcontent.propertyeditor.html | 6 +-- 2 files changed, 36 insertions(+), 20 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js index b61f8b94c2..7de3a5b567 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js @@ -41,7 +41,7 @@ if (vm.maxItems === 0) vm.maxItems = 1000; - vm.singleMode = vm.minItems === 1 && vm.maxItems === 1; + vm.singleMode = vm.minItems === 1 && vm.maxItems === 1 && model.config.contentTypes.length === 1;; vm.showIcons = Object.toBoolean(model.config.showIcons); vm.wideMode = Object.toBoolean(model.config.hideLabel); vm.hasContentTypes = model.config.contentTypes.length > 0; @@ -131,6 +131,7 @@ setCurrentNode(newNode); setDirty(); + validate(); }; vm.openNodeTypePicker = function ($event) { @@ -234,12 +235,22 @@ } }; + vm.canDeleteNode = function (idx) { + return (vm.nodes.length > vm.minItems) + ? true + : model.config.contentTypes.length > 1; + } + function deleteNode(idx) { vm.nodes.splice(idx, 1); setDirty(); updateModel(); + validate(); }; vm.requestDeleteNode = function (idx) { + if (!vm.canDeleteNode(idx)) { + return; + } if (model.config.confirmDeletes === true) { localizationService.localizeMany(["content_nestedContentDeleteItem", "general_delete", "general_cancel", "contentTypeEditor_yesDelete"]).then(function (data) { const overlay = { @@ -468,8 +479,8 @@ } } - // Auto-fill with elementTypes, but only if we have one type to choose from, and if this property is empty. - if (vm.singleMode === true && vm.nodes.length === 0 && model.config.minItems > 0) { + // Enforce min items if we only have one scaffold type + if (vm.nodes.length < vm.minItems && vm.scaffolds.length === 1) { for (var i = vm.nodes.length; i < model.config.minItems; i++) { addNode(vm.scaffolds[0].contentTypeAlias); } @@ -480,6 +491,8 @@ setCurrentNode(vm.nodes[0]); } + validate(); + vm.inited = true; updatePropertyActionStates(); @@ -585,25 +598,28 @@ updateModel(); }); + var validate = function () { + if (vm.nodes.length < vm.minItems) { + $scope.nestedContentForm.minCount.$setValidity("minCount", false); + } + else { + $scope.nestedContentForm.minCount.$setValidity("minCount", true); + } + + if (vm.nodes.length > vm.maxItems) { + $scope.nestedContentForm.maxCount.$setValidity("maxCount", false); + } + else { + $scope.nestedContentForm.maxCount.$setValidity("maxCount", true); + } + } + var watcher = $scope.$watch( function () { return vm.nodes.length; }, function () { - //Validate! - if (vm.nodes.length < vm.minItems) { - $scope.nestedContentForm.minCount.$setValidity("minCount", false); - } - else { - $scope.nestedContentForm.minCount.$setValidity("minCount", true); - } - - if (vm.nodes.length > vm.maxItems) { - $scope.nestedContentForm.maxCount.$setValidity("maxCount", false); - } - else { - $scope.nestedContentForm.maxCount.$setValidity("maxCount", true); - } + validate(); } ); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.propertyeditor.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.propertyeditor.html index b3821cff3d..47293e433b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.propertyeditor.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.propertyeditor.html @@ -2,7 +2,7 @@ - +
    @@ -17,7 +17,7 @@ {{vm.labels.copy_icon_title}} -
    From 2b1e33095769e1dc112308efb586afbc38216849 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Fri, 10 Jan 2020 20:09:51 +0100 Subject: [PATCH 144/202] =?UTF-8?q?V8:=20Reset=20paging=20in=20user=20sear?= =?UTF-8?q?ch=20when=20free=20text=20searching=20and=20fi=E2=80=A6=20(#704?= =?UTF-8?q?9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thanks Kenn :) --- .../src/views/users/views/users/users.controller.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js index 4ee31806a3..935b1bdfb1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js @@ -451,7 +451,7 @@ var search = _.debounce(function () { $scope.$apply(function () { - getUsers(); + changePageNumber(1); }); }, 500); @@ -512,7 +512,7 @@ } updateLocation("userStates", vm.usersOptions.userStates.join(",")); - getUsers(); + changePageNumber(1); } function setUserGroupFilter(userGroup) { @@ -529,7 +529,7 @@ } updateLocation("userGroups", vm.usersOptions.userGroups.join(",")); - getUsers(); + changePageNumber(1); } function setOrderByFilter(value, direction) { From 2906eafa791fc2c3a2e8a270e2a850a7c25f3b01 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Fri, 10 Jan 2020 20:14:11 +0100 Subject: [PATCH 145/202] =?UTF-8?q?V8:=20Make=20it=20possible=20to=20hide?= =?UTF-8?q?=20anchor/querystring=20input=20in=20the=20li=E2=80=A6=20(#7031?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wonderful work --- .../infiniteeditors/linkpicker/linkpicker.controller.js | 1 + .../views/common/infiniteeditors/linkpicker/linkpicker.html | 4 ++-- .../multiurlpicker/multiurlpicker.controller.js | 1 + .../PropertyEditors/MultiUrlPickerConfiguration.cs | 5 ++++- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.controller.js index b5043293e5..47607b7f0b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.controller.js @@ -33,6 +33,7 @@ angular.module("umbraco").controller("Umbraco.Editors.LinkPickerController", }; $scope.showTarget = $scope.model.hideTarget !== true; + $scope.showAnchor = $scope.model.hideAnchor !== true; // this ensures that we only sync the tree once and only when it's ready var oneTimeTreeSync = { diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.html index a7d2dbbee2..ad0aaab57c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.html @@ -14,7 +14,7 @@ -
    +
    - + Date: Mon, 13 Jan 2020 17:16:55 +1100 Subject: [PATCH 146/202] Fixes the issue of manipulating data for an existing Gen, adds notes, makes the Gen comparison operator consistent. --- .../PublishedCache/NuCache/ContentNode.cs | 6 ++++++ .../PublishedCache/NuCache/ContentStore.cs | 19 +++++++++++++------ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/ContentNode.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentNode.cs index 3b4859432d..a724e78b72 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentNode.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentNode.cs @@ -109,6 +109,8 @@ namespace Umbraco.Web.PublishedCache.NuCache // everything that is common to both draft and published versions // keep this as small as possible + + public readonly int Id; public readonly Guid Uid; public IPublishedContentType ContentType; @@ -116,10 +118,14 @@ namespace Umbraco.Web.PublishedCache.NuCache public readonly string Path; public readonly int SortOrder; public readonly int ParentContentId; + + // TODO: Can we make everything readonly?? This would make it easier to debug and be less error prone especially for new developers. + // Once a Node is created and exists in the cache it is readonly so we should be able to make that happen at the API level too. public int FirstChildContentId; public int LastChildContentId; public int NextSiblingContentId; public int PreviousSiblingContentId; + public readonly DateTime CreateDate; public readonly int CreatorId; diff --git a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs index 550fd507d5..3857a49a28 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs @@ -602,7 +602,7 @@ namespace Umbraco.Web.PublishedCache.NuCache private void ClearRootLocked() { - if (_root.Gen < _liveGen) + if (_root.Gen != _liveGen) _root = new LinkedNode(new ContentNode(), _liveGen, _root); else _root.Value.FirstChildContentId = -1; @@ -899,8 +899,18 @@ namespace Umbraco.Web.PublishedCache.NuCache return link; } + /// + /// This removes this current node from the tree hiearchy by removing it from it's parent's linked list + /// + /// + /// + /// This is called within a lock which means a new Gen is being created therefore this will not modify any existing content in a Gen. + /// private void RemoveTreeNodeLocked(ContentNode content) { + // NOTE: DO NOT modify `content` here, this would modify data for an existing Gen, all modifications are done to clones + // which would be targeting the new Gen. + var parentLink = content.ParentContentId < 0 ? _root : GetRequiredLinkedNode(content.ParentContentId, "parent", null); @@ -937,9 +947,6 @@ namespace Umbraco.Web.PublishedCache.NuCache var prev = GenCloneLocked(prevLink); prev.NextSiblingContentId = content.NextSiblingContentId; } - - content.NextSiblingContentId = -1; - content.PreviousSiblingContentId = -1; } private bool ParentPublishedLocked(ContentNodeKit kit) @@ -955,7 +962,7 @@ namespace Umbraco.Web.PublishedCache.NuCache { var node = link.Value; - if (node != null && link.Gen < _liveGen) + if (node != null && link.Gen != _liveGen) { node = new ContentNode(link.Value); if (link == _root) @@ -1109,7 +1116,7 @@ namespace Umbraco.Web.PublishedCache.NuCache // this is safe only because we're write-locked foreach (var kvp in dict.Where(x => x.Value != null)) { - if (kvp.Value.Gen < _liveGen) + if (kvp.Value.Gen != _liveGen) { var link = new LinkedNode(null, _liveGen, kvp.Value); dict.TryUpdate(kvp.Key, link, kvp.Value); From b02360518d2da1efb64be81fc6c724c9047699db Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 13 Jan 2020 22:25:46 +1100 Subject: [PATCH 147/202] Changed to GetAwaiter/GetResult and updates comment --- src/Umbraco.Core/Runtime/IMainDomLock.cs | 3 +-- src/Umbraco.Core/Runtime/MainDom.cs | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Core/Runtime/IMainDomLock.cs b/src/Umbraco.Core/Runtime/IMainDomLock.cs index c32e990114..6a62f48194 100644 --- a/src/Umbraco.Core/Runtime/IMainDomLock.cs +++ b/src/Umbraco.Core/Runtime/IMainDomLock.cs @@ -16,9 +16,8 @@ namespace Umbraco.Core.Runtime ///
/// /// - /// A disposable object which will be disposed in order to release the lock + /// An awaitable boolean value which will be false if the elapsed millsecondsTimeout value is exceeded /// - /// Throws a if the elapsed millsecondsTimeout value is exceeded Task AcquireLockAsync(int millisecondsTimeout); /// diff --git a/src/Umbraco.Core/Runtime/MainDom.cs b/src/Umbraco.Core/Runtime/MainDom.cs index 67c7be3d21..e6780ec876 100644 --- a/src/Umbraco.Core/Runtime/MainDom.cs +++ b/src/Umbraco.Core/Runtime/MainDom.cs @@ -145,7 +145,7 @@ namespace Umbraco.Core.Runtime _logger.Info("Acquiring."); // Get the lock - var acquired = _mainDomLock.AcquireLockAsync(LockTimeoutMilliseconds).Result; + var acquired = _mainDomLock.AcquireLockAsync(LockTimeoutMilliseconds).GetAwaiter().GetResult(); if (!acquired) { From 3e71de4698f8a46e92ef747e39366b80dcf635aa Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 13 Jan 2020 22:28:25 +1100 Subject: [PATCH 148/202] ensure locks the data types --- src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs index c959d9f67a..cecca3eb75 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs @@ -462,6 +462,8 @@ namespace Umbraco.Web.PublishedCache.NuCache /// public void UpdateDataTypesLocked(IEnumerable dataTypeIds, Func getContentType) { + EnsureLocked(); + var contentTypes = _contentTypesById .Where(kvp => kvp.Value.Value != null && From d48b33d86d0b81666dec4d33d224b6de2d1ff74c Mon Sep 17 00:00:00 2001 From: Jan Skovgaard Date: Mon, 13 Jan 2020 22:03:34 +0100 Subject: [PATCH 149/202] Icon picker: Accessibility improvements (#6990) --- .../src/less/components/umb-iconpicker.less | 10 ++++++---- .../common/infiniteeditors/iconpicker/iconpicker.html | 7 ++++--- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-iconpicker.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-iconpicker.less index e8a62f739d..98b2b1d72d 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-iconpicker.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-iconpicker.less @@ -15,7 +15,9 @@ overflow: hidden; } -.umb-iconpicker-item a { +.umb-iconpicker-item button { + background: transparent; + border: 0 none; display: flex; justify-content: center; align-items: center; @@ -26,8 +28,8 @@ border-radius: 3px; } -.umb-iconpicker-item a:hover, -.umb-iconpicker-item a:focus { +.umb-iconpicker-item button:hover, +.umb-iconpicker-item button:focus { background: @gray-10; outline: none; } @@ -39,7 +41,7 @@ box-sizing: border-box; } -.umb-iconpicker-item a:active { +.umb-iconpicker-item button:active { background: @gray-10; } diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/iconpicker/iconpicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/iconpicker/iconpicker.html index 3caa6ae03d..c2b0d0e458 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/iconpicker/iconpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/iconpicker/iconpicker.html @@ -46,9 +46,10 @@
  • - - - +
From 8176054432b6187b1cec4921f4542bfb45be2469 Mon Sep 17 00:00:00 2001 From: Jan Skovgaard Date: Mon, 13 Jan 2020 22:33:31 +0100 Subject: [PATCH 150/202] Add label and missing translations (#7019) --- .../components/application/umb-search.less | 6 +- .../components/application/umb-search.html | 14 +- src/Umbraco.Web.UI/Umbraco/config/lang/en.xml | 4737 +++++++++-------- .../Umbraco/config/lang/en_us.xml | 1 + 4 files changed, 2385 insertions(+), 2373 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/components/application/umb-search.less b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-search.less index 70e4f3d372..2f9430ef41 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/application/umb-search.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-search.less @@ -16,6 +16,10 @@ box-shadow: 0 10px 20px rgba(0,0,0,.12),0 6px 6px rgba(0,0,0,.14); } +.umb-search__label{ + margin: 0; +} + /* Search field */ @@ -107,4 +111,4 @@ .umb-search-result__description { color: @gray-5; font-size: 13px; -} \ No newline at end of file +} diff --git a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-search.html b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-search.html index 297ade28c0..cc1c8abb7e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-search.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-search.html @@ -2,15 +2,21 @@ diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-confirm-action.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-confirm-action.html index f4e4dff3af..b45a3bb843 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-confirm-action.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-confirm-action.html @@ -6,11 +6,11 @@ '-left': direction === 'left'}" on-outside-click="clickCancel()"> - + - +
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js index 3db1221f5e..5adf121a56 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js @@ -402,6 +402,15 @@ angular.module("umbraco") eventsService.emit("grid.rowAdded", { scope: $scope, element: $element, row: row }); + // TODO: find a nicer way to do this without relying on setTimeout + setTimeout(function () { + var newRowEl = $element.find("[data-rowid='" + row.$uniqueId + "']"); + + if(newRowEl !== null) { + newRowEl.focus(); + } + }, 0); + }; $scope.removeRow = function (section, $index) { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.html index e889067321..bde2b9148e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.html @@ -95,7 +95,7 @@
- + -
+
+
- +
- +
-
+
@@ -249,14 +249,16 @@
- - - - + + + Add new role + +
@@ -264,10 +266,11 @@

-
+ ng-click="addRow(section, layout)" + type="button">
@@ -285,7 +288,7 @@ {{layout.label || layout.name}} -
+
From 6ca0a8ba665075acf65efeafbfc0eb46dda665ef Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Thu, 16 Jan 2020 14:47:25 +0100 Subject: [PATCH 161/202] V8: A few UX updates to the listview move dialog (#6902) * Add some context to the move operation from listviews * Disable submit button in the listview move dialog until a target is chosen * Fix bad merge --- .../common/infiniteeditors/move/move.html | 6 +- src/Umbraco.Web.UI/Umbraco/config/lang/da.xml | 1 + src/Umbraco.Web.UI/Umbraco/config/lang/en.xml | 1 + .../Umbraco/config/lang/en_us.xml | 4801 +++++++++-------- 4 files changed, 2408 insertions(+), 2401 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/move/move.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/move/move.html index 8424bcefbe..1c9fd5b822 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/move/move.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/move/move.html @@ -13,6 +13,9 @@ +

+ Choose where to move the selected item(s) +

@@ -75,7 +78,8 @@ button-style="success" label-key="general_submit" state="vm.saveButtonState" - action="vm.submit(model)"> + action="vm.submit(model)" + disabled="!model.target"> diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml index 08a00f502e..78075d9b7a 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml @@ -35,6 +35,7 @@ Sæt rettigheder for siden %0% Vælg hvor du vil kopiere Vælg hvortil du vil flytte + Vælg hvor du vil flytte de valgte elementer hen til i træstrukturen nedenfor Vælg hvor du vil kopiere de valgte elementer til blev flyttet til diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index d8e94017aa..a2cf6ea475 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -37,6 +37,7 @@ Choose where to move to in the tree structure below Choose where to copy the selected item(s) + Choose where to move the selected item(s) was moved to was copied to was deleted diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml index 82ba12dc17..56d5f1ade2 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -1,2400 +1,2401 @@ - - - - The Umbraco community - https://our.umbraco.com/documentation/Extending-Umbraco/Language-Files - - - Culture and Hostnames - Audit Trail - Browse Node - Change Document Type - Copy - Create - Export - Create Package - Create group - Delete - Disable - Empty recycle bin - Enable - Export Document Type - Import Document Type - Import Package - Edit in Canvas - Exit - Move - Notifications - Public access - Publish - Unpublish - Reload - Republish entire site - Rename - Restore - Set permissions for the page %0% - Choose where to copy - Choose where to move - to in the tree structure below - Choose where to copy the selected item(s) - was moved to - was copied to - was deleted - Permissions - Rollback - Send To Publish - Send To Translation - Set group - Sort - Translate - Update - Set permissions - Unlock - Create Content Template - Resend Invitation - - - Content - Administration - Structure - Other - - - Allow access to assign culture and hostnames - Allow access to view a node's history log - Allow access to view a node - Allow access to change document type for a node - Allow access to copy a node - Allow access to create nodes - Allow access to delete nodes - Allow access to move a node - Allow access to set and change public access for a node - Allow access to publish a node - Allow access to unpublish a node - Allow access to change permissions for a node - Allow access to roll back a node to a previous state - Allow access to send a node for approval before publishing - Allow access to send a node for translation - Allow access to change the sort order for nodes - Allow access to translate a node - Allow access to save a node - Allow access to create a Content Template - - - Content - Info - - - Permission denied. - Add new Domain - remove - Invalid node. - One or more domains have an invalid format. - Domain has already been assigned. - Language - Domain - New domain '%0%' has been created - Domain '%0%' is deleted - Domain '%0%' has already been assigned - Domain '%0%' has been updated - Edit Current Domains - - - Inherit - Culture - or inherit culture from parent nodes. Will also apply
- to the current node, unless a domain below applies too.]]>
- Domains - - - Clear selection - Select - Do something else - Bold - Cancel Paragraph Indent - Insert form field - Insert graphic headline - Edit Html - Indent Paragraph - Italic - Center - Justify Left - Justify Right - Insert Link - Insert local link (anchor) - Bullet List - Numeric List - Insert macro - Insert picture - Publish and close - Publish with descendants - Edit relations - Return to list - Save - Save and close - Save and publish - Save and schedule - Send for approval - Save list view - Schedule - Preview - Preview is disabled because there's no template assigned - Choose style - Show styles - Insert table - Generate models and close - Save and generate models - Undo - Redo - Rollback - Delete tag - Cancel - Confirm - More publishing options - - - Viewing for - Content deleted - Content unpublished - Content unpublished for languages: %0% - Content published - Content published for languages: %0% - Content saved - Content saved for languages: %0% - Content moved - Content copied - Content rolled back - Content sent for publishing - Content sent for publishing for languages: %0% - Sort child items performed by user - Copy - Publish - Publish - Move - Save - Save - Delete - Unpublish - Unpublish - Rollback - Send To Publish - Send To Publish - Sort - History (all variants) - - - To change the document type for the selected content, first select from the list of valid types for this location. - Then confirm and/or amend the mapping of properties from the current type to the new, and click Save. - The content has been re-published. - Current Property - Current type - The document type cannot be changed, as there are no alternatives valid for this location. An alternative will be valid if it is allowed under the parent of the selected content item and that all existing child content items are allowed to be created under it. - Document Type Changed - Map Properties - Map to Property - New Template - New Type - none - Content - Select New Document Type - The document type of the selected content has been successfully changed to [new type] and the following properties mapped: - to - Could not complete property mapping as one or more properties have more than one mapping defined. - Only alternate types valid for the current location are displayed. - - - Failed to create a folder under parent with ID %0% - Failed to create a folder under parent with name %0% - The folder name cannot contain illegal characters. - Failed to delete item: %0% - - - Is Published - About this page - Alias - (how would you describe the picture over the phone) - Alternative Links - Click to edit this item - Created by - Original author - Updated by - Created - Date/time this document was created - Document Type - Editing - Remove at - This item has been changed after publication - This item is not published - Last published - There are no items to show - There are no items to show in the list. - No child items have been added - No members have been added - Media Type - Link to media item(s) - Member Group - Role - Member Type - No changes have been made - No date chosen - Page title - This media item has no link - No content can be added for this item - Properties - This document is published but is not visible because the parent '%0%' is unpublished - This culture is published but is not visible because it is unpublished on parent '%0%' - This document is published but is not in the cache - Could not get the url - This document is published but its url would collide with content %0% - This document is published but its url cannot be routed - Publish - Published - Published (pending changes)> - Publication Status - Publish with descendants to publish %0% and all content items underneath and thereby making their content publicly available.]]> - Publish with descendants to publish the selected languages and the same languages of content items underneath and thereby making their content publicly available.]]> - Publish at - Unpublish at - Clear Date - Set date - Sortorder is updated - To sort the nodes, simply drag the nodes or click one of the column headers. You can select multiple nodes by holding the "shift" or "control" key while selecting - Statistics - Title (optional) - Alternative text (optional) - Type - Unpublish - Draft - Not created - Last edited - Date/time this document was edited - Remove file(s) - Click here to remove the image from the media item - Click here to remove the file from the media item - Link to document - Member of group(s) - Not a member of group(s) - Child items - Target - This translates to the following time on the server: - What does this mean?]]> - Are you sure you want to delete this item? - Are you sure you want to delete all items? - Property %0% uses editor %1% which is not supported by Nested Content. - No content types are configured for this property. - Add element type - Select element type - Select the group whose properties should be displayed. If left blank, the first group on the element type will be used. - Enter an angular expression to evaluate against each item for its name. Use - to display the item index - Add another text box - Remove this text box - Content root - Include drafts: also publish unpublished content items. - This value is hidden. If you need access to view this value please contact your website administrator. - This value is hidden. - What languages would you like to publish? All languages with content are saved! - What languages would you like to publish? - What languages would you like to save? - All languages with content are saved on creation! - What languages would you like to send for approval? - What languages would you like to schedule? - Select the languages to unpublish. Unpublishing a mandatory language will unpublish all languages. - Published Languages - Unpublished Languages - Unmodified Languages - These languages haven't been created - Ready to Publish? - Ready to Save? - Send for approval - Select the date and time to publish and/or unpublish the content item. - Create new - Paste from clipboard - This item is in the Recycle Bin - - - Create a new Content Template from '%0%' - Blank - Select a Content Template - Content Template created - A Content Template was created from '%0%' - Another Content Template with the same name already exists - A Content Template is predefined content that an editor can select to use as the basis for creating new content - - - Click to upload - or click here to choose files - You can drag files here to upload. - Cannot upload this file, it does not have an approved file type - Max file size is - Media root - Failed to move media - Parent and destination folders cannot be the same - Failed to copy media - Failed to create a folder under parent id %0% - Failed to rename the folder with id %0% - Drag and drop your file(s) into the area - - - Create a new member - All Members - Member groups have no additional properties for editing. - - - Where do you want to create the new %0% - Create an item under - Select the document type you want to make a content template for - Enter a folder name - Choose a type and a title - Document Types within the Settings section, by editing the Allowed child node types under Permissions.]]> - Document Types within the Settings section.]]> - The selected page in the content tree doesn't allow for any pages to be created below it. - Edit permissions for this document type - Create a new document type - Document Types within the Settings section, by changing the Allow as root option under Permissions.]]> - Media Types Types within the Settings section, by editing the Allowed child node types under Permissions.]]> - The selected media in the tree doesn't allow for any other media to be created below it. - Edit permissions for this media type Document Type without a template - New folder - New data type - New JavaScript file - New empty partial view - New partial view macro - New partial view from snippet - New partial view macro from snippet - New partial view macro (without macro) - New style sheet file - New Rich Text Editor style sheet file - - - Browse your website - - Hide - If Umbraco isn't opening, you might need to allow popups from this site - has opened in a new window - Restart - Visit - Welcome - - - Stay - Discard changes - You have unsaved changes - Are you sure you want to navigate away from this page? - you have unsaved changes - Publishing will make the selected items visible on the site. - Unpublishing will remove the selected items and all their descendants from the site. - Unpublishing will remove this page and all its descendants from the site. - You have unsaved changes. Making changes to the Document Type will discard the changes. - - - Done - Deleted %0% item - Deleted %0% items - Deleted %0% out of %1% item - Deleted %0% out of %1% items - Published %0% item - Published %0% items - Published %0% out of %1% item - Published %0% out of %1% items - Unpublished %0% item - Unpublished %0% items - Unpublished %0% out of %1% item - Unpublished %0% out of %1% items - Moved %0% item - Moved %0% items - Moved %0% out of %1% item - Moved %0% out of %1% items - Copied %0% item - Copied %0% items - Copied %0% out of %1% item - Copied %0% out of %1% items - - - Link title - Link - Anchor / querystring - Name - Close this window - Are you sure you want to delete - Are you sure you want to disable - Are you sure? - Are you sure? - Cut - Edit Dictionary Item - Edit Language - Edit selected media - Insert local link - Insert character - Insert graphic headline - Insert picture - Insert link - Click to add a Macro - Insert table - This will delete the language - Changing the culture for a language may be an expensive operation and will result in the content cache and indexes being rebuilt - Last Edited - Link - Internal link: - When using local links, insert "#" in front of link - Open in new window? - Macro Settings - This macro does not contain any properties you can edit - Paste - Edit permissions for - Set permissions for - Set permissions for %0% for user group %1% - Select the users groups you want to set permissions for - The items in the recycle bin are now being deleted. Please do not close this window while this operation takes place - The recycle bin is now empty - When items are deleted from the recycle bin, they will be gone forever - regexlib.com's webservice is currently experiencing some problems, which we have no control over. We are very sorry for this inconvenience.]]> - Search for a regular expression to add validation to a form field. Example: 'email, 'zip-code' 'url' - Remove Macro - Required Field - Site is reindexed - The website cache has been refreshed. All publish content is now up to date. While all unpublished content is still unpublished - The website cache will be refreshed. All published content will be updated, while unpublished content will stay unpublished. - Number of columns - Number of rows - Click on the image to see full size - Pick item - View Cache Item - Relate to original - Include descendants - The friendliest community - Link to page - Opens the linked document in a new window or tab - Link to media - Select content start node - Select media - Select media type - Select icon - Select item - Select link - Select macro - Select content - Select content type - Select media start node - Select member - Select member group - Select member type - Select node - Select sections - Select users - No icons were found - There are no parameters for this macro - There are no macros available to insert - External login providers - Exception Details - Stacktrace - Inner Exception - Link your - Un-link your - account - Select editor - Select snippet - This will delete the node and all its languages. If you only want to delete one language, you should unpublish the node in that language instead. - - - There are no dictionary items. - - - %0%' below - ]]> - Culture Name - - Dictionary overview - - - Configured Searchers - Shows properties and tools for any configured Searcher (i.e. such as a multi-index searcher) - Field values - Health status - The health status of the index and if it can be read - Indexers - Index info - Lists the properties of the index - Manage Examine's indexes - Allows you to view the details of each index and provides some tools for managing the indexes - Rebuild index - - Depending on how much content there is in your site this could take a while.
- It is not recommended to rebuild an index during times of high website traffic or when editors are editing content. - ]]> -
- Searchers - Search the index and view the results - Tools - Tools to manage the index - fields - The index cannot be read and will need to be rebuilt - The process is taking longer than expected, check the umbraco log to see if there have been any errors during this operation - This index cannot be rebuilt because it has no assigned - IIndexPopulator - - - Enter your username - Enter your password - Confirm your password - Name the %0%... - Enter a name... - Enter an email... - Enter a username... - Label... - Enter a description... - Type to search... - Type to filter... - Type to add tags (press enter after each tag)... - Enter your email - Enter a message... - Your username is usually your email - #value or ?key=value - Enter alias... - Generating alias... - - - Create custom list view - Remove custom list view - A content type, media type or member type with this alias already exists - - - Renamed - Enter a new folder name here - %0% was renamed to %1% - - - Add prevalue - Database datatype - Property editor GUID - Property editor - Buttons - Enable advanced settings for - Enable context menu - Maximum default size of inserted images - Related stylesheets - Show label - Width and height - All property types & property data - using this data type will be deleted permanently, please confirm you want to delete these as well - Yes, delete - and all property types & property data using this data type - Select the folder to move - to in the tree structure below - was moved underneath - %0% will delete the properties and their data from the following items]]> - I understand this action will delete the properties and data based on this Data Type - - - Your data has been saved, but before you can publish this page there are some errors you need to fix first: - The current membership provider does not support changing password (EnablePasswordRetrieval need to be true) - %0% already exists - There were errors: - There were errors: - The password should be a minimum of %0% characters long and contain at least %1% non-alpha numeric character(s) - %0% must be an integer - The %0% field in the %1% tab is mandatory - %0% is a mandatory field - %0% at %1% is not in a correct format - %0% is not in a correct format - - - Received an error from the server - The specified file type has been disallowed by the administrator - NOTE! Even though CodeMirror is enabled by configuration, it is disabled in Internet Explorer because it's not stable enough. - Please fill both alias and name on the new property type! - There is a problem with read/write access to a specific file or folder - Error loading Partial View script (file: %0%) - Please enter a title - Please choose a type - You're about to make the picture larger than the original size. Are you sure that you want to proceed? - Startnode deleted, please contact your administrator - Please mark content before changing style - No active styles available - Please place cursor at the left of the two cells you wish to merge - You cannot split a cell that hasn't been merged. - This property is invalid - - - Options - About - Action - Actions - Add - Alias - All - Are you sure? - Back - Back to overview - Border - by - Cancel - Cell margin - Choose - Clear - Close - Close Window - Comment - Confirm - Constrain - Constrain proportions - Content - Continue - Copy - Create - Database - Date - Default - Delete - Deleted - Deleting... - Design - Dictionary - Dimensions - Down - Download - Edit - Edited - Elements - Email - Error - Field - Find - First - Focal point - General - Groups - Group - Height - Help - Hide - History - Icon - Id - Import - Include subfolders in search - Info - Inner margin - Insert - Install - Invalid - Justify - Label - Language - Last - Layout - Links - Loading - Locked - Login - Log off - Logout - Macro - Mandatory - Message - Move - Name - New - Next - No - of - Off - OK - Open - On - or - Order by - Password - Path - One moment please... - Previous - Properties - Rebuild - Email to receive form data - Recycle Bin - Your recycle bin is empty - Reload - Remaining - Remove - Rename - Renew - Required - Retrieve - Retry - Permissions - Scheduled Publishing - Search - Sorry, we can not find what you are looking for. - No items have been added - Server - Settings - Show - Show page on Send - Size - Sort - Status - Submit - Type - Type to search... - under - Up - Update - Upgrade - Upload - Url - User - Username - Value - View - Welcome... - Width - Yes - Folder - Search results - Reorder - I am done reordering - Preview - Change password - to - List view - Saving... - current - Embed - selected - Other - Articles - Videos - Clear - Installing - - - Blue - - - Add group - Add property - Add editor - Add template - Add child node - Add child - Edit data type - Navigate sections - Shortcuts - show shortcuts - Toggle list view - Toggle allow as root - Comment/Uncomment lines - Remove line - Copy Lines Up - Copy Lines Down - Move Lines Up - Move Lines Down - General - Editor - Toggle allow culture variants - - - Background color - Bold - Text color - Font - Text - - - Page - - - The installer cannot connect to the database. - Could not save the web.config file. Please modify the connection string manually. - Your database has been found and is identified as - Database configuration - install button to install the Umbraco %0% database - ]]> - Next to proceed.]]> - Database not found! Please check that the information in the "connection string" of the "web.config" file is correct.

-

To proceed, please edit the "web.config" file (using Visual Studio or your favourite text editor), scroll to the bottom, add the connection string for your database in the key named "UmbracoDbDSN" and save the file.

-

- Click the retry button when - done.
- More information on editing web.config here.

]]>
- - Please contact your ISP if necessary. - If you're installing on a local machine or server you might need information from your system administrator.]]> - - Press the upgrade button to upgrade your database to Umbraco %0%

-

- Don't worry - no content will be deleted and everything will continue working afterwards! -

- ]]>
- Press Next to - proceed. ]]> - next to continue the configuration wizard]]> - The Default users' password needs to be changed!]]> - The Default user has been disabled or has no access to Umbraco!

No further actions needs to be taken. Click Next to proceed.]]> - The Default user's password has been successfully changed since the installation!

No further actions needs to be taken. Click Next to proceed.]]> - The password is changed! - Get a great start, watch our introduction videos - By clicking the next button (or modifying the umbracoConfigurationStatus in web.config), you accept the license for this software as specified in the box below. Notice that this Umbraco distribution consists of two different licenses, the open source MIT license for the framework and the Umbraco freeware license that covers the UI. - Not installed yet. - Affected files and folders - More information on setting up permissions for Umbraco here - You need to grant ASP.NET modify permissions to the following files/folders - Your permission settings are almost perfect!

- You can run Umbraco without problems, but you will not be able to install packages which are recommended to take full advantage of Umbraco.]]>
- How to Resolve - Click here to read the text version - video tutorial on setting up folder permissions for Umbraco or read the text version.]]> - Your permission settings might be an issue! -

- You can run Umbraco without problems, but you will not be able to create folders or install packages which are recommended to take full advantage of Umbraco.]]>
- Your permission settings are not ready for Umbraco! -

- In order to run Umbraco, you'll need to update your permission settings.]]>
- Your permission settings are perfect!

- You are ready to run Umbraco and install packages!]]>
- Resolving folder issue - Follow this link for more information on problems with ASP.NET and creating folders - Setting up folder permissions - - I want to start from scratch - learn how) - You can still choose to install Runway later on. Please go to the Developer section and choose Packages. - ]]> - You've just set up a clean Umbraco platform. What do you want to do next? - Runway is installed - - This is our list of recommended modules, check off the ones you would like to install, or view the full list of modules - ]]> - Only recommended for experienced users - I want to start with a simple website - - "Runway" is a simple website providing some basic document types and templates. The installer can set up Runway for you automatically, - but you can easily edit, extend or remove it. It's not necessary and you can perfectly use Umbraco without it. However, - Runway offers an easy foundation based on best practices to get you started faster than ever. - If you choose to install Runway, you can optionally select basic building blocks called Runway Modules to enhance your Runway pages. -

- - Included with Runway: Home page, Getting Started page, Installing Modules page.
- Optional Modules: Top Navigation, Sitemap, Contact, Gallery. -
- ]]>
- What is Runway - Step 1/5 Accept license - Step 2/5: Database configuration - Step 3/5: Validating File Permissions - Step 4/5: Check Umbraco security - Step 5/5: Umbraco is ready to get you started - Thank you for choosing Umbraco - Browse your new site -You installed Runway, so why not see how your new website looks.]]> - Further help and information -Get help from our award winning community, browse the documentation or watch some free videos on how to build a simple site, how to use packages and a quick guide to the Umbraco terminology]]> - Umbraco %0% is installed and ready for use - /web.config file and update the AppSetting key UmbracoConfigurationStatus in the bottom to the value of '%0%'.]]> - started instantly by clicking the "Launch Umbraco" button below.
If you are new to Umbraco, -you can find plenty of resources on our getting started pages.]]>
- Launch Umbraco -To manage your website, simply open the Umbraco back office and start adding content, updating the templates and stylesheets or add new functionality]]> - Connection to database failed. - Umbraco Version 3 - Umbraco Version 4 - Watch - Umbraco %0% for a fresh install or upgrading from version 3.0. -

- Press "next" to start the wizard.]]>
- - - Culture Code - Culture Name - - - You've been idle and logout will automatically occur in - Renew now to save your work - - - Happy super Sunday - Happy manic Monday - Happy tubular Tuesday - Happy wonderful Wednesday - Happy thunderous Thursday - Happy funky Friday - Happy Caturday - Log in below - Sign in with - Session timed out - © 2001 - %0%
Umbraco.com

]]>
- Forgotten password? - An email will be sent to the address specified with a link to reset your password - An email with password reset instructions will be sent to the specified address if it matched our records - Show password - Hide password - Return to login form - Please provide a new password - Your Password has been updated - The link you have clicked on is invalid or has expired - Umbraco: Reset Password - - - - - - - - - - - -
- - - - - -
- -
- -
-
- - - - - - -
-
-
- - - - -
- - - - -
-

- Password reset requested -

-

- Your username to login to the Umbraco back-office is: %0% -

-

- - - - - - -
- - Click this link to reset your password - -
-

-

If you cannot click on the link, copy and paste this URL into your browser window:

- - - - -
- - %1% - -
-

-
-
-


-
-
- - - ]]>
- - - Dashboard - Sections - Content - - - Choose page above... - %0% has been copied to %1% - Select where the document %0% should be copied to below - %0% has been moved to %1% - Select where the document %0% should be moved to below - has been selected as the root of your new content, click 'ok' below. - No node selected yet, please select a node in the list above before clicking 'ok' - The current node is not allowed under the chosen node because of its type - The current node cannot be moved to one of its subpages - The current node cannot exist at the root - The action isn't allowed since you have insufficient permissions on 1 or more child documents. - Relate copied items to original - - - %0%]]> - Notification settings saved for - - The following languages have been modified %0% - - - - - - - - - - - -
- - - - - -
- -
- -
-
- - - - - - -
-
-
- - - - -
- - - - -
-

- Hi %0%, -

-

- This is an automated mail to inform you that the task '%1%' has been performed on the page '%2%' by the user '%3%' -

- - - - - - -
- -
- EDIT
-
-

-

Update summary:

- %6% -

-

- Have a nice day!

- Cheers from the Umbraco robot -

-
-
-


-
-
- - - ]]>
- The following languages have been modified:

- %0% - ]]>
- [%0%] Notification about %1% performed on %2% - Notifications - - - Actions - Created - Create package - - button and locating the package. Umbraco packages usually have a ".umb" or ".zip" extension. - ]]> - This will delete the package - Drop to upload - Include all child nodes - or click here to choose package file - Upload package - Install a local package by selecting it from your machine. Only install packages from sources you know and trust - Upload another package - Cancel and upload another package - I accept - terms of use - Path to file - Absolute path to file (ie: /bin/umbraco.bin) - Installed - Installed packages - Install local - Finish - This package has no configuration view - No packages have been created yet - You don’t have any packages installed - 'Packages' icon in the top right of your screen]]> - Package Actions - Author URL - Package Content - Package Files - Icon URL - Install package - License - License URL - Package Properties - Search for packages - Results for - We couldn’t find anything for - Please try searching for another package or browse through the categories - Popular - New releases - has - karma points - Information - Owner - Contributors - Created - Current version - .NET version - Downloads - Likes - Compatibility - This package is compatible with the following versions of Umbraco, as reported by community members. Full compatability cannot be guaranteed for versions reported below 100% - External sources - Author - Documentation - Package meta data - Package name - Package doesn't contain any items -
- You can safely remove this from the system by clicking "uninstall package" below.]]>
- Package options - Package readme - Package repository - Confirm package uninstall - Package was uninstalled - The package was successfully uninstalled - Uninstall package - - Notice: any documents, media etc depending on the items you remove, will stop working, and could lead to system instability, - so uninstall with caution. If in doubt, contact the package author.]]> - Package version - Upgrading from version - Package already installed - This package cannot be installed, it requires a minimum Umbraco version of - Uninstalling... - Downloading... - Importing... - Installing... - Restarting, please wait... - All done, your browser will now refresh, please wait... - Please click 'Finish' to complete installation and reload the page. - Uploading package... - - - Paste with full formatting (Not recommended) - The text you're trying to paste contains special characters or formatting. This could be caused by copying text from Microsoft Word. Umbraco can remove special characters or formatting automatically, so the pasted content will be more suitable for the web. - Paste as raw text without any formatting at all - Paste, but remove formatting (Recommended) - - - Group based protection - If you want to grant access to all members of specific member groups - You need to create a member group before you can use group based authentication - Error Page - Used when people are logged on, but do not have access - %0%]]> - %0% is now protected]]> - %0%]]> - Login Page - Choose the page that contains the login form - Remove protection... - %0%?]]> - Select the pages that contain login form and error messages - %0%]]> - %0%]]> - Specific members protection - If you wish to grant access to specific members - - - Insufficient user permissions to publish all descendant documents - - - - - - - - Validation failed for required language '%0%'. This language was saved but not published. - Publishing in progress - please wait... - %0% out of %1% pages have been published... - %0% has been published - %0% and subpages have been published - Publish %0% and all its subpages - Publish to publish %0% and thereby making its content publicly available.

- You can publish this page and all its subpages by checking Include unpublished subpages below. - ]]>
- - - You have not configured any approved colors - - - You can only select items of type(s): %0% - You have picked a content item currently deleted or in the recycle bin - You have picked content items currently deleted or in the recycle bin - - - Deleted item - You have picked a media item currently deleted or in the recycle bin - You have picked media items currently deleted or in the recycle bin - Trashed - - - enter external link - choose internal page - Caption - Link - Open in new window - enter the display caption - Enter the link - - - Reset crop - Save crop - Add new crop - Done - Undo edits - - - Select a version to compare with the current version - Current version - Red text will not be shown in the selected version. , green means added]]> - Document has been rolled back - This displays the selected version as HTML, if you wish to see the difference between 2 versions at the same time, use the diff view - Rollback to - Select version - View - - - Edit script file - - - Content - Forms - Media - Members - Packages - Settings - Translation - Users - - - Tours - The best Umbraco video tutorials - Visit our.umbraco.com - Visit umbraco.tv - - - Default template - To import a document type, find the ".udt" file on your computer by clicking the "Browse" button and click "Import" (you'll be asked for confirmation on the next screen) - New Tab Title - Node type - Type - Stylesheet - Script - Tab - Tab Title - Tabs - Master Content Type enabled - This Content Type uses - as a Master Content Type. Tabs from Master Content Types are not shown and can only be edited on the Master Content Type itself - No properties defined on this tab. Click on the "add a new property" link at the top to create a new property. - Create matching template - Add icon - - - Sort order - Creation date - Sorting complete. - Drag the different items up or down below to set how they should be arranged. Or click the column headers to sort the entire collection of items - - This node has no child nodes to sort - - - Validation - Validation errors must be fixed before the item can be saved - Failed - Saved - Insufficient user permissions, could not complete the operation - Cancelled - Operation was cancelled by a 3rd party add-in - Property type already exists - Property type created - DataType: %1%]]> - Propertytype deleted - Document Type saved - Tab created - Tab deleted - Tab with id: %0% deleted - Stylesheet not saved - Stylesheet saved - Stylesheet saved without any errors - Datatype saved - Dictionary item saved - Content published - and is visible on the website - %0% documents published and visible on the website - %0% published and visible on the website - %0% documents published for languages %1% and visible on the website - Content saved - Remember to publish to make changes visible - A schedule for publishing has been updated - %0% saved - Sent For Approval - Changes have been sent for approval - %0% changes have been sent for approval - Media saved - Media saved without any errors - Member saved - Member group saved - Stylesheet Property Saved - Stylesheet saved - Template saved - Error saving user (check log) - User Saved - User type saved - User group saved - Cultures and hostnames saved - Error saving cultures and hostnames - File not saved - file could not be saved. Please check file permissions - File saved - File saved without any errors - Language saved - Media Type saved - Member Type saved - Member Group saved - Template not saved - Please make sure that you do not have 2 templates with the same alias - Template saved - Template saved without any errors! - Content unpublished - Content variation %0% unpublished - The mandatory language '%0%' was unpublished. All languages for this content item are now unpublished. - Partial view saved - Partial view saved without any errors! - Partial view not saved - An error occurred saving the file. - Permissions saved for - Deleted %0% user groups - %0% was deleted - Enabled %0% users - Disabled %0% users - %0% is now enabled - %0% is now disabled - User groups have been set - Unlocked %0% users - %0% is now unlocked - Member was exported to file - An error occurred while exporting the member - User %0% was deleted - Invite user - Invitation has been re-sent to %0% - Cannot publish the document since the required '%0%' is not published - Validation failed for language '%0%' - Document type was exported to file - An error occurred while exporting the document type - The release date cannot be in the past - Cannot schedule the document for publishing since the required '%0%' is not published - Cannot schedule the document for publishing since the required '%0%' has a publish date later than a non mandatory language - The expire date cannot be in the past - The expire date cannot be before the release date - - - Add style - Edit style - Rich text editor styles - Define the styles that should be available in the rich text editor for this stylesheet - Edit stylesheet - Edit stylesheet property - The name displayed in the editor style selector - Preview - How the text will look like in the rich text editor. - Selector - Uses CSS syntax, e.g. "h1" or ".redHeader" - Styles - The CSS that should be applied in the rich text editor, e.g. "color:red;" - Code - Rich Text Editor - - - Failed to delete template with ID %0% - Edit template - Sections - Insert content area - Insert content area placeholder - Insert - Choose what to insert into your template - Dictionary item - A dictionary item is a placeholder for a translatable piece of text, which makes it easy to create designs for multilingual websites. - Macro - - A Macro is a configurable component which is great for - reusable parts of your design, where you need the option to provide parameters, - such as galleries, forms and lists. - - Value - Displays the value of a named field from the current page, with options to modify the value or fallback to alternative values. - Partial view - - A partial view is a separate template file which can be rendered inside another - template, it's great for reusing markup or for separating complex templates into separate files. - - Master template - No master - Render child template - @RenderBody() placeholder. - ]]> - Define a named section - @section { ... }. This can be rendered in a - specific area of the parent of this template, by using @RenderSection. - ]]> - Render a named section - @RenderSection(name) placeholder. - This renders an area of a child template which is wrapped in a corresponding @section [name]{ ... } definition. - ]]> - Section Name - Section is mandatory - - If mandatory, the child template must contain a @section definition, otherwise an error is shown. - - Query builder - items returned, in - copy to clipboard - I want - all content - content of type "%0%" - from - my website - where - and - is - is not - before - before (including selected date) - after - after (including selected date) - equals - does not equal - contains - does not contain - greater than - greater than or equal to - less than - less than or equal to - Id - Name - Created Date - Last Updated Date - order by - ascending - descending - Template - - - Image - Macro - Choose type of content - Choose a layout - Add a row - Add content - Drop content - Settings applied - This content is not allowed here - This content is allowed here - Click to embed - Click to insert image - Image caption... - Write here... - Grid Layouts - Layouts are the overall work area for the grid editor, usually you only need one or two different layouts - Add Grid Layout - Adjust the layout by setting column widths and adding additional sections - Row configurations - Rows are predefined cells arranged horizontally - Add row configuration - Adjust the row by setting cell widths and adding additional cells - Columns - Total combined number of columns in the grid layout - Settings - Configure what settings editors can change - Styles - Configure what styling editors can change - Allow all editors - Allow all row configurations - Maximum items - Leave blank or set to 0 for unlimited - Set as default - Choose extra - Choose default - are added - Warning - You are deleting the row configuration - - Deleting a row configuration name will result in loss of data for any existing content that is based on this configuration. - - - - Compositions - Group - You have not added any groups - Add group - Inherited from - Add property - Required label - Enable list view - Configures the content item to show a sortable and searchable list of its children, the children will not be shown in the tree - Allowed Templates - Choose which templates editors are allowed to use on content of this type - Allow as root - Allow editors to create content of this type in the root of the content tree. - Allowed child node types - Allow content of the specified types to be created underneath content of this type. - Choose child node - Inherit tabs and properties from an existing document type. New tabs will be added to the current document type or merged if a tab with an identical name exists. - This content type is used in a composition, and therefore cannot be composed itself. - There are no content types available to use as a composition. - Removing a composition will delete all the associated property data. Once you save the document type there's no way back. - Create new - Use existing - Editor settings - Configuration - Yes, delete - was moved underneath - was copied underneath - Select the folder to move - Select the folder to copy - to in the tree structure below - All Document types - All Documents - All media items - using this document type will be deleted permanently, please confirm you want to delete these as well. - using this media type will be deleted permanently, please confirm you want to delete these as well. - using this member type will be deleted permanently, please confirm you want to delete these as well - and all documents using this type - and all media items using this type - and all members using this type - Member can edit - Allow this property value to be edited by the member on their profile page - Is sensitive data - Hide this property value from content editors that don't have access to view sensitive information - Show on member profile - Allow this property value to be displayed on the member profile page - tab has no sort order - Where is this composition used? - This composition is currently used in the composition of the following content types: - Allow varying by culture - Allow editors to create content of this type in different languages. - Allow varying by culture - Element type - Is an Element type - An Element type is meant to be used for instance in Nested Content, and not in the tree. - This is not applicable for an Element type - You have made changes to this property. Are you sure you want to discard them? - - - Add language - Mandatory language - Properties on this language have to be filled out before the node can be published. - Default language - An Umbraco site can only have one default language set. - Switching default language may result in default content missing. - Falls back to - No fall back language - To allow multi-lingual content to fall back to another language if not present in the requested language, select it here. - Fall back language - none - - - Add parameter - Edit parameter - Enter macro name - Parameters - Define the parameters that should be available when using this macro. - Select partial view macro file - - - Building models - this can take a bit of time, don't worry - Models generated - Models could not be generated - Models generation has failed, see exception in U log - - - Add fallback field - Fallback field - Add default value - Default value - Fallback field - Default value - Casing - Encoding - Choose field - Convert line breaks - Yes, convert line breaks - Replaces line breaks with 'br' html tag - Custom Fields - Date only - Format and encoding - Format as date - Format the value as a date, or a date with time, according to the active culture - HTML encode - Will replace special characters by their HTML equivalent. - Will be inserted after the field value - Will be inserted before the field value - Lowercase - Modify output - None - Output sample - Insert after field - Insert before field - Recursive - Yes, make it recursive - Separator - Standard Fields - Uppercase - URL encode - Will format special characters in URLs - Will only be used when the field values above are empty - This field will only be used if the primary field is empty - Date and time - - - Translation details - Download XML DTD - Fields - Include subpages - - No translator users found. Please create a translator user before you start sending content to translation - The page '%0%' has been send to translation - Send the page '%0%' to translation - Total words - Translate to - Translation completed. - You can preview the pages, you've just translated, by clicking below. If the original page is found, you will get a comparison of the 2 pages. - Translation failed, the XML file might be corrupt - Translation options - Translator - Upload translation XML - - - Content - Content Templates - Media - Cache Browser - Recycle Bin - Created packages - Data Types - Dictionary - Installed packages - Install skin - Install starter kit - Languages - Install local package - Macros - Media Types - Members - Member Groups - Member Roles - Member Types - Document Types - Relation Types - Packages - Packages - Partial Views - Partial View Macro Files - Install from repository - Install Runway - Runway modules - Scripting Files - Scripts - Stylesheets - Templates - Log Viewer - Users - Settings - Templating - Third Party - - - New update ready - %0% is ready, click here for download - No connection to server - Error checking for update. Please review trace-stack for further information - - - Access - Based on the assigned groups and start nodes, the user has access to the following nodes - Assign access - Administrator - Category field - User created - Change Your Password - Change photo - New password - hasn't been locked out - The password hasn't been changed - Confirm new password - You can change your password for accessing the Umbraco Back Office by filling out the form below and click the 'Change Password' button - Content Channel - Create another user - Create new users to give them access to Umbraco. When a new user is created a password will be generated that you can share with the user. - Description field - Disable User - Document Type - Editor - Excerpt field - Failed login attempts - Go to user profile - Add groups to assign access and permissions - Invite another user - Invite new users to give them access to Umbraco. An invite email will be sent to the user with information on how to log in to Umbraco. Invites last for 72 hours. - Language - Set the language you will see in menus and dialogs - Last lockout date - Last login - Password last changed - Username - Media start node - Limit the media library to a specific start node - Media start nodes - Limit the media library to specific start nodes - Sections - Disable Umbraco Access - has not logged in yet - Old password - Password - Reset password - Your password has been changed! - Please confirm the new password - Enter your new password - Your new password cannot be blank! - Current password - Invalid current password - There was a difference between the new password and the confirmed password. Please try again! - The confirmed password doesn't match the new password! - Replace child node permissions - You are currently modifying permissions for the pages: - Select pages to modify their permissions - Remove photo - Default permissions - Granular permissions - Set permissions for specific nodes - Profile - Search all children - Add sections to give users access - Select user groups - No start node selected - No start nodes selected - Content start node - Limit the content tree to a specific start node - Content start nodes - Limit the content tree to specific start nodes - User last updated - has been created - The new user has successfully been created. To log in to Umbraco use the password below. - User management - Name - User permissions - User group - has been invited - An invitation has been sent to the new user with details about how to log in to Umbraco. - Hello there and welcome to Umbraco! In just 1 minute you’ll be good to go, we just need you to setup a password and add a picture for your avatar. - Welcome to Umbraco! Unfortunately your invite has expired. Please contact your administrator and ask them to resend it. - Uploading a photo of yourself will make it easy for other users to recognize you. Click the circle above to upload your photo. - Writer - Change - Your profile - Your recent history - Session expires in - Invite user - Create user - Send invite - Back to users - Umbraco: Invitation - - - - - - - - - - - -
- - - - - -
- -
- -
-
- - - - - - -
-
-
- - - - -
- - - - -
-

- Hi %0%, -

-

- You have been invited by %1% to the Umbraco Back Office. -

-

- Message from %1%: -
- %2% -

- - - - - - -
- - - - - - -
- - Click this link to accept the invite - -
-
-

If you cannot click on the link, copy and paste this URL into your browser window:

- - - - -
- - %3% - -
-

-
-
-


-
-
- - ]]>
- Invite - Resending invitation... - Delete User - Are you sure you wish to delete this user account? - All - Active - Disabled - Locked out - Invited - Inactive - Name (A-Z) - Name (Z-A) - Newest - Oldest - Last login - No user groups have been added - - - Validation - Validate as an email address - Validate as a number - Validate as a URL - ...or enter a custom validation - Field is mandatory - Enter a custom validation error message (optional) - Enter a regular expression - Enter a custom validation error message (optional) - You need to add at least - You can only have - items - items selected - Invalid date - Not a number - Invalid email - Value cannot be null - Value cannot be empty - Value is invalid, it does not match the correct pattern - Custom validation - %1% more.]]> - %1% too many.]]> - - - - Value is set to the recommended value: '%0%'. - Value was set to '%1%' for XPath '%2%' in configuration file '%3%'. - Expected value '%1%' for '%2%' in configuration file '%3%', but found '%0%'. - Found unexpected value '%0%' for '%2%' in configuration file '%3%'. - - Custom errors are set to '%0%'. - Custom errors are currently set to '%0%'. It is recommended to set this to '%1%' before go live. - Custom errors successfully set to '%0%'. - MacroErrors are set to '%0%'. - MacroErrors are set to '%0%' which will prevent some or all pages in your site from loading completely if there are any errors in macros. Rectifying this will set the value to '%1%'. - MacroErrors are now set to '%0%'. - - Try Skip IIS Custom Errors is set to '%0%' and you're using IIS version '%1%'. - Try Skip IIS Custom Errors is currently '%0%'. It is recommended to set this to '%1%' for your IIS version (%2%). - Try Skip IIS Custom Errors successfully set to '%0%'. - - File does not exist: '%0%'. - '%0%' in config file '%1%'.]]> - There was an error, check log for full error: %0%. - Database - The database schema is correct for this version of Umbraco - %0% problems were detected with your database schema (Check the log for details) - Some errors were detected while validating the database schema against the current version of Umbraco. - Your website's certificate is valid. - Certificate validation error: '%0%' - Your website's SSL certificate has expired. - Your website's SSL certificate is expiring in %0% days. - Error pinging the URL %0% - '%1%' - You are currently %0% viewing the site using the HTTPS scheme. - The appSetting 'Umbraco.Core.UseHttps' is set to 'false' in your web.config file. Once you access this site using the HTTPS scheme, that should be set to 'true'. - The appSetting 'Umbraco.Core.UseHttps' is set to '%0%' in your web.config file, your cookies are %1% marked as secure. - Could not update the 'Umbraco.Core.UseHttps' setting in your web.config file. Error: %0% - - Enable HTTPS - Sets umbracoSSL setting to true in the appSettings of the web.config file. - The appSetting 'Umbraco.Core.UseHttps' is now set to 'true' in your web.config file, your cookies will be marked as secure. - Fix - Cannot fix a check with a value comparison type of 'ShouldNotEqual'. - Cannot fix a check with a value comparison type of 'ShouldEqual' with a provided value. - Value to fix check not provided. - Debug compilation mode is disabled. - Debug compilation mode is currently enabled. It is recommended to disable this setting before go live. - Debug compilation mode successfully disabled. - Trace mode is disabled. - Trace mode is currently enabled. It is recommended to disable this setting before go live. - Trace mode successfully disabled. - All folders have the correct permissions set. - - %0%.]]> - %0%. If they aren't being written to no action need be taken.]]> - All files have the correct permissions set. - - %0%.]]> - %0%. If they aren't being written to no action need be taken.]]> - X-Frame-Options used to control whether a site can be IFRAMEd by another was found.]]> - X-Frame-Options used to control whether a site can be IFRAMEd by another was not found.]]> - Set Header in Config - Adds a value to the httpProtocol/customHeaders section of web.config to prevent the site being IFRAMEd by other websites. - A setting to create a header preventing IFRAMEing of the site by other websites has been added to your web.config file. - Could not update web.config file. Error: %0% - X-Content-Type-Options used to protect against MIME sniffing vulnerabilities was found.]]> - X-Content-Type-Options used to protect against MIME sniffing vulnerabilities was not found.]]> - Adds a value to the httpProtocol/customHeaders section of web.config to protect against MIME sniffing vulnerabilities. - A setting to create a header protecting against MIME sniffing vulnerabilities has been added to your web.config file. - Strict-Transport-Security, also known as the HSTS-header, was found.]]> - Strict-Transport-Security was not found.]]> - Adds the header 'Strict-Transport-Security' with the value 'max-age=10886400' to the httpProtocol/customHeaders section of web.config. Use this fix only if you will have your domains running with https for the next 18 weeks (minimum). - The HSTS header has been added to your web.config file. - X-XSS-Protection was found.]]> - X-XSS-Protection was not found.]]> - Adds the header 'X-XSS-Protection' with the value '1; mode=block' to the httpProtocol/customHeaders section of web.config. - The X-XSS-Protection header has been added to your web.config file. - - %0%.]]> - No headers revealing information about the website technology were found. - In the Web.config file, system.net/mailsettings could not be found. - In the Web.config file system.net/mailsettings section, the host is not configured. - SMTP settings are configured correctly and the service is operating as expected. - The SMTP server configured with host '%0%' and port '%1%' could not be reached. Please check to ensure the SMTP settings in the Web.config file system.net/mailsettings are correct. - %0%.]]> - %0%.]]> -

Results of the scheduled Umbraco Health Checks run on %0% at %1% are as follows:

%2%]]>
- Umbraco Health Check Status: %0% - Check All Groups - Check group - - The health checker evaluates various areas of your site for best practice settings, configuration, potential problems, etc. You can easily fix problems by pressing a button. - You can add your own health checks, have a look at the documentation for more information about custom health checks.

- ]]> -
- - - Disable URL tracker - Enable URL tracker - Culture - Original URL - Redirected To - Redirect Url Management - The following URLs redirect to this content item: - No redirects have been made - When a published page gets renamed or moved a redirect will automatically be made to the new page. - Are you sure you want to remove the redirect from '%0%' to '%1%'? - Redirect URL removed. - Error removing redirect URL. - This will remove the redirect - Are you sure you want to disable the URL tracker? - URL tracker has now been disabled. - Error disabling the URL tracker, more information can be found in your log file. - URL tracker has now been enabled. - Error enabling the URL tracker, more information can be found in your log file. - - - No Dictionary items to choose from - - - %0% characters left.]]> - %1% too many.]]> - - - Trashed content with Id: {0} related to original parent content with Id: {1} - Trashed media with Id: {0} related to original parent media item with Id: {1} - Cannot automatically restore this item - There is no location where this item can be automatically restored. You can move the item manually using the tree below. - was restored under - - - Direction - Parent to child - Bidirectional - Parent - Child - Count - Relations - Created - Comment - Name - No relations for this relation type. - Relation Type - Relations - - - Getting Started - Redirect URL Management - Content - Welcome - Examine Management - Published Status - Models Builder - Health Check - Profiling - Getting Started - Install Umbraco Forms - - - Go back - Active layout: - Jump to - group - passed - warning - failed - suggestion - Check passed - Check failed - Open backoffice search - Open/Close backoffice help - Open/Close your profile options - Open context menu for - Current language - Switch language to - Create new folder - Partial View - Partial View Macro - Member - Data type - Search the redirect dashboard - Search the user group section - Search the users section - Create item - Create - Edit - Name - - - References - This Data Type has no references. - Used in Document Types - No references to Document Types. - Used in Media Types - No references to Media Types. - Used in Member Types - No references to Member Types. - Used by - - - Log Levels - Saved Searches - Total Items - Timestamp - Level - Machine - Message - Exception - Properties - Search With Google - Search this message with Google - Search With Bing - Search this message with Bing - Search Our Umbraco - Search this message on Our Umbraco forums and docs - Search Our Umbraco with Google - Search Our Umbraco forums using Google - Search Umbraco Source - Search within Umbraco source code on Github - Search Umbraco Issues - Search Umbraco Issues on Github - Delete this search - Find Logs with Request ID - Find Logs with Namespace - Find Logs with Machine Name - Open - - - Copy %0% - %0% from %1% - Remove all items - - - Open Property Actions - - - Wait - Refresh status - Memory Cache - - - - Reload - Database Cache - - Rebuilding can be expensive. - Use it when reloading is not enough, and you think that the database cache has not been - properly generated—which would indicate some critical Umbraco issue. - ]]> - - Rebuild - Internals - - not need to use it. - ]]> - - Collect - Published Cache Status - Caches - - - Performance profiling - - - Umbraco currently runs in debug mode. This means you can use the built-in performance profiler to assess the performance when rendering pages. -

-

- If you want to activate the profiler for a specific page rendering, simply add umbDebug=true to the querystring when requesting the page. -

-

- If you want the profiler to be activated by default for all page renderings, you can use the toggle below. - It will set a cookie in your browser, which then activates the profiler automatically. - In other words, the profiler will only be active by default in your browser - not everyone else's. -

- ]]> -
- Activate the profiler by default - Friendly reminder - - - You should never let a production site run in debug mode. Debug mode is turned off by setting debug="false" on the <compilation /> element in web.config. -

- ]]> -
- - - Umbraco currently does not run in debug mode, so you can't use the built-in profiler. This is how it should be for a production site. -

-

- Debug mode is turned on by setting debug="true" on the <compilation /> element in web.config. -

- ]]> -
- - - Hours of Umbraco training videos are only a click away - - Want to master Umbraco? Spend a couple of minutes learning some best practices by watching one of these videos about using Umbraco. And visit umbraco.tv for even more Umbraco videos

- ]]> -
- To get you started - - - Start here - This section contains the building blocks for your Umbraco site. Follow the below links to find out more about working with the items in the Settings section - Find out more - - in the Documentation section of Our Umbraco - ]]> - - - Community Forum - ]]> - - - tutorial videos (some are free, some require a subscription) - ]]> - - - productivity boosting tools and commercial support - ]]> - - - training and certification opportunities - ]]> - - - - Welcome to The Friendly CMS - Thank you for choosing Umbraco - we think this could be the beginning of something beautiful. While it may feel overwhelming at first, we've done a lot to make the learning curve as smooth and fast as possible. - - - Umbraco Forms - Create forms using an intuitive drag and drop interface. From simple contact forms that sends e-mails to advanced questionaires that integrate with CRM systems. Your clients will love it! - -
+ + + + The Umbraco community + https://our.umbraco.com/documentation/Extending-Umbraco/Language-Files + + + Culture and Hostnames + Audit Trail + Browse Node + Change Document Type + Copy + Create + Export + Create Package + Create group + Delete + Disable + Empty recycle bin + Enable + Export Document Type + Import Document Type + Import Package + Edit in Canvas + Exit + Move + Notifications + Public access + Publish + Unpublish + Reload + Republish entire site + Rename + Restore + Set permissions for the page %0% + Choose where to copy + Choose where to move + to in the tree structure below + Choose where to copy the selected item(s) + Choose where to move the selected item(s) + was moved to + was copied to + was deleted + Permissions + Rollback + Send To Publish + Send To Translation + Set group + Sort + Translate + Update + Set permissions + Unlock + Create Content Template + Resend Invitation + + + Content + Administration + Structure + Other + + + Allow access to assign culture and hostnames + Allow access to view a node's history log + Allow access to view a node + Allow access to change document type for a node + Allow access to copy a node + Allow access to create nodes + Allow access to delete nodes + Allow access to move a node + Allow access to set and change public access for a node + Allow access to publish a node + Allow access to unpublish a node + Allow access to change permissions for a node + Allow access to roll back a node to a previous state + Allow access to send a node for approval before publishing + Allow access to send a node for translation + Allow access to change the sort order for nodes + Allow access to translate a node + Allow access to save a node + Allow access to create a Content Template + + + Content + Info + + + Permission denied. + Add new Domain + remove + Invalid node. + One or more domains have an invalid format. + Domain has already been assigned. + Language + Domain + New domain '%0%' has been created + Domain '%0%' is deleted + Domain '%0%' has already been assigned + Domain '%0%' has been updated + Edit Current Domains + + + Inherit + Culture + or inherit culture from parent nodes. Will also apply
+ to the current node, unless a domain below applies too.]]>
+ Domains + + + Clear selection + Select + Do something else + Bold + Cancel Paragraph Indent + Insert form field + Insert graphic headline + Edit Html + Indent Paragraph + Italic + Center + Justify Left + Justify Right + Insert Link + Insert local link (anchor) + Bullet List + Numeric List + Insert macro + Insert picture + Publish and close + Publish with descendants + Edit relations + Return to list + Save + Save and close + Save and publish + Save and schedule + Send for approval + Save list view + Schedule + Preview + Preview is disabled because there's no template assigned + Choose style + Show styles + Insert table + Generate models and close + Save and generate models + Undo + Redo + Rollback + Delete tag + Cancel + Confirm + More publishing options + + + Viewing for + Content deleted + Content unpublished + Content unpublished for languages: %0% + Content published + Content published for languages: %0% + Content saved + Content saved for languages: %0% + Content moved + Content copied + Content rolled back + Content sent for publishing + Content sent for publishing for languages: %0% + Sort child items performed by user + Copy + Publish + Publish + Move + Save + Save + Delete + Unpublish + Unpublish + Rollback + Send To Publish + Send To Publish + Sort + History (all variants) + + + To change the document type for the selected content, first select from the list of valid types for this location. + Then confirm and/or amend the mapping of properties from the current type to the new, and click Save. + The content has been re-published. + Current Property + Current type + The document type cannot be changed, as there are no alternatives valid for this location. An alternative will be valid if it is allowed under the parent of the selected content item and that all existing child content items are allowed to be created under it. + Document Type Changed + Map Properties + Map to Property + New Template + New Type + none + Content + Select New Document Type + The document type of the selected content has been successfully changed to [new type] and the following properties mapped: + to + Could not complete property mapping as one or more properties have more than one mapping defined. + Only alternate types valid for the current location are displayed. + + + Failed to create a folder under parent with ID %0% + Failed to create a folder under parent with name %0% + The folder name cannot contain illegal characters. + Failed to delete item: %0% + + + Is Published + About this page + Alias + (how would you describe the picture over the phone) + Alternative Links + Click to edit this item + Created by + Original author + Updated by + Created + Date/time this document was created + Document Type + Editing + Remove at + This item has been changed after publication + This item is not published + Last published + There are no items to show + There are no items to show in the list. + No child items have been added + No members have been added + Media Type + Link to media item(s) + Member Group + Role + Member Type + No changes have been made + No date chosen + Page title + This media item has no link + No content can be added for this item + Properties + This document is published but is not visible because the parent '%0%' is unpublished + This culture is published but is not visible because it is unpublished on parent '%0%' + This document is published but is not in the cache + Could not get the url + This document is published but its url would collide with content %0% + This document is published but its url cannot be routed + Publish + Published + Published (pending changes)> + Publication Status + Publish with descendants to publish %0% and all content items underneath and thereby making their content publicly available.]]> + Publish with descendants to publish the selected languages and the same languages of content items underneath and thereby making their content publicly available.]]> + Publish at + Unpublish at + Clear Date + Set date + Sortorder is updated + To sort the nodes, simply drag the nodes or click one of the column headers. You can select multiple nodes by holding the "shift" or "control" key while selecting + Statistics + Title (optional) + Alternative text (optional) + Type + Unpublish + Draft + Not created + Last edited + Date/time this document was edited + Remove file(s) + Click here to remove the image from the media item + Click here to remove the file from the media item + Link to document + Member of group(s) + Not a member of group(s) + Child items + Target + This translates to the following time on the server: + What does this mean?]]> + Are you sure you want to delete this item? + Are you sure you want to delete all items? + Property %0% uses editor %1% which is not supported by Nested Content. + No content types are configured for this property. + Add element type + Select element type + Select the group whose properties should be displayed. If left blank, the first group on the element type will be used. + Enter an angular expression to evaluate against each item for its name. Use + to display the item index + Add another text box + Remove this text box + Content root + Include drafts: also publish unpublished content items. + This value is hidden. If you need access to view this value please contact your website administrator. + This value is hidden. + What languages would you like to publish? All languages with content are saved! + What languages would you like to publish? + What languages would you like to save? + All languages with content are saved on creation! + What languages would you like to send for approval? + What languages would you like to schedule? + Select the languages to unpublish. Unpublishing a mandatory language will unpublish all languages. + Published Languages + Unpublished Languages + Unmodified Languages + These languages haven't been created + Ready to Publish? + Ready to Save? + Send for approval + Select the date and time to publish and/or unpublish the content item. + Create new + Paste from clipboard + This item is in the Recycle Bin + + + Create a new Content Template from '%0%' + Blank + Select a Content Template + Content Template created + A Content Template was created from '%0%' + Another Content Template with the same name already exists + A Content Template is predefined content that an editor can select to use as the basis for creating new content + + + Click to upload + or click here to choose files + You can drag files here to upload. + Cannot upload this file, it does not have an approved file type + Max file size is + Media root + Failed to move media + Parent and destination folders cannot be the same + Failed to copy media + Failed to create a folder under parent id %0% + Failed to rename the folder with id %0% + Drag and drop your file(s) into the area + + + Create a new member + All Members + Member groups have no additional properties for editing. + + + Where do you want to create the new %0% + Create an item under + Select the document type you want to make a content template for + Enter a folder name + Choose a type and a title + Document Types within the Settings section, by editing the Allowed child node types under Permissions.]]> + Document Types within the Settings section.]]> + The selected page in the content tree doesn't allow for any pages to be created below it. + Edit permissions for this document type + Create a new document type + Document Types within the Settings section, by changing the Allow as root option under Permissions.]]> + Media Types Types within the Settings section, by editing the Allowed child node types under Permissions.]]> + The selected media in the tree doesn't allow for any other media to be created below it. + Edit permissions for this media type Document Type without a template + New folder + New data type + New JavaScript file + New empty partial view + New partial view macro + New partial view from snippet + New partial view macro from snippet + New partial view macro (without macro) + New style sheet file + New Rich Text Editor style sheet file + + + Browse your website + - Hide + If Umbraco isn't opening, you might need to allow popups from this site + has opened in a new window + Restart + Visit + Welcome + + + Stay + Discard changes + You have unsaved changes + Are you sure you want to navigate away from this page? - you have unsaved changes + Publishing will make the selected items visible on the site. + Unpublishing will remove the selected items and all their descendants from the site. + Unpublishing will remove this page and all its descendants from the site. + You have unsaved changes. Making changes to the Document Type will discard the changes. + + + Done + Deleted %0% item + Deleted %0% items + Deleted %0% out of %1% item + Deleted %0% out of %1% items + Published %0% item + Published %0% items + Published %0% out of %1% item + Published %0% out of %1% items + Unpublished %0% item + Unpublished %0% items + Unpublished %0% out of %1% item + Unpublished %0% out of %1% items + Moved %0% item + Moved %0% items + Moved %0% out of %1% item + Moved %0% out of %1% items + Copied %0% item + Copied %0% items + Copied %0% out of %1% item + Copied %0% out of %1% items + + + Link title + Link + Anchor / querystring + Name + Close this window + Are you sure you want to delete + Are you sure you want to disable + Are you sure? + Are you sure? + Cut + Edit Dictionary Item + Edit Language + Edit selected media + Insert local link + Insert character + Insert graphic headline + Insert picture + Insert link + Click to add a Macro + Insert table + This will delete the language + Changing the culture for a language may be an expensive operation and will result in the content cache and indexes being rebuilt + Last Edited + Link + Internal link: + When using local links, insert "#" in front of link + Open in new window? + Macro Settings + This macro does not contain any properties you can edit + Paste + Edit permissions for + Set permissions for + Set permissions for %0% for user group %1% + Select the users groups you want to set permissions for + The items in the recycle bin are now being deleted. Please do not close this window while this operation takes place + The recycle bin is now empty + When items are deleted from the recycle bin, they will be gone forever + regexlib.com's webservice is currently experiencing some problems, which we have no control over. We are very sorry for this inconvenience.]]> + Search for a regular expression to add validation to a form field. Example: 'email, 'zip-code' 'url' + Remove Macro + Required Field + Site is reindexed + The website cache has been refreshed. All publish content is now up to date. While all unpublished content is still unpublished + The website cache will be refreshed. All published content will be updated, while unpublished content will stay unpublished. + Number of columns + Number of rows + Click on the image to see full size + Pick item + View Cache Item + Relate to original + Include descendants + The friendliest community + Link to page + Opens the linked document in a new window or tab + Link to media + Select content start node + Select media + Select media type + Select icon + Select item + Select link + Select macro + Select content + Select content type + Select media start node + Select member + Select member group + Select member type + Select node + Select sections + Select users + No icons were found + There are no parameters for this macro + There are no macros available to insert + External login providers + Exception Details + Stacktrace + Inner Exception + Link your + Un-link your + account + Select editor + Select snippet + This will delete the node and all its languages. If you only want to delete one language, you should unpublish the node in that language instead. + + + There are no dictionary items. + + + %0%' below + ]]> + Culture Name + + Dictionary overview + + + Configured Searchers + Shows properties and tools for any configured Searcher (i.e. such as a multi-index searcher) + Field values + Health status + The health status of the index and if it can be read + Indexers + Index info + Lists the properties of the index + Manage Examine's indexes + Allows you to view the details of each index and provides some tools for managing the indexes + Rebuild index + + Depending on how much content there is in your site this could take a while.
+ It is not recommended to rebuild an index during times of high website traffic or when editors are editing content. + ]]> +
+ Searchers + Search the index and view the results + Tools + Tools to manage the index + fields + The index cannot be read and will need to be rebuilt + The process is taking longer than expected, check the umbraco log to see if there have been any errors during this operation + This index cannot be rebuilt because it has no assigned + IIndexPopulator + + + Enter your username + Enter your password + Confirm your password + Name the %0%... + Enter a name... + Enter an email... + Enter a username... + Label... + Enter a description... + Type to search... + Type to filter... + Type to add tags (press enter after each tag)... + Enter your email + Enter a message... + Your username is usually your email + #value or ?key=value + Enter alias... + Generating alias... + + + Create custom list view + Remove custom list view + A content type, media type or member type with this alias already exists + + + Renamed + Enter a new folder name here + %0% was renamed to %1% + + + Add prevalue + Database datatype + Property editor GUID + Property editor + Buttons + Enable advanced settings for + Enable context menu + Maximum default size of inserted images + Related stylesheets + Show label + Width and height + All property types & property data + using this data type will be deleted permanently, please confirm you want to delete these as well + Yes, delete + and all property types & property data using this data type + Select the folder to move + to in the tree structure below + was moved underneath + %0% will delete the properties and their data from the following items]]> + I understand this action will delete the properties and data based on this Data Type + + + Your data has been saved, but before you can publish this page there are some errors you need to fix first: + The current membership provider does not support changing password (EnablePasswordRetrieval need to be true) + %0% already exists + There were errors: + There were errors: + The password should be a minimum of %0% characters long and contain at least %1% non-alpha numeric character(s) + %0% must be an integer + The %0% field in the %1% tab is mandatory + %0% is a mandatory field + %0% at %1% is not in a correct format + %0% is not in a correct format + + + Received an error from the server + The specified file type has been disallowed by the administrator + NOTE! Even though CodeMirror is enabled by configuration, it is disabled in Internet Explorer because it's not stable enough. + Please fill both alias and name on the new property type! + There is a problem with read/write access to a specific file or folder + Error loading Partial View script (file: %0%) + Please enter a title + Please choose a type + You're about to make the picture larger than the original size. Are you sure that you want to proceed? + Startnode deleted, please contact your administrator + Please mark content before changing style + No active styles available + Please place cursor at the left of the two cells you wish to merge + You cannot split a cell that hasn't been merged. + This property is invalid + + + Options + About + Action + Actions + Add + Alias + All + Are you sure? + Back + Back to overview + Border + by + Cancel + Cell margin + Choose + Clear + Close + Close Window + Comment + Confirm + Constrain + Constrain proportions + Content + Continue + Copy + Create + Database + Date + Default + Delete + Deleted + Deleting... + Design + Dictionary + Dimensions + Down + Download + Edit + Edited + Elements + Email + Error + Field + Find + First + Focal point + General + Groups + Group + Height + Help + Hide + History + Icon + Id + Import + Include subfolders in search + Info + Inner margin + Insert + Install + Invalid + Justify + Label + Language + Last + Layout + Links + Loading + Locked + Login + Log off + Logout + Macro + Mandatory + Message + Move + Name + New + Next + No + of + Off + OK + Open + On + or + Order by + Password + Path + One moment please... + Previous + Properties + Rebuild + Email to receive form data + Recycle Bin + Your recycle bin is empty + Reload + Remaining + Remove + Rename + Renew + Required + Retrieve + Retry + Permissions + Scheduled Publishing + Search + Sorry, we can not find what you are looking for. + No items have been added + Server + Settings + Show + Show page on Send + Size + Sort + Status + Submit + Type + Type to search... + under + Up + Update + Upgrade + Upload + Url + User + Username + Value + View + Welcome... + Width + Yes + Folder + Search results + Reorder + I am done reordering + Preview + Change password + to + List view + Saving... + current + Embed + selected + Other + Articles + Videos + Clear + Installing + + + Blue + + + Add group + Add property + Add editor + Add template + Add child node + Add child + Edit data type + Navigate sections + Shortcuts + show shortcuts + Toggle list view + Toggle allow as root + Comment/Uncomment lines + Remove line + Copy Lines Up + Copy Lines Down + Move Lines Up + Move Lines Down + General + Editor + Toggle allow culture variants + + + Background color + Bold + Text color + Font + Text + + + Page + + + The installer cannot connect to the database. + Could not save the web.config file. Please modify the connection string manually. + Your database has been found and is identified as + Database configuration + install button to install the Umbraco %0% database + ]]> + Next to proceed.]]> + Database not found! Please check that the information in the "connection string" of the "web.config" file is correct.

+

To proceed, please edit the "web.config" file (using Visual Studio or your favourite text editor), scroll to the bottom, add the connection string for your database in the key named "UmbracoDbDSN" and save the file.

+

+ Click the retry button when + done.
+ More information on editing web.config here.

]]>
+ + Please contact your ISP if necessary. + If you're installing on a local machine or server you might need information from your system administrator.]]> + + Press the upgrade button to upgrade your database to Umbraco %0%

+

+ Don't worry - no content will be deleted and everything will continue working afterwards! +

+ ]]>
+ Press Next to + proceed. ]]> + next to continue the configuration wizard]]> + The Default users' password needs to be changed!]]> + The Default user has been disabled or has no access to Umbraco!

No further actions needs to be taken. Click Next to proceed.]]> + The Default user's password has been successfully changed since the installation!

No further actions needs to be taken. Click Next to proceed.]]> + The password is changed! + Get a great start, watch our introduction videos + By clicking the next button (or modifying the umbracoConfigurationStatus in web.config), you accept the license for this software as specified in the box below. Notice that this Umbraco distribution consists of two different licenses, the open source MIT license for the framework and the Umbraco freeware license that covers the UI. + Not installed yet. + Affected files and folders + More information on setting up permissions for Umbraco here + You need to grant ASP.NET modify permissions to the following files/folders + Your permission settings are almost perfect!

+ You can run Umbraco without problems, but you will not be able to install packages which are recommended to take full advantage of Umbraco.]]>
+ How to Resolve + Click here to read the text version + video tutorial on setting up folder permissions for Umbraco or read the text version.]]> + Your permission settings might be an issue! +

+ You can run Umbraco without problems, but you will not be able to create folders or install packages which are recommended to take full advantage of Umbraco.]]>
+ Your permission settings are not ready for Umbraco! +

+ In order to run Umbraco, you'll need to update your permission settings.]]>
+ Your permission settings are perfect!

+ You are ready to run Umbraco and install packages!]]>
+ Resolving folder issue + Follow this link for more information on problems with ASP.NET and creating folders + Setting up folder permissions + + I want to start from scratch + learn how) + You can still choose to install Runway later on. Please go to the Developer section and choose Packages. + ]]> + You've just set up a clean Umbraco platform. What do you want to do next? + Runway is installed + + This is our list of recommended modules, check off the ones you would like to install, or view the full list of modules + ]]> + Only recommended for experienced users + I want to start with a simple website + + "Runway" is a simple website providing some basic document types and templates. The installer can set up Runway for you automatically, + but you can easily edit, extend or remove it. It's not necessary and you can perfectly use Umbraco without it. However, + Runway offers an easy foundation based on best practices to get you started faster than ever. + If you choose to install Runway, you can optionally select basic building blocks called Runway Modules to enhance your Runway pages. +

+ + Included with Runway: Home page, Getting Started page, Installing Modules page.
+ Optional Modules: Top Navigation, Sitemap, Contact, Gallery. +
+ ]]>
+ What is Runway + Step 1/5 Accept license + Step 2/5: Database configuration + Step 3/5: Validating File Permissions + Step 4/5: Check Umbraco security + Step 5/5: Umbraco is ready to get you started + Thank you for choosing Umbraco + Browse your new site +You installed Runway, so why not see how your new website looks.]]> + Further help and information +Get help from our award winning community, browse the documentation or watch some free videos on how to build a simple site, how to use packages and a quick guide to the Umbraco terminology]]> + Umbraco %0% is installed and ready for use + /web.config file and update the AppSetting key UmbracoConfigurationStatus in the bottom to the value of '%0%'.]]> + started instantly by clicking the "Launch Umbraco" button below.
If you are new to Umbraco, +you can find plenty of resources on our getting started pages.]]>
+ Launch Umbraco +To manage your website, simply open the Umbraco back office and start adding content, updating the templates and stylesheets or add new functionality]]> + Connection to database failed. + Umbraco Version 3 + Umbraco Version 4 + Watch + Umbraco %0% for a fresh install or upgrading from version 3.0. +

+ Press "next" to start the wizard.]]>
+ + + Culture Code + Culture Name + + + You've been idle and logout will automatically occur in + Renew now to save your work + + + Happy super Sunday + Happy manic Monday + Happy tubular Tuesday + Happy wonderful Wednesday + Happy thunderous Thursday + Happy funky Friday + Happy Caturday + Log in below + Sign in with + Session timed out + © 2001 - %0%
Umbraco.com

]]>
+ Forgotten password? + An email will be sent to the address specified with a link to reset your password + An email with password reset instructions will be sent to the specified address if it matched our records + Show password + Hide password + Return to login form + Please provide a new password + Your Password has been updated + The link you have clicked on is invalid or has expired + Umbraco: Reset Password + + + + + + + + + + + +
+ + + + + +
+ +
+ +
+
+ + + + + + +
+
+
+ + + + +
+ + + + +
+

+ Password reset requested +

+

+ Your username to login to the Umbraco back-office is: %0% +

+

+ + + + + + +
+ + Click this link to reset your password + +
+

+

If you cannot click on the link, copy and paste this URL into your browser window:

+ + + + +
+ + %1% + +
+

+
+
+


+
+
+ + + ]]>
+ + + Dashboard + Sections + Content + + + Choose page above... + %0% has been copied to %1% + Select where the document %0% should be copied to below + %0% has been moved to %1% + Select where the document %0% should be moved to below + has been selected as the root of your new content, click 'ok' below. + No node selected yet, please select a node in the list above before clicking 'ok' + The current node is not allowed under the chosen node because of its type + The current node cannot be moved to one of its subpages + The current node cannot exist at the root + The action isn't allowed since you have insufficient permissions on 1 or more child documents. + Relate copied items to original + + + %0%]]> + Notification settings saved for + + The following languages have been modified %0% + + + + + + + + + + + +
+ + + + + +
+ +
+ +
+
+ + + + + + +
+
+
+ + + + +
+ + + + +
+

+ Hi %0%, +

+

+ This is an automated mail to inform you that the task '%1%' has been performed on the page '%2%' by the user '%3%' +

+ + + + + + +
+ +
+ EDIT
+
+

+

Update summary:

+ %6% +

+

+ Have a nice day!

+ Cheers from the Umbraco robot +

+
+
+


+
+
+ + + ]]>
+ The following languages have been modified:

+ %0% + ]]>
+ [%0%] Notification about %1% performed on %2% + Notifications + + + Actions + Created + Create package + + button and locating the package. Umbraco packages usually have a ".umb" or ".zip" extension. + ]]> + This will delete the package + Drop to upload + Include all child nodes + or click here to choose package file + Upload package + Install a local package by selecting it from your machine. Only install packages from sources you know and trust + Upload another package + Cancel and upload another package + I accept + terms of use + Path to file + Absolute path to file (ie: /bin/umbraco.bin) + Installed + Installed packages + Install local + Finish + This package has no configuration view + No packages have been created yet + You don’t have any packages installed + 'Packages' icon in the top right of your screen]]> + Package Actions + Author URL + Package Content + Package Files + Icon URL + Install package + License + License URL + Package Properties + Search for packages + Results for + We couldn’t find anything for + Please try searching for another package or browse through the categories + Popular + New releases + has + karma points + Information + Owner + Contributors + Created + Current version + .NET version + Downloads + Likes + Compatibility + This package is compatible with the following versions of Umbraco, as reported by community members. Full compatability cannot be guaranteed for versions reported below 100% + External sources + Author + Documentation + Package meta data + Package name + Package doesn't contain any items +
+ You can safely remove this from the system by clicking "uninstall package" below.]]>
+ Package options + Package readme + Package repository + Confirm package uninstall + Package was uninstalled + The package was successfully uninstalled + Uninstall package + + Notice: any documents, media etc depending on the items you remove, will stop working, and could lead to system instability, + so uninstall with caution. If in doubt, contact the package author.]]> + Package version + Upgrading from version + Package already installed + This package cannot be installed, it requires a minimum Umbraco version of + Uninstalling... + Downloading... + Importing... + Installing... + Restarting, please wait... + All done, your browser will now refresh, please wait... + Please click 'Finish' to complete installation and reload the page. + Uploading package... + + + Paste with full formatting (Not recommended) + The text you're trying to paste contains special characters or formatting. This could be caused by copying text from Microsoft Word. Umbraco can remove special characters or formatting automatically, so the pasted content will be more suitable for the web. + Paste as raw text without any formatting at all + Paste, but remove formatting (Recommended) + + + Group based protection + If you want to grant access to all members of specific member groups + You need to create a member group before you can use group based authentication + Error Page + Used when people are logged on, but do not have access + %0%]]> + %0% is now protected]]> + %0%]]> + Login Page + Choose the page that contains the login form + Remove protection... + %0%?]]> + Select the pages that contain login form and error messages + %0%]]> + %0%]]> + Specific members protection + If you wish to grant access to specific members + + + Insufficient user permissions to publish all descendant documents + + + + + + + + Validation failed for required language '%0%'. This language was saved but not published. + Publishing in progress - please wait... + %0% out of %1% pages have been published... + %0% has been published + %0% and subpages have been published + Publish %0% and all its subpages + Publish to publish %0% and thereby making its content publicly available.

+ You can publish this page and all its subpages by checking Include unpublished subpages below. + ]]>
+ + + You have not configured any approved colors + + + You can only select items of type(s): %0% + You have picked a content item currently deleted or in the recycle bin + You have picked content items currently deleted or in the recycle bin + + + Deleted item + You have picked a media item currently deleted or in the recycle bin + You have picked media items currently deleted or in the recycle bin + Trashed + + + enter external link + choose internal page + Caption + Link + Open in new window + enter the display caption + Enter the link + + + Reset crop + Save crop + Add new crop + Done + Undo edits + + + Select a version to compare with the current version + Current version + Red text will not be shown in the selected version. , green means added]]> + Document has been rolled back + This displays the selected version as HTML, if you wish to see the difference between 2 versions at the same time, use the diff view + Rollback to + Select version + View + + + Edit script file + + + Content + Forms + Media + Members + Packages + Settings + Translation + Users + + + Tours + The best Umbraco video tutorials + Visit our.umbraco.com + Visit umbraco.tv + + + Default template + To import a document type, find the ".udt" file on your computer by clicking the "Browse" button and click "Import" (you'll be asked for confirmation on the next screen) + New Tab Title + Node type + Type + Stylesheet + Script + Tab + Tab Title + Tabs + Master Content Type enabled + This Content Type uses + as a Master Content Type. Tabs from Master Content Types are not shown and can only be edited on the Master Content Type itself + No properties defined on this tab. Click on the "add a new property" link at the top to create a new property. + Create matching template + Add icon + + + Sort order + Creation date + Sorting complete. + Drag the different items up or down below to set how they should be arranged. Or click the column headers to sort the entire collection of items + + This node has no child nodes to sort + + + Validation + Validation errors must be fixed before the item can be saved + Failed + Saved + Insufficient user permissions, could not complete the operation + Cancelled + Operation was cancelled by a 3rd party add-in + Property type already exists + Property type created + DataType: %1%]]> + Propertytype deleted + Document Type saved + Tab created + Tab deleted + Tab with id: %0% deleted + Stylesheet not saved + Stylesheet saved + Stylesheet saved without any errors + Datatype saved + Dictionary item saved + Content published + and is visible on the website + %0% documents published and visible on the website + %0% published and visible on the website + %0% documents published for languages %1% and visible on the website + Content saved + Remember to publish to make changes visible + A schedule for publishing has been updated + %0% saved + Sent For Approval + Changes have been sent for approval + %0% changes have been sent for approval + Media saved + Media saved without any errors + Member saved + Member group saved + Stylesheet Property Saved + Stylesheet saved + Template saved + Error saving user (check log) + User Saved + User type saved + User group saved + Cultures and hostnames saved + Error saving cultures and hostnames + File not saved + file could not be saved. Please check file permissions + File saved + File saved without any errors + Language saved + Media Type saved + Member Type saved + Member Group saved + Template not saved + Please make sure that you do not have 2 templates with the same alias + Template saved + Template saved without any errors! + Content unpublished + Content variation %0% unpublished + The mandatory language '%0%' was unpublished. All languages for this content item are now unpublished. + Partial view saved + Partial view saved without any errors! + Partial view not saved + An error occurred saving the file. + Permissions saved for + Deleted %0% user groups + %0% was deleted + Enabled %0% users + Disabled %0% users + %0% is now enabled + %0% is now disabled + User groups have been set + Unlocked %0% users + %0% is now unlocked + Member was exported to file + An error occurred while exporting the member + User %0% was deleted + Invite user + Invitation has been re-sent to %0% + Cannot publish the document since the required '%0%' is not published + Validation failed for language '%0%' + Document type was exported to file + An error occurred while exporting the document type + The release date cannot be in the past + Cannot schedule the document for publishing since the required '%0%' is not published + Cannot schedule the document for publishing since the required '%0%' has a publish date later than a non mandatory language + The expire date cannot be in the past + The expire date cannot be before the release date + + + Add style + Edit style + Rich text editor styles + Define the styles that should be available in the rich text editor for this stylesheet + Edit stylesheet + Edit stylesheet property + The name displayed in the editor style selector + Preview + How the text will look like in the rich text editor. + Selector + Uses CSS syntax, e.g. "h1" or ".redHeader" + Styles + The CSS that should be applied in the rich text editor, e.g. "color:red;" + Code + Rich Text Editor + + + Failed to delete template with ID %0% + Edit template + Sections + Insert content area + Insert content area placeholder + Insert + Choose what to insert into your template + Dictionary item + A dictionary item is a placeholder for a translatable piece of text, which makes it easy to create designs for multilingual websites. + Macro + + A Macro is a configurable component which is great for + reusable parts of your design, where you need the option to provide parameters, + such as galleries, forms and lists. + + Value + Displays the value of a named field from the current page, with options to modify the value or fallback to alternative values. + Partial view + + A partial view is a separate template file which can be rendered inside another + template, it's great for reusing markup or for separating complex templates into separate files. + + Master template + No master + Render child template + @RenderBody() placeholder. + ]]> + Define a named section + @section { ... }. This can be rendered in a + specific area of the parent of this template, by using @RenderSection. + ]]> + Render a named section + @RenderSection(name) placeholder. + This renders an area of a child template which is wrapped in a corresponding @section [name]{ ... } definition. + ]]> + Section Name + Section is mandatory + + If mandatory, the child template must contain a @section definition, otherwise an error is shown. + + Query builder + items returned, in + copy to clipboard + I want + all content + content of type "%0%" + from + my website + where + and + is + is not + before + before (including selected date) + after + after (including selected date) + equals + does not equal + contains + does not contain + greater than + greater than or equal to + less than + less than or equal to + Id + Name + Created Date + Last Updated Date + order by + ascending + descending + Template + + + Image + Macro + Choose type of content + Choose a layout + Add a row + Add content + Drop content + Settings applied + This content is not allowed here + This content is allowed here + Click to embed + Click to insert image + Image caption... + Write here... + Grid Layouts + Layouts are the overall work area for the grid editor, usually you only need one or two different layouts + Add Grid Layout + Adjust the layout by setting column widths and adding additional sections + Row configurations + Rows are predefined cells arranged horizontally + Add row configuration + Adjust the row by setting cell widths and adding additional cells + Columns + Total combined number of columns in the grid layout + Settings + Configure what settings editors can change + Styles + Configure what styling editors can change + Allow all editors + Allow all row configurations + Maximum items + Leave blank or set to 0 for unlimited + Set as default + Choose extra + Choose default + are added + Warning + You are deleting the row configuration + + Deleting a row configuration name will result in loss of data for any existing content that is based on this configuration. + + + + Compositions + Group + You have not added any groups + Add group + Inherited from + Add property + Required label + Enable list view + Configures the content item to show a sortable and searchable list of its children, the children will not be shown in the tree + Allowed Templates + Choose which templates editors are allowed to use on content of this type + Allow as root + Allow editors to create content of this type in the root of the content tree. + Allowed child node types + Allow content of the specified types to be created underneath content of this type. + Choose child node + Inherit tabs and properties from an existing document type. New tabs will be added to the current document type or merged if a tab with an identical name exists. + This content type is used in a composition, and therefore cannot be composed itself. + There are no content types available to use as a composition. + Removing a composition will delete all the associated property data. Once you save the document type there's no way back. + Create new + Use existing + Editor settings + Configuration + Yes, delete + was moved underneath + was copied underneath + Select the folder to move + Select the folder to copy + to in the tree structure below + All Document types + All Documents + All media items + using this document type will be deleted permanently, please confirm you want to delete these as well. + using this media type will be deleted permanently, please confirm you want to delete these as well. + using this member type will be deleted permanently, please confirm you want to delete these as well + and all documents using this type + and all media items using this type + and all members using this type + Member can edit + Allow this property value to be edited by the member on their profile page + Is sensitive data + Hide this property value from content editors that don't have access to view sensitive information + Show on member profile + Allow this property value to be displayed on the member profile page + tab has no sort order + Where is this composition used? + This composition is currently used in the composition of the following content types: + Allow varying by culture + Allow editors to create content of this type in different languages. + Allow varying by culture + Element type + Is an Element type + An Element type is meant to be used for instance in Nested Content, and not in the tree. + This is not applicable for an Element type + You have made changes to this property. Are you sure you want to discard them? + + + Add language + Mandatory language + Properties on this language have to be filled out before the node can be published. + Default language + An Umbraco site can only have one default language set. + Switching default language may result in default content missing. + Falls back to + No fall back language + To allow multi-lingual content to fall back to another language if not present in the requested language, select it here. + Fall back language + none + + + Add parameter + Edit parameter + Enter macro name + Parameters + Define the parameters that should be available when using this macro. + Select partial view macro file + + + Building models + this can take a bit of time, don't worry + Models generated + Models could not be generated + Models generation has failed, see exception in U log + + + Add fallback field + Fallback field + Add default value + Default value + Fallback field + Default value + Casing + Encoding + Choose field + Convert line breaks + Yes, convert line breaks + Replaces line breaks with 'br' html tag + Custom Fields + Date only + Format and encoding + Format as date + Format the value as a date, or a date with time, according to the active culture + HTML encode + Will replace special characters by their HTML equivalent. + Will be inserted after the field value + Will be inserted before the field value + Lowercase + Modify output + None + Output sample + Insert after field + Insert before field + Recursive + Yes, make it recursive + Separator + Standard Fields + Uppercase + URL encode + Will format special characters in URLs + Will only be used when the field values above are empty + This field will only be used if the primary field is empty + Date and time + + + Translation details + Download XML DTD + Fields + Include subpages + + No translator users found. Please create a translator user before you start sending content to translation + The page '%0%' has been send to translation + Send the page '%0%' to translation + Total words + Translate to + Translation completed. + You can preview the pages, you've just translated, by clicking below. If the original page is found, you will get a comparison of the 2 pages. + Translation failed, the XML file might be corrupt + Translation options + Translator + Upload translation XML + + + Content + Content Templates + Media + Cache Browser + Recycle Bin + Created packages + Data Types + Dictionary + Installed packages + Install skin + Install starter kit + Languages + Install local package + Macros + Media Types + Members + Member Groups + Member Roles + Member Types + Document Types + Relation Types + Packages + Packages + Partial Views + Partial View Macro Files + Install from repository + Install Runway + Runway modules + Scripting Files + Scripts + Stylesheets + Templates + Log Viewer + Users + Settings + Templating + Third Party + + + New update ready + %0% is ready, click here for download + No connection to server + Error checking for update. Please review trace-stack for further information + + + Access + Based on the assigned groups and start nodes, the user has access to the following nodes + Assign access + Administrator + Category field + User created + Change Your Password + Change photo + New password + hasn't been locked out + The password hasn't been changed + Confirm new password + You can change your password for accessing the Umbraco Back Office by filling out the form below and click the 'Change Password' button + Content Channel + Create another user + Create new users to give them access to Umbraco. When a new user is created a password will be generated that you can share with the user. + Description field + Disable User + Document Type + Editor + Excerpt field + Failed login attempts + Go to user profile + Add groups to assign access and permissions + Invite another user + Invite new users to give them access to Umbraco. An invite email will be sent to the user with information on how to log in to Umbraco. Invites last for 72 hours. + Language + Set the language you will see in menus and dialogs + Last lockout date + Last login + Password last changed + Username + Media start node + Limit the media library to a specific start node + Media start nodes + Limit the media library to specific start nodes + Sections + Disable Umbraco Access + has not logged in yet + Old password + Password + Reset password + Your password has been changed! + Please confirm the new password + Enter your new password + Your new password cannot be blank! + Current password + Invalid current password + There was a difference between the new password and the confirmed password. Please try again! + The confirmed password doesn't match the new password! + Replace child node permissions + You are currently modifying permissions for the pages: + Select pages to modify their permissions + Remove photo + Default permissions + Granular permissions + Set permissions for specific nodes + Profile + Search all children + Add sections to give users access + Select user groups + No start node selected + No start nodes selected + Content start node + Limit the content tree to a specific start node + Content start nodes + Limit the content tree to specific start nodes + User last updated + has been created + The new user has successfully been created. To log in to Umbraco use the password below. + User management + Name + User permissions + User group + has been invited + An invitation has been sent to the new user with details about how to log in to Umbraco. + Hello there and welcome to Umbraco! In just 1 minute you’ll be good to go, we just need you to setup a password and add a picture for your avatar. + Welcome to Umbraco! Unfortunately your invite has expired. Please contact your administrator and ask them to resend it. + Uploading a photo of yourself will make it easy for other users to recognize you. Click the circle above to upload your photo. + Writer + Change + Your profile + Your recent history + Session expires in + Invite user + Create user + Send invite + Back to users + Umbraco: Invitation + + + + + + + + + + + +
+ + + + + +
+ +
+ +
+
+ + + + + + +
+
+
+ + + + +
+ + + + +
+

+ Hi %0%, +

+

+ You have been invited by %1% to the Umbraco Back Office. +

+

+ Message from %1%: +
+ %2% +

+ + + + + + +
+ + + + + + +
+ + Click this link to accept the invite + +
+
+

If you cannot click on the link, copy and paste this URL into your browser window:

+ + + + +
+ + %3% + +
+

+
+
+


+
+
+ + ]]>
+ Invite + Resending invitation... + Delete User + Are you sure you wish to delete this user account? + All + Active + Disabled + Locked out + Invited + Inactive + Name (A-Z) + Name (Z-A) + Newest + Oldest + Last login + No user groups have been added + + + Validation + Validate as an email address + Validate as a number + Validate as a URL + ...or enter a custom validation + Field is mandatory + Enter a custom validation error message (optional) + Enter a regular expression + Enter a custom validation error message (optional) + You need to add at least + You can only have + items + items selected + Invalid date + Not a number + Invalid email + Value cannot be null + Value cannot be empty + Value is invalid, it does not match the correct pattern + Custom validation + %1% more.]]> + %1% too many.]]> + + + + Value is set to the recommended value: '%0%'. + Value was set to '%1%' for XPath '%2%' in configuration file '%3%'. + Expected value '%1%' for '%2%' in configuration file '%3%', but found '%0%'. + Found unexpected value '%0%' for '%2%' in configuration file '%3%'. + + Custom errors are set to '%0%'. + Custom errors are currently set to '%0%'. It is recommended to set this to '%1%' before go live. + Custom errors successfully set to '%0%'. + MacroErrors are set to '%0%'. + MacroErrors are set to '%0%' which will prevent some or all pages in your site from loading completely if there are any errors in macros. Rectifying this will set the value to '%1%'. + MacroErrors are now set to '%0%'. + + Try Skip IIS Custom Errors is set to '%0%' and you're using IIS version '%1%'. + Try Skip IIS Custom Errors is currently '%0%'. It is recommended to set this to '%1%' for your IIS version (%2%). + Try Skip IIS Custom Errors successfully set to '%0%'. + + File does not exist: '%0%'. + '%0%' in config file '%1%'.]]> + There was an error, check log for full error: %0%. + Database - The database schema is correct for this version of Umbraco + %0% problems were detected with your database schema (Check the log for details) + Some errors were detected while validating the database schema against the current version of Umbraco. + Your website's certificate is valid. + Certificate validation error: '%0%' + Your website's SSL certificate has expired. + Your website's SSL certificate is expiring in %0% days. + Error pinging the URL %0% - '%1%' + You are currently %0% viewing the site using the HTTPS scheme. + The appSetting 'Umbraco.Core.UseHttps' is set to 'false' in your web.config file. Once you access this site using the HTTPS scheme, that should be set to 'true'. + The appSetting 'Umbraco.Core.UseHttps' is set to '%0%' in your web.config file, your cookies are %1% marked as secure. + Could not update the 'Umbraco.Core.UseHttps' setting in your web.config file. Error: %0% + + Enable HTTPS + Sets umbracoSSL setting to true in the appSettings of the web.config file. + The appSetting 'Umbraco.Core.UseHttps' is now set to 'true' in your web.config file, your cookies will be marked as secure. + Fix + Cannot fix a check with a value comparison type of 'ShouldNotEqual'. + Cannot fix a check with a value comparison type of 'ShouldEqual' with a provided value. + Value to fix check not provided. + Debug compilation mode is disabled. + Debug compilation mode is currently enabled. It is recommended to disable this setting before go live. + Debug compilation mode successfully disabled. + Trace mode is disabled. + Trace mode is currently enabled. It is recommended to disable this setting before go live. + Trace mode successfully disabled. + All folders have the correct permissions set. + + %0%.]]> + %0%. If they aren't being written to no action need be taken.]]> + All files have the correct permissions set. + + %0%.]]> + %0%. If they aren't being written to no action need be taken.]]> + X-Frame-Options used to control whether a site can be IFRAMEd by another was found.]]> + X-Frame-Options used to control whether a site can be IFRAMEd by another was not found.]]> + Set Header in Config + Adds a value to the httpProtocol/customHeaders section of web.config to prevent the site being IFRAMEd by other websites. + A setting to create a header preventing IFRAMEing of the site by other websites has been added to your web.config file. + Could not update web.config file. Error: %0% + X-Content-Type-Options used to protect against MIME sniffing vulnerabilities was found.]]> + X-Content-Type-Options used to protect against MIME sniffing vulnerabilities was not found.]]> + Adds a value to the httpProtocol/customHeaders section of web.config to protect against MIME sniffing vulnerabilities. + A setting to create a header protecting against MIME sniffing vulnerabilities has been added to your web.config file. + Strict-Transport-Security, also known as the HSTS-header, was found.]]> + Strict-Transport-Security was not found.]]> + Adds the header 'Strict-Transport-Security' with the value 'max-age=10886400' to the httpProtocol/customHeaders section of web.config. Use this fix only if you will have your domains running with https for the next 18 weeks (minimum). + The HSTS header has been added to your web.config file. + X-XSS-Protection was found.]]> + X-XSS-Protection was not found.]]> + Adds the header 'X-XSS-Protection' with the value '1; mode=block' to the httpProtocol/customHeaders section of web.config. + The X-XSS-Protection header has been added to your web.config file. + + %0%.]]> + No headers revealing information about the website technology were found. + In the Web.config file, system.net/mailsettings could not be found. + In the Web.config file system.net/mailsettings section, the host is not configured. + SMTP settings are configured correctly and the service is operating as expected. + The SMTP server configured with host '%0%' and port '%1%' could not be reached. Please check to ensure the SMTP settings in the Web.config file system.net/mailsettings are correct. + %0%.]]> + %0%.]]> +

Results of the scheduled Umbraco Health Checks run on %0% at %1% are as follows:

%2%]]>
+ Umbraco Health Check Status: %0% + Check All Groups + Check group + + The health checker evaluates various areas of your site for best practice settings, configuration, potential problems, etc. You can easily fix problems by pressing a button. + You can add your own health checks, have a look at the documentation for more information about custom health checks.

+ ]]> +
+ + + Disable URL tracker + Enable URL tracker + Culture + Original URL + Redirected To + Redirect Url Management + The following URLs redirect to this content item: + No redirects have been made + When a published page gets renamed or moved a redirect will automatically be made to the new page. + Are you sure you want to remove the redirect from '%0%' to '%1%'? + Redirect URL removed. + Error removing redirect URL. + This will remove the redirect + Are you sure you want to disable the URL tracker? + URL tracker has now been disabled. + Error disabling the URL tracker, more information can be found in your log file. + URL tracker has now been enabled. + Error enabling the URL tracker, more information can be found in your log file. + + + No Dictionary items to choose from + + + %0% characters left.]]> + %1% too many.]]> + + + Trashed content with Id: {0} related to original parent content with Id: {1} + Trashed media with Id: {0} related to original parent media item with Id: {1} + Cannot automatically restore this item + There is no location where this item can be automatically restored. You can move the item manually using the tree below. + was restored under + + + Direction + Parent to child + Bidirectional + Parent + Child + Count + Relations + Created + Comment + Name + No relations for this relation type. + Relation Type + Relations + + + Getting Started + Redirect URL Management + Content + Welcome + Examine Management + Published Status + Models Builder + Health Check + Profiling + Getting Started + Install Umbraco Forms + + + Go back + Active layout: + Jump to + group + passed + warning + failed + suggestion + Check passed + Check failed + Open backoffice search + Open/Close backoffice help + Open/Close your profile options + Open context menu for + Current language + Switch language to + Create new folder + Partial View + Partial View Macro + Member + Data type + Search the redirect dashboard + Search the user group section + Search the users section + Create item + Create + Edit + Name + + + References + This Data Type has no references. + Used in Document Types + No references to Document Types. + Used in Media Types + No references to Media Types. + Used in Member Types + No references to Member Types. + Used by + + + Log Levels + Saved Searches + Total Items + Timestamp + Level + Machine + Message + Exception + Properties + Search With Google + Search this message with Google + Search With Bing + Search this message with Bing + Search Our Umbraco + Search this message on Our Umbraco forums and docs + Search Our Umbraco with Google + Search Our Umbraco forums using Google + Search Umbraco Source + Search within Umbraco source code on Github + Search Umbraco Issues + Search Umbraco Issues on Github + Delete this search + Find Logs with Request ID + Find Logs with Namespace + Find Logs with Machine Name + Open + + + Copy %0% + %0% from %1% + Remove all items + + + Open Property Actions + + + Wait + Refresh status + Memory Cache + + + + Reload + Database Cache + + Rebuilding can be expensive. + Use it when reloading is not enough, and you think that the database cache has not been + properly generated—which would indicate some critical Umbraco issue. + ]]> + + Rebuild + Internals + + not need to use it. + ]]> + + Collect + Published Cache Status + Caches + + + Performance profiling + + + Umbraco currently runs in debug mode. This means you can use the built-in performance profiler to assess the performance when rendering pages. +

+

+ If you want to activate the profiler for a specific page rendering, simply add umbDebug=true to the querystring when requesting the page. +

+

+ If you want the profiler to be activated by default for all page renderings, you can use the toggle below. + It will set a cookie in your browser, which then activates the profiler automatically. + In other words, the profiler will only be active by default in your browser - not everyone else's. +

+ ]]> +
+ Activate the profiler by default + Friendly reminder + + + You should never let a production site run in debug mode. Debug mode is turned off by setting debug="false" on the <compilation /> element in web.config. +

+ ]]> +
+ + + Umbraco currently does not run in debug mode, so you can't use the built-in profiler. This is how it should be for a production site. +

+

+ Debug mode is turned on by setting debug="true" on the <compilation /> element in web.config. +

+ ]]> +
+ + + Hours of Umbraco training videos are only a click away + + Want to master Umbraco? Spend a couple of minutes learning some best practices by watching one of these videos about using Umbraco. And visit umbraco.tv for even more Umbraco videos

+ ]]> +
+ To get you started + + + Start here + This section contains the building blocks for your Umbraco site. Follow the below links to find out more about working with the items in the Settings section + Find out more + + in the Documentation section of Our Umbraco + ]]> + + + Community Forum + ]]> + + + tutorial videos (some are free, some require a subscription) + ]]> + + + productivity boosting tools and commercial support + ]]> + + + training and certification opportunities + ]]> + + + + Welcome to The Friendly CMS + Thank you for choosing Umbraco - we think this could be the beginning of something beautiful. While it may feel overwhelming at first, we've done a lot to make the learning curve as smooth and fast as possible. + + + Umbraco Forms + Create forms using an intuitive drag and drop interface. From simple contact forms that sends e-mails to advanced questionaires that integrate with CRM systems. Your clients will love it! + +
From e85640c48338a2e407b181930b940ef53ab3fb39 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Thu, 16 Jan 2020 15:15:04 +0100 Subject: [PATCH 162/202] V8: Set tab focus on newly created nested content items (#6914) * Set tab focus on newly created nested content items * Fix bad merge * Undo unintentional change --- .../nestedcontent/nestedcontent.propertyeditor.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.propertyeditor.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.propertyeditor.html index 47293e433b..91afbf3ba9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.propertyeditor.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.propertyeditor.html @@ -8,7 +8,7 @@
-
+
From dbe088eedb89b18a3813af1ae523c01e6e93b75d Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Thu, 16 Jan 2020 15:36:54 +0100 Subject: [PATCH 163/202] V8: Add infinite editing to datatype references tab (#6907) * Add infinite editing to datatype references tab * Use correct save button label in infinite editing mode --- .../src/common/services/editor.service.js | 18 ++++++ .../views/datatype.info.controller.js | 56 ++++++++++++++++++- .../views/datatypes/views/datatype.info.html | 6 +- .../src/views/membertypes/edit.controller.js | 34 ++++++++--- .../src/views/membertypes/edit.html | 11 +++- 5 files changed, 112 insertions(+), 13 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js b/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js index 1d80d3a3ed..284a7db4d8 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js @@ -640,6 +640,23 @@ When building a custom infinite editor view you can use the same components as a editor.view = "views/mediatypes/edit.html"; open(editor); } + + /** + * @ngdoc method + * @name umbraco.services.editorService#memberTypeEditor + * @methodOf umbraco.services.editorService + * + * @description + * Opens the member type editor in infinite editing, the submit callback returns the saved member type + * @param {Object} editor rendering options + * @param {Callback} editor.submit Submits the editor + * @param {Callback} editor.close Closes the editor + * @returns {Object} editor object + */ + function memberTypeEditor(editor) { + editor.view = "views/membertypes/edit.html"; + open(editor); + } /** * @ngdoc method @@ -1011,6 +1028,7 @@ When building a custom infinite editor view you can use the same components as a iconPicker: iconPicker, documentTypeEditor: documentTypeEditor, mediaTypeEditor: mediaTypeEditor, + memberTypeEditor: memberTypeEditor, queryBuilder: queryBuilder, treePicker: treePicker, nodePermissions: nodePermissions, diff --git a/src/Umbraco.Web.UI.Client/src/views/datatypes/views/datatype.info.controller.js b/src/Umbraco.Web.UI.Client/src/views/datatypes/views/datatype.info.controller.js index d4aff871a2..be8ddba592 100644 --- a/src/Umbraco.Web.UI.Client/src/views/datatypes/views/datatype.info.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/datatypes/views/datatype.info.controller.js @@ -6,7 +6,7 @@ * @description * The controller for the info view of the datatype editor */ -function DataTypeInfoController($scope, $routeParams, dataTypeResource, eventsService, $timeout) { +function DataTypeInfoController($scope, $routeParams, dataTypeResource, eventsService, $timeout, editorService) { var vm = this; var evts = []; @@ -17,6 +17,9 @@ function DataTypeInfoController($scope, $routeParams, dataTypeResource, eventsSe vm.view = {}; vm.view.loading = true; + vm.openDocumentType = openDocumentType; + vm.openMediaType = openMediaType; + vm.openMemberType = openMemberType; /** Loads in the data type references one time */ function loadRelations() { @@ -31,6 +34,57 @@ function DataTypeInfoController($scope, $routeParams, dataTypeResource, eventsSe } } + function openDocumentType(id, event) { + open(id, event, "documentType"); + } + + function openMediaType(id, event) { + open(id, event, "mediaType"); + } + + function openMemberType(id, event) { + open(id, event, "memberType"); + } + + function open(id, event, type) { + // targeting a new tab/window? + if (event.ctrlKey || + event.shiftKey || + event.metaKey || // apple + (event.button && event.button === 1) // middle click, >IE9 + everyone else + ) { + // yes, let the link open itself + return; + } + event.stopPropagation(); + event.preventDefault(); + + const editor = { + id: id, + submit: function (model) { + editorService.close(); + vm.view.loading = true; + referencesLoaded = false; + loadRelations(); + }, + close: function () { + editorService.close(); + } + }; + + switch (type) { + case "documentType": + editorService.documentTypeEditor(editor); + break; + case "mediaType": + editorService.mediaTypeEditor(editor); + break; + case "memberType": + editorService.memberTypeEditor(editor); + break; + } + } + // load data type references when the references tab is activated evts.push(eventsService.on("app.tabChange", function (event, args) { $timeout(function () { diff --git a/src/Umbraco.Web.UI.Client/src/views/datatypes/views/datatype.info.html b/src/Umbraco.Web.UI.Client/src/views/datatypes/views/datatype.info.html index 16b2d4b263..0af1634b29 100644 --- a/src/Umbraco.Web.UI.Client/src/views/datatypes/views/datatype.info.html +++ b/src/Umbraco.Web.UI.Client/src/views/datatypes/views/datatype.info.html @@ -43,7 +43,7 @@
{{::reference.name}}
{{::reference.alias}}
{{::reference.properties | umbCmsJoinArray:', ':'name'}}
- +
@@ -73,7 +73,7 @@
{{::reference.name}}
{{::reference.alias}}
{{::reference.properties | umbCmsJoinArray:', ':'name'}}
- +
@@ -104,7 +104,7 @@
{{::reference.name}}
{{::reference.alias}}
{{::reference.properties | umbCmsJoinArray:', ':'name'}}
- +
diff --git a/src/Umbraco.Web.UI.Client/src/views/membertypes/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/membertypes/edit.controller.js index ab54a453b5..b2e515e187 100644 --- a/src/Umbraco.Web.UI.Client/src/views/membertypes/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/membertypes/edit.controller.js @@ -13,8 +13,13 @@ var evts = []; var vm = this; + var infiniteMode = $scope.model && $scope.model.infiniteMode; + var memberTypeId = infiniteMode ? $scope.model.id : $routeParams.id; + var create = infiniteMode ? $scope.model.create : $routeParams.create; vm.save = save; + vm.close = close; + vm.editorfor = "visuallyHiddenTexts_newMember"; vm.header = {}; vm.header.editorfor = "content_membergroup"; @@ -25,6 +30,7 @@ vm.page.loading = false; vm.page.saveButtonState = "init"; vm.labels = {}; + vm.saveButtonKey = infiniteMode ? "buttons_saveAndClose" : "buttons_save"; var labelKeys = [ "general_design", @@ -86,7 +92,7 @@ vm.page.defaultButton = { hotKey: "ctrl+s", hotKeyWhenHidden: true, - labelKey: "buttons_save", + labelKey: vm.saveButtonKey, letter: "S", type: "submit", handler: function () { vm.save(); } @@ -94,7 +100,7 @@ vm.page.subButtons = [{ hotKey: "ctrl+g", hotKeyWhenHidden: true, - labelKey: "buttons_saveAndGenerateModels", + labelKey: infiniteMode ? "buttons_generateModelsAndClose" : "buttons_saveAndGenerateModels", letter: "G", handler: function () { @@ -147,12 +153,12 @@ } }); - if ($routeParams.create) { + if (create) { vm.page.loading = true; //we are creating so get an empty data type item - memberTypeResource.getScaffold($routeParams.id) + memberTypeResource.getScaffold(memberTypeId) .then(function (dt) { init(dt); @@ -163,10 +169,12 @@ vm.page.loading = true; - memberTypeResource.getById($routeParams.id).then(function (dt) { + memberTypeResource.getById(memberTypeId).then(function (dt) { init(dt); - syncTreeNode(vm.contentType, dt.path, true); + if(!infiniteMode) { + syncTreeNode(vm.contentType, dt.path, true); + } vm.page.loading = false; }); @@ -219,10 +227,16 @@ } }).then(function (data) { //success - syncTreeNode(vm.contentType, data.path); + if(!infiniteMode) { + syncTreeNode(vm.contentType, data.path); + } vm.page.saveButtonState = "success"; + if(infiniteMode && $scope.model.submit) { + $scope.model.submit(); + } + deferred.resolve(data); }, function (err) { //error @@ -307,6 +321,12 @@ }); } + + function close() { + if(infiniteMode && $scope.model.close) { + $scope.model.close(); + } + } evts.push(eventsService.on("editors.groupsBuilder.changed", function(name, args) { angularHelper.getCurrentForm($scope).$setDirty(); diff --git a/src/Umbraco.Web.UI.Client/src/views/membertypes/edit.html b/src/Umbraco.Web.UI.Client/src/views/membertypes/edit.html index 07824bc7ec..c4c521c857 100644 --- a/src/Umbraco.Web.UI.Client/src/views/membertypes/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/membertypes/edit.html @@ -40,6 +40,14 @@ + + + + label-key="{{vm.saveButtonKey}}"> Date: Thu, 16 Jan 2020 16:11:08 +0100 Subject: [PATCH 164/202] Responsive Angular API documentation (#7030) --- src/Umbraco.Web.UI.Docs/umb-docs.css | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/Umbraco.Web.UI.Docs/umb-docs.css b/src/Umbraco.Web.UI.Docs/umb-docs.css index 931c22b255..0f2e3e7f74 100644 --- a/src/Umbraco.Web.UI.Docs/umb-docs.css +++ b/src/Umbraco.Web.UI.Docs/umb-docs.css @@ -7,6 +7,21 @@ body { } +.container, .navbar-static-top .container, .navbar-fixed-top .container, .navbar-fixed-bottom .container { + max-width: 1500px; + width: 95%; +} + +.span3 { + width: 220px; + width: calc(90% / 12 * 3); +} + +.span9 { + width: 700px; + width: calc(90% / 12 * 9); +} + .h1, .h2, .h3, .h4, .h5, .h6, h1, h2, h3, h4, h5, h6 { font-family: inherit; font-weight: 400; From 4c68f7e53a324aae9fb0dbc57de5f0ab967e4bde Mon Sep 17 00:00:00 2001 From: Jan Skovgaard Date: Thu, 16 Jan 2020 16:13:25 +0100 Subject: [PATCH 165/202] List view configuration - Sort order distance fix (#6905) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thanks, Jan. You're a 🌟 --- .../lib/bootstrap/less/type.less | 4 ++++ .../listview/orderDirection.prevalues.html | 12 +++++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/lib/bootstrap/less/type.less b/src/Umbraco.Web.UI.Client/lib/bootstrap/less/type.less index bf1167f950..3f93deaf56 100644 --- a/src/Umbraco.Web.UI.Client/lib/bootstrap/less/type.less +++ b/src/Umbraco.Web.UI.Client/lib/bootstrap/less/type.less @@ -132,6 +132,10 @@ ol.inline { display: inline-block; padding-left: 5px; padding-right: 5px; + + &.-no-padding-left{ + padding-left: 0; + } } } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/orderDirection.prevalues.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/orderDirection.prevalues.html index 83a905ccf7..7ec0895936 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/orderDirection.prevalues.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/orderDirection.prevalues.html @@ -1,10 +1,16 @@ 
- - +
    +
  • + +
  • +
  • + +
  • +
- Required + Required
From 4d5f34a63ddb00507bffbf409366834bdb59c627 Mon Sep 17 00:00:00 2001 From: Kieron Boswell Date: Thu, 16 Jan 2020 16:15:07 +0000 Subject: [PATCH 166/202] Spelling mistake on word Success in instructional comment. (#6966) --- .../Umbraco/PartialViewMacros/Templates/EditProfile.cshtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/EditProfile.cshtml b/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/EditProfile.cshtml index 7034404ca3..74ec033f25 100644 --- a/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/EditProfile.cshtml +++ b/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/EditProfile.cshtml @@ -23,7 +23,7 @@ { if (success) { - @* This message will show if RedirectOnSucces is set to false (default) *@ + @* This message will show if profileModel.RedirectUrl is not defined (default) *@

Profile updated

} From f5a5a9a2971fd87829f5a7561e8a0afc0f9d2003 Mon Sep 17 00:00:00 2001 From: Kieron Boswell Date: Thu, 16 Jan 2020 16:19:54 +0000 Subject: [PATCH 167/202] Spelling mistake in comments of partial snippet (#6975) --- .../Umbraco/PartialViewMacros/Templates/RegisterMember.cshtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/RegisterMember.cshtml b/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/RegisterMember.cshtml index 17ed95ea31..804e2307f0 100644 --- a/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/RegisterMember.cshtml +++ b/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/RegisterMember.cshtml @@ -45,7 +45,7 @@ @if (success) { - @* This message will show if RedirectOnSucces is set to false (default) *@ + @* This message will show if registerModel.RedirectUrl is not defined (default) *@

Registration succeeded.

} else From b272a3b791a80c566459198df8e11ab920b20130 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Thu, 16 Jan 2020 19:39:17 +0100 Subject: [PATCH 168/202] https://github.com/umbraco/Umbraco-CMS/issues/7466 - Fixes issue with external ModelsBuilder and Dll or LiveDll mode. The issue is because the Mode is determined in the contructor, and not lazy. --- .../Configuration/ModelsBuilderConfig.cs | 90 ++++++++++++------- 1 file changed, 56 insertions(+), 34 deletions(-) diff --git a/src/Umbraco.ModelsBuilder.Embedded/Configuration/ModelsBuilderConfig.cs b/src/Umbraco.ModelsBuilder.Embedded/Configuration/ModelsBuilderConfig.cs index c6bccdcf87..b5c66b649c 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/Configuration/ModelsBuilderConfig.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/Configuration/ModelsBuilderConfig.cs @@ -12,6 +12,9 @@ namespace Umbraco.ModelsBuilder.Embedded.Configuration ///
public class ModelsBuilderConfig : IModelsBuilderConfig { + private const string prefix = "Umbraco.ModelsBuilder."; + private ModelsMode? _modelsMode; + private bool? _flagOutOfDateModels; public const string DefaultModelsNamespace = "Umbraco.Web.PublishedModels"; public const string DefaultModelsDirectory = "~/App_Data/Models"; @@ -20,8 +23,6 @@ namespace Umbraco.ModelsBuilder.Embedded.Configuration ///
public ModelsBuilderConfig() { - const string prefix = "Umbraco.ModelsBuilder."; - // giant kill switch, default: false // must be explicitely set to true for anything else to happen Enable = ConfigurationManager.AppSettings[prefix + "Enable"] == "true"; @@ -34,36 +35,11 @@ namespace Umbraco.ModelsBuilder.Embedded.Configuration // stop here, everything is false if (!Enable) return; - // mode - var modelsMode = ConfigurationManager.AppSettings[prefix + "ModelsMode"]; - if (!string.IsNullOrWhiteSpace(modelsMode)) - { - switch (modelsMode) - { - case nameof(ModelsMode.Nothing): - ModelsMode = ModelsMode.Nothing; - break; - case nameof(ModelsMode.PureLive): - ModelsMode = ModelsMode.PureLive; - break; - case nameof(ModelsMode.AppData): - ModelsMode = ModelsMode.AppData; - break; - case nameof(ModelsMode.LiveAppData): - ModelsMode = ModelsMode.LiveAppData; - break; - default: - throw new ConfigurationErrorsException($"ModelsMode \"{modelsMode}\" is not a valid mode." - + " Note that modes are case-sensitive. Possible values are: " + string.Join(", ", Enum.GetNames(typeof(ModelsMode)))); - } - } - // default: false AcceptUnsafeModelsDirectory = ConfigurationManager.AppSettings[prefix + "AcceptUnsafeModelsDirectory"].InvariantEquals("true"); // default: true EnableFactory = !ConfigurationManager.AppSettings[prefix + "EnableFactory"].InvariantEquals("false"); - FlagOutOfDateModels = !ConfigurationManager.AppSettings[prefix + "FlagOutOfDateModels"].InvariantEquals("false"); // default: initialized above with DefaultModelsNamespace const var value = ConfigurationManager.AppSettings[prefix + "ModelsNamespace"]; @@ -92,9 +68,6 @@ namespace Umbraco.ModelsBuilder.Embedded.Configuration DebugLevel = debugLevel; } - // not flagging if not generating, or live (incl. pure) - if (ModelsMode == ModelsMode.Nothing || ModelsMode.IsLive()) - FlagOutOfDateModels = false; } /// @@ -111,11 +84,11 @@ namespace Umbraco.ModelsBuilder.Embedded.Configuration int debugLevel = 0) { Enable = enable; - ModelsMode = modelsMode; + _modelsMode = modelsMode; ModelsNamespace = string.IsNullOrWhiteSpace(modelsNamespace) ? DefaultModelsNamespace : modelsNamespace; EnableFactory = enableFactory; - FlagOutOfDateModels = flagOutOfDateModels; + _flagOutOfDateModels = flagOutOfDateModels; ModelsDirectory = string.IsNullOrWhiteSpace(modelsDirectory) ? DefaultModelsDirectory : modelsDirectory; AcceptUnsafeModelsDirectory = acceptUnsafeModelsDirectory; DebugLevel = debugLevel; @@ -164,7 +137,39 @@ namespace Umbraco.ModelsBuilder.Embedded.Configuration /// /// Gets the models mode. /// - public ModelsMode ModelsMode { get; } + public ModelsMode ModelsMode + { + get + { + if (!_modelsMode.HasValue) + { + // mode + var modelsMode = ConfigurationManager.AppSettings[prefix + "ModelsMode"]; + if (!string.IsNullOrWhiteSpace(modelsMode)) + { + switch (modelsMode) + { + case nameof(ModelsMode.Nothing): + _modelsMode = ModelsMode.Nothing; + break; + case nameof(ModelsMode.PureLive): + _modelsMode = ModelsMode.PureLive; + break; + case nameof(ModelsMode.AppData): + _modelsMode = ModelsMode.AppData; + break; + case nameof(ModelsMode.LiveAppData): + _modelsMode = ModelsMode.LiveAppData; + break; + default: + throw new ConfigurationErrorsException($"ModelsMode \"{modelsMode}\" is not a valid mode." + + " Note that modes are case-sensitive. Possible values are: " + string.Join(", ", Enum.GetNames(typeof(ModelsMode)))); + } + } + } + return _modelsMode.Value; + } + } /// /// Gets a value indicating whether system.web/compilation/@debug is true. @@ -196,7 +201,24 @@ namespace Umbraco.ModelsBuilder.Embedded.Configuration /// Models become out-of-date when data types or content types are updated. When this /// setting is activated the ~/App_Data/Models/ood.txt file is then created. When models are /// generated through the dashboard, the files is cleared. Default value is false. - public bool FlagOutOfDateModels { get; } + public bool FlagOutOfDateModels + { + get + { + if (!_flagOutOfDateModels.HasValue) + { + var flagOutOfDateModels = !ConfigurationManager.AppSettings[prefix + "FlagOutOfDateModels"].InvariantEquals("false"); + + if (ModelsMode == ModelsMode.Nothing || ModelsMode.IsLive()) + { + flagOutOfDateModels = false; + } + + _flagOutOfDateModels = flagOutOfDateModels; + } + return _flagOutOfDateModels.Value; + } + } /// /// Gets the models directory. From f3d9a01800765b26478a2495c02c9c072571f7af Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Thu, 16 Jan 2020 23:00:28 +0100 Subject: [PATCH 169/202] Hide the "Add" button for Nested Content in "single mode" (#7327) --- .../nestedcontent/nestedcontent.propertyeditor.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.propertyeditor.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.propertyeditor.html index 91afbf3ba9..da6e466b50 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.propertyeditor.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.propertyeditor.html @@ -40,7 +40,7 @@ - public class ModelsBuilderConfig : IModelsBuilderConfig { - private const string prefix = "Umbraco.ModelsBuilder."; - private ModelsMode? _modelsMode; - private bool? _flagOutOfDateModels; + private const string Prefix = "Umbraco.ModelsBuilder."; + private object _modelsModelLock; + private bool _modelsModelConfigured; + private ModelsMode _modelsMode; + private object _flagOutOfDateModelsLock; + private bool _flagOutOfDateModelsConfigured; + private bool _flagOutOfDateModels; public const string DefaultModelsNamespace = "Umbraco.Web.PublishedModels"; public const string DefaultModelsDirectory = "~/App_Data/Models"; @@ -25,7 +30,7 @@ namespace Umbraco.ModelsBuilder.Embedded.Configuration { // giant kill switch, default: false // must be explicitely set to true for anything else to happen - Enable = ConfigurationManager.AppSettings[prefix + "Enable"] == "true"; + Enable = ConfigurationManager.AppSettings[Prefix + "Enable"] == "true"; // ensure defaults are initialized for tests ModelsNamespace = DefaultModelsNamespace; @@ -36,18 +41,18 @@ namespace Umbraco.ModelsBuilder.Embedded.Configuration if (!Enable) return; // default: false - AcceptUnsafeModelsDirectory = ConfigurationManager.AppSettings[prefix + "AcceptUnsafeModelsDirectory"].InvariantEquals("true"); + AcceptUnsafeModelsDirectory = ConfigurationManager.AppSettings[Prefix + "AcceptUnsafeModelsDirectory"].InvariantEquals("true"); // default: true - EnableFactory = !ConfigurationManager.AppSettings[prefix + "EnableFactory"].InvariantEquals("false"); + EnableFactory = !ConfigurationManager.AppSettings[Prefix + "EnableFactory"].InvariantEquals("false"); // default: initialized above with DefaultModelsNamespace const - var value = ConfigurationManager.AppSettings[prefix + "ModelsNamespace"]; + var value = ConfigurationManager.AppSettings[Prefix + "ModelsNamespace"]; if (!string.IsNullOrWhiteSpace(value)) ModelsNamespace = value; // default: initialized above with DefaultModelsDirectory const - value = ConfigurationManager.AppSettings[prefix + "ModelsDirectory"]; + value = ConfigurationManager.AppSettings[Prefix + "ModelsDirectory"]; if (!string.IsNullOrWhiteSpace(value)) { var root = IOHelper.MapPath("~/"); @@ -59,11 +64,10 @@ namespace Umbraco.ModelsBuilder.Embedded.Configuration } // default: 0 - value = ConfigurationManager.AppSettings[prefix + "DebugLevel"]; + value = ConfigurationManager.AppSettings[Prefix + "DebugLevel"]; if (!string.IsNullOrWhiteSpace(value)) { - int debugLevel; - if (!int.TryParse(value, out debugLevel)) + if (!int.TryParse(value, out var debugLevel)) throw new ConfigurationErrorsException($"Invalid debug level \"{value}\"."); DebugLevel = debugLevel; } @@ -137,39 +141,26 @@ namespace Umbraco.ModelsBuilder.Embedded.Configuration /// /// Gets the models mode. /// - public ModelsMode ModelsMode - { - get + public ModelsMode ModelsMode => + LazyInitializer.EnsureInitialized(ref _modelsMode, ref _modelsModelConfigured, ref _modelsModelLock, () => { - if (!_modelsMode.HasValue) + // mode + var modelsMode = ConfigurationManager.AppSettings[Prefix + "ModelsMode"]; + if (string.IsNullOrWhiteSpace(modelsMode)) return ModelsMode.Nothing; //default + switch (modelsMode) { - // mode - var modelsMode = ConfigurationManager.AppSettings[prefix + "ModelsMode"]; - if (!string.IsNullOrWhiteSpace(modelsMode)) - { - switch (modelsMode) - { - case nameof(ModelsMode.Nothing): - _modelsMode = ModelsMode.Nothing; - break; - case nameof(ModelsMode.PureLive): - _modelsMode = ModelsMode.PureLive; - break; - case nameof(ModelsMode.AppData): - _modelsMode = ModelsMode.AppData; - break; - case nameof(ModelsMode.LiveAppData): - _modelsMode = ModelsMode.LiveAppData; - break; - default: - throw new ConfigurationErrorsException($"ModelsMode \"{modelsMode}\" is not a valid mode." - + " Note that modes are case-sensitive. Possible values are: " + string.Join(", ", Enum.GetNames(typeof(ModelsMode)))); - } - } + case nameof(ModelsMode.Nothing): + return ModelsMode.Nothing; + case nameof(ModelsMode.PureLive): + return ModelsMode.PureLive; + case nameof(ModelsMode.AppData): + return ModelsMode.AppData; + case nameof(ModelsMode.LiveAppData): + return ModelsMode.LiveAppData; + default: + throw new ConfigurationErrorsException($"ModelsMode \"{modelsMode}\" is not a valid mode." + " Note that modes are case-sensitive. Possible values are: " + string.Join(", ", Enum.GetNames(typeof(ModelsMode)))); } - return _modelsMode.Value; - } - } + }); /// /// Gets a value indicating whether system.web/compilation/@debug is true. @@ -202,23 +193,16 @@ namespace Umbraco.ModelsBuilder.Embedded.Configuration /// setting is activated the ~/App_Data/Models/ood.txt file is then created. When models are /// generated through the dashboard, the files is cleared. Default value is false. public bool FlagOutOfDateModels - { - get + => LazyInitializer.EnsureInitialized(ref _flagOutOfDateModels, ref _flagOutOfDateModelsConfigured, ref _flagOutOfDateModelsLock, () => { - if (!_flagOutOfDateModels.HasValue) + var flagOutOfDateModels = !ConfigurationManager.AppSettings[Prefix + "FlagOutOfDateModels"].InvariantEquals("false"); + if (ModelsMode == ModelsMode.Nothing || ModelsMode.IsLive()) { - var flagOutOfDateModels = !ConfigurationManager.AppSettings[prefix + "FlagOutOfDateModels"].InvariantEquals("false"); - - if (ModelsMode == ModelsMode.Nothing || ModelsMode.IsLive()) - { - flagOutOfDateModels = false; - } - - _flagOutOfDateModels = flagOutOfDateModels; + flagOutOfDateModels = false; } - return _flagOutOfDateModels.Value; - } - } + + return flagOutOfDateModels; + }); /// /// Gets the models directory. From 5ddc4b8efb73b15b1ab99286fa27cb042fc39057 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Fri, 17 Jan 2020 11:51:31 +0100 Subject: [PATCH 171/202] Bump version to 8.5.2 --- src/SolutionInfo.cs | 4 ++-- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/SolutionInfo.cs b/src/SolutionInfo.cs index 3ffc767f06..9dba3adef1 100644 --- a/src/SolutionInfo.cs +++ b/src/SolutionInfo.cs @@ -18,5 +18,5 @@ using System.Resources; [assembly: AssemblyVersion("8.0.0")] // these are FYI and changed automatically -[assembly: AssemblyFileVersion("8.5.1")] -[assembly: AssemblyInformationalVersion("8.5.1")] +[assembly: AssemblyFileVersion("8.5.2")] +[assembly: AssemblyInformationalVersion("8.5.2")] diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 87712d497e..0c80ccb70b 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -345,9 +345,9 @@ False True - 8510 + 8520 / - http://localhost:8510 + http://localhost:8520 False False From 6637bf520c62e2eab5717b74e68c9206595c4896 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Fri, 17 Jan 2020 13:22:13 +0100 Subject: [PATCH 172/202] https://github.com/umbraco/Umbraco-CMS/issues/7469 - Fixed Ambiguous extension method issue by renaming our extension method from Value to GetValue --- .../PublishedElementExtensions.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.ModelsBuilder.Embedded/PublishedElementExtensions.cs b/src/Umbraco.ModelsBuilder.Embedded/PublishedElementExtensions.cs index 29429ba74f..33b1803169 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/PublishedElementExtensions.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/PublishedElementExtensions.cs @@ -17,14 +17,13 @@ namespace Umbraco.Web /// /// Gets the value of a property. /// - public static TValue Value(this TModel model, Expression> property, string culture = null, string segment = null, Fallback fallback = default, TValue defaultValue = default) + public static TValue GetValue(this TModel model, Expression> property, string culture = null, string segment = null, Fallback fallback = default, TValue defaultValue = default) where TModel : IPublishedElement { var alias = GetAlias(model, property); return model.Value(alias, culture, segment, fallback, defaultValue); } - // fixme that one should be public so ppl can use it private static string GetAlias(TModel model, Expression> property) { if (property.NodeType != ExpressionType.Lambda) @@ -45,7 +44,7 @@ namespace Umbraco.Web var attribute = member.GetCustomAttribute(); if (attribute == null) throw new InvalidOperationException("Property is not marked with ImplementPropertyType attribute."); - + return attribute.Alias; } } From baef282b108599166fcf470bbc07ec5923519270 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Fri, 17 Jan 2020 13:53:03 +0100 Subject: [PATCH 173/202] https://github.com/umbraco/Umbraco-CMS/issues/7469 - Fixed Ambiguous extension method issue by renaming our extension method from Value to GetValue --- .../PublishedElementExtensions.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.ModelsBuilder.Embedded/PublishedElementExtensions.cs b/src/Umbraco.ModelsBuilder.Embedded/PublishedElementExtensions.cs index 33b1803169..2c22caf87d 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/PublishedElementExtensions.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/PublishedElementExtensions.cs @@ -17,13 +17,14 @@ namespace Umbraco.Web /// /// Gets the value of a property. /// - public static TValue GetValue(this TModel model, Expression> property, string culture = null, string segment = null, Fallback fallback = default, TValue defaultValue = default) + public static TValue ValueByExpression(this TModel model, Expression> property, string culture = null, string segment = null, Fallback fallback = default, TValue defaultValue = default) where TModel : IPublishedElement { var alias = GetAlias(model, property); return model.Value(alias, culture, segment, fallback, defaultValue); } + //This cannot be public due to ambiguous issue with external ModelsBuilder if we do not rename. private static string GetAlias(TModel model, Expression> property) { if (property.NodeType != ExpressionType.Lambda) From bcf4c97270792285d2717eb0faaa7452bfb2a5a3 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Fri, 17 Jan 2020 13:58:18 +0100 Subject: [PATCH 174/202] V8: Support culture variant URLs in multi URL picker (#7130) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thanks, Kenn - great work as usual 👍 --- .../src/common/resources/entity.resource.js | 27 +++++++++++++-- .../multiurlpicker.controller.js | 10 ++++++ src/Umbraco.Web/Editors/EntityController.cs | 33 ++++++++++++++++++- 3 files changed, 67 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js index 9cf1181cfa..61d646afc0 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js @@ -127,6 +127,25 @@ function entityResource($q, $http, umbRequestHelper) { 'Failed to retrieve url for id:' + id); }, + getUrlByUdi: function (udi, culture) { + + if (!udi) { + return ""; + } + + if (!culture) { + culture = ""; + } + + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "entityApiBaseUrl", + "GetUrl", + [{ udi: udi }, {culture: culture }])), + 'Failed to retrieve url for UDI:' + udi); + }, + /** * @ngdoc method * @name umbraco.resources.entityResource#getById @@ -166,18 +185,22 @@ function entityResource($q, $http, umbRequestHelper) { }, - getUrlAndAnchors: function (id) { + getUrlAndAnchors: function (id, culture) { if (id === -1 || id === "-1") { return null; } + if (!culture) { + culture = ""; + } + return umbRequestHelper.resourcePromise( $http.get( umbRequestHelper.getApiUrl( "entityApiBaseUrl", "GetUrlAndAnchors", - [{ id: id }])), + [{ id: id }, {culture: culture }])), 'Failed to retrieve url and anchors data for id ' + id); }, diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/multiurlpicker/multiurlpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/multiurlpicker/multiurlpicker.controller.js index 951b593b8b..2e4313ec76 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/multiurlpicker/multiurlpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/multiurlpicker/multiurlpicker.controller.js @@ -144,6 +144,16 @@ function multiUrlPickerController($scope, angularHelper, localizationService, en if ($scope.model.validation && $scope.model.validation.mandatory && !$scope.model.config.minNumber) { $scope.model.config.minNumber = 1; } + + _.each($scope.model.value, function (item){ + // we must reload the "document" link URLs to match the current editor culture + if (item.udi.indexOf("/document/") > 0) { + item.url = null; + entityResource.getUrlByUdi(item.udi).then(function (data) { + item.url = data; + }); + } + }); } init(); diff --git a/src/Umbraco.Web/Editors/EntityController.cs b/src/Umbraco.Web/Editors/EntityController.cs index 0513017b70..ca5bec4fce 100644 --- a/src/Umbraco.Web/Editors/EntityController.cs +++ b/src/Umbraco.Web/Editors/EntityController.cs @@ -206,6 +206,35 @@ namespace Umbraco.Web.Editors throw new HttpResponseException(HttpStatusCode.NotFound); } + /// + /// Gets the url of an entity + /// + /// UDI of the entity to fetch URL for + /// The culture to fetch the URL for + /// The URL or path to the item + public HttpResponseMessage GetUrl(Udi udi, string culture = "*") + { + var intId = Services.EntityService.GetId(udi); + if (!intId.Success) + throw new HttpResponseException(HttpStatusCode.NotFound); + UmbracoEntityTypes entityType; + switch(udi.EntityType) + { + case Constants.UdiEntityType.Document: + entityType = UmbracoEntityTypes.Document; + break; + case Constants.UdiEntityType.Media: + entityType = UmbracoEntityTypes.Media; + break; + case Constants.UdiEntityType.Member: + entityType = UmbracoEntityTypes.Member; + break; + default: + throw new HttpResponseException(HttpStatusCode.NotFound); + } + return GetUrl(intId.Result, entityType, culture); + } + /// /// Gets the url of an entity /// @@ -303,7 +332,9 @@ namespace Umbraco.Web.Editors [HttpGet] public UrlAndAnchors GetUrlAndAnchors(int id, string culture = "*") { - var url = UmbracoContext.UrlProvider.GetUrl(id); + culture = culture ?? ClientCulture(); + + var url = UmbracoContext.UrlProvider.GetUrl(id, culture: culture); var anchorValues = Services.ContentService.GetAnchorValuesFromRTEs(id, culture); return new UrlAndAnchors(url, anchorValues); } From 05f7d24a9a4b8bfea675b70243c396408e7cfce5 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Fri, 17 Jan 2020 22:42:05 +0100 Subject: [PATCH 175/202] Add VariesBySegment and VariesByCultureAndSegment extension methods for ISimpleContentType --- src/Umbraco.Core/ContentVariationExtensions.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/Umbraco.Core/ContentVariationExtensions.cs b/src/Umbraco.Core/ContentVariationExtensions.cs index b3688b08b4..4a918ca70d 100644 --- a/src/Umbraco.Core/ContentVariationExtensions.cs +++ b/src/Umbraco.Core/ContentVariationExtensions.cs @@ -27,6 +27,24 @@ namespace Umbraco.Core /// public static bool VariesByCulture(this ISimpleContentType contentType) => contentType.Variations.VariesByCulture(); + /// + /// Determines whether the content type varies by segment. + /// + /// The content type. + /// + /// A value indicating whether the content type varies by segment. + /// + public static bool VariesBySegment(this ISimpleContentType contentType) => contentType.Variations.VariesBySegment(); + + /// + /// Determines whether the content type varies by culture and segment. + /// + /// The content type. + /// + /// A value indicating whether the content type varies by culture and segment. + /// + public static bool VariesByCultureAndSegment(this ISimpleContentType contentType) => contentType.Variations.VariesByCultureAndSegment(); + /// /// Determines whether the content type is invariant. /// From d9dfea8ef55de0d07d0159a4752a34b5cda557e8 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Fri, 17 Jan 2020 22:43:11 +0100 Subject: [PATCH 176/202] Add remarks and better summary on SetVariesBy extension methods --- src/Umbraco.Core/ContentVariationExtensions.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Core/ContentVariationExtensions.cs b/src/Umbraco.Core/ContentVariationExtensions.cs index 4a918ca70d..7cec5df27f 100644 --- a/src/Umbraco.Core/ContentVariationExtensions.cs +++ b/src/Umbraco.Core/ContentVariationExtensions.cs @@ -305,6 +305,9 @@ namespace Umbraco.Core /// The content type. /// The variation to set or remove. /// If set to true sets the variation; otherwise, removes the variation. + /// + /// This method does not support setting the variation to nothing. + /// public static void SetVariesBy(this IContentTypeBase contentType, ContentVariation variation, bool value = true) => contentType.Variations = contentType.Variations.SetVariesBy(variation, value); /// @@ -313,17 +316,23 @@ namespace Umbraco.Core /// The property type. /// The variation to set or remove. /// If set to true sets the variation; otherwise, removes the variation. + /// + /// This method does not support setting the variation to nothing. + /// public static void SetVariesBy(this PropertyType propertyType, ContentVariation variation, bool value = true) => propertyType.Variations = propertyType.Variations.SetVariesBy(variation, value); /// - /// Sets or removes the variation depending on the specified value. + /// Returns the variations with the variation set or removed depending on the specified value. /// /// The existing variations. /// The variation to set or remove. /// If set to true sets the variation; otherwise, removes the variation. /// - /// The variations with the specified variation set or removed. + /// The variations with the variation set or removed. /// + /// + /// This method does not support setting the variation to nothing. + /// public static ContentVariation SetVariesBy(this ContentVariation variations, ContentVariation variation, bool value = true) { return value From b3829bfff83b3c0df3aac476964c2aa18186be4c Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Fri, 17 Jan 2020 22:44:44 +0100 Subject: [PATCH 177/202] Reorder methods to keep overloads adjacent --- .../ContentVariationExtensions.cs | 360 +++++++++--------- 1 file changed, 180 insertions(+), 180 deletions(-) diff --git a/src/Umbraco.Core/ContentVariationExtensions.cs b/src/Umbraco.Core/ContentVariationExtensions.cs index 7cec5df27f..6430c75686 100644 --- a/src/Umbraco.Core/ContentVariationExtensions.cs +++ b/src/Umbraco.Core/ContentVariationExtensions.cs @@ -18,33 +18,6 @@ namespace Umbraco.Core /// public static bool VariesByNothing(this ISimpleContentType contentType) => contentType.Variations.VariesByNothing(); - /// - /// Determines whether the content type varies by culture. - /// - /// The content type. - /// - /// A value indicating whether the content type varies by culture. - /// - public static bool VariesByCulture(this ISimpleContentType contentType) => contentType.Variations.VariesByCulture(); - - /// - /// Determines whether the content type varies by segment. - /// - /// The content type. - /// - /// A value indicating whether the content type varies by segment. - /// - public static bool VariesBySegment(this ISimpleContentType contentType) => contentType.Variations.VariesBySegment(); - - /// - /// Determines whether the content type varies by culture and segment. - /// - /// The content type. - /// - /// A value indicating whether the content type varies by culture and segment. - /// - public static bool VariesByCultureAndSegment(this ISimpleContentType contentType) => contentType.Variations.VariesByCultureAndSegment(); - /// /// Determines whether the content type is invariant. /// @@ -54,69 +27,6 @@ namespace Umbraco.Core /// public static bool VariesByNothing(this IContentTypeBase contentType) => contentType.Variations.VariesByNothing(); - /// - /// Determines whether the content type varies by culture. - /// - /// The content type. - /// - /// A value indicating whether the content type varies by culture. - /// - public static bool VariesByCulture(this IContentTypeBase contentType) => contentType.Variations.VariesByCulture(); - - /// - /// Determines whether the content type varies by segment. - /// - /// The content type. - /// - /// A value indicating whether the content type varies by segment. - /// - public static bool VariesBySegment(this IContentTypeBase contentType) => contentType.Variations.VariesBySegment(); - - /// - /// Determines whether the content type varies by culture and segment. - /// - /// The content type. - /// - /// A value indicating whether the content type varies by culture and segment. - /// - public static bool VariesByCultureAndSegment(this IContentTypeBase contentType) => contentType.Variations.VariesByCultureAndSegment(); - - /// - /// Determines whether the property type is invariant. - /// - /// The property type. - /// - /// A value indicating whether the property type is invariant. - /// - public static bool VariesByNothing(this PropertyType propertyType) => propertyType.Variations.VariesByNothing(); - - /// - /// Determines whether the property type varies by culture. - /// - /// The property type. - /// - /// A value indicating whether the property type varies by culture. - /// - public static bool VariesByCulture(this PropertyType propertyType) => propertyType.Variations.VariesByCulture(); - - /// - /// Determines whether the property type varies by segment. - /// - /// The property type. - /// - /// A value indicating whether the property type varies by segment. - /// - public static bool VariesBySegment(this PropertyType propertyType) => propertyType.Variations.VariesBySegment(); - - /// - /// Determines whether the property type varies by culture and segment. - /// - /// The property type. - /// - /// A value indicating whether the property type varies by culture and segment. - /// - public static bool VariesByCultureAndSegment(this PropertyType propertyType) => propertyType.Variations.VariesByCultureAndSegment(); - /// /// Determines whether the content type is invariant. /// @@ -127,31 +37,13 @@ namespace Umbraco.Core public static bool VariesByNothing(this IPublishedContentType contentType) => contentType.Variations.VariesByNothing(); /// - /// Determines whether the content type varies by culture. + /// Determines whether the property type is invariant. /// - /// The content type. + /// The property type. /// - /// A value indicating whether the content type varies by culture. + /// A value indicating whether the property type is invariant. /// - public static bool VariesByCulture(this IPublishedContentType contentType) => contentType.Variations.VariesByCulture(); - - /// - /// Determines whether the content type varies by segment. - /// - /// The content type. - /// - /// A value indicating whether the content type varies by segment. - /// - public static bool VariesBySegment(this IPublishedContentType contentType) => contentType.Variations.VariesBySegment(); - - /// - /// Determines whether the content type varies by culture and segment. - /// - /// The content type. - /// - /// A value indicating whether the content type varies by culture and segment. - /// - public static bool VariesByCultureAndSegment(this IPublishedContentType contentType) => contentType.Variations.VariesByCultureAndSegment(); + public static bool VariesByNothing(this PropertyType propertyType) => propertyType.Variations.VariesByNothing(); /// /// Determines whether the property type is invariant. @@ -162,33 +54,6 @@ namespace Umbraco.Core /// public static bool VariesByNothing(this IPublishedPropertyType propertyType) => propertyType.Variations.VariesByNothing(); - /// - /// Determines whether the property type varies by culture. - /// - /// The property type. - /// - /// A value indicating whether the property type varies by culture. - /// - public static bool VariesByCulture(this IPublishedPropertyType propertyType) => propertyType.Variations.VariesByCulture(); - - /// - /// Determines whether the property type varies by segment. - /// - /// The property type. - /// - /// A value indicating whether the property type varies by segment. - /// - public static bool VariesBySegment(this IPublishedPropertyType propertyType) => propertyType.Variations.VariesBySegment(); - - /// - /// Determines whether the property type varies by culture and segment. - /// - /// The property type. - /// - /// A value indicating whether the property type varies by culture and segment. - /// - public static bool VariesByCultureAndSegment(this IPublishedPropertyType propertyType) => propertyType.Variations.VariesByCultureAndSegment(); - /// /// Determines whether a variation is invariant. /// @@ -198,6 +63,51 @@ namespace Umbraco.Core /// public static bool VariesByNothing(this ContentVariation variation) => variation == ContentVariation.Nothing; + /// + /// Determines whether the content type varies by culture. + /// + /// The content type. + /// + /// A value indicating whether the content type varies by culture. + /// + public static bool VariesByCulture(this ISimpleContentType contentType) => contentType.Variations.VariesByCulture(); + + /// + /// Determines whether the content type varies by culture. + /// + /// The content type. + /// + /// A value indicating whether the content type varies by culture. + /// + public static bool VariesByCulture(this IContentTypeBase contentType) => contentType.Variations.VariesByCulture(); + + /// + /// Determines whether the content type varies by culture. + /// + /// The content type. + /// + /// A value indicating whether the content type varies by culture. + /// + public static bool VariesByCulture(this IPublishedContentType contentType) => contentType.Variations.VariesByCulture(); + + /// + /// Determines whether the property type varies by culture. + /// + /// The property type. + /// + /// A value indicating whether the property type varies by culture. + /// + public static bool VariesByCulture(this PropertyType propertyType) => propertyType.Variations.VariesByCulture(); + + /// + /// Determines whether the property type varies by culture. + /// + /// The property type. + /// + /// A value indicating whether the property type varies by culture. + /// + public static bool VariesByCulture(this IPublishedPropertyType propertyType) => propertyType.Variations.VariesByCulture(); + /// /// Determines whether a variation varies by culture. /// @@ -207,6 +117,51 @@ namespace Umbraco.Core /// public static bool VariesByCulture(this ContentVariation variation) => (variation & ContentVariation.Culture) > 0; + /// + /// Determines whether the content type varies by segment. + /// + /// The content type. + /// + /// A value indicating whether the content type varies by segment. + /// + public static bool VariesBySegment(this ISimpleContentType contentType) => contentType.Variations.VariesBySegment(); + + /// + /// Determines whether the content type varies by segment. + /// + /// The content type. + /// + /// A value indicating whether the content type varies by segment. + /// + public static bool VariesBySegment(this IContentTypeBase contentType) => contentType.Variations.VariesBySegment(); + + /// + /// Determines whether the content type varies by segment. + /// + /// The content type. + /// + /// A value indicating whether the content type varies by segment. + /// + public static bool VariesBySegment(this IPublishedContentType contentType) => contentType.Variations.VariesBySegment(); + + /// + /// Determines whether the property type varies by segment. + /// + /// The property type. + /// + /// A value indicating whether the property type varies by segment. + /// + public static bool VariesBySegment(this PropertyType propertyType) => propertyType.Variations.VariesBySegment(); + + /// + /// Determines whether the property type varies by segment. + /// + /// The property type. + /// + /// A value indicating whether the property type varies by segment. + /// + public static bool VariesBySegment(this IPublishedPropertyType propertyType) => propertyType.Variations.VariesBySegment(); + /// /// Determines whether a variation varies by segment. /// @@ -216,6 +171,51 @@ namespace Umbraco.Core /// public static bool VariesBySegment(this ContentVariation variation) => (variation & ContentVariation.Segment) > 0; + /// + /// Determines whether the content type varies by culture and segment. + /// + /// The content type. + /// + /// A value indicating whether the content type varies by culture and segment. + /// + public static bool VariesByCultureAndSegment(this ISimpleContentType contentType) => contentType.Variations.VariesByCultureAndSegment(); + + /// + /// Determines whether the content type varies by culture and segment. + /// + /// The content type. + /// + /// A value indicating whether the content type varies by culture and segment. + /// + public static bool VariesByCultureAndSegment(this IContentTypeBase contentType) => contentType.Variations.VariesByCultureAndSegment(); + + /// + /// Determines whether the content type varies by culture and segment. + /// + /// The content type. + /// + /// A value indicating whether the content type varies by culture and segment. + /// + public static bool VariesByCultureAndSegment(this IPublishedContentType contentType) => contentType.Variations.VariesByCultureAndSegment(); + + /// + /// Determines whether the property type varies by culture and segment. + /// + /// The property type. + /// + /// A value indicating whether the property type varies by culture and segment. + /// + public static bool VariesByCultureAndSegment(this PropertyType propertyType) => propertyType.Variations.VariesByCultureAndSegment(); + + /// + /// Determines whether the property type varies by culture and segment. + /// + /// The property type. + /// + /// A value indicating whether the property type varies by culture and segment. + /// + public static bool VariesByCultureAndSegment(this IPublishedPropertyType propertyType) => propertyType.Variations.VariesByCultureAndSegment(); + /// /// Determines whether a variation varies by culture and segment. /// @@ -225,6 +225,47 @@ namespace Umbraco.Core /// public static bool VariesByCultureAndSegment(this ContentVariation variation) => (variation & ContentVariation.CultureAndSegment) == ContentVariation.CultureAndSegment; + /// + /// Sets or removes the content type variation depending on the specified value. + /// + /// The content type. + /// The variation to set or remove. + /// If set to true sets the variation; otherwise, removes the variation. + /// + /// This method does not support setting the variation to nothing. + /// + public static void SetVariesBy(this IContentTypeBase contentType, ContentVariation variation, bool value = true) => contentType.Variations = contentType.Variations.SetVariesBy(variation, value); + + /// + /// Sets or removes the property type variation depending on the specified value. + /// + /// The property type. + /// The variation to set or remove. + /// If set to true sets the variation; otherwise, removes the variation. + /// + /// This method does not support setting the variation to nothing. + /// + public static void SetVariesBy(this PropertyType propertyType, ContentVariation variation, bool value = true) => propertyType.Variations = propertyType.Variations.SetVariesBy(variation, value); + + /// + /// Returns the variations with the variation set or removed depending on the specified value. + /// + /// The existing variations. + /// The variation to set or remove. + /// If set to true sets the variation; otherwise, removes the variation. + /// + /// The variations with the variation set or removed. + /// + /// + /// This method does not support setting the variation to nothing. + /// + public static ContentVariation SetVariesBy(this ContentVariation variations, ContentVariation variation, bool value = true) + { + return value + ? variations | variation // Set flag using bitwise logical OR + : variations & ~variation; // Remove flag using bitwise logical AND with bitwise complement (reversing the bit) + } + /// /// Validates that a combination of culture and segment is valid for the variation. /// @@ -298,46 +339,5 @@ namespace Umbraco.Core return true; } - - /// - /// Sets or removes the content type variation depending on the specified value. - /// - /// The content type. - /// The variation to set or remove. - /// If set to true sets the variation; otherwise, removes the variation. - /// - /// This method does not support setting the variation to nothing. - /// - public static void SetVariesBy(this IContentTypeBase contentType, ContentVariation variation, bool value = true) => contentType.Variations = contentType.Variations.SetVariesBy(variation, value); - - /// - /// Sets or removes the property type variation depending on the specified value. - /// - /// The property type. - /// The variation to set or remove. - /// If set to true sets the variation; otherwise, removes the variation. - /// - /// This method does not support setting the variation to nothing. - /// - public static void SetVariesBy(this PropertyType propertyType, ContentVariation variation, bool value = true) => propertyType.Variations = propertyType.Variations.SetVariesBy(variation, value); - - /// - /// Returns the variations with the variation set or removed depending on the specified value. - /// - /// The existing variations. - /// The variation to set or remove. - /// If set to true sets the variation; otherwise, removes the variation. - /// - /// The variations with the variation set or removed. - /// - /// - /// This method does not support setting the variation to nothing. - /// - public static ContentVariation SetVariesBy(this ContentVariation variations, ContentVariation variation, bool value = true) - { - return value - ? variations | variation // Set flag using bitwise logical OR - : variations & ~variation; // Remove flag using bitwise logical AND with bitwise complement (reversing the bit) - } } } From bc82ccd82458ac0824baa6547b1f84625857fbea Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Sun, 19 Jan 2020 10:34:04 +0100 Subject: [PATCH 178/202] Prevented user from changing a document type to an element type if it's already been used to create content (#5668) --- .../IContentTypeRepositoryBase.cs | 5 ++++ .../Implement/ContentTypeRepositoryBase.cs | 11 ++++++++ .../Services/IContentTypeServiceBase.cs | 5 ++++ ...peServiceBaseOfTRepositoryTItemTService.cs | 9 +++++++ .../Repositories/ContentTypeRepositoryTest.cs | 26 +++++++++++++++++++ .../common/resources/contenttype.resource.js | 10 +++++++ .../permissions/permissions.controller.js | 10 ++++++- .../views/permissions/permissions.html | 2 ++ src/Umbraco.Web.UI/Umbraco/config/lang/en.xml | 8 +++--- .../Umbraco/config/lang/en_us.xml | 7 ++--- .../Editors/ContentTypeController.cs | 7 +++++ 11 files changed, 92 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/IContentTypeRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/IContentTypeRepositoryBase.cs index 69b0698a96..254e04d2d5 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IContentTypeRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IContentTypeRepositoryBase.cs @@ -26,5 +26,10 @@ namespace Umbraco.Core.Persistence.Repositories /// /// bool HasContainerInPath(string contentPath); + + /// + /// Returns true or false depending on whether content nodes have been created based on the provided content type id. + /// + bool HasContentNodes(int id); } } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs index e2c3d8c9b5..6f714ff187 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs @@ -1323,6 +1323,17 @@ WHERE {Constants.DatabaseSchema.Tables.Content}.nodeId IN (@ids) AND cmsContentT return Database.ExecuteScalar(sql) > 0; } + /// + /// Returns true or false depending on whether content nodes have been created based on the provided content type id. + /// + public bool HasContentNodes(int id) + { + var sql = new Sql( + $"SELECT CASE WHEN EXISTS (SELECT * FROM {Constants.DatabaseSchema.Tables.Content} WHERE contentTypeId = @id) THEN 1 ELSE 0 END", + new { id }); + return Database.ExecuteScalar(sql) == 1; + } + protected override IEnumerable GetDeleteClauses() { // in theory, services should have ensured that content items of the given content type diff --git a/src/Umbraco.Core/Services/IContentTypeServiceBase.cs b/src/Umbraco.Core/Services/IContentTypeServiceBase.cs index 51e5d756eb..6ed3c85e91 100644 --- a/src/Umbraco.Core/Services/IContentTypeServiceBase.cs +++ b/src/Umbraco.Core/Services/IContentTypeServiceBase.cs @@ -39,6 +39,11 @@ namespace Umbraco.Core.Services int Count(); + /// + /// Returns true or false depending on whether content nodes have been created based on the provided content type id. + /// + bool HasContentNodes(int id); + IEnumerable GetAll(params int[] ids); IEnumerable GetAll(IEnumerable ids); diff --git a/src/Umbraco.Core/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs b/src/Umbraco.Core/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs index 7ae330f8f1..da532e2765 100644 --- a/src/Umbraco.Core/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs @@ -372,6 +372,15 @@ namespace Umbraco.Core.Services.Implement } } + public bool HasContentNodes(int id) + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + scope.ReadLock(ReadLockIds); + return Repository.HasContentNodes(id); + } + } + #endregion #region Save diff --git a/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs index f953b9cce6..bd80d6b154 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs @@ -965,6 +965,32 @@ namespace Umbraco.Tests.Persistence.Repositories } } + [Test] + public void Can_Verify_Content_Type_Has_Content_Nodes() + { + // Arrange + var provider = TestObjects.GetScopeProvider(Logger); + using (var scope = provider.CreateScope()) + { + ContentTypeRepository repository; + var contentRepository = CreateRepository((IScopeAccessor)provider, out repository); + var contentTypeId = NodeDto.NodeIdSeed + 1; + var contentType = repository.Get(contentTypeId); + + // Act + var result = repository.HasContentNodes(contentTypeId); + + var subpage = MockedContent.CreateTextpageContent(contentType, "Test Page 1", contentType.Id); + contentRepository.Save(subpage); + + var result2 = repository.HasContentNodes(contentTypeId); + + // Assert + Assert.That(result, Is.False); + Assert.That(result2, Is.True); + } + } + public void CreateTestData() { //Create and Save ContentType "umbTextpage" -> (NodeDto.NodeIdSeed) diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/contenttype.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/contenttype.resource.js index 64accc18c1..97bebef062 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/contenttype.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/contenttype.resource.js @@ -351,6 +351,16 @@ function contentTypeResource($q, $http, umbRequestHelper, umbDataFormatter, loca return umbRequestHelper.resourcePromise( $http.post(umbRequestHelper.getApiUrl("contentTypeApiBaseUrl", "PostCreateDefaultTemplate", { id: id })), 'Failed to create default template for content type with id ' + id); + }, + + hasContentNodes: function (id) { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "contentTypeApiBaseUrl", + "HasContentNodes", + [{ id: id }])), + 'Failed to retrieve indication for whether content type with id ' + id + ' has associated content nodes'); } }; } diff --git a/src/Umbraco.Web.UI.Client/src/views/documenttypes/views/permissions/permissions.controller.js b/src/Umbraco.Web.UI.Client/src/views/documenttypes/views/permissions/permissions.controller.js index a3b422e4f3..a4e8c6373c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/documenttypes/views/permissions/permissions.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/documenttypes/views/permissions/permissions.controller.js @@ -25,6 +25,7 @@ vm.removeChild = removeChild; vm.toggleAllowAsRoot = toggleAllowAsRoot; vm.toggleAllowCultureVariants = toggleAllowCultureVariants; + vm.canToggleIsElement = false; vm.toggleIsElement = toggleIsElement; /* ---------- INIT ---------- */ @@ -48,9 +49,16 @@ if($scope.model.id === 0) { contentTypeHelper.insertChildNodePlaceholder(vm.contentTypes, $scope.model.name, $scope.model.icon, $scope.model.id); } - }); + // Can only switch to an element type if there are no content nodes already created from the type. + if ($scope.model.id > 0 && !$scope.model.isElement ) { + contentTypeResource.hasContentNodes($scope.model.id).then(function (result) { + vm.canToggleIsElement = !result; + }); + } else { + vm.canToggleIsElement = true; + } } function addChild($event) { diff --git a/src/Umbraco.Web.UI.Client/src/views/documenttypes/views/permissions/permissions.html b/src/Umbraco.Web.UI.Client/src/views/documenttypes/views/permissions/permissions.html index e890921ad4..d459bd9fae 100644 --- a/src/Umbraco.Web.UI.Client/src/views/documenttypes/views/permissions/permissions.html +++ b/src/Umbraco.Web.UI.Client/src/views/documenttypes/views/permissions/permissions.html @@ -61,11 +61,13 @@
+
diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index a2cf6ea475..880700c74a 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -1643,10 +1643,10 @@ To manage your website, simply open the Umbraco back office and start adding con Allow varying by culture Allow editors to create content of this type in different languages. Allow varying by culture - Element type - Is an Element type - An Element type is meant to be used for instance in Nested Content, and not in the tree. - This is not applicable for an Element type + Is an element type + An element type is meant to be used for instance in Nested Content, and not in the tree. + A document type cannot be changed to an element type once it has been used to create one or more content items. + This is not applicable for an element type You have made changes to this property. Are you sure you want to discard them? diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml index 56d5f1ade2..546d085b4a 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -1658,9 +1658,10 @@ To manage your website, simply open the Umbraco back office and start adding con Allow editors to create content of this type in different languages. Allow varying by culture Element type - Is an Element type - An Element type is meant to be used for instance in Nested Content, and not in the tree. - This is not applicable for an Element type + Is an element type + An element type is meant to be used for instance in Nested Content, and not in the tree. + A document type cannot be changed to an element type once it has been used to create one or more content items. + This is not applicable for an element type You have made changes to this property. Are you sure you want to discard them? diff --git a/src/Umbraco.Web/Editors/ContentTypeController.cs b/src/Umbraco.Web/Editors/ContentTypeController.cs index 0f51c35a14..a566c2cdc7 100644 --- a/src/Umbraco.Web/Editors/ContentTypeController.cs +++ b/src/Umbraco.Web/Editors/ContentTypeController.cs @@ -70,6 +70,13 @@ namespace Umbraco.Web.Editors return Services.ContentTypeService.Count(); } + [HttpGet] + [UmbracoTreeAuthorize(Constants.Trees.DocumentTypes)] + public bool HasContentNodes(int id) + { + return Services.ContentTypeService.HasContentNodes(id); + } + public DocumentTypeDisplay GetById(int id) { var ct = Services.ContentTypeService.Get(id); From c27634d8a9b0e467e11466247e09542931d125a5 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Sun, 19 Jan 2020 10:50:18 +0100 Subject: [PATCH 179/202] V8: Allow reordering of allowed child types (#4927) * Allow explicitly sorting allowed child types for content types * Allow explicitly sorting allowed child types for media types * Remove console.log * Fix the mapping of allowed content types (after introduction of the new mapper) * Fix sorting after merge --- .../components/umbchildselector.directive.js | 19 ++++++++++++++++++- .../less/components/umb-child-selector.less | 3 +++ .../views/components/umb-child-selector.html | 2 +- .../src/views/content/create.html | 2 +- .../permissions/permissions.controller.js | 10 +++++++++- .../views/permissions/permissions.html | 3 ++- .../permissions/permissions.controller.js | 10 +++++++++- .../views/permissions/permissions.html | 3 ++- .../Editors/ContentTypeController.cs | 6 +++--- .../Editors/MediaTypeController.cs | 6 +++--- .../Mapping/ContentTypeMapDefinition.cs | 2 +- 11 files changed, 52 insertions(+), 14 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbchildselector.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbchildselector.directive.js index 4fc22c4b74..a33fd4be53 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbchildselector.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbchildselector.directive.js @@ -188,6 +188,22 @@ Use this directive to render a ui component for selecting child items to a paren syncParentIcon(); })); + // sortable options for allowed child content types + scope.sortableOptions = { + axis: "y", + containment: "parent", + distance: 10, + opacity: 0.7, + tolerance: "pointer", + scroll: true, + zIndex: 6000, + update: function (e, ui) { + if(scope.onSort) { + scope.onSort(); + } + } + }; + // clean up scope.$on('$destroy', function(){ // unbind watchers @@ -209,7 +225,8 @@ Use this directive to render a ui component for selecting child items to a paren parentIcon: "=", parentId: "=", onRemove: "=", - onAdd: "=" + onAdd: "=", + onSort: "=" }, link: link }; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-child-selector.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-child-selector.less index b6cdc0e8d9..da690663d0 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-child-selector.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-child-selector.less @@ -30,6 +30,9 @@ .umb-child-selector__children-container { margin-left: 30px; + .umb-child-selector__child { + cursor: move; + } } .umb-child-selector__child-description { diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-child-selector.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-child-selector.html index 686937e8c9..0866e1bdf7 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-child-selector.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-child-selector.html @@ -14,7 +14,7 @@ -
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/content/create.html b/src/Umbraco.Web.UI.Client/src/views/content/create.html index cfb4b2c573..1d4b646e05 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/create.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/create.html @@ -36,7 +36,7 @@
    -
  • +
diff --git a/src/Umbraco.Web.UI.Client/src/views/mediatypes/views/permissions/permissions.controller.js b/src/Umbraco.Web.UI.Client/src/views/mediatypes/views/permissions/permissions.controller.js index 1d865f2fa0..53a098a26e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/mediatypes/views/permissions/permissions.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/mediatypes/views/permissions/permissions.controller.js @@ -1,7 +1,7 @@ (function() { 'use strict'; - function PermissionsController($scope, mediaTypeResource, iconHelper, contentTypeHelper, localizationService, overlayService) { + function PermissionsController($scope, $timeout, mediaTypeResource, iconHelper, contentTypeHelper, localizationService, overlayService) { /* ----------- SCOPE VARIABLES ----------- */ @@ -13,6 +13,7 @@ vm.addChild = addChild; vm.removeChild = removeChild; + vm.sortChildren = sortChildren; vm.toggle = toggle; /* ---------- INIT ---------- */ @@ -71,6 +72,13 @@ $scope.model.allowedContentTypes.splice(selectedChildIndex, 1); } + function sortChildren() { + // we need to wait until the next digest cycle for vm.selectedChildren to be updated + $timeout(function () { + $scope.model.allowedContentTypes = _.pluck(vm.selectedChildren, "id"); + }); + } + /** * Toggle the $scope.model.allowAsRoot value to either true or false */ diff --git a/src/Umbraco.Web.UI.Client/src/views/mediatypes/views/permissions/permissions.html b/src/Umbraco.Web.UI.Client/src/views/mediatypes/views/permissions/permissions.html index caa4c4dc11..09e7ab4f41 100644 --- a/src/Umbraco.Web.UI.Client/src/views/mediatypes/views/permissions/permissions.html +++ b/src/Umbraco.Web.UI.Client/src/views/mediatypes/views/permissions/permissions.html @@ -27,7 +27,8 @@ parent-icon="model.icon" parent-id="model.id" on-add="vm.addChild" - on-remove="vm.removeChild"> + on-remove="vm.removeChild" + on-sort="vm.sortChildren">
diff --git a/src/Umbraco.Web/Editors/ContentTypeController.cs b/src/Umbraco.Web/Editors/ContentTypeController.cs index a566c2cdc7..9c248f186b 100644 --- a/src/Umbraco.Web/Editors/ContentTypeController.cs +++ b/src/Umbraco.Web/Editors/ContentTypeController.cs @@ -432,11 +432,11 @@ namespace Umbraco.Web.Editors } var contentType = Services.ContentTypeBaseServices.GetContentTypeOf(contentItem); - var ids = contentType.AllowedContentTypes.Select(x => x.Id.Value).ToArray(); + var ids = contentType.AllowedContentTypes.OrderBy(c => c.SortOrder).Select(x => x.Id.Value).ToArray(); if (ids.Any() == false) return Enumerable.Empty(); - types = Services.ContentTypeService.GetAll(ids).ToList(); + types = Services.ContentTypeService.GetAll(ids).OrderBy(c => ids.IndexOf(c.Id)).ToList(); } var basics = types.Where(type => type.IsElement == false).Select(Mapper.Map).ToList(); @@ -459,7 +459,7 @@ namespace Umbraco.Web.Editors } } - return basics; + return basics.OrderBy(c => contentId == Constants.System.Root ? c.Name : string.Empty); } /// diff --git a/src/Umbraco.Web/Editors/MediaTypeController.cs b/src/Umbraco.Web/Editors/MediaTypeController.cs index 43569c77e2..3a4026423a 100644 --- a/src/Umbraco.Web/Editors/MediaTypeController.cs +++ b/src/Umbraco.Web/Editors/MediaTypeController.cs @@ -240,11 +240,11 @@ namespace Umbraco.Web.Editors } var contentType = Services.MediaTypeService.Get(contentItem.ContentTypeId); - var ids = contentType.AllowedContentTypes.Select(x => x.Id.Value).ToArray(); + var ids = contentType.AllowedContentTypes.OrderBy(c => c.SortOrder).Select(x => x.Id.Value).ToArray(); if (ids.Any() == false) return Enumerable.Empty(); - types = Services.MediaTypeService.GetAll(ids).ToList(); + types = Services.MediaTypeService.GetAll(ids).OrderBy(c => ids.IndexOf(c.Id)).ToList(); } var basics = types.Select(Mapper.Map).ToList(); @@ -255,7 +255,7 @@ namespace Umbraco.Web.Editors basic.Description = TranslateItem(basic.Description); } - return basics.OrderBy(x => x.Name); + return basics.OrderBy(c => contentId == Constants.System.Root ? c.Name : string.Empty); } /// diff --git a/src/Umbraco.Web/Models/Mapping/ContentTypeMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/ContentTypeMapDefinition.cs index 266cd894b6..f18c481297 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentTypeMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentTypeMapDefinition.cs @@ -493,7 +493,7 @@ namespace Umbraco.Web.Models.Mapping target.Udi = MapContentTypeUdi(source); target.UpdateDate = source.UpdateDate; - target.AllowedContentTypes = source.AllowedContentTypes.Select(x => x.Id.Value); + target.AllowedContentTypes = source.AllowedContentTypes.OrderBy(c => c.SortOrder).Select(x => x.Id.Value); target.CompositeContentTypes = source.ContentTypeComposition.Select(x => x.Alias); target.LockedCompositeContentTypes = MapLockedCompositions(source); } From d378495942fe1828571444aeb45e84143059f3c4 Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Mon, 20 Jan 2020 09:11:29 +0000 Subject: [PATCH 180/202] Log message format fix for PR6617 --- src/Umbraco.Core/Composing/ComponentCollection.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Composing/ComponentCollection.cs b/src/Umbraco.Core/Composing/ComponentCollection.cs index fa4a1849b6..62b240f10f 100644 --- a/src/Umbraco.Core/Composing/ComponentCollection.cs +++ b/src/Umbraco.Core/Composing/ComponentCollection.cs @@ -51,7 +51,7 @@ namespace Umbraco.Core.Composing } catch (Exception ex) { - _logger.Error(componentType, ex, "Error while terminating component {ComponentType}.", componentType.FullName); + _logger.Error(ex, "Error while terminating component {ComponentType}.", componentType.FullName); } } } From 7fbe4829196d7f131a4bb5e9a6d8263b25eda8a4 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Mon, 20 Jan 2020 14:09:13 +0100 Subject: [PATCH 181/202] Revert "https://github.com/umbraco/Umbraco-CMS/issues/7469 - Fixed Ambiguous extension method issue by renaming our extension method from Value to GetValue" This reverts commit baef282b --- .../PublishedElementExtensions.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.ModelsBuilder.Embedded/PublishedElementExtensions.cs b/src/Umbraco.ModelsBuilder.Embedded/PublishedElementExtensions.cs index 2c22caf87d..29429ba74f 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/PublishedElementExtensions.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/PublishedElementExtensions.cs @@ -17,14 +17,14 @@ namespace Umbraco.Web /// /// Gets the value of a property. /// - public static TValue ValueByExpression(this TModel model, Expression> property, string culture = null, string segment = null, Fallback fallback = default, TValue defaultValue = default) + public static TValue Value(this TModel model, Expression> property, string culture = null, string segment = null, Fallback fallback = default, TValue defaultValue = default) where TModel : IPublishedElement { var alias = GetAlias(model, property); return model.Value(alias, culture, segment, fallback, defaultValue); } - //This cannot be public due to ambiguous issue with external ModelsBuilder if we do not rename. + // fixme that one should be public so ppl can use it private static string GetAlias(TModel model, Expression> property) { if (property.NodeType != ExpressionType.Lambda) @@ -45,7 +45,7 @@ namespace Umbraco.Web var attribute = member.GetCustomAttribute(); if (attribute == null) throw new InvalidOperationException("Property is not marked with ImplementPropertyType attribute."); - + return attribute.Alias; } } From 8cc78631a08f0084fbcf19a296c1cec348a1d09a Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 21 Jan 2020 13:33:39 +1100 Subject: [PATCH 182/202] Adds TestData project along with test endpoints for enabling/disabling segments --- .../Properties/AssemblyInfo.cs | 36 +++ src/Umbraco.TestData/SegmentTestController.cs | 77 +++++ src/Umbraco.TestData/Umbraco.TestData.csproj | 70 +++++ .../UmbracoTestDataController.cs | 288 ++++++++++++++++++ src/Umbraco.TestData/readme.md | 51 ++++ src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 4 + src/Umbraco.Web.UI/web.Template.Debug.config | 5 + src/umbraco.sln | 7 + 8 files changed, 538 insertions(+) create mode 100644 src/Umbraco.TestData/Properties/AssemblyInfo.cs create mode 100644 src/Umbraco.TestData/SegmentTestController.cs create mode 100644 src/Umbraco.TestData/Umbraco.TestData.csproj create mode 100644 src/Umbraco.TestData/UmbracoTestDataController.cs create mode 100644 src/Umbraco.TestData/readme.md diff --git a/src/Umbraco.TestData/Properties/AssemblyInfo.cs b/src/Umbraco.TestData/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..3c4251cdf6 --- /dev/null +++ b/src/Umbraco.TestData/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Umbraco.TestData")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Umbraco.TestData")] +[assembly: AssemblyCopyright("Copyright © 2019")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("fb5676ed-7a69-492c-b802-e7b24144c0fc")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/Umbraco.TestData/SegmentTestController.cs b/src/Umbraco.TestData/SegmentTestController.cs new file mode 100644 index 0000000000..bcfdfc4ac4 --- /dev/null +++ b/src/Umbraco.TestData/SegmentTestController.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.Configuration; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Web.Mvc; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Web.Mvc; + +namespace Umbraco.TestData +{ + public class SegmentTestController : SurfaceController + { + + public ActionResult EnableDocTypeSegments(string alias, string propertyTypeAlias) + { + if (ConfigurationManager.AppSettings["Umbraco.TestData.Enabled"] != "true") + return HttpNotFound(); + + var ct = Services.ContentTypeService.Get(alias); + if (ct == null) + return Content($"No document type found by alias {alias}"); + + var propType = ct.PropertyTypes.FirstOrDefault(x => x.Alias == propertyTypeAlias); + if (propType == null) + return Content($"The document type {alias} does not have a property type {propertyTypeAlias ?? "null"}"); + + if (ct.Variations.VariesBySegment()) + return Content($"The document type {alias} already allows segments, nothing has been changed"); + + ct.Variations = ct.Variations.SetFlag(ContentVariation.Segment); + + propType.Variations = propType.Variations.SetFlag(ContentVariation.Segment); + + Services.ContentTypeService.Save(ct); + return Content($"The document type {alias} and property type {propertyTypeAlias} now allows segments"); + } + + public ActionResult DisableDocTypeSegments(string alias) + { + if (ConfigurationManager.AppSettings["Umbraco.TestData.Enabled"] != "true") + return HttpNotFound(); + + var ct = Services.ContentTypeService.Get(alias); + if (ct == null) + return Content($"No document type found by alias {alias}"); + + if (!ct.VariesBySegment()) + return Content($"The document type {alias} does not allow segments, nothing has been changed"); + + ct.Variations = ct.Variations.UnsetFlag(ContentVariation.Segment); + + Services.ContentTypeService.Save(ct); + return Content($"The document type {alias} no longer allows segments"); + } + + public ActionResult AddSegmentData(int contentId, string propertyAlias, string value, string segment, string culture = null) + { + var content = Services.ContentService.GetById(contentId); + if (content == null) + return Content($"No content found by id {contentId}"); + + if (!content.HasProperty(propertyAlias)) + return Content($"The content by id {contentId} does not contain a property with alias {propertyAlias}"); + + if (content.ContentType.VariesByCulture() && culture.IsNullOrWhiteSpace()) + return Content($"The content by id {contentId} varies by culture but no culture was specified"); + + content.SetValue(propertyAlias, value, culture, segment); + Services.ContentService.Save(content); + + return Content($"Segment value has been set on content {contentId} for property {propertyAlias}"); + } + } +} diff --git a/src/Umbraco.TestData/Umbraco.TestData.csproj b/src/Umbraco.TestData/Umbraco.TestData.csproj new file mode 100644 index 0000000000..d61321ebb8 --- /dev/null +++ b/src/Umbraco.TestData/Umbraco.TestData.csproj @@ -0,0 +1,70 @@ + + + + + Debug + AnyCPU + {FB5676ED-7A69-492C-B802-E7B24144C0FC} + Library + Properties + Umbraco.TestData + Umbraco.TestData + v4.7.2 + 512 + true + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + + {31785bc3-256c-4613-b2f5-a1b0bdded8c1} + Umbraco.Core + + + {651e1350-91b6-44b7-bd60-7207006d7003} + Umbraco.Web + + + + + 28.4.4 + + + 5.2.7 + + + + \ No newline at end of file diff --git a/src/Umbraco.TestData/UmbracoTestDataController.cs b/src/Umbraco.TestData/UmbracoTestDataController.cs new file mode 100644 index 0000000000..402c05cc1c --- /dev/null +++ b/src/Umbraco.TestData/UmbracoTestDataController.cs @@ -0,0 +1,288 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Umbraco.Core; +using System.Web.Mvc; +using Umbraco.Core.Cache; +using Umbraco.Core.Logging; +using Umbraco.Core.Models; +using Umbraco.Core.Persistence; +using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Services; +using Umbraco.Web; +using Umbraco.Web.Mvc; +using System.Configuration; +using Bogus; +using Umbraco.Core.Scoping; + +namespace Umbraco.TestData +{ + /// + /// Creates test data + /// + public class UmbracoTestDataController : SurfaceController + { + private const string RichTextDataTypeName = "UmbracoTestDataContent.RTE"; + private const string MediaPickerDataTypeName = "UmbracoTestDataContent.MediaPicker"; + private const string TextDataTypeName = "UmbracoTestDataContent.Text"; + private const string TestDataContentTypeAlias = "umbTestDataContent"; + private readonly IScopeProvider _scopeProvider; + private readonly PropertyEditorCollection _propertyEditors; + + public UmbracoTestDataController(IScopeProvider scopeProvider, PropertyEditorCollection propertyEditors, IUmbracoContextAccessor umbracoContextAccessor, IUmbracoDatabaseFactory databaseFactory, ServiceContext services, AppCaches appCaches, ILogger logger, IProfilingLogger profilingLogger, UmbracoHelper umbracoHelper) : base(umbracoContextAccessor, databaseFactory, services, appCaches, logger, profilingLogger, umbracoHelper) + { + _scopeProvider = scopeProvider; + _propertyEditors = propertyEditors; + } + + /// + /// Creates a content and associated media tree (hierarchy) + /// + /// + /// + /// + /// + /// + /// Each content item created is associated to a media item via a media picker and therefore a relation is created between the two + /// + public ActionResult CreateTree(int count, int depth, string locale = "en") + { + if (ConfigurationManager.AppSettings["Umbraco.TestData.Enabled"] != "true") + return HttpNotFound(); + + if (!Validate(count, depth, out var message, out var perLevel)) + throw new InvalidOperationException(message); + + var faker = new Faker(locale); + var company = faker.Company.CompanyName(); + + using (var scope = _scopeProvider.CreateScope()) + { + var imageIds = CreateMediaTree(company, faker, count, depth).ToList(); + var contentIds = CreateContentTree(company, faker, count, depth, imageIds, out var root).ToList(); + + Services.ContentService.SaveAndPublishBranch(root, true); + + scope.Complete(); + } + + + return Content("Done"); + } + + private bool Validate(int count, int depth, out string message, out int perLevel) + { + perLevel = 0; + message = null; + + if (count <= 0) + { + message = "Count must be more than 0"; + return false; + } + + perLevel = count / depth; + if (perLevel < 1) + { + message = "Count not high enough for specified for number of levels required"; + return false; + } + + return true; + } + + /// + /// Utility to create a tree hierarchy + /// + /// + /// + /// + /// + /// + /// A callback that returns a tuple of Content and another callback to produce a Container. + /// For media, a container will be another folder, for content the container will be the Content itself. + /// + /// + private IEnumerable CreateHierarchy( + T parent, int count, int depth, + Func container)> create) + where T: class, IContentBase + { + yield return parent.GetUdi(); + + // This will not calculate a balanced tree but it will ensure that there will be enough nodes deep enough to not fill up the tree. + var totalDescendants = count - 1; + var perLevel = Math.Ceiling(totalDescendants / (double)depth); + var perBranch = Math.Ceiling(perLevel / depth); + + var tracked = new Stack<(T parent, int childCount)>(); + + var currChildCount = 0; + + for (int i = 0; i < count; i++) + { + var created = create(parent); + var contentItem = created.content; + + yield return contentItem.GetUdi(); + + currChildCount++; + + if (currChildCount == perBranch) + { + // move back up... + + var prev = tracked.Pop(); + + // restore child count + currChildCount = prev.childCount; + // restore the parent + parent = prev.parent; + + } + else if (contentItem.Level < depth) + { + // track the current parent and it's current child count + tracked.Push((parent, currChildCount)); + + // not at max depth, create below + parent = created.container(); + + currChildCount = 0; + } + + } + } + + /// + /// Creates the media tree hiearachy + /// + /// + /// + /// + /// + /// + private IEnumerable CreateMediaTree(string company, Faker faker, int count, int depth) + { + var parent = Services.MediaService.CreateMediaWithIdentity(company, -1, Constants.Conventions.MediaTypes.Folder); + + return CreateHierarchy(parent, count, depth, currParent => + { + var imageUrl = faker.Image.PicsumUrl(); + + // we are appending a &ext=.jpg to the end of this for a reason. The result of this url will be something like: + // https://picsum.photos/640/480/?image=106 + // and due to the way that we detect images there must be an extension so we'll change it to + // https://picsum.photos/640/480/?image=106&ext=.jpg + // which will trick our app into parsing this and thinking it's an image ... which it is so that's good. + // if we don't do this we don't get thumbnails in the back office. + imageUrl += "&ext=.jpg"; + + var media = Services.MediaService.CreateMedia(faker.Commerce.ProductName(), currParent, Constants.Conventions.MediaTypes.Image); + media.SetValue(Constants.Conventions.Media.File, imageUrl); + Services.MediaService.Save(media); + return (media, () => + { + // create a folder to contain child media + var container = Services.MediaService.CreateMediaWithIdentity(faker.Commerce.Department(), currParent, Constants.Conventions.MediaTypes.Folder); + return container; + }); + }); + } + + /// + /// Creates the content tree hiearachy + /// + /// + /// + /// + /// + /// + /// + private IEnumerable CreateContentTree(string company, Faker faker, int count, int depth, List imageIds, out IContent root) + { + var random = new Random(company.GetHashCode()); + + var docType = GetOrCreateContentType(); + + var parent = Services.ContentService.Create(company, -1, docType.Alias); + parent.SetValue("review", faker.Rant.Review()); + parent.SetValue("desc", company); + parent.SetValue("media", imageIds[random.Next(0, imageIds.Count - 1)]); + Services.ContentService.Save(parent); + + root = parent; + + return CreateHierarchy(parent, count, depth, currParent => + { + var content = Services.ContentService.Create(faker.Commerce.ProductName(), currParent, docType.Alias); + content.SetValue("review", faker.Rant.Review()); + content.SetValue("desc", string.Join(", ", Enumerable.Range(0, 5).Select(x => faker.Commerce.ProductAdjective()))); ; + content.SetValue("media", imageIds[random.Next(0, imageIds.Count - 1)]); + + Services.ContentService.Save(content); + return (content, () => content); + }); + + } + + private IContentType GetOrCreateContentType() + { + var docType = Services.ContentTypeService.Get(TestDataContentTypeAlias); + if (docType != null) + return docType; + + docType = new ContentType(-1) + { + Alias = TestDataContentTypeAlias, + Name = "Umbraco Test Data Content", + Icon = "icon-science color-green" + }; + docType.AddPropertyGroup("Content"); + docType.AddPropertyType(new PropertyType(GetOrCreateRichText(), "review") + { + Name = "Review" + }); + docType.AddPropertyType(new PropertyType(GetOrCreateMediaPicker(), "media") + { + Name = "Media" + }); + docType.AddPropertyType(new PropertyType(GetOrCreateText(), "desc") + { + Name = "Description" + }); + Services.ContentTypeService.Save(docType); + docType.AllowedContentTypes = new[] { new ContentTypeSort(docType.Id, 0) }; + Services.ContentTypeService.Save(docType); + return docType; + } + + private IDataType GetOrCreateRichText() => GetOrCreateDataType(RichTextDataTypeName, Constants.PropertyEditors.Aliases.TinyMce); + + private IDataType GetOrCreateMediaPicker() => GetOrCreateDataType(MediaPickerDataTypeName, Constants.PropertyEditors.Aliases.MediaPicker); + + private IDataType GetOrCreateText() => GetOrCreateDataType(TextDataTypeName, Constants.PropertyEditors.Aliases.TextBox); + + private IDataType GetOrCreateDataType(string name, string editorAlias) + { + var dt = Services.DataTypeService.GetDataType(name); + if (dt != null) return dt; + + var editor = _propertyEditors.FirstOrDefault(x => x.Alias == editorAlias); + if (editor == null) + throw new InvalidOperationException($"No {editorAlias} editor found"); + + dt = new DataType(editor) + { + Name = name, + Configuration = editor.GetConfigurationEditor().DefaultConfigurationObject, + DatabaseType = ValueStorageType.Ntext + }; + + Services.DataTypeService.Save(dt); + return dt; + } + } +} diff --git a/src/Umbraco.TestData/readme.md b/src/Umbraco.TestData/readme.md new file mode 100644 index 0000000000..f943326303 --- /dev/null +++ b/src/Umbraco.TestData/readme.md @@ -0,0 +1,51 @@ +## Umbraco Test Data + +This project is a utility to be able to generate large amounts of content and media in an +Umbraco installation for testing. + +Currently this project is referenced in the Umbraco.Web.UI project but only when it's being built +in Debug mode (i.e. when testing within Visual Studio). + +## Usage + +You must use SQL Server for this, using SQLCE will die if you try to bulk create huge amounts of data. + +It has to be enabled by an appSetting: + +```xml + +``` + +Once this is enabled this endpoint can be executed: + +`/umbraco/surface/umbracotestdata/CreateTree?count=100&depth=5` + +The query string options are: + +* `count` = the number of content and media nodes to create +* `depth` = how deep the trees created will be +* `locale` (optional, default = "en") = the language that the data will be generated in + +This creates a content and associated media tree (hierarchy). Each content item created is associated +to a media item via a media picker and therefore a relation is created between the two. Each content and +media tree created have the same root node name so it's easy to know which content branch relates to +which media branch. + +All values are generated using the very handy `Bogus` package. + +## Schema + +This will install some schema items: + +* `umbTestDataContent` Document Type. __TIP__: If you want to delete all of the content data generated with this tool, just delete this content type +* `UmbracoTestDataContent.RTE` Data Type +* `UmbracoTestDataContent.MediaPicker` Data Type +* `UmbracoTestDataContent.Text` Data Type + +For media, the normal folder and image is used + +## Media + +This does not upload physical files, it just uses a randomized online image as the `umbracoFile` value. +This works when viewing the media item in the media section and the image will show up and with recent changes this will also work +when editing content to view the thumbnail for the picked media. diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index a2dfa0ffd2..96701c1c4e 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -126,6 +126,10 @@ {52ac0ba8-a60e-4e36-897b-e8b97a54ed1c} Umbraco.ModelsBuilder.Embedded + + {fb5676ed-7a69-492c-b802-e7b24144c0fc} + Umbraco.TestData + {651e1350-91b6-44b7-bd60-7207006d7003} Umbraco.Web diff --git a/src/Umbraco.Web.UI/web.Template.Debug.config b/src/Umbraco.Web.UI/web.Template.Debug.config index ff42f098f7..0026f23514 100644 --- a/src/Umbraco.Web.UI/web.Template.Debug.config +++ b/src/Umbraco.Web.UI/web.Template.Debug.config @@ -21,6 +21,11 @@ + + + + + diff --git a/src/umbraco.sln b/src/umbraco.sln index ba9df633bb..a747f21d19 100644 --- a/src/umbraco.sln +++ b/src/umbraco.sln @@ -104,6 +104,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "IssueTemplates", "IssueTemp EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Umbraco.ModelsBuilder.Embedded", "Umbraco.ModelsBuilder.Embedded\Umbraco.ModelsBuilder.Embedded.csproj", "{52AC0BA8-A60E-4E36-897B-E8B97A54ED1C}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Umbraco.TestData", "Umbraco.TestData\Umbraco.TestData.csproj", "{FB5676ED-7A69-492C-B802-E7B24144C0FC}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -140,6 +142,10 @@ Global {52AC0BA8-A60E-4E36-897B-E8B97A54ED1C}.Debug|Any CPU.Build.0 = Debug|Any CPU {52AC0BA8-A60E-4E36-897B-E8B97A54ED1C}.Release|Any CPU.ActiveCfg = Release|Any CPU {52AC0BA8-A60E-4E36-897B-E8B97A54ED1C}.Release|Any CPU.Build.0 = Release|Any CPU + {FB5676ED-7A69-492C-B802-E7B24144C0FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FB5676ED-7A69-492C-B802-E7B24144C0FC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FB5676ED-7A69-492C-B802-E7B24144C0FC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FB5676ED-7A69-492C-B802-E7B24144C0FC}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -152,6 +158,7 @@ Global {53594E5B-64A2-4545-8367-E3627D266AE8} = {FD962632-184C-4005-A5F3-E705D92FC645} {3A33ADC9-C6C0-4DB1-A613-A9AF0210DF3D} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} {C7311C00-2184-409B-B506-52A5FAEA8736} = {FD962632-184C-4005-A5F3-E705D92FC645} + {FB5676ED-7A69-492C-B802-E7B24144C0FC} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {7A0F2E34-D2AF-4DAB-86A0-7D7764B3D0EC} From be38b9639188689765632ea71bb9c83c3d22103c Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 21 Jan 2020 13:52:53 +1100 Subject: [PATCH 183/202] fixes up the AddSegmentData endpoint --- src/Umbraco.TestData/SegmentTestController.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.TestData/SegmentTestController.cs b/src/Umbraco.TestData/SegmentTestController.cs index bcfdfc4ac4..33badbbb55 100644 --- a/src/Umbraco.TestData/SegmentTestController.cs +++ b/src/Umbraco.TestData/SegmentTestController.cs @@ -62,12 +62,18 @@ namespace Umbraco.TestData if (content == null) return Content($"No content found by id {contentId}"); - if (!content.HasProperty(propertyAlias)) - return Content($"The content by id {contentId} does not contain a property with alias {propertyAlias}"); + if (propertyAlias.IsNullOrWhiteSpace() || !content.HasProperty(propertyAlias)) + return Content($"The content by id {contentId} does not contain a property with alias {propertyAlias ?? "null"}"); if (content.ContentType.VariesByCulture() && culture.IsNullOrWhiteSpace()) return Content($"The content by id {contentId} varies by culture but no culture was specified"); + if (value.IsNullOrWhiteSpace()) + return Content("'value' cannot be null"); + + if (segment.IsNullOrWhiteSpace()) + return Content("'segment' cannot be null"); + content.SetValue(propertyAlias, value, culture, segment); Services.ContentService.Save(content); From 466f8ca18584f1ed4c02eb4c93b0d1604927cbc8 Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Tue, 21 Jan 2020 07:56:51 +0000 Subject: [PATCH 184/202] V8: Email Marketing Opt In (#7366) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Enable BackOfficeTours to have a bool to hide them in the help drawer * New hidden tour to display the email marketing option on login to backoffice * Update to tourService to use the new bool property of hidden to show/hide the tour in the help drawer * AngularJS Resource to call the Azure Function EmailService proxy code - currently set to DEV * New method on userService.addUserToEmailMarketing that in turn calls the new emailMarketingResource * New AngularJS view & controller in the tour step to deal with user clicking yes/accept to the email opt-in * Modifies the init script to auto launch the hidden email marketing tour at login If it has been accepted or dismissed before we then try to launch the original intro tour * Only show the email marketing tour when the intro tour has been dismissed or completed and will appear for one time only the next time you login * When using X to close email tour, it does not disable and never show it again but just closes it, similar to intro tour * Adds new localStorageService key for 'emailMarketingTourShown' to prevent the tour being shown again in the same logged in session, if you refresh the backoffice in your browser * Update URL to email function * Adding new COMA copy for email marketing tour - needs fine tuning pixel pushing from Niels L * Prettified layout of e-mail marketing promotion tour * fixing whitespace * text=auto * adding xml to gitattributes * Ensures the email tour is not shown if you dismiss the intro tour and manually refresh the page Co-authored-by: Niels Lyngsø --- .gitattributes | 16 ++++--- .../resources/emailmarketing.resource.js | 34 ++++++++++++++ .../src/common/services/tour.service.js | 12 +++-- .../src/common/services/user.service.js | 7 ++- src/Umbraco.Web.UI.Client/src/init.js | 30 +++++++++++-- src/Umbraco.Web.UI.Client/src/less/belle.less | 2 + .../less/components/application/umb-tour.less | 14 ++++++ .../less/components/umbemailmarketing.less | 44 +++++++++++++++++++ .../src/main.controller.js | 7 ++- .../emails/emails.controller.js | 24 ++++++++++ .../umbEmailMarketing/emails/emails.html | 26 +++++++++++ .../components/application/umb-tour.html | 2 +- .../Umbraco/js/main.controller.js | 7 ++- .../BackOfficeTours/getting-started.json | 18 ++++++++ src/Umbraco.Web/Models/BackOfficeTour.cs | 9 ++++ 15 files changed, 236 insertions(+), 16 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/common/resources/emailmarketing.resource.js create mode 100644 src/Umbraco.Web.UI.Client/src/less/components/umbemailmarketing.less create mode 100644 src/Umbraco.Web.UI.Client/src/views/common/tours/umbEmailMarketing/emails/emails.controller.js create mode 100644 src/Umbraco.Web.UI.Client/src/views/common/tours/umbEmailMarketing/emails/emails.html diff --git a/.gitattributes b/.gitattributes index a664be3a85..c8987ade67 100644 --- a/.gitattributes +++ b/.gitattributes @@ -13,7 +13,7 @@ *.png binary *.gif binary -*.cs text=auto diff=csharp +*.cs text=auto diff=csharp *.vb text=auto *.c text=auto *.cpp text=auto @@ -41,9 +41,13 @@ *.fs text=auto *.fsx text=auto *.hs text=auto +*.json text=auto +*.xml text=auto -*.csproj text=auto merge=union -*.vbproj text=auto merge=union -*.fsproj text=auto merge=union -*.dbproj text=auto merge=union -*.sln text=auto eol=crlf merge=union +*.csproj text=auto merge=union +*.vbproj text=auto merge=union +*.fsproj text=auto merge=union +*.dbproj text=auto merge=union +*.sln text=auto eol=crlf merge=union + +*.gitattributes text=auto diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/emailmarketing.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/emailmarketing.resource.js new file mode 100644 index 0000000000..4ac56ad13b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/resources/emailmarketing.resource.js @@ -0,0 +1,34 @@ +/** + * @ngdoc service + * @name umbraco.resources.emailMarketingResource + * @description Used to add a backoffice user to Umbraco's email marketing system, if user opts in + * + * + **/ +function emailMarketingResource($http, umbRequestHelper) { + + // LOCAL + // http://localhost:7071/api/EmailProxy + + // LIVE + // https://emailcollector.umbraco.io/api/EmailProxy + + const emailApiUrl = 'https://emailcollector.umbraco.io/api/EmailProxy'; + + //the factory object returned + return { + + postAddUserToEmailMarketing: (user) => { + return umbRequestHelper.resourcePromise( + $http.post(emailApiUrl, + { + name: user.name, + email: user.email, + usergroup: user.userGroups // [ "admin", "sensitiveData" ] + }), + 'Failed to add user to email marketing list'); + } + }; +} + +angular.module('umbraco.resources').factory('emailMarketingResource', emailMarketingResource); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/tour.service.js b/src/Umbraco.Web.UI.Client/src/common/services/tour.service.js index e102da5d34..62af17146c 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/tour.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/tour.service.js @@ -147,7 +147,10 @@ group.groupOrder = item.groupOrder } groupExists = true; - group.tours.push(item) + + if(item.hidden === false){ + group.tours.push(item); + } } }); @@ -157,8 +160,11 @@ if(item.groupOrder) { newGroup.groupOrder = item.groupOrder } - newGroup.tours.push(item); - groupedTours.push(newGroup); + + if(item.hidden === false){ + newGroup.tours.push(item); + groupedTours.push(newGroup); + } } }); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/user.service.js b/src/Umbraco.Web.UI.Client/src/common/services/user.service.js index 7723c8f4bb..afd7b606e7 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/user.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/user.service.js @@ -1,5 +1,5 @@ angular.module('umbraco.services') - .factory('userService', function ($rootScope, eventsService, $q, $location, requestRetryQueue, authResource, $timeout, angularHelper) { + .factory('userService', function ($rootScope, eventsService, $q, $location, requestRetryQueue, authResource, emailMarketingResource, $timeout, angularHelper) { var currentUser = null; var lastUserId = null; @@ -262,6 +262,11 @@ angular.module('umbraco.services') /** Called whenever a server request is made that contains a x-umb-user-seconds response header for which we can update the user's remaining timeout seconds */ setUserTimeout: function (newTimeout) { setUserTimeoutInternal(newTimeout); + }, + + /** Calls out to a Remote Azure Function to deal with email marketing service */ + addUserToEmailMarketing: (user) => { + return emailMarketingResource.postAddUserToEmailMarketing(user); } }; diff --git a/src/Umbraco.Web.UI.Client/src/init.js b/src/Umbraco.Web.UI.Client/src/init.js index 7d199c5c4f..d5c5166d21 100644 --- a/src/Umbraco.Web.UI.Client/src/init.js +++ b/src/Umbraco.Web.UI.Client/src/init.js @@ -1,6 +1,6 @@ /** Executed when the application starts, binds to events and set global state */ -app.run(['$rootScope', '$route', '$location', 'urlHelper', 'navigationService', 'appState', 'assetsService', 'eventsService', '$cookies', 'tourService', - function ($rootScope, $route, $location, urlHelper, navigationService, appState, assetsService, eventsService, $cookies, tourService) { +app.run(['$rootScope', '$route', '$location', 'urlHelper', 'navigationService', 'appState', 'assetsService', 'eventsService', '$cookies', 'tourService', 'localStorageService', + function ($rootScope, $route, $location, urlHelper, navigationService, appState, assetsService, eventsService, $cookies, tourService, localStorageService) { //This sets the default jquery ajax headers to include our csrf token, we // need to user the beforeSend method because our token changes per user/login so @@ -23,11 +23,35 @@ app.run(['$rootScope', '$route', '$location', 'urlHelper', 'navigationService', appReady(data); tourService.registerAllTours().then(function () { - // Auto start intro tour + + // Start intro tour tourService.getTourByAlias("umbIntroIntroduction").then(function (introTour) { // start intro tour if it hasn't been completed or disabled if (introTour && introTour.disabled !== true && introTour.completed !== true) { tourService.startTour(introTour); + localStorageService.set("introTourShown", true); + } + else { + + const introTourShown = localStorageService.get("introTourShown"); + if(!introTourShown){ + // Go & show email marketing tour (ONLY when intro tour is completed or been dismissed) + tourService.getTourByAlias("umbEmailMarketing").then(function (emailMarketingTour) { + // Only show the email marketing tour one time - dismissing it or saying no will make sure it never appears again + // Unless invoked from tourService JS Client code explicitly. + // Accepted mails = Completed and Declicned mails = Disabled + if (emailMarketingTour && emailMarketingTour.disabled !== true && emailMarketingTour.completed !== true) { + + // Only show the email tour once per logged in session + // The localstorage key is removed on logout or user session timeout + const emailMarketingTourShown = localStorageService.get("emailMarketingTourShown"); + if(!emailMarketingTourShown){ + tourService.startTour(emailMarketingTour); + localStorageService.set("emailMarketingTourShown", true); + } + } + }); + } } }); }); diff --git a/src/Umbraco.Web.UI.Client/src/less/belle.less b/src/Umbraco.Web.UI.Client/src/less/belle.less index b5e032f9fb..0921f46aac 100644 --- a/src/Umbraco.Web.UI.Client/src/less/belle.less +++ b/src/Umbraco.Web.UI.Client/src/less/belle.less @@ -194,6 +194,8 @@ @import "components/contextdialogs/umb-dialog-datatype-delete.less"; +@import "components/umbemailmarketing.less"; + // Utilities @import "utilities/layout/_display.less"; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/application/umb-tour.less b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-tour.less index 42403c65b1..bf2f030cea 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/application/umb-tour.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-tour.less @@ -119,3 +119,17 @@ border: none; padding: 0; } + +.umb-tour__popover--promotion { + width: 800px; + min-height: 400px; + padding: 40px; + border-radius: @baseBorderRadius * 2; + .umb-tour-step__close { + top: 40px; + right: 40px; + } + a { + text-decoration: underline; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umbemailmarketing.less b/src/Umbraco.Web.UI.Client/src/less/components/umbemailmarketing.less new file mode 100644 index 0000000000..f4b3183045 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/umbemailmarketing.less @@ -0,0 +1,44 @@ +.umb-email-marketing { + + h2 { + font-weight: 800; + max-width: 26ex; + margin-top: 20px; + } + + .layout { + display: flex; + align-items: center; + align-content: stretch; + + .primary { + flex-basis: 50%; + padding-right: 40px; + padding-top: 20px; + padding-bottom: 20px; + .notice { + color: @gray-5; + font-style: italic; + a { + color: @gray-5; + &:hover { + color: @ui-action-type-hover; + } + } + } + } + + .secondary { + flex-basis: 50%; + svg { + height: 200px; + width: 100%; + margin-top: -60px; + } + } + } + + .cta { + text-align: right; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/main.controller.js b/src/Umbraco.Web.UI.Client/src/main.controller.js index 93870f8a56..883907d1dc 100644 --- a/src/Umbraco.Web.UI.Client/src/main.controller.js +++ b/src/Umbraco.Web.UI.Client/src/main.controller.js @@ -67,13 +67,18 @@ function MainController($scope, $location, appState, treeService, notificationsS }; var evts = []; - + //when a user logs out or timesout evts.push(eventsService.on("app.notAuthenticated", function (evt, data) { $scope.authenticated = null; $scope.user = null; const isTimedOut = data && data.isTimedOut ? true : false; $scope.showLoginScreen(isTimedOut); + + // Remove the localstorage items for tours shown + // Means that when next logged in they can be re-shown if not already dismissed etc + localStorageService.remove("emailMarketingTourShown"); + localStorageService.remove("introTourShown"); })); evts.push(eventsService.on("app.userRefresh", function(evt) { diff --git a/src/Umbraco.Web.UI.Client/src/views/common/tours/umbEmailMarketing/emails/emails.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/tours/umbEmailMarketing/emails/emails.controller.js new file mode 100644 index 0000000000..8ecc737278 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/tours/umbEmailMarketing/emails/emails.controller.js @@ -0,0 +1,24 @@ +(function () { + "use strict"; + + function EmailsController($scope, userService) { + + var vm = this; + + vm.optIn = function() { + // Get the current user in backoffice + userService.getCurrentUser().then(function(user){ + // Send this user along to opt in + // It's a fire & forget - not sure we need to check the response + userService.addUserToEmailMarketing(user); + }); + + // Mark Tour as complete + // This is also can help us indicate that the user accepted + // Where disabled is set if user closes modal or chooses NO + $scope.model.completeTour(); + } + } + + angular.module("umbraco").controller("Umbraco.Tours.UmbEmailMarketing.EmailsController", EmailsController); +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/tours/umbEmailMarketing/emails/emails.html b/src/Umbraco.Web.UI.Client/src/views/common/tours/umbEmailMarketing/emails/emails.html new file mode 100644 index 0000000000..887624ed05 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/tours/umbEmailMarketing/emails/emails.html @@ -0,0 +1,26 @@ +
+ + + +

{{ model.currentStep.title }}

+ +
+ +
+
+
+ + +
+ paperplane +
+
+ +
+ + +
+ +
+ +
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-tour.html b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-tour.html index 5dd56941a9..e358d75b9e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-tour.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-tour.html @@ -4,7 +4,7 @@
-
+
diff --git a/src/Umbraco.Web.UI/Umbraco/js/main.controller.js b/src/Umbraco.Web.UI/Umbraco/js/main.controller.js index 93870f8a56..883907d1dc 100644 --- a/src/Umbraco.Web.UI/Umbraco/js/main.controller.js +++ b/src/Umbraco.Web.UI/Umbraco/js/main.controller.js @@ -67,13 +67,18 @@ function MainController($scope, $location, appState, treeService, notificationsS }; var evts = []; - + //when a user logs out or timesout evts.push(eventsService.on("app.notAuthenticated", function (evt, data) { $scope.authenticated = null; $scope.user = null; const isTimedOut = data && data.isTimedOut ? true : false; $scope.showLoginScreen(isTimedOut); + + // Remove the localstorage items for tours shown + // Means that when next logged in they can be re-shown if not already dismissed etc + localStorageService.remove("emailMarketingTourShown"); + localStorageService.remove("introTourShown"); })); evts.push(eventsService.on("app.userRefresh", function(evt) { diff --git a/src/Umbraco.Web.UI/config/BackOfficeTours/getting-started.json b/src/Umbraco.Web.UI/config/BackOfficeTours/getting-started.json index e300e6562e..7b3f2a2184 100644 --- a/src/Umbraco.Web.UI/config/BackOfficeTours/getting-started.json +++ b/src/Umbraco.Web.UI/config/BackOfficeTours/getting-started.json @@ -1,4 +1,22 @@ [ + { + "name": "Email Marketing", + "alias": "umbEmailMarketing", + "group": "Email Marketing", + "groupOrder": 10, + "hidden": true, + "requiredSections": [ + "content" + ], + "steps": [ + { + "title": "Do you want to stay updated on everything Umbraco?", + "content": "

Thank you for using Umbraco! Would you like to stay up-to-date with Umbraco product updates, security advisories, community news and special offers? Sign up for our newsletter and never miss out on the latest Umbraco news.

By signing up, you agree that we can use your info according to our privacy policy.

", + "view": "emails", + "type": "promotion" + } + ] + }, { "name": "Introduction", "alias": "umbIntroIntroduction", diff --git a/src/Umbraco.Web/Models/BackOfficeTour.cs b/src/Umbraco.Web/Models/BackOfficeTour.cs index d5987ec5bc..7391765193 100644 --- a/src/Umbraco.Web/Models/BackOfficeTour.cs +++ b/src/Umbraco.Web/Models/BackOfficeTour.cs @@ -16,16 +16,25 @@ namespace Umbraco.Web.Models [DataMember(Name = "name")] public string Name { get; set; } + [DataMember(Name = "alias")] public string Alias { get; set; } + [DataMember(Name = "group")] public string Group { get; set; } + [DataMember(Name = "groupOrder")] public int GroupOrder { get; set; } + + [DataMember(Name = "hidden")] + public bool Hidden { get; set; } + [DataMember(Name = "allowDisable")] public bool AllowDisable { get; set; } + [DataMember(Name = "requiredSections")] public List RequiredSections { get; set; } + [DataMember(Name = "steps")] public BackOfficeTourStep[] Steps { get; set; } From 0d12852cd8520dd21c4e33136a93d4747506d816 Mon Sep 17 00:00:00 2001 From: abi Date: Tue, 21 Jan 2020 09:07:10 +0000 Subject: [PATCH 185/202] Address Shannon comments Add code documentation, return empty enumerable instead of allocate empty list --- .../Search/IUmbracoTreeSearcherFields.cs | 16 +++++++++++++++- .../Search/UmbracoTreeSearcherFields.cs | 7 +++---- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Web/Search/IUmbracoTreeSearcherFields.cs b/src/Umbraco.Web/Search/IUmbracoTreeSearcherFields.cs index eaa5d743a1..c5a6c53d19 100644 --- a/src/Umbraco.Web/Search/IUmbracoTreeSearcherFields.cs +++ b/src/Umbraco.Web/Search/IUmbracoTreeSearcherFields.cs @@ -2,12 +2,26 @@ using System.Collections.Generic; namespace Umbraco.Web.Search { + /// + /// Used to propagate hardcoded internal Field lists + /// public interface IUmbracoTreeSearcherFields { + /// + /// Propagate list of searchable fields for all node types + /// IEnumerable GetBackOfficeFields(); + /// + /// Propagate list of searchable fields for Members + /// IEnumerable GetBackOfficeMembersFields(); - + /// + /// Propagate list of searchable fields for Media + /// IEnumerable GetBackOfficeMediaFields(); + /// + /// Propagate list of searchable fields for Documents + /// IEnumerable GetBackOfficeDocumentFields(); } } diff --git a/src/Umbraco.Web/Search/UmbracoTreeSearcherFields.cs b/src/Umbraco.Web/Search/UmbracoTreeSearcherFields.cs index d1c663024b..f90d7bc6b6 100644 --- a/src/Umbraco.Web/Search/UmbracoTreeSearcherFields.cs +++ b/src/Umbraco.Web/Search/UmbracoTreeSearcherFields.cs @@ -1,8 +1,8 @@ using System.Collections.Generic; +using System.Linq; using Umbraco.Examine; -using Umbraco.Web.Search; -namespace Umbraco.Web +namespace Umbraco.Web.Search { public class UmbracoTreeSearcherFields : IUmbracoTreeSearcherFields { @@ -23,10 +23,9 @@ namespace Umbraco.Web { return _backOfficeMediaFields; } - private IReadOnlyList _backOfficeDocumentFields = new List (); public IEnumerable GetBackOfficeDocumentFields() { - return _backOfficeDocumentFields; + return Enumerable.Empty(); } } } From 163c1f7028eb419eda33e8a0c0548f66fd1ee8e2 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Tue, 21 Jan 2020 19:16:07 +0100 Subject: [PATCH 186/202] Fix automatic merge gone wrong --- src/Umbraco.Web.UI.Client/src/common/services/tour.service.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/tour.service.js b/src/Umbraco.Web.UI.Client/src/common/services/tour.service.js index 91b41cc68d..1c2da43814 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/tour.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/tour.service.js @@ -168,12 +168,12 @@ } } - }); + } deferred.resolve(groupedTours); }); return deferred.promise; - } + }); /** * @ngdoc method From 80309011847af5664d3765d824dd2ec3e93985c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 22 Jan 2020 08:36:52 +0100 Subject: [PATCH 187/202] Fix mistakes made in commit a4a6b77 --- .../src/common/services/tour.service.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/tour.service.js b/src/Umbraco.Web.UI.Client/src/common/services/tour.service.js index 1c2da43814..8fcab445b3 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/tour.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/tour.service.js @@ -162,18 +162,19 @@ newGroup.groupOrder = item.groupOrder; } - if(item.hidden === false){ - newGroup.tours.push(item); - groupedTours.push(newGroup); + if(item.hidden === false){ + newGroup.tours.push(item); + groupedTours.push(newGroup); + } } } - } + }); deferred.resolve(groupedTours); }); return deferred.promise; - }); + } /** * @ngdoc method From 10f15a7648c939c0469892bf04f9ae6e5930ef6f Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Wed, 22 Jan 2020 11:10:28 +0100 Subject: [PATCH 188/202] Remove duplicate VariesBySegment overload after merge --- src/Umbraco.Core/ContentVariationExtensions.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/Umbraco.Core/ContentVariationExtensions.cs b/src/Umbraco.Core/ContentVariationExtensions.cs index 8939f7133b..6430c75686 100644 --- a/src/Umbraco.Core/ContentVariationExtensions.cs +++ b/src/Umbraco.Core/ContentVariationExtensions.cs @@ -27,11 +27,6 @@ namespace Umbraco.Core /// public static bool VariesByNothing(this IContentTypeBase contentType) => contentType.Variations.VariesByNothing(); - /// - /// Determines whether the content type varies by segment. - /// - public static bool VariesBySegment(this ISimpleContentType contentType) => contentType.Variations.VariesBySegment(); - /// /// Determines whether the content type is invariant. /// From 6c2e1b29fd7221a259be97e39eb0ee3ae0f657cd Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Wed, 22 Jan 2020 11:10:56 +0100 Subject: [PATCH 189/202] Use SetVariesBy extension methods instead of SetFlag/UnsetFlag --- src/Umbraco.TestData/SegmentTestController.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.TestData/SegmentTestController.cs b/src/Umbraco.TestData/SegmentTestController.cs index 33badbbb55..650820760e 100644 --- a/src/Umbraco.TestData/SegmentTestController.cs +++ b/src/Umbraco.TestData/SegmentTestController.cs @@ -30,9 +30,8 @@ namespace Umbraco.TestData if (ct.Variations.VariesBySegment()) return Content($"The document type {alias} already allows segments, nothing has been changed"); - ct.Variations = ct.Variations.SetFlag(ContentVariation.Segment); - - propType.Variations = propType.Variations.SetFlag(ContentVariation.Segment); + ct.SetVariesBy(ContentVariation.Segment); + propType.SetVariesBy(ContentVariation.Segment); Services.ContentTypeService.Save(ct); return Content($"The document type {alias} and property type {propertyTypeAlias} now allows segments"); @@ -50,7 +49,7 @@ namespace Umbraco.TestData if (!ct.VariesBySegment()) return Content($"The document type {alias} does not allow segments, nothing has been changed"); - ct.Variations = ct.Variations.UnsetFlag(ContentVariation.Segment); + ct.SetVariesBy(ContentVariation.Segment, false); Services.ContentTypeService.Save(ct); return Content($"The document type {alias} no longer allows segments"); From ac675d14e952788881e4980e0f20c57aae967d9c Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Wed, 22 Jan 2020 11:13:49 +0100 Subject: [PATCH 190/202] Fixes build errors --- src/Umbraco.Core/ContentVariationExtensions.cs | 13 ++++--------- src/Umbraco.TestData/SegmentTestController.cs | 8 ++++---- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/src/Umbraco.Core/ContentVariationExtensions.cs b/src/Umbraco.Core/ContentVariationExtensions.cs index 8939f7133b..4fc6f418aa 100644 --- a/src/Umbraco.Core/ContentVariationExtensions.cs +++ b/src/Umbraco.Core/ContentVariationExtensions.cs @@ -30,6 +30,10 @@ namespace Umbraco.Core /// /// Determines whether the content type varies by segment. /// + /// The content type. + /// + /// A value indicating whether the content type varies by segment. + /// public static bool VariesBySegment(this ISimpleContentType contentType) => contentType.Variations.VariesBySegment(); /// @@ -122,15 +126,6 @@ namespace Umbraco.Core /// public static bool VariesByCulture(this ContentVariation variation) => (variation & ContentVariation.Culture) > 0; - /// - /// Determines whether the content type varies by segment. - /// - /// The content type. - /// - /// A value indicating whether the content type varies by segment. - /// - public static bool VariesBySegment(this ISimpleContentType contentType) => contentType.Variations.VariesBySegment(); - /// /// Determines whether the content type varies by segment. /// diff --git a/src/Umbraco.TestData/SegmentTestController.cs b/src/Umbraco.TestData/SegmentTestController.cs index 33badbbb55..c78d12ab0e 100644 --- a/src/Umbraco.TestData/SegmentTestController.cs +++ b/src/Umbraco.TestData/SegmentTestController.cs @@ -30,9 +30,9 @@ namespace Umbraco.TestData if (ct.Variations.VariesBySegment()) return Content($"The document type {alias} already allows segments, nothing has been changed"); - ct.Variations = ct.Variations.SetFlag(ContentVariation.Segment); - - propType.Variations = propType.Variations.SetFlag(ContentVariation.Segment); + ct.Variations = ct.Variations.SetVariesBy(ContentVariation.Segment); + + propType.Variations = propType.Variations.SetVariesBy(ContentVariation.Segment); Services.ContentTypeService.Save(ct); return Content($"The document type {alias} and property type {propertyTypeAlias} now allows segments"); @@ -50,7 +50,7 @@ namespace Umbraco.TestData if (!ct.VariesBySegment()) return Content($"The document type {alias} does not allow segments, nothing has been changed"); - ct.Variations = ct.Variations.UnsetFlag(ContentVariation.Segment); + ct.Variations = ct.Variations.SetVariesBy(ContentVariation.Nothing); Services.ContentTypeService.Save(ct); return Content($"The document type {alias} no longer allows segments"); From 1cc6a8cb4511c74ad5d5a96fed9ca1805bf7831a Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Wed, 22 Jan 2020 11:14:31 +0100 Subject: [PATCH 191/202] Rename SetVariesBy to SetFlag on the enum overload --- src/Umbraco.Core/ContentVariationExtensions.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Core/ContentVariationExtensions.cs b/src/Umbraco.Core/ContentVariationExtensions.cs index 6430c75686..29442b78e6 100644 --- a/src/Umbraco.Core/ContentVariationExtensions.cs +++ b/src/Umbraco.Core/ContentVariationExtensions.cs @@ -234,7 +234,7 @@ namespace Umbraco.Core /// /// This method does not support setting the variation to nothing. /// - public static void SetVariesBy(this IContentTypeBase contentType, ContentVariation variation, bool value = true) => contentType.Variations = contentType.Variations.SetVariesBy(variation, value); + public static void SetVariesBy(this IContentTypeBase contentType, ContentVariation variation, bool value = true) => contentType.Variations = contentType.Variations.SetFlag(variation, value); /// /// Sets or removes the property type variation depending on the specified value. @@ -245,7 +245,7 @@ namespace Umbraco.Core /// /// This method does not support setting the variation to nothing. /// - public static void SetVariesBy(this PropertyType propertyType, ContentVariation variation, bool value = true) => propertyType.Variations = propertyType.Variations.SetVariesBy(variation, value); + public static void SetVariesBy(this PropertyType propertyType, ContentVariation variation, bool value = true) => propertyType.Variations = propertyType.Variations.SetFlag(variation, value); /// /// Returns the variations with the variation set or removed depending on the specified value. @@ -259,7 +259,7 @@ namespace Umbraco.Core /// /// This method does not support setting the variation to nothing. /// - public static ContentVariation SetVariesBy(this ContentVariation variations, ContentVariation variation, bool value = true) + public static ContentVariation SetFlag(this ContentVariation variations, ContentVariation variation, bool value = true) { return value ? variations | variation // Set flag using bitwise logical OR From d61090a225c9e99606f096c18971e5beb5c6c9a7 Mon Sep 17 00:00:00 2001 From: Dave Woestenborghs Date: Tue, 14 Jan 2020 14:02:10 +0100 Subject: [PATCH 192/202] #7452 set save state correctly for variant when setting schedule publishing --- .../src/common/directives/components/content/edit.controller.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js index cab71842b1..7429f60e0d 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js @@ -791,6 +791,7 @@ $scope.content.variants[i].expireDate = model.variants[i].expireDate; $scope.content.variants[i].releaseDateFormatted = model.variants[i].releaseDateFormatted; $scope.content.variants[i].expireDateFormatted = model.variants[i].expireDateFormatted; + $scope.content.variants[i].save = model.variants[i].save; } model.submitButtonState = "busy"; From 201927580c299b223c5eb17f798e1aaa263944aa Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 23 Jan 2020 16:23:27 +1100 Subject: [PATCH 193/202] Fixing accidental api signature breaking change --- .../Persistence/SqlSyntax/SqlServerSyntaxProvider.cs | 6 +++--- src/Umbraco.Core/Runtime/SqlMainDomLock.cs | 11 ++++++++--- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs b/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs index 6dda49cd5e..bb50fa98a1 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs @@ -251,10 +251,10 @@ where tbl.[name]=@0 and col.[name]=@1;", tableName, columnName) public override void WriteLock(IDatabase db, params int[] lockIds) { - WriteLock(db, 1800, lockIds); + WriteLock(db, TimeSpan.FromMilliseconds(1800), lockIds); } - public void WriteLock(IDatabase db, int millisecondsTimeout, params int[] lockIds) + public void WriteLock(IDatabase db, TimeSpan timeout, params int[] lockIds) { // soon as we get Database, a transaction is started @@ -265,7 +265,7 @@ where tbl.[name]=@0 and col.[name]=@1;", tableName, columnName) // *not* using a unique 'WHERE IN' query here because the *order* of lockIds is important to avoid deadlocks foreach (var lockId in lockIds) { - db.Execute($"SET LOCK_TIMEOUT {millisecondsTimeout};"); + db.Execute($"SET LOCK_TIMEOUT {timeout.TotalMilliseconds};"); var i = db.Execute(@"UPDATE umbracoLock WITH (REPEATABLEREAD) SET value = (CASE WHEN (value=1) THEN -1 ELSE 1 END) WHERE id=@id", new { id = lockId }); if (i == 0) // ensure we are actually locking! throw new ArgumentException($"LockObject with id={lockId} does not exist."); diff --git a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs index 31f8b36ed0..418e4c13fc 100644 --- a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs +++ b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs @@ -39,6 +39,11 @@ namespace Umbraco.Core.Runtime public async Task AcquireLockAsync(int millisecondsTimeout) { + if (!(_dbFactory.SqlContext.SqlSyntax is SqlServerSyntaxProvider sqlServerSyntaxProvider)) + throw new NotSupportedException("SqlMainDomLock is only supported for Sql Server"); + + _sqlServerSyntax = sqlServerSyntaxProvider; + _logger.Debug("Acquiring lock..."); var db = GetDatabase(); @@ -52,7 +57,7 @@ namespace Umbraco.Core.Runtime try { // wait to get a write lock - _sqlServerSyntax.WriteLock(db, millisecondsTimeout, Constants.Locks.MainDom); + _sqlServerSyntax.WriteLock(db, TimeSpan.FromMilliseconds(millisecondsTimeout), Constants.Locks.MainDom); } catch (Exception ex) { @@ -136,7 +141,7 @@ namespace Umbraco.Core.Runtime db.BeginTransaction(IsolationLevel.ReadCommitted); // get a read lock - _sqlServerSyntax.ReadLock(db, Constants.Locks.MainDom); + _dbFactory.SqlContext.SqlSyntax.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 @@ -209,7 +214,7 @@ namespace Umbraco.Core.Runtime db.BeginTransaction(IsolationLevel.ReadCommitted); // get a read lock - _sqlServerSyntax.ReadLock(db, Constants.Locks.MainDom); + _dbFactory.SqlContext.SqlSyntax.ReadLock(db, Constants.Locks.MainDom); // the row var mainDomRows = db.Fetch("SELECT * FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey }); From 8d7ed7dd32b6ff0bb415d126b56f4c0a57b49491 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 23 Jan 2020 16:26:13 +1100 Subject: [PATCH 194/202] adds const fixing naming --- src/Umbraco.Core/Runtime/SqlMainDomLock.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs index 418e4c13fc..37db2899c6 100644 --- a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs +++ b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs @@ -17,6 +17,7 @@ namespace Umbraco.Core.Runtime { private string _lockId; private const string MainDomKey = "Umbraco.Core.Runtime.SqlMainDom"; + private const string UpdatedSuffix = "_updated"; private readonly ILogger _logger; private IUmbracoDatabase _db; private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); @@ -197,7 +198,7 @@ namespace Umbraco.Core.Runtime /// private Task WaitForExistingAsync(string tempId, int millisecondsTimeout) { - var updatedTempId = tempId + "_updated"; + var updatedTempId = tempId + UpdatedSuffix; return Task.Run(() => { @@ -343,11 +344,11 @@ namespace Umbraco.Core.Runtime private bool IsLockTimeoutException(Exception exception) => exception is SqlException sqlException && sqlException.Number == 1222; #region IDisposable Support - private bool disposedValue = false; // To detect redundant calls + private bool _disposedValue = false; // To detect redundant calls protected virtual void Dispose(bool disposing) { - if (!disposedValue) + if (!_disposedValue) { if (disposing) { @@ -374,7 +375,7 @@ namespace Umbraco.Core.Runtime if (_mainDomChanging) { _logger.Debug("Releasing MainDom, updating row, new application is booting."); - db.Execute("UPDATE umbracoKeyValue SET [value] = [value] + '_updated' WHERE [key] = @key", new { key = MainDomKey }); + db.Execute($"UPDATE umbracoKeyValue SET [value] = [value] + '{UpdatedSuffix}' WHERE [key] = @key", new { key = MainDomKey }); } else { @@ -396,7 +397,7 @@ namespace Umbraco.Core.Runtime } } - disposedValue = true; + _disposedValue = true; } } From 52b93bfc9c0c11a1225383681de5f6a716b67730 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 23 Jan 2020 18:33:55 +1100 Subject: [PATCH 195/202] ensures only _sqlServerSyntax is used --- src/Umbraco.Core/Runtime/SqlMainDomLock.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs index 37db2899c6..4433a8e307 100644 --- a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs +++ b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs @@ -142,7 +142,7 @@ namespace Umbraco.Core.Runtime db.BeginTransaction(IsolationLevel.ReadCommitted); // get a read lock - _dbFactory.SqlContext.SqlSyntax.ReadLock(db, Constants.Locks.MainDom); + _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 @@ -215,7 +215,7 @@ namespace Umbraco.Core.Runtime db.BeginTransaction(IsolationLevel.ReadCommitted); // get a read lock - _dbFactory.SqlContext.SqlSyntax.ReadLock(db, Constants.Locks.MainDom); + _sqlServerSyntax.ReadLock(db, Constants.Locks.MainDom); // the row var mainDomRows = db.Fetch("SELECT * FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey }); From aaf53921eb1a09aa6297599432e79ab37d16119d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 23 Jan 2020 15:15:48 +0100 Subject: [PATCH 196/202] upgrade to Angular 1.7.9 + npm 6.13.6 --- src/Umbraco.Web.UI.Client/package-lock.json | 143 +++++++++++--------- src/Umbraco.Web.UI.Client/package.json | 4 +- 2 files changed, 78 insertions(+), 69 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/package-lock.json b/src/Umbraco.Web.UI.Client/package-lock.json index d69ef62f16..dd58b5ca18 100644 --- a/src/Umbraco.Web.UI.Client/package-lock.json +++ b/src/Umbraco.Web.UI.Client/package-lock.json @@ -1099,9 +1099,9 @@ "dev": true }, "angular": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/angular/-/angular-1.7.5.tgz", - "integrity": "sha512-760183yxtGzni740IBTieNuWLtPNAoMqvmC0Z62UoU0I3nqk+VJuO3JbQAXOyvo3Oy/ZsdNQwrSTh/B0OQZjNw==" + "version": "1.7.9", + "resolved": "https://registry.npmjs.org/angular/-/angular-1.7.9.tgz", + "integrity": "sha512-5se7ZpcOtu0MBFlzGv5dsM1quQDoDeUTwZrWjGtTNA7O88cD8TEk5IEKCTDa3uECV9XnvKREVUr7du1ACiWGFQ==" }, "angular-animate": { "version": "1.7.5", @@ -9324,9 +9324,9 @@ "dev": true }, "nouislider": { - "version": "14.0.2", - "resolved": "https://registry.npmjs.org/nouislider/-/nouislider-14.0.2.tgz", - "integrity": "sha512-N4AQStV4frh+XcLUwMI/hZpBP6tRboDE/4LZ7gzfxMVXFi/2J9URphnm40Ff4KEyrAVGSGaWApvljoMzTNWBlA==" + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/nouislider/-/nouislider-14.1.1.tgz", + "integrity": "sha512-3/+Z/pTBoWoJf2YXSEWRmS27LW2XxOBmGEzkPyRzB/J6QvL+0mS3QwcQp0SmWhgO5CMzbSxPmb1lDDD4HP12bg==" }, "now-and-later": { "version": "2.0.1", @@ -9338,9 +9338,9 @@ } }, "npm": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/npm/-/npm-6.12.0.tgz", - "integrity": "sha512-juj5VkB3/k+PWbJUnXD7A/8oc8zLusDnK/sV9PybSalsbOVOTIp5vSE0rz5rQ7BsmUgQS47f/L2GYQnWXaKgnQ==", + "version": "6.13.6", + "resolved": "https://registry.npmjs.org/npm/-/npm-6.13.6.tgz", + "integrity": "sha512-NomC08kv7HIl1FOyLOe9Hp89kYsOsvx52huVIJ7i8hFW8Xp65lDwe/8wTIrh9q9SaQhA8hTrfXPh3BEL3TmMpw==", "requires": { "JSONStream": "^1.3.5", "abbrev": "~1.1.1", @@ -9348,12 +9348,12 @@ "ansistyles": "~0.1.3", "aproba": "^2.0.0", "archy": "~1.0.0", - "bin-links": "^1.1.3", + "bin-links": "^1.1.6", "bluebird": "^3.5.5", "byte-size": "^5.0.1", "cacache": "^12.0.3", "call-limit": "^1.1.1", - "chownr": "^1.1.2", + "chownr": "^1.1.3", "ci-info": "^2.0.0", "cli-columns": "^3.1.2", "cli-table3": "^0.5.1", @@ -9369,9 +9369,9 @@ "find-npm-prefix": "^1.0.2", "fs-vacuum": "~1.2.10", "fs-write-stream-atomic": "~1.0.10", - "gentle-fs": "^2.2.1", + "gentle-fs": "^2.3.0", "glob": "^7.1.4", - "graceful-fs": "^4.2.2", + "graceful-fs": "^4.2.3", "has-unicode": "~2.0.1", "hosted-git-info": "^2.8.5", "iferr": "^1.0.2", @@ -9384,7 +9384,7 @@ "is-cidr": "^3.0.0", "json-parse-better-errors": "^1.0.2", "lazy-property": "~1.0.0", - "libcipm": "^4.0.4", + "libcipm": "^4.0.7", "libnpm": "^3.0.1", "libnpmaccess": "^3.0.2", "libnpmhook": "^5.0.3", @@ -9418,25 +9418,25 @@ "npm-install-checks": "^3.0.2", "npm-lifecycle": "^3.1.4", "npm-package-arg": "^6.1.1", - "npm-packlist": "^1.4.4", + "npm-packlist": "^1.4.7", "npm-pick-manifest": "^3.0.2", "npm-profile": "^4.0.2", - "npm-registry-fetch": "^4.0.0", + "npm-registry-fetch": "^4.0.2", "npm-user-validate": "~1.0.0", "npmlog": "~4.1.2", "once": "~1.4.0", "opener": "^1.5.1", "osenv": "^0.1.5", - "pacote": "^9.5.8", + "pacote": "^9.5.12", "path-is-inside": "~1.0.2", "promise-inflight": "~1.0.1", "qrcode-terminal": "^0.12.0", "query-string": "^6.8.2", "qw": "~1.0.1", "read": "~1.0.7", - "read-cmd-shim": "^1.0.4", + "read-cmd-shim": "^1.0.5", "read-installed": "~4.0.3", - "read-package-json": "^2.1.0", + "read-package-json": "^2.1.1", "read-package-tree": "^5.3.1", "readable-stream": "^3.4.0", "readdir-scoped-modules": "^1.1.0", @@ -9451,7 +9451,7 @@ "sorted-union-stream": "~2.1.3", "ssri": "^6.0.1", "stringify-package": "^1.0.1", - "tar": "^4.4.12", + "tar": "^4.4.13", "text-table": "~0.2.0", "tiny-relative-date": "^1.3.0", "uid-number": "0.0.6", @@ -9459,7 +9459,7 @@ "unique-filename": "^1.1.1", "unpipe": "~1.0.0", "update-notifier": "^2.5.0", - "uuid": "^3.3.2", + "uuid": "^3.3.3", "validate-npm-package-license": "^3.0.4", "validate-npm-package-name": "~3.0.0", "which": "^1.3.1", @@ -9607,13 +9607,14 @@ } }, "bin-links": { - "version": "1.1.3", + "version": "1.1.6", "bundled": true, "requires": { "bluebird": "^3.5.3", "cmd-shim": "^3.0.0", - "gentle-fs": "^2.0.1", + "gentle-fs": "^2.3.0", "graceful-fs": "^4.1.15", + "npm-normalize-package-bin": "^1.0.0", "write-file-atomic": "^2.3.0" } }, @@ -9705,7 +9706,7 @@ } }, "chownr": { - "version": "1.1.2", + "version": "1.1.3", "bundled": true }, "ci-info": { @@ -10266,7 +10267,7 @@ }, "dependencies": { "minipass": { - "version": "2.8.6", + "version": "2.9.0", "bundled": true, "requires": { "safe-buffer": "^5.1.2", @@ -10362,11 +10363,12 @@ "bundled": true }, "gentle-fs": { - "version": "2.2.1", + "version": "2.3.0", "bundled": true, "requires": { "aproba": "^1.1.2", "chownr": "^1.1.2", + "cmd-shim": "^3.0.3", "fs-vacuum": "^1.2.10", "graceful-fs": "^4.1.11", "iferr": "^0.1.5", @@ -10448,7 +10450,7 @@ } }, "graceful-fs": { - "version": "4.2.2", + "version": "4.2.3", "bundled": true }, "har-schema": { @@ -10508,7 +10510,7 @@ } }, "https-proxy-agent": { - "version": "2.2.2", + "version": "2.2.4", "bundled": true, "requires": { "agent-base": "^4.3.0", @@ -10534,7 +10536,7 @@ "bundled": true }, "ignore-walk": { - "version": "3.0.1", + "version": "3.0.3", "bundled": true, "requires": { "minimatch": "^3.0.4" @@ -10748,7 +10750,7 @@ } }, "libcipm": { - "version": "4.0.4", + "version": "4.0.7", "bundled": true, "requires": { "bin-links": "^1.1.2", @@ -11017,14 +11019,14 @@ } }, "make-fetch-happen": { - "version": "5.0.0", + "version": "5.0.2", "bundled": true, "requires": { "agentkeepalive": "^3.4.1", "cacache": "^12.0.0", "http-cache-semantics": "^3.8.1", "http-proxy-agent": "^2.1.0", - "https-proxy-agent": "^2.2.1", + "https-proxy-agent": "^2.2.3", "lru-cache": "^5.1.1", "mississippi": "^3.0.0", "node-fetch-npm": "^2.0.2", @@ -11070,27 +11072,23 @@ "version": "0.0.8", "bundled": true }, - "minipass": { - "version": "2.3.3", + "minizlib": { + "version": "1.3.3", "bundled": true, "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" + "minipass": "^2.9.0" }, "dependencies": { - "yallist": { - "version": "3.0.2", - "bundled": true + "minipass": { + "version": "2.9.0", + "bundled": true, + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } } } }, - "minizlib": { - "version": "1.2.2", - "bundled": true, - "requires": { - "minipass": "^2.2.1" - } - }, "mississippi": { "version": "3.0.0", "bundled": true, @@ -11215,8 +11213,11 @@ } }, "npm-bundled": { - "version": "1.0.6", - "bundled": true + "version": "1.1.1", + "bundled": true, + "requires": { + "npm-normalize-package-bin": "^1.0.1" + } }, "npm-cache-filename": { "version": "1.0.2", @@ -11247,6 +11248,10 @@ "version": "1.2.1", "bundled": true }, + "npm-normalize-package-bin": { + "version": "1.0.1", + "bundled": true + }, "npm-package-arg": { "version": "6.1.1", "bundled": true, @@ -11258,7 +11263,7 @@ } }, "npm-packlist": { - "version": "1.4.4", + "version": "1.4.7", "bundled": true, "requires": { "ignore-walk": "^3.0.1", @@ -11284,7 +11289,7 @@ } }, "npm-registry-fetch": { - "version": "4.0.0", + "version": "4.0.2", "bundled": true, "requires": { "JSONStream": "^1.3.4", @@ -11292,7 +11297,14 @@ "figgy-pudding": "^3.4.1", "lru-cache": "^5.1.1", "make-fetch-happen": "^5.0.0", - "npm-package-arg": "^6.1.0" + "npm-package-arg": "^6.1.0", + "safe-buffer": "^5.2.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.0", + "bundled": true + } } }, "npm-run-path": { @@ -11409,7 +11421,7 @@ } }, "pacote": { - "version": "9.5.8", + "version": "9.5.12", "bundled": true, "requires": { "bluebird": "^3.5.3", @@ -11426,6 +11438,7 @@ "mississippi": "^3.0.0", "mkdirp": "^0.5.1", "normalize-package-data": "^2.4.0", + "npm-normalize-package-bin": "^1.0.0", "npm-package-arg": "^6.1.0", "npm-packlist": "^1.1.12", "npm-pick-manifest": "^3.0.0", @@ -11444,7 +11457,7 @@ }, "dependencies": { "minipass": { - "version": "2.3.5", + "version": "2.9.0", "bundled": true, "requires": { "safe-buffer": "^5.1.2", @@ -11644,7 +11657,7 @@ } }, "read-cmd-shim": { - "version": "1.0.4", + "version": "1.0.5", "bundled": true, "requires": { "graceful-fs": "^4.1.2" @@ -11664,14 +11677,14 @@ } }, "read-package-json": { - "version": "2.1.0", + "version": "2.1.1", "bundled": true, "requires": { "glob": "^7.1.1", "graceful-fs": "^4.1.2", "json-parse-better-errors": "^1.0.1", "normalize-package-data": "^2.0.0", - "slash": "^1.0.0" + "npm-normalize-package-bin": "^1.0.0" } }, "read-package-tree": { @@ -11824,24 +11837,20 @@ "version": "3.0.2", "bundled": true }, - "slash": { - "version": "1.0.0", - "bundled": true - }, "slide": { "version": "1.1.6", "bundled": true }, "smart-buffer": { - "version": "4.0.2", + "version": "4.1.0", "bundled": true }, "socks": { - "version": "2.3.2", + "version": "2.3.3", "bundled": true, "requires": { - "ip": "^1.1.5", - "smart-buffer": "4.0.2" + "ip": "1.1.5", + "smart-buffer": "^4.1.0" } }, "socks-proxy-agent": { @@ -12056,7 +12065,7 @@ } }, "tar": { - "version": "4.4.12", + "version": "4.4.13", "bundled": true, "requires": { "chownr": "^1.1.1", @@ -12069,7 +12078,7 @@ }, "dependencies": { "minipass": { - "version": "2.8.6", + "version": "2.9.0", "bundled": true, "requires": { "safe-buffer": "^5.1.2", @@ -12231,7 +12240,7 @@ } }, "uuid": { - "version": "3.3.2", + "version": "3.3.3", "bundled": true }, "validate-npm-package-license": { diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index 0f02aba5e2..d60da6e790 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -11,7 +11,7 @@ }, "dependencies": { "ace-builds": "1.4.2", - "angular": "1.7.5", + "angular": "^1.7.9", "angular-animate": "1.7.5", "angular-aria": "1.7.5", "angular-chart.js": "^1.1.1", @@ -39,7 +39,7 @@ "moment": "2.22.2", "ng-file-upload": "12.2.13", "nouislider": "14.1.1", - "npm": "6.12.0", + "npm": "^6.13.6", "signalr": "2.4.0", "spectrum-colorpicker": "1.8.0", "tinymce": "4.9.2", From 7fc0f01887cf57b3a6e393155939a56e15bf6314 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 23 Jan 2020 15:23:32 +0100 Subject: [PATCH 197/202] fixed versions for angular + npm --- src/Umbraco.Web.UI.Client/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index d60da6e790..beff2b45d1 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -11,7 +11,7 @@ }, "dependencies": { "ace-builds": "1.4.2", - "angular": "^1.7.9", + "angular": "1.7.9", "angular-animate": "1.7.5", "angular-aria": "1.7.5", "angular-chart.js": "^1.1.1", @@ -39,7 +39,7 @@ "moment": "2.22.2", "ng-file-upload": "12.2.13", "nouislider": "14.1.1", - "npm": "^6.13.6", + "npm": "6.13.6", "signalr": "2.4.0", "spectrum-colorpicker": "1.8.0", "tinymce": "4.9.2", From 384ecbe22a217fd9487ee48d04a18a130280ab70 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Fri, 24 Jan 2020 08:25:03 +0100 Subject: [PATCH 198/202] Removed reference from Umbraco.Web.UI to Umbraco.TestData --- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index ead0cd1592..8f0db76c1d 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -113,12 +113,6 @@ - - - {fb5676ed-7a69-492c-b802-e7b24144c0fc} - Umbraco.TestData - - {31785bc3-256c-4613-b2f5-a1b0bdded8c1} @@ -132,10 +126,6 @@ {52ac0ba8-a60e-4e36-897b-e8b97a54ed1c} Umbraco.ModelsBuilder.Embedded - - {fb5676ed-7a69-492c-b802-e7b24144c0fc} - Umbraco.TestData - {651e1350-91b6-44b7-bd60-7207006d7003} Umbraco.Web @@ -439,4 +429,4 @@ - + \ No newline at end of file From 05fe604e221b9eae4c0b63bc5538347b5988fbbf Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Fri, 24 Jan 2020 09:19:29 +0100 Subject: [PATCH 199/202] https://github.com/umbraco/Umbraco-CMS/issues/7469 Renamed extension method to avoid ambiguous signature --- .../PublishedElementExtensions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.ModelsBuilder.Embedded/PublishedElementExtensions.cs b/src/Umbraco.ModelsBuilder.Embedded/PublishedElementExtensions.cs index 29429ba74f..8a0a688942 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/PublishedElementExtensions.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/PublishedElementExtensions.cs @@ -17,7 +17,7 @@ namespace Umbraco.Web /// /// Gets the value of a property. /// - public static TValue Value(this TModel model, Expression> property, string culture = null, string segment = null, Fallback fallback = default, TValue defaultValue = default) + public static TValue ValueFor(this TModel model, Expression> property, string culture = null, string segment = null, Fallback fallback = default, TValue defaultValue = default) where TModel : IPublishedElement { var alias = GetAlias(model, property); @@ -45,7 +45,7 @@ namespace Umbraco.Web var attribute = member.GetCustomAttribute(); if (attribute == null) throw new InvalidOperationException("Property is not marked with ImplementPropertyType attribute."); - + return attribute.Alias; } } From 7bdf97c4313aca414e243553cdd9b48434b9b037 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Fri, 24 Jan 2020 09:20:16 +0100 Subject: [PATCH 200/202] https://github.com/umbraco/Umbraco-CMS/issues/7469 Unsealed class to allow override --- .../ImplementPropertyTypeAttribute.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.ModelsBuilder.Embedded/ImplementPropertyTypeAttribute.cs b/src/Umbraco.ModelsBuilder.Embedded/ImplementPropertyTypeAttribute.cs index 0359c49654..b7b2695a08 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/ImplementPropertyTypeAttribute.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/ImplementPropertyTypeAttribute.cs @@ -7,7 +7,7 @@ namespace Umbraco.ModelsBuilder.Embedded /// /// And therefore it should not be generated. [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)] - public sealed class ImplementPropertyTypeAttribute : Attribute + public class ImplementPropertyTypeAttribute : Attribute { public ImplementPropertyTypeAttribute(string alias) { From 4cf78d6ef049b58e513da4bf9ccc95180b36eebb Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Fri, 24 Jan 2020 09:22:34 +0100 Subject: [PATCH 201/202] Bump version to 8.5.3 --- src/SolutionInfo.cs | 4 ++-- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/SolutionInfo.cs b/src/SolutionInfo.cs index 9dba3adef1..841e986f15 100644 --- a/src/SolutionInfo.cs +++ b/src/SolutionInfo.cs @@ -18,5 +18,5 @@ using System.Resources; [assembly: AssemblyVersion("8.0.0")] // these are FYI and changed automatically -[assembly: AssemblyFileVersion("8.5.2")] -[assembly: AssemblyInformationalVersion("8.5.2")] +[assembly: AssemblyFileVersion("8.5.3")] +[assembly: AssemblyInformationalVersion("8.5.3")] diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 0c80ccb70b..e12eb8d19b 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -345,9 +345,9 @@ False True - 8520 + 8530 / - http://localhost:8520 + http://localhost:8530 False False From 8bf457a88d3e3ca632339af9b727e93f38d544de Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Fri, 24 Jan 2020 09:10:54 +0000 Subject: [PATCH 202/202] Set v8/dev to 8.7.0 with powershell .\build setumbracoversion 8.7.0 --- src/SolutionInfo.cs | 4 ++-- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/SolutionInfo.cs b/src/SolutionInfo.cs index 363677b826..a0d969c1b8 100644 --- a/src/SolutionInfo.cs +++ b/src/SolutionInfo.cs @@ -18,5 +18,5 @@ using System.Resources; [assembly: AssemblyVersion("8.0.0")] // these are FYI and changed automatically -[assembly: AssemblyFileVersion("8.6.0")] -[assembly: AssemblyInformationalVersion("8.6.0")] +[assembly: AssemblyFileVersion("8.7.0")] +[assembly: AssemblyInformationalVersion("8.7.0")] diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index a2dfa0ffd2..9914528e58 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -345,9 +345,9 @@ False True - 8600 + 8700 / - http://localhost:8600 + http://localhost:8700 False False @@ -429,4 +429,4 @@ - + \ No newline at end of file