Compare commits
71
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ab0abca99 | ||
|
|
009a999e22 | ||
|
|
d3c1443b14 | ||
|
|
8a9db11422 | ||
|
|
d9fb6df16e | ||
|
|
14ed3348bf | ||
|
|
9e7a36865a | ||
|
|
db1d999721 | ||
|
|
18047a7cfb | ||
|
|
def7ebd48c | ||
|
|
8c2b1ebdc5 | ||
|
|
9227517a50 | ||
|
|
5322d0f7b5 | ||
|
|
a282cc5691 | ||
|
|
048f8bcdf9 | ||
|
|
7bcbc748d4 | ||
|
|
f30e6cfe7f | ||
|
|
026e80e026 | ||
|
|
a3b77cff63 | ||
|
|
4ca68d6995 | ||
|
|
280cb7f2b1 | ||
|
|
095a73132c | ||
|
|
cfb0fc23ac | ||
|
|
6620aca9fe | ||
|
|
2422a02b23 | ||
|
|
b4a9dc0770 | ||
|
|
e7411244fd | ||
|
|
9f357173c3 | ||
|
|
7dc6e3ed5b | ||
|
|
5dfff212d0 | ||
|
|
17615f966b | ||
|
|
7a2d6b6c63 | ||
|
|
343867f6ee | ||
|
|
b9837ac77c | ||
|
|
bb73ec6c68 | ||
|
|
ee2d7bbb1b | ||
|
|
5d48bc7371 | ||
|
|
59a4649528 | ||
|
|
f54b6033ab | ||
|
|
bf340cd7d4 | ||
|
|
313417cb91 | ||
|
|
7850078623 | ||
|
|
44bf3b77b3 | ||
|
|
8485458896 | ||
|
|
3d253f5f06 | ||
|
|
64287d7e1a | ||
|
|
edc78a5a4c | ||
|
|
8056a59a10 | ||
|
|
1a18d6c035 | ||
|
|
3e877182b5 | ||
|
|
0e4f883bc1 | ||
|
|
265cc62ab8 | ||
|
|
800c16970e | ||
|
|
6bd11bf233 | ||
|
|
a2fd82a3f3 | ||
|
|
7567990da1 | ||
|
|
c7d157bbfd | ||
|
|
abc312c9b4 | ||
|
|
2161edb871 | ||
|
|
e77e9c5691 | ||
|
|
e934a943b5 | ||
|
|
65bb2801b0 | ||
|
|
a0a4af6a0c | ||
|
|
e8d6cded2b | ||
|
|
c65204a146 | ||
|
|
95eb58587b | ||
|
|
86f3033334 | ||
|
|
25628a8b76 | ||
|
|
7552e315fb | ||
|
|
d15b5b221e | ||
|
|
0166727eee |
@@ -73,7 +73,7 @@
|
||||
<PackageVersion Include="Serilog.Sinks.Async" Version="1.5.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.File" Version="5.0.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.Map" Version="1.0.2" />
|
||||
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.6" />
|
||||
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.7" />
|
||||
<PackageVersion Include="SixLabors.ImageSharp.Web" Version="3.1.3" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.9.0" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -3,13 +3,12 @@ name: Nightly_E2E_Test_$(TeamProject)_$(Build.DefinitionName)_$(SourceBranchName
|
||||
pr: none
|
||||
trigger: none
|
||||
|
||||
schedules:
|
||||
- cron: '0 0 * * *'
|
||||
displayName: Daily midnight build
|
||||
branches:
|
||||
include:
|
||||
- v14/dev
|
||||
- v15/dev
|
||||
# schedules:
|
||||
# - cron: '0 0 * * *'
|
||||
# displayName: Daily midnight build
|
||||
# branches:
|
||||
# include:
|
||||
# - v14/dev
|
||||
|
||||
variables:
|
||||
nodeVersion: 20
|
||||
|
||||
@@ -42,31 +42,42 @@ internal sealed class RequestRedirectService : RoutingServiceBase, IRequestRedir
|
||||
{
|
||||
requestedPath = requestedPath.EnsureStartsWith("/");
|
||||
|
||||
IPublishedContent? startItem = GetStartItem();
|
||||
|
||||
// must append the root content url segment if it is not hidden by config, because
|
||||
// the URL tracking is based on the actual URL, including the root content url segment
|
||||
if (_globalSettings.HideTopLevelNodeFromPath == false)
|
||||
if (_globalSettings.HideTopLevelNodeFromPath == false && startItem?.UrlSegment != null)
|
||||
{
|
||||
IPublishedContent? startItem = GetStartItem();
|
||||
if (startItem?.UrlSegment != null)
|
||||
{
|
||||
requestedPath = $"{startItem.UrlSegment.EnsureStartsWith("/")}{requestedPath}";
|
||||
}
|
||||
requestedPath = $"{startItem.UrlSegment.EnsureStartsWith("/")}{requestedPath}";
|
||||
}
|
||||
|
||||
var culture = _requestCultureService.GetRequestedCulture();
|
||||
|
||||
// append the configured domain content ID to the path if we have a domain bound request,
|
||||
// because URL tracking registers the tracked url like "{domain content ID}/{content path}"
|
||||
Uri contentRoute = GetDefaultRequestUri(requestedPath);
|
||||
DomainAndUri? domainAndUri = GetDomainAndUriForRoute(contentRoute);
|
||||
if (domainAndUri != null)
|
||||
// important: redirect URLs are always tracked without trailing slashes
|
||||
requestedPath = requestedPath.TrimEnd("/");
|
||||
IRedirectUrl? redirectUrl = _redirectUrlService.GetMostRecentRedirectUrl(requestedPath, culture);
|
||||
|
||||
// if a redirect URL was not found, try by appending the start item ID because URL tracking might have tracked
|
||||
// a redirect with "{root content ID}/{content path}"
|
||||
if (redirectUrl is null && startItem is not null)
|
||||
{
|
||||
requestedPath = GetContentRoute(domainAndUri, contentRoute);
|
||||
culture ??= domainAndUri.Culture;
|
||||
redirectUrl = _redirectUrlService.GetMostRecentRedirectUrl($"{startItem.Id}{requestedPath}", culture);
|
||||
}
|
||||
|
||||
// still no redirect URL found - try looking for a configured domain if we have a domain bound request,
|
||||
// because URL tracking might have tracked a redirect with "{domain content ID}/{content path}"
|
||||
if (redirectUrl is null)
|
||||
{
|
||||
Uri contentRoute = GetDefaultRequestUri(requestedPath);
|
||||
DomainAndUri? domainAndUri = GetDomainAndUriForRoute(contentRoute);
|
||||
if (domainAndUri is not null)
|
||||
{
|
||||
requestedPath = GetContentRoute(domainAndUri, contentRoute);
|
||||
culture ??= domainAndUri.Culture;
|
||||
redirectUrl = _redirectUrlService.GetMostRecentRedirectUrl(requestedPath, culture);
|
||||
}
|
||||
}
|
||||
|
||||
// important: redirect URLs are always tracked without trailing slashes
|
||||
IRedirectUrl? redirectUrl = _redirectUrlService.GetMostRecentRedirectUrl(requestedPath.TrimEnd("/"), culture);
|
||||
IPublishedContent? content = redirectUrl != null
|
||||
? _apiPublishedContentCache.GetById(redirectUrl.ContentKey)
|
||||
: null;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.DataType;
|
||||
@@ -7,10 +8,12 @@ using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.DataType;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessDataTypes)]
|
||||
public class CopyDataTypeController : DataTypeControllerBase
|
||||
{
|
||||
private readonly IDataTypeService _dataTypeService;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Asp.Versioning;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.Factories;
|
||||
@@ -8,10 +9,12 @@ using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.DataType;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessDataTypes)]
|
||||
public class CreateDataTypeController : DataTypeControllerBase
|
||||
{
|
||||
private readonly IDataTypeService _dataTypeService;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Asp.Versioning;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Core;
|
||||
@@ -6,10 +7,12 @@ using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.DataType;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessDataTypes)]
|
||||
public class DeleteDataTypeController : DataTypeControllerBase
|
||||
{
|
||||
private readonly IDataTypeService _dataTypeService;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.DataType;
|
||||
@@ -7,10 +8,12 @@ using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.DataType;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessDataTypes)]
|
||||
public class MoveDataTypeController : DataTypeControllerBase
|
||||
{
|
||||
private readonly IDataTypeService _dataTypeService;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Asp.Versioning;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.Factories;
|
||||
@@ -8,10 +9,12 @@ using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.DataType;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessDataTypes)]
|
||||
public class UpdateDataTypeController : DataTypeControllerBase
|
||||
{
|
||||
private readonly IDataTypeService _dataTypeService;
|
||||
|
||||
+3
@@ -1,14 +1,17 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.Factories;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.DocumentType;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)]
|
||||
public class ExportDocumentTypeController : DocumentTypeControllerBase
|
||||
{
|
||||
private readonly IContentTypeService _contentTypeService;
|
||||
|
||||
+3
-1
@@ -1,17 +1,19 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.DocumentType;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Membership;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Services.ImportExport;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.DocumentType;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)]
|
||||
public class ImportExistingDocumentTypeController : DocumentTypeControllerBase
|
||||
{
|
||||
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
|
||||
|
||||
+3
-1
@@ -1,17 +1,19 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.DocumentType;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Membership;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Services.ImportExport;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.DocumentType;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)]
|
||||
public class ImportNewDocumentTypeController : DocumentTypeControllerBase
|
||||
{
|
||||
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.Factories;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.MediaType;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessMediaTypes)]
|
||||
public class ExportMediaTypeController : MediaTypeControllerBase
|
||||
{
|
||||
private readonly IMediaTypeService _mediaTypeService;
|
||||
|
||||
+3
-1
@@ -1,17 +1,19 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.MediaType;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Membership;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Services.ImportExport;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.MediaType;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessMediaTypes)]
|
||||
public class ImportExistingMediaTypeController : MediaTypeControllerBase
|
||||
{
|
||||
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
|
||||
|
||||
+3
-2
@@ -1,18 +1,19 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.Controllers.DocumentType;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.MediaType;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Membership;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Services.ImportExport;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.MediaType;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessMediaTypes)]
|
||||
public class ImportNewMediaTypeController : MediaTypeControllerBase
|
||||
{
|
||||
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.Controllers.DocumentType;
|
||||
using Umbraco.Cms.Api.Management.Routing;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.MediaType;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
|
||||
@@ -34,6 +34,8 @@ namespace Umbraco.Cms.Api.Management.Controllers.Security;
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public class BackOfficeController : SecurityControllerBase
|
||||
{
|
||||
private static long? _loginDurationAverage;
|
||||
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
private readonly IBackOfficeSignInManager _backOfficeSignInManager;
|
||||
private readonly IBackOfficeUserManager _backOfficeUserManager;
|
||||
@@ -72,45 +74,65 @@ public class BackOfficeController : SecurityControllerBase
|
||||
[Authorize(Policy = AuthorizationPolicies.DenyLocalLoginIfConfigured)]
|
||||
public async Task<IActionResult> Login(CancellationToken cancellationToken, LoginRequestModel model)
|
||||
{
|
||||
IdentitySignInResult result = await _backOfficeSignInManager.PasswordSignInAsync(
|
||||
model.Username, model.Password, true, true);
|
||||
// Start a timed scope to ensure failed responses return is a consistent time
|
||||
var loginDuration = Math.Max(_loginDurationAverage ?? _securitySettings.Value.UserDefaultFailedLoginDurationInMilliseconds, _securitySettings.Value.UserMinimumFailedLoginDurationInMilliseconds);
|
||||
await using var timedScope = new TimedScope(loginDuration, cancellationToken);
|
||||
|
||||
if (result.IsNotAllowed)
|
||||
IdentitySignInResult result = await _backOfficeSignInManager.PasswordSignInAsync(model.Username, model.Password, true, true);
|
||||
if (result.Succeeded is false)
|
||||
{
|
||||
return StatusCode(StatusCodes.Status403Forbidden, new ProblemDetailsBuilder()
|
||||
.WithTitle("User is not allowed")
|
||||
.WithDetail("The operation is not allowed on the user")
|
||||
.Build());
|
||||
}
|
||||
|
||||
if (result.IsLockedOut)
|
||||
{
|
||||
return StatusCode(StatusCodes.Status403Forbidden, new ProblemDetailsBuilder()
|
||||
.WithTitle("User is locked")
|
||||
.WithDetail("The user is locked, and need to be unlocked before more login attempts can be executed.")
|
||||
.Build());
|
||||
}
|
||||
|
||||
if(result.RequiresTwoFactor)
|
||||
{
|
||||
string? twofactorView = _backOfficeTwoFactorOptions.GetTwoFactorView(model.Username);
|
||||
BackOfficeIdentityUser? attemptingUser = await _backOfficeUserManager.FindByNameAsync(model.Username);
|
||||
IEnumerable<string> enabledProviders = (await _userTwoFactorLoginService.GetProviderNamesAsync(attemptingUser!.Key)).Result.Where(x=>x.IsEnabledOnUser).Select(x=>x.ProviderName);
|
||||
return StatusCode(StatusCodes.Status402PaymentRequired, new RequiresTwoFactorResponseModel()
|
||||
// TODO: The result should include the user and whether the credentials were valid to avoid these additional checks
|
||||
BackOfficeIdentityUser? user = await _backOfficeUserManager.FindByNameAsync(model.Username.Trim()); // Align with UmbracoSignInManager and trim username!
|
||||
if (user is not null &&
|
||||
await _backOfficeUserManager.CheckPasswordAsync(user, model.Password))
|
||||
{
|
||||
TwoFactorLoginView = twofactorView,
|
||||
EnabledTwoFactorProviderNames = enabledProviders
|
||||
});
|
||||
// The credentials were correct, so cancel timed scope and provide a more detailed failure response
|
||||
await timedScope.CancelAsync();
|
||||
|
||||
if (result.IsNotAllowed)
|
||||
{
|
||||
return StatusCode(StatusCodes.Status403Forbidden, new ProblemDetailsBuilder()
|
||||
.WithTitle("User is not allowed")
|
||||
.WithDetail("The operation is not allowed on the user")
|
||||
.Build());
|
||||
}
|
||||
|
||||
if (result.IsLockedOut)
|
||||
{
|
||||
return StatusCode(StatusCodes.Status403Forbidden, new ProblemDetailsBuilder()
|
||||
.WithTitle("User is locked")
|
||||
.WithDetail("The user is locked, and need to be unlocked before more login attempts can be executed.")
|
||||
.Build());
|
||||
}
|
||||
|
||||
if (result.RequiresTwoFactor)
|
||||
{
|
||||
string? twofactorView = _backOfficeTwoFactorOptions.GetTwoFactorView(model.Username);
|
||||
IEnumerable<string> enabledProviders = (await _userTwoFactorLoginService.GetProviderNamesAsync(user.Key)).Result.Where(x => x.IsEnabledOnUser).Select(x => x.ProviderName);
|
||||
|
||||
return StatusCode(StatusCodes.Status402PaymentRequired, new RequiresTwoFactorResponseModel()
|
||||
{
|
||||
TwoFactorLoginView = twofactorView,
|
||||
EnabledTwoFactorProviderNames = enabledProviders
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return StatusCode(StatusCodes.Status401Unauthorized, new ProblemDetailsBuilder()
|
||||
.WithTitle("Invalid credentials")
|
||||
.WithDetail("The provided credentials are invalid. User has not been signed in.")
|
||||
.Build());
|
||||
}
|
||||
|
||||
if (result.Succeeded)
|
||||
{
|
||||
return Ok();
|
||||
}
|
||||
return StatusCode(StatusCodes.Status401Unauthorized, new ProblemDetailsBuilder()
|
||||
.WithTitle("Invalid credentials")
|
||||
.WithDetail("The provided credentials are invalid. User has not been signed in.")
|
||||
.Build());
|
||||
// Set initial or update average (successful) login duration
|
||||
_loginDurationAverage = _loginDurationAverage is long average
|
||||
? (average + (long)timedScope.Elapsed.TotalMilliseconds) / 2
|
||||
: (long)timedScope.Elapsed.TotalMilliseconds;
|
||||
|
||||
// Cancel the timed scope (we don't want to unnecessarily wait on a successful response)
|
||||
await timedScope.CancelAsync();
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
|
||||
+4
-1
@@ -1,4 +1,4 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Common.Builders;
|
||||
using Umbraco.Cms.Api.Management.Routing;
|
||||
@@ -17,6 +17,9 @@ public abstract class TemporaryFileControllerBase : ManagementApiControllerBase
|
||||
.WithTitle("File extension not allowed")
|
||||
.WithDetail("The file extension is not allowed.")
|
||||
.Build()),
|
||||
TemporaryFileOperationStatus.InvalidFileName => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("The provided file name is not valid")
|
||||
.Build()),
|
||||
TemporaryFileOperationStatus.KeyAlreadyUsed => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("Key already used")
|
||||
.WithDetail("The specified key is already used.")
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<Description>Adds imaging support using ImageSharp/ImageSharp.Web version 2 to Umbraco CMS.</Description>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="SixLabors.ImageSharp" VersionOverride="[2.1.9, 3)" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" VersionOverride="[2.1.10, 3)" />
|
||||
<PackageReference Include="SixLabors.ImageSharp.Web" VersionOverride="[2.0.2, 3)" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ public class RepositoryCachePolicyOptions
|
||||
public RepositoryCachePolicyOptions(Func<int> performCount)
|
||||
{
|
||||
PerformCount = performCount;
|
||||
CacheNullValues = false;
|
||||
GetAllCacheValidateCount = true;
|
||||
GetAllCacheAllowZeroCount = false;
|
||||
}
|
||||
@@ -21,6 +22,7 @@ public class RepositoryCachePolicyOptions
|
||||
public RepositoryCachePolicyOptions()
|
||||
{
|
||||
PerformCount = null;
|
||||
CacheNullValues = false;
|
||||
GetAllCacheValidateCount = false;
|
||||
GetAllCacheAllowZeroCount = false;
|
||||
}
|
||||
@@ -30,6 +32,11 @@ public class RepositoryCachePolicyOptions
|
||||
/// </summary>
|
||||
public Func<int>? PerformCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// True if the Get method will cache null results so that the db is not hit for repeated lookups
|
||||
/// </summary>
|
||||
public bool CacheNullValues { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// True/false as to validate the total item count when all items are returned from cache, the default is true but this
|
||||
/// means that a db lookup will occur - though that lookup will probably be significantly less expensive than the
|
||||
|
||||
@@ -34,7 +34,7 @@ public class TypeFinder : ITypeFinder
|
||||
"ServiceStack.", "SqlCE4Umbraco,", "Superpower,", // used by Serilog
|
||||
"System.", "TidyNet,", "TidyNet.", "WebDriver,", "itextsharp,", "mscorlib,", "NUnit,", "NUnit.", "NUnit3.",
|
||||
"Selenium.", "ImageProcessor", "MiniProfiler.", "Owin,", "SQLite",
|
||||
"ReSharperTestRunner", "ReSharperTestRunner32", "ReSharperTestRunner64", // These are used by the Jetbrains Rider IDE and Visual Studio ReSharper Extension
|
||||
"ReSharperTestRunner", "ReSharperTestRunner32", "ReSharperTestRunner64", "ReSharperTestRunnerArm32", "ReSharperTestRunnerArm64", // These are used by the Jetbrains Rider IDE and Visual Studio ReSharper Extension
|
||||
};
|
||||
|
||||
private static readonly ConcurrentDictionary<string, Type?> TypeNamesCache = new();
|
||||
|
||||
@@ -16,6 +16,7 @@ public class ModelsBuilderSettings
|
||||
internal const string StaticModelsDirectory = "~/umbraco/models";
|
||||
internal const bool StaticAcceptUnsafeModelsDirectory = false;
|
||||
internal const int StaticDebugLevel = 0;
|
||||
internal const bool StaticIncludeVersionNumberInGeneratedModels = true;
|
||||
private bool _flagOutOfDateModels = true;
|
||||
|
||||
/// <summary>
|
||||
@@ -78,4 +79,16 @@ public class ModelsBuilderSettings
|
||||
/// <remarks>0 means minimal (safe on live site), anything else means more and more details (maybe not safe).</remarks>
|
||||
[DefaultValue(StaticDebugLevel)]
|
||||
public int DebugLevel { get; set; } = StaticDebugLevel;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the version number should be included in generated models.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// By default this is written to the <see cref="System.CodeDom.Compiler.GeneratedCodeAttribute"/> output in
|
||||
/// generated code for each property of the model. This can be useful for debugging purposes but isn't essential,
|
||||
/// and it has the causes the generated code to change every time Umbraco is upgraded. In turn, this leads
|
||||
/// to unnecessary code file changes that need to be checked into source control. Default is <c>true</c>.
|
||||
/// </remarks>
|
||||
[DefaultValue(StaticIncludeVersionNumberInGeneratedModels)]
|
||||
public bool IncludeVersionNumberInGeneratedModels { get; set; } = StaticIncludeVersionNumberInGeneratedModels;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// See LICENSE for more details.
|
||||
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Umbraco.Cms.Core.Configuration.Models;
|
||||
|
||||
@@ -19,12 +20,16 @@ public class SecuritySettings
|
||||
internal const bool StaticAllowEditInvariantFromNonDefault = false;
|
||||
internal const bool StaticAllowConcurrentLogins = false;
|
||||
internal const string StaticAuthCookieName = "UMB_UCONTEXT";
|
||||
internal const bool StaticUsernameIsEmail = true;
|
||||
internal const bool StaticMemberRequireUniqueEmail = true;
|
||||
|
||||
internal const string StaticAllowedUserNameCharacters =
|
||||
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+\\";
|
||||
|
||||
internal const int StaticMemberDefaultLockoutTimeInMinutes = 30 * 24 * 60;
|
||||
internal const int StaticUserDefaultLockoutTimeInMinutes = 30 * 24 * 60;
|
||||
private const long StaticUserDefaultFailedLoginDurationInMilliseconds = 1000;
|
||||
private const long StaticUserMinimumFailedLoginDurationInMilliseconds = 250;
|
||||
internal const string StaticAuthorizeCallbackPathName = "/umbraco/oauth_complete";
|
||||
internal const string StaticAuthorizeCallbackLogoutPathName = "/umbraco/logout";
|
||||
internal const string StaticAuthorizeCallbackErrorPathName = "/umbraco/error";
|
||||
@@ -61,7 +66,14 @@ public class SecuritySettings
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the user's email address is to be considered as their username.
|
||||
/// </summary>
|
||||
public bool UsernameIsEmail { get; set; } = true;
|
||||
[DefaultValue(StaticUsernameIsEmail)]
|
||||
public bool UsernameIsEmail { get; set; } = StaticUsernameIsEmail;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the member's email address must be unique.
|
||||
/// </summary>
|
||||
[DefaultValue(StaticMemberRequireUniqueEmail)]
|
||||
public bool MemberRequireUniqueEmail { get; set; } = StaticMemberRequireUniqueEmail;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the set of allowed characters for a username
|
||||
@@ -108,6 +120,30 @@ public class SecuritySettings
|
||||
[DefaultValue(StaticAllowConcurrentLogins)]
|
||||
public bool AllowConcurrentLogins { get; set; } = StaticAllowConcurrentLogins;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the default duration (in milliseconds) of failed login attempts.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The default duration (in milliseconds) of failed login attempts.
|
||||
/// </value>
|
||||
/// <remarks>
|
||||
/// The user login endpoint ensures that failed login attempts take at least as long as the average successful login.
|
||||
/// However, if no successful logins have occurred, this value is used as the default duration.
|
||||
/// </remarks>
|
||||
[Range(0, long.MaxValue)]
|
||||
[DefaultValue(StaticUserDefaultFailedLoginDurationInMilliseconds)]
|
||||
public long UserDefaultFailedLoginDurationInMilliseconds { get; set; } = StaticUserDefaultFailedLoginDurationInMilliseconds;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the minimum duration (in milliseconds) of failed login attempts.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The minimum duration (in milliseconds) of failed login attempts.
|
||||
/// </value>
|
||||
[Range(0, long.MaxValue)]
|
||||
[DefaultValue(StaticUserMinimumFailedLoginDurationInMilliseconds)]
|
||||
public long UserMinimumFailedLoginDurationInMilliseconds { get; set; } = StaticUserMinimumFailedLoginDurationInMilliseconds;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value of the back-office host URI. Use this when running the back-office client and the Management API on different hosts. Leave empty when running both on the same host.
|
||||
/// </summary>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
@using Umbraco.Extensions
|
||||
|
||||
@{
|
||||
var isLoggedIn = Context.User?.Identity?.IsAuthenticated ?? false;
|
||||
var isLoggedIn = Context.User.GetMemberIdentity()?.IsAuthenticated ?? false;
|
||||
var logoutModel = new PostRedirectModel();
|
||||
// You can modify this to redirect to a different URL instead of the current one
|
||||
logoutModel.RedirectUrl = null;
|
||||
@@ -15,7 +15,7 @@
|
||||
{
|
||||
<div class="login-status">
|
||||
|
||||
<p>Welcome back <strong>@Context?.User?.Identity?.Name</strong>!</p>
|
||||
<p>Welcome back <strong>@Context.User?.GetMemberIdentity()?.Name</strong>!</p>
|
||||
|
||||
@using (Html.BeginUmbracoForm<UmbLoginStatusController>("HandleLogout", new { RedirectUrl = logoutModel.RedirectUrl }))
|
||||
{
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
namespace Umbraco.Cms.Core.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Describes the options available with publishing a content branch for force publishing.
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum PublishBranchFilter
|
||||
{
|
||||
/// <summary>
|
||||
/// The default behavior is to publish only the published content that has changed.
|
||||
/// </summary>
|
||||
Default = 0,
|
||||
|
||||
/// <summary>
|
||||
/// For publishing a branch, publish all changed content, including content that is not published.
|
||||
/// </summary>
|
||||
IncludeUnpublished = 1,
|
||||
|
||||
/// <summary>
|
||||
/// For publishing a branch, force republishing of all published content, including content that has not changed.
|
||||
/// </summary>
|
||||
ForceRepublish = 2,
|
||||
|
||||
/// <summary>
|
||||
/// For publishing a branch, publish all content, including content that is not published and content that has not changed.
|
||||
/// </summary>
|
||||
All = IncludeUnpublished | ForceRepublish,
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
namespace Umbraco.Cms.Core.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies options for publishing notifcations when saving.
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum PublishNotificationSaveOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Do not publish any notifications.
|
||||
/// </summary>
|
||||
None = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Only publish the saving notification.
|
||||
/// </summary>
|
||||
Saving = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Only publish the saved notification.
|
||||
/// </summary>
|
||||
Saved = 2,
|
||||
|
||||
/// <summary>
|
||||
/// Publish all the notifications.
|
||||
/// </summary>
|
||||
All = Saving | Saved,
|
||||
}
|
||||
@@ -33,7 +33,7 @@ public interface ITagQuery
|
||||
/// <summary>
|
||||
/// Gets all document tags.
|
||||
/// </summary>
|
||||
/// /// <remarks>
|
||||
/// <remarks>
|
||||
/// If no culture is specified, it retrieves tags with an invariant culture.
|
||||
/// If a culture is specified, it only retrieves tags for that culture.
|
||||
/// Use "*" to retrieve tags for all cultures.
|
||||
|
||||
@@ -216,6 +216,15 @@ public interface IMemberService : IMembershipMemberService, IContentServiceBase<
|
||||
/// </returns>
|
||||
IMember CreateMemberWithIdentity(string username, string email, string name, IMemberType memberType);
|
||||
|
||||
/// <summary>
|
||||
/// Saves an <see cref="IMembershipUser" />
|
||||
/// </summary>
|
||||
/// <remarks>An <see cref="IMembershipUser" /> can be of type <see cref="IMember" /> or <see cref="IUser" /></remarks>
|
||||
/// <param name="member"><see cref="IMember" /> or <see cref="IUser" /> to Save</param>
|
||||
/// <param name="publishNotificationSaveOptions"> Enum for deciding which notifications to publish.</param>
|
||||
/// <param name="userId">Id of the User saving the Member</param>
|
||||
Attempt<OperationResult?> Save(IMember member, PublishNotificationSaveOptions publishNotificationSaveOptions, int userId = Constants.Security.SuperUserId) => Save(member, userId);
|
||||
|
||||
/// <summary>
|
||||
/// Saves a single <see cref="IMember" /> object
|
||||
/// </summary>
|
||||
@@ -267,6 +276,21 @@ public interface IMemberService : IMembershipMemberService, IContentServiceBase<
|
||||
/// </returns>
|
||||
IMember? GetById(int id);
|
||||
|
||||
/// <summary>
|
||||
/// Get an list of <see cref="IMember"/> for all members with the specified email.
|
||||
/// </summary>
|
||||
/// <param name="email">Email to use for retrieval</param>
|
||||
/// <returns>
|
||||
/// <see cref="IEnumerable{IMember}" />
|
||||
/// </returns>
|
||||
IEnumerable<IMember> GetMembersByEmail(string email)
|
||||
=>
|
||||
// TODO (V16): Remove this default implementation.
|
||||
// The following is very inefficient, but will return the correct data, so probably better than throwing a NotImplementedException
|
||||
// in the default implentation here, for, presumably rare, cases where a custom IMemberService implementation has been registered and
|
||||
// does not override this method.
|
||||
GetAllMembers().Where(x => x.Email.Equals(email));
|
||||
|
||||
/// <summary>
|
||||
/// Gets all Members for the specified MemberType alias
|
||||
/// </summary>
|
||||
|
||||
@@ -408,16 +408,23 @@ namespace Umbraco.Cms.Core.Services
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get an <see cref="IMember"/> by email
|
||||
/// Get an <see cref="IMember"/> by email. If RequireUniqueEmailForMembers is set to false, then the first member found with the specified email will be returned.
|
||||
/// </summary>
|
||||
/// <param name="email">Email to use for retrieval</param>
|
||||
/// <returns><see cref="IMember"/></returns>
|
||||
public IMember? GetByEmail(string email)
|
||||
public IMember? GetByEmail(string email) => GetMembersByEmail(email).FirstOrDefault();
|
||||
|
||||
/// <summary>
|
||||
/// Get an list of <see cref="IMember"/> for all members with the specified email.
|
||||
/// </summary>
|
||||
/// <param name="email">Email to use for retrieval</param>
|
||||
/// <returns><see cref="IEnumerable{IMember}"/></returns>
|
||||
public IEnumerable<IMember> GetMembersByEmail(string email)
|
||||
{
|
||||
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
|
||||
scope.ReadLock(Constants.Locks.MemberTree);
|
||||
IQuery<IMember> query = Query<IMember>().Where(x => x.Email.Equals(email));
|
||||
return _memberRepository.Get(query)?.FirstOrDefault();
|
||||
return _memberRepository.Get(query);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -765,6 +772,9 @@ namespace Umbraco.Cms.Core.Services
|
||||
|
||||
/// <inheritdoc />
|
||||
public Attempt<OperationResult?> Save(IMember member, int userId = Constants.Security.SuperUserId)
|
||||
=> Save(member, PublishNotificationSaveOptions.All, userId);
|
||||
|
||||
public Attempt<OperationResult?> Save(IMember member, PublishNotificationSaveOptions publishNotificationSaveOptions, int userId = Constants.Security.SuperUserId)
|
||||
{
|
||||
// trimming username and email to make sure we have no trailing space
|
||||
member.Username = member.Username.Trim();
|
||||
@@ -773,11 +783,15 @@ namespace Umbraco.Cms.Core.Services
|
||||
EventMessages evtMsgs = EventMessagesFactory.Get();
|
||||
|
||||
using ICoreScope scope = ScopeProvider.CreateCoreScope();
|
||||
var savingNotification = new MemberSavingNotification(member, evtMsgs);
|
||||
if (scope.Notifications.PublishCancelable(savingNotification))
|
||||
MemberSavingNotification? savingNotification = null;
|
||||
if (publishNotificationSaveOptions.HasFlag(PublishNotificationSaveOptions.Saving))
|
||||
{
|
||||
scope.Complete();
|
||||
return OperationResult.Attempt.Cancel(evtMsgs);
|
||||
savingNotification = new MemberSavingNotification(member, evtMsgs);
|
||||
if (scope.Notifications.PublishCancelable(savingNotification))
|
||||
{
|
||||
scope.Complete();
|
||||
return OperationResult.Attempt.Cancel(evtMsgs);
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(member.Name))
|
||||
@@ -789,7 +803,13 @@ namespace Umbraco.Cms.Core.Services
|
||||
|
||||
_memberRepository.Save(member);
|
||||
|
||||
scope.Notifications.Publish(new MemberSavedNotification(member, evtMsgs).WithStateFrom(savingNotification));
|
||||
if (publishNotificationSaveOptions.HasFlag(PublishNotificationSaveOptions.Saved))
|
||||
{
|
||||
scope.Notifications.Publish(
|
||||
savingNotification is null
|
||||
? new MemberSavedNotification(member, evtMsgs)
|
||||
: new MemberSavedNotification(member, evtMsgs).WithStateFrom(savingNotification));
|
||||
}
|
||||
|
||||
Audit(AuditType.Save, 0, member.Id);
|
||||
|
||||
|
||||
@@ -6,5 +6,6 @@ public enum TemporaryFileOperationStatus
|
||||
FileExtensionNotAllowed = 1,
|
||||
KeyAlreadyUsed = 2,
|
||||
NotFound = 3,
|
||||
UploadBlocked
|
||||
UploadBlocked = 4,
|
||||
InvalidFileName = 5,
|
||||
}
|
||||
|
||||
@@ -45,7 +45,6 @@ internal sealed class TemporaryFileService : ITemporaryFileService
|
||||
return Attempt.FailWithStatus<TemporaryFileModel?, TemporaryFileOperationStatus>(TemporaryFileOperationStatus.KeyAlreadyUsed, null);
|
||||
}
|
||||
|
||||
|
||||
await using Stream dataStream = createModel.OpenReadStream();
|
||||
dataStream.Seek(0, SeekOrigin.Begin);
|
||||
if (_fileStreamSecurityValidator.IsConsideredSafe(dataStream) is false)
|
||||
@@ -53,13 +52,12 @@ internal sealed class TemporaryFileService : ITemporaryFileService
|
||||
return Attempt.FailWithStatus<TemporaryFileModel?, TemporaryFileOperationStatus>(TemporaryFileOperationStatus.UploadBlocked, null);
|
||||
}
|
||||
|
||||
|
||||
temporaryFileModel = new TemporaryFileModel
|
||||
{
|
||||
Key = createModel.Key,
|
||||
FileName = createModel.FileName,
|
||||
OpenReadStream = createModel.OpenReadStream,
|
||||
AvailableUntil = DateTime.Now.Add(_runtimeSettings.TemporaryFileLifeTime)
|
||||
AvailableUntil = DateTime.Now.Add(_runtimeSettings.TemporaryFileLifeTime),
|
||||
};
|
||||
|
||||
await _temporaryFileRepository.SaveAsync(temporaryFileModel);
|
||||
@@ -68,17 +66,29 @@ internal sealed class TemporaryFileService : ITemporaryFileService
|
||||
}
|
||||
|
||||
private TemporaryFileOperationStatus Validate(TemporaryFileModelBase temporaryFileModel)
|
||||
=> IsAllowedFileExtension(temporaryFileModel) == false
|
||||
? TemporaryFileOperationStatus.FileExtensionNotAllowed
|
||||
: TemporaryFileOperationStatus.Success;
|
||||
|
||||
private bool IsAllowedFileExtension(TemporaryFileModelBase temporaryFileModel)
|
||||
{
|
||||
var extension = Path.GetExtension(temporaryFileModel.FileName)[1..];
|
||||
if (IsAllowedFileExtension(temporaryFileModel.FileName) == false)
|
||||
{
|
||||
return TemporaryFileOperationStatus.FileExtensionNotAllowed;
|
||||
}
|
||||
|
||||
if (IsValidFileName(temporaryFileModel.FileName) == false)
|
||||
{
|
||||
return TemporaryFileOperationStatus.InvalidFileName;
|
||||
}
|
||||
|
||||
return TemporaryFileOperationStatus.Success;
|
||||
}
|
||||
|
||||
private bool IsAllowedFileExtension(string fileName)
|
||||
{
|
||||
var extension = Path.GetExtension(fileName)[1..];
|
||||
return _contentSettings.IsFileAllowedForUpload(extension);
|
||||
}
|
||||
|
||||
private static bool IsValidFileName(string fileName) =>
|
||||
!string.IsNullOrEmpty(fileName) && fileName.IndexOfAny(Path.GetInvalidFileNameChars()) < 0;
|
||||
|
||||
public async Task<Attempt<TemporaryFileModel?, TemporaryFileOperationStatus>> DeleteAsync(Guid key)
|
||||
{
|
||||
TemporaryFileModel? model = await _temporaryFileRepository.GetAsync(key);
|
||||
|
||||
@@ -87,6 +87,6 @@ public static class UserServiceExtensions
|
||||
});
|
||||
}
|
||||
|
||||
[Obsolete("Use IUserService.Get that takes a Guid instead. Scheduled for removal in V15.")]
|
||||
[Obsolete("Use IUserService.GetAsync that takes a Guid instead. Scheduled for removal in V15.")]
|
||||
public static IUser? GetByKey(this IUserService userService, Guid key) => userService.GetAsync(key).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
namespace Umbraco.Cms.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Makes a code block timed (take at least a certain amount of time). This class cannot be inherited.
|
||||
/// </summary>
|
||||
public sealed class TimedScope : IDisposable, IAsyncDisposable
|
||||
{
|
||||
private readonly TimeSpan _duration;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly CancellationTokenSource _cancellationTokenSource;
|
||||
private readonly long _startingTimestamp;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the elapsed time.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The elapsed time.
|
||||
/// </value>
|
||||
public TimeSpan Elapsed
|
||||
=> _timeProvider.GetElapsedTime(_startingTimestamp);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the remaining time.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The remaining time.
|
||||
/// </value>
|
||||
public TimeSpan Remaining
|
||||
=> TryGetRemaining(out TimeSpan remaining) ? remaining : TimeSpan.Zero;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TimedScope" /> class.
|
||||
/// </summary>
|
||||
/// <param name="millisecondsDuration">The number of milliseconds the scope should at least take.</param>
|
||||
public TimedScope(long millisecondsDuration)
|
||||
: this(TimeSpan.FromMilliseconds(millisecondsDuration))
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TimedScope" /> class.
|
||||
/// </summary>
|
||||
/// <param name="millisecondsDuration">The number of milliseconds the scope should at least take.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
public TimedScope(long millisecondsDuration, CancellationToken cancellationToken)
|
||||
: this(TimeSpan.FromMilliseconds(millisecondsDuration), cancellationToken)
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TimedScope" /> class.
|
||||
/// </summary>
|
||||
/// <param name="millisecondsDuration">The number of milliseconds the scope should at least take.</param>
|
||||
/// <param name="timeProvider">The time provider.</param>
|
||||
public TimedScope(long millisecondsDuration, TimeProvider timeProvider)
|
||||
: this(TimeSpan.FromMilliseconds(millisecondsDuration), timeProvider)
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TimedScope" /> class.
|
||||
/// </summary>
|
||||
/// <param name="millisecondsDuration">The number of milliseconds the scope should at least take.</param>
|
||||
/// <param name="timeProvider">The time provider.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
public TimedScope(long millisecondsDuration, TimeProvider timeProvider, CancellationToken cancellationToken)
|
||||
: this(TimeSpan.FromMilliseconds(millisecondsDuration), timeProvider, cancellationToken)
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TimedScope"/> class.
|
||||
/// </summary>
|
||||
/// <param name="duration">The duration the scope should at least take.</param>
|
||||
public TimedScope(TimeSpan duration)
|
||||
: this(duration, TimeProvider.System)
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TimedScope" /> class.
|
||||
/// </summary>
|
||||
/// <param name="duration">The duration the scope should at least take.</param>
|
||||
/// <param name="timeProvider">The time provider.</param>
|
||||
public TimedScope(TimeSpan duration, TimeProvider timeProvider)
|
||||
: this(duration, timeProvider, new CancellationTokenSource())
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TimedScope" /> class.
|
||||
/// </summary>
|
||||
/// <param name="duration">The duration the scope should at least take.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
public TimedScope(TimeSpan duration, CancellationToken cancellationToken)
|
||||
: this(duration, TimeProvider.System, cancellationToken)
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TimedScope" /> class.
|
||||
/// </summary>
|
||||
/// <param name="duration">The duration the scope should at least take.</param>
|
||||
/// <param name="timeProvider">The time provider.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
public TimedScope(TimeSpan duration, TimeProvider timeProvider, CancellationToken cancellationToken)
|
||||
: this(duration, timeProvider, CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
|
||||
{ }
|
||||
|
||||
private TimedScope(TimeSpan duration, TimeProvider timeProvider, CancellationTokenSource cancellationTokenSource)
|
||||
{
|
||||
_duration = duration;
|
||||
_timeProvider = timeProvider;
|
||||
_cancellationTokenSource = cancellationTokenSource;
|
||||
_startingTimestamp = timeProvider.GetTimestamp();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cancels the timed scope.
|
||||
/// </summary>
|
||||
public void Cancel()
|
||||
=> _cancellationTokenSource.Cancel();
|
||||
|
||||
/// <summary>
|
||||
/// Cancels the timed scope asynchronously.
|
||||
/// </summary>
|
||||
public async Task CancelAsync()
|
||||
=> await _cancellationTokenSource.CancelAsync().ConfigureAwait(false);
|
||||
|
||||
/// <summary>
|
||||
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This will block using <see cref="Thread.Sleep(TimeSpan)" /> until the remaining time has elapsed, if not cancelled.
|
||||
/// </remarks>
|
||||
public void Dispose()
|
||||
{
|
||||
if (_cancellationTokenSource.IsCancellationRequested is false &&
|
||||
TryGetRemaining(out TimeSpan remaining))
|
||||
{
|
||||
Thread.Sleep(remaining);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources asynchronously.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// A task that represents the asynchronous dispose operation.
|
||||
/// </returns>
|
||||
/// <remarks>
|
||||
/// This will delay using <see cref="Task.Delay(TimeSpan, TimeProvider, CancellationToken)" /> until the remaining time has elapsed, if not cancelled.
|
||||
/// </remarks>
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (_cancellationTokenSource.IsCancellationRequested is false &&
|
||||
TryGetRemaining(out TimeSpan remaining))
|
||||
{
|
||||
await Task.Delay(remaining, _timeProvider, _cancellationTokenSource.Token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryGetRemaining(out TimeSpan remaining)
|
||||
{
|
||||
remaining = _duration.Subtract(Elapsed);
|
||||
|
||||
return remaining > TimeSpan.Zero;
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,8 @@ public class DefaultRepositoryCachePolicy<TEntity, TId> : RepositoryCachePolicyB
|
||||
private static readonly TEntity[] _emptyEntities = new TEntity[0]; // const
|
||||
private readonly RepositoryCachePolicyOptions _options;
|
||||
|
||||
private const string NullRepresentationInCache = "*NULL*";
|
||||
|
||||
public DefaultRepositoryCachePolicy(IAppPolicyCache cache, IScopeAccessor scopeAccessor, RepositoryCachePolicyOptions options)
|
||||
: base(cache, scopeAccessor) =>
|
||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
@@ -116,6 +118,7 @@ public class DefaultRepositoryCachePolicy<TEntity, TId> : RepositoryCachePolicyB
|
||||
{
|
||||
// whatever happens, clear the cache
|
||||
var cacheKey = GetEntityCacheKey(entity.Id);
|
||||
|
||||
Cache.Clear(cacheKey);
|
||||
|
||||
// if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared
|
||||
@@ -127,20 +130,36 @@ public class DefaultRepositoryCachePolicy<TEntity, TId> : RepositoryCachePolicyB
|
||||
public override TEntity? Get(TId? id, Func<TId?, TEntity?> performGet, Func<TId[]?, IEnumerable<TEntity>?> performGetAll)
|
||||
{
|
||||
var cacheKey = GetEntityCacheKey(id);
|
||||
|
||||
TEntity? fromCache = Cache.GetCacheItem<TEntity>(cacheKey);
|
||||
|
||||
// if found in cache then return else fetch and cache
|
||||
if (fromCache != null)
|
||||
// If found in cache then return immediately.
|
||||
if (fromCache is not null)
|
||||
{
|
||||
return fromCache;
|
||||
}
|
||||
|
||||
// Because TEntity can never be a string, we will never be in a position where the proxy value collides withs a real value.
|
||||
// Therefore this point can only be reached if there is a proxy null value => becomes null when cast to TEntity above OR the item simply does not exist.
|
||||
// If we've cached a "null" value, return null.
|
||||
if (_options.CacheNullValues && Cache.GetCacheItem<string>(cacheKey) == NullRepresentationInCache)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Otherwise go to the database to retrieve.
|
||||
TEntity? entity = performGet(id);
|
||||
|
||||
if (entity != null && entity.HasIdentity)
|
||||
{
|
||||
// If we've found an identified entity, cache it for subsequent retrieval.
|
||||
InsertEntity(cacheKey, entity);
|
||||
}
|
||||
else if (entity is null && _options.CacheNullValues)
|
||||
{
|
||||
// If we've not found an entity, and we're caching null values, cache a "null" value.
|
||||
InsertNull(cacheKey);
|
||||
}
|
||||
|
||||
return entity;
|
||||
}
|
||||
@@ -248,6 +267,15 @@ public class DefaultRepositoryCachePolicy<TEntity, TId> : RepositoryCachePolicyB
|
||||
protected virtual void InsertEntity(string cacheKey, TEntity entity)
|
||||
=> Cache.Insert(cacheKey, () => entity, TimeSpan.FromMinutes(5), true);
|
||||
|
||||
protected virtual void InsertNull(string cacheKey)
|
||||
{
|
||||
// We can't actually cache a null value, as in doing so wouldn't be able to distinguish between
|
||||
// a value that does exist but isn't yet cached, or a value that has been explicitly cached with a null value.
|
||||
// Both would return null when we retrieve from the cache and we couldn't distinguish between the two.
|
||||
// So we cache a special value that represents null, and then we can check for that value when we retrieve from the cache.
|
||||
Cache.Insert(cacheKey, () => NullRepresentationInCache, TimeSpan.FromMinutes(5), true);
|
||||
}
|
||||
|
||||
protected virtual void InsertEntities(TId[]? ids, TEntity[]? entities)
|
||||
{
|
||||
if (ids?.Length == 0 && entities?.Length == 0 && _options.GetAllCacheAllowZeroCount)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using NPoco;
|
||||
using NPoco;
|
||||
using Umbraco.Cms.Infrastructure.Persistence;
|
||||
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
|
||||
using ColumnInfo = Umbraco.Cms.Infrastructure.Persistence.SqlSyntax.ColumnInfo;
|
||||
@@ -153,16 +153,26 @@ SELECT obj_Constraint.NAME AS 'constraintName'
|
||||
");
|
||||
var currentConstraintName = Database.ExecuteScalar<string>(constraintNameQuery);
|
||||
|
||||
|
||||
// only rename the constraint if necessary
|
||||
// Only rename the constraint if necessary.
|
||||
if (currentConstraintName == expectedConstraintName)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Sql<ISqlContext> renameConstraintQuery = Database.SqlContext.Sql(
|
||||
$"EXEC sp_rename N'{currentConstraintName}', N'{expectedConstraintName}', N'OBJECT'");
|
||||
Database.Execute(renameConstraintQuery);
|
||||
if (currentConstraintName is null)
|
||||
{
|
||||
// Constraint does not exist, so we need to create it.
|
||||
Sql<ISqlContext> createConstraintStatement = Database.SqlContext.Sql(@$"
|
||||
ALTER TABLE umbracoContentVersion ADD CONSTRAINT [DF_umbracoContentVersion_versionDate] DEFAULT (getdate()) FOR [versionDate]");
|
||||
Database.Execute(createConstraintStatement);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Constraint exists, and differs from the expected name, so we need to rename it.
|
||||
Sql<ISqlContext> renameConstraintQuery = Database.SqlContext.Sql(
|
||||
$"EXEC sp_rename N'{currentConstraintName}', N'{expectedConstraintName}', N'OBJECT'");
|
||||
Database.Execute(renameConstraintQuery);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateExternalLoginIndexes(IEnumerable<Tuple<string, string, string, bool>> indexes)
|
||||
|
||||
@@ -143,14 +143,17 @@ public class TextBuilder : Builder
|
||||
//
|
||||
// note that the blog post above clearly states that "Nor should it be applied at the type level if the type being generated is a partial class."
|
||||
// and since our models are partial classes, we have to apply the attribute against the individual members, not the class itself.
|
||||
private static void WriteGeneratedCodeAttribute(StringBuilder sb, string tabs) => sb.AppendFormat(
|
||||
private void WriteGeneratedCodeAttribute(StringBuilder sb, string tabs) => sb.AppendFormat(
|
||||
"{0}[global::System.CodeDom.Compiler.GeneratedCodeAttribute(\"Umbraco.ModelsBuilder.Embedded\", \"{1}\")]\n",
|
||||
tabs, ApiVersion.Current.Version);
|
||||
tabs,
|
||||
Config.IncludeVersionNumberInGeneratedModels ? ApiVersion.Current.Version : null);
|
||||
|
||||
// writes an attribute that specifies that an output may be null.
|
||||
// (useful for consuming projects with nullable reference types enabled)
|
||||
private static void WriteMaybeNullAttribute(StringBuilder sb, string tabs, bool isReturn = false) =>
|
||||
sb.AppendFormat("{0}[{1}global::System.Diagnostics.CodeAnalysis.MaybeNull]\n", tabs,
|
||||
sb.AppendFormat(
|
||||
"{0}[{1}global::System.Diagnostics.CodeAnalysis.MaybeNull]\n",
|
||||
tabs,
|
||||
isReturn ? "return: " : string.Empty);
|
||||
|
||||
private static string MixinStaticGetterName(string clrName) => string.Format("Get{0}", clrName);
|
||||
|
||||
+8
-9
@@ -122,11 +122,10 @@ internal class DictionaryRepository : EntityRepositoryBase<int, IDictionaryItem>
|
||||
var options = new RepositoryCachePolicyOptions
|
||||
{
|
||||
// allow zero to be cached
|
||||
GetAllCacheAllowZeroCount = true,
|
||||
GetAllCacheAllowZeroCount = true
|
||||
};
|
||||
|
||||
return new SingleItemsOnlyRepositoryCachePolicy<IDictionaryItem, int>(GlobalIsolatedCache, ScopeAccessor,
|
||||
options);
|
||||
return new SingleItemsOnlyRepositoryCachePolicy<IDictionaryItem, int>(GlobalIsolatedCache, ScopeAccessor, options);
|
||||
}
|
||||
|
||||
private IDictionaryItem ConvertFromDto(DictionaryDto dto, IDictionary<int, ILanguage> languagesById)
|
||||
@@ -217,11 +216,10 @@ internal class DictionaryRepository : EntityRepositoryBase<int, IDictionaryItem>
|
||||
var options = new RepositoryCachePolicyOptions
|
||||
{
|
||||
// allow zero to be cached
|
||||
GetAllCacheAllowZeroCount = true,
|
||||
GetAllCacheAllowZeroCount = true
|
||||
};
|
||||
|
||||
return new SingleItemsOnlyRepositoryCachePolicy<IDictionaryItem, Guid>(GlobalIsolatedCache, ScopeAccessor,
|
||||
options);
|
||||
return new SingleItemsOnlyRepositoryCachePolicy<IDictionaryItem, Guid>(GlobalIsolatedCache, ScopeAccessor, options);
|
||||
}
|
||||
|
||||
protected override IEnumerable<IDictionaryItem> PerformGetAll(params Guid[]? ids)
|
||||
@@ -272,12 +270,13 @@ internal class DictionaryRepository : EntityRepositoryBase<int, IDictionaryItem>
|
||||
{
|
||||
var options = new RepositoryCachePolicyOptions
|
||||
{
|
||||
// allow null to be cached
|
||||
CacheNullValues = true,
|
||||
// allow zero to be cached
|
||||
GetAllCacheAllowZeroCount = true,
|
||||
GetAllCacheAllowZeroCount = true
|
||||
};
|
||||
|
||||
return new SingleItemsOnlyRepositoryCachePolicy<IDictionaryItem, string>(GlobalIsolatedCache, ScopeAccessor,
|
||||
options);
|
||||
return new SingleItemsOnlyRepositoryCachePolicy<IDictionaryItem, string>(GlobalIsolatedCache, ScopeAccessor, options);
|
||||
}
|
||||
|
||||
protected override IEnumerable<IDictionaryItem> PerformGetAll(params string[]? ids)
|
||||
|
||||
@@ -389,7 +389,9 @@ WHERE r.tagId IS NULL";
|
||||
}).ToList();
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<ITag> GetTagsForEntityType(TaggableObjectTypes objectType, string? group = null,
|
||||
public IEnumerable<ITag> GetTagsForEntityType(
|
||||
TaggableObjectTypes objectType,
|
||||
string? group = null,
|
||||
string? culture = null)
|
||||
{
|
||||
Sql<ISqlContext> sql = GetTagsSql(culture, true);
|
||||
@@ -403,6 +405,9 @@ WHERE r.tagId IS NULL";
|
||||
.Where<NodeDto>(dto => dto.NodeObjectType == nodeObjectType);
|
||||
}
|
||||
|
||||
sql = sql
|
||||
.Where<NodeDto>(dto => !dto.Trashed);
|
||||
|
||||
if (group.IsNullOrWhiteSpace() == false)
|
||||
{
|
||||
sql = sql
|
||||
|
||||
@@ -5,6 +5,7 @@ using HtmlAgilityPack;
|
||||
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.Exceptions;
|
||||
@@ -33,6 +34,8 @@ public sealed class RichTextEditorPastedImages
|
||||
private readonly IScopeProvider _scopeProvider;
|
||||
private readonly IMediaImportService _mediaImportService;
|
||||
private readonly IImageUrlGenerator _imageUrlGenerator;
|
||||
private readonly IEntityService _entityService;
|
||||
private readonly AppCaches _appCaches;
|
||||
private readonly IUserService _userService;
|
||||
|
||||
[Obsolete("Please use the non-obsolete constructor. Will be removed in V16.")]
|
||||
@@ -84,6 +87,30 @@ public sealed class RichTextEditorPastedImages
|
||||
{
|
||||
}
|
||||
|
||||
// highest overload to be picked by DI, pointing to newest ctor
|
||||
[Obsolete("Please use the non-obsolete constructor. Will be removed in V17.")]
|
||||
public RichTextEditorPastedImages(
|
||||
IUmbracoContextAccessor umbracoContextAccessor,
|
||||
ILogger<RichTextEditorPastedImages> logger,
|
||||
IHostingEnvironment hostingEnvironment,
|
||||
IMediaService mediaService,
|
||||
IContentTypeBaseServiceProvider contentTypeBaseServiceProvider,
|
||||
MediaFileManager mediaFileManager,
|
||||
MediaUrlGeneratorCollection mediaUrlGenerators,
|
||||
IShortStringHelper shortStringHelper,
|
||||
IPublishedUrlProvider publishedUrlProvider,
|
||||
ITemporaryFileService temporaryFileService,
|
||||
IScopeProvider scopeProvider,
|
||||
IMediaImportService mediaImportService,
|
||||
IImageUrlGenerator imageUrlGenerator,
|
||||
IOptions<ContentSettings> contentSettings,
|
||||
IEntityService entityService,
|
||||
AppCaches appCaches)
|
||||
: this(umbracoContextAccessor, publishedUrlProvider, temporaryFileService, scopeProvider, mediaImportService, imageUrlGenerator, entityService, appCaches)
|
||||
{
|
||||
}
|
||||
|
||||
[Obsolete("Please use the non-obsolete constructor. Will be removed in V17.")]
|
||||
public RichTextEditorPastedImages(
|
||||
IUmbracoContextAccessor umbracoContextAccessor,
|
||||
IPublishedUrlProvider publishedUrlProvider,
|
||||
@@ -91,6 +118,27 @@ public sealed class RichTextEditorPastedImages
|
||||
IScopeProvider scopeProvider,
|
||||
IMediaImportService mediaImportService,
|
||||
IImageUrlGenerator imageUrlGenerator)
|
||||
: this(
|
||||
umbracoContextAccessor,
|
||||
publishedUrlProvider,
|
||||
temporaryFileService,
|
||||
scopeProvider,
|
||||
mediaImportService,
|
||||
imageUrlGenerator,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IEntityService>(),
|
||||
StaticServiceProvider.Instance.GetRequiredService<AppCaches>())
|
||||
{
|
||||
}
|
||||
|
||||
public RichTextEditorPastedImages(
|
||||
IUmbracoContextAccessor umbracoContextAccessor,
|
||||
IPublishedUrlProvider publishedUrlProvider,
|
||||
ITemporaryFileService temporaryFileService,
|
||||
IScopeProvider scopeProvider,
|
||||
IMediaImportService mediaImportService,
|
||||
IImageUrlGenerator imageUrlGenerator,
|
||||
IEntityService entityService,
|
||||
AppCaches appCaches)
|
||||
{
|
||||
_umbracoContextAccessor =
|
||||
umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor));
|
||||
@@ -99,6 +147,8 @@ public sealed class RichTextEditorPastedImages
|
||||
_scopeProvider = scopeProvider;
|
||||
_mediaImportService = mediaImportService;
|
||||
_imageUrlGenerator = imageUrlGenerator;
|
||||
_entityService = entityService;
|
||||
_appCaches = appCaches;
|
||||
|
||||
// this obviously is not correct. however, we only use IUserService in an obsolete method,
|
||||
// so this is better than having even more obsolete constructors for V16
|
||||
@@ -161,7 +211,7 @@ public sealed class RichTextEditorPastedImages
|
||||
if (uploadedImages.ContainsKey(temporaryFileKey) == false)
|
||||
{
|
||||
using Stream fileStream = temporaryFile.OpenReadStream();
|
||||
Guid? parentFolderKey = mediaParentFolder == Guid.Empty ? Constants.System.RootKey : mediaParentFolder;
|
||||
Guid? parentFolderKey = mediaParentFolder == Guid.Empty ? await GetDefaultMediaRoot(userKey) : mediaParentFolder;
|
||||
IMedia mediaFile = await _mediaImportService.ImportAsync(temporaryFile.FileName, fileStream, parentFolderKey, MediaTypeAlias(temporaryFile.FileName), userKey);
|
||||
udi = mediaFile.GetUdi();
|
||||
}
|
||||
@@ -214,6 +264,20 @@ public sealed class RichTextEditorPastedImages
|
||||
return htmlDoc.DocumentNode.OuterHtml;
|
||||
}
|
||||
|
||||
private async Task<Guid?> GetDefaultMediaRoot(Guid userKey)
|
||||
{
|
||||
IUser user = await _userService.GetAsync(userKey) ?? throw new ArgumentException("User could not be found");
|
||||
var userStartNodes = user.CalculateMediaStartNodeIds(_entityService, _appCaches);
|
||||
var firstNodeId = userStartNodes?.FirstOrDefault();
|
||||
if (firstNodeId is null)
|
||||
{
|
||||
return Constants.System.RootKey;
|
||||
}
|
||||
|
||||
Attempt<Guid> firstNodeKeyAttempt = _entityService.GetKey(firstNodeId.Value, UmbracoObjectTypes.Media);
|
||||
return firstNodeKeyAttempt.Success ? firstNodeKeyAttempt.Result : Constants.System.RootKey;
|
||||
}
|
||||
|
||||
private string MediaTypeAlias(string fileName)
|
||||
=> fileName.InvariantEndsWith(".svg")
|
||||
? Constants.Conventions.MediaTypes.VectorGraphicsAlias
|
||||
|
||||
@@ -15,6 +15,7 @@ internal class FileSystemMainDomLock : IMainDomLock
|
||||
private readonly string _lockFilePath;
|
||||
private readonly ILogger<FileSystemMainDomLock> _logger;
|
||||
private readonly string _releaseSignalFilePath;
|
||||
private bool _disposed;
|
||||
private Task? _listenForReleaseSignalFileTask;
|
||||
|
||||
private FileStream? _lockFileStream;
|
||||
@@ -89,16 +90,14 @@ internal class FileSystemMainDomLock : IMainDomLock
|
||||
ListeningLoop,
|
||||
_cancellationTokenSource.Token,
|
||||
TaskCreationOptions.LongRunning,
|
||||
TaskScheduler.Default);
|
||||
TaskScheduler.Default)
|
||||
.Unwrap(); // Because ListeningLoop is an async method, we need to use Unwrap to return the inner task.
|
||||
|
||||
return _listenForReleaseSignalFileTask;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_lockFileStream?.Close();
|
||||
_lockFileStream = null;
|
||||
}
|
||||
/// <summary>Releases the resources used by this <see cref="FileSystemMainDomLock" />.</summary>
|
||||
public void Dispose() => Dispose(true);
|
||||
|
||||
public void CreateLockReleaseSignalFile() =>
|
||||
File.Open(_releaseSignalFilePath, FileMode.OpenOrCreate, FileAccess.ReadWrite,
|
||||
@@ -108,7 +107,27 @@ internal class FileSystemMainDomLock : IMainDomLock
|
||||
public void DeleteLockReleaseSignalFile() =>
|
||||
File.Delete(_releaseSignalFilePath);
|
||||
|
||||
private void ListeningLoop()
|
||||
/// <summary>Releases the resources used by this <see cref="FileSystemMainDomLock" />.</summary>
|
||||
/// <param name="disposing">true to release both managed resources.</param>
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && !_disposed)
|
||||
{
|
||||
_logger.LogInformation($"{nameof(FileSystemMainDomLock)} Disposing...");
|
||||
_cancellationTokenSource.Cancel();
|
||||
_cancellationTokenSource.Dispose();
|
||||
ReleaseLock();
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void ReleaseLock()
|
||||
{
|
||||
_lockFileStream?.Close();
|
||||
_lockFileStream = null;
|
||||
}
|
||||
|
||||
private async Task ListeningLoop()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
@@ -127,12 +146,12 @@ internal class FileSystemMainDomLock : IMainDomLock
|
||||
{
|
||||
_logger.LogDebug("Found lock release signal file, releasing lock on {lockFilePath}", _lockFilePath);
|
||||
}
|
||||
_lockFileStream?.Close();
|
||||
_lockFileStream = null;
|
||||
|
||||
ReleaseLock();
|
||||
break;
|
||||
}
|
||||
|
||||
Thread.Sleep(_globalSettings.CurrentValue.MainDomReleaseSignalPollingInterval);
|
||||
await Task.Delay(_globalSettings.CurrentValue.MainDomReleaseSignalPollingInterval, _cancellationTokenSource.Token);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,7 +128,7 @@ public class MemberUserStore : UmbracoUserStore<MemberIdentityUser, UmbracoIdent
|
||||
UpdateMemberProperties(memberEntity, user, out bool _);
|
||||
|
||||
// create the member
|
||||
Attempt<OperationResult?> saveAttempt = _memberService.Save(memberEntity);
|
||||
Attempt<OperationResult?> saveAttempt = _memberService.Save(memberEntity, PublishNotificationSaveOptions.Saving);
|
||||
if (saveAttempt.Success is false)
|
||||
{
|
||||
scope.Complete();
|
||||
|
||||
@@ -35,18 +35,29 @@ public abstract class UmbracoUserStore<TUser, TRole>
|
||||
[Obsolete("Use TryConvertIdentityIdToInt instead. Scheduled for removal in V15.")]
|
||||
protected static int UserIdToInt(string? userId)
|
||||
{
|
||||
if (int.TryParse(userId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
|
||||
if (TryUserIdToInt(userId, out int result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"Unable to convert user ID ({userId})to int using InvariantCulture");
|
||||
}
|
||||
|
||||
protected static bool TryUserIdToInt(string? userId, out int result)
|
||||
{
|
||||
if (int.TryParse(userId, NumberStyles.Integer, CultureInfo.InvariantCulture, out result))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Guid.TryParse(userId, out Guid key))
|
||||
{
|
||||
// Reverse the IntExtensions.ToGuid
|
||||
return BitConverter.ToInt32(key.ToByteArray(), 0);
|
||||
result = BitConverter.ToInt32(key.ToByteArray(), 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"Unable to convert user ID ({userId})to int using InvariantCulture");
|
||||
return false;
|
||||
}
|
||||
|
||||
protected abstract Task<int> ResolveEntityIdFromIdentityId(string? identityId);
|
||||
|
||||
@@ -62,9 +62,16 @@ public static class HttpContextExtensions
|
||||
// Update the HttpContext's user with the authenticated user's principal to ensure
|
||||
// that subsequent requests within the same context will recognize the user
|
||||
// as authenticated.
|
||||
if (result.Succeeded)
|
||||
if (result is { Succeeded: true, Principal.Identity: not null })
|
||||
{
|
||||
httpContext.User = result.Principal;
|
||||
// We need to get existing identities that are not the backoffice kind and flow them to the new identity
|
||||
// Otherwise we can't log in as both a member and a backoffice user
|
||||
// For instance if you've enabled basic auth.
|
||||
ClaimsPrincipal? authenticatedPrincipal = result.Principal;
|
||||
IEnumerable<ClaimsIdentity> existingIdentities = httpContext.User.Identities.Where(x => x.IsAuthenticated && x.AuthenticationType != authenticatedPrincipal.Identity.AuthenticationType);
|
||||
authenticatedPrincipal.AddIdentities(existingIdentities);
|
||||
|
||||
httpContext.User = authenticatedPrincipal;
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace Umbraco.Extensions;
|
||||
|
||||
public static class MemberClaimsPrincipalExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Tries to get specifically the member identity from the ClaimsPrincipal
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The identity returned is the one with default authentication type.
|
||||
/// </remarks>
|
||||
/// <param name="principal">The principal to find the identity in.</param>
|
||||
/// <returns>The default authenticated authentication type identity.</returns>
|
||||
public static ClaimsIdentity? GetMemberIdentity(this ClaimsPrincipal principal)
|
||||
=> principal.Identities.FirstOrDefault(x => x.AuthenticationType == IdentityConstants.ApplicationScheme);
|
||||
}
|
||||
@@ -24,7 +24,7 @@ public sealed class ConfigureMemberIdentityOptions : IConfigureOptions<IdentityO
|
||||
options.SignIn.RequireConfirmedEmail = false; // not implemented
|
||||
options.SignIn.RequireConfirmedPhoneNumber = false; // not implemented
|
||||
|
||||
options.User.RequireUniqueEmail = true;
|
||||
options.User.RequireUniqueEmail = _securitySettings.MemberRequireUniqueEmail;
|
||||
|
||||
// Support validation of member names using Down-Level Logon Name format
|
||||
options.User.AllowedUserNameCharacters = _securitySettings.AllowedUserNameCharacters;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Globalization;
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -113,8 +114,11 @@ public class MemberManager : UmbracoUserManager<MemberIdentityUser, MemberPasswo
|
||||
/// <inheritdoc />
|
||||
public virtual bool IsLoggedIn()
|
||||
{
|
||||
HttpContext? httpContext = _httpContextAccessor.HttpContext;
|
||||
return httpContext?.User.Identity?.IsAuthenticated ?? false;
|
||||
// We have to try and specifically find the member identity, it's entirely possible for there to be both backoffice and member.
|
||||
ClaimsIdentity? memberIdentity = _httpContextAccessor.HttpContext?.User.GetMemberIdentity();
|
||||
|
||||
return memberIdentity is not null &&
|
||||
memberIdentity.IsAuthenticated;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -170,23 +174,27 @@ public class MemberManager : UmbracoUserManager<MemberIdentityUser, MemberPasswo
|
||||
/// <inheritdoc />
|
||||
public virtual async Task<MemberIdentityUser?> GetCurrentMemberAsync()
|
||||
{
|
||||
if (_currentMember == null)
|
||||
if (_currentMember is not null)
|
||||
{
|
||||
if (!IsLoggedIn())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
_currentMember = await GetUserAsync(_httpContextAccessor.HttpContext?.User!);
|
||||
return _currentMember;
|
||||
}
|
||||
|
||||
if (IsLoggedIn() is false)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Create a principal the represents the member security context.
|
||||
var memberPrincipal = new ClaimsPrincipal(_httpContextAccessor.HttpContext?.User.GetMemberIdentity()!);
|
||||
_currentMember = await GetUserAsync(memberPrincipal);
|
||||
|
||||
return _currentMember;
|
||||
}
|
||||
|
||||
public virtual IPublishedContent? AsPublishedMember(MemberIdentityUser user) => _store.GetPublishedMember(user);
|
||||
|
||||
/// <summary>
|
||||
/// This will check if the member has access to this path
|
||||
/// This will check if the member has access to this path.
|
||||
/// </summary>
|
||||
/// <param name="path"></param>
|
||||
/// <returns></returns>
|
||||
|
||||
@@ -141,7 +141,10 @@ public abstract class UmbracoViewPage<TModel> : RazorPage<TModel>
|
||||
string.Format(
|
||||
ContentSettings.PreviewBadge,
|
||||
HostingEnvironment.ToAbsolute(GlobalSettings.UmbracoPath),
|
||||
Context.Request.GetEncodedUrl(),
|
||||
System.Web.HttpUtility.HtmlEncode(Context.Request.GetEncodedUrl()), // Belt and braces - via a browser at least it doesn't seem possible to have anything other than
|
||||
// a valid culture code provided in the querystring of this URL.
|
||||
// But just to be sure of prevention of an XSS vulnterablity we'll HTML encode here too.
|
||||
// An expected URL is untouched by this encoding.
|
||||
UmbracoContext.PublishedRequest?.PublishedContent?.Key);
|
||||
}
|
||||
else
|
||||
|
||||
Submodule src/Umbraco.Web.UI.Client updated: 586bde9f23...b530e31bad
@@ -0,0 +1,86 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.Models.TemporaryFile;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Cms.Tests.Common.Testing;
|
||||
using Umbraco.Cms.Tests.Integration.Testing;
|
||||
|
||||
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Services;
|
||||
|
||||
[TestFixture]
|
||||
[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerFixture)]
|
||||
public class TemporaryFileServiceTests : UmbracoIntegrationTest
|
||||
{
|
||||
private ITemporaryFileService TemporaryFileService => GetRequiredService<ITemporaryFileService>();
|
||||
|
||||
protected override void CustomTestSetup(IUmbracoBuilder builder) =>
|
||||
builder.Services.Configure<ContentSettings>(config =>
|
||||
config.AllowedUploadedFileExtensions = ["txt"]);
|
||||
|
||||
[Test]
|
||||
public async Task Can_Create_Get_And_Delete_Temporary_File()
|
||||
{
|
||||
var key = Guid.NewGuid();
|
||||
const string FileName = "test.txt";
|
||||
const string FileContents = "test";
|
||||
var model = new CreateTemporaryFileModel
|
||||
{
|
||||
FileName = FileName,
|
||||
Key = key,
|
||||
OpenReadStream = () =>
|
||||
{
|
||||
var stream = new MemoryStream();
|
||||
var writer = new StreamWriter(stream);
|
||||
writer.Write(FileContents);
|
||||
writer.Flush();
|
||||
stream.Position = 0;
|
||||
return stream;
|
||||
}
|
||||
};
|
||||
var createAttempt = await TemporaryFileService.CreateAsync(model);
|
||||
Assert.IsTrue(createAttempt.Success);
|
||||
|
||||
TemporaryFileModel? fileModel = await TemporaryFileService.GetAsync(key);
|
||||
Assert.IsNotNull(fileModel);
|
||||
Assert.AreEqual(key, fileModel.Key);
|
||||
Assert.AreEqual(FileName, fileModel.FileName);
|
||||
|
||||
using (var reader = new StreamReader(fileModel.OpenReadStream()))
|
||||
{
|
||||
string fileContents = reader.ReadToEnd();
|
||||
Assert.AreEqual(FileContents, fileContents);
|
||||
}
|
||||
|
||||
var deleteAttempt = await TemporaryFileService.DeleteAsync(key);
|
||||
Assert.IsTrue(createAttempt.Success);
|
||||
|
||||
fileModel = await TemporaryFileService.GetAsync(key);
|
||||
Assert.IsNull(fileModel);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Cannot_Create_File_Outside_Of_Temporary_Files_Root()
|
||||
{
|
||||
var key = Guid.NewGuid();
|
||||
const string FileName = "../test.txt";
|
||||
var model = new CreateTemporaryFileModel
|
||||
{
|
||||
FileName = FileName,
|
||||
Key = key,
|
||||
OpenReadStream = () =>
|
||||
{
|
||||
var stream = new MemoryStream();
|
||||
var writer = new StreamWriter(stream);
|
||||
writer.Write(string.Empty);
|
||||
writer.Flush();
|
||||
stream.Position = 0;
|
||||
return stream;
|
||||
}
|
||||
};
|
||||
var createAttempt = await TemporaryFileService.CreateAsync(model);
|
||||
Assert.IsFalse(createAttempt.Success);
|
||||
Assert.AreEqual(TemporaryFileOperationStatus.InvalidFileName, createAttempt.Status);
|
||||
}
|
||||
}
|
||||
+83
@@ -638,6 +638,89 @@ public class TagRepositoryTest : UmbracoIntegrationTest
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Can_Get_Tags_For_Entity_Type_Excluding_Trashed_Entity()
|
||||
{
|
||||
var provider = ScopeProvider;
|
||||
using (ScopeProvider.CreateScope())
|
||||
{
|
||||
var template = TemplateBuilder.CreateTextPageTemplate();
|
||||
FileService.SaveTemplate(template);
|
||||
|
||||
var contentType = ContentTypeBuilder.CreateSimpleContentType("test", "Test", defaultTemplateId: template.Id);
|
||||
ContentTypeRepository.Save(contentType);
|
||||
|
||||
var content1 = ContentBuilder.CreateSimpleContent(contentType);
|
||||
content1.PublishCulture(CultureImpact.Invariant);
|
||||
content1.PublishedState = PublishedState.Publishing;
|
||||
DocumentRepository.Save(content1);
|
||||
|
||||
var content2 = ContentBuilder.CreateSimpleContent(contentType);
|
||||
content2.PublishCulture(CultureImpact.Invariant);
|
||||
content2.PublishedState = PublishedState.Publishing;
|
||||
content2.Trashed = true;
|
||||
DocumentRepository.Save(content2);
|
||||
|
||||
var mediaType = MediaTypeBuilder.CreateImageMediaType("image2");
|
||||
MediaTypeRepository.Save(mediaType);
|
||||
|
||||
var media1 = MediaBuilder.CreateMediaImage(mediaType, -1);
|
||||
MediaRepository.Save(media1);
|
||||
|
||||
var media2 = MediaBuilder.CreateMediaImage(mediaType, -1);
|
||||
media2.Trashed = true;
|
||||
MediaRepository.Save(media2);
|
||||
|
||||
var repository = CreateRepository(provider);
|
||||
Tag[] tags =
|
||||
{
|
||||
new Tag {Text = "tag1", Group = "test"},
|
||||
new Tag {Text = "tag2", Group = "test1"},
|
||||
new Tag {Text = "tag3", Group = "test"}
|
||||
};
|
||||
|
||||
Tag[] tags2 =
|
||||
{
|
||||
new Tag {Text = "tag4", Group = "test"},
|
||||
new Tag {Text = "tag5", Group = "test1"},
|
||||
new Tag {Text = "tag6", Group = "test"}
|
||||
};
|
||||
|
||||
repository.Assign(
|
||||
content1.Id,
|
||||
contentType.PropertyTypes.First().Id,
|
||||
tags,
|
||||
false);
|
||||
|
||||
repository.Assign(
|
||||
content2.Id,
|
||||
contentType.PropertyTypes.First().Id,
|
||||
tags2,
|
||||
false);
|
||||
|
||||
repository.Assign(
|
||||
media1.Id,
|
||||
contentType.PropertyTypes.First().Id,
|
||||
tags,
|
||||
false);
|
||||
|
||||
repository.Assign(
|
||||
media2.Id,
|
||||
contentType.PropertyTypes.First().Id,
|
||||
tags2,
|
||||
false);
|
||||
|
||||
var result1 = repository.GetTagsForEntityType(TaggableObjectTypes.Content).ToArray();
|
||||
var result2 = repository.GetTagsForEntityType(TaggableObjectTypes.Media).ToArray();
|
||||
var result3 = repository.GetTagsForEntityType(TaggableObjectTypes.All).ToArray();
|
||||
|
||||
const string ExpectedTags = "tag1,tag2,tag3";
|
||||
Assert.AreEqual(ExpectedTags, string.Join(",", result1.Select(x => x.Text)));
|
||||
Assert.AreEqual(ExpectedTags, string.Join(",", result2.Select(x => x.Text)));
|
||||
Assert.AreEqual(ExpectedTags, string.Join(",", result3.Select(x => x.Text)));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Can_Get_Tags_For_Entity_Type()
|
||||
{
|
||||
|
||||
@@ -268,7 +268,7 @@ public class MemberManagerTests
|
||||
.Setup(x => x.CreateMember(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()))
|
||||
.Returns(fakeMember);
|
||||
_mockMemberService
|
||||
.Setup(x => x.Save(fakeMember, Constants.Security.SuperUserId))
|
||||
.Setup(x => x.Save(fakeMember, It.IsAny<PublishNotificationSaveOptions>(), Constants.Security.SuperUserId))
|
||||
.Returns(Attempt.Succeed<OperationResult?>(null));
|
||||
|
||||
}
|
||||
|
||||
+2
-2
@@ -124,7 +124,7 @@ public class MemberUserStoreTests
|
||||
.Setup(x => x.CreateMember(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()))
|
||||
.Returns(mockMember);
|
||||
_mockMemberService
|
||||
.Setup(x => x.Save(mockMember, Constants.Security.SuperUserId))
|
||||
.Setup(x => x.Save(mockMember, PublishNotificationSaveOptions.Saving, Constants.Security.SuperUserId))
|
||||
.Returns(Attempt.Succeed<OperationResult?>(null));
|
||||
// act
|
||||
var identityResult = await sut.CreateAsync(fakeUser, CancellationToken.None);
|
||||
@@ -134,7 +134,7 @@ public class MemberUserStoreTests
|
||||
Assert.IsTrue(!identityResult.Errors.Any());
|
||||
_mockMemberService.Verify(x =>
|
||||
x.CreateMember(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()));
|
||||
_mockMemberService.Verify(x => x.Save(mockMember, Constants.Security.SuperUserId));
|
||||
_mockMemberService.Verify(x => x.Save(mockMember, PublishNotificationSaveOptions.Saving, Constants.Security.SuperUserId));
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -71,25 +71,23 @@ internal class UmbracoCmsSchema
|
||||
|
||||
public required HelpPageSettings HelpPage { get; set; }
|
||||
|
||||
public required InstallDefaultDataSettings DefaultDataCreation { get; set; }
|
||||
public required InstallDefaultDataNamedOptions InstallDefaultData { get; set; }
|
||||
|
||||
public required DataTypesSettings DataTypes { get; set; }
|
||||
|
||||
public required MarketplaceSettings Marketplace { get; set; }
|
||||
|
||||
public InstallDefaultDataNamedOptions InstallDefaultData { get; set; } = null!;
|
||||
|
||||
public required WebhookSettings Webhook { get; set; }
|
||||
}
|
||||
|
||||
public class InstallDefaultDataNamedOptions
|
||||
{
|
||||
public InstallDefaultDataSettings Languages { get; set; } = null!;
|
||||
public required InstallDefaultDataSettings Languages { get; set; }
|
||||
|
||||
public InstallDefaultDataSettings DataTypes { get; set; } = null!;
|
||||
public required InstallDefaultDataSettings DataTypes { get; set; }
|
||||
|
||||
public InstallDefaultDataSettings MediaTypes { get; set; } = null!;
|
||||
public required InstallDefaultDataSettings MediaTypes { get; set; }
|
||||
|
||||
public InstallDefaultDataSettings MemberTypes { get; set; } = null!;
|
||||
public required InstallDefaultDataSettings MemberTypes { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user