From eb0ec7bee89bd677428d9aa70aa87f33cd39f179 Mon Sep 17 00:00:00 2001 From: kjac Date: Mon, 13 Feb 2023 19:03:58 +0100 Subject: [PATCH] Initial implementation for extension manifests --- .../Package/AllPackagesController.cs | 35 ++++++ .../Package/PackageControllerBase.cs | 12 +++ .../PackageBuilderExtensions.cs | 15 +++ .../ManagementApiComposer.cs | 1 + ...ExtensionManifestViewModelMapDefinition.cs | 19 ++++ .../Serialization/SystemTextJsonSerializer.cs | 1 + .../Package/ExtensionManifestViewModel.cs | 10 ++ .../Manifest/ExtensionManifest.cs | 12 +++ .../Manifest/IExtensionManifestService.cs | 6 ++ .../Telemetry/ITelemetryService.cs | 12 ++- .../Telemetry/TelemetryService.cs | 100 +++++++++++------- .../UmbracoBuilder.CoreServices.cs | 4 +- .../Manifest/ExtensionManifestReader.cs | 90 ++++++++++++++++ .../Manifest/ExtensionManifestService.cs | 12 +++ .../Manifest/IExtensionManifestReader.cs | 6 ++ .../Serialization/ContextualJsonSerializer.cs | 70 ++++++++++++ ...emTextConfigurationEditorJsonSerializer.cs | 77 +------------- .../Telemetry/TelemetryServiceTests.cs | 89 ++++++++-------- 18 files changed, 410 insertions(+), 161 deletions(-) create mode 100644 src/Umbraco.Cms.Api.Management/Controllers/Package/AllPackagesController.cs create mode 100644 src/Umbraco.Cms.Api.Management/Controllers/Package/PackageControllerBase.cs create mode 100644 src/Umbraco.Cms.Api.Management/DependencyInjection/PackageBuilderExtensions.cs create mode 100644 src/Umbraco.Cms.Api.Management/Mapping/Package/ExtensionManifestViewModelMapDefinition.cs create mode 100644 src/Umbraco.Cms.Api.Management/ViewModels/Package/ExtensionManifestViewModel.cs create mode 100644 src/Umbraco.Core/Manifest/ExtensionManifest.cs create mode 100644 src/Umbraco.Core/Manifest/IExtensionManifestService.cs create mode 100644 src/Umbraco.Infrastructure/Manifest/ExtensionManifestReader.cs create mode 100644 src/Umbraco.Infrastructure/Manifest/ExtensionManifestService.cs create mode 100644 src/Umbraco.Infrastructure/Manifest/IExtensionManifestReader.cs create mode 100644 src/Umbraco.Infrastructure/Serialization/ContextualJsonSerializer.cs diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Package/AllPackagesController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Package/AllPackagesController.cs new file mode 100644 index 0000000000..c77b2a2b86 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/Package/AllPackagesController.cs @@ -0,0 +1,35 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Common.ViewModels.Pagination; +using Umbraco.Cms.Api.Management.ViewModels.Package; +using Umbraco.Cms.Core.Manifest; +using Umbraco.Cms.Core.Mapping; + +namespace Umbraco.Cms.Api.Management.Controllers.Package; + +public class AllPackagesController : PackageControllerBase +{ + private readonly IExtensionManifestService _extensionManifestService; + private readonly IUmbracoMapper _umbracoMapper; + + public AllPackagesController(IExtensionManifestService extensionManifestService, IUmbracoMapper umbracoMapper) + { + _extensionManifestService = extensionManifestService; + _umbracoMapper = umbracoMapper; + } + + [HttpGet("all")] + [MapToApiVersion("1.0")] + // TODO: proper view model + mapper + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task>> AllMigrationStatuses(int skip = 0, int take = 100) + { + ExtensionManifest[] extensionManifests = (await _extensionManifestService.GetManifestsAsync()).ToArray(); + return Ok( + new PagedViewModel + { + Items = _umbracoMapper.MapEnumerable(extensionManifests.Skip(skip).Take(take)), + Total = extensionManifests.Length + }); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Package/PackageControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Package/PackageControllerBase.cs new file mode 100644 index 0000000000..8e7684b439 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/Package/PackageControllerBase.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.Routing; + +namespace Umbraco.Cms.Api.Management.Controllers.Package; + +[ApiController] +[VersionedApiBackOfficeRoute("package")] +[ApiExplorerSettings(GroupName = "Package")] +[ApiVersion("1.0")] +public abstract class PackageControllerBase : ManagementApiControllerBase +{ +} diff --git a/src/Umbraco.Cms.Api.Management/DependencyInjection/PackageBuilderExtensions.cs b/src/Umbraco.Cms.Api.Management/DependencyInjection/PackageBuilderExtensions.cs new file mode 100644 index 0000000000..7ffe39bc79 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/DependencyInjection/PackageBuilderExtensions.cs @@ -0,0 +1,15 @@ +using Umbraco.Cms.Api.Management.Mapping.Package; +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.Mapping; + +namespace Umbraco.Cms.Api.Management.DependencyInjection; + +internal static class PackageBuilderExtensions +{ + internal static IUmbracoBuilder AddPackages(this IUmbracoBuilder builder) + { + builder.WithCollectionBuilder().Add(); + + return builder; + } +} diff --git a/src/Umbraco.Cms.Api.Management/ManagementApiComposer.cs b/src/Umbraco.Cms.Api.Management/ManagementApiComposer.cs index 30b5f13b86..fc5cc5c492 100644 --- a/src/Umbraco.Cms.Api.Management/ManagementApiComposer.cs +++ b/src/Umbraco.Cms.Api.Management/ManagementApiComposer.cs @@ -39,6 +39,7 @@ public class ManagementApiComposer : IComposer .AddDataTypes() .AddTemplates() .AddLogViewer() + .AddPackages() .AddBackOfficeAuthentication() .AddApiVersioning() .AddSwaggerGen(); diff --git a/src/Umbraco.Cms.Api.Management/Mapping/Package/ExtensionManifestViewModelMapDefinition.cs b/src/Umbraco.Cms.Api.Management/Mapping/Package/ExtensionManifestViewModelMapDefinition.cs new file mode 100644 index 0000000000..c7dac7d881 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Mapping/Package/ExtensionManifestViewModelMapDefinition.cs @@ -0,0 +1,19 @@ +using Umbraco.Cms.Api.Management.ViewModels.Package; +using Umbraco.Cms.Core.Manifest; +using Umbraco.Cms.Core.Mapping; + +namespace Umbraco.Cms.Api.Management.Mapping.Package; + +public class ExtensionManifestViewModelMapDefinition : IMapDefinition +{ + public void DefineMaps(IUmbracoMapper mapper) + => mapper.Define((_, _) => new ExtensionManifestViewModel(), Map); + + // Umbraco.Code.MapAll + private static void Map(ExtensionManifest source, ExtensionManifestViewModel target, MapperContext context) + { + target.Name = source.Name; + target.Version = source.Version; + target.Extensions = source.Extensions; + } +} diff --git a/src/Umbraco.Cms.Api.Management/Serialization/SystemTextJsonSerializer.cs b/src/Umbraco.Cms.Api.Management/Serialization/SystemTextJsonSerializer.cs index fce9df39cd..e423f2ad2c 100644 --- a/src/Umbraco.Cms.Api.Management/Serialization/SystemTextJsonSerializer.cs +++ b/src/Umbraco.Cms.Api.Management/Serialization/SystemTextJsonSerializer.cs @@ -2,6 +2,7 @@ namespace Umbraco.Cms.Api.Management.Serialization; +// TOOD: move this to Infrastructure.Serialization + get rid of ISystemTextJsonSerializer public class SystemTextJsonSerializer : ISystemTextJsonSerializer { private JsonSerializerOptions _jsonSerializerOptions; diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Package/ExtensionManifestViewModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Package/ExtensionManifestViewModel.cs new file mode 100644 index 0000000000..5bdae198c9 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Package/ExtensionManifestViewModel.cs @@ -0,0 +1,10 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.Package; + +public class ExtensionManifestViewModel +{ + public string Name { get; set; } = string.Empty; + + public string? Version { get; set; } + + public object[] Extensions { get; set; } = Array.Empty(); +} diff --git a/src/Umbraco.Core/Manifest/ExtensionManifest.cs b/src/Umbraco.Core/Manifest/ExtensionManifest.cs new file mode 100644 index 0000000000..3b88c8e826 --- /dev/null +++ b/src/Umbraco.Core/Manifest/ExtensionManifest.cs @@ -0,0 +1,12 @@ +namespace Umbraco.Cms.Core.Manifest; + +public class ExtensionManifest +{ + public required string Name { get; set; } + + public string? Version { get; set; } + + public bool AllowTelemetry { get; set; } = true; + + public required object[] Extensions { get; set; } +} diff --git a/src/Umbraco.Core/Manifest/IExtensionManifestService.cs b/src/Umbraco.Core/Manifest/IExtensionManifestService.cs new file mode 100644 index 0000000000..d11d6fd389 --- /dev/null +++ b/src/Umbraco.Core/Manifest/IExtensionManifestService.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Core.Manifest; + +public interface IExtensionManifestService +{ + Task> GetManifestsAsync(); +} diff --git a/src/Umbraco.Core/Telemetry/ITelemetryService.cs b/src/Umbraco.Core/Telemetry/ITelemetryService.cs index 23b0d154a4..b018cd4c40 100644 --- a/src/Umbraco.Core/Telemetry/ITelemetryService.cs +++ b/src/Umbraco.Core/Telemetry/ITelemetryService.cs @@ -7,8 +7,14 @@ namespace Umbraco.Cms.Core.Telemetry; /// public interface ITelemetryService { - /// - /// Try and get the - /// + [Obsolete("Please use GetTelemetryReportDataAsync. Will be removed in V15.")] bool TryGetTelemetryReportData(out TelemetryReportData? telemetryReportData); + + /// + /// Attempts to get the + /// + /// + /// May return null if the site is in an unknown state. + /// + Task GetTelemetryReportDataAsync(); } diff --git a/src/Umbraco.Core/Telemetry/TelemetryService.cs b/src/Umbraco.Core/Telemetry/TelemetryService.cs index 4ebf1ba0b9..541ee3ea85 100644 --- a/src/Umbraco.Core/Telemetry/TelemetryService.cs +++ b/src/Umbraco.Core/Telemetry/TelemetryService.cs @@ -1,7 +1,9 @@ // Copyright (c) Umbraco. // See LICENSE for more details. +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.Configuration; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Manifest; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services; @@ -13,82 +15,106 @@ namespace Umbraco.Cms.Core.Telemetry; /// internal class TelemetryService : ITelemetryService { - private readonly IManifestParser _manifestParser; private readonly IMetricsConsentService _metricsConsentService; private readonly ISiteIdentifierService _siteIdentifierService; private readonly IUmbracoVersion _umbracoVersion; private readonly IUsageInformationService _usageInformationService; + private readonly IExtensionManifestService _extensionManifestService; - /// - /// Initializes a new instance of the class. - /// + [Obsolete("Please use the constructor that does not take an IManifestParser. Will be removed in V15.")] public TelemetryService( IManifestParser manifestParser, IUmbracoVersion umbracoVersion, ISiteIdentifierService siteIdentifierService, IUsageInformationService usageInformationService, IMetricsConsentService metricsConsentService) + : this( + manifestParser, + umbracoVersion, + siteIdentifierService, + usageInformationService, + metricsConsentService, + StaticServiceProvider.Instance.GetRequiredService()) + { + } + + [Obsolete("Please use the constructor that does not take an IManifestParser. Will be removed in V15.")] + public TelemetryService( + IManifestParser manifestParser, + IUmbracoVersion umbracoVersion, + ISiteIdentifierService siteIdentifierService, + IUsageInformationService usageInformationService, + IMetricsConsentService metricsConsentService, + IExtensionManifestService extensionManifestService) + : this( + umbracoVersion, + siteIdentifierService, + usageInformationService, + metricsConsentService, + extensionManifestService) + { + } + + /// + /// Initializes a new instance of the class. + /// + public TelemetryService( + IUmbracoVersion umbracoVersion, + ISiteIdentifierService siteIdentifierService, + IUsageInformationService usageInformationService, + IMetricsConsentService metricsConsentService, + IExtensionManifestService extensionManifestService) { - _manifestParser = manifestParser; _umbracoVersion = umbracoVersion; _siteIdentifierService = siteIdentifierService; _usageInformationService = usageInformationService; _metricsConsentService = metricsConsentService; + _extensionManifestService = extensionManifestService; + } + + [Obsolete("Please use GetTelemetryReportDataAsync. Will be removed in V15.")] + public bool TryGetTelemetryReportData(out TelemetryReportData? telemetryReportData) + { + telemetryReportData = GetTelemetryReportDataAsync().GetAwaiter().GetResult(); + return telemetryReportData != null; } /// - public bool TryGetTelemetryReportData(out TelemetryReportData? telemetryReportData) + public async Task GetTelemetryReportDataAsync() { if (_siteIdentifierService.TryGetOrCreateSiteIdentifier(out Guid telemetryId) is false) { - telemetryReportData = null; - return false; + return null; } - telemetryReportData = new TelemetryReportData + return new TelemetryReportData { Id = telemetryId, Version = GetVersion(), - Packages = GetPackageTelemetry(), + Packages = await GetPackageTelemetryAsync(), Detailed = _usageInformationService.GetDetailed(), }; - return true; } - private string? GetVersion() + private string? GetVersion() => _metricsConsentService.GetConsentLevel() == TelemetryLevel.Minimal + ? null + : _umbracoVersion.SemanticVersion.ToSemanticStringWithoutBuild(); + + private async Task?> GetPackageTelemetryAsync() { if (_metricsConsentService.GetConsentLevel() == TelemetryLevel.Minimal) { return null; } - return _umbracoVersion.SemanticVersion.ToSemanticStringWithoutBuild(); - } + IEnumerable manifests = await _extensionManifestService.GetManifestsAsync(); - private IEnumerable? GetPackageTelemetry() - { - if (_metricsConsentService.GetConsentLevel() == TelemetryLevel.Minimal) - { - return null; - } - - List packages = new(); - IEnumerable manifests = _manifestParser.GetManifests(); - - foreach (PackageManifest manifest in manifests) - { - if (manifest.AllowPackageTelemetry is false) + return manifests + .Where(manifest => manifest.AllowTelemetry) + .Select(manifest => new PackageTelemetry { - continue; - } - - packages.Add(new PackageTelemetry - { - Name = manifest.PackageName, - Version = manifest.Version ?? string.Empty, + Name = manifest.Name, + Version = manifest.Version ?? string.Empty }); - } - - return packages; } } diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs index bcdb6c3eeb..994dac3564 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs @@ -117,7 +117,7 @@ public static partial class UmbracoBuilderExtensions builder.Services.AddScoped(); - builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); @@ -127,6 +127,8 @@ public static partial class UmbracoBuilderExtensions // register manifest parser, will be injected in collection builders where needed builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); // register the manifest filter collection builder (collection is empty by default) builder.ManifestFilters(); diff --git a/src/Umbraco.Infrastructure/Manifest/ExtensionManifestReader.cs b/src/Umbraco.Infrastructure/Manifest/ExtensionManifestReader.cs new file mode 100644 index 0000000000..0bddec1f50 --- /dev/null +++ b/src/Umbraco.Infrastructure/Manifest/ExtensionManifestReader.cs @@ -0,0 +1,90 @@ +using System.Text; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core.IO; +using Umbraco.Cms.Core.Routing; +using Umbraco.Cms.Core.Serialization; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.Manifest; + +internal sealed class ExtensionManifestReader : IExtensionManifestReader +{ + private readonly IManifestFileProviderFactory _manifestFileProviderFactory; + private readonly IJsonSerializer _jsonSerializer; + private readonly ILogger _logger; + + public ExtensionManifestReader(IManifestFileProviderFactory manifestFileProviderFactory, IJsonSerializer jsonSerializer, ILogger logger) + { + _manifestFileProviderFactory = manifestFileProviderFactory; + _jsonSerializer = jsonSerializer; + _logger = logger; + } + + public async Task> GetManifestsAsync() + { + var manifests = new List(); + IFileProvider? manifestFileProvider = _manifestFileProviderFactory.Create(); + + if (manifestFileProvider is null) + { + throw new ArgumentNullException(nameof(manifestFileProvider)); + } + + IFileInfo[] manifestFiles = GetAllManifestFiles(manifestFileProvider, Constants.SystemDirectories.AppPlugins).ToArray(); + foreach (IFileInfo fileInfo in manifestFiles) + { + string fileContent; + await using (Stream stream = fileInfo.CreateReadStream()) + { + using (var reader = new StreamReader(stream, Encoding.UTF8)) + { + fileContent = await reader.ReadToEndAsync(); + } + } + + if (fileContent.IsNullOrWhiteSpace()) + { + continue; + } + + try + { + ExtensionManifest? manifest = _jsonSerializer.Deserialize(fileContent); + if (manifest != null) + { + manifests.Add(manifest); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Unable to load extension manifest file: {FileName}", fileInfo.Name); + } + } + + return manifests; + } + + // get all extension manifest files (recursively) + private static IEnumerable GetAllManifestFiles(IFileProvider fileProvider, string path) + { + foreach (IFileInfo fileInfo in fileProvider.GetDirectoryContents(path)) + { + if (fileInfo.IsDirectory) + { + var virtualPath = WebPath.Combine(path, fileInfo.Name); + + // recursively find nested extension manifest files + foreach (IFileInfo nested in GetAllManifestFiles(fileProvider, virtualPath)) + { + yield return nested; + } + } + // TODO: use the correct file name + else if (fileInfo.Name.InvariantEquals("extension.json") && !string.IsNullOrEmpty(fileInfo.PhysicalPath)) + { + yield return fileInfo; + } + } + } +} diff --git a/src/Umbraco.Infrastructure/Manifest/ExtensionManifestService.cs b/src/Umbraco.Infrastructure/Manifest/ExtensionManifestService.cs new file mode 100644 index 0000000000..b8ce09884c --- /dev/null +++ b/src/Umbraco.Infrastructure/Manifest/ExtensionManifestService.cs @@ -0,0 +1,12 @@ +namespace Umbraco.Cms.Core.Manifest; + +internal sealed class ExtensionManifestService : IExtensionManifestService +{ + private readonly IExtensionManifestReader _extensionManifestReader; + + public ExtensionManifestService(IExtensionManifestReader extensionManifestReader) + => _extensionManifestReader = extensionManifestReader; + + // TODO: cache manifests for the app lifetime + public async Task> GetManifestsAsync() => await _extensionManifestReader.GetManifestsAsync(); +} diff --git a/src/Umbraco.Infrastructure/Manifest/IExtensionManifestReader.cs b/src/Umbraco.Infrastructure/Manifest/IExtensionManifestReader.cs new file mode 100644 index 0000000000..e3668e912b --- /dev/null +++ b/src/Umbraco.Infrastructure/Manifest/IExtensionManifestReader.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Core.Manifest; + +public interface IExtensionManifestReader +{ + Task> GetManifestsAsync(); +} diff --git a/src/Umbraco.Infrastructure/Serialization/ContextualJsonSerializer.cs b/src/Umbraco.Infrastructure/Serialization/ContextualJsonSerializer.cs new file mode 100644 index 0000000000..0645e3a30e --- /dev/null +++ b/src/Umbraco.Infrastructure/Serialization/ContextualJsonSerializer.cs @@ -0,0 +1,70 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Umbraco.Cms.Core.Serialization; +using Umbraco.Cms.Core.Web; + +namespace Umbraco.Cms.Infrastructure.Serialization; + +// FIXME: move away from Json.NET; this is a temporary fix that attempts to use System.Text.Json for management API operations, Json.NET for other operations +public class ContextualJsonSerializer : IJsonSerializer +{ + private readonly IRequestAccessor _requestAccessor; + private readonly IJsonSerializer _jsonNetSerializer; + private readonly IJsonSerializer _systemTextSerializer; + + public ContextualJsonSerializer(IRequestAccessor requestAccessor) + { + _requestAccessor = requestAccessor; + _jsonNetSerializer = new JsonNetSerializer(); + _systemTextSerializer = new SystemTextJsonSerializer(); + } + + public string Serialize(object? input) => ContextualizedSerializer().Serialize(input); + + public T? Deserialize(string input) => ContextualizedSerializer().Deserialize(input); + + public T? DeserializeSubset(string input, string key) => throw new NotSupportedException(); + + private IJsonSerializer ContextualizedSerializer() + { + try + { + var requestedPath = _requestAccessor.GetRequestUrl()?.AbsolutePath; + if (requestedPath != null) + { + // add white listed paths for the System.Text.Json config serializer here + // - always use it for the new management API + if (requestedPath.Contains("/umbraco/management/api/")) + { + return _systemTextSerializer; + } + } + } + catch (Exception ex) + { + // ignore - this whole thing is a temporary workaround, let's not make a fuss + } + + return _jsonNetSerializer; + } + + private class SystemTextJsonSerializer : IJsonSerializer + { + private JsonSerializerOptions _jsonSerializerOptions; + + public SystemTextJsonSerializer() + { + _jsonSerializerOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; + _jsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); + _jsonSerializerOptions.Converters.Add(new JsonObjectConverter()); + } + + public string Serialize(object? input) => JsonSerializer.Serialize(input, _jsonSerializerOptions); + + public T? Deserialize(string input) => JsonSerializer.Deserialize(input, _jsonSerializerOptions); + + public T? DeserializeSubset(string input, string key) => throw new NotSupportedException(); + } +} + + diff --git a/src/Umbraco.Infrastructure/Serialization/SystemTextConfigurationEditorJsonSerializer.cs b/src/Umbraco.Infrastructure/Serialization/SystemTextConfigurationEditorJsonSerializer.cs index 088a16be88..c7fcd456fa 100644 --- a/src/Umbraco.Infrastructure/Serialization/SystemTextConfigurationEditorJsonSerializer.cs +++ b/src/Umbraco.Infrastructure/Serialization/SystemTextConfigurationEditorJsonSerializer.cs @@ -1,10 +1,10 @@ using System.Text.Json; -using System.Text.Json.Nodes; +using System.Text.Json.Serialization; using Umbraco.Cms.Core.Serialization; namespace Umbraco.Cms.Infrastructure.Serialization; -// TODO: clean up all config editor serializers when we can migrate fully to System.Text.Json +// FIXME: clean up all config editor serializers when we can migrate fully to System.Text.Json // - move this implementation to ConfigurationEditorJsonSerializer (delete the old implementation) // - use this implementation as the registered singleton (delete ContextualConfigurationEditorJsonSerializer) // - reuse the JsonObjectConverter implementation from management API (delete the local implementation - pending V12 branch update) @@ -21,9 +21,9 @@ public class SystemTextConfigurationEditorJsonSerializer : IConfigurationEditorJ // in some cases, configs aren't camel cased in the DB, so we have to resort to case insensitive // property name resolving when creating configuration objects (deserializing DB configs) PropertyNameCaseInsensitive = true, - NumberHandling = System.Text.Json.Serialization.JsonNumberHandling.AllowReadingFromString + NumberHandling = JsonNumberHandling.AllowReadingFromString }; - _jsonSerializerOptions.Converters.Add(new System.Text.Json.Serialization.JsonStringEnumConverter()); + _jsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); _jsonSerializerOptions.Converters.Add(new JsonObjectConverter()); } @@ -32,73 +32,4 @@ public class SystemTextConfigurationEditorJsonSerializer : IConfigurationEditorJ public T? Deserialize(string input) => JsonSerializer.Deserialize(input, _jsonSerializerOptions); public T? DeserializeSubset(string input, string key) => throw new NotSupportedException(); - - // TODO: reuse the JsonObjectConverter implementation from management API - private class JsonObjectConverter : System.Text.Json.Serialization.JsonConverter - { - public override object Read( - ref Utf8JsonReader reader, - Type typeToConvert, - JsonSerializerOptions options) => - ParseObject(ref reader); - - public override void Write( - Utf8JsonWriter writer, - object objectToWrite, - JsonSerializerOptions options) - { - if (objectToWrite is null) - { - return; - } - - // If an object is equals "new object()", Json.Serialize would recurse forever and cause a stack overflow - // We have no good way of checking if its an empty object - // which is why we try to check if the object has any properties, and thus will be empty. - if (objectToWrite.GetType().Name is "Object" && !objectToWrite.GetType().GetProperties().Any()) - { - writer.WriteStartObject(); - writer.WriteEndObject(); - } - else - { - JsonSerializer.Serialize(writer, objectToWrite, objectToWrite.GetType(), options); - } - } - - private object ParseObject(ref Utf8JsonReader reader) - { - if (reader.TokenType == JsonTokenType.StartArray) - { - var items = new List(); - while (reader.Read() && reader.TokenType != JsonTokenType.EndArray) - { - items.Add(ParseObject(ref reader)); - } - - return items.ToArray(); - } - - if (reader.TokenType == JsonTokenType.StartObject) - { - var jsonNode = JsonNode.Parse(ref reader); - if (jsonNode is JsonObject jsonObject) - { - return jsonObject; - } - } - - return reader.TokenType switch - { - JsonTokenType.True => true, - JsonTokenType.False => false, - JsonTokenType.Number when reader.TryGetInt32(out int i) => i, - JsonTokenType.Number when reader.TryGetInt64(out long l) => l, - JsonTokenType.Number => reader.GetDouble(), - JsonTokenType.String when reader.TryGetDateTime(out DateTime datetime) => datetime, - JsonTokenType.String => reader.GetString()!, - _ => JsonDocument.ParseValue(ref reader).RootElement.Clone() - }; - } - } } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Telemetry/TelemetryServiceTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Telemetry/TelemetryServiceTests.cs index 83aa368d70..88bcbb710c 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Telemetry/TelemetryServiceTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Telemetry/TelemetryServiceTests.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Moq; using NUnit.Framework; using Umbraco.Cms.Core.Configuration; @@ -16,132 +13,130 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Telemetry; public class TelemetryServiceTests { [Test] - public void UsesGetOrCreateSiteId() + public async Task UsesGetOrCreateSiteId() { var version = CreateUmbracoVersion(9, 3, 1); var siteIdentifierServiceMock = new Mock(); var usageInformationServiceMock = new Mock(); var sut = new TelemetryService( - Mock.Of(), version, siteIdentifierServiceMock.Object, usageInformationServiceMock.Object, - Mock.Of()); + Mock.Of(), + Mock.Of()); Guid guid; - sut.TryGetTelemetryReportData(out _); + await sut.GetTelemetryReportDataAsync(); siteIdentifierServiceMock.Verify(x => x.TryGetOrCreateSiteIdentifier(out guid), Times.Once); } [Test] - public void SkipsIfCantGetOrCreateId() + public async Task SkipsIfCantGetOrCreateId() { var version = CreateUmbracoVersion(9, 3, 1); var sut = new TelemetryService( - Mock.Of(), version, CreateSiteIdentifierService(false), Mock.Of(), - Mock.Of()); + Mock.Of(), + Mock.Of()); - var result = sut.TryGetTelemetryReportData(out var telemetry); - - Assert.IsFalse(result); - Assert.IsNull(telemetry); + var result = await sut.GetTelemetryReportDataAsync(); + Assert.IsNull(result); } [Test] - public void ReturnsSemanticVersionWithoutBuild() + public async Task ReturnsSemanticVersionWithoutBuild() { var version = CreateUmbracoVersion(9, 1, 1, "-rc", "-ad2f4k2d"); var metricsConsentService = new Mock(); metricsConsentService.Setup(x => x.GetConsentLevel()).Returns(TelemetryLevel.Detailed); var sut = new TelemetryService( - Mock.Of(), version, CreateSiteIdentifierService(), Mock.Of(), - metricsConsentService.Object); + metricsConsentService.Object, + Mock.Of()); - var result = sut.TryGetTelemetryReportData(out var telemetry); + var result = await sut.GetTelemetryReportDataAsync(); - Assert.IsTrue(result); - Assert.AreEqual("9.1.1-rc", telemetry.Version); + Assert.IsNotNull(result); + Assert.AreEqual("9.1.1-rc", result.Version); } [Test] - public void CanGatherPackageTelemetry() + public async Task CanGatherPackageTelemetry() { var version = CreateUmbracoVersion(9, 1, 1); var versionPackageName = "VersionPackage"; var packageVersion = "1.0.0"; var noVersionPackageName = "NoVersionPackage"; - PackageManifest[] manifests = + ExtensionManifest[] manifests = { - new() { PackageName = versionPackageName, Version = packageVersion }, - new() { PackageName = noVersionPackageName }, + new() { Name = versionPackageName, Version = packageVersion, Extensions = Array.Empty()}, + new() { Name = noVersionPackageName, Extensions = Array.Empty() }, }; - var manifestParser = CreateManifestParser(manifests); + var extensionManifestService = CreateExtensionManifestService(manifests); var metricsConsentService = new Mock(); metricsConsentService.Setup(x => x.GetConsentLevel()).Returns(TelemetryLevel.Basic); var sut = new TelemetryService( - manifestParser, version, CreateSiteIdentifierService(), Mock.Of(), - metricsConsentService.Object); + metricsConsentService.Object, + extensionManifestService); - var success = sut.TryGetTelemetryReportData(out var telemetry); + var result = await sut.GetTelemetryReportDataAsync(); - Assert.IsTrue(success); + Assert.IsNotNull(result); Assert.Multiple(() => { - Assert.AreEqual(2, telemetry.Packages.Count()); - var versionPackage = telemetry.Packages.FirstOrDefault(x => x.Name == versionPackageName); + Assert.AreEqual(2, result.Packages.Count()); + var versionPackage = result.Packages.FirstOrDefault(x => x.Name == versionPackageName); Assert.AreEqual(versionPackageName, versionPackage.Name); Assert.AreEqual(packageVersion, versionPackage.Version); - var noVersionPackage = telemetry.Packages.FirstOrDefault(x => x.Name == noVersionPackageName); + var noVersionPackage = result.Packages.FirstOrDefault(x => x.Name == noVersionPackageName); Assert.AreEqual(noVersionPackageName, noVersionPackage.Name); Assert.AreEqual(string.Empty, noVersionPackage.Version); }); } [Test] - public void RespectsAllowPackageTelemetry() + public async Task RespectsAllowPackageTelemetry() { var version = CreateUmbracoVersion(9, 1, 1); - PackageManifest[] manifests = + ExtensionManifest[] manifests = { - new() { PackageName = "DoNotTrack", AllowPackageTelemetry = false }, - new() { PackageName = "TrackingAllowed", AllowPackageTelemetry = true }, + new() { Name = "DoNotTrack", AllowTelemetry = false, Extensions = Array.Empty() }, + new() { Name = "TrackingAllowed", AllowTelemetry = true, Extensions = Array.Empty() }, }; - var manifestParser = CreateManifestParser(manifests); + var extensionManifestService = CreateExtensionManifestService(manifests); var metricsConsentService = new Mock(); metricsConsentService.Setup(x => x.GetConsentLevel()).Returns(TelemetryLevel.Basic); var sut = new TelemetryService( - manifestParser, version, CreateSiteIdentifierService(), Mock.Of(), - metricsConsentService.Object); + metricsConsentService.Object, + extensionManifestService); - var success = sut.TryGetTelemetryReportData(out var telemetry); + var result = await sut.GetTelemetryReportDataAsync(); - Assert.IsTrue(success); + Assert.IsNotNull(result); Assert.Multiple(() => { - Assert.AreEqual(1, telemetry.Packages.Count()); - Assert.AreEqual("TrackingAllowed", telemetry.Packages.First().Name); + Assert.AreEqual(1, result.Packages.Count()); + Assert.AreEqual("TrackingAllowed", result.Packages.First().Name); }); } - private IManifestParser CreateManifestParser(IEnumerable manifests) + private IExtensionManifestService CreateExtensionManifestService(IEnumerable manifests) { - var manifestParserMock = new Mock(); - manifestParserMock.Setup(x => x.GetManifests()).Returns(manifests); - return manifestParserMock.Object; + var mock = new Mock(); + mock.Setup(x => x.GetManifestsAsync()).Returns(Task.FromResult(manifests)); + return mock.Object; } private IUmbracoVersion CreateUmbracoVersion(int major, int minor, int patch, string prerelease = "", string build = "")