Merge remote-tracking branch 'origin/v14/dev' into v15/dev
# Conflicts: # Directory.Packages.props # src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs # tests/Directory.Packages.props # version.json
This commit is contained in:
@@ -44,14 +44,14 @@
|
||||
<PackageVersion Include="Asp.Versioning.Mvc" Version="8.1.0" />
|
||||
<PackageVersion Include="Asp.Versioning.Mvc.ApiExplorer" Version="8.1.0" />
|
||||
<PackageVersion Include="Dazinator.Extensions.FileProviders" Version="2.0.0" />
|
||||
<PackageVersion Include="Examine" Version="3.2.1" />
|
||||
<PackageVersion Include="Examine.Core" Version="3.2.1" />
|
||||
<PackageVersion Include="HtmlAgilityPack" Version="1.11.61" />
|
||||
<PackageVersion Include="JsonPatch.Net" Version="3.1.0" />
|
||||
<PackageVersion Include="Examine" Version="3.3.0" />
|
||||
<PackageVersion Include="Examine.Core" Version="3.3.0" />
|
||||
<PackageVersion Include="HtmlAgilityPack" Version="1.11.62" />
|
||||
<PackageVersion Include="JsonPatch.Net" Version="3.1.1" />
|
||||
<PackageVersion Include="K4os.Compression.LZ4" Version="1.3.8" />
|
||||
<PackageVersion Include="MailKit" Version="4.6.0" />
|
||||
<PackageVersion Include="MailKit" Version="4.7.1.1" />
|
||||
<PackageVersion Include="Markdown" Version="2.2.1" />
|
||||
<PackageVersion Include="MessagePack" Version="2.5.168" />
|
||||
<PackageVersion Include="MessagePack" Version="2.5.172" />
|
||||
<PackageVersion Include="MiniProfiler.AspNetCore.Mvc" Version="4.3.8" />
|
||||
<PackageVersion Include="MiniProfiler.Shared" Version="4.3.8" />
|
||||
<PackageVersion Include="ncrontab" Version="3.3.3" />
|
||||
@@ -61,20 +61,20 @@
|
||||
<PackageVersion Include="OpenIddict.AspNetCore" Version="5.7.0" />
|
||||
<PackageVersion Include="OpenIddict.EntityFrameworkCore" Version="5.7.0" />
|
||||
<PackageVersion Include="Serilog" Version="3.1.1" />
|
||||
<PackageVersion Include="Serilog.AspNetCore" Version="8.0.1" />
|
||||
<PackageVersion Include="Serilog.AspNetCore" Version="8.0.2" />
|
||||
<PackageVersion Include="Serilog.Enrichers.Process" Version="2.0.2" />
|
||||
<PackageVersion Include="Serilog.Enrichers.Thread" Version="3.1.0" />
|
||||
<PackageVersion Include="Serilog.Expressions" Version="4.0.0" />
|
||||
<PackageVersion Include="Serilog.Extensions.Hosting" Version="8.0.0" />
|
||||
<PackageVersion Include="Serilog.Formatting.Compact" Version="2.0.0" />
|
||||
<PackageVersion Include="Serilog.Formatting.Compact.Reader" Version="3.0.0" />
|
||||
<PackageVersion Include="Serilog.Settings.Configuration" Version="8.0.1" />
|
||||
<PackageVersion Include="Serilog.Settings.Configuration" Version="8.0.2" />
|
||||
<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.4" />
|
||||
<PackageVersion Include="SixLabors.ImageSharp.Web" Version="3.1.2" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.6.2" />
|
||||
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.5" />
|
||||
<PackageVersion Include="SixLabors.ImageSharp.Web" Version="3.1.3" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.7.0" />
|
||||
</ItemGroup>
|
||||
<!-- Transitive pinned versions (only required because our direct dependencies have vulnerable versions of transitive dependencies) -->
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"sdk": {
|
||||
"version": "9.0.100-preview.5.24307.3",
|
||||
"rollForward": "latestFeature"
|
||||
"rollForward": "latestFeature",
|
||||
"allowPrerelease": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,14 +10,15 @@ public class ProcessRequestContextHandler
|
||||
: IOpenIddictServerHandler<OpenIddictServerEvents.ProcessRequestContext>, IOpenIddictValidationHandler<OpenIddictValidationEvents.ProcessRequestContext>
|
||||
{
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
private readonly string _backOfficePathSegment;
|
||||
private readonly string[] _pathsToHandle;
|
||||
|
||||
public ProcessRequestContextHandler(IHttpContextAccessor httpContextAccessor)
|
||||
{
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
_backOfficePathSegment = Constants.System.DefaultUmbracoPath.TrimStart(Constants.CharArrays.Tilde)
|
||||
var backOfficePathSegment = Constants.System.DefaultUmbracoPath.TrimStart(Constants.CharArrays.Tilde)
|
||||
.EnsureStartsWith('/')
|
||||
.EnsureEndsWith('/');
|
||||
_pathsToHandle = [backOfficePathSegment, "/.well-known/openid-configuration"];
|
||||
}
|
||||
|
||||
public ValueTask HandleAsync(OpenIddictServerEvents.ProcessRequestContext context)
|
||||
@@ -48,6 +49,14 @@ public class ProcessRequestContextHandler
|
||||
return false;
|
||||
}
|
||||
|
||||
return requestPath.StartsWith(_backOfficePathSegment) is false;
|
||||
foreach (var path in _pathsToHandle)
|
||||
{
|
||||
if (requestPath.StartsWith(path))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,11 +218,12 @@ public class MemberController : DeliveryApiControllerBase
|
||||
claim.SetDestinations(OpenIddictConstants.Destinations.AccessToken);
|
||||
}
|
||||
|
||||
if (request.GetScopes().Contains(OpenIddictConstants.Scopes.OfflineAccess))
|
||||
{
|
||||
// "offline_access" scope is required to use refresh tokens
|
||||
memberPrincipal.SetScopes(OpenIddictConstants.Scopes.OfflineAccess);
|
||||
}
|
||||
// "openid" and "offline_access" are the only scopes allowed for members; explicitly ensure we only add those
|
||||
// NOTE: the "offline_access" scope is required to use refresh tokens
|
||||
IEnumerable<string> allowedScopes = request
|
||||
.GetScopes()
|
||||
.Intersect(new[] { OpenIddictConstants.Scopes.OpenId, OpenIddictConstants.Scopes.OfflineAccess });
|
||||
memberPrincipal.SetScopes(allowedScopes);
|
||||
|
||||
return new SignInResult(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, memberPrincipal);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace Umbraco.Cms.Api.Management.Controllers.DataType;
|
||||
|
||||
[VersionedApiBackOfficeRoute(Constants.UdiEntityType.DataType)]
|
||||
[ApiExplorerSettings(GroupName = "Data Type")]
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentsOrDocumentTypes)]
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentOrMediaOrContentTypes)]
|
||||
public abstract class DataTypeControllerBase : ManagementApiControllerBase
|
||||
{
|
||||
protected IActionResult DataTypeOperationStatusResult(DataTypeOperationStatus status) =>
|
||||
|
||||
@@ -39,7 +39,7 @@ public class GetAuditLogDocumentController : DocumentControllerBase
|
||||
{
|
||||
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
|
||||
User,
|
||||
ContentPermissionResource.WithKeys(ActionProtect.ActionLetter, id),
|
||||
ContentPermissionResource.WithKeys(ActionBrowse.ActionLetter, id),
|
||||
AuthorizationPolicies.ContentPermissionByResource);
|
||||
|
||||
if (!authorizationResult.Succeeded)
|
||||
|
||||
@@ -66,6 +66,10 @@ public class UpdateDomainsController : DocumentControllerBase
|
||||
.WithDetail("One or more of the specified domain names were conflicting with domain assignments to other content items.")
|
||||
.WithExtension("conflictingDomainNames", _domainPresentationFactory.CreateDomainAssignmentModels(result.Result.ConflictingDomains.EmptyNull()))
|
||||
.Build()),
|
||||
DomainOperationStatus.InvalidDomainName => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("Invalid domain name detected")
|
||||
.WithDetail("One or more of the specified domain names were invalid.")
|
||||
.Build()),
|
||||
_ => StatusCode(StatusCodes.Status500InternalServerError, problemDetailsBuilder
|
||||
.WithTitle("Unknown domain update operation status.")
|
||||
.Build()),
|
||||
|
||||
@@ -98,6 +98,18 @@ public abstract class DocumentTypeControllerBase : ManagementApiControllerBase
|
||||
.WithTitle("Name was too long")
|
||||
.WithDetail("Name cannot be more than 255 characters in length.")
|
||||
.Build()),
|
||||
ContentTypeOperationStatus.InvalidElementFlagDocumentHasContent => new BadRequestObjectResult(problemDetailsBuilder
|
||||
.WithTitle("Invalid IsElement flag")
|
||||
.WithDetail("Cannot change to element type because content has already been created with this document type.")
|
||||
.Build()),
|
||||
ContentTypeOperationStatus.InvalidElementFlagElementIsUsedInPropertyEditorConfiguration => new BadRequestObjectResult(problemDetailsBuilder
|
||||
.WithTitle("Invalid IsElement flag")
|
||||
.WithDetail("Cannot change to document type because this element type is used in the configuration of a data type.")
|
||||
.Build()),
|
||||
ContentTypeOperationStatus.InvalidElementFlagComparedToParent => new BadRequestObjectResult(problemDetailsBuilder
|
||||
.WithTitle("Invalid IsElement flag")
|
||||
.WithDetail("Can not create a documentType with inheritance composition where the parent and the new type's IsElement flag are different.")
|
||||
.Build()),
|
||||
_ => new ObjectResult("Unknown content type operation status") { StatusCode = StatusCodes.Status500InternalServerError },
|
||||
});
|
||||
|
||||
|
||||
@@ -45,7 +45,8 @@ public abstract class UserStartNodeTreeControllerBase<TItem> : EntityTreeControl
|
||||
IEntitySlim[] children = base.GetPagedChildEntities(parentKey, skip, take, out totalItems);
|
||||
return UserHasRootAccess() || IgnoreUserStartNodes()
|
||||
? children
|
||||
: CalculateAccessMap(() => _userStartNodeEntitiesService.ChildUserAccessEntities(children, UserStartNodePaths), out totalItems);
|
||||
// Keeping the correct totalItems amount from GetPagedChildEntities
|
||||
: CalculateAccessMap(() => _userStartNodeEntitiesService.ChildUserAccessEntities(children, UserStartNodePaths), out _);
|
||||
}
|
||||
|
||||
protected override TItem[] MapTreeItemViewModels(Guid? parentKey, IEntitySlim[] entities)
|
||||
|
||||
@@ -33,6 +33,7 @@ internal static class ApplicationBuilderExtensions
|
||||
{
|
||||
innerBuilder.UseExceptionHandler(exceptionBuilder => exceptionBuilder.Run(async context =>
|
||||
{
|
||||
var isDebug = context.RequestServices.GetRequiredService<IHostingEnvironment>().IsDebugMode;
|
||||
Exception? exception = context.Features.Get<IExceptionHandlerPathFeature>()?.Error;
|
||||
if (exception is null)
|
||||
{
|
||||
@@ -42,9 +43,9 @@ internal static class ApplicationBuilderExtensions
|
||||
var response = new ProblemDetails
|
||||
{
|
||||
Title = exception.Message,
|
||||
Detail = exception.StackTrace,
|
||||
Detail = isDebug ? exception.StackTrace : null,
|
||||
Status = StatusCodes.Status500InternalServerError,
|
||||
Instance = exception.GetType().Name,
|
||||
Instance = isDebug ? exception.GetType().Name : null,
|
||||
Type = "Error"
|
||||
};
|
||||
await context.Response.WriteAsJsonAsync(response);
|
||||
|
||||
@@ -29,6 +29,7 @@ internal static class BackOfficeAuthPolicyBuilderExtensions
|
||||
builder.Services.AddSingleton<IAuthorizationHandler, UserGroupPermissionHandler>();
|
||||
builder.Services.AddSingleton<IAuthorizationHandler, UserPermissionHandler>();
|
||||
builder.Services.AddSingleton<IAuthorizationHandler, AllowedApplicationHandler>();
|
||||
builder.Services.AddSingleton<IAuthorizationHandler, BackOfficeHandler>();
|
||||
|
||||
builder.Services.AddAuthorization(CreatePolicies);
|
||||
return builder;
|
||||
@@ -46,7 +47,7 @@ internal static class BackOfficeAuthPolicyBuilderExtensions
|
||||
options.AddPolicy(AuthorizationPolicies.BackOfficeAccess, policy =>
|
||||
{
|
||||
policy.AuthenticationSchemes.Add(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme);
|
||||
policy.RequireAuthenticatedUser();
|
||||
policy.Requirements.Add(new BackOfficeRequirement());
|
||||
});
|
||||
|
||||
options.AddPolicy(AuthorizationPolicies.RequireAdminAccess, policy =>
|
||||
@@ -76,6 +77,7 @@ internal static class BackOfficeAuthPolicyBuilderExtensions
|
||||
AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessDictionaryOrTemplates, Constants.Applications.Translation, Constants.Applications.Settings);
|
||||
AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessDocuments, Constants.Applications.Content);
|
||||
AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessDocumentsOrDocumentTypes, Constants.Applications.Content, Constants.Applications.Settings);
|
||||
AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessDocumentOrMediaOrContentTypes, Constants.Applications.Content, Constants.Applications.Settings, Constants.Applications.Media);
|
||||
AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessDocumentTypes, Constants.Applications.Settings);
|
||||
AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessLanguages, Constants.Applications.Settings);
|
||||
AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessMediaTypes, Constants.Applications.Settings);
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
using Umbraco.Cms.Api.Management.Mapping;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Umbraco.Cms.Api.Management.Mapping;
|
||||
using Umbraco.Cms.Api.Management.ViewModels;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.UserGroup;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.UserGroup.Permissions;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Membership;
|
||||
using Umbraco.Cms.Core.Models.Membership.Permissions;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Cms.Core.Strings;
|
||||
@@ -20,17 +21,30 @@ public class UserGroupPresentationFactory : IUserGroupPresentationFactory
|
||||
private readonly IShortStringHelper _shortStringHelper;
|
||||
private readonly ILanguageService _languageService;
|
||||
private readonly IPermissionPresentationFactory _permissionPresentationFactory;
|
||||
private readonly ILogger<UserGroupPresentationFactory> _logger;
|
||||
|
||||
[Obsolete("Use the new constructor instead, will be removed in v16.")]
|
||||
public UserGroupPresentationFactory(
|
||||
IEntityService entityService,
|
||||
IShortStringHelper shortStringHelper,
|
||||
ILanguageService languageService,
|
||||
IPermissionPresentationFactory permissionPresentationFactory)
|
||||
: this(entityService, shortStringHelper, languageService, permissionPresentationFactory, StaticServiceProvider.Instance.GetRequiredService<ILogger<UserGroupPresentationFactory>>())
|
||||
{
|
||||
}
|
||||
|
||||
public UserGroupPresentationFactory(
|
||||
IEntityService entityService,
|
||||
IShortStringHelper shortStringHelper,
|
||||
ILanguageService languageService,
|
||||
IPermissionPresentationFactory permissionPresentationFactory,
|
||||
ILogger<UserGroupPresentationFactory> logger)
|
||||
{
|
||||
_entityService = entityService;
|
||||
_shortStringHelper = shortStringHelper;
|
||||
_languageService = languageService;
|
||||
_permissionPresentationFactory = permissionPresentationFactory;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -43,10 +57,9 @@ public class UserGroupPresentationFactory : IUserGroupPresentationFactory
|
||||
|
||||
Attempt<IEnumerable<string>, UserGroupOperationStatus> languageIsoCodesMappingAttempt = await MapLanguageIdsToIsoCodeAsync(userGroup.AllowedLanguages);
|
||||
|
||||
// We've gotten this data from the database, so the mapping should not fail
|
||||
if (languageIsoCodesMappingAttempt.Success is false)
|
||||
{
|
||||
throw new InvalidOperationException($"Unknown language ID in User Group: {userGroup.Name}");
|
||||
_logger.LogDebug("Unknown language ID in User Group: {0}", userGroup.Name);
|
||||
}
|
||||
|
||||
return new UserGroupResponseModel
|
||||
@@ -77,10 +90,9 @@ public class UserGroupPresentationFactory : IUserGroupPresentationFactory
|
||||
Guid? mediaStartNodeKey = GetKeyFromId(userGroup.StartMediaId, UmbracoObjectTypes.Media);
|
||||
Attempt<IEnumerable<string>, UserGroupOperationStatus> languageIsoCodesMappingAttempt = await MapLanguageIdsToIsoCodeAsync(userGroup.AllowedLanguages);
|
||||
|
||||
// We've gotten this data from the database, so the mapping should not fail
|
||||
if (languageIsoCodesMappingAttempt.Success is false)
|
||||
{
|
||||
throw new InvalidOperationException($"Unknown language ID in User Group: {userGroup.Name}");
|
||||
_logger.LogDebug("Unknown language ID in User Group: {0}", userGroup.Name);
|
||||
}
|
||||
|
||||
return new UserGroupResponseModel
|
||||
@@ -217,9 +229,10 @@ public class UserGroupPresentationFactory : IUserGroupPresentationFactory
|
||||
.Select(x => x.IsoCode)
|
||||
.ToArray();
|
||||
|
||||
return isoCodes.Length == ids.Count()
|
||||
? Attempt.SucceedWithStatus<IEnumerable<string>, UserGroupOperationStatus>(UserGroupOperationStatus.Success, isoCodes)
|
||||
: Attempt.FailWithStatus<IEnumerable<string>, UserGroupOperationStatus>(UserGroupOperationStatus.LanguageNotFound, isoCodes);
|
||||
// if a language id does not exist, it simply not returned.
|
||||
// We do this so we don't have to clean up user group data when deleting languages and to make it easier to restore accidentally removed languages
|
||||
return Attempt.SucceedWithStatus<IEnumerable<string>, UserGroupOperationStatus>(
|
||||
UserGroupOperationStatus.Success, isoCodes);
|
||||
}
|
||||
|
||||
private async Task<Attempt<IEnumerable<int>, UserGroupOperationStatus>> MapLanguageIsoCodesToIdsAsync(IEnumerable<string> isoCodes)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Authorization.Infrastructure;
|
||||
using Umbraco.Cms.Api.Management.Security.Authorization.User;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Security.Authorization.DenyLocalLogin;
|
||||
|
||||
@@ -24,12 +25,12 @@ public class DenyLocalLoginHandler : MustSatisfyRequirementAuthorizationHandler<
|
||||
|
||||
if (isDenied is false)
|
||||
{
|
||||
// AuthorizationPolicies.BackOfficeAccess policy adds this requirement by policy.RequireAuthenticatedUser()
|
||||
// AuthorizationPolicies.BackOfficeAccess policy adds this requirement by policy.Requirements.Add(new BackOfficeRequirement());
|
||||
// Since we want to "allow anonymous" for some endpoints (i.e. BackOfficeController.Login()), it is necessary to succeed this requirement
|
||||
IEnumerable<DenyAnonymousAuthorizationRequirement> denyAnonymousUserRequirements = context.PendingRequirements.OfType<DenyAnonymousAuthorizationRequirement>();
|
||||
foreach (DenyAnonymousAuthorizationRequirement denyAnonymousUserRequirement in denyAnonymousUserRequirements)
|
||||
IEnumerable<BackOfficeRequirement> backOfficeRequirements = context.PendingRequirements.OfType<BackOfficeRequirement>();
|
||||
foreach (BackOfficeRequirement backOfficeRequirement in backOfficeRequirements)
|
||||
{
|
||||
context.Succeed(denyAnonymousUserRequirement);
|
||||
context.Succeed(backOfficeRequirement);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,8 +17,8 @@ internal sealed class AllowedApplicationHandler : MustSatisfyRequirementAuthoriz
|
||||
|
||||
protected override Task<bool> IsAuthorized(AuthorizationHandlerContext context, AllowedApplicationRequirement requirement)
|
||||
{
|
||||
IUser user = _authorizationHelper.GetUmbracoUser(context.User);
|
||||
var allowed = user.AllowedSections.ContainsAny(requirement.Applications);
|
||||
var allowed = _authorizationHelper.TryGetUmbracoUser(context.User, out IUser? user)
|
||||
&& user.AllowedSections.ContainsAny(requirement.Applications);
|
||||
return Task.FromResult(allowed);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Security.Authorization.User;
|
||||
|
||||
/// <summary>
|
||||
/// Ensures authorization is successful for a back office user.
|
||||
/// </summary>
|
||||
public class BackOfficeHandler : MustSatisfyRequirementAuthorizationHandler<BackOfficeRequirement>
|
||||
{
|
||||
private readonly IBackOfficeSecurityAccessor _backOfficeSecurity;
|
||||
|
||||
public BackOfficeHandler(IBackOfficeSecurityAccessor backOfficeSecurity)
|
||||
{
|
||||
_backOfficeSecurity = backOfficeSecurity;
|
||||
}
|
||||
|
||||
protected override Task<bool> IsAuthorized(AuthorizationHandlerContext context, BackOfficeRequirement requirement)
|
||||
{
|
||||
|
||||
if (context.HasFailed is false && context.HasSucceeded is true)
|
||||
{
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
if (!_backOfficeSecurity.BackOfficeSecurity?.IsAuthenticated() ?? false)
|
||||
{
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
|
||||
var userApprovalSucceeded = !requirement.RequireApproval ||
|
||||
(_backOfficeSecurity.BackOfficeSecurity?.CurrentUser?.IsApproved ?? false);
|
||||
return Task.FromResult(userApprovalSucceeded);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Security.Authorization.User;
|
||||
|
||||
/// <summary>
|
||||
/// Authorization requirement for the <see cref="BackOfficeHandler" />.
|
||||
/// </summary>
|
||||
public class BackOfficeRequirement : IAuthorizationRequirement
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BackOfficeRequirement" /> class.
|
||||
/// </summary>
|
||||
/// <param name="requireApproval">Flag for whether back-office user approval is required.</param>
|
||||
public BackOfficeRequirement(bool requireApproval = true) => RequireApproval = requireApproval;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether back-office user approval is required.
|
||||
/// </summary>
|
||||
public bool RequireApproval { get; }
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="SixLabors.ImageSharp" VersionOverride="[2.1.8, 3)" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" VersionOverride="[2.1.9, 3)" />
|
||||
<PackageReference Include="SixLabors.ImageSharp.Web" VersionOverride="[2.0.2, 3)" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ public static partial class Constants
|
||||
public static class LiveEnvironment
|
||||
{
|
||||
public const string CompilationDebugCheck = "https://umbra.co/healthchecks-compilation-debug";
|
||||
public const string RuntimeModeCheck = "https://docs.umbraco.com/umbraco-cms/fundamentals/setup/server-setup/runtime-modes";
|
||||
}
|
||||
|
||||
public static class Configuration
|
||||
|
||||
@@ -403,6 +403,9 @@ namespace Umbraco.Cms.Core.DependencyInjection
|
||||
Services.AddUnique<ITemporaryFileToXmlImportService, TemporaryFileToXmlImportService>();
|
||||
Services.AddUnique<IContentTypeImportService, ContentTypeImportService>();
|
||||
Services.AddUnique<IMediaTypeImportService, MediaTypeImportService>();
|
||||
|
||||
// add validation services
|
||||
Services.AddUnique<IElementSwitchValidator, ElementSwitchValidator>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
|
||||
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
|
||||
<language alias="en" intName="English (UK)" localName="English (UK)" lcid="" culture="en-GB">
|
||||
<creator>
|
||||
<name>The Umbraco community</name>
|
||||
@@ -428,6 +428,8 @@
|
||||
<key alias="compilationDebugCheckErrorMessage">Debug compilation mode is currently enabled. It is recommended to
|
||||
disable this setting before go live.
|
||||
</key>
|
||||
<key alias="runtimeModeCheckSuccessMessage">Runtime mode is set to production.</key>
|
||||
<key alias="runtimeModeCheckErrorMessage">Runtime mode is not set to Production. It is recommended to set the Runtime Mode to Production for live/production environments.</key>
|
||||
<!-- The following keys get these tokens passed in:
|
||||
0: Path to the file not found
|
||||
-->
|
||||
|
||||
@@ -419,6 +419,8 @@
|
||||
<key alias="compilationDebugCheckErrorMessage">Debug compilation mode is currently enabled. It is recommended to
|
||||
disable this setting before go live.
|
||||
</key>
|
||||
<key alias="runtimeModeCheckSuccessMessage">Runtime mode is set to production.</key>
|
||||
<key alias="runtimeModeCheckErrorMessage">Runtime mode is not set to Production. It is recommended to set the Runtime Mode to Production for live/production environments.</key>
|
||||
<!-- The following keys get these tokens passed in:
|
||||
0: Comma delimitted list of failed folder paths
|
||||
-->
|
||||
|
||||
@@ -219,6 +219,24 @@ public static class ObjectExtensions
|
||||
}
|
||||
}
|
||||
|
||||
if (target == typeof(DateTime) && input is DateTimeOffset dateTimeOffset)
|
||||
{
|
||||
// IMPORTANT: for compatability with various editors, we must discard any Offset information and assume UTC time here
|
||||
return Attempt.Succeed((object?)new DateTime(
|
||||
new DateOnly(dateTimeOffset.Year, dateTimeOffset.Month, dateTimeOffset.Day),
|
||||
new TimeOnly(dateTimeOffset.Hour, dateTimeOffset.Minute, dateTimeOffset.Second, dateTimeOffset.Millisecond, dateTimeOffset.Microsecond),
|
||||
DateTimeKind.Utc));
|
||||
}
|
||||
|
||||
if (target == typeof(DateTimeOffset) && input is DateTime dateTime)
|
||||
{
|
||||
// IMPORTANT: for compatability with various editors, we must discard any DateTimeKind information and assume UTC time here
|
||||
return Attempt.Succeed((object?)new DateTimeOffset(
|
||||
new DateOnly(dateTime.Year, dateTime.Month, dateTime.Day),
|
||||
new TimeOnly(dateTime.Hour, dateTime.Minute, dateTime.Second, dateTime.Millisecond, dateTime.Microsecond),
|
||||
TimeSpan.Zero));
|
||||
}
|
||||
|
||||
TypeConverter? inputConverter = GetCachedSourceTypeConverter(inputType, target);
|
||||
if (inputConverter != null)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
using Umbraco.Cms.Core.Events;
|
||||
using Umbraco.Cms.Core.Extensions;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Notifications;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.ContentTypeEditing;
|
||||
|
||||
namespace Umbraco.Cms.Core.Handlers;
|
||||
|
||||
public class WarnDocumentTypeElementSwitchNotificationHandler :
|
||||
INotificationAsyncHandler<ContentTypeSavingNotification>,
|
||||
INotificationAsyncHandler<ContentTypeSavedNotification>
|
||||
{
|
||||
private const string NotificationStateKey =
|
||||
"Umbraco.Cms.Core.Handlers.WarnDocumentTypeElementSwitchNotificationHandler";
|
||||
|
||||
private readonly IEventMessagesFactory _eventMessagesFactory;
|
||||
private readonly IContentTypeService _contentTypeService;
|
||||
private readonly IElementSwitchValidator _elementSwitchValidator;
|
||||
|
||||
public WarnDocumentTypeElementSwitchNotificationHandler(
|
||||
IEventMessagesFactory eventMessagesFactory,
|
||||
IContentTypeService contentTypeService,
|
||||
IElementSwitchValidator elementSwitchValidator)
|
||||
{
|
||||
_eventMessagesFactory = eventMessagesFactory;
|
||||
_contentTypeService = contentTypeService;
|
||||
_elementSwitchValidator = elementSwitchValidator;
|
||||
}
|
||||
|
||||
// To figure out whether a warning should be generated, we need both the state before and after saving
|
||||
public async Task HandleAsync(ContentTypeSavingNotification notification, CancellationToken cancellationToken)
|
||||
{
|
||||
IEnumerable<Guid> updatedKeys = notification.SavedEntities
|
||||
.Where(e => e.HasIdentity)
|
||||
.Select(e => e.Key);
|
||||
|
||||
IEnumerable<IContentType> persistedItems = _contentTypeService.GetAll(updatedKeys);
|
||||
|
||||
var stateInformation = persistedItems
|
||||
.ToDictionary(
|
||||
contentType => contentType.Key,
|
||||
contentType => new DocumentTypeElementSwitchInformation { WasElement = contentType.IsElement });
|
||||
notification.State[NotificationStateKey] = stateInformation;
|
||||
}
|
||||
|
||||
public async Task HandleAsync(ContentTypeSavedNotification notification, CancellationToken cancellationToken)
|
||||
{
|
||||
if (notification.State[NotificationStateKey] is not Dictionary<Guid, DocumentTypeElementSwitchInformation>
|
||||
stateInformation)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
EventMessages eventMessages = _eventMessagesFactory.Get();
|
||||
|
||||
foreach (IContentType savedDocumentType in notification.SavedEntities)
|
||||
{
|
||||
if (stateInformation.ContainsKey(savedDocumentType.Key) is false)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
DocumentTypeElementSwitchInformation state = stateInformation[savedDocumentType.Key];
|
||||
if (state.WasElement == savedDocumentType.IsElement)
|
||||
{
|
||||
// no change
|
||||
continue;
|
||||
}
|
||||
|
||||
await WarnIfAncestorsAreMisaligned(savedDocumentType, eventMessages);
|
||||
await WarnIfDescendantsAreMisaligned(savedDocumentType, eventMessages);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task WarnIfAncestorsAreMisaligned(IContentType contentType, EventMessages eventMessages)
|
||||
{
|
||||
if (await _elementSwitchValidator.AncestorsAreAlignedAsync(contentType) == false)
|
||||
{
|
||||
// todo update this message when the format has been agreed upon on with the client
|
||||
eventMessages.Add(new EventMessage(
|
||||
"DocumentType saved",
|
||||
"One or more ancestors have a mismatching element flag",
|
||||
EventMessageType.Warning));
|
||||
}
|
||||
}
|
||||
|
||||
private async Task WarnIfDescendantsAreMisaligned(IContentType contentType, EventMessages eventMessages)
|
||||
{
|
||||
if (await _elementSwitchValidator.DescendantsAreAlignedAsync(contentType) == false)
|
||||
{
|
||||
// todo update this message when the format has been agreed upon on with the client
|
||||
eventMessages.Add(new EventMessage(
|
||||
"DocumentType saved",
|
||||
"One or more descendants have a mismatching element flag",
|
||||
EventMessageType.Warning));
|
||||
}
|
||||
}
|
||||
|
||||
private class DocumentTypeElementSwitchInformation
|
||||
{
|
||||
public bool WasElement { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// Copyright (c) Umbraco.
|
||||
// See LICENSE for more details.
|
||||
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Core.HealthChecks.Checks.LiveEnvironment;
|
||||
|
||||
/// <summary>
|
||||
/// Health check for the recommended production configuration for the runtime mode.
|
||||
/// </summary>
|
||||
[HealthCheck(
|
||||
"8E31E5C9-7A1D-4ACB-A3A8-6495F3EDB932",
|
||||
"Runtime Mode",
|
||||
Description = "The Production Runtime Mode disables development features and checks that settings are configured optimally for production.",
|
||||
Group = "Live Environment")]
|
||||
public class RuntimeModeCheck : AbstractSettingsCheck
|
||||
{
|
||||
private readonly IOptionsMonitor<RuntimeSettings> _runtimeSettings;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RuntimeModeCheck" /> class.
|
||||
/// </summary>
|
||||
public RuntimeModeCheck(ILocalizedTextService textService, IOptionsMonitor<RuntimeSettings> runtimeSettings)
|
||||
: base(textService) =>
|
||||
_runtimeSettings = runtimeSettings;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ItemPath => Constants.Configuration.ConfigRuntimeMode;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override ValueComparisonType ValueComparisonType => ValueComparisonType.ShouldEqual;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IEnumerable<AcceptableConfiguration> Values => new List<AcceptableConfiguration>
|
||||
{
|
||||
new() { IsRecommended = true, Value = RuntimeMode.Production.ToString() },
|
||||
};
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string CurrentValue => _runtimeSettings.CurrentValue.Mode.ToString();
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string CheckSuccessMessage => LocalizedTextService.Localize("healthcheck", "runtimeModeCheckSuccessMessage");
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string CheckErrorMessage => LocalizedTextService.Localize("healthcheck", "runtimeModeCheckErrorMessage");
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ReadMoreLink => Constants.HealthChecks.DocumentationLinks.LiveEnvironment.RuntimeModeCheck;
|
||||
}
|
||||
@@ -27,7 +27,6 @@ public interface IDataValueEditor
|
||||
/// </summary>
|
||||
bool SupportsReadOnly => false;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets the validators to use to validate the edited value.
|
||||
/// </summary>
|
||||
@@ -75,4 +74,6 @@ public interface IDataValueEditor
|
||||
XNode ConvertDbToXml(IPropertyType propertyType, object value);
|
||||
|
||||
string ConvertDbToString(IPropertyType propertyType, object? value);
|
||||
|
||||
IEnumerable<Guid> ConfiguredElementTypeKeys() => Enumerable.Empty<Guid>();
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace Umbraco.Cms.Core.PropertyEditors;
|
||||
public class BlockListConfiguration
|
||||
{
|
||||
[ConfigurationField("blocks")]
|
||||
public BlockConfiguration[] Blocks { get; set; } = null!;
|
||||
public BlockConfiguration[] Blocks { get; set; } = Array.Empty<BlockConfiguration>();
|
||||
|
||||
[ConfigurationField("validationLimit")]
|
||||
public NumberRange ValidationLimit { get; set; } = new();
|
||||
|
||||
@@ -75,6 +75,10 @@ public class DataEditor : IDataEditor
|
||||
[DataMember(Name = "supportsReadOnly", IsRequired = true)]
|
||||
public bool SupportsReadOnly { get; set; }
|
||||
|
||||
// Adding a virtual method that wraps the default implementation allows derived classes
|
||||
// to override the default implementation without having to explicitly inherit the interface.
|
||||
public virtual bool SupportsConfigurableElements => false;
|
||||
|
||||
/// <inheritdoc />
|
||||
[IgnoreDataMember]
|
||||
public bool IsDeprecated { get; }
|
||||
|
||||
@@ -356,6 +356,10 @@ public class DataValueEditor : IDataValueEditor
|
||||
}
|
||||
}
|
||||
|
||||
// Adding a virtual method that wraps the default implementation allows derived classes
|
||||
// to override the default implementation without having to explicitly inherit the interface.
|
||||
public virtual IEnumerable<Guid> ConfiguredElementTypeKeys() => Enumerable.Empty<Guid>();
|
||||
|
||||
/// <summary>
|
||||
/// Used to try to convert the string value to the correct CLR type based on the <see cref="ValueType" /> specified for
|
||||
/// this value editor.
|
||||
|
||||
@@ -16,6 +16,8 @@ public interface IDataEditor : IDiscoverable
|
||||
|
||||
bool SupportsReadOnly => false;
|
||||
|
||||
bool SupportsConfigurableElements => false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the editor is deprecated.
|
||||
/// </summary>
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace Umbraco.Cms.Core.PropertyEditors;
|
||||
public class RichTextConfiguration : IIgnoreUserStartNodesConfig
|
||||
{
|
||||
[ConfigurationField("blocks")]
|
||||
public RichTextBlockConfiguration[]? Blocks { get; set; } = null!;
|
||||
public RichTextBlockConfiguration[]? Blocks { get; set; } = Array.Empty<RichTextBlockConfiguration>();
|
||||
|
||||
[ConfigurationField("mediaParentId")]
|
||||
public Guid? MediaParentId { get; set; }
|
||||
|
||||
@@ -20,23 +20,19 @@ public class DatePickerValueConverter : PropertyValueConverterBase
|
||||
|
||||
internal static DateTime ParseDateTimeValue(object? source)
|
||||
{
|
||||
if (source == null)
|
||||
if (source is null)
|
||||
{
|
||||
return DateTime.MinValue;
|
||||
}
|
||||
|
||||
// in XML a DateTime is: string - format "yyyy-MM-ddTHH:mm:ss"
|
||||
// Actually, not always sometimes it is formatted in UTC style with 'Z' suffixed on the end but that is due to this bug:
|
||||
// http://issues.umbraco.org/issue/U4-4145, http://issues.umbraco.org/issue/U4-3894
|
||||
// We should just be using TryConvertTo instead.
|
||||
if (source is string sourceString)
|
||||
if (source is DateTime dateTimeValue)
|
||||
{
|
||||
Attempt<DateTime> attempt = sourceString.TryConvertTo<DateTime>();
|
||||
return attempt.Success == false ? DateTime.MinValue : attempt.Result;
|
||||
return dateTimeValue;
|
||||
}
|
||||
|
||||
// in the database a DateTime is: DateTime
|
||||
// default value is: DateTime.MinValue
|
||||
return source is DateTime dateTimeValue ? dateTimeValue : DateTime.MinValue;
|
||||
Attempt<DateTime> attempt = source.TryConvertTo<DateTime>();
|
||||
return attempt.Success
|
||||
? attempt.Result
|
||||
: DateTime.MinValue;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Security.Claims;
|
||||
using System.Security.Principal;
|
||||
using Umbraco.Cms.Core.Models.Membership;
|
||||
@@ -20,8 +21,14 @@ internal sealed class AuthorizationHelper : IAuthorizationHelper
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IUser GetUmbracoUser(IPrincipal currentUser)
|
||||
=> TryGetUmbracoUser(currentUser, out IUser? user)
|
||||
? user
|
||||
: throw new InvalidOperationException($"Could not obtain an {nameof(IUser)} instance from {nameof(IPrincipal)}");
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool TryGetUmbracoUser(IPrincipal currentUser, [NotNullWhen(true)] out IUser? user)
|
||||
{
|
||||
IUser? user = null;
|
||||
user = null;
|
||||
ClaimsIdentity? umbIdentity = currentUser.GetUmbracoIdentity();
|
||||
Guid? currentUserKey = umbIdentity?.GetUserKey();
|
||||
|
||||
@@ -38,12 +45,6 @@ internal sealed class AuthorizationHelper : IAuthorizationHelper
|
||||
user = _userService.GetAsync(currentUserKey.Value).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
if (user is null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Could not obtain an {nameof(IUser)} instance from {nameof(IPrincipal)}");
|
||||
}
|
||||
|
||||
return user;
|
||||
return user is not null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Security.Principal;
|
||||
using Umbraco.Cms.Core.Models.Membership;
|
||||
|
||||
@@ -9,11 +10,31 @@ namespace Umbraco.Cms.Core.Security.Authorization;
|
||||
public interface IAuthorizationHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts an <see cref="IUser" /> into <see cref="IPrincipal" />.
|
||||
/// Converts an <see cref="IPrincipal" /> into an <see cref="IUser" />.
|
||||
/// </summary>
|
||||
/// <param name="currentUser">The current user's principal.</param>
|
||||
/// <returns>
|
||||
/// <see cref="IUser" />.
|
||||
/// </returns>
|
||||
IUser GetUmbracoUser(IPrincipal currentUser);
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to convert an <see cref="IPrincipal" /> into an <see cref="IUser" />.
|
||||
/// </summary>
|
||||
/// <param name="currentUser">The current user's principal.</param>
|
||||
/// <param name="user">The resulting <see cref="IUser" />, if the conversion is successful.</param>
|
||||
/// <returns>True if the conversion is successful, false otherwise</returns>
|
||||
bool TryGetUmbracoUser(IPrincipal currentUser, [NotNullWhen(true)] out IUser? user)
|
||||
{
|
||||
try
|
||||
{
|
||||
user = GetUmbracoUser(currentUser);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
user = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.ContentEditing;
|
||||
using Umbraco.Cms.Core.Models.ContentTypeEditing;
|
||||
using Umbraco.Cms.Core.PropertyEditors;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Cms.Core.Strings;
|
||||
using Umbraco.Extensions;
|
||||
@@ -12,8 +15,24 @@ namespace Umbraco.Cms.Core.Services.ContentTypeEditing;
|
||||
internal sealed class ContentTypeEditingService : ContentTypeEditingServiceBase<IContentType, IContentTypeService, ContentTypePropertyTypeModel, ContentTypePropertyContainerModel>, IContentTypeEditingService
|
||||
{
|
||||
private readonly ITemplateService _templateService;
|
||||
private readonly IElementSwitchValidator _elementSwitchValidator;
|
||||
private readonly IContentTypeService _contentTypeService;
|
||||
|
||||
public ContentTypeEditingService(
|
||||
IContentTypeService contentTypeService,
|
||||
ITemplateService templateService,
|
||||
IDataTypeService dataTypeService,
|
||||
IEntityService entityService,
|
||||
IShortStringHelper shortStringHelper,
|
||||
IElementSwitchValidator elementSwitchValidator)
|
||||
: base(contentTypeService, contentTypeService, dataTypeService, entityService, shortStringHelper)
|
||||
{
|
||||
_contentTypeService = contentTypeService;
|
||||
_templateService = templateService;
|
||||
_elementSwitchValidator = elementSwitchValidator;
|
||||
}
|
||||
|
||||
[Obsolete("Use the constructor that is not marked obsolete, will be removed in v16")]
|
||||
public ContentTypeEditingService(
|
||||
IContentTypeService contentTypeService,
|
||||
ITemplateService templateService,
|
||||
@@ -24,6 +43,7 @@ internal sealed class ContentTypeEditingService : ContentTypeEditingServiceBase<
|
||||
{
|
||||
_contentTypeService = contentTypeService;
|
||||
_templateService = templateService;
|
||||
_elementSwitchValidator = StaticServiceProvider.Instance.GetRequiredService<IElementSwitchValidator>();
|
||||
}
|
||||
|
||||
public async Task<Attempt<IContentType?, ContentTypeOperationStatus>> CreateAsync(ContentTypeCreateModel model, Guid userKey)
|
||||
@@ -52,13 +72,20 @@ internal sealed class ContentTypeEditingService : ContentTypeEditingServiceBase<
|
||||
|
||||
public async Task<Attempt<IContentType?, ContentTypeOperationStatus>> UpdateAsync(IContentType contentType, ContentTypeUpdateModel model, Guid userKey)
|
||||
{
|
||||
Attempt<IContentType?, ContentTypeOperationStatus> result = await ValidateAndMapForUpdateAsync(contentType, model);
|
||||
if (result.Success is false)
|
||||
// this needs to happen before the base call as that one is not a pure function
|
||||
ContentTypeOperationStatus elementValidationStatus = await ValidateElementStatusForUpdateAsync(contentType, model);
|
||||
if (elementValidationStatus is not ContentTypeOperationStatus.Success)
|
||||
{
|
||||
return result;
|
||||
return Attempt<IContentType?, ContentTypeOperationStatus>.Fail(elementValidationStatus);
|
||||
}
|
||||
|
||||
contentType = result.Result ?? throw new InvalidOperationException($"{nameof(ValidateAndMapForUpdateAsync)} succeeded but did not yield any result");
|
||||
Attempt<IContentType?, ContentTypeOperationStatus> baseValidationAttempt = await ValidateAndMapForUpdateAsync(contentType, model);
|
||||
if (baseValidationAttempt.Success is false)
|
||||
{
|
||||
return baseValidationAttempt;
|
||||
}
|
||||
|
||||
contentType = baseValidationAttempt.Result ?? throw new InvalidOperationException($"{nameof(ValidateAndMapForUpdateAsync)} succeeded but did not yield any result");
|
||||
|
||||
UpdateHistoryCleanup(contentType, model);
|
||||
UpdateTemplates(contentType, model);
|
||||
@@ -77,6 +104,13 @@ internal sealed class ContentTypeEditingService : ContentTypeEditingServiceBase<
|
||||
bool isElement) =>
|
||||
await FindAvailableCompositionsAsync(key, currentCompositeKeys, currentPropertyAliases, isElement);
|
||||
|
||||
protected override async Task<ContentTypeOperationStatus> AdditionalCreateValidationAsync(
|
||||
ContentTypeEditingModelBase<ContentTypePropertyTypeModel, ContentTypePropertyContainerModel> model)
|
||||
{
|
||||
// validate if the parent documentType (if set) has the same element status as the documentType being created
|
||||
return await ValidateCreateParentElementStatusAsync(model);
|
||||
}
|
||||
|
||||
// update content type history clean-up
|
||||
private void UpdateHistoryCleanup(IContentType contentType, ContentTypeModelBase model)
|
||||
{
|
||||
@@ -100,6 +134,48 @@ internal sealed class ContentTypeEditingService : ContentTypeEditingServiceBase<
|
||||
contentType.SetDefaultTemplate(allowedTemplates.FirstOrDefault(t => t.Key == model.DefaultTemplateKey));
|
||||
}
|
||||
|
||||
private async Task<ContentTypeOperationStatus> ValidateElementStatusForUpdateAsync(IContentTypeBase contentType, ContentTypeModelBase model)
|
||||
{
|
||||
// no change, ignore rest of validation
|
||||
if (contentType.IsElement == model.IsElement)
|
||||
{
|
||||
return ContentTypeOperationStatus.Success;
|
||||
}
|
||||
|
||||
// this method should only contain blocking validation, warnings are handled by WarnDocumentTypeElementSwitchNotificationHandler
|
||||
|
||||
// => check whether the element was used in a block structure prior to updating
|
||||
if (model.IsElement is false)
|
||||
{
|
||||
return await _elementSwitchValidator.ElementToDocumentNotUsedInBlockStructuresAsync(contentType)
|
||||
? ContentTypeOperationStatus.Success
|
||||
: ContentTypeOperationStatus.InvalidElementFlagElementIsUsedInPropertyEditorConfiguration;
|
||||
}
|
||||
|
||||
return await _elementSwitchValidator.DocumentToElementHasNoContentAsync(contentType)
|
||||
? ContentTypeOperationStatus.Success
|
||||
: ContentTypeOperationStatus.InvalidElementFlagDocumentHasContent;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Should be called after it has been established that the composition list is in a valid state and the (composition) parent exists
|
||||
/// </summary>
|
||||
private async Task<ContentTypeOperationStatus> ValidateCreateParentElementStatusAsync(
|
||||
ContentTypeEditingModelBase<ContentTypePropertyTypeModel, ContentTypePropertyContainerModel> model)
|
||||
{
|
||||
Guid? parentId = model.Compositions
|
||||
.SingleOrDefault(composition => composition.CompositionType == CompositionType.Inheritance)?.Key;
|
||||
if (parentId is null)
|
||||
{
|
||||
return ContentTypeOperationStatus.Success;
|
||||
}
|
||||
|
||||
IContentType? parent = await _contentTypeService.GetAsync(parentId.Value);
|
||||
return parent!.IsElement == model.IsElement
|
||||
? ContentTypeOperationStatus.Success
|
||||
: ContentTypeOperationStatus.InvalidElementFlagComparedToParent;
|
||||
}
|
||||
|
||||
protected override IContentType CreateContentType(IShortStringHelper shortStringHelper, int parentId)
|
||||
=> new ContentType(shortStringHelper, parentId);
|
||||
|
||||
|
||||
@@ -91,6 +91,8 @@ internal abstract class ContentTypeEditingServiceBase<TContentType, TContentType
|
||||
return Attempt.FailWithStatus<TContentType?, ContentTypeOperationStatus>(operationStatus, null);
|
||||
}
|
||||
|
||||
await AdditionalCreateValidationAsync(model);
|
||||
|
||||
// get the ID of the parent to create the content type under (we already validated that it exists)
|
||||
var parentId = GetParentId(model, containerKey) ?? throw new ArgumentException("Parent ID could not be found", nameof(model));
|
||||
TContentType contentType = CreateContentType(_shortStringHelper, parentId);
|
||||
@@ -137,6 +139,10 @@ internal abstract class ContentTypeEditingServiceBase<TContentType, TContentType
|
||||
return Attempt.SucceedWithStatus<TContentType?, ContentTypeOperationStatus>(ContentTypeOperationStatus.Success, contentType);
|
||||
}
|
||||
|
||||
protected virtual async Task<ContentTypeOperationStatus> AdditionalCreateValidationAsync(
|
||||
ContentTypeEditingModelBase<TPropertyTypeModel, TPropertyTypeContainer> model)
|
||||
=> await Task.FromResult(ContentTypeOperationStatus.Success);
|
||||
|
||||
#region Sanitization
|
||||
|
||||
private void SanitizeModelAliases(ContentTypeEditingModelBase<TPropertyTypeModel, TPropertyTypeContainer> model)
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
using Umbraco.Cms.Core.Extensions;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.PropertyEditors;
|
||||
|
||||
namespace Umbraco.Cms.Core.Services.ContentTypeEditing;
|
||||
|
||||
public class ElementSwitchValidator : IElementSwitchValidator
|
||||
{
|
||||
private readonly IContentTypeService _contentTypeService;
|
||||
private readonly PropertyEditorCollection _propertyEditorCollection;
|
||||
private readonly IDataTypeService _dataTypeService;
|
||||
|
||||
public ElementSwitchValidator(
|
||||
IContentTypeService contentTypeService,
|
||||
PropertyEditorCollection propertyEditorCollection,
|
||||
IDataTypeService dataTypeService)
|
||||
{
|
||||
_contentTypeService = contentTypeService;
|
||||
_propertyEditorCollection = propertyEditorCollection;
|
||||
_dataTypeService = dataTypeService;
|
||||
}
|
||||
|
||||
public async Task<bool> AncestorsAreAlignedAsync(IContentType contentType)
|
||||
{
|
||||
// this call does not return the system roots
|
||||
var ancestorIds = contentType.AncestorIds();
|
||||
if (ancestorIds.Length == 0)
|
||||
{
|
||||
// if there are no ancestors, validation passes
|
||||
return true;
|
||||
}
|
||||
|
||||
// if there are any ancestors where IsElement is different from the contentType, the validation fails
|
||||
return await Task.FromResult(_contentTypeService.GetAll(ancestorIds)
|
||||
.Any(ancestor => ancestor.IsElement != contentType.IsElement) is false);
|
||||
}
|
||||
|
||||
public async Task<bool> DescendantsAreAlignedAsync(IContentType contentType)
|
||||
{
|
||||
IEnumerable<IContentType> descendants = _contentTypeService.GetDescendants(contentType.Id, false);
|
||||
|
||||
// if there are any descendants where IsElement is different from the contentType, the validation fails
|
||||
return await Task.FromResult(descendants.Any(descendant => descendant.IsElement != contentType.IsElement) is false);
|
||||
}
|
||||
|
||||
public async Task<bool> ElementToDocumentNotUsedInBlockStructuresAsync(IContentTypeBase contentType)
|
||||
{
|
||||
// get all propertyEditors that support block usage
|
||||
IDataEditor[] editors = _propertyEditorCollection.Where(pe => pe.SupportsConfigurableElements).ToArray();
|
||||
var blockEditorAliases = editors.Select(pe => pe.Alias).ToArray();
|
||||
|
||||
// get all dataTypes that are based on those propertyEditors
|
||||
IEnumerable<IDataType> dataTypes = await _dataTypeService.GetByEditorAliasAsync(blockEditorAliases);
|
||||
|
||||
// if any dataType has a configuration where this element is selected as a possible block, the validation fails.
|
||||
return dataTypes.Any(dataType =>
|
||||
editors.First(editor => editor.Alias == dataType.EditorAlias)
|
||||
.GetValueEditor(dataType.ConfigurationObject)
|
||||
.ConfiguredElementTypeKeys().Contains(contentType.Key)) is false;
|
||||
}
|
||||
|
||||
public async Task<bool> DocumentToElementHasNoContentAsync(IContentTypeBase contentType) =>
|
||||
|
||||
// if any content for the content type exists, the validation fails.
|
||||
await Task.FromResult(_contentTypeService.HasContentNodes(contentType.Id) is false);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using Umbraco.Cms.Core.Models;
|
||||
|
||||
namespace Umbraco.Cms.Core.Services.ContentTypeEditing;
|
||||
|
||||
public interface IElementSwitchValidator
|
||||
{
|
||||
Task<bool> AncestorsAreAlignedAsync(IContentType contentType);
|
||||
|
||||
Task<bool> DescendantsAreAlignedAsync(IContentType contentType);
|
||||
|
||||
Task<bool> ElementToDocumentNotUsedInBlockStructuresAsync(IContentTypeBase contentType);
|
||||
|
||||
Task<bool> DocumentToElementHasNoContentAsync(IContentTypeBase contentType);
|
||||
}
|
||||
@@ -331,6 +331,16 @@ namespace Umbraco.Cms.Core.Services.Implement
|
||||
return Task.FromResult(dataTypes);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IEnumerable<IDataType>> GetByEditorAliasAsync(string[] propertyEditorAlias)
|
||||
{
|
||||
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
|
||||
IQuery<IDataType> query = Query<IDataType>().Where(x => propertyEditorAlias.Contains(x.EditorAlias));
|
||||
IEnumerable<IDataType> dataTypes = _dataTypeRepository.Get(query).ToArray();
|
||||
ConvertMissingEditorsOfDataTypesToLabels(dataTypes);
|
||||
return await Task.FromResult(dataTypes);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IEnumerable<IDataType>> GetByEditorUiAlias(string editorUiAlias)
|
||||
{
|
||||
|
||||
@@ -6,6 +6,7 @@ using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.ContentEditing;
|
||||
using Umbraco.Cms.Core.Notifications;
|
||||
using Umbraco.Cms.Core.Persistence.Repositories;
|
||||
using Umbraco.Cms.Core.Routing;
|
||||
using Umbraco.Cms.Core.Scoping;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Extensions;
|
||||
@@ -201,6 +202,11 @@ public class DomainService : RepositoryService, IDomainService
|
||||
foreach (DomainModel domainModel in updateModel.Domains)
|
||||
{
|
||||
domainModel.DomainName = domainModel.DomainName.ToLowerInvariant();
|
||||
|
||||
if(Uri.IsWellFormedUriString(domainModel.DomainName, UriKind.RelativeOrAbsolute) is false)
|
||||
{
|
||||
return Attempt.FailWithStatus(DomainOperationStatus.InvalidDomainName, new DomainUpdateResult());
|
||||
}
|
||||
}
|
||||
|
||||
// make sure we're not attempting to assign duplicate domains
|
||||
|
||||
@@ -240,4 +240,11 @@ public interface IDataTypeService : IService
|
||||
/// <param name="dataType">The data type whose configuration to validate.</param>
|
||||
/// <returns>One or more <see cref="ValidationResult"/> if the configuration data is invalid, an empty collection otherwise.</returns>
|
||||
IEnumerable<ValidationResult> ValidateConfigurationData(IDataType dataType);
|
||||
|
||||
/// <summary>
|
||||
/// Gets all <see cref="IDataType" /> for a set of property editors
|
||||
/// </summary>
|
||||
/// <param name="propertyEditorAlias">Aliases of the property editors</param>
|
||||
/// <returns>Collection of <see cref="IDataType" /> configured for the property editors</returns>
|
||||
Task<IEnumerable<IDataType>> GetByEditorAliasAsync(string[] propertyEditorAlias);
|
||||
}
|
||||
|
||||
@@ -1002,7 +1002,6 @@ namespace Umbraco.Cms.Core.Services
|
||||
MoveToRecycleBinEventInfo<IMedia>[] moveInfo = moves.Select(x => new MoveToRecycleBinEventInfo<IMedia>(x.Item1, x.Item2)).ToArray();
|
||||
scope.Notifications.Publish(new MediaMovedToRecycleBinNotification(moveInfo, messages).WithStateFrom(movingToRecycleBinNotification));
|
||||
Audit(AuditType.Move, userId, media.Id, "Move Media to recycle bin");
|
||||
|
||||
scope.Complete();
|
||||
}
|
||||
|
||||
|
||||
@@ -21,4 +21,7 @@ public enum ContentTypeOperationStatus
|
||||
NotFound,
|
||||
NotAllowed,
|
||||
CancelledByNotification,
|
||||
InvalidElementFlagDocumentHasContent,
|
||||
InvalidElementFlagElementIsUsedInPropertyEditorConfiguration,
|
||||
InvalidElementFlagComparedToParent,
|
||||
}
|
||||
|
||||
@@ -7,5 +7,6 @@ public enum DomainOperationStatus
|
||||
ContentNotFound,
|
||||
LanguageNotFound,
|
||||
DuplicateDomainName,
|
||||
ConflictingDomainName
|
||||
ConflictingDomainName,
|
||||
InvalidDomainName
|
||||
}
|
||||
|
||||
@@ -35,11 +35,11 @@ public sealed class HtmlLocalLinkParser
|
||||
|
||||
public IEnumerable<Udi?> FindUdisFromLocalLinks(string text)
|
||||
{
|
||||
foreach ((var intId, GuidUdi? udi, var tagValue) in FindLocalLinkIds(text))
|
||||
foreach (LocalLinkTag tagData in FindLocalLinkIds(text))
|
||||
{
|
||||
if (udi is not null)
|
||||
if (tagData.Udi is not null)
|
||||
{
|
||||
yield return udi; // In v8, we only care abuot UDIs
|
||||
yield return tagData.Udi; // In v8, we only care about UDIs
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -80,38 +80,41 @@ public sealed class HtmlLocalLinkParser
|
||||
throw new InvalidOperationException("Could not parse internal links, there is no current UmbracoContext");
|
||||
}
|
||||
|
||||
foreach ((var intId, GuidUdi? udi, var tagValue) in FindLocalLinkIds(text))
|
||||
foreach (LocalLinkTag tagData in FindLocalLinkIds(text))
|
||||
{
|
||||
if (udi is not null)
|
||||
if (tagData.Udi is not null)
|
||||
{
|
||||
var newLink = "#";
|
||||
if (udi?.EntityType == Constants.UdiEntityType.Document)
|
||||
if (tagData.Udi?.EntityType == Constants.UdiEntityType.Document)
|
||||
{
|
||||
newLink = _publishedUrlProvider.GetUrl(udi.Guid);
|
||||
newLink = _publishedUrlProvider.GetUrl(tagData.Udi.Guid);
|
||||
}
|
||||
else if (udi?.EntityType == Constants.UdiEntityType.Media)
|
||||
else if (tagData.Udi?.EntityType == Constants.UdiEntityType.Media)
|
||||
{
|
||||
newLink = _publishedUrlProvider.GetMediaUrl(udi.Guid);
|
||||
newLink = _publishedUrlProvider.GetMediaUrl(tagData.Udi.Guid);
|
||||
}
|
||||
|
||||
if (newLink == null)
|
||||
{
|
||||
newLink = "#";
|
||||
}
|
||||
|
||||
text = text.Replace(tagValue, "href=\"" + newLink);
|
||||
text = StripTypeAttributeFromTag(text, tagData.Udi!.EntityType);
|
||||
text = text.Replace(tagData.TagHref, "href=\"" + newLink);
|
||||
}
|
||||
else if (intId.HasValue)
|
||||
else if (tagData.IntId.HasValue)
|
||||
{
|
||||
var newLink = _publishedUrlProvider.GetUrl(intId.Value);
|
||||
text = text.Replace(tagValue, "href=\"" + newLink);
|
||||
var newLink = _publishedUrlProvider.GetUrl(tagData.IntId.Value);
|
||||
text = text.Replace(tagData.TagHref, "href=\"" + newLink);
|
||||
}
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
private IEnumerable<(int? intId, GuidUdi? udi, string tagValue)> FindLocalLinkIds(string text)
|
||||
// under normal circumstances, the type attribute is preceded by a space
|
||||
// to cover the rare occasion where it isn't, we first replace with a a space and then without.
|
||||
private string StripTypeAttributeFromTag(string tag, string type) =>
|
||||
tag.Replace($" type=\"{type}\"", string.Empty)
|
||||
.Replace($"type=\"{type}\"", string.Empty);
|
||||
|
||||
private IEnumerable<LocalLinkTag> FindLocalLinkIds(string text)
|
||||
{
|
||||
MatchCollection localLinkTagMatches = LocalLinkTagPattern.Matches(text);
|
||||
foreach (Match linkTag in localLinkTagMatches)
|
||||
@@ -126,18 +129,22 @@ public sealed class HtmlLocalLinkParser
|
||||
continue;
|
||||
}
|
||||
|
||||
yield return (null, new GuidUdi(linkTag.Groups["type"].Value, guid), linkTag.Groups["locallink"].Value);
|
||||
yield return new LocalLinkTag(
|
||||
null,
|
||||
new GuidUdi(linkTag.Groups["type"].Value, guid),
|
||||
linkTag.Groups["locallink"].Value,
|
||||
linkTag.Value);
|
||||
}
|
||||
|
||||
// also return legacy results for values that have not been migrated
|
||||
foreach ((int? intId, GuidUdi? udi, string tagValue) legacyResult in FindLegacyLocalLinkIds(text))
|
||||
foreach (LocalLinkTag legacyResult in FindLegacyLocalLinkIds(text))
|
||||
{
|
||||
yield return legacyResult;
|
||||
}
|
||||
}
|
||||
|
||||
// todo remove at some point?
|
||||
private IEnumerable<(int? intId, GuidUdi? udi, string tagValue)> FindLegacyLocalLinkIds(string text)
|
||||
private IEnumerable<LocalLinkTag> FindLegacyLocalLinkIds(string text)
|
||||
{
|
||||
// Parse internal links
|
||||
MatchCollection tags = LocalLinkPattern.Matches(text);
|
||||
@@ -153,15 +160,41 @@ public sealed class HtmlLocalLinkParser
|
||||
var guidUdi = udi as GuidUdi;
|
||||
if (guidUdi is not null)
|
||||
{
|
||||
yield return (null, guidUdi, tag.Value);
|
||||
yield return new LocalLinkTag(null, guidUdi, tag.Value, null);
|
||||
}
|
||||
}
|
||||
|
||||
if (int.TryParse(id, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intId))
|
||||
{
|
||||
yield return (intId, null, tag.Value);
|
||||
yield return new LocalLinkTag (intId, null, tag.Value, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class LocalLinkTag
|
||||
{
|
||||
public LocalLinkTag(int? intId, GuidUdi? udi, string tagHref)
|
||||
{
|
||||
IntId = intId;
|
||||
Udi = udi;
|
||||
TagHref = tagHref;
|
||||
}
|
||||
|
||||
public LocalLinkTag(int? intId, GuidUdi? udi, string tagHref, string? fullTag)
|
||||
{
|
||||
IntId = intId;
|
||||
Udi = udi;
|
||||
TagHref = tagHref;
|
||||
FullTag = fullTag;
|
||||
}
|
||||
|
||||
public int? IntId { get; }
|
||||
|
||||
public GuidUdi? Udi { get; }
|
||||
|
||||
public string TagHref { get; }
|
||||
|
||||
public string? FullTag { get; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,17 +75,25 @@ public class UdiRange
|
||||
|
||||
public static bool operator !=(UdiRange range1, UdiRange range2) => !(range1 == range2);
|
||||
|
||||
public static UdiRange Parse(string s)
|
||||
public static UdiRange Parse(string value)
|
||||
{
|
||||
if (Uri.IsWellFormedUriString(s, UriKind.Absolute) == false
|
||||
|| Uri.TryCreate(s, UriKind.Absolute, out Uri? uri) == false)
|
||||
if (Uri.TryCreate(value, UriKind.Absolute, out Uri? uri) is false ||
|
||||
uri.IsWellFormedOriginalString() is false)
|
||||
{
|
||||
// if (tryParse) return false;
|
||||
throw new FormatException(string.Format("String \"{0}\" is not a valid udi range.", s));
|
||||
throw new FormatException($"String \"{value}\" is not a valid UDI range.");
|
||||
}
|
||||
|
||||
Uri udiUri = uri.Query == string.Empty ? uri : new UriBuilder(uri) { Query = string.Empty }.Uri;
|
||||
return new UdiRange(Udi.Create(udiUri), uri.Query.TrimStart(Constants.CharArrays.QuestionMark));
|
||||
// Remove selector from UDI
|
||||
Uri udiUri = string.IsNullOrEmpty(uri.Query)
|
||||
? uri
|
||||
: new UriBuilder(uri) { Query = string.Empty }.Uri;
|
||||
|
||||
var udi = Udi.Create(udiUri);
|
||||
|
||||
// Only specify selector if query string is not empty
|
||||
return string.IsNullOrEmpty(uri.Query)
|
||||
? new UdiRange(udi)
|
||||
: new UdiRange(udi, uri.Query.TrimStart(Constants.CharArrays.QuestionMark));
|
||||
}
|
||||
|
||||
public override string ToString() => _uriValue.ToString();
|
||||
|
||||
@@ -60,9 +60,18 @@ internal sealed class ApiRichTextMarkupParser : ApiRichTextParserBase, IApiRichT
|
||||
link.SetAttributeValue("href", route.Path);
|
||||
link.SetAttributeValue("data-start-item-path", route.StartItem.Path);
|
||||
link.SetAttributeValue("data-start-item-id", route.StartItem.Id.ToString("D"));
|
||||
link.Attributes["type"]?.Remove();
|
||||
},
|
||||
url => link.SetAttributeValue("href", url),
|
||||
() => link.Attributes.Remove("href"));
|
||||
url =>
|
||||
{
|
||||
link.SetAttributeValue("href", url);
|
||||
link.Attributes["type"]?.Remove();
|
||||
},
|
||||
() =>
|
||||
{
|
||||
link.Attributes.Remove("href");
|
||||
link.Attributes["type"]?.Remove();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -407,6 +407,11 @@ public static partial class UmbracoBuilderExtensions
|
||||
builder
|
||||
.AddNotificationHandler<ContentPublishedNotification, AddDomainWarningsWhenPublishingNotificationHandler>();
|
||||
|
||||
// Handlers for save warnings
|
||||
builder
|
||||
.AddNotificationAsyncHandler<ContentTypeSavingNotification, WarnDocumentTypeElementSwitchNotificationHandler>()
|
||||
.AddNotificationAsyncHandler<ContentTypeSavedNotification, WarnDocumentTypeElementSwitchNotificationHandler>();
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
// See LICENSE for more details.
|
||||
|
||||
using System.Globalization;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Notifications;
|
||||
using Umbraco.Cms.Core.Scoping;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Extensions;
|
||||
using IScope = Umbraco.Cms.Infrastructure.Scoping.IScope;
|
||||
|
||||
namespace Umbraco.Cms.Core.Events;
|
||||
|
||||
@@ -21,21 +23,35 @@ public sealed class RelateOnTrashNotificationHandler :
|
||||
private readonly IAuditService _auditService;
|
||||
private readonly IEntityService _entityService;
|
||||
private readonly IRelationService _relationService;
|
||||
private readonly IScopeProvider _scopeProvider;
|
||||
private readonly ICoreScopeProvider _scopeProvider;
|
||||
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
|
||||
private readonly ILocalizedTextService _textService;
|
||||
|
||||
[Obsolete("Use the new constructor instead, will be removed in V16")]
|
||||
public RelateOnTrashNotificationHandler(
|
||||
IRelationService relationService,
|
||||
IEntityService entityService,
|
||||
ILocalizedTextService textService,
|
||||
IAuditService auditService,
|
||||
IScopeProvider scopeProvider)
|
||||
: this(relationService, entityService, textService, auditService, scopeProvider, StaticServiceProvider.Instance.GetRequiredService<IBackOfficeSecurityAccessor>())
|
||||
{
|
||||
}
|
||||
|
||||
public RelateOnTrashNotificationHandler(
|
||||
IRelationService relationService,
|
||||
IEntityService entityService,
|
||||
ILocalizedTextService textService,
|
||||
IAuditService auditService,
|
||||
IScopeProvider scopeProvider,
|
||||
IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
|
||||
{
|
||||
_relationService = relationService;
|
||||
_entityService = entityService;
|
||||
_textService = textService;
|
||||
_auditService = auditService;
|
||||
_scopeProvider = scopeProvider;
|
||||
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
|
||||
}
|
||||
|
||||
public void Handle(ContentMovedNotification notification)
|
||||
@@ -56,7 +72,7 @@ public sealed class RelateOnTrashNotificationHandler :
|
||||
|
||||
public void Handle(ContentMovedToRecycleBinNotification notification)
|
||||
{
|
||||
using (IScope scope = _scopeProvider.CreateScope())
|
||||
using (ICoreScope scope = _scopeProvider.CreateCoreScope())
|
||||
{
|
||||
const string relationTypeAlias = Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteAlias;
|
||||
IRelationType? relationType = _relationService.GetRelationTypeByAlias(relationTypeAlias);
|
||||
@@ -90,7 +106,7 @@ public sealed class RelateOnTrashNotificationHandler :
|
||||
|
||||
_auditService.Add(
|
||||
AuditType.Delete,
|
||||
item.Entity.WriterId,
|
||||
_backOfficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.Id ?? item.Entity.WriterId,
|
||||
item.Entity.Id,
|
||||
UmbracoObjectTypes.Document.GetName(),
|
||||
string.Format(_textService.Localize("recycleBin", "contentTrashed"), item.Entity.Id, originalParentId));
|
||||
@@ -118,7 +134,7 @@ public sealed class RelateOnTrashNotificationHandler :
|
||||
|
||||
public void Handle(MediaMovedToRecycleBinNotification notification)
|
||||
{
|
||||
using (IScope scope = _scopeProvider.CreateScope())
|
||||
using (ICoreScope scope = _scopeProvider.CreateCoreScope())
|
||||
{
|
||||
const string relationTypeAlias = Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteAlias;
|
||||
IRelationType? relationType = _relationService.GetRelationTypeByAlias(relationTypeAlias);
|
||||
@@ -150,7 +166,7 @@ public sealed class RelateOnTrashNotificationHandler :
|
||||
_relationService.Save(relation);
|
||||
_auditService.Add(
|
||||
AuditType.Delete,
|
||||
item.Entity.CreatorId,
|
||||
_backOfficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.Id ?? item.Entity.WriterId,
|
||||
item.Entity.Id,
|
||||
UmbracoObjectTypes.Media.GetName(),
|
||||
string.Format(_textService.Localize("recycleBin", "mediaTrashed"), item.Entity.Id, originalParentId));
|
||||
|
||||
@@ -129,7 +129,7 @@ public abstract class RecurringHostedServiceBase : IHostedService, IDisposable
|
||||
/// Executes the task.
|
||||
/// </summary>
|
||||
/// <param name="state">The task state.</param>
|
||||
public async void ExecuteAsync(object? state)
|
||||
public virtual async void ExecuteAsync(object? state)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
@@ -51,7 +51,6 @@ public class FilePermissionHelper : IFilePermissionHelper
|
||||
{
|
||||
hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Bin),
|
||||
hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Umbraco),
|
||||
hostingEnvironment.MapPathWebRoot(_globalSettings.UmbracoPath),
|
||||
hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Packages),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -89,6 +89,9 @@ public class UmbracoPlan : MigrationPlan
|
||||
To<V_14_1_0.MigrateRichTextConfiguration>("{FEF2DAF4-5408-4636-BB0E-B8798DF8F095}");
|
||||
To<V_14_1_0.MigrateOldRichTextSeedConfiguration>("{A385C5DF-48DC-46B4-A742-D5BB846483BC}");
|
||||
|
||||
// To 14.2.0
|
||||
To<V_14_2_0.AddMissingDateTimeConfiguration>("{20ED404C-6FF9-4F91-8AC9-2B298E0002EB}");
|
||||
|
||||
// To 15.0.0
|
||||
To<V_15_0_0.AddUserClientId>("{7F4F31D8-DD71-4F0D-93FC-2690A924D84B}");
|
||||
To<V_15_0_0.AddTypeToUser>("{1A8835EF-F8AB-4472-B4D8-D75B7C164022}");
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
using NPoco;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Serialization;
|
||||
using Umbraco.Cms.Infrastructure.Persistence;
|
||||
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_14_2_0;
|
||||
|
||||
public class AddMissingDateTimeConfiguration : MigrationBase
|
||||
{
|
||||
private readonly IConfigurationEditorJsonSerializer _configurationEditorJsonSerializer;
|
||||
|
||||
public AddMissingDateTimeConfiguration(IMigrationContext context, IConfigurationEditorJsonSerializer configurationEditorJsonSerializer)
|
||||
: base(context)
|
||||
=> _configurationEditorJsonSerializer = configurationEditorJsonSerializer;
|
||||
|
||||
protected override void Migrate()
|
||||
{
|
||||
Sql<ISqlContext> sql = Sql()
|
||||
.Select<DataTypeDto>()
|
||||
.From<DataTypeDto>()
|
||||
.Where<DataTypeDto>(dto =>
|
||||
dto.NodeId == Constants.DataTypes.DateTime
|
||||
&& dto.EditorAlias.Equals(Constants.PropertyEditors.Aliases.DateTime));
|
||||
|
||||
DataTypeDto? dataTypeDto = Database.FirstOrDefault<DataTypeDto>(sql);
|
||||
if (dataTypeDto is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Dictionary<string, object> configurationData = dataTypeDto.Configuration.IsNullOrWhiteSpace()
|
||||
? new Dictionary<string, object>()
|
||||
: _configurationEditorJsonSerializer
|
||||
.Deserialize<Dictionary<string, object?>>(dataTypeDto.Configuration)?
|
||||
.Where(item => item.Value is not null)
|
||||
.ToDictionary(item => item.Key, item => item.Value!)
|
||||
?? new Dictionary<string, object>();
|
||||
|
||||
// only proceed with the migration if the data-type has no format assigned
|
||||
if (configurationData.TryAdd("format", "YYYY-MM-DD HH:mm:ss") is false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
dataTypeDto.Configuration = _configurationEditorJsonSerializer.Serialize(configurationData);
|
||||
Database.Update(dataTypeDto);
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ using Umbraco.Cms.Core.Strings;
|
||||
|
||||
namespace Umbraco.Cms.Core.PropertyEditors;
|
||||
|
||||
internal abstract class BlockEditorPropertyValueEditor<TValue, TLayout> : BlockValuePropertyValueEditorBase<TValue, TLayout>
|
||||
public abstract class BlockEditorPropertyValueEditor<TValue, TLayout> : BlockValuePropertyValueEditorBase<TValue, TLayout>
|
||||
where TValue : BlockValue<TLayout>, new()
|
||||
where TLayout : class, IBlockLayoutItem, new()
|
||||
{
|
||||
|
||||
@@ -7,7 +7,7 @@ using Umbraco.Cms.Core.Services;
|
||||
|
||||
namespace Umbraco.Cms.Core.PropertyEditors;
|
||||
|
||||
internal class BlockEditorValidator<TValue, TLayout> : BlockEditorValidatorBase<TValue, TLayout>
|
||||
public class BlockEditorValidator<TValue, TLayout> : BlockEditorValidatorBase<TValue, TLayout>
|
||||
where TValue : BlockValue<TLayout>, new()
|
||||
where TLayout : class, IBlockLayoutItem, new()
|
||||
{
|
||||
|
||||
@@ -6,7 +6,7 @@ using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Core.PropertyEditors;
|
||||
|
||||
internal abstract class BlockEditorValidatorBase<TValue, TLayout> : ComplexEditorValidator
|
||||
public abstract class BlockEditorValidatorBase<TValue, TLayout> : ComplexEditorValidator
|
||||
where TValue : BlockValue<TLayout>, new()
|
||||
where TLayout : class, IBlockLayoutItem, new()
|
||||
{
|
||||
|
||||
@@ -10,9 +10,9 @@ using Umbraco.Extensions;
|
||||
namespace Umbraco.Cms.Core.PropertyEditors;
|
||||
|
||||
/// <summary>
|
||||
/// Used to deserialize json values and clean up any values based on the existence of element types and layout structure
|
||||
/// Used to deserialize json values and clean up any values based on the existence of element types and layout structure.
|
||||
/// </summary>
|
||||
internal class BlockEditorValues<TValue, TLayout>
|
||||
public class BlockEditorValues<TValue, TLayout>
|
||||
where TValue : BlockValue<TLayout>, new()
|
||||
where TLayout : class, IBlockLayoutItem, new()
|
||||
{
|
||||
|
||||
@@ -22,6 +22,7 @@ public class BlockGridPropertyEditor : BlockGridPropertyEditorBase
|
||||
: base(dataValueEditorFactory, blockValuePropertyIndexValueFactory)
|
||||
=> _ioHelper = ioHelper;
|
||||
|
||||
public override bool SupportsConfigurableElements => true;
|
||||
|
||||
#region Pre Value Editor
|
||||
|
||||
|
||||
@@ -111,6 +111,12 @@ public abstract class BlockGridPropertyEditorBase : DataEditor
|
||||
return validationResults;
|
||||
}
|
||||
}
|
||||
|
||||
public override IEnumerable<Guid> ConfiguredElementTypeKeys()
|
||||
{
|
||||
var configuration = ConfigurationObject as BlockGridConfiguration;
|
||||
return configuration?.Blocks.SelectMany(ConfiguredElementTypeKeys) ?? Enumerable.Empty<Guid>();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -36,6 +36,8 @@ public class BlockListPropertyEditor : BlockListPropertyEditorBase
|
||||
{
|
||||
}
|
||||
|
||||
public override bool SupportsConfigurableElements => true;
|
||||
|
||||
#region Pre Value Editor
|
||||
|
||||
protected override IConfigurationEditor CreateConfigurationEditor() =>
|
||||
|
||||
@@ -93,6 +93,12 @@ public abstract class BlockListPropertyEditorBase : DataEditor
|
||||
return ValidateNumberOfBlocks(blockEditorData, validationLimit.Min, validationLimit.Max);
|
||||
}
|
||||
}
|
||||
|
||||
public override IEnumerable<Guid> ConfiguredElementTypeKeys()
|
||||
{
|
||||
var configuration = ConfigurationObject as BlockListConfiguration;
|
||||
return configuration?.Blocks.SelectMany(ConfiguredElementTypeKeys) ?? Enumerable.Empty<Guid>();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -10,7 +10,7 @@ using Umbraco.Cms.Core.Strings;
|
||||
|
||||
namespace Umbraco.Cms.Core.PropertyEditors;
|
||||
|
||||
internal abstract class BlockValuePropertyValueEditorBase<TValue, TLayout> : DataValueEditor, IDataValueReference, IDataValueTags
|
||||
public abstract class BlockValuePropertyValueEditorBase<TValue, TLayout> : DataValueEditor, IDataValueReference, IDataValueTags
|
||||
where TValue : BlockValue<TLayout>, new()
|
||||
where TLayout : class, IBlockLayoutItem, new()
|
||||
{
|
||||
@@ -114,6 +114,15 @@ internal abstract class BlockValuePropertyValueEditorBase<TValue, TLayout> : Dat
|
||||
MapBlockItemDataToEditor(property, blockValue.SettingsData);
|
||||
}
|
||||
|
||||
protected IEnumerable<Guid> ConfiguredElementTypeKeys(IBlockConfiguration configuration)
|
||||
{
|
||||
yield return configuration.ContentElementTypeKey;
|
||||
if (configuration.SettingsElementTypeKey is not null)
|
||||
{
|
||||
yield return configuration.SettingsElementTypeKey.Value;
|
||||
}
|
||||
}
|
||||
|
||||
private void MapBlockItemDataToEditor(IProperty property, List<BlockItemData> items)
|
||||
{
|
||||
var valEditors = new Dictionary<Guid, IDataValueEditor>();
|
||||
|
||||
@@ -47,6 +47,8 @@ public class RichTextPropertyEditor : DataEditor
|
||||
|
||||
public override IPropertyIndexValueFactory PropertyIndexValueFactory => _richTextPropertyIndexValueFactory;
|
||||
|
||||
public override bool SupportsConfigurableElements => true;
|
||||
|
||||
/// <summary>
|
||||
/// Create a custom value editor
|
||||
/// </summary>
|
||||
@@ -238,6 +240,12 @@ public class RichTextPropertyEditor : DataEditor
|
||||
return RichTextPropertyEditorHelper.SerializeRichTextEditorValue(cleanedUpRichTextEditorValue, _jsonSerializer);
|
||||
}
|
||||
|
||||
public override IEnumerable<Guid> ConfiguredElementTypeKeys()
|
||||
{
|
||||
var configuration = ConfigurationObject as RichTextConfiguration;
|
||||
return configuration?.Blocks?.SelectMany(ConfiguredElementTypeKeys) ?? Enumerable.Empty<Guid>();
|
||||
}
|
||||
|
||||
private bool TryParseEditorValue(object? value, [NotNullWhen(true)] out RichTextEditorValue? richTextEditorValue)
|
||||
=> RichTextPropertyEditorHelper.TryParseRichTextEditorValue(value, _jsonSerializer, _logger, out richTextEditorValue);
|
||||
|
||||
|
||||
@@ -113,7 +113,7 @@ public class IdentityMapDefinition : IMapDefinition
|
||||
target.PasswordConfig = source.PasswordConfiguration;
|
||||
target.IsApproved = source.IsApproved;
|
||||
target.SecurityStamp = source.SecurityStamp;
|
||||
DateTime? lockedOutUntil = source.LastLockoutDate?.AddMinutes(_securitySettings.UserDefaultLockoutTimeInMinutes);
|
||||
DateTime? lockedOutUntil = source.LastLockoutDate?.AddMinutes(_securitySettings.MemberDefaultLockoutTimeInMinutes);
|
||||
target.LockoutEnd = source.IsLockedOut ? (lockedOutUntil ?? DateTime.MaxValue).ToUniversalTime() : null;
|
||||
target.Comments = source.Comments;
|
||||
target.LastLockoutDateUtc = source.LastLockoutDate == DateTime.MinValue
|
||||
|
||||
@@ -221,10 +221,17 @@ AND cmsContentNu.nodeId IS NULL
|
||||
.Append(SqlObjectTypeNotTrashed(SqlContext, Constants.ObjectTypes.Document))
|
||||
.Append(SqlOrderByLevelIdSortOrder(SqlContext));
|
||||
|
||||
// Use a more efficient COUNT query
|
||||
Sql<ISqlContext>? sqlCountQuery = SqlContentSourcesCount()
|
||||
.Append(SqlObjectTypeNotTrashed(SqlContext, Constants.ObjectTypes.Document));
|
||||
|
||||
Sql<ISqlContext>? sqlCount =
|
||||
SqlContext.Sql("SELECT COUNT(*) FROM (").Append(sqlCountQuery).Append(") npoco_tbl");
|
||||
|
||||
IContentCacheDataSerializer serializer =
|
||||
_contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Document);
|
||||
|
||||
IEnumerable<ContentSourceDto> dtos = GetContentNodeDtos(sql);
|
||||
IEnumerable<ContentSourceDto> dtos = GetContentNodeDtos(sql, sqlCount);
|
||||
|
||||
foreach (ContentSourceDto row in dtos)
|
||||
{
|
||||
@@ -239,10 +246,18 @@ AND cmsContentNu.nodeId IS NULL
|
||||
.Append(SqlWhereNodeIdX(SqlContext, id))
|
||||
.Append(SqlOrderByLevelIdSortOrder(SqlContext));
|
||||
|
||||
// Use a more efficient COUNT query
|
||||
Sql<ISqlContext>? sqlCountQuery = SqlContentSourcesCount(SqlContentSourcesSelectUmbracoNodeJoin)
|
||||
.Append(SqlObjectTypeNotTrashed(SqlContext, Constants.ObjectTypes.Document))
|
||||
.Append(SqlWhereNodeIdX(SqlContext, id));
|
||||
|
||||
Sql<ISqlContext>? sqlCount =
|
||||
SqlContext.Sql("SELECT COUNT(*) FROM (").Append(sqlCountQuery).Append(") npoco_tbl");
|
||||
|
||||
IContentCacheDataSerializer serializer =
|
||||
_contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Document);
|
||||
|
||||
IEnumerable<ContentSourceDto> dtos = GetContentNodeDtos(sql);
|
||||
IEnumerable<ContentSourceDto> dtos = GetContentNodeDtos(sql, sqlCount);
|
||||
|
||||
foreach (ContentSourceDto row in dtos)
|
||||
{
|
||||
@@ -262,10 +277,18 @@ AND cmsContentNu.nodeId IS NULL
|
||||
.WhereIn<ContentDto>(x => x.ContentTypeId, ids)
|
||||
.Append(SqlOrderByLevelIdSortOrder(SqlContext));
|
||||
|
||||
// Use a more efficient COUNT query
|
||||
Sql<ISqlContext> sqlCountQuery = SqlContentSourcesCount()
|
||||
.Append(SqlObjectTypeNotTrashed(SqlContext, Constants.ObjectTypes.Document))
|
||||
.WhereIn<ContentDto>(x => x.ContentTypeId, ids);
|
||||
|
||||
Sql<ISqlContext>? sqlCount =
|
||||
SqlContext.Sql("SELECT COUNT(*) FROM (").Append(sqlCountQuery).Append(") npoco_tbl");
|
||||
|
||||
IContentCacheDataSerializer serializer =
|
||||
_contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Document);
|
||||
|
||||
IEnumerable<ContentSourceDto> dtos = GetContentNodeDtos(sql);
|
||||
IEnumerable<ContentSourceDto> dtos = GetContentNodeDtos(sql, sqlCount);
|
||||
|
||||
foreach (ContentSourceDto row in dtos)
|
||||
{
|
||||
@@ -1015,27 +1038,14 @@ WHERE cmsContentNu.nodeId IN (
|
||||
return dtos;
|
||||
}
|
||||
|
||||
private IEnumerable<ContentSourceDto> GetContentNodeDtos(Sql<ISqlContext> sql)
|
||||
private IEnumerable<ContentSourceDto> GetContentNodeDtos(Sql<ISqlContext> sql, Sql<ISqlContext> sqlCount)
|
||||
{
|
||||
// We need to page here. We don't want to iterate over every single row in one connection cuz this can cause an SQL Timeout.
|
||||
// We also want to read with a db reader and not load everything into memory, QueryPaged lets us do that.
|
||||
// QueryPaged is very slow on large sites however, so use fetch if UsePagedSqlQuery is disabled.
|
||||
IEnumerable<ContentSourceDto> dtos;
|
||||
if (_nucacheSettings.Value.UsePagedSqlQuery)
|
||||
{
|
||||
// Use a more efficient COUNT query
|
||||
Sql<ISqlContext>? sqlCountQuery = SqlContentSourcesCount()
|
||||
.Append(SqlObjectTypeNotTrashed(SqlContext, Constants.ObjectTypes.Document));
|
||||
|
||||
Sql<ISqlContext>? sqlCount =
|
||||
SqlContext.Sql("SELECT COUNT(*) FROM (").Append(sqlCountQuery).Append(") npoco_tbl");
|
||||
|
||||
dtos = Database.QueryPaged<ContentSourceDto>(_nucacheSettings.Value.SqlPageSize, sql, sqlCount);
|
||||
}
|
||||
else
|
||||
{
|
||||
dtos = Database.Fetch<ContentSourceDto>(sql);
|
||||
}
|
||||
IEnumerable<ContentSourceDto> dtos = _nucacheSettings.Value.UsePagedSqlQuery ?
|
||||
Database.QueryPaged<ContentSourceDto>(_nucacheSettings.Value.SqlPageSize, sql, sqlCount) :
|
||||
Database.Fetch<ContentSourceDto>(sql);
|
||||
|
||||
return dtos;
|
||||
}
|
||||
|
||||
@@ -43,14 +43,14 @@ public class AspNetCoreHostingEnvironment : IHostingEnvironment
|
||||
_webHostEnvironment = webHostEnvironment ?? throw new ArgumentNullException(nameof(webHostEnvironment));
|
||||
_urlProviderMode = _webRoutingSettings.CurrentValue.UrlProviderMode;
|
||||
|
||||
SetSiteName(hostingSettings.CurrentValue.SiteName);
|
||||
SetSiteNameAndDebugMode(hostingSettings.CurrentValue);
|
||||
|
||||
// We have to ensure that the OptionsMonitor is an actual options monitor since we have a hack
|
||||
// where we initially use an OptionsMonitorAdapter, which doesn't implement OnChange.
|
||||
// See summery of OptionsMonitorAdapter for more information.
|
||||
if (hostingSettings is OptionsMonitor<HostingSettings>)
|
||||
{
|
||||
hostingSettings.OnChange(settings => SetSiteName(settings.SiteName));
|
||||
hostingSettings.OnChange(settings => SetSiteNameAndDebugMode(settings));
|
||||
}
|
||||
|
||||
ApplicationPhysicalPath = webHostEnvironment.ContentRootPath;
|
||||
@@ -95,7 +95,7 @@ public class AspNetCoreHostingEnvironment : IHostingEnvironment
|
||||
_hostingSettings.CurrentValue.ApplicationVirtualPath?.EnsureStartsWith('/') ?? "/";
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsDebugMode => _hostingSettings.CurrentValue.Debug;
|
||||
public bool IsDebugMode { get; private set; }
|
||||
|
||||
public string LocalTempPath
|
||||
{
|
||||
@@ -188,8 +188,12 @@ public class AspNetCoreHostingEnvironment : IHostingEnvironment
|
||||
}
|
||||
}
|
||||
|
||||
private void SetSiteName(string? siteName) =>
|
||||
SiteName = string.IsNullOrWhiteSpace(siteName)
|
||||
private void SetSiteNameAndDebugMode(HostingSettings hostingSettings)
|
||||
{
|
||||
SiteName = string.IsNullOrWhiteSpace(hostingSettings.SiteName)
|
||||
? _webHostEnvironment.ApplicationName
|
||||
: siteName;
|
||||
: hostingSettings.SiteName;
|
||||
|
||||
IsDebugMode = hostingSettings.Debug;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@ public static class AuthorizationPolicies
|
||||
public const string TreeAccessDocumentsOrDocumentTypes = nameof(TreeAccessDocumentsOrDocumentTypes);
|
||||
public const string TreeAccessMediaOrMediaTypes = nameof(TreeAccessMediaOrMediaTypes);
|
||||
public const string TreeAccessDictionaryOrTemplates = nameof(TreeAccessDictionaryOrTemplates);
|
||||
public const string TreeAccessDocumentOrMediaOrContentTypes = nameof(TreeAccessDocumentOrMediaOrContentTypes);
|
||||
|
||||
// other
|
||||
public const string DictionaryPermissionByResource = nameof(DictionaryPermissionByResource);
|
||||
|
||||
Submodule src/Umbraco.Web.UI.Client updated: 0c74f2a34c...a9d3a43969
@@ -3,7 +3,9 @@ WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
|
||||
builder.CreateUmbracoBuilder()
|
||||
.AddBackOffice()
|
||||
.AddWebsite()
|
||||
#if UseDeliveryApi
|
||||
.AddDeliveryApi()
|
||||
#endif
|
||||
.AddComposers()
|
||||
.Build();
|
||||
|
||||
@@ -23,6 +25,9 @@ app.UseUmbraco()
|
||||
})
|
||||
.WithEndpoints(u =>
|
||||
{
|
||||
/*#if (UmbracoRelease = 'LTS')
|
||||
u.UseInstallerEndpoints();
|
||||
#endif */
|
||||
u.UseBackOfficeEndpoints();
|
||||
u.UseWebsiteEndpoints();
|
||||
});
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
"KeepUserLoggedIn": false,
|
||||
"UsernameIsEmail": true,
|
||||
"HideDisabledUsersInBackoffice": false,
|
||||
"AllowedUserNameCharacters": "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+\\",
|
||||
"AllowedUserNameCharacters": "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-'._@+\\",
|
||||
"UserPassword": {
|
||||
"RequiredLength": 10,
|
||||
"RequireNonLetterOrDigit": false,
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
<ContentTargetFolders>.</ContentTargetFolders>
|
||||
<NoWarn>NU5128</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="..\src\Umbraco.Web.UI\Program.cs">
|
||||
<Link>UmbracoProject\Program.cs</Link>
|
||||
@@ -20,6 +19,7 @@
|
||||
<Content Include="UmbracoPackage\**" Exclude="bin;obj" />
|
||||
<Content Include="UmbracoPackageRcl\**" Exclude="bin;obj" />
|
||||
<Content Include="UmbracoProject\**" Exclude="bin;obj" />
|
||||
<Content Include="UmbracoDockerCompose\**" Exclude="bin;obj"/>
|
||||
<Content Include="..\src\Umbraco.Web.UI\Views\Partials\blocklist\**">
|
||||
<Link>UmbracoProject\Views\Partials\blocklist\%(RecursiveDir)%(Filename)%(Extension)</Link>
|
||||
<PackagePath>UmbracoProject\Views\Partials\blocklist</PackagePath>
|
||||
@@ -32,13 +32,7 @@
|
||||
<Link>UmbracoProject\Views\_ViewImports.cshtml</Link>
|
||||
<PackagePath>UmbracoProject\Views</PackagePath>
|
||||
</Content>
|
||||
<Content Include="..\src\Umbraco.Web.UI\wwwroot\favicon.ico">
|
||||
<Link>UmbracoProject\wwwroot\favicon.ico</Link>
|
||||
<PackagePath>UmbracoProject\wwwroot</PackagePath>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
<!-- Update template.json files with the default UmbracoVersion value set to the current build version -->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Umbraco.JsonSchema.Extensions" PrivateAssets="all" />
|
||||
@@ -54,7 +48,7 @@
|
||||
</_TemplateJsonFiles>
|
||||
</ItemGroup>
|
||||
<Copy SourceFiles="@(_TemplateJsonFiles)" DestinationFiles="%(DestinationFile)" />
|
||||
<JsonPathUpdateValue JsonFile="%(_TemplateJsonFiles.DestinationFile)" Path="$.symbols.UmbracoVersion.defaultValue" Value=""$(PackageVersion)"" />
|
||||
<JsonPathUpdateValue JsonFile="%(_TemplateJsonFiles.DestinationFile)" Path="$.symbols.FinalVersion.parameters.cases.[0].value" Value=""$(PackageVersion)"" />
|
||||
<ItemGroup>
|
||||
<_PackageFiles Include="%(_TemplateJsonFiles.DestinationFile)">
|
||||
<PackagePath>%(_TemplateJsonFiles.RelativeDir)</PackagePath>
|
||||
|
||||
1
templates/UmbracoDockerCompose/.env
Normal file
1
templates/UmbracoDockerCompose/.env
Normal file
@@ -0,0 +1 @@
|
||||
DB_PASSWORD=Password1234
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/dotnetcli.host.json",
|
||||
"symbolInfo": {
|
||||
"ProjectName": {
|
||||
"longName": "ProjectName",
|
||||
"shortName": "P"
|
||||
},
|
||||
"DatabasePassword": {
|
||||
"longName": "DatabasePassword",
|
||||
"shortName": "dbpw"
|
||||
},
|
||||
"Port":
|
||||
{
|
||||
"longName": "Port",
|
||||
"shortName": "p"
|
||||
}
|
||||
},
|
||||
"usageExamples": [
|
||||
"dotnet new umbraco-compose -P MyProject",
|
||||
"dotnet new umbraco-compose --ProjectName MyProject",
|
||||
"dotnet new umbraco-compose -P -MyProject -dbpw MyStr0ngP@ssword",
|
||||
"dotnet new umbraco-compose -P -MyProject --DatabasePassword MyStr0ngP@ssword"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/ide.host.json",
|
||||
"order": 0,
|
||||
"icon": "../../icon.png",
|
||||
"description": {
|
||||
"id": "UmbracoDockerCompose",
|
||||
"text": "Umbraco Docker Compose - Docker compose for Umbraco CMS and associated database"
|
||||
},
|
||||
"symbolInfo": [
|
||||
{
|
||||
"id": "ProjectName",
|
||||
"isVisible": true
|
||||
},
|
||||
{
|
||||
"id": "DatabasePassword",
|
||||
"isVisible": true
|
||||
},
|
||||
{
|
||||
"id": "Port",
|
||||
"isVisible": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/template.json",
|
||||
"author": "Umbraco HQ",
|
||||
"classifications": [
|
||||
"Web",
|
||||
"CMS",
|
||||
"Umbraco"
|
||||
],
|
||||
"name": "Umbraco Docker Compose",
|
||||
"description": "Creates the prerequisites for developing Umbraco in Docker containers",
|
||||
"groupIdentity": "Umbraco.Templates.UmbracoDockerCompose",
|
||||
"identity": "Umbraco.Templates.UmbracoDockerCompose",
|
||||
"shortName": "umbraco-compose",
|
||||
"tags": {
|
||||
"type": "item"
|
||||
},
|
||||
"symbols": {
|
||||
"ProjectName": {
|
||||
"type": "parameter",
|
||||
"description": "The name of the project the Docker Compose file will be created for",
|
||||
"datatype": "string",
|
||||
"replaces": "UmbracoProject",
|
||||
"isRequired": true
|
||||
},
|
||||
"DatabasePassword": {
|
||||
"type": "parameter",
|
||||
"description": "The password to the database, will be stored in .env file",
|
||||
"datatype": "string",
|
||||
"replaces": "Password1234",
|
||||
"defaultValue": "Password1234"
|
||||
},
|
||||
"Port": {
|
||||
"type": "parameter",
|
||||
"description": "The port forward on the docker container, this is the port you use to access the site",
|
||||
"datatype": "string",
|
||||
"replaces": "TEMPLATE_PORT",
|
||||
"defaultValue": "44372"
|
||||
},
|
||||
"ImageName": {
|
||||
"type": "generated",
|
||||
"generator": "casing",
|
||||
"parameters": {
|
||||
"source": "ProjectName",
|
||||
"toLower": true
|
||||
},
|
||||
"replaces": "umbraco_image"
|
||||
}
|
||||
}
|
||||
}
|
||||
21
templates/UmbracoDockerCompose/Database/Dockerfile
Normal file
21
templates/UmbracoDockerCompose/Database/Dockerfile
Normal file
@@ -0,0 +1,21 @@
|
||||
FROM mcr.microsoft.com/azure-sql-edge:latest
|
||||
|
||||
ENV ACCEPT_EULA=Y
|
||||
|
||||
USER root
|
||||
|
||||
RUN mkdir /var/opt/sqlserver
|
||||
|
||||
RUN chown mssql /var/opt/sqlserver
|
||||
|
||||
ENV MSSQL_BACKUP_DIR="/var/opt/mssql"
|
||||
ENV MSSQL_DATA_DIR="/var/opt/mssql/data"
|
||||
ENV MSSQL_LOG_DIR="/var/opt/mssql/log"
|
||||
|
||||
EXPOSE 1433/tcp
|
||||
COPY setup.sql /
|
||||
COPY startup.sh /
|
||||
COPY healthcheck.sh /
|
||||
|
||||
ENTRYPOINT [ "/bin/bash", "startup.sh" ]
|
||||
CMD [ "/opt/mssql/bin/sqlservr" ]
|
||||
15
templates/UmbracoDockerCompose/Database/healthcheck.sh
Normal file
15
templates/UmbracoDockerCompose/Database/healthcheck.sh
Normal file
@@ -0,0 +1,15 @@
|
||||
value="$(/opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P "$SA_PASSWORD" -d master -Q "SELECT state_desc FROM sys.databases WHERE name = 'umbracoDb'" | awk 'NR==3')"
|
||||
|
||||
# This checks for any non-zero length string, and $value will be empty when the database does not exist.
|
||||
if [ -n "$value" ]
|
||||
then
|
||||
echo "ONLINE"
|
||||
return 0 # With docker 0 = success
|
||||
else
|
||||
echo "OFFLINE"
|
||||
return 1 # And 1 = unhealthy
|
||||
fi
|
||||
|
||||
# This is useful for debugging
|
||||
# echo "Value is:"
|
||||
# echo "$value"
|
||||
10
templates/UmbracoDockerCompose/Database/setup.sql
Normal file
10
templates/UmbracoDockerCompose/Database/setup.sql
Normal file
@@ -0,0 +1,10 @@
|
||||
USE [master]
|
||||
GO
|
||||
|
||||
IF NOT EXISTS (SELECT * FROM sys.databases WHERE name = 'UmbracoDb')
|
||||
BEGIN
|
||||
CREATE DATABASE [umbracoDb]
|
||||
END;
|
||||
GO
|
||||
|
||||
USE UmbracoDb;
|
||||
23
templates/UmbracoDockerCompose/Database/startup.sh
Normal file
23
templates/UmbracoDockerCompose/Database/startup.sh
Normal file
@@ -0,0 +1,23 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Taken from: https://github.com/CarlSargunar/Umbraco-Docker-Workshop
|
||||
if [ "$1" = '/opt/mssql/bin/sqlservr' ]; then
|
||||
# If this is the container's first run, initialize the application database
|
||||
if [ ! -f /tmp/app-initialized ]; then
|
||||
# Initialize the application database asynchronously in a background process. This allows a) the SQL Server process to be the main process in the container, which allows graceful shutdown and other goodies, and b) us to only start the SQL Server process once, as opposed to starting, stopping, then starting it again.
|
||||
function initialize_app_database() {
|
||||
# Wait a bit for SQL Server to start. SQL Server's process doesn't provide a clever way to check if it's up or not, and it needs to be up before we can import the application database
|
||||
sleep 15s
|
||||
|
||||
#run the setup script to create the DB and the schema in the DB
|
||||
/opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P "$SA_PASSWORD" -d master -i setup.sql
|
||||
|
||||
# Note that the container has been initialized so future starts won't wipe changes to the data
|
||||
touch /tmp/app-initialized
|
||||
}
|
||||
initialize_app_database &
|
||||
fi
|
||||
fi
|
||||
|
||||
exec "$@"
|
||||
102
templates/UmbracoDockerCompose/docker-compose.yml
Normal file
102
templates/UmbracoDockerCompose/docker-compose.yml
Normal file
@@ -0,0 +1,102 @@
|
||||
services:
|
||||
umb_database:
|
||||
container_name: umbraco_image_database
|
||||
build:
|
||||
context: ./Database
|
||||
environment:
|
||||
SA_PASSWORD: ${DB_PASSWORD}
|
||||
MSSQL_SA_PASSWORD: ${DB_PASSWORD}
|
||||
ports:
|
||||
- "1433:1433"
|
||||
- "1434:1434"
|
||||
volumes:
|
||||
- umb_database:/var/opt/mssql
|
||||
networks:
|
||||
- umbnet
|
||||
healthcheck:
|
||||
# This healthcheck is to make sure that the database is up and running before the umbraco container starts.
|
||||
# It works by querying the database for the state of the umbracoDb database, ensuring it exists.
|
||||
test: ./healthcheck.sh
|
||||
interval: 5m
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 15s # Bootstrap duration, for this duration failures does not count towards max retries.
|
||||
start_interval: 5s # How long after the health check has started to run the healthcheck again.
|
||||
|
||||
umbraco_image:
|
||||
image: umbraco_image
|
||||
environment:
|
||||
- ASPNETCORE_ENVIRONMENT=Development
|
||||
- ConnectionStrings__umbracoDbDSN=Server=umb_database;Database=umbracoDb;User Id=sa;Password=${DB_PASSWORD};TrustServerCertificate=true;
|
||||
- ConnectionStrings__umbracoDbDSN_ProviderName=Microsoft.Data.SqlClient
|
||||
volumes:
|
||||
- umb_media:/app/wwwroot/media
|
||||
- umb_scripts:/app/wwwroot/scripts
|
||||
- umb_styles:/app/wwwroot/css
|
||||
- umb_logs:/app/umbraco/Logs
|
||||
- umb_views:/app/Views
|
||||
- umb_data:/app/umbraco
|
||||
- umb_models:/app/umbraco/models
|
||||
build:
|
||||
context: .
|
||||
dockerfile: UmbracoProject/Dockerfile
|
||||
args:
|
||||
- BUILD_CONFIGURATION=Debug
|
||||
|
||||
depends_on:
|
||||
umb_database:
|
||||
condition: service_healthy
|
||||
restart: always
|
||||
ports:
|
||||
- "TEMPLATE_PORT:8080"
|
||||
networks:
|
||||
- umbnet
|
||||
develop:
|
||||
# This allows you to run docker compose watch, after doing so the container will rebuild when the models are changed.
|
||||
# Once a restart only feature is implemented (https://github.com/docker/compose/issues/11446)
|
||||
# It would be really nice to add a restart only watch to \Views, since the file watchers for recompilation of Razor views does not work with docker.
|
||||
watch:
|
||||
- path: ./UmbracoProject/umbraco/models
|
||||
action: rebuild
|
||||
|
||||
# These volumes are all made as bind mounts, meaning that they are bound to the host machine's file system.
|
||||
# This is to better facilitate local development in the IDE, so the views, models, etc... are available in the IDE.
|
||||
# This can be changed by removing the driver and driver_opts from the volumes.
|
||||
volumes:
|
||||
umb_media:
|
||||
driver: local
|
||||
driver_opts:
|
||||
type: none
|
||||
device: ./UmbracoProject/wwwroot/media
|
||||
o: bind
|
||||
umb_scripts:
|
||||
driver: local
|
||||
driver_opts:
|
||||
type: none
|
||||
device: ./UmbracoProject/wwwroot/scripts
|
||||
o: bind
|
||||
umb_styles:
|
||||
driver: local
|
||||
driver_opts:
|
||||
type: none
|
||||
device: ./UmbracoProject/wwwroot/css
|
||||
o: bind
|
||||
umb_logs:
|
||||
umb_views:
|
||||
driver: local
|
||||
driver_opts:
|
||||
type: none
|
||||
device: ./UmbracoProject/Views
|
||||
o: bind
|
||||
umb_data:
|
||||
umb_models:
|
||||
driver: local
|
||||
driver_opts:
|
||||
type: none
|
||||
device: ./UmbracoProject/umbraco/models
|
||||
o: bind
|
||||
umb_database:
|
||||
|
||||
networks:
|
||||
umbnet:
|
||||
driver: bridge
|
||||
25
templates/UmbracoProject/.dockerignore
Normal file
25
templates/UmbracoProject/.dockerignore
Normal file
@@ -0,0 +1,25 @@
|
||||
**/.dockerignore
|
||||
**/.env
|
||||
**/.git
|
||||
**/.gitignore
|
||||
**/.project
|
||||
**/.settings
|
||||
**/.toolstarget
|
||||
**/.vs
|
||||
**/.vscode
|
||||
**/.idea
|
||||
**/*.*proj.user
|
||||
**/*.dbmdl
|
||||
**/*.jfm
|
||||
**/azds.yaml
|
||||
**/bin
|
||||
**/charts
|
||||
**/docker-compose*
|
||||
**/Dockerfile*
|
||||
**/node_modules
|
||||
**/npm-debug.log
|
||||
**/obj
|
||||
**/secrets.dev.yaml
|
||||
**/values.dev.yaml
|
||||
LICENSE
|
||||
README.md
|
||||
@@ -8,12 +8,25 @@
|
||||
},
|
||||
"UmbracoVersion": {
|
||||
"longName": "version",
|
||||
"shortName": "v"
|
||||
"shortName": "v",
|
||||
"isHidden": true
|
||||
},
|
||||
"UmbracoRelease": {
|
||||
"longName": "release",
|
||||
"shortName": "r"
|
||||
},
|
||||
"UseHttpsRedirect": {
|
||||
"longName": "use-https-redirect",
|
||||
"shortName": ""
|
||||
},
|
||||
"UseDeliveryApi": {
|
||||
"longName": "use-delivery-api",
|
||||
"shortName": "da"
|
||||
},
|
||||
"Docker": {
|
||||
"longName": "add-docker",
|
||||
"shortName": ""
|
||||
},
|
||||
"SkipRestore": {
|
||||
"longName": "no-restore",
|
||||
"shortName": ""
|
||||
@@ -58,6 +71,18 @@
|
||||
"longName": "PackageTestSiteName",
|
||||
"shortName": "p",
|
||||
"isHidden": true
|
||||
},
|
||||
"ModelsBuilderMode": {
|
||||
"longName": "models-mode",
|
||||
"shortName": "mm"
|
||||
},
|
||||
"StarterKit": {
|
||||
"longName": "starter-kit",
|
||||
"shortName": "sk"
|
||||
},
|
||||
"DevelopmentMode": {
|
||||
"longName": "development-mode",
|
||||
"shortName": "dm"
|
||||
}
|
||||
},
|
||||
"usageExamples": [
|
||||
|
||||
@@ -9,11 +9,22 @@
|
||||
"symbolInfo": [
|
||||
{
|
||||
"id": "UmbracoVersion",
|
||||
"isVisible": true
|
||||
"isVisible": false
|
||||
},
|
||||
{
|
||||
"id": "UseHttpsRedirect",
|
||||
"isVisible": true,
|
||||
"persistenceScope": "templateGroup",
|
||||
"defaultValue": "true"
|
||||
},
|
||||
{
|
||||
"id": "UseDeliveryApi",
|
||||
"isVisible": true,
|
||||
"persistenceScope": "templateGroup"
|
||||
},
|
||||
{
|
||||
"id": "ModelsBuilderMode",
|
||||
"isVisible": true,
|
||||
"persistenceScope": "templateGroup"
|
||||
},
|
||||
{
|
||||
@@ -54,6 +65,23 @@
|
||||
{
|
||||
"id": "NoNodesViewPath",
|
||||
"isVisible": true
|
||||
},
|
||||
{
|
||||
"id": "Docker",
|
||||
"isVisible": true
|
||||
},
|
||||
{
|
||||
"id": "StarterKit",
|
||||
"isVisible": true
|
||||
},
|
||||
{
|
||||
"id": "UmbracoRelease",
|
||||
"isVisible": true
|
||||
},
|
||||
{
|
||||
"id": "DevelopmentMode",
|
||||
"isVisible": true,
|
||||
"defaultValue": "IDEDevelopment"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/template.json",
|
||||
"symbols": {
|
||||
"StarterKit": {
|
||||
"displayName": "Starter kit",
|
||||
"type": "parameter",
|
||||
"datatype": "choice",
|
||||
"description": "Choose a starter kit to install.",
|
||||
"defaultValue": "None",
|
||||
"replaces": "STARTER_KIT_NAME",
|
||||
// The choice here should be the name of the starter kit package, since it will be used directly for package reference.
|
||||
"choices": [
|
||||
{
|
||||
"choice": "None",
|
||||
"description": "No starter kit."
|
||||
},
|
||||
{
|
||||
"choice": "Umbraco.TheStarterKit",
|
||||
"description": "The Umbraco starter kit.",
|
||||
"displayName": "The Starter Kit"
|
||||
}
|
||||
]
|
||||
},
|
||||
// Used to determine the version of the starter kit to install.
|
||||
// there should be cases for Latest, LTS and Custom for every starterkit added above.
|
||||
// This has the benefit that all maintenance of starter kits in template can be done from this file.
|
||||
"StarterKitVersion": {
|
||||
"type": "generated",
|
||||
"generator": "switch",
|
||||
"replaces": "STARTER_KIT_VERSION",
|
||||
"parameters": {
|
||||
"evaluator": "C++",
|
||||
"datatype": "string",
|
||||
"cases": [
|
||||
{
|
||||
"condition": "(StarterKit == 'Umbraco.TheStarterKit' && (UmbracoRelease == 'Latest' || UmbracoRelease == 'Custom'))",
|
||||
"value": "14.0.0"
|
||||
},
|
||||
{
|
||||
"condition": "(StarterKit == 'Umbraco.TheStarterKit' && UmbracoRelease == 'LTS')",
|
||||
"value": "13.0.0"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@
|
||||
"sourceName": "UmbracoProject",
|
||||
"defaultName": "UmbracoProject1",
|
||||
"preferNameDirectory": true,
|
||||
"additionalConfigFiles": [ "starterkits.template.json"],
|
||||
"sources": [
|
||||
{
|
||||
"modifiers": [
|
||||
@@ -26,6 +27,13 @@
|
||||
"exclude": [
|
||||
".gitignore"
|
||||
]
|
||||
},
|
||||
{
|
||||
"condition": "(!Docker)",
|
||||
"exclude": [
|
||||
"Dockerfile",
|
||||
".dockerignore"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -46,13 +54,72 @@
|
||||
"defaultValue": "net9.0",
|
||||
"replaces": "net9.0"
|
||||
},
|
||||
"UmbracoRelease": {
|
||||
"displayName": "Umbraco Version",
|
||||
"description": "The Umbraco release to use, either latest or latest long term supported",
|
||||
"type": "parameter",
|
||||
"datatype": "choice",
|
||||
"defaultValue": "Latest",
|
||||
"choices": [
|
||||
{
|
||||
"choice": "Latest",
|
||||
"description": "The latest umbraco release"
|
||||
},
|
||||
{
|
||||
"choice": "LTS",
|
||||
"description": "The most recent long term supported version",
|
||||
"displayName": "Long Term Supported"
|
||||
}
|
||||
],
|
||||
"isRequired": false
|
||||
},
|
||||
"UmbracoVersion": {
|
||||
"displayName": "Umbraco version",
|
||||
"description": "The version of Umbraco.Cms to add as PackageReference.",
|
||||
"displayName": "Custom Version",
|
||||
"description": "The selected custom version of Umbraco, this is obsoleted, and will be removed in a future version of the template.",
|
||||
"type": "parameter",
|
||||
"datatype": "string",
|
||||
"defaultValue": "*",
|
||||
"replaces": "UMBRACO_VERSION_FROM_TEMPLATE"
|
||||
"defaultValue": "null",
|
||||
"replaces": "CUSTOM_VERSION",
|
||||
"isRequired": false
|
||||
},
|
||||
"FinalVersion" : {
|
||||
"type": "generated",
|
||||
"generator": "switch",
|
||||
"datatype": "text",
|
||||
"description": "The calculated version of Umbraco to use",
|
||||
"replaces": "UMBRACO_VERSION_FROM_TEMPLATE",
|
||||
"parameters": {
|
||||
"evaluator": "C++",
|
||||
"datatype": "text",
|
||||
"cases": [
|
||||
{
|
||||
"condition": "(UmbracoRelease == 'Latest')",
|
||||
"value": "*"
|
||||
},
|
||||
{
|
||||
"condition": "(UmbracoRelease == 'LTS')",
|
||||
"value": "13.4.1"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"DotnetVersion":
|
||||
{
|
||||
"type": "generated",
|
||||
"generator": "switch",
|
||||
"datatype": "text",
|
||||
"description": "Not relevant at the moment, but if we need to change the dotnet version based on the Umbraco version, we can do it here",
|
||||
"replaces": "DOTNET_VERSION_FROM_TEMPLATE",
|
||||
"parameters": {
|
||||
"evaluator": "C++",
|
||||
"datatype": "text",
|
||||
"cases": [
|
||||
{
|
||||
"condition": "(true)",
|
||||
"value": "net8.0"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"UseHttpsRedirect": {
|
||||
"displayName": "Use HTTPS redirect",
|
||||
@@ -61,6 +128,20 @@
|
||||
"datatype": "bool",
|
||||
"defaultValue": "false"
|
||||
},
|
||||
"UseDeliveryApi": {
|
||||
"displayName": "Use Delivery API",
|
||||
"description": "Enables the Delivery API",
|
||||
"type": "parameter",
|
||||
"datatype": "bool",
|
||||
"defaultValue": "false"
|
||||
},
|
||||
"Docker": {
|
||||
"displayName": "Add Docker file",
|
||||
"description": "Adds a docker file to the project.",
|
||||
"type": "parameter",
|
||||
"datatype": "bool",
|
||||
"defaultValue": "false"
|
||||
},
|
||||
"SkipRestore": {
|
||||
"displayName": "Skip restore",
|
||||
"description": "If specified, skips the automatic restore of the project on create.",
|
||||
@@ -244,6 +325,58 @@
|
||||
"defaultValue": "",
|
||||
"replaces": "PACKAGE_PROJECT_NAME_FROM_TEMPLATE"
|
||||
},
|
||||
"DevelopmentMode": {
|
||||
"type": "parameter",
|
||||
"displayName": "Development mode",
|
||||
"datatype": "choice",
|
||||
"description": "Choose the development mode to use for the project.",
|
||||
"defaultValue": "BackofficeDevelopment",
|
||||
"choices": [
|
||||
{
|
||||
"choice": "BackofficeDevelopment",
|
||||
"description": "Enables backoffice development, allowing you to develop from within the backoffice, this is the default behaviour.",
|
||||
"displayName": "Backoffice Development"
|
||||
},
|
||||
{
|
||||
"choice": "IDEDevelopment",
|
||||
"description": "Configures appsettings.Development.json to Development runtime mode and SourceCodeAuto models builder mode, and configures appsettings.json to Production runtime mode, Nothing models builder mode, and enables UseHttps",
|
||||
"displayName": "IDE Development"
|
||||
}
|
||||
]
|
||||
},
|
||||
"ModelsBuilderMode": {
|
||||
"type": "parameter",
|
||||
"displayName": "Models builder mode",
|
||||
"datatype": "choice",
|
||||
"description": "Choose the models builder mode to use for the project. When development mode is set to IDEDevelopment this only changes the models builder mode appsetttings.development.json",
|
||||
"defaultValue": "Default",
|
||||
"replaces": "MODELS_MODE",
|
||||
"choices": [
|
||||
{
|
||||
"choice": "Default",
|
||||
"description": "Let DevelopmentMode determine the models builder mode."
|
||||
},
|
||||
{
|
||||
"choice": "InMemoryAuto",
|
||||
"description": "Generate models in memory, automatically updating when a content type change, this means no need for app rebuild, however models are only available in views.",
|
||||
"displayName": "In Memory Auto"
|
||||
},
|
||||
{
|
||||
"choice": "SourceCodeManual",
|
||||
"description": "Generate models as source code, only updating when requested manually, this means a interaction and rebuild is required when content type(s) change, however models are available in code.",
|
||||
"displayName": "Source Code Manual"
|
||||
},
|
||||
{
|
||||
"choice": "SourceCodeAuto",
|
||||
"description": "Generate models as source code, automatically updating when a content type change, this means a rebuild is required when content type(s) change, however models are available in code.",
|
||||
"displayName": "Source Code Auto"
|
||||
},
|
||||
{
|
||||
"choice": "Nothing",
|
||||
"description": "No models are generated, this is recommended for production assuming generated models are used for development."
|
||||
}
|
||||
]
|
||||
},
|
||||
"Namespace": {
|
||||
"type": "derived",
|
||||
"valueSource": "name",
|
||||
|
||||
33
templates/UmbracoProject/Dockerfile
Normal file
33
templates/UmbracoProject/Dockerfile
Normal file
@@ -0,0 +1,33 @@
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
|
||||
USER $APP_UID
|
||||
WORKDIR /app
|
||||
EXPOSE 8080
|
||||
EXPOSE 8081
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
WORKDIR /src
|
||||
COPY ["UmbracoProject/UmbracoProject.csproj", "UmbracoProject/"]
|
||||
RUN dotnet restore "UmbracoProject/UmbracoProject.csproj"
|
||||
COPY . .
|
||||
WORKDIR "/src/UmbracoProject"
|
||||
RUN dotnet build "UmbracoProject.csproj" -c $BUILD_CONFIGURATION -o /app/build
|
||||
|
||||
FROM build AS publish
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
RUN dotnet publish "UmbracoProject.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
|
||||
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=publish /app/publish .
|
||||
# We need to make sure that the user running the app has write access to the umbraco folder, in order to write logs and other files.
|
||||
# Since these are volumes they are created as root by the docker daemon.
|
||||
USER root
|
||||
RUN mkdir umbraco
|
||||
RUN mkdir umbraco/Logs
|
||||
RUN chown $APP_UID umbraco --recursive
|
||||
#if (UmbracoRelease = 'LTS')
|
||||
RUN chown $APP_UID wwwroot/umbraco --recursive
|
||||
#endif
|
||||
USER $APP_UID
|
||||
ENTRYPOINT ["dotnet", "UmbracoProject.dll"]
|
||||
@@ -1,13 +1,21 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>DOTNET_VERSION_FROM_TEMPLATE</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace Condition="'$(name)' != '$(name{-VALUE-FORMS-}safe_namespace)'">Umbraco.Cms.Web.UI</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!--#if (UmbracoVersion != "null")
|
||||
<PackageReference Include="Umbraco.Cms" Version="CUSTOM_VERSION" />
|
||||
#else
|
||||
<PackageReference Include="Umbraco.Cms" Version="UMBRACO_VERSION_FROM_TEMPLATE" />
|
||||
#endif-->
|
||||
<!--#if (StarterKit != "None") -->
|
||||
<PackageReference Include="STARTER_KIT_NAME" Version="STARTER_KIT_VERSION"/>
|
||||
<!--#endif -->
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -21,11 +29,13 @@
|
||||
<CopyRazorGenerateFilesToPublishDirectory>true</CopyRazorGenerateFilesToPublishDirectory>
|
||||
</PropertyGroup>
|
||||
|
||||
<!--#if (ModelsBuilderMode == "InMemoryAuto" || (DevelopmentMode == "BackofficeDevelopment" && ModelsBuilderMode == "Default")) -->
|
||||
<PropertyGroup>
|
||||
<!-- Remove RazorCompileOnBuild and RazorCompileOnPublish when not using ModelsMode InMemoryAuto -->
|
||||
<RazorCompileOnBuild>false</RazorCompileOnBuild>
|
||||
<RazorCompileOnPublish>false</RazorCompileOnPublish>
|
||||
</PropertyGroup>
|
||||
<!--#endif -->
|
||||
|
||||
<Import Project="..\PACKAGE_PROJECT_NAME_FROM_TEMPLATE\buildTransitive\PACKAGE_PROJECT_NAME_FROM_TEMPLATE.targets" Condition="'$(PackageProjectName)' != ''" />
|
||||
<ItemGroup Condition="'$(PackageProjectName)' != ''">
|
||||
|
||||
@@ -25,6 +25,11 @@
|
||||
//#endif
|
||||
"Umbraco": {
|
||||
"CMS": {
|
||||
//#if (UseHttpsRedirect || DevelopmentMode == "IDEDevelopment")
|
||||
"Global": {
|
||||
"UseHttps": false
|
||||
},
|
||||
//#endif
|
||||
//#if (UsingUnattenedInstall)
|
||||
"Unattended": {
|
||||
"InstallUnattended": true,
|
||||
@@ -36,12 +41,22 @@
|
||||
"Content": {
|
||||
"MacroErrors": "Throw"
|
||||
},
|
||||
//#if (DevelopmentMode == "IDEDevelopment")
|
||||
"Runtime": {
|
||||
"Mode": "Development"
|
||||
},
|
||||
//#if (ModelsBuilderMode == "Default")
|
||||
"ModelsBuilder": {
|
||||
"ModelsMode": "SourceCodeAuto"
|
||||
},
|
||||
////#else
|
||||
//"ModelsBuilder": {
|
||||
// "ModelsMode": "MODELS_MODE"
|
||||
//},
|
||||
//#endif
|
||||
//#endif
|
||||
"Hosting": {
|
||||
"Debug": true
|
||||
},
|
||||
"RuntimeMinification": {
|
||||
"UseInMemoryCache": true,
|
||||
"CacheBuster": "Timestamp"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"CMS": {
|
||||
"Global": {
|
||||
"Id": "TELEMETRYID_FROM_TEMPLATE",
|
||||
//#if (UseHttpsRedirect)
|
||||
//#if (UseHttpsRedirect || DevelopmentMode == "IDEDevelopment")
|
||||
"UseHttps": true,
|
||||
//#endif
|
||||
//#if (HasNoNodesViewPath)
|
||||
@@ -37,6 +37,24 @@
|
||||
"Unattended": {
|
||||
"UpgradeUnattended": true
|
||||
},
|
||||
//#if (UseDeliveryApi)
|
||||
"DeliveryApi": {
|
||||
"Enabled": true
|
||||
},
|
||||
//#endif
|
||||
//#if (ModelsBuilderMode != "Default" && DevelopmentMode == "BackOfficeDevelopment")
|
||||
"ModelsBuilder": {
|
||||
"ModelsMode": "MODELS_MODE"
|
||||
},
|
||||
//#endif
|
||||
//#if (DevelopmentMode == "IDEDevelopment")
|
||||
"Runtime": {
|
||||
"Mode": "Production"
|
||||
},
|
||||
"ModelsBuilder": {
|
||||
"ModelsMode": "Nothing"
|
||||
},
|
||||
//#endif
|
||||
"Security": {
|
||||
"AllowConcurrentLogins": false
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<!-- Third-party packages -->
|
||||
<PackageVersion Include="AutoFixture.AutoMoq" Version="4.18.1" />
|
||||
<PackageVersion Include="AutoFixture.NUnit3" Version="4.18.1" />
|
||||
<PackageVersion Include="Bogus" Version="35.5.1" />
|
||||
<PackageVersion Include="Bogus" Version="35.6.0" />
|
||||
<PackageVersion Include="Moq" Version="4.18.4" />
|
||||
<PackageVersion Include="NUnit" Version="3.14.0" />
|
||||
<PackageVersion Include="NUnit3TestAdapter" Version="4.5.0" PrivateAssets="all" />
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
38
tests/Umbraco.Tests.AcceptanceTest/package-lock.json
generated
38
tests/Umbraco.Tests.AcceptanceTest/package-lock.json
generated
@@ -7,8 +7,8 @@
|
||||
"name": "acceptancetest",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@umbraco/json-models-builders": "^2.0.9",
|
||||
"@umbraco/playwright-testhelpers": "^2.0.0-beta.65",
|
||||
"@umbraco/json-models-builders": "^2.0.17",
|
||||
"@umbraco/playwright-testhelpers": "^2.0.0-beta.78",
|
||||
"camelize": "^1.0.0",
|
||||
"dotenv": "^16.3.1",
|
||||
"faker": "^4.1.0",
|
||||
@@ -132,24 +132,20 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@umbraco/json-models-builders": {
|
||||
"version": "2.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@umbraco/json-models-builders/-/json-models-builders-2.0.9.tgz",
|
||||
"integrity": "sha512-p6LjcE38WsFCvLtRRRVOCuMvris3OXeoueFu0FZBOHk2r7PXiqYCBUls/KbKxqpixzVDAb48RBd1hV7sKPcm5A==",
|
||||
"version": "2.0.17",
|
||||
"resolved": "https://registry.npmjs.org/@umbraco/json-models-builders/-/json-models-builders-2.0.17.tgz",
|
||||
"integrity": "sha512-i7uuojDjWuXkch9XkEClGtlKJ0Lw3BTGpp4qKaUM+btb7g1sn1Gi50+f+478cJvLG6+q6rmQDZCIXqrTU6Ryhg==",
|
||||
"dependencies": {
|
||||
"camelize": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@umbraco/playwright-testhelpers": {
|
||||
"version": "2.0.0-beta.65",
|
||||
"resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-2.0.0-beta.65.tgz",
|
||||
"integrity": "sha512-plSD/4hhVaMl2TItAaBOUQyuy0Qo5rW3EGIF0TvL3a01s6hNoW1DrOCZhWsOOsMTkgf+oScLEsVIBMk0uDLQrg==",
|
||||
"version": "2.0.0-beta.78",
|
||||
"resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-2.0.0-beta.78.tgz",
|
||||
"integrity": "sha512-s9jLCKQRfXH2zAkT4iUzu/XsrrPQRFVWdj7Ps3uvBV8YzdM1EYMAaCKwgZ5OnCSCN87gysYTW++NZyKT2Fg6qQ==",
|
||||
"dependencies": {
|
||||
"@umbraco/json-models-builders": "2.0.9",
|
||||
"camelize": "^1.0.0",
|
||||
"faker": "^4.1.0",
|
||||
"form-data": "^4.0.0",
|
||||
"node-fetch": "^2.6.7",
|
||||
"xhr2": "^0.2.1"
|
||||
"@umbraco/json-models-builders": "2.0.17",
|
||||
"node-fetch": "^2.6.7"
|
||||
}
|
||||
},
|
||||
"node_modules/aggregate-error": {
|
||||
@@ -419,20 +415,6 @@
|
||||
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "7.2.3",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||
|
||||
@@ -21,8 +21,8 @@
|
||||
"wait-on": "^7.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@umbraco/json-models-builders": "^2.0.9",
|
||||
"@umbraco/playwright-testhelpers": "^2.0.0-beta.65",
|
||||
"@umbraco/json-models-builders": "^2.0.17",
|
||||
"@umbraco/playwright-testhelpers": "^2.0.0-beta.78",
|
||||
"camelize": "^1.0.0",
|
||||
"dotenv": "^16.3.1",
|
||||
"faker": "^4.1.0",
|
||||
|
||||
@@ -9,6 +9,7 @@ test.describe('DataType tests', () => {
|
||||
const dataTypeName = 'TestDataType';
|
||||
const folderName = 'TestDataTypeFolder';
|
||||
const editorAlias = 'Umbraco.DateTime';
|
||||
const editorUiAlias = 'Umb.PropertyEditorUi.DatePicker';
|
||||
const dataTypeData = [
|
||||
{
|
||||
"alias": "tester",
|
||||
@@ -29,7 +30,7 @@ test.describe('DataType tests', () => {
|
||||
|
||||
test('can create dataType', async ({umbracoApi}) => {
|
||||
// Act
|
||||
dataTypeId = await umbracoApi.dataType.create(dataTypeName, editorAlias, dataTypeData);
|
||||
dataTypeId = await umbracoApi.dataType.create(dataTypeName, editorAlias, editorUiAlias, dataTypeData);
|
||||
|
||||
// Assert
|
||||
expect(umbracoApi.dataType.doesExist(dataTypeId)).toBeTruthy();
|
||||
@@ -37,7 +38,7 @@ test.describe('DataType tests', () => {
|
||||
|
||||
test('can update dataType', async ({umbracoApi}) => {
|
||||
// Arrange
|
||||
dataTypeId = await umbracoApi.dataType.create(dataTypeName, editorAlias, []);
|
||||
dataTypeId = await umbracoApi.dataType.create(dataTypeName, editorAlias, editorUiAlias, []);
|
||||
const dataType = await umbracoApi.dataType.get(dataTypeId);
|
||||
dataType.values = dataTypeData;
|
||||
|
||||
@@ -52,7 +53,7 @@ test.describe('DataType tests', () => {
|
||||
|
||||
test('can delete dataType', async ({umbracoApi}) => {
|
||||
// Arrange
|
||||
dataTypeId = await umbracoApi.dataType.create(dataTypeName, editorAlias, dataTypeData);
|
||||
dataTypeId = await umbracoApi.dataType.create(dataTypeName, editorAlias, editorUiAlias, dataTypeData);
|
||||
expect(await umbracoApi.dataType.doesExist(dataTypeId)).toBeTruthy();
|
||||
|
||||
// Act
|
||||
@@ -65,7 +66,7 @@ test.describe('DataType tests', () => {
|
||||
test('can move a dataType to a folder', async ({umbracoApi}) => {
|
||||
// Arrange
|
||||
await umbracoApi.dataType.ensureNameNotExists(folderName);
|
||||
dataTypeId = await umbracoApi.dataType.create(dataTypeName, editorAlias, dataTypeData);
|
||||
dataTypeId = await umbracoApi.dataType.create(dataTypeName, editorAlias, editorUiAlias, dataTypeData);
|
||||
expect(await umbracoApi.dataType.doesNameExist(dataTypeName)).toBeTruthy();
|
||||
dataTypeFolderId = await umbracoApi.dataType.createFolder(folderName);
|
||||
expect(await umbracoApi.dataType.doesFolderExist(dataTypeFolderId)).toBeTruthy();
|
||||
@@ -82,7 +83,7 @@ test.describe('DataType tests', () => {
|
||||
test('can copy a dataType to a folder', async ({umbracoApi}) => {
|
||||
// Arrange
|
||||
await umbracoApi.dataType.ensureNameNotExists(folderName);
|
||||
dataTypeId = await umbracoApi.dataType.create(dataTypeName, editorAlias, dataTypeData);
|
||||
dataTypeId = await umbracoApi.dataType.create(dataTypeName, editorAlias, editorUiAlias, dataTypeData);
|
||||
dataTypeFolderId = await umbracoApi.dataType.createFolder(folderName);
|
||||
|
||||
const dataType = await umbracoApi.dataType.get(dataTypeId);
|
||||
|
||||
@@ -7,7 +7,7 @@ let contentId = '';
|
||||
const contentName = 'TestContent';
|
||||
const childContentName = 'ChildContent';
|
||||
const documentTypeName = 'DocumentTypeForContent';
|
||||
const childDocumentTypeName = 'ChildDocumentTypeForContent';
|
||||
const childDocumentTypeName = 'ChildDocumentType';
|
||||
|
||||
test.beforeEach(async ({umbracoApi}) => {
|
||||
await umbracoApi.documentType.ensureNameNotExists(documentTypeName);
|
||||
@@ -24,7 +24,7 @@ test.afterEach(async ({umbracoApi}) => {
|
||||
test('can create child node', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => {
|
||||
// Arrange
|
||||
childDocumentTypeId = await umbracoApi.documentType.createDefaultDocumentType(childDocumentTypeName);
|
||||
documentTypeId = await umbracoApi.documentType.createDocumentTypeWithAllowedChildNode(documentTypeName, childDocumentTypeId, true);
|
||||
documentTypeId = await umbracoApi.documentType.createDocumentTypeWithAllowedChildNode(documentTypeName, childDocumentTypeId);
|
||||
contentId = await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
|
||||
await umbracoUi.goToBackOffice();
|
||||
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
|
||||
@@ -32,7 +32,9 @@ test('can create child node', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) =
|
||||
// Act
|
||||
await umbracoUi.content.clickActionsMenuForContent(contentName);
|
||||
await umbracoUi.content.clickCreateButton();
|
||||
await umbracoUi.content.chooseDocumentType(documentTypeName);
|
||||
await umbracoUi.content.chooseDocumentType(childDocumentTypeName);
|
||||
// This wait is needed
|
||||
await umbracoUi.waitForTimeout(500);
|
||||
await umbracoUi.content.enterContentName(childContentName);
|
||||
await umbracoUi.content.clickSaveButton();
|
||||
|
||||
@@ -51,8 +53,7 @@ test('can create child node', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) =
|
||||
await umbracoApi.document.ensureNameNotExists(childContentName);
|
||||
});
|
||||
|
||||
// TODO: Remove skip when the front-end is ready.
|
||||
test.skip('can create child node in child node', async ({umbracoApi, umbracoUi}) => {
|
||||
test('can create child node in child node', async ({umbracoApi, umbracoUi}) => {
|
||||
// Arrange
|
||||
const childOfChildContentName = 'ChildOfChildContent';
|
||||
const childOfChildDocumentTypeName = 'ChildOfChildDocumentType';
|
||||
@@ -60,8 +61,8 @@ test.skip('can create child node in child node', async ({umbracoApi, umbracoUi})
|
||||
let childContentId: any;
|
||||
await umbracoApi.documentType.ensureNameNotExists(childOfChildDocumentTypeName);
|
||||
childOfChildDocumentTypeId = await umbracoApi.documentType.createDefaultDocumentType(childOfChildDocumentTypeName);
|
||||
childDocumentTypeId = await umbracoApi.documentType.createDocumentTypeWithAllowedChildNode(childDocumentTypeName, childOfChildDocumentTypeId, true);
|
||||
documentTypeId = await umbracoApi.documentType.createDocumentTypeWithAllowedChildNode(documentTypeName, childDocumentTypeId, true);
|
||||
childDocumentTypeId = await umbracoApi.documentType.createDocumentTypeWithAllowedChildNode(childDocumentTypeName, childOfChildDocumentTypeId);
|
||||
documentTypeId = await umbracoApi.documentType.createDocumentTypeWithAllowedChildNode(documentTypeName, childDocumentTypeId);
|
||||
contentId = await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
|
||||
childContentId = await umbracoApi.document.createDefaultDocumentWithParent(childContentName, childDocumentTypeId, contentId);
|
||||
await umbracoUi.goToBackOffice();
|
||||
@@ -71,7 +72,7 @@ test.skip('can create child node in child node', async ({umbracoApi, umbracoUi})
|
||||
await umbracoUi.content.clickCaretButtonForContentName(contentName);
|
||||
await umbracoUi.content.clickActionsMenuForContent(childContentName);
|
||||
await umbracoUi.content.clickCreateButton();
|
||||
await umbracoUi.content.clickLabelWithName(childOfChildDocumentTypeName);
|
||||
await umbracoUi.content.chooseDocumentType(childOfChildDocumentTypeName);
|
||||
// This wait is needed
|
||||
await umbracoUi.waitForTimeout(500);
|
||||
await umbracoUi.content.enterContentName(childOfChildContentName);
|
||||
@@ -81,9 +82,7 @@ test.skip('can create child node in child node', async ({umbracoApi, umbracoUi})
|
||||
await umbracoUi.content.isSuccessNotificationVisible();
|
||||
const childOfChildData = await umbracoApi.document.getChildren(childContentId);
|
||||
expect(childOfChildData[0].variants[0].name).toBe(childOfChildContentName);
|
||||
// verify that the child content displays in the tree after reloading children
|
||||
await umbracoUi.content.clickActionsMenuForContent(contentName);
|
||||
await umbracoUi.content.clickReloadButton();
|
||||
// verify that the child content displays in the tree
|
||||
await umbracoUi.content.clickCaretButtonForContentName(childContentName);
|
||||
await umbracoUi.content.doesContentTreeHaveName(childOfChildContentName);
|
||||
|
||||
@@ -95,7 +94,7 @@ test.skip('can create child node in child node', async ({umbracoApi, umbracoUi})
|
||||
test('cannot publish child if the parent is not published', async ({umbracoApi, umbracoUi}) => {
|
||||
// Arrange
|
||||
childDocumentTypeId = await umbracoApi.documentType.createDefaultDocumentType(childDocumentTypeName);
|
||||
documentTypeId = await umbracoApi.documentType.createDocumentTypeWithAllowedChildNode(documentTypeName, childDocumentTypeId, true);
|
||||
documentTypeId = await umbracoApi.documentType.createDocumentTypeWithAllowedChildNode(documentTypeName, childDocumentTypeId);
|
||||
contentId = await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
|
||||
await umbracoApi.document.createDefaultDocumentWithParent(childContentName, childDocumentTypeId, contentId);
|
||||
await umbracoUi.goToBackOffice();
|
||||
@@ -115,7 +114,7 @@ test('cannot publish child if the parent is not published', async ({umbracoApi,
|
||||
test('can publish with descendants', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => {
|
||||
// Arrange
|
||||
childDocumentTypeId = await umbracoApi.documentType.createDefaultDocumentType(childDocumentTypeName);
|
||||
documentTypeId = await umbracoApi.documentType.createDocumentTypeWithAllowedChildNode(documentTypeName, childDocumentTypeId, true);
|
||||
documentTypeId = await umbracoApi.documentType.createDocumentTypeWithAllowedChildNode(documentTypeName, childDocumentTypeId);
|
||||
contentId = await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
|
||||
await umbracoApi.document.createDefaultDocumentWithParent(childContentName, childDocumentTypeId, contentId);
|
||||
await umbracoUi.goToBackOffice();
|
||||
|
||||
@@ -91,7 +91,7 @@ test('can rename content', async ({umbracoApi, umbracoUi}) => {
|
||||
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
|
||||
|
||||
// Act
|
||||
await umbracoUi.content.openContent(wrongContentName);
|
||||
await umbracoUi.content.goToContentWithName(wrongContentName);
|
||||
await umbracoUi.content.enterContentName(contentName);
|
||||
await umbracoUi.content.clickSaveButton();
|
||||
|
||||
@@ -111,7 +111,7 @@ test('can update content', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => {
|
||||
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
|
||||
|
||||
// Act
|
||||
await umbracoUi.content.openContent(contentName);
|
||||
await umbracoUi.content.goToContentWithName(contentName);
|
||||
await umbracoUi.content.enterTextstring(contentText);
|
||||
await umbracoUi.content.clickSaveButton();
|
||||
|
||||
|
||||
@@ -18,13 +18,16 @@ test.afterEach(async ({umbracoApi}) => {
|
||||
test('can see correct information when published', async ({umbracoApi, umbracoUi}) => {
|
||||
// Arrange
|
||||
const notPublishContentLink = 'This document is published but is not in the cache';
|
||||
documentTypeId = await umbracoApi.documentType.createDefaultDocumentTypeWithAllowAsRoot(documentTypeName);
|
||||
contentId = await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
|
||||
const dataTypeName = 'Textstring';
|
||||
const contentText = 'This is test content text';
|
||||
const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName);
|
||||
documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id);
|
||||
contentId = await umbracoApi.document.createDocumentWithTextContent(contentName, documentTypeId, contentText, dataTypeName);
|
||||
await umbracoUi.goToBackOffice();
|
||||
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
|
||||
|
||||
// Act
|
||||
await umbracoUi.content.openContent(contentName);
|
||||
await umbracoUi.content.goToContentWithName(contentName);
|
||||
await umbracoUi.content.clickInfoTab();
|
||||
await umbracoUi.content.doesLinkHaveText(notPublishContentLink);
|
||||
await umbracoUi.content.clickSaveAndPublishButton();
|
||||
@@ -47,8 +50,7 @@ test('can see correct information when published', async ({umbracoApi, umbracoUi
|
||||
await umbracoUi.content.doesCreatedDateHaveText(expectedCreatedDate);
|
||||
});
|
||||
|
||||
// TODO: Remove skip when the frond-end is ready. Currently the document type is not opened after clicking to the button
|
||||
test.skip('can open document type', async ({umbracoApi, umbracoUi}) => {
|
||||
test('can open document type', async ({umbracoApi, umbracoUi}) => {
|
||||
// Arrange
|
||||
documentTypeId = await umbracoApi.documentType.createDefaultDocumentTypeWithAllowAsRoot(documentTypeName);
|
||||
contentId = await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
|
||||
@@ -56,8 +58,7 @@ test.skip('can open document type', async ({umbracoApi, umbracoUi}) => {
|
||||
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
|
||||
|
||||
// Act
|
||||
await umbracoUi.content.openContent(contentName);
|
||||
await umbracoUi.content.clickInfoTab();
|
||||
await umbracoUi.content.goToContentWithName(contentName);
|
||||
await umbracoUi.content.clickDocumentTypeByName(documentTypeName);
|
||||
|
||||
// Assert
|
||||
@@ -66,7 +67,7 @@ test.skip('can open document type', async ({umbracoApi, umbracoUi}) => {
|
||||
|
||||
test('can open template', async ({umbracoApi, umbracoUi}) => {
|
||||
// Arrange
|
||||
const templateName = "TestTemplateForContent";
|
||||
const templateName = 'TestTemplateForContent';
|
||||
await umbracoApi.template.ensureNameNotExists(templateName);
|
||||
const templateId = await umbracoApi.template.createDefaultTemplate(templateName);
|
||||
documentTypeId = await umbracoApi.documentType.createDocumentTypeWithAllowedTemplate(documentTypeName, templateId, true);
|
||||
@@ -75,8 +76,7 @@ test('can open template', async ({umbracoApi, umbracoUi}) => {
|
||||
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
|
||||
|
||||
// Act
|
||||
await umbracoUi.content.openContent(contentName);
|
||||
await umbracoUi.content.clickInfoTab();
|
||||
await umbracoUi.content.goToContentWithName(contentName);
|
||||
await umbracoUi.content.clickTemplateByName(templateName);
|
||||
|
||||
// Assert
|
||||
@@ -88,8 +88,8 @@ test('can open template', async ({umbracoApi, umbracoUi}) => {
|
||||
|
||||
test('can change template', async ({umbracoApi, umbracoUi}) => {
|
||||
// Arrange
|
||||
const firstTemplateName = "TestTemplateOneForContent";
|
||||
const secondTemplateName = "TestTemplateTwoForContent";
|
||||
const firstTemplateName = 'TestTemplateOneForContent';
|
||||
const secondTemplateName = 'TestTemplateTwoForContent';
|
||||
await umbracoApi.template.ensureNameNotExists(firstTemplateName);
|
||||
await umbracoApi.template.ensureNameNotExists(secondTemplateName);
|
||||
const firstTemplateId = await umbracoApi.template.createDefaultTemplate(firstTemplateName);
|
||||
@@ -100,8 +100,7 @@ test('can change template', async ({umbracoApi, umbracoUi}) => {
|
||||
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
|
||||
|
||||
// Act
|
||||
await umbracoUi.content.openContent(contentName);
|
||||
await umbracoUi.content.clickInfoTab();
|
||||
await umbracoUi.content.goToContentWithName(contentName);
|
||||
await umbracoUi.content.changeTemplate(firstTemplateName, secondTemplateName);
|
||||
await umbracoUi.content.clickSaveButton();
|
||||
|
||||
@@ -116,8 +115,8 @@ test('can change template', async ({umbracoApi, umbracoUi}) => {
|
||||
|
||||
test('cannot change to a template that is not allowed in the document type', async ({umbracoApi, umbracoUi}) => {
|
||||
// Arrange
|
||||
const firstTemplateName = "TestTemplateOneForContent";
|
||||
const secondTemplateName = "TestTemplateTwoForContent";
|
||||
const firstTemplateName = 'TestTemplateOneForContent';
|
||||
const secondTemplateName = 'TestTemplateTwoForContent';
|
||||
await umbracoApi.template.ensureNameNotExists(firstTemplateName);
|
||||
await umbracoApi.template.ensureNameNotExists(secondTemplateName);
|
||||
const firstTemplateId = await umbracoApi.template.createDefaultTemplate(firstTemplateName);
|
||||
@@ -128,8 +127,7 @@ test('cannot change to a template that is not allowed in the document type', asy
|
||||
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
|
||||
|
||||
// Act
|
||||
await umbracoUi.content.openContent(contentName);
|
||||
await umbracoUi.content.clickInfoTab();
|
||||
await umbracoUi.content.goToContentWithName(contentName);
|
||||
await umbracoUi.content.clickEditTemplateByName(firstTemplateName);
|
||||
|
||||
// Assert
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user