diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/da.xml b/src/Umbraco.Core/EmbeddedResources/Lang/da.xml index 049d7477e7..f621d27fa6 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/da.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/da.xml @@ -719,6 +719,7 @@ af Fortryd Celle margen + Skift Vælg Ryd Luk diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml index c7ad610ef0..38c4916e31 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml @@ -759,6 +759,7 @@ by Cancel Cell margin + Change Choose Clear Close diff --git a/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs b/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs index a269c3605a..00b924db5d 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs @@ -583,10 +583,10 @@ public class MediaController : ContentControllerBase Directory.CreateDirectory(root); //must have a file - if (file.Count == 0) + if (file is null || file.Count == 0) { _postAddFileSemaphore.Release(); - return NotFound(); + return NotFound("No file was uploaded"); } //get the string json from the request @@ -769,12 +769,26 @@ public class MediaController : ContentControllerBase break; } - // If media type is still File then let's check if it's an image. + // If media type is still File then let's check if it's an imageor a custom image type. if (mediaTypeAlias == Constants.Conventions.MediaTypes.File && _imageUrlGenerator.IsSupportedImageFormat(ext)) + { + if (allowedContentTypes.Any(mt => mt.Alias == Constants.Conventions.MediaTypes.Image)) { mediaTypeAlias = Constants.Conventions.MediaTypes.Image; } + else + { + IMediaType? customType = allowedContentTypes.FirstOrDefault(mt => + mt.CompositionPropertyTypes.Any(pt => + pt.PropertyEditorAlias == Constants.PropertyEditors.Aliases.ImageCropper)); + + if (customType is not null) + { + mediaTypeAlias = customType.Alias; + } + } + } } else { diff --git a/src/Umbraco.Web.Common/Filters/UmbracoMemberAuthorizeFilter.cs b/src/Umbraco.Web.Common/Filters/UmbracoMemberAuthorizeFilter.cs index 95c4ae5cec..2f56cdb51f 100644 --- a/src/Umbraco.Web.Common/Filters/UmbracoMemberAuthorizeFilter.cs +++ b/src/Umbraco.Web.Common/Filters/UmbracoMemberAuthorizeFilter.cs @@ -60,14 +60,14 @@ public class UmbracoMemberAuthorizeFilter : IAsyncAuthorizationFilter { context.HttpContext.SetReasonPhrase( "Resource restricted: the member is not of a permitted type or group."); + context.HttpContext.Response.StatusCode = 403; context.Result = new ForbidResult(); } } else { - context.HttpContext.SetReasonPhrase( - "Resource restricted: the member is not logged in."); - context.Result = new UnauthorizedResult(); + context.HttpContext.Response.StatusCode = 401; + context.Result = new ForbidResult(); } } diff --git a/src/Umbraco.Web.Common/Security/ConfigureMemberCookieOptions.cs b/src/Umbraco.Web.Common/Security/ConfigureMemberCookieOptions.cs index b8c2874641..1ba9a52526 100644 --- a/src/Umbraco.Web.Common/Security/ConfigureMemberCookieOptions.cs +++ b/src/Umbraco.Web.Common/Security/ConfigureMemberCookieOptions.cs @@ -1,10 +1,12 @@ using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Web.Common.Controllers; using Umbraco.Extensions; namespace Umbraco.Cms.Web.Common.Security; @@ -58,7 +60,16 @@ public sealed class ConfigureMemberCookieOptions : IConfigureNamedOptions { - ctx.Response.StatusCode = StatusCodes.Status403Forbidden; + // When the controller is an UmbracoAPIController, we want to return a StatusCode instead of a redirect. + // All other cases should use the default Redirect of the CookieAuthenticationEvent. + var controllerDescriptor = ctx.HttpContext.GetEndpoint()?.Metadata + .OfType() + .FirstOrDefault(); + + if (!controllerDescriptor?.ControllerTypeInfo.IsSubclassOf(typeof(UmbracoApiController)) ?? false) + { + new CookieAuthenticationEvents().OnRedirectToAccessDenied(ctx); + } return Task.CompletedTask; }, diff --git a/src/Umbraco.Web.UI.Client/src/common/services/clipboard.service.js b/src/Umbraco.Web.UI.Client/src/common/services/clipboard.service.js index abf173b129..238d9a8ee6 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/clipboard.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/clipboard.service.js @@ -344,10 +344,14 @@ function clipboardService($window, notificationsService, eventsService, localSto // Clean up each entry var copiedDatas = datas.map(data => prepareEntryForStorage(type, data, firstLevelClearupMethod)); - // remove previous copies of this entry: + // remove previous copies of this entry (Make sure to not remove copies from unsaved content): storage.entries = storage.entries.filter( (entry) => { - return entry.unique !== uniqueKey; + if (entry.unique === 0) { + return displayLabel !== entry.label; + } else { + return entry.unique !== uniqueKey; + } } ); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Web.Website/Security/MemberAuthorizeTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Web.Website/Security/MemberAuthorizeTests.cs new file mode 100644 index 0000000000..0fc1dfa85d --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Web.Website/Security/MemberAuthorizeTests.cs @@ -0,0 +1,126 @@ +using System.Net; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Logging; +using Umbraco.Cms.Core.Routing; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Web; +using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Tests.Integration.TestServerTest; +using Umbraco.Cms.Web.Common.Controllers; +using Umbraco.Cms.Web.Common.Filters; +using Umbraco.Cms.Web.Common.Security; +using Umbraco.Cms.Web.Website.Controllers; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Web.Website.Security +{ + public class MemberAuthorizeTests : UmbracoTestServerTestBase + { + private Mock _memberManagerMock = new(); + + protected override void ConfigureTestServices(IServiceCollection services) + { + _memberManagerMock = new Mock(); + services.Remove(new ServiceDescriptor(typeof(IMemberManager), typeof(MemberManager), ServiceLifetime.Scoped)); + services.Remove(new ServiceDescriptor(typeof(MemberManager), ServiceLifetime.Scoped)); + services.AddScoped(_ => _memberManagerMock.Object); + } + + [Test] + public async Task Secure_SurfaceController_Should_Return_Redirect_WhenNotLoggedIn() + { + _memberManagerMock.Setup(x => x.IsLoggedIn()).Returns(false); + + var url = PrepareSurfaceControllerUrl(x => x.Secure()); + + var response = await Client.GetAsync(url); + + var cookieAuthenticationOptions = Services.GetService>(); + Assert.AreEqual(HttpStatusCode.Redirect, response.StatusCode); + Assert.AreEqual(cookieAuthenticationOptions.Value.AccessDeniedPath.ToString(), response.Headers.Location?.AbsolutePath); + } + + [Test] + public async Task Secure_SurfaceController_Should_Return_Redirect_WhenNotAuthorized() + { + _memberManagerMock.Setup(x => x.IsLoggedIn()).Returns(true); + _memberManagerMock.Setup(x => x.IsMemberAuthorizedAsync( + It.IsAny>(), + It.IsAny>(), + It.IsAny>())) + .ReturnsAsync(false); + + var url = PrepareSurfaceControllerUrl(x => x.Secure()); + + var response = await Client.GetAsync(url); + + var cookieAuthenticationOptions = Services.GetService>(); + Assert.AreEqual(HttpStatusCode.Redirect, response.StatusCode); + Assert.AreEqual(cookieAuthenticationOptions.Value.AccessDeniedPath.ToString(), response.Headers.Location?.AbsolutePath); + } + + + [Test] + public async Task Secure_ApiController_Should_Return_Unauthorized_WhenNotLoggedIn() + { + _memberManagerMock.Setup(x => x.IsLoggedIn()).Returns(false); + var url = PrepareApiControllerUrl(x => x.Secure()); + + var response = await Client.GetAsync(url); + + Assert.AreEqual(HttpStatusCode.Unauthorized, response.StatusCode); + } + + [Test] + public async Task Secure_ApiController_Should_Return_Forbidden_WhenNotAuthorized() + { + _memberManagerMock.Setup(x => x.IsLoggedIn()).Returns(true); + _memberManagerMock.Setup(x => x.IsMemberAuthorizedAsync( + It.IsAny>(), + It.IsAny>(), + It.IsAny>())) + .ReturnsAsync(false); + + var url = PrepareApiControllerUrl(x => x.Secure()); + + var response = await Client.GetAsync(url); + + Assert.AreEqual(HttpStatusCode.Forbidden, response.StatusCode); + } + } + + public class TestSurfaceController : SurfaceController + { + public TestSurfaceController( + IUmbracoContextAccessor umbracoContextAccessor, + IUmbracoDatabaseFactory databaseFactory, + ServiceContext services, + AppCaches appCaches, + IProfilingLogger profilingLogger, + IPublishedUrlProvider publishedUrlProvider) + : base( + umbracoContextAccessor, + databaseFactory, + services, + appCaches, + profilingLogger, + publishedUrlProvider) + { + } + + [UmbracoMemberAuthorize] + public IActionResult Secure() => NoContent(); + } + + public class TestApiController : UmbracoApiController + { + [UmbracoMemberAuthorize] + public IActionResult Secure() => NoContent(); + } +}