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/BackOfficeServerVariables.cs b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs
index 9325f9e8ae..6dbd5f1e79 100644
--- a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs
+++ b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs
@@ -704,9 +704,6 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
[DataMember(Name = "packageFolder")]
public string? PackageFolder { get; set; }
-
- [DataMember(Name = "sectionAlias")]
- public string? SectionAlias { get; set; }
}
private IEnumerable GetPluginTrees()
@@ -738,7 +735,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
continue;
}
- yield return new PluginTree { Alias = tree.TreeAlias, PackageFolder = pluginController.AreaName, SectionAlias = tree.SectionAlias };
+ yield return new PluginTree { Alias = tree.TreeAlias, PackageFolder = pluginController.AreaName };
}
}
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.BackOffice/Trees/ApplicationTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/ApplicationTreeController.cs
index 28f685bf0d..461d1fc82f 100644
--- a/src/Umbraco.Web.BackOffice/Trees/ApplicationTreeController.cs
+++ b/src/Umbraco.Web.BackOffice/Trees/ApplicationTreeController.cs
@@ -360,7 +360,7 @@ public class ApplicationTreeController : UmbracoAuthorizedApiController
ControllerActionDescriptor? actionDescriptor = _actionDescriptorCollectionProvider.ActionDescriptors.Items
.Cast()
.First(x =>
- (x.ControllerTypeInfo.FullName ?? string.Empty).Equals(controllerType.FullName) &&
+ x.ControllerName.Equals(controllerName) &&
x.ActionName == action);
var actionContext = new ActionContext(HttpContext, routeData, actionDescriptor);
diff --git a/src/Umbraco.Web.BackOffice/Trees/ScriptsTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/ScriptsTreeController.cs
index 0c46e809d2..630584a839 100644
--- a/src/Umbraco.Web.BackOffice/Trees/ScriptsTreeController.cs
+++ b/src/Umbraco.Web.BackOffice/Trees/ScriptsTreeController.cs
@@ -3,12 +3,10 @@ using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Trees;
-using Umbraco.Cms.Web.Common.Attributes;
namespace Umbraco.Cms.Web.BackOffice.Trees;
[CoreTree]
-[PluginController(Constants.Web.Mvc.BackOfficeTreeArea)]
[Tree(Constants.Applications.Settings, Constants.Trees.Scripts, TreeTitle = "Scripts", SortOrder = 10, TreeGroup = Constants.Trees.Groups.Templating)]
public class ScriptsTreeController : FileSystemTreeController
{
diff --git a/src/Umbraco.Web.BackOffice/Trees/StylesheetsTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/StylesheetsTreeController.cs
index 32f2a3e465..3ff7a7ecfc 100644
--- a/src/Umbraco.Web.BackOffice/Trees/StylesheetsTreeController.cs
+++ b/src/Umbraco.Web.BackOffice/Trees/StylesheetsTreeController.cs
@@ -3,12 +3,10 @@ using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Trees;
-using Umbraco.Cms.Web.Common.Attributes;
namespace Umbraco.Cms.Web.BackOffice.Trees;
[CoreTree]
-[PluginController(Constants.Web.Mvc.BackOfficeTreeArea)]
[Tree(Constants.Applications.Settings, Constants.Trees.Stylesheets, TreeTitle = "Stylesheets", SortOrder = 9, TreeGroup = Constants.Trees.Groups.Templating)]
public class StylesheetsTreeController : FileSystemTreeController
{
diff --git a/src/Umbraco.Web.BackOffice/Trees/UrlHelperExtensions.cs b/src/Umbraco.Web.BackOffice/Trees/UrlHelperExtensions.cs
index d8506f5692..1688a99ec2 100644
--- a/src/Umbraco.Web.BackOffice/Trees/UrlHelperExtensions.cs
+++ b/src/Umbraco.Web.BackOffice/Trees/UrlHelperExtensions.cs
@@ -47,15 +47,14 @@ public static class UrlHelperExtensions
string nodeId,
FormCollection? queryStrings)
{
- var actionName = "GetNodes";
- var actionUrl = urlHelper.GetUmbracoApiService(umbracoApiControllerTypeCollection, actionName, treeType);
- actionUrl = StartOrContinueQueryString(actionUrl, actionName);
-
- // Now we need to append the query strings
- // Always ignore the custom start node id when generating URLs for tree nodes since this is a custom once-only parameter
- // that should only ever be used when requesting a tree to render (root), not a tree node
- actionUrl += "id=" + nodeId.EnsureEndsWith('&') + queryStrings?.ToQueryString("id", TreeQueryStringParameters.StartNodeId);
+ var actionUrl = urlHelper.GetUmbracoApiService(umbracoApiControllerTypeCollection, "GetNodes", treeType)?
+ .EnsureEndsWith('?');
+ //now we need to append the query strings
+ actionUrl += "id=" + nodeId.EnsureEndsWith('&') + queryStrings?.ToQueryString("id",
+ //Always ignore the custom start node id when generating URLs for tree nodes since this is a custom once-only parameter
+ // that should only ever be used when requesting a tree to render (root), not a tree node
+ TreeQueryStringParameters.StartNodeId);
return actionUrl;
}
@@ -66,29 +65,11 @@ public static class UrlHelperExtensions
string nodeId,
FormCollection? queryStrings)
{
- var actionName = "GetMenu";
- var actionUrl = urlHelper.GetUmbracoApiService(umbracoApiControllerTypeCollection, actionName, treeType);
- actionUrl = StartOrContinueQueryString(actionUrl, actionName);
+ var actionUrl = urlHelper.GetUmbracoApiService(umbracoApiControllerTypeCollection, "GetMenu", treeType)?
+ .EnsureEndsWith('?');
- // now we need to append the query strings
+ //now we need to append the query strings
actionUrl += "id=" + nodeId.EnsureEndsWith('&') + queryStrings?.ToQueryString("id");
return actionUrl;
}
-
- ///
- /// Check the provided string already includes a querystring fragment
- /// If so, result has "&" appended, else has "?" appended.
- ///
- ///
- ///
- ///
- private static string? StartOrContinueQueryString(string? actionUrl, string? delimiter)
- {
- if (actionUrl is null)
- {
- return actionUrl;
- }
-
- return actionUrl.EnsureEndsWith(actionUrl.Contains($"{delimiter}?") ? "&" : "?");
- }
}
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/mocks/umbraco.servervariables.js b/src/Umbraco.Web.UI.Client/src/common/mocks/umbraco.servervariables.js
index 4b8e8fa146..51029234f5 100644
--- a/src/Umbraco.Web.UI.Client/src/common/mocks/umbraco.servervariables.js
+++ b/src/Umbraco.Web.UI.Client/src/common/mocks/umbraco.servervariables.js
@@ -32,7 +32,7 @@ Umbraco.Sys.ServerVariables = {
},
umbracoPlugins: {
trees: [
- { alias: "myTree", packageFolder: "MyPackage", sectionAlias: "myPackageSectionAlias" }
+ { alias: "myTree", packageFolder: "MyPackage" }
]
},
isDebuggingEnabled: true,
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/src/Umbraco.Web.UI.Client/src/common/services/navigation.service.js b/src/Umbraco.Web.UI.Client/src/common/services/navigation.service.js
index 248e78880a..77b97545b6 100644
--- a/src/Umbraco.Web.UI.Client/src/common/services/navigation.service.js
+++ b/src/Umbraco.Web.UI.Client/src/common/services/navigation.service.js
@@ -616,7 +616,7 @@ function navigationService($routeParams, $location, $q, $injector, eventsService
if (!treeAlias) {
throw "Could not get tree alias for node " + args.node.id;
}
- templateUrl = this.getTreeTemplateUrl(treeAlias, args.action.alias, args.node.section);
+ templateUrl = this.getTreeTemplateUrl(treeAlias, args.action.alias);
}
setMode("dialog");
@@ -633,7 +633,6 @@ function navigationService($routeParams, $location, $q, $injector, eventsService
*
* @param {string} treeAlias the alias of the tree to look up
* @param {string} action the view file name
- * @param {string} sectionAlias the alias of the current section
* @description
* creates the templateUrl based on treeAlias and action
* by convention we will look into the /views/{treetype}/{action}.html
@@ -641,8 +640,8 @@ function navigationService($routeParams, $location, $q, $injector, eventsService
* we will also check for a 'packageName' for the current tree, if it exists then the convention will be:
* for example: /App_Plugins/{mypackage}/backoffice/{treetype}/create.html
*/
- getTreeTemplateUrl: function (treeAlias, action, sectionAlias) {
- var packageTreeFolder = treeService.getTreePackageFolder(treeAlias, sectionAlias);
+ getTreeTemplateUrl: function (treeAlias, action) {
+ var packageTreeFolder = treeService.getTreePackageFolder(treeAlias);
if (packageTreeFolder) {
return (Umbraco.Sys.ServerVariables.umbracoSettings.appPluginsPath +
"/" + packageTreeFolder +
diff --git a/src/Umbraco.Web.UI.Client/src/common/services/tree.service.js b/src/Umbraco.Web.UI.Client/src/common/services/tree.service.js
index fa5d297a88..ba9ebc1b00 100644
--- a/src/Umbraco.Web.UI.Client/src/common/services/tree.service.js
+++ b/src/Umbraco.Web.UI.Client/src/common/services/tree.service.js
@@ -166,27 +166,23 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS
*
* @description
* Determines if the current tree is a plugin tree and if so returns the package folder it has declared
- * so we know where to find its views, otherwise it will just return undefined.
+ * so we know where to find it's views, otherwise it will just return undefined.
*
* @param {String} treeAlias The tree alias to check
- * @param {String} sectionAlias The current section
*/
- getTreePackageFolder: function (treeAlias, sectionAlias) {
+ getTreePackageFolder: function (treeAlias) {
//we determine this based on the server variables
- if (!Umbraco.Sys.ServerVariables.umbracoPlugins || !Utilities.isArray(Umbraco.Sys.ServerVariables.umbracoPlugins.trees)) {
- return undefined;
- }
+ if (Umbraco.Sys.ServerVariables.umbracoPlugins &&
+ Umbraco.Sys.ServerVariables.umbracoPlugins.trees &&
+ Utilities.isArray(Umbraco.Sys.ServerVariables.umbracoPlugins.trees)) {
- let found;
- if (sectionAlias !== undefined) {
- found = Umbraco.Sys.ServerVariables.umbracoPlugins.trees.find(item =>
- invariantEquals(item.alias, treeAlias) && invariantEquals(item.sectionAlias, sectionAlias));
- } else {
- found = Umbraco.Sys.ServerVariables.umbracoPlugins.trees.find(item =>
- invariantEquals(item.alias, treeAlias));
- }
+ var found = _.find(Umbraco.Sys.ServerVariables.umbracoPlugins.trees, function (item) {
+ return invariantEquals(item.alias, treeAlias);
+ });
- return found ? found.packageFolder : undefined;
+ return found ? found.packageFolder : undefined;
+ }
+ return undefined;
},
/**
@@ -872,7 +868,7 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS
//start
var wrappedPromise = doSync();
- //then wrap it
+ //then wrap it
wrappedPromise.then(function (args) {
deferred.resolve(args);
}, function (args) {
diff --git a/src/Umbraco.Web.UI.Client/src/routes.js b/src/Umbraco.Web.UI.Client/src/routes.js
index e6d97ea18f..7e65346d1b 100644
--- a/src/Umbraco.Web.UI.Client/src/routes.js
+++ b/src/Umbraco.Web.UI.Client/src/routes.js
@@ -1,5 +1,5 @@
window.app.config(function ($routeProvider) {
-
+
/**
* This determines if the route can continue depending on authentication and initialization requirements
* @param {boolean} authRequired If true, it checks if the user is authenticated and will resolve successfully
@@ -117,9 +117,9 @@ window.app.config(function ($routeProvider) {
template: "",
//This controller will execute for this route, then we can execute some code in order to set the template Url
controller: function ($scope, $route, $routeParams, $location, sectionService) {
-
+
//We are going to check the currently loaded sections for the user and if the section we are navigating
- //to has a custom route path we'll use that
+ //to has a custom route path we'll use that
sectionService.getSectionsForUser().then(function(sections) {
//find the one we're requesting
var found = _.find(sections, function(s) {
@@ -175,9 +175,8 @@ window.app.config(function ($routeProvider) {
if ($routeParams.section.toLowerCase() === "users" && $routeParams.tree.toLowerCase() === "users" && usersPages.indexOf($routeParams.method.toLowerCase()) === -1) {
$scope.templateUrl = "views/users/overview.html";
return;
- }
-
- $scope.templateUrl = navigationService.getTreeTemplateUrl($routeParams.tree, $routeParams.method, $routeParams.section);
+ }
+ $scope.templateUrl = navigationService.getTreeTemplateUrl($routeParams.tree, $routeParams.method);
},
reloadOnSearch: false,
resolve: canRoute(true)
@@ -191,9 +190,8 @@ window.app.config(function ($routeProvider) {
if (!$routeParams.tree || !$routeParams.method) {
$scope.templateUrl = "views/common/dashboard.html";
return;
- }
-
- $scope.templateUrl = navigationService.getTreeTemplateUrl($routeParams.tree, $routeParams.method, $routeParams.section);
+ }
+ $scope.templateUrl = navigationService.getTreeTemplateUrl($routeParams.tree, $routeParams.method);
},
reloadOnSearch: false,
reloadOnUrl: false,
@@ -201,7 +199,7 @@ window.app.config(function ($routeProvider) {
})
.otherwise({ redirectTo: '/login' });
}).config(function ($locationProvider) {
-
+
$locationProvider.html5Mode(false); //turn html5 mode off
$locationProvider.hashPrefix('');
});
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-sub-views.html b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-sub-views.html
index 56c7a9cf48..be6f21ed96 100644
--- a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-sub-views.html
+++ b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-sub-views.html
@@ -5,7 +5,7 @@
ng-repeat="subView in subViews track by subView.alias"
ng-class="'sub-view-' + subView.name"
val-sub-view="subView"
- ng-if="subView.active"
+ ng-show="subView.active"
>
diff --git a/src/Umbraco.Web.UI.Client/test/unit/common/services/tree-service.spec.js b/src/Umbraco.Web.UI.Client/test/unit/common/services/tree-service.spec.js
index 63800a9e12..4d19cf557a 100644
--- a/src/Umbraco.Web.UI.Client/test/unit/common/services/tree-service.spec.js
+++ b/src/Umbraco.Web.UI.Client/test/unit/common/services/tree-service.spec.js
@@ -298,13 +298,13 @@ describe('tree service tests', function () {
it('can find a plugin based tree', function () {
//we know this exists in the mock umbraco server vars
- var found = treeService.getTreePackageFolder("myTree", "MyPackageSectionAlias");
+ var found = treeService.getTreePackageFolder("myTree");
expect(found).toBe("MyPackage");
});
it('returns undefined for a not found tree', function () {
//we know this does not exist in the mock umbraco server vars
- var found = treeService.getTreePackageFolder("asdfasdf", "fdsafdsa");
+ var found = treeService.getTreePackageFolder("asdfasdf");
expect(found).not.toBeDefined();
});
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();
+ }
+}