Merge remote-tracking branch 'origin/v11/dev' into v12/dev
This commit is contained in:
@@ -568,7 +568,7 @@ public class EntityController : UmbracoAuthorizedJsonController
|
||||
[HttpGet]
|
||||
public UrlAndAnchors GetUrlAndAnchors(int id, string? culture = "*")
|
||||
{
|
||||
culture ??= ClientCulture();
|
||||
culture = culture is null or "*" ? ClientCulture() : culture;
|
||||
|
||||
var url = _publishedUrlProvider.GetUrl(id, culture: culture);
|
||||
IEnumerable<string> anchorValues = _contentService.GetAnchorValuesFromRTEs(id, culture);
|
||||
|
||||
@@ -35,6 +35,7 @@ using Umbraco.Cms.Web.BackOffice.Security;
|
||||
using Umbraco.Cms.Web.Common.ActionsResults;
|
||||
using Umbraco.Cms.Web.Common.Attributes;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
using Umbraco.Cms.Web.Common.Models;
|
||||
using Umbraco.Cms.Web.Common.Security;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
@@ -795,22 +796,44 @@ public class UsersController : BackOfficeNotificationsController
|
||||
return ValidationProblem("The current user cannot disable itself");
|
||||
}
|
||||
|
||||
IUser[] users = _userService.GetUsersById(userIds).ToArray();
|
||||
var users = _userService.GetUsersById(userIds).ToList();
|
||||
List<IUser> skippedUsers = new();
|
||||
foreach (IUser u in users)
|
||||
{
|
||||
if (u.UserState is UserState.Invited)
|
||||
{
|
||||
_logger.LogWarning("Could not disable invited user {Username}", u.Name);
|
||||
skippedUsers.Add(u);
|
||||
continue;
|
||||
}
|
||||
|
||||
u.IsApproved = false;
|
||||
u.InvitedDate = null;
|
||||
}
|
||||
|
||||
_userService.Save(users);
|
||||
users = users.Except(skippedUsers).ToList();
|
||||
|
||||
if (users.Length > 1)
|
||||
if (users.Any())
|
||||
{
|
||||
return Ok(_localizedTextService.Localize("speechBubbles", "disableUsersSuccess",
|
||||
new[] { userIds.Length.ToString() }));
|
||||
_userService.Save(users);
|
||||
}
|
||||
else
|
||||
{
|
||||
return Ok(new DisabledUsersModel());
|
||||
}
|
||||
|
||||
return Ok(_localizedTextService.Localize("speechBubbles", "disableUserSuccess", new[] { users[0].Name }));
|
||||
var disabledUsersModel = new DisabledUsersModel
|
||||
{
|
||||
DisabledUserIds = users.Select(x => x.Id),
|
||||
};
|
||||
|
||||
var message= users.Count > 1
|
||||
? _localizedTextService.Localize("speechBubbles", "disableUsersSuccess", new[] { userIds.Length.ToString() })
|
||||
: _localizedTextService.Localize("speechBubbles", "disableUserSuccess", new[] { users[0].Name });
|
||||
|
||||
var header = _localizedTextService.Localize("general", "success");
|
||||
disabledUsersModel.Notifications.Add(new BackOfficeNotification(header, message, NotificationStyle.Success));
|
||||
return Ok(disabledUsersModel);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -42,7 +42,7 @@ public class InstallAuthorizeAttribute : TypeFilterAttribute
|
||||
// Only authorize when the installer is enabled
|
||||
context.Result = new ForbidResult(new AuthenticationProperties()
|
||||
{
|
||||
RedirectUri = _linkGenerator.GetBackOfficeUrl(_hostingEnvironment)
|
||||
RedirectUri = _linkGenerator.GetUmbracoBackOfficeUrl(_hostingEnvironment)
|
||||
});
|
||||
}
|
||||
else if (_runtimeState.Level == RuntimeLevel.Upgrade && (await context.HttpContext.AuthenticateBackOfficeAsync()).Succeeded == false)
|
||||
|
||||
@@ -149,7 +149,7 @@ public static partial class UmbracoBuilderExtensions
|
||||
|
||||
// WebRootFileProviderFactory is just a wrapper around the IWebHostEnvironment.WebRootFileProvider,
|
||||
// therefore no need to register it as singleton
|
||||
builder.Services.AddSingleton<IManifestFileProviderFactory, WebRootFileProviderFactory>();
|
||||
builder.Services.AddSingleton<IManifestFileProviderFactory, ContentAndWebRootFileProviderFactory>();
|
||||
builder.Services.AddSingleton<IGridEditorsConfigFileProviderFactory, WebRootFileProviderFactory>();
|
||||
|
||||
// Must be added here because DbProviderFactories is netstandard 2.1 so cannot exist in Infra for now
|
||||
|
||||
@@ -12,33 +12,36 @@ namespace Umbraco.Extensions;
|
||||
|
||||
public static class LinkGeneratorExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the Umbraco backoffice URL (if Umbraco is installed).
|
||||
/// </summary>
|
||||
/// <param name="linkGenerator">The link generator.</param>
|
||||
/// <returns>
|
||||
/// The Umbraco backoffice URL.
|
||||
/// </returns>
|
||||
public static string? GetUmbracoBackOfficeUrl(this LinkGenerator linkGenerator)
|
||||
=> linkGenerator.GetPathByAction("Default", "BackOffice", new { area = Constants.Web.Mvc.BackOfficeArea });
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Umbraco backoffice URL (if Umbraco is installed) or application virtual path (in most cases just <c>"/"</c>).
|
||||
/// </summary>
|
||||
/// <param name="linkGenerator">The link generator.</param>
|
||||
/// <param name="hostingEnvironment">The hosting environment.</param>
|
||||
/// <returns>
|
||||
/// The Umbraco backoffice URL.
|
||||
/// </returns>
|
||||
public static string GetUmbracoBackOfficeUrl(this LinkGenerator linkGenerator, IHostingEnvironment hostingEnvironment)
|
||||
=> GetUmbracoBackOfficeUrl(linkGenerator) ?? hostingEnvironment.ApplicationVirtualPath;
|
||||
|
||||
/// <summary>
|
||||
/// Return the back office url if the back office is installed
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method contained a bug that would result in always returning "/".
|
||||
/// </remarks>
|
||||
[Obsolete("Use the GetUmbracoBackOfficeUrl extension method instead. This method will be removed in Umbraco 13.")]
|
||||
public static string? GetBackOfficeUrl(this LinkGenerator linkGenerator, IHostingEnvironment hostingEnvironment)
|
||||
{
|
||||
Type? backOfficeControllerType;
|
||||
try
|
||||
{
|
||||
backOfficeControllerType = Assembly.Load("Umbraco.Web.BackOffice")
|
||||
.GetType("Umbraco.Web.BackOffice.Controllers.BackOfficeController");
|
||||
if (backOfficeControllerType == null)
|
||||
{
|
||||
return "/"; // this would indicate that the installer is installed without the back office
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
return
|
||||
hostingEnvironment
|
||||
.ApplicationVirtualPath; // this would indicate that the installer is installed without the back office
|
||||
}
|
||||
|
||||
return linkGenerator.GetPathByAction(
|
||||
"Default",
|
||||
ControllerExtensions.GetControllerName(backOfficeControllerType),
|
||||
new { area = Constants.Web.Mvc.BackOfficeApiArea });
|
||||
}
|
||||
=> "/";
|
||||
|
||||
/// <summary>
|
||||
/// Return the Url for a Web Api service
|
||||
|
||||
@@ -22,21 +22,27 @@ namespace Umbraco.Extensions;
|
||||
|
||||
public static class UrlHelperExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the Umbraco backoffice URL (if Umbraco is installed).
|
||||
/// </summary>
|
||||
/// <param name="urlHelper">The URL helper.</param>
|
||||
/// <returns>
|
||||
/// The Umbraco backoffice URL.
|
||||
/// </returns>
|
||||
public static string? GetUmbracoBackOfficeUrl(this IUrlHelper urlHelper)
|
||||
=> urlHelper.Action("Default", "BackOffice", new { area = Constants.Web.Mvc.BackOfficeArea });
|
||||
|
||||
/// <summary>
|
||||
/// Return the back office url if the back office is installed
|
||||
/// </summary>
|
||||
/// <param name="url"></param>
|
||||
/// <returns></returns>
|
||||
/// <remarks>
|
||||
/// This method contained a bug that would result in always returning "/".
|
||||
/// </remarks>
|
||||
[Obsolete("Use the GetUmbracoBackOfficeUrl extension method instead. This method will be removed in Umbraco 13.")]
|
||||
public static string? GetBackOfficeUrl(this IUrlHelper url)
|
||||
{
|
||||
var backOfficeControllerType = Type.GetType("Umbraco.Web.BackOffice.Controllers");
|
||||
if (backOfficeControllerType == null)
|
||||
{
|
||||
return "/"; // this would indicate that the installer is installed without the back office
|
||||
}
|
||||
|
||||
return url.Action("Default", ControllerExtensions.GetControllerName(backOfficeControllerType), new { area = Constants.Web.Mvc.BackOfficeApiArea });
|
||||
}
|
||||
=> "/";
|
||||
|
||||
/// <summary>
|
||||
/// Return the Url for a Web Api service
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
using Umbraco.Cms.Core.IO;
|
||||
|
||||
namespace Umbraco.Cms.Web.Common.FileProviders;
|
||||
|
||||
public class ContentAndWebRootFileProviderFactory : IManifestFileProviderFactory
|
||||
{
|
||||
private readonly IWebHostEnvironment _webHostEnvironment;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ContentAndWebRootFileProviderFactory"/> class.
|
||||
/// </summary>
|
||||
/// <param name="webHostEnvironment">The web hosting environment an application is running in.</param>
|
||||
public ContentAndWebRootFileProviderFactory(IWebHostEnvironment webHostEnvironment)
|
||||
{
|
||||
_webHostEnvironment = webHostEnvironment;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="IFileProvider" /> instance, pointing at WebRootPath and ContentRootPath.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// The newly created <see cref="IFileProvider" /> instance.
|
||||
/// </returns>
|
||||
public IFileProvider Create() => new CompositeFileProvider(_webHostEnvironment.WebRootFileProvider, _webHostEnvironment.ContentRootFileProvider);
|
||||
}
|
||||
13
src/Umbraco.Web.Common/Models/DisabledUsersModel.cs
Normal file
13
src/Umbraco.Web.Common/Models/DisabledUsersModel.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using System.Runtime.Serialization;
|
||||
using Umbraco.Cms.Core.Models.ContentEditing;
|
||||
|
||||
namespace Umbraco.Cms.Web.Common.Models;
|
||||
|
||||
[DataContract]
|
||||
public class DisabledUsersModel : INotificationModel
|
||||
{
|
||||
public List<BackOfficeNotification> Notifications { get; } = new();
|
||||
|
||||
[DataMember(Name = "disabledUserIds")]
|
||||
public IEnumerable<int> DisabledUserIds { get; set; } = Enumerable.Empty<int>();
|
||||
}
|
||||
@@ -92,7 +92,7 @@
|
||||
var sources = {
|
||||
//see: https://github.com/twitter/typeahead.js/blob/master/doc/jquery_typeahead.md#options
|
||||
// name = the data set name, we'll make this the tag group name + culture
|
||||
name: vm.config.group + (vm.culture ? vm.culture : ""),
|
||||
name: (vm.config.group + (vm.culture ? vm.culture : "")).replace(/\W/g, '-'),
|
||||
display: "text",
|
||||
//source: tagsHound
|
||||
source: function (query, syncCallback, asyncCallback) {
|
||||
|
||||
@@ -170,9 +170,6 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s
|
||||
}));
|
||||
});
|
||||
}
|
||||
else {
|
||||
styleFormats = fallbackStyles;
|
||||
}
|
||||
|
||||
return $q.all(promises).then(function () {
|
||||
// Always push our Umbraco RTE stylesheet
|
||||
@@ -375,7 +372,6 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s
|
||||
autoresize_bottom_margin: 10,
|
||||
content_css: styles.stylesheets,
|
||||
style_formats: styles.styleFormats,
|
||||
style_formats_autohide: true,
|
||||
language: getLanguage(),
|
||||
|
||||
//this would be for a theme other than inlite
|
||||
@@ -450,9 +446,20 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s
|
||||
}
|
||||
}
|
||||
|
||||
// if we have style_formats at this point they originate from the RTE CSS config. we don't want any custom
|
||||
// style_formats to interfere with the RTE CSS config, so let's explicitly remove the custom style_formats.
|
||||
if(tinyMceConfig.customConfig.style_formats && config.style_formats && config.style_formats.length){
|
||||
delete tinyMceConfig.customConfig.style_formats;
|
||||
}
|
||||
|
||||
Utilities.extend(config, tinyMceConfig.customConfig);
|
||||
}
|
||||
|
||||
if(!config.style_formats || !config.style_formats.length){
|
||||
// if we have no style_formats at this point we'll revert to using the default ones (fallbackStyles)
|
||||
config.style_formats = fallbackStyles;
|
||||
}
|
||||
|
||||
return config;
|
||||
|
||||
});
|
||||
|
||||
@@ -21,10 +21,6 @@ function multiUrlPickerController($scope, localizationService, entityResource, i
|
||||
|
||||
$scope.renderModel = [];
|
||||
|
||||
if ($scope.preview) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($scope.model.config && parseInt($scope.model.config.maxNumber) !== 1 && $scope.umbProperty) {
|
||||
var propertyActions = [
|
||||
removeAllEntriesAction
|
||||
@@ -83,7 +79,7 @@ function multiUrlPickerController($scope, localizationService, entityResource, i
|
||||
$scope.sortableOptions.disabled = $scope.renderModel.length === 1 || $scope.readonly;
|
||||
|
||||
removeAllEntriesAction.isDisabled = $scope.renderModel.length === 0 || $scope.readonly;
|
||||
|
||||
|
||||
//Update value
|
||||
$scope.model.value = $scope.renderModel;
|
||||
}
|
||||
@@ -93,7 +89,7 @@ function multiUrlPickerController($scope, localizationService, entityResource, i
|
||||
if (!$scope.allowRemove) return;
|
||||
|
||||
$scope.renderModel.splice($index, 1);
|
||||
|
||||
|
||||
setDirty();
|
||||
};
|
||||
|
||||
@@ -208,15 +204,23 @@ function multiUrlPickerController($scope, localizationService, entityResource, i
|
||||
$scope.model.config.minNumber = 1;
|
||||
}
|
||||
|
||||
_.each($scope.model.value, function (item) {
|
||||
const ids = [];
|
||||
$scope.model.value.forEach(item => {
|
||||
// we must reload the "document" link URLs to match the current editor culture
|
||||
if (item.udi && item.udi.indexOf("/document/") > 0) {
|
||||
if (item.udi && item.udi.indexOf("/document/") > 0 && ids.indexOf(item.udi) < 0) {
|
||||
ids.push(item.udi);
|
||||
item.url = null;
|
||||
entityResource.getUrlByUdi(item.udi).then(data => {
|
||||
item.url = data;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if(ids.length){
|
||||
entityResource.getUrlsByIds(ids, "Document").then(function(urlMap){
|
||||
Object.keys(urlMap).forEach((udi) => {
|
||||
const items = $scope.model.value.filter(item => item.udi === udi);
|
||||
items.forEach(item => item.url = urlMap[udi]);
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
init();
|
||||
|
||||
@@ -334,7 +334,7 @@
|
||||
vm.disableUserButtonState = "busy";
|
||||
usersResource.disableUsers(vm.selection).then(function (data) {
|
||||
// update userState
|
||||
vm.selection.forEach(function (userId) {
|
||||
data.disabledUserIds.forEach(function (userId) {
|
||||
var user = getUserFromArrayById(userId, vm.users);
|
||||
if (user) {
|
||||
user.userState = "Disabled";
|
||||
@@ -808,6 +808,7 @@
|
||||
|
||||
if (user.userDisplayState && user.userDisplayState.key === "Invited") {
|
||||
vm.allowEnableUser = false;
|
||||
vm.allowDisableUser = false;
|
||||
}
|
||||
|
||||
if (user.userDisplayState && user.userDisplayState.key === "LockedOut") {
|
||||
|
||||
@@ -56,6 +56,7 @@ public class RegisterModelBuilder : MemberModelBuilderBase
|
||||
|
||||
var model = new RegisterModel
|
||||
{
|
||||
RedirectUrl = _redirectUrl,
|
||||
MemberTypeAlias = providedOrDefaultMemberTypeAlias,
|
||||
UsernameIsEmail = _usernameIsEmail,
|
||||
MemberProperties = _lookupProperties
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
// Copyright (c) Umbraco.
|
||||
// See LICENSE for more details.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Mime;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Cms.Core;
|
||||
@@ -21,7 +16,7 @@ using Umbraco.Cms.Tests.Common.Builders.Extensions;
|
||||
using Umbraco.Cms.Tests.Integration.TestServerTest;
|
||||
using Umbraco.Cms.Web.BackOffice.Controllers;
|
||||
using Umbraco.Cms.Web.Common.Formatters;
|
||||
using Umbraco.Extensions;
|
||||
using Umbraco.Cms.Web.Common.Models;
|
||||
|
||||
namespace Umbraco.Cms.Tests.Integration.Umbraco.Web.BackOffice.Controllers;
|
||||
|
||||
@@ -231,4 +226,69 @@ public class UsersControllerTests : UmbracoTestServerTestBase
|
||||
Assert.AreEqual($"Unlocked {users.Count()} users", actual.Message);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Cannot_Disable_Invited_User()
|
||||
{
|
||||
var userService = GetRequiredService<IUserService>();
|
||||
|
||||
var user = new UserBuilder()
|
||||
.AddUserGroup()
|
||||
.WithAlias("writer") // Needs to be an existing alias
|
||||
.Done()
|
||||
.Build();
|
||||
|
||||
user.LastLoginDate = default;
|
||||
user.InvitedDate = DateTime.Now;
|
||||
userService.Save(user);
|
||||
var createdUser = userService.GetByEmail("test@test.com");
|
||||
|
||||
// Act
|
||||
var url = PrepareApiControllerUrl<UsersController>(x => x.PostDisableUsers(new []{createdUser.Id}));
|
||||
var response = await Client.PostAsync(url, null);
|
||||
|
||||
// Assert
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
|
||||
var body = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
|
||||
|
||||
body = body.TrimStart(AngularJsonMediaTypeFormatter.XsrfPrefix);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Can_Disable_Active_User()
|
||||
{
|
||||
var userService = GetRequiredService<IUserService>();
|
||||
|
||||
var user = new UserBuilder()
|
||||
.AddUserGroup()
|
||||
.WithAlias("writer") // Needs to be an existing alias
|
||||
.Done()
|
||||
.Build();
|
||||
|
||||
user.IsApproved = true;
|
||||
userService.Save(user);
|
||||
|
||||
var createdUser = userService.GetByEmail("test@test.com");
|
||||
|
||||
// Act
|
||||
var url = PrepareApiControllerUrl<UsersController>(x => x.PostDisableUsers(new[] { createdUser.Id }));
|
||||
var response = await Client.PostAsync(url, null);
|
||||
|
||||
// Assert
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
|
||||
var body = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
|
||||
|
||||
body = body.TrimStart(AngularJsonMediaTypeFormatter.XsrfPrefix);
|
||||
var affectedUsers = JsonConvert.DeserializeObject<DisabledUsersModel>(body, new JsonSerializerSettings { ContractResolver = new IgnoreRequiredAttributesResolver() });
|
||||
Assert.AreEqual(affectedUsers!.DisabledUserIds.First(), createdUser!.Id);
|
||||
|
||||
var disabledUser = userService.GetByEmail("test@test.com");
|
||||
Assert.AreEqual(disabledUser!.UserState, UserState.Disabled);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user