diff --git a/src/Umbraco.Cms.Api.Delivery/Controllers/ByRouteContentApiController.cs b/src/Umbraco.Cms.Api.Delivery/Controllers/ByRouteContentApiController.cs index 2209c1a330..02181f6129 100644 --- a/src/Umbraco.Cms.Api.Delivery/Controllers/ByRouteContentApiController.cs +++ b/src/Umbraco.Cms.Api.Delivery/Controllers/ByRouteContentApiController.cs @@ -58,7 +58,8 @@ public class ByRouteContentApiController : ContentApiItemControllerBase path = WebUtility.UrlDecode(path); } - path = path.EnsureStartsWith("/"); + path = path.TrimStart("/"); + path = path.Length == 0 ? "/" : path; IPublishedContent? contentItem = GetContent(path); if (contentItem is not null) diff --git a/src/Umbraco.Core/DeliveryApi/ApiContentBuilder.cs b/src/Umbraco.Core/DeliveryApi/ApiContentBuilder.cs index 11ea4faf77..135afe068a 100644 --- a/src/Umbraco.Core/DeliveryApi/ApiContentBuilder.cs +++ b/src/Umbraco.Core/DeliveryApi/ApiContentBuilder.cs @@ -10,6 +10,6 @@ public sealed class ApiContentBuilder : ApiContentBuilderBase, IApi { } - protected override IApiContent Create(IPublishedContent content, Guid id, string name, string contentType, IApiContentRoute route, IDictionary properties) - => new ApiContent(id, name, contentType, route, properties); + protected override IApiContent Create(IPublishedContent content, string name, IApiContentRoute route, IDictionary properties) + => new ApiContent(content.Key, name, content.ContentType.Alias, content.CreateDate, content.UpdateDate, route, properties); } diff --git a/src/Umbraco.Core/DeliveryApi/ApiContentBuilderBase.cs b/src/Umbraco.Core/DeliveryApi/ApiContentBuilderBase.cs index ae70f0fdde..8ffcd6d849 100644 --- a/src/Umbraco.Core/DeliveryApi/ApiContentBuilderBase.cs +++ b/src/Umbraco.Core/DeliveryApi/ApiContentBuilderBase.cs @@ -17,7 +17,7 @@ public abstract class ApiContentBuilderBase _outputExpansionStrategyAccessor = outputExpansionStrategyAccessor; } - protected abstract T Create(IPublishedContent content, Guid id, string name, string contentType, IApiContentRoute route, IDictionary properties); + protected abstract T Create(IPublishedContent content, string name, IApiContentRoute route, IDictionary properties); public virtual T? Build(IPublishedContent content) { @@ -34,9 +34,7 @@ public abstract class ApiContentBuilderBase return Create( content, - content.Key, _apiContentNameProvider.GetName(content), - content.ContentType.Alias, route, properties); } diff --git a/src/Umbraco.Core/DeliveryApi/ApiContentResponseBuilder.cs b/src/Umbraco.Core/DeliveryApi/ApiContentResponseBuilder.cs index eb9cea6961..a551115a1e 100644 --- a/src/Umbraco.Core/DeliveryApi/ApiContentResponseBuilder.cs +++ b/src/Umbraco.Core/DeliveryApi/ApiContentResponseBuilder.cs @@ -13,7 +13,7 @@ public sealed class ApiContentResponseBuilder : ApiContentBuilderBase _apiContentRouteBuilder = apiContentRouteBuilder; - protected override IApiContentResponse Create(IPublishedContent content, Guid id, string name, string contentType, IApiContentRoute route, IDictionary properties) + protected override IApiContentResponse Create(IPublishedContent content, string name, IApiContentRoute route, IDictionary properties) { var routesByCulture = new Dictionary(); @@ -35,6 +35,6 @@ public sealed class ApiContentResponseBuilder : ApiContentBuilderBase properties) + public ApiContent(Guid id, string name, string contentType, DateTime createDate, DateTime updateDate, IApiContentRoute route, IDictionary properties) : base(id, contentType, properties) { Name = name; + CreateDate = createDate; + UpdateDate = updateDate; Route = route; } public string Name { get; } + public DateTime CreateDate { get; } + + public DateTime UpdateDate { get; } + public IApiContentRoute Route { get; } } diff --git a/src/Umbraco.Core/Models/DeliveryApi/ApiContentResponse.cs b/src/Umbraco.Core/Models/DeliveryApi/ApiContentResponse.cs index d692ff464e..ce57cf7417 100644 --- a/src/Umbraco.Core/Models/DeliveryApi/ApiContentResponse.cs +++ b/src/Umbraco.Core/Models/DeliveryApi/ApiContentResponse.cs @@ -4,8 +4,8 @@ namespace Umbraco.Cms.Core.Models.DeliveryApi; public class ApiContentResponse : ApiContent, IApiContentResponse { - public ApiContentResponse(Guid id, string name, string contentType, IApiContentRoute route, IDictionary properties, IDictionary cultures) - : base(id, name, contentType, route, properties) + public ApiContentResponse(Guid id, string name, string contentType, DateTime createDate, DateTime updateDate, IApiContentRoute route, IDictionary properties, IDictionary cultures) + : base(id, name, contentType, createDate, updateDate, route, properties) => Cultures = cultures; // a little DX; by default this dictionary will be serialized as the first part of the response due to the inner workings of the serializer. diff --git a/src/Umbraco.Core/Models/DeliveryApi/IApiContent.cs b/src/Umbraco.Core/Models/DeliveryApi/IApiContent.cs index 0f605eda19..9192cbe7d5 100644 --- a/src/Umbraco.Core/Models/DeliveryApi/IApiContent.cs +++ b/src/Umbraco.Core/Models/DeliveryApi/IApiContent.cs @@ -4,5 +4,9 @@ public interface IApiContent : IApiElement { string? Name { get; } + public DateTime CreateDate { get; } + + public DateTime UpdateDate { get; } + IApiContentRoute Route { get; } } diff --git a/src/Umbraco.Examine.Lucene/UmbracoContentIndex.cs b/src/Umbraco.Examine.Lucene/UmbracoContentIndex.cs index 3053da44cf..f4f333f0ad 100644 --- a/src/Umbraco.Examine.Lucene/UmbracoContentIndex.cs +++ b/src/Umbraco.Examine.Lucene/UmbracoContentIndex.cs @@ -41,6 +41,7 @@ public class UmbracoContentIndex : UmbracoExamineIndex, IUmbracoContentIndex if (namedOptions.Validator is IContentValueSetValidator contentValueSetValidator) { PublishedValuesOnly = contentValueSetValidator.PublishedValuesOnly; + SupportProtectedContent = contentValueSetValidator.SupportProtectedContent; } } diff --git a/src/Umbraco.Examine.Lucene/UmbracoExamineIndex.cs b/src/Umbraco.Examine.Lucene/UmbracoExamineIndex.cs index 969c58ac55..5faccf581f 100644 --- a/src/Umbraco.Examine.Lucene/UmbracoExamineIndex.cs +++ b/src/Umbraco.Examine.Lucene/UmbracoExamineIndex.cs @@ -47,6 +47,8 @@ public abstract class UmbracoExamineIndex : LuceneIndex, IUmbracoIndex, IIndexDi public bool PublishedValuesOnly { get; protected set; } = false; + public bool SupportProtectedContent { get; protected set; } = true; + /// /// override to check if we can actually initialize. /// diff --git a/src/Umbraco.Infrastructure/CompatibilitySuppressions.xml b/src/Umbraco.Infrastructure/CompatibilitySuppressions.xml index 2e495b5071..a9076b112d 100644 --- a/src/Umbraco.Infrastructure/CompatibilitySuppressions.xml +++ b/src/Umbraco.Infrastructure/CompatibilitySuppressions.xml @@ -85,4 +85,18 @@ lib/net7.0/Umbraco.Infrastructure.dll true - \ No newline at end of file + + CP0006 + M:Umbraco.Cms.Infrastructure.Search.IUmbracoIndexingHandler.RemoveProtectedContent + lib/net7.0/Umbraco.Infrastructure.dll + lib/net7.0/Umbraco.Infrastructure.dll + true + + + CP0006 + P:Umbraco.Cms.Infrastructure.Examine.IUmbracoIndex.SupportProtectedContent + lib/net7.0/Umbraco.Infrastructure.dll + lib/net7.0/Umbraco.Infrastructure.dll + true + + diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Examine.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Examine.cs index fb3adb9219..5139cba48f 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Examine.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Examine.cs @@ -59,6 +59,7 @@ public static partial class UmbracoBuilderExtensions builder.Services.AddSingleton(); builder.AddNotificationHandler(); + builder.AddNotificationHandler(); builder.AddNotificationHandler(); builder.AddNotificationHandler(); builder.AddNotificationHandler(); diff --git a/src/Umbraco.Infrastructure/Examine/ExamineUmbracoIndexingHandler.cs b/src/Umbraco.Infrastructure/Examine/ExamineUmbracoIndexingHandler.cs index ea3727f31a..c77606e97a 100644 --- a/src/Umbraco.Infrastructure/Examine/ExamineUmbracoIndexingHandler.cs +++ b/src/Umbraco.Infrastructure/Examine/ExamineUmbracoIndexingHandler.cs @@ -4,6 +4,7 @@ using Examine.Search; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.HostedServices; using Umbraco.Cms.Infrastructure.Search; using Umbraco.Extensions; @@ -25,6 +26,7 @@ internal class ExamineUmbracoIndexingHandler : IUmbracoIndexingHandler private readonly IPublishedContentValueSetBuilder _publishedContentValueSetBuilder; private readonly ICoreScopeProvider _scopeProvider; private readonly ExamineIndexingMainDomHandler _mainDomHandler; + private readonly IPublicAccessService _publicAccessService; public ExamineUmbracoIndexingHandler( ILogger logger, @@ -35,7 +37,8 @@ internal class ExamineUmbracoIndexingHandler : IUmbracoIndexingHandler IPublishedContentValueSetBuilder publishedContentValueSetBuilder, IValueSetBuilder mediaValueSetBuilder, IValueSetBuilder memberValueSetBuilder, - ExamineIndexingMainDomHandler mainDomHandler) + ExamineIndexingMainDomHandler mainDomHandler, + IPublicAccessService publicAccessService) { _logger = logger; _scopeProvider = scopeProvider; @@ -46,6 +49,7 @@ internal class ExamineUmbracoIndexingHandler : IUmbracoIndexingHandler _mediaValueSetBuilder = mediaValueSetBuilder; _memberValueSetBuilder = memberValueSetBuilder; _mainDomHandler = mainDomHandler; + _publicAccessService = publicAccessService; _enabled = new Lazy(IsEnabled); } @@ -122,6 +126,20 @@ internal class ExamineUmbracoIndexingHandler : IUmbracoIndexingHandler } } + /// + public void RemoveProtectedContent() + { + var actions = DeferredActions.Get(_scopeProvider); + if (actions != null) + { + actions.Add(new DeferredRemoveProtectedContent(_backgroundTaskQueue, this, _publicAccessService)); + } + else + { + DeferredRemoveProtectedContent.Execute(_backgroundTaskQueue, this, _publicAccessService); + } + } + /// public void DeleteDocumentsForContentTypes(IReadOnlyCollection removedContentTypes) { @@ -391,5 +409,50 @@ internal class ExamineUmbracoIndexingHandler : IUmbracoIndexingHandler } } + /// + /// Removes all protected content from applicable indexes on a background thread + /// + private class DeferredRemoveProtectedContent : IDeferredAction + { + private readonly IBackgroundTaskQueue _backgroundTaskQueue; + private readonly ExamineUmbracoIndexingHandler _examineUmbracoIndexingHandler; + private readonly IPublicAccessService _publicAccessService; + + public DeferredRemoveProtectedContent(IBackgroundTaskQueue backgroundTaskQueue, ExamineUmbracoIndexingHandler examineUmbracoIndexingHandler, IPublicAccessService publicAccessService) + { + _backgroundTaskQueue = backgroundTaskQueue; + _examineUmbracoIndexingHandler = examineUmbracoIndexingHandler; + _publicAccessService = publicAccessService; + } + + public void Execute() => Execute(_backgroundTaskQueue, _examineUmbracoIndexingHandler, _publicAccessService); + + public static void Execute(IBackgroundTaskQueue backgroundTaskQueue, ExamineUmbracoIndexingHandler examineUmbracoIndexingHandler, IPublicAccessService publicAccessService) + => backgroundTaskQueue.QueueBackgroundWorkItem(cancellationToken => + { + using ICoreScope scope = examineUmbracoIndexingHandler._scopeProvider.CreateCoreScope(autoComplete: true); + + var protectedContentIds = publicAccessService.GetAll().Select(entry => entry.ProtectedNodeId).ToArray(); + if (protectedContentIds.Any() is false) + { + return Task.CompletedTask; + } + + foreach (IUmbracoContentIndex index in examineUmbracoIndexingHandler._examineManager.Indexes + .OfType() + .Where(x => x is { EnableDefaultEventHandler: true, SupportProtectedContent: false })) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.CompletedTask; + } + + index.DeleteFromIndex(protectedContentIds.Select(id => id.ToString())); + } + + return Task.CompletedTask; + }); + } + #endregion } diff --git a/src/Umbraco.Infrastructure/Examine/IUmbracoIndex.cs b/src/Umbraco.Infrastructure/Examine/IUmbracoIndex.cs index a94201894a..0091618ec0 100644 --- a/src/Umbraco.Infrastructure/Examine/IUmbracoIndex.cs +++ b/src/Umbraco.Infrastructure/Examine/IUmbracoIndex.cs @@ -21,4 +21,12 @@ public interface IUmbracoIndex : IIndex, IIndexStats /// * non-published Variants /// bool PublishedValuesOnly { get; } + + /// + /// Whether the index can contain protected content + /// + /// + /// To retain backwards compatability, the default value is true + /// + bool SupportProtectedContent { get; } } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/MarkdownEditorValueConverter.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/MarkdownEditorValueConverter.cs index 3efbce8fb9..9aeeb02d92 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/MarkdownEditorValueConverter.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/MarkdownEditorValueConverter.cs @@ -72,7 +72,6 @@ public class MarkdownEditorValueConverter : PropertyValueConverterBase, IDeliver return string.Empty; } - var mark = new Markdown(); - return mark.Transform(markdownString); + return markdownString; } } diff --git a/src/Umbraco.Infrastructure/Search/IUmbracoIndexingHandler.cs b/src/Umbraco.Infrastructure/Search/IUmbracoIndexingHandler.cs index 53f9988052..20ff27b287 100644 --- a/src/Umbraco.Infrastructure/Search/IUmbracoIndexingHandler.cs +++ b/src/Umbraco.Infrastructure/Search/IUmbracoIndexingHandler.cs @@ -19,6 +19,11 @@ public interface IUmbracoIndexingHandler void ReIndexForMedia(IMedia sender, bool isPublished); + /// + /// Removes any content that is flagged as protected + /// + void RemoveProtectedContent(); + /// /// Deletes all documents for the content type Ids /// diff --git a/src/Umbraco.Infrastructure/Search/IndexingNotificationHandler.Content.cs b/src/Umbraco.Infrastructure/Search/IndexingNotificationHandler.Content.cs index 66eb61ce3c..8ab2c14f89 100644 --- a/src/Umbraco.Infrastructure/Search/IndexingNotificationHandler.Content.cs +++ b/src/Umbraco.Infrastructure/Search/IndexingNotificationHandler.Content.cs @@ -9,7 +9,9 @@ using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Search; -public sealed class ContentIndexingNotificationHandler : INotificationHandler +public sealed class ContentIndexingNotificationHandler : + INotificationHandler, + INotificationHandler { private readonly IContentService _contentService; private readonly IUmbracoIndexingHandler _umbracoIndexingHandler; @@ -158,4 +160,7 @@ public sealed class ContentIndexingNotificationHandler : INotificationHandler _umbracoIndexingHandler.RemoveProtectedContent(); } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/PublishedContentQueryTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/PublishedContentQueryTests.cs index 1a9a37fd3b..80aa1fe662 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/PublishedContentQueryTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/PublishedContentQueryTests.cs @@ -36,6 +36,7 @@ public class PublishedContentQueryTests : ExamineBaseTest public bool EnableDefaultEventHandler => throw new NotImplementedException(); public bool PublishedValuesOnly => throw new NotImplementedException(); + public bool SupportProtectedContent => throw new NotImplementedException(); public IEnumerable GetFields() => _fieldNames; } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentBuilderTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentBuilderTests.cs index 026ab42c0c..6e0dfd7e6f 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentBuilderTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentBuilderTests.cs @@ -28,6 +28,8 @@ public class ContentBuilderTests : DeliveryApiTests var urlSegment = "url-segment"; var name = "The page"; ConfigurePublishedContentMock(content, key, name, urlSegment, contentType.Object, new[] { prop1, prop2 }); + content.SetupGet(c => c.CreateDate).Returns(new DateTime(2023, 06, 01)); + content.SetupGet(c => c.UpdateDate).Returns(new DateTime(2023, 07, 12)); var publishedUrlProvider = new Mock(); publishedUrlProvider @@ -47,6 +49,8 @@ public class ContentBuilderTests : DeliveryApiTests Assert.AreEqual(2, result.Properties.Count); Assert.AreEqual("Delivery API value", result.Properties["deliveryApi"]); Assert.AreEqual("Default value", result.Properties["default"]); + Assert.AreEqual(new DateTime(2023, 06, 01), result.CreateDate); + Assert.AreEqual(new DateTime(2023, 07, 12), result.UpdateDate); } [Test] diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/MarkdownEditorValueConverterTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/MarkdownEditorValueConverterTests.cs index 8b9ef31bc7..84ee7b0841 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/MarkdownEditorValueConverterTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/MarkdownEditorValueConverterTests.cs @@ -17,8 +17,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.DeliveryApi; [TestFixture] public class MarkdownEditorValueConverterTests : PropertyValueConverterTests { - [TestCase("hello world", "

hello world

")] - [TestCase("hello *world*", "

hello world

")] + [TestCase("hello world", "hello world")] + [TestCase("hello *world*", "hello *world*")] [TestCase("", "")] [TestCase(null, "")] [TestCase(123, "")]