diff --git a/Directory.Packages.props b/Directory.Packages.props index e5ff6712ea..871c5b2e8f 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -17,7 +17,7 @@ - + @@ -90,7 +90,7 @@ - + diff --git a/src/Umbraco.Cms.Api.Management/Controllers/BackOfficeLoginController.cs b/src/Umbraco.Cms.Api.Management/Controllers/BackOfficeLoginController.cs index aa8b711257..c6ff576b93 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/BackOfficeLoginController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/BackOfficeLoginController.cs @@ -51,7 +51,7 @@ public class BackOfficeLoginController : Controller if (string.IsNullOrEmpty(model.UmbracoUrl)) { - model.UmbracoUrl = _hostingEnvironment.ToAbsolute(_globalSettings.UmbracoPath); + model.UmbracoUrl = _hostingEnvironment.ToAbsolute(Constants.System.DefaultUmbracoPath); } if (string.IsNullOrEmpty(model.ReturnUrl)) diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/ByKeyPublishedDocumentController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/ByKeyPublishedDocumentController.cs new file mode 100644 index 0000000000..2368afc732 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/ByKeyPublishedDocumentController.cs @@ -0,0 +1,58 @@ +using Asp.Versioning; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.Factories; +using Umbraco.Cms.Api.Management.ViewModels.Document; +using Umbraco.Cms.Core.Actions; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Security.Authorization; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Web.Common.Authorization; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Api.Management.Controllers.Document; + +[ApiVersion("1.0")] +public class ByKeyPublishedDocumentController : DocumentControllerBase +{ + private readonly IAuthorizationService _authorizationService; + private readonly IContentEditingService _contentEditingService; + private readonly IDocumentPresentationFactory _documentPresentationFactory; + + public ByKeyPublishedDocumentController( + IAuthorizationService authorizationService, + IContentEditingService contentEditingService, + IDocumentPresentationFactory documentPresentationFactory) + { + _authorizationService = authorizationService; + _contentEditingService = contentEditingService; + _documentPresentationFactory = documentPresentationFactory; + } + + [HttpGet("{id:guid}/published")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PublishedDocumentResponseModel), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + public async Task ByKeyPublished(CancellationToken cancellationToken, Guid id) + { + AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync( + User, + ContentPermissionResource.WithKeys(ActionBrowse.ActionLetter, id), + AuthorizationPolicies.ContentPermissionByResource); + + if (!authorizationResult.Succeeded) + { + return Forbidden(); + } + + IContent? content = await _contentEditingService.GetAsync(id); + if (content == null || content.Published is false) + { + return DocumentNotFound(); + } + + PublishedDocumentResponseModel model = await _documentPresentationFactory.CreatePublishedResponseModelAsync(content); + return Ok(model); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/PartialView/ByPathPartialViewController.cs b/src/Umbraco.Cms.Api.Management/Controllers/PartialView/ByPathPartialViewController.cs index 3fdae52115..bbf1dec98f 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/PartialView/ByPathPartialViewController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/PartialView/ByPathPartialViewController.cs @@ -23,7 +23,7 @@ public class ByPathPartialViewController : PartialViewControllerBase _mapper = mapper; } - [HttpGet("{path}")] + [HttpGet("{*path}")] [MapToApiVersion("1.0")] [ProducesResponseType(typeof(PartialViewResponseModel), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] diff --git a/src/Umbraco.Cms.Api.Management/Controllers/PartialView/DeletePartialViewController.cs b/src/Umbraco.Cms.Api.Management/Controllers/PartialView/DeletePartialViewController.cs index a72c8129b5..b44990b41e 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/PartialView/DeletePartialViewController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/PartialView/DeletePartialViewController.cs @@ -22,7 +22,7 @@ public class DeletePartialViewController : PartialViewControllerBase _backOfficeSecurityAccessor = backOfficeSecurityAccessor; } - [HttpDelete("{path}")] + [HttpDelete("{*path}")] [MapToApiVersion("1.0")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] diff --git a/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Folder/ByPathPartialViewFolderController.cs b/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Folder/ByPathPartialViewFolderController.cs index df15c726b5..727f4d1d68 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Folder/ByPathPartialViewFolderController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Folder/ByPathPartialViewFolderController.cs @@ -22,7 +22,7 @@ public class ByPathPartialViewFolderController : PartialViewFolderControllerBase _mapper = mapper; } - [HttpGet("{path}")] + [HttpGet("{*path}")] [MapToApiVersion("1.0")] [ProducesResponseType(typeof(PartialViewFolderResponseModel), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] diff --git a/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Folder/DeletePartialViewFolderController.cs b/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Folder/DeletePartialViewFolderController.cs index 121cb1dae7..41cf9548d5 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Folder/DeletePartialViewFolderController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Folder/DeletePartialViewFolderController.cs @@ -15,7 +15,7 @@ public class DeletePartialViewFolderController : PartialViewFolderControllerBase public DeletePartialViewFolderController(IPartialViewFolderService partialViewFolderService) => _partialViewFolderService = partialViewFolderService; - [HttpDelete("{path}")] + [HttpDelete("{*path}")] [MapToApiVersion("1.0")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] diff --git a/src/Umbraco.Cms.Api.Management/Controllers/PartialView/UpdatePartialViewController.cs b/src/Umbraco.Cms.Api.Management/Controllers/PartialView/UpdatePartialViewController.cs index a935f87a80..e460b0adf0 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/PartialView/UpdatePartialViewController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/PartialView/UpdatePartialViewController.cs @@ -29,7 +29,7 @@ public class UpdatePartialViewController : PartialViewControllerBase _mapper = mapper; } - [HttpPut("{path}")] + [HttpPut("{*path}")] [MapToApiVersion("1.0")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] diff --git a/src/Umbraco.Cms.Api.Management/Controllers/PublishedCache/CollectPublishedCacheController.cs b/src/Umbraco.Cms.Api.Management/Controllers/PublishedCache/CollectPublishedCacheController.cs index 4cbac68446..ef759ca5a4 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/PublishedCache/CollectPublishedCacheController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/PublishedCache/CollectPublishedCacheController.cs @@ -1,7 +1,6 @@ using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Umbraco.Cms.Core.PublishedCache; namespace Umbraco.Cms.Api.Management.Controllers.PublishedCache; @@ -11,9 +10,9 @@ public class CollectPublishedCacheController : PublishedCacheControllerBase { [HttpPost("collect")] [MapToApiVersion("1.0")] - [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status501NotImplemented)] public async Task Collect(CancellationToken cancellationToken) { - return Ok(); + return StatusCode(StatusCodes.Status501NotImplemented); } } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/PublishedCache/StatusPublishedCacheController.cs b/src/Umbraco.Cms.Api.Management/Controllers/PublishedCache/StatusPublishedCacheController.cs index aad76e7dbf..3a9d72c12c 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/PublishedCache/StatusPublishedCacheController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/PublishedCache/StatusPublishedCacheController.cs @@ -1,7 +1,6 @@ using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Umbraco.Cms.Core.PublishedCache; namespace Umbraco.Cms.Api.Management.Controllers.PublishedCache; @@ -11,7 +10,9 @@ public class StatusPublishedCacheController : PublishedCacheControllerBase { [HttpGet("status")] [MapToApiVersion("1.0")] - [ProducesResponseType(typeof(string), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status501NotImplemented)] public async Task> Status(CancellationToken cancellationToken) - => await Task.FromResult(Ok("Obsoleted")); + { + return StatusCode(StatusCodes.Status501NotImplemented); + } } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Script/ByPathScriptController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Script/ByPathScriptController.cs index 67a3b605a7..60c5fe644a 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Script/ByPathScriptController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Script/ByPathScriptController.cs @@ -23,7 +23,7 @@ public class ByPathScriptController : ScriptControllerBase _mapper = mapper; } - [HttpGet("{path}")] + [HttpGet("{*path}")] [MapToApiVersion("1.0")] [ProducesResponseType(typeof(ScriptResponseModel), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Script/DeleteScriptController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Script/DeleteScriptController.cs index 74dc1554e8..94c2e0a1c0 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Script/DeleteScriptController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Script/DeleteScriptController.cs @@ -22,7 +22,7 @@ public class DeleteScriptController : ScriptControllerBase _backOfficeSecurityAccessor = backOfficeSecurityAccessor; } - [HttpDelete("{path}")] + [HttpDelete("{*path}")] [MapToApiVersion("1.0")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Script/Folder/ByPathScriptFolderController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Script/Folder/ByPathScriptFolderController.cs index 84543bffe4..698c94f685 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Script/Folder/ByPathScriptFolderController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Script/Folder/ByPathScriptFolderController.cs @@ -22,7 +22,7 @@ public class ByPathScriptFolderController : ScriptFolderControllerBase _mapper = mapper; } - [HttpGet("{path}")] + [HttpGet("{*path}")] [MapToApiVersion("1.0")] [ProducesResponseType(typeof(ScriptFolderResponseModel), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Script/Folder/DeleteScriptFolderController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Script/Folder/DeleteScriptFolderController.cs index b5fd5022b7..48d97e9b4b 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Script/Folder/DeleteScriptFolderController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Script/Folder/DeleteScriptFolderController.cs @@ -15,7 +15,7 @@ public class DeleteScriptFolderController : ScriptFolderControllerBase public DeleteScriptFolderController(IScriptFolderService scriptFolderService) => _scriptFolderService = scriptFolderService; - [HttpDelete("{path}")] + [HttpDelete("{*path}")] [MapToApiVersion("1.0")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Script/UpdateScriptController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Script/UpdateScriptController.cs index 87ff3502ba..a18f82ebc8 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Script/UpdateScriptController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Script/UpdateScriptController.cs @@ -29,7 +29,7 @@ public class UpdateScriptController : ScriptControllerBase _backOfficeSecurityAccessor = backOfficeSecurityAccessor; } - [HttpPut("{path}")] + [HttpPut("{*path}")] [MapToApiVersion("1.0")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/ByPathStylesheetController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/ByPathStylesheetController.cs index a7e3a6139f..87aec60133 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/ByPathStylesheetController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/ByPathStylesheetController.cs @@ -23,7 +23,7 @@ public class ByPathStylesheetController : StylesheetControllerBase _umbracoMapper = umbracoMapper; } - [HttpGet("{path}")] + [HttpGet("{*path}")] [MapToApiVersion("1.0")] [ProducesResponseType(typeof(StylesheetResponseModel), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/DeleteStylesheetController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/DeleteStylesheetController.cs index 3dcdb92bbd..9996ddeae0 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/DeleteStylesheetController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/DeleteStylesheetController.cs @@ -25,7 +25,7 @@ public class DeleteStylesheetController : StylesheetControllerBase _backOfficeSecurityAccessor = backOfficeSecurityAccessor; } - [HttpDelete("{path}")] + [HttpDelete("{*path}")] [MapToApiVersion("1.0")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Folder/ByPathStylesheetFolderController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Folder/ByPathStylesheetFolderController.cs index d5d0cca84f..95c7d853ab 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Folder/ByPathStylesheetFolderController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Folder/ByPathStylesheetFolderController.cs @@ -24,7 +24,7 @@ public class ByPathStylesheetFolderController : StylesheetFolderControllerBase _mapper = mapper; } - [HttpGet("{path}")] + [HttpGet("{*path}")] [MapToApiVersion("1.0")] [ProducesResponseType(typeof(StylesheetFolderResponseModel), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Folder/DeleteStylesheetFolderController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Folder/DeleteStylesheetFolderController.cs index 38760b34e7..deb38f0865 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Folder/DeleteStylesheetFolderController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Folder/DeleteStylesheetFolderController.cs @@ -15,7 +15,7 @@ public class DeleteStylesheetFolderController : StylesheetFolderControllerBase public DeleteStylesheetFolderController(IStylesheetFolderService stylesheetFolderService) => _stylesheetFolderService = stylesheetFolderService; - [HttpDelete("{path}")] + [HttpDelete("{*path}")] [MapToApiVersion("1.0")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/UpdateStylesheetController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/UpdateStylesheetController.cs index 377fef87d1..7000ca7b40 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/UpdateStylesheetController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/UpdateStylesheetController.cs @@ -32,7 +32,7 @@ public class UpdateStylesheetController : StylesheetControllerBase _backOfficeSecurityAccessor = backOfficeSecurityAccessor; } - [HttpPut("{path}")] + [HttpPut("{*path}")] [MapToApiVersion("1.0")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] diff --git a/src/Umbraco.Cms.Api.Management/Controllers/User/ClientCredentials/ClientCredentialsUserControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/User/ClientCredentials/ClientCredentialsUserControllerBase.cs index 9a30da3811..1751564b37 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/User/ClientCredentials/ClientCredentialsUserControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/User/ClientCredentials/ClientCredentialsUserControllerBase.cs @@ -18,6 +18,10 @@ public abstract class ClientCredentialsUserControllerBase : UserControllerBase .WithTitle("Duplicate client ID") .WithDetail("The specified client ID is already in use. Choose another client ID.") .Build()), + BackOfficeUserClientCredentialsOperationStatus.InvalidClientId => BadRequest(problemDetailsBuilder + .WithTitle("Invalid client ID") + .WithDetail("The specified client ID is invalid. A valid client ID can only contain [a-z], [A-Z], [0-9], and [-._~].") + .Build()), _ => StatusCode(StatusCodes.Status500InternalServerError, problemDetailsBuilder .WithTitle("Unknown client credentials operation status.") .Build()), diff --git a/src/Umbraco.Cms.Api.Management/Factories/DocumentPresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/DocumentPresentationFactory.cs index c6291f4db0..817cd9fa2e 100644 --- a/src/Umbraco.Cms.Api.Management/Factories/DocumentPresentationFactory.cs +++ b/src/Umbraco.Cms.Api.Management/Factories/DocumentPresentationFactory.cs @@ -54,6 +54,23 @@ internal sealed class DocumentPresentationFactory : IDocumentPresentationFactory return responseModel; } + public async Task CreatePublishedResponseModelAsync(IContent content) + { + PublishedDocumentResponseModel responseModel = _umbracoMapper.Map(content)!; + + responseModel.Urls = await _documentUrlFactory.CreateUrlsAsync(content); + + Guid? templateKey = content.PublishTemplateId.HasValue + ? _templateService.GetAsync(content.PublishTemplateId.Value).Result?.Key + : null; + + responseModel.Template = templateKey.HasValue + ? new ReferenceByIdModel { Id = templateKey.Value } + : null; + + return responseModel; + } + public DocumentItemResponseModel CreateItemResponseModel(IDocumentEntitySlim entity) { var responseModel = new DocumentItemResponseModel diff --git a/src/Umbraco.Cms.Api.Management/Factories/IDocumentPresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/IDocumentPresentationFactory.cs index 23b11b0e4f..40a537e01a 100644 --- a/src/Umbraco.Cms.Api.Management/Factories/IDocumentPresentationFactory.cs +++ b/src/Umbraco.Cms.Api.Management/Factories/IDocumentPresentationFactory.cs @@ -14,6 +14,8 @@ public interface IDocumentPresentationFactory { Task CreateResponseModelAsync(IContent content); + Task CreatePublishedResponseModelAsync(IContent content); + DocumentItemResponseModel CreateItemResponseModel(IDocumentEntitySlim entity); DocumentBlueprintItemResponseModel CreateBlueprintItemResponseModel(IDocumentEntitySlim entity); diff --git a/src/Umbraco.Cms.Api.Management/Mapping/Content/ContentMapDefinition.cs b/src/Umbraco.Cms.Api.Management/Mapping/Content/ContentMapDefinition.cs index 038e3fbf8b..6970e7d251 100644 --- a/src/Umbraco.Cms.Api.Management/Mapping/Content/ContentMapDefinition.cs +++ b/src/Umbraco.Cms.Api.Management/Mapping/Content/ContentMapDefinition.cs @@ -19,7 +19,7 @@ public abstract class ContentMapDefinition MapValueViewModels(IEnumerable properties, ValueViewModelMapping? additionalPropertyMapping = null) => + protected IEnumerable MapValueViewModels(IEnumerable properties, ValueViewModelMapping? additionalPropertyMapping = null, bool published = false) => properties .SelectMany(property => property .Values @@ -31,12 +31,19 @@ public abstract class ContentMapDefinition((_, _) => new DocumentResponseModel(), Map); + mapper.Define((_, _) => new PublishedDocumentResponseModel(), Map); mapper.Define((_, _) => new DocumentCollectionResponseModel(), Map); mapper.Define((_, _) => new DocumentBlueprintResponseModel(), Map); } @@ -44,6 +45,28 @@ public class DocumentMapDefinition : ContentMapDefinition(source.ContentType)!; + target.Values = MapValueViewModels(source.Properties, published: true); + target.Variants = MapVariantViewModels( + source, + (culture, _, documentVariantViewModel) => + { + documentVariantViewModel.Name = source.GetPublishName(culture) ?? documentVariantViewModel.Name; + DocumentVariantState variantState = DocumentVariantStateHelper.GetState(source, culture); + documentVariantViewModel.State = variantState == DocumentVariantState.PublishedPendingChanges + ? DocumentVariantState.Published + : variantState; + documentVariantViewModel.PublishDate = culture == null + ? source.PublishDate + : source.GetPublishDate(culture); + }); + target.IsTrashed = source.Trashed; + } + // Umbraco.Code.MapAll private void Map(IContent source, DocumentCollectionResponseModel target, MapperContext context) { diff --git a/src/Umbraco.Cms.Api.Management/OpenApi.json b/src/Umbraco.Cms.Api.Management/OpenApi.json index eb912bf856..6240675b2c 100644 --- a/src/Umbraco.Cms.Api.Management/OpenApi.json +++ b/src/Umbraco.Cms.Api.Management/OpenApi.json @@ -8967,6 +8967,66 @@ ] } }, + "/umbraco/management/api/v1/document/{id}/published": { + "get": { + "tags": [ + "Document" + ], + "operationId": "GetDocumentByIdPublished", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/PublishedDocumentResponseModel" + } + ] + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ProblemDetails" + } + ] + } + } + } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + }, + "403": { + "description": "The authenticated user do not have access to this resource" + } + }, + "security": [ + { + "Backoffice User": [ ] + } + ] + } + }, "/umbraco/management/api/v1/document/{id}/referenced-by": { "get": { "tags": [ @@ -42471,6 +42531,72 @@ }, "additionalProperties": false }, + "PublishedDocumentResponseModel": { + "required": [ + "documentType", + "id", + "isTrashed", + "urls", + "values", + "variants" + ], + "type": "object", + "properties": { + "values": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/DocumentValueResponseModel" + } + ] + } + }, + "variants": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/DocumentVariantResponseModel" + } + ] + } + }, + "id": { + "type": "string", + "format": "uuid" + }, + "documentType": { + "oneOf": [ + { + "$ref": "#/components/schemas/DocumentTypeReferenceResponseModel" + } + ] + }, + "urls": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/DocumentUrlInfoModel" + } + ] + } + }, + "template": { + "oneOf": [ + { + "$ref": "#/components/schemas/ReferenceByIdModel" + } + ], + "nullable": true + }, + "isTrashed": { + "type": "boolean" + } + }, + "additionalProperties": false + }, "RedirectStatusModel": { "enum": [ "Enabled", diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Document/PublishedDocumentResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Document/PublishedDocumentResponseModel.cs new file mode 100644 index 0000000000..a228a3954b --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Document/PublishedDocumentResponseModel.cs @@ -0,0 +1,5 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.Document; + +public class PublishedDocumentResponseModel : DocumentResponseModel +{ +} diff --git a/src/Umbraco.Cms.Persistence.EFCore/Extensions/BackOfficeAuthBuilderOpenIddictExtensions.cs b/src/Umbraco.Cms.Persistence.EFCore/Extensions/BackOfficeAuthBuilderOpenIddictExtensions.cs deleted file mode 100644 index 3764537988..0000000000 --- a/src/Umbraco.Cms.Persistence.EFCore/Extensions/BackOfficeAuthBuilderOpenIddictExtensions.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Umbraco.Cms.Core.DependencyInjection; -using Umbraco.Cms.Persistence.EFCore; - -namespace Umbraco.Extensions; - -public static class BackOfficeAuthBuilderOpenIddictExtensions -{ - [Obsolete("This is no longer used and will be removed in V15. Instead use the overload that specifies the DbContext type.")] - public static IUmbracoBuilder AddUmbracoEFCoreDbContext(this IUmbracoBuilder builder) - { - builder.Services.AddUmbracoEFCoreContext((options, connectionString, providerName) => - { - // Register the entity sets needed by OpenIddict. - options.UseOpenIddict(); - }); - - return builder; - } -} diff --git a/src/Umbraco.Cms.Persistence.EFCore/Extensions/UmbracoEFCoreServiceCollectionExtensions.cs b/src/Umbraco.Cms.Persistence.EFCore/Extensions/UmbracoEFCoreServiceCollectionExtensions.cs index 7694f83dd2..d7da8a65fe 100644 --- a/src/Umbraco.Cms.Persistence.EFCore/Extensions/UmbracoEFCoreServiceCollectionExtensions.cs +++ b/src/Umbraco.Cms.Persistence.EFCore/Extensions/UmbracoEFCoreServiceCollectionExtensions.cs @@ -14,45 +14,6 @@ public static class UmbracoEFCoreServiceCollectionExtensions { public delegate void DefaultEFCoreOptionsAction(DbContextOptionsBuilder options, string? providerName, string? connectionString); - [Obsolete("Use AddUmbracoDbContext(this IServiceCollection services, Action? optionsAction = null) instead.")] - public static IServiceCollection AddUmbracoEFCoreContext(this IServiceCollection services, DefaultEFCoreOptionsAction? defaultEFCoreOptionsAction = null) - where T : DbContext - { - services.AddPooledDbContextFactory((provider, builder) => SetupDbContext(defaultEFCoreOptionsAction, provider, builder)); - services.AddTransient(services => services.GetRequiredService>().CreateDbContext()); - - services.AddUnique, AmbientEFCoreScopeStack>(); - services.AddUnique, EFCoreScopeAccessor>(); - services.AddUnique, EFCoreScopeProvider>(); - services.AddSingleton>(); - services.AddSingleton>(); - - return services; - } - - [Obsolete("Use AddUmbracoDbContext(this IServiceCollection services, Action? optionsAction = null) instead.")] - public static IServiceCollection AddUmbracoEFCoreContext(this IServiceCollection services, string connectionString, string providerName, DefaultEFCoreOptionsAction? defaultEFCoreOptionsAction = null) - where T : DbContext - { - // Replace data directory - string? dataDirectory = AppDomain.CurrentDomain.GetData(Constants.System.DataDirectoryName)?.ToString(); - if (string.IsNullOrEmpty(dataDirectory) is false) - { - connectionString = connectionString.Replace(Constants.System.DataDirectoryPlaceholder, dataDirectory); - } - - services.AddPooledDbContextFactory(options => defaultEFCoreOptionsAction?.Invoke(options, providerName, connectionString)); - services.AddTransient(services => services.GetRequiredService>().CreateDbContext()); - - services.AddUnique, AmbientEFCoreScopeStack>(); - services.AddUnique, EFCoreScopeAccessor>(); - services.AddUnique, EFCoreScopeProvider>(); - services.AddSingleton>(); - services.AddSingleton>(); - - return services; - } - /// /// Adds a EFCore DbContext with all the services needed to integrate with Umbraco scopes. /// @@ -149,26 +110,4 @@ public static class UmbracoEFCoreServiceCollectionExtensions builder.UseDatabaseProvider(connectionStrings.ProviderName, connectionStrings.ConnectionString); } - - [Obsolete] - private static void SetupDbContext(DefaultEFCoreOptionsAction? defaultEFCoreOptionsAction, IServiceProvider provider, DbContextOptionsBuilder builder) - { - ConnectionStrings connectionStrings = GetConnectionStringAndProviderName(provider); - defaultEFCoreOptionsAction?.Invoke(builder, connectionStrings.ConnectionString, connectionStrings.ProviderName); - } - - [Obsolete] - private static ConnectionStrings GetConnectionStringAndProviderName(IServiceProvider serviceProvider) - { - ConnectionStrings connectionStrings = serviceProvider.GetRequiredService>().CurrentValue; - - // Replace data directory - string? dataDirectory = AppDomain.CurrentDomain.GetData(Constants.System.DataDirectoryName)?.ToString(); - if (string.IsNullOrEmpty(dataDirectory) is false) - { - connectionStrings.ConnectionString = connectionStrings.ConnectionString?.Replace(Constants.System.DataDirectoryPlaceholder, dataDirectory); - } - - return connectionStrings; - } } diff --git a/src/Umbraco.Core/Actions/ActionToPublish.cs b/src/Umbraco.Core/Actions/ActionToPublish.cs deleted file mode 100644 index a980d819aa..0000000000 --- a/src/Umbraco.Core/Actions/ActionToPublish.cs +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) Umbraco. -// See LICENSE for more details. - -namespace Umbraco.Cms.Core.Actions; - -/// -/// This action is invoked when children to a document is being sent to published (by an editor without publishrights). -/// -[Obsolete("Scheduled for removal in v13")] -public class ActionToPublish : IAction -{ - /// - public const string ActionLetter = "Umb.Document.SendForApproval"; - - /// - public const string ActionAlias = "sendtopublish"; - - /// - public string Letter => ActionLetter; - - /// - public string Alias => ActionAlias; - - /// - public string Category => Constants.Conventions.PermissionCategories.ContentCategory; - - /// - public string Icon => "icon-outbox"; - - /// - public bool ShowInNotifier => true; - - /// - public bool CanBePermissionAssigned => true; -} diff --git a/src/Umbraco.Core/Cache/DataTypeConfigurationCache.cs b/src/Umbraco.Core/Cache/DataTypeConfigurationCache.cs index 78ee17d2f8..70c9b4161b 100644 --- a/src/Umbraco.Core/Cache/DataTypeConfigurationCache.cs +++ b/src/Umbraco.Core/Cache/DataTypeConfigurationCache.cs @@ -26,7 +26,7 @@ internal sealed class DataTypeConfigurationCache : IDataTypeConfigurationCache var cacheKey = GetCacheKey(key); if (_memoryCache.TryGetValue(cacheKey, out T? configuration) is false) { - IDataType? dataType = _dataTypeService.GetDataType(key); + IDataType? dataType = _dataTypeService.GetAsync(key).GetAwaiter().GetResult(); configuration = dataType?.ConfigurationAs(); // Only cache if data type was found (but still cache null configurations) diff --git a/src/Umbraco.Core/Cache/Refreshers/Implement/ContentCacheRefresher.cs b/src/Umbraco.Core/Cache/Refreshers/Implement/ContentCacheRefresher.cs index da70761466..f84b597b15 100644 --- a/src/Umbraco.Core/Cache/Refreshers/Implement/ContentCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/Refreshers/Implement/ContentCacheRefresher.cs @@ -86,15 +86,16 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase(); IAppPolicyCache isolatedCache = AppCaches.IsolatedCaches.GetOrCreate(); - foreach (JsonPayload payload in payloads.Where(x => x.Id != default)) + foreach (JsonPayload payload in payloads) { - // By INT Id - isolatedCache.Clear(RepositoryCacheKeys.GetKey(payload.Id)); - - // By GUID Key - isolatedCache.Clear(RepositoryCacheKeys.GetKey(payload.Key)); - + if (payload.Id != default) + { + // By INT Id + isolatedCache.Clear(RepositoryCacheKeys.GetKey(payload.Id)); + // By GUID Key + isolatedCache.Clear(RepositoryCacheKeys.GetKey(payload.Key)); + } // remove those that are in the branch if (payload.ChangeTypes.HasTypesAny(TreeChangeTypes.RefreshBranch | TreeChangeTypes.Remove)) @@ -115,7 +116,10 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase? _assemblies; - /// - /// Initializes a new instance of the class. - /// - [Obsolete("Please use an alternative constructor.")] - public TypeLoader( - ITypeFinder typeFinder, - IRuntimeHash runtimeHash, - IAppPolicyCache runtimeCache, - DirectoryInfo localTempPath, - ILogger logger, - IProfiler profiler, - IEnumerable? assembliesToScan = null) - : this(typeFinder, logger, assembliesToScan) - { - } - - /// - /// Initializes a new instance of the class. - /// - [Obsolete("Please use an alternative constructor.")] - public TypeLoader( - ITypeFinder typeFinder, - IRuntimeHash runtimeHash, - IAppPolicyCache runtimeCache, - DirectoryInfo localTempPath, - ILogger logger, - IProfiler profiler, - bool detectChanges, - IEnumerable? assembliesToScan = null) - : this(typeFinder, logger, assembliesToScan) - { - } - public TypeLoader( ITypeFinder typeFinder, ILogger logger, @@ -100,18 +67,6 @@ public sealed class TypeLoader [Obsolete("This will be removed in a future version.")] public IEnumerable TypeLists => _types.Values; - /// - /// Sets a type list. - /// - /// For unit tests. - // internal for tests - [Obsolete("This will be removed in a future version.")] - public void AddTypeList(TypeList typeList) - { - Type tobject = typeof(object); // CompositeTypeTypeKey does not support null values - _types[new CompositeTypeTypeKey(typeList.BaseType ?? tobject, typeList.AttributeType ?? tobject)] = typeList; - } - #region Get Assembly Attributes /// @@ -136,34 +91,6 @@ public sealed class TypeLoader #region Cache - // internal for tests - [Obsolete("This will be removed in a future version.")] - public Attempt> TryGetCached(Type baseType, Type attributeType) => - Attempt>.Fail(); - - // internal for tests - [Obsolete("This will be removed in a future version.")] - public Dictionary<(string, string), IEnumerable>? ReadCache() => null; - - // internal for tests - [Obsolete("This will be removed in a future version.")] - public string? GetTypesListFilePath() => null; - - // internal for tests - [Obsolete("This will be removed in a future version.")] - public void WriteCache() - { - } - - /// - /// Clears cache. - /// - /// Generally only used for resetting cache, for example during the install process. - [Obsolete("This will be removed in a future version.")] - public void ClearTypesCache() - { - } - #endregion #region Get Types diff --git a/src/Umbraco.Core/Configuration/GlobalSettingsExtensions.cs b/src/Umbraco.Core/Configuration/GlobalSettingsExtensions.cs index 2f49bfd146..67966c9849 100644 --- a/src/Umbraco.Core/Configuration/GlobalSettingsExtensions.cs +++ b/src/Umbraco.Core/Configuration/GlobalSettingsExtensions.cs @@ -22,7 +22,7 @@ public static class GlobalSettingsExtensions return _backOfficePath; } - _backOfficePath = hostingEnvironment.ToAbsolute(globalSettings.UmbracoPath); + _backOfficePath = hostingEnvironment.ToAbsolute(Constants.System.DefaultUmbracoPath); return _backOfficePath; } @@ -54,9 +54,9 @@ public static class GlobalSettingsExtensions this GlobalSettings globalSettings, IHostingEnvironment hostingEnvironment) { - var path = string.IsNullOrEmpty(globalSettings.UmbracoPath) + var path = string.IsNullOrEmpty(Constants.System.DefaultUmbracoPath) ? string.Empty - : hostingEnvironment.ToAbsolute(globalSettings.UmbracoPath); + : hostingEnvironment.ToAbsolute(Constants.System.DefaultUmbracoPath); if (path.IsNullOrWhiteSpace()) { diff --git a/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs b/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs index 58d1bb7134..7d2556ea60 100644 --- a/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs @@ -76,16 +76,6 @@ public class GlobalSettings [DefaultValue(StaticVersionCheckPeriod)] public int VersionCheckPeriod { get; set; } = StaticVersionCheckPeriod; - /// - /// Gets or sets a value for the Umbraco back-office path. - /// - [Obsolete($"UmbracoPath is no longer configurable, use Constants.System.DefaultUmbracoPath instead. This property is scheduled for removal in a future version.")] - public string UmbracoPath - { - get => Constants.System.DefaultUmbracoPath; - set { } - } - /// /// Gets or sets a value for the Umbraco icons path. /// diff --git a/src/Umbraco.Core/Configuration/Models/NuCacheSettings.cs b/src/Umbraco.Core/Configuration/Models/NuCacheSettings.cs index 9b1fde9826..0095a8f165 100644 --- a/src/Umbraco.Core/Configuration/Models/NuCacheSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/NuCacheSettings.cs @@ -19,7 +19,7 @@ public class NuCacheSettings /// /// Gets or sets a value defining the BTree block size. /// - [Obsolete("This property is no longer used")] + [Obsolete("This property is no longer used. Scheduled for removal in v16")] public int? BTreeBlockSize { get; set; } /// diff --git a/src/Umbraco.Core/Configuration/Models/SecuritySettings.cs b/src/Umbraco.Core/Configuration/Models/SecuritySettings.cs index 2a57a0a74c..b5d81c1ab3 100644 --- a/src/Umbraco.Core/Configuration/Models/SecuritySettings.cs +++ b/src/Umbraco.Core/Configuration/Models/SecuritySettings.cs @@ -95,13 +95,6 @@ public class SecuritySettings [DefaultValue(StaticUserDefaultLockoutTimeInMinutes)] public int UserDefaultLockoutTimeInMinutes { get; set; } = StaticUserDefaultLockoutTimeInMinutes; - /// - /// Gets or sets a value indicating whether to allow editing invariant properties from a non-default language variation. - /// - [Obsolete("Use ContentSettings.AllowEditFromInvariant instead")] - [DefaultValue(StaticAllowEditInvariantFromNonDefault)] - public bool AllowEditInvariantFromNonDefault { get; set; } = StaticAllowEditInvariantFromNonDefault; - /// /// Gets or sets a value indicating whether to allow concurrent logins. /// diff --git a/src/Umbraco.Core/Configuration/Models/WebRoutingSettings.cs b/src/Umbraco.Core/Configuration/Models/WebRoutingSettings.cs index 37a39a6c92..9c96f87c31 100644 --- a/src/Umbraco.Core/Configuration/Models/WebRoutingSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/WebRoutingSettings.cs @@ -60,10 +60,6 @@ public class WebRoutingSettings [DefaultValue(StaticValidateAlternativeTemplates)] public bool ValidateAlternativeTemplates { get; set; } = StaticValidateAlternativeTemplates; - [Obsolete("Use DisableFindContentByIdentifierPath instead. This will be removed in Umbraco 15." )] - [DefaultValue(StaticDisableFindContentByIdPath)] - public bool DisableFindContentByIdPath { get; set; } = StaticDisableFindContentByIdPath; - [DefaultValue(StaticDisableFindContentByIdentifierPath)] public bool DisableFindContentByIdentifierPath { get; set; } = StaticDisableFindContentByIdentifierPath; /// diff --git a/src/Umbraco.Core/Configuration/ModelsBuilderConfigExtensions.cs b/src/Umbraco.Core/Configuration/ModelsBuilderConfigExtensions.cs index 294040f414..0ade116444 100644 --- a/src/Umbraco.Core/Configuration/ModelsBuilderConfigExtensions.cs +++ b/src/Umbraco.Core/Configuration/ModelsBuilderConfigExtensions.cs @@ -1,6 +1,8 @@ +using Microsoft.Extensions.Hosting; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Exceptions; -using Umbraco.Cms.Core.Hosting; +using Umbraco.Cms.Core.Extensions; +using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment; namespace Umbraco.Extensions; @@ -8,6 +10,22 @@ public static class ModelsBuilderConfigExtensions { private static string? _modelsDirectoryAbsolute; + public static string ModelsDirectoryAbsolute( + this ModelsBuilderSettings modelsBuilderConfig, + IHostEnvironment hostEnvironment) + { + if (_modelsDirectoryAbsolute is null) + { + var modelsDirectory = modelsBuilderConfig.ModelsDirectory; + var root = hostEnvironment.MapPathContentRoot("~/"); + + _modelsDirectoryAbsolute = GetModelsDirectory(root, modelsDirectory, modelsBuilderConfig.AcceptUnsafeModelsDirectory); + } + + return _modelsDirectoryAbsolute; + } + + [Obsolete("Use the non obsoleted equivalent instead. Scheduled for removal in v16")] public static string ModelsDirectoryAbsolute( this ModelsBuilderSettings modelsBuilderConfig, IHostingEnvironment hostingEnvironment) diff --git a/src/Umbraco.Core/Constants-Configuration.cs b/src/Umbraco.Core/Constants-Configuration.cs index a569110f01..60b3397eeb 100644 --- a/src/Umbraco.Core/Constants-Configuration.cs +++ b/src/Umbraco.Core/Constants-Configuration.cs @@ -66,7 +66,6 @@ public static partial class Constants public const string ConfigPackageManifests = ConfigPrefix + "PackageManifests"; public const string ConfigWebhook = ConfigPrefix + "Webhook"; public const string ConfigCache = ConfigPrefix + "Cache"; - public const string ConfigCacheEntry = ConfigCache + ":Entry"; public static class NamedOptions { @@ -80,13 +79,6 @@ public static partial class Constants public const string MemberTypes = "MemberTypes"; } - - public static class CacheEntry - { - public const string Document = "Document"; - - public const string Media = "Media"; - } } } } diff --git a/src/Umbraco.Core/Constants-Security.cs b/src/Umbraco.Core/Constants-Security.cs index 2f24fa182d..df6f26ca1a 100644 --- a/src/Umbraco.Core/Constants-Security.cs +++ b/src/Umbraco.Core/Constants-Security.cs @@ -49,27 +49,35 @@ public static partial class Constants /// /// The key of the admin group /// - public static readonly Guid AdminGroupKey = new("E5E7F6C8-7F9C-4B5B-8D5D-9E1E5A4F7E4D"); + public static readonly Guid AdminGroupKey = new(AdminGroupKeyString); + internal const string AdminGroupKeyString = "E5E7F6C8-7F9C-4B5B-8D5D-9E1E5A4F7E4D"; + /// /// The key of the editor group /// - public static readonly Guid EditorGroupKey = new("44DC260E-B4D4-4DD9-9081-EEC5598F1641"); + public static readonly Guid EditorGroupKey = new(EditorGroupKeyString); + internal const string EditorGroupKeyString = "44DC260E-B4D4-4DD9-9081-EEC5598F1641"; + /// /// The key of the sensitive data group /// - public static readonly Guid SensitiveDataGroupKey = new("8C6AD70F-D307-4E4A-AF58-72C2E4E9439D"); + public static readonly Guid SensitiveDataGroupKey = new(SensitiveDataGroupKeyString); + internal const string SensitiveDataGroupKeyString = "8C6AD70F-D307-4E4A-AF58-72C2E4E9439D"; /// /// The key of the translator group /// - public static readonly Guid TranslatorGroupKey = new("F2012E4C-D232-4BD1-8EAE-4384032D97D8"); + public static readonly Guid TranslatorGroupKey = new(TranslatorGroupString); + internal const string TranslatorGroupString = "F2012E4C-D232-4BD1-8EAE-4384032D97D8"; /// /// The key of the writer group /// - public static readonly Guid WriterGroupKey = new("9FC2A16F-528C-46D6-A014-75BF4EC2480C"); + public static readonly Guid WriterGroupKey = new(WriterGroupKeyString); + internal const string WriterGroupKeyString = "9FC2A16F-528C-46D6-A014-75BF4EC2480C"; + public const string BackOfficeAuthenticationType = "UmbracoBackOffice"; public const string BackOfficeExternalAuthenticationType = "UmbracoExternalCookie"; diff --git a/src/Umbraco.Core/Constants-UdiEntityType.cs b/src/Umbraco.Core/Constants-UdiEntityType.cs index 81e64b534c..9c2a25b314 100644 --- a/src/Umbraco.Core/Constants-UdiEntityType.cs +++ b/src/Umbraco.Core/Constants-UdiEntityType.cs @@ -17,48 +17,42 @@ public static partial class Constants // need to keep it around in a field nor to make it readonly public const string Unknown = "unknown"; - // guid entity types + // GUID entity types public const string AnyGuid = "any-guid"; // that one is for tests - - public const string Element = "element"; - public const string Document = "document"; - - public const string DocumentBlueprint = "document-blueprint"; - - public const string Media = "media"; - public const string Member = "member"; - - public const string DictionaryItem = "dictionary-item"; - public const string Template = "template"; - - public const string DocumentType = "document-type"; - public const string DocumentTypeContainer = "document-type-container"; - - public const string DocumentBlueprintContainer = "document-blueprint-container"; - public const string MediaType = "media-type"; - public const string MediaTypeContainer = "media-type-container"; public const string DataType = "data-type"; public const string DataTypeContainer = "data-type-container"; - public const string MemberType = "member-type"; + public const string DictionaryItem = "dictionary-item"; + public const string Document = "document"; + public const string DocumentBlueprint = "document-blueprint"; + public const string DocumentBlueprintContainer = "document-blueprint-container"; + public const string DocumentType = "document-type"; + public const string DocumentTypeContainer = "document-type-container"; + public const string Element = "element"; + public const string Media = "media"; + public const string MediaType = "media-type"; + public const string MediaTypeContainer = "media-type-container"; + public const string Member = "member"; public const string MemberGroup = "member-group"; - + public const string MemberType = "member-type"; + public const string Relation = "relation"; public const string RelationType = "relation-type"; - + public const string Template = "template"; + public const string User = "user"; + public const string UserGroup = "user-group"; public const string Webhook = "webhook"; - // forms - public const string FormsForm = "forms-form"; - public const string FormsPreValue = "forms-prevalue"; - public const string FormsDataSource = "forms-datasource"; - - // string entity types + // String entity types public const string AnyString = "any-string"; // that one is for tests - public const string Language = "language"; public const string MediaFile = "media-file"; - public const string TemplateFile = "template-file"; + public const string PartialView = "partial-view"; public const string Script = "script"; public const string Stylesheet = "stylesheet"; - public const string PartialView = "partial-view"; + public const string TemplateFile = "template-file"; + + // Forms entity types + public const string FormsDataSource = "forms-datasource"; + public const string FormsForm = "forms-form"; + public const string FormsPreValue = "forms-prevalue"; } } diff --git a/src/Umbraco.Core/DeliveryApi/ApiPublishedContentCache.cs b/src/Umbraco.Core/DeliveryApi/ApiPublishedContentCache.cs index 1c153b0143..5ee6f8d379 100644 --- a/src/Umbraco.Core/DeliveryApi/ApiPublishedContentCache.cs +++ b/src/Umbraco.Core/DeliveryApi/ApiPublishedContentCache.cs @@ -80,9 +80,18 @@ public sealed class ApiPublishedContentCache : IApiPublishedContentCache } } + var requestCulture = _requestCultureService.GetRequestedCulture(); + + if (requestCulture?.Trim().Length <= 0) + { + // documentUrlService does not like empty strings + // todo: align culture null vs empty string behaviour across the codebase + requestCulture = null; + } + Guid? documentKey = _documentUrlService.GetDocumentKeyByRoute( route, - _requestCultureService.GetRequestedCulture(), + requestCulture, documentStartNodeId, _requestPreviewService.IsPreview() ); diff --git a/src/Umbraco.Core/DeliveryApi/IApiRichTextElementParser.cs b/src/Umbraco.Core/DeliveryApi/IApiRichTextElementParser.cs index 50b9b5d581..4b1b1e92d3 100644 --- a/src/Umbraco.Core/DeliveryApi/IApiRichTextElementParser.cs +++ b/src/Umbraco.Core/DeliveryApi/IApiRichTextElementParser.cs @@ -5,9 +5,6 @@ namespace Umbraco.Cms.Core.DeliveryApi; public interface IApiRichTextElementParser { - // NOTE: remember to also remove the default implementation of the method overload when this one is removed. - [Obsolete($"Please use the overload that accepts {nameof(RichTextBlockModel)}. Will be removed in V15.")] - IRichTextElement? Parse(string html); - IRichTextElement? Parse(string html, RichTextBlockModel? richTextBlockModel) => null; + IRichTextElement? Parse(string html, RichTextBlockModel? richTextBlockModel); } diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs index 83f046dfb1..f5aad4d0fb 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs @@ -104,26 +104,6 @@ public static partial class UmbracoBuilderExtensions builder.Services.Configure( Constants.Configuration.NamedOptions.InstallDefaultData.MemberTypes, builder.Config.GetSection($"{Constants.Configuration.ConfigInstallDefaultData}:{Constants.Configuration.NamedOptions.InstallDefaultData.MemberTypes}")); - builder.Services.Configure( - Constants.Configuration.NamedOptions.CacheEntry.Media, - builder.Config.GetSection($"{Constants.Configuration.ConfigCacheEntry}:{Constants.Configuration.NamedOptions.CacheEntry.Media}")); - builder.Services.Configure( - Constants.Configuration.NamedOptions.CacheEntry.Document, - builder.Config.GetSection($"{Constants.Configuration.ConfigCacheEntry}:{Constants.Configuration.NamedOptions.CacheEntry.Document}")); - - // TODO: Remove this in V12 - // This is to make the move of the AllowEditInvariantFromNonDefault setting from SecuritySettings to ContentSettings backwards compatible - // If there is a value in security settings, but no value in content setting we'll use that value, otherwise content settings always wins. - builder.Services.Configure(settings => - { - var securitySettingsValue = builder.Config.GetSection($"{Constants.Configuration.ConfigSecurity}").GetValue(nameof(SecuritySettings.AllowEditInvariantFromNonDefault)); - var contentSettingsValue = builder.Config.GetSection($"{Constants.Configuration.ConfigContent}").GetValue(nameof(ContentSettings.AllowEditInvariantFromNonDefault)); - - if (securitySettingsValue is not null && contentSettingsValue is null) - { - settings.AllowEditInvariantFromNonDefault = securitySettingsValue.Value; - } - }); return builder; } diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs index 9fb195481f..5e06ea5042 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs @@ -63,6 +63,7 @@ namespace Umbraco.Cms.Core.DependencyInjection public ILoggerFactory BuilderLoggerFactory { get; } /// + [Obsolete("Only here to comply with obsolete implementation. Scheduled for removal in v16")] public IHostingEnvironment? BuilderHostingEnvironment { get; } public IProfiler Profiler { get; } @@ -79,6 +80,7 @@ namespace Umbraco.Cms.Core.DependencyInjection /// /// Initializes a new instance of the class. /// + [Obsolete("Use a non obsolete constructor instead. Scheduled for removal in v16")] public UmbracoBuilder( IServiceCollection services, IConfiguration config, @@ -99,6 +101,27 @@ namespace Umbraco.Cms.Core.DependencyInjection AddCoreServices(); } + /// + /// Initializes a new instance of the class. + /// + public UmbracoBuilder( + IServiceCollection services, + IConfiguration config, + TypeLoader typeLoader, + ILoggerFactory loggerFactory, + IProfiler profiler, + AppCaches appCaches) + { + Services = services; + Config = config; + BuilderLoggerFactory = loggerFactory; + Profiler = profiler; + AppCaches = appCaches; + TypeLoader = typeLoader; + + AddCoreServices(); + } + /// /// Gets a collection builder (and registers the collection). /// diff --git a/src/Umbraco.Core/Deploy/IDataTypeConfigurationConnector.cs b/src/Umbraco.Core/Deploy/IDataTypeConfigurationConnector.cs index 052d3df388..bfaf32196e 100644 --- a/src/Umbraco.Core/Deploy/IDataTypeConfigurationConnector.cs +++ b/src/Umbraco.Core/Deploy/IDataTypeConfigurationConnector.cs @@ -19,18 +19,6 @@ public interface IDataTypeConfigurationConnector /// IEnumerable PropertyEditorAliases { get; } - /// - /// Gets the artifact configuration value corresponding to a data type configuration and gather dependencies. - /// - /// The data type. - /// The dependencies. - /// The context cache. - /// - /// The artifact configuration value. - /// - [Obsolete("Use ToArtifactAsync() instead. This method will be removed in a future version.")] - string? ToArtifact(IDataType dataType, ICollection dependencies, IContextCache contextCache); - /// /// Gets the artifact configuration value corresponding to a data type configuration and gather dependencies. /// @@ -41,22 +29,11 @@ public interface IDataTypeConfigurationConnector /// /// A task that represents the asynchronous operation. The task result contains the artifact configuration value. /// - Task ToArtifactAsync(IDataType dataType, ICollection dependencies, IContextCache contextCache, CancellationToken cancellationToken = default) -#pragma warning disable CS0618 // Type or member is obsolete - => Task.FromResult(ToArtifact(dataType, dependencies, contextCache)); // TODO: Remove default implementation in v15 -#pragma warning restore CS0618 // Type or member is obsolete - - /// - /// Gets the data type configuration corresponding to an artifact configuration value. - /// - /// The data type. - /// The artifact configuration value. - /// The context cache. - /// - /// The data type configuration. - /// - [Obsolete("Use FromArtifactAsync() instead. This method will be removed in a future version.")] - IDictionary FromArtifact(IDataType dataType, string? configuration, IContextCache contextCache); + Task ToArtifactAsync( + IDataType dataType, + ICollection dependencies, + IContextCache contextCache, + CancellationToken cancellationToken = default); /// /// Gets the data type configuration corresponding to an artifact configuration value. @@ -68,8 +45,9 @@ public interface IDataTypeConfigurationConnector /// /// A task that represents the asynchronous operation. The task result contains the data type configuration. /// - Task> FromArtifactAsync(IDataType dataType, string? configuration, IContextCache contextCache, CancellationToken cancellationToken = default) -#pragma warning disable CS0618 // Type or member is obsolete - => Task.FromResult(FromArtifact(dataType, configuration, contextCache)); // TODO: Remove default implementation in v15 -#pragma warning restore CS0618 // Type or member is obsolete + Task> FromArtifactAsync( + IDataType dataType, + string? configuration, + IContextCache contextCache, + CancellationToken cancellationToken = default); } diff --git a/src/Umbraco.Core/Deploy/IFileSource.cs b/src/Umbraco.Core/Deploy/IFileSource.cs index 53db3e26e6..230140c9bf 100644 --- a/src/Umbraco.Core/Deploy/IFileSource.cs +++ b/src/Umbraco.Core/Deploy/IFileSource.cs @@ -6,19 +6,6 @@ namespace Umbraco.Cms.Core.Deploy; /// public interface IFileSource { - /// - /// Gets the content of a file as a stream. - /// - /// A file entity identifier. - /// - /// A stream with read access to the file content. - /// - /// - /// Returns null if no content could be read. - /// The caller should ensure that the stream is properly closed/disposed. - /// - [Obsolete("Use GetFileStreamAsync() instead. This method will be removed in a future version.")] - Stream GetFileStream(StringUdi udi); /// /// Gets the content of a file as a stream. @@ -34,19 +21,6 @@ public interface IFileSource /// Task GetFileStreamAsync(StringUdi udi, CancellationToken token); - /// - /// Gets the content of a file as a string. - /// - /// A file entity identifier. - /// - /// A string containing the file content. - /// - /// - /// Returns null if no content could be read. - /// - [Obsolete("Use GetFileContentAsync() instead. This method will be removed in a future version.")] - string GetFileContent(StringUdi udi); - /// /// Gets the content of a file as a string. /// @@ -60,16 +34,6 @@ public interface IFileSource /// Task GetFileContentAsync(StringUdi udi, CancellationToken token); - /// - /// Gets the length of a file. - /// - /// A file entity identifier. - /// - /// The length of the file, or -1 if the file does not exist. - /// - [Obsolete("Use GetFileLengthAsync() instead. This method will be removed in a future version.")] - long GetFileLength(StringUdi udi); - /// /// Gets the length of a file. /// @@ -80,15 +44,6 @@ public interface IFileSource /// Task GetFileLengthAsync(StringUdi udi, CancellationToken token); - /// - /// Gets files and store them using a file store. - /// - /// The UDIs of the files to get. - /// A flag indicating whether to continue if a file isn't found or to stop and throw a FileNotFoundException. - /// A collection of file types which can store the files. - [Obsolete("Use GetFilesAsync() instead. This method will be removed in a future version.")] - void GetFiles(IEnumerable udis, bool continueOnFileNotFound, IFileTypeCollection fileTypes); - /// /// Gets files and store them using a file store. /// diff --git a/src/Umbraco.Core/Deploy/IFileType.cs b/src/Umbraco.Core/Deploy/IFileType.cs index a135481eb4..f8fa5ba92d 100644 --- a/src/Umbraco.Core/Deploy/IFileType.cs +++ b/src/Umbraco.Core/Deploy/IFileType.cs @@ -13,16 +13,6 @@ public interface IFileType /// bool CanSetPhysical { get; } - /// - /// Gets the stream. - /// - /// The UDI. - /// - /// The stream. - /// - [Obsolete("Use GetStreamAsync() instead. This method will be removed in a future version.")] - Stream GetStream(StringUdi udi); - /// /// Gets the stream as an asynchronous operation. /// @@ -51,14 +41,6 @@ public interface IFileType /// long GetLength(StringUdi udi); - /// - /// Sets the stream. - /// - /// The UDI. - /// The stream. - [Obsolete("Use SetStreamAsync() instead. This method will be removed in a future version.")] - void SetStream(StringUdi udi, Stream stream); - /// /// Sets the stream as an asynchronous operation. /// diff --git a/src/Umbraco.Core/Deploy/IImageSourceParser.cs b/src/Umbraco.Core/Deploy/IImageSourceParser.cs index 3817dc6fb8..dd4124e889 100644 --- a/src/Umbraco.Core/Deploy/IImageSourceParser.cs +++ b/src/Umbraco.Core/Deploy/IImageSourceParser.cs @@ -5,21 +5,6 @@ namespace Umbraco.Cms.Core.Deploy; /// public interface IImageSourceParser { - /// - /// Parses an Umbraco property value and produces an artifact property value. - /// - /// The property value. - /// A list of dependencies. - /// The context cache. - /// - /// The parsed value. - /// - /// - /// Turns src="/media/..." into src="umb://media/..." and adds the corresponding udi to the dependencies. - /// - [Obsolete("Use ToArtifactAsync() instead. This method will be removed in a future version.")] - string ToArtifact(string value, ICollection dependencies, IContextCache contextCache); - /// /// Parses an Umbraco property value and produces an artifact property value. /// @@ -33,24 +18,7 @@ public interface IImageSourceParser /// /// Turns src="/media/..." into src="umb://media/..." and adds the corresponding udi to the dependencies. /// - Task ToArtifactAsync(string value, ICollection dependencies, IContextCache contextCache, CancellationToken cancellationToken = default) -#pragma warning disable CS0618 // Type or member is obsolete - => Task.FromResult(ToArtifact(value, dependencies, contextCache)); // TODO: Remove default implementation in v15 -#pragma warning restore CS0618 // Type or member is obsolete - - /// - /// Parses an artifact property value and produces an Umbraco property value. - /// - /// The artifact property value. - /// The context cache. - /// - /// The parsed value. - /// - /// - /// Turns umb://media/... into /media/.... - /// - [Obsolete("Use FromArtifactAsync() instead. This method will be removed in a future version.")] - string FromArtifact(string value, IContextCache contextCache); + Task ToArtifactAsync(string value, ICollection dependencies, IContextCache contextCache, CancellationToken cancellationToken = default); /// /// Parses an artifact property value and produces an Umbraco property value. @@ -64,8 +32,5 @@ public interface IImageSourceParser /// /// Turns umb://media/... into /media/.... /// - Task FromArtifactAsync(string value, IContextCache contextCache, CancellationToken cancellationToken = default) -#pragma warning disable CS0618 // Type or member is obsolete - => Task.FromResult(FromArtifact(value, contextCache)); // TODO: Remove default implementation in v15 -#pragma warning restore CS0618 // Type or member is obsolete + Task FromArtifactAsync(string value, IContextCache contextCache, CancellationToken cancellationToken = default); } diff --git a/src/Umbraco.Core/Deploy/ILocalLinkParser.cs b/src/Umbraco.Core/Deploy/ILocalLinkParser.cs index fcd3405a3c..357b0ecda6 100644 --- a/src/Umbraco.Core/Deploy/ILocalLinkParser.cs +++ b/src/Umbraco.Core/Deploy/ILocalLinkParser.cs @@ -5,21 +5,6 @@ namespace Umbraco.Cms.Core.Deploy; /// public interface ILocalLinkParser { - /// - /// Parses an Umbraco property value and produces an artifact property value. - /// - /// The property value. - /// A list of dependencies. - /// The context cache. - /// - /// The parsed value. - /// - /// - /// Turns {{localLink:1234}} into {{localLink:umb://{type}/{id}}} and adds the corresponding udi to the dependencies. - /// - [Obsolete("Use ToArtifactAsync() instead. This method will be removed in a future version.")] - string ToArtifact(string value, ICollection dependencies, IContextCache contextCache); - /// /// Parses an Umbraco property value and produces an artifact property value. /// @@ -33,24 +18,7 @@ public interface ILocalLinkParser /// /// Turns {{localLink:1234}} into {{localLink:umb://{type}/{id}}} and adds the corresponding udi to the dependencies. /// - Task ToArtifactAsync(string value, ICollection dependencies, IContextCache contextCache, CancellationToken cancellationToken = default) -#pragma warning disable CS0618 // Type or member is obsolete - => Task.FromResult(ToArtifact(value, dependencies, contextCache)); // TODO: Remove default implementation in v15 -#pragma warning restore CS0618 // Type or member is obsolete - - /// - /// Parses an artifact property value and produces an Umbraco property value. - /// - /// The artifact property value. - /// The context cache. - /// - /// The parsed value. - /// - /// - /// Turns {{localLink:umb://{type}/{id}}} into {{localLink:1234}}. - /// - [Obsolete("Use FromArtifactAsync() instead. This method will be removed in a future version.")] - string FromArtifact(string value, IContextCache contextCache); + Task ToArtifactAsync(string value, ICollection dependencies, IContextCache contextCache, CancellationToken cancellationToken = default); /// /// Parses an artifact property value and produces an Umbraco property value. @@ -64,8 +32,5 @@ public interface ILocalLinkParser /// /// Turns {{localLink:umb://{type}/{id}}} into {{localLink:1234}}. /// - Task FromArtifactAsync(string value, IContextCache contextCache, CancellationToken cancellationToken = default) -#pragma warning disable CS0618 // Type or member is obsolete - => Task.FromResult(FromArtifact(value, contextCache)); // TODO: Remove default implementation in v15 -#pragma warning restore CS0618 // Type or member is obsolete + Task FromArtifactAsync(string value, IContextCache contextCache, CancellationToken cancellationToken = default); } diff --git a/src/Umbraco.Core/Deploy/IServiceConnector.cs b/src/Umbraco.Core/Deploy/IServiceConnector.cs index 79666f2fa7..4f8b2bfe5d 100644 --- a/src/Umbraco.Core/Deploy/IServiceConnector.cs +++ b/src/Umbraco.Core/Deploy/IServiceConnector.cs @@ -8,17 +8,6 @@ namespace Umbraco.Cms.Core.Deploy; /// public interface IServiceConnector : IDiscoverable { - /// - /// Gets an artifact. - /// - /// The entity identifier of the artifact. - /// The context cache. - /// - /// The corresponding artifact or null. - /// - [Obsolete("Use GetArtifactAsync() instead. This method will be removed in a future version.")] - IArtifact? GetArtifact(Udi udi, IContextCache contextCache); - /// /// Gets an artifact. /// @@ -28,21 +17,7 @@ public interface IServiceConnector : IDiscoverable /// /// A task that represents the asynchronous operation. The task result contains the corresponding artifact or null. /// - Task GetArtifactAsync(Udi udi, IContextCache contextCache, CancellationToken cancellationToken = default) -#pragma warning disable CS0618 // Type or member is obsolete - => Task.FromResult(GetArtifact(udi, contextCache)); // TODO: Remove default implementation in v15 -#pragma warning restore CS0618 // Type or member is obsolete - - /// - /// Gets an artifact. - /// - /// The entity. - /// The context cache. - /// - /// The corresponding artifact. - /// - [Obsolete("Use GetArtifactAsync() instead. This method will be removed in a future version.")] - IArtifact GetArtifact(object entity, IContextCache contextCache); + Task GetArtifactAsync(Udi udi, IContextCache contextCache, CancellationToken cancellationToken = default); /// /// Gets an artifact. @@ -53,21 +28,7 @@ public interface IServiceConnector : IDiscoverable /// /// A task that represents the asynchronous operation. The task result contains the corresponding artifact. /// - Task GetArtifactAsync(object entity, IContextCache contextCache, CancellationToken cancellationToken = default) -#pragma warning disable CS0618 // Type or member is obsolete - => Task.FromResult(GetArtifact(entity, contextCache)); // TODO: Remove default implementation in v15 -#pragma warning restore CS0618 // Type or member is obsolete - - /// - /// Initializes processing for an artifact. - /// - /// The artifact. - /// The deploy context. - /// - /// The state of an artifact being deployed. - /// - [Obsolete("Use ProcessInitAsync() instead. This method will be removed in a future version.")] - ArtifactDeployState ProcessInit(IArtifact art, IDeployContext context); + Task GetArtifactAsync(object entity, IContextCache contextCache, CancellationToken cancellationToken = default); /// /// Initializes processing for an artifact. @@ -78,19 +39,7 @@ public interface IServiceConnector : IDiscoverable /// /// A task that represents the asynchronous operation. The task result contains the state of an artifact being deployed. /// - Task ProcessInitAsync(IArtifact artifact, IDeployContext context, CancellationToken cancellationToken = default) -#pragma warning disable CS0618 // Type or member is obsolete - => Task.FromResult(ProcessInit(artifact, context)); // TODO: Remove default implementation in v15 -#pragma warning restore CS0618 // Type or member is obsolete - - /// - /// Processes an artifact. - /// - /// The state of the artifact being deployed. - /// The deploy context. - /// The processing pass number. - [Obsolete("Use ProcessAsync() instead. This method will be removed in a future version.")] - void Process(ArtifactDeployState dart, IDeployContext context, int pass); + Task ProcessInitAsync(IArtifact artifact, IDeployContext context, CancellationToken cancellationToken = default); /// /// Processes an artifact. @@ -102,23 +51,7 @@ public interface IServiceConnector : IDiscoverable /// /// A task that represents the asynchronous operation. /// - Task ProcessAsync(ArtifactDeployState state, IDeployContext context, int pass, CancellationToken cancellationToken = default) - { - // TODO: Remove default implementation in v15 -#pragma warning disable CS0618 // Type or member is obsolete - Process(state, context, pass); -#pragma warning restore CS0618 // Type or member is obsolete - - return Task.CompletedTask; - } - - /// - /// Explodes/expands an UDI range into UDIs. - /// - /// The UDI range. - /// The list of UDIs where to add the exploded/expanded UDIs. - [Obsolete("Use ExpandRangeAsync() instead. This method will be removed in a future version.")] - void Explode(UdiRange range, List udis); + Task ProcessAsync(ArtifactDeployState state, IDeployContext context, int pass, CancellationToken cancellationToken = default); /// /// Expands an UDI range into UDIs. @@ -128,30 +61,7 @@ public interface IServiceConnector : IDiscoverable /// /// Returns an which when enumerated will asynchronously expand the UDI range into UDIs. /// - async IAsyncEnumerable ExpandRangeAsync(UdiRange range, [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - // TODO: Remove default implementation in v15 - var udis = new List(); -#pragma warning disable CS0618 // Type or member is obsolete - Explode(range, udis); -#pragma warning restore CS0618 // Type or member is obsolete - - foreach (Udi udi in udis) - { - yield return await ValueTask.FromResult(udi); - } - } - - /// - /// Gets a named range for a specified UDI and selector. - /// - /// The UDI. - /// The selector. - /// - /// The named range for the specified UDI and selector. - /// - [Obsolete("Use GetRangeAsync() instead. This method will be removed in a future version.")] - NamedUdiRange GetRange(Udi udi, string selector); + IAsyncEnumerable ExpandRangeAsync(UdiRange range, CancellationToken cancellationToken = default); /// /// Gets a named range for a specified UDI and selector. @@ -162,31 +72,7 @@ public interface IServiceConnector : IDiscoverable /// /// A task that represents the asynchronous operation. The task result contains the named range for the specified UDI and selector. /// - Task GetRangeAsync(Udi udi, string selector, CancellationToken cancellationToken = default) -#pragma warning disable CS0618 // Type or member is obsolete - => Task.FromResult(GetRange(udi, selector)); // TODO: Remove default implementation in v15 -#pragma warning restore CS0618 // Type or member is obsolete - - /// - /// Gets a named range for specified entity type, identifier and selector. - /// - /// The entity type. - /// The identifier. - /// The selector. - /// - /// The named range for the specified entity type, identifier and selector. - /// - /// - /// This is temporary. At least we thought it would be, in sept. 2016. What day is it now? - /// - /// At the moment our UI has a hard time returning proper UDIs, mainly because Core's tree do - /// not manage GUIDs but only integers... so we have to provide a way to support it. The string id here - /// can be either a real string (for string UDIs) or an "integer as a string", using the value "-1" to - /// indicate the "root" i.e. an open UDI. - /// - /// - [Obsolete("Use GetRangeAsync() instead. This method will be removed in a future version.")] - NamedUdiRange GetRange(string entityType, string sid, string selector); + Task GetRangeAsync(Udi udi, string selector, CancellationToken cancellationToken = default); /// /// Gets a named range for specified entity type, identifier and selector. @@ -207,10 +93,7 @@ public interface IServiceConnector : IDiscoverable /// indicate the "root" i.e. an open UDI. /// /// - Task GetRangeAsync(string entityType, string sid, string selector, CancellationToken cancellationToken = default) -#pragma warning disable CS0618 // Type or member is obsolete - => Task.FromResult(GetRange(entityType, sid, selector)); // TODO: Remove default implementation in v15 -#pragma warning restore CS0618 // Type or member is obsolete + Task GetRangeAsync(string entityType, string sid, string selector, CancellationToken cancellationToken = default); /// /// Compares two artifacts. diff --git a/src/Umbraco.Core/Deploy/IValueConnector.cs b/src/Umbraco.Core/Deploy/IValueConnector.cs index 06d37e792e..663d3bd471 100644 --- a/src/Umbraco.Core/Deploy/IValueConnector.cs +++ b/src/Umbraco.Core/Deploy/IValueConnector.cs @@ -20,19 +20,6 @@ public interface IValueConnector /// IEnumerable PropertyEditorAliases { get; } - /// - /// Gets the deploy property value corresponding to a content property value, and gather dependencies. - /// - /// The content property value. - /// The value property type - /// The content dependencies. - /// The context cache. - /// - /// The deploy property value. - /// - [Obsolete("Use ToArtifactAsync() instead. This method will be removed in a future version.")] - string? ToArtifact(object? value, IPropertyType propertyType, ICollection dependencies, IContextCache contextCache); - /// /// Gets the deploy property value corresponding to a content property value, and gather dependencies. /// @@ -44,23 +31,11 @@ public interface IValueConnector /// /// A task that represents the asynchronous operation. The task result contains the deploy property value. /// - Task ToArtifactAsync(object? value, IPropertyType propertyType, ICollection dependencies, IContextCache contextCache, CancellationToken cancellationToken = default) -#pragma warning disable CS0618 // Type or member is obsolete - => Task.FromResult(ToArtifact(value, propertyType, dependencies, contextCache)); // TODO: Remove default implementation in v15 -#pragma warning restore CS0618 // Type or member is obsolete - - /// - /// Gets the content property value corresponding to a deploy property value. - /// - /// The deploy property value. - /// The value property type - /// The current content property value. - /// The context cache. - /// - /// The content property value. - /// - [Obsolete("Use FromArtifactAsync() instead. This method will be removed in a future version.")] - object? FromArtifact(string? value, IPropertyType propertyType, object? currentValue, IContextCache contextCache); + Task ToArtifactAsync(object? value, + IPropertyType propertyType, + ICollection dependencies, + IContextCache contextCache, + CancellationToken cancellationToken = default); /// /// Gets the content property value corresponding to a deploy property value. @@ -73,8 +48,10 @@ public interface IValueConnector /// /// A task that represents the asynchronous operation. The task result contains the content property value. /// - Task FromArtifactAsync(string? value, IPropertyType propertyType, object? currentValue, IContextCache contextCache, CancellationToken cancellationToken = default) -#pragma warning disable CS0618 // Type or member is obsolete - => Task.FromResult(FromArtifact(value, propertyType, currentValue, contextCache)); // TODO: Remove default implementation in v15 -#pragma warning restore CS0618 // Type or member is obsolete + Task FromArtifactAsync( + string? value, + IPropertyType propertyType, + object? currentValue, + IContextCache contextCache, + CancellationToken cancellationToken = default); } diff --git a/src/Umbraco.Core/EmbeddedResources/BlockGrid/items.cshtml b/src/Umbraco.Core/EmbeddedResources/BlockGrid/items.cshtml index f00e2481ac..58f4b4fcd6 100644 --- a/src/Umbraco.Core/EmbeddedResources/BlockGrid/items.cshtml +++ b/src/Umbraco.Core/EmbeddedResources/BlockGrid/items.cshtml @@ -11,7 +11,7 @@ class="umb-block-grid__layout-item" data-content-element-type-alias="@item.Content.ContentType.Alias" data-content-element-type-key="@item.Content.ContentType.Key" - data-element-udi="@item.ContentUdi" + data-element-key="@item.ContentKey" data-col-span="@item.ColumnSpan" data-row-span="@item.RowSpan" style=" --umb-block-grid--item-column-span: @item.ColumnSpan; --umb-block-grid--item-row-span: @item.RowSpan; "> diff --git a/src/Umbraco.Core/Events/UserNotificationsHandler.cs b/src/Umbraco.Core/Events/UserNotificationsHandler.cs index ebc7840fa1..6356046963 100644 --- a/src/Umbraco.Core/Events/UserNotificationsHandler.cs +++ b/src/Umbraco.Core/Events/UserNotificationsHandler.cs @@ -25,7 +25,6 @@ public sealed class UserNotificationsHandler : INotificationHandler, INotificationHandler, INotificationHandler, - INotificationHandler, INotificationHandler, INotificationHandler, INotificationHandler @@ -107,10 +106,6 @@ public sealed class UserNotificationsHandler : _notifier.Notify(_actions.GetAction(), updatedEntities.ToArray()); } - [Obsolete("Scheduled for removal in v13")] - public void Handle(ContentSentToPublishNotification notification) => - _notifier.Notify(_actions.GetAction(), notification.Entity); - public void Handle(ContentSortedNotification notification) { var parentId = notification.SortedEntities.Select(x => x.ParentId).Distinct().ToList(); diff --git a/src/Umbraco.Core/Extensions/PublishedContentExtensions.cs b/src/Umbraco.Core/Extensions/PublishedContentExtensions.cs index 2dd971114d..4ce45e841a 100644 --- a/src/Umbraco.Core/Extensions/PublishedContentExtensions.cs +++ b/src/Umbraco.Core/Extensions/PublishedContentExtensions.cs @@ -560,8 +560,12 @@ public static class PublishedContentExtensions this IPublishedContent content, IPublishedCache publishedCache, INavigationQueryService navigationQueryService, - string contentTypeAlias) => - content.AncestorsOrSelf(publishedCache, navigationQueryService, false, n => n.ContentType.Alias.InvariantEquals(contentTypeAlias)); + string contentTypeAlias) + { + ArgumentNullException.ThrowIfNull(content); + + return content.EnumerateAncestorsOrSelfInternal(publishedCache, navigationQueryService, false, contentTypeAlias); + } /// /// Gets the ancestors of the content, of a specified content type. @@ -652,8 +656,12 @@ public static class PublishedContentExtensions this IPublishedContent content, IPublishedCache publishedCache, INavigationQueryService navigationQueryService, - string contentTypeAlias) => - content.AncestorsOrSelf(publishedCache, navigationQueryService, true, n => n.ContentType.Alias.InvariantEquals(contentTypeAlias)); + string contentTypeAlias) + { + ArgumentNullException.ThrowIfNull(content); + + return content.EnumerateAncestorsOrSelfInternal(publishedCache, navigationQueryService, true, contentTypeAlias); + } /// /// Gets the content and its ancestors, of a specified content type. @@ -736,8 +744,14 @@ public static class PublishedContentExtensions this IPublishedContent content, IPublishedCache publishedCache, INavigationQueryService navigationQueryService, - string contentTypeAlias) => - content.EnumerateAncestors(publishedCache, navigationQueryService, false).FirstOrDefault(x => x.ContentType.Alias.InvariantEquals(contentTypeAlias)); + string contentTypeAlias) + { + ArgumentNullException.ThrowIfNull(content); + + return content + .EnumerateAncestorsOrSelfInternal(publishedCache, navigationQueryService, false, contentTypeAlias) + .FirstOrDefault(); + } /// /// Gets the nearest ancestor of the content, of a specified content type. @@ -813,8 +827,14 @@ public static class PublishedContentExtensions this IPublishedContent content, IPublishedCache publishedCache, INavigationQueryService navigationQueryService, - string contentTypeAlias) => content - .EnumerateAncestors(publishedCache, navigationQueryService, true).FirstOrDefault(x => x.ContentType.Alias.InvariantEquals(contentTypeAlias)) ?? content; + string contentTypeAlias) + { + ArgumentNullException.ThrowIfNull(content); + + return content + .EnumerateAncestorsOrSelfInternal(publishedCache, navigationQueryService, true, contentTypeAlias) + .FirstOrDefault() ?? content; + } /// /// Gets the content or its nearest ancestor, of a specified content type. @@ -875,20 +895,9 @@ public static class PublishedContentExtensions INavigationQueryService navigationQueryService, bool orSelf) { - if (content == null) - { - throw new ArgumentNullException(nameof(content)); - } + ArgumentNullException.ThrowIfNull(content); - if (orSelf) - { - yield return content; - } - - while ((content = content.GetParent(publishedCache, navigationQueryService)) != null) - { - yield return content; - } + return content.EnumerateAncestorsOrSelfInternal(publishedCache, navigationQueryService, orSelf); } #endregion @@ -1061,8 +1070,15 @@ public static class PublishedContentExtensions IVariationContextAccessor variationContextAccessor, IPublishedCache publishedCache, INavigationQueryService navigationQueryService, - string contentTypeAlias, string? culture = null) => - content.DescendantsOrSelf(variationContextAccessor, publishedCache, navigationQueryService, false, p => p.ContentType.Alias.InvariantEquals(contentTypeAlias), culture); + string contentTypeAlias, + string? culture = null) => + content.EnumerateDescendantsOrSelfInternal( + variationContextAccessor, + publishedCache, + navigationQueryService, + culture, + false, + contentTypeAlias); public static IEnumerable Descendants( this IPublishedContent content, @@ -1107,7 +1123,13 @@ public static class PublishedContentExtensions INavigationQueryService navigationQueryService, string contentTypeAlias, string? culture = null) => - content.DescendantsOrSelf(variationContextAccessor, publishedCache, navigationQueryService, true, p => p.ContentType.Alias.InvariantEquals(contentTypeAlias), culture); + content.EnumerateDescendantsOrSelfInternal( + variationContextAccessor, + publishedCache, + navigationQueryService, + culture, + true, + contentTypeAlias); public static IEnumerable DescendantsOrSelf( this IPublishedContent content, @@ -1152,8 +1174,14 @@ public static class PublishedContentExtensions INavigationQueryService navigationQueryService, string contentTypeAlias, string? culture = null) => content - .EnumerateDescendants(variationContextAccessor, publishedCache, navigationQueryService, false, culture) - .FirstOrDefault(x => x.ContentType.Alias.InvariantEquals(contentTypeAlias)); + .EnumerateDescendantsOrSelfInternal( + variationContextAccessor, + publishedCache, + navigationQueryService, + culture, + false, + contentTypeAlias) + .FirstOrDefault(); public static T? Descendant( this IPublishedContent content, @@ -1192,8 +1220,14 @@ public static class PublishedContentExtensions INavigationQueryService navigationQueryService, string contentTypeAlias, string? culture = null) => content - .EnumerateDescendants(variationContextAccessor, publishedCache, navigationQueryService, true, culture) - .FirstOrDefault(x => x.ContentType.Alias.InvariantEquals(contentTypeAlias)); + .EnumerateDescendantsOrSelfInternal( + variationContextAccessor, + publishedCache, + navigationQueryService, + culture, + true, + contentTypeAlias) + .FirstOrDefault(); public static T? DescendantOrSelf( this IPublishedContent content, @@ -1233,24 +1267,16 @@ public static class PublishedContentExtensions bool orSelf, string? culture = null) { - if (content == null) - { - throw new ArgumentNullException(nameof(content)); - } + ArgumentNullException.ThrowIfNull(content); - if (orSelf) + foreach (IPublishedContent desc in content.EnumerateDescendantsOrSelfInternal( + variationContextAccessor, + publishedCache, + navigationQueryService, + culture, + orSelf)) { - yield return content; - } - - IEnumerable? children = content.Children(variationContextAccessor, publishedCache, navigationQueryService, culture); - if (children is not null) - { - foreach (IPublishedContent desc in children.SelectMany(x => - x.EnumerateDescendants(variationContextAccessor, publishedCache, navigationQueryService, culture))) - { - yield return desc; - } + yield return desc; } } @@ -1262,14 +1288,15 @@ public static class PublishedContentExtensions string? culture = null) { yield return content; - IEnumerable? children = content.Children(variationContextAccessor, publishedCache, navigationQueryService, culture); - if (children is not null) + + foreach (IPublishedContent desc in content.EnumerateDescendantsOrSelfInternal( + variationContextAccessor, + publishedCache, + navigationQueryService, + culture, + false)) { - foreach (IPublishedContent desc in children.SelectMany(x => - x.EnumerateDescendants(variationContextAccessor, publishedCache, navigationQueryService, culture))) - { - yield return desc; - } + yield return desc; } } @@ -1312,25 +1339,9 @@ public static class PublishedContentExtensions INavigationQueryService navigationQueryService, string? culture = null) { - // handle context culture for variant - if (culture is null) - { - culture = variationContextAccessor?.VariationContext?.Culture ?? string.Empty; - } + IEnumerable children = GetChildren(navigationQueryService, publishedCache, content.Key); - if (navigationQueryService.TryGetChildrenKeys(content.Key, out IEnumerable childrenKeys) is false) - { - return []; - } - - IEnumerable children = childrenKeys.Select(publishedCache.GetById).WhereNotNull(); - - if (culture == "*") - { - return children; - } - - return children.Where(x => x.IsInvariantOrHasCulture(culture)) ?? []; + return children.FilterByCulture(culture, variationContextAccessor); } /// @@ -1377,9 +1388,14 @@ public static class PublishedContentExtensions IPublishedCache publishedCache, INavigationQueryService navigationQueryService, string? contentTypeAlias, - string? culture = null) => - content.Children(variationContextAccessor, publishedCache, navigationQueryService, x => x.ContentType.Alias.InvariantEquals(contentTypeAlias), - culture); + string? culture = null) + { + IEnumerable children = contentTypeAlias is not null + ? GetChildren(navigationQueryService, publishedCache, content.Key, contentTypeAlias) + : []; + + return children.FilterByCulture(culture, variationContextAccessor); + } /// /// Gets the children of the content, of a given content type. @@ -1423,8 +1439,9 @@ public static class PublishedContentExtensions IPublishedCache publishedCache, INavigationQueryService navigationQueryService, string contentTypeAlias, - string? culture = null) => - content.ChildrenOfType(variationContextAccessor, publishedCache, navigationQueryService, contentTypeAlias, culture)?.FirstOrDefault(); + string? culture = null) => content + .ChildrenOfType(variationContextAccessor, publishedCache, navigationQueryService, contentTypeAlias, culture) + .FirstOrDefault(); public static IPublishedContent? FirstChild( this IPublishedContent content, @@ -1515,7 +1532,7 @@ public static class PublishedContentExtensions string contentTypeAlias, string? culture = null) => SiblingsAndSelfOfType(content, variationContextAccessor, publishedCache, navigationQueryService, contentTypeAlias, culture) - ?.Where(x => x.Id != content.Id) ?? Enumerable.Empty(); + .Where(x => x.Id != content.Id); /// /// Gets the siblings of the content, of a given content type. @@ -1603,25 +1620,26 @@ public static class PublishedContentExtensions string contentTypeAlias, string? culture = null) { - var parentSuccess = navigationQueryService.TryGetParentKey(content.Key, out Guid? parentKey); + var parentExists = navigationQueryService.TryGetParentKey(content.Key, out Guid? parentKey); - IPublishedContent? parent = parentKey is null ? null : publishedCache.GetById(parentKey.Value); + IPublishedContent? parent = parentKey is null + ? null + : publishedCache.GetById(parentKey.Value); - if (parentSuccess is false || parent is null) + if (parentExists && parent is not null) { - if (navigationQueryService.TryGetRootKeys(out IEnumerable childrenKeys) is false) - { - return Enumerable.Empty(); - } - - return childrenKeys - .Select(publishedCache.GetById) - .WhereNotNull() - .OfTypes(contentTypeAlias) - .WhereIsInvariantOrHasCulture(variationContextAccessor, culture); + return parent.ChildrenOfType(variationContextAccessor, publishedCache, navigationQueryService, contentTypeAlias, culture); } - return parent.ChildrenOfType(variationContextAccessor, publishedCache, navigationQueryService, contentTypeAlias, culture); + if (navigationQueryService.TryGetRootKeysOfType(contentTypeAlias, out IEnumerable rootKeysOfType) is false) + { + return []; + } + + return rootKeysOfType + .Select(publishedCache.GetById) + .WhereNotNull() + .WhereIsInvariantOrHasCulture(variationContextAccessor, culture); } /// @@ -1916,125 +1934,112 @@ public static class PublishedContentExtensions #endregion - [Obsolete("Please use IPublishedCache and IDocumentNavigationQueryService or IMediaNavigationQueryService directly. This will be removed in a future version of Umbraco")] + public static IPublishedContent? Ancestor(this IPublishedContent content, int maxLevel) { return content.Ancestor(GetPublishedCache(content), GetNavigationQueryService(content), maxLevel); } - [Obsolete("Please use IPublishedCache and IDocumentNavigationQueryService or IMediaNavigationQueryService directly. This will be removed in a future version of Umbraco")] + public static IPublishedContent? Ancestor(this IPublishedContent content, string contentTypeAlias) { return content.Ancestor(GetPublishedCache(content), GetNavigationQueryService(content), contentTypeAlias); } - [Obsolete("Please use IPublishedCache and IDocumentNavigationQueryService or IMediaNavigationQueryService directly. This will be removed in a future version of Umbraco")] + public static T? Ancestor(this IPublishedContent content, int maxLevel) where T : class, IPublishedContent { return Ancestor(content, GetPublishedCache(content), GetNavigationQueryService(content), maxLevel); } - [Obsolete("Please use IPublishedCache and IDocumentNavigationQueryService or IMediaNavigationQueryService directly. This will be removed in a future version of Umbraco")] + public static IEnumerable Ancestors(this IPublishedContent content, int maxLevel) { return content.Ancestors(GetPublishedCache(content), GetNavigationQueryService(content), maxLevel); } - [Obsolete("Please use IPublishedCache and IDocumentNavigationQueryService or IMediaNavigationQueryService directly. This will be removed in a future version of Umbraco")] + public static IEnumerable Ancestors(this IPublishedContent content, string contentTypeAlias) { return content.Ancestors(GetPublishedCache(content), GetNavigationQueryService(content), contentTypeAlias); } - [Obsolete("Please use IPublishedCache and IDocumentNavigationQueryService or IMediaNavigationQueryService directly. This will be removed in a future version of Umbraco")] + public static IEnumerable Ancestors(this IPublishedContent content) where T : class, IPublishedContent { return Ancestors(content, GetPublishedCache(content), GetNavigationQueryService(content)); } - [Obsolete("Please use IPublishedCache and IDocumentNavigationQueryService or IMediaNavigationQueryService directly. This will be removed in a future version of Umbraco")] + public static IEnumerable Ancestors(this IPublishedContent content, int maxLevel) where T : class, IPublishedContent { return Ancestors(content, GetPublishedCache(content), GetNavigationQueryService(content), maxLevel); } - [Obsolete("Please use IPublishedCache and IDocumentNavigationQueryService or IMediaNavigationQueryService directly. This will be removed in a future version of Umbraco")] public static IPublishedContent AncestorOrSelf(this IPublishedContent content, int maxLevel) { return AncestorOrSelf(content, GetPublishedCache(content), GetNavigationQueryService(content), maxLevel); } - [Obsolete("Please use IPublishedCache and IDocumentNavigationQueryService or IMediaNavigationQueryService directly. This will be removed in a future version of Umbraco")] public static IPublishedContent AncestorOrSelf(this IPublishedContent content, string contentTypeAlias) { return AncestorOrSelf(content, GetPublishedCache(content), GetNavigationQueryService(content), contentTypeAlias); } - [Obsolete("Please use IPublishedCache and IDocumentNavigationQueryService or IMediaNavigationQueryService directly. This will be removed in a future version of Umbraco")] public static T? AncestorOrSelf(this IPublishedContent content, int maxLevel) where T : class, IPublishedContent { return AncestorOrSelf(content, GetPublishedCache(content), GetNavigationQueryService(content), maxLevel); } - [Obsolete("Please use IPublishedCache and IDocumentNavigationQueryService or IMediaNavigationQueryService directly. This will be removed in a future version of Umbraco")] public static IEnumerable AncestorsOrSelf(this IPublishedContent content, int maxLevel) { return content.AncestorsOrSelf(GetPublishedCache(content), GetNavigationQueryService(content), maxLevel); } - [Obsolete("Please use IPublishedCache and IDocumentNavigationQueryService or IMediaNavigationQueryService directly. This will be removed in a future version of Umbraco")] public static IEnumerable AncestorsOrSelf(this IPublishedContent content, string contentTypeAlias) { return content.Ancestors(GetPublishedCache(content), GetNavigationQueryService(content), contentTypeAlias); } - [Obsolete("Please use IPublishedCache and IDocumentNavigationQueryService or IMediaNavigationQueryService directly. This will be removed in a future version of Umbraco")] public static IEnumerable AncestorsOrSelf(this IPublishedContent content, int maxLevel) where T : class, IPublishedContent { return AncestorsOrSelf(content, GetPublishedCache(content), GetNavigationQueryService(content), maxLevel); } - [Obsolete("Please use IPublishedCache and IDocumentNavigationQueryService or IMediaNavigationQueryService directly. This will be removed in a future version of Umbraco")] public static IEnumerable AncestorsOrSelf(this IPublishedContent content, bool orSelf, Func? func) { return AncestorsOrSelf(content, GetPublishedCache(content), GetNavigationQueryService(content), orSelf, func); } - [Obsolete( - "Please use IPublishedCache and IDocumentNavigationQueryService or IMediaNavigationQueryService directly. This will be removed in a future version of Umbraco")] public static IEnumerable Breadcrumbs( this IPublishedContent content, bool andSelf = true) => content.Breadcrumbs(GetPublishedCache(content), GetNavigationQueryService(content), andSelf); - [Obsolete("Please use IPublishedCache and IDocumentNavigationQueryService or IMediaNavigationQueryService directly. This will be removed in a future version of Umbraco")] public static IEnumerable Breadcrumbs( this IPublishedContent content, int minLevel, bool andSelf = true) => content.Breadcrumbs(GetPublishedCache(content), GetNavigationQueryService(content), minLevel, andSelf); - [Obsolete("Please use IPublishedCache and IDocumentNavigationQueryService or IMediaNavigationQueryService directly. This will be removed in a future version of Umbraco")] public static IEnumerable Breadcrumbs( this IPublishedContent content, bool andSelf = true) where T : class, IPublishedContent=> content.Breadcrumbs(GetPublishedCache(content), GetNavigationQueryService(content), andSelf); - [Obsolete("Please use IPublishedCache and IDocumentNavigationQueryService or IMediaNavigationQueryService directly. This will be removed in a future version of Umbraco")] public static IEnumerable Children( this IPublishedContent content, IVariationContextAccessor? variationContextAccessor, string? culture = null) => Children(content, variationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), culture); - [Obsolete("Please use IPublishedCache and IDocumentNavigationQueryService or IMediaNavigationQueryService directly. This will be removed in a future version of Umbraco")] public static IEnumerable Children( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, @@ -2042,17 +2047,19 @@ public static class PublishedContentExtensions string? culture = null) => content.Children(variationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), culture).Where(predicate); - [Obsolete("Please use IPublishedCache and IDocumentNavigationQueryService or IMediaNavigationQueryService directly. This will be removed in a future version of Umbraco")] public static IEnumerable ChildrenOfType( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? contentTypeAlias, - string? culture = null) => - content.Children(variationContextAccessor, GetPublishedCache(content), - GetNavigationQueryService(content), x => x.ContentType.Alias.InvariantEquals(contentTypeAlias), - culture); + string? culture = null) + { + IEnumerable children = contentTypeAlias is not null + ? GetChildren(GetNavigationQueryService(content), GetPublishedCache(content), content.Key, contentTypeAlias) + : []; + + return children.FilterByCulture(culture, variationContextAccessor); + } - [Obsolete("Please use IPublishedCache and IDocumentNavigationQueryService or IMediaNavigationQueryService directly. This will be removed in a future version of Umbraco")] public static IEnumerable Children( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, @@ -2061,7 +2068,6 @@ public static class PublishedContentExtensions content.Children(variationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), culture).OfType(); - [Obsolete("Please use IPublishedCache and IDocumentNavigationQueryService or IMediaNavigationQueryService directly. This will be removed in a future version of Umbraco")] public static DataTable ChildrenAsTable( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, @@ -2074,7 +2080,6 @@ public static class PublishedContentExtensions => GenerateDataTable(content, variationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), contentTypeService, mediaTypeService, memberTypeService, publishedUrlProvider, contentTypeAliasFilter, culture); - [Obsolete("Please use IPublishedCache and IDocumentNavigationQueryService or IMediaNavigationQueryService directly. This will be removed in a future version of Umbraco")] public static IEnumerable DescendantsOrSelfOfType( this IEnumerable parentNodes, IVariationContextAccessor variationContextAccessor, @@ -2083,7 +2088,7 @@ public static class PublishedContentExtensions x.DescendantsOrSelfOfType(variationContextAccessor, GetPublishedCache(parentNodes.First()), GetNavigationQueryService(parentNodes.First()), docTypeAlias, culture)); - [Obsolete("Please use IPublishedCache and IDocumentNavigationQueryService or IMediaNavigationQueryService directly. This will be removed in a future version of Umbraco")] + public static IEnumerable DescendantsOrSelf( this IEnumerable parentNodes, IVariationContextAccessor variationContextAccessor, @@ -2093,7 +2098,7 @@ public static class PublishedContentExtensions GetNavigationQueryService(parentNodes.First()), culture)); - [Obsolete("Please use IPublishedCache and IDocumentNavigationQueryService or IMediaNavigationQueryService directly. This will be removed in a future version of Umbraco")] + public static IEnumerable Descendants( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, @@ -2101,7 +2106,7 @@ public static class PublishedContentExtensions content.DescendantsOrSelf(variationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), false, null, culture); - [Obsolete("Please use IPublishedCache and IDocumentNavigationQueryService or IMediaNavigationQueryService directly. This will be removed in a future version of Umbraco")] + public static IEnumerable Descendants( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, @@ -2110,15 +2115,20 @@ public static class PublishedContentExtensions content.DescendantsOrSelf(variationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), false, p => p.Level >= level, culture); - [Obsolete("Please use IPublishedCache and IDocumentNavigationQueryService or IMediaNavigationQueryService directly. This will be removed in a future version of Umbraco")] + public static IEnumerable DescendantsOfType( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string contentTypeAlias, string? culture = null) => - content.DescendantsOrSelf(variationContextAccessor, GetPublishedCache(content), - GetNavigationQueryService(content), false, p => p.ContentType.Alias.InvariantEquals(contentTypeAlias), culture); + content.EnumerateDescendantsOrSelfInternal( + variationContextAccessor, + GetPublishedCache(content), + GetNavigationQueryService(content), + culture, + false, + contentTypeAlias); + - [Obsolete("Please use IPublishedCache and IDocumentNavigationQueryService or IMediaNavigationQueryService directly. This will be removed in a future version of Umbraco")] public static IEnumerable Descendants( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, @@ -2127,7 +2137,7 @@ public static class PublishedContentExtensions content.Descendants(variationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), culture).OfType(); - [Obsolete("Please use IPublishedCache and IDocumentNavigationQueryService or IMediaNavigationQueryService directly. This will be removed in a future version of Umbraco")] + public static IEnumerable Descendants( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, @@ -2137,7 +2147,7 @@ public static class PublishedContentExtensions content.Descendants(variationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), level, culture).OfType(); - [Obsolete("Please use IPublishedCache and IDocumentNavigationQueryService or IMediaNavigationQueryService directly. This will be removed in a future version of Umbraco")] + public static IEnumerable DescendantsOrSelf( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, @@ -2145,7 +2155,7 @@ public static class PublishedContentExtensions content.DescendantsOrSelf(variationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), true, null, culture); - [Obsolete("Please use IPublishedCache and IDocumentNavigationQueryService or IMediaNavigationQueryService directly. This will be removed in a future version of Umbraco")] + public static IEnumerable DescendantsOrSelf( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, @@ -2154,16 +2164,21 @@ public static class PublishedContentExtensions content.DescendantsOrSelf(variationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), true, p => p.Level >= level, culture); - [Obsolete("Please use IPublishedCache and IDocumentNavigationQueryService or IMediaNavigationQueryService directly. This will be removed in a future version of Umbraco")] + public static IEnumerable DescendantsOrSelfOfType( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string contentTypeAlias, string? culture = null) => - content.DescendantsOrSelf(variationContextAccessor, GetPublishedCache(content), - GetNavigationQueryService(content), true, p => p.ContentType.Alias.InvariantEquals(contentTypeAlias), culture); + content.EnumerateDescendantsOrSelfInternal( + variationContextAccessor, + GetPublishedCache(content), + GetNavigationQueryService(content), + culture, + true, + contentTypeAlias); + - [Obsolete("Please use IPublishedCache and IDocumentNavigationQueryService or IMediaNavigationQueryService directly. This will be removed in a future version of Umbraco")] public static IEnumerable DescendantsOrSelf( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, @@ -2172,7 +2187,7 @@ public static class PublishedContentExtensions content.DescendantsOrSelf(variationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), culture).OfType(); - [Obsolete("Please use IPublishedCache and IDocumentNavigationQueryService or IMediaNavigationQueryService directly. This will be removed in a future version of Umbraco")] + public static IEnumerable DescendantsOrSelf( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, @@ -2182,7 +2197,7 @@ public static class PublishedContentExtensions content.DescendantsOrSelf(variationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), level, culture).OfType(); - [Obsolete("Please use IPublishedCache and IDocumentNavigationQueryService or IMediaNavigationQueryService directly. This will be removed in a future version of Umbraco")] + public static IPublishedContent? Descendant( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, @@ -2190,7 +2205,7 @@ public static class PublishedContentExtensions content.Children(variationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), culture)?.FirstOrDefault(); - [Obsolete("Please use IPublishedCache and IDocumentNavigationQueryService or IMediaNavigationQueryService directly. This will be removed in a future version of Umbraco")] + public static IPublishedContent? Descendant( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, @@ -2199,17 +2214,22 @@ public static class PublishedContentExtensions .EnumerateDescendants(variationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), false, culture).FirstOrDefault(x => x.Level == level); - [Obsolete("Please use IPublishedCache and IDocumentNavigationQueryService or IMediaNavigationQueryService directly. This will be removed in a future version of Umbraco")] + public static IPublishedContent? DescendantOfType( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string contentTypeAlias, string? culture = null) => content - .EnumerateDescendants(variationContextAccessor, GetPublishedCache(content), - GetNavigationQueryService(content), false, culture) - .FirstOrDefault(x => x.ContentType.Alias.InvariantEquals(contentTypeAlias)); + .EnumerateDescendantsOrSelfInternal( + variationContextAccessor, + GetPublishedCache(content), + GetNavigationQueryService(content), + culture, + false, + contentTypeAlias) + .FirstOrDefault(); + - [Obsolete("Please use IPublishedCache and IDocumentNavigationQueryService or IMediaNavigationQueryService directly. This will be removed in a future version of Umbraco")] public static T? Descendant( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, @@ -2218,7 +2238,7 @@ public static class PublishedContentExtensions content.EnumerateDescendants(variationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), false, culture).FirstOrDefault(x => x is T) as T; - [Obsolete("Please use IPublishedCache and IDocumentNavigationQueryService or IMediaNavigationQueryService directly. This will be removed in a future version of Umbraco")] + public static T? Descendant( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, @@ -2228,7 +2248,7 @@ public static class PublishedContentExtensions content.Descendant(variationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), level, culture) as T; - [Obsolete("Please use IPublishedCache and IDocumentNavigationQueryService or IMediaNavigationQueryService directly. This will be removed in a future version of Umbraco")] + public static IPublishedContent? DescendantOrSelf( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, @@ -2237,17 +2257,22 @@ public static class PublishedContentExtensions .EnumerateDescendants(variationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), true, culture).FirstOrDefault(x => x.Level == level); - [Obsolete("Please use IPublishedCache and IDocumentNavigationQueryService or IMediaNavigationQueryService directly. This will be removed in a future version of Umbraco")] + public static IPublishedContent? DescendantOrSelfOfType( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string contentTypeAlias, string? culture = null) => content - .EnumerateDescendants(variationContextAccessor, GetPublishedCache(content), - GetNavigationQueryService(content), true, culture) - .FirstOrDefault(x => x.ContentType.Alias.InvariantEquals(contentTypeAlias)); + .EnumerateDescendantsOrSelfInternal( + variationContextAccessor, + GetPublishedCache(content), + GetNavigationQueryService(content), + culture, + true, + contentTypeAlias) + .FirstOrDefault(); + - [Obsolete("Please use IPublishedCache and IDocumentNavigationQueryService or IMediaNavigationQueryService directly. This will be removed in a future version of Umbraco")] public static T? DescendantOrSelf( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, @@ -2256,7 +2281,7 @@ public static class PublishedContentExtensions content.EnumerateDescendants(variationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), true, culture).FirstOrDefault(x => x is T) as T; - [Obsolete("Please use IPublishedCache and IDocumentNavigationQueryService or IMediaNavigationQueryService directly. This will be removed in a future version of Umbraco")] + public static T? DescendantOrSelf( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, @@ -2267,7 +2292,7 @@ public static class PublishedContentExtensions GetNavigationQueryService(content), level, culture) as T; - [Obsolete("Please use IPublishedCache and IDocumentNavigationQueryService or IMediaNavigationQueryService directly. This will be removed in a future version of Umbraco")] + public static IPublishedContent? FirstChild( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, @@ -2275,7 +2300,7 @@ public static class PublishedContentExtensions content.Children(variationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), culture)?.FirstOrDefault(); - [Obsolete("Please use IPublishedCache and IDocumentNavigationQueryService or IMediaNavigationQueryService directly. This will be removed in a future version of Umbraco")] + public static IPublishedContent? FirstChildOfType( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, @@ -2284,7 +2309,7 @@ public static class PublishedContentExtensions content.ChildrenOfType(variationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), contentTypeAlias, culture)?.FirstOrDefault(); - [Obsolete("Please use IPublishedCache and IDocumentNavigationQueryService or IMediaNavigationQueryService directly. This will be removed in a future version of Umbraco")] + public static IPublishedContent? FirstChild( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, @@ -2293,7 +2318,7 @@ public static class PublishedContentExtensions => content.Children(variationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), predicate, culture)?.FirstOrDefault(); - [Obsolete("Please use IPublishedCache and IDocumentNavigationQueryService or IMediaNavigationQueryService directly. This will be removed in a future version of Umbraco")] + public static IPublishedContent? FirstChild( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, @@ -2302,7 +2327,7 @@ public static class PublishedContentExtensions .Children(variationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), x => x.Key == uniqueId, culture)?.FirstOrDefault(); - [Obsolete("Please use IPublishedCache and IDocumentNavigationQueryService or IMediaNavigationQueryService directly. This will be removed in a future version of Umbraco")] + public static T? FirstChild( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, @@ -2311,7 +2336,7 @@ public static class PublishedContentExtensions content.Children(variationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), culture)?.FirstOrDefault(); - [Obsolete("Please use IPublishedCache and IDocumentNavigationQueryService or IMediaNavigationQueryService directly. This will be removed in a future version of Umbraco")] + public static T? FirstChild( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, @@ -2364,7 +2389,7 @@ public static class PublishedContentExtensions string? culture = null) => SiblingsAndSelfOfType(content, variationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), contentTypeAlias, culture); - [Obsolete("Please use IPublishedCache and IDocumentNavigationQueryService or IMediaNavigationQueryService directly. This will be removed in a future version of Umbraco")] + public static IEnumerable SiblingsAndSelf( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, @@ -2399,4 +2424,102 @@ public static class PublishedContentExtensions throw new NotSupportedException("Unsupported content type."); } } + + private static IEnumerable GetChildren( + INavigationQueryService navigationQueryService, + IPublishedCache publishedCache, + Guid parentKey, + string? contentTypeAlias = null) + { + var nodeExists = contentTypeAlias is null + ? navigationQueryService.TryGetChildrenKeys(parentKey, out IEnumerable childrenKeys) + : navigationQueryService.TryGetChildrenKeysOfType(parentKey, contentTypeAlias, out childrenKeys); + + if (nodeExists is false) + { + return []; + } + + return childrenKeys + .Select(publishedCache.GetById) + .WhereNotNull(); + } + + private static IEnumerable FilterByCulture( + this IEnumerable contentNodes, + string? culture, + IVariationContextAccessor? variationContextAccessor) + { + // Determine the culture if not provided + culture ??= variationContextAccessor?.VariationContext?.Culture ?? string.Empty; + + return culture == "*" + ? contentNodes + : contentNodes.Where(x => x.IsInvariantOrHasCulture(culture)); + } + + private static IEnumerable EnumerateDescendantsOrSelfInternal( + this IPublishedContent content, + IVariationContextAccessor variationContextAccessor, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService, + string? culture, + bool orSelf, + string? contentTypeAlias = null) + { + if (orSelf) + { + yield return content; + } + + var nodeExists = contentTypeAlias is null + ? navigationQueryService.TryGetDescendantsKeys(content.Key, out IEnumerable descendantsKeys) + : navigationQueryService.TryGetDescendantsKeysOfType(content.Key, contentTypeAlias, out descendantsKeys); + + if (nodeExists is false) + { + yield break; + } + + IEnumerable descendants = descendantsKeys + .Select(publishedCache.GetById) + .WhereNotNull() + .FilterByCulture(culture, variationContextAccessor); + + foreach (IPublishedContent descendant in descendants) + { + yield return descendant; + } + } + + private static IEnumerable EnumerateAncestorsOrSelfInternal( + this IPublishedContent content, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService, + bool orSelf, + string? contentTypeAlias = null) + { + if (orSelf) + { + yield return content; + } + + var nodeExists = contentTypeAlias is null + ? navigationQueryService.TryGetAncestorsKeys(content.Key, out IEnumerable ancestorsKeys) + : navigationQueryService.TryGetAncestorsKeysOfType(content.Key, contentTypeAlias, out ancestorsKeys); + + if (nodeExists is false) + { + yield break; + } + + foreach (Guid ancestorKey in ancestorsKeys) + { + IPublishedContent? ancestor = publishedCache.GetById(ancestorKey); + if (ancestor is not null) + { + yield return ancestor; + } + } + } } diff --git a/src/Umbraco.Core/Extensions/UdiGetterExtensions.cs b/src/Umbraco.Core/Extensions/UdiGetterExtensions.cs index 66c5002604..a5adc0de2a 100644 --- a/src/Umbraco.Core/Extensions/UdiGetterExtensions.cs +++ b/src/Umbraco.Core/Extensions/UdiGetterExtensions.cs @@ -4,6 +4,7 @@ using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Models.Membership; namespace Umbraco.Extensions; @@ -27,8 +28,6 @@ public static class UdiGetterExtensions { // Concrete types EntityContainer container => container.GetUdi(), - Script script => script.GetUdi(), - Stylesheet stylesheet => stylesheet.GetUdi(), // Interfaces IContentBase contentBase => contentBase.GetUdi(), IContentTypeComposition contentTypeComposition => contentTypeComposition.GetUdi(), @@ -37,8 +36,13 @@ public static class UdiGetterExtensions ILanguage language => language.GetUdi(), IMemberGroup memberGroup => memberGroup.GetUdi(), IPartialView partialView => partialView.GetUdi(), + IRelation relation => relation.GetUdi(), IRelationType relationType => relationType.GetUdi(), + IScript script => script.GetUdi(), + IStylesheet stylesheet => stylesheet.GetUdi(), ITemplate template => template.GetUdi(), + IUser user => user.GetUdi(), + IUserGroup userGroup => userGroup.GetUdi(), IWebhook webhook => webhook.GetUdi(), _ => throw new NotSupportedException($"Entity type {entity.GetType().FullName} is not supported."), }; @@ -80,34 +84,6 @@ public static class UdiGetterExtensions return new GuidUdi(entityType, entity.Key).EnsureClosed(); } - /// - /// Gets the entity identifier of the entity. - /// - /// The entity. - /// - /// The entity identifier of the entity. - /// - public static StringUdi GetUdi(this Script entity) - { - ArgumentNullException.ThrowIfNull(entity); - - return GetUdiFromPath(Constants.UdiEntityType.Script, entity.Path); - } - - /// - /// Gets the entity identifier of the entity. - /// - /// The entity. - /// - /// The entity identifier of the entity. - /// - public static StringUdi GetUdi(this Stylesheet entity) - { - ArgumentNullException.ThrowIfNull(entity); - - return GetUdiFromPath(Constants.UdiEntityType.Stylesheet, entity.Path); - } - /// /// Gets the entity identifier of the entity. /// @@ -304,6 +280,20 @@ public static class UdiGetterExtensions return GetUdiFromPath(Constants.UdiEntityType.PartialView, entity.Path); } + /// + /// Gets the entity identifier of the entity. + /// + /// The entity. + /// + /// The entity identifier of the entity. + /// + public static GuidUdi GetUdi(this IRelation entity) + { + ArgumentNullException.ThrowIfNull(entity); + + return new GuidUdi(Constants.UdiEntityType.Relation, entity.Key).EnsureClosed(); + } + /// /// Gets the entity identifier of the entity. /// @@ -318,6 +308,34 @@ public static class UdiGetterExtensions return new GuidUdi(Constants.UdiEntityType.RelationType, entity.Key).EnsureClosed(); } + /// + /// Gets the entity identifier of the entity. + /// + /// The entity. + /// + /// The entity identifier of the entity. + /// + public static StringUdi GetUdi(this IScript entity) + { + ArgumentNullException.ThrowIfNull(entity); + + return GetUdiFromPath(Constants.UdiEntityType.Script, entity.Path); + } + + /// + /// Gets the entity identifier of the entity. + /// + /// The entity. + /// + /// The entity identifier of the entity. + /// + public static StringUdi GetUdi(this IStylesheet entity) + { + ArgumentNullException.ThrowIfNull(entity); + + return GetUdiFromPath(Constants.UdiEntityType.Stylesheet, entity.Path); + } + /// /// Gets the entity identifier of the entity. /// @@ -332,6 +350,34 @@ public static class UdiGetterExtensions return new GuidUdi(Constants.UdiEntityType.Template, entity.Key).EnsureClosed(); } + /// + /// Gets the entity identifier of the entity. + /// + /// The entity. + /// + /// The entity identifier of the entity. + /// + public static GuidUdi GetUdi(this IUser entity) + { + ArgumentNullException.ThrowIfNull(entity); + + return new GuidUdi(Constants.UdiEntityType.User, entity.Key).EnsureClosed(); + } + + /// + /// Gets the entity identifier of the entity. + /// + /// The entity. + /// + /// The entity identifier of the entity. + /// + public static GuidUdi GetUdi(this IUserGroup entity) + { + ArgumentNullException.ThrowIfNull(entity); + + return new GuidUdi(Constants.UdiEntityType.UserGroup, entity.Key).EnsureClosed(); + } + /// /// Gets the entity identifier of the entity. /// diff --git a/src/Umbraco.Core/Media/EmbedProviders/DailyMotion.cs b/src/Umbraco.Core/Media/EmbedProviders/DailyMotion.cs index 9c75db64e2..0f2abd3b8e 100644 --- a/src/Umbraco.Core/Media/EmbedProviders/DailyMotion.cs +++ b/src/Umbraco.Core/Media/EmbedProviders/DailyMotion.cs @@ -23,6 +23,11 @@ public class DailyMotion : OEmbedProviderBase { "format", "xml" }, }; + public override async Task GetMarkupAsync(string url, int? maxWidth, int? maxHeight, CancellationToken cancellationToken) + => await GetXmlBasedMarkupAsync(url, maxWidth, maxHeight, cancellationToken); + + [Obsolete("Use GetMarkupAsync instead. Planned for removal in v16")] + public override async Task GeOEmbedDataAsync(string url, int? maxWidth, int? maxHeight, CancellationToken cancellationToken) { var requestUrl = base.GetEmbedProviderUrl(url, maxWidth, maxHeight); @@ -33,7 +38,5 @@ public class DailyMotion : OEmbedProviderBase [Obsolete("Use GetMarkupAsync instead. This will be removed in Umbraco 15.")] public override string? GetMarkup(string url, int maxWidth = 0, int maxHeight = 0) - { - return GeOEmbedDataAsync(url, maxWidth, maxHeight, CancellationToken.None).GetAwaiter().GetResult(); - } + => GetMarkupAsync(url, maxWidth, maxHeight, CancellationToken.None).GetAwaiter().GetResult(); } diff --git a/src/Umbraco.Core/Media/EmbedProviders/Flickr.cs b/src/Umbraco.Core/Media/EmbedProviders/Flickr.cs index 6838c0f73f..0d7d8b3a0f 100644 --- a/src/Umbraco.Core/Media/EmbedProviders/Flickr.cs +++ b/src/Umbraco.Core/Media/EmbedProviders/Flickr.cs @@ -22,11 +22,9 @@ public class Flickr : OEmbedProviderBase [Obsolete("Use GetMarkupAsync instead. This will be removed in Umbraco 15.")] public override string? GetMarkup(string url, int maxWidth = 0, int maxHeight = 0) - { - return GeOEmbedDataAsync(url, maxWidth, maxHeight, CancellationToken.None).GetAwaiter().GetResult(); - } + => GetMarkupAsync(url, maxWidth, maxHeight, CancellationToken.None).GetAwaiter().GetResult(); - public override async Task GeOEmbedDataAsync(string url, int? maxWidth, int? maxHeight, CancellationToken cancellationToken) + public override async Task GetMarkupAsync(string url, int? maxWidth, int? maxHeight, CancellationToken cancellationToken) { var requestUrl = base.GetEmbedProviderUrl(url, maxWidth, maxHeight); XmlDocument xmlDocument = await base.GetXmlResponseAsync(requestUrl, cancellationToken); @@ -38,4 +36,8 @@ public class Flickr : OEmbedProviderBase return string.Format("\"{3}\"", imageUrl, imageWidth, imageHeight, WebUtility.HtmlEncode(imageTitle)); } + + [Obsolete("Use GetMarkupAsync instead. Planned for removal in v16")] + public override async Task GeOEmbedDataAsync(string url, int? maxWidth, int? maxHeight, CancellationToken cancellationToken) + => await GetMarkupAsync(url, maxWidth, maxHeight, cancellationToken); } diff --git a/src/Umbraco.Core/Media/EmbedProviders/GettyImages.cs b/src/Umbraco.Core/Media/EmbedProviders/GettyImages.cs index 02631542d0..41364111af 100644 --- a/src/Umbraco.Core/Media/EmbedProviders/GettyImages.cs +++ b/src/Umbraco.Core/Media/EmbedProviders/GettyImages.cs @@ -22,10 +22,12 @@ public class GettyImages : OEmbedProviderBase [Obsolete("Use GetMarkupAsync instead. This will be removed in Umbraco 15.")] public override string? GetMarkup(string url, int maxWidth = 0, int maxHeight = 0) - { - return GeOEmbedDataAsync(url, maxWidth, maxHeight, CancellationToken.None).GetAwaiter().GetResult(); - } + => GetMarkupAsync(url, maxWidth, maxHeight, CancellationToken.None).GetAwaiter().GetResult(); + public override async Task GetMarkupAsync(string url, int? maxWidth, int? maxHeight, CancellationToken cancellationToken) + => await GetJsonBasedMarkupAsync(url, maxWidth, maxHeight, cancellationToken); + + [Obsolete("Use GetMarkupAsync instead. Planned for removal in v16")] public override async Task GeOEmbedDataAsync(string url, int? maxWidth, int? maxHeight, CancellationToken cancellationToken) { var requestUrl = base.GetEmbedProviderUrl(url, maxWidth, maxHeight); diff --git a/src/Umbraco.Core/Media/EmbedProviders/Giphy.cs b/src/Umbraco.Core/Media/EmbedProviders/Giphy.cs index e9938f3164..1e8b2c96c8 100644 --- a/src/Umbraco.Core/Media/EmbedProviders/Giphy.cs +++ b/src/Umbraco.Core/Media/EmbedProviders/Giphy.cs @@ -20,10 +20,12 @@ public class Giphy : OEmbedProviderBase [Obsolete("Use GetMarkupAsync instead. This will be removed in Umbraco 15.")] public override string? GetMarkup(string url, int maxWidth = 0, int maxHeight = 0) - { - return GeOEmbedDataAsync(url, maxWidth, maxHeight, CancellationToken.None).GetAwaiter().GetResult(); - } + => GetMarkupAsync(url, maxWidth, maxHeight, CancellationToken.None).GetAwaiter().GetResult(); + public override async Task GetMarkupAsync(string url, int? maxWidth, int? maxHeight, CancellationToken cancellationToken) + => await GetJsonBasedMarkupAsync(url, maxWidth, maxHeight, cancellationToken); + + [Obsolete("Use GetMarkupAsync instead. Planned for removal in v16")] public override async Task GeOEmbedDataAsync(string url, int? maxWidth, int? maxHeight, CancellationToken cancellationToken) { var requestUrl = base.GetEmbedProviderUrl(url, maxWidth, maxHeight); diff --git a/src/Umbraco.Core/Media/EmbedProviders/Hulu.cs b/src/Umbraco.Core/Media/EmbedProviders/Hulu.cs index 5fdd22c667..4e4d53e6ad 100644 --- a/src/Umbraco.Core/Media/EmbedProviders/Hulu.cs +++ b/src/Umbraco.Core/Media/EmbedProviders/Hulu.cs @@ -20,10 +20,12 @@ public class Hulu : OEmbedProviderBase [Obsolete("Use GetMarkupAsync instead. This will be removed in Umbraco 15.")] public override string? GetMarkup(string url, int maxWidth = 0, int maxHeight = 0) - { - return GeOEmbedDataAsync(url, maxWidth, maxHeight, CancellationToken.None).GetAwaiter().GetResult(); - } + => GetMarkupAsync(url, maxWidth, maxHeight, CancellationToken.None).GetAwaiter().GetResult(); + public override async Task GetMarkupAsync(string url, int? maxWidth, int? maxHeight, CancellationToken cancellationToken) + => await GetJsonBasedMarkupAsync(url, maxWidth, maxHeight, cancellationToken); + + [Obsolete("Use GetMarkupAsync instead. Planned for removal in v16")] public override async Task GeOEmbedDataAsync(string url, int? maxWidth, int? maxHeight, CancellationToken cancellationToken) { var requestUrl = base.GetEmbedProviderUrl(url, maxWidth, maxHeight); diff --git a/src/Umbraco.Core/Media/EmbedProviders/Issuu.cs b/src/Umbraco.Core/Media/EmbedProviders/Issuu.cs index adeb6b45f8..42120b70c5 100644 --- a/src/Umbraco.Core/Media/EmbedProviders/Issuu.cs +++ b/src/Umbraco.Core/Media/EmbedProviders/Issuu.cs @@ -28,10 +28,12 @@ public class Issuu : OEmbedProviderBase [Obsolete("Use GetMarkupAsync instead. This will be removed in Umbraco 15.")] public override string? GetMarkup(string url, int maxWidth = 0, int maxHeight = 0) - { - return GeOEmbedDataAsync(url, maxWidth, maxHeight, CancellationToken.None).GetAwaiter().GetResult(); - } + => GetMarkupAsync(url, maxWidth, maxHeight, CancellationToken.None).GetAwaiter().GetResult(); + public override async Task GetMarkupAsync(string url, int? maxWidth, int? maxHeight, CancellationToken cancellationToken) + => await GetXmlBasedMarkupAsync(url, maxWidth, maxHeight, cancellationToken); + + [Obsolete("Use GetMarkupAsync instead. Planned for removal in v16")] public override async Task GeOEmbedDataAsync(string url, int? maxWidth, int? maxHeight, CancellationToken cancellationToken) { var requestUrl = base.GetEmbedProviderUrl(url, maxWidth, maxHeight); diff --git a/src/Umbraco.Core/Media/EmbedProviders/Kickstarter.cs b/src/Umbraco.Core/Media/EmbedProviders/Kickstarter.cs index 78c50253ec..a54c03455f 100644 --- a/src/Umbraco.Core/Media/EmbedProviders/Kickstarter.cs +++ b/src/Umbraco.Core/Media/EmbedProviders/Kickstarter.cs @@ -20,10 +20,12 @@ public class Kickstarter : OEmbedProviderBase [Obsolete("Use GetMarkupAsync instead. This will be removed in Umbraco 15.")] public override string? GetMarkup(string url, int maxWidth = 0, int maxHeight = 0) - { - return GeOEmbedDataAsync(url, maxWidth, maxHeight, CancellationToken.None).GetAwaiter().GetResult(); - } + => GetMarkupAsync(url, maxWidth, maxHeight, CancellationToken.None).GetAwaiter().GetResult(); + public override async Task GetMarkupAsync(string url, int? maxWidth, int? maxHeight, CancellationToken cancellationToken) + => await GetJsonBasedMarkupAsync(url, maxWidth, maxHeight, cancellationToken); + + [Obsolete("Use GetMarkupAsync instead. Planned for removal in v16")] public override async Task GeOEmbedDataAsync(string url, int? maxWidth, int? maxHeight, CancellationToken cancellationToken) { var requestUrl = base.GetEmbedProviderUrl(url, maxWidth, maxHeight); diff --git a/src/Umbraco.Core/Media/EmbedProviders/LottieFiles.cs b/src/Umbraco.Core/Media/EmbedProviders/LottieFiles.cs index 56c2c8a1fd..8ce00feea7 100644 --- a/src/Umbraco.Core/Media/EmbedProviders/LottieFiles.cs +++ b/src/Umbraco.Core/Media/EmbedProviders/LottieFiles.cs @@ -21,14 +21,12 @@ public class LottieFiles : OEmbedProviderBase [Obsolete("Use GetMarkupAsync instead. This will be removed in Umbraco 15.")] public override string? GetMarkup(string url, int maxWidth = 0, int maxHeight = 0) - { - return GeOEmbedDataAsync(url, maxWidth, maxHeight, CancellationToken.None).GetAwaiter().GetResult(); - } + => GetMarkupAsync(url, maxWidth, maxHeight, CancellationToken.None).GetAwaiter().GetResult(); - public override async Task GeOEmbedDataAsync(string url, int? maxWidth, int? maxHeight, CancellationToken cancellationToken) + public override async Task GetMarkupAsync(string url, int? maxWidth, int? maxHeight, CancellationToken cancellationToken) { - var requestUrl = this.GetEmbedProviderUrl(url, maxWidth, maxHeight); - OEmbedResponse? oembed = await this.GetJsonResponseAsync(requestUrl, cancellationToken); + var requestUrl = GetEmbedProviderUrl(url, maxWidth, maxHeight); + OEmbedResponse? oembed = await GetJsonResponseAsync(requestUrl, cancellationToken); var html = oembed?.GetHtml(); // LottieFiles doesn't seem to support maxwidth and maxheight via oembed @@ -53,4 +51,12 @@ public class LottieFiles : OEmbedProviderBase return html; } + + [Obsolete("Use GetMarkupAsync instead. Planned for removal in v16")] + public override async Task GeOEmbedDataAsync( + string url, + int? maxWidth, + int? maxHeight, + CancellationToken cancellationToken) + => await GetMarkupAsync(url, maxWidth, maxHeight, cancellationToken); } diff --git a/src/Umbraco.Core/Media/EmbedProviders/OEmbedProviderBase.cs b/src/Umbraco.Core/Media/EmbedProviders/OEmbedProviderBase.cs index f0b0f3a217..369b6d7249 100644 --- a/src/Umbraco.Core/Media/EmbedProviders/OEmbedProviderBase.cs +++ b/src/Umbraco.Core/Media/EmbedProviders/OEmbedProviderBase.cs @@ -21,9 +21,14 @@ public abstract class OEmbedProviderBase : IEmbedProvider [Obsolete("Use GetMarkupAsync instead. This will be removed in Umbraco 15.")] public abstract string? GetMarkup(string url, int maxWidth = 0, int maxHeight = 0); - public virtual Task GeOEmbedDataAsync(string url, int? maxWidth, int? maxHeight, CancellationToken cancellationToken) => Task.FromResult(GetMarkup(url, maxWidth ?? 0, maxHeight ?? 0)); + public abstract Task GetMarkupAsync(string url, int? maxWidth, int? maxHeight, CancellationToken cancellationToken); + + [Obsolete("Cleanup, only proxied to by GetMarkupAsync implementations. Planned for removal in v16")] + public virtual Task GeOEmbedDataAsync(string url, int? maxWidth, int? maxHeight, CancellationToken cancellationToken) + => Task.FromResult(GetMarkup(url, maxWidth ?? 0, maxHeight ?? 0)); public virtual string GetEmbedProviderUrl(string url, int? maxWidth, int? maxHeight) => GetEmbedProviderUrl(url, maxWidth ?? 0, maxHeight ?? 0); + public virtual string GetEmbedProviderUrl(string url, int maxWidth, int maxHeight) { if (Uri.IsWellFormedUriString(url, UriKind.RelativeOrAbsolute) == false) @@ -54,22 +59,6 @@ public abstract class OEmbedProviderBase : IEmbedProvider return fullUrl.ToString(); } - [Obsolete("Use DownloadResponseAsync instead. This will be removed in Umbraco 15.")] - public virtual string DownloadResponse(string url) - { - if (_httpClient == null) - { - _httpClient = new HttpClient(); - _httpClient.DefaultRequestHeaders.UserAgent.TryParseAdd(Constants.HttpClients.Headers.UserAgentProductName); - } - - using (var request = new HttpRequestMessage(HttpMethod.Get, url)) - { - using HttpResponseMessage response = _httpClient.SendAsync(request).GetAwaiter().GetResult(); - return response.Content.ReadAsStringAsync().Result; - } - } - public virtual async Task DownloadResponseAsync(string url, CancellationToken cancellationToken) { if (_httpClient == null) @@ -85,14 +74,6 @@ public abstract class OEmbedProviderBase : IEmbedProvider } } - [Obsolete("Use GetJsonResponseAsync instead. This will be removed in Umbraco 15.")] - public virtual T? GetJsonResponse(string url) - where T : class - { - var response = DownloadResponse(url); - return _jsonSerializer.Deserialize(response); - } - public virtual async Task GetJsonResponseAsync(string url, CancellationToken cancellationToken) where T : class { @@ -109,19 +90,30 @@ public abstract class OEmbedProviderBase : IEmbedProvider return doc; } - [Obsolete("Use GetXmlResponseAsync instead. This will be removed in Umbraco 15.")] - public virtual XmlDocument GetXmlResponse(string url) - { - var response = DownloadResponse(url); - var doc = new XmlDocument(); - doc.LoadXml(response); - - return doc; - } - public virtual string GetXmlProperty(XmlDocument doc, string property) { XmlNode? selectSingleNode = doc.SelectSingleNode(property); return selectSingleNode != null ? selectSingleNode.InnerText : string.Empty; } + + public virtual async Task GetJsonBasedMarkupAsync(string url, int? maxWidth, int? maxHeight, CancellationToken cancellationToken) + { + var requestUrl = GetEmbedProviderUrl(url, maxWidth, maxHeight); + OEmbedResponse? oembed = await GetJsonResponseAsync(requestUrl, cancellationToken); + + return oembed?.GetHtml(); + } + + public virtual async Task GetXmlBasedMarkupAsync( + string url, + int? maxWidth, + int? maxHeight, + CancellationToken cancellationToken, + string property = "/oembed/html") + { + var requestUrl = GetEmbedProviderUrl(url, maxWidth, maxHeight); + XmlDocument xmlDocument = await GetXmlResponseAsync(requestUrl, cancellationToken); + + return GetXmlProperty(xmlDocument, property); + } } diff --git a/src/Umbraco.Core/Media/EmbedProviders/Slideshare.cs b/src/Umbraco.Core/Media/EmbedProviders/Slideshare.cs index c1b4731bfb..f7b5a0830c 100644 --- a/src/Umbraco.Core/Media/EmbedProviders/Slideshare.cs +++ b/src/Umbraco.Core/Media/EmbedProviders/Slideshare.cs @@ -21,10 +21,12 @@ public class Slideshare : OEmbedProviderBase [Obsolete("Use GetMarkupAsync instead. This will be removed in Umbraco 15.")] public override string? GetMarkup(string url, int maxWidth = 0, int maxHeight = 0) - { - return GeOEmbedDataAsync(url, maxWidth, maxHeight, CancellationToken.None).GetAwaiter().GetResult(); - } + => GetMarkupAsync(url, maxWidth, maxHeight, CancellationToken.None).GetAwaiter().GetResult(); + public override async Task GetMarkupAsync(string url, int? maxWidth, int? maxHeight, CancellationToken cancellationToken) + => await GetXmlBasedMarkupAsync(url, maxWidth, maxHeight, cancellationToken); + + [Obsolete("Use GetMarkupAsync instead. Planned for removal in v16")] public override async Task GeOEmbedDataAsync(string url, int? maxWidth, int? maxHeight, CancellationToken cancellationToken) { var requestUrl = base.GetEmbedProviderUrl(url, maxWidth, maxHeight); diff --git a/src/Umbraco.Core/Media/EmbedProviders/SoundCloud.cs b/src/Umbraco.Core/Media/EmbedProviders/SoundCloud.cs index 43092dddaa..ab8e7ab0a5 100644 --- a/src/Umbraco.Core/Media/EmbedProviders/SoundCloud.cs +++ b/src/Umbraco.Core/Media/EmbedProviders/SoundCloud.cs @@ -22,10 +22,12 @@ public class Soundcloud : OEmbedProviderBase [Obsolete("Use GetMarkupAsync instead. This will be removed in Umbraco 15.")] public override string? GetMarkup(string url, int maxWidth = 0, int maxHeight = 0) - { - return GeOEmbedDataAsync(url, maxWidth, maxHeight, CancellationToken.None).GetAwaiter().GetResult(); - } + => GetMarkupAsync(url, maxWidth, maxHeight, CancellationToken.None).GetAwaiter().GetResult(); + public override async Task GetMarkupAsync(string url, int? maxWidth, int? maxHeight, CancellationToken cancellationToken) + => await GetXmlBasedMarkupAsync(url, maxWidth, maxHeight, cancellationToken); + + [Obsolete("Use GetMarkupAsync instead. Planned for removal in v16")] public override async Task GeOEmbedDataAsync(string url, int? maxWidth, int? maxHeight, CancellationToken cancellationToken) { var requestUrl = base.GetEmbedProviderUrl(url, maxWidth, maxHeight); diff --git a/src/Umbraco.Core/Media/EmbedProviders/Ted.cs b/src/Umbraco.Core/Media/EmbedProviders/Ted.cs index 8816382cf8..a68df4f6fb 100644 --- a/src/Umbraco.Core/Media/EmbedProviders/Ted.cs +++ b/src/Umbraco.Core/Media/EmbedProviders/Ted.cs @@ -21,10 +21,12 @@ public class Ted : OEmbedProviderBase [Obsolete("Use GetMarkupAsync instead. This will be removed in Umbraco 15.")] public override string? GetMarkup(string url, int maxWidth = 0, int maxHeight = 0) - { - return GeOEmbedDataAsync(url, maxWidth, maxHeight, CancellationToken.None).GetAwaiter().GetResult(); - } + => GetMarkupAsync(url, maxWidth, maxHeight, CancellationToken.None).GetAwaiter().GetResult(); + public override async Task GetMarkupAsync(string url, int? maxWidth, int? maxHeight, CancellationToken cancellationToken) + => await GetXmlBasedMarkupAsync(url, maxWidth, maxHeight, cancellationToken); + + [Obsolete("Use GetMarkupAsync instead. Planned for removal in v16")] public override async Task GeOEmbedDataAsync(string url, int? maxWidth, int? maxHeight, CancellationToken cancellationToken) { var requestUrl = base.GetEmbedProviderUrl(url, maxWidth, maxHeight); diff --git a/src/Umbraco.Core/Media/EmbedProviders/Twitter.cs b/src/Umbraco.Core/Media/EmbedProviders/Twitter.cs index ac5cab8604..3b57f0c44d 100644 --- a/src/Umbraco.Core/Media/EmbedProviders/Twitter.cs +++ b/src/Umbraco.Core/Media/EmbedProviders/Twitter.cs @@ -21,10 +21,12 @@ public class Twitter : OEmbedProviderBase [Obsolete("Use GetMarkupAsync instead. This will be removed in Umbraco 15.")] public override string? GetMarkup(string url, int maxWidth = 0, int maxHeight = 0) - { - return GeOEmbedDataAsync(url, maxWidth, maxHeight, CancellationToken.None).GetAwaiter().GetResult(); - } + => GetMarkupAsync(url, maxWidth, maxHeight, CancellationToken.None).GetAwaiter().GetResult(); + public override async Task GetMarkupAsync(string url, int? maxWidth, int? maxHeight, CancellationToken cancellationToken) + => await GetJsonBasedMarkupAsync(url, maxWidth, maxHeight, cancellationToken); + + [Obsolete("Use GetMarkupAsync instead. Planned for removal in v16")] public override async Task GeOEmbedDataAsync(string url, int? maxWidth, int? maxHeight, CancellationToken cancellationToken) { var requestUrl = base.GetEmbedProviderUrl(url, maxWidth, maxHeight); diff --git a/src/Umbraco.Core/Media/EmbedProviders/Vimeo.cs b/src/Umbraco.Core/Media/EmbedProviders/Vimeo.cs index f4db5b17c9..2f6816f4c1 100644 --- a/src/Umbraco.Core/Media/EmbedProviders/Vimeo.cs +++ b/src/Umbraco.Core/Media/EmbedProviders/Vimeo.cs @@ -21,10 +21,12 @@ public class Vimeo : OEmbedProviderBase [Obsolete("Use GetMarkupAsync instead. This will be removed in Umbraco 15.")] public override string? GetMarkup(string url, int maxWidth = 0, int maxHeight = 0) - { - return GeOEmbedDataAsync(url, maxWidth, maxHeight, CancellationToken.None).GetAwaiter().GetResult(); - } + => GetMarkupAsync(url, maxWidth, maxHeight, CancellationToken.None).GetAwaiter().GetResult(); + public override async Task GetMarkupAsync(string url, int? maxWidth, int? maxHeight, CancellationToken cancellationToken) + => await GetXmlBasedMarkupAsync(url, maxWidth, maxHeight, cancellationToken); + + [Obsolete("Use GetMarkupAsync instead. Planned for removal in v16")] public override async Task GeOEmbedDataAsync(string url, int? maxWidth, int? maxHeight, CancellationToken cancellationToken) { var requestUrl = base.GetEmbedProviderUrl(url, maxWidth, maxHeight); diff --git a/src/Umbraco.Core/Media/EmbedProviders/X.cs b/src/Umbraco.Core/Media/EmbedProviders/X.cs index c4d102e941..391796dc51 100644 --- a/src/Umbraco.Core/Media/EmbedProviders/X.cs +++ b/src/Umbraco.Core/Media/EmbedProviders/X.cs @@ -18,11 +18,10 @@ public class X : OEmbedProviderBase public override Dictionary RequestParams => new(); + [Obsolete("Use GetMarkupAsync instead. This will be removed in v16.")] public override string? GetMarkup(string url, int maxWidth = 0, int maxHeight = 0) - { - var requestUrl = base.GetEmbedProviderUrl(url, maxWidth, maxHeight); - OEmbedResponse? oembed = base.GetJsonResponse(requestUrl); + => GetMarkupAsync(url, maxWidth, maxHeight, CancellationToken.None).GetAwaiter().GetResult(); - return oembed?.GetHtml(); - } + public override async Task GetMarkupAsync(string url, int? maxWidth, int? maxHeight, CancellationToken cancellationToken) + => await GetJsonBasedMarkupAsync(url, maxWidth, maxHeight, cancellationToken); } diff --git a/src/Umbraco.Core/Media/EmbedProviders/Youtube.cs b/src/Umbraco.Core/Media/EmbedProviders/Youtube.cs index c711f1d81f..ab093771d7 100644 --- a/src/Umbraco.Core/Media/EmbedProviders/Youtube.cs +++ b/src/Umbraco.Core/Media/EmbedProviders/Youtube.cs @@ -24,10 +24,12 @@ public class YouTube : OEmbedProviderBase [Obsolete("Use GetMarkupAsync instead. This will be removed in Umbraco 15.")] public override string? GetMarkup(string url, int maxWidth = 0, int maxHeight = 0) - { - return GeOEmbedDataAsync(url, maxWidth, maxHeight, CancellationToken.None).GetAwaiter().GetResult(); - } + => GetMarkupAsync(url, maxWidth, maxHeight, CancellationToken.None).GetAwaiter().GetResult(); + public override async Task GetMarkupAsync(string url, int? maxWidth, int? maxHeight, CancellationToken cancellationToken) + => await GetJsonBasedMarkupAsync(url, maxWidth, maxHeight, cancellationToken); + + [Obsolete("Use GetMarkupAsync instead. Planned for removal in v16")] public override async Task GeOEmbedDataAsync(string url, int? maxWidth, int? maxHeight, CancellationToken cancellationToken) { var requestUrl = base.GetEmbedProviderUrl(url, maxWidth, maxHeight); diff --git a/src/Umbraco.Core/Media/IEmbedProvider.cs b/src/Umbraco.Core/Media/IEmbedProvider.cs index fb27b4bb9b..64e0947abd 100644 --- a/src/Umbraco.Core/Media/IEmbedProvider.cs +++ b/src/Umbraco.Core/Media/IEmbedProvider.cs @@ -20,5 +20,6 @@ public interface IEmbedProvider [Obsolete("Use GetMarkupAsync instead. This will be removed in Umbraco 15.")] string? GetMarkup(string url, int maxWidth = 0, int maxHeight = 0); + Task GetMarkupAsync(string url, int? maxWidth, int? maxHeight, CancellationToken cancellationToken) => Task.FromResult(GetMarkup(url, maxWidth ?? 0, maxHeight ?? 0)); } diff --git a/src/Umbraco.Core/Models/CacheEntrySettings.cs b/src/Umbraco.Core/Models/CacheEntrySettings.cs index 21748b73f5..e0015505ab 100644 --- a/src/Umbraco.Core/Models/CacheEntrySettings.cs +++ b/src/Umbraco.Core/Models/CacheEntrySettings.cs @@ -16,4 +16,5 @@ public class CacheEntrySettings [DefaultValue(StaticSeedCacheDuration)] public TimeSpan SeedCacheDuration { get; set; } = TimeSpan.Parse(StaticSeedCacheDuration); + } diff --git a/src/Umbraco.Core/Models/CacheSettings.cs b/src/Umbraco.Core/Models/CacheSettings.cs index f478756f0c..e5c0d8827e 100644 --- a/src/Umbraco.Core/Models/CacheSettings.cs +++ b/src/Umbraco.Core/Models/CacheSettings.cs @@ -25,4 +25,13 @@ public class CacheSettings [Obsolete("Use Cache:Entry:Document:SeedCacheDuration instead")] [DefaultValue(StaticSeedCacheDuration)] public TimeSpan SeedCacheDuration { get; set; } = TimeSpan.Parse(StaticSeedCacheDuration); + + public CacheEntry Entry { get; set; } = new CacheEntry(); + + public class CacheEntry + { + public CacheEntrySettings Document { get; set; } = new CacheEntrySettings(); + + public CacheEntrySettings Media { get; set; } = new CacheEntrySettings(); + } } diff --git a/src/Umbraco.Core/Models/INavigationModel.cs b/src/Umbraco.Core/Models/INavigationModel.cs index 9663419627..574d1b7c94 100644 --- a/src/Umbraco.Core/Models/INavigationModel.cs +++ b/src/Umbraco.Core/Models/INavigationModel.cs @@ -12,6 +12,11 @@ public interface INavigationModel /// Guid Key { get; set; } + /// + /// Gets or sets the Guid unique identifier of the entity's content type. + /// + public Guid ContentTypeKey { get; set; } + /// /// Gets or sets the integer identifier of the parent entity. /// diff --git a/src/Umbraco.Core/Models/Membership/UserGroupExtensions.cs b/src/Umbraco.Core/Models/Membership/UserGroupExtensions.cs index 8c5940ac75..ac6c8ce765 100644 --- a/src/Umbraco.Core/Models/Membership/UserGroupExtensions.cs +++ b/src/Umbraco.Core/Models/Membership/UserGroupExtensions.cs @@ -30,13 +30,13 @@ public static class UserGroupExtensions } public static bool IsSystemUserGroup(this IUserGroup group) => - IsSystemUserGroup(group.Alias); + IsSystemUserGroup(group.Key); public static bool IsSystemUserGroup(this IReadOnlyUserGroup group) => - IsSystemUserGroup(group.Alias); + IsSystemUserGroup(group.Key); - private static bool IsSystemUserGroup(this string? groupAlias) => - groupAlias == Constants.Security.AdminGroupAlias - || groupAlias == Constants.Security.SensitiveDataGroupAlias - || groupAlias == Constants.Security.TranslatorGroupAlias; + private static bool IsSystemUserGroup(this Guid? groupKey) => + groupKey == Constants.Security.AdminGroupKey + || groupKey == Constants.Security.SensitiveDataGroupKey + || groupKey == Constants.Security.TranslatorGroupKey; } diff --git a/src/Umbraco.Core/Models/Navigation/NavigationNode.cs b/src/Umbraco.Core/Models/Navigation/NavigationNode.cs index 5e8e412116..d2db0c6294 100644 --- a/src/Umbraco.Core/Models/Navigation/NavigationNode.cs +++ b/src/Umbraco.Core/Models/Navigation/NavigationNode.cs @@ -8,15 +8,18 @@ public sealed class NavigationNode public Guid Key { get; private set; } + public Guid ContentTypeKey { get; private set; } + public int SortOrder { get; private set; } public Guid? Parent { get; private set; } public ISet Children => _children; - public NavigationNode(Guid key, int sortOrder = 0) + public NavigationNode(Guid key, Guid contentTypeKey, int sortOrder = 0) { Key = key; + ContentTypeKey = contentTypeKey; SortOrder = sortOrder; _children = new HashSet(); } diff --git a/src/Umbraco.Core/Routing/ContentFinderByIdPath.cs b/src/Umbraco.Core/Routing/ContentFinderByIdPath.cs index 86a559b5db..32bca4aa45 100644 --- a/src/Umbraco.Core/Routing/ContentFinderByIdPath.cs +++ b/src/Umbraco.Core/Routing/ContentFinderByIdPath.cs @@ -53,7 +53,7 @@ public class ContentFinderByIdPath : ContentFinderByIdentifierPathBase, IContent return Task.FromResult(false); } - if (umbracoContext.InPreviewMode == false && (_webRoutingSettings.DisableFindContentByIdPath || _webRoutingSettings.DisableFindContentByIdentifierPath)) + if (umbracoContext.InPreviewMode == false && _webRoutingSettings.DisableFindContentByIdentifierPath) { return Task.FromResult(false); } diff --git a/src/Umbraco.Core/Services/DataTypeService.cs b/src/Umbraco.Core/Services/DataTypeService.cs index 7797a30015..f22948968a 100644 --- a/src/Umbraco.Core/Services/DataTypeService.cs +++ b/src/Umbraco.Core/Services/DataTypeService.cs @@ -292,15 +292,6 @@ namespace Umbraco.Cms.Core.Services.Implement return dataType; } - /// - /// Gets a by its unique guid Id - /// - /// Unique guid Id of the DataType - /// - [Obsolete("Please use GetAsync. Will be removed in V15.")] - public IDataType? GetDataType(Guid id) - => GetAsync(id).GetAwaiter().GetResult(); - /// public Task GetAsync(Guid id) { diff --git a/src/Umbraco.Core/Services/DateTypeServiceExtensions.cs b/src/Umbraco.Core/Services/DateTypeServiceExtensions.cs index c330a03772..ac7e8550b2 100644 --- a/src/Umbraco.Core/Services/DateTypeServiceExtensions.cs +++ b/src/Umbraco.Core/Services/DateTypeServiceExtensions.cs @@ -13,7 +13,7 @@ public static class DateTypeServiceExtensions return false; // built in ones can never be ignoring start nodes } - IDataType? dataType = dataTypeService.GetDataType(key); + IDataType? dataType = dataTypeService.GetAsync(key).GetAwaiter().GetResult(); if (dataType != null && dataType.ConfigurationObject is IIgnoreUserStartNodesConfig ignoreStartNodesConfig) { diff --git a/src/Umbraco.Core/Services/DocumentUrlService.cs b/src/Umbraco.Core/Services/DocumentUrlService.cs index 7dc95deacc..95e93c58eb 100644 --- a/src/Umbraco.Core/Services/DocumentUrlService.cs +++ b/src/Umbraco.Core/Services/DocumentUrlService.cs @@ -439,7 +439,7 @@ public class DocumentUrlService : IDocumentUrlService return "#"; } - if(isDraft is false && culture != null && _publishStatusQueryService.IsDocumentPublished(documentKey, culture) is false) + if(isDraft is false && string.IsNullOrWhiteSpace(culture) is false && _publishStatusQueryService.IsDocumentPublished(documentKey, culture) is false) { return "#"; } @@ -486,6 +486,13 @@ public class DocumentUrlService : IDocumentUrlService } } + var leftToRight = _globalSettings.ForceCombineUrlPathLeftToRight + || CultureInfo.GetCultureInfo(cultureOrDefault).TextInfo.IsRightToLeft is false; + if (leftToRight) + { + urlSegments.Reverse(); + } + if (foundDomain is not null) { //we found a domain, and not to construct the route in the funny legacy way @@ -493,7 +500,7 @@ public class DocumentUrlService : IDocumentUrlService } var isRootFirstItem = GetTopMostRootKey(isDraft, cultureOrDefault) == ancestorsOrSelfKeysArray.Last(); - return GetFullUrl(isRootFirstItem, urlSegments, null); + return GetFullUrl(isRootFirstItem, urlSegments, null, leftToRight); } public bool HasAny() @@ -546,8 +553,15 @@ public class DocumentUrlService : IDocumentUrlService Dictionary domainDictionary = await domainDictionaryTask; if (domainDictionary.TryGetValue(culture, out Domain? domain)) { - foundDomain = domain; - break; + Attempt domainKeyAttempt = _idKeyMap.GetKeyForId(domain.ContentId, UmbracoObjectTypes.Document); + if (domainKeyAttempt.Success) + { + if (_publishStatusQueryService.IsDocumentPublished(domainKeyAttempt.Result, culture)) + { + foundDomain = domain; + break; + } + } } } @@ -568,8 +582,16 @@ public class DocumentUrlService : IDocumentUrlService } var isRootFirstItem = GetTopMostRootKey(false, culture) == ancestorsOrSelfKeysArray.Last(); + + var leftToRight = _globalSettings.ForceCombineUrlPathLeftToRight + || CultureInfo.GetCultureInfo(culture).TextInfo.IsRightToLeft is false; + if (leftToRight) + { + urlSegments.Reverse(); + } + result.Add(new UrlInfo( - text: GetFullUrl(isRootFirstItem, urlSegments, foundDomain), + text: GetFullUrl(isRootFirstItem, urlSegments, foundDomain, leftToRight), isUrl: hasUrlInCulture, culture: culture )); @@ -579,17 +601,27 @@ public class DocumentUrlService : IDocumentUrlService return result; } - private string GetFullUrl(bool isRootFirstItem, List reversedUrlSegments, Domain? foundDomain) + private string GetFullUrl(bool isRootFirstItem, List segments, Domain? foundDomain, bool leftToRight) { - var urlSegments = new List(reversedUrlSegments); - urlSegments.Reverse(); + var urlSegments = new List(segments); if (foundDomain is not null) { return foundDomain.Name.EnsureEndsWith("/") + string.Join('/', urlSegments); } - return '/' + string.Join('/', urlSegments.Skip(_globalSettings.HideTopLevelNodeFromPath && isRootFirstItem ? 1 : 0)); + var hideTopLevel = _globalSettings.HideTopLevelNodeFromPath && isRootFirstItem; + if (leftToRight) + { + return '/' + string.Join('/', urlSegments.Skip(hideTopLevel ? 1 : 0)); + } + + if (hideTopLevel) + { + urlSegments.RemoveAt(urlSegments.Count - 1); + } + + return '/' + string.Join('/', urlSegments); } public async Task CreateOrUpdateUrlSegmentsWithDescendantsAsync(Guid key) diff --git a/src/Umbraco.Core/Services/IDataTypeService.cs b/src/Umbraco.Core/Services/IDataTypeService.cs index e2f808cc7e..3a4576552c 100644 --- a/src/Umbraco.Core/Services/IDataTypeService.cs +++ b/src/Umbraco.Core/Services/IDataTypeService.cs @@ -76,16 +76,6 @@ public interface IDataTypeService : IService [Obsolete("Please use GetAsync. Will be removed in V15.")] IDataType? GetDataType(int id); - /// - /// Gets a by its unique guid Id - /// - /// Unique guid Id of the DataType - /// - /// - /// - [Obsolete("Please use GetAsync. Will be removed in V15.")] - IDataType? GetDataType(Guid id); - /// /// Gets an by its Name /// diff --git a/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs b/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs index 9a19e31dd1..531b3952a7 100644 --- a/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs +++ b/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs @@ -7,19 +7,25 @@ using Umbraco.Cms.Core.Scoping; namespace Umbraco.Cms.Core.Services.Navigation; -internal abstract class ContentNavigationServiceBase +internal abstract class ContentNavigationServiceBase + where TContentType : class, IContentTypeComposition + where TContentTypeService : IContentTypeBaseService { private readonly ICoreScopeProvider _coreScopeProvider; private readonly INavigationRepository _navigationRepository; + private readonly TContentTypeService _typeService; + private Lazy> _contentTypeAliasToKeyMap; private ConcurrentDictionary _navigationStructure = new(); private ConcurrentDictionary _recycleBinNavigationStructure = new(); private IList _roots = new List(); private IList _recycleBinRoots = new List(); - protected ContentNavigationServiceBase(ICoreScopeProvider coreScopeProvider, INavigationRepository navigationRepository) + protected ContentNavigationServiceBase(ICoreScopeProvider coreScopeProvider, INavigationRepository navigationRepository, TContentTypeService typeService) { _coreScopeProvider = coreScopeProvider; _navigationRepository = navigationRepository; + _typeService = typeService; + _contentTypeAliasToKeyMap = new Lazy>(LoadContentTypes); } /// @@ -38,18 +44,78 @@ internal abstract class ContentNavigationServiceBase public bool TryGetRootKeys(out IEnumerable rootKeys) => TryGetRootKeysFromStructure(_roots, out rootKeys); + public bool TryGetRootKeysOfType(string contentTypeAlias, out IEnumerable rootKeys) + { + if (TryGetContentTypeKey(contentTypeAlias, out Guid? contentTypeKey)) + { + return TryGetRootKeysFromStructure(_roots, out rootKeys, contentTypeKey); + } + + // Content type alias doesn't exist + rootKeys = []; + return false; + } + public bool TryGetChildrenKeys(Guid parentKey, out IEnumerable childrenKeys) => TryGetChildrenKeysFromStructure(_navigationStructure, parentKey, out childrenKeys); + public bool TryGetChildrenKeysOfType(Guid parentKey, string contentTypeAlias, out IEnumerable childrenKeys) + { + if (TryGetContentTypeKey(contentTypeAlias, out Guid? contentTypeKey)) + { + return TryGetChildrenKeysFromStructure(_navigationStructure, parentKey, out childrenKeys, contentTypeKey); + } + + // Content type alias doesn't exist + childrenKeys = []; + return false; + } + public bool TryGetDescendantsKeys(Guid parentKey, out IEnumerable descendantsKeys) => TryGetDescendantsKeysFromStructure(_navigationStructure, parentKey, out descendantsKeys); + public bool TryGetDescendantsKeysOfType(Guid parentKey, string contentTypeAlias, out IEnumerable descendantsKeys) + { + if (TryGetContentTypeKey(contentTypeAlias, out Guid? contentTypeKey)) + { + return TryGetDescendantsKeysFromStructure(_navigationStructure, parentKey, out descendantsKeys, contentTypeKey); + } + + // Content type alias doesn't exist + descendantsKeys = []; + return false; + } + public bool TryGetAncestorsKeys(Guid childKey, out IEnumerable ancestorsKeys) => TryGetAncestorsKeysFromStructure(_navigationStructure, childKey, out ancestorsKeys); + public bool TryGetAncestorsKeysOfType(Guid parentKey, string contentTypeAlias, out IEnumerable ancestorsKeys) + { + if (TryGetContentTypeKey(contentTypeAlias, out Guid? contentTypeKey)) + { + return TryGetAncestorsKeysFromStructure(_navigationStructure, parentKey, out ancestorsKeys, contentTypeKey); + } + + // Content type alias doesn't exist + ancestorsKeys = []; + return false; + } + public bool TryGetSiblingsKeys(Guid key, out IEnumerable siblingsKeys) => TryGetSiblingsKeysFromStructure(_navigationStructure, key, out siblingsKeys); + public bool TryGetSiblingsKeysOfType(Guid key, string contentTypeAlias, out IEnumerable siblingsKeys) + { + if (TryGetContentTypeKey(contentTypeAlias, out Guid? contentTypeKey)) + { + return TryGetSiblingsKeysFromStructure(_navigationStructure, key, out siblingsKeys, contentTypeKey); + } + + // Content type alias doesn't exist + siblingsKeys = []; + return false; + } + public bool TryGetParentKeyInBin(Guid childKey, out Guid? parentKey) => TryGetParentKeyFromStructure(_recycleBinNavigationStructure, childKey, out parentKey); @@ -104,7 +170,7 @@ internal abstract class ContentNavigationServiceBase _navigationStructure.TryRemove(key, out _); } - public bool Add(Guid key, Guid? parentKey = null, int? sortOrder = null) + public bool Add(Guid key, Guid contentTypeKey, Guid? parentKey = null, int? sortOrder = null) { NavigationNode? parentNode = null; if (parentKey.HasValue) @@ -120,7 +186,7 @@ internal abstract class ContentNavigationServiceBase } // Note: sortOrder can't be automatically determined for items at root level, so it needs to be passed in - var newNode = new NavigationNode(key, sortOrder ?? 0); + var newNode = new NavigationNode(key, contentTypeKey, sortOrder ?? 0); if (_navigationStructure.TryAdd(key, newNode) is false) { return false; // Node with this key already exists @@ -264,18 +330,30 @@ internal abstract class ContentNavigationServiceBase return false; } - private bool TryGetRootKeysFromStructure(IList input, out IEnumerable rootKeys) + private bool TryGetRootKeysFromStructure( + IList input, + out IEnumerable rootKeys, + Guid? contentTypeKey = null) { + // Apply contentTypeKey filter + IEnumerable filteredKeys = contentTypeKey.HasValue + ? input.Where(key => _navigationStructure[key].ContentTypeKey == contentTypeKey.Value) + : input; + // TODO can we make this more efficient? // Sort by SortOrder - rootKeys = input + rootKeys = filteredKeys .OrderBy(key => _navigationStructure[key].SortOrder) .ToList(); return true; } - private bool TryGetChildrenKeysFromStructure(ConcurrentDictionary structure, Guid parentKey, out IEnumerable childrenKeys) + private bool TryGetChildrenKeysFromStructure( + ConcurrentDictionary structure, + Guid parentKey, + out IEnumerable childrenKeys, + Guid? contentTypeKey = null) { if (structure.TryGetValue(parentKey, out NavigationNode? parentNode) is false) { @@ -285,12 +363,16 @@ internal abstract class ContentNavigationServiceBase } // Keep children keys ordered based on their SortOrder - childrenKeys = GetOrderedChildren(parentNode, structure).ToList(); + childrenKeys = GetOrderedChildren(parentNode, structure, contentTypeKey).ToList(); return true; } - private bool TryGetDescendantsKeysFromStructure(ConcurrentDictionary structure, Guid parentKey, out IEnumerable descendantsKeys) + private bool TryGetDescendantsKeysFromStructure( + ConcurrentDictionary structure, + Guid parentKey, + out IEnumerable descendantsKeys, + Guid? contentTypeKey = null) { var descendants = new List(); @@ -301,13 +383,17 @@ internal abstract class ContentNavigationServiceBase return false; } - GetDescendantsRecursively(structure, parentNode, descendants); + GetDescendantsRecursively(structure, parentNode, descendants, contentTypeKey); descendantsKeys = descendants; return true; } - private bool TryGetAncestorsKeysFromStructure(ConcurrentDictionary structure, Guid childKey, out IEnumerable ancestorsKeys) + private bool TryGetAncestorsKeysFromStructure( + ConcurrentDictionary structure, + Guid childKey, + out IEnumerable ancestorsKeys, + Guid? contentTypeKey = null) { var ancestors = new List(); @@ -320,14 +406,22 @@ internal abstract class ContentNavigationServiceBase while (node.Parent is not null && structure.TryGetValue(node.Parent.Value, out node)) { - ancestors.Add(node.Key); + // Apply contentTypeKey filter + if (contentTypeKey.HasValue is false || node.ContentTypeKey == contentTypeKey.Value) + { + ancestors.Add(node.Key); + } } ancestorsKeys = ancestors; return true; } - private bool TryGetSiblingsKeysFromStructure(ConcurrentDictionary structure, Guid key, out IEnumerable siblingsKeys) + private bool TryGetSiblingsKeysFromStructure( + ConcurrentDictionary structure, + Guid key, + out IEnumerable siblingsKeys, + Guid? contentTypeKey = null) { siblingsKeys = []; @@ -339,15 +433,23 @@ internal abstract class ContentNavigationServiceBase if (node.Parent is null) { // To find siblings of a node at root level, we need to iterate over all items and add those with null Parent - siblingsKeys = structure - .Where(kv => kv.Value.Parent is null && kv.Key != key) + IEnumerable> filteredSiblings = structure + .Where(kv => kv.Value.Parent is null && kv.Key != key); + + // Apply contentTypeKey filter + if (contentTypeKey.HasValue) + { + filteredSiblings = filteredSiblings.Where(kv => kv.Value.ContentTypeKey == contentTypeKey.Value); + } + + siblingsKeys = filteredSiblings .OrderBy(kv => kv.Value.SortOrder) .Select(kv => kv.Key) .ToList(); return true; } - if (TryGetChildrenKeys(node.Parent.Value, out IEnumerable childrenKeys) is false) + if (TryGetChildrenKeysFromStructure(structure, node.Parent.Value, out IEnumerable childrenKeys, contentTypeKey) is false) { return false; // Couldn't retrieve children keys } @@ -357,17 +459,26 @@ internal abstract class ContentNavigationServiceBase return true; } - private void GetDescendantsRecursively(ConcurrentDictionary structure, NavigationNode node, List descendants) + private void GetDescendantsRecursively( + ConcurrentDictionary structure, + NavigationNode node, + List descendants, + Guid? contentTypeKey = null) { + // Get all children regardless of contentType var childrenKeys = GetOrderedChildren(node, structure).ToList(); foreach (Guid childKey in childrenKeys) { - descendants.Add(childKey); + // Apply contentTypeKey filter + if (contentTypeKey.HasValue is false || structure[childKey].ContentTypeKey == contentTypeKey.Value) + { + descendants.Add(childKey); + } // Retrieve the child node and its descendants if (structure.TryGetValue(childKey, out NavigationNode? childNode)) { - GetDescendantsRecursively(structure, childNode, descendants); + GetDescendantsRecursively(structure, childNode, descendants, contentTypeKey); } } } @@ -455,11 +566,48 @@ internal abstract class ContentNavigationServiceBase } } - private IEnumerable GetOrderedChildren(NavigationNode node, ConcurrentDictionary structure) - => node.Children - .Where(structure.ContainsKey) + private IEnumerable GetOrderedChildren( + NavigationNode node, + ConcurrentDictionary structure, + Guid? contentTypeKey = null) + { + IEnumerable children = node + .Children + .Where(structure.ContainsKey); + + // Apply contentTypeKey filter + if (contentTypeKey.HasValue) + { + children = children.Where(childKey => structure[childKey].ContentTypeKey == contentTypeKey.Value); + } + + return children .OrderBy(childKey => structure[childKey].SortOrder) .ToList(); + } + + private bool TryGetContentTypeKey(string contentTypeAlias, out Guid? contentTypeKey) + { + Dictionary aliasToKeyMap = _contentTypeAliasToKeyMap.Value; + + if (aliasToKeyMap.TryGetValue(contentTypeAlias, out Guid key)) + { + contentTypeKey = key; + return true; + } + + TContentType? contentType = _typeService.Get(contentTypeAlias); + if (contentType is null) + { + // Content type alias doesn't exist + contentTypeKey = null; + return false; + } + + aliasToKeyMap.TryAdd(contentTypeAlias, contentType.Key); + contentTypeKey = contentType.Key; + return true; + } private static void BuildNavigationDictionary(ConcurrentDictionary nodesStructure, IList roots, IEnumerable entities) { @@ -468,7 +616,7 @@ internal abstract class ContentNavigationServiceBase foreach (INavigationModel entity in entityList) { - var node = new NavigationNode(entity.Key, entity.SortOrder); + var node = new NavigationNode(entity.Key, entity.ContentTypeKey, entity.SortOrder); nodesStructure[entity.Key] = node; // We don't set the parent for items under root, it will stay null @@ -490,4 +638,7 @@ internal abstract class ContentNavigationServiceBase } } } + + private Dictionary LoadContentTypes() + => _typeService.GetAll().ToDictionary(ct => ct.Alias, ct => ct.Key); } diff --git a/src/Umbraco.Core/Services/Navigation/DocumentNavigationService.cs b/src/Umbraco.Core/Services/Navigation/DocumentNavigationService.cs index 44804d07c6..0f550be206 100644 --- a/src/Umbraco.Core/Services/Navigation/DocumentNavigationService.cs +++ b/src/Umbraco.Core/Services/Navigation/DocumentNavigationService.cs @@ -1,12 +1,13 @@ +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; namespace Umbraco.Cms.Core.Services.Navigation; -internal sealed class DocumentNavigationService : ContentNavigationServiceBase, IDocumentNavigationQueryService, IDocumentNavigationManagementService +internal sealed class DocumentNavigationService : ContentNavigationServiceBase, IDocumentNavigationQueryService, IDocumentNavigationManagementService { - public DocumentNavigationService(ICoreScopeProvider coreScopeProvider, INavigationRepository navigationRepository) - : base(coreScopeProvider, navigationRepository) + public DocumentNavigationService(ICoreScopeProvider coreScopeProvider, INavigationRepository navigationRepository, IContentTypeService contentTypeService) + : base(coreScopeProvider, navigationRepository, contentTypeService) { } diff --git a/src/Umbraco.Core/Services/Navigation/INavigationManagementService.cs b/src/Umbraco.Core/Services/Navigation/INavigationManagementService.cs index 80dce527e1..cf3d172278 100644 --- a/src/Umbraco.Core/Services/Navigation/INavigationManagementService.cs +++ b/src/Umbraco.Core/Services/Navigation/INavigationManagementService.cs @@ -29,6 +29,7 @@ public interface INavigationManagementService /// provided, the new node is added at the root level. /// /// The unique identifier of the new node to add. + /// The unique identifier of the node's content type. /// /// The unique identifier of the parent node. If null, the new node will be added to /// the root level. @@ -46,7 +47,7 @@ public interface INavigationManagementService /// when adding nodes directly to the root (where parentKey is null), a sort order must be provided /// to ensure the item appears in the correct position among other root-level items. /// - bool Add(Guid key, Guid? parentKey = null, int? sortOrder = null); + bool Add(Guid key, Guid contentTypeKey, Guid? parentKey = null, int? sortOrder = null); /// /// Moves an existing node to a new parent in the main navigation structure. If a diff --git a/src/Umbraco.Core/Services/Navigation/INavigationQueryService.cs b/src/Umbraco.Core/Services/Navigation/INavigationQueryService.cs index 984bf35efd..6ea28c8511 100644 --- a/src/Umbraco.Core/Services/Navigation/INavigationQueryService.cs +++ b/src/Umbraco.Core/Services/Navigation/INavigationQueryService.cs @@ -13,15 +13,19 @@ public interface INavigationQueryService bool TryGetRootKeys(out IEnumerable rootKeys); + bool TryGetRootKeysOfType(string contentTypeAlias, out IEnumerable rootKeys); + bool TryGetChildrenKeys(Guid parentKey, out IEnumerable childrenKeys); + bool TryGetChildrenKeysOfType(Guid parentKey, string contentTypeAlias, out IEnumerable childrenKeys); + bool TryGetDescendantsKeys(Guid parentKey, out IEnumerable descendantsKeys); - bool TryGetDescendantsKeysOrSelfKeys(Guid childKey, out IEnumerable descendantsOrSelfKeys) + bool TryGetDescendantsKeysOrSelfKeys(Guid parentKey, out IEnumerable descendantsOrSelfKeys) { - if (TryGetDescendantsKeys(childKey, out IEnumerable? descendantsKeys)) + if (TryGetDescendantsKeys(parentKey, out IEnumerable? descendantsKeys)) { - descendantsOrSelfKeys = childKey.Yield().Concat(descendantsKeys); + descendantsOrSelfKeys = parentKey.Yield().Concat(descendantsKeys); return true; } @@ -29,6 +33,8 @@ public interface INavigationQueryService return false; } + bool TryGetDescendantsKeysOfType(Guid parentKey, string contentTypeAlias, out IEnumerable descendantsKeys); + bool TryGetAncestorsKeys(Guid childKey, out IEnumerable ancestorsKeys); bool TryGetAncestorsOrSelfKeys(Guid childKey, out IEnumerable ancestorsOrSelfKeys) @@ -43,7 +49,11 @@ public interface INavigationQueryService return false; } + bool TryGetAncestorsKeysOfType(Guid parentKey, string contentTypeAlias, out IEnumerable ancestorsKeys); + bool TryGetSiblingsKeys(Guid key, out IEnumerable siblingsKeys); + bool TryGetSiblingsKeysOfType(Guid key, string contentTypeAlias, out IEnumerable siblingsKeys); + bool TryGetLevel(Guid contentKey, [NotNullWhen(true)] out int? level); } diff --git a/src/Umbraco.Core/Services/Navigation/IRecycleBinNavigationQueryService.cs b/src/Umbraco.Core/Services/Navigation/IRecycleBinNavigationQueryService.cs index 9741e86e1f..12578d5875 100644 --- a/src/Umbraco.Core/Services/Navigation/IRecycleBinNavigationQueryService.cs +++ b/src/Umbraco.Core/Services/Navigation/IRecycleBinNavigationQueryService.cs @@ -16,7 +16,7 @@ public interface IRecycleBinNavigationQueryService bool TryGetDescendantsKeysOrSelfKeysInBin(Guid childKey, out IEnumerable descendantsOrSelfKeys) { - if(TryGetDescendantsKeysInBin(childKey, out var descendantsKeys)) + if (TryGetDescendantsKeysInBin(childKey, out IEnumerable? descendantsKeys)) { descendantsOrSelfKeys = childKey.Yield().Concat(descendantsKeys); return true; diff --git a/src/Umbraco.Core/Services/Navigation/MediaNavigationService.cs b/src/Umbraco.Core/Services/Navigation/MediaNavigationService.cs index 62ab5a1617..5a0c3c47d6 100644 --- a/src/Umbraco.Core/Services/Navigation/MediaNavigationService.cs +++ b/src/Umbraco.Core/Services/Navigation/MediaNavigationService.cs @@ -1,12 +1,13 @@ +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; namespace Umbraco.Cms.Core.Services.Navigation; -internal sealed class MediaNavigationService : ContentNavigationServiceBase, IMediaNavigationQueryService, IMediaNavigationManagementService +internal sealed class MediaNavigationService : ContentNavigationServiceBase, IMediaNavigationQueryService, IMediaNavigationManagementService { - public MediaNavigationService(ICoreScopeProvider coreScopeProvider, INavigationRepository navigationRepository) - : base(coreScopeProvider, navigationRepository) + public MediaNavigationService(ICoreScopeProvider coreScopeProvider, INavigationRepository navigationRepository, IMediaTypeService mediaTypeService) + : base(coreScopeProvider, navigationRepository, mediaTypeService) { } diff --git a/src/Umbraco.Core/Services/NotificationService.cs b/src/Umbraco.Core/Services/NotificationService.cs index 142738d49f..e12183a1a6 100644 --- a/src/Umbraco.Core/Services/NotificationService.cs +++ b/src/Umbraco.Core/Services/NotificationService.cs @@ -461,7 +461,7 @@ public class NotificationService : INotificationService var protocol = _globalSettings.UseHttps ? "https" : "http"; var subjectVars = new NotificationEmailSubjectParams( - string.Concat(siteUri.Authority, _ioHelper.ResolveUrl(_globalSettings.UmbracoPath)), + string.Concat(siteUri.Authority, _ioHelper.ResolveUrl(Constants.System.DefaultUmbracoPath)), actionName, content.Name); @@ -479,7 +479,7 @@ public class NotificationService : INotificationService string.Concat(content.Id, ".aspx"), protocol), performingUser.Name, - string.Concat(siteUri.Authority, _ioHelper.ResolveUrl(_globalSettings.UmbracoPath)), + string.Concat(siteUri.Authority, _ioHelper.ResolveUrl(Constants.System.DefaultUmbracoPath)), summary.ToString()); var fromMail = _contentSettings.Notifications.Email ?? _globalSettings.Smtp?.From; diff --git a/src/Umbraco.Core/Services/OperationStatus/UserClientCredentialsOperationStatus.cs b/src/Umbraco.Core/Services/OperationStatus/UserClientCredentialsOperationStatus.cs index c33fd829b4..585d87c47a 100644 --- a/src/Umbraco.Core/Services/OperationStatus/UserClientCredentialsOperationStatus.cs +++ b/src/Umbraco.Core/Services/OperationStatus/UserClientCredentialsOperationStatus.cs @@ -4,5 +4,6 @@ public enum UserClientCredentialsOperationStatus { Success, DuplicateClientId, - InvalidUser + InvalidUser, + InvalidClientId } diff --git a/src/Umbraco.Core/Services/UserService.cs b/src/Umbraco.Core/Services/UserService.cs index 07da9f631a..3463516cc2 100644 --- a/src/Umbraco.Core/Services/UserService.cs +++ b/src/Umbraco.Core/Services/UserService.cs @@ -4,6 +4,7 @@ using System.Linq.Expressions; using Microsoft.Extensions.DependencyInjection; using System.Security.Claims; using System.Security.Cryptography; +using System.Text.RegularExpressions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Cache; @@ -35,7 +36,7 @@ namespace Umbraco.Cms.Core.Services; /// Represents the UserService, which is an easy access to operations involving , /// and eventually Backoffice Users. /// -internal class UserService : RepositoryService, IUserService +internal partial class UserService : RepositoryService, IUserService { private readonly GlobalSettings _globalSettings; private readonly SecuritySettings _securitySettings; @@ -477,6 +478,7 @@ internal class UserService : RepositoryService, IUserService /// This is just the default user group that the membership provider will use /// /// + [Obsolete("No (backend) code path is using this anymore, so it can not be considered the default. Planned for removal in V16.")] public string GetDefaultMemberType() => Constants.Security.WriterGroupAlias; /// @@ -2483,6 +2485,11 @@ internal class UserService : RepositoryService, IUserService public async Task AddClientIdAsync(Guid userKey, string clientId) { + if (ValidClientId().IsMatch(clientId) is false) + { + return UserClientCredentialsOperationStatus.InvalidClientId; + } + using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true); IEnumerable currentClientIds = _userRepository.GetAllClientIds(); @@ -2670,5 +2677,8 @@ internal class UserService : RepositoryService, IUserService } } + [GeneratedRegex(@"^[\w\d\-\._~]*$")] + private static partial Regex ValidClientId(); + #endregion } diff --git a/src/Umbraco.Core/UdiParser.cs b/src/Umbraco.Core/UdiParser.cs index 24ee238a69..ed1f16ae28 100644 --- a/src/Umbraco.Core/UdiParser.cs +++ b/src/Umbraco.Core/UdiParser.cs @@ -200,34 +200,40 @@ public sealed class UdiParser new() { { Constants.UdiEntityType.Unknown, UdiType.Unknown }, + // GUID UDI types { Constants.UdiEntityType.AnyGuid, UdiType.GuidUdi }, - { Constants.UdiEntityType.Element, UdiType.GuidUdi }, + { Constants.UdiEntityType.DataType, UdiType.GuidUdi }, + { Constants.UdiEntityType.DataTypeContainer, UdiType.GuidUdi }, + { Constants.UdiEntityType.DictionaryItem, UdiType.GuidUdi }, { Constants.UdiEntityType.Document, UdiType.GuidUdi }, { Constants.UdiEntityType.DocumentBlueprint, UdiType.GuidUdi }, { Constants.UdiEntityType.DocumentBlueprintContainer, UdiType.GuidUdi }, - { Constants.UdiEntityType.Media, UdiType.GuidUdi }, - { Constants.UdiEntityType.Member, UdiType.GuidUdi }, - { Constants.UdiEntityType.DictionaryItem, UdiType.GuidUdi }, - { Constants.UdiEntityType.Template, UdiType.GuidUdi }, { Constants.UdiEntityType.DocumentType, UdiType.GuidUdi }, { Constants.UdiEntityType.DocumentTypeContainer, UdiType.GuidUdi }, + { Constants.UdiEntityType.Element, UdiType.GuidUdi }, + { Constants.UdiEntityType.Media, UdiType.GuidUdi }, { Constants.UdiEntityType.MediaType, UdiType.GuidUdi }, { Constants.UdiEntityType.MediaTypeContainer, UdiType.GuidUdi }, - { Constants.UdiEntityType.DataType, UdiType.GuidUdi }, - { Constants.UdiEntityType.DataTypeContainer, UdiType.GuidUdi }, - { Constants.UdiEntityType.MemberType, UdiType.GuidUdi }, + { Constants.UdiEntityType.Member, UdiType.GuidUdi }, { Constants.UdiEntityType.MemberGroup, UdiType.GuidUdi }, + { Constants.UdiEntityType.MemberType, UdiType.GuidUdi }, + { Constants.UdiEntityType.Relation, UdiType.GuidUdi }, { Constants.UdiEntityType.RelationType, UdiType.GuidUdi }, - { Constants.UdiEntityType.FormsForm, UdiType.GuidUdi }, - { Constants.UdiEntityType.FormsPreValue, UdiType.GuidUdi }, - { Constants.UdiEntityType.FormsDataSource, UdiType.GuidUdi }, + { Constants.UdiEntityType.Template, UdiType.GuidUdi }, + { Constants.UdiEntityType.User, UdiType.GuidUdi }, + { Constants.UdiEntityType.UserGroup, UdiType.GuidUdi }, + { Constants.UdiEntityType.Webhook, UdiType.GuidUdi }, + // String UDI types { Constants.UdiEntityType.AnyString, UdiType.StringUdi }, { Constants.UdiEntityType.Language, UdiType.StringUdi }, { Constants.UdiEntityType.MediaFile, UdiType.StringUdi }, - { Constants.UdiEntityType.TemplateFile, UdiType.StringUdi }, - { Constants.UdiEntityType.Script, UdiType.StringUdi }, { Constants.UdiEntityType.PartialView, UdiType.StringUdi }, + { Constants.UdiEntityType.Script, UdiType.StringUdi }, { Constants.UdiEntityType.Stylesheet, UdiType.StringUdi }, - { Constants.UdiEntityType.Webhook, UdiType.GuidUdi }, + { Constants.UdiEntityType.TemplateFile, UdiType.StringUdi }, + // Forms UDI types + { Constants.UdiEntityType.FormsDataSource, UdiType.GuidUdi }, + { Constants.UdiEntityType.FormsForm, UdiType.GuidUdi }, + { Constants.UdiEntityType.FormsPreValue, UdiType.GuidUdi }, }; } diff --git a/src/Umbraco.Core/UdiParserServiceConnectors.cs b/src/Umbraco.Core/UdiParserServiceConnectors.cs index 4c307435de..465431476e 100644 --- a/src/Umbraco.Core/UdiParserServiceConnectors.cs +++ b/src/Umbraco.Core/UdiParserServiceConnectors.cs @@ -4,6 +4,7 @@ using Umbraco.Extensions; namespace Umbraco.Cms.Core; +[Obsolete("This class will be removed in a future version.")] public static class UdiParserServiceConnectors { private static readonly object ScanLocker = new(); @@ -19,6 +20,7 @@ public static class UdiParserServiceConnectors /// Scan for deploy in assemblies for known UDI types. /// /// + [Obsolete("Use UdiParser.RegisterUdiType() instead. This method will be removed in a future version.")] public static void ScanDeployServiceConnectorsForUdiTypes(TypeLoader typeLoader) { if (typeLoader is null) @@ -72,6 +74,7 @@ public static class UdiParserServiceConnectors /// Registers a single to add it's UDI type. /// /// + [Obsolete("Use UdiParser.RegisterUdiType() instead. This method will be removed in a future version.")] public static void RegisterServiceConnector() where T : IServiceConnector { diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs index a7a71d3300..1f8647c410 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs @@ -321,7 +321,6 @@ public static partial class UmbracoBuilderExtensions .AddNotificationHandler() .AddNotificationHandler() .AddNotificationHandler() - .AddNotificationHandler() .AddNotificationHandler() .AddNotificationHandler() .AddNotificationHandler(); diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs index d9eefbe76f..eac8aca1b5 100644 --- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs @@ -196,7 +196,6 @@ internal class DatabaseDataCreator var userGroupKeyToPermissions = new Dictionary>() { [Constants.Security.AdminGroupKey] = new[] { ActionNew.ActionLetter, ActionUpdate.ActionLetter, ActionDelete.ActionLetter, ActionMove.ActionLetter, ActionCopy.ActionLetter, ActionSort.ActionLetter, ActionRollback.ActionLetter, ActionProtect.ActionLetter, ActionAssignDomain.ActionLetter, ActionPublish.ActionLetter, ActionRights.ActionLetter, ActionUnpublish.ActionLetter, ActionBrowse.ActionLetter, ActionCreateBlueprintFromContent.ActionLetter, ActionNotify.ActionLetter, ":", "5", "7", "T" }, - [Constants.Security.WriterGroupKey] = new[] { ActionNew.ActionLetter, ActionUpdate.ActionLetter, ActionToPublish.ActionLetter, ActionBrowse.ActionLetter, ActionNotify.ActionLetter, ":" }, [Constants.Security.EditorGroupKey] = new[] { ActionNew.ActionLetter, ActionUpdate.ActionLetter, ActionDelete.ActionLetter, ActionMove.ActionLetter, ActionCopy.ActionLetter, ActionSort.ActionLetter, ActionRollback.ActionLetter, ActionProtect.ActionLetter, ActionPublish.ActionLetter, ActionUnpublish.ActionLetter, ActionBrowse.ActionLetter, ActionCreateBlueprintFromContent.ActionLetter, ActionNotify.ActionLetter, ":", "5", "T" }, [Constants.Security.TranslatorGroupKey] = new[] { ActionUpdate.ActionLetter, ActionBrowse.ActionLetter }, }; diff --git a/src/Umbraco.Infrastructure/ModelsBuilder/Building/ModelsGenerator.cs b/src/Umbraco.Infrastructure/ModelsBuilder/Building/ModelsGenerator.cs index 0a668f2d45..408d89d143 100644 --- a/src/Umbraco.Infrastructure/ModelsBuilder/Building/ModelsGenerator.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/Building/ModelsGenerator.cs @@ -1,31 +1,58 @@ using System.Text; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; -using Umbraco.Cms.Core.Hosting; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Extensions; +using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment; namespace Umbraco.Cms.Infrastructure.ModelsBuilder.Building; public class ModelsGenerator : IModelsGenerator { - private readonly IHostingEnvironment _hostingEnvironment; private readonly OutOfDateModelsStatus _outOfDateModels; + private readonly IHostEnvironment _hostEnvironment; private readonly UmbracoServices _umbracoService; private ModelsBuilderSettings _config; + [Obsolete("Use a not obsoleted constructor instead. Scheduled for removal in v16")] public ModelsGenerator(UmbracoServices umbracoService, IOptionsMonitor config, OutOfDateModelsStatus outOfDateModels, IHostingEnvironment hostingEnvironment) { _umbracoService = umbracoService; _config = config.CurrentValue; _outOfDateModels = outOfDateModels; - _hostingEnvironment = hostingEnvironment; + config.OnChange(x => _config = x); + + _hostEnvironment = StaticServiceProvider.Instance.GetRequiredService(); + } + + [Obsolete("Use a not obsoleted constructor instead. Scheduled for removal in v16")] + public ModelsGenerator(UmbracoServices umbracoService, IOptionsMonitor config, + OutOfDateModelsStatus outOfDateModels, IHostingEnvironment hostingEnvironment, IHostEnvironment hostEnvironment) + { + _umbracoService = umbracoService; + _config = config.CurrentValue; + _outOfDateModels = outOfDateModels; + config.OnChange(x => _config = x); + + _hostEnvironment = hostEnvironment; + } + + public ModelsGenerator(UmbracoServices umbracoService, IOptionsMonitor config, + OutOfDateModelsStatus outOfDateModels, IHostEnvironment hostEnvironment) + { + _umbracoService = umbracoService; + _config = config.CurrentValue; + _outOfDateModels = outOfDateModels; + _hostEnvironment = hostEnvironment; config.OnChange(x => _config = x); } public void GenerateModels() { - var modelsDirectory = _config.ModelsDirectoryAbsolute(_hostingEnvironment); + var modelsDirectory = _config.ModelsDirectoryAbsolute(_hostEnvironment); if (!Directory.Exists(modelsDirectory)) { Directory.CreateDirectory(modelsDirectory); diff --git a/src/Umbraco.Infrastructure/ModelsBuilder/ModelsGenerationError.cs b/src/Umbraco.Infrastructure/ModelsBuilder/ModelsGenerationError.cs index 02db02afda..e7ee821875 100644 --- a/src/Umbraco.Infrastructure/ModelsBuilder/ModelsGenerationError.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/ModelsGenerationError.cs @@ -1,23 +1,48 @@ using System.Text; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; -using Umbraco.Cms.Core.Hosting; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Extensions; +using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment; namespace Umbraco.Cms.Infrastructure.ModelsBuilder; public sealed class ModelsGenerationError { - private readonly IHostingEnvironment _hostingEnvironment; + private readonly IHostEnvironment _hostEnvironment; private ModelsBuilderSettings _config; /// /// Initializes a new instance of the class. /// + [Obsolete("Use a not obsoleted constructor instead. Scheduled for removal in v16")] public ModelsGenerationError(IOptionsMonitor config, IHostingEnvironment hostingEnvironment) { _config = config.CurrentValue; - _hostingEnvironment = hostingEnvironment; + config.OnChange(x => _config = x); + _hostEnvironment = StaticServiceProvider.Instance.GetRequiredService(); + } + + /// + /// Initializes a new instance of the class. + /// + [Obsolete("Use a not obsoleted constructor instead. Scheduled for removal in v16")] + public ModelsGenerationError( + IOptionsMonitor config, + IHostingEnvironment hostingEnvironment, + IHostEnvironment hostEnvironment) + { + _config = config.CurrentValue; + config.OnChange(x => _config = x); + _hostEnvironment = hostEnvironment; + } + + public ModelsGenerationError(IOptionsMonitor config, IHostEnvironment hostEnvironment) + { + _config = config.CurrentValue; + _hostEnvironment = hostEnvironment; config.OnChange(x => _config = x); } @@ -73,7 +98,7 @@ public sealed class ModelsGenerationError private string? GetErrFile() { - var modelsDirectory = _config.ModelsDirectoryAbsolute(_hostingEnvironment); + var modelsDirectory = _config.ModelsDirectoryAbsolute(_hostEnvironment); if (!Directory.Exists(modelsDirectory)) { return null; diff --git a/src/Umbraco.Infrastructure/ModelsBuilder/OutOfDateModelsStatus.cs b/src/Umbraco.Infrastructure/ModelsBuilder/OutOfDateModelsStatus.cs index 4336f8ec71..e0f4ce5ba0 100644 --- a/src/Umbraco.Infrastructure/ModelsBuilder/OutOfDateModelsStatus.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/OutOfDateModelsStatus.cs @@ -1,9 +1,12 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Events; -using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Notifications; using Umbraco.Extensions; +using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment; namespace Umbraco.Cms.Infrastructure.ModelsBuilder; @@ -13,8 +16,8 @@ namespace Umbraco.Cms.Infrastructure.ModelsBuilder; public sealed class OutOfDateModelsStatus : INotificationHandler, INotificationHandler { - private readonly IHostingEnvironment _hostingEnvironment; private ModelsBuilderSettings _config; + private readonly IHostEnvironment _hostEnvironment; /// /// Initializes a new instance of the class. @@ -22,7 +25,24 @@ public sealed class OutOfDateModelsStatus : INotificationHandler config, IHostingEnvironment hostingEnvironment) { _config = config.CurrentValue; - _hostingEnvironment = hostingEnvironment; + _hostEnvironment = StaticServiceProvider.Instance.GetRequiredService(); + config.OnChange(x => _config = x); + } + + public OutOfDateModelsStatus( + IOptionsMonitor config, + IHostingEnvironment hostingEnvironment, + IHostEnvironment hostEnvironment) + { + _config = config.CurrentValue; + _hostEnvironment = hostEnvironment; + config.OnChange(x => _config = x); + } + + public OutOfDateModelsStatus(IOptionsMonitor config, IHostEnvironment hostEnvironment) + { + _config = config.CurrentValue; + _hostEnvironment = hostEnvironment; config.OnChange(x => _config = x); } @@ -70,7 +90,7 @@ public sealed class OutOfDateModelsStatus : INotificationHandler [Column(NodeDto.IdColumnName)] public int Id { get; set; } @@ -15,6 +18,9 @@ internal class NavigationDto : INavigationModel [Column(NodeDto.KeyColumnName)] public Guid Key { get; set; } + [Column(ContentTypeKeyColumnName)] + public Guid ContentTypeKey { get; set; } + /// [Column(NodeDto.ParentIdColumnName)] public int ParentId { get; set; } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentNavigationRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentNavigationRepository.cs index 2f86d00143..b2553987d4 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentNavigationRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentNavigationRepository.cs @@ -27,10 +27,18 @@ public class ContentNavigationRepository : INavigationRepository private IEnumerable FetchNavigationDtos(Guid objectTypeKey, bool trashed) { Sql? sql = AmbientScope?.SqlContext.Sql() - .Select() - .From() - .Where(x => x.NodeObjectType == objectTypeKey && x.Trashed == trashed) - .OrderBy(x => x.Path); // make sure that we get the parent items first + .Select( + $"n.{NodeDto.IdColumnName} as {NodeDto.IdColumnName}", + $"n.{NodeDto.KeyColumnName} as {NodeDto.KeyColumnName}", + $"ctn.{NodeDto.KeyColumnName} as {NavigationDto.ContentTypeKeyColumnName}", + $"n.{NodeDto.ParentIdColumnName} as {NodeDto.ParentIdColumnName}", + $"n.{NodeDto.SortOrderColumnName} as {NodeDto.SortOrderColumnName}", + $"n.{NodeDto.TrashedColumnName} as {NodeDto.TrashedColumnName}") + .From("n") + .InnerJoin("c").On((n, c) => n.NodeId == c.NodeId, "n", "c") + .InnerJoin("ctn").On((c, ctn) => c.ContentTypeId == ctn.NodeId, "c", "ctn") + .Where(n => n.NodeObjectType == objectTypeKey && n.Trashed == trashed, "n") + .OrderBy(n => n.Path, "n"); // make sure that we get the parent items first return AmbientScope?.Database.Fetch(sql) ?? Enumerable.Empty(); } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyValueEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyValueEditor.cs index 798cdf1d7d..a43921156a 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyValueEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyValueEditor.cs @@ -35,7 +35,9 @@ public abstract class BlockEditorPropertyValueEditor : BlockVal IIOHelper ioHelper) : this(propertyEditors, dataValueReferenceFactories, dataTypeConfigurationCache, shortStringHelper, jsonSerializer, StaticServiceProvider.Instance.GetRequiredService(), - StaticServiceProvider.Instance.GetRequiredService()) + StaticServiceProvider.Instance.GetRequiredService(), + ioHelper, + attribute) { } @@ -46,8 +48,10 @@ public abstract class BlockEditorPropertyValueEditor : BlockVal IShortStringHelper shortStringHelper, IJsonSerializer jsonSerializer, BlockEditorVarianceHandler blockEditorVarianceHandler, - ILanguageService languageService) - : base(propertyEditors, dataTypeConfigurationCache, shortStringHelper, jsonSerializer, dataValueReferenceFactories, blockEditorVarianceHandler, languageService) => + ILanguageService languageService, + IIOHelper ioHelper, + DataEditorAttribute attribute) + : base(propertyEditors, dataTypeConfigurationCache, shortStringHelper, jsonSerializer, dataValueReferenceFactories, blockEditorVarianceHandler, languageService, ioHelper, attribute) => _jsonSerializer = jsonSerializer; /// diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockGridPropertyEditorBase.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockGridPropertyEditorBase.cs index 4e410841a1..1975faabc2 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockGridPropertyEditorBase.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockGridPropertyEditorBase.cs @@ -5,6 +5,7 @@ using System.ComponentModel.DataAnnotations; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Cache.PropertyEditors; +using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Blocks; using Umbraco.Cms.Core.Models.Validation; @@ -37,11 +38,12 @@ public abstract class BlockGridPropertyEditorBase : DataEditor #region Value Editor protected override IDataValueEditor CreateValueEditor() => - DataValueEditorFactory.Create(); + DataValueEditorFactory.Create(Attribute!); internal class BlockGridEditorPropertyValueEditor : BlockEditorPropertyValueEditor { public BlockGridEditorPropertyValueEditor( + DataEditorAttribute attribute, PropertyEditorCollection propertyEditors, DataValueReferenceFactoryCollection dataValueReferenceFactories, IDataTypeConfigurationCache dataTypeConfigurationCache, @@ -52,8 +54,9 @@ public abstract class BlockGridPropertyEditorBase : DataEditor IBlockEditorElementTypeCache elementTypeCache, IPropertyValidationService propertyValidationService, BlockEditorVarianceHandler blockEditorVarianceHandler, - ILanguageService languageService) - : base(propertyEditors, dataValueReferenceFactories, dataTypeConfigurationCache, shortStringHelper, jsonSerializer, blockEditorVarianceHandler, languageService) + ILanguageService languageService, + IIOHelper ioHelper) + : base(propertyEditors, dataValueReferenceFactories, dataTypeConfigurationCache, shortStringHelper, jsonSerializer, blockEditorVarianceHandler, languageService, ioHelper, attribute) { BlockEditorValues = new BlockEditorValues(new BlockGridEditorDataConverter(jsonSerializer), elementTypeCache, logger); Validators.Add(new BlockEditorValidator(propertyValidationService, BlockEditorValues, elementTypeCache)); diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorBase.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorBase.cs index 749937bd4d..93277438ce 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorBase.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorBase.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Cache.PropertyEditors; +using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Blocks; using Umbraco.Cms.Core.Models.Validation; @@ -47,11 +48,12 @@ public abstract class BlockListPropertyEditorBase : DataEditor protected virtual BlockEditorDataConverter CreateBlockEditorDataConverter() => new BlockListEditorDataConverter(_jsonSerializer); protected override IDataValueEditor CreateValueEditor() => - DataValueEditorFactory.Create(CreateBlockEditorDataConverter()); + DataValueEditorFactory.Create(Attribute!, CreateBlockEditorDataConverter()); internal class BlockListEditorPropertyValueEditor : BlockEditorPropertyValueEditor { public BlockListEditorPropertyValueEditor( + DataEditorAttribute attribute, BlockEditorDataConverter blockEditorDataConverter, PropertyEditorCollection propertyEditors, DataValueReferenceFactoryCollection dataValueReferenceFactories, @@ -63,8 +65,10 @@ public abstract class BlockListPropertyEditorBase : DataEditor IJsonSerializer jsonSerializer, IPropertyValidationService propertyValidationService, BlockEditorVarianceHandler blockEditorVarianceHandler, - ILanguageService languageService) - : base(propertyEditors, dataValueReferenceFactories, dataTypeConfigurationCache, shortStringHelper, jsonSerializer, blockEditorVarianceHandler, languageService) + ILanguageService languageService, + IIOHelper ioHelper + ) + : base(propertyEditors, dataValueReferenceFactories, dataTypeConfigurationCache, shortStringHelper, jsonSerializer, blockEditorVarianceHandler, languageService, ioHelper, attribute) { BlockEditorValues = new BlockEditorValues(blockEditorDataConverter, elementTypeCache, logger); Validators.Add(new BlockEditorValidator(propertyValidationService, BlockEditorValues, elementTypeCache)); diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockValuePropertyValueEditorBase.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockValuePropertyValueEditorBase.cs index bbd0a93f7b..11f45ab095 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockValuePropertyValueEditorBase.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockValuePropertyValueEditorBase.cs @@ -43,7 +43,10 @@ public abstract class BlockValuePropertyValueEditorBase : DataV jsonSerializer, dataValueReferenceFactoryCollection, StaticServiceProvider.Instance.GetRequiredService(), - StaticServiceProvider.Instance.GetRequiredService()) + StaticServiceProvider.Instance.GetRequiredService(), + ioHelper, + attribute + ) { } @@ -54,8 +57,10 @@ public abstract class BlockValuePropertyValueEditorBase : DataV IJsonSerializer jsonSerializer, DataValueReferenceFactoryCollection dataValueReferenceFactoryCollection, BlockEditorVarianceHandler blockEditorVarianceHandler, - ILanguageService languageService) - : base(shortStringHelper, jsonSerializer) + ILanguageService languageService, + IIOHelper ioHelper, + DataEditorAttribute attribute) + : base(shortStringHelper, jsonSerializer, ioHelper, attribute) { _propertyEditors = propertyEditors; _dataTypeConfigurationCache = dataTypeConfigurationCache; diff --git a/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs index f1281747c2..25b028f1e1 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs @@ -65,7 +65,7 @@ public class RichTextPropertyEditor : DataEditor /// /// protected override IDataValueEditor CreateValueEditor() => - DataValueEditorFactory.Create(); + DataValueEditorFactory.Create(Attribute!); protected override IConfigurationEditor CreateConfigurationEditor() => new RichTextConfigurationEditor(_ioHelper); @@ -87,6 +87,7 @@ public class RichTextPropertyEditor : DataEditor private readonly ILogger _logger; public RichTextPropertyValueEditor( + DataEditorAttribute attribute, PropertyEditorCollection propertyEditors, IDataTypeConfigurationCache dataTypeReadCache, ILogger logger, @@ -102,8 +103,9 @@ public class RichTextPropertyEditor : DataEditor DataValueReferenceFactoryCollection dataValueReferenceFactoryCollection, IRichTextRequiredValidator richTextRequiredValidator, BlockEditorVarianceHandler blockEditorVarianceHandler, - ILanguageService languageService) - : base(propertyEditors, dataTypeReadCache, shortStringHelper, jsonSerializer, dataValueReferenceFactoryCollection, blockEditorVarianceHandler, languageService) + ILanguageService languageService, + IIOHelper ioHelper) + : base(propertyEditors, dataTypeReadCache, shortStringHelper, jsonSerializer, dataValueReferenceFactoryCollection, blockEditorVarianceHandler, languageService, ioHelper, attribute) { _backOfficeSecurityAccessor = backOfficeSecurityAccessor; _imageSourceParser = imageSourceParser; diff --git a/src/Umbraco.Infrastructure/Routing/RedirectTracker.cs b/src/Umbraco.Infrastructure/Routing/RedirectTracker.cs index ee752af798..e26fa7d35c 100644 --- a/src/Umbraco.Infrastructure/Routing/RedirectTracker.cs +++ b/src/Umbraco.Infrastructure/Routing/RedirectTracker.cs @@ -20,7 +20,7 @@ namespace Umbraco.Cms.Infrastructure.Routing private readonly IPublishedContentCache _contentCache; private readonly IDocumentNavigationQueryService _navigationQueryService; private readonly ILogger _logger; - private readonly IDocumentUrlService _documentUrlService; + private readonly IPublishedUrlProvider _publishedUrlProvider; public RedirectTracker( IVariationContextAccessor variationContextAccessor, @@ -29,7 +29,7 @@ namespace Umbraco.Cms.Infrastructure.Routing IPublishedContentCache contentCache, IDocumentNavigationQueryService navigationQueryService, ILogger logger, - IDocumentUrlService documentUrlService) + IPublishedUrlProvider publishedUrlProvider) { _variationContextAccessor = variationContextAccessor; _localizationService = localizationService; @@ -37,7 +37,7 @@ namespace Umbraco.Cms.Infrastructure.Routing _contentCache = contentCache; _navigationQueryService = navigationQueryService; _logger = logger; - _documentUrlService = documentUrlService; + _publishedUrlProvider = publishedUrlProvider; } /// @@ -64,7 +64,8 @@ namespace Umbraco.Cms.Infrastructure.Routing { try { - var route = _contentCache.GetRouteById(publishedContent.Id, culture); + var route = _publishedUrlProvider.GetUrl(publishedContent.Id, UrlMode.Relative, culture).TrimEnd(Constants.CharArrays.ForwardSlash); + if (IsValidRoute(route)) { oldRoutes[(publishedContent.Id, culture)] = (publishedContent.Key, route); @@ -74,7 +75,7 @@ namespace Umbraco.Cms.Infrastructure.Routing // Retry using all languages, if this is invariant but has a variant ancestor. foreach (string languageIsoCode in languageIsoCodes.Value) { - route = _contentCache.GetRouteById(publishedContent.Id, languageIsoCode); + route = _publishedUrlProvider.GetUrl(publishedContent.Id, UrlMode.Relative, languageIsoCode).TrimEnd(Constants.CharArrays.ForwardSlash); if (IsValidRoute(route)) { oldRoutes[(publishedContent.Id, languageIsoCode)] = (publishedContent.Key, route); @@ -102,7 +103,7 @@ namespace Umbraco.Cms.Infrastructure.Routing { try { - var newRoute = _documentUrlService.GetLegacyRouteFormat(contentKey, culture, false); + var newRoute = _publishedUrlProvider.GetUrl(contentKey, UrlMode.Relative, culture).TrimEnd(Constants.CharArrays.ForwardSlash); if (!IsValidRoute(newRoute) || oldRoute == newRoute) { continue; diff --git a/src/Umbraco.Infrastructure/Security/BackOfficeUserClientCredentialsManager.cs b/src/Umbraco.Infrastructure/Security/BackOfficeUserClientCredentialsManager.cs index 7036fa0e88..21afdd0165 100644 --- a/src/Umbraco.Infrastructure/Security/BackOfficeUserClientCredentialsManager.cs +++ b/src/Umbraco.Infrastructure/Security/BackOfficeUserClientCredentialsManager.cs @@ -35,6 +35,7 @@ public sealed class BackOfficeUserClientCredentialsManager : ClientCredentialsMa { UserClientCredentialsOperationStatus.InvalidUser => Attempt.Fail(BackOfficeUserClientCredentialsOperationStatus.InvalidUser), UserClientCredentialsOperationStatus.DuplicateClientId => Attempt.Fail(BackOfficeUserClientCredentialsOperationStatus.DuplicateClientId), + UserClientCredentialsOperationStatus.InvalidClientId => Attempt.Fail(BackOfficeUserClientCredentialsOperationStatus.InvalidClientId), _ => throw new ArgumentOutOfRangeException($"Unsupported client ID operation status: {result}") }; } diff --git a/src/Umbraco.Infrastructure/Security/OperationStatus/BackOfficeUserClientCredentialsOperationStatus.cs b/src/Umbraco.Infrastructure/Security/OperationStatus/BackOfficeUserClientCredentialsOperationStatus.cs index fd86ce68cb..f0370a13a3 100644 --- a/src/Umbraco.Infrastructure/Security/OperationStatus/BackOfficeUserClientCredentialsOperationStatus.cs +++ b/src/Umbraco.Infrastructure/Security/OperationStatus/BackOfficeUserClientCredentialsOperationStatus.cs @@ -4,5 +4,6 @@ public enum BackOfficeUserClientCredentialsOperationStatus { Success, DuplicateClientId, - InvalidUser + InvalidUser, + InvalidClientId } diff --git a/src/Umbraco.PublishedCache.HybridCache/Services/DocumentCacheService.cs b/src/Umbraco.PublishedCache.HybridCache/Services/DocumentCacheService.cs index dc7885ee38..e3bb827393 100644 --- a/src/Umbraco.PublishedCache.HybridCache/Services/DocumentCacheService.cs +++ b/src/Umbraco.PublishedCache.HybridCache/Services/DocumentCacheService.cs @@ -24,7 +24,7 @@ internal sealed class DocumentCacheService : IDocumentCacheService private readonly IEnumerable _seedKeyProviders; private readonly IPublishedModelFactory _publishedModelFactory; private readonly IPreviewService _previewService; - private readonly CacheEntrySettings _cacheEntrySettings; + private readonly CacheSettings _cacheSettings; private HashSet? _seedKeys; private HashSet SeedKeys { @@ -54,7 +54,7 @@ internal sealed class DocumentCacheService : IDocumentCacheService IPublishedContentFactory publishedContentFactory, ICacheNodeFactory cacheNodeFactory, IEnumerable seedKeyProviders, - IOptionsMonitor cacheEntrySettings, + IOptions cacheSettings, IPublishedModelFactory publishedModelFactory, IPreviewService previewService) { @@ -67,7 +67,7 @@ internal sealed class DocumentCacheService : IDocumentCacheService _seedKeyProviders = seedKeyProviders; _publishedModelFactory = publishedModelFactory; _previewService = previewService; - _cacheEntrySettings = cacheEntrySettings.Get(Constants.Configuration.NamedOptions.CacheEntry.Document); + _cacheSettings = cacheSettings.Value; } public async Task GetByKeyAsync(Guid key, bool? preview = null) @@ -221,8 +221,8 @@ internal sealed class DocumentCacheService : IDocumentCacheService private HybridCacheEntryOptions GetSeedEntryOptions() => new() { - Expiration = _cacheEntrySettings.SeedCacheDuration, - LocalCacheExpiration = _cacheEntrySettings.SeedCacheDuration + Expiration = _cacheSettings.Entry.Document.SeedCacheDuration, + LocalCacheExpiration = _cacheSettings.Entry.Document.SeedCacheDuration }; private HybridCacheEntryOptions GetEntryOptions(Guid key) @@ -234,8 +234,8 @@ internal sealed class DocumentCacheService : IDocumentCacheService return new HybridCacheEntryOptions { - Expiration = _cacheEntrySettings.RemoteCacheDuration, - LocalCacheExpiration = _cacheEntrySettings.LocalCacheDuration, + Expiration = _cacheSettings.Entry.Document.RemoteCacheDuration, + LocalCacheExpiration = _cacheSettings.Entry.Document.LocalCacheDuration, }; } diff --git a/src/Umbraco.PublishedCache.HybridCache/Services/MediaCacheService.cs b/src/Umbraco.PublishedCache.HybridCache/Services/MediaCacheService.cs index 12327489b8..5061c09386 100644 --- a/src/Umbraco.PublishedCache.HybridCache/Services/MediaCacheService.cs +++ b/src/Umbraco.PublishedCache.HybridCache/Services/MediaCacheService.cs @@ -23,7 +23,7 @@ internal class MediaCacheService : IMediaCacheService private readonly ICacheNodeFactory _cacheNodeFactory; private readonly IEnumerable _seedKeyProviders; private readonly IPublishedModelFactory _publishedModelFactory; - private readonly CacheEntrySettings _cacheEntrySettings; + private readonly CacheSettings _cacheSettings; private HashSet? _seedKeys; private HashSet SeedKeys @@ -55,7 +55,7 @@ internal class MediaCacheService : IMediaCacheService ICacheNodeFactory cacheNodeFactory, IEnumerable seedKeyProviders, IPublishedModelFactory publishedModelFactory, - IOptionsMonitor cacheEntrySettings) + IOptions cacheSettings) { _databaseCacheRepository = databaseCacheRepository; _idKeyMap = idKeyMap; @@ -65,7 +65,7 @@ internal class MediaCacheService : IMediaCacheService _cacheNodeFactory = cacheNodeFactory; _seedKeyProviders = seedKeyProviders; _publishedModelFactory = publishedModelFactory; - _cacheEntrySettings = cacheEntrySettings.Get(Constants.Configuration.NamedOptions.CacheEntry.Media); + _cacheSettings = cacheSettings.Value; } public async Task GetByKeyAsync(Guid key) @@ -268,16 +268,16 @@ internal class MediaCacheService : IMediaCacheService return new HybridCacheEntryOptions { - Expiration = _cacheEntrySettings.RemoteCacheDuration, - LocalCacheExpiration = _cacheEntrySettings.LocalCacheDuration, + Expiration = _cacheSettings.Entry.Media.RemoteCacheDuration, + LocalCacheExpiration = _cacheSettings.Entry.Media.LocalCacheDuration, }; } private HybridCacheEntryOptions GetSeedEntryOptions() => new() { - Expiration = _cacheEntrySettings.SeedCacheDuration, - LocalCacheExpiration = _cacheEntrySettings.SeedCacheDuration, + Expiration = _cacheSettings.SeedCacheDuration, + LocalCacheExpiration = _cacheSettings.SeedCacheDuration, }; private string GetCacheKey(Guid key, bool preview) => preview ? $"{key}+draft" : $"{key}"; diff --git a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreBackOfficeInfo.cs b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreBackOfficeInfo.cs index d0f8f7b02b..8522ef59c5 100644 --- a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreBackOfficeInfo.cs +++ b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreBackOfficeInfo.cs @@ -34,7 +34,7 @@ public class AspNetCoreBackOfficeInfo : IBackOfficeInfo _getAbsoluteUrl = WebPath.Combine( _hostingEnvironment.ApplicationMainUrl.ToString(), - _globalSettings.CurrentValue.UmbracoPath.TrimStart(CharArrays.TildeForwardSlash)); + Core.Constants.System.DefaultUmbracoPath.TrimStart(CharArrays.TildeForwardSlash)); } return _getAbsoluteUrl; diff --git a/src/Umbraco.Web.Common/Extensions/FriendlyPublishedContentExtensions.cs b/src/Umbraco.Web.Common/Extensions/FriendlyPublishedContentExtensions.cs index 60c7a27a57..97bf4c8839 100644 --- a/src/Umbraco.Web.Common/Extensions/FriendlyPublishedContentExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/FriendlyPublishedContentExtensions.cs @@ -271,6 +271,14 @@ public static class FriendlyPublishedContentExtensions where T : class, IPublishedContent => content.Parent(GetPublishedCache(content), GetNavigationQueryService(content)); + /// + /// Gets the parent of the content item. + /// + /// The content. + /// The parent of content or null. + public static IPublishedContent? Parent(this IPublishedContent content) + => content.Parent(GetPublishedCache(content), GetNavigationQueryService(content)); + /// /// Gets the ancestors of the content. /// diff --git a/src/Umbraco.Web.Common/ModelsBuilder/InMemoryAuto/InMemoryModelFactory.cs b/src/Umbraco.Web.Common/ModelsBuilder/InMemoryAuto/InMemoryModelFactory.cs index 1da1b32a22..fe2718595c 100644 --- a/src/Umbraco.Web.Common/ModelsBuilder/InMemoryAuto/InMemoryModelFactory.cs +++ b/src/Umbraco.Web.Common/ModelsBuilder/InMemoryAuto/InMemoryModelFactory.cs @@ -5,11 +5,15 @@ using System.Runtime.Loader; using System.Text; using System.Text.RegularExpressions; using Microsoft.AspNetCore.Mvc.Razor; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.Extensions; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Logging; using Umbraco.Cms.Core.Models.PublishedContent; @@ -17,6 +21,7 @@ using Umbraco.Cms.Infrastructure.ModelsBuilder; using Umbraco.Cms.Infrastructure.ModelsBuilder.Building; using Umbraco.Extensions; using File = System.IO.File; +using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment; namespace Umbraco.Cms.Web.Common.ModelsBuilder.InMemoryAuto { @@ -31,7 +36,7 @@ namespace Umbraco.Cms.Web.Common.ModelsBuilder.InMemoryAuto private readonly ILogger _logger; private readonly FileSystemWatcher? _watcher; private readonly Lazy _umbracoServices; // TODO: this is because of circular refs :( - private readonly IHostingEnvironment _hostingEnvironment; + private readonly IHostEnvironment _hostEnvironment; private readonly IApplicationShutdownRegistry _hostingLifetime; private readonly ModelsGenerationError _errors; private readonly IPublishedValueFallback _publishedValueFallback; @@ -48,6 +53,7 @@ namespace Umbraco.Cms.Web.Common.ModelsBuilder.InMemoryAuto private ModelsBuilderSettings _config; private bool _disposedValue; + [Obsolete("Use a not obsoleted constructor instead. Scheduled for removal in v16")] public InMemoryModelFactory( Lazy umbracoServices, IProfilingLogger profilingLogger, @@ -63,12 +69,62 @@ namespace Umbraco.Cms.Web.Common.ModelsBuilder.InMemoryAuto _profilingLogger = profilingLogger; _logger = logger; _config = config.CurrentValue; - _hostingEnvironment = hostingEnvironment; + _hostEnvironment = StaticServiceProvider.Instance.GetRequiredService(); _hostingLifetime = hostingLifetime; _publishedValueFallback = publishedValueFallback; _loadContextManager = loadContextManager; _runtimeCompilationCacheBuster = runtimeCompilationCacheBuster; - _errors = new ModelsGenerationError(config, _hostingEnvironment); + _errors = new ModelsGenerationError(config, _hostEnvironment); + _ver = 1; // zero is for when we had no version + _skipver = -1; // nothing to skip + + if (!hostingEnvironment.IsHosted) + { + return; + } + + config.OnChange(x => _config = x); + _pureLiveDirectory = new Lazy(PureLiveDirectoryAbsolute); + + if (!Directory.Exists(_pureLiveDirectory.Value)) + { + Directory.CreateDirectory(_pureLiveDirectory.Value); + } + + // BEWARE! if the watcher is not properly released then for some reason the + // BuildManager will start confusing types - using a 'registered object' here + // though we should probably plug into Umbraco's MainDom - which is internal + _hostingLifetime.RegisterObject(this); + _watcher = new FileSystemWatcher(_pureLiveDirectory.Value); + _watcher.Changed += WatcherOnChanged; + _watcher.EnableRaisingEvents = true; + + // get it here, this need to be fast + _debugLevel = _config.DebugLevel; + } + + public InMemoryModelFactory( + Lazy umbracoServices, + IProfilingLogger profilingLogger, + ILogger logger, + IOptionsMonitor config, + IHostingEnvironment hostingEnvironment, + IHostEnvironment hostEnvironment, + IApplicationShutdownRegistry hostingLifetime, + IPublishedValueFallback publishedValueFallback, + InMemoryAssemblyLoadContextManager loadContextManager, + RuntimeCompilationCacheBuster runtimeCompilationCacheBuster) + { + _umbracoServices = umbracoServices; + _profilingLogger = profilingLogger; + _logger = logger; + _config = config.CurrentValue; + _hostEnvironment = hostEnvironment; + _hostingLifetime = hostingLifetime; + _publishedValueFallback = publishedValueFallback; + _loadContextManager = loadContextManager; + _runtimeCompilationCacheBuster = runtimeCompilationCacheBuster; + _errors = new ModelsGenerationError(config, _hostEnvironment); _ver = 1; // zero is for when we had no version _skipver = -1; // nothing to skip @@ -359,7 +415,7 @@ namespace Umbraco.Cms.Web.Common.ModelsBuilder.InMemoryAuto } } - public string PureLiveDirectoryAbsolute() => _hostingEnvironment.MapPathContentRoot(Core.Constants.SystemDirectories.TempData + "/InMemoryAuto"); + public string PureLiveDirectoryAbsolute() => _hostEnvironment.MapPathContentRoot(Core.Constants.SystemDirectories.TempData + "/InMemoryAuto"); // This is NOT thread safe but it is only called from within a lock private Assembly ReloadAssembly(string pathToAssembly) diff --git a/src/Umbraco.Web.Common/Mvc/UmbracoMvcConfigureOptions.cs b/src/Umbraco.Web.Common/Mvc/UmbracoMvcConfigureOptions.cs index 401e620d4c..8131a03160 100644 --- a/src/Umbraco.Web.Common/Mvc/UmbracoMvcConfigureOptions.cs +++ b/src/Umbraco.Web.Common/Mvc/UmbracoMvcConfigureOptions.cs @@ -32,7 +32,7 @@ public class UmbracoMvcConfigureOptions : IConfigureOptions if (options.Conventions.Any(convention => convention is UmbracoBackofficeToken) is false) { // Replace the BackOfficeToken in routes. - var backofficePath = _globalSettings.UmbracoPath.TrimStart(Core.Constants.CharArrays.TildeForwardSlash); + var backofficePath = Core.Constants.System.DefaultUmbracoPath.TrimStart(Core.Constants.CharArrays.TildeForwardSlash); options.Conventions.Add(new UmbracoBackofficeToken(Core.Constants.Web.AttributeRouting.BackOfficeToken, backofficePath)); } } diff --git a/src/Umbraco.Web.Common/Views/UmbracoViewPage.cs b/src/Umbraco.Web.Common/Views/UmbracoViewPage.cs index f851acab3b..4fbf5ccde8 100644 --- a/src/Umbraco.Web.Common/Views/UmbracoViewPage.cs +++ b/src/Umbraco.Web.Common/Views/UmbracoViewPage.cs @@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Razor.TagHelpers; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Logging; using Umbraco.Cms.Core.Models; @@ -140,7 +141,7 @@ public abstract class UmbracoViewPage : RazorPage markupToInject = string.Format( ContentSettings.PreviewBadge, - HostingEnvironment.ToAbsolute(GlobalSettings.UmbracoPath), + HostingEnvironment.ToAbsolute(Core.Constants.System.DefaultUmbracoPath), Context.Request.GetEncodedUrl(), UmbracoContext.PublishedRequest?.PublishedContent?.Key); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block/context/block-entry.context.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block/context/block-entry.context.ts index 1e5e0cd222..40cf5c2641 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block/context/block-entry.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block/context/block-entry.context.ts @@ -619,7 +619,7 @@ export abstract class UmbBlockEntryContext< if (!variantId || !this.#contentKey) return; // TODO: Handle variantId changes this.observe( - this._manager?.hasExposeOf(this.#contentKey), + this._manager?.hasExposeOf(this.#contentKey, variantId), (hasExpose) => { this.#hasExpose.setValue(hasExpose); }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block/context/block-manager.context.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block/context/block-manager.context.ts index 4fb02dc221..545a4e1d27 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block/context/block-manager.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block/context/block-manager.context.ts @@ -192,8 +192,7 @@ export abstract class UmbBlockManagerContext< ); } - hasExposeOf(contentKey: string) { - const variantId = this.#variantId.getValue(); + hasExposeOf(contentKey: string, variantId: UmbVariantId) { if (!variantId) return; return this.#exposes.asObservablePart((source) => source.some((x) => x.contentKey === contentKey && variantId.compare(x)), diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-workspace.context.ts index d7f83662fe..4b17155346 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-workspace.context.ts @@ -121,19 +121,19 @@ export class UmbBlockWorkspaceContext { - if (!contentKey) return; + observeMultiple([this.contentKey, this.variantId]), + ([contentKey, variantId]) => { + if (!contentKey || !variantId) return; this.observe( - manager.hasExposeOf(contentKey), + manager.hasExposeOf(contentKey, variantId), (exposed) => { this.#exposed.setValue(exposed); }, 'observeHasExpose', ); }, - 'observeContentKey', + 'observeContentKeyAndVariantId', ); this.observe( diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/validation/context/validation-messages.manager.ts b/src/Umbraco.Web.UI.Client/src/packages/core/validation/context/validation-messages.manager.ts index 7c6fc6c1b2..be8e796acb 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/validation/context/validation-messages.manager.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/validation/context/validation-messages.manager.ts @@ -16,7 +16,7 @@ export class UmbValidationMessagesManager { messages = this.#messages.asObservable(); debug(logName: string) { - this.#messages.asObservable().subscribe((x) => console.log(logName, x, this.#translators)); + this.#messages.asObservable().subscribe((x) => console.log(logName, x)); } getHasAnyMessages(): boolean { @@ -87,6 +87,9 @@ export class UmbValidationMessagesManager { removeMessageByKey(key: string): void { this.#messages.removeOne(key); } + removeMessageByKeys(keys: Array): void { + this.#messages.filter((x) => keys.indexOf(x.key) === -1); + } removeMessagesByTypeAndPath(type: UmbValidationMessageType, path: string): void { //path = path.toLowerCase(); this.#messages.filter((x) => !(x.type === type && x.path === path)); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/validation/controllers/bind-server-validation-to-form-control.controller.ts b/src/Umbraco.Web.UI.Client/src/packages/core/validation/controllers/bind-server-validation-to-form-control.controller.ts index 5410e8caa0..5aa5dd0bfe 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/validation/controllers/bind-server-validation-to-form-control.controller.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/validation/controllers/bind-server-validation-to-form-control.controller.ts @@ -30,11 +30,8 @@ export class UmbBindServerValidationToFormControl extends UmbControllerBase { if (!defaultMemoization(this.#value, value)) { this.#value = value; // Only remove server validations from validation context [NL] - this.#messages.forEach((message) => { - if (message.type === 'server') { - this.#context?.messages.removeMessageByKey(message.key); - } - }); + const toRemove = this.#messages.filter((x) => x.type === 'server').map((msg) => msg.key); + this.#context?.messages.removeMessageByKeys(toRemove); } } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/validation/controllers/validation.controller.ts b/src/Umbraco.Web.UI.Client/src/packages/core/validation/controllers/validation.controller.ts index e535dc933d..5ab675af49 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/validation/controllers/validation.controller.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/validation/controllers/validation.controller.ts @@ -138,9 +138,7 @@ export class UmbValidationController extends UmbControllerBase implements UmbVal if (this.#parentMessages) { // Remove the local messages that does not exist in the parent anymore: const toRemove = this.#parentMessages.filter((msg) => !msgs.find((m) => m.key === msg.key)); - toRemove.forEach((msg) => { - this.messages.removeMessageByKey(msg.key); - }); + this.#parent!.messages.removeMessageByKeys(toRemove.map((msg) => msg.key)); } this.#parentMessages = msgs; msgs.forEach((msg) => { @@ -160,9 +158,7 @@ export class UmbValidationController extends UmbControllerBase implements UmbVal if (this.#localMessages) { // Remove the parent messages that does not exist locally anymore: const toRemove = this.#localMessages.filter((msg) => !msgs.find((m) => m.key === msg.key)); - toRemove.forEach((msg) => { - this.#parent!.messages.removeMessageByKey(msg.key); - }); + this.#parent!.messages.removeMessageByKeys(toRemove.map((msg) => msg.key)); } this.#localMessages = msgs; msgs.forEach((msg) => { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/validation/mixins/form-control.mixin.ts b/src/Umbraco.Web.UI.Client/src/packages/core/validation/mixins/form-control.mixin.ts index e3b13b79be..148a9bd599 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/validation/mixins/form-control.mixin.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/validation/mixins/form-control.mixin.ts @@ -146,7 +146,7 @@ export function UmbFormControlMixin< public set pristine(value: boolean) { if (this._pristine !== value) { this._pristine = value; - this.#dispatchValidationState(); + this._runValidators(); } } public get pristine(): boolean { diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/types.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/types.ts index 8f27a81993..0c7a074373 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/types.ts @@ -25,7 +25,6 @@ export interface UmbDocumentDetailModel extends UmbContentDetailModel { entityType: UmbDocumentEntityType; isTrashed: boolean; template: { unique: string } | null; - unique: string; urls: Array; values: Array; variants: Array; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/media-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/media-workspace.context.ts index fa3dcd1a52..5e7c356c3a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/media-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/media-workspace.context.ts @@ -59,7 +59,7 @@ export class UmbMediaWorkspaceContext path: UMB_CREATE_MEDIA_WORKSPACE_PATH_PATTERN.toString(), component: () => import('./media-workspace-editor.element.js'), setup: async (_component, info) => { - const parentEntityType = info.match.params.entityType; + const parentEntityType = info.match.params.parentEntityType; const parentUnique = info.match.params.parentUnique === 'null' ? null : info.match.params.parentUnique; const mediaTypeUnique = info.match.params.mediaTypeUnique; await this.createScaffold({ diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/views/info/media-workspace-view-info.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/views/info/media-workspace-view-info.element.ts index 2e49276698..8d0a3ab103 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/views/info/media-workspace-view-info.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/views/info/media-workspace-view-info.element.ts @@ -38,10 +38,10 @@ export class UmbMediaWorkspaceViewInfoElement extends UmbLitElement { private _urls?: Array; @state() - private _createDate = 'Unknown'; + private _createDate?: string | null = null; @state() - private _updateDate = 'Unknown'; + private _updateDate?: string | null = null; constructor() { super(); @@ -91,8 +91,8 @@ export class UmbMediaWorkspaceViewInfoElement extends UmbLitElement { /** TODO: Doubt this is the right way to get the create date... */ this.observe(this.#workspaceContext.variants, (variants) => { - this._createDate = Array.isArray(variants) ? variants[0].createDate || 'Unknown' : 'Unknown'; - this._updateDate = Array.isArray(variants) ? variants[0].updateDate || 'Unknown' : 'Unknown'; + this._createDate = variants?.[0]?.createDate; + this._updateDate = variants?.[0]?.updateDate; }); } #openSvg(imagePath: string) { @@ -170,18 +170,7 @@ export class UmbMediaWorkspaceViewInfoElement extends UmbLitElement { #renderGeneralSection() { return html` -
- - - - -
-
- - - - -
+ ${this.#renderCreateDate()} ${this.#renderUpdateDate()}
Media Type + + + + +
+ `; + } + + #renderUpdateDate() { + if (!this._updateDate) return nothing; + return html` +
+ + + + +
+ `; + } + static override styles = [ UmbTextStyles, css` diff --git a/src/Umbraco.Web.UI.Client/src/packages/publish-cache/dashboard-published-status.element.ts b/src/Umbraco.Web.UI.Client/src/packages/publish-cache/dashboard-published-status.element.ts index 6c6ac09d36..eeead01e78 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/publish-cache/dashboard-published-status.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/publish-cache/dashboard-published-status.element.ts @@ -1,59 +1,27 @@ -import type { UUIButtonState } from '@umbraco-cms/backoffice/external/uui'; -import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit'; +import { css, customElement, html, state } from '@umbraco-cms/backoffice/external/lit'; +import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; import { umbConfirmModal } from '@umbraco-cms/backoffice/modal'; import { PublishedCacheService } from '@umbraco-cms/backoffice/external/backend-api'; -import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import type { UUIButtonState } from '@umbraco-cms/backoffice/external/uui'; @customElement('umb-dashboard-published-status') export class UmbDashboardPublishedStatusElement extends UmbLitElement { - @state() - private _publishedStatusText = ''; - - @state() - private _buttonState: UUIButtonState = undefined; - @state() private _buttonStateReload: UUIButtonState = undefined; @state() private _buttonStateRebuild: UUIButtonState = undefined; - @state() - private _buttonStateCollect: UUIButtonState = undefined; - - override connectedCallback() { - super.connectedCallback(); - this._getPublishedStatus(); - } - - // Refresh - private async _getPublishedStatus() { - const { data } = await tryExecuteAndNotify(this, PublishedCacheService.getPublishedCacheStatus()); - if (data) { - this._publishedStatusText = data; - } - } - - private async _onRefreshCacheHandler() { - this._buttonState = 'waiting'; - await this._getPublishedStatus(); - this._buttonState = 'success'; - } - //Reload private async _reloadMemoryCache() { this._buttonStateReload = 'waiting'; - this._buttonState = 'waiting'; const { error } = await tryExecuteAndNotify(this, PublishedCacheService.postPublishedCacheReload()); if (error) { this._buttonStateReload = 'failed'; - this._buttonState = 'failed'; } else { this._buttonStateReload = 'success'; - this._getPublishedStatus(); - this._buttonState = 'success'; } } private async _onReloadCacheHandler() { @@ -89,42 +57,8 @@ export class UmbDashboardPublishedStatusElement extends UmbLitElement { this._rebuildDatabaseCache(); } - //Collect - private async _cacheCollect() { - this._buttonStateCollect = 'waiting'; - const { error } = await tryExecuteAndNotify(this, PublishedCacheService.postPublishedCacheCollect()); - if (error) { - this._buttonStateCollect = 'failed'; - } else { - this._buttonStateCollect = 'success'; - } - } - - private async _onSnapshotCacheHandler() { - await umbConfirmModal(this, { - headline: 'Snapshot', - content: html` Trigger a NuCache snapshots collection.`, - color: 'danger', - confirmLabel: 'Continue', - }); - this._cacheCollect(); - } - override render() { return html` - -

${this._publishedStatusText}

- - Refresh Status - -
-

This button lets you reload the in-memory cache, by entirely reloading it from the database cache (but it does @@ -159,22 +93,6 @@ export class UmbDashboardPublishedStatusElement extends UmbLitElement { Rebuild Database Cache - - -

- This button lets you trigger a NuCache snapshots collection (after running a fullCLR GC). Unless you know what - that means, you probably do not need to use it. -

- - Snapshot Internal Cache - -
`; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/input-tiptap/input-tiptap.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/input-tiptap/input-tiptap.element.ts index 367d2c6edc..130d8aee6f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/input-tiptap/input-tiptap.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/input-tiptap/input-tiptap.element.ts @@ -209,7 +209,10 @@ export class UmbInputTiptapElement extends UmbFormControlMixin p { + margin: 0; + padding: 0; + } + } + .umb-embed-holder { display: inline-block; position: relative; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/input-tiptap/tiptap-toolbar.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/input-tiptap/tiptap-toolbar.element.ts index 087ee86123..d406a9dee9 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/input-tiptap/tiptap-toolbar.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/input-tiptap/tiptap-toolbar.element.ts @@ -62,24 +62,19 @@ export class UmbTiptapToolbarElement extends UmbLitElement { } override render() { - return html`${map(this.toolbar, (row, rowIndex) => - map( - row, - (group, groupIndex) => html` - ${map(group, (alias, aliasIndex) => { - const newRow = rowIndex !== 0 && groupIndex === 0 && aliasIndex === 0; - const component = this._lookup?.get(alias); - if (!component) return nothing; - return html` -
- ${component} -
- `; - })} -
+ return html` + ${map( + this.toolbar, + (row) => html` +
+ ${map( + row, + (group) => html`
${map(group, (alias) => this._lookup?.get(alias) ?? nothing)}
`, + )} +
`, - ), - )} `; + )} + `; } static override readonly styles = css` @@ -99,31 +94,35 @@ export class UmbTiptapToolbarElement extends UmbLitElement { background-color: var(--uui-color-surface-alt); color: var(--color-text); - display: grid; - grid-template-columns: repeat(auto-fill, 10px); - grid-auto-flow: row; + + display: flex; + flex-direction: column; position: sticky; top: -25px; left: 0px; right: 0px; - padding: var(--uui-size-space-3); + padding: var(--uui-size-3); z-index: 9999999; } - .item { - grid-column: span 3; - } + .row { + display: flex; + flex-direction: row; - .separator { - background-color: var(--uui-color-border); - width: 1px; - place-self: center; - height: 22px; - } - .separator:last-child, - .separator:has(+ [data-new-row]) { - display: none; + .group { + display: inline-flex; + align-items: stretch; + + &:not(:last-child)::after { + content: ''; + background-color: var(--uui-color-border); + width: 1px; + place-self: center; + height: 22px; + margin: 0 var(--uui-size-3); + } + } } `; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/toolbar/tiptap-toolbar-button.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/toolbar/tiptap-toolbar-button.element.ts index 35212303be..a141767a50 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/toolbar/tiptap-toolbar-button.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/toolbar/tiptap-toolbar-button.element.ts @@ -46,6 +46,7 @@ export class UmbTiptapToolbarButtonElement extends UmbLitElement { look=${this.isActive ? 'outline' : 'default'} label=${ifDefined(this.manifest?.meta.label)} title=${this.manifest?.meta.label ? this.localize.string(this.manifest.meta.label) : ''} + ?disabled=${this.api && this.editor && this.api.isDisabled(this.editor)} @click=${() => (this.api && this.editor ? this.api.execute(this.editor) : null)}> ${when( this.manifest?.meta.icon, diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/base.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/base.ts index 3fba20f6ba..5911ae8bc0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/base.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/base.ts @@ -34,6 +34,8 @@ export abstract class UmbTiptapExtensionApiBase extends UmbControllerBase implem } export abstract class UmbTiptapToolbarElementApiBase extends UmbControllerBase implements UmbTiptapToolbarElementApi { + #enabledExtensions?: Array; + /** * The manifest for the extension. */ @@ -46,8 +48,10 @@ export abstract class UmbTiptapToolbarElementApiBase extends UmbControllerBase i /** * A method to execute the toolbar element action. + * @see {ManifestTiptapToolbarExtension} + * @param {Editor} editor The editor instance. */ - public abstract execute(editor: Editor): void; + public abstract execute(editor?: Editor): void; /** * Informs the toolbar element if it is active or not. It uses the manifest meta alias to check if the toolbar element is active. @@ -55,7 +59,21 @@ export abstract class UmbTiptapToolbarElementApiBase extends UmbControllerBase i * @param {Editor} editor The editor instance. * @returns {boolean} Returns true if the toolbar element is active. */ - public isActive(editor: Editor) { - return editor && this.manifest?.meta.alias ? editor?.isActive(this.manifest.meta.alias) : false; + public isActive(editor?: Editor): boolean { + return editor && this.manifest?.meta.alias ? editor?.isActive(this.manifest.meta.alias) === true : false; + } + + /** + * Informs the toolbar element if it is disabled or not. + * @see {ManifestTiptapToolbarExtension} + * @param {Editor} editor The editor instance. + * @returns {boolean} Returns true if the toolbar element is disabled. + */ + isDisabled(editor?: Editor): boolean { + if (!editor) return true; + if (!this.#enabledExtensions) { + this.#enabledExtensions = this.configuration?.getValueByAlias('extensions') ?? []; + } + return this.manifest?.forExtensions?.every((ext) => this.#enabledExtensions?.includes(ext)) === false; } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/types.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/types.ts index bbf6f4e390..0791bf6b2a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/types.ts @@ -42,10 +42,15 @@ export interface UmbTiptapToolbarElementApi extends UmbApi, UmbTiptapExtensionAr /** * Executes the toolbar element action. */ - execute(editor: Editor): void; + execute(editor?: Editor): void; /** * Checks if the toolbar element is active. */ - isActive(editor: Editor): boolean; + isActive(editor?: Editor): boolean; + + /** + * Checks if the toolbar element is disabled. + */ + isDisabled(editor?: Editor): boolean; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/components/property-editor-ui-tiptap-extensions-configuration.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/components/property-editor-ui-tiptap-extensions-configuration.element.ts index 3762eae369..40fabc53e4 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/components/property-editor-ui-tiptap-extensions-configuration.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/components/property-editor-ui-tiptap-extensions-configuration.element.ts @@ -109,7 +109,7 @@ export class UmbPropertyEditorUiTiptapExtensionsConfigurationElement label: 'Rich Text Essentials', icon: 'icon-browser-window', group: '#tiptap_extGroup_formatting', - description: 'This is a core extension, it must be enabled', + description: 'This is a core extension, it is always enabled by default.', }); if (!this.value) { diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/components/property-editor-ui-tiptap-toolbar-configuration.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/components/property-editor-ui-tiptap-toolbar-configuration.element.ts index 4fd205cead..7fdef3e2c8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/components/property-editor-ui-tiptap-toolbar-configuration.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/components/property-editor-ui-tiptap-toolbar-configuration.element.ts @@ -36,10 +36,10 @@ export class UmbPropertyEditorUiTiptapToolbarConfigurationElement set value(value: UmbTiptapToolbarValue | undefined) { if (!value) value = [[[]]]; if (value === this.#value) return; - this.#context.setToolbar(value); + this.#value = this.#context.migrateTinyMceToolbar(value); } get value(): UmbTiptapToolbarValue | undefined { - return this.#value?.map((rows) => rows.map((groups) => [...groups])); + return this.#context.cloneToolbarValue(this.#value); } #value?: UmbTiptapToolbarValue; @@ -66,6 +66,10 @@ export class UmbPropertyEditorUiTiptapToolbarConfigurationElement }); } + protected override firstUpdated() { + this.#context.setToolbar(this.value); + } + #onClick(item: UmbTiptapToolbarExtension) { const lastRow = (this.#value?.length ?? 1) - 1; const lastGroup = (this.#value?.[lastRow].length ?? 1) - 1; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/contexts/tiptap-toolbar-configuration.context.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/contexts/tiptap-toolbar-configuration.context.ts index a6d73979c5..4eb24c9995 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/contexts/tiptap-toolbar-configuration.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/contexts/tiptap-toolbar-configuration.context.ts @@ -28,6 +28,49 @@ export class UmbTiptapToolbarConfigurationContext extends UmbContextBase([], (x) => x.unique); public readonly toolbar = this.#toolbar.asObservable(); + /** @deprecated This will be removed in Umbraco 16. */ + #tinyMceToolbarMapping: Record = { + undo: 'Umb.Tiptap.Toolbar.Undo', + redo: 'Umb.Tiptap.Toolbar.Redo', + cut: null, + copy: null, + paste: null, + styles: null, + fontname: null, + fontsize: null, + forecolor: null, + backcolor: null, + blockquote: 'Umb.Tiptap.Toolbar.Blockquote', + formatblock: null, + removeformat: 'Umb.Tiptap.Toolbar.ClearFormatting', + bold: 'Umb.Tiptap.Toolbar.Bold', + italic: 'Umb.Tiptap.Toolbar.Italic', + underline: 'Umb.Tiptap.Toolbar.Underline', + strikethrough: 'Umb.Tiptap.Toolbar.Strike', + alignleft: 'Umb.Tiptap.Toolbar.TextAlignLeft', + aligncenter: 'Umb.Tiptap.Toolbar.TextAlignCenter', + alignright: 'Umb.Tiptap.Toolbar.TextAlignRight', + alignjustify: 'Umb.Tiptap.Toolbar.TextAlignJustify', + bullist: 'Umb.Tiptap.Toolbar.BulletList', + numlist: 'Umb.Tiptap.Toolbar.OrderedList', + outdent: null, + indent: null, + anchor: null, + table: 'Umb.Tiptap.Toolbar.Table', + hr: 'Umb.Tiptap.Toolbar.HorizontalRule', + subscript: 'Umb.Tiptap.Toolbar.Subscript', + superscript: 'Umb.Tiptap.Toolbar.Superscript', + charmap: null, + rtl: null, + ltr: null, + link: 'Umb.Tiptap.Toolbar.Link', + unlink: 'Umb.Tiptap.Toolbar.Unlink', + sourcecode: 'Umb.Tiptap.Toolbar.SourceEditor', + umbmediapicker: 'Umb.Tiptap.Toolbar.MediaPicker', + umbembeddialog: 'Umb.Tiptap.Toolbar.EmbeddedMedia', + umbblockpicker: 'Umb.Tiptap.Toolbar.BlockPicker', + }; + constructor(host: UmbControllerHost) { super(host, UMB_TIPTAP_TOOLBAR_CONFIGURATION_CONTEXT); @@ -66,6 +109,11 @@ export class UmbTiptapToolbarConfigurationContext extends UmbContextBase row.map((group) => [...group])); + } + public filterExtensions(query: string): Array { return this.#extensions .getValue() @@ -132,6 +180,24 @@ export class UmbTiptapToolbarConfigurationContext extends UmbContextBase | null): UmbTiptapToolbarValue { + if (this.isValidToolbarValue(value)) return value; + + const items: Array = []; + + if (Array.isArray(value) && value.length > 0 && typeof value[0] === 'string') { + for (const alias of value) { + const mapping = this.#tinyMceToolbarMapping[alias]; + if (mapping) { + items.push(mapping); + } + } + } + + return [[items]]; + } + public moveToolbarItem(from: [number, number, number], to: [number, number, number]) { const [fromRowIndex, fromGroupIndex, fromItemIndex] = from; const [toRowIndex, toGroupIndex, toItemIndex] = to; diff --git a/src/Umbraco.Web.UI/Views/Partials/blockgrid/items.cshtml b/src/Umbraco.Web.UI/Views/Partials/blockgrid/items.cshtml index 2703fa5701..f0bad0d9b8 100644 --- a/src/Umbraco.Web.UI/Views/Partials/blockgrid/items.cshtml +++ b/src/Umbraco.Web.UI/Views/Partials/blockgrid/items.cshtml @@ -7,12 +7,12 @@
@foreach (var item in Model) { - +
diff --git a/src/Umbraco.Web.Website/Controllers/RenderNoContentController.cs b/src/Umbraco.Web.Website/Controllers/RenderNoContentController.cs index 06b6d4a818..92d1c67ba2 100644 --- a/src/Umbraco.Web.Website/Controllers/RenderNoContentController.cs +++ b/src/Umbraco.Web.Website/Controllers/RenderNoContentController.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Hosting; @@ -49,7 +50,7 @@ public class RenderNoContentController : Controller return Redirect("~/"); } - var model = new NoNodesViewModel { UmbracoPath = _hostingEnvironment.ToAbsolute(_globalSettings.UmbracoPath) }; + var model = new NoNodesViewModel { UmbracoPath = _hostingEnvironment.ToAbsolute(Constants.System.DefaultUmbracoPath) }; return View(_globalSettings.NoNodesViewPath, model); } diff --git a/src/Umbraco.Web.Website/Extensions/HtmlHelperRenderExtensions.cs b/src/Umbraco.Web.Website/Extensions/HtmlHelperRenderExtensions.cs index 3b3a95df2d..6546203229 100644 --- a/src/Umbraco.Web.Website/Extensions/HtmlHelperRenderExtensions.cs +++ b/src/Umbraco.Web.Website/Extensions/HtmlHelperRenderExtensions.cs @@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Hosting; @@ -85,7 +86,7 @@ public static class HtmlHelperRenderExtensions var htmlBadge = string.Format( contentSettings.PreviewBadge, - hostingEnvironment.ToAbsolute(globalSettings.UmbracoPath), + hostingEnvironment.ToAbsolute(Constants.System.DefaultUmbracoPath), WebUtility.UrlEncode(httpContextAccessor.GetRequiredHttpContext().Request.Path), umbracoContext.PublishedRequest?.PublishedContent?.Key); return new HtmlString(htmlBadge); diff --git a/tests/Umbraco.Tests.Common/Builders/UserGroupBuilder.cs b/tests/Umbraco.Tests.Common/Builders/UserGroupBuilder.cs index 97288789b7..c84b118abe 100644 --- a/tests/Umbraco.Tests.Common/Builders/UserGroupBuilder.cs +++ b/tests/Umbraco.Tests.Common/Builders/UserGroupBuilder.cs @@ -1,8 +1,6 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; -using System.Linq; using Moq; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Strings; @@ -22,6 +20,7 @@ public class UserGroupBuilder : UserGroupBuilder public class UserGroupBuilder : ChildBuilderBase, IWithIdBuilder, + IWithKeyBuilder, IWithIconBuilder, IWithAliasBuilder, IWithNameBuilder @@ -30,6 +29,7 @@ public class UserGroupBuilder private IEnumerable _allowedSections = Enumerable.Empty(); private string _icon; private int? _id; + private Guid? _key; private string _name; private ISet _permissions = new HashSet(); private int? _startContentId; @@ -60,6 +60,12 @@ public class UserGroupBuilder set => _id = value; } + Guid? IWithKeyBuilder.Key + { + get => _key; + set => _key = value; + } + string IWithNameBuilder.Name { get => _name; @@ -117,11 +123,13 @@ public class UserGroupBuilder x.StartContentId == userGroup.StartContentId && x.StartMediaId == userGroup.StartMediaId && x.AllowedSections == userGroup.AllowedSections && - x.Id == userGroup.Id); + x.Id == userGroup.Id && + x.Key == userGroup.Key); public override IUserGroup Build() { var id = _id ?? 0; + var key = _key ?? Guid.NewGuid(); var name = _name ?? "TestUserGroup" + _suffix; var alias = _alias ?? "testUserGroup" + _suffix; var userCount = _userCount ?? 0; @@ -134,6 +142,7 @@ public class UserGroupBuilder var userGroup = new UserGroup(shortStringHelper, userCount, alias, name, icon) { Id = id, + Key = key, StartContentId = startContentId, StartMediaId = startMediaId }; diff --git a/tests/Umbraco.Tests.Common/TestHelperBase.cs b/tests/Umbraco.Tests.Common/TestHelperBase.cs index 67da964bca..8fc86b2d3d 100644 --- a/tests/Umbraco.Tests.Common/TestHelperBase.cs +++ b/tests/Umbraco.Tests.Common/TestHelperBase.cs @@ -194,12 +194,7 @@ public abstract class TestHelperBase public TypeLoader GetMockedTypeLoader() => new( Mock.Of(), - new VaryingRuntimeHash(), - Mock.Of(), - new DirectoryInfo(GetHostingEnvironment() - .MapPathContentRoot(Constants.SystemDirectories.TempData)), - Mock.Of>(), - Mock.Of()); + Mock.Of>()); /// /// Some test files are copied to the /bin (/bin/debug) on build, this is a utility to return their physical path based diff --git a/tests/Umbraco.Tests.Integration/DependencyInjection/UmbracoBuilderExtensions.cs b/tests/Umbraco.Tests.Integration/DependencyInjection/UmbracoBuilderExtensions.cs index 732fc0a385..e08a72a8a6 100644 --- a/tests/Umbraco.Tests.Integration/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/tests/Umbraco.Tests.Integration/DependencyInjection/UmbracoBuilderExtensions.cs @@ -10,6 +10,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Moq; using NUnit.Framework; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.Configuration.Models; @@ -129,7 +130,7 @@ public static class UmbracoBuilderExtensions uiProject.Create(); } - var mainLangFolder = new DirectoryInfo(Path.Combine(uiProject.FullName, globalSettings.Value.UmbracoPath.TrimStartExact("~/"), "config", "lang")); + var mainLangFolder = new DirectoryInfo(Path.Combine(uiProject.FullName, Constants.System.DefaultUmbracoPath.TrimStartExact("~/"), "config", "lang")); return new LocalizedTextServiceFileSources( loggerFactory.CreateLogger(), diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentServiceTests.cs index 293c2eabfd..352400c4dd 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentServiceTests.cs @@ -55,6 +55,8 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent private IUserService UserService => GetRequiredService(); + private IUserGroupService UserGroupService => GetRequiredService(); + private IRelationService RelationService => GetRequiredService(); private ILocalizedTextService TextService => GetRequiredService(); @@ -2056,11 +2058,11 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent ContentService.Save(content2); Assert.IsTrue(ContentService.Publish(content2, content2.AvailableCultures.ToArray(), userId: -1).Success); - var editorGroup = UserService.GetUserGroupByAlias(Constants.Security.EditorGroupAlias); + var editorGroup = await UserGroupService.GetAsync(Constants.Security.EditorGroupKey); editorGroup.StartContentId = content1.Id; UserService.Save(editorGroup); - var admin = UserService.GetUserById(Constants.Security.SuperUserId); + var admin = await UserService.GetAsync(Constants.Security.SuperUserKey); admin.StartContentIds = new[] { content1.Id }; UserService.Save(admin); @@ -2072,7 +2074,7 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent new List { new() { RuleType = "test", RuleValue = "test" } })); Assert.IsTrue(PublicAccessService.AddRule(content1, "test2", "test2").Success); - var user = UserService.GetUserById(Constants.Security.SuperUserId); + var user = await UserService.GetAsync(Constants.Security.SuperUserKey); var userGroup = UserService.GetUserGroupByAlias(user.Groups.First().Alias); Assert.IsNotNull(NotificationService.CreateNotification(user, content1, "X")); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Create.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Create.cs index 3e0d3e85f5..79f1e7a42a 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Create.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Create.cs @@ -11,7 +11,7 @@ public partial class DocumentNavigationServiceTests // Arrange DocumentNavigationQueryService.TryGetSiblingsKeys(Root.Key, out IEnumerable initialSiblingsKeys); var initialRootNodeSiblingsCount = initialSiblingsKeys.Count(); - var createModel = CreateContentCreateModel("Root 2", Guid.NewGuid(), Constants.System.RootKey); + var createModel = CreateContentCreateModel("Root 2", Guid.NewGuid(), parentKey: Constants.System.RootKey); // Act var createAttempt = await ContentEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); @@ -36,7 +36,7 @@ public partial class DocumentNavigationServiceTests // Arrange DocumentNavigationQueryService.TryGetChildrenKeys(Child1.Key, out IEnumerable initialChildrenKeys); var initialChild1ChildrenCount = initialChildrenKeys.Count(); - var createModel = CreateContentCreateModel("Grandchild 3", Guid.NewGuid(), Child1.Key); + var createModel = CreateContentCreateModel("Grandchild 3", Guid.NewGuid(), parentKey: Child1.Key); // Act var createAttempt = await ContentEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); @@ -70,7 +70,7 @@ public partial class DocumentNavigationServiceTests { // Arrange Guid newNodeKey = Guid.NewGuid(); - var createModel = CreateContentCreateModel("Child", newNodeKey, parentKey); + var createModel = CreateContentCreateModel("Child", newNodeKey, parentKey: parentKey); // Act await ContentEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Delete.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Delete.cs index 32ba7934b1..e7fb137e19 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Delete.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Delete.cs @@ -63,7 +63,7 @@ public partial class DocumentNavigationServiceTests // Create a new sibling under the same parent var key = Guid.NewGuid(); - var createModel = CreateContentCreateModel("Child 4", key, Root.Key); + var createModel = CreateContentCreateModel("Child 4", key, parentKey: Root.Key); await ContentEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); DocumentNavigationQueryService.TryGetSiblingsKeys(node, out IEnumerable siblingsKeysAfterCreation); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Move.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Move.cs index 0c8dc8c502..54abc92ce5 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Move.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Move.cs @@ -148,7 +148,7 @@ public partial class DocumentNavigationServiceTests // Create a new sibling under the same parent var key = Guid.NewGuid(); - var createModel = CreateContentCreateModel("Child 4", key, Root.Key); + var createModel = CreateContentCreateModel("Child 4", key, parentKey: Root.Key); await ContentEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); DocumentNavigationQueryService.TryGetSiblingsKeys(node, out IEnumerable siblingsKeysAfterCreation); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.MoveToRecycleBin.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.MoveToRecycleBin.cs index f14c83d8fc..eeeaf39368 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.MoveToRecycleBin.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.MoveToRecycleBin.cs @@ -70,7 +70,7 @@ public partial class DocumentNavigationServiceTests // Create a new sibling under the same parent var key = Guid.NewGuid(); - var createModel = CreateContentCreateModel("Child 4", key, Root.Key); + var createModel = CreateContentCreateModel("Child 4", key, parentKey: Root.Key); await ContentEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); DocumentNavigationQueryService.TryGetSiblingsKeys(node, out IEnumerable siblingsKeysAfterCreation); @@ -93,7 +93,7 @@ public partial class DocumentNavigationServiceTests // Create a new grandchild under Child1 var key = Guid.NewGuid(); - var createModel = CreateContentCreateModel("Grandchild 3", key, nodeToMoveToRecycleBin); + var createModel = CreateContentCreateModel("Grandchild 3", key, parentKey: nodeToMoveToRecycleBin); await ContentEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); DocumentNavigationQueryService.TryGetChildrenKeys(nodeToMoveToRecycleBin, out IEnumerable childrenKeysBeforeDeletion); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Rebuild.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Rebuild.cs index addf668cb3..52f9bb77bd 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Rebuild.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Rebuild.cs @@ -2,6 +2,7 @@ using NUnit.Framework; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.Navigation; namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; @@ -22,7 +23,10 @@ public partial class DocumentNavigationServiceTests DocumentNavigationQueryService.TryGetSiblingsKeys(nodeKey, out IEnumerable originalSiblingsKeys); // In-memory navigation structure is empty here - var newDocumentNavigationService = new DocumentNavigationService(GetRequiredService(), GetRequiredService()); + var newDocumentNavigationService = new DocumentNavigationService( + GetRequiredService(), + GetRequiredService(), + GetRequiredService()); var initialNodeExists = newDocumentNavigationService.TryGetParentKey(nodeKey, out _); // Act @@ -67,7 +71,10 @@ public partial class DocumentNavigationServiceTests DocumentNavigationQueryService.TryGetSiblingsKeysInBin(nodeKey, out IEnumerable originalSiblingsKeys); // In-memory navigation structure is empty here - var newDocumentNavigationService = new DocumentNavigationService(GetRequiredService(), GetRequiredService()); + var newDocumentNavigationService = new DocumentNavigationService( + GetRequiredService(), + GetRequiredService(), + GetRequiredService()); var initialNodeExists = newDocumentNavigationService.TryGetParentKeyInBin(nodeKey, out _); // Act diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Sort.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Sort.cs index 7d1e113974..bf1768d0c0 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Sort.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Sort.cs @@ -87,7 +87,7 @@ public partial class DocumentNavigationServiceTests public async Task Structure_Updates_When_Sorting_Items_At_Root() { // Arrange - var anotherRootCreateModel = CreateContentCreateModel("Root 2", Guid.NewGuid(), Constants.System.RootKey); + var anotherRootCreateModel = CreateContentCreateModel("Root 2", Guid.NewGuid(), parentKey: Constants.System.RootKey); await ContentEditingService.CreateAsync(anotherRootCreateModel, Constants.Security.SuperUserKey); DocumentNavigationQueryService.TryGetRootKeys(out IEnumerable initialRootKeys); @@ -175,9 +175,9 @@ public partial class DocumentNavigationServiceTests { // Arrange Guid node = Root.Key; - var anotherRootCreateModel1 = CreateContentCreateModel("Root 2", Guid.NewGuid(), Constants.System.RootKey); + var anotherRootCreateModel1 = CreateContentCreateModel("Root 2", Guid.NewGuid(), parentKey: Constants.System.RootKey); await ContentEditingService.CreateAsync(anotherRootCreateModel1, Constants.Security.SuperUserKey); - var anotherRootCreateModel2 = CreateContentCreateModel("Root 3", Guid.NewGuid(), Constants.System.RootKey); + var anotherRootCreateModel2 = CreateContentCreateModel("Root 3", Guid.NewGuid(), parentKey: Constants.System.RootKey); await ContentEditingService.CreateAsync(anotherRootCreateModel2, Constants.Security.SuperUserKey); DocumentNavigationQueryService.TryGetRootKeys(out IEnumerable initialRootKeys); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.cs index 9fdedc5257..6d1db8132f 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.cs @@ -25,7 +25,6 @@ public partial class DocumentNavigationServiceTests : DocumentNavigationServiceT ContentType = ContentTypeBuilder.CreateSimpleContentType("page", "Page"); ContentType.Key = new Guid("DD72B8A6-2CE3-47F0-887E-B695A1A5D086"); ContentType.AllowedAsRoot = true; - ContentType.AllowedTemplates = null; ContentType.AllowedContentTypes = new[] { new ContentTypeSort(ContentType.Key, 0, ContentType.Alias) }; await ContentTypeService.CreateAsync(ContentType, Constants.Security.SuperUserKey); @@ -34,35 +33,35 @@ public partial class DocumentNavigationServiceTests : DocumentNavigationServiceT var rootCreateAttempt = await ContentEditingService.CreateAsync(rootModel, Constants.Security.SuperUserKey); Root = rootCreateAttempt.Result.Content!; - var child1Model = CreateContentCreateModel("Child 1", new Guid("C6173927-0C59-4778-825D-D7B9F45D8DDE"), Root.Key); + var child1Model = CreateContentCreateModel("Child 1", new Guid("C6173927-0C59-4778-825D-D7B9F45D8DDE"), parentKey: Root.Key); var child1CreateAttempt = await ContentEditingService.CreateAsync(child1Model, Constants.Security.SuperUserKey); Child1 = child1CreateAttempt.Result.Content!; - var grandchild1Model = CreateContentCreateModel("Grandchild 1", new Guid("E856AC03-C23E-4F63-9AA9-681B42A58573"), Child1.Key); + var grandchild1Model = CreateContentCreateModel("Grandchild 1", new Guid("E856AC03-C23E-4F63-9AA9-681B42A58573"), parentKey: Child1.Key); var grandchild1CreateAttempt = await ContentEditingService.CreateAsync(grandchild1Model, Constants.Security.SuperUserKey); Grandchild1 = grandchild1CreateAttempt.Result.Content!; - var grandchild2Model = CreateContentCreateModel("Grandchild 2", new Guid("A1B1B217-B02F-4307-862C-A5E22DB729EB"), Child1.Key); + var grandchild2Model = CreateContentCreateModel("Grandchild 2", new Guid("A1B1B217-B02F-4307-862C-A5E22DB729EB"), parentKey: Child1.Key); var grandchild2CreateAttempt = await ContentEditingService.CreateAsync(grandchild2Model, Constants.Security.SuperUserKey); Grandchild2 = grandchild2CreateAttempt.Result.Content!; - var child2Model = CreateContentCreateModel("Child 2", new Guid("60E0E5C4-084E-4144-A560-7393BEAD2E96"), Root.Key); + var child2Model = CreateContentCreateModel("Child 2", new Guid("60E0E5C4-084E-4144-A560-7393BEAD2E96"), parentKey: Root.Key); var child2CreateAttempt = await ContentEditingService.CreateAsync(child2Model, Constants.Security.SuperUserKey); Child2 = child2CreateAttempt.Result.Content!; - var grandchild3Model = CreateContentCreateModel("Grandchild 3", new Guid("D63C1621-C74A-4106-8587-817DEE5FB732"), Child2.Key); + var grandchild3Model = CreateContentCreateModel("Grandchild 3", new Guid("D63C1621-C74A-4106-8587-817DEE5FB732"), parentKey: Child2.Key); var grandchild3CreateAttempt = await ContentEditingService.CreateAsync(grandchild3Model, Constants.Security.SuperUserKey); Grandchild3 = grandchild3CreateAttempt.Result.Content!; - var greatGrandchild1Model = CreateContentCreateModel("Great-grandchild 1", new Guid("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7"), Grandchild3.Key); + var greatGrandchild1Model = CreateContentCreateModel("Great-grandchild 1", new Guid("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7"), parentKey: Grandchild3.Key); var greatGrandchild1CreateAttempt = await ContentEditingService.CreateAsync(greatGrandchild1Model, Constants.Security.SuperUserKey); GreatGrandchild1 = greatGrandchild1CreateAttempt.Result.Content!; - var child3Model = CreateContentCreateModel("Child 3", new Guid("B606E3FF-E070-4D46-8CB9-D31352029FDF"), Root.Key); + var child3Model = CreateContentCreateModel("Child 3", new Guid("B606E3FF-E070-4D46-8CB9-D31352029FDF"), parentKey: Root.Key); var child3CreateAttempt = await ContentEditingService.CreateAsync(child3Model, Constants.Security.SuperUserKey); Child3 = child3CreateAttempt.Result.Content!; - var grandchild4Model = CreateContentCreateModel("Grandchild 4", new Guid("F381906C-223C-4466-80F7-B63B4EE073F8"), Child3.Key); + var grandchild4Model = CreateContentCreateModel("Grandchild 4", new Guid("F381906C-223C-4466-80F7-B63B4EE073F8"), parentKey: Child3.Key); var grandchild4CreateAttempt = await ContentEditingService.CreateAsync(grandchild4Model, Constants.Security.SuperUserKey); Grandchild4 = grandchild4CreateAttempt.Result.Content!; } @@ -87,4 +86,199 @@ public partial class DocumentNavigationServiceTests : DocumentNavigationServiceT // Assert Assert.IsFalse(nodeExists); } + + [Test] + public async Task Can_Filter_Root_Items_By_Type() + { + // Arrange + DocumentNavigationQueryService.TryGetRootKeysOfType(ContentType.Alias, out IEnumerable initialRootKeysOfType); + List initialRootOfTypeList = initialRootKeysOfType.ToList(); + + // Doc Type + var anotherContentType = ContentTypeBuilder.CreateSimpleContentType("anotherPage", "Another page"); + anotherContentType.Key = new Guid("58A2958E-B34F-4289-A225-E99EEC2456AB"); + anotherContentType.AllowedContentTypes = new[] { new ContentTypeSort(anotherContentType.Key, 0, anotherContentType.Alias) }; + anotherContentType.AllowedAsRoot = true; + await ContentTypeService.CreateAsync(anotherContentType, Constants.Security.SuperUserKey); + + // Update old doc type + ContentType.AllowedContentTypes = new[] { new ContentTypeSort(ContentType.Key, 0, ContentType.Alias), new ContentTypeSort(anotherContentType.Key, 1, anotherContentType.Alias) }; + await ContentTypeService.UpdateAsync(ContentType, Constants.Security.SuperUserKey); + + // Content + var root2Model = CreateContentCreateModel("Root 2", new Guid("11233548-2E87-4D3E-8FC4-4400F9DBEF56"), anotherContentType.Key); // Using new doc type + await ContentEditingService.CreateAsync(root2Model, Constants.Security.SuperUserKey); + var root3Model = CreateContentCreateModel("Root 3", new Guid("6E10F212-CE7F-47B5-A796-345861AEE613"), anotherContentType.Key); // Using new doc type + await ContentEditingService.CreateAsync(root3Model, Constants.Security.SuperUserKey); + + // Act + DocumentNavigationQueryService.TryGetRootKeysOfType(anotherContentType.Alias, out IEnumerable filteredRootKeysOfType); + List filteredRootList = filteredRootKeysOfType.ToList(); + + DocumentNavigationQueryService.TryGetRootKeys(out IEnumerable allRootKeys); + List allRootList = allRootKeys.ToList(); + + // Assert + Assert.Multiple(() => + { + Assert.AreEqual(1, initialRootOfTypeList.Count); // Verify that loaded doc types can be used to filter + Assert.AreEqual(2, filteredRootList.Count); // Verify that new doc type can be used to filter + Assert.AreEqual(3, allRootList.Count); + }); + } + + [Test] + public async Task Can_Filter_Children_By_Type() + { + // Arrange + Guid parentKey = Root.Key; + DocumentNavigationQueryService.TryGetChildrenKeysOfType(parentKey, ContentType.Alias, out IEnumerable initialChildrenKeysOfType); + List initialChildrenOfTypeList = initialChildrenKeysOfType.ToList(); + + // Doc Type + var anotherContentType = ContentTypeBuilder.CreateSimpleContentType("anotherPage", "Another page"); + anotherContentType.Key = new Guid("58A2958E-B34F-4289-A225-E99EEC2456AB"); + anotherContentType.AllowedContentTypes = new[] { new ContentTypeSort(anotherContentType.Key, 0, anotherContentType.Alias) }; + await ContentTypeService.CreateAsync(anotherContentType, Constants.Security.SuperUserKey); + + // Update old doc type + ContentType.AllowedContentTypes = new[] { new ContentTypeSort(ContentType.Key, 0, ContentType.Alias), new ContentTypeSort(anotherContentType.Key, 1, anotherContentType.Alias) }; + await ContentTypeService.UpdateAsync(ContentType, Constants.Security.SuperUserKey); + + // Content + var child4Model = CreateContentCreateModel("Child 4", new Guid("11233548-2E87-4D3E-8FC4-4400F9DBEF56"), anotherContentType.Key, parentKey); // Using new doc type + await ContentEditingService.CreateAsync(child4Model, Constants.Security.SuperUserKey); + + // Act + DocumentNavigationQueryService.TryGetChildrenKeysOfType(parentKey, anotherContentType.Alias, out IEnumerable filteredChildrenKeysOfType); + List filteredChildrenList = filteredChildrenKeysOfType.ToList(); + + DocumentNavigationQueryService.TryGetChildrenKeys(parentKey, out IEnumerable allChildrenKeys); + List allChildrenList = allChildrenKeys.ToList(); + + // Assert + Assert.Multiple(() => + { + Assert.AreEqual(3, initialChildrenOfTypeList.Count); // Verify that loaded doc types can be used to filter + Assert.AreEqual(1, filteredChildrenList.Count); // Verify that new doc type can be used to filter + Assert.AreEqual(4, allChildrenList.Count); + }); + } + + [Test] + public async Task Can_Filter_Descendants_By_Type() + { + // Arrange + Guid parentKey = Child2.Key; + DocumentNavigationQueryService.TryGetDescendantsKeysOfType(parentKey, ContentType.Alias, out IEnumerable initialDescendantsKeysOfType); + List initialDescendantsOfTypeList = initialDescendantsKeysOfType.ToList(); + + // Doc Type + var anotherContentType = ContentTypeBuilder.CreateSimpleContentType("anotherPage", "Another page"); + anotherContentType.Key = new Guid("58A2958E-B34F-4289-A225-E99EEC2456AB"); + anotherContentType.AllowedContentTypes = new[] { new ContentTypeSort(anotherContentType.Key, 0, anotherContentType.Alias) }; + await ContentTypeService.CreateAsync(anotherContentType, Constants.Security.SuperUserKey); + + // Update old doc type + ContentType.AllowedContentTypes = new[] { new ContentTypeSort(ContentType.Key, 0, ContentType.Alias), new ContentTypeSort(anotherContentType.Key, 1, anotherContentType.Alias) }; + await ContentTypeService.UpdateAsync(ContentType, Constants.Security.SuperUserKey); + + // Content + var greatGreatGrandchild1Model = CreateContentCreateModel("Great-great-grandchild 1", new Guid("11233548-2E87-4D3E-8FC4-4400F9DBEF56"), anotherContentType.Key, GreatGrandchild1.Key); // Using new doc type + await ContentEditingService.CreateAsync(greatGreatGrandchild1Model, Constants.Security.SuperUserKey); + + // Act + DocumentNavigationQueryService.TryGetDescendantsKeysOfType(parentKey, anotherContentType.Alias, out IEnumerable filteredDescendantsKeysOfType); + List filteredDescendantsList = filteredDescendantsKeysOfType.ToList(); + + DocumentNavigationQueryService.TryGetDescendantsKeys(parentKey, out IEnumerable allDescendantsKeys); + List allDescendantsList = allDescendantsKeys.ToList(); + + // Assert + Assert.Multiple(() => + { + Assert.AreEqual(2, initialDescendantsOfTypeList.Count); // Verify that loaded doc types can be used to filter + Assert.AreEqual(1, filteredDescendantsList.Count); // Verify that new doc type can be used to filter + Assert.AreEqual(3, allDescendantsList.Count); + }); + } + + [Test] + public async Task Can_Filter_Ancestors_By_Type() + { + // Arrange + Guid childKey = new Guid("1802D6B4-4A3C-4EBA-AFA3-1AF82C2D6483"); + + // Doc Type + var anotherContentType = ContentTypeBuilder.CreateSimpleContentType("anotherPage", "Another page"); + anotherContentType.Key = new Guid("58A2958E-B34F-4289-A225-E99EEC2456AB"); + anotherContentType.AllowedContentTypes = new[] { new ContentTypeSort(anotherContentType.Key, 0, anotherContentType.Alias) }; + await ContentTypeService.CreateAsync(anotherContentType, Constants.Security.SuperUserKey); + + // Update old doc type + ContentType.AllowedContentTypes = new[] { new ContentTypeSort(ContentType.Key, 0, ContentType.Alias), new ContentTypeSort(anotherContentType.Key, 1, anotherContentType.Alias) }; + await ContentTypeService.UpdateAsync(ContentType, Constants.Security.SuperUserKey); + + // Content + var greatGrandchild2Model = CreateContentCreateModel("Great-grandchild 2", new Guid("11233548-2E87-4D3E-8FC4-4400F9DBEF56"), anotherContentType.Key, Grandchild4.Key); // Using new doc type + await ContentEditingService.CreateAsync(greatGrandchild2Model, Constants.Security.SuperUserKey); + var greatGreatGrandchild1Model = CreateContentCreateModel("Great-great-grandchild 1", childKey, anotherContentType.Key, greatGrandchild2Model.Key); // Using new doc type + await ContentEditingService.CreateAsync(greatGreatGrandchild1Model, Constants.Security.SuperUserKey); + + // Act + DocumentNavigationQueryService.TryGetAncestorsKeysOfType(childKey, ContentType.Alias, out IEnumerable ancestorsKeysOfOriginalType); + List ancestorsKeysOfOriginalTypeList = ancestorsKeysOfOriginalType.ToList(); + + DocumentNavigationQueryService.TryGetAncestorsKeysOfType(childKey, anotherContentType.Alias, out IEnumerable ancestorsKeysOfNewType); + List ancestorsKeysOfNewTypeList = ancestorsKeysOfNewType.ToList(); + + DocumentNavigationQueryService.TryGetAncestorsKeys(childKey, out IEnumerable allAncestorsKeys); + List allAncestorsList = allAncestorsKeys.ToList(); + + // Assert + Assert.Multiple(() => + { + Assert.AreEqual(3, ancestorsKeysOfOriginalTypeList.Count); // Verify that loaded doc types can be used to filter + Assert.AreEqual(1, ancestorsKeysOfNewTypeList.Count); // Verify that new doc type can be used to filter + Assert.AreEqual(4, allAncestorsList.Count); + }); + } + + [Test] + public async Task Can_Filter_Siblings_By_Type() + { + // Arrange + Guid parentKey = Child3.Key; + DocumentNavigationQueryService.TryGetSiblingsKeysOfType(parentKey, ContentType.Alias, out IEnumerable initialSiblingsKeysOfType); + List initialSiblingsOfTypeList = initialSiblingsKeysOfType.ToList(); + + // Doc Type + var anotherContentType = ContentTypeBuilder.CreateSimpleContentType("anotherPage", "Another page"); + anotherContentType.Key = new Guid("58A2958E-B34F-4289-A225-E99EEC2456AB"); + anotherContentType.AllowedContentTypes = new[] { new ContentTypeSort(anotherContentType.Key, 0, anotherContentType.Alias) }; + await ContentTypeService.CreateAsync(anotherContentType, Constants.Security.SuperUserKey); + + // Update old doc type + ContentType.AllowedContentTypes = new[] { new ContentTypeSort(ContentType.Key, 0, ContentType.Alias), new ContentTypeSort(anotherContentType.Key, 1, anotherContentType.Alias) }; + await ContentTypeService.UpdateAsync(ContentType, Constants.Security.SuperUserKey); + + // Content + var child4Model = CreateContentCreateModel("Child 4", new Guid("11233548-2E87-4D3E-8FC4-4400F9DBEF56"), anotherContentType.Key, Root.Key); // Using new doc type + await ContentEditingService.CreateAsync(child4Model, Constants.Security.SuperUserKey); + + // Act + DocumentNavigationQueryService.TryGetSiblingsKeysOfType(parentKey, anotherContentType.Alias, out IEnumerable filteredSiblingsKeysOfType); + List filteredSiblingsList = filteredSiblingsKeysOfType.ToList(); + + DocumentNavigationQueryService.TryGetSiblingsKeys(parentKey, out IEnumerable allSiblingsKeys); + List allSiblingsList = allSiblingsKeys.ToList(); + + // Assert + Assert.Multiple(() => + { + Assert.AreEqual(2, initialSiblingsOfTypeList.Count); // Verify that loaded doc types can be used to filter + Assert.AreEqual(1, filteredSiblingsList.Count); // Verify that new doc type can be used to filter + Assert.AreEqual(3, allSiblingsList.Count); + }); + } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTestsBase.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTestsBase.cs index 375ddb8391..4e7ea473de 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTestsBase.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTestsBase.cs @@ -1,4 +1,3 @@ -using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; @@ -45,10 +44,10 @@ public abstract class DocumentNavigationServiceTestsBase : UmbracoIntegrationTes protected IContent Grandchild4 { get; set; } - protected ContentCreateModel CreateContentCreateModel(string name, Guid key, Guid? parentKey = null) + protected ContentCreateModel CreateContentCreateModel(string name, Guid key, Guid? contentTypeKey = null, Guid? parentKey = null) => new() { - ContentTypeKey = ContentType.Key, + ContentTypeKey = contentTypeKey ?? ContentType.Key, ParentKey = parentKey ?? Constants.System.RootKey, InvariantName = name, Key = key, diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Rebuild.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Rebuild.cs index 65244743e3..c5e1487a7e 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Rebuild.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Rebuild.cs @@ -2,6 +2,7 @@ using NUnit.Framework; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.Navigation; namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; @@ -22,7 +23,10 @@ public partial class MediaNavigationServiceTests MediaNavigationQueryService.TryGetSiblingsKeys(nodeKey, out IEnumerable originalSiblingsKeys); // In-memory navigation structure is empty here - var newMediaNavigationService = new MediaNavigationService(GetRequiredService(), GetRequiredService()); + var newMediaNavigationService = new MediaNavigationService( + GetRequiredService(), + GetRequiredService(), + GetRequiredService()); var initialNodeExists = newMediaNavigationService.TryGetParentKey(nodeKey, out _); // Act @@ -67,7 +71,10 @@ public partial class MediaNavigationServiceTests MediaNavigationQueryService.TryGetSiblingsKeysInBin(nodeKey, out IEnumerable originalSiblingsKeys); // In-memory navigation structure is empty here - var newMediaNavigationService = new MediaNavigationService(GetRequiredService(), GetRequiredService()); + var newMediaNavigationService = new MediaNavigationService( + GetRequiredService(), + GetRequiredService(), + GetRequiredService()); var initialNodeExists = newMediaNavigationService.TryGetParentKeyInBin(nodeKey, out _); // Act diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.cs index ebef7fe046..6a4761fa81 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.cs @@ -76,4 +76,27 @@ public partial class MediaNavigationServiceTests : MediaNavigationServiceTestsBa // Assert Assert.IsFalse(nodeExists); } + + [Test] + public void Can_Filter_Children_By_Type() + { + // Arrange + MediaNavigationQueryService.TryGetChildrenKeys(Album.Key, out IEnumerable allChildrenKeys); + List allChildrenList = allChildrenKeys.ToList(); + + // Act + MediaNavigationQueryService.TryGetChildrenKeysOfType(Album.Key, ImageMediaType.Alias, out IEnumerable childrenKeysOfTypeImage); + List imageChildrenList = childrenKeysOfTypeImage.ToList(); + + MediaNavigationQueryService.TryGetChildrenKeysOfType(Album.Key, FolderMediaType.Alias, out IEnumerable childrenKeysOfTypeFolder); + List folderChildrenList = childrenKeysOfTypeFolder.ToList(); + + // Assert + Assert.Multiple(() => + { + Assert.AreEqual(1, imageChildrenList.Count); + Assert.AreEqual(2, folderChildrenList.Count); + Assert.AreEqual(3, allChildrenList.Count); + }); + } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/UserGroupServiceValidationTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/UserGroupServiceValidationTests.cs index 9d3d8aeee5..d14b887b57 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/UserGroupServiceValidationTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/UserGroupServiceValidationTests.cs @@ -169,11 +169,9 @@ public class UserGroupServiceValidationTests : UmbracoIntegrationTest Assert.AreEqual(updateName, updatedGroup.Name); } - // these keys are not defined as "const" in Constants.Security but as "static readonly", so we have to hardcode - // them here ("static readonly" can't be used as testcase inputs as they are not constants) - [TestCase("E5E7F6C8-7F9C-4B5B-8D5D-9E1E5A4F7E4D", "admin")] - [TestCase("8C6AD70F-D307-4E4A-AF58-72C2E4E9439D", "sensitiveData")] - [TestCase("F2012E4C-D232-4BD1-8EAE-4384032D97D8", "translator")] + [TestCase(Constants.Security.AdminGroupKeyString, "admin")] + [TestCase(Constants.Security.SensitiveDataGroupKeyString, "sensitiveData")] + [TestCase(Constants.Security.TranslatorGroupString, "translator")] public async Task Cannot_Delete_System_UserGroups(string userGroupKeyAsString, string expectedGroupAlias) { // since we can't use the constants as input, let's make sure we don't get false positives by double checking the group alias @@ -188,10 +186,8 @@ public class UserGroupServiceValidationTests : UmbracoIntegrationTest Assert.AreEqual(UserGroupOperationStatus.CanNotDeleteIsSystemUserGroup, result.Result); } - // these keys are not defined as "const" in Constants.Security but as "static readonly", so we have to hardcode - // them here ("static readonly" can't be used as testcase inputs as they are not constants) - [TestCase("44DC260E-B4D4-4DD9-9081-EEC5598F1641", "editor")] - [TestCase("9FC2A16F-528C-46D6-A014-75BF4EC2480C", "writer")] + [TestCase( Constants.Security.EditorGroupKeyString, "editor")] + [TestCase(Constants.Security.WriterGroupKeyString, "writer")] public async Task Can_Delete_Non_System_UserGroups(string userGroupKeyAsString, string expectedGroupAlias) { // since we can't use the constants as input, let's make sure we don't get false positives by double checking the group alias @@ -203,6 +199,6 @@ public class UserGroupServiceValidationTests : UmbracoIntegrationTest var result = await UserGroupService.DeleteAsync(key); Assert.IsTrue(result.Success); - Assert.AreEqual(UserGroupOperationStatus.Success, result.Result); + Assert.AreEqual(result.Result,UserGroupOperationStatus.Success); } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/UserServiceCrudTests.Filter.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/UserServiceCrudTests.Filter.cs index 82d28c5441..71b557d4d2 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/UserServiceCrudTests.Filter.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/UserServiceCrudTests.Filter.cs @@ -16,7 +16,7 @@ public partial class UserServiceCrudTests public async Task Cannot_Request_Disabled_If_Hidden(UserState includeState) { var userService = CreateUserService(new SecuritySettings {HideDisabledUsersInBackOffice = true}); - var editorGroup = await UserGroupService.GetAsync(Constants.Security.EditorGroupAlias); + var editorGroup = await UserGroupService.GetAsync(Constants.Security.EditorGroupKey); var createModel = new UserCreateModel { @@ -44,8 +44,8 @@ public partial class UserServiceCrudTests public async Task Only_Super_User_Can_Filter_Super_user() { var userService = CreateUserService(); - var editorGroup = await UserGroupService.GetAsync(Constants.Security.EditorGroupAlias); - var adminGroup = await UserGroupService.GetAsync(Constants.Security.AdminGroupAlias); + var editorGroup = await UserGroupService.GetAsync(Constants.Security.EditorGroupKey); + var adminGroup = await UserGroupService.GetAsync(Constants.Security.AdminGroupKey); var nonSuperCreateModel = new UserCreateModel { @@ -78,7 +78,7 @@ public partial class UserServiceCrudTests public async Task Super_User_Can_Filter_Super_User() { var userService = CreateUserService(); - var editorGroup = await UserGroupService.GetAsync(Constants.Security.EditorGroupAlias); + var editorGroup = await UserGroupService.GetAsync(Constants.Security.EditorGroupKey); var nonSuperCreateModel = new UserCreateModel { @@ -107,8 +107,8 @@ public partial class UserServiceCrudTests public async Task Non_Admins_Cannot_Filter_Admins() { var userService = CreateUserService(); - var adminGroup = await UserGroupService.GetAsync(Constants.Security.AdminGroupAlias); - var editorGroup = await UserGroupService.GetAsync(Constants.Security.EditorGroupAlias); + var adminGroup = await UserGroupService.GetAsync(Constants.Security.AdminGroupKey); + var editorGroup = await UserGroupService.GetAsync(Constants.Security.EditorGroupKey); var editorCreateModel = new UserCreateModel { @@ -146,8 +146,8 @@ public partial class UserServiceCrudTests public async Task Admins_Can_Filter_Admins() { var userService = CreateUserService(); - var adminGroup = await UserGroupService.GetAsync(Constants.Security.AdminGroupAlias); - var editorGroup = await UserGroupService.GetAsync(Constants.Security.EditorGroupAlias); + var adminGroup = await UserGroupService.GetAsync(Constants.Security.AdminGroupKey); + var editorGroup = await UserGroupService.GetAsync(Constants.Security.EditorGroupKey); var editorCreateModel = new UserCreateModel { @@ -185,10 +185,10 @@ public partial class UserServiceCrudTests private async Task CreateTestUsers(IUserService userService) { - var editorGroup = await UserGroupService.GetAsync(Constants.Security.EditorGroupAlias); - var adminGroup = await UserGroupService.GetAsync(Constants.Security.AdminGroupAlias); - var writerGroup = await UserGroupService.GetAsync(Constants.Security.WriterGroupAlias); - var translatorGroup = await UserGroupService.GetAsync(Constants.Security.TranslatorGroupAlias); + var editorGroup = await UserGroupService.GetAsync(Constants.Security.EditorGroupKey); + var adminGroup = await UserGroupService.GetAsync(Constants.Security.AdminGroupKey); + var writerGroup = await UserGroupService.GetAsync(Constants.Security.WriterGroupKey); + var translatorGroup = await UserGroupService.GetAsync(Constants.Security.TranslatorGroupKey); var createModels = new List { @@ -262,7 +262,7 @@ public partial class UserServiceCrudTests var userService = CreateUserService(); await CreateTestUsers(userService); - var editorGroup = await UserGroupService.GetAsync(Constants.Security.EditorGroupAlias); + var editorGroup = await UserGroupService.GetAsync(Constants.Security.EditorGroupKey); var filter = new UserFilter { ExcludeUserGroups = new HashSet { editorGroup!.Key } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/UserServiceCrudTests.Get.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/UserServiceCrudTests.Get.cs index 69263159e6..6d33c92a8a 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/UserServiceCrudTests.Get.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/UserServiceCrudTests.Get.cs @@ -13,8 +13,8 @@ public partial class UserServiceCrudTests public async Task Only_Super_User_Can_Get_Super_user() { var userService = CreateUserService(); - var editorGroup = await UserGroupService.GetAsync(Constants.Security.EditorGroupAlias); - var adminGroup = await UserGroupService.GetAsync(Constants.Security.AdminGroupAlias); + var editorGroup = await UserGroupService.GetAsync(Constants.Security.EditorGroupKey); + var adminGroup = await UserGroupService.GetAsync(Constants.Security.AdminGroupKey); var nonSuperCreateModel = new UserCreateModel { @@ -43,7 +43,7 @@ public partial class UserServiceCrudTests public async Task Super_User_Can_See_Super_User() { var userService = CreateUserService(); - var editorGroup = await UserGroupService.GetAsync(Constants.Security.EditorGroupAlias); + var editorGroup = await UserGroupService.GetAsync(Constants.Security.EditorGroupKey); var nonSuperCreateModel = new UserCreateModel { @@ -71,8 +71,8 @@ public partial class UserServiceCrudTests public async Task Non_Admins_Cannot_Get_admins() { var userService = CreateUserService(); - var adminGroup = await UserGroupService.GetAsync(Constants.Security.AdminGroupAlias); - var editorGroup = await UserGroupService.GetAsync(Constants.Security.EditorGroupAlias); + var adminGroup = await UserGroupService.GetAsync(Constants.Security.AdminGroupKey); + var editorGroup = await UserGroupService.GetAsync(Constants.Security.EditorGroupKey); var editorCreateModel = new UserCreateModel { @@ -107,8 +107,8 @@ public partial class UserServiceCrudTests public async Task Admins_Can_See_Admins() { var userService = CreateUserService(); - var adminGroup = await UserGroupService.GetAsync(Constants.Security.AdminGroupAlias); - var editorGroup = await UserGroupService.GetAsync(Constants.Security.EditorGroupAlias); + var adminGroup = await UserGroupService.GetAsync(Constants.Security.AdminGroupKey); + var editorGroup = await UserGroupService.GetAsync(Constants.Security.EditorGroupKey); var editorCreateModel = new UserCreateModel { @@ -144,7 +144,7 @@ public partial class UserServiceCrudTests public async Task Cannot_See_Disabled_When_HideDisabled_Is_True() { var userService = CreateUserService(securitySettings: new SecuritySettings { HideDisabledUsersInBackOffice = true }); - var editorGroup = await UserGroupService.GetAsync(Constants.Security.EditorGroupAlias); + var editorGroup = await UserGroupService.GetAsync(Constants.Security.EditorGroupKey); var firstEditorCreateModel = new UserCreateModel { diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/UserServiceCrudTests.PartialUpdates.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/UserServiceCrudTests.PartialUpdates.cs index 013e53057e..5eaf3b07ae 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/UserServiceCrudTests.PartialUpdates.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/UserServiceCrudTests.PartialUpdates.cs @@ -11,7 +11,7 @@ public partial class UserServiceCrudTests [Test] public async Task Can_Enable_User() { - var editorUserGroup = await UserGroupService.GetAsync(Constants.Security.EditorGroupAlias); + var editorUserGroup = await UserGroupService.GetAsync(Constants.Security.EditorGroupKey); var userCreateModel = new UserCreateModel { @@ -39,7 +39,7 @@ public partial class UserServiceCrudTests [Test] public async Task Can_Disable_User() { - var editorUserGroup = await UserGroupService.GetAsync(Constants.Security.EditorGroupAlias); + var editorUserGroup = await UserGroupService.GetAsync(Constants.Security.EditorGroupKey); var userCreateModel = new UserCreateModel { @@ -63,7 +63,7 @@ public partial class UserServiceCrudTests [Test] public async Task User_Cannot_Disable_Self() { - var adminUserGroup = await UserGroupService.GetAsync(Constants.Security.AdminGroupAlias); + var adminUserGroup = await UserGroupService.GetAsync(Constants.Security.AdminGroupKey); var userCreateModel = new UserCreateModel { @@ -85,7 +85,7 @@ public partial class UserServiceCrudTests [Test] public async Task Cannot_Disable_Invited_User() { - var editorUserGroup = await UserGroupService.GetAsync(Constants.Security.EditorGroupAlias); + var editorUserGroup = await UserGroupService.GetAsync(Constants.Security.EditorGroupKey); var userInviteModel = new UserInviteModel { @@ -108,7 +108,7 @@ public partial class UserServiceCrudTests [Test] public async Task Enable_Missing_User_Fails_With_Not_Found() { - var editorUserGroup = await UserGroupService.GetAsync(Constants.Security.EditorGroupAlias); + var editorUserGroup = await UserGroupService.GetAsync(Constants.Security.EditorGroupKey); var userCreateModel = new UserCreateModel { @@ -132,7 +132,7 @@ public partial class UserServiceCrudTests [Test] public async Task Disable_Missing_User_Fails_With_Not_Found() { - var editorUserGroup = await UserGroupService.GetAsync(Constants.Security.EditorGroupAlias); + var editorUserGroup = await UserGroupService.GetAsync(Constants.Security.EditorGroupKey); var userCreateModel = new UserCreateModel { diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/UserServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/UserServiceTests.cs index 67048e2900..e20fcb5310 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/UserServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/UserServiceTests.cs @@ -13,6 +13,7 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.OperationStatus; using Umbraco.Cms.Tests.Common.Builders; using Umbraco.Cms.Tests.Common.Testing; using Umbraco.Cms.Tests.Integration.Testing; @@ -967,6 +968,54 @@ public class UserServiceTests : UmbracoIntegrationTest } } + [TestCase(UserKind.Default, UserClientCredentialsOperationStatus.InvalidUser)] + [TestCase(UserKind.Api, UserClientCredentialsOperationStatus.Success)] + public async Task Can_Assign_ClientId_To_Api_User(UserKind userKind, UserClientCredentialsOperationStatus expectedResult) + { + // Arrange + var user = await CreateTestUser(userKind); + + // Act + var result = await UserService.AddClientIdAsync(user.Key, "client-one"); + + // Assert + Assert.AreEqual(expectedResult, result); + } + + [TestCase("abcdefghijklmnopqrstuvwxyz", UserClientCredentialsOperationStatus.Success)] + [TestCase("ABCDEFGHIJKLMNOPQRSTUVWXYZ", UserClientCredentialsOperationStatus.Success)] + [TestCase("1234567890", UserClientCredentialsOperationStatus.Success)] + [TestCase(".-_~", UserClientCredentialsOperationStatus.Success)] + [TestCase("!", UserClientCredentialsOperationStatus.InvalidClientId)] + [TestCase("#", UserClientCredentialsOperationStatus.InvalidClientId)] + [TestCase("$", UserClientCredentialsOperationStatus.InvalidClientId)] + [TestCase("&", UserClientCredentialsOperationStatus.InvalidClientId)] + [TestCase("'", UserClientCredentialsOperationStatus.InvalidClientId)] + [TestCase("(", UserClientCredentialsOperationStatus.InvalidClientId)] + [TestCase(")", UserClientCredentialsOperationStatus.InvalidClientId)] + [TestCase("*", UserClientCredentialsOperationStatus.InvalidClientId)] + [TestCase("+", UserClientCredentialsOperationStatus.InvalidClientId)] + [TestCase(",", UserClientCredentialsOperationStatus.InvalidClientId)] + [TestCase("/", UserClientCredentialsOperationStatus.InvalidClientId)] + [TestCase(":", UserClientCredentialsOperationStatus.InvalidClientId)] + [TestCase(";", UserClientCredentialsOperationStatus.InvalidClientId)] + [TestCase("=", UserClientCredentialsOperationStatus.InvalidClientId)] + [TestCase("?", UserClientCredentialsOperationStatus.InvalidClientId)] + [TestCase("@", UserClientCredentialsOperationStatus.InvalidClientId)] + [TestCase("[", UserClientCredentialsOperationStatus.InvalidClientId)] + [TestCase("]", UserClientCredentialsOperationStatus.InvalidClientId)] + public async Task Can_Use_Only_Unreserved_Characters_For_ClientId(string clientId, UserClientCredentialsOperationStatus expectedResult) + { + // Arrange + var user = await CreateTestUser(UserKind.Api); + + // Act + var result = await UserService.AddClientIdAsync(user.Key, clientId); + + // Assert + Assert.AreEqual(expectedResult, result); + } + private Content[] BuildContentItems(int numberToCreate) { var template = TemplateBuilder.CreateTextPageTemplate(); @@ -999,6 +1048,25 @@ public class UserServiceTests : UmbracoIntegrationTest return user; } + private async Task CreateTestUser(UserKind userKind) + { + var userGroup = CreateTestUserGroup(); + + var result = await UserService.CreateAsync( + Constants.Security.SuperUserKey, + new UserCreateModel + { + Email = "test1@test.com", + Id = Guid.NewGuid(), + Kind = userKind, + Name = "test1@test.com", + UserName = "test1@test.com", + UserGroupKeys = new HashSet { userGroup.Key } + }); + Assert.IsTrue(result.Success); + return result.Result.CreatedUser!; + } + private List CreateTestUsers(int[] startContentIds, IUserGroup userGroup, int numberToCreate) { var users = new List(); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Persistence.EFCore/DbContext/CustomDbContextUmbracoProviderTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Persistence.EFCore/DbContext/CustomDbContextUmbracoProviderTests.cs index b2a71e0a9c..1ba57c1a70 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Persistence.EFCore/DbContext/CustomDbContextUmbracoProviderTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Persistence.EFCore/DbContext/CustomDbContextUmbracoProviderTests.cs @@ -68,34 +68,3 @@ public class CustomDbContextCustomSqliteProviderTests : UmbracoIntegrationTest } } -[Obsolete] -[TestFixture] -[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest, Logger = UmbracoTestOptions.Logger.Console)] -public class CustomDbContextLegacyExtensionProviderTests : UmbracoIntegrationTest -{ - [Test] - public void Can_Register_Custom_DbContext_And_Resolve() - { - var dbContext = Services.GetRequiredService(); - - Assert.IsNotNull(dbContext); - Assert.IsNotEmpty(dbContext.Database.GetConnectionString()); - } - - protected override void CustomTestSetup(IUmbracoBuilder builder) - { - builder.Services.AddUmbracoEFCoreContext("Data Source=:memory:;Version=3;New=True;", "Microsoft.Data.Sqlite", (options, connectionString, providerName) => - { - options.UseSqlite(connectionString); - }); - } - - internal class CustomDbContext : Microsoft.EntityFrameworkCore.DbContext - { - public CustomDbContext(DbContextOptions options) - : base(options) - { - } - } -} - diff --git a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheMockTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheMockTests.cs index 5c38d4e4b0..848643d03b 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheMockTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheMockTests.cs @@ -94,8 +94,6 @@ public class DocumentHybridCacheMockTests : UmbracoIntegrationTestWithContent }); _mockedNucacheRepository.Setup(r => r.DeleteContentItemAsync(It.IsAny())); - var optionsMonitorMock = new Mock>(); - optionsMonitorMock.Setup(x => x.Get(It.IsAny())).Returns(new CacheEntrySettings()); _mockDocumentCacheService = new DocumentCacheService( _mockedNucacheRepository.Object, @@ -105,7 +103,7 @@ public class DocumentHybridCacheMockTests : UmbracoIntegrationTestWithContent GetRequiredService(), GetRequiredService(), GetSeedProviders(), - optionsMonitorMock.Object, + new OptionsWrapper(new CacheSettings()), GetRequiredService(), GetRequiredService()); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Components/ComponentTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Components/ComponentTests.cs index 94bff4a0c9..bc8b8f8a5b 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Components/ComponentTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Components/ComponentTests.cs @@ -108,11 +108,7 @@ public class ComponentTests private static TypeLoader MockTypeLoader() => new( Mock.Of(), - new VaryingRuntimeHash(), - Mock.Of(), - new DirectoryInfo(TestHelper.GetHostingEnvironment().MapPathContentRoot(Constants.SystemDirectories.TempData)), - Mock.Of>(), - Mock.Of()); + Mock.Of>()); [Test] public async Task Boot1A() @@ -140,16 +136,6 @@ public class ComponentTests return new Composer1(); } - if (type == typeof(Composer5)) - { - return new Composer5(); - } - - if (type == typeof(Component5)) - { - return new Component5(new SomeResource()); - } - if (type == typeof(IProfilingLogger)) { return new ProfilingLogger(Mock.Of>(), Mock.Of()); @@ -297,26 +283,6 @@ public class ComponentTests return new Composer1(); } - if (type == typeof(Composer5)) - { - return new Composer5(); - } - - if (type == typeof(Composer5a)) - { - return new Composer5a(); - } - - if (type == typeof(Component5)) - { - return new Component5(new SomeResource()); - } - - if (type == typeof(Component5a)) - { - return new Component5a(); - } - if (type == typeof(IProfilingLogger)) { return new ProfilingLogger(Mock.Of>(), Mock.Of()); @@ -337,24 +303,15 @@ public class ComponentTests }); var composition = new UmbracoBuilder(register, Mock.Of(), TestHelper.GetMockedTypeLoader()); - Type[] types = { typeof(Composer1), typeof(Composer5), typeof(Composer5a) }; + Type[] types = { typeof(Composer1) }; var composers = new ComposerGraph(composition, types, Enumerable.Empty(), Mock.Of>()); Assert.IsEmpty(Composed); composers.Compose(); - AssertTypeArray(TypeArray(), Composed); var builder = composition.WithCollectionBuilder(); builder.RegisterWith(register); var components = builder.CreateCollection(factory); - - Assert.IsEmpty(Initialized); - await components.InitializeAsync(false, default); - AssertTypeArray(TypeArray(), Initialized); - - Assert.IsEmpty(Terminated); - await components.TerminateAsync(false, default); - AssertTypeArray(TypeArray(), Terminated); } [Test] @@ -491,11 +448,7 @@ public class ComponentTests var typeFinder = TestHelper.GetTypeFinder(); var typeLoader = new TypeLoader( typeFinder, - new VaryingRuntimeHash(), - AppCaches.Disabled.RuntimeCache, - new DirectoryInfo(TestHelper.GetHostingEnvironment().MapPathContentRoot(Constants.SystemDirectories.TempData)), - Mock.Of>(), - Mock.Of()); + Mock.Of>()); var register = MockRegister(); var builder = new UmbracoBuilder(register, Mock.Of(), TestHelper.GetMockedTypeLoader()); @@ -537,43 +490,6 @@ public class ComponentTests { } - public class Composer5 : TestComposerBase - { - public override void Compose(IUmbracoBuilder builder) - { - base.Compose(builder); - builder.Components().Append(); - } - } - - [ComposeAfter(typeof(Composer5))] - public class Composer5a : TestComposerBase - { - public override void Compose(IUmbracoBuilder builder) - { - base.Compose(builder); - builder.Components().Append(); - } - } - - public class TestComponentBase : IComponent - { - public virtual void Initialize() => Initialized.Add(GetType()); - - public virtual void Terminate() => Terminated.Add(GetType()); - } - - public class Component5 : TestComponentBase - { - private readonly ISomeResource _resource; - - public Component5(ISomeResource resource) => _resource = resource; - } - - public class Component5a : TestComponentBase - { - } - [Disable] public class Composer6 : TestComposerBase { diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Composing/ComposingTestBase.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Composing/ComposingTestBase.cs index f3c615e98e..aa3ac2a3b8 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Composing/ComposingTestBase.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Composing/ComposingTestBase.cs @@ -31,12 +31,7 @@ public abstract class ComposingTestBase var typeFinder = TestHelper.GetTypeFinder(); TypeLoader = new TypeLoader( typeFinder, - new VaryingRuntimeHash(), - NoAppCache.Instance, - new DirectoryInfo(TestHelper.GetHostingEnvironment().MapPathContentRoot(Constants.SystemDirectories.TempData)), Mock.Of>(), - Mock.Of(), - false, AssembliesToScan); } } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Composing/TypeLoaderTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Composing/TypeLoaderTests.cs index d02da0dda9..068bf44db6 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Composing/TypeLoaderTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Composing/TypeLoaderTests.cs @@ -43,13 +43,7 @@ public class TypeLoaderTests }; _typeLoader = new TypeLoader( typeFinder, - new VaryingRuntimeHash(), - NoAppCache.Instance, - new DirectoryInfo(TestHelper.GetHostingEnvironment() - .MapPathContentRoot(Constants.SystemDirectories.TempData)), Mock.Of>(), - Mock.Of(), - false, assemblies); } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Configuration/Models/GlobalSettingsTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Configuration/Models/GlobalSettingsTests.cs index ca38c928f8..fbf1ea75ca 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Configuration/Models/GlobalSettingsTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Configuration/Models/GlobalSettingsTests.cs @@ -28,7 +28,7 @@ public class GlobalSettingsTests { hostingSettings.CurrentValue.ApplicationVirtualPath = rootPath; - var globalSettings = new GlobalSettings { UmbracoPath = path }; + var globalSettings = new GlobalSettings(); Assert.AreEqual(outcome, globalSettings.GetUmbracoMvcAreaNoCache(hostingEnvironment)); } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/CoreThings/UdiTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/CoreThings/UdiTests.cs index b7f9e4c4cd..1351bc57a0 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/CoreThings/UdiTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/CoreThings/UdiTests.cs @@ -293,44 +293,5 @@ public class UdiTests Assert.IsFalse(UdiParser.TryParse("umb://foo/A87F65C8D6B94E868F6949BA92C93045", true, out udi)); Assert.AreEqual(Constants.UdiEntityType.Unknown, udi.EntityType); Assert.AreEqual("Umbraco.Cms.Core.UnknownTypeUdi", udi.GetType().FullName); - - // scanned - UdiParserServiceConnectors - .RegisterServiceConnector< - FooConnector>(); // this is the equivalent of scanning but we'll just manually register this one - Assert.IsTrue(UdiParser.TryParse("umb://foo/A87F65C8D6B94E868F6949BA92C93045", out udi)); - Assert.IsInstanceOf(udi); - - // known - Assert.IsTrue(UdiParser.TryParse("umb://foo/A87F65C8D6B94E868F6949BA92C93045", true, out udi)); - Assert.IsInstanceOf(udi); - - // can get method for Deploy compatibility - var method = typeof(UdiParser).GetMethod("Parse", BindingFlags.Static | BindingFlags.Public, null, new[] { typeof(string), typeof(bool) }, null); - Assert.IsNotNull(method); - } - - [UdiDefinition("foo", UdiType.GuidUdi)] - public class FooConnector : IServiceConnector - { - public IArtifact GetArtifact(Udi udi, IContextCache contextCache) => throw new NotImplementedException(); - - public IArtifact GetArtifact(object entity, IContextCache contextCache) => throw new NotImplementedException(); - - public ArtifactDeployState ProcessInit(IArtifact art, IDeployContext context) => - throw new NotImplementedException(); - - public void Process(ArtifactDeployState dart, IDeployContext context, int pass) => - throw new NotImplementedException(); - - public void Explode(UdiRange range, List udis) => throw new NotImplementedException(); - - public NamedUdiRange GetRange(Udi udi, string selector) => throw new NotImplementedException(); - - public NamedUdiRange GetRange(string entityType, string sid, string selector) => - throw new NotImplementedException(); - - public bool Compare(IArtifact art1, IArtifact art2, ICollection differences = null) => - throw new NotImplementedException(); } } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentRouteBuilderTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentRouteBuilderTests.cs index eca04e10f2..06950c4ba9 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentRouteBuilderTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentRouteBuilderTests.cs @@ -46,6 +46,9 @@ public class ContentRouteBuilderTests : DeliveryApiTests var childKey = Guid.NewGuid(); var child = SetupInvariantPublishedContent("The Child", childKey, navigationQueryServiceMock, root); + IEnumerable ancestorsKeys = [rootKey]; + navigationQueryServiceMock.Setup(x=>x.TryGetAncestorsKeys(childKey, out ancestorsKeys)).Returns(true); + var contentCache = CreatePublishedContentCache("#"); Mock.Get(contentCache).Setup(x => x.GetById(root.Key)).Returns(root); Mock.Get(contentCache).Setup(x => x.GetById(child.Key)).Returns(child); @@ -78,6 +81,9 @@ public class ContentRouteBuilderTests : DeliveryApiTests Mock.Get(contentCache).Setup(x => x.GetById(child.Key)).Returns(child); Mock.Get(contentCache).Setup(x => x.GetById(grandchild.Key)).Returns(grandchild); + IEnumerable ancestorsKeys = [childKey, rootKey]; + navigationQueryServiceMock.Setup(x=>x.TryGetAncestorsKeys(grandchildKey, out ancestorsKeys)).Returns(true); + var builder = CreateApiContentRouteBuilder(hideTopLevelNodeFromPath, contentCache: contentCache, navigationQueryService: navigationQueryServiceMock.Object); var result = builder.Build(grandchild); Assert.IsNotNull(result); @@ -101,6 +107,9 @@ public class ContentRouteBuilderTests : DeliveryApiTests Mock.Get(contentCache).Setup(x => x.GetById(root.Key)).Returns(root); Mock.Get(contentCache).Setup(x => x.GetById(child.Key)).Returns(child); + IEnumerable ancestorsKeys = [rootKey]; + navigationQueryServiceMock.Setup(x=>x.TryGetAncestorsKeys(childKey, out ancestorsKeys)).Returns(true); + var builder = CreateApiContentRouteBuilder(false, contentCache: contentCache, navigationQueryService: navigationQueryServiceMock.Object); var result = builder.Build(child, "en-us"); Assert.IsNotNull(result); @@ -130,6 +139,9 @@ public class ContentRouteBuilderTests : DeliveryApiTests Mock.Get(contentCache).Setup(x => x.GetById(root.Key)).Returns(root); Mock.Get(contentCache).Setup(x => x.GetById(child.Key)).Returns(child); + IEnumerable ancestorsKeys = [rootKey]; + navigationQueryServiceMock.Setup(x=>x.TryGetAncestorsKeys(childKey, out ancestorsKeys)).Returns(true); + var builder = CreateApiContentRouteBuilder(false, contentCache: contentCache, navigationQueryService: navigationQueryServiceMock.Object); var result = builder.Build(child, "en-us"); Assert.IsNotNull(result); @@ -159,6 +171,9 @@ public class ContentRouteBuilderTests : DeliveryApiTests Mock.Get(contentCache).Setup(x => x.GetById(root.Key)).Returns(root); Mock.Get(contentCache).Setup(x => x.GetById(child.Key)).Returns(child); + IEnumerable ancestorsKeys = [rootKey]; + navigationQueryServiceMock.Setup(x=>x.TryGetAncestorsKeys(childKey, out ancestorsKeys)).Returns(true); + var builder = CreateApiContentRouteBuilder(false, contentCache: contentCache, navigationQueryService: navigationQueryServiceMock.Object); var result = builder.Build(child, "en-us"); Assert.IsNotNull(result); @@ -225,6 +240,12 @@ public class ContentRouteBuilderTests : DeliveryApiTests Mock.Get(contentCache).Setup(x => x.GetById(child.Key)).Returns(child); Mock.Get(contentCache).Setup(x => x.GetById(grandchild.Key)).Returns(grandchild); + IEnumerable grandchildAncestorsKeys = [childKey, rootKey]; + navigationQueryServiceMock.Setup(x=>x.TryGetAncestorsKeys(grandchildKey, out grandchildAncestorsKeys)).Returns(true); + + IEnumerable ancestorsKeys = [rootKey]; + navigationQueryServiceMock.Setup(x=>x.TryGetAncestorsKeys(childKey, out ancestorsKeys)).Returns(true); + // yes... actually testing the mock setup here. but it's important for the rest of the tests that this behave correct, so we better test it. var publishedUrlProvider = SetupPublishedUrlProvider(hideTopLevelNodeFromPath, contentCache, navigationQueryServiceMock.Object); Assert.AreEqual(hideTopLevelNodeFromPath ? "/" : "/the-root", publishedUrlProvider.GetUrl(root)); @@ -306,6 +327,9 @@ public class ContentRouteBuilderTests : DeliveryApiTests Mock.Get(contentCache).Setup(x => x.GetById(root.Key)).Returns(root); Mock.Get(contentCache).Setup(x => x.GetById(child.Key)).Returns(child); + IEnumerable ancestorsKeys = [rootKey]; + navigationQueryServiceMock.Setup(x=>x.TryGetAncestorsKeys(childKey, out ancestorsKeys)).Returns(true); + var builder = CreateApiContentRouteBuilder(true, contentCache: contentCache, isPreview: isPreview, navigationQueryService: navigationQueryServiceMock.Object); var result = builder.Build(child); @@ -342,6 +366,9 @@ public class ContentRouteBuilderTests : DeliveryApiTests .Setup(p => p.GetContentPath(It.IsAny(), It.IsAny())) .Returns((IPublishedContent content, string? culture) => $"my-custom-path-for-{content.UrlSegment}"); + IEnumerable ancestorsKeys = [rootKey]; + navigationQueryServiceMock.Setup(x=>x.TryGetAncestorsKeys(childKey, out ancestorsKeys)).Returns(true); + var builder = CreateApiContentRouteBuilder(true, contentCache: contentCache, apiContentPathProvider: apiContentPathProvider.Object, navigationQueryService: navigationQueryServiceMock.Object); var result = builder.Build(root); Assert.NotNull(result); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Extensions/UdiGetterExtensionsTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Extensions/UdiGetterExtensionsTests.cs index f5a5e79234..476fefec66 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Extensions/UdiGetterExtensionsTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Extensions/UdiGetterExtensionsTests.cs @@ -5,6 +5,7 @@ using NUnit.Framework; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Tests.Common.Builders; using Umbraco.Cms.Tests.Common.Builders.Extensions; using Umbraco.Extensions; @@ -32,38 +33,6 @@ public class UdiGetterExtensionsTests Assert.AreEqual(expected, udi.ToString()); } - [TestCase("script.js", "umb://script/script.js")] - [TestCase("editor\\script.js", "umb://script/editor/script.js")] - [TestCase("editor/script.js", "umb://script/editor/script.js")] - public void GetUdiForScript(string path, string expected) - { - Script entity = new ScriptBuilder() - .WithPath(path) - .Build(); - - Udi udi = entity.GetUdi(); - Assert.AreEqual(expected, udi.ToString()); - - udi = ((IEntity)entity).GetUdi(); - Assert.AreEqual(expected, udi.ToString()); - } - - [TestCase("style.css", "umb://stylesheet/style.css")] - [TestCase("editor\\style.css", "umb://stylesheet/editor/style.css")] - [TestCase("editor/style.css", "umb://stylesheet/editor/style.css")] - public void GetUdiForStylesheet(string path, string expected) - { - Stylesheet entity = new StylesheetBuilder() - .WithPath(path) - .Build(); - - Udi udi = entity.GetUdi(); - Assert.AreEqual(expected, udi.ToString()); - - udi = ((IEntity)entity).GetUdi(); - Assert.AreEqual(expected, udi.ToString()); - } - [TestCase("6ad82c70-685c-4e04-9b36-d81bd779d16f", false, "umb://document/6ad82c70685c4e049b36d81bd779d16f")] [TestCase("6ad82c70-685c-4e04-9b36-d81bd779d16f", true, "umb://document-blueprint/6ad82c70685c4e049b36d81bd779d16f")] public void GetUdiForContent(Guid key, bool blueprint, string expected) @@ -241,6 +210,21 @@ public class UdiGetterExtensionsTests Assert.AreEqual(expected, udi.ToString()); } + [TestCase("6ad82c70-685c-4e04-9b36-d81bd779d16f", "umb://relation/6ad82c70685c4e049b36d81bd779d16f")] + public void GetUdiForRelation(Guid key, string expected) + { + IRelation entity = new RelationBuilder() + .WithKey(key) + .AddRelationType().Done() + .Build(); + + Udi udi = entity.GetUdi(); + Assert.AreEqual(expected, udi.ToString()); + + udi = ((IEntity)entity).GetUdi(); + Assert.AreEqual(expected, udi.ToString()); + } + [TestCase("6ad82c70-685c-4e04-9b36-d81bd779d16f", "umb://relation-type/6ad82c70685c4e049b36d81bd779d16f")] public void GetUdiForRelationType(Guid key, string expected) { @@ -255,6 +239,38 @@ public class UdiGetterExtensionsTests Assert.AreEqual(expected, udi.ToString()); } + [TestCase("script.js", "umb://script/script.js")] + [TestCase("editor\\script.js", "umb://script/editor/script.js")] + [TestCase("editor/script.js", "umb://script/editor/script.js")] + public void GetUdiForScript(string path, string expected) + { + IScript entity = new ScriptBuilder() + .WithPath(path) + .Build(); + + Udi udi = entity.GetUdi(); + Assert.AreEqual(expected, udi.ToString()); + + udi = ((IEntity)entity).GetUdi(); + Assert.AreEqual(expected, udi.ToString()); + } + + [TestCase("style.css", "umb://stylesheet/style.css")] + [TestCase("editor\\style.css", "umb://stylesheet/editor/style.css")] + [TestCase("editor/style.css", "umb://stylesheet/editor/style.css")] + public void GetUdiForStylesheet(string path, string expected) + { + IStylesheet entity = new StylesheetBuilder() + .WithPath(path) + .Build(); + + Udi udi = entity.GetUdi(); + Assert.AreEqual(expected, udi.ToString()); + + udi = ((IEntity)entity).GetUdi(); + Assert.AreEqual(expected, udi.ToString()); + } + [TestCase("6ad82c70-685c-4e04-9b36-d81bd779d16f", "umb://template/6ad82c70685c4e049b36d81bd779d16f")] public void GetUdiForTemplate(Guid key, string expected) { @@ -269,6 +285,34 @@ public class UdiGetterExtensionsTests Assert.AreEqual(expected, udi.ToString()); } + [TestCase("6ad82c70-685c-4e04-9b36-d81bd779d16f", "umb://user/6ad82c70685c4e049b36d81bd779d16f")] + public void GetUdiForUser(Guid key, string expected) + { + IUser entity = new UserBuilder() + .WithKey(key) + .Build(); + + Udi udi = entity.GetUdi(); + Assert.AreEqual(expected, udi.ToString()); + + udi = ((IEntity)entity).GetUdi(); + Assert.AreEqual(expected, udi.ToString()); + } + + [TestCase("6ad82c70-685c-4e04-9b36-d81bd779d16f", "umb://user-group/6ad82c70685c4e049b36d81bd779d16f")] + public void GetUdiForUserGroup(Guid key, string expected) + { + IUserGroup entity = new UserGroupBuilder() + .WithKey(key) + .Build(); + + Udi udi = entity.GetUdi(); + Assert.AreEqual(expected, udi.ToString()); + + udi = ((IEntity)entity).GetUdi(); + Assert.AreEqual(expected, udi.ToString()); + } + [TestCase("6ad82c70-685c-4e04-9b36-d81bd779d16f", "umb://webhook/6ad82c70685c4e049b36d81bd779d16f")] public void GetUdiForWebhook(Guid key, string expected) { diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueEditorReuseTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueEditorReuseTests.cs index 4fe4c34b6e..39132ee7a8 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueEditorReuseTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueEditorReuseTests.cs @@ -39,8 +39,9 @@ public class DataValueEditorReuseTests var blockVarianceHandler = new BlockEditorVarianceHandler(Mock.Of()); _dataValueEditorFactoryMock .Setup(m => - m.Create(It.IsAny>())) + m.Create(It.IsAny(), It.IsAny>())) .Returns(() => new BlockListPropertyEditorBase.BlockListEditorPropertyValueEditor( + new DataEditorAttribute("a"), new BlockListEditorDataConverter(Mock.Of()), _propertyEditorCollection, _dataValueReferenceFactories, @@ -52,7 +53,9 @@ public class DataValueEditorReuseTests Mock.Of(), Mock.Of(), blockVarianceHandler, - Mock.Of())); + Mock.Of(), + Mock.Of() + )); } [Test] @@ -109,7 +112,7 @@ public class DataValueEditorReuseTests Assert.NotNull(dataValueEditor2); Assert.AreNotSame(dataValueEditor1, dataValueEditor2); _dataValueEditorFactoryMock.Verify( - m => m.Create(It.IsAny>()), + m => m.Create(It.IsAny(), It.IsAny>()), Times.Exactly(2)); } @@ -131,7 +134,7 @@ public class DataValueEditorReuseTests Assert.AreEqual("config", ((DataValueEditor)dataValueEditor2).ConfigurationObject); Assert.AreNotSame(dataValueEditor1, dataValueEditor2); _dataValueEditorFactoryMock.Verify( - m => m.Create(It.IsAny>()), + m => m.Create(It.IsAny(), It.IsAny>()), Times.Exactly(2)); } } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/ContentNavigationServiceBaseTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/ContentNavigationServiceBaseTests.cs index deb26ef901..42940332cd 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/ContentNavigationServiceBaseTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/ContentNavigationServiceBaseTests.cs @@ -1,7 +1,9 @@ using Moq; using NUnit.Framework; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.Navigation; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Services; @@ -11,6 +13,8 @@ public class ContentNavigationServiceBaseTests { private TestContentNavigationService _navigationService; + private Guid ContentType { get; set; } + private Guid Root { get; set; } private Guid Child1 { get; set; } @@ -32,46 +36,21 @@ public class ContentNavigationServiceBaseTests [SetUp] public void Setup() { - // Root - // - Child 1 - // - Grandchild 1 - // - Grandchild 2 - // - Child 2 - // - Grandchild 3 - // - Great-grandchild 1 - // - Child 3 - // - Grandchild 4 - _navigationService = new TestContentNavigationService( Mock.Of(), - Mock.Of()); + Mock.Of(), + Mock.Of()); - Root = new Guid("E48DD82A-7059-418E-9B82-CDD5205796CF"); - _navigationService.Add(Root); - - Child1 = new Guid("C6173927-0C59-4778-825D-D7B9F45D8DDE"); - _navigationService.Add(Child1, Root); - - Grandchild1 = new Guid("E856AC03-C23E-4F63-9AA9-681B42A58573"); - _navigationService.Add(Grandchild1, Child1); - - Grandchild2 = new Guid("A1B1B217-B02F-4307-862C-A5E22DB729EB"); - _navigationService.Add(Grandchild2, Child1); - - Child2 = new Guid("60E0E5C4-084E-4144-A560-7393BEAD2E96"); - _navigationService.Add(Child2, Root); - - Grandchild3 = new Guid("D63C1621-C74A-4106-8587-817DEE5FB732"); - _navigationService.Add(Grandchild3, Child2); - - GreatGrandchild1 = new Guid("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7"); - _navigationService.Add(GreatGrandchild1, Grandchild3); - - Child3 = new Guid("B606E3FF-E070-4D46-8CB9-D31352029FDF"); - _navigationService.Add(Child3, Root); - - Grandchild4 = new Guid("F381906C-223C-4466-80F7-B63B4EE073F8"); - _navigationService.Add(Grandchild4, Child3); + // Root - E48DD82A-7059-418E-9B82-CDD5205796CF + // - Child 1 - C6173927-0C59-4778-825D-D7B9F45D8DDE + // - Grandchild 1 - E856AC03-C23E-4F63-9AA9-681B42A58573 + // - Grandchild 2 - A1B1B217-B02F-4307-862C-A5E22DB729EB + // - Child 2 - 60E0E5C4-084E-4144-A560-7393BEAD2E96 + // - Grandchild 3 - D63C1621-C74A-4106-8587-817DEE5FB732 + // - Great-grandchild 1 - 56E29EA9-E224-4210-A59F-7C2C5C0C5CC7 + // - Child 3 - B606E3FF-E070-4D46-8CB9-D31352029FDF + // - Grandchild 4 - F381906C-223C-4466-80F7-B63B4EE073F8 + CreateTestData(); } [Test] @@ -129,7 +108,8 @@ public class ContentNavigationServiceBaseTests // Arrange var emptyNavigationService = new TestContentNavigationService( Mock.Of(), - Mock.Of()); + Mock.Of(), + Mock.Of()); // Act emptyNavigationService.TryGetRootKeys(out IEnumerable rootKeys); @@ -161,7 +141,7 @@ public class ContentNavigationServiceBaseTests { // Arrange Guid anotherRoot = Guid.NewGuid(); - _navigationService.Add(anotherRoot); + _navigationService.Add(anotherRoot, ContentType); // Act var result = _navigationService.TryGetRootKeys(out IEnumerable rootKeys); @@ -176,6 +156,179 @@ public class ContentNavigationServiceBaseTests }); } + [Test] + public void Cannot_Get_Root_Items_Of_Type_From_Non_Existing_Content_Type_Alias() + { + // Arrange + var nonExistingContentTypeAlias = string.Empty; + + // Act + var result = _navigationService.TryGetRootKeysOfType(nonExistingContentTypeAlias, out IEnumerable rootKeys); + + // Assert + Assert.Multiple(() => + { + Assert.IsFalse(result); + Assert.IsEmpty(rootKeys); + }); + } + + [Test] + public void Can_Get_Root_Items_Of_Type() + { + // Arrange + const string contentTypeAlias = "contentPage"; + + var contentTypeMock = new Mock(); + contentTypeMock.SetupGet(x => x.Alias).Returns(contentTypeAlias); + contentTypeMock.SetupGet(x => x.Key).Returns(ContentType); + + var contentTypeServiceMock = new Mock(); + contentTypeServiceMock.Setup(x => x.GetAll()).Returns(new[] { contentTypeMock.Object }); + + _navigationService = new TestContentNavigationService( + Mock.Of(), + Mock.Of(), + contentTypeServiceMock.Object); + + // We need to re-create the test data since we use new mock + CreateTestData(); + + Guid anotherRoot = Guid.NewGuid(); + _navigationService.Add(anotherRoot, ContentType); + + // Act + var result = _navigationService.TryGetRootKeysOfType(contentTypeAlias, out IEnumerable rootKeys); + + // Assert + Assert.Multiple(() => + { + Assert.IsTrue(result); + Assert.AreEqual(2, rootKeys.Count()); + }); + } + + [Test] + public void Can_Get_Root_Items_Of_Type_Filters_Result() + { + // Arrange + const string contentTypeAlias = "contentPage"; + const string anotherContentTypeAlias = "anotherContentPage"; + Guid anotherContentTypeKey = Guid.NewGuid(); + + var contentTypeMock = new Mock(); + contentTypeMock.SetupGet(x => x.Alias).Returns(contentTypeAlias); + contentTypeMock.SetupGet(x => x.Key).Returns(ContentType); + + var anotherContentTypeMock = new Mock(); + anotherContentTypeMock.SetupGet(x => x.Alias).Returns(anotherContentTypeAlias); + anotherContentTypeMock.SetupGet(x => x.Key).Returns(anotherContentTypeKey); + + var contentTypeServiceMock = new Mock(); + contentTypeServiceMock.Setup(x => x.GetAll()).Returns(new[] { contentTypeMock.Object, anotherContentTypeMock.Object }); + + _navigationService = new TestContentNavigationService( + Mock.Of(), + Mock.Of(), + contentTypeServiceMock.Object); + + // We need to re-create the test data since we use new mock + CreateTestData(); + + // Adding 2 new root items with different content type + _navigationService.Add(Guid.NewGuid(), anotherContentTypeKey); + _navigationService.Add(Guid.NewGuid(), anotherContentTypeKey); + + // Act + _navigationService.TryGetRootKeysOfType(anotherContentTypeAlias, out IEnumerable rootKeysOfType); + var rootsOfTypeCount = rootKeysOfType.Count(); + + // Assert + // Retrieve all root items without filtering to compare + _navigationService.TryGetRootKeys(out IEnumerable allRootKeys); + var allRootsCount = allRootKeys.Count(); + + Assert.Multiple(() => + { + Assert.IsTrue(allRootsCount > rootsOfTypeCount); + Assert.AreEqual(3, allRootsCount); + Assert.AreEqual(2, rootsOfTypeCount); + }); + } + + [Test] + public void Can_Get_Root_Items_Of_Type_Filters_Result_And_Maintains_Their_Order_Of_Creation() + { + // Arrange + const string contentTypeAlias = "contentPage"; + const string anotherContentTypeAlias = "anotherContentPage"; + Guid anotherContentTypeKey = Guid.NewGuid(); + + var contentTypeMock = new Mock(); + contentTypeMock.SetupGet(x => x.Alias).Returns(contentTypeAlias); + contentTypeMock.SetupGet(x => x.Key).Returns(ContentType); + + var anotherContentTypeMock = new Mock(); + anotherContentTypeMock.SetupGet(x => x.Alias).Returns(anotherContentTypeAlias); + anotherContentTypeMock.SetupGet(x => x.Key).Returns(anotherContentTypeKey); + + var contentTypeServiceMock = new Mock(); + contentTypeServiceMock.Setup(x => x.GetAll()).Returns(new[] { contentTypeMock.Object, anotherContentTypeMock.Object }); + + _navigationService = new TestContentNavigationService( + Mock.Of(), + Mock.Of(), + contentTypeServiceMock.Object); + + // We need to re-create the test data since we use new mock + CreateTestData(); + + // Adding 2 new root items with different content type + Guid root2 = Guid.NewGuid(); + Guid root3 = Guid.NewGuid(); + _navigationService.Add(root2, anotherContentTypeKey); + _navigationService.Add(root3, anotherContentTypeKey); + + var expectedRootsOrder = new List { root2, root3 }; + + // Act + _navigationService.TryGetRootKeysOfType(anotherContentTypeAlias, out IEnumerable rootKeysOfType); + + // Assert + // Check that the order matches what is expected + Assert.IsTrue(expectedRootsOrder.SequenceEqual(rootKeysOfType)); + } + + [Test] + public void Can_Get_Root_Items_Of_Type_Even_When_Content_Type_Was_Not_Initially_Loaded() + { + // Arrange + const string contentTypeAlias = "contentPage"; + + var contentTypeMock = new Mock(); + contentTypeMock.SetupGet(x => x.Alias).Returns(contentTypeAlias); + contentTypeMock.SetupGet(x => x.Key).Returns(ContentType); + + var contentTypeServiceMock = new Mock(); + contentTypeServiceMock.Setup(x => x + .Get(It.Is(alias => alias == contentTypeAlias))) + .Returns(contentTypeMock.Object); + + _navigationService = new TestContentNavigationService( + Mock.Of(), + Mock.Of(), + contentTypeServiceMock.Object); + + // We need to re-create the test data since we use new mock + CreateTestData(); + + // Act + _navigationService.TryGetRootKeysOfType(contentTypeAlias, out IEnumerable rootKeys); + + // Assert + Assert.AreEqual(1, rootKeys.Count()); + } + [Test] public void Cannot_Get_Children_From_Non_Existing_Content_Key() { @@ -217,7 +370,7 @@ public class ContentNavigationServiceBaseTests } [Test] - [TestCase("E48DD82A-7059-418E-9B82-CDD5205796CF", new[] { "C6173927-0C59-4778-825D-D7B9F45D8DDE", "60E0E5C4-084E-4144-A560-7393BEAD2E96", "B606E3FF-E070-4D46-8CB9-D31352029FDF" })] // Root + [TestCase("E48DD82A-7059-418E-9B82-CDD5205796CF", new[] { "C6173927-0C59-4778-825D-D7B9F45D8DDE", "60E0E5C4-084E-4144-A560-7393BEAD2E96", "B606E3FF-E070-4D46-8CB9-D31352029FDF", })] // Root [TestCase("C6173927-0C59-4778-825D-D7B9F45D8DDE", new[] { "E856AC03-C23E-4F63-9AA9-681B42A58573", "A1B1B217-B02F-4307-862C-A5E22DB729EB" })] // Child 1 [TestCase("E856AC03-C23E-4F63-9AA9-681B42A58573", new string[0])] // Grandchild 1 [TestCase("60E0E5C4-084E-4144-A560-7393BEAD2E96", new[] { "D63C1621-C74A-4106-8587-817DEE5FB732" })] // Child 2 @@ -240,6 +393,219 @@ public class ContentNavigationServiceBaseTests } } + [Test] + public void Cannot_Get_Children_Of_Type_From_Non_Existing_Content_Type_Alias() + { + // Arrange + Guid parentKey = Root; + var nonExistingContentTypeAlias = string.Empty; + + // Act + var result = _navigationService.TryGetChildrenKeysOfType(parentKey, nonExistingContentTypeAlias, out IEnumerable childrenKeys); + + // Assert + Assert.Multiple(() => + { + Assert.IsFalse(result); + Assert.IsEmpty(childrenKeys); + }); + } + + [Test] + public void Cannot_Get_Children_Of_Type_From_Non_Existing_Content_Key() + { + // Arrange + var nonExistingKey = Guid.NewGuid(); + const string contentTypeAlias = "contentPage"; + + var contentTypeMock = new Mock(); + contentTypeMock.SetupGet(x => x.Alias).Returns(contentTypeAlias); + contentTypeMock.SetupGet(x => x.Key).Returns(ContentType); + + var contentTypeServiceMock = new Mock(); + contentTypeServiceMock.Setup(x => x.GetAll()).Returns(new[] { contentTypeMock.Object }); + + _navigationService = new TestContentNavigationService( + Mock.Of(), + Mock.Of(), + contentTypeServiceMock.Object); + + // Act + var result = _navigationService.TryGetChildrenKeysOfType(nonExistingKey, contentTypeAlias, out IEnumerable childrenKeys); + + // Assert + Assert.Multiple(() => + { + Assert.IsFalse(result); + Assert.IsEmpty(childrenKeys); + }); + } + + [Test] + [TestCase("E48DD82A-7059-418E-9B82-CDD5205796CF", 3)] // Root - Child 1, Child 2, Child 3 + [TestCase("C6173927-0C59-4778-825D-D7B9F45D8DDE", 2)] // Child 1 - Grandchild 1, Grandchild 2 + [TestCase("E856AC03-C23E-4F63-9AA9-681B42A58573", 0)] // Grandchild 1 + [TestCase("A1B1B217-B02F-4307-862C-A5E22DB729EB", 0)] // Grandchild 2 + [TestCase("60E0E5C4-084E-4144-A560-7393BEAD2E96", 1)] // Child 2 - Grandchild 3 + [TestCase("D63C1621-C74A-4106-8587-817DEE5FB732", 1)] // Grandchild 3 - Great-grandchild 1 + [TestCase("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7", 0)] // Great-grandchild 1 + [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF", 1)] // Child 3 - Grandchild 4 + [TestCase("F381906C-223C-4466-80F7-B63B4EE073F8", 0)] // Grandchild 4 + public void Can_Get_Children_Of_Type(Guid parentKey, int childrenCount) + { + // Arrange + const string contentTypeAlias = "contentPage"; + + var contentTypeMock = new Mock(); + contentTypeMock.SetupGet(x => x.Alias).Returns(contentTypeAlias); + contentTypeMock.SetupGet(x => x.Key).Returns(ContentType); + + var contentTypeServiceMock = new Mock(); + contentTypeServiceMock.Setup(x => x.GetAll()).Returns(new[] { contentTypeMock.Object }); + + _navigationService = new TestContentNavigationService( + Mock.Of(), + Mock.Of(), + contentTypeServiceMock.Object); + + // We need to re-create the test data since we use new mock + CreateTestData(); + + // Act + var result = _navigationService.TryGetChildrenKeysOfType(parentKey, contentTypeAlias, out IEnumerable childrenKeys); + + // Assert + Assert.Multiple(() => + { + Assert.IsTrue(result); + Assert.AreEqual(childrenCount, childrenKeys.Count()); + }); + } + + [Test] + public void Can_Get_Children_Of_Type_Filters_Result() + { + // Arrange + Guid parentKey = Root; + const string contentTypeAlias = "contentPage"; + const string anotherContentTypeAlias = "anotherContentPage"; + Guid anotherContentTypeKey = Guid.NewGuid(); + + var contentTypeMock = new Mock(); + contentTypeMock.SetupGet(x => x.Alias).Returns(contentTypeAlias); + contentTypeMock.SetupGet(x => x.Key).Returns(ContentType); + + var anotherContentTypeMock = new Mock(); + anotherContentTypeMock.SetupGet(x => x.Alias).Returns(anotherContentTypeAlias); + anotherContentTypeMock.SetupGet(x => x.Key).Returns(anotherContentTypeKey); + + var contentTypeServiceMock = new Mock(); + contentTypeServiceMock.Setup(x => x.GetAll()).Returns(new[] { contentTypeMock.Object, anotherContentTypeMock.Object }); + + _navigationService = new TestContentNavigationService( + Mock.Of(), + Mock.Of(), + contentTypeServiceMock.Object); + + // We need to re-create the test data since we use new mock + CreateTestData(); + + // Adding 2 new children with different content type under Root + _navigationService.Add(Guid.NewGuid(), anotherContentTypeKey, Root); + _navigationService.Add(Guid.NewGuid(), anotherContentTypeKey, Root); + + // Act + _navigationService.TryGetChildrenKeysOfType(parentKey, anotherContentTypeAlias, out IEnumerable childrenKeysOfType); + var childrenOfTypeCount = childrenKeysOfType.Count(); + + // Assert + // Retrieve all children without filtering to compare + _navigationService.TryGetChildrenKeys(parentKey, out IEnumerable allChildrenKeys); + var allChildrenCount = allChildrenKeys.Count(); + + Assert.Multiple(() => + { + Assert.IsTrue(allChildrenCount > childrenOfTypeCount); + Assert.AreEqual(5, allChildrenCount); + Assert.AreEqual(2, childrenOfTypeCount); + }); + } + + [Test] + public void Can_Get_Children_Of_Type_Filters_Result_And_Maintains_Their_Order_Of_Creation() + { + // Arrange + Guid parentKey = Root; + const string contentTypeAlias = "contentPage"; + const string anotherContentTypeAlias = "anotherContentPage"; + Guid anotherContentTypeKey = Guid.NewGuid(); + + var contentTypeMock = new Mock(); + contentTypeMock.SetupGet(x => x.Alias).Returns(contentTypeAlias); + contentTypeMock.SetupGet(x => x.Key).Returns(ContentType); + + var anotherContentTypeMock = new Mock(); + anotherContentTypeMock.SetupGet(x => x.Alias).Returns(anotherContentTypeAlias); + anotherContentTypeMock.SetupGet(x => x.Key).Returns(anotherContentTypeKey); + + var contentTypeServiceMock = new Mock(); + contentTypeServiceMock.Setup(x => x.GetAll()).Returns(new[] { contentTypeMock.Object, anotherContentTypeMock.Object }); + + _navigationService = new TestContentNavigationService( + Mock.Of(), + Mock.Of(), + contentTypeServiceMock.Object); + + // We need to re-create the test data since we use new mock + CreateTestData(); + + // Adding 2 new children with different content type under Root + Guid child4 = Guid.NewGuid(); + Guid child5 = Guid.NewGuid(); + _navigationService.Add(child4, anotherContentTypeKey, Root); + _navigationService.Add(child5, anotherContentTypeKey, Root); + + var expectedChildrenOrder = new List { child4, child5 }; + + // Act + _navigationService.TryGetChildrenKeysOfType(parentKey, anotherContentTypeAlias, out IEnumerable childrenKeysOfType); + + // Assert + // Check that the order matches what is expected + Assert.IsTrue(expectedChildrenOrder.SequenceEqual(childrenKeysOfType)); + } + + [Test] + public void Can_Get_Children_Of_Type_Even_When_Content_Type_Was_Not_Initially_Loaded() + { + // Arrange + Guid parentKey = Child1; + const string contentTypeAlias = "contentPage"; + + var contentTypeMock = new Mock(); + contentTypeMock.SetupGet(x => x.Alias).Returns(contentTypeAlias); + contentTypeMock.SetupGet(x => x.Key).Returns(ContentType); + + var contentTypeServiceMock = new Mock(); + contentTypeServiceMock.Setup(x => x + .Get(It.Is(alias => alias == contentTypeAlias))) + .Returns(contentTypeMock.Object); + + _navigationService = new TestContentNavigationService( + Mock.Of(), + Mock.Of(), + contentTypeServiceMock.Object); + + // We need to re-create the test data since we use new mock + CreateTestData(); + + // Act + _navigationService.TryGetChildrenKeysOfType(parentKey, contentTypeAlias, out IEnumerable childrenKeys); + + // Assert + Assert.AreEqual(2, childrenKeys.Count()); + } + [Test] public void Cannot_Get_Descendants_From_Non_Existing_Content_Key() { @@ -281,7 +647,7 @@ public class ContentNavigationServiceBaseTests } [Test] - [TestCase("E48DD82A-7059-418E-9B82-CDD5205796CF", new[] { "C6173927-0C59-4778-825D-D7B9F45D8DDE", "E856AC03-C23E-4F63-9AA9-681B42A58573", "A1B1B217-B02F-4307-862C-A5E22DB729EB", "60E0E5C4-084E-4144-A560-7393BEAD2E96", "D63C1621-C74A-4106-8587-817DEE5FB732", "56E29EA9-E224-4210-A59F-7C2C5C0C5CC7", "B606E3FF-E070-4D46-8CB9-D31352029FDF", "F381906C-223C-4466-80F7-B63B4EE073F8" })] // Root + [TestCase("E48DD82A-7059-418E-9B82-CDD5205796CF", new[] { "C6173927-0C59-4778-825D-D7B9F45D8DDE", "E856AC03-C23E-4F63-9AA9-681B42A58573", "A1B1B217-B02F-4307-862C-A5E22DB729EB", "60E0E5C4-084E-4144-A560-7393BEAD2E96", "D63C1621-C74A-4106-8587-817DEE5FB732", "56E29EA9-E224-4210-A59F-7C2C5C0C5CC7", "B606E3FF-E070-4D46-8CB9-D31352029FDF", "F381906C-223C-4466-80F7-B63B4EE073F8", })] // Root [TestCase("C6173927-0C59-4778-825D-D7B9F45D8DDE", new[] { "E856AC03-C23E-4F63-9AA9-681B42A58573", "A1B1B217-B02F-4307-862C-A5E22DB729EB" })] // Child 1 [TestCase("E856AC03-C23E-4F63-9AA9-681B42A58573", new string[0])] // Grandchild 1 [TestCase("60E0E5C4-084E-4144-A560-7393BEAD2E96", new[] { "D63C1621-C74A-4106-8587-817DEE5FB732", "56E29EA9-E224-4210-A59F-7C2C5C0C5CC7" })] // Child 2 @@ -304,6 +670,189 @@ public class ContentNavigationServiceBaseTests } } + [Test] + public void Cannot_Get_Descendants_Of_Type_From_Non_Existing_Content_Type_Alias() + { + // Arrange + Guid parentKey = Root; + var nonExistingContentTypeAlias = string.Empty; + + // Act + var result = _navigationService.TryGetDescendantsKeysOfType(parentKey, nonExistingContentTypeAlias, out IEnumerable descendantsKeys); + + // Assert + Assert.Multiple(() => + { + Assert.IsFalse(result); + Assert.IsEmpty(descendantsKeys); + }); + } + + [Test] + public void Cannot_Get_Descendants_Of_Type_From_Non_Existing_Content_Key() + { + // Arrange + var nonExistingKey = Guid.NewGuid(); + const string contentTypeAlias = "contentPage"; + + var contentTypeMock = new Mock(); + contentTypeMock.SetupGet(x => x.Alias).Returns(contentTypeAlias); + contentTypeMock.SetupGet(x => x.Key).Returns(ContentType); + + var contentTypeServiceMock = new Mock(); + contentTypeServiceMock.Setup(x => x.GetAll()).Returns(new[] { contentTypeMock.Object }); + + _navigationService = new TestContentNavigationService( + Mock.Of(), + Mock.Of(), + contentTypeServiceMock.Object); + + // Act + var result = _navigationService.TryGetDescendantsKeysOfType(nonExistingKey, contentTypeAlias, out IEnumerable descendantsKeys); + + // Assert + Assert.Multiple(() => + { + Assert.IsFalse(result); + Assert.IsEmpty(descendantsKeys); + }); + } + + [Test] + [TestCase("E48DD82A-7059-418E-9B82-CDD5205796CF", + 8)] // Root - Child 1, Grandchild 1, Grandchild 2, Child 2, Grandchild 3, Great-grandchild 1, Child 3, Grandchild 4 + [TestCase("C6173927-0C59-4778-825D-D7B9F45D8DDE", 2)] // Child 1 - Grandchild 1, Grandchild 2 + [TestCase("E856AC03-C23E-4F63-9AA9-681B42A58573", 0)] // Grandchild 1 + [TestCase("A1B1B217-B02F-4307-862C-A5E22DB729EB", 0)] // Grandchild 2 + [TestCase("60E0E5C4-084E-4144-A560-7393BEAD2E96", 2)] // Child 2 - Grandchild 3, Great-grandchild 1 + [TestCase("D63C1621-C74A-4106-8587-817DEE5FB732", 1)] // Grandchild 3 - Great-grandchild 1 + [TestCase("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7", 0)] // Great-grandchild 1 + [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF", 1)] // Child 3 - Grandchild 4 + [TestCase("F381906C-223C-4466-80F7-B63B4EE073F8", 0)] // Grandchild 4 + public void Can_Get_Descendants_Of_Type(Guid parentKey, int descendantsCount) + { + // Arrange + const string contentTypeAlias = "contentPage"; + + var contentTypeMock = new Mock(); + contentTypeMock.SetupGet(x => x.Alias).Returns(contentTypeAlias); + contentTypeMock.SetupGet(x => x.Key).Returns(ContentType); + + var contentTypeServiceMock = new Mock(); + contentTypeServiceMock.Setup(x => x.GetAll()).Returns(new[] { contentTypeMock.Object }); + + _navigationService = new TestContentNavigationService( + Mock.Of(), + Mock.Of(), + contentTypeServiceMock.Object); + + // We need to re-create the test data since we use new mock + CreateTestData(); + + // Act + var result = _navigationService.TryGetDescendantsKeysOfType(parentKey, contentTypeAlias, out IEnumerable descendantsKeys); + + // Assert + Assert.Multiple(() => + { + Assert.IsTrue(result); + Assert.AreEqual(descendantsCount, descendantsKeys.Count()); + }); + } + + [Test] + public void Can_Get_Descendants_Of_Type_Filters_Result() + { + // Arrange + Guid parentKey = Child2; + const string contentTypeAlias = "contentPage"; + const string anotherContentTypeAlias = "anotherContentPage"; + Guid anotherContentTypeKey = Guid.NewGuid(); + + var contentTypeMock = new Mock(); + contentTypeMock.SetupGet(x => x.Alias).Returns(contentTypeAlias); + contentTypeMock.SetupGet(x => x.Key).Returns(ContentType); + + var anotherContentTypeMock = new Mock(); + anotherContentTypeMock.SetupGet(x => x.Alias).Returns(anotherContentTypeAlias); + anotherContentTypeMock.SetupGet(x => x.Key).Returns(anotherContentTypeKey); + + var contentTypeServiceMock = new Mock(); + contentTypeServiceMock.Setup(x => x.GetAll()).Returns(new[] { contentTypeMock.Object, anotherContentTypeMock.Object }); + + _navigationService = new TestContentNavigationService( + Mock.Of(), + Mock.Of(), + contentTypeServiceMock.Object); + + // We need to re-create the test data since we use new mock + CreateTestData(); + + // Adding 2 new descendants with different content type under Child2 + _navigationService.Add(Guid.NewGuid(), anotherContentTypeKey, Grandchild3); + _navigationService.Add(Guid.NewGuid(), anotherContentTypeKey, GreatGrandchild1); + + // Act + _navigationService.TryGetDescendantsKeysOfType(parentKey, anotherContentTypeAlias, out IEnumerable descendantsKeysOfType); + var descendantsOfTypeCount = descendantsKeysOfType.Count(); + + // Assert + // Retrieve descendants without filtering to compare + _navigationService.TryGetDescendantsKeys(parentKey, out IEnumerable allDescendantsKeys); + var allDescendantsCount = allDescendantsKeys.Count(); + + Assert.Multiple(() => + { + Assert.IsTrue(allDescendantsCount > descendantsOfTypeCount); + Assert.AreEqual(4, allDescendantsCount); + Assert.AreEqual(2, descendantsOfTypeCount); + }); + } + + [Test] + public void Can_Get_Descendants_Of_Type_Filters_Result_And_Maintains_Their_Order_Of_Creation() + { + // Arrange + Guid parentKey = Child2; + const string contentTypeAlias = "contentPage"; + const string anotherContentTypeAlias = "anotherContentPage"; + Guid anotherContentTypeKey = Guid.NewGuid(); + + var contentTypeMock = new Mock(); + contentTypeMock.SetupGet(x => x.Alias).Returns(contentTypeAlias); + contentTypeMock.SetupGet(x => x.Key).Returns(ContentType); + + var anotherContentTypeMock = new Mock(); + anotherContentTypeMock.SetupGet(x => x.Alias).Returns(anotherContentTypeAlias); + anotherContentTypeMock.SetupGet(x => x.Key).Returns(anotherContentTypeKey); + + var contentTypeServiceMock = new Mock(); + contentTypeServiceMock.Setup(x => x.GetAll()).Returns(new[] { contentTypeMock.Object, anotherContentTypeMock.Object }); + + _navigationService = new TestContentNavigationService( + Mock.Of(), + Mock.Of(), + contentTypeServiceMock.Object); + + // We need to re-create the test data since we use new mock + CreateTestData(); + + // Adding 2 new descendants with different content type under Child2 + Guid greatGreatGrandchild2 = Guid.NewGuid(); + Guid greatGreatGrandchild3 = Guid.NewGuid(); + _navigationService.Add(greatGreatGrandchild2, anotherContentTypeKey, Grandchild3); + _navigationService.Add(greatGreatGrandchild3, anotherContentTypeKey, Grandchild3); + + var expectedDescendantsOrder = new List { greatGreatGrandchild2, greatGreatGrandchild3 }; + + // Act + _navigationService.TryGetDescendantsKeysOfType(parentKey, anotherContentTypeAlias, out IEnumerable descendantsOfType); + + // Assert + // Check that the order matches what is expected + Assert.IsTrue(expectedDescendantsOrder.SequenceEqual(descendantsOfType)); + } + [Test] public void Cannot_Get_Ancestors_From_Non_Existing_Content_Key() { @@ -348,7 +897,8 @@ public class ContentNavigationServiceBaseTests [TestCase("E48DD82A-7059-418E-9B82-CDD5205796CF", new string[0])] // Root [TestCase("C6173927-0C59-4778-825D-D7B9F45D8DDE", new[] { "E48DD82A-7059-418E-9B82-CDD5205796CF" })] // Child 1 [TestCase("E856AC03-C23E-4F63-9AA9-681B42A58573", new[] { "C6173927-0C59-4778-825D-D7B9F45D8DDE", "E48DD82A-7059-418E-9B82-CDD5205796CF" })] // Grandchild 1 - [TestCase("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7", new[] { "D63C1621-C74A-4106-8587-817DEE5FB732", "60E0E5C4-084E-4144-A560-7393BEAD2E96", "E48DD82A-7059-418E-9B82-CDD5205796CF" })] // Great-grandchild 1 + [TestCase("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7", new[] { "D63C1621-C74A-4106-8587-817DEE5FB732", "60E0E5C4-084E-4144-A560-7393BEAD2E96", "E48DD82A-7059-418E-9B82-CDD5205796CF", + })] // Great-grandchild 1 public void Can_Get_Ancestors_From_Existing_Content_Key_In_Correct_Order_Of_Creation(Guid childKey, string[] ancestors) { // Arrange @@ -365,6 +915,145 @@ public class ContentNavigationServiceBaseTests } } + [Test] + public void Cannot_Get_Ancestors_Of_Type_From_Non_Existing_Content_Type_Alias() + { + // Arrange + Guid childKey = GreatGrandchild1; + var nonExistingContentTypeAlias = string.Empty; + + // Act + var result = _navigationService.TryGetAncestorsKeysOfType(childKey, nonExistingContentTypeAlias, out IEnumerable ancestorsKeys); + + // Assert + Assert.Multiple(() => + { + Assert.IsFalse(result); + Assert.IsEmpty(ancestorsKeys); + }); + } + + [Test] + public void Cannot_Get_Ancestors_Of_Type_From_Non_Existing_Content_Key() + { + // Arrange + var nonExistingKey = Guid.NewGuid(); + const string contentTypeAlias = "contentPage"; + + var contentTypeMock = new Mock(); + contentTypeMock.SetupGet(x => x.Alias).Returns(contentTypeAlias); + contentTypeMock.SetupGet(x => x.Key).Returns(ContentType); + + var contentTypeServiceMock = new Mock(); + contentTypeServiceMock.Setup(x => x.GetAll()).Returns(new[] { contentTypeMock.Object }); + + _navigationService = new TestContentNavigationService( + Mock.Of(), + Mock.Of(), + contentTypeServiceMock.Object); + + // Act + var result = _navigationService.TryGetAncestorsKeysOfType(nonExistingKey, contentTypeAlias, out IEnumerable ancestorsKeys); + + // Assert + Assert.Multiple(() => + { + Assert.IsFalse(result); + Assert.IsEmpty(ancestorsKeys); + }); + } + + [Test] + [TestCase("E48DD82A-7059-418E-9B82-CDD5205796CF", 0)] // Root + [TestCase("C6173927-0C59-4778-825D-D7B9F45D8DDE", 1)] // Child 1 - Root + [TestCase("E856AC03-C23E-4F63-9AA9-681B42A58573", 2)] // Grandchild 1 - Child 1, Root + [TestCase("A1B1B217-B02F-4307-862C-A5E22DB729EB", 2)] // Grandchild 2 - Child 1, Root + [TestCase("60E0E5C4-084E-4144-A560-7393BEAD2E96", 1)] // Child 2 - Root + [TestCase("D63C1621-C74A-4106-8587-817DEE5FB732", 2)] // Grandchild 3 - Child 2, Root + [TestCase("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7", 3)] // Great-grandchild 1 - Grandchild 3, Child 2, Root + [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF", 1)] // Child 3 - Root + [TestCase("F381906C-223C-4466-80F7-B63B4EE073F8", 2)] // Grandchild 4 - Child 3, Root + public void Can_Get_Ancestors_Of_Type(Guid childKey, int ancestorsCount) + { + // Arrange + const string contentTypeAlias = "contentPage"; + + var contentTypeMock = new Mock(); + contentTypeMock.SetupGet(x => x.Alias).Returns(contentTypeAlias); + contentTypeMock.SetupGet(x => x.Key).Returns(ContentType); + + var contentTypeServiceMock = new Mock(); + contentTypeServiceMock.Setup(x => x.GetAll()).Returns(new[] { contentTypeMock.Object }); + + _navigationService = new TestContentNavigationService( + Mock.Of(), + Mock.Of(), + contentTypeServiceMock.Object); + + // We need to re-create the test data since we use new mock + CreateTestData(); + + // Act + var result = _navigationService.TryGetAncestorsKeysOfType(childKey, contentTypeAlias, out IEnumerable ancestorsKeys); + + // Assert + Assert.Multiple(() => + { + Assert.IsTrue(result); + Assert.AreEqual(ancestorsCount, ancestorsKeys.Count()); + }); + } + + [Test] + public void Can_Get_Ancestors_Of_Type_Filters_Result() + { + // Arrange + Guid childKey = Guid.NewGuid(); + const string contentTypeAlias = "contentPage"; + const string anotherContentTypeAlias = "anotherContentPage"; + Guid anotherContentTypeKey = Guid.NewGuid(); + + var contentTypeMock = new Mock(); + contentTypeMock.SetupGet(x => x.Alias).Returns(contentTypeAlias); + contentTypeMock.SetupGet(x => x.Key).Returns(ContentType); + + var anotherContentTypeMock = new Mock(); + anotherContentTypeMock.SetupGet(x => x.Alias).Returns(anotherContentTypeAlias); + anotherContentTypeMock.SetupGet(x => x.Key).Returns(anotherContentTypeKey); + + var contentTypeServiceMock = new Mock(); + contentTypeServiceMock.Setup(x => x.GetAll()).Returns(new[] { contentTypeMock.Object, anotherContentTypeMock.Object }); + + _navigationService = new TestContentNavigationService( + Mock.Of(), + Mock.Of(), + contentTypeServiceMock.Object); + + // We need to re-create the test data since we use new mock + CreateTestData(); + + // Adding 2 new items with different content type under Grandchild 1 + var greatGrandchild2 = Guid.NewGuid(); + _navigationService.Add(greatGrandchild2, anotherContentTypeKey, Grandchild1); + _navigationService.Add(childKey, anotherContentTypeKey, greatGrandchild2); + + // Act + _navigationService.TryGetAncestorsKeysOfType(childKey, anotherContentTypeAlias, out IEnumerable ancestorsKeysOfType); + var ancestorsOfTypeCount = ancestorsKeysOfType.Count(); + + // Assert + // Retrieve all ancestors without filtering to compare + _navigationService.TryGetAncestorsKeys(childKey, out IEnumerable allAncestorsKeys); + var allAncestorsCount = allAncestorsKeys.Count(); + + Assert.Multiple(() => + { + Assert.IsTrue(allAncestorsCount > ancestorsOfTypeCount); + Assert.AreEqual(4, allAncestorsCount); + Assert.AreEqual(1, ancestorsOfTypeCount); + }); + } + [Test] public void Cannot_Get_Siblings_Of_Non_Existing_Content_Key() { @@ -406,7 +1095,7 @@ public class ContentNavigationServiceBaseTests { // Arrange Guid anotherRoot = Guid.NewGuid(); - _navigationService.Add(anotherRoot); + _navigationService.Add(anotherRoot, ContentType); // Act _navigationService.TryGetSiblingsKeys(anotherRoot, out IEnumerable siblingsKeys); @@ -464,6 +1153,188 @@ public class ContentNavigationServiceBaseTests } } + [Test] + public void Cannot_Get_Siblings_Of_Type_From_Non_Existing_Content_Type_Alias() + { + // Arrange + Guid nodeKey = Child1; + var nonExistingContentTypeAlias = string.Empty; + + // Act + var result = _navigationService.TryGetSiblingsKeysOfType(nodeKey, nonExistingContentTypeAlias, out IEnumerable siblingsKeys); + + // Assert + Assert.Multiple(() => + { + Assert.IsFalse(result); + Assert.IsEmpty(siblingsKeys); + }); + } + + [Test] + public void Cannot_Get_Siblings_Of_Type_From_Non_Existing_Content_Key() + { + // Arrange + var nonExistingKey = Guid.NewGuid(); + const string contentTypeAlias = "contentPage"; + + var contentTypeMock = new Mock(); + contentTypeMock.SetupGet(x => x.Alias).Returns(contentTypeAlias); + contentTypeMock.SetupGet(x => x.Key).Returns(ContentType); + + var contentTypeServiceMock = new Mock(); + contentTypeServiceMock.Setup(x => x.GetAll()).Returns(new[] { contentTypeMock.Object }); + + _navigationService = new TestContentNavigationService( + Mock.Of(), + Mock.Of(), + contentTypeServiceMock.Object); + + // Act + var result = _navigationService.TryGetSiblingsKeysOfType(nonExistingKey, contentTypeAlias, out IEnumerable siblingsKeys); + + // Assert + Assert.Multiple(() => + { + Assert.IsFalse(result); + Assert.IsEmpty(siblingsKeys); + }); + } + + [Test] + [TestCase("E48DD82A-7059-418E-9B82-CDD5205796CF", 0)] // Root + [TestCase("C6173927-0C59-4778-825D-D7B9F45D8DDE", 2)] // Child 1 - Child 2, Child 3 + [TestCase("E856AC03-C23E-4F63-9AA9-681B42A58573", 1)] // Grandchild 1 - Grandchild 2 + [TestCase("A1B1B217-B02F-4307-862C-A5E22DB729EB", 1)] // Grandchild 2 - Grandchild 1 + [TestCase("60E0E5C4-084E-4144-A560-7393BEAD2E96", 2)] // Child 2 - Child 1, Child 3 + [TestCase("D63C1621-C74A-4106-8587-817DEE5FB732", 0)] // Grandchild 3 + [TestCase("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7", 0)] // Great-grandchild 1 + [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF", 2)] // Child 3 - Child 1, Child 2 + [TestCase("F381906C-223C-4466-80F7-B63B4EE073F8", 0)] // Grandchild 4 + public void Can_Get_Siblings_Of_Type(Guid key, int siblingsCount) + { + // Arrange + const string contentTypeAlias = "contentPage"; + + var contentTypeMock = new Mock(); + contentTypeMock.SetupGet(x => x.Alias).Returns(contentTypeAlias); + contentTypeMock.SetupGet(x => x.Key).Returns(ContentType); + + var contentTypeServiceMock = new Mock(); + contentTypeServiceMock.Setup(x => x.GetAll()).Returns(new[] { contentTypeMock.Object }); + + _navigationService = new TestContentNavigationService( + Mock.Of(), + Mock.Of(), + contentTypeServiceMock.Object); + + // We need to re-create the test data since we use new mock + CreateTestData(); + + // Act + var result = _navigationService.TryGetSiblingsKeysOfType(key, contentTypeAlias, out IEnumerable siblingsKeys); + + // Assert + Assert.Multiple(() => + { + Assert.IsTrue(result); + Assert.AreEqual(siblingsCount, siblingsKeys.Count()); + }); + } + + [Test] + public void Can_Get_Siblings_Of_Type_Filters_Result() + { + // Arrange + Guid nodeKey = Child1; + const string contentTypeAlias = "contentPage"; + const string anotherContentTypeAlias = "anotherContentPage"; + Guid anotherContentTypeKey = Guid.NewGuid(); + + var contentTypeMock = new Mock(); + contentTypeMock.SetupGet(x => x.Alias).Returns(contentTypeAlias); + contentTypeMock.SetupGet(x => x.Key).Returns(ContentType); + + var anotherContentTypeMock = new Mock(); + anotherContentTypeMock.SetupGet(x => x.Alias).Returns(anotherContentTypeAlias); + anotherContentTypeMock.SetupGet(x => x.Key).Returns(anotherContentTypeKey); + + var contentTypeServiceMock = new Mock(); + contentTypeServiceMock.Setup(x => x.GetAll()).Returns(new[] { contentTypeMock.Object, anotherContentTypeMock.Object }); + + _navigationService = new TestContentNavigationService( + Mock.Of(), + Mock.Of(), + contentTypeServiceMock.Object); + + // We need to re-create the test data since we use new mock + CreateTestData(); + + // Adding 2 new children with different content type under Root + _navigationService.Add(Guid.NewGuid(), anotherContentTypeKey, Root); + _navigationService.Add(Guid.NewGuid(), anotherContentTypeKey, Root); + + // Act + _navigationService.TryGetSiblingsKeysOfType(nodeKey, anotherContentTypeAlias, out IEnumerable siblingsKeysOfType); + var siblingsOfTypeCount = siblingsKeysOfType.Count(); + + // Assert + // Retrieve all siblings without filtering to compare + _navigationService.TryGetSiblingsKeys(nodeKey, out IEnumerable allSiblingsKeys); + var allSiblingsCount = allSiblingsKeys.Count(); + + Assert.Multiple(() => + { + Assert.IsTrue(allSiblingsCount > siblingsOfTypeCount); + Assert.AreEqual(4, allSiblingsCount); + Assert.AreEqual(2, siblingsOfTypeCount); + }); + } + + [Test] + public void Can_Get_Siblings_Of_Type_Filters_Result_And_Maintains_Their_Order_Of_Creation() + { + // Arrange + Guid nodeKey = Child1; + const string contentTypeAlias = "contentPage"; + const string anotherContentTypeAlias = "anotherContentPage"; + Guid anotherContentTypeKey = Guid.NewGuid(); + + var contentTypeMock = new Mock(); + contentTypeMock.SetupGet(x => x.Alias).Returns(contentTypeAlias); + contentTypeMock.SetupGet(x => x.Key).Returns(ContentType); + + var anotherContentTypeMock = new Mock(); + anotherContentTypeMock.SetupGet(x => x.Alias).Returns(anotherContentTypeAlias); + anotherContentTypeMock.SetupGet(x => x.Key).Returns(anotherContentTypeKey); + + var contentTypeServiceMock = new Mock(); + contentTypeServiceMock.Setup(x => x.GetAll()).Returns(new[] { contentTypeMock.Object, anotherContentTypeMock.Object }); + + _navigationService = new TestContentNavigationService( + Mock.Of(), + Mock.Of(), + contentTypeServiceMock.Object); + + // We need to re-create the test data since we use new mock + CreateTestData(); + + // Adding 2 new children with different content type under Root + Guid child4 = Guid.NewGuid(); + Guid child5 = Guid.NewGuid(); + _navigationService.Add(child4, anotherContentTypeKey, Root); + _navigationService.Add(child5, anotherContentTypeKey, Root); + + var expectedSiblingsOrder = new List { child4, child5 }; + + // Act + _navigationService.TryGetSiblingsKeysOfType(nodeKey, anotherContentTypeAlias, out IEnumerable siblingsKeysOfType); + + // Assert + // Check that the order matches what is expected + Assert.IsTrue(expectedSiblingsOrder.SequenceEqual(siblingsKeysOfType)); + } + [Test] public void Cannot_Get_Level_From_Non_Existing_Content_Key() { @@ -661,7 +1532,7 @@ public class ContentNavigationServiceBaseTests var nonExistentParentKey = Guid.NewGuid(); // Act - var result = _navigationService.Add(newNodeKey, nonExistentParentKey); + var result = _navigationService.Add(newNodeKey, ContentType, nonExistentParentKey); // Assert Assert.IsFalse(result); @@ -671,7 +1542,7 @@ public class ContentNavigationServiceBaseTests public void Cannot_Add_When_Node_With_The_Same_Key_Already_Exists() { // Act - var result = _navigationService.Add(Child1); + var result = _navigationService.Add(Child1, ContentType); // Assert Assert.IsFalse(result); @@ -684,7 +1555,7 @@ public class ContentNavigationServiceBaseTests var newNodeKey = Guid.NewGuid(); // Act - var result = _navigationService.Add(newNodeKey); // parentKey is null + var result = _navigationService.Add(newNodeKey, ContentType); // parentKey is null // Assert Assert.IsTrue(result); @@ -710,7 +1581,7 @@ public class ContentNavigationServiceBaseTests var currentChildrenCount = currentChildrenKeys.Count(); // Act - var result = _navigationService.Add(newNodeKey, parentKey); + var result = _navigationService.Add(newNodeKey, ContentType, parentKey); // Assert Assert.IsTrue(result); @@ -741,7 +1612,7 @@ public class ContentNavigationServiceBaseTests var newNodeKey = Guid.NewGuid(); // Act - _navigationService.Add(newNodeKey, parentKey); + _navigationService.Add(newNodeKey, ContentType, parentKey); // Assert _navigationService.TryGetChildrenKeys(parentKey, out IEnumerable childrenKeys); @@ -1121,12 +1992,47 @@ public class ContentNavigationServiceBaseTests Assert.AreEqual(initialDescendantsCount, descendantsCountAfterRestore); } + + private void CreateTestData() + { + ContentType = new Guid("217C492D-0067-478C-BEA8-D0CE2DECBEB9"); + + Root = new Guid("E48DD82A-7059-418E-9B82-CDD5205796CF"); + _navigationService.Add(Root, ContentType); + + Child1 = new Guid("C6173927-0C59-4778-825D-D7B9F45D8DDE"); + _navigationService.Add(Child1, ContentType, Root); + + Grandchild1 = new Guid("E856AC03-C23E-4F63-9AA9-681B42A58573"); + _navigationService.Add(Grandchild1, ContentType, Child1); + + Grandchild2 = new Guid("A1B1B217-B02F-4307-862C-A5E22DB729EB"); + _navigationService.Add(Grandchild2, ContentType, Child1); + + Child2 = new Guid("60E0E5C4-084E-4144-A560-7393BEAD2E96"); + _navigationService.Add(Child2, ContentType, Root); + + Grandchild3 = new Guid("D63C1621-C74A-4106-8587-817DEE5FB732"); + _navigationService.Add(Grandchild3, ContentType, Child2); + + GreatGrandchild1 = new Guid("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7"); + _navigationService.Add(GreatGrandchild1, ContentType, Grandchild3); + + Child3 = new Guid("B606E3FF-E070-4D46-8CB9-D31352029FDF"); + _navigationService.Add(Child3, ContentType, Root); + + Grandchild4 = new Guid("F381906C-223C-4466-80F7-B63B4EE073F8"); + _navigationService.Add(Grandchild4, ContentType, Child3); + } } -internal class TestContentNavigationService : ContentNavigationServiceBase +internal class TestContentNavigationService : ContentNavigationServiceBase { - public TestContentNavigationService(ICoreScopeProvider coreScopeProvider, INavigationRepository navigationRepository) - : base(coreScopeProvider, navigationRepository) + public TestContentNavigationService( + ICoreScopeProvider coreScopeProvider, + INavigationRepository navigationRepository, + IContentTypeService contentTypeService) + : base(coreScopeProvider, navigationRepository, contentTypeService) { } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/ContentNavigationServiceTest.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/ContentNavigationServiceTest.cs index 52863f056f..916dac279c 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/ContentNavigationServiceTest.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/ContentNavigationServiceTest.cs @@ -6,6 +6,7 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.Navigation; using Umbraco.Cms.Infrastructure.Persistence.Dtos; @@ -23,7 +24,7 @@ public class ContentNavigationServiceTest navigationRepoMock.Setup(x => x.GetContentNodesByObjectType(Constants.ObjectTypes.Document)) .Returns(navigationNodes); - var contentNavigationService = new DocumentNavigationService(GetScopeProvider(), navigationRepoMock.Object); + var contentNavigationService = new DocumentNavigationService(GetScopeProvider(), navigationRepoMock.Object, Mock.Of()); await contentNavigationService.RebuildAsync(); var success = contentNavigationService.TryGetLevel(rootKey, out var level); @@ -47,7 +48,7 @@ public class ContentNavigationServiceTest navigationRepoMock.Setup(x => x.GetContentNodesByObjectType(Constants.ObjectTypes.Document)) .Returns(navigationNodes); - var contentNavigationService = new DocumentNavigationService(GetScopeProvider(), navigationRepoMock.Object); + var contentNavigationService = new DocumentNavigationService(GetScopeProvider(), navigationRepoMock.Object, Mock.Of()); await contentNavigationService.RebuildAsync(); var success = contentNavigationService.TryGetLevel(childKey, out var level); @@ -71,7 +72,7 @@ public class ContentNavigationServiceTest navigationRepoMock.Setup(x => x.GetContentNodesByObjectType(Constants.ObjectTypes.Document)) .Returns(navigationNodes); - var contentNavigationService = new DocumentNavigationService(GetScopeProvider(), navigationRepoMock.Object); + var contentNavigationService = new DocumentNavigationService(GetScopeProvider(), navigationRepoMock.Object, Mock.Of()); await contentNavigationService.RebuildAsync(); var success = contentNavigationService.TryGetLevel(grandChildKey, out var level); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/UserGroupServiceTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/UserGroupServiceTests.cs index 268b3f0659..314c38d9bb 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/UserGroupServiceTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/UserGroupServiceTests.cs @@ -90,21 +90,27 @@ public class UserGroupServiceTests }); } - [TestCase(false,UserGroupOperationStatus.Success)] - [TestCase(true,UserGroupOperationStatus.CanNotUpdateAliasIsSystemUserGroup)] - public async Task Can_Not_Update_SystemGroup_Alias(bool isSystemGroup, UserGroupOperationStatus status) + // Obsoletion will be resolved when they are converted to internal consts. + [TestCase(null, null, UserGroupOperationStatus.Success)] + [TestCase(Constants.Security.AdminGroupKeyString, Constants.Security.AdminGroupAlias, UserGroupOperationStatus.CanNotUpdateAliasIsSystemUserGroup)] + [TestCase(Constants.Security.SensitiveDataGroupKeyString, Constants.Security.SensitiveDataGroupAlias, UserGroupOperationStatus.CanNotUpdateAliasIsSystemUserGroup)] + [TestCase(Constants.Security.TranslatorGroupString, Constants.Security.TranslatorGroupAlias, UserGroupOperationStatus.CanNotUpdateAliasIsSystemUserGroup)] + public async Task Can_Not_Update_SystemGroup_Alias(string? systemGroupKey, string? systemGroupAlias, UserGroupOperationStatus status) { + // prep + var userGroupAlias = systemGroupAlias ?? "someNonSystemAlias"; + Guid userGroupKey = systemGroupKey is not null ? new Guid(systemGroupKey) : Guid.NewGuid(); + // Arrange var actingUserKey = Guid.NewGuid(); var mockUser = SetupUserWithGroupAccess(actingUserKey, [Constants.Security.AdminGroupAlias]); var userService = SetupUserServiceWithGetUserByKey(actingUserKey, mockUser); var userGroupRepository = new Mock(); - var userGroupKey = Guid.NewGuid(); var persistedUserGroup = new UserGroup( Mock.Of(), 0, - isSystemGroup ? Constants.Security.AdminGroupAlias : "someNonSystemAlias", + userGroupAlias, "Administrators", null) { diff --git a/tools/Umbraco.JsonSchema/UmbracoCmsSchema.cs b/tools/Umbraco.JsonSchema/UmbracoCmsSchema.cs index ae54bd51da..7f02686ae6 100644 --- a/tools/Umbraco.JsonSchema/UmbracoCmsSchema.cs +++ b/tools/Umbraco.JsonSchema/UmbracoCmsSchema.cs @@ -1,5 +1,6 @@ using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Models; internal class UmbracoCmsSchema { @@ -79,5 +80,6 @@ internal class UmbracoCmsSchema public required MarketplaceSettings Marketplace { get; set; } public required WebhookSettings Webhook { get; set; } + public required CacheSettings Cache { get; set; } } }