diff --git a/Directory.Packages.props b/Directory.Packages.props index 090afcd216..78d394ab7c 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -5,30 +5,31 @@ - + - + - - - - - - + + + + + + + - + - - + + @@ -44,22 +45,22 @@ - - + + - + - - - + + + @@ -68,7 +69,7 @@ - + @@ -79,7 +80,7 @@ - + diff --git a/src/Umbraco.Cms.Api.Common/Configuration/ConfigureOpenIddict.cs b/src/Umbraco.Cms.Api.Common/Configuration/ConfigureOpenIddict.cs new file mode 100644 index 0000000000..f428957bd9 --- /dev/null +++ b/src/Umbraco.Cms.Api.Common/Configuration/ConfigureOpenIddict.cs @@ -0,0 +1,15 @@ +using Microsoft.Extensions.Options; +using OpenIddict.Server.AspNetCore; +using Umbraco.Cms.Core.Configuration.Models; + +namespace Umbraco.Cms.Api.Common.Configuration; + +internal class ConfigureOpenIddict : IConfigureOptions +{ + private readonly IOptions _globalSettings; + + public ConfigureOpenIddict(IOptions globalSettings) => _globalSettings = globalSettings; + + public void Configure(OpenIddictServerAspNetCoreOptions options) + => options.DisableTransportSecurityRequirement = _globalSettings.Value.UseHttps is false; +} diff --git a/src/Umbraco.Cms.Api.Common/DependencyInjection/UmbracoBuilderAuthExtensions.cs b/src/Umbraco.Cms.Api.Common/DependencyInjection/UmbracoBuilderAuthExtensions.cs index d5556f63c2..98068791af 100644 --- a/src/Umbraco.Cms.Api.Common/DependencyInjection/UmbracoBuilderAuthExtensions.cs +++ b/src/Umbraco.Cms.Api.Common/DependencyInjection/UmbracoBuilderAuthExtensions.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.IdentityModel.Tokens; using OpenIddict.Server; using OpenIddict.Validation; +using Umbraco.Cms.Api.Common.Configuration; using Umbraco.Cms.Api.Common.Security; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; @@ -132,5 +133,6 @@ public static class UmbracoBuilderAuthExtensions }); builder.Services.AddRecurringBackgroundJob(); + builder.Services.ConfigureOptions(); } } diff --git a/src/Umbraco.Cms.Api.Delivery/Services/RequestHeaderHandler.cs b/src/Umbraco.Cms.Api.Delivery/Services/RequestHeaderHandler.cs index 08d7a916e6..37651a4158 100644 --- a/src/Umbraco.Cms.Api.Delivery/Services/RequestHeaderHandler.cs +++ b/src/Umbraco.Cms.Api.Delivery/Services/RequestHeaderHandler.cs @@ -8,11 +8,5 @@ internal abstract class RequestHeaderHandler protected RequestHeaderHandler(IHttpContextAccessor httpContextAccessor) => _httpContextAccessor = httpContextAccessor; - protected string? GetHeaderValue(string headerName) - { - HttpContext httpContext = _httpContextAccessor.HttpContext ?? - throw new InvalidOperationException("Could not obtain an HTTP context"); - - return httpContext.Request.Headers[headerName]; - } + protected string? GetHeaderValue(string headerName) => _httpContextAccessor.HttpContext?.Request.Headers[headerName]; } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/User/CalculateStartNodesUserController.cs b/src/Umbraco.Cms.Api.Management/Controllers/User/CalculateStartNodesUserController.cs new file mode 100644 index 0000000000..98f1526ce9 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/User/CalculateStartNodesUserController.cs @@ -0,0 +1,59 @@ +using Asp.Versioning; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.Factories; +using Umbraco.Cms.Api.Management.ViewModels.User; +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Security.Authorization; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Cms.Web.Common.Authorization; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Api.Management.Controllers.User; + +[ApiVersion("1.0")] +public class CalculatedStartNodesUserController : UserControllerBase +{ + private readonly IAuthorizationService _authorizationService; + private readonly IUserService _userService; + private readonly IUserPresentationFactory _userPresentationFactory; + + public CalculatedStartNodesUserController( + IAuthorizationService authorizationService, + IUserService userService, + IUserPresentationFactory userPresentationFactory) + { + _authorizationService = authorizationService; + _userService = userService; + _userPresentationFactory = userPresentationFactory; + } + + [HttpGet("{id:guid}/calculate-start-nodes")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(CalculatedUserStartNodesResponseModel), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + public async Task CalculatedStartNodes(CancellationToken cancellationToken, Guid id) + { + AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync( + User, + UserPermissionResource.WithKeys(id), + AuthorizationPolicies.UserPermissionByResource); + + if (!authorizationResult.Succeeded) + { + return Forbidden(); + } + + IUser? user = await _userService.GetAsync(id); + + if (user is null) + { + return UserOperationStatusResult(UserOperationStatus.UserNotFound); + } + + CalculatedUserStartNodesResponseModel responseModel = await _userPresentationFactory.CreateCalculatedUserStartNodesResponseModelAsync(user); + return Ok(responseModel); + } +} diff --git a/src/Umbraco.Cms.Api.Management/DependencyInjection/BackOfficeAuthBuilderExtensions.cs b/src/Umbraco.Cms.Api.Management/DependencyInjection/BackOfficeAuthBuilderExtensions.cs index e36f103e92..5ddb98b570 100644 --- a/src/Umbraco.Cms.Api.Management/DependencyInjection/BackOfficeAuthBuilderExtensions.cs +++ b/src/Umbraco.Cms.Api.Management/DependencyInjection/BackOfficeAuthBuilderExtensions.cs @@ -27,11 +27,8 @@ public static class BackOfficeAuthBuilderExtensions public static IUmbracoBuilder AddTokenRevocation(this IUmbracoBuilder builder) { - builder.AddNotificationAsyncHandler(); builder.AddNotificationAsyncHandler(); builder.AddNotificationAsyncHandler(); - builder.AddNotificationAsyncHandler(); - builder.AddNotificationAsyncHandler(); builder.AddNotificationAsyncHandler(); return builder; diff --git a/src/Umbraco.Cms.Api.Management/DependencyInjection/BackOfficeAuthPolicyBuilderExtensions.cs b/src/Umbraco.Cms.Api.Management/DependencyInjection/BackOfficeAuthPolicyBuilderExtensions.cs index 4714c54c74..45eccad5ec 100644 --- a/src/Umbraco.Cms.Api.Management/DependencyInjection/BackOfficeAuthPolicyBuilderExtensions.cs +++ b/src/Umbraco.Cms.Api.Management/DependencyInjection/BackOfficeAuthPolicyBuilderExtensions.cs @@ -28,6 +28,7 @@ internal static class BackOfficeAuthPolicyBuilderExtensions builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddAuthorization(CreatePolicies); return builder; @@ -35,14 +36,12 @@ internal static class BackOfficeAuthPolicyBuilderExtensions private static void CreatePolicies(AuthorizationOptions options) { - void AddPolicy(string policyName, string claimType, params string[] allowedClaimValues) - { - options.AddPolicy(policyName, policy => + void AddAllowedApplicationsPolicy(string policyName, params string[] allowedClaimValues) + => options.AddPolicy(policyName, policy => { policy.AuthenticationSchemes.Add(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme); - policy.RequireClaim(claimType, allowedClaimValues); + policy.Requirements.Add(new AllowedApplicationRequirement(allowedClaimValues)); }); - } options.AddPolicy(AuthorizationPolicies.BackOfficeAccess, policy => { @@ -56,39 +55,39 @@ internal static class BackOfficeAuthPolicyBuilderExtensions policy.RequireRole(Constants.Security.AdminGroupAlias); }); - AddPolicy(AuthorizationPolicies.SectionAccessContent, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Content); - AddPolicy(AuthorizationPolicies.SectionAccessContentOrMedia, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Content, Constants.Applications.Media); - AddPolicy(AuthorizationPolicies.SectionAccessForContentTree, Constants.Security.AllowedApplicationsClaimType, + AddAllowedApplicationsPolicy(AuthorizationPolicies.SectionAccessContent, Constants.Applications.Content); + AddAllowedApplicationsPolicy(AuthorizationPolicies.SectionAccessContentOrMedia, Constants.Applications.Content, Constants.Applications.Media); + AddAllowedApplicationsPolicy(AuthorizationPolicies.SectionAccessForContentTree, Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Users, Constants.Applications.Settings, Constants.Applications.Packages, Constants.Applications.Members); - AddPolicy(AuthorizationPolicies.SectionAccessForMediaTree, Constants.Security.AllowedApplicationsClaimType, + AddAllowedApplicationsPolicy(AuthorizationPolicies.SectionAccessForMediaTree, Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Users, Constants.Applications.Settings, Constants.Applications.Packages, Constants.Applications.Members); - AddPolicy(AuthorizationPolicies.SectionAccessForMemberTree, Constants.Security.AllowedApplicationsClaimType, + AddAllowedApplicationsPolicy(AuthorizationPolicies.SectionAccessForMemberTree, Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Members); - AddPolicy(AuthorizationPolicies.SectionAccessMedia, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Media); - AddPolicy(AuthorizationPolicies.SectionAccessMembers, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Members); - AddPolicy(AuthorizationPolicies.SectionAccessPackages, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Packages); - AddPolicy(AuthorizationPolicies.SectionAccessSettings, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Settings); - AddPolicy(AuthorizationPolicies.SectionAccessUsers, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Users); + AddAllowedApplicationsPolicy(AuthorizationPolicies.SectionAccessMedia, Constants.Applications.Media); + AddAllowedApplicationsPolicy(AuthorizationPolicies.SectionAccessMembers, Constants.Applications.Members); + AddAllowedApplicationsPolicy(AuthorizationPolicies.SectionAccessPackages, Constants.Applications.Packages); + AddAllowedApplicationsPolicy(AuthorizationPolicies.SectionAccessSettings, Constants.Applications.Settings); + AddAllowedApplicationsPolicy(AuthorizationPolicies.SectionAccessUsers, Constants.Applications.Users); - AddPolicy(AuthorizationPolicies.TreeAccessDataTypes, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Settings); - AddPolicy(AuthorizationPolicies.TreeAccessDictionary, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Translation); - AddPolicy(AuthorizationPolicies.TreeAccessDictionaryOrTemplates, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Translation, Constants.Applications.Settings); - AddPolicy(AuthorizationPolicies.TreeAccessDocuments, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Content); - AddPolicy(AuthorizationPolicies.TreeAccessDocumentsOrDocumentTypes, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Content, Constants.Applications.Settings); - AddPolicy(AuthorizationPolicies.TreeAccessDocumentTypes, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Settings); - AddPolicy(AuthorizationPolicies.TreeAccessLanguages, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Settings); - AddPolicy(AuthorizationPolicies.TreeAccessMediaTypes, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Settings); - AddPolicy(AuthorizationPolicies.TreeAccessMediaOrMediaTypes, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Media, Constants.Applications.Settings); - AddPolicy(AuthorizationPolicies.TreeAccessMemberGroups, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Members); - AddPolicy(AuthorizationPolicies.TreeAccessMemberTypes, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Settings); - AddPolicy(AuthorizationPolicies.TreeAccessPartialViews, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Settings); - AddPolicy(AuthorizationPolicies.TreeAccessRelationTypes, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Settings); - AddPolicy(AuthorizationPolicies.TreeAccessScripts, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Settings); - AddPolicy(AuthorizationPolicies.TreeAccessStylesheets, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Settings); - AddPolicy(AuthorizationPolicies.TreeAccessTemplates, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Settings); - AddPolicy(AuthorizationPolicies.TreeAccessWebhooks, Constants.Security.AllowedApplicationsClaimType, Constants.Applications.Settings); + AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessDataTypes, Constants.Applications.Settings); + AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessDictionary, Constants.Applications.Translation); + AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessDictionaryOrTemplates, Constants.Applications.Translation, Constants.Applications.Settings); + AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessDocuments, Constants.Applications.Content); + AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessDocumentsOrDocumentTypes, Constants.Applications.Content, Constants.Applications.Settings); + AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessDocumentTypes, Constants.Applications.Settings); + AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessLanguages, Constants.Applications.Settings); + AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessMediaTypes, Constants.Applications.Settings); + AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessMediaOrMediaTypes, Constants.Applications.Media, Constants.Applications.Settings); + AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessMemberGroups, Constants.Applications.Members); + AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessMemberTypes, Constants.Applications.Settings); + AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessPartialViews, Constants.Applications.Settings); + AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessRelationTypes, Constants.Applications.Settings); + AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessScripts, Constants.Applications.Settings); + AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessStylesheets, Constants.Applications.Settings); + AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessTemplates, Constants.Applications.Settings); + AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessWebhooks, Constants.Applications.Settings); // Contextual permissions options.AddPolicy(AuthorizationPolicies.ContentPermissionByResource, policy => diff --git a/src/Umbraco.Cms.Api.Management/Factories/IUserPresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/IUserPresentationFactory.cs index 9af4ace9e1..a1e8a79edf 100644 --- a/src/Umbraco.Cms.Api.Management/Factories/IUserPresentationFactory.cs +++ b/src/Umbraco.Cms.Api.Management/Factories/IUserPresentationFactory.cs @@ -25,4 +25,6 @@ public interface IUserPresentationFactory Task CreateCurrentUserConfigurationModelAsync(); UserItemResponseModel CreateItemResponseModel(IUser user); + + Task CreateCalculatedUserStartNodesResponseModelAsync(IUser user); } diff --git a/src/Umbraco.Cms.Api.Management/Factories/UserPresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/UserPresentationFactory.cs index 641f5883ed..9164be6772 100644 --- a/src/Umbraco.Cms.Api.Management/Factories/UserPresentationFactory.cs +++ b/src/Umbraco.Cms.Api.Management/Factories/UserPresentationFactory.cs @@ -212,6 +212,23 @@ public class UserPresentationFactory : IUserPresentationFactory }); } + public async Task CreateCalculatedUserStartNodesResponseModelAsync(IUser user) + { + var mediaStartNodeIds = user.CalculateMediaStartNodeIds(_entityService, _appCaches); + ISet mediaStartNodeKeys = GetKeysFromIds(mediaStartNodeIds, UmbracoObjectTypes.Media); + var contentStartNodeIds = user.CalculateContentStartNodeIds(_entityService, _appCaches); + ISet documentStartNodeKeys = GetKeysFromIds(contentStartNodeIds, UmbracoObjectTypes.Document); + + return await Task.FromResult(new CalculatedUserStartNodesResponseModel() + { + Id = user.Key, + MediaStartNodeIds = mediaStartNodeKeys, + HasMediaRootAccess = HasRootAccess(mediaStartNodeIds), + DocumentStartNodeIds = documentStartNodeKeys, + HasDocumentRootAccess = HasRootAccess(contentStartNodeIds), + }); + } + private ISet GetKeysFromIds(IEnumerable? ids, UmbracoObjectTypes type) { IEnumerable? models = ids? diff --git a/src/Umbraco.Cms.Api.Management/Handlers/RevokeUserAuthenticationTokensNotificationHandler.cs b/src/Umbraco.Cms.Api.Management/Handlers/RevokeUserAuthenticationTokensNotificationHandler.cs index acf6cb508a..e56e776f85 100644 --- a/src/Umbraco.Cms.Api.Management/Handlers/RevokeUserAuthenticationTokensNotificationHandler.cs +++ b/src/Umbraco.Cms.Api.Management/Handlers/RevokeUserAuthenticationTokensNotificationHandler.cs @@ -13,77 +13,31 @@ using Umbraco.Extensions; namespace Umbraco.Cms.Api.Management.Handlers; internal sealed class RevokeUserAuthenticationTokensNotificationHandler : - INotificationAsyncHandler, INotificationAsyncHandler, INotificationAsyncHandler, - INotificationAsyncHandler, - INotificationAsyncHandler, INotificationAsyncHandler { - private const string NotificationStateKey = "Umbraco.Cms.Api.Management.Handlers.RevokeUserAuthenticationTokensNotificationHandler"; - private readonly IUserService _userService; - private readonly IUserGroupService _userGroupService; private readonly IOpenIddictTokenManager _tokenManager; private readonly ILogger _logger; private readonly SecuritySettings _securitySettings; public RevokeUserAuthenticationTokensNotificationHandler( IUserService userService, - IUserGroupService userGroupService, IOpenIddictTokenManager tokenManager, ILogger logger, IOptions securitySettingsOptions) { _userService = userService; - _userGroupService = userGroupService; _tokenManager = tokenManager; _logger = logger; _securitySettings = securitySettingsOptions.Value; } - // We need to know the pre-saving state of the saved users in order to compare if their access has changed - public async Task HandleAsync(UserSavingNotification notification, CancellationToken cancellationToken) - { - try - { - var usersAccess = new Dictionary(); - foreach (IUser user in notification.SavedEntities) - { - UserStartNodesAndGroupAccess? priorUserAccess = await GetRelevantUserAccessDataByUserKeyAsync(user.Key); - if (priorUserAccess == null) - { - continue; - } - - usersAccess.Add(user.Key, priorUserAccess); - } - - notification.State[NotificationStateKey] = usersAccess; - } - catch (DbException e) - { - _logger.LogWarning(e, "This is expected when we upgrade from < Umbraco 14. Otherwise it should not happen"); - } - } - public async Task HandleAsync(UserSavedNotification notification, CancellationToken cancellationToken) { try { - Dictionary? preSavingUsersState = null; - - if (notification.State.TryGetValue(NotificationStateKey, out var value)) - { - preSavingUsersState = value as Dictionary; - } - - // If we have a new user, there is no token - if (preSavingUsersState is null || preSavingUsersState.Count == 0) - { - return; - } - foreach (IUser user in notification.SavedEntities) { if (user.IsSuper()) @@ -95,23 +49,6 @@ internal sealed class RevokeUserAuthenticationTokensNotificationHandler : if (user.IsLockedOut || user.IsApproved is false) { await RevokeTokensAsync(user); - continue; - } - - // Don't revoke admin tokens to prevent log out when accidental changes - if (user.IsAdmin()) - { - continue; - } - - // Check if the user access has changed - we also need to revoke all tokens in this case - if (preSavingUsersState.TryGetValue(user.Key, out UserStartNodesAndGroupAccess? preSavingState)) - { - UserStartNodesAndGroupAccess postSavingState = MapToUserStartNodesAndGroupAccess(user); - if (preSavingState.CompareAccess(postSavingState) == false) - { - await RevokeTokensAsync(user); - } } } } @@ -131,49 +68,6 @@ internal sealed class RevokeUserAuthenticationTokensNotificationHandler : } } - // We need to know the pre-deleting state of the users part of the deleted group to revoke their tokens - public async Task HandleAsync(UserGroupDeletingNotification notification, CancellationToken cancellationToken) - { - var usersInGroups = new Dictionary>(); - foreach (IUserGroup userGroup in notification.DeletedEntities) - { - var users = await GetUsersByGroupKeyAsync(userGroup.Key); - if (users == null) - { - continue; - } - - usersInGroups.Add(userGroup.Key, users); - } - - notification.State[NotificationStateKey] = usersInGroups; - } - - public async Task HandleAsync(UserGroupDeletedNotification notification, CancellationToken cancellationToken) - { - Dictionary>? preDeletingUsersInGroups = null; - - if (notification.State.TryGetValue(NotificationStateKey, out var value)) - { - preDeletingUsersInGroups = value as Dictionary>; - } - - if (preDeletingUsersInGroups is null) - { - return; - } - - // since the user group was deleted, we can only use the information we collected before the deletion - // this means that we will not be able to detect users in any groups that were eventually deleted (due to implementor/3th party supplier interference) - // that were not in the initial to be deleted list - foreach (IUser user in preDeletingUsersInGroups - .Where(group => notification.DeletedEntities.Any(entity => group.Key == entity.Key)) - .SelectMany(group => group.Value)) - { - await RevokeTokensAsync(user); - } - } - public async Task HandleAsync(UserLoginSuccessNotification notification, CancellationToken cancellationToken) { if (_securitySettings.AllowConcurrentLogins is false) @@ -190,29 +84,6 @@ internal sealed class RevokeUserAuthenticationTokensNotificationHandler : } } - // Get data about the user before saving - private async Task GetRelevantUserAccessDataByUserKeyAsync(Guid userKey) - { - IUser? user = await _userService.GetAsync(userKey); - - return user is null - ? null - : MapToUserStartNodesAndGroupAccess(user); - } - - private UserStartNodesAndGroupAccess MapToUserStartNodesAndGroupAccess(IUser user) - => new(user.Groups.Select(g => g.Key), user.StartContentIds, user.StartMediaIds); - - // Get data about the users part of a group before deleting it - private async Task?> GetUsersByGroupKeyAsync(Guid userGroupKey) - { - IUserGroup? userGroup = await _userGroupService.GetAsync(userGroupKey); - - return userGroup is null - ? null - : _userService.GetAllInGroup(userGroup.Id); - } - private async Task RevokeTokensAsync(IUser user) { _logger.LogInformation("Revoking active tokens for user with ID {id}", user.Id); @@ -236,35 +107,4 @@ internal sealed class RevokeUserAuthenticationTokensNotificationHandler : return null; } - - private class UserStartNodesAndGroupAccess - { - public IEnumerable GroupKeys { get; } - - public int[]? StartContentIds { get; } - - public int[]? StartMediaIds { get; } - - public UserStartNodesAndGroupAccess(IEnumerable groupKeys, int[]? startContentIds, int[]? startMediaIds) - { - GroupKeys = groupKeys; - StartContentIds = startContentIds; - StartMediaIds = startMediaIds; - } - - public bool CompareAccess(UserStartNodesAndGroupAccess other) - { - var areContentStartNodesEqual = (StartContentIds == null && other.StartContentIds == null) || - (StartContentIds != null && other.StartContentIds != null && - StartContentIds.SequenceEqual(other.StartContentIds)); - - var areMediaStartNodesEqual = (StartMediaIds == null && other.StartMediaIds == null) || - (StartMediaIds != null && other.StartMediaIds != null && - StartMediaIds.SequenceEqual(other.StartMediaIds)); - - return areContentStartNodesEqual && - areMediaStartNodesEqual && - GroupKeys.SequenceEqual(other.GroupKeys); - } - } } diff --git a/src/Umbraco.Cms.Api.Management/OpenApi.json b/src/Umbraco.Cms.Api.Management/OpenApi.json index 0adc3c36b7..11dee452ca 100644 --- a/src/Umbraco.Cms.Api.Management/OpenApi.json +++ b/src/Umbraco.Cms.Api.Management/OpenApi.json @@ -30298,6 +30298,66 @@ ] } }, + "/umbraco/management/api/v1/user/{id}/calculate-start-nodes": { + "get": { + "tags": [ + "User" + ], + "operationId": "GetUserByIdCalculateStartNodes", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/CalculatedUserStartNodesResponseModel" + } + ] + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ProblemDetails" + } + ] + } + } + } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + }, + "403": { + "description": "The authenticated user do not have access to this resource" + } + }, + "security": [ + { + "Backoffice User": [ ] + } + ] + } + }, "/umbraco/management/api/v1/user/{id}/change-password": { "post": { "tags": [ @@ -33447,6 +33507,51 @@ }, "additionalProperties": false }, + "CalculatedUserStartNodesResponseModel": { + "required": [ + "documentStartNodeIds", + "hasDocumentRootAccess", + "hasMediaRootAccess", + "id", + "mediaStartNodeIds" + ], + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "documentStartNodeIds": { + "uniqueItems": true, + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/ReferenceByIdModel" + } + ] + } + }, + "hasDocumentRootAccess": { + "type": "boolean" + }, + "mediaStartNodeIds": { + "uniqueItems": true, + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/ReferenceByIdModel" + } + ] + } + }, + "hasMediaRootAccess": { + "type": "boolean" + } + }, + "additionalProperties": false + }, "ChangePasswordCurrentUserRequestModel": { "required": [ "newPassword" diff --git a/src/Umbraco.Cms.Api.Management/Security/Authorization/User/AllowedApplicationHandler.cs b/src/Umbraco.Cms.Api.Management/Security/Authorization/User/AllowedApplicationHandler.cs new file mode 100644 index 0000000000..a36a592827 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Security/Authorization/User/AllowedApplicationHandler.cs @@ -0,0 +1,24 @@ +using Microsoft.AspNetCore.Authorization; +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Security.Authorization; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Api.Management.Security.Authorization.User; + +/// +/// Authorizes that the current user has the correct permission access to the applications listed in the requirement. +/// +internal sealed class AllowedApplicationHandler : MustSatisfyRequirementAuthorizationHandler +{ + private readonly IAuthorizationHelper _authorizationHelper; + + public AllowedApplicationHandler(IAuthorizationHelper authorizationHelper) + => _authorizationHelper = authorizationHelper; + + protected override Task IsAuthorized(AuthorizationHandlerContext context, AllowedApplicationRequirement requirement) + { + IUser user = _authorizationHelper.GetUmbracoUser(context.User); + var allowed = user.AllowedSections.ContainsAny(requirement.Applications); + return Task.FromResult(allowed); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Security/Authorization/User/AllowedApplicationRequirement.cs b/src/Umbraco.Cms.Api.Management/Security/Authorization/User/AllowedApplicationRequirement.cs new file mode 100644 index 0000000000..dce6d8773e --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Security/Authorization/User/AllowedApplicationRequirement.cs @@ -0,0 +1,14 @@ +using Microsoft.AspNetCore.Authorization; + +namespace Umbraco.Cms.Api.Management.Security.Authorization.User; + +/// +/// Authorization requirement for the . +/// +internal sealed class AllowedApplicationRequirement : IAuthorizationRequirement +{ + public string[] Applications { get; } + + public AllowedApplicationRequirement(params string[] applications) + => Applications = applications; +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/User/CalculatedUserStartNodesResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/User/CalculatedUserStartNodesResponseModel.cs new file mode 100644 index 0000000000..8cc71e8482 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/User/CalculatedUserStartNodesResponseModel.cs @@ -0,0 +1,14 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.User; + +public class CalculatedUserStartNodesResponseModel +{ + public required Guid Id { get; init; } + + public ISet DocumentStartNodeIds { get; set; } = new HashSet(); + + public bool HasDocumentRootAccess { get; set; } + + public ISet MediaStartNodeIds { get; set; } = new HashSet(); + + public bool HasMediaRootAccess { get; set; } +} diff --git a/src/Umbraco.Cms.Imaging.ImageSharp/Umbraco.Cms.Imaging.ImageSharp.csproj b/src/Umbraco.Cms.Imaging.ImageSharp/Umbraco.Cms.Imaging.ImageSharp.csproj index 13126a24b5..549ea5cb40 100644 --- a/src/Umbraco.Cms.Imaging.ImageSharp/Umbraco.Cms.Imaging.ImageSharp.csproj +++ b/src/Umbraco.Cms.Imaging.ImageSharp/Umbraco.Cms.Imaging.ImageSharp.csproj @@ -6,7 +6,7 @@ - + diff --git a/src/Umbraco.Core/Constants-Security.cs b/src/Umbraco.Core/Constants-Security.cs index 842add06fa..2f24fa182d 100644 --- a/src/Umbraco.Core/Constants-Security.cs +++ b/src/Umbraco.Core/Constants-Security.cs @@ -93,12 +93,15 @@ public static partial class Constants public const string MemberExternalAuthenticationTypePrefix = "UmbracoMembers."; + [Obsolete("Please use the UserExtensions class to access user start node info. Will be removed in V15.")] public const string StartContentNodeIdClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/startcontentnode"; + [Obsolete("Please use the UserExtensions class to access user start node info. Will be removed in V15.")] public const string StartMediaNodeIdClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/startmedianode"; + [Obsolete("Please use IUser.AllowedSections instead. Will be removed in V15.")] public const string AllowedApplicationsClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/allowedapp"; diff --git a/src/Umbraco.Core/DeliveryApi/ApiMediaUrlProvider.cs b/src/Umbraco.Core/DeliveryApi/ApiMediaUrlProvider.cs index 20373b1d3b..f8ebee826b 100644 --- a/src/Umbraco.Core/DeliveryApi/ApiMediaUrlProvider.cs +++ b/src/Umbraco.Core/DeliveryApi/ApiMediaUrlProvider.cs @@ -17,6 +17,6 @@ public sealed class ApiMediaUrlProvider : IApiMediaUrlProvider throw new ArgumentException("Media URLs can only be generated from Media items.", nameof(media)); } - return _publishedUrlProvider.GetMediaUrl(media, UrlMode.Relative); + return _publishedUrlProvider.GetMediaUrl(media); } } diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/bs.xml b/src/Umbraco.Core/EmbeddedResources/Lang/bs.xml index e72560cd5f..3e4e9ea08f 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/bs.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/bs.xml @@ -409,6 +409,27 @@ + + X-Frame-Options koji se koristi za kontrolu da li neko mjesto može biti IFRAMED od strane drugog je pronađen.]]> + + X-Frame-Options koji se koristi za kontrolu da li neko mjesto može biti IFRAMED od strane drugog nije pronađen.]]> + + X-Content-Type-Options koji se koristi za zaštitu od ranjivosti MIME sniffinga je pronađen.]]> + + X-Content-Type-Options koji se koristi za zaštitu od ranjivosti MIME sniffinga nije pronađen.]]> + + Strict-Transport-Security, takođe poznat kao HSTS-header, je pronađen.]]> + + Strict-Transport-Security nije pronađeno.]]> + + Strict-Transport-Security, takođe poznat kao HSTS-header, je pronađen. Ovo zaglavlje ne bi trebalo biti prisutno na lokalnom hostu.]]> + + + Strict-Transport-Security nije pronađeno. Ovo zaglavlje ne bi trebalo biti prisutno na lokalnom hostu.]]> + + X-XSS-Protection je pronađeno.]]> + + X-XSS-Protection nije pronađeno.]]> %0%.]]> Nisu pronađena zaglavlja koja otkrivaju informacije o tehnologiji web stranice. @@ -437,4 +458,4 @@ Pisanje fajlova Kreiranje medijskog foldera - \ No newline at end of file + diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/cs.xml b/src/Umbraco.Core/EmbeddedResources/Lang/cs.xml index 8efd2506a2..e2f37a9ec7 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/cs.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/cs.xml @@ -338,6 +338,14 @@ + X-Frame-Options, které určuje, zda může být obsah webu zobrazen na jiném webu pomocí IFRAME.]]> + X-Frame-Options, které určuje, zda může být obsah webu zobrazen na jiném webu pomocí IFRAME.]]> + X-Content-Type-Options použitá k ochraně před zranitelnostmi čichání MIME.]]> + X-Content-Type-Options použité k ochraně před zranitelnostmi čichání MIME nebyly nalezeny.]]> + Strict-Transport-Security, také známo jako HSTS-header, bylo nalezeno.]]> + Strict-Transport-Security nebylo nalezeno.]]> + X-XSS-Protection bylo nalezeno.]]> + X-XSS-Protection bylo nalezeno.]]> %0%.]]> Nebyly nalezeny žádné hlavičky odhalující informace o technologii webových stránek. Nastavení SMTP jsou správně nakonfigurována a služba funguje jak má. @@ -350,4 +358,4 @@ Obsah s ID: {0} v koši souvisí s původním nadřazeným obsahem s ID: {1} Média s ID: {0} v koši souvisí s původním nadřazeným médiem s ID: {1} - \ No newline at end of file + diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/cy.xml b/src/Umbraco.Core/EmbeddedResources/Lang/cy.xml index d0d67339bd..201f18059e 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/cy.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/cy.xml @@ -420,6 +420,20 @@ + X-Frame-Options sy'n cael ei ddefnyddio i reoli os mae safle'n gallu cael ei osod o fewn IFRAME gan safle arall wedi'i ganfod.]]> + X-Frame-Options sy'n cael ei ddefnyddio i reoli os mae safle'n gallu cael ei osod o fewn IFRAME gan safle arall wedi'i ganfod.]]> + X-Content-Type-Options sy'n cael ei ddefnyddio i amddiffyn yn erbyn gwendidau sniffio MIME wedi'i ganfod.]]> + X-Content-Type-Options sy'n cael ei ddefnyddio i amddiffyn yn erbyn gwendidau sniffio MIME wedi'i ganfod.]]> + Strict-Transport-Security, hefyd wedi'i adnabod fel HSTS-header, wedi'i ganfod.]]> + Strict-Transport-Security wedi'i ganfod.]]> + + Strict-Transport-Security, a elwir hefyd yn HSTS-header. Ni ddylai'r pennyn hwn fod yn bresennol ar localhost.]]> + + + Strict-Transport-Security. Ni ddylai'r pennyn hwn fod yn bresennol ar localhost.]]> + + X-XSS-Protection wedi'i ganfod.]]> + X-XSS-Protection wedi'i ganfod.]]> %0%.]]> Dim peniadau sy'n datgelu gwynodaeth am dechnoleg eich gwefan wedi'u canfod. Gosodiadau SMTP wedi ffurfweddu'n gywir ac mae'r gwasanaeth yn gweithio fel y disgwylir. @@ -453,4 +467,4 @@
  • ID safle dienw, fersiwn Umbraco, a phecynnau wedi'u gosod.
  • Nifer o: Nodau gwraidd, Nodau Cynnwys, Macros, Cyfryngau, Mathau o Ddogfen, Templedi, Ieithoedd, Parthau, Grŵp Defnyddwyr, Defnyddwyr, Aelodau, a Golygyddion Eiddo a ddefnyddir.
  • Gwybodaeth system: Webserver, gweinydd OS, fframwaith gweinydd, iaith gweinyddwr OS, a darparwr cronfa ddata.
  • Gosodiadau cyfluniad: Modd Modelsbuilder, os oes llwybr Umbraco arferol yn bodoli, amgylchedd ASP, ac os ydych chi yn y modd dadfygio.
Efallai y byddwn yn newid yr hyn a anfonwn ar y lefel Fanwl yn y dyfodol. Os felly, fe'i rhestrir uchod.
Drwy ddewis "Manwl" rydych yn cytuno i wybodaeth ddienw yn awr ac yn y dyfodol gael ei chasglu.
- \ No newline at end of file + diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/en.xml b/src/Umbraco.Core/EmbeddedResources/Lang/en.xml index aaccac8607..599232b13a 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/en.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/en.xml @@ -442,6 +442,29 @@ + + X-Frame-Options used to control whether a site can be IFRAMEd by another was found.]]> + + X-Frame-Options used to control whether a site can be IFRAMEd by another was not found.]]> + + X-Content-Type-Options used to protect against MIME sniffing vulnerabilities was found.]]> + + X-Content-Type-Options used to protect against MIME sniffing vulnerabilities was not found.]]> + + Strict-Transport-Security, also known as the HSTS-header, was found.]]> + + Strict-Transport-Security was not found.]]> + + Strict-Transport-Security, also known as the HSTS-header, was found. This header should not be present on localhost.]]> + + + Strict-Transport-Security was not found. This header should not be present on localhost.]]> + + + X-XSS-Protection was found. It is recommended not to add this header to your website.
+ You can read about this on the Mozilla website ]]>
+ + X-XSS-Protection was not found.]]> %0%.]]> No headers revealing information about the website technology were found. @@ -483,4 +506,4 @@ We might change what we send on the Detailed level in the future. If so, it will be listed above.
By choosing "Detailed" you agree to current and future anonymized information being collected.
]]> - \ No newline at end of file + diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml index 01482e3863..7517a43b12 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml @@ -430,6 +430,29 @@ --> %0%.]]> The appSetting 'Umbraco:CMS:WebRouting:UmbracoApplicationUrl' is not set. + + X-Frame-Options used to control whether a site can be IFRAMEd by another was found.]]> + + X-Frame-Options used to control whether a site can be IFRAMEd by another was not found.]]> + + X-Content-Type-Options used to protect against MIME sniffing vulnerabilities was found.]]> + + X-Content-Type-Options used to protect against MIME sniffing vulnerabilities was not found.]]> + + Strict-Transport-Security, also known as the HSTS-header, was found.]]> + + Strict-Transport-Security was not found.]]> + + Strict-Transport-Security, also known as the HSTS-header, was found. This header should not be present on localhost.]]> + + + Strict-Transport-Security was not found. This header should not be present on localhost.]]> + + + X-XSS-Protection was found. It is recommended not to add this header to your website.
+ You can read about this on the Mozilla website ]]>
+ + X-XSS-Protection was not found.]]> %0%.]]> No headers revealing information about the website technology were found. diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/es.xml b/src/Umbraco.Core/EmbeddedResources/Lang/es.xml index 7e82adb9c9..cfde5a04e7 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/es.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/es.xml @@ -285,6 +285,8 @@ + X-Frame-Options usado para controlar si un sitio puede ser IFRAMEd por otra fue encontrado.]]> + X-Frame-Options usado para controlar si un sitio puede ser IFRAMEd por otra no se ha encontrado.]]> %0%.]]> No se ha encontrado ninguna cabecera que revele información sobre la tecnología del sitio. Los valores SMTP están configurados correctamente y el servicio opera con normalidad. @@ -293,4 +295,4 @@

Los resultados de los Chequeos de Salud de Umbraco programados para ejecutarse el %0% a las %1% son:

%2%]]>
Status de los Chequeos de Salud de Umbraco: %0% - \ No newline at end of file + diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/fr.xml b/src/Umbraco.Core/EmbeddedResources/Lang/fr.xml index 2edf4bae04..e8f07079ef 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/fr.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/fr.xml @@ -400,6 +400,14 @@ + X-Frame-Options, utilisé pour contrôler si un site peut être intégré dans un autre via IFRAME, a été trouvé.]]> + X-Frame-Options , utilisé pour contrôler si un site peut être intégré dans un autre via IFRAME, n'a pas été trouvé.]]> + X-Content-Type-Options utilisé pour la protection contre les vulnérabilités de MIME sniffing a été trouvé.]]> + X-Content-Type-Options utilisé pour la protection contre les vulnérabilités de MIME sniffing n'a pas été trouvé.]]> + Strict-Transport-Security, aussi connu sous le nom de HSTS-header, a été trouvé.]]> + Strict-Transport-Security, aussi connu sous le nom de HSTS-header, n'a pas été trouvé.]]> + X-XSS-Protection a été trouvé.]]> + X-XSS-Protection n'a pas été trouvé.]]> %0%.]]> Aucun header révélant des informations à propos de la technologie du site web n'a été trouvé. La configuration SMTP est correcte et le service fonctionne comme prévu. @@ -412,4 +420,4 @@ Suppression du contenu avec l'Id : {0} lié au contenu parent original avec l'Id : {1} Suppression du media avec l'Id : {0} lié à l'élément media parent original avec l'Id : {1} - \ No newline at end of file + diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/hr.xml b/src/Umbraco.Core/EmbeddedResources/Lang/hr.xml index eb64032a66..8ac145f3ce 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/hr.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/hr.xml @@ -407,6 +407,27 @@ + + X-Frame-Options koji se koristi za kontrolu da li neko mjesto može biti IFRAMED od strane drugog je pronađen.]]> + + X-Frame-Options koji se koristi za kontrolu da li neko mjesto može biti IFRAMED od strane drugog nije pronađen.]]> + + X-Content-Type-Options koji se koristi za zaštitu od ranjivosti MIME sniffinga je pronađen.]]> + + X-Content-Type-Options koji se koristi za zaštitu od ranjivosti MIME sniffinga nije pronađen.]]> + + Strict-Transport-Security, također poznat kao HSTS-header, je pronađen.]]> + + Strict-Transport-Security nije pronađeno.]]> + + Strict-Transport-Security, također poznat kao HSTS-header, je pronađen. Ovo zaglavlje ne bi trebalo biti prisutno na lokalnom hostu.]]> + + + Strict-Transport-Security nije pronađeno. Ovo zaglavlje ne bi trebalo biti prisutno na lokalnom hostu.]]> + + X-XSS-Protection je pronađeno.]]> + + X-XSS-Protection nije pronađeno.]]> %0%.]]> Nisu pronađena zaglavlja koja otkrivaju informacije o tehnologiji web stranice. @@ -450,4 +471,4 @@
Odabirom "Detaljno" pristajete na prikupljanje trenutnih i budućih anonimiziranih informacija. ]]>
- \ No newline at end of file + diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/it.xml b/src/Umbraco.Core/EmbeddedResources/Lang/it.xml index 7fee004155..33cfd41534 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/it.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/it.xml @@ -397,6 +397,43 @@ + + X-Frame-Options usato per controllare se un sito può essere inserito in un IFRAME da un altro è stato trovato.]]> + + X-Frame-Options usato per controllare se un sito può essere inserito in un IFRAME da un altro non è stato trovato.]]> + Imposta l'header nella configurazione + Aggiunge un valore alla sezione httpProtocol/customHeaders del + file web.config per prevenire che un sito possa essere inserito in un IFRAME da altri siti web. + + + + Non posso aggiornare il file web.config. Errore: %0% + + X-Content-Type-Options usato per proteggere dalle vulnerabilità per MIME sniffing è stato trovato.]]> + + X-Content-Type-Options usato per proteggere dalle vulnerabilità per MIME sniffing è stato trovato.]]> + + + + + + Strict-Transport-Security, conosciuto anche come l'header HSTS, è stato trovato.]]> + + Strict-Transport-Security non è stato trovato.]]> + Aggiunge l'header 'Strict-Transport-Security' con il valore + 'max-age=10886400' alla sezione httpProtocol/customHeaders del file web.config. Usa questa correzione solo se + avrai i tuoi domini in esecuzione con https per le prossime 18 settimane (minimo). + + + + X-XSS-Protection è stato trovato.]]> + + X-XSS-Protection non è stato trovato.]]> + Aggiunge l'header 'X-XSS-Protection' con il valore '1; + mode=block' alla sezione httpProtocol/customHeaders del file web.config. + + + %0%.]]> Non sono stati trovati header che rivelano informazioni riguardo alla tecnologia utilizzata per il sito. @@ -417,4 +454,4 @@ Media cestinato con Id: {0} relativo al media principale originale con Id: {1} - \ No newline at end of file + diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/nl.xml b/src/Umbraco.Core/EmbeddedResources/Lang/nl.xml index c067ea7e2e..2337343c2c 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/nl.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/nl.xml @@ -279,6 +279,22 @@ + + X-Frame-Options header of meta-tag om IFRAMEing door andere websites te voorkomen is aanwezig!]]> + + X-Frame-Options header of meta-tag om IFRAMEing door andere websites te voorkomen is NIET aanwezig.]]> + + X-Content-Type-Options die beveiligt tegen MIME sniffing kwetsbaarheden is gevonden.]]> + + X-Content-Type-Options die beveiligt tegen MIME sniffing kwetsbaarheden is niet gevonden.]]> + + Strict-Transport-Security header, ook bekend als de HSTS-header, is gevonden.]]> + + Strict-Transport-Securityheader is niet gevonden.]]> + + X-XSS-Protection is gevonden.]]> + + X-XSS-Protection is niet gevonden.]]> %0%.]]> Er zijn geen headers gevonden die informatie vrijgeven over de gebruikte website technologie! @@ -294,4 +310,4 @@ Content verwijderd met id : {0} gerelateerd aan aan bovenliggend item met Id: {1} Media verwijderd met id: {0} gerelateerd aan aan bovenliggend item met Id: {1} - \ No newline at end of file + diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/pl.xml b/src/Umbraco.Core/EmbeddedResources/Lang/pl.xml index ffce42770c..97b599df37 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/pl.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/pl.xml @@ -148,10 +148,12 @@ + X-Frame-Options używany do kontrolowania czy strona może być IFRAME'owana przez inną został znaleziony.]]> + X-Frame-Options używany do kontrolowania czy strona może być IFRAME'owana przez inną nie został znaleziony.]]> %0%.]]> Nie znaleziono żadnych nagłówków, ujawniających informacji o technologii strony. Ustawienia SMTP są skonfigurowane poprawnie i serwis działa według oczekiwań. %0%.]]> %0%.]]> - \ No newline at end of file + diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/ru.xml b/src/Umbraco.Core/EmbeddedResources/Lang/ru.xml index 8b091b4017..7271ef2c6d 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/ru.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/ru.xml @@ -91,6 +91,14 @@ + X-Frame-Options, использующийся для управления возможностью помещать сайт в IFRAME на другом сайте.]]> + X-Frame-Options, использующийся для управления возможностью помещать сайт в IFRAME на другом сайте, не обнаружен.]]> + X-Content-Type-Options, использующиеся для защиты от MIME-уязвимостей, обнаружены.]]> + X-Content-Type-Options, использующиеся для защиты от MIME-уязвимостей, не найдены.]]> + Strict-Transport-Security, известный также как HSTS-header, обнаружен.]]> + Strict-Transport-Security не найден.]]> + X-XSS-Protection обнаружен.]]> + X-XSS-Protection не найден.]]> %0%.]]> Заголовки, позволяющие выяснить базовую технологию сайта, не обнаружены. Параметры отправки электронной почты (SMTP) настроены корректно, сервис работает как ожидается. @@ -379,4 +387,4 @@ неверный формат email-адреса - \ No newline at end of file + diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/tr.xml b/src/Umbraco.Core/EmbeddedResources/Lang/tr.xml index af1fe7db2f..90803e72a7 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/tr.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/tr.xml @@ -389,6 +389,14 @@ + ​​ X-Frame-Options .]]> + X-Frame-Options bulunamadı.]]> + ​​ X-Content-Type-Options bulundu.]]> + ​​ X-Content-Type-Options bulunamadı.]]> + ​​ Strict-Transport-Security başlığı bulundu.]]> + ​​ Strict-Transport-Security başlığı bulunamadı.]]> + ​​ X-XSS-Protection başlığı bulundu.]]> + ​​ X-XSS-Protection başlığı bulunamadı.]]> ​​%0%.]]> ​​Web sitesi teknolojisi hakkında bilgi veren hiçbir başlık bulunamadı. SMTP ayarları doğru yapılandırıldı ve hizmet beklendiği gibi çalışıyor. @@ -401,4 +409,4 @@ Çöp kutusuna gönderilmiş içerik: {0} Şu kimliğe sahip orijinal ana içerikle ilgili: {1} Şu kimliğe sahip çöp kutusuna gönderilen medya: {0} Şu kimliğe sahip orijinal ana medya öğesiyle ilgili: {1} - \ No newline at end of file + diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/ua.xml b/src/Umbraco.Core/EmbeddedResources/Lang/ua.xml index 0ca656292f..047ea953de 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/ua.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/ua.xml @@ -91,6 +91,14 @@ + X-Frame-Options, що використовується для управління можливістю розміщувати сайт у IFRAME на іншому сайті, знайдено.]]> + X-Frame-Options, що використовується для управління можливістю розміщувати сайт у IFRAME на іншому сайті, не знайдено.]]> + X-Content-Type-Options, що використовується для захисту від MIME-уязвимостей, знайдено.]]> + X-Content-Type-Options, що використовується для захисту від MIME-уязвимостей, не знайдено.]]> + Strict-Transport-Security, відомий також як HSTS-header, знайдено.]]> + Strict-Transport-Security не знайдено.]]> + X-XSS-Protection знайдено.]]> + X-XSS-Protection не знайдено.]]> %0%.]]> Заголовки, які дають змогу з'ясувати базову технологію сайту, не виявлено. Параметри надсилання електронної пошти (SMTP) налаштовані коректно, сервіс працює як очікується. @@ -379,4 +387,4 @@ Невірний формат email-адреси - \ No newline at end of file + diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/zh_tw.xml b/src/Umbraco.Core/EmbeddedResources/Lang/zh_tw.xml index 00667df60c..4c5e7c9dbc 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/zh_tw.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/zh_tw.xml @@ -137,10 +137,12 @@ + X-Frame-Options 設定能控制網站是否可以被其他人IFRAMEd已找到。]]> + X-Frame-Options 設定能控制網站是否可以被其他人IFRAMEd沒有找到。]]> %0%。]]> 在標頭中沒有找到揭露網站技術的資訊。 SMTP設定正確,而且服務正常運作。 %0%。]]> %0%。]]> - \ No newline at end of file + diff --git a/src/Umbraco.Core/Extensions/ClaimsIdentityExtensions.cs b/src/Umbraco.Core/Extensions/ClaimsIdentityExtensions.cs index 38f9bf15ff..73157767b2 100644 --- a/src/Umbraco.Core/Extensions/ClaimsIdentityExtensions.cs +++ b/src/Umbraco.Core/Extensions/ClaimsIdentityExtensions.cs @@ -34,8 +34,6 @@ public static class ClaimsIdentityExtensions ClaimTypes.Name, // username ClaimTypes.GivenName, - // Constants.Security.StartContentNodeIdClaimType, These seem to be able to be null... - // Constants.Security.StartMediaNodeIdClaimType, ClaimTypes.Locality, Constants.Security.SecurityStampClaimType, }; @@ -250,6 +248,7 @@ public static class ClaimsIdentityExtensions identity)); } + // NOTE: this can be removed when the obsolete claim type has been deleted if (identity.HasClaim(x => x.Type == Constants.Security.StartContentNodeIdClaimType) == false && startContentNodes != null) { @@ -265,6 +264,7 @@ public static class ClaimsIdentityExtensions } } + // NOTE: this can be removed when the obsolete claim type has been deleted if (identity.HasClaim(x => x.Type == Constants.Security.StartMediaNodeIdClaimType) == false && startMediaNodes != null) { @@ -304,6 +304,7 @@ public static class ClaimsIdentityExtensions } // Add each app as a separate claim + // NOTE: this can be removed when the obsolete claim type has been deleted if (identity.HasClaim(x => x.Type == Constants.Security.AllowedApplicationsClaimType) == false && allowedApps != null) { foreach (var application in allowedApps) @@ -343,6 +344,7 @@ public static class ClaimsIdentityExtensions /// /// /// Array of start content nodes + [Obsolete("Please use the UserExtensions class to access user start node info. Will be removed in V15.")] public static int[] GetStartContentNodes(this ClaimsIdentity identity) => identity.FindAll(x => x.Type == Constants.Security.StartContentNodeIdClaimType) .Select(node => int.TryParse(node.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i) @@ -355,6 +357,7 @@ public static class ClaimsIdentityExtensions /// /// /// Array of start media nodes + [Obsolete("Please use the UserExtensions class to access user start node info. Will be removed in V15.")] public static int[] GetStartMediaNodes(this ClaimsIdentity identity) => identity.FindAll(x => x.Type == Constants.Security.StartMediaNodeIdClaimType) .Select(node => int.TryParse(node.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i) @@ -367,6 +370,7 @@ public static class ClaimsIdentityExtensions /// /// /// + [Obsolete("Please use IUser.AllowedSections instead. Will be removed in V15.")] public static string[] GetAllowedApplications(this ClaimsIdentity identity) => identity .FindAll(x => x.Type == Constants.Security.AllowedApplicationsClaimType).Select(app => app.Value).ToArray(); diff --git a/src/Umbraco.Core/Models/UserExtensions.cs b/src/Umbraco.Core/Models/UserExtensions.cs index 6682e6e055..b3c115c41b 100644 --- a/src/Umbraco.Core/Models/UserExtensions.cs +++ b/src/Umbraco.Core/Models/UserExtensions.cs @@ -1,5 +1,3 @@ -using System.Net; -using System.Security.Cryptography; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Media; @@ -155,8 +153,8 @@ public static class UserExtensions public static int[]? CalculateContentStartNodeIds(this IUser user, IEntityService entityService, AppCaches appCaches) { - var cacheKey = CacheKeys.UserAllContentStartNodesPrefix + user.Key; - IAppPolicyCache runtimeCache = appCaches.IsolatedCaches.GetOrCreate(); + var cacheKey = user.UserCacheKey(CacheKeys.UserAllContentStartNodesPrefix); + IAppPolicyCache runtimeCache = GetUserCache(appCaches); var result = runtimeCache.GetCacheItem( cacheKey, () => @@ -189,8 +187,8 @@ public static class UserExtensions /// public static int[]? CalculateMediaStartNodeIds(this IUser user, IEntityService entityService, AppCaches appCaches) { - var cacheKey = CacheKeys.UserAllMediaStartNodesPrefix + user.Id; - IAppPolicyCache runtimeCache = appCaches.IsolatedCaches.GetOrCreate(); + var cacheKey = user.UserCacheKey(CacheKeys.UserAllMediaStartNodesPrefix); + IAppPolicyCache runtimeCache = GetUserCache(appCaches); var result = runtimeCache.GetCacheItem( cacheKey, () => @@ -214,8 +212,8 @@ public static class UserExtensions public static string[]? GetMediaStartNodePaths(this IUser user, IEntityService entityService, AppCaches appCaches) { - var cacheKey = CacheKeys.UserMediaStartNodePathsPrefix + user.Id; - IAppPolicyCache runtimeCache = appCaches.IsolatedCaches.GetOrCreate(); + var cacheKey = user.UserCacheKey(CacheKeys.UserMediaStartNodePathsPrefix); + IAppPolicyCache runtimeCache = GetUserCache(appCaches); var result = runtimeCache.GetCacheItem( cacheKey, () => @@ -232,8 +230,8 @@ public static class UserExtensions public static string[]? GetContentStartNodePaths(this IUser user, IEntityService entityService, AppCaches appCaches) { - var cacheKey = CacheKeys.UserContentStartNodePathsPrefix + user.Id; - IAppPolicyCache runtimeCache = appCaches.IsolatedCaches.GetOrCreate(); + var cacheKey = user.UserCacheKey(CacheKeys.UserContentStartNodePathsPrefix); + IAppPolicyCache runtimeCache = GetUserCache(appCaches); var result = runtimeCache.GetCacheItem( cacheKey, () => @@ -317,6 +315,12 @@ public static class UserExtensions return lsn.ToArray(); } + private static IAppPolicyCache GetUserCache(AppCaches appCaches) + => appCaches.IsolatedCaches.GetOrCreate(); + + private static string UserCacheKey(this IUser user, string cacheKey) + => $"{cacheKey}{user.Key}"; + private static bool StartsWithPath(string test, string path) => test.StartsWith(path) && test.Length > path.Length && test[path.Length] == ','; diff --git a/src/Umbraco.Core/Routing/IPublishedRouter.cs b/src/Umbraco.Core/Routing/IPublishedRouter.cs index 53a07ff325..a01e17c94d 100644 --- a/src/Umbraco.Core/Routing/IPublishedRouter.cs +++ b/src/Umbraco.Core/Routing/IPublishedRouter.cs @@ -47,4 +47,37 @@ public interface IPublishedRouter /// /// Task UpdateRequestAsync(IPublishedRequest request, IPublishedContent? publishedContent); + + /// + /// Finds the site root (if any) matching the http request, and updates the PublishedRequest and VariationContext accordingly. + /// + /// + /// This method is used for VirtualPage routing. + /// + /// + /// In this case we do not want to run the entire routing pipeline since ContentFinders are not needed here. + /// However, we do want to set the culture on VariationContext and PublishedRequest to the values specified by the domains. + /// + /// + /// + /// The request to update the culture on domain on + /// True if a domain was found otherwise false. + bool RouteDomain(IPublishedRequestBuilder request) => false; + + /// + /// Finds the site root (if any) matching the http request, and updates the VariationContext accordingly. + /// + /// + /// + /// This is used for VirtualPage routing. + /// + /// + /// This is required to set the culture on VariationContext to the values specified by the domains, before the FindContent method is called. + /// In order to allow the FindContent implementer to correctly find content based off the culture. Before the PublishedRequest is built. + /// + /// + /// The URI to resolve the domain from. + /// True if a domain was found, otherwise false. + bool UpdateVariationContext(Uri uri) => false; + } diff --git a/src/Umbraco.Core/Routing/PublishedRouter.cs b/src/Umbraco.Core/Routing/PublishedRouter.cs index f04fd04ca2..df1d459327 100644 --- a/src/Umbraco.Core/Routing/PublishedRouter.cs +++ b/src/Umbraco.Core/Routing/PublishedRouter.cs @@ -108,7 +108,7 @@ public class PublishedRouter : IPublishedRouter // find domain if (builder.Domain == null) { - FindDomain(builder); + FindAndSetDomain(builder); } await RouteRequestInternalAsync(builder); @@ -185,7 +185,7 @@ public class PublishedRouter : IPublishedRouter private async Task TryRouteRequest(IPublishedRequestBuilder request) { - FindDomain(request); + FindAndSetDomain(request); if (request.IsRedirect()) { @@ -270,18 +270,31 @@ public class PublishedRouter : IPublishedRouter // to find out the appropriate template } - /// - /// Finds the site root (if any) matching the http request, and updates the PublishedRequest accordingly. - /// - /// A value indicating whether a domain was found. - internal bool FindDomain(IPublishedRequestBuilder request) + /// + public bool RouteDomain(IPublishedRequestBuilder request) + { + var found = FindAndSetDomain(request); + HandleWildcardDomains(request); + SetVariationContext(request.Culture); + return found; + } + + /// + public bool UpdateVariationContext(Uri uri) + { + DomainAndUri? domain = FindDomain(uri, out _); + SetVariationContext(domain?.Culture); + return domain?.Culture is not null; + } + + private DomainAndUri? FindDomain(Uri uri, out string? defaultCulture) { const string tracePrefix = "FindDomain: "; // note - we are not handling schemes nor ports here. if (_logger.IsEnabled(LogLevel.Debug)) { - _logger.LogDebug("{TracePrefix}Uri={RequestUri}", tracePrefix, request.Uri); + _logger.LogDebug("{TracePrefix}Uri={RequestUri}", tracePrefix, uri); } IUmbracoContext umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); @@ -315,10 +328,20 @@ public class PublishedRouter : IPublishedRouter domains = domains?.Where(IsPublishedContentDomain).ToList(); - var defaultCulture = domainsCache?.DefaultCulture; + defaultCulture = domainsCache?.DefaultCulture; + return DomainUtilities.SelectDomain(domains, uri, defaultCulture: defaultCulture); + } + + /// + /// Finds the site root (if any) matching the http request, and updates the PublishedRequest accordingly. + /// + /// A value indicating whether a domain was found. + internal bool FindAndSetDomain(IPublishedRequestBuilder request) + { + const string tracePrefix = "FindDomain: "; // try to find a domain matching the current request - DomainAndUri? domainAndUri = DomainUtilities.SelectDomain(domains, request.Uri, defaultCulture: defaultCulture); + DomainAndUri? domainAndUri = FindDomain(request.Uri, out var defaultCulture); // handle domain - always has a contentId and a culture if (domainAndUri != null) diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index 765ce68f4c..dd61585203 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -1140,6 +1140,8 @@ public class ContentService : RepositoryService, IContentService throw new ArgumentException("Cultures cannot be null or whitespace", nameof(cultures)); } + EventMessages evtMsgs = EventMessagesFactory.Get(); + // we need to guard against unsaved changes before proceeding; the content will be saved, but we're not firing any saved notifications if (HasUnsavedChanges(content)) { @@ -1186,8 +1188,6 @@ public class ContentService : RepositoryService, IContentService var allLangs = _languageRepository.GetMany().ToList(); - EventMessages evtMsgs = EventMessagesFactory.Get(); - // this will create the correct culture impact even if culture is * or null IEnumerable impacts = cultures.Select(culture => _cultureImpactFactory.Create(culture, IsDefaultCulture(allLangs, culture), content)); @@ -1199,7 +1199,15 @@ public class ContentService : RepositoryService, IContentService content.PublishCulture(impact); } - PublishResult result = CommitDocumentChangesInternal(scope, content, evtMsgs, allLangs, userId, out _); + // Change state to publishing + content.PublishedState = PublishedState.Publishing; + var publishingNotification = new ContentPublishingNotification(content, evtMsgs); + if (scope.Notifications.PublishCancelable(publishingNotification)) + { + return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content); + } + + PublishResult result = CommitDocumentChangesInternal(scope, content, evtMsgs, allLangs, publishingNotification.State, userId); scope.Complete(); return result; } @@ -1254,6 +1262,12 @@ public class ContentService : RepositoryService, IContentService var allLangs = _languageRepository.GetMany().ToList(); + var savingNotification = new ContentSavingNotification(content, evtMsgs); + if (scope.Notifications.PublishCancelable(savingNotification)) + { + return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content); + } + // all cultures = unpublish whole if (culture == "*" || (!content.ContentType.VariesByCulture() && culture == null)) { @@ -1262,7 +1276,7 @@ public class ContentService : RepositoryService, IContentService // We are however unpublishing all cultures, so we will set this to unpublishing. content.UnpublishCulture(culture); content.PublishedState = PublishedState.Unpublishing; - PublishResult result = CommitDocumentChangesInternal(scope, content, evtMsgs, allLangs, userId, out _); + PublishResult result = CommitDocumentChangesInternal(scope, content, evtMsgs, allLangs, savingNotification.State, userId); scope.Complete(); return result; } @@ -1276,7 +1290,7 @@ public class ContentService : RepositoryService, IContentService var removed = content.UnpublishCulture(culture); // Save and publish any changes - PublishResult result = CommitDocumentChangesInternal(scope, content, evtMsgs, allLangs, userId, out _); + PublishResult result = CommitDocumentChangesInternal(scope, content, evtMsgs, allLangs, savingNotification.State, userId); scope.Complete(); @@ -1327,9 +1341,15 @@ public class ContentService : RepositoryService, IContentService scope.WriteLock(Constants.Locks.ContentTree); + var savingNotification = new ContentSavingNotification(content, evtMsgs); + if (scope.Notifications.PublishCancelable(savingNotification)) + { + return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content); + } + var allLangs = _languageRepository.GetMany().ToList(); - PublishResult result = CommitDocumentChangesInternal(scope, content, evtMsgs, allLangs, userId, out _); + PublishResult result = CommitDocumentChangesInternal(scope, content, evtMsgs, allLangs, savingNotification.State, userId); scope.Complete(); return result; } @@ -1359,8 +1379,8 @@ public class ContentService : RepositoryService, IContentService IContent content, EventMessages eventMessages, IReadOnlyCollection allLangs, + IDictionary? notificationState, int userId, - out IDictionary? initialNotificationState, bool branchOne = false, bool branchRoot = false) { @@ -1422,7 +1442,6 @@ public class ContentService : RepositoryService, IContentService _documentRepository.Save(c); } - initialNotificationState = null; if (publishing) { // Determine cultures publishing/unpublishing which will be based on previous calls to content.PublishCulture and ClearPublishInfo @@ -1440,9 +1459,18 @@ public class ContentService : RepositoryService, IContentService culturesUnpublishing, eventMessages, allLangs, - out initialNotificationState); + notificationState); + if (publishResult.Success) { + // raise Publishing notification + if (scope.Notifications.PublishCancelable( + new ContentPublishingNotification(content, eventMessages).WithState(notificationState))) + { + _logger.LogInformation("Document {ContentName} (id={ContentId}) cannot be published: {Reason}", content.Name, content.Id, "publishing was cancelled"); + return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, eventMessages, content); + } + // note: StrategyPublish flips the PublishedState to Publishing! publishResult = StrategyPublish(content, culturesPublishing, culturesUnpublishing, eventMessages); @@ -1503,7 +1531,7 @@ public class ContentService : RepositoryService, IContentService // handling events, business rules, etc // note: StrategyUnpublish flips the PublishedState to Unpublishing! // note: This unpublishes the entire document (not different variants) - unpublishResult = StrategyCanUnpublish(scope, content, eventMessages, out initialNotificationState); + unpublishResult = StrategyCanUnpublish(scope, content, eventMessages, notificationState); if (unpublishResult.Success) { unpublishResult = StrategyUnpublish(content, eventMessages); @@ -1539,7 +1567,7 @@ public class ContentService : RepositoryService, IContentService { // events and audit scope.Notifications.Publish( - new ContentUnpublishedNotification(content, eventMessages).WithState(initialNotificationState)); + new ContentUnpublishedNotification(content, eventMessages).WithState(notificationState)); scope.Notifications.Publish(new ContentTreeChangeNotification(content, TreeChangeTypes.RefreshBranch, eventMessages)); if (culturesUnpublishing != null) @@ -1601,7 +1629,7 @@ public class ContentService : RepositoryService, IContentService scope.Notifications.Publish( new ContentTreeChangeNotification(content, changeType, eventMessages)); scope.Notifications.Publish( - new ContentPublishedNotification(content, eventMessages).WithState(initialNotificationState)); + new ContentPublishedNotification(content, eventMessages).WithState(notificationState)); } // it was not published and now is... descendants that were 'published' (but @@ -1611,7 +1639,7 @@ public class ContentService : RepositoryService, IContentService { IContent[] descendants = GetPublishedDescendantsLocked(content).ToArray(); scope.Notifications.Publish( - new ContentPublishedNotification(descendants, eventMessages).WithState(initialNotificationState)); + new ContentPublishedNotification(descendants, eventMessages).WithState(notificationState)); } switch (publishResult.Result) @@ -1711,6 +1739,13 @@ public class ContentService : RepositoryService, IContentService continue; // shouldn't happen but no point in processing this document if there's nothing there } + var savingNotification = new ContentSavingNotification(d, evtMsgs); + if (scope.Notifications.PublishCancelable(savingNotification)) + { + results.Add(new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, d)); + continue; + } + foreach (var c in pendingCultures) { // Clear this schedule for this culture @@ -1721,7 +1756,7 @@ public class ContentService : RepositoryService, IContentService } _documentRepository.PersistContentSchedule(d, contentSchedule); - PublishResult result = CommitDocumentChangesInternal(scope, d, evtMsgs, allLangs.Value, d.WriterId, out _); + PublishResult result = CommitDocumentChangesInternal(scope, d, evtMsgs, allLangs.Value, savingNotification.State, d.WriterId); if (result.Success == false) { _logger.LogError(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result); @@ -1775,6 +1810,13 @@ public class ContentService : RepositoryService, IContentService { continue; // shouldn't happen but no point in processing this document if there's nothing there } + var savingNotification = new ContentSavingNotification(d, evtMsgs); + if (scope.Notifications.PublishCancelable(savingNotification)) + { + results.Add(new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, d)); + continue; + } + var publishing = true; foreach (var culture in pendingCultures) @@ -1820,7 +1862,7 @@ public class ContentService : RepositoryService, IContentService else { _documentRepository.PersistContentSchedule(d, contentSchedule); - result = CommitDocumentChangesInternal(scope, d, evtMsgs, allLangs.Value, d.WriterId, out _); + result = CommitDocumentChangesInternal(scope, d, evtMsgs, allLangs.Value, savingNotification.State, d.WriterId); } if (result.Success == false) @@ -2162,6 +2204,12 @@ public class ContentService : RepositoryService, IContentService return new PublishResult(PublishResultType.SuccessPublishAlready, evtMsgs, document); } + var savingNotification = new ContentSavingNotification(document, evtMsgs); + if (scope.Notifications.PublishCancelable(savingNotification)) + { + return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, document); + } + // publish & check if values are valid if (!publishCultures(document, culturesToPublish, allLangs)) { @@ -2169,7 +2217,7 @@ public class ContentService : RepositoryService, IContentService return new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, document); } - PublishResult result = CommitDocumentChangesInternal(scope, document, evtMsgs, allLangs, userId, out initialNotificationState, true, isRoot); + PublishResult result = CommitDocumentChangesInternal(scope, document, evtMsgs, allLangs, savingNotification.State, userId, true, isRoot); if (result.Success) { publishedDocuments.Add(document); @@ -3018,19 +3066,8 @@ public class ContentService : RepositoryService, IContentService IReadOnlyCollection? culturesUnpublishing, EventMessages evtMsgs, IReadOnlyCollection allLangs, - out IDictionary? initialNotificationState) + IDictionary? notificationState) { - // raise Publishing notification - var notification = new ContentPublishingNotification(content, evtMsgs); - var notificationResult = scope.Notifications.PublishCancelable(notification); - initialNotificationState = notification.State; - - if (notificationResult) - { - _logger.LogInformation("Document {ContentName} (id={ContentId}) cannot be published: {Reason}", content.Name, content.Id, "publishing was cancelled"); - return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content); - } - var variesByCulture = content.ContentType.VariesByCulture(); // If it's null it's invariant @@ -3268,12 +3305,11 @@ public class ContentService : RepositoryService, IContentService ICoreScope scope, IContent content, EventMessages evtMsgs, - out IDictionary? initialNotificationState) + IDictionary? notificationState) { // raise Unpublishing notification - var notification = new ContentUnpublishingNotification(content, evtMsgs); + var notification = new ContentUnpublishingNotification(content, evtMsgs).WithState(notificationState); var notificationResult = scope.Notifications.PublishCancelable(notification); - initialNotificationState = notification.State; if (notificationResult) { diff --git a/src/Umbraco.Core/Services/UserGroupService.cs b/src/Umbraco.Core/Services/UserGroupService.cs index 56c2222b3c..fbe6a52386 100644 --- a/src/Umbraco.Core/Services/UserGroupService.cs +++ b/src/Umbraco.Core/Services/UserGroupService.cs @@ -314,8 +314,7 @@ internal sealed class UserGroupService : RepositoryService, IUserGroupService // Since this is a brand new creation we don't have to be worried about what users were added and removed // simply put all members that are requested to be in the group will be "added" var userGroupWithUsers = new UserGroupWithUsers(userGroup, usersToAdd, Array.Empty()); - var savingUserGroupWithUsersNotification = - new UserGroupWithUsersSavingNotification(userGroupWithUsers, eventMessages); + var savingUserGroupWithUsersNotification = new UserGroupWithUsersSavingNotification(userGroupWithUsers, eventMessages); if (await scope.Notifications.PublishCancelableAsync(savingUserGroupWithUsersNotification)) { scope.Complete(); @@ -324,6 +323,11 @@ internal sealed class UserGroupService : RepositoryService, IUserGroupService _userGroupRepository.AddOrUpdateGroupWithUsers(userGroup, usersToAdd.Select(x => x.Id).ToArray()); + scope.Notifications.Publish( + new UserGroupSavedNotification(userGroup, eventMessages).WithStateFrom(savingNotification)); + scope.Notifications.Publish( + new UserGroupWithUsersSavedNotification(userGroupWithUsers, eventMessages).WithStateFrom(savingUserGroupWithUsersNotification)); + scope.Complete(); return Attempt.SucceedWithStatus(UserGroupOperationStatus.Success, userGroup); } @@ -385,9 +389,23 @@ internal sealed class UserGroupService : RepositoryService, IUserGroupService return Attempt.FailWithStatus(UserGroupOperationStatus.CancelledByNotification, userGroup); } + // We need to fire this notification - both for backwards compat, and to ensure caches across all servers. + // Since we are not adding or removing any users, we'll just fire the notification with empty collections + // for "added" and "removed" users. + var userGroupWithUsers = new UserGroupWithUsers(userGroup, [], []); + var savingUserGroupWithUsersNotification = new UserGroupWithUsersSavingNotification(userGroupWithUsers, eventMessages); + if (await scope.Notifications.PublishCancelableAsync(savingUserGroupWithUsersNotification)) + { + scope.Complete(); + return Attempt.FailWithStatus(UserGroupOperationStatus.CancelledByNotification, userGroup); + } + _userGroupRepository.Save(userGroup); + scope.Notifications.Publish( new UserGroupSavedNotification(userGroup, eventMessages).WithStateFrom(savingNotification)); + scope.Notifications.Publish( + new UserGroupWithUsersSavedNotification(userGroupWithUsers, eventMessages).WithStateFrom(savingUserGroupWithUsersNotification)); scope.Complete(); return Attempt.SucceedWithStatus(UserGroupOperationStatus.Success, userGroup); diff --git a/src/Umbraco.Core/Services/UserService.cs b/src/Umbraco.Core/Services/UserService.cs index cf58ec20ef..dd25791d13 100644 --- a/src/Umbraco.Core/Services/UserService.cs +++ b/src/Umbraco.Core/Services/UserService.cs @@ -1,14 +1,17 @@ using System.ComponentModel.DataAnnotations; using System.Globalization; using System.Linq.Expressions; +using Microsoft.Extensions.DependencyInjection; using System.Security.Claims; using System.Security.Cryptography; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Editors; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Exceptions; using Umbraco.Cms.Core.IO; @@ -2446,6 +2449,7 @@ internal class UserService : RepositoryService, IUserService EntityPermission[] groupPermissions = GetPermissionsForPath(user.Groups.ToArray(), nodeIds, true).ToArray(); return CalculatePermissionsForPathForUser(groupPermissions, nodeIds); + } /// diff --git a/src/Umbraco.Infrastructure/Cache/PropertyEditors/BlockEditorElementTypeCache.cs b/src/Umbraco.Infrastructure/Cache/PropertyEditors/BlockEditorElementTypeCache.cs new file mode 100644 index 0000000000..5cbf0e6dc2 --- /dev/null +++ b/src/Umbraco.Infrastructure/Cache/PropertyEditors/BlockEditorElementTypeCache.cs @@ -0,0 +1,32 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.Cache.PropertyEditors; + +internal sealed class BlockEditorElementTypeCache : IBlockEditorElementTypeCache +{ + private readonly IContentTypeService _contentTypeService; + private readonly AppCaches _appCaches; + + public BlockEditorElementTypeCache(IContentTypeService contentTypeService, AppCaches appCaches) + { + _contentTypeService = contentTypeService; + _appCaches = appCaches; + } + + public IEnumerable GetAll(IEnumerable keys) + { + // TODO: make this less dumb; don't fetch all elements, only fetch the items that aren't yet in the cache and amend the cache as more elements are loaded + + const string cacheKey = $"{nameof(BlockEditorElementTypeCache)}_ElementTypes"; + IEnumerable? cachedElements = _appCaches.RequestCache.GetCacheItem>(cacheKey); + if (cachedElements is null) + { + cachedElements = _contentTypeService.GetAllElementTypes(); + _appCaches.RequestCache.Set(cacheKey, cachedElements); + } + + return cachedElements.Where(elementType => keys.Contains(elementType.Key)); + } +} diff --git a/src/Umbraco.Infrastructure/Cache/PropertyEditors/IBlockEditorElementTypeCache.cs b/src/Umbraco.Infrastructure/Cache/PropertyEditors/IBlockEditorElementTypeCache.cs new file mode 100644 index 0000000000..5ab1dc49af --- /dev/null +++ b/src/Umbraco.Infrastructure/Cache/PropertyEditors/IBlockEditorElementTypeCache.cs @@ -0,0 +1,8 @@ +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Core.Cache.PropertyEditors; + +public interface IBlockEditorElementTypeCache +{ + IEnumerable GetAll(IEnumerable keys); +} diff --git a/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextElementParser.cs b/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextElementParser.cs index eeb279e1b7..eed3b848eb 100644 --- a/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextElementParser.cs +++ b/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextElementParser.cs @@ -22,28 +22,13 @@ internal sealed class ApiRichTextElementParser : ApiRichTextParserBase, IApiRich private const string TextNodeName = "#text"; private const string CommentNodeName = "#comment"; - [Obsolete($"Please use the constructor that accepts {nameof(IApiElementBuilder)}. Will be removed in V15.")] public ApiRichTextElementParser( IApiContentRouteBuilder apiContentRouteBuilder, - IPublishedUrlProvider publishedUrlProvider, - IPublishedSnapshotAccessor publishedSnapshotAccessor, - ILogger logger) - : this( - apiContentRouteBuilder, - publishedUrlProvider, - publishedSnapshotAccessor, - StaticServiceProvider.Instance.GetRequiredService(), - logger) - { - } - - public ApiRichTextElementParser( - IApiContentRouteBuilder apiContentRouteBuilder, - IPublishedUrlProvider publishedUrlProvider, + IApiMediaUrlProvider mediaUrlProvider, IPublishedSnapshotAccessor publishedSnapshotAccessor, IApiElementBuilder apiElementBuilder, ILogger logger) - : base(apiContentRouteBuilder, publishedUrlProvider) + : base(apiContentRouteBuilder, mediaUrlProvider) { _publishedSnapshotAccessor = publishedSnapshotAccessor; _apiElementBuilder = apiElementBuilder; diff --git a/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextMarkupParser.cs b/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextMarkupParser.cs index f7eeba0f18..42c8829868 100644 --- a/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextMarkupParser.cs +++ b/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextMarkupParser.cs @@ -3,7 +3,6 @@ using Microsoft.Extensions.Logging; using Umbraco.Cms.Core; using Umbraco.Cms.Core.DeliveryApi; using Umbraco.Cms.Core.PublishedCache; -using Umbraco.Cms.Core.Routing; using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.DeliveryApi; @@ -15,10 +14,10 @@ internal sealed class ApiRichTextMarkupParser : ApiRichTextParserBase, IApiRichT public ApiRichTextMarkupParser( IApiContentRouteBuilder apiContentRouteBuilder, - IPublishedUrlProvider publishedUrlProvider, + IApiMediaUrlProvider mediaUrlProvider, IPublishedSnapshotAccessor publishedSnapshotAccessor, ILogger logger) - : base(apiContentRouteBuilder, publishedUrlProvider) + : base(apiContentRouteBuilder, mediaUrlProvider) { _publishedSnapshotAccessor = publishedSnapshotAccessor; _logger = logger; diff --git a/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextParserBase.cs b/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextParserBase.cs index 0509105b05..7723fc835c 100644 --- a/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextParserBase.cs +++ b/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextParserBase.cs @@ -4,19 +4,18 @@ using Umbraco.Cms.Core.DeliveryApi; using Umbraco.Cms.Core.Models.DeliveryApi; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; -using Umbraco.Cms.Core.Routing; namespace Umbraco.Cms.Infrastructure.DeliveryApi; internal abstract partial class ApiRichTextParserBase { private readonly IApiContentRouteBuilder _apiContentRouteBuilder; - private readonly IPublishedUrlProvider _publishedUrlProvider; + private readonly IApiMediaUrlProvider _apiMediaUrlProvider; - protected ApiRichTextParserBase(IApiContentRouteBuilder apiContentRouteBuilder, IPublishedUrlProvider publishedUrlProvider) + protected ApiRichTextParserBase(IApiContentRouteBuilder apiContentRouteBuilder, IApiMediaUrlProvider apiMediaUrlProvider) { _apiContentRouteBuilder = apiContentRouteBuilder; - _publishedUrlProvider = publishedUrlProvider; + _apiMediaUrlProvider = apiMediaUrlProvider; } protected void ReplaceLocalLinks(IPublishedSnapshot publishedSnapshot, string href, Action handleContentRoute, Action handleMediaUrl, Action handleInvalidLink) @@ -52,7 +51,7 @@ internal abstract partial class ApiRichTextParserBase if (media != null) { handled = true; - handleMediaUrl(_publishedUrlProvider.GetMediaUrl(media, UrlMode.Absolute)); + handleMediaUrl(_apiMediaUrlProvider.GetUrl(media)); } break; @@ -77,7 +76,7 @@ internal abstract partial class ApiRichTextParserBase return; } - handleMediaUrl(_publishedUrlProvider.GetMediaUrl(media, UrlMode.Absolute)); + handleMediaUrl(_apiMediaUrlProvider.GetUrl(media)); } [GeneratedRegex("{localLink:(?umb:.+)}")] diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs index 47ff6f2489..e12c96557c 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Cache.PropertyEditors; using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.DeliveryApi; @@ -234,6 +235,8 @@ public static partial class UmbracoBuilderExtensions builder.Services.AddUnique, PasswordChanger>(); builder.Services.AddTransient(); + builder.Services.AddSingleton(); + return builder; } diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs index 009811bc7d..53f7495822 100644 --- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs @@ -1934,7 +1934,7 @@ internal class DatabaseDataCreator EditorUiAlias = "Umb.PropertyEditorUi.TinyMCE", DbType = "Ntext", Configuration = - "{\"toolbar\":[\"ace\",\"styles\",\"bold\",\"italic\",\"alignleft\",\"aligncenter\",\"alignright\",\"bullist\",\"numlist\",\"outdent\",\"indent\",\"link\",\"umbmediapicker\",\"umbembeddialog\"],\"stylesheets\":[],\"maxImageSize\":500,\"mode\":\"classic\"}", + "{\"toolbar\":[\"sourcecode\",\"styles\",\"bold\",\"italic\",\"alignleft\",\"aligncenter\",\"alignright\",\"bullist\",\"numlist\",\"outdent\",\"indent\",\"link\",\"umbmediapicker\",\"umbembeddialog\"],\"stylesheets\":[],\"maxImageSize\":500,\"mode\":\"classic\"}", }); } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs index a53833d99e..91ca3b4d58 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs @@ -84,5 +84,6 @@ public class UmbracoPlan : MigrationPlan // we need to re-run this migration, as it was flawed for V14 RC3 (the migration can run twice without any issues) To("{6FB5CA9E-C823-473B-A14C-FE760D75943C}"); To("{827360CA-0855-42A5-8F86-A51F168CB559}"); + To("{FEF2DAF4-5408-4636-BB0E-B8798DF8F095}"); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_14_0_0/MigrateRichTextConfiguration.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_14_0_0/MigrateRichTextConfiguration.cs new file mode 100644 index 0000000000..950259f575 --- /dev/null +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_14_0_0/MigrateRichTextConfiguration.cs @@ -0,0 +1,32 @@ +using NPoco; +using Umbraco.Cms.Core; +using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_14_0_0; + +public class MigrateRichTextConfiguration : MigrationBase +{ + + public MigrateRichTextConfiguration(IMigrationContext context) : base(context) + { + } + + protected override void Migrate() + { + Sql sql = Sql() + .Select() + .From() + .Where(x => x.EditorAlias.Equals(Constants.PropertyEditors.Aliases.RichText)); + + List dataTypeDtos = Database.Fetch(sql); + + foreach (DataTypeDto dataTypeDto in dataTypeDtos) + { + // Update the configuration + dataTypeDto.Configuration = dataTypeDto.Configuration?.Replace("\"ace", "\"sourcecode"); + Database.Update(dataTypeDto); + } + } +} diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValidator.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValidator.cs index ab01ebb94b..685dbae9aa 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValidator.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValidator.cs @@ -1,6 +1,7 @@ // Copyright (c) Umbraco. // See LICENSE for more details. +using Umbraco.Cms.Core.Cache.PropertyEditors; using Umbraco.Cms.Core.Models.Blocks; using Umbraco.Cms.Core.Services; @@ -15,8 +16,8 @@ internal class BlockEditorValidator : BlockEditorValidatorBase< public BlockEditorValidator( IPropertyValidationService propertyValidationService, BlockEditorValues blockEditorValues, - IContentTypeService contentTypeService) - : base(propertyValidationService, contentTypeService) + IBlockEditorElementTypeCache elementTypeCache) + : base(propertyValidationService, elementTypeCache) => _blockEditorValues = blockEditorValues; protected override IEnumerable GetElementTypeValidation(object? value) diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValidatorBase.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValidatorBase.cs index 43ce39a743..8452284cc9 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValidatorBase.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValidatorBase.cs @@ -1,4 +1,5 @@ -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Cache.PropertyEditors; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Blocks; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; @@ -9,11 +10,11 @@ internal abstract class BlockEditorValidatorBase : ComplexEdito where TValue : BlockValue, new() where TLayout : class, IBlockLayoutItem, new() { - private readonly IContentTypeService _contentTypeService; + private readonly IBlockEditorElementTypeCache _elementTypeCache; - protected BlockEditorValidatorBase(IPropertyValidationService propertyValidationService, IContentTypeService contentTypeService) + protected BlockEditorValidatorBase(IPropertyValidationService propertyValidationService, IBlockEditorElementTypeCache elementTypeCache) : base(propertyValidationService) - => _contentTypeService = contentTypeService; + => _elementTypeCache = elementTypeCache; protected IEnumerable GetBlockEditorDataValidation(BlockEditorData blockEditorData) { @@ -29,7 +30,7 @@ internal abstract class BlockEditorValidatorBase : ComplexEdito foreach (var group in itemDataGroups) { - var allElementTypes = _contentTypeService.GetAll(group.Items.Select(x => x.ContentTypeKey).ToArray()).ToDictionary(x => x.Key); + var allElementTypes = _elementTypeCache.GetAll(group.Items.Select(x => x.ContentTypeKey).ToArray()).ToDictionary(x => x.Key); for (var i = 0; i < group.Items.Count; i++) { diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValues.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValues.cs index a2ce20b95f..bf8ccee15f 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValues.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValues.cs @@ -2,9 +2,9 @@ // See LICENSE for more details. using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core.Cache.PropertyEditors; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Blocks; -using Umbraco.Cms.Core.Services; using Umbraco.Extensions; namespace Umbraco.Cms.Core.PropertyEditors; @@ -16,14 +16,14 @@ internal class BlockEditorValues where TValue : BlockValue, new() where TLayout : class, IBlockLayoutItem, new() { - private readonly IContentTypeService _contentTypeService; private readonly BlockEditorDataConverter _dataConverter; + private readonly IBlockEditorElementTypeCache _elementTypeCache; private readonly ILogger _logger; - public BlockEditorValues(BlockEditorDataConverter dataConverter, IContentTypeService contentTypeService, ILogger logger) + public BlockEditorValues(BlockEditorDataConverter dataConverter, IBlockEditorElementTypeCache elementTypeCache, ILogger logger) { _dataConverter = dataConverter; - _contentTypeService = contentTypeService; + _elementTypeCache = elementTypeCache; _logger = logger; } @@ -59,7 +59,7 @@ internal class BlockEditorValues // filter out any content that isn't referenced in the layout references IEnumerable contentTypeKeys = blockEditorData.BlockValue.ContentData.Select(x => x.ContentTypeKey) .Union(blockEditorData.BlockValue.SettingsData.Select(x => x.ContentTypeKey)).Distinct(); - IDictionary contentTypesDictionary = _contentTypeService.GetAll(contentTypeKeys).ToDictionary(x=>x.Key); + IDictionary contentTypesDictionary = _elementTypeCache.GetAll(contentTypeKeys).ToDictionary(x=>x.Key); foreach (BlockItemData block in blockEditorData.BlockValue.ContentData.Where(x => blockEditorData.References.Any(r => x.Udi is not null && r.ContentUdi == x.Udi))) diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockGridPropertyEditorBase.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockGridPropertyEditorBase.cs index 21e5278be5..bfd0e61e0b 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockGridPropertyEditorBase.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockGridPropertyEditorBase.cs @@ -4,6 +4,7 @@ using System.ComponentModel.DataAnnotations; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Cache.PropertyEditors; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Blocks; @@ -49,12 +50,12 @@ public abstract class BlockGridPropertyEditorBase : DataEditor IShortStringHelper shortStringHelper, IJsonSerializer jsonSerializer, IIOHelper ioHelper, - IContentTypeService contentTypeService, + IBlockEditorElementTypeCache elementTypeCache, IPropertyValidationService propertyValidationService) : base(attribute, propertyEditors, dataValueReferenceFactories, dataTypeConfigurationCache, textService, logger, shortStringHelper, jsonSerializer, ioHelper) { - BlockEditorValues = new BlockEditorValues(new BlockGridEditorDataConverter(jsonSerializer), contentTypeService, logger); - Validators.Add(new BlockEditorValidator(propertyValidationService, BlockEditorValues, contentTypeService)); + BlockEditorValues = new BlockEditorValues(new BlockGridEditorDataConverter(jsonSerializer), elementTypeCache, logger); + Validators.Add(new BlockEditorValidator(propertyValidationService, BlockEditorValues, elementTypeCache)); Validators.Add(new MinMaxValidator(BlockEditorValues, textService)); } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorBase.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorBase.cs index a91ffc1770..8d84254f16 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorBase.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorBase.cs @@ -2,6 +2,7 @@ using System.ComponentModel.DataAnnotations; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Cache.PropertyEditors; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Blocks; @@ -55,7 +56,7 @@ public abstract class BlockListPropertyEditorBase : DataEditor PropertyEditorCollection propertyEditors, DataValueReferenceFactoryCollection dataValueReferenceFactories, IDataTypeConfigurationCache dataTypeConfigurationCache, - IContentTypeService contentTypeService, + IBlockEditorElementTypeCache elementTypeCache, ILocalizedTextService textService, ILogger logger, IShortStringHelper shortStringHelper, @@ -64,8 +65,8 @@ public abstract class BlockListPropertyEditorBase : DataEditor IPropertyValidationService propertyValidationService) : base(attribute, propertyEditors, dataValueReferenceFactories, dataTypeConfigurationCache, textService, logger, shortStringHelper, jsonSerializer, ioHelper) { - BlockEditorValues = new BlockEditorValues(blockEditorDataConverter, contentTypeService, logger); - Validators.Add(new BlockEditorValidator(propertyValidationService, BlockEditorValues, contentTypeService)); + BlockEditorValues = new BlockEditorValues(blockEditorDataConverter, elementTypeCache, logger); + Validators.Add(new BlockEditorValidator(propertyValidationService, BlockEditorValues, elementTypeCache)); Validators.Add(new MinMaxValidator(BlockEditorValues, textService)); } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/RichTextEditorBlockValidator.cs b/src/Umbraco.Infrastructure/PropertyEditors/RichTextEditorBlockValidator.cs index 3322efde1a..c3a2a57c13 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/RichTextEditorBlockValidator.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/RichTextEditorBlockValidator.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core.Cache.PropertyEditors; using Umbraco.Cms.Core.Models.Blocks; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; @@ -14,10 +15,10 @@ internal class RichTextEditorBlockValidator: BlockEditorValidatorBase blockEditorValues, - IContentTypeService contentTypeService, + IBlockEditorElementTypeCache elementTypeCache, IJsonSerializer jsonSerializer, ILogger logger) - : base(propertyValidationService, contentTypeService) + : base(propertyValidationService, elementTypeCache) { _blockEditorValues = blockEditorValues; _jsonSerializer = jsonSerializer; diff --git a/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs index c282a77880..619e98743d 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs @@ -4,6 +4,8 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Cache.PropertyEditors; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Blocks; @@ -67,7 +69,7 @@ public class RichTextPropertyEditor : DataEditor private readonly HtmlLocalLinkParser _localLinkParser; private readonly RichTextEditorPastedImages _pastedImages; private readonly IJsonSerializer _jsonSerializer; - private readonly IContentTypeService _contentTypeService; + private readonly IBlockEditorElementTypeCache _elementTypeCache; private readonly ILogger _logger; public RichTextPropertyValueEditor( @@ -84,7 +86,7 @@ public class RichTextPropertyEditor : DataEditor IJsonSerializer jsonSerializer, IIOHelper ioHelper, IHtmlSanitizer htmlSanitizer, - IContentTypeService contentTypeService, + IBlockEditorElementTypeCache elementTypeCache, IPropertyValidationService propertyValidationService, DataValueReferenceFactoryCollection dataValueReferenceFactoryCollection) : base(attribute, propertyEditors, dataTypeReadCache, localizedTextService, logger, shortStringHelper, jsonSerializer, ioHelper, dataValueReferenceFactoryCollection) @@ -94,11 +96,11 @@ public class RichTextPropertyEditor : DataEditor _localLinkParser = localLinkParser; _pastedImages = pastedImages; _htmlSanitizer = htmlSanitizer; - _contentTypeService = contentTypeService; + _elementTypeCache = elementTypeCache; _jsonSerializer = jsonSerializer; _logger = logger; - Validators.Add(new RichTextEditorBlockValidator(propertyValidationService, CreateBlockEditorValues(), contentTypeService, jsonSerializer, logger)); + Validators.Add(new RichTextEditorBlockValidator(propertyValidationService, CreateBlockEditorValues(), elementTypeCache, jsonSerializer, logger)); } /// @@ -276,6 +278,6 @@ public class RichTextPropertyEditor : DataEditor } private BlockEditorValues CreateBlockEditorValues() - => new(new RichTextEditorBlockDataConverter(_jsonSerializer), _contentTypeService, _logger); + => new(new RichTextEditorBlockDataConverter(_jsonSerializer), _elementTypeCache, _logger); } } diff --git a/src/Umbraco.PublishedCache.NuCache/Umbraco.PublishedCache.NuCache.csproj b/src/Umbraco.PublishedCache.NuCache/Umbraco.PublishedCache.NuCache.csproj index c37aa27bb4..4894759e5f 100644 --- a/src/Umbraco.PublishedCache.NuCache/Umbraco.PublishedCache.NuCache.csproj +++ b/src/Umbraco.PublishedCache.NuCache/Umbraco.PublishedCache.NuCache.csproj @@ -7,7 +7,7 @@ - + diff --git a/src/Umbraco.Web.Common/Filters/UmbracoVirtualPageFilterAttribute.cs b/src/Umbraco.Web.Common/Filters/UmbracoVirtualPageFilterAttribute.cs index 707dfe0b8d..fadd0d19e2 100644 --- a/src/Umbraco.Web.Common/Filters/UmbracoVirtualPageFilterAttribute.cs +++ b/src/Umbraco.Web.Common/Filters/UmbracoVirtualPageFilterAttribute.cs @@ -1,9 +1,11 @@ using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Web.Common.Controllers; using Umbraco.Cms.Web.Common.Routing; @@ -40,6 +42,12 @@ public class UmbracoVirtualPageFilterAttribute : Attribute, IAsyncActionFilter if (endpoint != null) { IUmbracoVirtualPageRoute umbracoVirtualPageRoute = context.HttpContext.RequestServices.GetRequiredService(); + IPublishedRouter publishedRouter = context.HttpContext.RequestServices.GetRequiredService(); + UriUtility uriUtility = context.HttpContext.RequestServices.GetRequiredService(); + + var originalRequestUrl = new Uri(context.HttpContext.Request.GetEncodedUrl()); + Uri cleanedUri = uriUtility.UriToUmbraco(originalRequestUrl); + publishedRouter.UpdateVariationContext(cleanedUri); IPublishedContent? publishedContent = umbracoVirtualPageRoute.FindContent(endpoint, context); diff --git a/src/Umbraco.Web.Common/Routing/UmbracoVirtualPageRoute.cs b/src/Umbraco.Web.Common/Routing/UmbracoVirtualPageRoute.cs index cff5a589f6..9813cf3212 100644 --- a/src/Umbraco.Web.Common/Routing/UmbracoVirtualPageRoute.cs +++ b/src/Umbraco.Web.Common/Routing/UmbracoVirtualPageRoute.cs @@ -155,6 +155,7 @@ public class UmbracoVirtualPageRoute : IUmbracoVirtualPageRoute IPublishedRequestBuilder requestBuilder = await _publishedRouter.CreateRequestAsync(cleanedUrl); requestBuilder.SetPublishedContent(publishedContent); + _publishedRouter.RouteDomain(requestBuilder); return requestBuilder.Build(); } diff --git a/src/Umbraco.Web.UI.Client b/src/Umbraco.Web.UI.Client index 6366776159..9077e80b32 160000 --- a/src/Umbraco.Web.UI.Client +++ b/src/Umbraco.Web.UI.Client @@ -1 +1 @@ -Subproject commit 63667761595d0eba06137d358db214d7d72f7d14 +Subproject commit 9077e80b3298c3ef9ca491fca7a33cc662ea6f5b diff --git a/src/Umbraco.Web.UI/Properties/launchSettings.json b/src/Umbraco.Web.UI/Properties/launchSettings.json index 2631298a10..cf917cdcb4 100644 --- a/src/Umbraco.Web.UI/Properties/launchSettings.json +++ b/src/Umbraco.Web.UI/Properties/launchSettings.json @@ -11,14 +11,14 @@ "profiles": { "IIS Express": { "commandName": "IISExpress", - "launchBrowser": true, + "launchBrowser": false, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } }, "Umbraco.Web.UI": { "commandName": "Project", - "launchBrowser": true, + "launchBrowser": false, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, diff --git a/src/Umbraco.Web.Website/Routing/EagerMatcherPolicy.cs b/src/Umbraco.Web.Website/Routing/EagerMatcherPolicy.cs index 4098b21928..1752fa0a72 100644 --- a/src/Umbraco.Web.Website/Routing/EagerMatcherPolicy.cs +++ b/src/Umbraco.Web.Website/Routing/EagerMatcherPolicy.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.Http; +using System.Reflection; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Matching; @@ -7,7 +8,10 @@ using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Web; using Umbraco.Cms.Web.Common.Controllers; +using Umbraco.Cms.Web.Common.Routing; +using Umbraco.Cms.Web.Website.Controllers; using Umbraco.Extensions; namespace Umbraco.Cms.Web.Website.Routing; @@ -35,6 +39,8 @@ internal class EagerMatcherPolicy : MatcherPolicy, IEndpointSelectorPolicy private readonly IRuntimeState _runtimeState; private readonly EndpointDataSource _endpointDataSource; private readonly UmbracoRequestPaths _umbracoRequestPaths; + private readonly IUmbracoContextAccessor _umbracoContextAccessor; + private readonly IPublishedRouter _publishedRouter; private GlobalSettings _globalSettings; private readonly Lazy _installEndpoint; private readonly Lazy _renderEndpoint; @@ -43,11 +49,15 @@ internal class EagerMatcherPolicy : MatcherPolicy, IEndpointSelectorPolicy IRuntimeState runtimeState, EndpointDataSource endpointDataSource, UmbracoRequestPaths umbracoRequestPaths, - IOptionsMonitor globalSettings) + IOptionsMonitor globalSettings, + IUmbracoContextAccessor umbracoContextAccessor, + IPublishedRouter publishedRouter) { _runtimeState = runtimeState; _endpointDataSource = endpointDataSource; _umbracoRequestPaths = umbracoRequestPaths; + _umbracoContextAccessor = umbracoContextAccessor; + _publishedRouter = publishedRouter; _globalSettings = globalSettings.CurrentValue; globalSettings.OnChange(settings => _globalSettings = settings); _installEndpoint = new Lazy(GetInstallEndpoint); @@ -71,8 +81,10 @@ internal class EagerMatcherPolicy : MatcherPolicy, IEndpointSelectorPolicy } } - // If there's only one candidate, we don't need to do anything. - if (candidates.Count < 2) + // If there's only one candidate, or the request has the ufprt-token, we don't need to do anything . + // The ufprt-token is handled by the the and should not be discarded. + var candidateCount = candidates.Count; + if (candidateCount < 2 || string.IsNullOrEmpty(httpContext.Request.GetUfprt()) is false) { return; } @@ -85,6 +97,14 @@ internal class EagerMatcherPolicy : MatcherPolicy, IEndpointSelectorPolicy RouteEndpoint? dynamicEndpoint = null; for (var i = 0; i < candidates.Count; i++) { + if (candidates.IsValidCandidate(i) is false) + { + // If the candidate is not valid we reduce the candidate count so we can later ensure that there is always + // at least 1 candidate. + candidateCount -= 1; + continue; + } + CandidateState candidate = candidates[i]; // If it's not a RouteEndpoint there's not much we can do to count it in the order. @@ -93,6 +113,29 @@ internal class EagerMatcherPolicy : MatcherPolicy, IEndpointSelectorPolicy continue; } + // We have to ensure that none of the candidates is a render controller or surface controller + // Normally these shouldn't be statically routed, however some people do it. + // So we should probably be friendly and check for it. + // Do not add this to V14. + ControllerActionDescriptor? controllerDescriptor = routeEndpoint.Metadata.GetMetadata(); + TypeInfo? controllerTypeInfo = controllerDescriptor?.ControllerTypeInfo; + if (controllerTypeInfo is not null && + (controllerTypeInfo.IsType() + || controllerTypeInfo.IsType())) + { + return; + } + + // 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. + // 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. + if (controllerTypeInfo is not null && controllerTypeInfo.IsType()) + { + await RouteVirtualRequestAsync(httpContext); + } + if (routeEndpoint.Order < lowestOrder) { // We have to ensure that the route is valid for the current request method. @@ -123,12 +166,28 @@ internal class EagerMatcherPolicy : MatcherPolicy, IEndpointSelectorPolicy // Invalidate the dynamic route if another route has a lower order. // This means that if you register your static route after the dynamic route, the dynamic route will take precedence // This more closely resembles the existing behaviour. - if (dynamicEndpoint is not null && dynamicId is not null && dynamicEndpoint.Order > lowestOrder) + if (dynamicEndpoint is not null && dynamicId is not null && dynamicEndpoint.Order > lowestOrder && candidateCount > 1) { candidates.SetValidity(dynamicId.Value, false); } } + private async Task RouteVirtualRequestAsync(HttpContext context) + { + if (_umbracoContextAccessor.TryGetUmbracoContext(out IUmbracoContext? umbracoContext) is false) + { + return; + } + + IPublishedRequestBuilder requestBuilder = + await _publishedRouter.CreateRequestAsync(umbracoContext.CleanedUmbracoUrl); + _publishedRouter.RouteDomain(requestBuilder); + // This is just a temporary RouteValues object just for culture which will be overwritten later + // so we can just use a dummy action descriptor. + var umbracoRouteValues = new UmbracoRouteValues(requestBuilder.Build(), new ControllerActionDescriptor()); + context.Features.Set(umbracoRouteValues); + } + /// /// Replaces the first endpoint candidate with the specified endpoint, invalidating all other candidates, /// guaranteeing that the specified endpoint will be hit. diff --git a/tests/Directory.Packages.props b/tests/Directory.Packages.props index 4fce9e86f3..ba3af0bd42 100644 --- a/tests/Directory.Packages.props +++ b/tests/Directory.Packages.props @@ -5,7 +5,7 @@ - + diff --git a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json index 37b554808c..7916e3cb54 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json @@ -7,8 +7,8 @@ "name": "acceptancetest", "hasInstallScript": true, "dependencies": { - "@umbraco/json-models-builders": "^2.0.6", - "@umbraco/playwright-testhelpers": "^2.0.0-beta.60", + "@umbraco/json-models-builders": "^2.0.7", + "@umbraco/playwright-testhelpers": "^2.0.0-beta.63", "camelize": "^1.0.0", "dotenv": "^16.3.1", "faker": "^4.1.0", @@ -132,9 +132,9 @@ } }, "node_modules/@umbraco/json-models-builders": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@umbraco/json-models-builders/-/json-models-builders-2.0.6.tgz", - "integrity": "sha512-eoOhTSH7rcC7NESId0vhqtxNXPuoy+ZaQo1moXxpv8/T6vqmKoDdLEydjtDz0FOXzqVZ5yQ1xWK0cpag37Laag==", + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@umbraco/json-models-builders/-/json-models-builders-2.0.7.tgz", + "integrity": "sha512-roR5A+jzIFN9z1BhogMGOEzSzoR8jOrIYIAevT7EnyS3H3OM0m0uREgvjYCQo0+QMfVws4zq4Ydjx2TIfGYvlQ==", "dependencies": { "camelize": "^1.0.1", "faker": "^6.6.6" @@ -146,11 +146,11 @@ "integrity": "sha512-9tCqYEDHI5RYFQigXFwF1hnCwcWCOJl/hmll0lr5D2Ljjb0o4wphb69wikeJDz5qCEzXCoPvG6ss5SDP6IfOdg==" }, "node_modules/@umbraco/playwright-testhelpers": { - "version": "2.0.0-beta.60", - "resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-2.0.0-beta.60.tgz", - "integrity": "sha512-5KJkn1GtfCXqbwYP8RnDyjWUNqSQ/62UYFARuXhUzQoz4xvv3Fme8rPeiOBxqJRWWoj3MQCaP7nyPPs3FDe8vQ==", + "version": "2.0.0-beta.63", + "resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-2.0.0-beta.63.tgz", + "integrity": "sha512-fLXUcWNJupfGKkD6zOGg6WcU5cmqQ6gQkyIyG+UsKSrkgCxK23+N5LrOz2OVp2NZ8GQ8kB5pJ4izvCp+yMMOnA==", "dependencies": { - "@umbraco/json-models-builders": "2.0.6", + "@umbraco/json-models-builders": "2.0.7", "camelize": "^1.0.0", "faker": "^4.1.0", "form-data": "^4.0.0", diff --git a/tests/Umbraco.Tests.AcceptanceTest/package.json b/tests/Umbraco.Tests.AcceptanceTest/package.json index c7261b06f3..8126ebf602 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package.json @@ -21,8 +21,8 @@ "wait-on": "^7.2.0" }, "dependencies": { - "@umbraco/json-models-builders": "^2.0.6", - "@umbraco/playwright-testhelpers": "^2.0.0-beta.60", + "@umbraco/json-models-builders": "^2.0.7", + "@umbraco/playwright-testhelpers": "^2.0.0-beta.63", "camelize": "^1.0.0", "dotenv": "^16.3.1", "faker": "^4.1.0", diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Translation/Translation.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Dictionary/Dictionary.spec.ts similarity index 55% rename from tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Translation/Translation.spec.ts rename to tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Dictionary/Dictionary.spec.ts index 355442a365..68ad9ee39b 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Translation/Translation.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Dictionary/Dictionary.spec.ts @@ -15,64 +15,64 @@ test.afterEach(async ({umbracoApi}) => { test('can create a dictionary item', async ({umbracoApi, umbracoUi}) => { // Arrange await umbracoApi.dictionary.ensureNameNotExists(dictionaryName); - await umbracoUi.translation.goToSection(ConstantHelper.sections.translation); + await umbracoUi.dictionary.goToSection(ConstantHelper.sections.dictionary); // Act - await umbracoUi.translation.clickCreateLink(); - await umbracoUi.translation.enterDictionaryName(dictionaryName); - await umbracoUi.translation.clickSaveButton(); + await umbracoUi.dictionary.clickCreateLink(); + await umbracoUi.dictionary.enterDictionaryName(dictionaryName); + await umbracoUi.dictionary.clickSaveButton(); // Assert expect(await umbracoApi.dictionary.doesNameExist(dictionaryName)).toBeTruthy(); - await umbracoUi.translation.isSuccessNotificationVisible(); - await umbracoUi.translation.clickLeftArrowButton(); + await umbracoUi.dictionary.isSuccessNotificationVisible(); + await umbracoUi.dictionary.clickLeftArrowButton(); // Verify the dictionary item displays in the tree and in the list - await umbracoUi.translation.isDictionaryTreeItemVisible(dictionaryName); - expect(await umbracoUi.translation.doesDictionaryListHaveText(dictionaryName)).toBeTruthy(); + await umbracoUi.dictionary.isDictionaryTreeItemVisible(dictionaryName); + expect(await umbracoUi.dictionary.doesDictionaryListHaveText(dictionaryName)).toBeTruthy(); }); test('can delete a dictionary item', async ({umbracoApi, umbracoUi}) => { // Arrange await umbracoApi.dictionary.ensureNameNotExists(dictionaryName); await umbracoApi.dictionary.create(dictionaryName); - await umbracoUi.translation.goToSection(ConstantHelper.sections.translation); + await umbracoUi.dictionary.goToSection(ConstantHelper.sections.dictionary); // Act - await umbracoUi.translation.clickActionsMenuForDictionary(dictionaryName); - await umbracoUi.translation.deleteDictionary(); + await umbracoUi.dictionary.clickActionsMenuForDictionary(dictionaryName); + await umbracoUi.dictionary.deleteDictionary(); // Assert - await umbracoUi.translation.isSuccessNotificationVisible(); + await umbracoUi.dictionary.isSuccessNotificationVisible(); expect(await umbracoApi.dictionary.doesNameExist(dictionaryName)).toBeFalsy(); // Verify the dictionary item does not display in the tree - await umbracoUi.translation.isDictionaryTreeItemVisible(dictionaryName, false); + await umbracoUi.dictionary.isDictionaryTreeItemVisible(dictionaryName, false); // TODO: Uncomment this when the front-end is ready. Currently the dictionary list is not updated immediately. // Verify the dictionary item does not display in the list - //expect(await umbracoUi.translation.doesDictionaryListHaveText(dictionaryName)).toBeFalsy(); + //expect(await umbracoUi.dictionary.doesDictionaryListHaveText(dictionaryName)).toBeFalsy(); }); test('can create a dictionary item in a dictionary', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { // Arrange await umbracoApi.dictionary.ensureNameNotExists(parentDictionaryName); let parentDictionaryId = await umbracoApi.dictionary.create(parentDictionaryName); - await umbracoUi.translation.goToSection(ConstantHelper.sections.translation); + await umbracoUi.dictionary.goToSection(ConstantHelper.sections.dictionary); // Act - await umbracoUi.translation.clickActionsMenuForDictionary(parentDictionaryName); - await umbracoUi.translation.clickCreateDictionaryItemButton(); - await umbracoUi.translation.enterDictionaryName(dictionaryName); - await umbracoUi.translation.clickSaveButton(); + await umbracoUi.dictionary.clickActionsMenuForDictionary(parentDictionaryName); + await umbracoUi.dictionary.clickCreateDictionaryItemButton(); + await umbracoUi.dictionary.enterDictionaryName(dictionaryName); + await umbracoUi.dictionary.clickSaveButton(); // Assert - await umbracoUi.translation.isSuccessNotificationVisible(); + await umbracoUi.dictionary.isSuccessNotificationVisible(); const dictionaryChildren = await umbracoApi.dictionary.getChildren(parentDictionaryId); expect(dictionaryChildren[0].name).toEqual(dictionaryName); - await umbracoUi.translation.clickLeftArrowButton(); + await umbracoUi.dictionary.clickLeftArrowButton(); // Verify the new dictionary item displays in the list - expect(await umbracoUi.translation.doesDictionaryListHaveText(dictionaryName)).toBeTruthy(); + expect(await umbracoUi.dictionary.doesDictionaryListHaveText(dictionaryName)).toBeTruthy(); // Verify the new dictionary item displays in the tree - await umbracoUi.translation.reloadTree(parentDictionaryName); - await umbracoUi.translation.isDictionaryTreeItemVisible(dictionaryName); + await umbracoUi.dictionary.reloadTree(parentDictionaryName); + await umbracoUi.dictionary.isDictionaryTreeItemVisible(dictionaryName); // Clean await umbracoApi.dictionary.ensureNameNotExists(parentDictionaryName); @@ -82,12 +82,12 @@ test('can export a dictionary item', async ({umbracoApi, umbracoUi}) => { // Arrange await umbracoApi.dictionary.ensureNameNotExists(dictionaryName); const dictionaryId = await umbracoApi.dictionary.create(dictionaryName); - await umbracoUi.translation.goToSection(ConstantHelper.sections.translation); + await umbracoUi.dictionary.goToSection(ConstantHelper.sections.dictionary); // Act - await umbracoUi.translation.clickActionsMenuForDictionary(dictionaryName); - await umbracoUi.translation.clickExportMenu(); - const exportData = await umbracoUi.translation.exportDictionary(false); + await umbracoUi.dictionary.clickActionsMenuForDictionary(dictionaryName); + await umbracoUi.dictionary.clickExportMenu(); + const exportData = await umbracoUi.dictionary.exportDictionary(false); // Assert expect(exportData).toEqual(dictionaryId + '.udt'); @@ -98,12 +98,12 @@ test('can export a dictionary item with descendants', {tag: '@smoke'}, async ({u await umbracoApi.dictionary.ensureNameNotExists(parentDictionaryName); let parentDictionaryId = await umbracoApi.dictionary.create(parentDictionaryName); await umbracoApi.dictionary.create(dictionaryName, [], parentDictionaryId); - await umbracoUi.translation.goToSection(ConstantHelper.sections.translation); + await umbracoUi.dictionary.goToSection(ConstantHelper.sections.dictionary); // Act - await umbracoUi.translation.clickActionsMenuForDictionary(parentDictionaryName); - await umbracoUi.translation.clickExportMenu(); - const exportData = await umbracoUi.translation.exportDictionary(true); + await umbracoUi.dictionary.clickActionsMenuForDictionary(parentDictionaryName); + await umbracoUi.dictionary.clickExportMenu(); + const exportData = await umbracoUi.dictionary.exportDictionary(true); // Assert expect(exportData).toEqual(parentDictionaryId + '.udt'); @@ -119,20 +119,20 @@ test('can import a dictionary item', async ({umbracoApi, umbracoUi}) => { const importDictionaryName = 'TestImportDictionary'; await umbracoApi.dictionary.ensureNameNotExists(dictionaryName); await umbracoApi.dictionary.create(dictionaryName); - await umbracoUi.translation.goToSection(ConstantHelper.sections.translation); + await umbracoUi.dictionary.goToSection(ConstantHelper.sections.dictionary); // Act - await umbracoUi.translation.clickActionsMenuForDictionary(dictionaryName); - await umbracoUi.translation.clickImportMenu(); - await umbracoUi.translation.importDictionary(udtFilePath); + await umbracoUi.dictionary.clickActionsMenuForDictionary(dictionaryName); + await umbracoUi.dictionary.clickImportMenu(); + await umbracoUi.dictionary.importDictionary(udtFilePath); // Assert // Verify the imported dictionary item displays in the tree - await umbracoUi.translation.reloadTree(dictionaryName); - await umbracoUi.translation.isDictionaryTreeItemVisible(importDictionaryName); + await umbracoUi.dictionary.reloadTree(dictionaryName); + await umbracoUi.dictionary.isDictionaryTreeItemVisible(importDictionaryName); // TODO: Uncomment this when the front-end is ready. Currently the dictionary list is not updated immediately. // Verify the imported dictionary item displays in the list - //expect(await umbracoUi.translation.doesDictionaryListHaveText(importDictionaryName)).toBeTruthy(); + //expect(await umbracoUi.dictionary.doesDictionaryListHaveText(importDictionaryName)).toBeTruthy(); }); test('can import a dictionary item with descendants', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { @@ -143,23 +143,23 @@ test('can import a dictionary item with descendants', {tag: '@smoke'}, async ({u const importChildDictionaryName = 'TestImportChild'; await umbracoApi.dictionary.ensureNameNotExists(dictionaryName); await umbracoApi.dictionary.create(dictionaryName); - await umbracoUi.translation.goToSection(ConstantHelper.sections.translation); + await umbracoUi.dictionary.goToSection(ConstantHelper.sections.dictionary); // Act - await umbracoUi.translation.clickActionsMenuForDictionary(dictionaryName); - await umbracoUi.translation.clickImportMenu(); - await umbracoUi.translation.importDictionary(udtFilePath); + await umbracoUi.dictionary.clickActionsMenuForDictionary(dictionaryName); + await umbracoUi.dictionary.clickImportMenu(); + await umbracoUi.dictionary.importDictionary(udtFilePath); // Assert // Verify the imported dictionary items display in the tree - await umbracoUi.translation.reloadTree(dictionaryName); - await umbracoUi.translation.isDictionaryTreeItemVisible(importParentDictionaryName); - await umbracoUi.translation.reloadTree(importParentDictionaryName); - await umbracoUi.translation.isDictionaryTreeItemVisible(importChildDictionaryName); + await umbracoUi.dictionary.reloadTree(dictionaryName); + await umbracoUi.dictionary.isDictionaryTreeItemVisible(importParentDictionaryName); + await umbracoUi.dictionary.reloadTree(importParentDictionaryName); + await umbracoUi.dictionary.isDictionaryTreeItemVisible(importChildDictionaryName); // TODO: Uncomment this when the front-end is ready. Currently the dictionary list is not updated immediately. // Verify the imported dictionary items display in the list - //expect(await umbracoUi.translation.doesDictionaryListHaveText(importParentDictionaryName)).toBeTruthy(); - //expect(await umbracoUi.translation.doesDictionaryListHaveText(importChildDictionaryName)).toBeTruthy(); + //expect(await umbracoUi.dictionary.doesDictionaryListHaveText(importParentDictionaryName)).toBeTruthy(); + //expect(await umbracoUi.dictionary.doesDictionaryListHaveText(importChildDictionaryName)).toBeTruthy(); }); // Skip this test as the search function is removed @@ -167,13 +167,13 @@ test.skip('can search a dictionary item in list when have results', async ({umbr // Arrange await umbracoApi.dictionary.ensureNameNotExists(dictionaryName); await umbracoApi.dictionary.create(dictionaryName); - await umbracoUi.translation.goToSection(ConstantHelper.sections.translation); + await umbracoUi.dictionary.goToSection(ConstantHelper.sections.dictionary); // Act - await umbracoUi.translation.enterSearchKeywordAndPressEnter(dictionaryName); + await umbracoUi.dictionary.enterSearchKeywordAndPressEnter(dictionaryName); // Assert - expect(await umbracoUi.translation.doesDictionaryListHaveText(dictionaryName)).toBeTruthy(); + expect(await umbracoUi.dictionary.doesDictionaryListHaveText(dictionaryName)).toBeTruthy(); }); // Skip this test as the search function is removed @@ -182,11 +182,11 @@ test.skip('can search a dictionary item in list when have no results', async ({u const emptySearchResultMessage = 'No Dictionary items to choose from'; await umbracoApi.dictionary.ensureNameNotExists(dictionaryName); await umbracoApi.dictionary.create(dictionaryName); - await umbracoUi.translation.goToSection(ConstantHelper.sections.translation); + await umbracoUi.dictionary.goToSection(ConstantHelper.sections.dictionary); // Act - await umbracoUi.translation.enterSearchKeywordAndPressEnter('xyz'); + await umbracoUi.dictionary.enterSearchKeywordAndPressEnter('xyz'); // Assert - await umbracoUi.translation.isSearchResultMessageDisplayEmpty(emptySearchResultMessage); + await umbracoUi.dictionary.isSearchResultMessageDisplayEmpty(emptySearchResultMessage); }); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/User.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/User.spec.ts new file mode 100644 index 0000000000..789bf96939 --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/User.spec.ts @@ -0,0 +1,577 @@ +import {ConstantHelper, test} from '@umbraco/playwright-testhelpers'; +import {expect} from '@playwright/test'; + +const nameOfTheUser = 'TestUser'; +const userEmail = 'TestUser@EmailTest.test'; +const defaultUserGroupName = 'Writers'; + +test.beforeEach(async ({umbracoUi, umbracoApi}) => { + await umbracoUi.goToBackOffice(); + await umbracoApi.user.ensureNameNotExists(nameOfTheUser); +}); + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.user.ensureNameNotExists(nameOfTheUser); +}); + +test('can create a user', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + await umbracoUi.user.goToSection(ConstantHelper.sections.users); + + // Act + await umbracoUi.user.clickCreateButton(); + await umbracoUi.user.enterNameOfTheUser(nameOfTheUser); + await umbracoUi.user.enterUserEmail(userEmail); + await umbracoUi.user.clickChooseButton(); + await umbracoUi.user.clickButtonWithName(defaultUserGroupName); + await umbracoUi.user.clickSubmitButton(); + await umbracoUi.user.clickCreateUserButton(); + + // Assert + await umbracoUi.user.isSuccessNotificationVisible(); + expect(await umbracoApi.user.doesNameExist(nameOfTheUser)).toBeTruthy(); +}); + +test('can rename a user', async ({umbracoApi, umbracoUi}) => { + // Arrange + const wrongName = 'WrongName'; + await umbracoApi.user.ensureNameNotExists(wrongName); + const userGroup = await umbracoApi.userGroup.getByName(defaultUserGroupName); + await umbracoApi.user.createDefaultUser(wrongName, wrongName + userEmail, [userGroup.id]); + await umbracoUi.user.goToSection(ConstantHelper.sections.users); + + // Act + await umbracoUi.user.clickUserWithName(wrongName); + await umbracoUi.user.enterUpdatedNameOfUser(nameOfTheUser); + await umbracoUi.user.clickSaveButton(); + + // Assert + await umbracoUi.user.isSuccessNotificationVisible(); + expect(await umbracoApi.user.doesNameExist(nameOfTheUser)).toBeTruthy(); +}); + +test('can delete a user', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const userGroup = await umbracoApi.userGroup.getByName(defaultUserGroupName); + await umbracoApi.user.createDefaultUser(nameOfTheUser, userEmail, [userGroup.id]); + await umbracoUi.user.goToSection(ConstantHelper.sections.users); + + // Act + await umbracoUi.user.clickUserWithName(nameOfTheUser); + await umbracoUi.user.clickDeleteButton(); + await umbracoUi.user.clickConfirmToDeleteButton(); + + // Assert + await umbracoUi.user.isSuccessNotificationVisible(); + expect(await umbracoApi.user.doesNameExist(nameOfTheUser)).toBeFalsy(); + // Checks if the user is deleted from the list + await umbracoUi.user.clickUsersTabButton(); + await umbracoUi.user.isUserVisible(nameOfTheUser, false); +}); + +test('can add multiple user groups to a user', async ({umbracoApi, umbracoUi}) => { + // Arrange + const secondUserGroupName = 'Translators'; + const userGroupWriters = await umbracoApi.userGroup.getByName(defaultUserGroupName); + const userGroupTranslators = await umbracoApi.userGroup.getByName(secondUserGroupName); + await umbracoApi.user.createDefaultUser(nameOfTheUser, userEmail, [userGroupWriters.id]); + await umbracoUi.user.goToSection(ConstantHelper.sections.users); + + // Act + await umbracoUi.user.clickUserWithName(nameOfTheUser); + await umbracoUi.user.clickChooseUserGroupsButton(); + await umbracoUi.user.clickButtonWithName(secondUserGroupName); + await umbracoUi.user.clickSubmitButton(); + await umbracoUi.user.clickSaveButton(); + + // Assert + await umbracoUi.user.isSuccessNotificationVisible(); + const userData = await umbracoApi.user.getByName(nameOfTheUser); + expect(await umbracoApi.user.doesUserContainUserGroupIds(nameOfTheUser, [userGroupWriters.id, userGroupTranslators.id])).toBeTruthy(); +}); + +test('can remove a user group from a user', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const userGroup = await umbracoApi.userGroup.getByName(defaultUserGroupName); + await umbracoApi.user.createDefaultUser(nameOfTheUser, userEmail, [userGroup.id]); + await umbracoUi.user.goToSection(ConstantHelper.sections.users); + + // Act + await umbracoUi.user.clickUserWithName(nameOfTheUser); + await umbracoUi.user.clickRemoveButtonForUserGroupWithName(defaultUserGroupName); + await umbracoUi.user.clickConfirmRemoveButton(); + await umbracoUi.user.clickSaveButton(); + + // Assert + await umbracoUi.user.isSuccessNotificationVisible(); + const userData = await umbracoApi.user.getByName(nameOfTheUser); + expect(userData.userGroupIds).toEqual([]); +}); + +test('can update culture for a user', async ({umbracoApi, umbracoUi}) => { + // Arrange + const danishIsoCode = 'da-dk'; + const userGroup = await umbracoApi.userGroup.getByName(defaultUserGroupName); + await umbracoApi.user.createDefaultUser(nameOfTheUser, userEmail, [userGroup.id]); + await umbracoUi.user.goToSection(ConstantHelper.sections.users); + + // Act + await umbracoUi.user.clickUserWithName(nameOfTheUser); + await umbracoUi.user.selectUserLanguage('Dansk'); + await umbracoUi.user.clickSaveButton(); + + // Assert + await umbracoUi.user.isSuccessNotificationVisible(); + const userData = await umbracoApi.user.getByName(nameOfTheUser); + expect(userData.languageIsoCode).toEqual(danishIsoCode); +}); + +test('can add a content start node to a user', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const userGroup = await umbracoApi.userGroup.getByName(defaultUserGroupName); + await umbracoApi.user.createDefaultUser(nameOfTheUser, userEmail, [userGroup.id]); + const documentTypeName = 'TestDocumentType'; + const documentName = 'TestDocument'; + await umbracoApi.document.ensureNameNotExists(documentName); + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + const documentTypeId = await umbracoApi.documentType.createDefaultDocumentTypeWithAllowAsRoot(documentTypeName); + const documentId = await umbracoApi.document.createDefaultDocument(documentName, documentTypeId); + await umbracoUi.user.goToSection(ConstantHelper.sections.users); + + // Act + await umbracoUi.user.clickUserWithName(nameOfTheUser); + await umbracoUi.user.clickChooseContentStartNodeButton(); + await umbracoUi.user.clickLabelWithName(documentName); + await umbracoUi.user.clickChooseContainerButton(); + await umbracoUi.user.clickSaveButton(); + + // Assert + await umbracoUi.user.isSuccessNotificationVisible(); + expect(await umbracoApi.user.doesUserContainContentStartNodeIds(nameOfTheUser, [documentId])).toBeTruthy(); + + // Clean + await umbracoApi.document.ensureNameNotExists(documentName); + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); +}); + +test('can add multiple content start nodes for a user', async ({umbracoApi, umbracoUi}) => { + // Arrange + const userGroup = await umbracoApi.userGroup.getByName(defaultUserGroupName); + const userId = await umbracoApi.user.createDefaultUser(nameOfTheUser, userEmail, [userGroup.id]); + const documentTypeName = 'TestDocumentType'; + const documentName = 'TestDocument'; + const secondDocumentName = 'SecondDocument'; + await umbracoApi.document.ensureNameNotExists(documentName); + await umbracoApi.document.ensureNameNotExists(secondDocumentName); + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + const documentTypeId = await umbracoApi.documentType.createDefaultDocumentTypeWithAllowAsRoot(documentTypeName); + const documentId = await umbracoApi.document.createDefaultDocument(documentName, documentTypeId); + // Adds the content start node to the user + const userData = await umbracoApi.user.getByName(nameOfTheUser); + userData.documentStartNodeIds.push({id: documentId}); + await umbracoApi.user.update(userId, userData); + const secondDocumentId = await umbracoApi.document.createDefaultDocument(secondDocumentName, documentTypeId); + await umbracoUi.user.goToSection(ConstantHelper.sections.users); + + // Act + await umbracoUi.user.clickUserWithName(nameOfTheUser); + await umbracoUi.user.clickChooseContentStartNodeButton(); + await umbracoUi.user.clickLabelWithName(secondDocumentName); + await umbracoUi.user.clickChooseContainerButton(); + await umbracoUi.user.clickSaveButton(); + + // Assert + await umbracoUi.user.isSuccessNotificationVisible(); + expect(await umbracoApi.user.doesUserContainContentStartNodeIds(nameOfTheUser, [documentId, secondDocumentId])).toBeTruthy(); + + // Clean + await umbracoApi.document.ensureNameNotExists(documentName); + await umbracoApi.document.ensureNameNotExists(secondDocumentName); + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); +}); + +test('can remove a content start node from a user', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const userGroup = await umbracoApi.userGroup.getByName(defaultUserGroupName); + const userId = await umbracoApi.user.createDefaultUser(nameOfTheUser, userEmail, [userGroup.id]); + const documentTypeName = 'TestDocumentType'; + const documentName = 'TestDocument'; + await umbracoApi.document.ensureNameNotExists(documentName); + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + const documentTypeId = await umbracoApi.documentType.createDefaultDocumentTypeWithAllowAsRoot(documentTypeName); + const documentId = await umbracoApi.document.createDefaultDocument(documentName, documentTypeId); + // Adds the content start node to the user + const userData = await umbracoApi.user.getByName(nameOfTheUser); + userData.documentStartNodeIds.push({id: documentId}); + await umbracoApi.user.update(userId, userData); + expect(await umbracoApi.user.doesUserContainContentStartNodeIds(nameOfTheUser, [documentId])).toBeTruthy(); + await umbracoUi.user.goToSection(ConstantHelper.sections.users); + + // Act + await umbracoUi.user.clickUserWithName(nameOfTheUser); + await umbracoUi.user.clickRemoveButtonForContentNodeWithName(documentName); + await umbracoUi.user.clickConfirmRemoveButton(); + await umbracoUi.user.clickSaveButton(); + + // Assert + await umbracoUi.user.isSuccessNotificationVisible(); + expect(await umbracoApi.user.doesUserContainContentStartNodeIds(nameOfTheUser, [documentId])).toBeFalsy(); + + // Clean + await umbracoApi.document.ensureNameNotExists(documentName); + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); +}); + +test('can add media start nodes for a user', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const mediaTypeName = 'File'; + const mediaName = 'TestMediaFile'; + const userGroup = await umbracoApi.userGroup.getByName(defaultUserGroupName); + await umbracoApi.user.createDefaultUser(nameOfTheUser, userEmail, [userGroup.id]); + await umbracoApi.media.ensureNameNotExists(mediaName); + const mediaId = await umbracoApi.media.createDefaultMedia(mediaName, mediaTypeName); + await umbracoUi.user.goToSection(ConstantHelper.sections.users); + + // Act + await umbracoUi.user.clickUserWithName(nameOfTheUser); + await umbracoUi.user.clickChooseMediaStartNodeButton(); + await umbracoUi.user.clickMediaCardWithName(mediaName); + await umbracoUi.user.clickSubmitButton(); + await umbracoUi.user.clickSaveButton(); + + // Assert + await umbracoUi.user.isSuccessNotificationVisible(); + expect(await umbracoApi.user.doesUserContainMediaStartNodeIds(nameOfTheUser, [mediaId])).toBeTruthy(); + + // Clean + await umbracoApi.media.ensureNameNotExists(mediaName); +}); + +test('can add multiple media start nodes for a user', async ({umbracoApi, umbracoUi}) => { + // Arrange + const mediaTypeName = 'File'; + const userGroup = await umbracoApi.userGroup.getByName(defaultUserGroupName); + const userId = await umbracoApi.user.createDefaultUser(nameOfTheUser, userEmail, [userGroup.id]); + const mediaName = 'TestMediaFile'; + const secondMediaName = 'SecondMediaFile'; + await umbracoApi.media.ensureNameNotExists(mediaName); + await umbracoApi.media.ensureNameNotExists(secondMediaName); + const firstMediaId = await umbracoApi.media.createDefaultMedia(mediaName, mediaTypeName); + const secondMediaId = await umbracoApi.media.createDefaultMedia(secondMediaName, mediaTypeName); + // Adds the media start node to the user + const userData = await umbracoApi.user.getByName(nameOfTheUser); + userData.mediaStartNodeIds.push({id: firstMediaId}); + await umbracoApi.user.update(userId, userData); + expect(await umbracoApi.user.doesUserContainMediaStartNodeIds(nameOfTheUser, [firstMediaId])).toBeTruthy(); + await umbracoUi.user.goToSection(ConstantHelper.sections.users); + + // Act + await umbracoUi.user.clickUserWithName(nameOfTheUser); + await umbracoUi.user.clickChooseMediaStartNodeButton(); + await umbracoUi.user.clickMediaCardWithName(secondMediaName); + await umbracoUi.user.clickSubmitButton(); + await umbracoUi.user.clickSaveButton(); + + // Assert + await umbracoUi.user.isSuccessNotificationVisible(); + expect(await umbracoApi.user.doesUserContainMediaStartNodeIds(nameOfTheUser, [firstMediaId, secondMediaId])).toBeTruthy(); + + // Clean + await umbracoApi.media.ensureNameNotExists(mediaName); + await umbracoApi.media.ensureNameNotExists(secondMediaName); +}); + +test('can remove a media start node from a user', async ({umbracoApi, umbracoUi}) => { + // Arrange + const mediaTypeName = 'File'; + const userGroup = await umbracoApi.userGroup.getByName(defaultUserGroupName); + const userId = await umbracoApi.user.createDefaultUser(nameOfTheUser, userEmail, [userGroup.id]); + const mediaName = 'TestMediaFile'; + await umbracoApi.media.ensureNameNotExists(mediaName); + const mediaId = await umbracoApi.media.createDefaultMedia(mediaName, mediaTypeName); + // Adds the media start node to the user + const userData = await umbracoApi.user.getByName(nameOfTheUser); + userData.mediaStartNodeIds.push({id: mediaId}); + await umbracoApi.user.update(userId, userData); + expect(await umbracoApi.user.doesUserContainMediaStartNodeIds(nameOfTheUser, [mediaId])).toBeTruthy(); + await umbracoUi.user.goToSection(ConstantHelper.sections.users); + + // Act + await umbracoUi.user.clickUserWithName(nameOfTheUser); + await umbracoUi.user.clickRemoveButtonForMediaNodeWithName(mediaName); + await umbracoUi.user.clickConfirmRemoveButton(); + await umbracoUi.user.clickSaveButton(); + + // Assert + await umbracoUi.user.isSuccessNotificationVisible(); + expect(await umbracoApi.user.doesUserContainMediaStartNodeIds(nameOfTheUser, [mediaId])).toBeFalsy(); + + // Clean + await umbracoApi.media.ensureNameNotExists(mediaName); +}); + +test('can allow access to all documents for a user', async ({umbracoApi, umbracoUi}) => { + // Arrange + const userGroup = await umbracoApi.userGroup.getByName(defaultUserGroupName); + await umbracoApi.user.createDefaultUser(nameOfTheUser, userEmail, [userGroup.id]); + await umbracoUi.user.goToSection(ConstantHelper.sections.users); + + // Act + await umbracoUi.user.clickUserWithName(nameOfTheUser); + await umbracoUi.user.clickAllowAccessToAllDocumentsSlider(); + await umbracoUi.user.clickSaveButton(); + + // Assert + await umbracoUi.user.isSuccessNotificationVisible(); + const userData = await umbracoApi.user.getByName(nameOfTheUser); + expect(userData.hasDocumentRootAccess).toBeTruthy() +}); + +test('can allow access to all media for a user', async ({umbracoApi, umbracoUi}) => { + // Arrange + const userGroup = await umbracoApi.userGroup.getByName(defaultUserGroupName); + await umbracoApi.user.createDefaultUser(nameOfTheUser, userEmail, [userGroup.id]); + await umbracoUi.user.goToSection(ConstantHelper.sections.users); + + // Act + await umbracoUi.user.clickUserWithName(nameOfTheUser); + await umbracoUi.user.clickAllowAccessToAllMediaSlider(); + await umbracoUi.user.clickSaveButton(); + + // Assert + await umbracoUi.user.isSuccessNotificationVisible(); + const userData = await umbracoApi.user.getByName(nameOfTheUser); + expect(userData.hasMediaRootAccess).toBeTruthy(); +}); + +test('can see if the user has the correct access based on content start nodes', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const userGroup = await umbracoApi.userGroup.getByName(defaultUserGroupName); + const userId = await umbracoApi.user.createDefaultUser(nameOfTheUser, userEmail, [userGroup.id]); + const documentTypeName = 'TestDocumentType'; + const documentName = 'TestDocument'; + await umbracoApi.document.ensureNameNotExists(documentName); + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); + const documentTypeId = await umbracoApi.documentType.createDefaultDocumentTypeWithAllowAsRoot(documentTypeName); + const documentId = await umbracoApi.document.createDefaultDocument(documentName, documentTypeId); + // Adds the content start node to the user + const userData = await umbracoApi.user.getByName(nameOfTheUser); + userData.documentStartNodeIds.push({id: documentId}); + await umbracoApi.user.update(userId, userData); + expect(await umbracoApi.user.doesUserContainContentStartNodeIds(nameOfTheUser, [documentId])).toBeTruthy(); + await umbracoUi.user.goToSection(ConstantHelper.sections.users); + + // Act + await umbracoUi.user.clickUserWithName(nameOfTheUser); + + // Assert + await umbracoUi.user.doesUserHaveAccessToContentNode(documentName); + + // Clean + await umbracoApi.document.ensureNameNotExists(documentName); + await umbracoApi.documentType.ensureNameNotExists(documentTypeName); +}); + +test('can see if the user has the correct access based on media start nodes', async ({umbracoApi, umbracoUi}) => { + // Arrange + const mediaTypeName = 'File'; + const userGroup = await umbracoApi.userGroup.getByName(defaultUserGroupName); + const userId = await umbracoApi.user.createDefaultUser(nameOfTheUser, userEmail, [userGroup.id]); + const mediaName = 'TestMediaFile'; + await umbracoApi.media.ensureNameNotExists(mediaName); + const mediaId = await umbracoApi.media.createDefaultMedia(mediaName, mediaTypeName); + // Adds the media start node to the user + const userData = await umbracoApi.user.getByName(nameOfTheUser); + userData.mediaStartNodeIds.push({id: mediaId}); + await umbracoApi.user.update(userId, userData); + expect(await umbracoApi.user.doesUserContainMediaStartNodeIds(nameOfTheUser, [mediaId])).toBeTruthy(); + await umbracoUi.user.goToSection(ConstantHelper.sections.users); + + // Act + await umbracoUi.user.clickUserWithName(nameOfTheUser); + + // Assert + await umbracoUi.user.doesUserHaveAccessToMediaNode(mediaName); + + // Clean + await umbracoApi.media.ensureNameNotExists(mediaName); +}); + +test('can change password for a user', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const userPassword = 'TestPassword'; + const userGroup = await umbracoApi.userGroup.getByName(defaultUserGroupName); + const userId = await umbracoApi.user.createDefaultUser(nameOfTheUser, userEmail, [userGroup.id]); + await umbracoUi.user.goToSection(ConstantHelper.sections.users); + + // Act + await umbracoUi.user.clickUserWithName(nameOfTheUser); + await umbracoUi.user.clickChangePasswordButton(); + await umbracoUi.user.updatePassword(userPassword); + + // Assert + await umbracoUi.user.isPasswordUpdatedForUserWithId(userId); +}); + +test('can disable a user', async ({umbracoApi, umbracoUi}) => { + // Arrange + const disabledStatus = 'Disabled'; + const userGroup = await umbracoApi.userGroup.getByName(defaultUserGroupName); + await umbracoApi.user.createDefaultUser(nameOfTheUser, userEmail, [userGroup.id]); + await umbracoUi.user.goToSection(ConstantHelper.sections.users); + + // Act + await umbracoUi.user.clickUserWithName(nameOfTheUser); + await umbracoUi.user.clickDisableButton(); + await umbracoUi.user.clickConfirmDisableButton(); + + // Assert + await umbracoUi.user.isSuccessNotificationVisible(); + expect(umbracoUi.user.isUserDisabledTextVisible()).toBeTruthy(); + const userData = await umbracoApi.user.getByName(nameOfTheUser); + expect(userData.state).toBe(disabledStatus); +}); + +test('can enable a user', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const inactiveStatus = 'Inactive'; + const userGroup = await umbracoApi.userGroup.getByName(defaultUserGroupName); + const userId = await umbracoApi.user.createDefaultUser(nameOfTheUser, userEmail, [userGroup.id]); + await umbracoApi.user.disable([userId]); + await umbracoUi.user.goToSection(ConstantHelper.sections.users); + + // Act + await umbracoUi.user.clickUserWithName(nameOfTheUser); + await umbracoUi.user.clickEnableButton(); + await umbracoUi.user.clickConfirmEnableButton(); + + // Assert + await umbracoUi.user.isSuccessNotificationVisible(); + await umbracoUi.user.isUserActiveTextVisible(); + // The state of the user is not enabled. The reason for this is that the user has not logged in, resulting in the state Inactive. + const userData = await umbracoApi.user.getByName(nameOfTheUser); + expect(userData.state).toBe(inactiveStatus); +}); + +test('can add an avatar to a user', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const userGroup = await umbracoApi.userGroup.getByName(defaultUserGroupName); + await umbracoApi.user.createDefaultUser(nameOfTheUser, userEmail, [userGroup.id]); + const filePath = './fixtures/mediaLibrary/Umbraco.png'; + await umbracoUi.user.goToSection(ConstantHelper.sections.users); + + // Act + await umbracoUi.user.clickUserWithName(nameOfTheUser); + await umbracoUi.user.changePhotoWithFileChooser(filePath); + + // Assert + await umbracoUi.user.isSuccessNotificationVisible(); + const userData = await umbracoApi.user.getByName(nameOfTheUser); + expect(userData.avatarUrls).not.toHaveLength(0); +}); + +test('can remove an avatar from a user', async ({umbracoApi, umbracoUi}) => { + // Arrange + const userGroup = await umbracoApi.userGroup.getByName(defaultUserGroupName); + const userId = await umbracoApi.user.createDefaultUser(nameOfTheUser, userEmail, [userGroup.id]); + await umbracoApi.user.addDefaultAvatarImageToUser(userId); + await umbracoUi.user.goToSection(ConstantHelper.sections.users); + + // Act + await umbracoUi.user.clickUserWithName(nameOfTheUser); + await umbracoUi.user.clickRemovePhotoButton(); + + // Assert + await umbracoUi.user.isSuccessNotificationVisible(); + const userData = await umbracoApi.user.getByName(nameOfTheUser); + expect(userData.avatarUrls).toHaveLength(0); +}); + +test('can see if the inactive label is removed from the admin user', async ({umbracoApi, umbracoUi}) => { + // Arrange + const userLabel = 'Active'; + const currentUser = await umbracoApi.user.getCurrentUser(); + await umbracoUi.user.goToSection(ConstantHelper.sections.users); + + // Act + await umbracoUi.user.clickUserWithName(currentUser.name); + + // Assert + await umbracoUi.user.isTextWithExactNameVisible(userLabel); + const userData = await umbracoApi.user.getByName(currentUser.name); + expect(userData.state).toBe(userLabel); +}); + +test('can search for a user', async ({umbracoApi, umbracoUi}) => { + // Arrange + const userGroup = await umbracoApi.userGroup.getByName(defaultUserGroupName); + await umbracoApi.user.createDefaultUser(nameOfTheUser, userEmail, [userGroup.id]); + await umbracoUi.user.goToSection(ConstantHelper.sections.users); + + // Act + await umbracoUi.user.doesUserSectionContainUserAmount(2); + await umbracoUi.user.searchInUserSection(nameOfTheUser); + + // Assert + // Wait for filtering to be done + await umbracoUi.waitForTimeout(200); + await umbracoUi.user.doesUserSectionContainUserAmount(1); + await umbracoUi.user.doesUserSectionContainUserWithText(nameOfTheUser); +}); + +test('can filter by status', async ({umbracoApi, umbracoUi}) => { + // Arrange + const inactiveStatus = 'Inactive'; + const userGroup = await umbracoApi.userGroup.getByName(defaultUserGroupName); + await umbracoApi.user.createDefaultUser(nameOfTheUser, userEmail, [userGroup.id]); + await umbracoUi.user.goToSection(ConstantHelper.sections.users); + + // Act + await umbracoUi.user.doesUserSectionContainUserAmount(2); + await umbracoUi.user.filterByStatusName(inactiveStatus); + + // Assert + // Wait for filtering to be done + await umbracoUi.waitForTimeout(200); + await umbracoUi.user.doesUserSectionContainUserAmount(1); + await umbracoUi.user.doesUserSectionContainUserWithText(nameOfTheUser); + await umbracoUi.user.doesUserSectionContainUserWithText(inactiveStatus); +}); + +test('can filter by user groups', async ({umbracoApi, umbracoUi}) => { + // Arrange + const userGroup = await umbracoApi.userGroup.getByName(defaultUserGroupName); + await umbracoApi.user.createDefaultUser(nameOfTheUser, userEmail, [userGroup.id]); + await umbracoUi.user.goToSection(ConstantHelper.sections.users); + + // Act + await umbracoUi.user.doesUserSectionContainUserAmount(2); + await umbracoUi.user.filterByGroupName(defaultUserGroupName); + + // Assert + // Wait for filtering to be done + await umbracoUi.waitForTimeout(200); + await umbracoUi.user.doesUserSectionContainUserAmount(1); + await umbracoUi.user.doesUserSectionContainUserWithText(defaultUserGroupName); +}); + +test('can order by newest user', async ({umbracoApi, umbracoUi}) => { + // Arrange + const userGroup = await umbracoApi.userGroup.getByName(defaultUserGroupName); + await umbracoApi.user.createDefaultUser(nameOfTheUser, userEmail, [userGroup.id]); + await umbracoUi.user.goToSection(ConstantHelper.sections.users); + + // Act + await umbracoUi.user.doesUserSectionContainUserAmount(2); + await umbracoUi.user.orderByNewestUser(); + + // Assert + // Wait for filtering to be done + await umbracoUi.waitForTimeout(200); + await umbracoUi.user.doesUserSectionContainUserAmount(2); + await umbracoUi.user.isUserWithNameTheFirstUserInList(nameOfTheUser); +}); + +// TODO: Sometimes the frontend does not switch from grid to table, or table to grid. +test.skip('can change from grid to table view', async ({page, umbracoApi, umbracoUi}) => { +}); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Api.Delivery/Services/RequestHeaderHandlerTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Api.Delivery/Services/RequestHeaderHandlerTests.cs new file mode 100644 index 0000000000..9b4da511eb --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Api.Delivery/Services/RequestHeaderHandlerTests.cs @@ -0,0 +1,48 @@ +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 RequestHeaderHandlerTests +{ + private const string HeaderName = "TestHeader"; + [Test] + public void GetHeaderValue_return_null_when_http_context_is_unavailable() + { + IHttpContextAccessor httpContextAccessor = Mock.Of(); + + var sut = new TestRequestHeaderHandler(httpContextAccessor); + + Assert.IsNull(sut.TestGetHeaderValue(HeaderName)); + } + + [Test] + public void GetHeaderValue_return_header_value_when_http_context_is_available() + { + + const string headerValue = "TestValue"; + + HttpContext httpContext = new DefaultHttpContext(); + httpContext.Request.Headers[HeaderName] = headerValue; + + IHttpContextAccessor httpContextAccessor = Mock.Of(); + Mock.Get(httpContextAccessor).Setup(x => x.HttpContext).Returns(httpContext); + + var sut = new TestRequestHeaderHandler(httpContextAccessor); + + Assert.AreEqual(headerValue, sut.TestGetHeaderValue(HeaderName)); + } +} + + +internal class TestRequestHeaderHandler : RequestHeaderHandler +{ + public TestRequestHeaderHandler(IHttpContextAccessor httpContextAccessor) : base(httpContextAccessor) + { + } + + public string? TestGetHeaderValue(string headerName) => base.GetHeaderValue(headerName); +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ApiMediaUrlProviderTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ApiMediaUrlProviderTests.cs index e1900d203f..0dfffbf638 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ApiMediaUrlProviderTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ApiMediaUrlProviderTests.cs @@ -21,7 +21,7 @@ public class ApiMediaUrlProviderTests : PropertyValueConverterTests var publishedUrlProvider = new Mock(); publishedUrlProvider - .Setup(p => p.GetMediaUrl(content.Object, UrlMode.Relative, It.IsAny(), It.IsAny(), It.IsAny())) + .Setup(p => p.GetMediaUrl(content.Object, UrlMode.Default, It.IsAny(), It.IsAny(), It.IsAny())) .Returns(publishedUrl); var apiMediaUrlProvider = new ApiMediaUrlProvider(publishedUrlProvider.Object); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/RichTextParserTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/RichTextParserTests.cs index b7712b5346..a2522a5ecd 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/RichTextParserTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/RichTextParserTests.cs @@ -9,7 +9,6 @@ using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.PropertyEditors.ValueConverters; using Umbraco.Cms.Core.PublishedCache; -using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Infrastructure.DeliveryApi; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.DeliveryApi; @@ -474,7 +473,7 @@ public class RichTextParserTests : PropertyValueConverterTests Mock.Of>()); } - private void SetupTestContent(out IApiContentRouteBuilder routeBuilder, out IPublishedSnapshotAccessor snapshotAccessor, out IPublishedUrlProvider urlProvider) + private void SetupTestContent(out IApiContentRouteBuilder routeBuilder, out IPublishedSnapshotAccessor snapshotAccessor, out IApiMediaUrlProvider apiMediaUrlProvider) { var contentMock = new Mock(); contentMock.SetupGet(m => m.Key).Returns(_contentKey); @@ -502,14 +501,14 @@ public class RichTextParserTests : PropertyValueConverterTests .Setup(m => m.Build(contentMock.Object, null)) .Returns(new ApiContentRoute("/some-content-path", new ApiContentStartItem(_contentRootKey, "the-root-path"))); - var urlProviderMock = new Mock(); - urlProviderMock - .Setup(m => m.GetMediaUrl(mediaMock.Object, It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + var apiMediaUrlProviderMock = new Mock(); + apiMediaUrlProviderMock + .Setup(m => m.GetUrl(mediaMock.Object)) .Returns("/some-media-url"); routeBuilder = routeBuilderMock.Object; snapshotAccessor = snapshotAccessorMock.Object; - urlProvider = urlProviderMock.Object; + apiMediaUrlProvider = apiMediaUrlProviderMock.Object; } private IPublishedElement CreateElement(Guid id, int propertyValue) diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueEditorReuseTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueEditorReuseTests.cs index 786d9d056b..5c3c761765 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueEditorReuseTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueEditorReuseTests.cs @@ -2,6 +2,7 @@ using Moq; using NUnit.Framework; using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Cache.PropertyEditors; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models.Blocks; using Umbraco.Cms.Core.PropertyEditors; @@ -43,7 +44,7 @@ public class DataValueEditorReuseTests _propertyEditorCollection, _dataValueReferenceFactories, Mock.Of(), - Mock.Of(), + Mock.Of(), Mock.Of(), Mock.Of>(), Mock.Of(), diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByAliasWithDomainsTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByAliasWithDomainsTests.cs index 3f639965cd..30bb4ae70c 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByAliasWithDomainsTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByAliasWithDomainsTests.cs @@ -31,7 +31,7 @@ public class ContentFinderByAliasWithDomainsTests : UrlRoutingTestBase var request = await publishedRouter.CreateRequestAsync(umbracoContext.CleanedUmbracoUrl); // must lookup domain - publishedRouter.FindDomain(request); + publishedRouter.FindAndSetDomain(request); if (expectedNode > 0) { diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByUrlWithDomainsTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByUrlWithDomainsTests.cs index c9069046ac..4606641265 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByUrlWithDomainsTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByUrlWithDomainsTests.cs @@ -207,7 +207,7 @@ public class ContentFinderByUrlWithDomainsTests : UrlRoutingTestBase var frequest = await publishedRouter.CreateRequestAsync(umbracoContext.CleanedUmbracoUrl); // must lookup domain else lookup by URL fails - publishedRouter.FindDomain(frequest); + publishedRouter.FindAndSetDomain(frequest); var lookup = new ContentFinderByUrl(Mock.Of>(), umbracoContextAccessor); var result = await lookup.TryFindContent(frequest); @@ -245,7 +245,7 @@ public class ContentFinderByUrlWithDomainsTests : UrlRoutingTestBase var frequest = await publishedRouter.CreateRequestAsync(umbracoContext.CleanedUmbracoUrl); // must lookup domain else lookup by URL fails - publishedRouter.FindDomain(frequest); + publishedRouter.FindAndSetDomain(frequest); Assert.AreEqual(expectedCulture, frequest.Culture); var lookup = new ContentFinderByUrl(Mock.Of>(), umbracoContextAccessor); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/DomainsAndCulturesTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/DomainsAndCulturesTests.cs index c54e540864..3945e2346d 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/DomainsAndCulturesTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/DomainsAndCulturesTests.cs @@ -261,7 +261,7 @@ public class DomainsAndCulturesTests : UrlRoutingTestBase var frequest = await publishedRouter.CreateRequestAsync(umbracoContext.CleanedUmbracoUrl); // lookup domain - publishedRouter.FindDomain(frequest); + publishedRouter.FindAndSetDomain(frequest); Assert.AreEqual(expectedCulture, frequest.Culture); @@ -310,7 +310,7 @@ public class DomainsAndCulturesTests : UrlRoutingTestBase var frequest = await publishedRouter.CreateRequestAsync(umbracoContext.CleanedUmbracoUrl); // lookup domain - publishedRouter.FindDomain(frequest); + publishedRouter.FindAndSetDomain(frequest); // find document var finder = new ContentFinderByUrl(Mock.Of>(), umbracoContextAccessor); @@ -345,7 +345,7 @@ public class DomainsAndCulturesTests : UrlRoutingTestBase var frequest = await publishedRouter.CreateRequestAsync(umbracoContext.CleanedUmbracoUrl); // lookup domain - publishedRouter.FindDomain(frequest); + publishedRouter.FindAndSetDomain(frequest); Assert.IsNotNull(frequest.Domain); Assert.AreEqual(expectedCulture, frequest.Culture); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/UrlsWithNestedDomains.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/UrlsWithNestedDomains.cs index 9edce34707..d0536640e2 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/UrlsWithNestedDomains.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/UrlsWithNestedDomains.cs @@ -62,7 +62,7 @@ public class UrlsWithNestedDomains : UrlRoutingTestBase var publishedRouter = CreatePublishedRouter(umbracoContextAccessor); var frequest = await publishedRouter.CreateRequestAsync(umbracoContext.CleanedUmbracoUrl); - publishedRouter.FindDomain(frequest); + publishedRouter.FindAndSetDomain(frequest); Assert.IsTrue(frequest.HasDomain()); // check that it's been routed diff --git a/tools/Umbraco.JsonSchema/Umbraco.JsonSchema.csproj b/tools/Umbraco.JsonSchema/Umbraco.JsonSchema.csproj index 7771d9c195..431674852e 100644 --- a/tools/Umbraco.JsonSchema/Umbraco.JsonSchema.csproj +++ b/tools/Umbraco.JsonSchema/Umbraco.JsonSchema.csproj @@ -7,7 +7,7 @@ - +