Initial implementation for extension manifests
This commit is contained in:
@@ -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<ExtensionManifestViewModel>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<PagedViewModel<ExtensionManifestViewModel>>> AllMigrationStatuses(int skip = 0, int take = 100)
|
||||
{
|
||||
ExtensionManifest[] extensionManifests = (await _extensionManifestService.GetManifestsAsync()).ToArray();
|
||||
return Ok(
|
||||
new PagedViewModel<ExtensionManifestViewModel>
|
||||
{
|
||||
Items = _umbracoMapper.MapEnumerable<ExtensionManifest, ExtensionManifestViewModel>(extensionManifests.Skip(skip).Take(take)),
|
||||
Total = extensionManifests.Length
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
}
|
||||
@@ -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<MapDefinitionCollectionBuilder>().Add<ExtensionManifestViewModelMapDefinition>();
|
||||
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,7 @@ public class ManagementApiComposer : IComposer
|
||||
.AddDataTypes()
|
||||
.AddTemplates()
|
||||
.AddLogViewer()
|
||||
.AddPackages()
|
||||
.AddBackOfficeAuthentication()
|
||||
.AddApiVersioning()
|
||||
.AddSwaggerGen();
|
||||
|
||||
@@ -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<ExtensionManifest, ExtensionManifestViewModel>((_, _) => 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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<object>();
|
||||
}
|
||||
12
src/Umbraco.Core/Manifest/ExtensionManifest.cs
Normal file
12
src/Umbraco.Core/Manifest/ExtensionManifest.cs
Normal file
@@ -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; }
|
||||
}
|
||||
6
src/Umbraco.Core/Manifest/IExtensionManifestService.cs
Normal file
6
src/Umbraco.Core/Manifest/IExtensionManifestService.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace Umbraco.Cms.Core.Manifest;
|
||||
|
||||
public interface IExtensionManifestService
|
||||
{
|
||||
Task<IEnumerable<ExtensionManifest>> GetManifestsAsync();
|
||||
}
|
||||
@@ -7,8 +7,14 @@ namespace Umbraco.Cms.Core.Telemetry;
|
||||
/// </summary>
|
||||
public interface ITelemetryService
|
||||
{
|
||||
/// <summary>
|
||||
/// Try and get the <see cref="TelemetryReportData" />
|
||||
/// </summary>
|
||||
[Obsolete("Please use GetTelemetryReportDataAsync. Will be removed in V15.")]
|
||||
bool TryGetTelemetryReportData(out TelemetryReportData? telemetryReportData);
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to get the <see cref="TelemetryReportData" />
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// May return null if the site is in an unknown state.
|
||||
/// </remarks>
|
||||
Task<TelemetryReportData?> GetTelemetryReportDataAsync();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
/// <inheritdoc />
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TelemetryService" /> class.
|
||||
/// </summary>
|
||||
[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<IExtensionManifestService>())
|
||||
{
|
||||
}
|
||||
|
||||
[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)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TelemetryService" /> class.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryGetTelemetryReportData(out TelemetryReportData? telemetryReportData)
|
||||
public async Task<TelemetryReportData?> 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<IEnumerable<PackageTelemetry>?> GetPackageTelemetryAsync()
|
||||
{
|
||||
if (_metricsConsentService.GetConsentLevel() == TelemetryLevel.Minimal)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return _umbracoVersion.SemanticVersion.ToSemanticStringWithoutBuild();
|
||||
}
|
||||
IEnumerable<ExtensionManifest> manifests = await _extensionManifestService.GetManifestsAsync();
|
||||
|
||||
private IEnumerable<PackageTelemetry>? GetPackageTelemetry()
|
||||
{
|
||||
if (_metricsConsentService.GetConsentLevel() == TelemetryLevel.Minimal)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
List<PackageTelemetry> packages = new();
|
||||
IEnumerable<PackageManifest> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ public static partial class UmbracoBuilderExtensions
|
||||
|
||||
builder.Services.AddScoped<IHttpScopeReference, HttpScopeReference>();
|
||||
|
||||
builder.Services.AddSingleton<IJsonSerializer, JsonNetSerializer>();
|
||||
builder.Services.AddSingleton<IJsonSerializer, ContextualJsonSerializer>();
|
||||
builder.Services.AddSingleton<IConfigurationEditorJsonSerializer, ContextualConfigurationEditorJsonSerializer>();
|
||||
builder.Services.AddSingleton<IMenuItemCollectionFactory, MenuItemCollectionFactory>();
|
||||
|
||||
@@ -127,6 +127,8 @@ public static partial class UmbracoBuilderExtensions
|
||||
|
||||
// register manifest parser, will be injected in collection builders where needed
|
||||
builder.Services.AddSingleton<IManifestParser, ManifestParser>();
|
||||
builder.Services.AddSingleton<IExtensionManifestReader, ExtensionManifestReader>();
|
||||
builder.Services.AddSingleton<IExtensionManifestService, ExtensionManifestService>();
|
||||
|
||||
// register the manifest filter collection builder (collection is empty by default)
|
||||
builder.ManifestFilters();
|
||||
|
||||
@@ -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<ExtensionManifestReader> _logger;
|
||||
|
||||
public ExtensionManifestReader(IManifestFileProviderFactory manifestFileProviderFactory, IJsonSerializer jsonSerializer, ILogger<ExtensionManifestReader> logger)
|
||||
{
|
||||
_manifestFileProviderFactory = manifestFileProviderFactory;
|
||||
_jsonSerializer = jsonSerializer;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<ExtensionManifest>> GetManifestsAsync()
|
||||
{
|
||||
var manifests = new List<ExtensionManifest>();
|
||||
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<ExtensionManifest>(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<IFileInfo> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<IEnumerable<ExtensionManifest>> GetManifestsAsync() => await _extensionManifestReader.GetManifestsAsync();
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Umbraco.Cms.Core.Manifest;
|
||||
|
||||
public interface IExtensionManifestReader
|
||||
{
|
||||
Task<IEnumerable<ExtensionManifest>> GetManifestsAsync();
|
||||
}
|
||||
@@ -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<T>(string input) => ContextualizedSerializer().Deserialize<T>(input);
|
||||
|
||||
public T? DeserializeSubset<T>(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<T>(string input) => JsonSerializer.Deserialize<T>(input, _jsonSerializerOptions);
|
||||
|
||||
public T? DeserializeSubset<T>(string input, string key) => throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<T>(string input) => JsonSerializer.Deserialize<T>(input, _jsonSerializerOptions);
|
||||
|
||||
public T? DeserializeSubset<T>(string input, string key) => throw new NotSupportedException();
|
||||
|
||||
// TODO: reuse the JsonObjectConverter implementation from management API
|
||||
private class JsonObjectConverter : System.Text.Json.Serialization.JsonConverter<object>
|
||||
{
|
||||
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<object>();
|
||||
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()
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<ISiteIdentifierService>();
|
||||
var usageInformationServiceMock = new Mock<IUsageInformationService>();
|
||||
var sut = new TelemetryService(
|
||||
Mock.Of<IManifestParser>(),
|
||||
version,
|
||||
siteIdentifierServiceMock.Object,
|
||||
usageInformationServiceMock.Object,
|
||||
Mock.Of<IMetricsConsentService>());
|
||||
Mock.Of<IMetricsConsentService>(),
|
||||
Mock.Of<IExtensionManifestService>());
|
||||
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<IManifestParser>(),
|
||||
version,
|
||||
CreateSiteIdentifierService(false),
|
||||
Mock.Of<IUsageInformationService>(),
|
||||
Mock.Of<IMetricsConsentService>());
|
||||
Mock.Of<IMetricsConsentService>(),
|
||||
Mock.Of<IExtensionManifestService>());
|
||||
|
||||
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<IMetricsConsentService>();
|
||||
metricsConsentService.Setup(x => x.GetConsentLevel()).Returns(TelemetryLevel.Detailed);
|
||||
var sut = new TelemetryService(
|
||||
Mock.Of<IManifestParser>(),
|
||||
version,
|
||||
CreateSiteIdentifierService(),
|
||||
Mock.Of<IUsageInformationService>(),
|
||||
metricsConsentService.Object);
|
||||
metricsConsentService.Object,
|
||||
Mock.Of<IExtensionManifestService>());
|
||||
|
||||
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<object>()},
|
||||
new() { Name = noVersionPackageName, Extensions = Array.Empty<object>() },
|
||||
};
|
||||
var manifestParser = CreateManifestParser(manifests);
|
||||
var extensionManifestService = CreateExtensionManifestService(manifests);
|
||||
var metricsConsentService = new Mock<IMetricsConsentService>();
|
||||
metricsConsentService.Setup(x => x.GetConsentLevel()).Returns(TelemetryLevel.Basic);
|
||||
var sut = new TelemetryService(
|
||||
manifestParser,
|
||||
version,
|
||||
CreateSiteIdentifierService(),
|
||||
Mock.Of<IUsageInformationService>(),
|
||||
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<object>() },
|
||||
new() { Name = "TrackingAllowed", AllowTelemetry = true, Extensions = Array.Empty<object>() },
|
||||
};
|
||||
var manifestParser = CreateManifestParser(manifests);
|
||||
var extensionManifestService = CreateExtensionManifestService(manifests);
|
||||
var metricsConsentService = new Mock<IMetricsConsentService>();
|
||||
metricsConsentService.Setup(x => x.GetConsentLevel()).Returns(TelemetryLevel.Basic);
|
||||
var sut = new TelemetryService(
|
||||
manifestParser,
|
||||
version,
|
||||
CreateSiteIdentifierService(),
|
||||
Mock.Of<IUsageInformationService>(),
|
||||
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<PackageManifest> manifests)
|
||||
private IExtensionManifestService CreateExtensionManifestService(IEnumerable<ExtensionManifest> manifests)
|
||||
{
|
||||
var manifestParserMock = new Mock<IManifestParser>();
|
||||
manifestParserMock.Setup(x => x.GetManifests()).Returns(manifests);
|
||||
return manifestParserMock.Object;
|
||||
var mock = new Mock<IExtensionManifestService>();
|
||||
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 = "")
|
||||
|
||||
Reference in New Issue
Block a user