diff --git a/src/Umbraco.Cms.Api.Common/Umbraco.Cms.Api.Common.csproj b/src/Umbraco.Cms.Api.Common/Umbraco.Cms.Api.Common.csproj index 9deb226783..f52572d515 100644 --- a/src/Umbraco.Cms.Api.Common/Umbraco.Cms.Api.Common.csproj +++ b/src/Umbraco.Cms.Api.Common/Umbraco.Cms.Api.Common.csproj @@ -10,8 +10,8 @@ - - + + diff --git a/src/Umbraco.Cms.Api.Delivery/Controllers/ByRouteContentApiController.cs b/src/Umbraco.Cms.Api.Delivery/Controllers/ByRouteContentApiController.cs index a506b0ce53..2209c1a330 100644 --- a/src/Umbraco.Cms.Api.Delivery/Controllers/ByRouteContentApiController.cs +++ b/src/Umbraco.Cms.Api.Delivery/Controllers/ByRouteContentApiController.cs @@ -1,10 +1,13 @@ +using System.Net; using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.DeliveryApi; using Umbraco.Cms.Core.Models.DeliveryApi; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Services; +using Umbraco.Extensions; namespace Umbraco.Cms.Api.Delivery.Controllers; @@ -13,17 +16,20 @@ public class ByRouteContentApiController : ContentApiItemControllerBase { private readonly IRequestRoutingService _requestRoutingService; private readonly IRequestRedirectService _requestRedirectService; + private readonly IRequestPreviewService _requestPreviewService; public ByRouteContentApiController( IApiPublishedContentCache apiPublishedContentCache, IApiContentResponseBuilder apiContentResponseBuilder, IPublicAccessService publicAccessService, IRequestRoutingService requestRoutingService, - IRequestRedirectService requestRedirectService) + IRequestRedirectService requestRedirectService, + IRequestPreviewService requestPreviewService) : base(apiPublishedContentCache, apiContentResponseBuilder, publicAccessService) { _requestRoutingService = requestRoutingService; _requestRedirectService = requestRedirectService; + _requestPreviewService = requestPreviewService; } /// @@ -40,11 +46,21 @@ public class ByRouteContentApiController : ContentApiItemControllerBase [ProducesResponseType(typeof(IApiContentResponse), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task ByRoute(string path = "/") + public async Task ByRoute(string path = "") { - var contentRoute = _requestRoutingService.GetContentRoute(path); + // OpenAPI does not allow reserved chars as "in:path" parameters, so clients based on the Swagger JSON will URL + // encode the path. Normally, ASP.NET Core handles that encoding with an automatic decoding - apparently just not + // for forward slashes, for whatever reason... so we need to deal with those. Hopefully this will be addressed in + // an upcoming version of ASP.NET Core. + // See also https://github.com/dotnet/aspnetcore/issues/11544 + if (path.Contains("%2F", StringComparison.OrdinalIgnoreCase)) + { + path = WebUtility.UrlDecode(path); + } - IPublishedContent? contentItem = ApiPublishedContentCache.GetByRoute(contentRoute); + path = path.EnsureStartsWith("/"); + + IPublishedContent? contentItem = GetContent(path); if (contentItem is not null) { if (IsProtected(contentItem)) @@ -61,6 +77,35 @@ public class ByRouteContentApiController : ContentApiItemControllerBase : NotFound(); } + private IPublishedContent? GetContent(string path) + => path.StartsWith(Constants.DeliveryApi.Routing.PreviewContentPathPrefix) + ? GetPreviewContent(path) + : GetPublishedContent(path); + + private IPublishedContent? GetPublishedContent(string path) + { + var contentRoute = _requestRoutingService.GetContentRoute(path); + + IPublishedContent? contentItem = ApiPublishedContentCache.GetByRoute(contentRoute); + return contentItem; + } + + private IPublishedContent? GetPreviewContent(string path) + { + if (_requestPreviewService.IsPreview() is false) + { + return null; + } + + if (Guid.TryParse(path.AsSpan(Constants.DeliveryApi.Routing.PreviewContentPathPrefix.Length).TrimEnd("/"), out Guid contentId) is false) + { + return null; + } + + IPublishedContent? contentItem = ApiPublishedContentCache.GetById(contentId); + return contentItem; + } + private IActionResult RedirectTo(IApiContentRoute redirectRoute) { Response.Headers.Add("Location-Start-Item-Path", redirectRoute.StartItem.Path); diff --git a/src/Umbraco.Cms.Api.Delivery/Controllers/QueryContentApiController.cs b/src/Umbraco.Cms.Api.Delivery/Controllers/QueryContentApiController.cs index 2b2efb8cda..e46204f85e 100644 --- a/src/Umbraco.Cms.Api.Delivery/Controllers/QueryContentApiController.cs +++ b/src/Umbraco.Cms.Api.Delivery/Controllers/QueryContentApiController.cs @@ -4,11 +4,11 @@ using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Api.Common.ViewModels.Pagination; using Umbraco.Cms.Core; using Umbraco.Cms.Core.DeliveryApi; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.DeliveryApi; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Services.OperationStatus; using Umbraco.Extensions; -using Umbraco.New.Cms.Core.Models; namespace Umbraco.Cms.Api.Delivery.Controllers; diff --git a/src/Umbraco.Cms.Api.Delivery/Indexing/Filters/NameFilterIndexer.cs b/src/Umbraco.Cms.Api.Delivery/Indexing/Filters/NameFilterIndexer.cs new file mode 100644 index 0000000000..eec3c1027d --- /dev/null +++ b/src/Umbraco.Cms.Api.Delivery/Indexing/Filters/NameFilterIndexer.cs @@ -0,0 +1,15 @@ +using Umbraco.Cms.Core.DeliveryApi; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Api.Delivery.Indexing.Filters; + +public sealed class NameFilterIndexer : IContentIndexHandler +{ + internal const string FieldName = "name"; + + public IEnumerable GetFieldValues(IContent content, string? culture) + => new[] { new IndexFieldValue { FieldName = FieldName, Values = new object[] { content.GetCultureName(culture) ?? string.Empty } } }; + + public IEnumerable GetFields() + => new[] { new IndexField { FieldName = FieldName, FieldType = FieldType.StringAnalyzed, VariesByCulture = true } }; +} diff --git a/src/Umbraco.Cms.Api.Delivery/Indexing/Sorts/NameSortIndexer.cs b/src/Umbraco.Cms.Api.Delivery/Indexing/Sorts/NameSortIndexer.cs index 5f5334cd05..65a1d67ed4 100644 --- a/src/Umbraco.Cms.Api.Delivery/Indexing/Sorts/NameSortIndexer.cs +++ b/src/Umbraco.Cms.Api.Delivery/Indexing/Sorts/NameSortIndexer.cs @@ -5,7 +5,7 @@ namespace Umbraco.Cms.Api.Delivery.Indexing.Sorts; public sealed class NameSortIndexer : IContentIndexHandler { - internal const string FieldName = "name"; + internal const string FieldName = "sortName"; public IEnumerable GetFieldValues(IContent content, string? culture) => new[] { new IndexFieldValue { FieldName = FieldName, Values = new object[] { content.GetCultureName(culture) ?? string.Empty } } }; diff --git a/src/Umbraco.Cms.Api.Delivery/Querying/Filters/NameFilter.cs b/src/Umbraco.Cms.Api.Delivery/Querying/Filters/NameFilter.cs index 0bf3d5e460..e12b20e81c 100644 --- a/src/Umbraco.Cms.Api.Delivery/Querying/Filters/NameFilter.cs +++ b/src/Umbraco.Cms.Api.Delivery/Querying/Filters/NameFilter.cs @@ -1,6 +1,6 @@ -using Umbraco.Cms.Api.Delivery.Indexing.Sorts; +using Umbraco.Cms.Api.Delivery.Indexing.Filters; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.DeliveryApi; -using Umbraco.Extensions; namespace Umbraco.Cms.Api.Delivery.Querying.Filters; @@ -16,16 +16,16 @@ public sealed class NameFilter : IFilterHandler public FilterOption BuildFilterOption(string filter) { var value = filter.Substring(NameSpecifier.Length); + var negate = value.StartsWith('!'); + var values = value.TrimStart('!').Split(Constants.CharArrays.Comma); return new FilterOption { - FieldName = NameSortIndexer.FieldName, - Values = value.IsNullOrWhiteSpace() == false - ? new[] { value.TrimStart('!') } - : Array.Empty(), - Operator = value.StartsWith('!') - ? FilterOperation.IsNot - : FilterOperation.Is + FieldName = NameFilterIndexer.FieldName, + Values = values, + Operator = negate + ? FilterOperation.DoesNotContain + : FilterOperation.Contains }; } } diff --git a/src/Umbraco.Cms.Api.Delivery/Services/ApiContentQueryProvider.cs b/src/Umbraco.Cms.Api.Delivery/Services/ApiContentQueryProvider.cs index d9ad8d0f15..45ae32b4f5 100644 --- a/src/Umbraco.Cms.Api.Delivery/Services/ApiContentQueryProvider.cs +++ b/src/Umbraco.Cms.Api.Delivery/Services/ApiContentQueryProvider.cs @@ -1,11 +1,13 @@ using Examine; +using Examine.Lucene.Providers; +using Examine.Lucene.Search; using Examine.Search; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core; using Umbraco.Cms.Core.DeliveryApi; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Examine; using Umbraco.Extensions; -using Umbraco.New.Cms.Core.Models; namespace Umbraco.Cms.Api.Delivery.Services; @@ -39,7 +41,7 @@ internal sealed class ApiContentQueryProvider : IApiContentQueryProvider .ToDictionary(field => field.FieldName, field => field.FieldType, StringComparer.InvariantCultureIgnoreCase); } - public PagedModel ExecuteQuery(SelectorOption selectorOption, IList filterOptions, IList sortOptions, string culture, int skip, int take) + public PagedModel ExecuteQuery(SelectorOption selectorOption, IList filterOptions, IList sortOptions, string culture, bool preview, int skip, int take) { if (!_examineManager.TryGetIndex(Constants.UmbracoIndexes.DeliveryApiContentIndexName, out IIndex? index)) { @@ -47,7 +49,7 @@ internal sealed class ApiContentQueryProvider : IApiContentQueryProvider return new PagedModel(); } - IBooleanOperation queryOperation = BuildSelectorOperation(selectorOption, index, culture); + IBooleanOperation queryOperation = BuildSelectorOperation(selectorOption, index, culture, preview); ApplyFiltering(filterOptions, queryOperation); ApplySorting(sortOptions, queryOperation); @@ -75,9 +77,16 @@ internal sealed class ApiContentQueryProvider : IApiContentQueryProvider FieldName = UmbracoExamineFieldNames.CategoryFieldName, Values = new[] { "content" } }; - private IBooleanOperation BuildSelectorOperation(SelectorOption selectorOption, IIndex index, string culture) + private IBooleanOperation BuildSelectorOperation(SelectorOption selectorOption, IIndex index, string culture, bool preview) { - IQuery query = index.Searcher.CreateQuery(); + // Needed for enabling leading wildcards searches + BaseLuceneSearcher searcher = index.Searcher as BaseLuceneSearcher ?? throw new InvalidOperationException($"Index searcher must be of type {nameof(BaseLuceneSearcher)}."); + + IQuery query = searcher.CreateQuery( + IndexTypes.Content, + BooleanOperation.And, + searcher.LuceneAnalyzer, + new LuceneSearchOptions { AllowLeadingWildcard = true }); IBooleanOperation selectorOperation = selectorOption.Values.Length == 1 ? query.Field(selectorOption.FieldName, selectorOption.Values.First()) @@ -86,6 +95,12 @@ internal sealed class ApiContentQueryProvider : IApiContentQueryProvider // Item culture must be either the requested culture or "none" selectorOperation.And().GroupedOr(new[] { UmbracoExamineFieldNames.DeliveryApiContentIndex.Culture }, culture.ToLowerInvariant().IfNullOrWhiteSpace(_fallbackGuidValue), "none"); + // when not fetching for preview, make sure the "published" field is "y" + if (preview is false) + { + selectorOperation.And().Field(UmbracoExamineFieldNames.DeliveryApiContentIndex.Published, "y"); + } + return selectorOperation; } @@ -103,6 +118,23 @@ internal sealed class ApiContentQueryProvider : IApiContentQueryProvider } } + void HandleContains(IQuery query, string fieldName, string[] values) + { + if (values.Length == 1) + { + // The trailing wildcard is added automatically + query.Field(fieldName, (IExamineValue)new ExamineValue(Examineness.ComplexWildcard, $"*{values[0]}")); + } + else + { + // The trailing wildcard is added automatically + IExamineValue[] examineValues = values + .Select(value => (IExamineValue)new ExamineValue(Examineness.ComplexWildcard, $"*{value}")) + .ToArray(); + query.GroupedOr(new[] { fieldName }, examineValues); + } + } + foreach (FilterOption filterOption in filterOptions) { var values = filterOption.Values.Any() @@ -112,18 +144,16 @@ internal sealed class ApiContentQueryProvider : IApiContentQueryProvider switch (filterOption.Operator) { case FilterOperation.Is: - // TODO: test this for explicit word matching HandleExact(queryOperation.And(), filterOption.FieldName, values); break; case FilterOperation.IsNot: - // TODO: test this for explicit word matching HandleExact(queryOperation.Not(), filterOption.FieldName, values); break; - // TODO: Fix case FilterOperation.Contains: + HandleContains(queryOperation.And(), filterOption.FieldName, values); break; - // TODO: Fix case FilterOperation.DoesNotContain: + HandleContains(queryOperation.Not(), filterOption.FieldName, values); break; default: continue; diff --git a/src/Umbraco.Cms.Api.Delivery/Services/ApiContentQueryService.cs b/src/Umbraco.Cms.Api.Delivery/Services/ApiContentQueryService.cs index 8aac5db6ee..dc03a79e19 100644 --- a/src/Umbraco.Cms.Api.Delivery/Services/ApiContentQueryService.cs +++ b/src/Umbraco.Cms.Api.Delivery/Services/ApiContentQueryService.cs @@ -1,9 +1,10 @@ using Umbraco.Cms.Api.Delivery.Indexing.Selectors; using Umbraco.Cms.Core; using Umbraco.Cms.Core.DeliveryApi; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Services.OperationStatus; -using Umbraco.New.Cms.Core.Models; +using Umbraco.Extensions; namespace Umbraco.Cms.Api.Delivery.Services; @@ -15,6 +16,7 @@ internal sealed class ApiContentQueryService : IApiContentQueryService private readonly SortHandlerCollection _sortHandlers; private readonly IVariationContextAccessor _variationContextAccessor; private readonly IApiContentQueryProvider _apiContentQueryProvider; + private readonly IRequestPreviewService _requestPreviewService; public ApiContentQueryService( IRequestStartItemProviderAccessor requestStartItemProviderAccessor, @@ -22,7 +24,8 @@ internal sealed class ApiContentQueryService : IApiContentQueryService FilterHandlerCollection filterHandlers, SortHandlerCollection sortHandlers, IVariationContextAccessor variationContextAccessor, - IApiContentQueryProvider apiContentQueryProvider) + IApiContentQueryProvider apiContentQueryProvider, + IRequestPreviewService requestPreviewService) { _requestStartItemProviderAccessor = requestStartItemProviderAccessor; _selectorHandlers = selectorHandlers; @@ -30,6 +33,7 @@ internal sealed class ApiContentQueryService : IApiContentQueryService _sortHandlers = sortHandlers; _variationContextAccessor = variationContextAccessor; _apiContentQueryProvider = apiContentQueryProvider; + _requestPreviewService = requestPreviewService; } /// @@ -45,7 +49,7 @@ internal sealed class ApiContentQueryService : IApiContentQueryService } var filterOptions = new List(); - foreach (var filter in filters) + foreach (var filter in filters.Where(filter => filter.IsNullOrWhiteSpace() is false)) { FilterOption? filterOption = GetFilterOption(filter); if (filterOption is null) @@ -58,7 +62,7 @@ internal sealed class ApiContentQueryService : IApiContentQueryService } var sortOptions = new List(); - foreach (var sort in sorts) + foreach (var sort in sorts.Where(sort => sort.IsNullOrWhiteSpace() is false)) { SortOption? sortOption = GetSortOption(sort); if (sortOption is null) @@ -71,8 +75,9 @@ internal sealed class ApiContentQueryService : IApiContentQueryService } var culture = _variationContextAccessor.VariationContext?.Culture ?? string.Empty; + var isPreview = _requestPreviewService.IsPreview(); - PagedModel result = _apiContentQueryProvider.ExecuteQuery(selectorOption, filterOptions, sortOptions, culture, skip, take); + PagedModel result = _apiContentQueryProvider.ExecuteQuery(selectorOption, filterOptions, sortOptions, culture, isPreview, skip, take); return Attempt.SucceedWithStatus(ApiContentQueryOperationStatus.Success, result); } diff --git a/src/Umbraco.Cms.Api.Delivery/Services/RequestCultureService.cs b/src/Umbraco.Cms.Api.Delivery/Services/RequestCultureService.cs index 9fe8b79251..4c2d877701 100644 --- a/src/Umbraco.Cms.Api.Delivery/Services/RequestCultureService.cs +++ b/src/Umbraco.Cms.Api.Delivery/Services/RequestCultureService.cs @@ -1,11 +1,12 @@ -using Microsoft.AspNetCore.Http; +using System.Text.RegularExpressions; +using Microsoft.AspNetCore.Http; using Microsoft.Net.Http.Headers; using Umbraco.Cms.Core.DeliveryApi; using Umbraco.Cms.Core.Models.PublishedContent; namespace Umbraco.Cms.Api.Delivery.Services; -internal sealed class RequestCultureService : RequestHeaderHandler, IRequestCultureService +internal sealed partial class RequestCultureService : RequestHeaderHandler, IRequestCultureService { private readonly IVariationContextAccessor _variationContextAccessor; @@ -14,7 +15,11 @@ internal sealed class RequestCultureService : RequestHeaderHandler, IRequestCult _variationContextAccessor = variationContextAccessor; /// - public string? GetRequestedCulture() => GetHeaderValue(HeaderNames.AcceptLanguage); + public string? GetRequestedCulture() + { + var acceptLanguage = GetHeaderValue(HeaderNames.AcceptLanguage) ?? string.Empty; + return ValidLanguageHeaderRegex().IsMatch(acceptLanguage) ? acceptLanguage : null; + } /// public void SetRequestCulture(string culture) @@ -26,4 +31,9 @@ internal sealed class RequestCultureService : RequestHeaderHandler, IRequestCult _variationContextAccessor.VariationContext = new VariationContext(culture); } + + // at the time of writing we're introducing this to get rid of accept-language header values like "en-GB,en-US;q=0.9,en;q=0.8", + // so we don't want to be too restrictive in this regex - keep it simple for now. + [GeneratedRegex(@"^[\w-]*$")] + private static partial Regex ValidLanguageHeaderRegex(); } diff --git a/src/Umbraco.Cms.Persistence.EFCore/Umbraco.Cms.Persistence.EFCore.csproj b/src/Umbraco.Cms.Persistence.EFCore/Umbraco.Cms.Persistence.EFCore.csproj index 3702db461b..95c5575901 100644 --- a/src/Umbraco.Cms.Persistence.EFCore/Umbraco.Cms.Persistence.EFCore.csproj +++ b/src/Umbraco.Cms.Persistence.EFCore/Umbraco.Cms.Persistence.EFCore.csproj @@ -9,7 +9,7 @@ - + diff --git a/src/Umbraco.Core/CompatibilitySuppressions.xml b/src/Umbraco.Core/CompatibilitySuppressions.xml index 61c15feaf4..bb0f66b2eb 100644 --- a/src/Umbraco.Core/CompatibilitySuppressions.xml +++ b/src/Umbraco.Core/CompatibilitySuppressions.xml @@ -1,6 +1,13 @@  + + CP0001 + T:Umbraco.New.Cms.Core.Models.PagedModel`1 + lib/net7.0/Umbraco.Core.dll + lib/net7.0/Umbraco.Core.dll + true + CP0005 M:Umbraco.Cms.Core.Models.PublishedContent.PublishedPropertyBase.GetDeliveryApiValue(System.Boolean,System.String,System.String) @@ -15,6 +22,13 @@ lib/net7.0/Umbraco.Core.dll true + + CP0006 + M:Umbraco.Cms.Core.Models.PublishedContent.IPublishedPropertyType.ConvertInterToDeliveryApiObject(Umbraco.Cms.Core.Models.PublishedContent.IPublishedElement,Umbraco.Cms.Core.PropertyEditors.PropertyCacheLevel,System.Object,System.Boolean,System.Boolean) + lib/net7.0/Umbraco.Core.dll + lib/net7.0/Umbraco.Core.dll + true + CP0006 M:Umbraco.Cms.Core.Models.PublishedContent.IPublishedPropertyType.ConvertInterToDeliveryApiObject(Umbraco.Cms.Core.Models.PublishedContent.IPublishedElement,Umbraco.Cms.Core.PropertyEditors.PropertyCacheLevel,System.Object,System.Boolean) @@ -36,4 +50,4 @@ lib/net7.0/Umbraco.Core.dll true - \ No newline at end of file + diff --git a/src/Umbraco.Core/Constants-DeliveryApi.cs b/src/Umbraco.Core/Constants-DeliveryApi.cs new file mode 100644 index 0000000000..e2f23e414c --- /dev/null +++ b/src/Umbraco.Core/Constants-DeliveryApi.cs @@ -0,0 +1,21 @@ +namespace Umbraco.Cms.Core; + +public static partial class Constants +{ + /// + /// Defines constants for the Delivery API. + /// + public static class DeliveryApi + { + /// + /// Constants for Delivery API routing purposes. + /// + public static class Routing + { + /// + /// Path prefix for unpublished content requested in a preview context. + /// + public const string PreviewContentPathPrefix = "preview-"; + } + } +} diff --git a/src/Umbraco.Core/Constants-Telemetry.cs b/src/Umbraco.Core/Constants-Telemetry.cs index f8a382b0bb..0e7c96d250 100644 --- a/src/Umbraco.Core/Constants-Telemetry.cs +++ b/src/Umbraco.Core/Constants-Telemetry.cs @@ -29,5 +29,6 @@ public static partial class Constants public static string DatabaseProvider = "DatabaseProvider"; public static string CurrentServerRole = "CurrentServerRole"; public static string RuntimeMode = "RuntimeMode"; + public static string BackofficeExternalLoginProviderCount = "BackofficeExternalLoginProviderCount"; } } diff --git a/src/Umbraco.Core/DeliveryApi/ApiContentRouteBuilder.cs b/src/Umbraco.Core/DeliveryApi/ApiContentRouteBuilder.cs index 80552b6488..0ae310d585 100644 --- a/src/Umbraco.Core/DeliveryApi/ApiContentRouteBuilder.cs +++ b/src/Umbraco.Core/DeliveryApi/ApiContentRouteBuilder.cs @@ -14,17 +14,24 @@ public sealed class ApiContentRouteBuilder : IApiContentRouteBuilder private readonly GlobalSettings _globalSettings; private readonly IVariationContextAccessor _variationContextAccessor; private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; + private readonly IRequestPreviewService _requestPreviewService; + private RequestHandlerSettings _requestSettings; public ApiContentRouteBuilder( IPublishedUrlProvider publishedUrlProvider, IOptions globalSettings, IVariationContextAccessor variationContextAccessor, - IPublishedSnapshotAccessor publishedSnapshotAccessor) + IPublishedSnapshotAccessor publishedSnapshotAccessor, + IRequestPreviewService requestPreviewService, + IOptionsMonitor requestSettings) { _publishedUrlProvider = publishedUrlProvider; _variationContextAccessor = variationContextAccessor; _publishedSnapshotAccessor = publishedSnapshotAccessor; + _requestPreviewService = requestPreviewService; _globalSettings = globalSettings.Value; + _requestSettings = requestSettings.CurrentValue; + requestSettings.OnChange(settings => _requestSettings = settings); } public IApiContentRoute? Build(IPublishedContent content, string? culture = null) @@ -34,9 +41,37 @@ public sealed class ApiContentRouteBuilder : IApiContentRouteBuilder throw new ArgumentException("Content locations can only be built from Content items.", nameof(content)); } - IPublishedContent root = content.Root(); + var isPreview = _requestPreviewService.IsPreview(); + + var contentPath = GetContentPath(content, culture, isPreview); + if (contentPath == null) + { + return null; + } + + contentPath = contentPath.EnsureStartsWith("/"); + + IPublishedContent root = GetRoot(content, isPreview); var rootPath = root.UrlSegment(_variationContextAccessor, culture) ?? string.Empty; + if (_globalSettings.HideTopLevelNodeFromPath == false) + { + contentPath = contentPath.TrimStart(rootPath.EnsureStartsWith("/")).EnsureStartsWith("/"); + } + + return new ApiContentRoute(contentPath, new ApiContentStartItem(root.Key, rootPath)); + } + + private string? GetContentPath(IPublishedContent content, string? culture, bool isPreview) + { + // entirely unpublished content does not resolve any route, but we need one i.e. for preview to work, + // so we'll use the content key as path. + if (isPreview && content.IsPublished(culture) is false) + { + return ContentPreviewPath(content); + } + + // grab the content path from the URL provider var contentPath = _publishedUrlProvider.GetUrl(content, UrlMode.Relative, culture); // in some scenarios the published content is actually routable, but due to the built-in handling of i.e. lacking culture setup @@ -48,19 +83,35 @@ public sealed class ApiContentRouteBuilder : IApiContentRouteBuilder } // if the content path has still not been resolved as a valid path, the content is un-routable in this culture + // - unless we are routing for preview if (IsInvalidContentPath(contentPath)) { - return null; + return isPreview + ? ContentPreviewPath(content) + : null; } - contentPath = contentPath.EnsureStartsWith("/"); - if (_globalSettings.HideTopLevelNodeFromPath == false) - { - contentPath = contentPath.TrimStart(rootPath.EnsureStartsWith("/")).EnsureStartsWith("/"); - } - - return new ApiContentRoute(contentPath, new ApiContentStartItem(root.Key, rootPath)); + return contentPath; } + private string ContentPreviewPath(IPublishedContent content) => $"{Constants.DeliveryApi.Routing.PreviewContentPathPrefix}{content.Key:D}{(_requestSettings.AddTrailingSlash ? "/" : string.Empty)}"; + private static bool IsInvalidContentPath(string path) => path.IsNullOrWhiteSpace() || "#".Equals(path); + + private IPublishedContent GetRoot(IPublishedContent content, bool isPreview) + { + if (isPreview is false) + { + return content.Root(); + } + + // in very edge case scenarios during preview, content.Root() does not map to the root. + // we'll code our way around it for the time being. + return _publishedSnapshotAccessor + .GetRequiredPublishedSnapshot() + .Content? + .GetAtRoot(true) + .FirstOrDefault(root => root.IsAncestorOrSelf(content)) + ?? content.Root(); + } } diff --git a/src/Umbraco.Core/DeliveryApi/IApiContentQueryProvider.cs b/src/Umbraco.Core/DeliveryApi/IApiContentQueryProvider.cs index df889184fd..69d7025d60 100644 --- a/src/Umbraco.Core/DeliveryApi/IApiContentQueryProvider.cs +++ b/src/Umbraco.Core/DeliveryApi/IApiContentQueryProvider.cs @@ -1,4 +1,4 @@ -using Umbraco.New.Cms.Core.Models; +using Umbraco.Cms.Core.Models; namespace Umbraco.Cms.Core.DeliveryApi; @@ -14,10 +14,11 @@ public interface IApiContentQueryProvider /// The filter options of the search criteria. /// The sorting options of the search criteria. /// The requested culture. + /// Whether or not to search for preview content. /// Number of search results to skip (for pagination). /// Number of search results to retrieve (for pagination). /// A paged model containing the resulting IDs and the total number of results that matching the search criteria. - PagedModel ExecuteQuery(SelectorOption selectorOption, IList filterOptions, IList sortOptions, string culture, int skip, int take); + PagedModel ExecuteQuery(SelectorOption selectorOption, IList filterOptions, IList sortOptions, string culture, bool preview, int skip, int take); /// /// Returns a selector option that can be applied to fetch "all content" (i.e. if a selector option is not present when performing a search). diff --git a/src/Umbraco.Core/DeliveryApi/IApiContentQueryService.cs b/src/Umbraco.Core/DeliveryApi/IApiContentQueryService.cs index e026264ba2..4a01cd926c 100644 --- a/src/Umbraco.Core/DeliveryApi/IApiContentQueryService.cs +++ b/src/Umbraco.Core/DeliveryApi/IApiContentQueryService.cs @@ -1,5 +1,5 @@ +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services.OperationStatus; -using Umbraco.New.Cms.Core.Models; namespace Umbraco.Cms.Core.DeliveryApi; diff --git a/src/Umbraco.Core/DeliveryApi/IApiRichTextParser.cs b/src/Umbraco.Core/DeliveryApi/IApiRichTextElementParser.cs similarity index 75% rename from src/Umbraco.Core/DeliveryApi/IApiRichTextParser.cs rename to src/Umbraco.Core/DeliveryApi/IApiRichTextElementParser.cs index 2f6c952987..067cdf068d 100644 --- a/src/Umbraco.Core/DeliveryApi/IApiRichTextParser.cs +++ b/src/Umbraco.Core/DeliveryApi/IApiRichTextElementParser.cs @@ -2,7 +2,7 @@ namespace Umbraco.Cms.Core.DeliveryApi; -public interface IApiRichTextParser +public interface IApiRichTextElementParser { IRichTextElement? Parse(string html); } diff --git a/src/Umbraco.Core/DeliveryApi/IApiRichTextMarkupParser.cs b/src/Umbraco.Core/DeliveryApi/IApiRichTextMarkupParser.cs new file mode 100644 index 0000000000..4a6c41be29 --- /dev/null +++ b/src/Umbraco.Core/DeliveryApi/IApiRichTextMarkupParser.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Core.DeliveryApi; + +public interface IApiRichTextMarkupParser +{ + string Parse(string html); +} diff --git a/src/Umbraco.Core/DeliveryApi/NoopApiContentQueryService.cs b/src/Umbraco.Core/DeliveryApi/NoopApiContentQueryService.cs index 96c60eeae7..d8dda6313c 100644 --- a/src/Umbraco.Core/DeliveryApi/NoopApiContentQueryService.cs +++ b/src/Umbraco.Core/DeliveryApi/NoopApiContentQueryService.cs @@ -1,5 +1,5 @@ +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services.OperationStatus; -using Umbraco.New.Cms.Core.Models; namespace Umbraco.Cms.Core.DeliveryApi; diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml index dae0637e46..9136f09c5e 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml @@ -3012,7 +3012,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont We will send:
  • Anonymized site ID, Umbraco version, and packages installed.
  • -
  • Number of: Root nodes, Content nodes, Macros, Media, Document Types, Templates, Languages, Domains, User Group, Users, Members, and Property Editors in use.
  • +
  • Number of: Root nodes, Content nodes, Macros, Media, Document Types, Templates, Languages, Domains, User Group, Users, Members, Backoffice external login providers, and Property Editors in use.
  • System information: Webserver, server OS, server framework, server OS language, and database provider.
  • Configuration settings: Modelsbuilder mode, if custom Umbraco path exists, ASP environment, and if you are in debug mode.
diff --git a/src/Umbraco.Core/Extensions/AssemblyExtensions.cs b/src/Umbraco.Core/Extensions/AssemblyExtensions.cs index 45ae9ceafe..b7cf65c414 100644 --- a/src/Umbraco.Core/Extensions/AssemblyExtensions.cs +++ b/src/Umbraco.Core/Extensions/AssemblyExtensions.cs @@ -1,7 +1,9 @@ // Copyright (c) Umbraco. // See LICENSE for more details. +using System.Diagnostics.CodeAnalysis; using System.Reflection; +using Umbraco.Cms.Core.Semver; namespace Umbraco.Extensions; @@ -104,4 +106,35 @@ public static class AssemblyExtensions return null; } + + /// + /// Gets the assembly informational version for the specified . + /// + /// The assembly. + /// The assembly version. + /// + /// true if the assembly information version is retrieved; otherwise, false. + /// + public static bool TryGetInformationalVersion(this Assembly assembly, [NotNullWhen(true)] out string? version) + { + AssemblyInformationalVersionAttribute? assemblyInformationalVersionAttribute = assembly.GetCustomAttribute(); + if (assemblyInformationalVersionAttribute is not null && + SemVersion.TryParse(assemblyInformationalVersionAttribute.InformationalVersion, out SemVersion? semVersion)) + { + version = semVersion.ToSemanticStringWithoutBuild(); + return true; + } + else + { + AssemblyName assemblyName = assembly.GetName(); + if (assemblyName.Version is not null) + { + version = assemblyName.Version.ToString(3); + return true; + } + } + + version = null; + return false; + } } diff --git a/src/Umbraco.Core/IO/IOHelperExtensions.cs b/src/Umbraco.Core/IO/IOHelperExtensions.cs index 7ae90e7f8e..52e1bbe0cd 100644 --- a/src/Umbraco.Core/IO/IOHelperExtensions.cs +++ b/src/Umbraco.Core/IO/IOHelperExtensions.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using Umbraco.Cms.Core.IO; namespace Umbraco.Extensions; @@ -11,6 +12,7 @@ public static class IOHelperExtensions /// /// /// + [return: NotNullIfNotNull("path")] public static string? ResolveRelativeOrVirtualUrl(this IIOHelper ioHelper, string? path) { if (string.IsNullOrWhiteSpace(path)) diff --git a/src/Umbraco.Core/Manifest/PackageManifest.cs b/src/Umbraco.Core/Manifest/PackageManifest.cs index 7bf07cfde9..1ef8e4fb08 100644 --- a/src/Umbraco.Core/Manifest/PackageManifest.cs +++ b/src/Umbraco.Core/Manifest/PackageManifest.cs @@ -5,7 +5,7 @@ using Umbraco.Extensions; namespace Umbraco.Cms.Core.Manifest; /// -/// Represents the content of a package manifest. +/// Represents the content of a package manifest. /// [DataContract] public class PackageManifest @@ -13,8 +13,20 @@ public class PackageManifest private string? _packageName; /// - /// An optional package name. If not specified then the directory name is used. + /// Gets or sets the package identifier. /// + /// + /// The package identifier. + /// + [DataMember(Name = "id")] + public string? PackageId { get; set; } + + /// + /// Gets or sets the name of the package. If not specified, uses the package identifier or directory name instead. + /// + /// + /// The name of the package. + /// [DataMember(Name = "name")] public string? PackageName { @@ -24,6 +36,11 @@ public class PackageManifest { return _packageName; } + + if (!PackageId.IsNullOrWhiteSpace()) + { + _packageName = PackageId; + } if (!Source.IsNullOrWhiteSpace()) { @@ -35,81 +52,132 @@ public class PackageManifest set => _packageName = value; } + /// + /// Gets or sets the package view. + /// + /// + /// The package view. + /// [DataMember(Name = "packageView")] public string? PackageView { get; set; } /// - /// Gets the source path of the manifest. + /// Gets or sets the source path of the manifest. /// + /// + /// The source path. + /// /// - /// - /// Gets the full absolute file path of the manifest, - /// using system directory separators. - /// + /// Gets the full/absolute file path of the manifest, using system directory separators. /// [IgnoreDataMember] public string Source { get; set; } = null!; /// - /// Gets or sets the version of the package + /// Gets or sets the version of the package. /// + /// + /// The version of the package. + /// [DataMember(Name = "version")] public string Version { get; set; } = string.Empty; /// - /// Gets or sets a value indicating whether telemetry is allowed + /// Gets or sets the assembly name to get the package version from. /// + /// + /// The assembly name to get the package version from. + /// + [DataMember(Name = "versionAssemblyName")] + public string? VersionAssemblyName { get; set; } + + /// + /// Gets or sets a value indicating whether telemetry is allowed. + /// + /// + /// true if package telemetry is allowed; otherwise, false. + /// [DataMember(Name = "allowPackageTelemetry")] public bool AllowPackageTelemetry { get; set; } = true; + /// + /// Gets or sets the bundle options. + /// + /// + /// The bundle options. + /// [DataMember(Name = "bundleOptions")] public BundleOptions BundleOptions { get; set; } /// - /// Gets or sets the scripts listed in the manifest. + /// Gets or sets the scripts listed in the manifest. /// + /// + /// The scripts. + /// [DataMember(Name = "javascript")] public string[] Scripts { get; set; } = Array.Empty(); /// - /// Gets or sets the stylesheets listed in the manifest. + /// Gets or sets the stylesheets listed in the manifest. /// + /// + /// The stylesheets. + /// [DataMember(Name = "css")] public string[] Stylesheets { get; set; } = Array.Empty(); /// - /// Gets or sets the property editors listed in the manifest. + /// Gets or sets the property editors listed in the manifest. /// + /// + /// The property editors. + /// [DataMember(Name = "propertyEditors")] public IDataEditor[] PropertyEditors { get; set; } = Array.Empty(); /// - /// Gets or sets the parameter editors listed in the manifest. + /// Gets or sets the parameter editors listed in the manifest. /// + /// + /// The parameter editors. + /// [DataMember(Name = "parameterEditors")] public IDataEditor[] ParameterEditors { get; set; } = Array.Empty(); /// - /// Gets or sets the grid editors listed in the manifest. + /// Gets or sets the grid editors listed in the manifest. /// + /// + /// The grid editors. + /// [DataMember(Name = "gridEditors")] public GridEditor[] GridEditors { get; set; } = Array.Empty(); /// - /// Gets or sets the content apps listed in the manifest. + /// Gets or sets the content apps listed in the manifest. /// + /// + /// The content apps. + /// [DataMember(Name = "contentApps")] public ManifestContentAppDefinition[] ContentApps { get; set; } = Array.Empty(); /// - /// Gets or sets the dashboards listed in the manifest. + /// Gets or sets the dashboards listed in the manifest. /// + /// + /// The dashboards. + /// [DataMember(Name = "dashboards")] public ManifestDashboard[] Dashboards { get; set; } = Array.Empty(); /// - /// Gets or sets the sections listed in the manifest. + /// Gets or sets the sections listed in the manifest. /// + /// + /// The sections. + /// [DataMember(Name = "sections")] public ManifestSection[] Sections { get; set; } = Array.Empty(); } diff --git a/src/Umbraco.Core/Models/PagedModel.cs b/src/Umbraco.Core/Models/PagedModel.cs index 7ccb582b56..f7b9d02260 100644 --- a/src/Umbraco.Core/Models/PagedModel.cs +++ b/src/Umbraco.Core/Models/PagedModel.cs @@ -1,4 +1,4 @@ -namespace Umbraco.New.Cms.Core.Models; +namespace Umbraco.Cms.Core.Models; public class PagedModel { diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedPropertyType.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedPropertyType.cs index 718f0421a1..83ca0c49df 100644 --- a/src/Umbraco.Core/Models/PublishedContent/IPublishedPropertyType.cs +++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedPropertyType.cs @@ -122,6 +122,11 @@ public interface IPublishedPropertyType /// The reference cache level. /// The intermediate value. /// A value indicating whether content should be considered draft. + /// A value indicating whether the property value should be expanded. /// The object value. - object? ConvertInterToDeliveryApiObject(IPublishedElement owner, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview); + object? ConvertInterToDeliveryApiObject(IPublishedElement owner, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview, bool expanding); + + [Obsolete($"Use the {nameof(ConvertInterToDeliveryApiObject)} that supports property expansion. Will be removed in V14.")] + object? ConvertInterToDeliveryApiObject(IPublishedElement owner, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) + => ConvertInterToDeliveryApiObject(owner, referenceCacheLevel, inter, preview, false); } diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyType.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyType.cs index de8cc43f15..0efa4e7653 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyType.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyType.cs @@ -301,7 +301,7 @@ namespace Umbraco.Cms.Core.Models.PublishedContent } /// - public object? ConvertInterToDeliveryApiObject(IPublishedElement owner, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) + public object? ConvertInterToDeliveryApiObject(IPublishedElement owner, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview, bool expanding) { if (!_initialized) { @@ -311,7 +311,7 @@ namespace Umbraco.Cms.Core.Models.PublishedContent // use the converter if any, else just return the inter value return _converter != null ? _converter is IDeliveryApiPropertyValueConverter deliveryApiPropertyValueConverter - ? deliveryApiPropertyValueConverter.ConvertIntermediateToDeliveryApiObject(owner, this, referenceCacheLevel, inter, preview) + ? deliveryApiPropertyValueConverter.ConvertIntermediateToDeliveryApiObject(owner, this, referenceCacheLevel, inter, preview, expanding) : _converter.ConvertIntermediateToObject(owner, this, referenceCacheLevel, inter, preview) : inter; } diff --git a/src/Umbraco.Core/Models/PublishedContent/RawValueProperty.cs b/src/Umbraco.Core/Models/PublishedContent/RawValueProperty.cs index cec2556342..3d29744bac 100644 --- a/src/Umbraco.Core/Models/PublishedContent/RawValueProperty.cs +++ b/src/Umbraco.Core/Models/PublishedContent/RawValueProperty.cs @@ -41,7 +41,7 @@ public class RawValueProperty : PublishedPropertyBase _xpathValue = new Lazy(() => PropertyType.ConvertInterToXPath(content, PropertyCacheLevel.Unknown, interValue?.Value, isPreviewing)); _deliveryApiValue = new Lazy(() => - PropertyType.ConvertInterToDeliveryApiObject(content, PropertyCacheLevel.Unknown, interValue?.Value, isPreviewing)); + PropertyType.ConvertInterToDeliveryApiObject(content, PropertyCacheLevel.Unknown, interValue?.Value, isPreviewing, false)); } // RawValueProperty does not (yet?) support variants, diff --git a/src/Umbraco.Core/Packaging/InstalledPackage.cs b/src/Umbraco.Core/Packaging/InstalledPackage.cs index 8a18cd1da6..42d62317c5 100644 --- a/src/Umbraco.Core/Packaging/InstalledPackage.cs +++ b/src/Umbraco.Core/Packaging/InstalledPackage.cs @@ -6,30 +6,40 @@ namespace Umbraco.Cms.Core.Packaging; [DataContract(Name = "installedPackage")] public class InstalledPackage { + [DataMember(Name = "id")] + public string? PackageId { get; set; } + [DataMember(Name = "name", IsRequired = true)] [Required] public string? PackageName { get; set; } - // TODO: Version? Icon? Other metadata? This would need to come from querying the package on Our [DataMember(Name = "packageView")] public string? PackageView { get; set; } [DataMember(Name = "version")] public string? Version { get; set; } + [DataMember(Name = "allowPackageTelemetry")] + public bool AllowPackageTelemetry { get; set; } = true; + [DataMember(Name = "plans")] - public IEnumerable PackageMigrationPlans { get; set; } = - Enumerable.Empty(); + public IEnumerable PackageMigrationPlans { get; set; } = Enumerable.Empty(); /// - /// It the package contains any migrations at all + /// Gets a value indicating whether this package has migrations. /// + /// + /// true if this package has migrations; otherwise, false. + /// [DataMember(Name = "hasMigrations")] public bool HasMigrations => PackageMigrationPlans.Any(); /// - /// If the package has any pending migrations to run + /// Gets a value indicating whether this package has pending migrations. /// + /// + /// true if this package has pending migrations; otherwise, false. + /// [DataMember(Name = "hasPendingMigrations")] public bool HasPendingMigrations => PackageMigrationPlans.Any(x => x.HasPendingMigrations); } diff --git a/src/Umbraco.Core/PropertyEditors/DefaultPropertyIndexValueFactory.cs b/src/Umbraco.Core/PropertyEditors/DefaultPropertyIndexValueFactory.cs index 705ab034fc..0193f45778 100644 --- a/src/Umbraco.Core/PropertyEditors/DefaultPropertyIndexValueFactory.cs +++ b/src/Umbraco.Core/PropertyEditors/DefaultPropertyIndexValueFactory.cs @@ -10,10 +10,14 @@ namespace Umbraco.Cms.Core.PropertyEditors; public class DefaultPropertyIndexValueFactory : IPropertyIndexValueFactory { /// - public IEnumerable>> GetIndexValues(IProperty property, string? culture, string? segment, bool published) + public IEnumerable>> GetIndexValues(IProperty property, string? culture, string? segment, bool published, IEnumerable availableCultures) { yield return new KeyValuePair>( property.Alias, property.GetValue(culture, segment, published).Yield()); } + + [Obsolete("Use the overload with the availableCultures parameter instead, scheduled for removal in v14")] + public IEnumerable>> GetIndexValues(IProperty property, string? culture, string? segment, bool published) + => GetIndexValues(property, culture, segment, published, Enumerable.Empty()); } diff --git a/src/Umbraco.Core/PropertyEditors/DeliveryApi/IDeliveryApiPropertyValueConverter.cs b/src/Umbraco.Core/PropertyEditors/DeliveryApi/IDeliveryApiPropertyValueConverter.cs index 7f4ce9558e..51d9f95873 100644 --- a/src/Umbraco.Core/PropertyEditors/DeliveryApi/IDeliveryApiPropertyValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/DeliveryApi/IDeliveryApiPropertyValueConverter.cs @@ -30,6 +30,7 @@ public interface IDeliveryApiPropertyValueConverter : IPropertyValueConverter /// The reference cache level. /// The intermediate value. /// A value indicating whether conversion should take place in preview mode. + /// A value indicating whether the property value should be expanded (if applicable). /// The result of the conversion. /// /// @@ -43,5 +44,9 @@ public interface IDeliveryApiPropertyValueConverter : IPropertyValueConverter /// the cache levels of property values. It is not meant to be used by the converter. /// /// - object? ConvertIntermediateToDeliveryApiObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview); + object? ConvertIntermediateToDeliveryApiObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview, bool expanding); + + [Obsolete($"Use the {nameof(ConvertIntermediateToDeliveryApiObject)} that supports property expansion. Will be removed in V14.")] + object? ConvertIntermediateToDeliveryApiObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) + => ConvertIntermediateToDeliveryApiObject(owner, propertyType, referenceCacheLevel, inter, preview, false); } diff --git a/src/Umbraco.Core/PropertyEditors/IPropertyIndexValueFactory.cs b/src/Umbraco.Core/PropertyEditors/IPropertyIndexValueFactory.cs index fd607f4054..732644b288 100644 --- a/src/Umbraco.Core/PropertyEditors/IPropertyIndexValueFactory.cs +++ b/src/Umbraco.Core/PropertyEditors/IPropertyIndexValueFactory.cs @@ -22,5 +22,9 @@ public interface IPropertyIndexValueFactory /// more than one value for a given field. /// /// + IEnumerable>> GetIndexValues(IProperty property, string? culture, string? segment, bool published, IEnumerable availableCultures) + => GetIndexValues(property, culture, segment, published); + + [Obsolete("Use the overload with the availableCultures parameter instead, scheduled for removal in v14")] IEnumerable>> GetIndexValues(IProperty property, string? culture, string? segment, bool published); } diff --git a/src/Umbraco.Core/PropertyEditors/JsonPropertyIndexValueFactoryBase.cs b/src/Umbraco.Core/PropertyEditors/JsonPropertyIndexValueFactoryBase.cs index e639ff7ca8..a56bf0ed88 100644 --- a/src/Umbraco.Core/PropertyEditors/JsonPropertyIndexValueFactoryBase.cs +++ b/src/Umbraco.Core/PropertyEditors/JsonPropertyIndexValueFactoryBase.cs @@ -25,7 +25,8 @@ public abstract class JsonPropertyIndexValueFactoryBase : IProperty IProperty property, string? culture, string? segment, - bool published) + bool published, + IEnumerable availableCultures) { var result = new List>>(); @@ -43,7 +44,7 @@ public abstract class JsonPropertyIndexValueFactoryBase : IProperty return result; } - result.AddRange(Handle(deserializedPropertyValue, property, culture, segment, published)); + result.AddRange(Handle(deserializedPropertyValue, property, culture, segment, published, availableCultures)); } catch (InvalidCastException) { @@ -62,6 +63,10 @@ public abstract class JsonPropertyIndexValueFactoryBase : IProperty return result; } + [Obsolete("Use method overload that has availableCultures, scheduled for removal in v14")] + public IEnumerable>> GetIndexValues(IProperty property, string? culture, string? segment, bool published) + => GetIndexValues(property, culture, segment, published, Enumerable.Empty()); + /// /// Method to return a list of resume of the content. By default this returns an empty list /// @@ -75,10 +80,23 @@ public abstract class JsonPropertyIndexValueFactoryBase : IProperty /// /// Method that handle the deserialized object. /// + [Obsolete("Use the overload with the availableCultures parameter instead, scheduled for removal in v14")] protected abstract IEnumerable>> Handle( TSerialized deserializedPropertyValue, IProperty property, string? culture, string? segment, bool published); + + /// + /// Method that handle the deserialized object. + /// + protected virtual IEnumerable>> Handle( + TSerialized deserializedPropertyValue, + IProperty property, + string? culture, + string? segment, + bool published, + IEnumerable availableCultures) => + Handle(deserializedPropertyValue, property, culture, segment, published); } diff --git a/src/Umbraco.Core/PropertyEditors/NoopPropertyIndexValueFactory.cs b/src/Umbraco.Core/PropertyEditors/NoopPropertyIndexValueFactory.cs index 7e64b368c4..223f8632ff 100644 --- a/src/Umbraco.Core/PropertyEditors/NoopPropertyIndexValueFactory.cs +++ b/src/Umbraco.Core/PropertyEditors/NoopPropertyIndexValueFactory.cs @@ -8,5 +8,9 @@ namespace Umbraco.Cms.Core.PropertyEditors; public class NoopPropertyIndexValueFactory : IPropertyIndexValueFactory { /// - public IEnumerable>> GetIndexValues(IProperty property, string? culture, string? segment, bool published) => Array.Empty>>(); + public IEnumerable>> GetIndexValues(IProperty property, string? culture, string? segment, bool published, IEnumerable availableCultures) => Array.Empty>>(); + + [Obsolete("Use the overload with the availableCultures parameter instead, scheduled for removal in v14")] + public IEnumerable>> GetIndexValues(IProperty property, string? culture, string? segment, bool published) + => GetIndexValues(property, culture, segment, published); } diff --git a/src/Umbraco.Core/PropertyEditors/TagPropertyIndexValueFactory.cs b/src/Umbraco.Core/PropertyEditors/TagPropertyIndexValueFactory.cs index 83a327e0ef..b3a8e9a2b8 100644 --- a/src/Umbraco.Core/PropertyEditors/TagPropertyIndexValueFactory.cs +++ b/src/Umbraco.Core/PropertyEditors/TagPropertyIndexValueFactory.cs @@ -9,6 +9,18 @@ public class TagPropertyIndexValueFactory : JsonPropertyIndexValueFactoryBase>> Handle( + string[] deserializedPropertyValue, + IProperty property, + string? culture, + string? segment, + bool published, + IEnumerable availableCultures) + { + yield return new KeyValuePair>(property.Alias, deserializedPropertyValue); + } + + [Obsolete("Use the overload that specifies availableCultures, scheduled for removal in v14")] protected override IEnumerable>> Handle( string[] deserializedPropertyValue, IProperty property, diff --git a/src/Umbraco.Core/PropertyEditors/TextStringValueConverter.cs b/src/Umbraco.Core/PropertyEditors/TextStringValueConverter.cs index 90b0574086..6c9c80ce81 100644 --- a/src/Umbraco.Core/PropertyEditors/TextStringValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/TextStringValueConverter.cs @@ -62,6 +62,6 @@ public class TextStringValueConverter : PropertyValueConverterBase, IDeliveryApi public Type GetDeliveryApiPropertyValueType(IPublishedPropertyType propertyType) => GetPropertyValueType(propertyType); - public object ConvertIntermediateToDeliveryApiObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) + public object ConvertIntermediateToDeliveryApiObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview, bool expanding) => ConvertIntermediateToObject(owner, propertyType, referenceCacheLevel, inter, preview); } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/ContentPickerValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/ContentPickerValueConverter.cs index 05e8bc9019..06bfd1b1f2 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/ContentPickerValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/ContentPickerValueConverter.cs @@ -100,7 +100,7 @@ public class ContentPickerValueConverter : PropertyValueConverterBase, IDelivery public Type GetDeliveryApiPropertyValueType(IPublishedPropertyType propertyType) => typeof(IApiContent); - public object? ConvertIntermediateToDeliveryApiObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) + public object? ConvertIntermediateToDeliveryApiObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview, bool expanding) { IPublishedContent? content = GetContent(propertyType, inter); if (content == null) diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/MediaPickerValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/MediaPickerValueConverter.cs index 30ff7fb779..468d4cdd0c 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/MediaPickerValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/MediaPickerValueConverter.cs @@ -126,7 +126,7 @@ public class MediaPickerValueConverter : PropertyValueConverterBase, IDeliveryAp public Type GetDeliveryApiPropertyValueType(IPublishedPropertyType propertyType) => typeof(IEnumerable); - public object? ConvertIntermediateToDeliveryApiObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) + public object? ConvertIntermediateToDeliveryApiObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview, bool expanding) { var isMultiple = IsMultipleDataType(propertyType.DataType); diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/MemberGroupPickerValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/MemberGroupPickerValueConverter.cs index 12c849b8a3..fd9e1651c8 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/MemberGroupPickerValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/MemberGroupPickerValueConverter.cs @@ -35,7 +35,7 @@ public class MemberGroupPickerValueConverter : PropertyValueConverterBase, IDeli public Type GetDeliveryApiPropertyValueType(IPublishedPropertyType propertyType) => typeof(string[]); - public object? ConvertIntermediateToDeliveryApiObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) + public object? ConvertIntermediateToDeliveryApiObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview, bool expanding) { var memberGroupIds = inter? .ToString()? diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/MemberPickerValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/MemberPickerValueConverter.cs index 4a7c65de31..014a0a1a8c 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/MemberPickerValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/MemberPickerValueConverter.cs @@ -108,6 +108,6 @@ public class MemberPickerValueConverter : PropertyValueConverterBase, IDeliveryA public Type GetDeliveryApiPropertyValueType(IPublishedPropertyType propertyType) => typeof(string); // member picker is unsupported for Delivery API output to avoid leaking member data by accident. - public object? ConvertIntermediateToDeliveryApiObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) + public object? ConvertIntermediateToDeliveryApiObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview, bool expanding) => "(unsupported)"; } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/MultiNodeTreePickerValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/MultiNodeTreePickerValueConverter.cs index d0ab92223b..c14a56a0bd 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/MultiNodeTreePickerValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/MultiNodeTreePickerValueConverter.cs @@ -192,7 +192,7 @@ public class MultiNodeTreePickerValueConverter : PropertyValueConverterBase, IDe }; - public object? ConvertIntermediateToDeliveryApiObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) + public object? ConvertIntermediateToDeliveryApiObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview, bool expanding) { IEnumerable DefaultValue() => Array.Empty(); diff --git a/src/Umbraco.Core/PublishedCache/PublishedElementPropertyBase.cs b/src/Umbraco.Core/PublishedCache/PublishedElementPropertyBase.cs index 13d096f846..b4e56897a7 100644 --- a/src/Umbraco.Core/PublishedCache/PublishedElementPropertyBase.cs +++ b/src/Umbraco.Core/PublishedCache/PublishedElementPropertyBase.cs @@ -228,7 +228,7 @@ internal class PublishedElementPropertyBase : PublishedPropertyBase { CacheValues cacheValues = GetCacheValues(cacheLevel); - object? GetDeliveryApiObject() => PropertyType.ConvertInterToDeliveryApiObject(Element, referenceCacheLevel, GetInterValue(), IsPreviewing); + object? GetDeliveryApiObject() => PropertyType.ConvertInterToDeliveryApiObject(Element, referenceCacheLevel, GetInterValue(), IsPreviewing, expanding); return expanding ? GetDeliveryApiExpandedObject(cacheValues, GetDeliveryApiObject) : GetDeliveryApiDefaultObject(cacheValues, GetDeliveryApiObject); diff --git a/src/Umbraco.Core/Semver/Semver.cs b/src/Umbraco.Core/Semver/Semver.cs index 3c33f43087..a8261f3054 100644 --- a/src/Umbraco.Core/Semver/Semver.cs +++ b/src/Umbraco.Core/Semver/Semver.cs @@ -1,4 +1,5 @@ -using System.Text.RegularExpressions; +using System.Text.RegularExpressions; +using System.Diagnostics.CodeAnalysis; #if !NETSTANDARD using System.Globalization; using System.Runtime.Serialization; @@ -195,7 +196,7 @@ namespace Umbraco.Cms.Core.Semver /// /// If set to true minor and patch version are required, else they default to 0. /// False when a invalid version string is passed, otherwise true. - public static bool TryParse(string version, out SemVersion? semver, bool strict = false) + public static bool TryParse(string version, [NotNullWhen(true)] out SemVersion? semver, bool strict = false) { try { diff --git a/src/Umbraco.Core/Services/ILocalizationService.cs b/src/Umbraco.Core/Services/ILocalizationService.cs index dbfb01d3e1..7f851d529a 100644 --- a/src/Umbraco.Core/Services/ILocalizationService.cs +++ b/src/Umbraco.Core/Services/ILocalizationService.cs @@ -1,5 +1,4 @@ using Umbraco.Cms.Core.Models; -using Umbraco.New.Cms.Core.Models; namespace Umbraco.Cms.Core.Services; diff --git a/src/Umbraco.Core/Services/ITrackedReferencesService.cs b/src/Umbraco.Core/Services/ITrackedReferencesService.cs index 94a8871e7f..5ffe5e3651 100644 --- a/src/Umbraco.Core/Services/ITrackedReferencesService.cs +++ b/src/Umbraco.Core/Services/ITrackedReferencesService.cs @@ -1,5 +1,4 @@ using Umbraco.Cms.Core.Models; -using Umbraco.New.Cms.Core.Models; namespace Umbraco.Cms.Core.Services; diff --git a/src/Umbraco.Core/Services/TrackedReferencesService.cs b/src/Umbraco.Core/Services/TrackedReferencesService.cs index 280e648327..a2ab4c9767 100644 --- a/src/Umbraco.Core/Services/TrackedReferencesService.cs +++ b/src/Umbraco.Core/Services/TrackedReferencesService.cs @@ -1,7 +1,6 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; -using Umbraco.New.Cms.Core.Models; namespace Umbraco.Cms.Core.Services; diff --git a/src/Umbraco.Core/Telemetry/Models/PackageTelemetry.cs b/src/Umbraco.Core/Telemetry/Models/PackageTelemetry.cs index 53c07766e8..b0500c0127 100644 --- a/src/Umbraco.Core/Telemetry/Models/PackageTelemetry.cs +++ b/src/Umbraco.Core/Telemetry/Models/PackageTelemetry.cs @@ -3,23 +3,35 @@ using System.Runtime.Serialization; namespace Umbraco.Cms.Core.Telemetry.Models; /// -/// Serializable class containing information about an installed package. +/// Serializable class containing information about an installed package. /// [DataContract(Name = "packageTelemetry")] public class PackageTelemetry { /// - /// Gets or sets the name of the installed package. + /// Gets or sets the identifier of the installed package. /// + /// + /// The identifier. + /// + [DataMember(Name = "id")] + public string? Id { get; set; } + + /// + /// Gets or sets the name of the installed package. + /// + /// + /// The name. + /// [DataMember(Name = "name")] public string? Name { get; set; } /// - /// Gets or sets the version of the installed package. + /// Gets or sets the version of the installed package. /// - /// - /// This may be an empty string if no version is specified, or if package telemetry has been restricted. - /// + /// + /// The version. + /// [DataMember(Name = "version")] public string? Version { get; set; } } diff --git a/src/Umbraco.Core/Telemetry/TelemetryService.cs b/src/Umbraco.Core/Telemetry/TelemetryService.cs index 4ebf1ba0b9..82d1b19493 100644 --- a/src/Umbraco.Core/Telemetry/TelemetryService.cs +++ b/src/Umbraco.Core/Telemetry/TelemetryService.cs @@ -2,8 +2,8 @@ // See LICENSE for more details. using Umbraco.Cms.Core.Configuration; -using Umbraco.Cms.Core.Manifest; using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Packaging; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Telemetry.Models; using Umbraco.Extensions; @@ -13,7 +13,7 @@ namespace Umbraco.Cms.Core.Telemetry; /// internal class TelemetryService : ITelemetryService { - private readonly IManifestParser _manifestParser; + private readonly IPackagingService _packagingService; private readonly IMetricsConsentService _metricsConsentService; private readonly ISiteIdentifierService _siteIdentifierService; private readonly IUmbracoVersion _umbracoVersion; @@ -23,13 +23,13 @@ internal class TelemetryService : ITelemetryService /// Initializes a new instance of the class. ///
public TelemetryService( - IManifestParser manifestParser, + IPackagingService packagingService, IUmbracoVersion umbracoVersion, ISiteIdentifierService siteIdentifierService, IUsageInformationService usageInformationService, IMetricsConsentService metricsConsentService) { - _manifestParser = manifestParser; + _packagingService = packagingService; _umbracoVersion = umbracoVersion; _siteIdentifierService = siteIdentifierService; _usageInformationService = usageInformationService; @@ -73,19 +73,20 @@ internal class TelemetryService : ITelemetryService } List packages = new(); - IEnumerable manifests = _manifestParser.GetManifests(); + IEnumerable installedPackages = _packagingService.GetAllInstalledPackages(); - foreach (PackageManifest manifest in manifests) + foreach (InstalledPackage installedPackage in installedPackages) { - if (manifest.AllowPackageTelemetry is false) + if (installedPackage.AllowPackageTelemetry is false) { continue; } packages.Add(new PackageTelemetry { - Name = manifest.PackageName, - Version = manifest.Version ?? string.Empty, + Id = installedPackage.PackageId, + Name = installedPackage.PackageName, + Version = installedPackage.Version, }); } diff --git a/src/Umbraco.Examine.Lucene/DeliveryApiContentIndex.cs b/src/Umbraco.Examine.Lucene/DeliveryApiContentIndex.cs index 0967022d77..c70c9d4da0 100644 --- a/src/Umbraco.Examine.Lucene/DeliveryApiContentIndex.cs +++ b/src/Umbraco.Examine.Lucene/DeliveryApiContentIndex.cs @@ -21,7 +21,7 @@ public class DeliveryApiContentIndex : UmbracoExamineIndex IRuntimeState runtimeState) : base(loggerFactory, name, indexOptions, hostingEnvironment, runtimeState) { - PublishedValuesOnly = true; + PublishedValuesOnly = false; EnableDefaultEventHandler = false; _logger = loggerFactory.CreateLogger(); diff --git a/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextParser.cs b/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextElementParser.cs similarity index 59% rename from src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextParser.cs rename to src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextElementParser.cs index 4044b37516..c40debd690 100644 --- a/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextParser.cs +++ b/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextElementParser.cs @@ -1,34 +1,28 @@ -using System.Text.RegularExpressions; -using HtmlAgilityPack; +using HtmlAgilityPack; using Microsoft.Extensions.Logging; -using Umbraco.Cms.Core; using Umbraco.Cms.Core.DeliveryApi; using Umbraco.Cms.Core.Models.DeliveryApi; -using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Routing; using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.DeliveryApi; -internal sealed partial class ApiRichTextParser : IApiRichTextParser +internal sealed class ApiRichTextElementParser : ApiRichTextParserBase, IApiRichTextElementParser { - private readonly IApiContentRouteBuilder _apiContentRouteBuilder; private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; - private readonly IPublishedUrlProvider _publishedUrlProvider; - private readonly ILogger _logger; + private readonly ILogger _logger; private const string TextNodeName = "#text"; - public ApiRichTextParser( + public ApiRichTextElementParser( IApiContentRouteBuilder apiContentRouteBuilder, - IPublishedSnapshotAccessor publishedSnapshotAccessor, IPublishedUrlProvider publishedUrlProvider, - ILogger logger) + IPublishedSnapshotAccessor publishedSnapshotAccessor, + ILogger logger) + : base(apiContentRouteBuilder, publishedUrlProvider) { - _apiContentRouteBuilder = apiContentRouteBuilder; _publishedSnapshotAccessor = publishedSnapshotAccessor; - _publishedUrlProvider = publishedUrlProvider; _logger = logger; } @@ -100,70 +94,30 @@ internal sealed partial class ApiRichTextParser : IApiRichTextParser return; } - Match match = LocalLinkRegex().Match(href); - if (match.Success is false) - { - return; - } - - attributes.Remove("href"); - - if (UdiParser.TryParse(match.Groups["udi"].Value, out Udi? udi) is false) - { - return; - } - - switch (udi.EntityType) - { - case Constants.UdiEntityType.Document: - IPublishedContent? content = publishedSnapshot.Content?.GetById(udi); - IApiContentRoute? route = content != null - ? _apiContentRouteBuilder.Build(content) - : null; - if (route != null) - { - attributes["route"] = route; - } - - break; - case Constants.UdiEntityType.Media: - IPublishedContent? media = publishedSnapshot.Media?.GetById(udi); - if (media != null) - { - attributes["href"] = _publishedUrlProvider.GetMediaUrl(media, UrlMode.Absolute); - } - - break; - } + ReplaceLocalLinks( + publishedSnapshot, + href, + route => + { + attributes["route"] = route; + attributes.Remove("href"); + }, + url => attributes["href"] = url, + () => attributes.Remove("href")); } private void ReplaceLocalImages(IPublishedSnapshot publishedSnapshot, string tag, Dictionary attributes) { - if (tag is not "img" || attributes.ContainsKey("data-udi") is false) + if (tag is not "img" || attributes.ContainsKey("data-udi") is false || attributes["data-udi"] is not string dataUdi) { return; } - var dataUdiValue = attributes["data-udi"]; - attributes.Remove("data-udi"); - - if (dataUdiValue is not string dataUdi || UdiParser.TryParse(dataUdi, out Udi? udi) is false) + ReplaceLocalImages(publishedSnapshot, dataUdi, mediaUrl => { - return; - } - - IPublishedContent? media = publishedSnapshot.Media?.GetById(udi); - if (media is not null) - { - attributes["src"] = _publishedUrlProvider.GetMediaUrl(media, UrlMode.Absolute); - - // this may be relevant if we can't find width and height in the attributes ... for now we seem quite able to, though - // if (currentSrc != null) - // { - // NameValueCollection queryString = HttpUtility.ParseQueryString(HttpUtility.HtmlDecode(currentSrc)); - // attributes["params"] = queryString.AllKeys.WhereNotNull().ToDictionary(key => key, key => queryString[key]); - // } - } + attributes["src"] = mediaUrl; + attributes.Remove("data-udi"); + }); } private static void SanitizeAttributes(Dictionary attributes) @@ -180,7 +134,4 @@ internal sealed partial class ApiRichTextParser : IApiRichTextParser attributes.Remove(dataAttribute.Key); } } - - [GeneratedRegex("{localLink:(?umb:.+)}")] - private static partial Regex LocalLinkRegex(); } diff --git a/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextMarkupParser.cs b/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextMarkupParser.cs new file mode 100644 index 0000000000..04344905e4 --- /dev/null +++ b/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextMarkupParser.cs @@ -0,0 +1,94 @@ +using HtmlAgilityPack; +using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core.DeliveryApi; +using Umbraco.Cms.Core.PublishedCache; +using Umbraco.Cms.Core.Routing; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Infrastructure.DeliveryApi; + +internal sealed class ApiRichTextMarkupParser : ApiRichTextParserBase, IApiRichTextMarkupParser +{ + private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; + private readonly ILogger _logger; + + public ApiRichTextMarkupParser( + IApiContentRouteBuilder apiContentRouteBuilder, + IPublishedUrlProvider publishedUrlProvider, + IPublishedSnapshotAccessor publishedSnapshotAccessor, + ILogger logger) + : base(apiContentRouteBuilder, publishedUrlProvider) + { + _publishedSnapshotAccessor = publishedSnapshotAccessor; + _logger = logger; + } + + public string Parse(string html) + { + try + { + IPublishedSnapshot publishedSnapshot = _publishedSnapshotAccessor.GetRequiredPublishedSnapshot(); + var doc = new HtmlDocument(); + doc.LoadHtml(html); + + ReplaceLocalLinks(doc, publishedSnapshot); + + ReplaceLocalImages(doc, publishedSnapshot); + + return doc.DocumentNode.InnerHtml; + } + catch (Exception ex) + { + _logger.LogError(ex, "Could not parse rich text HTML, see exception for details"); + return html; + } + } + + private void ReplaceLocalLinks(HtmlDocument doc, IPublishedSnapshot publishedSnapshot) + { + HtmlNode[] links = doc.DocumentNode.SelectNodes("//a")?.ToArray() ?? Array.Empty(); + foreach (HtmlNode link in links) + { + ReplaceLocalLinks( + publishedSnapshot, + link.GetAttributeValue("href", string.Empty), + route => + { + link.SetAttributeValue("href", route.Path); + link.SetAttributeValue("data-start-item-path", route.StartItem.Path); + link.SetAttributeValue("data-start-item-id", route.StartItem.Id.ToString("D")); + }, + url => link.SetAttributeValue("href", url), + () => link.Attributes.Remove("href")); + } + } + + private void ReplaceLocalImages(HtmlDocument doc, IPublishedSnapshot publishedSnapshot) + { + HtmlNode[] images = doc.DocumentNode.SelectNodes("//img")?.ToArray() ?? Array.Empty(); + foreach (HtmlNode image in images) + { + var dataUdi = image.GetAttributeValue("data-udi", string.Empty); + if (dataUdi.IsNullOrWhiteSpace()) + { + continue; + } + + ReplaceLocalImages(publishedSnapshot, dataUdi, mediaUrl => + { + // the image source likely contains query string parameters for image cropping; we need to + // preserve those, so let's extract the image query string (if present). + var currentImageSource = image.GetAttributeValue("src", string.Empty); + var currentImageQueryString = currentImageSource.Contains('?') + ? $"?{currentImageSource.Split('?').Last()}" + : null; + + image.SetAttributeValue("src", $"{mediaUrl}{currentImageQueryString}"); + image.Attributes.Remove("data-udi"); + + // we don't want the "data-caption" attribute, it's already part of the output as
+ image.Attributes.Remove("data-caption"); + }); + } + } +} diff --git a/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextParserBase.cs b/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextParserBase.cs new file mode 100644 index 0000000000..0509105b05 --- /dev/null +++ b/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextParserBase.cs @@ -0,0 +1,85 @@ +using System.Text.RegularExpressions; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.DeliveryApi; +using Umbraco.Cms.Core.Models.DeliveryApi; +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.PublishedCache; +using Umbraco.Cms.Core.Routing; + +namespace Umbraco.Cms.Infrastructure.DeliveryApi; + +internal abstract partial class ApiRichTextParserBase +{ + private readonly IApiContentRouteBuilder _apiContentRouteBuilder; + private readonly IPublishedUrlProvider _publishedUrlProvider; + + protected ApiRichTextParserBase(IApiContentRouteBuilder apiContentRouteBuilder, IPublishedUrlProvider publishedUrlProvider) + { + _apiContentRouteBuilder = apiContentRouteBuilder; + _publishedUrlProvider = publishedUrlProvider; + } + + protected void ReplaceLocalLinks(IPublishedSnapshot publishedSnapshot, string href, Action handleContentRoute, Action handleMediaUrl, Action handleInvalidLink) + { + Match match = LocalLinkRegex().Match(href); + if (match.Success is false) + { + return; + } + + if (UdiParser.TryParse(match.Groups["udi"].Value, out Udi? udi) is false) + { + return; + } + + bool handled = false; + switch (udi.EntityType) + { + case Constants.UdiEntityType.Document: + IPublishedContent? content = publishedSnapshot.Content?.GetById(udi); + IApiContentRoute? route = content != null + ? _apiContentRouteBuilder.Build(content) + : null; + if (route != null) + { + handled = true; + handleContentRoute(route); + } + + break; + case Constants.UdiEntityType.Media: + IPublishedContent? media = publishedSnapshot.Media?.GetById(udi); + if (media != null) + { + handled = true; + handleMediaUrl(_publishedUrlProvider.GetMediaUrl(media, UrlMode.Absolute)); + } + + break; + } + + if(handled is false) + { + handleInvalidLink(); + } + } + + protected void ReplaceLocalImages(IPublishedSnapshot publishedSnapshot, string udi, Action handleMediaUrl) + { + if (UdiParser.TryParse(udi, out Udi? udiValue) is false) + { + return; + } + + IPublishedContent? media = publishedSnapshot.Media?.GetById(udiValue); + if (media is null) + { + return; + } + + handleMediaUrl(_publishedUrlProvider.GetMediaUrl(media, UrlMode.Absolute)); + } + + [GeneratedRegex("{localLink:(?umb:.+)}")] + private static partial Regex LocalLinkRegex(); +} diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs index 73b4b9f87a..90784aec45 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs @@ -435,7 +435,8 @@ public static partial class UmbracoBuilderExtensions builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); - builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); return builder; } diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Examine.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Examine.cs index 0e7b0f5faa..fb3adb9219 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Examine.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Examine.cs @@ -39,7 +39,8 @@ public static partial class UmbracoBuilderExtensions factory.GetRequiredService(), factory.GetRequiredService(), factory.GetRequiredService(), - true)); + true, + factory.GetRequiredService())); builder.Services.AddUnique(factory => new ContentValueSetBuilder( factory.GetRequiredService(), @@ -47,7 +48,8 @@ public static partial class UmbracoBuilderExtensions factory.GetRequiredService(), factory.GetRequiredService(), factory.GetRequiredService(), - false)); + false, + factory.GetRequiredService())); builder.Services.AddUnique, MediaValueSetBuilder>(); builder.Services.AddUnique, MemberValueSetBuilder>(); builder.Services.AddUnique(); diff --git a/src/Umbraco.Infrastructure/Examine/BaseValueSetBuilder.cs b/src/Umbraco.Infrastructure/Examine/BaseValueSetBuilder.cs index db6182ee3e..3574c3077f 100644 --- a/src/Umbraco.Infrastructure/Examine/BaseValueSetBuilder.cs +++ b/src/Umbraco.Infrastructure/Examine/BaseValueSetBuilder.cs @@ -22,7 +22,11 @@ public abstract class BaseValueSetBuilder : IValueSetBuilder /// public abstract IEnumerable GetValueSets(params TContent[] content); + [Obsolete("Use the overload that specifies availableCultures, scheduled for removal in v14")] protected void AddPropertyValue(IProperty property, string? culture, string? segment, IDictionary>? values) + => AddPropertyValue(property, culture, segment, values, Enumerable.Empty()); + + protected void AddPropertyValue(IProperty property, string? culture, string? segment, IDictionary>? values, IEnumerable availableCultures) { IDataEditor? editor = _propertyEditors[property.PropertyType.PropertyEditorAlias]; if (editor == null) @@ -31,7 +35,7 @@ public abstract class BaseValueSetBuilder : IValueSetBuilder } IEnumerable>> indexVals = - editor.PropertyIndexValueFactory.GetIndexValues(property, culture, segment, PublishedValuesOnly); + editor.PropertyIndexValueFactory.GetIndexValues(property, culture, segment, PublishedValuesOnly, availableCultures); foreach (KeyValuePair> keyVal in indexVals) { if (keyVal.Key.IsNullOrWhiteSpace()) diff --git a/src/Umbraco.Infrastructure/Examine/ContentValueSetBuilder.cs b/src/Umbraco.Infrastructure/Examine/ContentValueSetBuilder.cs index 05274fc28e..228610879d 100644 --- a/src/Umbraco.Infrastructure/Examine/ContentValueSetBuilder.cs +++ b/src/Umbraco.Infrastructure/Examine/ContentValueSetBuilder.cs @@ -1,10 +1,12 @@ using Examine; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; +using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Extensions; using IScope = Umbraco.Cms.Infrastructure.Scoping.IScope; @@ -24,6 +26,7 @@ public class ContentValueSetBuilder : BaseValueSetBuilder, IContentVal private readonly IShortStringHelper _shortStringHelper; private readonly UrlSegmentProviderCollection _urlSegmentProviders; private readonly IUserService _userService; + private readonly ILocalizationService _localizationService; public ContentValueSetBuilder( PropertyEditorCollection propertyEditors, @@ -31,13 +34,34 @@ public class ContentValueSetBuilder : BaseValueSetBuilder, IContentVal IUserService userService, IShortStringHelper shortStringHelper, IScopeProvider scopeProvider, - bool publishedValuesOnly) + bool publishedValuesOnly, + ILocalizationService localizationService) : base(propertyEditors, publishedValuesOnly) { _urlSegmentProviders = urlSegmentProviders; _userService = userService; _shortStringHelper = shortStringHelper; _scopeProvider = scopeProvider; + _localizationService = localizationService; + } + + [Obsolete("Use the constructor that takes an ILocalizationService, scheduled for removal in v14")] + public ContentValueSetBuilder( + PropertyEditorCollection propertyEditors, + UrlSegmentProviderCollection urlSegmentProviders, + IUserService userService, + IShortStringHelper shortStringHelper, + IScopeProvider scopeProvider, + bool publishedValuesOnly) + : this( + propertyEditors, + urlSegmentProviders, + userService, + shortStringHelper, + scopeProvider, + publishedValuesOnly, + StaticServiceProvider.Instance.GetRequiredService()) + { } /// @@ -128,17 +152,23 @@ public class ContentValueSetBuilder : BaseValueSetBuilder, IContentVal } } + var availableCultures = new List(c.AvailableCultures); + if (availableCultures.Any() is false) + { + availableCultures.Add(_localizationService.GetDefaultLanguageIsoCode()); + } + foreach (IProperty property in c.Properties) { if (!property.PropertyType.VariesByCulture()) { - AddPropertyValue(property, null, null, values); + AddPropertyValue(property, null, null, values, availableCultures); } else { foreach (var culture in c.AvailableCultures) { - AddPropertyValue(property, culture.ToLowerInvariant(), null, values); + AddPropertyValue(property, culture.ToLowerInvariant(), null, values, availableCultures); } } } diff --git a/src/Umbraco.Infrastructure/Examine/Deferred/DeliveryApiContentIndexHandleContentChanges.cs b/src/Umbraco.Infrastructure/Examine/Deferred/DeliveryApiContentIndexHandleContentChanges.cs index c93f42b6e8..66859edd7d 100644 --- a/src/Umbraco.Infrastructure/Examine/Deferred/DeliveryApiContentIndexHandleContentChanges.cs +++ b/src/Umbraco.Infrastructure/Examine/Deferred/DeliveryApiContentIndexHandleContentChanges.cs @@ -71,56 +71,57 @@ internal sealed class DeliveryApiContentIndexHandleContentChanges : DeliveryApiC private void Reindex(IContent content, IIndex index) { // get the currently indexed cultures for the content - var existingIndexCultures = index + CulturePublishStatus[] existingCultures = index .Searcher .CreateQuery() .Field(UmbracoExamineFieldNames.DeliveryApiContentIndex.Id, content.Id.ToString()) - .SelectField(UmbracoExamineFieldNames.DeliveryApiContentIndex.Culture) + .SelectFields(new HashSet + { + UmbracoExamineFieldNames.DeliveryApiContentIndex.Culture, + UmbracoExamineFieldNames.DeliveryApiContentIndex.Published + }) .Execute() - .SelectMany(f => f.GetValues(UmbracoExamineFieldNames.DeliveryApiContentIndex.Culture)) + .Select(f => new CulturePublishStatus + { + Culture = f.GetValues(UmbracoExamineFieldNames.DeliveryApiContentIndex.Culture).Single(), + Published = f.GetValues(UmbracoExamineFieldNames.DeliveryApiContentIndex.Published).Single() + }) .ToArray(); // index the content - var indexedCultures = UpdateIndex(content, index); + CulturePublishStatus[] indexedCultures = UpdateIndex(content, index); if (indexedCultures.Any() is false) { - // we likely got here because unpublishing triggered a "refresh branch" notification, now we + // we likely got here because a removal triggered a "refresh branch" notification, now we // need to delete every last culture of this content and all descendants RemoveFromIndex(content.Id, index); return; } - // if any of the content cultures did not exist in the index before, nor will any of its published descendants - // in those cultures be at this point, so make sure those are added as well - if (indexedCultures.Except(existingIndexCultures).Any()) + // if the published state changed of any culture, chances are there are similar changes ot the content descendants + // that need to be reflected in the index, so we'll reindex all descendants + var changedCulturePublishStatus = indexedCultures.Intersect(existingCultures).Count() != existingCultures.Length; + if (changedCulturePublishStatus) { ReindexDescendants(content, index); } - - // ensure that any unpublished cultures are removed from the index - var unpublishedCultures = existingIndexCultures.Except(indexedCultures).ToArray(); - if (unpublishedCultures.Any() is false) - { - return; - } - - var idsToDelete = unpublishedCultures - .Select(culture => DeliveryApiContentIndexUtilites.IndexId(content, culture)).ToArray(); - RemoveFromIndex(idsToDelete, index); } - private string[] UpdateIndex(IContent content, IIndex index) + private CulturePublishStatus[] UpdateIndex(IContent content, IIndex index) { ValueSet[] valueSets = _deliveryApiContentIndexValueSetBuilder.GetValueSets(content).ToArray(); if (valueSets.Any() is false) { - return Array.Empty(); + return Array.Empty(); } index.IndexItems(valueSets); return valueSets - .SelectMany(v => v.GetValues("culture").Select(c => c.ToString())) - .WhereNotNull() + .Select(v => new CulturePublishStatus + { + Culture = v.GetValue(UmbracoExamineFieldNames.DeliveryApiContentIndex.Culture).ToString()!, + Published = v.GetValue(UmbracoExamineFieldNames.DeliveryApiContentIndex.Published).ToString()! + }) .ToArray(); } @@ -134,4 +135,48 @@ internal sealed class DeliveryApiContentIndexHandleContentChanges : DeliveryApiC UpdateIndex(descendant, index); } }); + + private class CulturePublishStatus : IEquatable + { + public required string Culture { get; set; } + + public required string Published { get; set; } + + public bool Equals(CulturePublishStatus? other) + { + if (ReferenceEquals(null, other)) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return Culture == other.Culture && Published == other.Published; + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((CulturePublishStatus)obj); + } + + public override int GetHashCode() => HashCode.Combine(Culture, Published); + } } diff --git a/src/Umbraco.Infrastructure/Examine/DeliveryApiContentIndexFieldDefinitionBuilder.cs b/src/Umbraco.Infrastructure/Examine/DeliveryApiContentIndexFieldDefinitionBuilder.cs index bfd11defde..f7d4024c57 100644 --- a/src/Umbraco.Infrastructure/Examine/DeliveryApiContentIndexFieldDefinitionBuilder.cs +++ b/src/Umbraco.Infrastructure/Examine/DeliveryApiContentIndexFieldDefinitionBuilder.cs @@ -35,6 +35,7 @@ internal sealed class DeliveryApiContentIndexFieldDefinitionBuilder : IDeliveryA fieldDefinitions.Add(new(UmbracoExamineFieldNames.DeliveryApiContentIndex.Id, FieldDefinitionTypes.Raw)); fieldDefinitions.Add(new(UmbracoExamineFieldNames.DeliveryApiContentIndex.ContentTypeId, FieldDefinitionTypes.Raw)); fieldDefinitions.Add(new(UmbracoExamineFieldNames.DeliveryApiContentIndex.Culture, FieldDefinitionTypes.Raw)); + fieldDefinitions.Add(new(UmbracoExamineFieldNames.DeliveryApiContentIndex.Published, FieldDefinitionTypes.Raw)); fieldDefinitions.Add(new(UmbracoExamineFieldNames.IndexPathFieldName, FieldDefinitionTypes.Raw)); fieldDefinitions.Add(new(UmbracoExamineFieldNames.NodeNameFieldName, FieldDefinitionTypes.Raw)); } @@ -68,7 +69,7 @@ internal sealed class DeliveryApiContentIndexFieldDefinitionBuilder : IDeliveryA FieldType.Number => FieldDefinitionTypes.Integer, FieldType.StringRaw => FieldDefinitionTypes.Raw, FieldType.StringAnalyzed => FieldDefinitionTypes.FullText, - FieldType.StringSortable => FieldDefinitionTypes.FullTextSortable, + FieldType.StringSortable => FieldDefinitionTypes.InvariantCultureIgnoreCase, _ => throw new ArgumentOutOfRangeException(nameof(field.FieldType)) }; diff --git a/src/Umbraco.Infrastructure/Examine/DeliveryApiContentIndexHelper.cs b/src/Umbraco.Infrastructure/Examine/DeliveryApiContentIndexHelper.cs index 34897bd7af..744502b3de 100644 --- a/src/Umbraco.Infrastructure/Examine/DeliveryApiContentIndexHelper.cs +++ b/src/Umbraco.Infrastructure/Examine/DeliveryApiContentIndexHelper.cs @@ -29,38 +29,17 @@ internal sealed class DeliveryApiContentIndexHelper : IDeliveryApiContentIndexHe { const int pageSize = 10000; var pageIndex = 0; - var publishedContentIds = new HashSet { rootContentId }; IContent[] descendants; - IQuery publishedQuery = _umbracoDatabaseFactory.SqlContext.Query().Where(x => x.Published && x.Trashed == false); + IQuery query = _umbracoDatabaseFactory.SqlContext.Query().Where(content => content.Trashed == false); do { - descendants = _contentService.GetPagedDescendants(rootContentId, pageIndex, pageSize, out _, publishedQuery, Ordering.By("Path")).ToArray(); + descendants = _contentService + .GetPagedDescendants(rootContentId, pageIndex, pageSize, out _, query, Ordering.By("Path")) + .Where(descendant => _deliveryApiSettings.IsAllowedContentType(descendant.ContentType.Alias)) + .ToArray(); - // there are a few rules we need to abide to when populating the index: - // - children of unpublished content can still be published; we need to filter them out, as they're not supposed to go into the index. - // - content of disallowed content types are not allowed in the index, but their children are - // as we're querying published content and ordering by path, we can construct a list of "allowed" published content IDs like this. - var allowedDescendants = new List(); - foreach (IContent descendant in descendants) - { - if (_deliveryApiSettings.IsDisallowedContentType(descendant.ContentType.Alias)) - { - // the content type is disallowed; make sure we consider all its children as candidates for the index anyway - publishedContentIds.Add(descendant.Id); - continue; - } - - // content at root level is by definition published, because we only fetch published content in the query above. - // content not at root level should be included only if their parents are included (unbroken chain of published content) - if (descendant.Level == 1 || publishedContentIds.Contains(descendant.ParentId)) - { - publishedContentIds.Add(descendant.Id); - allowedDescendants.Add(descendant); - } - } - - actionToPerform(allowedDescendants.ToArray()); + actionToPerform(descendants.ToArray()); pageIndex++; } diff --git a/src/Umbraco.Infrastructure/Examine/DeliveryApiContentIndexValueSetBuilder.cs b/src/Umbraco.Infrastructure/Examine/DeliveryApiContentIndexValueSetBuilder.cs index 20942336ab..4d95dc1cde 100644 --- a/src/Umbraco.Infrastructure/Examine/DeliveryApiContentIndexValueSetBuilder.cs +++ b/src/Umbraco.Infrastructure/Examine/DeliveryApiContentIndexValueSetBuilder.cs @@ -37,11 +37,13 @@ internal sealed class DeliveryApiContentIndexValueSetBuilder : IDeliveryApiConte { foreach (IContent content in contents.Where(CanIndex)) { - var cultures = IndexableCultures(content); + var publishedCultures = PublishedCultures(content); + var availableCultures = AvailableCultures(content); - foreach (var culture in cultures) + foreach (var culture in availableCultures) { var indexCulture = culture ?? "none"; + var isPublished = publishedCultures.Contains(culture); // required index values go here var indexValues = new Dictionary>(StringComparer.InvariantCultureIgnoreCase) @@ -49,8 +51,9 @@ internal sealed class DeliveryApiContentIndexValueSetBuilder : IDeliveryApiConte [UmbracoExamineFieldNames.DeliveryApiContentIndex.Id] = new object[] { content.Id.ToString() }, // required for correct publishing handling and also needed for backoffice index browsing [UmbracoExamineFieldNames.DeliveryApiContentIndex.ContentTypeId] = new object[] { content.ContentTypeId.ToString() }, // required for correct content type change handling [UmbracoExamineFieldNames.DeliveryApiContentIndex.Culture] = new object[] { indexCulture }, // required for culture variant querying + [UmbracoExamineFieldNames.DeliveryApiContentIndex.Published] = new object[] { isPublished ? "y" : "n" }, // required for querying draft content [UmbracoExamineFieldNames.IndexPathFieldName] = new object[] { content.Path }, // required for unpublishing/deletion handling - [UmbracoExamineFieldNames.NodeNameFieldName] = new object[] { content.GetPublishName(culture) ?? string.Empty }, // primarily needed for backoffice index browsing + [UmbracoExamineFieldNames.NodeNameFieldName] = new object[] { content.GetPublishName(culture) ?? content.GetCultureName(culture) ?? string.Empty }, // primarily needed for backoffice index browsing }; AddContentIndexHandlerFields(content, culture, indexValues); @@ -60,8 +63,18 @@ internal sealed class DeliveryApiContentIndexValueSetBuilder : IDeliveryApiConte } } - private string?[] IndexableCultures(IContent content) + private string?[] AvailableCultures(IContent content) + => content.ContentType.VariesByCulture() + ? content.AvailableCultures.ToArray() + : new string?[] { null }; + + private string?[] PublishedCultures(IContent content) { + if (content.Published == false) + { + return Array.Empty(); + } + var variesByCulture = content.ContentType.VariesByCulture(); // if the content varies by culture, the indexable cultures are the published @@ -116,7 +129,7 @@ internal sealed class DeliveryApiContentIndexValueSetBuilder : IDeliveryApiConte private bool CanIndex(IContent content) { // is the content in a state that is allowed in the index? - if (content.Published is false || content.Trashed) + if (content.Trashed) { return false; } diff --git a/src/Umbraco.Infrastructure/Examine/MediaValueSetBuilder.cs b/src/Umbraco.Infrastructure/Examine/MediaValueSetBuilder.cs index 344c7d08d2..fa7d6509cd 100644 --- a/src/Umbraco.Infrastructure/Examine/MediaValueSetBuilder.cs +++ b/src/Umbraco.Infrastructure/Examine/MediaValueSetBuilder.cs @@ -65,7 +65,7 @@ public class MediaValueSetBuilder : BaseValueSetBuilder foreach (IProperty property in m.Properties) { - AddPropertyValue(property, null, null, values); + AddPropertyValue(property, null, null, values, m.AvailableCultures); } var vs = new ValueSet(m.Id.ToInvariantString(), IndexTypes.Media, m.ContentType.Alias, values); diff --git a/src/Umbraco.Infrastructure/Examine/MemberValueSetBuilder.cs b/src/Umbraco.Infrastructure/Examine/MemberValueSetBuilder.cs index 74d3829d6a..1b0bf7219f 100644 --- a/src/Umbraco.Infrastructure/Examine/MemberValueSetBuilder.cs +++ b/src/Umbraco.Infrastructure/Examine/MemberValueSetBuilder.cs @@ -37,7 +37,7 @@ public class MemberValueSetBuilder : BaseValueSetBuilder foreach (IProperty property in m.Properties) { - AddPropertyValue(property, null, null, values); + AddPropertyValue(property, null, null, values, m.AvailableCultures); } var vs = new ValueSet(m.Id.ToInvariantString(), IndexTypes.Member, m.ContentType.Alias, values); diff --git a/src/Umbraco.Infrastructure/Examine/UmbracoExamineFieldNames.cs b/src/Umbraco.Infrastructure/Examine/UmbracoExamineFieldNames.cs index 12b2eb2207..5376e91897 100644 --- a/src/Umbraco.Infrastructure/Examine/UmbracoExamineFieldNames.cs +++ b/src/Umbraco.Infrastructure/Examine/UmbracoExamineFieldNames.cs @@ -45,5 +45,10 @@ public static class UmbracoExamineFieldNames /// The content culture ///
public const string Culture = "culture"; + + /// + /// Whether or not the content exists in a published state + /// + public const string Published = "published"; } } diff --git a/src/Umbraco.Infrastructure/Manifest/ManifestParser.cs b/src/Umbraco.Infrastructure/Manifest/ManifestParser.cs index 887ac05dc4..18da814b59 100644 --- a/src/Umbraco.Infrastructure/Manifest/ManifestParser.cs +++ b/src/Umbraco.Infrastructure/Manifest/ManifestParser.cs @@ -1,3 +1,6 @@ +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using System.Runtime.Loader; using System.Text; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.FileProviders; @@ -17,7 +20,7 @@ using Umbraco.Extensions; namespace Umbraco.Cms.Core.Manifest; /// -/// Parses the Main.js file and replaces all tokens accordingly. +/// Parses the Main.js file and replaces all tokens accordingly. /// public class ManifestParser : IManifestParser { @@ -39,7 +42,7 @@ public class ManifestParser : IManifestParser private string _path = null!; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// public ManifestParser( AppCaches appCaches, @@ -163,31 +166,44 @@ public class ManifestParser : IManifestParser /// public PackageManifest ParseManifest(string text) { - if (text == null) - { - throw new ArgumentNullException(nameof(text)); - } + ArgumentNullException.ThrowIfNull(text); if (string.IsNullOrWhiteSpace(text)) { throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(text)); } - PackageManifest? manifest = JsonConvert.DeserializeObject( + PackageManifest manifest = JsonConvert.DeserializeObject( text, new DataEditorConverter(_dataValueEditorFactory, _ioHelper, _localizedTextService, _shortStringHelper, _jsonSerializer), new ValueValidatorConverter(_validators), - new DashboardAccessRuleConverter()); + new DashboardAccessRuleConverter())!; + + if (string.IsNullOrEmpty(manifest.Version)) + { + string? assemblyName = manifest.VersionAssemblyName; + if (string.IsNullOrEmpty(assemblyName)) + { + // Fallback to package ID + assemblyName = manifest.PackageId; + } + + if (!string.IsNullOrEmpty(assemblyName) && + TryGetAssemblyInformationalVersion(assemblyName, out string? version)) + { + manifest.Version = version; + } + } // scripts and stylesheets are raw string, must process here - for (var i = 0; i < manifest!.Scripts.Length; i++) + for (var i = 0; i < manifest.Scripts.Length; i++) { - manifest.Scripts[i] = _ioHelper.ResolveRelativeOrVirtualUrl(manifest.Scripts[i])!; + manifest.Scripts[i] = _ioHelper.ResolveRelativeOrVirtualUrl(manifest.Scripts[i]); } for (var i = 0; i < manifest.Stylesheets.Length; i++) { - manifest.Stylesheets[i] = _ioHelper.ResolveRelativeOrVirtualUrl(manifest.Stylesheets[i])!; + manifest.Stylesheets[i] = _ioHelper.ResolveRelativeOrVirtualUrl(manifest.Stylesheets[i]); } foreach (ManifestContentAppDefinition contentApp in manifest.ContentApps) @@ -197,7 +213,7 @@ public class ManifestParser : IManifestParser foreach (ManifestDashboard dashboard in manifest.Dashboards) { - dashboard.View = _ioHelper.ResolveRelativeOrVirtualUrl(dashboard.View)!; + dashboard.View = _ioHelper.ResolveRelativeOrVirtualUrl(dashboard.View); } foreach (GridEditor gridEditor in manifest.GridEditors) @@ -217,6 +233,22 @@ public class ManifestParser : IManifestParser return manifest; } + private bool TryGetAssemblyInformationalVersion(string name, [NotNullWhen(true)] out string? version) + { + foreach (Assembly assembly in AssemblyLoadContext.Default.Assemblies) + { + AssemblyName assemblyName = assembly.GetName(); + if (string.Equals(assemblyName.Name, name, StringComparison.OrdinalIgnoreCase) && + assembly.TryGetInformationalVersion(out version)) + { + return true; + } + } + + version = null; + return false; + } + /// /// Merges all manifests into one. /// diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_12_0_0/ResetCache.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_12_0_0/ResetCache.cs index a3ec96ad7e..b55b4c4ca7 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_12_0_0/ResetCache.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_12_0_0/ResetCache.cs @@ -1,21 +1,20 @@ -using Microsoft.Extensions.Hosting; -using Umbraco.Cms.Core; -using Umbraco.Cms.Core.Extensions; +using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment; namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_12_0_0; public class ResetCache : MigrationBase { - private readonly IHostEnvironment _hostEnvironment; + private readonly IHostingEnvironment _hostingEnvironment; - public ResetCache(IMigrationContext context, IHostEnvironment hostEnvironment) - : base(context) => _hostEnvironment = hostEnvironment; + public ResetCache(IMigrationContext context, IHostingEnvironment hostingEnvironment) + : base(context) => + _hostingEnvironment = hostingEnvironment; protected override void Migrate() { RebuildCache = true; - var distCacheFolderAbsolutePath = _hostEnvironment.MapPathContentRoot(Constants.SystemDirectories.TempData + "/DistCache"); - var nuCacheFolderAbsolutePath = _hostEnvironment.MapPathContentRoot(Constants.SystemDirectories.TempData + "/NuCache"); + var distCacheFolderAbsolutePath = Path.Combine(_hostingEnvironment.LocalTempPath, "DistCache"); + var nuCacheFolderAbsolutePath = Path.Combine(_hostingEnvironment.LocalTempPath, "NuCache"); DeleteAllFilesInFolder(distCacheFolderAbsolutePath); DeleteAllFilesInFolder(nuCacheFolderAbsolutePath); } diff --git a/src/Umbraco.Infrastructure/Packaging/AutomaticPackageMigrationPlan.cs b/src/Umbraco.Infrastructure/Packaging/AutomaticPackageMigrationPlan.cs index 3e3c2cfae1..d8becf0bfa 100644 --- a/src/Umbraco.Infrastructure/Packaging/AutomaticPackageMigrationPlan.cs +++ b/src/Umbraco.Infrastructure/Packaging/AutomaticPackageMigrationPlan.cs @@ -11,20 +11,41 @@ using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Packaging; /// -/// Used to automatically indicate that a package has an embedded package data manifest that needs to be installed +/// Represents a package migration plan that automatically imports an embedded package data manifest. /// public abstract class AutomaticPackageMigrationPlan : PackageMigrationPlan { + /// + /// Initializes a new instance of the class. + /// + /// The package name that the plan is for. If the package has a package.manifest these must match. protected AutomaticPackageMigrationPlan(string packageName) : this(packageName, packageName) { } + /// + /// Initializes a new instance of the class. + /// + /// The package name that the plan is for. If the package has a package.manifest these must match. + /// The plan name for the package. This should be the same name as the package name, if there is only one plan in the package. protected AutomaticPackageMigrationPlan(string packageName, string planName) - : base(packageName, planName) + : this(null!, packageName, planName) { } + /// + /// Initializes a new instance of the class. + /// + /// The package identifier that the plan is for. If the package has a package.manifest these must match. + /// The package name that the plan is for. If the package has a package.manifest these must match. + /// The plan name for the package. This should be the same name as the package name, if there is only one plan in the package. + protected AutomaticPackageMigrationPlan(string packageId, string packageName, string planName) + : base(packageId, packageName, planName) + { + } + + /// protected sealed override void DefinePlan() { // calculate the final state based on the hash value of the embedded resource @@ -35,8 +56,22 @@ public abstract class AutomaticPackageMigrationPlan : PackageMigrationPlan To(finalId); } + /// + /// Provides a migration that imports an embedded package data manifest. + /// private class MigrateToPackageData : PackageMigrationBase { + /// + /// Initializes a new instance of the class. + /// + /// The packaging service. + /// The media service. + /// The media file manager. + /// The media URL generators. + /// The short string helper. + /// The content type base service provider. + /// The migration context. + /// The package migration settings. public MigrateToPackageData( IPackagingService packagingService, IMediaService mediaService, @@ -44,11 +79,13 @@ public abstract class AutomaticPackageMigrationPlan : PackageMigrationPlan MediaUrlGeneratorCollection mediaUrlGenerators, IShortStringHelper shortStringHelper, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, - IMigrationContext context, IOptions options) + IMigrationContext context, + IOptions options) : base(packagingService, mediaService, mediaFileManager, mediaUrlGenerators, shortStringHelper, contentTypeBaseServiceProvider, context, options) { } + /// protected override void Migrate() { var plan = (AutomaticPackageMigrationPlan)Context.Plan; diff --git a/src/Umbraco.Infrastructure/Packaging/PackageMigrationPlan.cs b/src/Umbraco.Infrastructure/Packaging/PackageMigrationPlan.cs index bdbce82fcb..be7caa1aad 100644 --- a/src/Umbraco.Infrastructure/Packaging/PackageMigrationPlan.cs +++ b/src/Umbraco.Infrastructure/Packaging/PackageMigrationPlan.cs @@ -4,50 +4,70 @@ using Umbraco.Cms.Infrastructure.Migrations; namespace Umbraco.Cms.Core.Packaging; /// -/// Base class for package migration plans +/// Represents a package migration plan. /// public abstract class PackageMigrationPlan : MigrationPlan, IDiscoverable { /// - /// Creates a package migration plan + /// Initializes a new instance of the class. /// - /// The name of the package. If the package has a package.manifest these must match. + /// The package name that the plan is for. If the package has a package.manifest these must match. protected PackageMigrationPlan(string packageName) : this(packageName, packageName) { } /// - /// Create a plan for a Package Name + /// Initializes a new instance of the class. /// - /// - /// The package name that the plan is for. If the package has a package.manifest these must - /// match. - /// - /// - /// The plan name for the package. This should be the same name as the - /// package name if there is only one plan in the package. - /// + /// The package name that the plan is for. If the package has a package.manifest these must match. + /// The plan name for the package. This should be the same name as the package name, if there is only one plan in the package. protected PackageMigrationPlan(string packageName, string planName) - : base(planName) + : this(null!, packageName, planName) { - // A call to From must be done first - From(string.Empty); - - DefinePlan(); - PackageName = packageName; } /// - /// Inform the plan executor to ignore all saved package state and - /// run the migration from initial state to it's end state. + /// Initializes a new instance of the class. + /// + /// The package identifier that the plan is for. If the package has a package.manifest these must match. + /// The package name that the plan is for. If the package has a package.manifest these must match. + /// The plan name for the package. This should be the same name as the package name, if there is only one plan in the package. + protected PackageMigrationPlan(string packageId, string packageName, string planName) + : base(planName) + { + PackageId = packageId; + PackageName = packageName; + + // A call to From must be done first + From(string.Empty); + DefinePlan(); + } + + /// + /// Inform the plan executor to ignore all saved package state and + /// run the migration from initial state to it's end state. /// public override bool IgnoreCurrentState => true; /// - /// Returns the Package Name for this plan + /// Gets the package identifier. /// - public string PackageName { get; } + /// + /// The package identifier. + /// + public string? PackageId { get; init; } + /// + /// Gets the package name. + /// + /// + /// The package name. + /// + public string PackageName { get; init; } + + /// + /// Defines the plan. + /// protected abstract void DefinePlan(); } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CreatedPackageSchemaRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CreatedPackageSchemaRepository.cs index 9f921266ca..61f0fe126d 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CreatedPackageSchemaRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CreatedPackageSchemaRepository.cs @@ -2,16 +2,19 @@ using System.ComponentModel.DataAnnotations; using System.Globalization; using System.IO.Compression; using System.Xml.Linq; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using NPoco; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Packaging; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; using File = System.IO.File; @@ -22,6 +25,7 @@ public class CreatedPackageSchemaRepository : ICreatedPackagesRepository { private readonly IContentService _contentService; private readonly IContentTypeService _contentTypeService; + private readonly IScopeAccessor _scopeAccessor; private readonly string _createdPackagesFolderPath; private readonly IDataTypeService _dataTypeService; private readonly IFileService _fileService; @@ -34,7 +38,6 @@ public class CreatedPackageSchemaRepository : ICreatedPackagesRepository private readonly IMediaTypeService _mediaTypeService; private readonly IEntityXmlSerializer _serializer; private readonly string _tempFolderPath; - private readonly IUmbracoDatabase? _umbracoDatabase; private readonly PackageDefinitionXmlParser _xmlParser; /// @@ -55,10 +58,10 @@ public class CreatedPackageSchemaRepository : ICreatedPackagesRepository MediaFileManager mediaFileManager, IMacroService macroService, IContentTypeService contentTypeService, + IScopeAccessor scopeAccessor, string? mediaFolderPath = null, string? tempFolderPath = null) { - _umbracoDatabase = umbracoDatabaseFactory.CreateDatabase(); _hostingEnvironment = hostingEnvironment; _fileSystems = fileSystems; _serializer = serializer; @@ -71,21 +74,63 @@ public class CreatedPackageSchemaRepository : ICreatedPackagesRepository _mediaFileManager = mediaFileManager; _macroService = macroService; _contentTypeService = contentTypeService; + _scopeAccessor = scopeAccessor; _xmlParser = new PackageDefinitionXmlParser(); _createdPackagesFolderPath = mediaFolderPath ?? Constants.SystemDirectories.CreatedPackages; _tempFolderPath = tempFolderPath ?? Constants.SystemDirectories.TempData + "/PackageFiles"; } + [Obsolete("use ctor with all dependencies instead")] + public CreatedPackageSchemaRepository( + IUmbracoDatabaseFactory umbracoDatabaseFactory, + IHostingEnvironment hostingEnvironment, + IOptions globalSettings, + FileSystems fileSystems, + IEntityXmlSerializer serializer, + IDataTypeService dataTypeService, + ILocalizationService localizationService, + IFileService fileService, + IMediaService mediaService, + IMediaTypeService mediaTypeService, + IContentService contentService, + MediaFileManager mediaFileManager, + IMacroService macroService, + IContentTypeService contentTypeService, + string? mediaFolderPath = null, + string? tempFolderPath = null) + : this( + umbracoDatabaseFactory, + hostingEnvironment, + globalSettings, + fileSystems, + serializer, + dataTypeService, + localizationService, + fileService, + mediaService, + mediaTypeService, + contentService, + mediaFileManager, + macroService, + contentTypeService, + StaticServiceProvider.Instance.GetRequiredService(), + mediaFolderPath, + tempFolderPath) + { + } + + private IUmbracoDatabase Database => _scopeAccessor.AmbientScope?.Database ?? throw new InvalidOperationException("A scope is required to query the database"); + public IEnumerable GetAll() { - Sql query = new Sql(_umbracoDatabase!.SqlContext) + Sql query = new Sql(Database.SqlContext) .Select() .From() .OrderBy(x => x.Id); var packageDefinitions = new List(); - List xmlSchemas = _umbracoDatabase.Fetch(query); + List xmlSchemas = Database.Fetch(query); foreach (CreatedPackageSchemaDto packageSchema in xmlSchemas) { var packageDefinition = _xmlParser.ToPackageDefinition(XElement.Parse(packageSchema.Value)); @@ -103,11 +148,11 @@ public class CreatedPackageSchemaRepository : ICreatedPackagesRepository public PackageDefinition? GetById(int id) { - Sql query = new Sql(_umbracoDatabase!.SqlContext) + Sql query = new Sql(Database.SqlContext) .Select() .From() .Where(x => x.Id == id); - List schemaDtos = _umbracoDatabase.Fetch(query); + List schemaDtos = Database.Fetch(query); if (schemaDtos.IsCollectionEmpty()) { @@ -135,11 +180,11 @@ public class CreatedPackageSchemaRepository : ICreatedPackagesRepository File.Delete(packageDef.PackagePath); } - Sql query = new Sql(_umbracoDatabase!.SqlContext) + Sql query = new Sql(Database.SqlContext) .Delete() .Where(x => x.Id == id); - _umbracoDatabase.Execute(query); + Database.Execute(query); } public bool SavePackage(PackageDefinition? definition) @@ -169,7 +214,7 @@ public class CreatedPackageSchemaRepository : ICreatedPackagesRepository }; // Set the ids, we have to save in database first to get the Id - _umbracoDatabase!.Insert(dto); + Database!.Insert(dto); definition.Id = dto.Id; } @@ -185,7 +230,7 @@ public class CreatedPackageSchemaRepository : ICreatedPackagesRepository PackageId = definition.PackageId, UpdateDate = DateTime.Now, }; - _umbracoDatabase?.Update(updatedDto); + Database?.Update(updatedDto); return true; } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ExternalLoginRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ExternalLoginRepository.cs index e49e2ffda9..352b1dd3fd 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ExternalLoginRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ExternalLoginRepository.cs @@ -96,6 +96,9 @@ internal class ExternalLoginRepository : EntityRepositoryBase 0) { + // Before we can remove the external login, we must remove the external login tokens associated with that external login, + // otherwise we'll get foreign key constraint errors + Database.DeleteMany().Where(x => toDelete.Contains(x.ExternalLoginId)).Execute(); Database.DeleteMany().Where(x => toDelete.Contains(x.Id)).Execute(); } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/GridPropertyIndexValueFactory.cs b/src/Umbraco.Infrastructure/PropertyEditors/GridPropertyIndexValueFactory.cs index e0a13d2e9d..cc3c912f4a 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/GridPropertyIndexValueFactory.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/GridPropertyIndexValueFactory.cs @@ -17,7 +17,7 @@ namespace Umbraco.Cms.Core.PropertyEditors [Obsolete("The grid is obsolete, will be removed in V13")] public class GridPropertyIndexValueFactory : IPropertyIndexValueFactory { - public IEnumerable>> GetIndexValues(IProperty property, string? culture, string? segment, bool published) + public IEnumerable>> GetIndexValues(IProperty property, string? culture, string? segment, bool published, IEnumerable availableCultures) { var result = new List>>(); @@ -89,5 +89,9 @@ namespace Umbraco.Cms.Core.PropertyEditors return result; } + + [Obsolete("Use the overload that specifies availableCultures, scheduled for removal in v14")] + public IEnumerable>> GetIndexValues(IProperty property, string? culture, string? segment, bool published) + => GetIndexValues(property, culture, segment, published); } } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/NestedPropertyIndexValueFactoryBase.cs b/src/Umbraco.Infrastructure/PropertyEditors/NestedPropertyIndexValueFactoryBase.cs index 4eb7051745..b3799aaa95 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/NestedPropertyIndexValueFactoryBase.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/NestedPropertyIndexValueFactoryBase.cs @@ -18,15 +18,26 @@ internal abstract class NestedPropertyIndexValueFactoryBase _propertyEditorCollection = propertyEditorCollection; } + [Obsolete("Use the overload that specifies availableCultures, scheduled for removal in v14")] protected override IEnumerable>> Handle( TSerialized deserializedPropertyValue, IProperty property, string? culture, string? segment, - bool published) + bool published) => + Handle(deserializedPropertyValue, property, culture, segment, published, Enumerable.Empty()); + + protected override IEnumerable>> Handle( + TSerialized deserializedPropertyValue, + IProperty property, + string? culture, + string? segment, + bool published, + IEnumerable availableCultures) { var result = new List>>(); + var index = 0; foreach (TItem nestedContentRowValue in GetDataItems(deserializedPropertyValue)) { IContentType? contentType = GetContentTypeOfNestedItem(nestedContentRowValue); @@ -60,12 +71,15 @@ internal abstract class NestedPropertyIndexValueFactoryBase .ToDictionary(x => x.Alias); result.AddRange(GetNestedResults( - property.Alias, + $"{property.Alias}.items[{index}]", culture, segment, published, propertyTypeDictionary, - nestedContentRowValue)); + nestedContentRowValue, + availableCultures)); + + index++; } return RenameKeysToEnsureRawSegmentsIsAPrefix(result); @@ -160,39 +174,51 @@ internal abstract class NestedPropertyIndexValueFactoryBase string? segment, bool published, IDictionary propertyTypeDictionary, - TItem nestedContentRowValue) + TItem nestedContentRowValue, + IEnumerable availableCultures) { - var blockIndex = 0; - foreach ((var propertyAlias, var propertyValue) in GetRawProperty(nestedContentRowValue)) { if (propertyTypeDictionary.TryGetValue(propertyAlias, out IPropertyType? propertyType)) { - IProperty subProperty = new Property(propertyType); - subProperty.SetValue(propertyValue, culture, segment); - - if (published) - { - subProperty.PublishValues(culture, segment ?? "*"); - } - IDataEditor? editor = _propertyEditorCollection[propertyType.PropertyEditorAlias]; if (editor is null) { continue; } - IEnumerable>> indexValues = - editor.PropertyIndexValueFactory.GetIndexValues(subProperty, culture, segment, published); + IProperty subProperty = new Property(propertyType); + IEnumerable>> indexValues = null!; + + if (propertyType.VariesByCulture() && culture is null) + { + foreach (var availableCulture in availableCultures) + { + subProperty.SetValue(propertyValue, availableCulture, segment); + if (published) + { + subProperty.PublishValues(availableCulture, segment ?? "*"); + } + indexValues = + editor.PropertyIndexValueFactory.GetIndexValues(subProperty, availableCulture, segment, published, availableCultures); + } + } + else + { + subProperty.SetValue(propertyValue, culture, segment); + if (published) + { + subProperty.PublishValues(culture ?? "*", segment ?? "*"); + } + indexValues = editor.PropertyIndexValueFactory.GetIndexValues(subProperty, culture, segment, published, availableCultures); + } foreach ((var nestedAlias, IEnumerable nestedValue) in indexValues) { yield return new KeyValuePair>( - $"{keyPrefix}.items[{blockIndex}].{nestedAlias}", nestedValue!); + $"{keyPrefix}.{nestedAlias}", nestedValue!); } } - - blockIndex++; } } } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs index 3e0bf324a6..85811a4c5e 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs @@ -305,7 +305,7 @@ public class RichTextPropertyEditor : DataEditor internal class RichTextPropertyIndexValueFactory : IPropertyIndexValueFactory { - public IEnumerable>> GetIndexValues(IProperty property, string? culture, string? segment, bool published) + public IEnumerable>> GetIndexValues(IProperty property, string? culture, string? segment, bool published, IEnumerable availableCultures) { var val = property.GetValue(culture, segment, published); @@ -323,5 +323,9 @@ public class RichTextPropertyEditor : DataEditor yield return new KeyValuePair>( $"{UmbracoExamineFieldNames.RawFieldPrefix}{property.Alias}", new object[] { strVal }); } + + [Obsolete("Use the overload with the 'availableCultures' parameter instead, scheduled for removal in v14")] + public IEnumerable>> GetIndexValues(IProperty property, string? culture, string? segment, bool published) + => GetIndexValues(property, culture, segment, published); } } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockGridPropertyValueConverter.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockGridPropertyValueConverter.cs index 02c12d809c..d7748f7e98 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockGridPropertyValueConverter.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockGridPropertyValueConverter.cs @@ -57,7 +57,7 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters public Type GetDeliveryApiPropertyValueType(IPublishedPropertyType propertyType) => typeof(ApiBlockGridModel); - public object? ConvertIntermediateToDeliveryApiObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) + public object? ConvertIntermediateToDeliveryApiObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview, bool expanding) { const int defaultColumns = 12; diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs index 891e6a8e4d..334b1f1aa3 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs @@ -116,7 +116,7 @@ public class BlockListPropertyValueConverter : BlockPropertyValueConverterBase typeof(IEnumerable); /// - public object? ConvertIntermediateToDeliveryApiObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) + public object? ConvertIntermediateToDeliveryApiObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview, bool expanding) { BlockListModel? model = ConvertIntermediateToBlockListModel(owner, propertyType, referenceCacheLevel, inter, preview); diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/ImageCropperValueConverter.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/ImageCropperValueConverter.cs index a80cea3394..37a4b406fc 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/ImageCropperValueConverter.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/ImageCropperValueConverter.cs @@ -72,7 +72,7 @@ public class ImageCropperValueConverter : PropertyValueConverterBase, IDeliveryA public Type GetDeliveryApiPropertyValueType(IPublishedPropertyType propertyType) => typeof(ApiImageCropperValue); - public object? ConvertIntermediateToDeliveryApiObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) + public object? ConvertIntermediateToDeliveryApiObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview, bool expanding) => inter is ImageCropperValue {Src: { }} imageCropperValue ? new ApiImageCropperValue(imageCropperValue.Src, imageCropperValue.FocalPoint, imageCropperValue.Crops) : null; diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/MarkdownEditorValueConverter.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/MarkdownEditorValueConverter.cs index ff1554563a..3efbce8fb9 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/MarkdownEditorValueConverter.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/MarkdownEditorValueConverter.cs @@ -65,7 +65,7 @@ public class MarkdownEditorValueConverter : PropertyValueConverterBase, IDeliver public Type GetDeliveryApiPropertyValueType(IPublishedPropertyType propertyType) => typeof(string); - public object ConvertIntermediateToDeliveryApiObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) + public object ConvertIntermediateToDeliveryApiObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview, bool expanding) { if (inter is not string markdownString || markdownString.IsNullOrWhiteSpace()) { diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/MediaPickerWithCropsValueConverter.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/MediaPickerWithCropsValueConverter.cs index 19f9b5a908..58020c5554 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/MediaPickerWithCropsValueConverter.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/MediaPickerWithCropsValueConverter.cs @@ -124,7 +124,7 @@ public class MediaPickerWithCropsValueConverter : PropertyValueConverterBase, ID public Type GetDeliveryApiPropertyValueType(IPublishedPropertyType propertyType) => typeof(IEnumerable); - public object? ConvertIntermediateToDeliveryApiObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) + public object? ConvertIntermediateToDeliveryApiObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview, bool expanding) { var isMultiple = IsMultipleDataType(propertyType.DataType); diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/MultiUrlPickerValueConverter.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/MultiUrlPickerValueConverter.cs index a40571637e..18891003bc 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/MultiUrlPickerValueConverter.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/MultiUrlPickerValueConverter.cs @@ -154,7 +154,7 @@ public class MultiUrlPickerValueConverter : PropertyValueConverterBase, IDeliver public Type GetDeliveryApiPropertyValueType(IPublishedPropertyType propertyType) => typeof(IEnumerable); - public object? ConvertIntermediateToDeliveryApiObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) + public object? ConvertIntermediateToDeliveryApiObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview, bool expanding) { IEnumerable DefaultValue() => Array.Empty(); diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/NestedContentManyValueConverter.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/NestedContentManyValueConverter.cs index cc0a04a9f4..4b6ab30ded 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/NestedContentManyValueConverter.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/NestedContentManyValueConverter.cs @@ -119,7 +119,7 @@ public class NestedContentManyValueConverter : NestedContentValueConverterBase, public Type GetDeliveryApiPropertyValueType(IPublishedPropertyType propertyType) => typeof(IEnumerable); - public object? ConvertIntermediateToDeliveryApiObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) + public object? ConvertIntermediateToDeliveryApiObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview, bool expanding) { var converted = ConvertIntermediateToObject(owner, propertyType, referenceCacheLevel, inter, preview); if (converted is not IEnumerable elements) diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/NestedContentSingleValueConverter.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/NestedContentSingleValueConverter.cs index 4beefb3318..03ff098cbf 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/NestedContentSingleValueConverter.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/NestedContentSingleValueConverter.cs @@ -103,7 +103,7 @@ public class NestedContentSingleValueConverter : NestedContentValueConverterBase public Type GetDeliveryApiPropertyValueType(IPublishedPropertyType propertyType) => typeof(IEnumerable); - public object? ConvertIntermediateToDeliveryApiObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) + public object? ConvertIntermediateToDeliveryApiObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview, bool expanding) { var converted = ConvertIntermediateToObject(owner, propertyType, referenceCacheLevel, inter, preview); if (converted is not IPublishedElement element) diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/RteMacroRenderingValueConverter.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/RteMacroRenderingValueConverter.cs index 9d8010ab5f..5af2520cfc 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/RteMacroRenderingValueConverter.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/RteMacroRenderingValueConverter.cs @@ -33,7 +33,8 @@ public class RteMacroRenderingValueConverter : SimpleTinyMceValueConverter, IDel private readonly IMacroRenderer _macroRenderer; private readonly IUmbracoContextAccessor _umbracoContextAccessor; private readonly HtmlUrlParser _urlParser; - private readonly IApiRichTextParser _apiRichTextParser; + private readonly IApiRichTextElementParser _apiRichTextElementParser; + private readonly IApiRichTextMarkupParser _apiRichTextMarkupParser; private DeliveryApiSettings _deliveryApiSettings; [Obsolete("Please use the constructor that takes all arguments. Will be removed in V14.")] @@ -45,20 +46,23 @@ public class RteMacroRenderingValueConverter : SimpleTinyMceValueConverter, IDel linkParser, urlParser, imageSourceParser, - StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService(), StaticServiceProvider.Instance.GetRequiredService>()) { } public RteMacroRenderingValueConverter(IUmbracoContextAccessor umbracoContextAccessor, IMacroRenderer macroRenderer, - HtmlLocalLinkParser linkParser, HtmlUrlParser urlParser, HtmlImageSourceParser imageSourceParser, IApiRichTextParser apiRichTextParser, IOptionsMonitor deliveryApiSettingsMonitor) + HtmlLocalLinkParser linkParser, HtmlUrlParser urlParser, HtmlImageSourceParser imageSourceParser, + IApiRichTextElementParser apiRichTextElementParser, IApiRichTextMarkupParser apiRichTextMarkupParser, IOptionsMonitor deliveryApiSettingsMonitor) { _umbracoContextAccessor = umbracoContextAccessor; _macroRenderer = macroRenderer; _linkParser = linkParser; _urlParser = urlParser; _imageSourceParser = imageSourceParser; - _apiRichTextParser = apiRichTextParser; + _apiRichTextElementParser = apiRichTextElementParser; + _apiRichTextMarkupParser = apiRichTextMarkupParser; _deliveryApiSettings = deliveryApiSettingsMonitor.CurrentValue; deliveryApiSettingsMonitor.OnChange(settings => _deliveryApiSettings = settings); } @@ -84,20 +88,20 @@ public class RteMacroRenderingValueConverter : SimpleTinyMceValueConverter, IDel ? typeof(IRichTextElement) : typeof(RichTextModel); - public object? ConvertIntermediateToDeliveryApiObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) + public object? ConvertIntermediateToDeliveryApiObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview, bool expanding) { - if (_deliveryApiSettings.RichTextOutputAsJson is false) + var sourceString = inter?.ToString(); + if (sourceString.IsNullOrWhiteSpace()) { - return new RichTextModel - { - Markup = Convert(inter, preview, false) ?? string.Empty - }; + // different return types for the JSON configuration forces us to have different return values for empty properties + return _deliveryApiSettings.RichTextOutputAsJson is false + ? new RichTextModel { Markup = string.Empty } + : null; } - var sourceString = inter?.ToString(); - return sourceString != null - ? _apiRichTextParser.Parse(sourceString) - : null; + return _deliveryApiSettings.RichTextOutputAsJson is false + ? new RichTextModel { Markup = _apiRichTextMarkupParser.Parse(sourceString) } + : _apiRichTextElementParser.Parse(sourceString); } // NOT thread-safe over a request because it modifies the @@ -129,7 +133,7 @@ public class RteMacroRenderingValueConverter : SimpleTinyMceValueConverter, IDel } } - private string? Convert(object? source, bool preview, bool handleMacros = true) + private string? Convert(object? source, bool preview) { if (source == null) { @@ -144,10 +148,7 @@ public class RteMacroRenderingValueConverter : SimpleTinyMceValueConverter, IDel sourceString = _imageSourceParser.EnsureImageSources(sourceString); // ensure string is parsed for macros and macros are executed correctly - if (handleMacros) - { - sourceString = RenderRteMacros(sourceString, preview); - } + sourceString = RenderRteMacros(sourceString, preview); // find and remove the rel attributes used in the Umbraco UI from img tags var doc = new HtmlDocument(); diff --git a/src/Umbraco.Infrastructure/Services/Implement/PackagingService.cs b/src/Umbraco.Infrastructure/Services/Implement/PackagingService.cs index a0330d75fd..34ede610a0 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/PackagingService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/PackagingService.cs @@ -1,10 +1,13 @@ using System.Xml.Linq; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Manifest; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Packaging; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Packaging; +using Umbraco.Cms.Core.Scoping; using Umbraco.Extensions; using File = System.IO.File; @@ -23,6 +26,7 @@ public class PackagingService : IPackagingService private readonly IManifestParser _manifestParser; private readonly IPackageInstallation _packageInstallation; private readonly PackageMigrationPlanCollection _packageMigrationPlans; + private readonly ICoreScopeProvider _coreScopeProvider; public PackagingService( IAuditService auditService, @@ -31,7 +35,8 @@ public class PackagingService : IPackagingService IEventAggregator eventAggregator, IManifestParser manifestParser, IKeyValueService keyValueService, - PackageMigrationPlanCollection packageMigrationPlans) + PackageMigrationPlanCollection packageMigrationPlans, + ICoreScopeProvider coreScopeProvider) { _auditService = auditService; _createdPackages = createdPackages; @@ -40,6 +45,28 @@ public class PackagingService : IPackagingService _manifestParser = manifestParser; _keyValueService = keyValueService; _packageMigrationPlans = packageMigrationPlans; + _coreScopeProvider = coreScopeProvider; + } + + [Obsolete("Use the ctor which is not obsolete, scheduled for removal in v15")] + public PackagingService( + IAuditService auditService, + ICreatedPackagesRepository createdPackages, + IPackageInstallation packageInstallation, + IEventAggregator eventAggregator, + IManifestParser manifestParser, + IKeyValueService keyValueService, + PackageMigrationPlanCollection packageMigrationPlans) + : this( + auditService, + createdPackages, + packageInstallation, + eventAggregator, + manifestParser, + keyValueService, + packageMigrationPlans, + StaticServiceProvider.Instance.GetRequiredService()) + { } #region Installation @@ -93,6 +120,7 @@ public class PackagingService : IPackagingService public void DeleteCreatedPackage(int id, int userId = Constants.Security.SuperUserId) { + using ICoreScope scope = _coreScopeProvider.CreateCoreScope(); PackageDefinition? package = GetCreatedPackageById(id); if (package == null) { @@ -101,39 +129,83 @@ public class PackagingService : IPackagingService _auditService.Add(AuditType.PackagerUninstall, userId, -1, "Package", $"Created package '{package.Name}' deleted. Package id: {package.Id}"); _createdPackages.Delete(id); + + scope.Complete(); } - public IEnumerable GetAllCreatedPackages() => _createdPackages.GetAll(); + public IEnumerable GetAllCreatedPackages() + { + using ICoreScope scope = _coreScopeProvider.CreateCoreScope(autoComplete: true); + return _createdPackages.GetAll(); + } - public PackageDefinition? GetCreatedPackageById(int id) => _createdPackages.GetById(id); + public PackageDefinition? GetCreatedPackageById(int id) + { + using ICoreScope scope = _coreScopeProvider.CreateCoreScope(autoComplete: true); + return _createdPackages.GetById(id); + } - public bool SaveCreatedPackage(PackageDefinition definition) => _createdPackages.SavePackage(definition); + public bool SaveCreatedPackage(PackageDefinition definition) + { + using ICoreScope scope = _coreScopeProvider.CreateCoreScope(); - public string ExportCreatedPackage(PackageDefinition definition) => _createdPackages.ExportPackage(definition); + var success = _createdPackages.SavePackage(definition); + scope.Complete(); + return success; + } + + public string ExportCreatedPackage(PackageDefinition definition) + { + using ICoreScope scope = _coreScopeProvider.CreateCoreScope(autoComplete: true); + return _createdPackages.ExportPackage(definition); + } public InstalledPackage? GetInstalledPackageByName(string packageName) => GetAllInstalledPackages().Where(x => x.PackageName?.InvariantEquals(packageName) ?? false).FirstOrDefault(); public IEnumerable GetAllInstalledPackages() { - IReadOnlyDictionary? keyValues = - _keyValueService.FindByKeyPrefix(Constants.Conventions.Migrations.KeyValuePrefix); + using ICoreScope scope = _coreScopeProvider.CreateCoreScope(autoComplete: true); - var installedPackages = new Dictionary(); + IReadOnlyDictionary? keyValues = _keyValueService.FindByKeyPrefix(Constants.Conventions.Migrations.KeyValuePrefix); + + var installedPackages = new List(); // Collect the package from the package migration plans foreach (PackageMigrationPlan plan in _packageMigrationPlans) { - if (!installedPackages.TryGetValue(plan.PackageName, out InstalledPackage? installedPackage)) + InstalledPackage installedPackage; + if (plan.PackageId is not null && installedPackages.FirstOrDefault(x => x.PackageId == plan.PackageId) is InstalledPackage installedPackageById) { - installedPackage = new InstalledPackage { PackageName = plan.PackageName }; - installedPackages.Add(plan.PackageName, installedPackage); + installedPackage = installedPackageById; + } + else if (installedPackages.FirstOrDefault(x => x.PackageName == plan.PackageName) is InstalledPackage installedPackageByName) + { + installedPackage = installedPackageByName; + + // Ensure package ID is set + installedPackage.PackageId ??= plan.PackageId; + } + else + { + installedPackage = new InstalledPackage + { + PackageId = plan.PackageId, + PackageName = plan.PackageName, + }; + + installedPackages.Add(installedPackage); } + if (installedPackage.Version is null && + plan.GetType().Assembly.TryGetInformationalVersion(out string? version)) + { + installedPackage.Version = version; + } + + // Combine all package migration plans for a package var currentPlans = installedPackage.PackageMigrationPlans.ToList(); - if (keyValues is null || keyValues.TryGetValue( - Constants.Conventions.Migrations.KeyValuePrefix + plan.Name, - out var currentState) is false) + if (keyValues is null || keyValues.TryGetValue(Constants.Conventions.Migrations.KeyValuePrefix + plan.Name, out var currentState) is false) { currentState = null; } @@ -150,26 +222,49 @@ public class PackagingService : IPackagingService // Collect and merge the packages from the manifests foreach (PackageManifest package in _manifestParser.GetManifests()) { - if (package.PackageName is null) + if (package.PackageId is null && package.PackageName is null) { continue; } - if (!installedPackages.TryGetValue(package.PackageName, out InstalledPackage? installedPackage)) + InstalledPackage installedPackage; + if (package.PackageId is not null && installedPackages.FirstOrDefault(x => x.PackageId == package.PackageId) is InstalledPackage installedPackageById) { - installedPackage = new InstalledPackage { + installedPackage = installedPackageById; + + // Always use package name from manifest + installedPackage.PackageName = package.PackageName; + } + else if (installedPackages.FirstOrDefault(x => x.PackageName == package.PackageName) is InstalledPackage installedPackageByName) + { + installedPackage = installedPackageByName; + + // Ensure package ID is set + installedPackage.PackageId ??= package.PackageId; + } + else + { + installedPackage = new InstalledPackage + { + PackageId = package.PackageId, PackageName = package.PackageName, - Version = string.IsNullOrEmpty(package.Version) ? "Unknown" : package.Version, }; - installedPackages.Add(package.PackageName, installedPackage); + installedPackages.Add(installedPackage); } + // Set additional values + installedPackage.AllowPackageTelemetry = package.AllowPackageTelemetry; installedPackage.PackageView = package.PackageView; + + if (!string.IsNullOrEmpty(package.Version)) + { + installedPackage.Version = package.Version; + } } - // Return all packages with a name in the package.manifest or package migrations - return installedPackages.Values; + // Return all packages with an ID or name in the package.manifest or package migrations + return installedPackages; } #endregion diff --git a/src/Umbraco.Infrastructure/Telemetry/Interfaces/IDetailedTelemetryProvider.cs b/src/Umbraco.Infrastructure/Telemetry/Interfaces/IDetailedTelemetryProvider.cs index b21b216e68..e6ce3b005f 100644 --- a/src/Umbraco.Infrastructure/Telemetry/Interfaces/IDetailedTelemetryProvider.cs +++ b/src/Umbraco.Infrastructure/Telemetry/Interfaces/IDetailedTelemetryProvider.cs @@ -2,7 +2,7 @@ using Umbraco.Cms.Core.Models; namespace Umbraco.Cms.Infrastructure.Telemetry.Interfaces; -internal interface IDetailedTelemetryProvider +public interface IDetailedTelemetryProvider { IEnumerable GetInformation(); } diff --git a/src/Umbraco.PublishedCache.NuCache/Property.cs b/src/Umbraco.PublishedCache.NuCache/Property.cs index 8377d369a7..d50553d95f 100644 --- a/src/Umbraco.PublishedCache.NuCache/Property.cs +++ b/src/Umbraco.PublishedCache.NuCache/Property.cs @@ -322,7 +322,7 @@ internal class Property : PublishedPropertyBase // initial reference cache level always is .Content const PropertyCacheLevel initialCacheLevel = PropertyCacheLevel.Element; - object? GetDeliveryApiObject() => PropertyType.ConvertInterToDeliveryApiObject(_content, initialCacheLevel, GetInterValue(culture, segment), _isPreviewing); + object? GetDeliveryApiObject() => PropertyType.ConvertInterToDeliveryApiObject(_content, initialCacheLevel, GetInterValue(culture, segment), _isPreviewing, expanding); value = expanding ? GetDeliveryApiExpandedObject(cacheValues, GetDeliveryApiObject) : GetDeliveryApiDefaultObject(cacheValues, GetDeliveryApiObject); diff --git a/src/Umbraco.Web.BackOffice/Controllers/TinyMceController.cs b/src/Umbraco.Web.BackOffice/Controllers/TinyMceController.cs index 3d93f9af6c..a9f81344a0 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/TinyMceController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/TinyMceController.cs @@ -26,6 +26,19 @@ public class TinyMceController : UmbracoAuthorizedApiController private readonly IIOHelper _ioHelper; private readonly IShortStringHelper _shortStringHelper; + private readonly Dictionary _fileContentTypeMappings = + new() + { + { "image/png", "png" }, + { "image/jpeg", "jpg" }, + { "image/gif", "gif" }, + { "image/bmp", "bmp" }, + { "image/x-icon", "ico" }, + { "image/svg+xml", "svg" }, + { "image/tiff", "tiff" }, + { "image/webp", "webp" }, + }; + public TinyMceController( IHostingEnvironment hostingEnvironment, IShortStringHelper shortStringHelper, @@ -43,16 +56,6 @@ public class TinyMceController : UmbracoAuthorizedApiController [HttpPost] public async Task UploadImage(List file) { - // Create an unique folder path to help with concurrent users to avoid filename clash - var imageTempPath = - _hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.TempImageUploads + "/" + Guid.NewGuid()); - - // Ensure image temp path exists - if (Directory.Exists(imageTempPath) == false) - { - Directory.CreateDirectory(imageTempPath); - } - // Must have a file if (file.Count == 0) { @@ -65,13 +68,36 @@ public class TinyMceController : UmbracoAuthorizedApiController return new UmbracoProblemResult("Only one file can be uploaded at a time", HttpStatusCode.BadRequest); } + // Create an unique folder path to help with concurrent users to avoid filename clash + var imageTempPath = + _hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.TempImageUploads + "/" + Guid.NewGuid()); + + // Ensure image temp path exists + if (Directory.Exists(imageTempPath) == false) + { + Directory.CreateDirectory(imageTempPath); + } + IFormFile formFile = file.First(); // Really we should only have one file per request to this endpoint // var file = result.FileData[0]; - var fileName = formFile.FileName.Trim(new[] { '\"' }).TrimEnd(); + var fileName = formFile.FileName.Trim(new[] {'\"'}).TrimEnd(); var safeFileName = fileName.ToSafeFileName(_shortStringHelper); - var ext = safeFileName.Substring(safeFileName.LastIndexOf('.') + 1).ToLowerInvariant(); + string ext; + var fileExtensionIndex = safeFileName.LastIndexOf('.'); + if (fileExtensionIndex is not -1) + { + ext = safeFileName.Substring(fileExtensionIndex + 1).ToLowerInvariant(); + } + else + { + _fileContentTypeMappings.TryGetValue(formFile.ContentType, out var fileExtension); + ext = fileExtension ?? string.Empty; + + // safeFileName will not have a file extension, so we need to add it back + safeFileName += $".{ext}"; + } if (_contentSettings.IsFileAllowedForUpload(ext) == false || _imageUrlGenerator.IsSupportedImageFormat(ext) == false) diff --git a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeIdentity.cs b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeIdentity.cs index 6d3ff7edda..1844cf5885 100644 --- a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeIdentity.cs +++ b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeIdentity.cs @@ -11,7 +11,9 @@ using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Security; +using Umbraco.Cms.Infrastructure.Telemetry.Interfaces; using Umbraco.Cms.Web.BackOffice.Security; +using Umbraco.Cms.Web.BackOffice.Telemetry; using Umbraco.Cms.Web.Common.AspNetCore; using Umbraco.Cms.Web.Common.Security; @@ -65,6 +67,7 @@ public static partial class UmbracoBuilderExtensions services.TryAddScoped(); services.TryAddSingleton(); services.TryAddSingleton(); + services.AddTransient(); return new BackOfficeIdentityBuilder(services); } diff --git a/src/Umbraco.Web.BackOffice/Telemetry/ExternalLoginTelemetryProvider.cs b/src/Umbraco.Web.BackOffice/Telemetry/ExternalLoginTelemetryProvider.cs new file mode 100644 index 0000000000..21a59796b3 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Telemetry/ExternalLoginTelemetryProvider.cs @@ -0,0 +1,22 @@ +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Infrastructure.Telemetry.Interfaces; +using Umbraco.Cms.Web.BackOffice.Security; + +namespace Umbraco.Cms.Web.BackOffice.Telemetry; + +public class ExternalLoginTelemetryProvider : IDetailedTelemetryProvider +{ + private readonly IBackOfficeExternalLoginProviders _externalLoginProviders; + + public ExternalLoginTelemetryProvider(IBackOfficeExternalLoginProviders externalLoginProviders) + { + _externalLoginProviders = externalLoginProviders; + } + + public IEnumerable GetInformation() + { + IEnumerable providers = _externalLoginProviders.GetBackOfficeProvidersAsync().GetAwaiter().GetResult(); + yield return new UsageInformation(Constants.Telemetry.BackofficeExternalLoginProviderCount, providers.Count()); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-packages.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-packages.less index 14cfa5f007..d4415ce0f6 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-packages.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-packages.less @@ -546,6 +546,11 @@ font-weight: bold; } +.umb-package-list__item-id { + font-size: 12px; + color: @gray-6; +} + .umb-package-list__item-description { font-size: 14px; color: @gray-4; diff --git a/src/Umbraco.Web.UI.Client/src/views/packages/views/installed.html b/src/Umbraco.Web.UI.Client/src/views/packages/views/installed.html index 248e926c98..ef420921fe 100644 --- a/src/Umbraco.Web.UI.Client/src/views/packages/views/installed.html +++ b/src/Umbraco.Web.UI.Client/src/views/packages/views/installed.html @@ -17,7 +17,8 @@
{{ installedPackage.name }}
-
Version: {{ installedPackage.version }}
+
{{ installedPackage.id }}
+
Version: {{ installedPackage.version }}
No pending migrations diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/umbBlockGridPropertyEditor.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/umbBlockGridPropertyEditor.component.js index 9111beb250..c37b41aca3 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/umbBlockGridPropertyEditor.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/umbBlockGridPropertyEditor.component.js @@ -1011,7 +1011,7 @@ blockObject = vm.layout[createIndex].$block; } // edit block if not `hideContentInOverlay` and there is content properties. - if(blockObject.hideContentInOverlay !== true && blockObject.content.variants[0].tabs[0]?.properties.length > 0) { + if(blockObject.hideContentInOverlay !== true && blockObject.content.variants[0].tabs.find(tab => tab.properties.length > 0) !== undefined) { vm.options.createFlow = true; blockObject.edit(); vm.options.createFlow = false; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbBlockListPropertyEditor.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbBlockListPropertyEditor.component.js index 49a6a26169..6c562a865b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbBlockListPropertyEditor.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbBlockListPropertyEditor.component.js @@ -621,7 +621,7 @@ var blockObject = vm.layout[createIndex].$block; if (inlineEditing === true) { blockObject.activate(); - } else if (inlineEditing === false && blockObject.hideContentInOverlay !== true && blockObject.content.variants[0].tabs[0]?.properties.length > 0) { + } else if (inlineEditing === false && blockObject.hideContentInOverlay !== true && blockObject.content.variants[0].tabs.find(tab => tab.properties.length > 0) !== undefined) { vm.options.createFlow = true; blockObject.edit(); vm.options.createFlow = false; diff --git a/templates/UmbracoPackage/App_Plugins/UmbracoPackage/package.manifest b/templates/UmbracoPackage/App_Plugins/UmbracoPackage/package.manifest index 906db79b7a..db8d6385fc 100644 --- a/templates/UmbracoPackage/App_Plugins/UmbracoPackage/package.manifest +++ b/templates/UmbracoPackage/App_Plugins/UmbracoPackage/package.manifest @@ -1,5 +1,5 @@ -{ +{ + "id": "UmbracoPackage", "name": "UmbracoPackage", - "version": "", "allowPackageTelemetry": true -} \ No newline at end of file +} diff --git a/templates/UmbracoPackageRcl/wwwroot/package.manifest b/templates/UmbracoPackageRcl/wwwroot/package.manifest index 6aadd0cee6..db8d6385fc 100644 --- a/templates/UmbracoPackageRcl/wwwroot/package.manifest +++ b/templates/UmbracoPackageRcl/wwwroot/package.manifest @@ -1,5 +1,5 @@ { + "id": "UmbracoPackage", "name": "UmbracoPackage", - "version": "", "allowPackageTelemetry": true } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/DeliveryApi/CacheTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/DeliveryApi/CacheTests.cs index 56f6dec0df..750be34ea1 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/DeliveryApi/CacheTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/DeliveryApi/CacheTests.cs @@ -50,7 +50,7 @@ public class CacheTests propertyType.SetupGet(p => p.CacheLevel).Returns(cacheLevel); propertyType.SetupGet(p => p.DeliveryApiCacheLevel).Returns(cacheLevel); propertyType - .Setup(p => p.ConvertInterToDeliveryApiObject(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Setup(p => p.ConvertInterToDeliveryApiObject(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(() => $"Delivery API value: {++invocationCount}"); var prop1 = new Property(propertyType.Object, content, publishedSnapshotAccessor.Object); @@ -68,6 +68,7 @@ public class CacheTests It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny()), Times.Exactly(expectedConverterHits)); } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Telemetry/TelemetryServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Telemetry/TelemetryServiceTests.cs index f44c381172..7bedf05911 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Telemetry/TelemetryServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Telemetry/TelemetryServiceTests.cs @@ -49,7 +49,8 @@ public class TelemetryServiceTests : UmbracoIntegrationTest Constants.Telemetry.IsDebug, Constants.Telemetry.DatabaseProvider, Constants.Telemetry.CurrentServerRole, - Constants.Telemetry.RuntimeMode, + Constants.Telemetry.BackofficeExternalLoginProviderCount, + Constants.Telemetry.RuntimeMode }; MetricsConsentService.SetConsentLevel(TelemetryLevel.Detailed); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/IndexInitializer.cs b/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/IndexInitializer.cs index 6ae7f3628e..01ca9284f7 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/IndexInitializer.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/IndexInitializer.cs @@ -3,6 +3,7 @@ using Examine.Lucene; using Examine.Lucene.Directories; using Lucene.Net.Analysis; using Lucene.Net.Analysis.Standard; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Moq; @@ -18,7 +19,9 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Infrastructure.Examine; using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Web.Common.DependencyInjection; using Directory = Lucene.Net.Store.Directory; +using StaticServiceProvider = Umbraco.Cms.Core.DependencyInjection.StaticServiceProvider; namespace Umbraco.Cms.Tests.Integration.Umbraco.Examine.Lucene.UmbracoExamine; @@ -28,6 +31,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Examine.Lucene.UmbracoExamine; public class IndexInitializer { private readonly IOptions _contentSettings; + private readonly ILocalizationService _localizationService; private readonly ILoggerFactory _loggerFactory; private readonly MediaUrlGeneratorCollection _mediaUrlGenerators; private readonly PropertyEditorCollection _propertyEditors; @@ -40,7 +44,8 @@ public class IndexInitializer MediaUrlGeneratorCollection mediaUrlGenerators, IScopeProvider scopeProvider, ILoggerFactory loggerFactory, - IOptions contentSettings) + IOptions contentSettings, + ILocalizationService localizationService) { _shortStringHelper = shortStringHelper; _propertyEditors = propertyEditors; @@ -48,6 +53,25 @@ public class IndexInitializer _scopeProvider = scopeProvider; _loggerFactory = loggerFactory; _contentSettings = contentSettings; + _localizationService = localizationService; + } + + public IndexInitializer( + IShortStringHelper shortStringHelper, + PropertyEditorCollection propertyEditors, + MediaUrlGeneratorCollection mediaUrlGenerators, + IScopeProvider scopeProvider, + ILoggerFactory loggerFactory, + IOptions contentSettings) + : this( + shortStringHelper, + propertyEditors, + mediaUrlGenerators, + scopeProvider, + loggerFactory, + contentSettings, + StaticServiceProvider.Instance.GetRequiredService()) + { } public ContentValueSetBuilder GetContentValueSetBuilder(bool publishedValuesOnly) @@ -58,7 +82,8 @@ public class IndexInitializer GetMockUserService(), _shortStringHelper, _scopeProvider, - publishedValuesOnly); + publishedValuesOnly, + _localizationService); return contentValueSetBuilder; } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/IndexTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/IndexTest.cs index 4d31b9c314..33e8d44c14 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/IndexTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/IndexTest.cs @@ -9,7 +9,6 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Examine; using Umbraco.Cms.Tests.Common.Builders; using Umbraco.Cms.Tests.Common.Testing; -using Umbraco.Extensions; using Constants = Umbraco.Cms.Core.Constants; namespace Umbraco.Cms.Tests.Integration.Umbraco.Examine.Lucene.UmbracoExamine; @@ -18,7 +17,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Examine.Lucene.UmbracoExamine; /// Tests the standard indexing capabilities ///
[TestFixture] -[UmbracoTest(Database = UmbracoTestOptions.Database.None)] +[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] public class IndexTest : ExamineBaseTest { [Test] diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Packaging/CreatedPackageSchemaTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Packaging/CreatedPackageSchemaTests.cs index f37aac5d64..f9bdbe5dac 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Packaging/CreatedPackageSchemaTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Packaging/CreatedPackageSchemaTests.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using NUnit.Framework; using Umbraco.Cms.Core.Packaging; +using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Tests.Common.Testing; using Umbraco.Cms.Tests.Integration.Testing; @@ -14,17 +15,23 @@ public class CreatedPackageSchemaTests : UmbracoIntegrationTest private ICreatedPackagesRepository CreatedPackageSchemaRepository => GetRequiredService(); + private ICoreScopeProvider ScopeProvider => GetRequiredService(); + [Test] public void PackagesRepository_Can_Save_PackageDefinition() { + using var scope = ScopeProvider.CreateCoreScope(); var packageDefinition = new PackageDefinition { Name = "NewPack", DocumentTypes = new List { "Root" } }; var result = CreatedPackageSchemaRepository.SavePackage(packageDefinition); + scope.Complete(); Assert.IsTrue(result); } [Test] public void PackageRepository_GetAll_Returns_All_PackageDefinitions() { + using var scope = ScopeProvider.CreateCoreScope(); + var packageDefinitionList = new List { new() {Name = "PackOne"}, new() {Name = "PackTwo"}, new() {Name = "PackThree"} @@ -35,6 +42,7 @@ public class CreatedPackageSchemaTests : UmbracoIntegrationTest } var loadedPackageDefinitions = CreatedPackageSchemaRepository.GetAll().ToList(); + scope.Complete(); CollectionAssert.IsNotEmpty(loadedPackageDefinitions); CollectionAssert.AllItemsAreUnique(loadedPackageDefinitions); Assert.AreEqual(loadedPackageDefinitions.Count, 3); @@ -43,13 +51,14 @@ public class CreatedPackageSchemaTests : UmbracoIntegrationTest [Test] public void PackageRepository_Can_Update_Package() { + using var scope = ScopeProvider.CreateCoreScope(); var packageDefinition = new PackageDefinition { Name = "TestPackage" }; CreatedPackageSchemaRepository.SavePackage(packageDefinition); packageDefinition.Name = "UpdatedName"; CreatedPackageSchemaRepository.SavePackage(packageDefinition); var results = CreatedPackageSchemaRepository.GetAll().ToList(); - + scope.Complete(); Assert.AreEqual(1, results.Count); Assert.AreEqual("UpdatedName", results.FirstOrDefault()?.Name); } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/CacheTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/CacheTests.cs index f75b4cb90f..48007a13bb 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/CacheTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/CacheTests.cs @@ -27,6 +27,7 @@ public class CacheTests : DeliveryApiTests It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny()) ).Returns(() => $"Delivery API value: {++invocationCount}"); propertyValueConverter.Setup(p => p.IsConverter(It.IsAny())).Returns(true); @@ -54,6 +55,7 @@ public class CacheTests : DeliveryApiTests It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny()), Times.Exactly(expectedConverterHits)); } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentBuilderTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentBuilderTests.cs index f214c05446..026ab42c0c 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentBuilderTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentBuilderTests.cs @@ -34,7 +34,7 @@ public class ContentBuilderTests : DeliveryApiTests .Setup(p => p.GetUrl(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns((IPublishedContent content, UrlMode mode, string? culture, Uri? current) => $"url:{content.UrlSegment}"); - var routeBuilder = new ApiContentRouteBuilder(publishedUrlProvider.Object, CreateGlobalSettings(), Mock.Of(), Mock.Of()); + var routeBuilder = CreateContentRouteBuilder(publishedUrlProvider.Object, CreateGlobalSettings()); var builder = new ApiContentBuilder(new ApiContentNameProvider(), routeBuilder, CreateOutputExpansionStrategyAccessor()); var result = builder.Build(content.Object); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentPickerValueConverterTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentPickerValueConverterTests.cs index f769913b23..61e55b76db 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentPickerValueConverterTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentPickerValueConverterTests.cs @@ -18,7 +18,7 @@ public class ContentPickerValueConverterTests : PropertyValueConverterTests PublishedSnapshotAccessor, new ApiContentBuilder( nameProvider ?? new ApiContentNameProvider(), - new ApiContentRouteBuilder(PublishedUrlProvider, CreateGlobalSettings(), Mock.Of(), Mock.Of()), + CreateContentRouteBuilder(PublishedUrlProvider, CreateGlobalSettings()), CreateOutputExpansionStrategyAccessor())); [Test] @@ -34,6 +34,7 @@ public class ContentPickerValueConverterTests : PropertyValueConverterTests publishedPropertyType.Object, PropertyCacheLevel.Element, new GuidUdi(Constants.UdiEntityType.Document, PublishedContent.Key), + false, false) as IApiContent; Assert.NotNull(result); @@ -59,6 +60,7 @@ public class ContentPickerValueConverterTests : PropertyValueConverterTests publishedPropertyType.Object, PropertyCacheLevel.Element, new GuidUdi(Constants.UdiEntityType.Document, PublishedContent.Key), + false, false) as IApiContent; Assert.NotNull(result); @@ -95,6 +97,7 @@ public class ContentPickerValueConverterTests : PropertyValueConverterTests publishedPropertyType.Object, PropertyCacheLevel.Element, new GuidUdi(Constants.UdiEntityType.Document, key), + false, false) as IApiContent; Assert.NotNull(result); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentRouteBuilderTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentRouteBuilderTests.cs index 90774c5e25..d60097bf69 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentRouteBuilderTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentRouteBuilderTests.cs @@ -1,5 +1,8 @@ -using Moq; +using Microsoft.Extensions.Options; +using Moq; using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.DeliveryApi; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.DeliveryApi; @@ -188,18 +191,81 @@ public class ContentRouteBuilderTests : DeliveryApiTests Assert.AreEqual(hideTopLevelNodeFromPath ? "/the-child/the-grandchild" : "/the-root/the-child/the-grandchild", publishedUrlProvider.GetUrl(grandchild)); } - private IPublishedContent SetupInvariantPublishedContent(string name, Guid key, IPublishedContent? parent = null) + [TestCase(true)] + [TestCase(false)] + public void CanRouteUnpublishedChild(bool hideTopLevelNodeFromPath) + { + var rootKey = Guid.NewGuid(); + var root = SetupInvariantPublishedContent("The Root", rootKey); + + var childKey = Guid.NewGuid(); + var child = SetupInvariantPublishedContent("The Child", childKey, root, false); + + var builder = CreateApiContentRouteBuilder(hideTopLevelNodeFromPath, isPreview: true); + var result = builder.Build(child); + Assert.IsNotNull(result); + Assert.AreEqual($"/{Constants.DeliveryApi.Routing.PreviewContentPathPrefix}{childKey:D}", result.Path); + Assert.AreEqual(rootKey, result.StartItem.Id); + Assert.AreEqual("the-root", result.StartItem.Path); + } + + [TestCase(true)] + [TestCase(false)] + public void UnpublishedChildRouteRespectsTrailingSlashSettings(bool addTrailingSlash) + { + var rootKey = Guid.NewGuid(); + var root = SetupInvariantPublishedContent("The Root", rootKey); + + var childKey = Guid.NewGuid(); + var child = SetupInvariantPublishedContent("The Child", childKey, root, false); + + var builder = CreateApiContentRouteBuilder(true, addTrailingSlash, isPreview: true); + var result = builder.Build(child); + Assert.IsNotNull(result); + Assert.AreEqual(addTrailingSlash, result.Path.EndsWith("/")); + } + + [TestCase(true)] + [TestCase(false)] + public void CanRoutePublishedChildOfUnpublishedParentInPreview(bool isPreview) + { + var rootKey = Guid.NewGuid(); + var root = SetupInvariantPublishedContent("The Root", rootKey, published: false); + + var childKey = Guid.NewGuid(); + var child = SetupInvariantPublishedContent("The Child", childKey, root); + + var requestPreviewServiceMock = new Mock(); + requestPreviewServiceMock.Setup(m => m.IsPreview()).Returns(isPreview); + + var builder = CreateApiContentRouteBuilder(true, isPreview: isPreview); + var result = builder.Build(child); + + if (isPreview) + { + Assert.IsNotNull(result); + Assert.AreEqual($"/{Constants.DeliveryApi.Routing.PreviewContentPathPrefix}{childKey:D}", result.Path); + Assert.AreEqual(rootKey, result.StartItem.Id); + Assert.AreEqual("the-root", result.StartItem.Path); + } + else + { + Assert.IsNull(result); + } + } + + private IPublishedContent SetupInvariantPublishedContent(string name, Guid key, IPublishedContent? parent = null, bool published = true) { var publishedContentType = CreatePublishedContentType(); - var content = CreatePublishedContentMock(publishedContentType.Object, name, key, parent); + var content = CreatePublishedContentMock(publishedContentType.Object, name, key, parent, published); return content.Object; } - private IPublishedContent SetupVariantPublishedContent(string name, Guid key, IPublishedContent? parent = null) + private IPublishedContent SetupVariantPublishedContent(string name, Guid key, IPublishedContent? parent = null, bool published = true) { var publishedContentType = CreatePublishedContentType(); publishedContentType.SetupGet(m => m.Variations).Returns(ContentVariation.Culture); - var content = CreatePublishedContentMock(publishedContentType.Object, name, key, parent); + var content = CreatePublishedContentMock(publishedContentType.Object, name, key, parent, published); var cultures = new[] { "en-us", "da-dk" }; content .SetupGet(m => m.Cultures) @@ -209,10 +275,11 @@ public class ContentRouteBuilderTests : DeliveryApiTests return content.Object; } - private Mock CreatePublishedContentMock(IPublishedContentType publishedContentType, string name, Guid key, IPublishedContent? parent) + private Mock CreatePublishedContentMock(IPublishedContentType publishedContentType, string name, Guid key, IPublishedContent? parent, bool published) { var content = new Mock(); ConfigurePublishedContentMock(content, key, name, DefaultUrlSegment(name), publishedContentType, Array.Empty()); + content.Setup(c => c.IsPublished(It.IsAny())).Returns(published); content.SetupGet(c => c.Parent).Returns(parent); content.SetupGet(c => c.Level).Returns((parent?.Level ?? 0) + 1); return content; @@ -230,7 +297,11 @@ public class ContentRouteBuilderTests : DeliveryApiTests { var variantContextAccessor = Mock.Of(); string Url(IPublishedContent content, string? culture) - => string.Join("/", content.AncestorsOrSelf().Reverse().Skip(hideTopLevelNodeFromPath ? 1 : 0).Select(c => c.UrlSegment(variantContextAccessor, culture))).EnsureStartsWith("/"); + { + return content.AncestorsOrSelf().All(c => c.IsPublished(culture)) + ? string.Join("/", content.AncestorsOrSelf().Reverse().Skip(hideTopLevelNodeFromPath ? 1 : 0).Select(c => c.UrlSegment(variantContextAccessor, culture))).EnsureStartsWith("/") + : "#"; + } var publishedUrlProvider = new Mock(); publishedUrlProvider @@ -239,12 +310,24 @@ public class ContentRouteBuilderTests : DeliveryApiTests return publishedUrlProvider.Object; } - private ApiContentRouteBuilder CreateApiContentRouteBuilder(bool hideTopLevelNodeFromPath) - => new( + private ApiContentRouteBuilder CreateApiContentRouteBuilder(bool hideTopLevelNodeFromPath, bool addTrailingSlash = false, bool isPreview = false, IPublishedSnapshotAccessor? publishedSnapshotAccessor = null) + { + var requestHandlerSettings = new RequestHandlerSettings { AddTrailingSlash = addTrailingSlash }; + var requestHandlerSettingsMonitorMock = new Mock>(); + requestHandlerSettingsMonitorMock.Setup(m => m.CurrentValue).Returns(requestHandlerSettings); + + var requestPreviewServiceMock = new Mock(); + requestPreviewServiceMock.Setup(m => m.IsPreview()).Returns(isPreview); + + publishedSnapshotAccessor ??= CreatePublishedSnapshotAccessorForRoute("#"); + + return CreateContentRouteBuilder( SetupPublishedUrlProvider(hideTopLevelNodeFromPath), CreateGlobalSettings(hideTopLevelNodeFromPath), - Mock.Of(), - Mock.Of()); + requestHandlerSettingsMonitor: requestHandlerSettingsMonitorMock.Object, + requestPreviewService: requestPreviewServiceMock.Object, + publishedSnapshotAccessor: publishedSnapshotAccessor); + } private IApiContentRoute? GetUnRoutableRoute(string publishedUrl, string routeById) { @@ -253,6 +336,19 @@ public class ContentRouteBuilderTests : DeliveryApiTests .Setup(p => p.GetUrl(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(publishedUrl); + var publishedSnapshotAccessor = CreatePublishedSnapshotAccessorForRoute(routeById); + var content = SetupVariantPublishedContent("The Content", Guid.NewGuid()); + + var builder = CreateContentRouteBuilder( + publishedUrlProviderMock.Object, + CreateGlobalSettings(), + publishedSnapshotAccessor: publishedSnapshotAccessor); + + return builder.Build(content); + } + + private IPublishedSnapshotAccessor CreatePublishedSnapshotAccessorForRoute(string routeById) + { var publishedContentCacheMock = new Mock(); publishedContentCacheMock .Setup(c => c.GetRouteById(It.IsAny(), It.IsAny())) @@ -269,14 +365,6 @@ public class ContentRouteBuilderTests : DeliveryApiTests .Setup(a => a.TryGetPublishedSnapshot(out publishedSnapshot)) .Returns(true); - var content = SetupVariantPublishedContent("The Content", Guid.NewGuid()); - - var builder = new ApiContentRouteBuilder( - publishedUrlProviderMock.Object, - CreateGlobalSettings(), - Mock.Of(), - publishedSnapshotAccessorMock.Object); - - return builder.Build(content); + return publishedSnapshotAccessorMock.Object; } } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/DeliveryApiTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/DeliveryApiTests.cs index 3bb7339bc5..3a8adb6a1e 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/DeliveryApiTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/DeliveryApiTests.cs @@ -8,6 +8,8 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.PropertyEditors.DeliveryApi; +using Umbraco.Cms.Core.PublishedCache; +using Umbraco.Cms.Core.Routing; using Umbraco.Extensions; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.DeliveryApi; @@ -27,6 +29,7 @@ public class DeliveryApiTests It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny()) ).Returns("Delivery API value"); deliveryApiPropertyValueConverter.Setup(p => p.ConvertIntermediateToObject( @@ -101,8 +104,33 @@ public class DeliveryApiTests content.SetupGet(c => c.ContentType).Returns(contentType); content.SetupGet(c => c.Properties).Returns(properties); content.SetupGet(c => c.ItemType).Returns(contentType.ItemType); + content.Setup(c => c.IsPublished(It.IsAny())).Returns(true); } protected string DefaultUrlSegment(string name, string? culture = null) => $"{name.ToLowerInvariant().Replace(" ", "-")}{(culture.IsNullOrWhiteSpace() ? string.Empty : $"-{culture}")}"; + + protected ApiContentRouteBuilder CreateContentRouteBuilder( + IPublishedUrlProvider publishedUrlProvider, + IOptions globalSettings, + IVariationContextAccessor? variationContextAccessor = null, + IPublishedSnapshotAccessor? publishedSnapshotAccessor = null, + IRequestPreviewService? requestPreviewService = null, + IOptionsMonitor? requestHandlerSettingsMonitor = null) + { + if (requestHandlerSettingsMonitor == null) + { + var mock = new Mock>(); + mock.SetupGet(m => m.CurrentValue).Returns(new RequestHandlerSettings()); + requestHandlerSettingsMonitor = mock.Object; + } + + return new ApiContentRouteBuilder( + publishedUrlProvider, + globalSettings, + variationContextAccessor ?? Mock.Of(), + publishedSnapshotAccessor ?? Mock.Of(), + requestPreviewService ?? Mock.Of(), + requestHandlerSettingsMonitor); + } } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ImageCropperValueConverterTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ImageCropperValueConverterTests.cs index 29d99b6e68..fedb71cac6 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ImageCropperValueConverterTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ImageCropperValueConverterTests.cs @@ -48,7 +48,7 @@ public class ImageCropperValueConverterTests : PropertyValueConverterTests } ); var inter = valueConverter.ConvertSourceToIntermediate(Mock.Of(), publishedPropertyType.Object, source, false); - var result = valueConverter.ConvertIntermediateToDeliveryApiObject(Mock.Of(), publishedPropertyType.Object, PropertyCacheLevel.Element, inter, false) as ApiImageCropperValue; + var result = valueConverter.ConvertIntermediateToDeliveryApiObject(Mock.Of(), publishedPropertyType.Object, PropertyCacheLevel.Element, inter, false, false) as ApiImageCropperValue; Assert.NotNull(result); Assert.AreEqual("/some/file.jpg", result.Url); Assert.NotNull(result.FocalPoint); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/MarkdownEditorValueConverterTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/MarkdownEditorValueConverterTests.cs index 1540df68bb..8b9ef31bc7 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/MarkdownEditorValueConverterTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/MarkdownEditorValueConverterTests.cs @@ -29,7 +29,7 @@ public class MarkdownEditorValueConverterTests : PropertyValueConverterTests var valueConverter = new MarkdownEditorValueConverter(linkParser, urlParser); Assert.AreEqual(typeof(string), valueConverter.GetDeliveryApiPropertyValueType(Mock.Of())); - var result = valueConverter.ConvertIntermediateToDeliveryApiObject(Mock.Of(), Mock.Of(), PropertyCacheLevel.Element, inter, false); + var result = valueConverter.ConvertIntermediateToDeliveryApiObject(Mock.Of(), Mock.Of(), PropertyCacheLevel.Element, inter, false, false); Assert.AreEqual(expected, result); } } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/MediaPickerValueConverterTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/MediaPickerValueConverterTests.cs index 0bb8fa46e8..e569f94931 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/MediaPickerValueConverterTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/MediaPickerValueConverterTests.cs @@ -29,7 +29,7 @@ public class MediaPickerValueConverterTests : PropertyValueConverterTests var inter = new[] {new GuidUdi(Constants.UdiEntityType.MediaType, PublishedMedia.Key)}; - var result = valueConverter.ConvertIntermediateToDeliveryApiObject(Mock.Of(), publishedPropertyType, PropertyCacheLevel.Element, inter, false) as IEnumerable; + var result = valueConverter.ConvertIntermediateToDeliveryApiObject(Mock.Of(), publishedPropertyType, PropertyCacheLevel.Element, inter, false, false) as IEnumerable; Assert.NotNull(result); Assert.AreEqual(1, result.Count()); @@ -65,7 +65,7 @@ public class MediaPickerValueConverterTests : PropertyValueConverterTests var inter = new[] { new GuidUdi(Constants.UdiEntityType.MediaType, PublishedMedia.Key), new GuidUdi(Constants.UdiEntityType.MediaType, otherMediaKey) }; - var result = valueConverter.ConvertIntermediateToDeliveryApiObject(Mock.Of(), publishedPropertyType, PropertyCacheLevel.Element, inter, false) as IEnumerable; + var result = valueConverter.ConvertIntermediateToDeliveryApiObject(Mock.Of(), publishedPropertyType, PropertyCacheLevel.Element, inter, false, false) as IEnumerable; Assert.NotNull(result); Assert.AreEqual(2, result.Count()); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/MediaPickerWithCropsValueConverterTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/MediaPickerWithCropsValueConverterTests.cs index c02526054e..e39d9d83e5 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/MediaPickerWithCropsValueConverterTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/MediaPickerWithCropsValueConverterTests.cs @@ -59,7 +59,7 @@ public class MediaPickerWithCropsValueConverterTests : PropertyValueConverterTes } }); - var result = valueConverter.ConvertIntermediateToDeliveryApiObject(Mock.Of(), publishedPropertyType, PropertyCacheLevel.Element, inter, false) as IEnumerable; + var result = valueConverter.ConvertIntermediateToDeliveryApiObject(Mock.Of(), publishedPropertyType, PropertyCacheLevel.Element, inter, false, false) as IEnumerable; Assert.NotNull(result); Assert.AreEqual(1, result.Count()); var first = result.Single(); @@ -117,7 +117,7 @@ public class MediaPickerWithCropsValueConverterTests : PropertyValueConverterTes } }); - var result = valueConverter.ConvertIntermediateToDeliveryApiObject(Mock.Of(), publishedPropertyType, PropertyCacheLevel.Element, inter, false) as IEnumerable; + var result = valueConverter.ConvertIntermediateToDeliveryApiObject(Mock.Of(), publishedPropertyType, PropertyCacheLevel.Element, inter, false, false) as IEnumerable; Assert.NotNull(result); Assert.AreEqual(2, result.Count()); var first = result.First(); @@ -184,7 +184,7 @@ public class MediaPickerWithCropsValueConverterTests : PropertyValueConverterTes } }); - var result = valueConverter.ConvertIntermediateToDeliveryApiObject(Mock.Of(), publishedPropertyType, PropertyCacheLevel.Element, inter, false) as IEnumerable; + var result = valueConverter.ConvertIntermediateToDeliveryApiObject(Mock.Of(), publishedPropertyType, PropertyCacheLevel.Element, inter, false, false) as IEnumerable; Assert.NotNull(result); Assert.AreEqual(1, result.Count()); var mediaWithCrops = result.Single(); @@ -246,7 +246,7 @@ public class MediaPickerWithCropsValueConverterTests : PropertyValueConverterTes } }); - var result = valueConverter.ConvertIntermediateToDeliveryApiObject(Mock.Of(), publishedPropertyType, PropertyCacheLevel.Element, inter, false) as IEnumerable; + var result = valueConverter.ConvertIntermediateToDeliveryApiObject(Mock.Of(), publishedPropertyType, PropertyCacheLevel.Element, inter, false, false) as IEnumerable; Assert.NotNull(result); Assert.AreEqual(1, result.Count()); var mediaWithCrops = result.Single(); @@ -273,7 +273,7 @@ public class MediaPickerWithCropsValueConverterTests : PropertyValueConverterTes var valueConverter = MediaPickerWithCropsValueConverter(); - var result = valueConverter.ConvertIntermediateToDeliveryApiObject(Mock.Of(), publishedPropertyType, PropertyCacheLevel.Element, inter, false) as IEnumerable; + var result = valueConverter.ConvertIntermediateToDeliveryApiObject(Mock.Of(), publishedPropertyType, PropertyCacheLevel.Element, inter, false, false) as IEnumerable; Assert.NotNull(result); Assert.IsEmpty(result); } @@ -288,7 +288,7 @@ public class MediaPickerWithCropsValueConverterTests : PropertyValueConverterTes var valueConverter = MediaPickerWithCropsValueConverter(); - var result = valueConverter.ConvertIntermediateToDeliveryApiObject(Mock.Of(), publishedPropertyType, PropertyCacheLevel.Element, inter, false) as IEnumerable; + var result = valueConverter.ConvertIntermediateToDeliveryApiObject(Mock.Of(), publishedPropertyType, PropertyCacheLevel.Element, inter, false, false) as IEnumerable; Assert.NotNull(result); Assert.IsEmpty(result); } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/MultiNodeTreePickerValueConverterTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/MultiNodeTreePickerValueConverterTests.cs index 276df9a223..b38aa33c8a 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/MultiNodeTreePickerValueConverterTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/MultiNodeTreePickerValueConverterTests.cs @@ -21,7 +21,7 @@ public class MultiNodeTreePickerValueConverterTests : PropertyValueConverterTest var contentNameProvider = new ApiContentNameProvider(); var apiUrProvider = new ApiMediaUrlProvider(PublishedUrlProvider); - routeBuilder = routeBuilder ?? new ApiContentRouteBuilder(PublishedUrlProvider, CreateGlobalSettings(), Mock.Of(), Mock.Of()); + routeBuilder = routeBuilder ?? CreateContentRouteBuilder(PublishedUrlProvider, CreateGlobalSettings()); return new MultiNodeTreePickerValueConverter( PublishedSnapshotAccessor, Mock.Of(), @@ -52,7 +52,7 @@ public class MultiNodeTreePickerValueConverterTests : PropertyValueConverterTest Assert.AreEqual(typeof(IEnumerable), valueConverter.GetDeliveryApiPropertyValueType(publishedPropertyType.Object)); var inter = new Udi[] { new GuidUdi(Constants.UdiEntityType.Document, PublishedContent.Key) }; - var result = valueConverter.ConvertIntermediateToDeliveryApiObject(Mock.Of(), publishedPropertyType.Object, PropertyCacheLevel.Element, inter, false) as IEnumerable; + var result = valueConverter.ConvertIntermediateToDeliveryApiObject(Mock.Of(), publishedPropertyType.Object, PropertyCacheLevel.Element, inter, false, false) as IEnumerable; Assert.NotNull(result); Assert.AreEqual(1, result.Count()); Assert.AreEqual(PublishedContent.Name, result.First().Name); @@ -78,7 +78,7 @@ public class MultiNodeTreePickerValueConverterTests : PropertyValueConverterTest Assert.AreEqual(typeof(IEnumerable), valueConverter.GetDeliveryApiPropertyValueType(publishedPropertyType.Object)); var inter = new Udi[] { new GuidUdi(Constants.UdiEntityType.Document, PublishedContent.Key), new GuidUdi(Constants.UdiEntityType.Document, otherContentKey) }; - var result = valueConverter.ConvertIntermediateToDeliveryApiObject(Mock.Of(), publishedPropertyType.Object, PropertyCacheLevel.Element, inter, false) as IEnumerable; + var result = valueConverter.ConvertIntermediateToDeliveryApiObject(Mock.Of(), publishedPropertyType.Object, PropertyCacheLevel.Element, inter, false, false) as IEnumerable; Assert.NotNull(result); Assert.AreEqual(2, result.Count()); @@ -123,7 +123,7 @@ public class MultiNodeTreePickerValueConverterTests : PropertyValueConverterTest Assert.AreEqual(typeof(IEnumerable), valueConverter.GetDeliveryApiPropertyValueType(publishedPropertyType.Object)); var inter = new Udi[] { new GuidUdi(Constants.UdiEntityType.Document, key) }; - var result = valueConverter.ConvertIntermediateToDeliveryApiObject(Mock.Of(), publishedPropertyType.Object, PropertyCacheLevel.Element, inter, false) as IEnumerable; + var result = valueConverter.ConvertIntermediateToDeliveryApiObject(Mock.Of(), publishedPropertyType.Object, PropertyCacheLevel.Element, inter, false, false) as IEnumerable; Assert.NotNull(result); Assert.AreEqual(1, result.Count()); Assert.AreEqual("The page", result.First().Name); @@ -147,7 +147,7 @@ public class MultiNodeTreePickerValueConverterTests : PropertyValueConverterTest Assert.AreEqual(typeof(IEnumerable), valueConverter.GetDeliveryApiPropertyValueType(publishedPropertyType.Object)); var inter = new Udi[] { new GuidUdi(Constants.UdiEntityType.Media, PublishedMedia.Key) }; - var result = valueConverter.ConvertIntermediateToDeliveryApiObject(Mock.Of(), publishedPropertyType.Object, PropertyCacheLevel.Element, inter, false) as IEnumerable; + var result = valueConverter.ConvertIntermediateToDeliveryApiObject(Mock.Of(), publishedPropertyType.Object, PropertyCacheLevel.Element, inter, false, false) as IEnumerable; Assert.NotNull(result); Assert.AreEqual(1, result.Count()); Assert.AreEqual(PublishedMedia.Name, result.First().Name); @@ -172,7 +172,7 @@ public class MultiNodeTreePickerValueConverterTests : PropertyValueConverterTest Assert.AreEqual(typeof(IEnumerable), valueConverter.GetDeliveryApiPropertyValueType(publishedPropertyType.Object)); var inter = new Udi[] { new GuidUdi(Constants.UdiEntityType.Media, PublishedMedia.Key), new GuidUdi(Constants.UdiEntityType.Media, otherMediaKey) }; - var result = valueConverter.ConvertIntermediateToDeliveryApiObject(Mock.Of(), publishedPropertyType.Object, PropertyCacheLevel.Element, inter, false) as IEnumerable; + var result = valueConverter.ConvertIntermediateToDeliveryApiObject(Mock.Of(), publishedPropertyType.Object, PropertyCacheLevel.Element, inter, false, false) as IEnumerable; Assert.NotNull(result); Assert.AreEqual(2, result.Count()); Assert.AreEqual(PublishedMedia.Name, result.First().Name); @@ -197,7 +197,7 @@ public class MultiNodeTreePickerValueConverterTests : PropertyValueConverterTest Assert.AreEqual(typeof(IEnumerable), valueConverter.GetDeliveryApiPropertyValueType(publishedPropertyType.Object)); var inter = new Udi[] { new GuidUdi(Constants.UdiEntityType.Media, PublishedMedia.Key), new GuidUdi(Constants.UdiEntityType.Document, PublishedContent.Key) }; - var result = valueConverter.ConvertIntermediateToDeliveryApiObject(Mock.Of(), publishedPropertyType.Object, PropertyCacheLevel.Element, inter, false) as IEnumerable; + var result = valueConverter.ConvertIntermediateToDeliveryApiObject(Mock.Of(), publishedPropertyType.Object, PropertyCacheLevel.Element, inter, false, false) as IEnumerable; Assert.NotNull(result); Assert.AreEqual(1, result.Count()); Assert.AreEqual(PublishedContent.Name, result.First().Name); @@ -218,7 +218,7 @@ public class MultiNodeTreePickerValueConverterTests : PropertyValueConverterTest Assert.AreEqual(typeof(IEnumerable), valueConverter.GetDeliveryApiPropertyValueType(publishedPropertyType.Object)); var inter = new Udi[] { new GuidUdi(Constants.UdiEntityType.Media, PublishedMedia.Key), new GuidUdi(Constants.UdiEntityType.Document, PublishedContent.Key) }; - var result = valueConverter.ConvertIntermediateToDeliveryApiObject(Mock.Of(), publishedPropertyType.Object, PropertyCacheLevel.Element, inter, false) as IEnumerable; + var result = valueConverter.ConvertIntermediateToDeliveryApiObject(Mock.Of(), publishedPropertyType.Object, PropertyCacheLevel.Element, inter, false, false) as IEnumerable; Assert.NotNull(result); Assert.AreEqual(1, result.Count()); Assert.AreEqual(PublishedMedia.Name, result.First().Name); @@ -240,7 +240,7 @@ public class MultiNodeTreePickerValueConverterTests : PropertyValueConverterTest Assert.AreEqual(typeof(string), valueConverter.GetDeliveryApiPropertyValueType(publishedPropertyType.Object)); var inter = new Udi[] { new GuidUdi(Constants.UdiEntityType.Media, PublishedMedia.Key), new GuidUdi(Constants.UdiEntityType.Document, PublishedContent.Key) }; - var result = valueConverter.ConvertIntermediateToDeliveryApiObject(Mock.Of(), publishedPropertyType.Object, PropertyCacheLevel.Element, inter, false) as string; + var result = valueConverter.ConvertIntermediateToDeliveryApiObject(Mock.Of(), publishedPropertyType.Object, PropertyCacheLevel.Element, inter, false, false) as string; Assert.NotNull(result); Assert.AreEqual("(unsupported)", result); } @@ -256,7 +256,7 @@ public class MultiNodeTreePickerValueConverterTests : PropertyValueConverterTest var valueConverter = MultiNodeTreePickerValueConverter(); - var result = valueConverter.ConvertIntermediateToDeliveryApiObject(Mock.Of(), publishedPropertyType.Object, PropertyCacheLevel.Element, inter, false) as IEnumerable; + var result = valueConverter.ConvertIntermediateToDeliveryApiObject(Mock.Of(), publishedPropertyType.Object, PropertyCacheLevel.Element, inter, false, false) as IEnumerable; Assert.NotNull(result); Assert.IsEmpty(result); } @@ -272,7 +272,7 @@ public class MultiNodeTreePickerValueConverterTests : PropertyValueConverterTest var valueConverter = MultiNodeTreePickerValueConverter(); - var result = valueConverter.ConvertIntermediateToDeliveryApiObject(Mock.Of(), publishedPropertyType.Object, PropertyCacheLevel.Element, inter, false) as IEnumerable; + var result = valueConverter.ConvertIntermediateToDeliveryApiObject(Mock.Of(), publishedPropertyType.Object, PropertyCacheLevel.Element, inter, false, false) as IEnumerable; Assert.NotNull(result); Assert.IsEmpty(result); } @@ -289,7 +289,7 @@ public class MultiNodeTreePickerValueConverterTests : PropertyValueConverterTest var valueConverter = MultiNodeTreePickerValueConverter(routeBuilder.Object); var inter = new Udi[] { new GuidUdi(Constants.UdiEntityType.Document, PublishedContent.Key) }; - var result = valueConverter.ConvertIntermediateToDeliveryApiObject(Mock.Of(), publishedPropertyType.Object, PropertyCacheLevel.Element, inter, false) as IEnumerable; + var result = valueConverter.ConvertIntermediateToDeliveryApiObject(Mock.Of(), publishedPropertyType.Object, PropertyCacheLevel.Element, inter, false, false) as IEnumerable; Assert.NotNull(result); Assert.IsEmpty(result); } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/MultiUrlPickerValueConverterTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/MultiUrlPickerValueConverterTests.cs index 970d3492e3..818f55861d 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/MultiUrlPickerValueConverterTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/MultiUrlPickerValueConverterTests.cs @@ -8,7 +8,6 @@ using Umbraco.Cms.Core.Models.DeliveryApi; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.PropertyEditors.ValueConverters; -using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Web; using Umbraco.Cms.Infrastructure.Serialization; @@ -35,7 +34,7 @@ public class MultiUrlPickerValueConverterTests : PropertyValueConverterTests Udi = new GuidUdi(Constants.UdiEntityType.Document, PublishedContent.Key) } }); - var result = valueConverter.ConvertIntermediateToDeliveryApiObject(Mock.Of(), publishedPropertyType.Object, PropertyCacheLevel.Element, inter, false) as IEnumerable; + var result = valueConverter.ConvertIntermediateToDeliveryApiObject(Mock.Of(), publishedPropertyType.Object, PropertyCacheLevel.Element, inter, false, false) as IEnumerable; Assert.NotNull(result); Assert.AreEqual(1, result.Count()); var link = result.First(); @@ -67,7 +66,7 @@ public class MultiUrlPickerValueConverterTests : PropertyValueConverterTests Udi = new GuidUdi(Constants.UdiEntityType.Media, PublishedMedia.Key) } }); - var result = valueConverter.ConvertIntermediateToDeliveryApiObject(Mock.Of(), publishedPropertyType.Object, PropertyCacheLevel.Element, inter, false) as IEnumerable; + var result = valueConverter.ConvertIntermediateToDeliveryApiObject(Mock.Of(), publishedPropertyType.Object, PropertyCacheLevel.Element, inter, false, false) as IEnumerable; Assert.NotNull(result); Assert.AreEqual(1, result.Count()); var link = result.First(); @@ -108,7 +107,7 @@ public class MultiUrlPickerValueConverterTests : PropertyValueConverterTests Url = "https://umbraco.com/" } }); - var result = valueConverter.ConvertIntermediateToDeliveryApiObject(Mock.Of(), publishedPropertyType.Object, PropertyCacheLevel.Element, inter, false) as IEnumerable; + var result = valueConverter.ConvertIntermediateToDeliveryApiObject(Mock.Of(), publishedPropertyType.Object, PropertyCacheLevel.Element, inter, false, false) as IEnumerable; Assert.NotNull(result); Assert.AreEqual(3, result.Count()); @@ -152,7 +151,7 @@ public class MultiUrlPickerValueConverterTests : PropertyValueConverterTests Url = "https://umbraco.com/" } }); - var result = valueConverter.ConvertIntermediateToDeliveryApiObject(Mock.Of(), publishedPropertyType.Object, PropertyCacheLevel.Element, inter, false) as IEnumerable; + var result = valueConverter.ConvertIntermediateToDeliveryApiObject(Mock.Of(), publishedPropertyType.Object, PropertyCacheLevel.Element, inter, false, false) as IEnumerable; Assert.NotNull(result); Assert.AreEqual(1, result.Count()); var link = result.First(); @@ -182,7 +181,7 @@ public class MultiUrlPickerValueConverterTests : PropertyValueConverterTests Target = "_blank" } }); - var result = valueConverter.ConvertIntermediateToDeliveryApiObject(Mock.Of(), publishedPropertyType.Object, PropertyCacheLevel.Element, inter, false) as IEnumerable; + var result = valueConverter.ConvertIntermediateToDeliveryApiObject(Mock.Of(), publishedPropertyType.Object, PropertyCacheLevel.Element, inter, false, false) as IEnumerable; Assert.NotNull(result); Assert.AreEqual(1, result.Count()); var link = result.First(); @@ -212,7 +211,7 @@ public class MultiUrlPickerValueConverterTests : PropertyValueConverterTests QueryString = "?something=true" } }); - var result = valueConverter.ConvertIntermediateToDeliveryApiObject(Mock.Of(), publishedPropertyType.Object, PropertyCacheLevel.Element, inter, false) as IEnumerable; + var result = valueConverter.ConvertIntermediateToDeliveryApiObject(Mock.Of(), publishedPropertyType.Object, PropertyCacheLevel.Element, inter, false, false) as IEnumerable; Assert.NotNull(result); Assert.AreEqual(1, result.Count()); var link = result.First(); @@ -235,7 +234,7 @@ public class MultiUrlPickerValueConverterTests : PropertyValueConverterTests var valueConverter = MultiUrlPickerValueConverter(); - var result = valueConverter.ConvertIntermediateToDeliveryApiObject(Mock.Of(), publishedPropertyType.Object, PropertyCacheLevel.Element, inter, false) as IEnumerable; + var result = valueConverter.ConvertIntermediateToDeliveryApiObject(Mock.Of(), publishedPropertyType.Object, PropertyCacheLevel.Element, inter, false, false) as IEnumerable; Assert.NotNull(result); Assert.IsEmpty(result); } @@ -251,7 +250,7 @@ public class MultiUrlPickerValueConverterTests : PropertyValueConverterTests var valueConverter = MultiUrlPickerValueConverter(); - var result = valueConverter.ConvertIntermediateToDeliveryApiObject(Mock.Of(), publishedPropertyType.Object, PropertyCacheLevel.Element, inter, false) as IEnumerable; + var result = valueConverter.ConvertIntermediateToDeliveryApiObject(Mock.Of(), publishedPropertyType.Object, PropertyCacheLevel.Element, inter, false, false) as IEnumerable; Assert.NotNull(result); Assert.IsEmpty(result); } @@ -260,7 +259,7 @@ public class MultiUrlPickerValueConverterTests : PropertyValueConverterTests private MultiUrlPickerValueConverter MultiUrlPickerValueConverter() { - var routeBuilder = new ApiContentRouteBuilder(PublishedUrlProvider, CreateGlobalSettings(), Mock.Of(), Mock.Of()); + var routeBuilder = CreateContentRouteBuilder(PublishedUrlProvider, CreateGlobalSettings()); return new MultiUrlPickerValueConverter( PublishedSnapshotAccessor, Mock.Of(), diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/NestedContentValueConverterTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/NestedContentValueConverterTests.cs index c8d2b5d9ad..aa31842bea 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/NestedContentValueConverterTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/NestedContentValueConverterTests.cs @@ -44,8 +44,8 @@ public class NestedContentValueConverterTests : PropertyValueConverterTests .Setup(p => p.ConvertSourceToInter(It.IsAny(), It.IsAny(), It.IsAny())) .Returns((IPublishedElement owner, object? source, bool preview) => source); publishedPropertyType - .Setup(p => p.ConvertInterToDeliveryApiObject(It.IsAny(), PropertyCacheLevel.Element, It.IsAny(), It.IsAny())) - .Returns((IPublishedElement owner, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) => inter?.ToString()); + .Setup(p => p.ConvertInterToDeliveryApiObject(It.IsAny(), PropertyCacheLevel.Element, It.IsAny(), It.IsAny(), It.IsAny())) + .Returns((IPublishedElement owner, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview, bool expanding) => inter?.ToString()); _publishedPropertyType = publishedPropertyType.Object; var publishedContentType = new Mock(); @@ -69,7 +69,7 @@ public class NestedContentValueConverterTests : PropertyValueConverterTests public void NestedContentSingleValueConverter_WithOneItem_ConvertsItemToListOfElements() { var nestedContentValue = "[{\"ncContentTypeAlias\": \"contentType1\",\"key\": \"1E68FB92-727A-4473-B10C-FA108ADCF16F\",\"prop1\": \"Hello, world\"}]"; - var result = _nestedContentSingleValueConverter.ConvertIntermediateToDeliveryApiObject(Mock.Of(), _publishedPropertyType, PropertyCacheLevel.Element, nestedContentValue, false) as IEnumerable; + var result = _nestedContentSingleValueConverter.ConvertIntermediateToDeliveryApiObject(Mock.Of(), _publishedPropertyType, PropertyCacheLevel.Element, nestedContentValue, false, false) as IEnumerable; Assert.IsNotNull(result); Assert.AreEqual(1, result.Count()); @@ -83,7 +83,7 @@ public class NestedContentValueConverterTests : PropertyValueConverterTests public void NestedContentSingleValueConverter_WithMultipleItems_ConvertsFirstItemToListOfElements() { var nestedContentValue = "[{\"ncContentTypeAlias\": \"contentType1\",\"key\": \"1E68FB92-727A-4473-B10C-FA108ADCF16F\",\"prop1\": \"Hello, world\"},{\"ncContentTypeAlias\": \"contentType1\",\"key\": \"40F59DD9-7E9F-4053-BD32-89FB086D18C9\",\"prop1\": \"One more\"}]"; - var result = _nestedContentSingleValueConverter.ConvertIntermediateToDeliveryApiObject(Mock.Of(), _publishedPropertyType, PropertyCacheLevel.Element, nestedContentValue, false) as IEnumerable; + var result = _nestedContentSingleValueConverter.ConvertIntermediateToDeliveryApiObject(Mock.Of(), _publishedPropertyType, PropertyCacheLevel.Element, nestedContentValue, false, false) as IEnumerable; Assert.IsNotNull(result); Assert.AreEqual(1, result.Count()); @@ -96,7 +96,7 @@ public class NestedContentValueConverterTests : PropertyValueConverterTests [Test] public void NestedContentSingleValueConverter_WithNoData_ReturnsEmptyArray() { - var result = _nestedContentSingleValueConverter.ConvertIntermediateToDeliveryApiObject(Mock.Of(), _publishedPropertyType, PropertyCacheLevel.Element, null, false) as IEnumerable; + var result = _nestedContentSingleValueConverter.ConvertIntermediateToDeliveryApiObject(Mock.Of(), _publishedPropertyType, PropertyCacheLevel.Element, null, false, false) as IEnumerable; Assert.IsNotNull(result); Assert.IsEmpty(result); @@ -111,7 +111,7 @@ public class NestedContentValueConverterTests : PropertyValueConverterTests public void NestedContentManyValueConverter_WithOneItem_ConvertsItemToListOfElements() { var nestedContentValue = "[{\"ncContentTypeAlias\": \"contentType1\",\"key\": \"1E68FB92-727A-4473-B10C-FA108ADCF16F\",\"prop1\": \"Hello, world\"}]"; - var result = _nestedContentManyValueConverter.ConvertIntermediateToDeliveryApiObject(Mock.Of(), _publishedPropertyType, PropertyCacheLevel.Element, nestedContentValue, false) as IEnumerable; + var result = _nestedContentManyValueConverter.ConvertIntermediateToDeliveryApiObject(Mock.Of(), _publishedPropertyType, PropertyCacheLevel.Element, nestedContentValue, false, false) as IEnumerable; Assert.IsNotNull(result); Assert.AreEqual(1, result.Count()); @@ -126,7 +126,7 @@ public class NestedContentValueConverterTests : PropertyValueConverterTests public void NestedContentManyValueConverter_WithMultipleItems_ConvertsAllItemsToElements() { var nestedContentValue = "[{\"ncContentTypeAlias\": \"contentType1\",\"key\": \"1E68FB92-727A-4473-B10C-FA108ADCF16F\",\"prop1\": \"Hello, world\"},{\"ncContentTypeAlias\": \"contentType1\",\"key\": \"40F59DD9-7E9F-4053-BD32-89FB086D18C9\",\"prop1\": \"One more\"}]"; - var result = _nestedContentManyValueConverter.ConvertIntermediateToDeliveryApiObject(Mock.Of(), _publishedPropertyType, PropertyCacheLevel.Element, nestedContentValue, false) as IEnumerable; + var result = _nestedContentManyValueConverter.ConvertIntermediateToDeliveryApiObject(Mock.Of(), _publishedPropertyType, PropertyCacheLevel.Element, nestedContentValue, false, false) as IEnumerable; Assert.IsNotNull(result); Assert.AreEqual(2, result.Count()); @@ -145,7 +145,7 @@ public class NestedContentValueConverterTests : PropertyValueConverterTests [Test] public void NestedContentManyValueConverter_WithNoData_ReturnsEmptyArray() { - var result = _nestedContentManyValueConverter.ConvertIntermediateToDeliveryApiObject(Mock.Of(), _publishedPropertyType, PropertyCacheLevel.Element, null, false) as IEnumerable; + var result = _nestedContentManyValueConverter.ConvertIntermediateToDeliveryApiObject(Mock.Of(), _publishedPropertyType, PropertyCacheLevel.Element, null, false, false) as IEnumerable; Assert.IsNotNull(result); Assert.IsEmpty(result); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/OutputExpansionStrategyTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/OutputExpansionStrategyTests.cs index fe3a9bdf71..1e12eef746 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/OutputExpansionStrategyTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/OutputExpansionStrategyTests.cs @@ -410,6 +410,39 @@ public class OutputExpansionStrategyTests : PropertyValueConverterTests Assert.Throws(() => outputExpansionStrategy.MapMediaProperties(PublishedContent)); } + [TestCase(true)] + [TestCase(false)] + public void OutputExpansionStrategy_ForwardsExpansionStateToPropertyValueConverter(bool expanding) + { + var accessor = CreateOutputExpansionStrategyAccessor(false, new[] { expanding ? "theAlias" : "noSuchAlias" }); + var apiContentBuilder = new ApiContentBuilder(new ApiContentNameProvider(), ApiContentRouteBuilder(), accessor); + + var content = new Mock(); + + var valueConverterMock = new Mock(); + valueConverterMock.Setup(v => v.IsConverter(It.IsAny())).Returns(true); + valueConverterMock.Setup(v => v.GetPropertyCacheLevel(It.IsAny())).Returns(PropertyCacheLevel.None); + valueConverterMock.Setup(v => v.GetDeliveryApiPropertyCacheLevel(It.IsAny())).Returns(PropertyCacheLevel.None); + valueConverterMock.Setup(v => v.ConvertIntermediateToDeliveryApiObject( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns(expanding ? "Expanding" : "Not expanding"); + + var propertyType = SetupPublishedPropertyType(valueConverterMock.Object, "theAlias", Constants.PropertyEditors.Aliases.Label); + var property = new PublishedElementPropertyBase(propertyType, content.Object, false, PropertyCacheLevel.None, "The Value"); + + SetupContentMock(content, property); + + var result = apiContentBuilder.Build(content.Object); + + Assert.AreEqual(1, result.Properties.Count); + Assert.AreEqual(expanding ? "Expanding" : "Not expanding", result.Properties["theAlias"] as string); + } + private IOutputExpansionStrategyAccessor CreateOutputExpansionStrategyAccessor(bool expandAll = false, string[]? expandPropertyAliases = null) { var httpContextMock = new Mock(); @@ -547,6 +580,7 @@ public class OutputExpansionStrategyTests : PropertyValueConverterTests It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny())) .Returns(() => apiElementBuilder.Build(element.Object)); elementValueConverter.Setup(p => p.IsConverter(It.IsAny())).Returns(true); @@ -557,5 +591,5 @@ public class OutputExpansionStrategyTests : PropertyValueConverterTests return new PublishedElementPropertyBase(elementPropertyType, parent, false, PropertyCacheLevel.None); } - private IApiContentRouteBuilder ApiContentRouteBuilder() => new ApiContentRouteBuilder(PublishedUrlProvider, CreateGlobalSettings(), Mock.Of(), Mock.Of()); + private IApiContentRouteBuilder ApiContentRouteBuilder() => CreateContentRouteBuilder(PublishedUrlProvider, CreateGlobalSettings()); } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/PublishedPropertyTypeTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/PublishedPropertyTypeTests.cs index ebeb83a6a0..eb7b39a778 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/PublishedPropertyTypeTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/PublishedPropertyTypeTests.cs @@ -11,7 +11,7 @@ public class PublishedPropertyTypeTests : DeliveryApiTests [Test] public void PropertyDeliveryApiValue_UsesDeliveryApiValueForDeliveryApiOutput() { - var result = DeliveryApiPropertyType.ConvertInterToDeliveryApiObject(new Mock().Object, PropertyCacheLevel.None, null, false); + var result = DeliveryApiPropertyType.ConvertInterToDeliveryApiObject(new Mock().Object, PropertyCacheLevel.None, null, false, false); Assert.NotNull(result); Assert.AreEqual("Delivery API value", result); } @@ -27,7 +27,7 @@ public class PublishedPropertyTypeTests : DeliveryApiTests [Test] public void NonDeliveryApiPropertyValueConverter_PerformsFallbackToDefaultValueForDeliveryApiOutput() { - var result = DefaultPropertyType.ConvertInterToDeliveryApiObject(new Mock().Object, PropertyCacheLevel.None, null, false); + var result = DefaultPropertyType.ConvertInterToDeliveryApiObject(new Mock().Object, PropertyCacheLevel.None, null, false, false); Assert.NotNull(result); Assert.AreEqual("Default value", result); } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/RichTextParserTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/RichTextParserTests.cs index eff3e8ebb9..2fef067c4c 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/RichTextParserTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/RichTextParserTests.cs @@ -7,7 +7,7 @@ using Umbraco.Cms.Core.Models.DeliveryApi; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Routing; -using ApiRichTextParser = Umbraco.Cms.Infrastructure.DeliveryApi.ApiRichTextParser; +using Umbraco.Cms.Infrastructure.DeliveryApi; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.DeliveryApi; @@ -19,9 +19,9 @@ public class RichTextParserTests private readonly Guid _mediaKey = Guid.NewGuid(); [Test] - public void DocumentElementIsCalledRoot() + public void ParseElement_DocumentElementIsCalledRoot() { - var parser = CreateRichTextParser(); + var parser = CreateRichTextElementParser(); var element = parser.Parse("

Hello

"); Assert.IsNotNull(element); @@ -29,9 +29,9 @@ public class RichTextParserTests } [Test] - public void SimpleParagraphHasSingleTextElement() + public void ParseElement_SimpleParagraphHasSingleTextElement() { - var parser = CreateRichTextParser(); + var parser = CreateRichTextElementParser(); var element = parser.Parse("

Some text paragraph

") as RichTextGenericElement; Assert.IsNotNull(element); @@ -45,9 +45,9 @@ public class RichTextParserTests } [Test] - public void ParagraphWithLineBreaksWrapsTextInElements() + public void ParseElement_ParagraphWithLineBreaksWrapsTextInElements() { - var parser = CreateRichTextParser(); + var parser = CreateRichTextElementParser(); var element = parser.Parse("

Some text
More text
Even more text

") as RichTextGenericElement; Assert.IsNotNull(element); @@ -93,9 +93,9 @@ public class RichTextParserTests } [Test] - public void DataAttributesAreSanitized() + public void ParseElement_DataAttributesAreSanitized() { - var parser = CreateRichTextParser(); + var parser = CreateRichTextElementParser(); var element = parser.Parse("

Text in a data-something SPAN

") as RichTextGenericElement; Assert.IsNotNull(element); @@ -111,9 +111,9 @@ public class RichTextParserTests } [Test] - public void DataAttributesDoNotOverwriteExistingAttributes() + public void ParseElement_DataAttributesDoNotOverwriteExistingAttributes() { - var parser = CreateRichTextParser(); + var parser = CreateRichTextElementParser(); var element = parser.Parse("

Text in a data-something SPAN

") as RichTextGenericElement; Assert.IsNotNull(element); @@ -126,9 +126,9 @@ public class RichTextParserTests } [Test] - public void CanParseContentLink() + public void ParseElement_CanParseContentLink() { - var parser = CreateRichTextParser(); + var parser = CreateRichTextElementParser(); var element = parser.Parse($"

") as RichTextGenericElement; Assert.IsNotNull(element); @@ -145,9 +145,9 @@ public class RichTextParserTests } [Test] - public void CanParseMediaLink() + public void ParseElement_CanParseMediaLink() { - var parser = CreateRichTextParser(); + var parser = CreateRichTextElementParser(); var element = parser.Parse($"

") as RichTextGenericElement; Assert.IsNotNull(element); @@ -160,9 +160,9 @@ public class RichTextParserTests } [Test] - public void CanHandleNonLocalLink() + public void ParseElement_CanHandleNonLocalLink() { - var parser = CreateRichTextParser(); + var parser = CreateRichTextElementParser(); var element = parser.Parse($"

") as RichTextGenericElement; Assert.IsNotNull(element); @@ -175,9 +175,9 @@ public class RichTextParserTests } [Test] - public void LinkTextIsWrappedInTextElement() + public void ParseElement_LinkTextIsWrappedInTextElement() { - var parser = CreateRichTextParser(); + var parser = CreateRichTextElementParser(); var element = parser.Parse($"

This is the link text

") as RichTextGenericElement; Assert.IsNotNull(element); @@ -191,9 +191,9 @@ public class RichTextParserTests [TestCase("{localLink:umb://document/fe5bf80d37db4373adb9b206896b4a3b}")] [TestCase("{localLink:umb://media/03b9a8721c4749a9a7026033ec78d860}")] - public void InvalidLocalLinkYieldsEmptyLink(string href) + public void ParseElement_InvalidLocalLinkYieldsEmptyLink(string href) { - var parser = CreateRichTextParser(); + var parser = CreateRichTextElementParser(); var element = parser.Parse($"

") as RichTextGenericElement; Assert.IsNotNull(element); @@ -204,11 +204,11 @@ public class RichTextParserTests } [Test] - public void CanParseMediaImage() + public void ParseElement_CanParseMediaImage() { - var parser = CreateRichTextParser(); + var parser = CreateRichTextElementParser(); - var element = parser.Parse($"

") as RichTextGenericElement; + var element = parser.Parse($"

") as RichTextGenericElement; Assert.IsNotNull(element); var link = element.Elements.OfType().Single().Elements.Single() as RichTextGenericElement; Assert.IsNotNull(link); @@ -219,11 +219,11 @@ public class RichTextParserTests } [Test] - public void CanHandleNonLocalImage() + public void ParseElement_CanHandleNonLocalImage() { - var parser = CreateRichTextParser(); + var parser = CreateRichTextElementParser(); - var element = parser.Parse($"

") as RichTextGenericElement; + var element = parser.Parse($"

") as RichTextGenericElement; Assert.IsNotNull(element); var link = element.Elements.OfType().Single().Elements.Single() as RichTextGenericElement; Assert.IsNotNull(link); @@ -233,7 +233,99 @@ public class RichTextParserTests Assert.AreEqual("https://some.where/something.png?rmode=max&width=500", link.Attributes.First().Value); } - private ApiRichTextParser CreateRichTextParser() + [Test] + public void ParseMarkup_CanParseContentLink() + { + var parser = CreateRichTextMarkupParser(); + + var result = parser.Parse($"

"); + Assert.IsTrue(result.Contains("href=\"/some-content-path\"")); + Assert.IsTrue(result.Contains("data-start-item-path=\"the-root-path\"")); + Assert.IsTrue(result.Contains($"data-start-item-id=\"{_contentRootKey:D}\"")); + } + + [Test] + public void ParseMarkup_CanParseMediaLink() + { + var parser = CreateRichTextMarkupParser(); + + var result = parser.Parse($"

"); + Assert.IsTrue(result.Contains("href=\"/some-media-url\"")); + } + + [TestCase("{localLink:umb://document/fe5bf80d37db4373adb9b206896b4a3b}")] + [TestCase("{localLink:umb://media/03b9a8721c4749a9a7026033ec78d860}")] + public void ParseMarkup_InvalidLocalLinkYieldsEmptyLink(string href) + { + var parser = CreateRichTextMarkupParser(); + + var result = parser.Parse($"

"); + Assert.AreEqual($"

", result); + } + + [TestCase("

")] + [TestCase("

")] + public void ParseMarkup_CanHandleNonLocalReferences(string html) + { + var parser = CreateRichTextMarkupParser(); + + var result = parser.Parse(html); + Assert.AreEqual(html, result); + } + + [Test] + public void ParseMarkup_CanParseMediaImage() + { + var parser = CreateRichTextMarkupParser(); + + var result = parser.Parse($"

"); + Assert.IsTrue(result.Contains("src=\"/some-media-url?rmode=max&width=500\"")); + Assert.IsFalse(result.Contains("data-udi")); + } + + [Test] + public void ParseMarkup_RemovesMediaDataCaption() + { + var parser = CreateRichTextMarkupParser(); + + var result = parser.Parse($"

"); + Assert.IsTrue(result.Contains("src=\"/some-media-url?rmode=max&width=500\"")); + Assert.IsFalse(result.Contains("data-udi")); + } + + [Test] + public void ParseMarkup_DataAttributesAreRetained() + { + var parser = CreateRichTextMarkupParser(); + + const string html = "

Text in a data-something SPAN

"; + var result = parser.Parse(html); + Assert.AreEqual(html, result); + } + + private ApiRichTextElementParser CreateRichTextElementParser() + { + SetupTestContent(out var routeBuilder, out var snapshotAccessor, out var urlProvider); + + return new ApiRichTextElementParser( + routeBuilder, + urlProvider, + snapshotAccessor, + Mock.Of>()); + } + + private ApiRichTextMarkupParser CreateRichTextMarkupParser() + { + SetupTestContent(out var routeBuilder, out var snapshotAccessor, out var urlProvider); + + return new ApiRichTextMarkupParser( + routeBuilder, + urlProvider, + snapshotAccessor, + Mock.Of>()); + } + + private void SetupTestContent(out IApiContentRouteBuilder routeBuilder, out IPublishedSnapshotAccessor snapshotAccessor, out IPublishedUrlProvider urlProvider) { var contentMock = new Mock(); contentMock.SetupGet(m => m.Key).Returns(_contentKey); @@ -266,10 +358,8 @@ public class RichTextParserTests .Setup(m => m.GetMediaUrl(mediaMock.Object, It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns("/some-media-url"); - return new ApiRichTextParser( - routeBuilderMock.Object, - snapshotAccessorMock.Object, - urlProviderMock.Object, - Mock.Of>()); + routeBuilder = routeBuilderMock.Object; + snapshotAccessor = snapshotAccessorMock.Object; + urlProvider = urlProviderMock.Object; } } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Published/NestedContentTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Published/NestedContentTests.cs index 86f40b624c..1ad2e2be76 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Published/NestedContentTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Published/NestedContentTests.cs @@ -279,6 +279,6 @@ public class NestedContentTests throw new InvalidOperationException("This method won't be implemented."); public override object GetDeliveryApiValue(bool expanding, string culture = null, string segment = null) => - PropertyType.ConvertInterToDeliveryApiObject(_owner, ReferenceCacheLevel, InterValue, _preview); + PropertyType.ConvertInterToDeliveryApiObject(_owner, ReferenceCacheLevel, InterValue, _preview, false); } } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Telemetry/TelemetryServiceTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Telemetry/TelemetryServiceTests.cs index 4b192d556f..9190468958 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Telemetry/TelemetryServiceTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Telemetry/TelemetryServiceTests.cs @@ -1,10 +1,8 @@ -using System.Collections.Generic; -using System.Linq; using Moq; using NUnit.Framework; using Umbraco.Cms.Core.Configuration; -using Umbraco.Cms.Core.Manifest; using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Packaging; using Umbraco.Cms.Core.Semver; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Telemetry; @@ -21,7 +19,7 @@ public class TelemetryServiceTests var siteIdentifierServiceMock = new Mock(); var usageInformationServiceMock = new Mock(); var sut = new TelemetryService( - Mock.Of(), + Mock.Of(), version, siteIdentifierServiceMock.Object, usageInformationServiceMock.Object, @@ -37,7 +35,7 @@ public class TelemetryServiceTests { var version = CreateUmbracoVersion(9, 3, 1); var sut = new TelemetryService( - Mock.Of(), + Mock.Of(), version, CreateSiteIdentifierService(false), Mock.Of(), @@ -57,7 +55,7 @@ public class TelemetryServiceTests var metricsConsentService = new Mock(); metricsConsentService.Setup(x => x.GetConsentLevel()).Returns(TelemetryLevel.Detailed); var sut = new TelemetryService( - Mock.Of(), + Mock.Of(), version, CreateSiteIdentifierService(), Mock.Of(), @@ -73,19 +71,20 @@ public class TelemetryServiceTests public void CanGatherPackageTelemetry() { var version = CreateUmbracoVersion(9, 1, 1); - var versionPackageName = "VersionPackage"; + var versionPackageId = "VersionPackageId"; + var versionPackageName = "VersionPackageName"; var packageVersion = "1.0.0"; - var noVersionPackageName = "NoVersionPackage"; - PackageManifest[] manifests = + var noVersionPackageName = "NoVersionPackageName"; + InstalledPackage[] installedPackages = { - new() { PackageName = versionPackageName, Version = packageVersion }, + new() { PackageId = versionPackageId, PackageName = versionPackageName, Version = packageVersion }, new() { PackageName = noVersionPackageName }, }; - var manifestParser = CreateManifestParser(manifests); + var packagingService = CreatePackagingService(installedPackages); var metricsConsentService = new Mock(); metricsConsentService.Setup(x => x.GetConsentLevel()).Returns(TelemetryLevel.Detailed); var sut = new TelemetryService( - manifestParser, + packagingService, version, CreateSiteIdentifierService(), Mock.Of(), @@ -98,12 +97,14 @@ public class TelemetryServiceTests { Assert.AreEqual(2, telemetry.Packages.Count()); var versionPackage = telemetry.Packages.FirstOrDefault(x => x.Name == versionPackageName); + Assert.AreEqual(versionPackageId, versionPackage.Id); Assert.AreEqual(versionPackageName, versionPackage.Name); Assert.AreEqual(packageVersion, versionPackage.Version); var noVersionPackage = telemetry.Packages.FirstOrDefault(x => x.Name == noVersionPackageName); + Assert.AreEqual(null, noVersionPackage.Id); Assert.AreEqual(noVersionPackageName, noVersionPackage.Name); - Assert.AreEqual(string.Empty, noVersionPackage.Version); + Assert.AreEqual(null, noVersionPackage.Version); }); } @@ -111,16 +112,16 @@ public class TelemetryServiceTests public void RespectsAllowPackageTelemetry() { var version = CreateUmbracoVersion(9, 1, 1); - PackageManifest[] manifests = + InstalledPackage[] installedPackages = { new() { PackageName = "DoNotTrack", AllowPackageTelemetry = false }, new() { PackageName = "TrackingAllowed", AllowPackageTelemetry = true }, }; - var manifestParser = CreateManifestParser(manifests); + var packagingService = CreatePackagingService(installedPackages); var metricsConsentService = new Mock(); metricsConsentService.Setup(x => x.GetConsentLevel()).Returns(TelemetryLevel.Detailed); var sut = new TelemetryService( - manifestParser, + packagingService, version, CreateSiteIdentifierService(), Mock.Of(), @@ -136,11 +137,11 @@ public class TelemetryServiceTests }); } - private IManifestParser CreateManifestParser(IEnumerable manifests) + private IPackagingService CreatePackagingService(IEnumerable installedPackages) { - var manifestParserMock = new Mock(); - manifestParserMock.Setup(x => x.GetManifests()).Returns(manifests); - return manifestParserMock.Object; + var packagingServiceMock = new Mock(); + packagingServiceMock.Setup(x => x.GetAllInstalledPackages()).Returns(installedPackages); + return packagingServiceMock.Object; } private IUmbracoVersion CreateUmbracoVersion(int major, int minor, int patch, string prerelease = "", string build = "")