From 8746d4c13dde6a77136233661b03298d27ba4c8a Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Wed, 9 Mar 2022 14:59:55 +0100 Subject: [PATCH 01/10] Small typos fixed.. --- .../references/umbtrackedreferences.component.js | 10 +++++----- src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/references/umbtrackedreferences.component.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/references/umbtrackedreferences.component.js index 000e87146c..bf1891bf0f 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/references/umbtrackedreferences.component.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/references/umbtrackedreferences.component.js @@ -16,17 +16,17 @@ function onInit() { - vm.referencesTitle = this.hideNoneDependencies ? "The following items depends on this" : "Referenced by the following items"; - vm.referencedDescendantsTitle = this.hideNoneDependencies ? "The following descending items has dependencies" : "The following descending items are referenced"; - + vm.referencesTitle = this.hideNoneDependencies ? "The following items depend on this" : "Referenced by the following items"; + vm.referencedDescendantsTitle = this.hideNoneDependencies ? "The following descending items has dependencies" : "The following descendant items have dependencies"; + localizationService.localize(this.hideNoneDependencies ? "references_labelDependsOnThis" : "references_labelUsedByItems").then(function (value) { vm.referencesTitle = value; }); - + localizationService.localize(this.hideNoneDependencies ? "references_labelDependentDescendants" : "references_labelUsedDescendants").then(function (value) { vm.referencedDescendantsTitle = value; }); - + vm.descendantsOptions = {}; vm.descendantsOptions.filterMustBeIsDependency = this.hideNoneDependencies; vm.hasReferencesInDescendants = false; 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 e056647ebc..bed9f227dd 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml @@ -2559,12 +2559,12 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Referenced by the following Member Types Referenced by Referenced by the following items - The following items depends on this + The following items depend on this Referenced by the following Documents Referenced by the following Members Referenced by the following Media The following items are referenced - The following descending items are referenced + The following descendant items have dependencies The following descending items has dependencies One or more of this item's descendants is being referenced in a media item. One or more of this item's descendants is being referenced in a content item. From 1ab0bb9254c2edfd32238532e7fecbc04542056c Mon Sep 17 00:00:00 2001 From: Paul Johnson Date: Thu, 10 Mar 2022 13:59:19 +0000 Subject: [PATCH 02/10] fix additional legacy password formats (#12120) * Added failing test to demo issue. * Handle old machine key default. --- .../Security/LegacyPasswordSecurity.cs | 10 +++ .../Security/UmbracoPasswordHasherTests.cs | 81 +++++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/UmbracoPasswordHasherTests.cs diff --git a/src/Umbraco.Core/Security/LegacyPasswordSecurity.cs b/src/Umbraco.Core/Security/LegacyPasswordSecurity.cs index ca3045a4de..35528a48ca 100644 --- a/src/Umbraco.Core/Security/LegacyPasswordSecurity.cs +++ b/src/Umbraco.Core/Security/LegacyPasswordSecurity.cs @@ -209,11 +209,21 @@ namespace Umbraco.Cms.Core.Security { // This is for the v6-v8 hashing algorithm if (algorithm.InvariantEquals(Constants.Security.AspNetUmbraco8PasswordHashAlgorithmName)) + { return true; + } + + // Default validation value for old machine keys (switched to HMACSHA256 aspnet 4 https://docs.microsoft.com/en-us/aspnet/whitepapers/aspnet4/breaking-changes) + if (algorithm.InvariantEquals("SHA1")) + { + return true; + } // This is for the <= v4 hashing algorithm if (IsLegacySHA1Algorithm(algorithm)) + { return true; + } return false; } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/UmbracoPasswordHasherTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/UmbracoPasswordHasherTests.cs new file mode 100644 index 0000000000..aa6bb4156b --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/UmbracoPasswordHasherTests.cs @@ -0,0 +1,81 @@ +using AutoFixture.NUnit3; +using Microsoft.AspNetCore.Identity; +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Serialization; +using Umbraco.Cms.Tests.UnitTests.AutoFixture; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security +{ + [TestFixture] + public class UmbracoPasswordHasherTests + { + // Technically MD5, HMACSHA384 & HMACSHA512 were also possible but opt in as opposed to historic defaults. + [Test] + [InlineAutoMoqData("HMACSHA256", "Umbraco9Rocks!", "uB/pLEhhe1W7EtWMv/pSgg==1y8+aso9+h3AKRtJXlVYeg2TZKJUr64hccj82ZZ7Ksk=")] // Actually HMACSHA256 + [InlineAutoMoqData("HMACSHA256", "Umbraco9Rocks!", "t0U8atXTX/efNCtTafukwZeIpr8=")] // v4 site legacy password, with incorrect algorithm specified in database actually HMACSHA1 with password used as key. + [InlineAutoMoqData("SHA1", "Umbraco9Rocks!", "6tZGfG9NTxJJYp19Fac9og==zzRggqANxhb+CbD/VabEt8cIde8=")] // When SHA1 is set on machine key. + public void VerifyHashedPassword_WithValidLegacyPasswordHash_ReturnsSuccessRehashNeeded( + string algorithm, + string providedPassword, + string hashedPassword, + [Frozen] IJsonSerializer jsonSerializer, + TestUserStub aUser, + UmbracoPasswordHasher sut) + { + Mock.Get(jsonSerializer) + .Setup(x => x.Deserialize(It.IsAny())) + .Returns(new PersistedPasswordSettings{ HashAlgorithm = algorithm }); + + var result = sut.VerifyHashedPassword(aUser, hashedPassword, providedPassword); + + Assert.AreEqual(PasswordVerificationResult.SuccessRehashNeeded, result); + } + + + [Test] + [InlineAutoMoqData("PBKDF2.ASPNETCORE.V3", "Umbraco9Rocks!", "AQAAAAEAACcQAAAAEDCrYcnIhHKr38yuchsDu6AFqqmLNvRooKObV25GC1LC1tLY+gWGU4xNug0lc17PHA==")] + public void VerifyHashedPassword_WithValidModernPasswordHash_ReturnsSuccess( + string algorithm, + string providedPassword, + string hashedPassword, + [Frozen] IJsonSerializer jsonSerializer, + TestUserStub aUser, + UmbracoPasswordHasher sut) + { + Mock.Get(jsonSerializer) + .Setup(x => x.Deserialize(It.IsAny())) + .Returns(new PersistedPasswordSettings { HashAlgorithm = algorithm }); + + var result = sut.VerifyHashedPassword(aUser, hashedPassword, providedPassword); + + Assert.AreEqual(PasswordVerificationResult.Success, result); + } + + [Test] + [InlineAutoMoqData("HMACSHA256", "Umbraco9Rocks!", "aB/cDeFaBcDefAbcD/EfaB==1y8+aso9+h3AKRtJXlVYeg2TZKJUr64hccj82ZZ7Ksk=")] + public void VerifyHashedPassword_WithIncorrectPassword_ReturnsFailed( + string algorithm, + string providedPassword, + string hashedPassword, + [Frozen] IJsonSerializer jsonSerializer, + TestUserStub aUser, + UmbracoPasswordHasher sut) + { + Mock.Get(jsonSerializer) + .Setup(x => x.Deserialize(It.IsAny())) + .Returns(new PersistedPasswordSettings { HashAlgorithm = algorithm }); + + var result = sut.VerifyHashedPassword(aUser, hashedPassword, providedPassword); + + Assert.AreEqual(PasswordVerificationResult.Failed, result); + } + + public class TestUserStub : UmbracoIdentityUser + { + public TestUserStub() => PasswordConfig = "not null or empty"; + } + } +} From c7e45ae13a84422b04f789851a8b657c3b34f7dc Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Date: Thu, 17 Mar 2022 14:52:15 +0100 Subject: [PATCH 03/10] Encode path (#12132) Co-authored-by: Nikolaj Geisle --- .../Controllers/ImagesController.cs | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Web.BackOffice/Controllers/ImagesController.cs b/src/Umbraco.Web.BackOffice/Controllers/ImagesController.cs index a10d524c03..327884689e 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ImagesController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ImagesController.cs @@ -1,9 +1,11 @@ using System; using System.IO; +using System.Web; using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Media; using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Web.Common.Attributes; using Umbraco.Extensions; using Constants = Umbraco.Cms.Core.Constants; @@ -53,20 +55,25 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// public IActionResult GetResized(string imagePath, int width) { - var ext = Path.GetExtension(imagePath); - + // We have to use HttpUtility to encode the path here, for non-ASCII characters + // We cannot use the WebUtility, as we only want to encode the path, and not the entire string + var encodedImagePath = HttpUtility.UrlPathEncode(imagePath); + + + var ext = Path.GetExtension(encodedImagePath); + // check if imagePath is local to prevent open redirect - if (!Uri.IsWellFormedUriString(imagePath, UriKind.Relative)) + if (!Uri.IsWellFormedUriString(encodedImagePath, UriKind.Relative)) { return Unauthorized(); } - + // we need to check if it is an image by extension if (_imageUrlGenerator.IsSupportedImageFormat(ext) == false) { return NotFound(); } - + // redirect to ImageProcessor thumbnail with rnd generated from last modified time of original media file DateTimeOffset? imageLastModified = null; try @@ -82,7 +89,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers } var rnd = imageLastModified.HasValue ? $"&rnd={imageLastModified:yyyyMMddHHmmss}" : null; - var imageUrl = _imageUrlGenerator.GetImageUrl(new ImageUrlGenerationOptions(imagePath) + var imageUrl = _imageUrlGenerator.GetImageUrl(new ImageUrlGenerationOptions(encodedImagePath) { Width = width, ImageCropMode = ImageCropMode.Max, From a9daab5a155b85534421847f397a47dd956959df Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Date: Mon, 21 Mar 2022 08:24:19 +0100 Subject: [PATCH 04/10] Merge pull request #12139 from umbraco/v9/bugfix/track-media-items-picked-as-macro-params Fix media tracking of items added via macro parameters in RTE and Grid --- src/Umbraco.Core/Cache/MacroCacheRefresher.cs | 9 +- .../Repositories/IMacroWithAliasRepository.cs | 14 ++ .../MultipleContentPickerParameterEditor.cs | 16 +- .../MultipleMediaPickerParameterEditor.cs | 20 ++- ...ultiplePickerParamateterValueEditorBase.cs | 61 +++++++ src/Umbraco.Core/Services/IMacroService.cs | 9 +- .../Services/IMacroWithAliasService.cs | 17 ++ .../UmbracoBuilder.Repositories.cs | 1 - .../UmbracoBuilder.Services.cs | 3 +- .../Repositories/Implement/MacroRepository.cs | 36 +++- .../PropertyEditors/GridPropertyEditor.cs | 58 ++++++- .../PropertyEditors/RichTextPropertyEditor.cs | 53 +++++- .../Services/Implement/MacroService.cs | 27 ++- .../Templates/HtmlMacroParameterParser.cs | 155 ++++++++++++++++++ .../Templates/IHtmlMacroParameterParser.cs | 26 +++ .../Services/MacroServiceTests.cs | 17 +- .../SnapDictionaryTests.cs | 1 + 17 files changed, 489 insertions(+), 34 deletions(-) create mode 100644 src/Umbraco.Core/Persistence/Repositories/IMacroWithAliasRepository.cs create mode 100644 src/Umbraco.Core/PropertyEditors/ParameterEditors/MultiplePickerParamateterValueEditorBase.cs create mode 100644 src/Umbraco.Core/Services/IMacroWithAliasService.cs create mode 100644 src/Umbraco.Infrastructure/Templates/HtmlMacroParameterParser.cs create mode 100644 src/Umbraco.Infrastructure/Templates/IHtmlMacroParameterParser.cs diff --git a/src/Umbraco.Core/Cache/MacroCacheRefresher.cs b/src/Umbraco.Core/Cache/MacroCacheRefresher.cs index 77550b81d1..e1b65e2a32 100644 --- a/src/Umbraco.Core/Cache/MacroCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/MacroCacheRefresher.cs @@ -46,6 +46,11 @@ namespace Umbraco.Cms.Core.Cache { var payloads = Deserialize(json); + Refresh(payloads); + } + + public override void Refresh(JsonPayload[] payloads) + { foreach (var payload in payloads) { foreach (var alias in GetCacheKeysForAlias(payload.Alias)) @@ -55,11 +60,13 @@ namespace Umbraco.Cms.Core.Cache if (macroRepoCache) { macroRepoCache.Result.Clear(RepositoryCacheKeys.GetKey(payload.Id)); + macroRepoCache.Result.Clear(RepositoryCacheKeys.GetKey(payload.Alias)); // Repository caching of macro definition by alias } } - base.Refresh(json); + base.Refresh(payloads); } + #endregion #region Json diff --git a/src/Umbraco.Core/Persistence/Repositories/IMacroWithAliasRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IMacroWithAliasRepository.cs new file mode 100644 index 0000000000..f6cd27ad60 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Repositories/IMacroWithAliasRepository.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Core.Persistence.Repositories +{ + [Obsolete("This interface will be merged with IMacroRepository in Umbraco 11")] + public interface IMacroWithAliasRepository : IMacroRepository + { + IMacro GetByAlias(string alias); + + IEnumerable GetAllByAlias(string[] aliases); + } +} diff --git a/src/Umbraco.Core/PropertyEditors/ParameterEditors/MultipleContentPickerParameterEditor.cs b/src/Umbraco.Core/PropertyEditors/ParameterEditors/MultipleContentPickerParameterEditor.cs index 048ad40ac0..4d88431e7c 100644 --- a/src/Umbraco.Core/PropertyEditors/ParameterEditors/MultipleContentPickerParameterEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/ParameterEditors/MultipleContentPickerParameterEditor.cs @@ -1,5 +1,5 @@ -using Microsoft.Extensions.Logging; -using Umbraco.Cms.Core.Hosting; +using Umbraco.Cms.Core.IO; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; @@ -28,5 +28,17 @@ namespace Umbraco.Cms.Core.PropertyEditors.ParameterEditors DefaultConfiguration.Add("minNumber",0 ); DefaultConfiguration.Add("maxNumber", 0); } + + protected override IDataValueEditor CreateValueEditor() => DataValueEditorFactory.Create(Attribute); + + internal class MultipleContentPickerParamateterValueEditor : MultiplePickerParamateterValueEditorBase + { + public MultipleContentPickerParamateterValueEditor(ILocalizedTextService localizedTextService, IShortStringHelper shortStringHelper, IJsonSerializer jsonSerializer, IIOHelper ioHelper, DataEditorAttribute attribute, IEntityService entityService) : base(localizedTextService, shortStringHelper, jsonSerializer, ioHelper, attribute, entityService) + { + } + + public override string UdiEntityType { get; } = Constants.UdiEntityType.Document; + public override UmbracoObjectTypes UmbracoObjectType { get; } = UmbracoObjectTypes.Document; + } } } diff --git a/src/Umbraco.Core/PropertyEditors/ParameterEditors/MultipleMediaPickerParameterEditor.cs b/src/Umbraco.Core/PropertyEditors/ParameterEditors/MultipleMediaPickerParameterEditor.cs index d8f74b1b28..dfdd6f9b9c 100644 --- a/src/Umbraco.Core/PropertyEditors/ParameterEditors/MultipleMediaPickerParameterEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/ParameterEditors/MultipleMediaPickerParameterEditor.cs @@ -1,5 +1,9 @@ -using Microsoft.Extensions.Logging; -using Umbraco.Cms.Core.Hosting; +using System; +using System.Collections.Generic; +using System.Reflection.Metadata; +using Umbraco.Cms.Core.IO; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Editors; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; @@ -26,5 +30,17 @@ namespace Umbraco.Cms.Core.PropertyEditors.ParameterEditors { DefaultConfiguration.Add("multiPicker", "1"); } + + protected override IDataValueEditor CreateValueEditor() => DataValueEditorFactory.Create(Attribute); + + internal class MultipleMediaPickerPropertyValueEditor : MultiplePickerParamateterValueEditorBase + { + public MultipleMediaPickerPropertyValueEditor(ILocalizedTextService localizedTextService, IShortStringHelper shortStringHelper, IJsonSerializer jsonSerializer, IIOHelper ioHelper, DataEditorAttribute attribute, IEntityService entityService) : base(localizedTextService, shortStringHelper, jsonSerializer, ioHelper, attribute, entityService) + { + } + + public override string UdiEntityType { get; } = Constants.UdiEntityType.Media; + public override UmbracoObjectTypes UmbracoObjectType { get; } = UmbracoObjectTypes.Media; + } } } diff --git a/src/Umbraco.Core/PropertyEditors/ParameterEditors/MultiplePickerParamateterValueEditorBase.cs b/src/Umbraco.Core/PropertyEditors/ParameterEditors/MultiplePickerParamateterValueEditorBase.cs new file mode 100644 index 0000000000..2c4f27b560 --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/ParameterEditors/MultiplePickerParamateterValueEditorBase.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using Umbraco.Cms.Core.IO; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Editors; +using Umbraco.Cms.Core.Serialization; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Strings; + +namespace Umbraco.Cms.Core.PropertyEditors.ParameterEditors +{ + internal abstract class MultiplePickerParamateterValueEditorBase : DataValueEditor, IDataValueReference + { + private readonly IEntityService _entityService; + + public MultiplePickerParamateterValueEditorBase( + ILocalizedTextService localizedTextService, + IShortStringHelper shortStringHelper, + IJsonSerializer jsonSerializer, + IIOHelper ioHelper, + DataEditorAttribute attribute, + IEntityService entityService) + : base(localizedTextService, shortStringHelper, jsonSerializer, ioHelper, attribute) + { + _entityService = entityService; + } + + public abstract string UdiEntityType { get; } + public abstract UmbracoObjectTypes UmbracoObjectType { get; } + public IEnumerable GetReferences(object value) + { + var asString = value is string str ? str : value?.ToString(); + + if (string.IsNullOrEmpty(asString)) + { + yield break; + } + + foreach (var udiStr in asString.Split(',')) + { + if (UdiParser.TryParse(udiStr, out Udi udi)) + { + yield return new UmbracoEntityReference(udi); + } + + // this is needed to support the legacy case when the multiple media picker parameter editor stores ints not udis + if (int.TryParse(udiStr, out var id)) + { + Attempt guidAttempt = _entityService.GetKey(id, UmbracoObjectType); + Guid guid = guidAttempt.Success ? guidAttempt.Result : Guid.Empty; + + if (guid != Guid.Empty) + { + yield return new UmbracoEntityReference(new GuidUdi(Constants.UdiEntityType.Media, guid)); + } + + } + } + } + } +} diff --git a/src/Umbraco.Core/Services/IMacroService.cs b/src/Umbraco.Core/Services/IMacroService.cs index c4bc34997f..e1eb97ac00 100644 --- a/src/Umbraco.Core/Services/IMacroService.cs +++ b/src/Umbraco.Core/Services/IMacroService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using Umbraco.Cms.Core.Models; @@ -17,13 +17,6 @@ namespace Umbraco.Cms.Core.Services /// An object IMacro GetByAlias(string alias); - ///// - ///// Gets a list all available objects - ///// - ///// Optional array of aliases to limit the results - ///// An enumerable list of objects - //IEnumerable GetAll(params string[] aliases); - IEnumerable GetAll(); IEnumerable GetAll(params int[] ids); diff --git a/src/Umbraco.Core/Services/IMacroWithAliasService.cs b/src/Umbraco.Core/Services/IMacroWithAliasService.cs new file mode 100644 index 0000000000..6e72777bfa --- /dev/null +++ b/src/Umbraco.Core/Services/IMacroWithAliasService.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Core.Services +{ + [Obsolete("This interface will be merged with IMacroService in Umbraco 11")] + public interface IMacroWithAliasService : IMacroService + { + /// + /// Gets a list of available objects by alias. + /// + /// Optional array of aliases to limit the results + /// An enumerable list of objects + IEnumerable GetAll(params string[] aliases); + } +} diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs index 966b54633b..13196c1879 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs @@ -1,7 +1,6 @@ using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Persistence.Repositories; -using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; using Umbraco.Extensions; diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs index 915b815033..157d49fd39 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs @@ -18,9 +18,9 @@ using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.Implement; using Umbraco.Cms.Infrastructure.Packaging; -using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; using Umbraco.Cms.Infrastructure.Services.Implement; +using Umbraco.Cms.Infrastructure.Templates; using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.DependencyInjection @@ -92,6 +92,7 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection builder.Services.AddUnique(); builder.Services.AddSingleton(); builder.Services.AddUnique(); + builder.Services.AddUnique(); return builder; } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MacroRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MacroRepository.cs index 535895e8ed..21638027ea 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MacroRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MacroRepository.cs @@ -18,14 +18,16 @@ using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement { - internal class MacroRepository : EntityRepositoryBase, IMacroRepository + internal class MacroRepository : EntityRepositoryBase, IMacroWithAliasRepository { private readonly IShortStringHelper _shortStringHelper; + private readonly IRepositoryCachePolicy _macroByAliasCachePolicy; public MacroRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IShortStringHelper shortStringHelper) : base(scopeAccessor, cache, logger) { _shortStringHelper = shortStringHelper; + _macroByAliasCachePolicy = new DefaultRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, DefaultOptions); } protected override IMacro PerformGet(int id) @@ -68,6 +70,38 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement return Get(id) != null; } + public IMacro GetByAlias(string alias) + { + return _macroByAliasCachePolicy.Get(alias, PerformGetByAlias, PerformGetAllByAlias); + } + + public IEnumerable GetAllByAlias(string[] aliases) + { + if (aliases.Any() is false) + { + return base.GetMany(); + } + + return _macroByAliasCachePolicy.GetAll(aliases, PerformGetAllByAlias); + } + + private IMacro PerformGetByAlias(string alias) + { + var query = Query().Where(x => x.Alias.Equals(alias)); + return PerformGetByQuery(query).FirstOrDefault(); + } + + private IEnumerable PerformGetAllByAlias(params string[] aliases) + { + if (aliases.Any() is false) + { + return base.GetMany(); + } + + var query = Query().Where(x => aliases.Contains(x.Alias)); + return PerformGetByQuery(query); + } + protected override IEnumerable PerformGetAll(params int[] ids) { return ids.Length > 0 ? ids.Select(Get) : GetAllNoIds(); diff --git a/src/Umbraco.Infrastructure/PropertyEditors/GridPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/GridPropertyEditor.cs index f149757919..c3d8be8f50 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/GridPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/GridPropertyEditor.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Microsoft.Extensions.DependencyInjection; using Newtonsoft.Json; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Media; @@ -14,6 +15,8 @@ using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Core.Templates; +using Umbraco.Cms.Infrastructure.Templates; +using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Extensions; namespace Umbraco.Cms.Core.PropertyEditors @@ -37,6 +40,7 @@ namespace Umbraco.Cms.Core.PropertyEditors private readonly RichTextEditorPastedImages _pastedImages; private readonly HtmlLocalLinkParser _localLinkParser; private readonly IImageUrlGenerator _imageUrlGenerator; + private readonly IHtmlMacroParameterParser _macroParameterParser; public GridPropertyEditor( IDataValueEditorFactory dataValueEditorFactory, @@ -45,7 +49,8 @@ namespace Umbraco.Cms.Core.PropertyEditors RichTextEditorPastedImages pastedImages, HtmlLocalLinkParser localLinkParser, IIOHelper ioHelper, - IImageUrlGenerator imageUrlGenerator) + IImageUrlGenerator imageUrlGenerator, + IHtmlMacroParameterParser macroParameterParser) : base(dataValueEditorFactory) { _backOfficeSecurityAccessor = backOfficeSecurityAccessor; @@ -54,6 +59,20 @@ namespace Umbraco.Cms.Core.PropertyEditors _pastedImages = pastedImages; _localLinkParser = localLinkParser; _imageUrlGenerator = imageUrlGenerator; + _macroParameterParser = macroParameterParser; + } + + [Obsolete("Use the constructor which takes an IHtmlMacroParameterParser instead")] + public GridPropertyEditor( + IDataValueEditorFactory dataValueEditorFactory, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor, + HtmlImageSourceParser imageSourceParser, + RichTextEditorPastedImages pastedImages, + HtmlLocalLinkParser localLinkParser, + IIOHelper ioHelper, + IImageUrlGenerator imageUrlGenerator) + : this (dataValueEditorFactory, backOfficeSecurityAccessor, imageSourceParser, pastedImages, localLinkParser, ioHelper, imageUrlGenerator, StaticServiceProvider.Instance.GetRequiredService()) + { } public override IPropertyIndexValueFactory PropertyIndexValueFactory => new GridPropertyIndexValueFactory(); @@ -74,6 +93,7 @@ namespace Umbraco.Cms.Core.PropertyEditors private readonly RichTextPropertyEditor.RichTextPropertyValueEditor _richTextPropertyValueEditor; private readonly MediaPickerPropertyEditor.MediaPickerPropertyValueEditor _mediaPickerPropertyValueEditor; private readonly IImageUrlGenerator _imageUrlGenerator; + private readonly IHtmlMacroParameterParser _macroParameterParser; public GridPropertyValueEditor( IDataValueEditorFactory dataValueEditorFactory, @@ -85,7 +105,8 @@ namespace Umbraco.Cms.Core.PropertyEditors IShortStringHelper shortStringHelper, IImageUrlGenerator imageUrlGenerator, IJsonSerializer jsonSerializer, - IIOHelper ioHelper) + IIOHelper ioHelper, + IHtmlMacroParameterParser macroParameterParser) : base(localizedTextService, shortStringHelper, jsonSerializer, ioHelper, attribute) { _backOfficeSecurityAccessor = backOfficeSecurityAccessor; @@ -96,6 +117,25 @@ namespace Umbraco.Cms.Core.PropertyEditors _mediaPickerPropertyValueEditor = dataValueEditorFactory.Create(attribute); _imageUrlGenerator = imageUrlGenerator; + _macroParameterParser = macroParameterParser; + } + + [Obsolete("Use the constructor which takes an IHtmlMacroParameterParser instead")] + public GridPropertyValueEditor( + IDataValueEditorFactory dataValueEditorFactory, + DataEditorAttribute attribute, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor, + ILocalizedTextService localizedTextService, + HtmlImageSourceParser imageSourceParser, + RichTextEditorPastedImages pastedImages, + IShortStringHelper shortStringHelper, + IImageUrlGenerator imageUrlGenerator, + IJsonSerializer jsonSerializer, + IIOHelper ioHelper) + : this (dataValueEditorFactory, attribute, backOfficeSecurityAccessor, localizedTextService, + imageSourceParser, pastedImages, shortStringHelper, imageUrlGenerator, jsonSerializer, ioHelper, + StaticServiceProvider.Instance.GetRequiredService()) + { } /// @@ -120,7 +160,7 @@ namespace Umbraco.Cms.Core.PropertyEditors var mediaParent = config?.MediaParentId; var mediaParentId = mediaParent == null ? Guid.Empty : mediaParent.Guid; - var grid = DeserializeGridValue(rawJson, out var rtes, out _); + var grid = DeserializeGridValue(rawJson, out var rtes, out _, out _); var userId = _backOfficeSecurityAccessor?.BackOfficeSecurity?.CurrentUser?.Id ?? Constants.Security.SuperUserId; @@ -154,7 +194,7 @@ namespace Umbraco.Cms.Core.PropertyEditors if (val.IsNullOrWhiteSpace()) return string.Empty; - var grid = DeserializeGridValue(val, out var rtes, out _); + var grid = DeserializeGridValue(val, out var rtes, out _, out _); //process the rte values foreach (var rte in rtes.ToList()) @@ -168,7 +208,7 @@ namespace Umbraco.Cms.Core.PropertyEditors return grid; } - private GridValue DeserializeGridValue(string rawJson, out IEnumerable richTextValues, out IEnumerable mediaValues) + private GridValue DeserializeGridValue(string rawJson, out IEnumerable richTextValues, out IEnumerable mediaValues, out IEnumerable macroValues) { var grid = JsonConvert.DeserializeObject(rawJson); @@ -177,6 +217,9 @@ namespace Umbraco.Cms.Core.PropertyEditors richTextValues = controls.Where(x => x.Editor.Alias.ToLowerInvariant() == "rte"); mediaValues = controls.Where(x => x.Editor.Alias.ToLowerInvariant() == "media"); + // Find all the macros + macroValues = controls.Where(x => x.Editor.Alias.ToLowerInvariant() == "macro"); + return grid; } @@ -192,7 +235,7 @@ namespace Umbraco.Cms.Core.PropertyEditors if (rawJson.IsNullOrWhiteSpace()) yield break; - DeserializeGridValue(rawJson, out var richTextEditorValues, out var mediaValues); + DeserializeGridValue(rawJson, out var richTextEditorValues, out var mediaValues, out var macroValues); foreach (var umbracoEntityReference in richTextEditorValues.SelectMany(x => _richTextPropertyValueEditor.GetReferences(x.Value))) @@ -201,6 +244,9 @@ namespace Umbraco.Cms.Core.PropertyEditors foreach (var umbracoEntityReference in mediaValues.Where(x => x.Value.HasValues) .SelectMany(x => _mediaPickerPropertyValueEditor.GetReferences(x.Value["udi"]))) yield return umbracoEntityReference; + + foreach (var umbracoEntityReference in _macroParameterParser.FindUmbracoEntityReferencesFromGridControlMacros(macroValues)) + yield return umbracoEntityReference; } } } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs index 1cfbc3449e..18c3fe0902 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs @@ -3,8 +3,7 @@ using System; using System.Collections.Generic; -using Microsoft.Extensions.Logging; -using Umbraco.Cms.Core.Hosting; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Media; using Umbraco.Cms.Core.Models; @@ -16,6 +15,8 @@ using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Core.Templates; using Umbraco.Cms.Infrastructure.Examine; using Umbraco.Cms.Infrastructure.Macros; +using Umbraco.Cms.Infrastructure.Templates; +using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Extensions; namespace Umbraco.Cms.Core.PropertyEditors @@ -36,12 +37,13 @@ namespace Umbraco.Cms.Core.PropertyEditors private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; private readonly HtmlImageSourceParser _imageSourceParser; private readonly HtmlLocalLinkParser _localLinkParser; + private readonly IHtmlMacroParameterParser _macroParameterParser; private readonly RichTextEditorPastedImages _pastedImages; private readonly IIOHelper _ioHelper; private readonly IImageUrlGenerator _imageUrlGenerator; /// - /// The constructor will setup the property editor based on the attribute if one is found + /// The constructor will setup the property editor based on the attribute if one is found. /// public RichTextPropertyEditor( IDataValueEditorFactory dataValueEditorFactory, @@ -50,7 +52,8 @@ namespace Umbraco.Cms.Core.PropertyEditors HtmlLocalLinkParser localLinkParser, RichTextEditorPastedImages pastedImages, IIOHelper ioHelper, - IImageUrlGenerator imageUrlGenerator) + IImageUrlGenerator imageUrlGenerator, + IHtmlMacroParameterParser macroParameterParser) : base(dataValueEditorFactory) { _backOfficeSecurityAccessor = backOfficeSecurityAccessor; @@ -59,6 +62,20 @@ namespace Umbraco.Cms.Core.PropertyEditors _pastedImages = pastedImages; _ioHelper = ioHelper; _imageUrlGenerator = imageUrlGenerator; + _macroParameterParser = macroParameterParser; + } + + [Obsolete("Use the constructor which takes an IHtmlMacroParameterParser instead")] + public RichTextPropertyEditor( + IDataValueEditorFactory dataValueEditorFactory, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor, + HtmlImageSourceParser imageSourceParser, + HtmlLocalLinkParser localLinkParser, + RichTextEditorPastedImages pastedImages, + IIOHelper ioHelper, + IImageUrlGenerator imageUrlGenerator) + : this (dataValueEditorFactory, backOfficeSecurityAccessor, imageSourceParser, localLinkParser, pastedImages, ioHelper, imageUrlGenerator, StaticServiceProvider.Instance.GetRequiredService()) + { } /// @@ -79,6 +96,7 @@ namespace Umbraco.Cms.Core.PropertyEditors private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; private readonly HtmlImageSourceParser _imageSourceParser; private readonly HtmlLocalLinkParser _localLinkParser; + private readonly IHtmlMacroParameterParser _macroParameterParser; private readonly RichTextEditorPastedImages _pastedImages; private readonly IImageUrlGenerator _imageUrlGenerator; private readonly IHtmlSanitizer _htmlSanitizer; @@ -94,7 +112,8 @@ namespace Umbraco.Cms.Core.PropertyEditors IImageUrlGenerator imageUrlGenerator, IJsonSerializer jsonSerializer, IIOHelper ioHelper, - IHtmlSanitizer htmlSanitizer) + IHtmlSanitizer htmlSanitizer, + IHtmlMacroParameterParser macroParameterParser) : base(localizedTextService, shortStringHelper, jsonSerializer, ioHelper, attribute) { _backOfficeSecurityAccessor = backOfficeSecurityAccessor; @@ -103,6 +122,26 @@ namespace Umbraco.Cms.Core.PropertyEditors _pastedImages = pastedImages; _imageUrlGenerator = imageUrlGenerator; _htmlSanitizer = htmlSanitizer; + _macroParameterParser = macroParameterParser; + } + + [Obsolete("Use the constructor which takes an HtmlMacroParameterParser instead")] + public RichTextPropertyValueEditor( + DataEditorAttribute attribute, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor, + ILocalizedTextService localizedTextService, + IShortStringHelper shortStringHelper, + HtmlImageSourceParser imageSourceParser, + HtmlLocalLinkParser localLinkParser, + RichTextEditorPastedImages pastedImages, + IImageUrlGenerator imageUrlGenerator, + IJsonSerializer jsonSerializer, + IIOHelper ioHelper, + IHtmlSanitizer htmlSanitizer) + : this(attribute, backOfficeSecurityAccessor, localizedTextService, shortStringHelper, imageSourceParser, + localLinkParser, pastedImages, imageUrlGenerator, jsonSerializer, ioHelper, htmlSanitizer, + StaticServiceProvider.Instance.GetRequiredService()) + { } /// @@ -182,6 +221,10 @@ namespace Umbraco.Cms.Core.PropertyEditors yield return new UmbracoEntityReference(udi); //TODO: Detect Macros too ... but we can save that for a later date, right now need to do media refs + //UPDATE: We are getting the Macros in 'FindUmbracoEntityReferencesFromEmbeddedMacros' - perhaps we just return the macro Udis here too or do they need their own relationAlias? + + foreach (var umbracoEntityReference in _macroParameterParser.FindUmbracoEntityReferencesFromEmbeddedMacros(asString)) + yield return umbracoEntityReference; } } diff --git a/src/Umbraco.Infrastructure/Services/Implement/MacroService.cs b/src/Umbraco.Infrastructure/Services/Implement/MacroService.cs index a79d9fddce..a1d556d805 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/MacroService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/MacroService.cs @@ -13,13 +13,12 @@ namespace Umbraco.Cms.Core.Services.Implement /// /// Represents the Macro Service, which is an easy access to operations involving /// - internal class MacroService : RepositoryService, IMacroService + internal class MacroService : RepositoryService, IMacroWithAliasService { private readonly IMacroRepository _macroRepository; private readonly IAuditRepository _auditRepository; - public MacroService(IScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory, - IMacroRepository macroRepository, IAuditRepository auditRepository) + public MacroService(IScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory, IMacroRepository macroRepository, IAuditRepository auditRepository) : base(provider, loggerFactory, eventMessagesFactory) { _macroRepository = macroRepository; @@ -33,10 +32,14 @@ namespace Umbraco.Cms.Core.Services.Implement /// An object public IMacro GetByAlias(string alias) { + if (_macroRepository is not IMacroWithAliasRepository macroWithAliasRepository) + { + return GetAll().FirstOrDefault(x => x.Alias == alias); + } + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { - var q = Query().Where(x => x.Alias == alias); - return _macroRepository.Get(q).FirstOrDefault(); + return macroWithAliasRepository.GetByAlias(alias); } } @@ -61,6 +64,20 @@ namespace Umbraco.Cms.Core.Services.Implement } } + public IEnumerable GetAll(params string[] aliases) + { + if (_macroRepository is not IMacroWithAliasRepository macroWithAliasRepository) + { + var hashset = new HashSet(aliases); + return GetAll().Where(x => hashset.Contains(x.Alias)); + } + + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + return macroWithAliasRepository.GetAllByAlias(aliases); + } + } + public IMacro GetById(int id) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) diff --git a/src/Umbraco.Infrastructure/Templates/HtmlMacroParameterParser.cs b/src/Umbraco.Infrastructure/Templates/HtmlMacroParameterParser.cs new file mode 100644 index 0000000000..6323139137 --- /dev/null +++ b/src/Umbraco.Infrastructure/Templates/HtmlMacroParameterParser.cs @@ -0,0 +1,155 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Editors; +using Umbraco.Cms.Core.PropertyEditors; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.Macros; + +namespace Umbraco.Cms.Infrastructure.Templates +{ + public sealed class HtmlMacroParameterParser : IHtmlMacroParameterParser + { + private readonly IMacroService _macroService; + private readonly ILogger _logger; + private readonly ParameterEditorCollection _parameterEditors; + + public HtmlMacroParameterParser(IMacroService macroService, ILogger logger, ParameterEditorCollection parameterEditors) + { + _macroService = macroService; + _logger = logger; + _parameterEditors = parameterEditors; + } + + /// + /// Parses out media UDIs from an HTML string based on embedded macro parameter values. + /// + /// HTML string + /// + public IEnumerable FindUmbracoEntityReferencesFromEmbeddedMacros(string text) + { + // There may be more than one macro with the same alias on the page so using a tuple + var foundMacros = new List>>(); + + // This legacy ParseMacros() already finds the macros within a Rich Text Editor using regexes + // It seems to lowercase the macro parameter alias - so making the dictionary case insensitive + MacroTagParser.ParseMacros(text, textblock => { }, (macroAlias, macroAttributes) => foundMacros.Add(new Tuple>(macroAlias, new Dictionary(macroAttributes, StringComparer.OrdinalIgnoreCase)))); + foreach (var umbracoEntityReference in GetUmbracoEntityReferencesFromMacros(foundMacros)) + { + yield return umbracoEntityReference; + } + } + + /// + /// Parses out media UDIs from Macro Grid Control parameters. + /// + /// + /// + public IEnumerable FindUmbracoEntityReferencesFromGridControlMacros(IEnumerable macroGridControls) + { + var foundMacros = new List>>(); + + foreach (var macroGridControl in macroGridControls) + { + // Deserialise JSON of Macro Grid Control to a class + var gridMacro = macroGridControl.Value.ToObject(); + // Collect any macro parameters that contain the media udi format + if (gridMacro is not null && gridMacro.MacroParameters is not null && gridMacro.MacroParameters.Any()) + { + foundMacros.Add(new Tuple>(gridMacro.MacroAlias, gridMacro.MacroParameters)); + } + } + + foreach (var umbracoEntityReference in GetUmbracoEntityReferencesFromMacros(foundMacros)) + { + yield return umbracoEntityReference; + } + } + + private IEnumerable GetUmbracoEntityReferencesFromMacros(List>> macros) + { + + if (_macroService is not IMacroWithAliasService macroWithAliasService) + { + yield break; + } + + var uniqueMacroAliases = macros.Select(f => f.Item1).Distinct(); + // TODO: Tracking Macro references + // Here we are finding the used macros' Udis (there should be a Related Macro relation type - but Relations don't accept 'Macro' as an option) + var foundMacroUmbracoEntityReferences = new List(); + // Get all the macro configs in one hit for these unique macro aliases - this is now cached with a custom cache policy + var macroConfigs = macroWithAliasService.GetAll(uniqueMacroAliases.ToArray()); + + foreach (var macro in macros) + { + var macroConfig = macroConfigs.FirstOrDefault(f => f.Alias == macro.Item1); + if (macroConfig is null) + { + continue; + } + foundMacroUmbracoEntityReferences.Add(new UmbracoEntityReference(Udi.Create(Constants.UdiEntityType.Macro, macroConfig.Key))); + // Only do this if the macros actually have parameters + if (macroConfig.Properties is not null && macroConfig.Properties.Keys.Any(f => f != "macroAlias")) + { + foreach (var umbracoEntityReference in GetUmbracoEntityReferencesFromMacroParameters(macro.Item2, macroConfig, _parameterEditors)) + { + yield return umbracoEntityReference; + } + } + } + } + + /// + /// Finds media UDIs in Macro Parameter Values by calling the GetReference method for all the Macro Parameter Editors for a particular macro. + /// + /// The parameters for the macro a dictionary of key/value strings + /// The macro configuration for this particular macro - contains the types of editors used for each parameter + /// A list of all the registered parameter editors used in the Umbraco implmentation - to look up the corresponding property editor for a macro parameter + /// + private IEnumerable GetUmbracoEntityReferencesFromMacroParameters(Dictionary macroParameters, IMacro macroConfig, ParameterEditorCollection parameterEditors) + { + var foundUmbracoEntityReferences = new List(); + foreach (var parameter in macroConfig.Properties) + { + if (macroParameters.TryGetValue(parameter.Alias, out string parameterValue)) + { + var parameterEditorAlias = parameter.EditorAlias; + // Lookup propertyEditor from the registered ParameterEditors with the implmementation to avoid looking up for each parameter + var parameterEditor = parameterEditors.FirstOrDefault(f => string.Equals(f.Alias, parameterEditorAlias, StringComparison.OrdinalIgnoreCase)); + if (parameterEditor is not null) + { + // Get the ParameterValueEditor for this PropertyEditor (where the GetReferences method is implemented) - cast as IDataValueReference to determine if 'it is' implemented for the editor + if (parameterEditor.GetValueEditor() is IDataValueReference parameterValueEditor) + { + foreach (var entityReference in parameterValueEditor.GetReferences(parameterValue)) + { + foundUmbracoEntityReferences.Add(entityReference); + } + } + else + { + _logger.LogInformation("{0} doesn't have a ValueEditor that implements IDataValueReference", parameterEditor.Alias); + } + } + } + } + + return foundUmbracoEntityReferences; + } + + // Poco class to deserialise the Json for a Macro Control + private class GridMacro + { + [JsonProperty("macroAlias")] + public string MacroAlias { get; set; } + + [JsonProperty("macroParamsDictionary")] + public Dictionary MacroParameters { get; set; } + } + } +} diff --git a/src/Umbraco.Infrastructure/Templates/IHtmlMacroParameterParser.cs b/src/Umbraco.Infrastructure/Templates/IHtmlMacroParameterParser.cs new file mode 100644 index 0000000000..6e484cc30a --- /dev/null +++ b/src/Umbraco.Infrastructure/Templates/IHtmlMacroParameterParser.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Editors; + +namespace Umbraco.Cms.Infrastructure.Templates +{ + /// + /// Provides methods to parse referenced entities as Macro parameters. + /// + public interface IHtmlMacroParameterParser + { + /// + /// Parses out media UDIs from an HTML string based on embedded macro parameter values. + /// + /// HTML string + /// + IEnumerable FindUmbracoEntityReferencesFromEmbeddedMacros(string text); + + /// + /// Parses out media UDIs from Macro Grid Control parameters. + /// + /// + /// + IEnumerable FindUmbracoEntityReferencesFromGridControlMacros(IEnumerable macroGridControls); + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/MacroServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/MacroServiceTests.cs index 75dae7515b..621762917d 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/MacroServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/MacroServiceTests.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Threading; using Microsoft.Extensions.Logging; using Moq; using NUnit.Framework; @@ -24,7 +23,8 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] public class MacroServiceTests : UmbracoIntegrationTest { - private IMacroService MacroService => GetRequiredService(); + [Obsolete("After merging IMacroWithAliasService interface with IMacroService in Umbraco 11, this should go back to just being GetRequiredService()")] + private IMacroWithAliasService MacroService => GetRequiredService() as IMacroWithAliasService; [SetUp] public void SetupTest() @@ -52,6 +52,19 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services Assert.AreEqual("Test1", macro.Name); } + [Test] + public void Can_Get_By_Aliases() + { + // Act + IEnumerable macros = MacroService.GetAll("test1", "test2"); + + // Assert + Assert.IsNotNull(macros); + Assert.AreEqual(2, macros.Count()); + Assert.AreEqual("Test1", macros.ToArray()[0].Name); + Assert.AreEqual("Test2", macros.ToArray()[1].Name); + } + [Test] public void Can_Get_All() { diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.PublishedCache.NuCache/SnapDictionaryTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.PublishedCache.NuCache/SnapDictionaryTests.cs index c25b2fde1e..fdb29f88e6 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.PublishedCache.NuCache/SnapDictionaryTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.PublishedCache.NuCache/SnapDictionaryTests.cs @@ -323,6 +323,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.PublishedCache.NuCache } [Test] + [Retry(5)] // TODO make this test non-flaky. public async Task EventuallyCollectNulls() { var d = new SnapDictionary(); From cde312b6d40e0a00f50c575bb1a99c65fb142b67 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Mon, 21 Mar 2022 15:47:51 +0100 Subject: [PATCH 05/10] Use an umbra.co link for the TV replacement channel so we can change it in the future if we need to --- .../src/views/dashboard/settings/settingsdashboardintro.html | 2 +- src/Umbraco.Web.UI/umbraco/config/lang/en.xml | 2 +- src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/settingsdashboardintro.html b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/settingsdashboardintro.html index 436155de72..b33444177a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/settingsdashboardintro.html +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/settingsdashboardintro.html @@ -17,7 +17,7 @@ Ask a question in the Community Forum
  • - Watch our free tutorial videos on the Umbraco Learning Base + Watch our free tutorial videos on the Umbraco Learning Base
  • Find out about our productivity boosting tools and commercial support diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml index 6b5d301c5f..e9b63f92a1 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml @@ -2639,7 +2639,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont tutorial videos on the Umbraco Learning Base + Watch our free tutorial videos on the Umbraco Learning Base ]]> 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 bed9f227dd..f01b878d19 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml @@ -2728,7 +2728,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont tutorial videos on the Umbraco Learning Base + Watch our free tutorial videos on the Umbraco Learning Base ]]> From 0aa4d1956aa1e075dc1e8a98368a4caba7692c4c Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Mon, 21 Mar 2022 16:14:48 +0100 Subject: [PATCH 06/10] Also update Umbraco TV link + text in the help panel --- .../src/views/common/drawers/help/help.html | 6 +++--- src/Umbraco.Web.UI/umbraco/config/lang/en.xml | 2 ++ src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml | 2 ++ 3 files changed, 7 insertions(+), 3 deletions(-) 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 6f32e89988..c14110437d 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 @@ -128,13 +128,13 @@
    - +
    - Visit umbraco.tv + Watch our free tutorial videos
    - The best Umbraco video tutorials + on the Umbraco Learning Base
    diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml index e9b63f92a1..6d78c7db5b 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml @@ -1469,6 +1469,8 @@ To manage your website, simply open the Umbraco backoffice and start adding cont The best Umbraco video tutorials Visit our.umbraco.com Visit umbraco.tv + Watch our free tutorial videos + on the Umbraco Learning Base Default template 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 f01b878d19..fab7392ad8 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml @@ -1494,6 +1494,8 @@ To manage your website, simply open the Umbraco backoffice and start adding cont The best Umbraco video tutorials Visit our.umbraco.com Visit umbraco.tv + Watch our free tutorial videos + on the Umbraco Learning Base Default template From 04c292f1675073ef2be800eeeb3332028ab61f16 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Tue, 22 Mar 2022 10:09:36 +0100 Subject: [PATCH 07/10] Merge pull request #12155 from umbraco/v9/bugfix/10066 Same fix as #12154 - fixes #10066 (cherry picked from commit a302b10f66119c8253e1e3630d0456cb805bdc8a) --- .../Repositories/Implement/RedirectUrlRepository.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RedirectUrlRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RedirectUrlRepository.cs index 6ab29aa47e..5e9a8413b4 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RedirectUrlRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RedirectUrlRepository.cs @@ -60,14 +60,14 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement var urlHash = url.GenerateHash(); Sql sql = GetBaseQuery(false) .Where(x => x.Url == url && x.UrlHash == urlHash && - (x.Culture == culture.ToLower() || x.Culture == string.Empty)) + (x.Culture == culture.ToLower() || x.Culture == null || x.Culture == string.Empty)) .OrderByDescending(x => x.CreateDateUtc); List dtos = Database.Fetch(sql); RedirectUrlDto dto = dtos.FirstOrDefault(f => f.Culture == culture.ToLower()); if (dto == null) { - dto = dtos.FirstOrDefault(f => f.Culture == string.Empty); + dto = dtos.FirstOrDefault(f => string.IsNullOrWhiteSpace(f.Culture)); } return dto == null ? null : Map(dto); From 0d836875c77f67a08f428c00ce882f8a1e1b78f9 Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Date: Tue, 22 Mar 2022 12:58:38 +0100 Subject: [PATCH 08/10] Merge pull request #12153 from vsilvar/v9/bugfix/12022_recurring_hosted_service_scope_leak Fixes RecurringHostServices leaking the execution context / ambient scope --- .../HostedServices/ContentVersionCleanup.cs | 2 +- .../HostedServices/HealthCheckNotifier.cs | 1 + .../HostedServices/KeepAlive.cs | 2 +- .../HostedServices/LogScrubber.cs | 2 +- .../RecurringHostedServiceBase.cs | 20 +++++++++++++++---- .../HostedServices/ReportSiteTask.cs | 2 +- .../HostedServices/ScheduledPublishing.cs | 2 +- .../InstructionProcessTask.cs | 2 +- .../ServerRegistration/TouchServerTask.cs | 2 +- .../HostedServices/TempFileCleanup.cs | 2 +- .../Implement/CacheInstructionService.cs | 7 +++++++ 11 files changed, 32 insertions(+), 12 deletions(-) diff --git a/src/Umbraco.Infrastructure/HostedServices/ContentVersionCleanup.cs b/src/Umbraco.Infrastructure/HostedServices/ContentVersionCleanup.cs index 5f3aba5f3f..8c9f3223f0 100644 --- a/src/Umbraco.Infrastructure/HostedServices/ContentVersionCleanup.cs +++ b/src/Umbraco.Infrastructure/HostedServices/ContentVersionCleanup.cs @@ -32,7 +32,7 @@ namespace Umbraco.Cms.Infrastructure.HostedServices IContentVersionService service, IMainDom mainDom, IServerRoleAccessor serverRoleAccessor) - : base(TimeSpan.FromHours(1), TimeSpan.FromMinutes(3)) + : base(logger, TimeSpan.FromHours(1), TimeSpan.FromMinutes(1)) { _runtimeState = runtimeState; _logger = logger; diff --git a/src/Umbraco.Infrastructure/HostedServices/HealthCheckNotifier.cs b/src/Umbraco.Infrastructure/HostedServices/HealthCheckNotifier.cs index 6a0828fad3..e6d8e75304 100644 --- a/src/Umbraco.Infrastructure/HostedServices/HealthCheckNotifier.cs +++ b/src/Umbraco.Infrastructure/HostedServices/HealthCheckNotifier.cs @@ -61,6 +61,7 @@ namespace Umbraco.Cms.Infrastructure.HostedServices IProfilingLogger profilingLogger, ICronTabParser cronTabParser) : base( + logger, healthChecksSettings.Value.Notification.Period, healthChecksSettings.Value.GetNotificationDelay(cronTabParser, DateTime.Now, DefaultDelay)) { diff --git a/src/Umbraco.Infrastructure/HostedServices/KeepAlive.cs b/src/Umbraco.Infrastructure/HostedServices/KeepAlive.cs index 22160b8f6e..3233cfa8f2 100644 --- a/src/Umbraco.Infrastructure/HostedServices/KeepAlive.cs +++ b/src/Umbraco.Infrastructure/HostedServices/KeepAlive.cs @@ -49,7 +49,7 @@ namespace Umbraco.Cms.Infrastructure.HostedServices IProfilingLogger profilingLogger, IServerRoleAccessor serverRegistrar, IHttpClientFactory httpClientFactory) - : base(TimeSpan.FromMinutes(5), DefaultDelay) + : base(logger, TimeSpan.FromMinutes(5), DefaultDelay) { _hostingEnvironment = hostingEnvironment; _mainDom = mainDom; diff --git a/src/Umbraco.Infrastructure/HostedServices/LogScrubber.cs b/src/Umbraco.Infrastructure/HostedServices/LogScrubber.cs index 27d9c29e8d..79c1c4b8ea 100644 --- a/src/Umbraco.Infrastructure/HostedServices/LogScrubber.cs +++ b/src/Umbraco.Infrastructure/HostedServices/LogScrubber.cs @@ -48,7 +48,7 @@ namespace Umbraco.Cms.Infrastructure.HostedServices IScopeProvider scopeProvider, ILogger logger, IProfilingLogger profilingLogger) - : base(TimeSpan.FromHours(4), DefaultDelay) + : base(logger, TimeSpan.FromHours(4), DefaultDelay) { _mainDom = mainDom; _serverRegistrar = serverRegistrar; diff --git a/src/Umbraco.Infrastructure/HostedServices/RecurringHostedServiceBase.cs b/src/Umbraco.Infrastructure/HostedServices/RecurringHostedServiceBase.cs index d97737a8f8..18fe9fc47f 100644 --- a/src/Umbraco.Infrastructure/HostedServices/RecurringHostedServiceBase.cs +++ b/src/Umbraco.Infrastructure/HostedServices/RecurringHostedServiceBase.cs @@ -5,6 +5,7 @@ using System; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; namespace Umbraco.Cms.Infrastructure.HostedServices { @@ -21,6 +22,7 @@ namespace Umbraco.Cms.Infrastructure.HostedServices ///
  • protected static readonly TimeSpan DefaultDelay = TimeSpan.FromMinutes(3); + private readonly ILogger _logger; private TimeSpan _period; private readonly TimeSpan _delay; private Timer _timer; @@ -29,10 +31,12 @@ namespace Umbraco.Cms.Infrastructure.HostedServices /// /// Initializes a new instance of the class. /// - /// Timepsan representing how often the task should recur. - /// Timespan represeting the initial delay after application start-up before the first run of the task occurs. - protected RecurringHostedServiceBase(TimeSpan period, TimeSpan delay) + /// Logger. + /// Timespan representing how often the task should recur. + /// Timespan representing the initial delay after application start-up before the first run of the task occurs. + protected RecurringHostedServiceBase(ILogger logger, TimeSpan period, TimeSpan delay) { + _logger = logger; _period = period; _delay = delay; } @@ -40,7 +44,11 @@ namespace Umbraco.Cms.Infrastructure.HostedServices /// public Task StartAsync(CancellationToken cancellationToken) { - _timer = new Timer(ExecuteAsync, null, (int)_delay.TotalMilliseconds, (int)_period.TotalMilliseconds); + using (!ExecutionContext.IsFlowSuppressed() ? (IDisposable)ExecutionContext.SuppressFlow() : null) + { + _timer = new Timer(ExecuteAsync, null, (int)_delay.TotalMilliseconds, (int)_period.TotalMilliseconds); + } + return Task.CompletedTask; } @@ -61,6 +69,10 @@ namespace Umbraco.Cms.Infrastructure.HostedServices // Hat-tip: https://stackoverflow.com/a/14207615/489433 await PerformExecuteAsync(state); } + catch (Exception ex) + { + _logger.LogError(ex, "Unhandled exception in recurring hosted service {serviceName}.", GetType().Name); + } finally { // Resume now that the task is complete - Note we use period in both because we don't want to execute again after the delay. diff --git a/src/Umbraco.Infrastructure/HostedServices/ReportSiteTask.cs b/src/Umbraco.Infrastructure/HostedServices/ReportSiteTask.cs index cfce96281c..6e5d412e71 100644 --- a/src/Umbraco.Infrastructure/HostedServices/ReportSiteTask.cs +++ b/src/Umbraco.Infrastructure/HostedServices/ReportSiteTask.cs @@ -23,7 +23,7 @@ namespace Umbraco.Cms.Infrastructure.HostedServices public ReportSiteTask( ILogger logger, ITelemetryService telemetryService) - : base(TimeSpan.FromDays(1), TimeSpan.FromMinutes(1)) + : base(logger, TimeSpan.FromDays(1), TimeSpan.FromMinutes(1)) { _logger = logger; _telemetryService = telemetryService; diff --git a/src/Umbraco.Infrastructure/HostedServices/ScheduledPublishing.cs b/src/Umbraco.Infrastructure/HostedServices/ScheduledPublishing.cs index 429389273f..fd70c05fc1 100644 --- a/src/Umbraco.Infrastructure/HostedServices/ScheduledPublishing.cs +++ b/src/Umbraco.Infrastructure/HostedServices/ScheduledPublishing.cs @@ -71,7 +71,7 @@ namespace Umbraco.Cms.Infrastructure.HostedServices ILogger logger, IServerMessenger serverMessenger, IScopeProvider scopeProvider) - : base(TimeSpan.FromMinutes(1), DefaultDelay) + : base(logger, TimeSpan.FromMinutes(1), DefaultDelay) { _runtimeState = runtimeState; _mainDom = mainDom; diff --git a/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/InstructionProcessTask.cs b/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/InstructionProcessTask.cs index 43e2522efd..3aa49f3f71 100644 --- a/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/InstructionProcessTask.cs +++ b/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/InstructionProcessTask.cs @@ -30,7 +30,7 @@ namespace Umbraco.Cms.Infrastructure.HostedServices.ServerRegistration /// The typed logger. /// The configuration for global settings. public InstructionProcessTask(IRuntimeState runtimeState, IServerMessenger messenger, ILogger logger, IOptions globalSettings) - : base(globalSettings.Value.DatabaseServerMessenger.TimeBetweenSyncOperations, TimeSpan.FromMinutes(1)) + : base(logger, globalSettings.Value.DatabaseServerMessenger.TimeBetweenSyncOperations, TimeSpan.FromMinutes(1)) { _runtimeState = runtimeState; _messenger = messenger; diff --git a/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/TouchServerTask.cs b/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/TouchServerTask.cs index d54d67338e..5f20a3654e 100644 --- a/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/TouchServerTask.cs +++ b/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/TouchServerTask.cs @@ -44,7 +44,7 @@ namespace Umbraco.Cms.Infrastructure.HostedServices.ServerRegistration ILogger logger, IOptions globalSettings, IServerRoleAccessor serverRoleAccessor) - : base(globalSettings.Value.DatabaseServerRegistrar.WaitTimeBetweenCalls, TimeSpan.FromSeconds(15)) + : base(logger, globalSettings.Value.DatabaseServerRegistrar.WaitTimeBetweenCalls, TimeSpan.FromSeconds(15)) { _runtimeState = runtimeState; _serverRegistrationService = serverRegistrationService ?? throw new ArgumentNullException(nameof(serverRegistrationService)); diff --git a/src/Umbraco.Infrastructure/HostedServices/TempFileCleanup.cs b/src/Umbraco.Infrastructure/HostedServices/TempFileCleanup.cs index e59cca5fbd..8a2a312455 100644 --- a/src/Umbraco.Infrastructure/HostedServices/TempFileCleanup.cs +++ b/src/Umbraco.Infrastructure/HostedServices/TempFileCleanup.cs @@ -33,7 +33,7 @@ namespace Umbraco.Cms.Infrastructure.HostedServices /// Representation of the main application domain. /// The typed logger. public TempFileCleanup(IIOHelper ioHelper, IMainDom mainDom, ILogger logger) - : base(TimeSpan.FromMinutes(60), DefaultDelay) + : base(logger, TimeSpan.FromMinutes(60), DefaultDelay) { _ioHelper = ioHelper; _mainDom = mainDom; diff --git a/src/Umbraco.Infrastructure/Services/Implement/CacheInstructionService.cs b/src/Umbraco.Infrastructure/Services/Implement/CacheInstructionService.cs index a037cd1095..b4af98ad0a 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/CacheInstructionService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/CacheInstructionService.cs @@ -247,6 +247,13 @@ namespace Umbraco.Cms.Core.Services.Implement ///
    private bool TryDeserializeInstructions(CacheInstruction instruction, out JArray jsonInstructions) { + if (instruction.Instructions is null) + { + _logger.LogError("Failed to deserialize instructions ({DtoId}: 'null').", instruction.Id); + jsonInstructions = null; + return false; + } + try { jsonInstructions = JsonConvert.DeserializeObject(instruction.Instructions); From 20f0ceeda6923194f64c39f150999e3dfbe35db8 Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Date: Wed, 23 Mar 2022 09:05:30 +0100 Subject: [PATCH 09/10] Merge pull request #12161 from umbraco/v9/bugfix/amend_breaking_change_in_RecurringHostedServiceBase Amend breaking change in RecurringHostedServiceBase --- src/Umbraco.Core/StaticApplicationLogging.cs | 1 + .../HostedServices/ContentVersionCleanup.cs | 2 +- .../HostedServices/RecurringHostedServiceBase.cs | 13 ++++++++++++- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Core/StaticApplicationLogging.cs b/src/Umbraco.Core/StaticApplicationLogging.cs index e216011014..73078b0f42 100644 --- a/src/Umbraco.Core/StaticApplicationLogging.cs +++ b/src/Umbraco.Core/StaticApplicationLogging.cs @@ -1,3 +1,4 @@ +using System; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; diff --git a/src/Umbraco.Infrastructure/HostedServices/ContentVersionCleanup.cs b/src/Umbraco.Infrastructure/HostedServices/ContentVersionCleanup.cs index 8c9f3223f0..d037c91d86 100644 --- a/src/Umbraco.Infrastructure/HostedServices/ContentVersionCleanup.cs +++ b/src/Umbraco.Infrastructure/HostedServices/ContentVersionCleanup.cs @@ -32,7 +32,7 @@ namespace Umbraco.Cms.Infrastructure.HostedServices IContentVersionService service, IMainDom mainDom, IServerRoleAccessor serverRoleAccessor) - : base(logger, TimeSpan.FromHours(1), TimeSpan.FromMinutes(1)) + : base(logger, TimeSpan.FromHours(1), TimeSpan.FromMinutes(3)) { _runtimeState = runtimeState; _logger = logger; diff --git a/src/Umbraco.Infrastructure/HostedServices/RecurringHostedServiceBase.cs b/src/Umbraco.Infrastructure/HostedServices/RecurringHostedServiceBase.cs index 18fe9fc47f..c1c7cdf3cf 100644 --- a/src/Umbraco.Infrastructure/HostedServices/RecurringHostedServiceBase.cs +++ b/src/Umbraco.Infrastructure/HostedServices/RecurringHostedServiceBase.cs @@ -4,8 +4,10 @@ using System; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using Umbraco.Cms.Web.Common.DependencyInjection; namespace Umbraco.Cms.Infrastructure.HostedServices { @@ -41,6 +43,15 @@ namespace Umbraco.Cms.Infrastructure.HostedServices _delay = delay; } + // Scheduled for removal in V11 + [Obsolete("Please use constructor that takes an ILogger instead")] + protected RecurringHostedServiceBase(TimeSpan period, TimeSpan delay) + { + _period = period; + _delay = delay; + _logger = StaticServiceProvider.Instance.GetRequiredService().CreateLogger(GetType()); + } + /// public Task StartAsync(CancellationToken cancellationToken) { @@ -71,7 +82,7 @@ namespace Umbraco.Cms.Infrastructure.HostedServices } catch (Exception ex) { - _logger.LogError(ex, "Unhandled exception in recurring hosted service {serviceName}.", GetType().Name); + _logger.LogError(ex, "Unhandled exception in recurring hosted service."); } finally { From 78cfb29908835ac1db40c01079bd8f0a53b87e3b Mon Sep 17 00:00:00 2001 From: Elitsa Marinovska <21998037+elit0451@users.noreply.github.com> Date: Fri, 18 Mar 2022 11:21:49 +0100 Subject: [PATCH 10/10] Item tracking fixes (#12146) * Cleanup; Fix lang keys * Documentation * Typos * Distinct the results * Changed GetPagedRelationsForItems to GetPagedRelationsForItem as we would only expect a single id to be passed when calling this + fix more docs * Changed to the correct reference * Unused code * Only load references when info tab is clicked Co-authored-by: Bjarke Berg --- .../ITrackedReferencesRepository.cs | 38 +++++++-- .../Services/ITrackedReferencesService.cs | 29 ++++++- .../Persistence/NPocoSqlExtensions.cs | 6 ++ .../Implement/RelationRepository.cs | 33 -------- .../Implement/TrackedReferencesRepository.cs | 83 ++++++++++--------- .../Implement/TrackedReferencesService.cs | 23 +++-- .../Controllers/MediaController.cs | 2 +- .../TrackedReferencesController.cs | 43 +++++----- .../content/umbcontentnodeinfo.directive.js | 7 ++ .../resources/trackedreferences.resource.js | 2 +- .../content/umb-content-node-info.html | 2 +- .../umb-tracked-references-table.html | 4 +- .../references/umb-tracked-references.html | 10 +-- .../src/views/users/user.controller.js | 2 +- src/Umbraco.Web.UI/umbraco/config/lang/en.xml | 10 +-- .../umbraco/config/lang/en_us.xml | 14 +--- 16 files changed, 173 insertions(+), 135 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/ITrackedReferencesRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ITrackedReferencesRepository.cs index 42746a9565..e6ca8eaa50 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ITrackedReferencesRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ITrackedReferencesRepository.cs @@ -1,14 +1,42 @@ -using System; using System.Collections.Generic; using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.Models.Entities; namespace Umbraco.Cms.Core.Persistence.Repositories { public interface ITrackedReferencesRepository { - IEnumerable GetPagedRelationsForItems(int[] ids, long pageIndex, int pageSize, bool filterMustBeIsDependency,out long totalRecords); - IEnumerable GetPagedItemsWithRelations(int[] ids, long pageIndex, int pageSize, bool filterMustBeIsDependency,out long totalRecords); - IEnumerable GetPagedDescendantsInReferences(int parentId, long pageIndex, int pageSize, bool filterMustBeIsDependency,out long totalRecords); + /// + /// Gets a page of items which are in relation with the current item. + /// Basically, shows the items which depend on the current item. + /// + /// The identifier of the entity to retrieve relations for. + /// The page index. + /// The page size. + /// A boolean indicating whether to filter only the RelationTypes which are dependencies (isDependency field is set to true). + /// The total count of the items with reference to the current item. + /// An enumerable list of objects. + IEnumerable GetPagedRelationsForItem(int id, long pageIndex, int pageSize, bool filterMustBeIsDependency, out long totalRecords); + + /// + /// Gets a page of items used in any kind of relation from selected integer ids. + /// + /// The identifiers of the entities to check for relations. + /// The page index. + /// The page size. + /// A boolean indicating whether to filter only the RelationTypes which are dependencies (isDependency field is set to true). + /// The total count of the items in any kind of relation. + /// An enumerable list of objects. + IEnumerable GetPagedItemsWithRelations(int[] ids, long pageIndex, int pageSize, bool filterMustBeIsDependency, out long totalRecords); + + /// + /// Gets a page of the descending items that have any references, given a parent id. + /// + /// The unique identifier of the parent to retrieve descendants for. + /// The page index. + /// The page size. + /// A boolean indicating whether to filter only the RelationTypes which are dependencies (isDependency field is set to true). + /// The total count of descending items. + /// An enumerable list of objects. + IEnumerable GetPagedDescendantsInReferences(int parentId, long pageIndex, int pageSize, bool filterMustBeIsDependency, out long totalRecords); } } diff --git a/src/Umbraco.Core/Services/ITrackedReferencesService.cs b/src/Umbraco.Core/Services/ITrackedReferencesService.cs index eee8a324df..dea99c0f6d 100644 --- a/src/Umbraco.Core/Services/ITrackedReferencesService.cs +++ b/src/Umbraco.Core/Services/ITrackedReferencesService.cs @@ -4,12 +4,35 @@ namespace Umbraco.Cms.Core.Services { public interface ITrackedReferencesService { - PagedResult GetPagedRelationsForItems(int[] ids, long pageIndex, int pageSize, bool filterMustBeIsDependency); - + /// + /// Gets a paged result of items which are in relation with the current item. + /// Basically, shows the items which depend on the current item. + /// + /// The identifier of the entity to retrieve relations for. + /// The page index. + /// The page size. + /// A boolean indicating whether to filter only the RelationTypes which are dependencies (isDependency field is set to true). + /// A paged result of objects. + PagedResult GetPagedRelationsForItem(int id, long pageIndex, int pageSize, bool filterMustBeIsDependency); + /// + /// Gets a paged result of the descending items that have any references, given a parent id. + /// + /// The unique identifier of the parent to retrieve descendants for. + /// The page index. + /// The page size. + /// A boolean indicating whether to filter only the RelationTypes which are dependencies (isDependency field is set to true). + /// A paged result of objects. PagedResult GetPagedDescendantsInReferences(int parentId, long pageIndex, int pageSize, bool filterMustBeIsDependency); - + /// + /// Gets a paged result of items used in any kind of relation from selected integer ids. + /// + /// The identifiers of the entities to check for relations. + /// The page index. + /// The page size. + /// A boolean indicating whether to filter only the RelationTypes which are dependencies (isDependency field is set to true). + /// A paged result of objects. PagedResult GetPagedItemsWithRelations(int[] ids, long pageIndex, int pageSize, bool filterMustBeIsDependency); } } diff --git a/src/Umbraco.Infrastructure/Persistence/NPocoSqlExtensions.cs b/src/Umbraco.Infrastructure/Persistence/NPocoSqlExtensions.cs index 6b7c34dc15..47cca58ce2 100644 --- a/src/Umbraco.Infrastructure/Persistence/NPocoSqlExtensions.cs +++ b/src/Umbraco.Infrastructure/Persistence/NPocoSqlExtensions.cs @@ -659,6 +659,12 @@ namespace Umbraco.Extensions return sql; } + public static Sql SelectDistinct(this Sql sql, params object[] columns) + { + sql.Append("SELECT DISTINCT " + string.Join(", ", columns)); + return sql; + } + //this.Append("SELECT " + string.Join(", ", columns), new object[0]); /// diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RelationRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RelationRepository.cs index a18782ca82..7ba20d1db5 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RelationRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RelationRepository.cs @@ -198,22 +198,6 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement }); } - public IEnumerable GetPagedParentEntitiesByChildIds(int[] childIds, long pageIndex, int pageSize, out long totalRecords, int[] relationTypes, params Guid[] entityTypes) - { - return _entityRepository.GetPagedResultsByQuery(Query(), entityTypes, pageIndex, pageSize, out totalRecords, null, null, sql => - { - SqlJoinRelations(sql); - - sql.WhereIn(rel => rel.ChildId, childIds); - sql.WhereAny(s => s.WhereIn(rel => rel.ParentId, childIds), s => s.WhereNotIn(node => node.NodeId, childIds)); - - if (relationTypes != null && relationTypes.Any()) - { - sql.WhereIn(rel => rel.RelationType, relationTypes); - } - }); - } - public IEnumerable GetPagedChildEntitiesByParentId(int parentId, long pageIndex, int pageSize, out long totalRecords, params Guid[] entityTypes) { return GetPagedChildEntitiesByParentId(parentId, pageIndex, pageSize, out totalRecords, new int[0], entityTypes); @@ -241,21 +225,6 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement }); } - public IEnumerable GetPagedEntitiesForItemsInRelation(int[] itemIds, long pageIndex, int pageSize, out long totalRecords, params Guid[] entityTypes) - { - return _entityRepository.GetPagedResultsByQuery(Query(), entityTypes, pageIndex, pageSize, out totalRecords, null, null, sql => - { - SqlJoinRelations(sql); - - sql.WhereIn(rel => rel.ChildId, itemIds); - sql.Where((rel, node) => rel.ChildId == node.NodeId); - sql.Where(type => type.IsDependency); - }); - } - - - - public void Save(IEnumerable relations) { foreach (var hasIdentityGroup in relations.GroupBy(r => r.HasIdentity)) @@ -475,8 +444,6 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement [Column(Name = "contentTypeName")] public string ChildContentTypeName { get; set; } - - [Column(Name = "relationTypeName")] public string RelationTypeName { get; set; } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TrackedReferencesRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TrackedReferencesRepository.cs index f5fb945464..0e70d47cbf 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TrackedReferencesRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TrackedReferencesRepository.cs @@ -1,9 +1,8 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using NPoco; using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.Persistence; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Infrastructure.Persistence.Dtos; @@ -20,10 +19,12 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement _scopeAccessor = scopeAccessor; } - public IEnumerable GetPagedItemsWithRelations(int[] ids, long pageIndex, int pageSize, - bool filterMustBeIsDependency, out long totalRecords) + /// + /// Gets a page of items used in any kind of relation from selected integer ids. + /// + public IEnumerable GetPagedItemsWithRelations(int[] ids, long pageIndex, int pageSize, bool filterMustBeIsDependency, out long totalRecords) { - var sql = _scopeAccessor.AmbientScope.Database.SqlContext.Sql().Select( + var sql = _scopeAccessor.AmbientScope.Database.SqlContext.Sql().SelectDistinct( "[pn].[id] as nodeId", "[pn].[uniqueId] as nodeKey", "[pn].[text] as nodeName", @@ -36,12 +37,12 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement "[umbracoRelationType].[isDependency] as relationTypeIsDependency", "[umbracoRelationType].[dual] as relationTypeIsBidirectional") .From("r") - .InnerJoin("umbracoRelationType").On((left, right) => left.RelationType == right.Id, aliasLeft: "r", aliasRight:"umbracoRelationType") - .InnerJoin("cn").On((r, cn, rt) => (!rt.Dual && r.ParentId == cn.NodeId) || (rt.Dual && (r.ChildId == cn.NodeId || r.ParentId == cn.NodeId )), aliasLeft: "r", aliasRight:"cn", aliasOther: "umbracoRelationType" ) - .InnerJoin("pn").On((r, pn, cn) => (pn.NodeId == r.ChildId && cn.NodeId == r.ParentId) || (pn.NodeId == r.ParentId && cn.NodeId == r.ChildId), aliasLeft: "r", aliasRight:"pn", aliasOther:"cn" ) - .LeftJoin("c").On((left, right) => left.NodeId == right.NodeId, aliasLeft:"pn", aliasRight:"c") - .LeftJoin("ct").On((left, right) => left.ContentTypeId == right.NodeId, aliasLeft:"c", aliasRight:"ct") - .LeftJoin("ctn").On((left, right) => left.NodeId == right.NodeId, aliasLeft:"ct", aliasRight:"ctn"); + .InnerJoin("umbracoRelationType").On((left, right) => left.RelationType == right.Id, aliasLeft: "r", aliasRight: "umbracoRelationType") + .InnerJoin("cn").On((r, cn, rt) => (!rt.Dual && r.ParentId == cn.NodeId) || (rt.Dual && (r.ChildId == cn.NodeId || r.ParentId == cn.NodeId)), aliasLeft: "r", aliasRight: "cn", aliasOther: "umbracoRelationType") + .InnerJoin("pn").On((r, pn, cn) => (pn.NodeId == r.ChildId && cn.NodeId == r.ParentId) || (pn.NodeId == r.ParentId && cn.NodeId == r.ChildId), aliasLeft: "r", aliasRight: "pn", aliasOther: "cn") + .LeftJoin("c").On((left, right) => left.NodeId == right.NodeId, aliasLeft: "pn", aliasRight: "c") + .LeftJoin("ct").On((left, right) => left.ContentTypeId == right.NodeId, aliasLeft: "c", aliasRight: "ct") + .LeftJoin("ctn").On((left, right) => left.NodeId == right.NodeId, aliasLeft: "ct", aliasRight: "ctn"); if (ids.Any()) { @@ -57,13 +58,15 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement sql = sql.OrderBy(x => x.Alias); var pagedResult = _scopeAccessor.AmbientScope.Database.Page(pageIndex + 1, pageSize, sql); - totalRecords = Convert.ToInt32(pagedResult.TotalItems); + totalRecords = pagedResult.TotalItems; return pagedResult.Items.Select(MapDtoToEntity); } - public IEnumerable GetPagedDescendantsInReferences(int parentId, long pageIndex, int pageSize, bool filterMustBeIsDependency, - out long totalRecords) + /// + /// Gets a page of the descending items that have any references, given a parent id. + /// + public IEnumerable GetPagedDescendantsInReferences(int parentId, long pageIndex, int pageSize, bool filterMustBeIsDependency, out long totalRecords) { var syntax = _scopeAccessor.AmbientScope.Database.SqlContext.SqlSyntax; @@ -73,13 +76,12 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement .From("node") .Where(x => x.NodeId == parentId, "node"); - // Gets the descendants of the parent node Sql subQuery; if (_scopeAccessor.AmbientScope.Database.DatabaseType.IsSqlCe()) { - // SqlCE do not support nested selects that returns a scalar. So we need to do this in multiple queries + // SqlCE does not support nested selects that returns a scalar. So we need to do this in multiple queries var pathForLike = _scopeAccessor.AmbientScope.Database.ExecuteScalar(subsubQuery); @@ -96,10 +98,8 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement .WhereLike(x => x.Path, subsubQuery); } - - // Get all relations where parent is in the sub query - var sql = _scopeAccessor.AmbientScope.Database.SqlContext.Sql().Select( + var sql = _scopeAccessor.AmbientScope.Database.SqlContext.Sql().SelectDistinct( "[pn].[id] as nodeId", "[pn].[uniqueId] as nodeKey", "[pn].[text] as nodeName", @@ -112,29 +112,35 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement "[umbracoRelationType].[isDependency] as relationTypeIsDependency", "[umbracoRelationType].[dual] as relationTypeIsBidirectional") .From("r") - .InnerJoin("umbracoRelationType").On((left, right) => left.RelationType == right.Id, aliasLeft: "r", aliasRight:"umbracoRelationType") - .InnerJoin("cn").On((r, cn, rt) => (!rt.Dual && r.ParentId == cn.NodeId) || (rt.Dual && (r.ChildId == cn.NodeId || r.ParentId == cn.NodeId )), aliasLeft: "r", aliasRight:"cn", aliasOther: "umbracoRelationType" ) - .InnerJoin("pn").On((r, pn, cn) => (pn.NodeId == r.ChildId && cn.NodeId == r.ParentId) || (pn.NodeId == r.ParentId && cn.NodeId == r.ChildId), aliasLeft: "r", aliasRight:"pn", aliasOther:"cn" ) - .LeftJoin("c").On((left, right) => left.NodeId == right.NodeId, aliasLeft:"pn", aliasRight:"c") - .LeftJoin("ct").On((left, right) => left.ContentTypeId == right.NodeId, aliasLeft:"c", aliasRight:"ct") - .LeftJoin("ctn").On((left, right) => left.NodeId == right.NodeId, aliasLeft:"ct", aliasRight:"ctn") + .InnerJoin("umbracoRelationType").On((left, right) => left.RelationType == right.Id, aliasLeft: "r", aliasRight: "umbracoRelationType") + .InnerJoin("cn").On((r, cn, rt) => (!rt.Dual && r.ParentId == cn.NodeId) || (rt.Dual && (r.ChildId == cn.NodeId || r.ParentId == cn.NodeId)), aliasLeft: "r", aliasRight: "cn", aliasOther: "umbracoRelationType") + .InnerJoin("pn").On((r, pn, cn) => (pn.NodeId == r.ChildId && cn.NodeId == r.ParentId) || (pn.NodeId == r.ParentId && cn.NodeId == r.ChildId), aliasLeft: "r", aliasRight: "pn", aliasOther: "cn") + .LeftJoin("c").On((left, right) => left.NodeId == right.NodeId, aliasLeft: "pn", aliasRight: "c") + .LeftJoin("ct").On((left, right) => left.ContentTypeId == right.NodeId, aliasLeft: "c", aliasRight: "ct") + .LeftJoin("ctn").On((left, right) => left.NodeId == right.NodeId, aliasLeft: "ct", aliasRight: "ctn") .WhereIn((System.Linq.Expressions.Expression>)(x => x.NodeId), subQuery, "pn"); + if (filterMustBeIsDependency) { sql = sql.Where(rt => rt.IsDependency, "umbracoRelationType"); } + // Ordering is required for paging sql = sql.OrderBy(x => x.Alias); var pagedResult = _scopeAccessor.AmbientScope.Database.Page(pageIndex + 1, pageSize, sql); - totalRecords = Convert.ToInt32(pagedResult.TotalItems); + totalRecords = pagedResult.TotalItems; return pagedResult.Items.Select(MapDtoToEntity); } - public IEnumerable GetPagedRelationsForItems(int[] ids, long pageIndex, int pageSize, bool filterMustBeIsDependency, out long totalRecords) + /// + /// Gets a page of items which are in relation with the current item. + /// Basically, shows the items which depend on the current item. + /// + public IEnumerable GetPagedRelationsForItem(int id, long pageIndex, int pageSize, bool filterMustBeIsDependency, out long totalRecords) { - var sql = _scopeAccessor.AmbientScope.Database.SqlContext.Sql().Select( + var sql = _scopeAccessor.AmbientScope.Database.SqlContext.Sql().SelectDistinct( "[cn].[id] as nodeId", "[cn].[uniqueId] as nodeKey", "[cn].[text] as nodeName", @@ -147,17 +153,13 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement "[umbracoRelationType].[isDependency] as relationTypeIsDependency", "[umbracoRelationType].[dual] as relationTypeIsBidirectional") .From("r") - .InnerJoin("umbracoRelationType").On((left, right) => left.RelationType == right.Id, aliasLeft: "r", aliasRight:"umbracoRelationType") - .InnerJoin("cn").On((r, cn, rt) => (!rt.Dual && r.ParentId == cn.NodeId) || (rt.Dual && (r.ChildId == cn.NodeId || r.ParentId == cn.NodeId )), aliasLeft: "r", aliasRight:"cn", aliasOther: "umbracoRelationType" ) - .InnerJoin("pn").On((r, pn, cn) => (pn.NodeId == r.ChildId && cn.NodeId == r.ParentId) || (pn.NodeId == r.ParentId && cn.NodeId == r.ChildId), aliasLeft: "r", aliasRight:"pn", aliasOther:"cn" ) - .LeftJoin("c").On((left, right) => left.NodeId == right.NodeId, aliasLeft:"cn", aliasRight:"c") - .LeftJoin("ct").On((left, right) => left.ContentTypeId == right.NodeId, aliasLeft:"c", aliasRight:"ct") - .LeftJoin("ctn").On((left, right) => left.NodeId == right.NodeId, aliasLeft:"ct", aliasRight:"ctn"); - - if (ids.Any()) - { - sql = sql.Where(x => ids.Contains(x.NodeId), "pn"); - } + .InnerJoin("umbracoRelationType").On((left, right) => left.RelationType == right.Id, aliasLeft: "r", aliasRight: "umbracoRelationType") + .InnerJoin("cn").On((r, cn, rt) => (!rt.Dual && r.ParentId == cn.NodeId) || (rt.Dual && (r.ChildId == cn.NodeId || r.ParentId == cn.NodeId)), aliasLeft: "r", aliasRight: "cn", aliasOther: "umbracoRelationType") + .InnerJoin("pn").On((r, pn, cn) => (pn.NodeId == r.ChildId && cn.NodeId == r.ParentId) || (pn.NodeId == r.ParentId && cn.NodeId == r.ChildId), aliasLeft: "r", aliasRight: "pn", aliasOther: "cn") + .LeftJoin("c").On((left, right) => left.NodeId == right.NodeId, aliasLeft: "cn", aliasRight: "c") + .LeftJoin("ct").On((left, right) => left.ContentTypeId == right.NodeId, aliasLeft: "c", aliasRight: "ct") + .LeftJoin("ctn").On((left, right) => left.NodeId == right.NodeId, aliasLeft: "ct", aliasRight: "ctn") + .Where(x => x.NodeId == id, "pn"); if (filterMustBeIsDependency) { @@ -168,14 +170,13 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement sql = sql.OrderBy(x => x.Alias); var pagedResult = _scopeAccessor.AmbientScope.Database.Page(pageIndex + 1, pageSize, sql); - totalRecords = Convert.ToInt32(pagedResult.TotalItems); + totalRecords = pagedResult.TotalItems; return pagedResult.Items.Select(MapDtoToEntity); } private RelationItem MapDtoToEntity(RelationItemDto dto) { - var type = ObjectTypes.GetUdiType(dto.ChildNodeObjectType); return new RelationItem() { NodeId = dto.ChildNodeId, diff --git a/src/Umbraco.Infrastructure/Services/Implement/TrackedReferencesService.cs b/src/Umbraco.Infrastructure/Services/Implement/TrackedReferencesService.cs index 9a4cc8860e..ec22e1095c 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/TrackedReferencesService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/TrackedReferencesService.cs @@ -1,4 +1,3 @@ -using System.Linq; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; @@ -18,22 +17,32 @@ namespace Umbraco.Cms.Core.Services.Implement _entityService = entityService; } - public PagedResult GetPagedRelationsForItems(int[] ids, long pageIndex, int pageSize, bool filterMustBeIsDependency) + /// + /// Gets a paged result of items which are in relation with the current item. + /// Basically, shows the items which depend on the current item. + /// + public PagedResult GetPagedRelationsForItem(int id, long pageIndex, int pageSize, bool filterMustBeIsDependency) { using IScope scope = _scopeProvider.CreateScope(autoComplete: true); - var items = _trackedReferencesRepository.GetPagedRelationsForItems(ids, pageIndex, pageSize, filterMustBeIsDependency, out var totalItems); + var items = _trackedReferencesRepository.GetPagedRelationsForItem(id, pageIndex, pageSize, filterMustBeIsDependency, out var totalItems); - return new PagedResult(totalItems, pageIndex+1, pageSize) { Items = items }; + return new PagedResult(totalItems, pageIndex + 1, pageSize) { Items = items }; } + /// + /// Gets a paged result of items used in any kind of relation from selected integer ids. + /// public PagedResult GetPagedItemsWithRelations(int[] ids, long pageIndex, int pageSize, bool filterMustBeIsDependency) { using IScope scope = _scopeProvider.CreateScope(autoComplete: true); - var items = _trackedReferencesRepository.GetPagedItemsWithRelations(ids, pageIndex, pageSize, filterMustBeIsDependency, out var totalItems); + var items = _trackedReferencesRepository.GetPagedItemsWithRelations(ids, pageIndex, pageSize, filterMustBeIsDependency, out var totalItems); - return new PagedResult(totalItems, pageIndex+1, pageSize) { Items = items }; + return new PagedResult(totalItems, pageIndex + 1, pageSize) { Items = items }; } + /// + /// Gets a paged result of the descending items that have any references, given a parent id. + /// public PagedResult GetPagedDescendantsInReferences(int parentId, long pageIndex, int pageSize, bool filterMustBeIsDependency) { using IScope scope = _scopeProvider.CreateScope(autoComplete: true); @@ -44,7 +53,7 @@ namespace Umbraco.Cms.Core.Services.Implement pageSize, filterMustBeIsDependency, out var totalItems); - return new PagedResult(totalItems, pageIndex+1, pageSize) { Items = items }; + return new PagedResult(totalItems, pageIndex + 1, pageSize) { Items = items }; } } } diff --git a/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs b/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs index d8fa641891..c4328da2d4 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs @@ -1082,7 +1082,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers return new ActionResult(toMove); } - [Obsolete("Please use TrackedReferencesController.GetPagedReferences() instead. Scheduled for removal in V11.")] + [Obsolete("Please use TrackedReferencesController.GetPagedRelationsForItem() instead. Scheduled for removal in V11.")] public PagedResult GetPagedReferences(int id, string entityType, int pageNumber = 1, int pageSize = 100) { if (pageNumber <= 0 || pageSize <= 0) diff --git a/src/Umbraco.Web.BackOffice/Controllers/TrackedReferencesController.cs b/src/Umbraco.Web.BackOffice/Controllers/TrackedReferencesController.cs index 2cef8d61af..aa1a0ee86e 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/TrackedReferencesController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/TrackedReferencesController.cs @@ -1,12 +1,7 @@ -using System.Collections.Generic; -using System.Linq; -using System.Runtime.Serialization; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.Models.ContentEditing; -using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Web.BackOffice.ModelBinders; using Umbraco.Cms.Web.Common.Attributes; @@ -19,28 +14,36 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers public class TrackedReferencesController : BackOfficeNotificationsController { private readonly ITrackedReferencesService _relationService; - private readonly IEntityService _entityService; - public TrackedReferencesController(ITrackedReferencesService relationService, - IEntityService entityService) + public TrackedReferencesController(ITrackedReferencesService relationService) { _relationService = relationService; - _entityService = entityService; } - // Used by info tabs on content, media etc. So this is basically finding childs of relations. - public ActionResult> GetPagedReferences(int id, int pageNumber = 1, - int pageSize = 100, bool filterMustBeIsDependency = false) + /// + /// Gets a page list of tracked references for the current item, so you can see where an item is being used. + /// + /// + /// Used by info tabs on content, media etc. and for the delete and unpublish of single items. + /// This is basically finding parents of relations. + /// + public ActionResult> GetPagedReferences(int id, int pageNumber = 1, int pageSize = 100, bool filterMustBeIsDependency = false) { if (pageNumber <= 0 || pageSize <= 0) { return BadRequest("Both pageNumber and pageSize must be greater than zero"); } - return _relationService.GetPagedRelationsForItems(new []{id}, pageNumber - 1, pageSize, filterMustBeIsDependency); + return _relationService.GetPagedRelationsForItem(id, pageNumber - 1, pageSize, filterMustBeIsDependency); } - // Used on delete, finds + /// + /// Gets a page list of the child nodes of the current item used in any kind of relation. + /// + /// + /// Used when deleting and unpublishing a single item to check if this item has any descending items that are in any kind of relation. + /// This is basically finding the descending items which are children in relations. + /// public ActionResult> GetPagedDescendantsInReferences(int parentId, int pageNumber = 1, int pageSize = 100, bool filterMustBeIsDependency = true) { if (pageNumber <= 0 || pageSize <= 0) @@ -48,12 +51,16 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers return BadRequest("Both pageNumber and pageSize must be greater than zero"); } - return _relationService.GetPagedDescendantsInReferences(parentId, pageNumber - 1, pageSize, filterMustBeIsDependency); - } - // Used by unpublish content. So this is basically finding parents of relations. + /// + /// Gets a page list of the items used in any kind of relation from selected integer ids. + /// + /// + /// Used when bulk deleting content/media and bulk unpublishing content (delete and unpublish on List view). + /// This is basically finding children of relations. + /// [HttpGet] [HttpPost] public ActionResult> GetPagedReferencedItems([FromJsonPath] int[] ids, int pageNumber = 1, int pageSize = 100, bool filterMustBeIsDependency = true) @@ -64,8 +71,6 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers } return _relationService.GetPagedItemsWithRelations(ids, pageNumber - 1, pageSize, filterMustBeIsDependency); - } } - } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbcontentnodeinfo.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbcontentnodeinfo.directive.js index 83292251da..501ea9f81a 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbcontentnodeinfo.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbcontentnodeinfo.directive.js @@ -12,6 +12,7 @@ scope.publishStatus = []; scope.currentVariant = null; scope.currentUrls = []; + scope.loadingReferences = false; scope.disableTemplates = Umbraco.Sys.ServerVariables.features.disabledFeatures.disableTemplates; scope.allowChangeDocumentType = false; @@ -229,6 +230,10 @@ }); } + + function loadReferences(){ + scope.loadingReferences = true; + } function loadRedirectUrls() { scope.loadingRedirectUrls = true; //check if Redirect URL Management is enabled @@ -335,6 +340,7 @@ loadRedirectUrls(); setNodePublishStatus(); formatDatesToLocal(); + loadReferences(); } else { isInfoTab = false; } @@ -352,6 +358,7 @@ loadRedirectUrls(); setNodePublishStatus(); formatDatesToLocal(); + loadReferences(); } updateCurrentUrls(); }); diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/trackedreferences.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/trackedreferences.resource.js index cd64c89589..d64951a6d0 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/trackedreferences.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/trackedreferences.resource.js @@ -162,7 +162,7 @@ function trackedReferencesResource($q, $http, umbRequestHelper) { $http.post( umbRequestHelper.getApiUrl( "trackedReferencesApiBaseUrl", - "getPagedReferencedItems", + "GetPagedReferencedItems", query), { ids: ids, diff --git a/src/Umbraco.Web.UI.Client/src/views/components/content/umb-content-node-info.html b/src/Umbraco.Web.UI.Client/src/views/components/content/umb-content-node-info.html index 6429e39db6..1c7545f9ec 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/content/umb-content-node-info.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/content/umb-content-node-info.html @@ -21,7 +21,7 @@ - + diff --git a/src/Umbraco.Web.UI.Client/src/views/components/references/umb-tracked-references-table.html b/src/Umbraco.Web.UI.Client/src/views/components/references/umb-tracked-references-table.html index afc8f9a3e6..d09bc23318 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/references/umb-tracked-references-table.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/references/umb-tracked-references-table.html @@ -7,8 +7,8 @@
    -
    Node Name
    -
    Type Name
    +
    Node Name
    +
    Type Name
    Type
    Relation
    diff --git a/src/Umbraco.Web.UI.Client/src/views/components/references/umb-tracked-references.html b/src/Umbraco.Web.UI.Client/src/views/components/references/umb-tracked-references.html index fd788cc598..9e08c5fbae 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/references/umb-tracked-references.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/references/umb-tracked-references.html @@ -26,11 +26,11 @@
    - +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js index 79164a2457..684ce6d2f0 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js @@ -374,7 +374,7 @@ } function enableUser() { - vm.enableUserButtonState = "busfy"; + vm.enableUserButtonState = "busy"; usersResource.enableUsers([vm.user.id]).then(function (data) { vm.user.userState = "Active"; setUserDisplayState(); diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml index 6d78c7db5b..bf2de30f2e 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml @@ -788,6 +788,7 @@ New Next No + Node Name of Off OK @@ -829,6 +830,7 @@ Submit Success Type + Type Name Type to search... under Up @@ -2390,6 +2392,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Parent Child Count + Relation Relations Created Comment @@ -2470,18 +2473,13 @@ To manage your website, simply open the Umbraco backoffice and start adding cont References This Data Type has no references. + This item has no references. Used in Document Types Used in Media Types Used in Member Types Used by - Used in Documents - Used in Members - Used in Media Items in use Descendants in use - One or more of this item's descendants is being used in a media item. - One or more of this item's descendants is being used in a content item. - One or more of this item's descendants is being used in a member. This item or its descendants is being used. Deletion can lead to broken links on your website. This item or its descendants is being used. Unpublishing can lead to broken links on your website. Please take the appropriate actions. This item or its descendants is being used. Therefore, deletion has been disabled. 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 fab7392ad8..873361169a 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml @@ -809,7 +809,7 @@ New Next No - Node Name + Node Name of Off OK @@ -850,7 +850,7 @@ Submit Success Type - Type Name + Type Name Type to search... under Up @@ -1495,7 +1495,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Visit our.umbraco.com Visit umbraco.tv Watch our free tutorial videos - on the Umbraco Learning Base + on the Umbraco Learning Base Default template @@ -2562,15 +2562,9 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Referenced by Referenced by the following items The following items depend on this - Referenced by the following Documents - Referenced by the following Members - Referenced by the following Media The following items are referenced The following descendant items have dependencies - The following descending items has dependencies - One or more of this item's descendants is being referenced in a media item. - One or more of this item's descendants is being referenced in a content item. - One or more of this item's descendants is being referenced in a member. + The following descending items have dependencies This item or its descendants is being referenced. Deletion can lead to broken links on your website. This item or its descendants is being referenced. Unpublishing can lead to broken links on your website. Please take the appropriate actions. This item or its descendants is being referenced. Therefore, deletion has been disabled.