From eb979625d1ad8a56a74cf968ee27444a157c224e Mon Sep 17 00:00:00 2001 From: Andy Butland <abutland73@gmail.com> Date: Tue, 18 Mar 2025 11:05:54 +0100 Subject: [PATCH 1/8] Render folders before files in static files picker. (#18701) --- .../Trees/StaticFilesTreeController.cs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Umbraco.Web.BackOffice/Trees/StaticFilesTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/StaticFilesTreeController.cs index 92931f14ca44..1daefc0bc54a 100644 --- a/src/Umbraco.Web.BackOffice/Trees/StaticFilesTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/StaticFilesTreeController.cs @@ -98,16 +98,6 @@ private void AddRootFolder(string directory, FormCollection queryStrings, TreeNo private void AddPhysicalFiles(string path, FormCollection queryStrings, TreeNodeCollection nodes) { - IEnumerable<string> files = _fileSystem.GetFiles(path) - .Where(x => x.StartsWith(AppPlugins) || x.StartsWith(Webroot)); - - foreach (var file in files) - { - var name = Path.GetFileName(file); - TreeNode node = CreateTreeNode(WebUtility.UrlEncode(file), path, queryStrings, name, Constants.Icons.DefaultIcon, false); - nodes.Add(node); - } - IEnumerable<string> directories = _fileSystem.GetDirectories(path); foreach (var directory in directories) @@ -117,6 +107,16 @@ private void AddPhysicalFiles(string path, FormCollection queryStrings, TreeNode TreeNode node = CreateTreeNode(WebUtility.UrlEncode(directory), path, queryStrings, name, Constants.Icons.Folder, hasChildren); nodes.Add(node); } + + IEnumerable<string> files = _fileSystem.GetFiles(path) + .Where(x => x.StartsWith(AppPlugins) || x.StartsWith(Webroot)); + + foreach (var file in files) + { + var name = Path.GetFileName(file); + TreeNode node = CreateTreeNode(WebUtility.UrlEncode(file), path, queryStrings, name, Constants.Icons.DefaultIcon, false); + nodes.Add(node); + } } private void AddWebRootFiles(string path, FormCollection queryStrings, TreeNodeCollection nodes) From 5570583f707b1872b9bae3e1593dc5bcc35dc13b Mon Sep 17 00:00:00 2001 From: Andy Butland <abutland73@gmail.com> Date: Tue, 18 Mar 2025 11:10:23 +0100 Subject: [PATCH 2/8] Fixes issue with macro rendering in an RTE when GUIDs are used for backoffice document routes (#18691) * Fixes issue with macro rendering in an RTE when GUIDs are used for backoffice document routes. * Fixed null reference error. --- .../Controllers/MacroRenderingController.cs | 46 +++++++++++++++++-- 1 file changed, 41 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Web.BackOffice/Controllers/MacroRenderingController.cs b/src/Umbraco.Web.BackOffice/Controllers/MacroRenderingController.cs index efa322a88ccc..1d2fb7eb9b67 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MacroRenderingController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MacroRenderingController.cs @@ -87,7 +87,7 @@ public ActionResult<IEnumerable<MacroParameter>> GetMacroParameters(int macroId) [HttpGet] public async Task<IActionResult> GetMacroResultAsHtmlForEditor(string macroAlias, int pageId, [FromQuery] IDictionary<string, object> macroParams) => - await GetMacroResultAsHtml(macroAlias, pageId, macroParams); + await GetMacroResultAsHtml(macroAlias, pageId.ToString(), macroParams); /// <summary> /// Gets a rendered macro as HTML for rendering in the rich text editor. @@ -98,11 +98,24 @@ public async Task<IActionResult> GetMacroResultAsHtmlForEditor(string macroAlias /// <param name="model"></param> /// <returns></returns> [HttpPost] + [NonAction] + [Obsolete("This endpoint is no longer used.")] public async Task<IActionResult> GetMacroResultAsHtmlForEditor(MacroParameterModel model) => + await GetMacroResultAsHtml(model.MacroAlias, model.PageId.ToString(), model.MacroParams); + + /// <summary> + /// Gets a rendered macro as HTML for rendering in the rich text editor. + /// Using HTTP POST instead of GET allows for more parameters to be passed as it's not dependent on URL-length + /// limitations like GET. + /// The method using GET is kept to maintain backwards compatibility + /// </summary> + /// <param name="model"></param> + /// <returns></returns> + [HttpPost] + public async Task<IActionResult> GetMacroResultAsHtmlForEditor(MacroParameterModel2 model) => await GetMacroResultAsHtml(model.MacroAlias, model.PageId, model.MacroParams); - private async Task<IActionResult> GetMacroResultAsHtml(string? macroAlias, int pageId, - IDictionary<string, object>? macroParams) + private async Task<IActionResult> GetMacroResultAsHtml(string? macroAlias, string pageId, IDictionary<string, object>? macroParams) { IMacro? m = macroAlias is null ? null : _macroService.GetByAlias(macroAlias); if (m == null) @@ -111,11 +124,11 @@ private async Task<IActionResult> GetMacroResultAsHtml(string? macroAlias, int p } IUmbracoContext umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); - IPublishedContent? publishedContent = umbracoContext.Content?.GetById(true, pageId); + IPublishedContent? publishedContent = GetPagePublishedContent(pageId, umbracoContext); //if it isn't supposed to be rendered in the editor then return an empty string //currently we cannot render a macro if the page doesn't yet exist - if (pageId == -1 || publishedContent == null || m.DontRender) + if (publishedContent == null || m.DontRender) { //need to create a specific content result formatted as HTML since this controller has been configured //with only json formatters. @@ -149,6 +162,21 @@ private async Task<IActionResult> GetMacroResultAsHtml(string? macroAlias, int p } } + private static IPublishedContent? GetPagePublishedContent(string pageId, IUmbracoContext umbracoContext) + { + if (int.TryParse(pageId, NumberStyles.Integer, CultureInfo.InvariantCulture, out int pageIdAsInt)) + { + return umbracoContext.Content?.GetById(true, pageIdAsInt); + } + + if (Guid.TryParse(pageId, out Guid pageIdAsGuid)) + { + return umbracoContext.Content?.GetById(true, pageIdAsGuid); + } + + return null; + } + [HttpPost] public IActionResult CreatePartialViewMacroWithFile(CreatePartialViewMacroWithFileModel model) { @@ -180,6 +208,7 @@ public IActionResult CreatePartialViewMacroWithFile(CreatePartialViewMacroWithFi return Ok(); } + [Obsolete("This model is no longer used and has been replaced with MacroParameterModel2 that changes the type of the PageId property.")] public class MacroParameterModel { public string? MacroAlias { get; set; } @@ -187,6 +216,13 @@ public class MacroParameterModel public IDictionary<string, object>? MacroParams { get; set; } } + public class MacroParameterModel2 + { + public string? MacroAlias { get; set; } + public string PageId { get; set; } = string.Empty; + public IDictionary<string, object>? MacroParams { get; set; } + } + public class CreatePartialViewMacroWithFileModel { public string? Filename { get; set; } From 68acc2aa517060ace3d86366639007acc36a5bb2 Mon Sep 17 00:00:00 2001 From: Andy Butland <abutland73@gmail.com> Date: Wed, 19 Mar 2025 16:39:34 +0100 Subject: [PATCH 3/8] Bumped version to 13.9.0-rc. --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index 94daac6c7ac6..d6effe648ee1 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json", - "version": "13.8.0-rc", + "version": "13.9.0-rc", "assemblyVersion": { "precision": "build" }, From eb91f4fef4244e5c0a039ab22212449444f1fed8 Mon Sep 17 00:00:00 2001 From: Andy Butland <abutland73@gmail.com> Date: Thu, 20 Mar 2025 06:53:43 +0100 Subject: [PATCH 4/8] Make preview check for delivery API content case insensitive. (#18731) --- .../Services/RequestPreviewService.cs | 4 +-- .../Services/RequestPreviewServiceTests.cs | 31 +++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Api.Delivery/Services/RequestPreviewServiceTests.cs diff --git a/src/Umbraco.Cms.Api.Delivery/Services/RequestPreviewService.cs b/src/Umbraco.Cms.Api.Delivery/Services/RequestPreviewService.cs index 874e2af7bb1e..f891aee6894f 100644 --- a/src/Umbraco.Cms.Api.Delivery/Services/RequestPreviewService.cs +++ b/src/Umbraco.Cms.Api.Delivery/Services/RequestPreviewService.cs @@ -1,4 +1,4 @@ -using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http; using Umbraco.Cms.Core.DeliveryApi; namespace Umbraco.Cms.Api.Delivery.Services; @@ -11,5 +11,5 @@ public RequestPreviewService(IHttpContextAccessor httpContextAccessor) } /// <inheritdoc /> - public bool IsPreview() => GetHeaderValue("Preview") == "true"; + public bool IsPreview() => string.Equals(GetHeaderValue("Preview"), "true", StringComparison.OrdinalIgnoreCase); } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Api.Delivery/Services/RequestPreviewServiceTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Api.Delivery/Services/RequestPreviewServiceTests.cs new file mode 100644 index 000000000000..6f5c5e0790a2 --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Api.Delivery/Services/RequestPreviewServiceTests.cs @@ -0,0 +1,31 @@ +using Microsoft.AspNetCore.Http; +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Api.Delivery.Services; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Cms.Api.Delivery.Services; + +[TestFixture] +public class RequestPreviewServiceTests +{ + [TestCase(null, false)] + [TestCase("", false)] + [TestCase("false", false)] + [TestCase("true", true)] + [TestCase("True", true)] + public void IsPreview_Returns_Expected_Result(string? headerValue, bool expected) + { + var httpContext = new DefaultHttpContext(); + httpContext.Request.Headers["Preview"] = headerValue; + + var httpContextAccessorMock = new Mock<IHttpContextAccessor>(); + httpContextAccessorMock + .Setup(x => x.HttpContext) + .Returns(httpContext); + var sut = new RequestPreviewService(httpContextAccessorMock.Object); + + var result = sut.IsPreview(); + + Assert.AreEqual(expected, result); + } +} From 05a7d337de628d4c09964464deb387572d752b05 Mon Sep 17 00:00:00 2001 From: Andy Butland <abutland73@gmail.com> Date: Thu, 3 Apr 2025 10:32:27 +0200 Subject: [PATCH 5/8] Check we have matched a preview URL by ID when exiting preview. (#18841) --- src/Umbraco.Web.BackOffice/Controllers/PreviewController.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Umbraco.Web.BackOffice/Controllers/PreviewController.cs b/src/Umbraco.Web.BackOffice/Controllers/PreviewController.cs index 676e18a58905..3af0628313b7 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/PreviewController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/PreviewController.cs @@ -217,10 +217,8 @@ public ActionResult End(string? redir = null) // are we attempting a redirect to the default route (by ID with optional culture)? Match match = DefaultPreviewRedirectRegex().Match(redir ?? string.Empty); - if (match.Success) + if (match.Success && int.TryParse(match.Groups["id"].Value, out int id)) { - var id = int.Parse(match.Groups["id"].Value); - // first try to resolve the published URL if (_umbracoContextAccessor.TryGetUmbracoContext(out IUmbracoContext? umbracoContext) && umbracoContext.Content is not null) From d60a2a217c77fdcf06368165d2c045d13a515613 Mon Sep 17 00:00:00 2001 From: Andy Butland <abutland73@gmail.com> Date: Thu, 3 Apr 2025 21:13:45 +0200 Subject: [PATCH 6/8] Verify endpoint selection candidates with host attribute are ignored if request doesn't match the configured hosts. (#18820) --- .../Routing/EagerMatcherPolicy.cs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.Website/Routing/EagerMatcherPolicy.cs b/src/Umbraco.Web.Website/Routing/EagerMatcherPolicy.cs index b6cd4a361522..1f6ff7e2f00f 100644 --- a/src/Umbraco.Web.Website/Routing/EagerMatcherPolicy.cs +++ b/src/Umbraco.Web.Website/Routing/EagerMatcherPolicy.cs @@ -1,4 +1,4 @@ -using System.Reflection; +using System.Reflection; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Routing; @@ -126,8 +126,20 @@ public async Task ApplyAsync(HttpContext httpContext, CandidateSet candidates) return; } + // If it's an attribute routed IVirtualPageController with a Host attribute we should ignore if the host doesn't match the current request. + // Maybe we would expect that it wouldn't be in the provided CandidateSet, but it will be included just based on the Route. + // See: https://github.com/umbraco/Umbraco-CMS/issues/16816 + if (controllerTypeInfo is not null && controllerTypeInfo.IsType<IVirtualPageController>()) + { + HostAttribute? hostAttribute = controllerTypeInfo.GetCustomAttribute<HostAttribute>(); + if (hostAttribute is not null && hostAttribute.Hosts.InvariantContains(httpContext.Request.Host.Value) is false) + { + continue; + } + } + // If it's an UmbracoPageController we need to do some domain routing. - // We need to do this in oder to handle cultures for our Dictionary. + // We need to do this in order to handle cultures for our Dictionary. // This is because UmbracoPublishedContentCultureProvider is ued to set the Thread.CurrentThread.CurrentUICulture // The CultureProvider is run before the actual routing, this means that our UmbracoVirtualPageFilterAttribute is hit AFTER the culture is set. // Meaning we have to route the domain part already now, this is not pretty, but it beats having to look for content we know doesn't exist. From a3db45609aaa910dabc38fddeec36a32a3cd664d Mon Sep 17 00:00:00 2001 From: Andy Butland <abutland73@gmail.com> Date: Thu, 3 Apr 2025 21:58:56 +0200 Subject: [PATCH 7/8] Move database cache rebuild to a background task with polling (13) (#18922) * Converts rebuild database cache operation to submit and poll. * Update src/Umbraco.Web.UI.Client/src/views/dashboard/settings/publishedsnapshotcache.controller.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Handle HTTP error in status retrieval. * Fixed test build. --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../IPublishedSnapshotService.cs | 54 +++++++++++++++ .../PublishedSnapshotService.cs | 68 ++++++++++++++++++- .../PublishedSnapshotStatus.cs | 5 ++ .../PublishedSnapshotCacheStatusController.cs | 23 ++++++- .../publishedsnapshotcache.controller.js | 25 +++++-- .../PublishedSnapshotServiceTestBase.cs | 6 +- 6 files changed, 169 insertions(+), 12 deletions(-) diff --git a/src/Umbraco.Core/PublishedCache/IPublishedSnapshotService.cs b/src/Umbraco.Core/PublishedCache/IPublishedSnapshotService.cs index 8e661aa75805..02a419f8d585 100644 --- a/src/Umbraco.Core/PublishedCache/IPublishedSnapshotService.cs +++ b/src/Umbraco.Core/PublishedCache/IPublishedSnapshotService.cs @@ -31,6 +31,12 @@ public interface IPublishedSnapshotService : IDisposable /// </remarks> IPublishedSnapshot CreatePublishedSnapshot(string? previewToken); + /// <summary> + /// Indicates if the database cache is in the process of being rebuilt. + /// </summary> + /// <returns></returns> + bool IsRebuilding() => false; + /// <summary> /// Rebuilds internal database caches (but does not reload). /// </summary> @@ -61,6 +67,38 @@ void Rebuild( IReadOnlyCollection<int>? mediaTypeIds = null, IReadOnlyCollection<int>? memberTypeIds = null); + /// <summary> + /// Rebuilds internal database caches (but does not reload). + /// </summary> + /// <param name="contentTypeIds"> + /// If not null will process content for the matching content types, if empty will process all + /// content + /// </param> + /// <param name="mediaTypeIds"> + /// If not null will process content for the matching media types, if empty will process all + /// media + /// </param> + /// <param name="memberTypeIds"> + /// If not null will process content for the matching members types, if empty will process all + /// members + /// </param> + /// <param name="useBackgroundThread">Flag indicating whether to use a background thread for the operation and immediately return to the caller.</param> + /// <remarks> + /// <para> + /// Forces the snapshot service to rebuild its internal database caches. For instance, some caches + /// may rely on a database table to store pre-serialized version of documents. + /// </para> + /// <para> + /// This does *not* reload the caches. Caches need to be reloaded, for instance via + /// <see cref="DistributedCache" /> RefreshAllPublishedSnapshot method. + /// </para> + /// </remarks> + void Rebuild( + bool useBackgroundThread, + IReadOnlyCollection<int>? contentTypeIds = null, + IReadOnlyCollection<int>? mediaTypeIds = null, + IReadOnlyCollection<int>? memberTypeIds = null) => Rebuild(contentTypeIds, mediaTypeIds, memberTypeIds); + /// <summary> /// Rebuilds all internal database caches (but does not reload). @@ -77,6 +115,22 @@ void Rebuild( /// </remarks> void RebuildAll() => Rebuild(Array.Empty<int>(), Array.Empty<int>(), Array.Empty<int>()); + /// <summary> + /// Rebuilds all internal database caches (but does not reload). + /// </summary> + /// <param name="useBackgroundThread">Flag indicating whether to use a background thread for the operation and immediately return to the caller.</param> + /// <remarks> + /// <para> + /// Forces the snapshot service to rebuild its internal database caches. For instance, some caches + /// may rely on a database table to store pre-serialized version of documents. + /// </para> + /// <para> + /// This does *not* reload the caches. Caches need to be reloaded, for instance via + /// <see cref="DistributedCache" /> RefreshAllPublishedSnapshot method. + /// </para> + /// </remarks> + void RebuildAll(bool useBackgroundThread) => Rebuild(useBackgroundThread, Array.Empty<int>(), Array.Empty<int>(), Array.Empty<int>()); + /* An IPublishedCachesService implementation can rely on transaction-level events to update * its internal, database-level data, as these events are purely internal. However, it cannot * rely on cache refreshers CacheUpdated events to update itself, as these events are external diff --git a/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotService.cs b/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotService.cs index 8aa012d11f4a..af6b8ef4cbe2 100644 --- a/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotService.cs @@ -15,6 +15,7 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.Changes; using Umbraco.Cms.Core.Sync; +using Umbraco.Cms.Infrastructure.HostedServices; using Umbraco.Cms.Infrastructure.PublishedCache.DataSource; using Umbraco.Cms.Infrastructure.PublishedCache.Persistence; using Umbraco.Extensions; @@ -31,6 +32,9 @@ internal class PublishedSnapshotService : IPublishedSnapshotService // means faster execution, but uses memory - not sure if we want it // so making it configurable. public static readonly bool FullCacheWhenPreviewing = true; + + private const string IsRebuildingDatabaseCacheRuntimeCacheKey = "temp_database_cache_rebuild_op"; + private readonly NuCacheSettings _config; private readonly ContentDataSerializer _contentDataSerializer; private readonly IDefaultCultureAccessor _defaultCultureAccessor; @@ -51,6 +55,8 @@ internal class PublishedSnapshotService : IPublishedSnapshotService private readonly object _storesLock = new(); private readonly ISyncBootStateAccessor _syncBootStateAccessor; private readonly IVariationContextAccessor _variationContextAccessor; + private readonly IBackgroundTaskQueue _backgroundTaskQueue; + private readonly IAppPolicyCache _runtimeCache; private long _contentGen; @@ -91,7 +97,9 @@ public PublishedSnapshotService( IPublishedModelFactory publishedModelFactory, IHostingEnvironment hostingEnvironment, IOptions<NuCacheSettings> config, - ContentDataSerializer contentDataSerializer) + ContentDataSerializer contentDataSerializer, + IBackgroundTaskQueue backgroundTaskQueue, + AppCaches appCaches) { _options = options; _syncBootStateAccessor = syncBootStateAccessor; @@ -111,6 +119,8 @@ public PublishedSnapshotService( _contentDataSerializer = contentDataSerializer; _config = config.Value; _publishedModelFactory = publishedModelFactory; + _backgroundTaskQueue = backgroundTaskQueue; + _runtimeCache = appCaches.RuntimeCache; } protected PublishedSnapshot? CurrentPublishedSnapshot @@ -349,12 +359,66 @@ public IPublishedSnapshot CreatePublishedSnapshot(string? previewToken) return new PublishedSnapshot(this, preview); } + /// <inheritdoc /> + public bool IsRebuilding() => _runtimeCache.Get(IsRebuildingDatabaseCacheRuntimeCacheKey) is not null; + /// <inheritdoc /> public void Rebuild( IReadOnlyCollection<int>? contentTypeIds = null, IReadOnlyCollection<int>? mediaTypeIds = null, IReadOnlyCollection<int>? memberTypeIds = null) - => _publishedContentService.Rebuild(contentTypeIds, mediaTypeIds, memberTypeIds); + => Rebuild(false, contentTypeIds, mediaTypeIds, memberTypeIds); + + /// <inheritdoc /> + public void Rebuild( + bool useBackgroundThread, + IReadOnlyCollection<int>? contentTypeIds = null, + IReadOnlyCollection<int>? mediaTypeIds = null, + IReadOnlyCollection<int>? memberTypeIds = null) + { + if (useBackgroundThread) + { + _logger.LogInformation("Starting async background thread for rebuilding database cache."); + + _backgroundTaskQueue.QueueBackgroundWorkItem( + cancellationToken => + { + // Do not flow AsyncLocal to the child thread + using (ExecutionContext.SuppressFlow()) + { + Task.Run(() => PerformRebuild(contentTypeIds, mediaTypeIds, memberTypeIds)); + + // immediately return so the request isn't waiting. + return Task.CompletedTask; + } + }); + } + else + { + PerformRebuild(contentTypeIds, mediaTypeIds, memberTypeIds); + } + } + + private void PerformRebuild( + IReadOnlyCollection<int>? contentTypeIds = null, + IReadOnlyCollection<int>? mediaTypeIds = null, + IReadOnlyCollection<int>? memberTypeIds = null) + { + try + { + SetIsRebuilding(); + + _publishedContentService.Rebuild(contentTypeIds, mediaTypeIds, memberTypeIds); + } + finally + { + ClearIsRebuilding(); + } + } + + private void SetIsRebuilding() => _runtimeCache.Insert(IsRebuildingDatabaseCacheRuntimeCacheKey, () => "tempValue", TimeSpan.FromMinutes(10)); + + private void ClearIsRebuilding() => _runtimeCache.Clear(IsRebuildingDatabaseCacheRuntimeCacheKey); public async Task CollectAsync() { diff --git a/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotStatus.cs b/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotStatus.cs index c706e35ca6f4..61213bf2e524 100644 --- a/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotStatus.cs +++ b/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotStatus.cs @@ -29,6 +29,11 @@ public string GetStatus() $"The current {typeof(IPublishedSnapshotService)} is not the default type. A status cannot be determined."; } + if (_service.IsRebuilding()) + { + return "Rebuild in progress. Please wait."; + } + // TODO: This should be private _service.EnsureCaches(); diff --git a/src/Umbraco.Web.BackOffice/Controllers/PublishedSnapshotCacheStatusController.cs b/src/Umbraco.Web.BackOffice/Controllers/PublishedSnapshotCacheStatusController.cs index 91cd16a0f6ba..33750d434bfe 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/PublishedSnapshotCacheStatusController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/PublishedSnapshotCacheStatusController.cs @@ -37,13 +37,32 @@ public PublishedSnapshotCacheStatusController( [HttpPost] public string RebuildDbCache() { - //Rebuild All + if (_publishedSnapshotService.IsRebuilding()) + { + return "Rebuild already in progress."; + } + _publishedSnapshotService.RebuildAll(); return _publishedSnapshotStatus.GetStatus(); } /// <summary> - /// Gets a status report + /// Rebuilds the Database cache on a background thread. + /// </summary> + [HttpPost] + public IActionResult RebuildDbCacheInBackground() + { + if (_publishedSnapshotService.IsRebuilding()) + { + return BadRequest("Rebuild already in progress."); + } + + _publishedSnapshotService.RebuildAll(true); + return Ok(); + } + + /// <summary> + /// Gets a status report. /// </summary> [HttpGet] public string GetStatus() => _publishedSnapshotStatus.GetStatus(); diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/publishedsnapshotcache.controller.js b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/publishedsnapshotcache.controller.js index 5270892fa59a..264e24d5641d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/publishedsnapshotcache.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/publishedsnapshotcache.controller.js @@ -1,4 +1,4 @@ -function publishedSnapshotCacheController($scope, $http, umbRequestHelper, localizationService, overlayService) { +function publishedSnapshotCacheController($scope, $http, umbRequestHelper, localizationService, overlayService) { var vm = this; @@ -94,12 +94,23 @@ vm.working = true; umbRequestHelper.resourcePromise( - $http.post(umbRequestHelper.getApiUrl("publishedSnapshotCacheStatusBaseUrl", "RebuildDbCache")), - 'Failed to rebuild the cache.') - .then(function (result) { - vm.working = false; - vm.status = result; - }); + $http.post(umbRequestHelper.getApiUrl("publishedSnapshotCacheStatusBaseUrl", "RebuildDbCacheInBackground")), "Failed to queue the rebuild task.") + .then(function () { + const interval = setInterval(function () { + $http.get(umbRequestHelper.getApiUrl("publishedSnapshotCacheStatusBaseUrl", "GetStatus")) + .then(function (result) { + if (!result.data.toString().startsWith("Rebuild in progress")) { + vm.working = false; + vm.status = result.data; + clearInterval(interval); + } + }, function () { + vm.working = false; + vm.status = "Could not retrieve rebuild cache status"; + }); + + }, 2000); + }); } function init() { diff --git a/tests/Umbraco.Tests.UnitTests/TestHelpers/PublishedSnapshotServiceTestBase.cs b/tests/Umbraco.Tests.UnitTests/TestHelpers/PublishedSnapshotServiceTestBase.cs index 46f950ee603f..dded4a1bf54f 100644 --- a/tests/Umbraco.Tests.UnitTests/TestHelpers/PublishedSnapshotServiceTestBase.cs +++ b/tests/Umbraco.Tests.UnitTests/TestHelpers/PublishedSnapshotServiceTestBase.cs @@ -7,6 +7,7 @@ using Moq; using NUnit.Framework; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Hosting; @@ -22,6 +23,7 @@ using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Core.Sync; using Umbraco.Cms.Core.Web; +using Umbraco.Cms.Infrastructure.HostedServices; using Umbraco.Cms.Infrastructure.PublishedCache; using Umbraco.Cms.Infrastructure.PublishedCache.DataSource; using Umbraco.Cms.Infrastructure.Serialization; @@ -280,7 +282,9 @@ protected void InitializedCache( PublishedModelFactory, TestHelper.GetHostingEnvironment(), Options.Create(nuCacheSettings), - new ContentDataSerializer(new DictionaryOfPropertyDataSerializer())); + new ContentDataSerializer(new DictionaryOfPropertyDataSerializer()), + Mock.Of<IBackgroundTaskQueue>(), + AppCaches.NoCache); // invariant is the current default VariationContextAccessor.VariationContext = new VariationContext(); From 0f025841434c83427f64910a46356491ae2806d1 Mon Sep 17 00:00:00 2001 From: Navya Sinha <143813469+Navya-Sinhaa@users.noreply.github.com> Date: Fri, 4 Apr 2025 02:52:48 +0100 Subject: [PATCH 8/8] attempted fix for Datepicker v13 issue #16008 (#18903) Co-authored-by: Navya Sinha <navya.sinha@method4.co.uk> --- .../views/propertyeditors/datepicker/datepicker.controller.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.controller.js index e210e94aa1c9..5d1375a27f47 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.controller.js @@ -111,7 +111,7 @@ function dateTimePickerController($scope, angularHelper, dateHelper, validationM // $scope.hasDatetimePickerValue indicates that we had a value before the input was changed, // but now the input is empty. $scope.clearDate(); - } else if ($scope.model.datetimePickerValue) { + } else if ($scope.model.datetimePickerInputValue) { var momentDate = moment($scope.model.datetimePickerInputValue, $scope.model.config.format, true); if (!momentDate || !momentDate.isValid()) { momentDate = moment(new Date($scope.model.datetimePickerInputValue)); @@ -120,7 +120,7 @@ function dateTimePickerController($scope, angularHelper, dateHelper, validationM setDate(momentDate); } setDatePickerVal(); - flatPickr.setDate($scope.model.datetimePickerValue, false); + flatPickr.setDate($scope.model.datetimePickerInputValue, false); } }