From 15ebae5025be0c00ec78ac99b010a11b8b229973 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Fri, 28 Feb 2025 06:57:00 +0100 Subject: [PATCH 01/10] Bumped version to 13.7.1. --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index c976fce100..a6ac6f44b4 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.7.0", + "version": "13.7.1", "assemblyVersion": { "precision": "build" }, From 451b5f96e7757694d6efb597d70d1a6eccccc24b Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Fri, 28 Feb 2025 06:57:35 +0100 Subject: [PATCH 02/10] Use windows for build agent to avoid hanging issues with Linux. --- build/azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index 8d3f9edc0f..aa0ddf1db6 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -71,7 +71,7 @@ stages: - job: A displayName: Build Umbraco CMS pool: - vmImage: 'ubuntu-latest' + vmImage: 'windows-latest' steps: - checkout: self submodules: false From 2ec6ff4ebc6a1775d8cc6268a8b68f0d75c70ecd Mon Sep 17 00:00:00 2001 From: twoday-rolandkock Date: Sun, 9 Mar 2025 08:41:06 +0100 Subject: [PATCH 03/10] bumped imagesharp to prevent CVE-2025-27598 (#18602) --- Directory.Packages.props | 2 +- .../Umbraco.Cms.Imaging.ImageSharp2.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index eb15199e0a..3428b9a284 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -73,7 +73,7 @@ - + diff --git a/src/Umbraco.Cms.Imaging.ImageSharp2/Umbraco.Cms.Imaging.ImageSharp2.csproj b/src/Umbraco.Cms.Imaging.ImageSharp2/Umbraco.Cms.Imaging.ImageSharp2.csproj index c513082675..5551cf5dc0 100644 --- a/src/Umbraco.Cms.Imaging.ImageSharp2/Umbraco.Cms.Imaging.ImageSharp2.csproj +++ b/src/Umbraco.Cms.Imaging.ImageSharp2/Umbraco.Cms.Imaging.ImageSharp2.csproj @@ -5,7 +5,7 @@ - + From d3b60277d7b61313e4ad9b511a97bb2fd4c75b61 Mon Sep 17 00:00:00 2001 From: Anders Bjerner Date: Mon, 10 Mar 2025 15:12:49 +0100 Subject: [PATCH 04/10] Fixed typo in TinyMCE's da.js --- src/Umbraco.Web.UI.Client/lib/tinymce/langs/da.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/langs/da.js b/src/Umbraco.Web.UI.Client/lib/tinymce/langs/da.js index a50f2f10ec..b23756f954 100644 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/langs/da.js +++ b/src/Umbraco.Web.UI.Client/lib/tinymce/langs/da.js @@ -15,7 +15,7 @@ tinymce.addI18n('da',{ "Strikethrough": "Gennemstreg", "Superscript": "H\u00e6vet", "Subscript": "S\u00e6nket", -"Clear formatting": "Nulstil formattering", +"Clear formatting": "Nulstil formatering", "Align left": "Venstrejusteret", "Align center": "Centreret", "Align right": "H\u00f8jrejusteret", From 5b54bed406682ceff57903bf7d3c57814eef31a7 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Tue, 11 Mar 2025 05:11:08 +0100 Subject: [PATCH 05/10] Merge commit from fork * Fixed parsing of node if in content and media permission querystring handlers to retrieve expected value when multiple are provided in the querystring. * Add HttpPost attributes to backoffice endpoints that should only accept post requests. * Bumped version to 13.6.1. * Narrow PermissionQueryString parsing to the releveant UmbracoObjectType * Add missed update from v10 --------- Co-authored-by: Sven Geusens --- .../ContentPermissionsQueryStringHandler.cs | 8 +++- .../MediaPermissionsQueryStringHandler.cs | 8 +++- .../PermissionsQueryStringHandler.cs | 10 +++- .../Controllers/ContentController.cs | 10 ++++ .../Controllers/MediaController.cs | 5 ++ ...ntentPermissionsQueryStringHandlerTests.cs | 46 +++++++++++++------ ...MediaPermissionsQueryStringHandlerTests.cs | 45 ++++++++++++------ .../MediaPermissionsResourceHandlerTests.cs | 2 +- 8 files changed, 102 insertions(+), 32 deletions(-) diff --git a/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsQueryStringHandler.cs b/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsQueryStringHandler.cs index 4e2faeb1c7..fc226c5589 100644 --- a/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsQueryStringHandler.cs +++ b/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsQueryStringHandler.cs @@ -20,6 +20,8 @@ public class { private readonly ContentPermissions _contentPermissions; + protected override UmbracoObjectTypes KeyParsingFilterType => UmbracoObjectTypes.Document; + /// /// Initializes a new instance of the class. /// @@ -48,7 +50,11 @@ public class return Task.FromResult(true); } - var argument = routeVal.ToString(); + // Handle case where the incoming querystring could contain more than one value (e.g. ?id=1000&id=1001). + // It's the first one that'll be processed by the protected method so we should verify that. + var argument = routeVal.Count == 1 + ? routeVal.ToString() + : routeVal.FirstOrDefault()?.ToString() ?? string.Empty; if (!TryParseNodeId(argument, out nodeId)) { diff --git a/src/Umbraco.Web.BackOffice/Authorization/MediaPermissionsQueryStringHandler.cs b/src/Umbraco.Web.BackOffice/Authorization/MediaPermissionsQueryStringHandler.cs index 7b662e5fc0..e8a8f36aca 100644 --- a/src/Umbraco.Web.BackOffice/Authorization/MediaPermissionsQueryStringHandler.cs +++ b/src/Umbraco.Web.BackOffice/Authorization/MediaPermissionsQueryStringHandler.cs @@ -18,6 +18,8 @@ public class MediaPermissionsQueryStringHandler : PermissionsQueryStringHandler< { private readonly MediaPermissions _mediaPermissions; + protected override UmbracoObjectTypes KeyParsingFilterType => UmbracoObjectTypes.Media; + /// /// Initializes a new instance of the class. /// @@ -44,7 +46,11 @@ public class MediaPermissionsQueryStringHandler : PermissionsQueryStringHandler< return Task.FromResult(true); } - var argument = routeVal.ToString(); + // Handle case where the incoming querystring could contain more than one value (e.g. ?id=1000&id=1001). + // It's the first one that'll be processed by the protected method so we should verify that. + var argument = routeVal.Count == 1 + ? routeVal.ToString() + : routeVal.FirstOrDefault()?.ToString() ?? string.Empty; if (!TryParseNodeId(argument, out var nodeId)) { diff --git a/src/Umbraco.Web.BackOffice/Authorization/PermissionsQueryStringHandler.cs b/src/Umbraco.Web.BackOffice/Authorization/PermissionsQueryStringHandler.cs index 5367208c79..44b7c5ba8d 100644 --- a/src/Umbraco.Web.BackOffice/Authorization/PermissionsQueryStringHandler.cs +++ b/src/Umbraco.Web.BackOffice/Authorization/PermissionsQueryStringHandler.cs @@ -49,12 +49,18 @@ public abstract class PermissionsQueryStringHandler : MustSatisfyRequirementA /// protected IEntityService EntityService { get; set; } + /// + /// Defaults to Unknown so all types are allowed, since Keys are unique across all node types this works, + /// but it if you are certain you are looking for a specific type this should be overwritten for DB query performance. + /// + protected virtual UmbracoObjectTypes KeyParsingFilterType => UmbracoObjectTypes.Unknown; + /// /// Attempts to parse a node ID from a string representation found in a querystring value. /// /// Querystring value. /// Output parsed Id. - /// True of node ID could be parased, false it not. + /// True of node ID could be parsed, false it not. protected bool TryParseNodeId(string argument, out int nodeId) { // If the argument is an int, it will parse and can be assigned to nodeId. @@ -75,7 +81,7 @@ public abstract class PermissionsQueryStringHandler : MustSatisfyRequirementA if (Guid.TryParse(argument, out Guid key)) { - nodeId = EntityService.GetId(key, UmbracoObjectTypes.Document).Result; + nodeId = EntityService.GetId(key, KeyParsingFilterType).Result; return true; } diff --git a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs index c676cee2ca..d8de90780c 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs @@ -256,6 +256,7 @@ public class ContentController : ContentControllerBase /// Permission check is done for letter 'R' which is for which the user must have access to /// update /// + [HttpPost] public async Task?>> PostSaveUserGroupPermissions( UserGroupPermissionsSave saveModel) { @@ -902,6 +903,7 @@ public class ContentController : ContentControllerBase [Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)] [FileUploadCleanupFilter] [ContentSaveValidation(skipUserAccessValidation:true)] // skip user access validation because we "only" require Settings access to create new blueprints from scratch + [HttpPost] public async Task?>?> PostSaveBlueprint( [ModelBinder(typeof(BlueprintItemBinder))] ContentItemSave contentItem) { @@ -939,6 +941,7 @@ public class ContentController : ContentControllerBase [FileUploadCleanupFilter] [ContentSaveValidation] [OutgoingEditorModelEvent] + [HttpPost] public async Task?>> PostSave( [ModelBinder(typeof(ContentItemBinder))] ContentItemSave contentItem) { @@ -2089,6 +2092,7 @@ public class ContentController : ContentControllerBase /// does not have Publish access to this node. /// [Authorize(Policy = AuthorizationPolicies.ContentPermissionPublishById)] + [HttpPost] public IActionResult PostPublishById(int id) { IContent? foundContent = GetObjectFromRequest(() => _contentService.GetById(id)); @@ -2120,6 +2124,7 @@ public class ContentController : ContentControllerBase /// does not have Publish access to this node. /// [Authorize(Policy = AuthorizationPolicies.ContentPermissionPublishById)] + [HttpPost] public IActionResult PostPublishByIdAndCulture(PublishContent model) { var languageCount = _allLangs.Value.Count(); @@ -2243,6 +2248,7 @@ public class ContentController : ContentControllerBase /// /// /// + [HttpPost] public async Task PostSort(ContentSortOrder sorted) { if (sorted == null) @@ -2294,6 +2300,7 @@ public class ContentController : ContentControllerBase /// /// /// + [HttpPost] public async Task PostMove(MoveOrCopy move) { // Authorize... @@ -2333,6 +2340,7 @@ public class ContentController : ContentControllerBase /// /// /// + [HttpPost] public async Task?> PostCopy(MoveOrCopy copy) { // Authorize... @@ -2372,6 +2380,7 @@ public class ContentController : ContentControllerBase /// The content and variants to unpublish /// [OutgoingEditorModelEvent] + [HttpPost] public async Task> PostUnpublish(UnpublishContent model) { IContent? foundContent = _contentService.GetById(model.Id); @@ -3096,6 +3105,7 @@ public class ContentController : ContentControllerBase return notifications; } + [HttpPost] public IActionResult PostNotificationOptions( int contentId, [FromQuery(Name = "notifyOptions[]")] string[] notifyOptions) diff --git a/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs b/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs index e6a131950e..404713a2b9 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs @@ -386,6 +386,7 @@ public class MediaController : ContentControllerBase /// /// /// + [HttpPost] public async Task PostMove(MoveOrCopy move) { // Authorize... @@ -436,6 +437,7 @@ public class MediaController : ContentControllerBase [FileUploadCleanupFilter] [MediaItemSaveValidation] [OutgoingEditorModelEvent] + [HttpPost] public ActionResult? PostSave( [ModelBinder(typeof(MediaItemBinder))] MediaItemSave contentItem) { @@ -551,6 +553,7 @@ public class MediaController : ContentControllerBase /// /// /// + [HttpPost] public async Task PostSort(ContentSortOrder sorted) { if (sorted == null) @@ -595,6 +598,7 @@ public class MediaController : ContentControllerBase } } + [HttpPost] public async Task> PostAddFolder(PostedFolder folder) { ActionResult? parentIdResult = await GetParentIdAsIntAsync(folder.ParentId, true); @@ -628,6 +632,7 @@ public class MediaController : ContentControllerBase /// /// We cannot validate this request with attributes (nicely) due to the nature of the multi-part for data. /// + [HttpPost] public async Task PostAddFile([FromForm] string path, [FromForm] string currentFolder, [FromForm] string contentTypeAlias, List file) { diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Authorization/ContentPermissionsQueryStringHandlerTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Authorization/ContentPermissionsQueryStringHandlerTests.cs index c562eb67ea..f7782e8d99 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Authorization/ContentPermissionsQueryStringHandlerTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Authorization/ContentPermissionsQueryStringHandlerTests.cs @@ -1,9 +1,7 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using System.Security.Claims; -using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; @@ -34,7 +32,7 @@ public class ContentPermissionsQueryStringHandlerTests public async Task Node_Id_From_Requirement_With_Permission_Is_Authorized() { var authHandlerContext = CreateAuthorizationHandlerContext(NodeId); - var mockHttpContextAccessor = CreateMockHttpContextAccessor(); + var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(); var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, new[] { "A" }); await sut.HandleAsync(authHandlerContext); @@ -46,7 +44,7 @@ public class ContentPermissionsQueryStringHandlerTests public async Task Node_Id_From_Requirement_Without_Permission_Is_Not_Authorized() { var authHandlerContext = CreateAuthorizationHandlerContext(NodeId); - var mockHttpContextAccessor = CreateMockHttpContextAccessor(); + var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(); var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, new[] { "B" }); await sut.HandleAsync(authHandlerContext); @@ -59,7 +57,7 @@ public class ContentPermissionsQueryStringHandlerTests public async Task Node_Id_Missing_From_Requirement_And_QueryString_Is_Authorized() { var authHandlerContext = CreateAuthorizationHandlerContext(); - var mockHttpContextAccessor = CreateMockHttpContextAccessor("xxx"); + var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue("xxx"); var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, new[] { "A" }); await sut.HandleAsync(authHandlerContext); @@ -71,7 +69,7 @@ public class ContentPermissionsQueryStringHandlerTests public async Task Node_Integer_Id_From_QueryString_With_Permission_Is_Authorized() { var authHandlerContext = CreateAuthorizationHandlerContext(); - var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: NodeId.ToString()); + var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: NodeId.ToString()); var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, new[] { "A" }); await sut.HandleAsync(authHandlerContext); @@ -84,7 +82,21 @@ public class ContentPermissionsQueryStringHandlerTests public async Task Node_Integer_Id_From_QueryString_Without_Permission_Is_Not_Authorized() { var authHandlerContext = CreateAuthorizationHandlerContext(); - var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: NodeId.ToString()); + var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: NodeId.ToString()); + var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, new[] { "B" }); + + await sut.HandleAsync(authHandlerContext); + + Assert.IsFalse(authHandlerContext.HasSucceeded); + AssertContentCached(mockHttpContextAccessor); + } + + [Test] + public async Task Node_Integer_Id_From_QueryString_Without_Permission_Is_Not_Authorized_Even_When_Additional_Parameter_For_Id_With_Permission_Is_Provided() + { + // Provides initially failing test and verifies fix for advisory https://github.com/umbraco/Umbraco-CMS/security/advisories/GHSA-wx5h-wqfq-v698 + var authHandlerContext = CreateAuthorizationHandlerContext(); + var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValues(queryStringValues: [NodeId.ToString(), 1001.ToString()]); var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, new[] { "B" }); await sut.HandleAsync(authHandlerContext); @@ -97,7 +109,7 @@ public class ContentPermissionsQueryStringHandlerTests public async Task Node_Udi_Id_From_QueryString_With_Permission_Is_Authorized() { var authHandlerContext = CreateAuthorizationHandlerContext(); - var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: s_nodeUdi.ToString()); + var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: s_nodeUdi.ToString()); var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, new[] { "A" }); await sut.HandleAsync(authHandlerContext); @@ -110,7 +122,7 @@ public class ContentPermissionsQueryStringHandlerTests public async Task Node_Udi_Id_From_QueryString_Without_Permission_Is_Not_Authorized() { var authHandlerContext = CreateAuthorizationHandlerContext(); - var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: s_nodeUdi.ToString()); + var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: s_nodeUdi.ToString()); var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, new[] { "B" }); await sut.HandleAsync(authHandlerContext); @@ -123,7 +135,7 @@ public class ContentPermissionsQueryStringHandlerTests public async Task Node_Guid_Id_From_QueryString_With_Permission_Is_Authorized() { var authHandlerContext = CreateAuthorizationHandlerContext(); - var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: s_nodeGuid.ToString()); + var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: s_nodeGuid.ToString()); var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, new[] { "A" }); await sut.HandleAsync(authHandlerContext); @@ -136,7 +148,7 @@ public class ContentPermissionsQueryStringHandlerTests public async Task Node_Guid_Id_From_QueryString_Without_Permission_Is_Not_Authorized() { var authHandlerContext = CreateAuthorizationHandlerContext(); - var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: s_nodeGuid.ToString()); + var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: s_nodeGuid.ToString()); var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, new[] { "B" }); await sut.HandleAsync(authHandlerContext); @@ -149,7 +161,7 @@ public class ContentPermissionsQueryStringHandlerTests public async Task Node_Invalid_Id_From_QueryString_Is_Authorized() { var authHandlerContext = CreateAuthorizationHandlerContext(); - var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: "invalid"); + var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: "invalid"); var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, new[] { "A" }); await sut.HandleAsync(authHandlerContext); @@ -168,14 +180,20 @@ public class ContentPermissionsQueryStringHandlerTests return new AuthorizationHandlerContext(new List { requirement }, user, resource); } - private static Mock CreateMockHttpContextAccessor( + private static Mock CreateMockHttpContextAccessorWithQueryStringValue( string queryStringName = QueryStringName, string queryStringValue = "") + => CreateMockHttpContextAccessorWithQueryStringValues(queryStringName, [queryStringValue]); + + private static Mock CreateMockHttpContextAccessorWithQueryStringValues( + string queryStringName = QueryStringName, + string[]? queryStringValues = null) { + queryStringValues ??= []; var mockHttpContextAccessor = new Mock(); var mockHttpContext = new Mock(); var mockHttpRequest = new Mock(); - var queryParams = new Dictionary { { queryStringName, queryStringValue } }; + var queryParams = new Dictionary { { queryStringName, new StringValues(queryStringValues) } }; mockHttpRequest.SetupGet(x => x.Query).Returns(new QueryCollection(queryParams)); mockHttpContext.SetupGet(x => x.Request).Returns(mockHttpRequest.Object); mockHttpContext.SetupGet(x => x.Items).Returns(new Dictionary()); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Authorization/MediaPermissionsQueryStringHandlerTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Authorization/MediaPermissionsQueryStringHandlerTests.cs index 08ae10b071..6bd5fe1d87 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Authorization/MediaPermissionsQueryStringHandlerTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Authorization/MediaPermissionsQueryStringHandlerTests.cs @@ -1,9 +1,7 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using System.Security.Claims; -using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; @@ -33,7 +31,7 @@ public class MediaPermissionsQueryStringHandlerTests public async Task Node_Id_Missing_From_QueryString_Is_Authorized() { var authHandlerContext = CreateAuthorizationHandlerContext(); - var mockHttpContextAccessor = CreateMockHttpContextAccessor("xxx"); + var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue("xxx"); var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId); await sut.HandleAsync(authHandlerContext); @@ -45,7 +43,7 @@ public class MediaPermissionsQueryStringHandlerTests public async Task Node_Integer_Id_From_QueryString_With_Permission_Is_Authorized() { var authHandlerContext = CreateAuthorizationHandlerContext(); - var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: NodeId.ToString()); + var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: NodeId.ToString()); var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId); await sut.HandleAsync(authHandlerContext); @@ -58,7 +56,21 @@ public class MediaPermissionsQueryStringHandlerTests public async Task Node_Integer_Id_From_QueryString_Without_Permission_Is_Not_Authorized() { var authHandlerContext = CreateAuthorizationHandlerContext(); - var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: NodeId.ToString()); + var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: NodeId.ToString()); + var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, 1001); + + await sut.HandleAsync(authHandlerContext); + + Assert.IsFalse(authHandlerContext.HasSucceeded); + AssertMediaCached(mockHttpContextAccessor); + } + + [Test] + public async Task Node_Integer_Id_From_QueryString_Without_Permission_Is_Not_Authorized_Even_When_Additional_Parameter_For_Id_With_Permission_Is_Provided() + { + // Provides initially failing test and verifies fix for advisory https://github.com/umbraco/Umbraco-CMS/security/advisories/GHSA-wx5h-wqfq-v698 + var authHandlerContext = CreateAuthorizationHandlerContext(); + var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValues(queryStringValues: [NodeId.ToString(), 1001.ToString()]); var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, 1001); await sut.HandleAsync(authHandlerContext); @@ -71,7 +83,7 @@ public class MediaPermissionsQueryStringHandlerTests public async Task Node_Udi_Id_From_QueryString_With_Permission_Is_Authorized() { var authHandlerContext = CreateAuthorizationHandlerContext(); - var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: s_nodeUdi.ToString()); + var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: s_nodeUdi.ToString()); var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId); await sut.HandleAsync(authHandlerContext); @@ -84,7 +96,7 @@ public class MediaPermissionsQueryStringHandlerTests public async Task Node_Udi_Id_From_QueryString_Without_Permission_Is_Not_Authorized() { var authHandlerContext = CreateAuthorizationHandlerContext(); - var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: s_nodeUdi.ToString()); + var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: s_nodeUdi.ToString()); var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, 1001); await sut.HandleAsync(authHandlerContext); @@ -97,7 +109,7 @@ public class MediaPermissionsQueryStringHandlerTests public async Task Node_Guid_Id_From_QueryString_With_Permission_Is_Authorized() { var authHandlerContext = CreateAuthorizationHandlerContext(); - var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: s_nodeGuid.ToString()); + var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: s_nodeGuid.ToString()); var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId); await sut.HandleAsync(authHandlerContext); @@ -110,7 +122,7 @@ public class MediaPermissionsQueryStringHandlerTests public async Task Node_Guid_Id_From_QueryString_Without_Permission_Is_Not_Authorized() { var authHandlerContext = CreateAuthorizationHandlerContext(); - var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: s_nodeGuid.ToString()); + var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: s_nodeGuid.ToString()); var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, 1001); await sut.HandleAsync(authHandlerContext); @@ -123,7 +135,7 @@ public class MediaPermissionsQueryStringHandlerTests public async Task Node_Invalid_Id_From_QueryString_Is_Authorized() { var authHandlerContext = CreateAuthorizationHandlerContext(); - var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: "invalid"); + var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: "invalid"); var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId); await sut.HandleAsync(authHandlerContext); @@ -139,14 +151,21 @@ public class MediaPermissionsQueryStringHandlerTests return new AuthorizationHandlerContext(new List { requirement }, user, resource); } - private static Mock CreateMockHttpContextAccessor( + private static Mock CreateMockHttpContextAccessorWithQueryStringValue( string queryStringName = QueryStringName, string queryStringValue = "") + => CreateMockHttpContextAccessorWithQueryStringValues(queryStringName, [queryStringValue]); + + private static Mock CreateMockHttpContextAccessorWithQueryStringValues( + string queryStringName = QueryStringName, + string[]? queryStringValues = null) { + queryStringValues ??= []; + var mockHttpContextAccessor = new Mock(); var mockHttpContext = new Mock(); var mockHttpRequest = new Mock(); - var queryParams = new Dictionary { { queryStringName, queryStringValue } }; + var queryParams = new Dictionary { { queryStringName, new StringValues(queryStringValues) } }; mockHttpRequest.SetupGet(x => x.Query).Returns(new QueryCollection(queryParams)); mockHttpContext.SetupGet(x => x.Request).Returns(mockHttpRequest.Object); mockHttpContext.SetupGet(x => x.Items).Returns(new Dictionary()); @@ -178,7 +197,7 @@ public class MediaPermissionsQueryStringHandlerTests mockEntityService .Setup(x => x.GetId( It.Is(y => y == s_nodeGuid), - It.Is(y => y == UmbracoObjectTypes.Document))) + It.Is(y => y == UmbracoObjectTypes.Media))) .Returns(Attempt.Succeed(NodeId)); return mockEntityService; } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Authorization/MediaPermissionsResourceHandlerTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Authorization/MediaPermissionsResourceHandlerTests.cs index fb31411660..dc534f8b41 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Authorization/MediaPermissionsResourceHandlerTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Authorization/MediaPermissionsResourceHandlerTests.cs @@ -44,7 +44,7 @@ public class MediaPermissionsResourceHandlerTests } [Test] - public async Task Resource_With_Node_Id_Withou_Permission_Is_Not_Authorized() + public async Task Resource_With_Node_Id_Without_Permission_Is_Not_Authorized() { var authHandlerContext = CreateAuthorizationHandlerContext(NodeId, true); var sut = CreateHandler(NodeId, 1001); From e3d9b042c24e44eeeec3f02d01467e1e2a81e934 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Tue, 4 Mar 2025 12:12:29 +0100 Subject: [PATCH 06/10] Avoids collection was modified issue when flowing identities to the authenticated user's principal. (#18527) --- .../Extensions/HttpContextExtensions.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.Common/Extensions/HttpContextExtensions.cs b/src/Umbraco.Web.Common/Extensions/HttpContextExtensions.cs index fd46ef6903..cf90229513 100644 --- a/src/Umbraco.Web.Common/Extensions/HttpContextExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/HttpContextExtensions.cs @@ -68,7 +68,13 @@ public static class HttpContextExtensions // Otherwise we can't log in as both a member and a backoffice user // For instance if you've enabled basic auth. ClaimsPrincipal? authenticatedPrincipal = result.Principal; - IEnumerable existingIdentities = httpContext.User.Identities.Where(x => x.IsAuthenticated && x.AuthenticationType != authenticatedPrincipal.Identity.AuthenticationType); + + // Make sure to copy into a list before attempting to update the authenticated principal, so we don't attempt to modify + // the collection while iterating it. + // See: https://github.com/umbraco/Umbraco-CMS/issues/18509 + var existingIdentities = httpContext.User.Identities + .Where(x => x.IsAuthenticated && x.AuthenticationType != authenticatedPrincipal.Identity.AuthenticationType) + .ToList(); authenticatedPrincipal.AddIdentities(existingIdentities); httpContext.User = authenticatedPrincipal; From fd5b8de53349b067cfddf700d006aefd1471c7b4 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Wed, 12 Mar 2025 07:25:35 +0100 Subject: [PATCH 07/10] Bumped version to 13.7.2 --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index a6ac6f44b4..043d77464c 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.7.1", + "version": "13.7.2", "assemblyVersion": { "precision": "build" }, From e270adc50f6f54005c8e6e7b9a04c2c355dabff5 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Wed, 12 Mar 2025 08:10:34 +0100 Subject: [PATCH 08/10] Reverts UI updates from publish with desdendants dialog. (#18647) --- .../EmbeddedResources/Lang/da.xml | 3 -- .../EmbeddedResources/Lang/en.xml | 3 -- .../EmbeddedResources/Lang/en_us.xml | 3 -- .../ContentEditing/ContentSaveAction.cs | 32 ------------------ .../Controllers/ContentController.cs | 33 ++----------------- .../Filters/ContentSaveValidationAttribute.cs | 10 ------ .../components/content/edit.controller.js | 2 +- .../src/common/resources/content.resource.js | 10 ++---- .../overlays/publishdescendants.controller.js | 12 ------- .../content/overlays/publishdescendants.html | 32 ------------------ 10 files changed, 6 insertions(+), 134 deletions(-) diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/da.xml b/src/Umbraco.Core/EmbeddedResources/Lang/da.xml index ceaf24aea8..73ef388cb1 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/da.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/da.xml @@ -298,9 +298,6 @@ Fjern denne tekstboks Indholdsrod Inkluder ikke-udgivet indhold. - Udgiv uændrede elementer. - ADVARSEL: Udgivelse af alle sider under denne i indholdstræet, uanset om de er ændret eller ej, kan være en ressourcekrævende og langvarig proces. - Dette bør ikke være nødvendigt under normale omstændigheder, så fortsæt kun med denne handling, hvis du er sikker på, at det er nødvendigt. Denne værdi er skjult. Hvis du har brug for adgang til at se denne værdi, bedes du kontakte din administrator. diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/en.xml b/src/Umbraco.Core/EmbeddedResources/Lang/en.xml index a0494a0204..5f48939766 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/en.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/en.xml @@ -309,9 +309,6 @@ Remove this text box Content root Include unpublished content items. - Publish unchanged items. - WARNING: Publishing all pages below this one in the content tree, whether or not they have changed, can be an expensive and long-running operation. - This should not be necessary in normal circumstances so please only proceed with this option selected if you are certain it is required. This value is hidden. If you need access to view this value please contact your website administrator. diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml index 74d87e8979..f86b082513 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml @@ -308,9 +308,6 @@ Remove this text box Content root Include unpublished content items. - Publish unchanged items. - WARNING: Publishing all pages below this one in the content tree, whether or not they have changed, can be an expensive and long-running operation. - This should not be necessary in normal circumstances so please only proceed with this option selected if you are certain it is required. This value is hidden. If you need access to view this value please contact your website administrator. diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentSaveAction.cs b/src/Umbraco.Core/Models/ContentEditing/ContentSaveAction.cs index 929ee7c097..87d5e7728a 100644 --- a/src/Umbraco.Core/Models/ContentEditing/ContentSaveAction.cs +++ b/src/Umbraco.Core/Models/ContentEditing/ContentSaveAction.cs @@ -59,43 +59,11 @@ public enum ContentSaveAction /// Saves and publishes the content item including all descendants regardless of whether they have a published version /// or not. /// - [Obsolete("This option is no longer used as the 'force' aspect has been extended into options for publishing unpublished and re-publishing changed content. Please use one of those options instead.")] PublishWithDescendantsForce = 10, /// /// Creates and publishes the new content item including all descendants regardless of whether they have a published /// version or not. /// - [Obsolete("This option is no longer used as the 'force' aspect has been extended into options for publishing unpublished and re-publishing changed content. Please use one of those options instead.")] PublishWithDescendantsForceNew = 11, - - /// - /// Saves and publishes the content item including all descendants including publishing previously unpublished content. - /// - PublishWithDescendantsIncludeUnpublished = 12, - - /// - /// Saves and publishes the new content item including all descendants including publishing previously unpublished content. - /// - PublishWithDescendantsIncludeUnpublishedNew = 13, - - /// - /// Saves and publishes the content item including all descendants irrespective of whether there are any pending changes. - /// - PublishWithDescendantsForceRepublish = 14, - - /// - /// Saves and publishes the new content item including all descendants including publishing previously unpublished content. - /// - PublishWithDescendantsForceRepublishNew = 15, - - /// - /// Saves and publishes the content item including all descendants including publishing previously unpublished content and irrespective of whether there are any pending changes. - /// - PublishWithDescendantsIncludeUnpublishedAndForceRepublish = 16, - - /// - /// Saves and publishes the new content item including all descendants including publishing previously unpublished content and irrespective of whether there are any pending changes. - /// - PublishWithDescendantsIncludeUnpublishedAndForceRepublishNew = 17, } diff --git a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs index 2f71f5ef3f..79a0ceeecd 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs @@ -1007,24 +1007,14 @@ public class ContentController : ContentControllerBase { case ContentSaveAction.Publish: case ContentSaveAction.PublishWithDescendants: -#pragma warning disable CS0618 // Type or member is obsolete case ContentSaveAction.PublishWithDescendantsForce: -#pragma warning restore CS0618 // Type or member is obsolete - case ContentSaveAction.PublishWithDescendantsIncludeUnpublished: - case ContentSaveAction.PublishWithDescendantsForceRepublish: - case ContentSaveAction.PublishWithDescendantsIncludeUnpublishedAndForceRepublish: case ContentSaveAction.SendPublish: case ContentSaveAction.Schedule: contentItem.Action = ContentSaveAction.Save; break; case ContentSaveAction.PublishNew: case ContentSaveAction.PublishWithDescendantsNew: -#pragma warning disable CS0618 // Type or member is obsolete case ContentSaveAction.PublishWithDescendantsForceNew: -#pragma warning restore CS0618 // Type or member is obsolete - case ContentSaveAction.PublishWithDescendantsIncludeUnpublishedNew: - case ContentSaveAction.PublishWithDescendantsForceRepublishNew: - case ContentSaveAction.PublishWithDescendantsIncludeUnpublishedAndForceRepublishNew: case ContentSaveAction.SendPublishNew: case ContentSaveAction.ScheduleNew: contentItem.Action = ContentSaveAction.SaveNew; @@ -1158,16 +1148,8 @@ public class ContentController : ContentControllerBase break; case ContentSaveAction.PublishWithDescendants: case ContentSaveAction.PublishWithDescendantsNew: -#pragma warning disable CS0618 // Type or member is obsolete case ContentSaveAction.PublishWithDescendantsForce: case ContentSaveAction.PublishWithDescendantsForceNew: -#pragma warning restore CS0618 // Type or member is obsolete - case ContentSaveAction.PublishWithDescendantsIncludeUnpublished: - case ContentSaveAction.PublishWithDescendantsIncludeUnpublishedNew: - case ContentSaveAction.PublishWithDescendantsForceRepublish: - case ContentSaveAction.PublishWithDescendantsForceRepublishNew: - case ContentSaveAction.PublishWithDescendantsIncludeUnpublishedAndForceRepublish: - case ContentSaveAction.PublishWithDescendantsIncludeUnpublishedAndForceRepublishNew: { if (!await ValidatePublishBranchPermissionsAsync(contentItem)) { @@ -1238,14 +1220,8 @@ public class ContentController : ContentControllerBase private static PublishBranchFilter BuildPublishBranchFilter(ContentSaveAction contentSaveAction) { - var includeUnpublished = contentSaveAction == ContentSaveAction.PublishWithDescendantsIncludeUnpublished - || contentSaveAction == ContentSaveAction.PublishWithDescendantsIncludeUnpublishedNew - || contentSaveAction == ContentSaveAction.PublishWithDescendantsIncludeUnpublishedAndForceRepublish - || contentSaveAction == ContentSaveAction.PublishWithDescendantsIncludeUnpublishedAndForceRepublishNew; - var forceRepublish = contentSaveAction == ContentSaveAction.PublishWithDescendantsForceRepublish - || contentSaveAction == ContentSaveAction.PublishWithDescendantsForceRepublishNew - || contentSaveAction == ContentSaveAction.PublishWithDescendantsIncludeUnpublishedAndForceRepublish - || contentSaveAction == ContentSaveAction.PublishWithDescendantsIncludeUnpublishedAndForceRepublishNew; + var includeUnpublished = contentSaveAction == ContentSaveAction.PublishWithDescendantsForce + || contentSaveAction == ContentSaveAction.PublishWithDescendantsForceNew; PublishBranchFilter publishBranchFilter = PublishBranchFilter.Default; if (includeUnpublished) @@ -1253,11 +1229,6 @@ public class ContentController : ContentControllerBase publishBranchFilter |= PublishBranchFilter.IncludeUnpublished; } - if (forceRepublish) - { - publishBranchFilter |= PublishBranchFilter.ForceRepublish; - } - return publishBranchFilter; } diff --git a/src/Umbraco.Web.BackOffice/Filters/ContentSaveValidationAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/ContentSaveValidationAttribute.cs index 45948b5960..c75bbd5a80 100644 --- a/src/Umbraco.Web.BackOffice/Filters/ContentSaveValidationAttribute.cs +++ b/src/Umbraco.Web.BackOffice/Filters/ContentSaveValidationAttribute.cs @@ -182,12 +182,7 @@ public sealed class ContentSaveValidationAttribute : TypeFilterAttribute break; case ContentSaveAction.Publish: case ContentSaveAction.PublishWithDescendants: -#pragma warning disable CS0618 // Type or member is obsolete case ContentSaveAction.PublishWithDescendantsForce: -#pragma warning restore CS0618 // Type or member is obsolete - case ContentSaveAction.PublishWithDescendantsIncludeUnpublished: - case ContentSaveAction.PublishWithDescendantsForceRepublish: - case ContentSaveAction.PublishWithDescendantsIncludeUnpublishedAndForceRepublish: permissionToCheck.Add(ActionPublish.ActionLetter); contentToCheck = contentItem.PersistedContent; contentIdToCheck = contentToCheck?.Id ?? default; @@ -237,12 +232,7 @@ public sealed class ContentSaveValidationAttribute : TypeFilterAttribute break; case ContentSaveAction.PublishNew: case ContentSaveAction.PublishWithDescendantsNew: -#pragma warning disable CS0618 // Type or member is obsolete case ContentSaveAction.PublishWithDescendantsForceNew: -#pragma warning restore CS0618 // Type or member is obsolete - case ContentSaveAction.PublishWithDescendantsIncludeUnpublishedNew: - case ContentSaveAction.PublishWithDescendantsForceRepublishNew: - case ContentSaveAction.PublishWithDescendantsIncludeUnpublishedAndForceRepublishNew: //Publish new requires both ActionNew AND ActionPublish // TODO: Shouldn't publish also require ActionUpdate since it will definitely perform an update to publish but maybe that's just implied diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js index 6d5503301b..72c5f3fec1 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js @@ -963,7 +963,7 @@ //we need to return this promise so that the dialog can handle the result and wire up the validation response return performSave({ saveMethod: function (content, create, files, showNotifications) { - return contentResource.publishWithDescendants(content, create, model.includeUnpublished, model.forceRepublish, files, showNotifications); + return contentResource.publishWithDescendants(content, create, model.includeUnpublished, files, showNotifications); }, action: "publishDescendants", showNotifications: false, diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js index a86675916f..dab8d8629b 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js @@ -1003,18 +1003,14 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { * @returns {Promise} resourcePromise object containing the saved content item. * */ - publishWithDescendants: function (content, isNew, includeUnpublished, forceRepublish, files, showNotifications) { + publishWithDescendants: function (content, isNew, includeUnpublished, files, showNotifications) { var endpoint = umbRequestHelper.getApiUrl( "contentApiBaseUrl", "PostSave"); var action = "publishWithDescendants"; - if (includeUnpublished === true && forceRepublish === true) { - action += "IncludeUnpublishedAndForceRepublish"; - } else if (includeUnpublished === true) { - action += "IncludeUnpublished"; - } else if (forceRepublish === true) { - action += "ForceRepublish"; + if (includeUnpublished === true) { + action += "Force"; } return saveContentItem(content, action + (isNew ? "New" : ""), files, endpoint, showNotifications); diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/publishdescendants.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/overlays/publishdescendants.controller.js index 40e7e2d4ba..ec0074aca0 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/publishdescendants.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/publishdescendants.controller.js @@ -5,12 +5,10 @@ var vm = this; vm.includeUnpublished = $scope.model.includeUnpublished || false; - vm.forceRepublish = $scope.model.forceRepublish || false; vm.publishAll = false; vm.changeSelection = changeSelection; vm.toggleIncludeUnpublished = toggleIncludeUnpublished; - vm.toggleForceRepublish = toggleForceRepublish; vm.changePublishAllSelection = changePublishAllSelection; function onInit() { @@ -30,11 +28,6 @@ vm.labels.includeUnpublished = value; }); } - if (!vm.labels.forceRepublish) { - localizationService.localize("content_forceRepublish").then(value => { - vm.labels.forceRepublish = value; - }); - } vm.variants.forEach(variant => { variant.isMandatory = isMandatoryFilter(variant); @@ -74,11 +67,6 @@ $scope.model.includeUnpublished = vm.includeUnpublished; } - function toggleForceRepublish() { - vm.forceRepublish = !vm.forceRepublish; - $scope.model.forceRepublish = vm.forceRepublish; - } - /** Returns true if publishing is possible based on if there are un-published mandatory languages */ function canPublish() { var selected = []; diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/publishdescendants.html b/src/Umbraco.Web.UI.Client/src/views/content/overlays/publishdescendants.html index 106f363c71..1e727e71ec 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/publishdescendants.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/publishdescendants.html @@ -16,22 +16,6 @@ show-labels="true"> - -
- - -
-
-

-

-
@@ -51,22 +35,6 @@
-
- - -
-
-

-

-
-
From f016dbd223f71c26dcf0246d5c649a7d67c36557 Mon Sep 17 00:00:00 2001 From: Lan Nguyen Thuy Date: Wed, 12 Mar 2025 16:46:07 +0700 Subject: [PATCH 09/10] Fix issue text overflow when user name is too long --- .../less/components/users/umb-user-cards.less | 24 +++++++++++++++---- .../src/views/users/views/users/users.html | 9 +++---- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-cards.less b/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-cards.less index 7c327bfb88..d985ce8063 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-cards.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-cards.less @@ -89,11 +89,16 @@ .umb-user-card__name { - font-size: 15px; - font-weight: bold; - text-align: center; - margin-bottom: 2px; - word-wrap: break-word; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; + font-size: 15px; + font-weight: bold; + text-align: center; + margin-bottom: 2px; + word-wrap: break-word; } .umb-user-card__group { @@ -107,3 +112,12 @@ text-align: center; margin-top: auto; } + +.umb-user-name__last-login { + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; + word-wrap: break-word; +} diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html index acf86263e3..2ce813485c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html @@ -196,7 +196,7 @@
-
{{user.name}}
+
{{user.name}}
{{ userGroup.name }}, @@ -209,9 +209,10 @@ {{ user.formattedLastLogin }}
-
{{ user.name | umbWordLimit:1 }} - has not logged in yet -
+ + has not logged in yet
From e1e5f527e4db3e3c6b43e7049f3df14f287457a0 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Mon, 17 Mar 2025 09:13:09 +0100 Subject: [PATCH 10/10] Updates dependency on Examine to 3.7. (#18676) --- Directory.Packages.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 3428b9a284..75ca1ce51b 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -45,8 +45,8 @@ - - + +