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();
+ }
+}