From 4087f7805e28eafdda336333b9a96550f806b50b Mon Sep 17 00:00:00 2001 From: kjac Date: Tue, 14 Feb 2023 16:59:08 +0100 Subject: [PATCH] Rename ExtensionManifest to PluginConfiguration --- .../Package/AllPackagesController.cs | 35 ------- .../Package/AllPluginsController.cs | 34 +++++++ .../PackageBuilderExtensions.cs | 2 +- ...ginConfigurationViewModelMapDefinition.cs} | 7 +- ...del.cs => PluginConfigurationViewModel.cs} | 2 +- ...IPluginConfigurationFileProviderFactory.cs | 10 ++ .../Manifest/IExtensionManifestService.cs | 6 -- .../Plugin/IPluginConfigurationService.cs | 6 ++ .../PluginConfiguration.cs} | 4 +- .../Telemetry/TelemetryService.cs | 15 +-- .../UmbracoBuilder.CoreServices.cs | 6 +- .../Manifest/ExtensionManifestReader.cs | 93 ------------------ .../Manifest/ExtensionManifestService.cs | 23 ----- .../Manifest/IExtensionManifestReader.cs | 6 -- .../Plugin/IPluginConfigurationReader.cs | 8 ++ .../Plugin/PluginConfigurationReader.cs | 98 +++++++++++++++++++ .../Plugin/PluginConfigurationService.cs | 24 +++++ .../UmbracoBuilderExtensions.cs | 1 + .../ContentAndWebRootFileProviderFactory.cs | 2 +- .../Manifest/ExtensionManifestServiceTests.cs | 66 ------------- .../PluginConfigurationReaderTests.cs} | 98 +++++++++---------- .../Plugin/PluginConfigurationServiceTests.cs | 67 +++++++++++++ .../Telemetry/TelemetryServiceTests.cs | 17 ++-- 23 files changed, 327 insertions(+), 303 deletions(-) delete mode 100644 src/Umbraco.Cms.Api.Management/Controllers/Package/AllPackagesController.cs create mode 100644 src/Umbraco.Cms.Api.Management/Controllers/Package/AllPluginsController.cs rename src/Umbraco.Cms.Api.Management/Mapping/Package/{ExtensionManifestViewModelMapDefinition.cs => PluginConfigurationViewModelMapDefinition.cs} (53%) rename src/Umbraco.Cms.Api.Management/ViewModels/Package/{ExtensionManifestViewModel.cs => PluginConfigurationViewModel.cs} (84%) create mode 100644 src/Umbraco.Core/IO/IPluginConfigurationFileProviderFactory.cs delete mode 100644 src/Umbraco.Core/Manifest/IExtensionManifestService.cs create mode 100644 src/Umbraco.Core/Plugin/IPluginConfigurationService.cs rename src/Umbraco.Core/{Manifest/ExtensionManifest.cs => Plugin/PluginConfiguration.cs} (73%) delete mode 100644 src/Umbraco.Infrastructure/Manifest/ExtensionManifestReader.cs delete mode 100644 src/Umbraco.Infrastructure/Manifest/ExtensionManifestService.cs delete mode 100644 src/Umbraco.Infrastructure/Manifest/IExtensionManifestReader.cs create mode 100644 src/Umbraco.Infrastructure/Plugin/IPluginConfigurationReader.cs create mode 100644 src/Umbraco.Infrastructure/Plugin/PluginConfigurationReader.cs create mode 100644 src/Umbraco.Infrastructure/Plugin/PluginConfigurationService.cs delete mode 100644 tests/Umbraco.Tests.UnitTests/Umbraco.Core/Manifest/ExtensionManifestServiceTests.cs rename tests/Umbraco.Tests.UnitTests/Umbraco.Core/{Manifest/ExtensionManifestReaderTests.cs => Plugin/PluginConfigurationReaderTests.cs} (60%) create mode 100644 tests/Umbraco.Tests.UnitTests/Umbraco.Core/Plugin/PluginConfigurationServiceTests.cs diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Package/AllPackagesController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Package/AllPackagesController.cs deleted file mode 100644 index c77b2a2b86..0000000000 --- a/src/Umbraco.Cms.Api.Management/Controllers/Package/AllPackagesController.cs +++ /dev/null @@ -1,35 +0,0 @@ -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/AllPluginsController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Package/AllPluginsController.cs new file mode 100644 index 0000000000..bbae667125 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/Package/AllPluginsController.cs @@ -0,0 +1,34 @@ +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.Plugin; +using Umbraco.Cms.Core.Mapping; + +namespace Umbraco.Cms.Api.Management.Controllers.Package; + +public class AllPluginsController : PackageControllerBase +{ + private readonly IPluginConfigurationService _pluginConfigurationService; + private readonly IUmbracoMapper _umbracoMapper; + + public AllPluginsController(IPluginConfigurationService pluginConfigurationService, IUmbracoMapper umbracoMapper) + { + _pluginConfigurationService = pluginConfigurationService; + _umbracoMapper = umbracoMapper; + } + + [HttpGet("plugins")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task>> AllPlugins(int skip = 0, int take = 100) + { + PluginConfiguration[] pluginConfigurations = (await _pluginConfigurationService.GetPluginConfigurationsAsync()).ToArray(); + return Ok( + new PagedViewModel + { + Items = _umbracoMapper.MapEnumerable(pluginConfigurations.Skip(skip).Take(take)), + Total = pluginConfigurations.Length + }); + } +} diff --git a/src/Umbraco.Cms.Api.Management/DependencyInjection/PackageBuilderExtensions.cs b/src/Umbraco.Cms.Api.Management/DependencyInjection/PackageBuilderExtensions.cs index 7ffe39bc79..55c8439ec0 100644 --- a/src/Umbraco.Cms.Api.Management/DependencyInjection/PackageBuilderExtensions.cs +++ b/src/Umbraco.Cms.Api.Management/DependencyInjection/PackageBuilderExtensions.cs @@ -8,7 +8,7 @@ internal static class PackageBuilderExtensions { internal static IUmbracoBuilder AddPackages(this IUmbracoBuilder builder) { - builder.WithCollectionBuilder().Add(); + builder.WithCollectionBuilder().Add(); return builder; } diff --git a/src/Umbraco.Cms.Api.Management/Mapping/Package/ExtensionManifestViewModelMapDefinition.cs b/src/Umbraco.Cms.Api.Management/Mapping/Package/PluginConfigurationViewModelMapDefinition.cs similarity index 53% rename from src/Umbraco.Cms.Api.Management/Mapping/Package/ExtensionManifestViewModelMapDefinition.cs rename to src/Umbraco.Cms.Api.Management/Mapping/Package/PluginConfigurationViewModelMapDefinition.cs index c7dac7d881..18318ab3fa 100644 --- a/src/Umbraco.Cms.Api.Management/Mapping/Package/ExtensionManifestViewModelMapDefinition.cs +++ b/src/Umbraco.Cms.Api.Management/Mapping/Package/PluginConfigurationViewModelMapDefinition.cs @@ -1,16 +1,17 @@ using Umbraco.Cms.Api.Management.ViewModels.Package; +using Umbraco.Cms.Core.Plugin; using Umbraco.Cms.Core.Manifest; using Umbraco.Cms.Core.Mapping; namespace Umbraco.Cms.Api.Management.Mapping.Package; -public class ExtensionManifestViewModelMapDefinition : IMapDefinition +public class PluginConfigurationViewModelMapDefinition : IMapDefinition { public void DefineMaps(IUmbracoMapper mapper) - => mapper.Define((_, _) => new ExtensionManifestViewModel(), Map); + => mapper.Define((_, _) => new PluginConfigurationViewModel(), Map); // Umbraco.Code.MapAll - private static void Map(ExtensionManifest source, ExtensionManifestViewModel target, MapperContext context) + private static void Map(PluginConfiguration source, PluginConfigurationViewModel target, MapperContext context) { target.Name = source.Name; target.Version = source.Version; diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Package/ExtensionManifestViewModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Package/PluginConfigurationViewModel.cs similarity index 84% rename from src/Umbraco.Cms.Api.Management/ViewModels/Package/ExtensionManifestViewModel.cs rename to src/Umbraco.Cms.Api.Management/ViewModels/Package/PluginConfigurationViewModel.cs index 5bdae198c9..034a2c6239 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/Package/ExtensionManifestViewModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Package/PluginConfigurationViewModel.cs @@ -1,6 +1,6 @@ namespace Umbraco.Cms.Api.Management.ViewModels.Package; -public class ExtensionManifestViewModel +public class PluginConfigurationViewModel { public string Name { get; set; } = string.Empty; diff --git a/src/Umbraco.Core/IO/IPluginConfigurationFileProviderFactory.cs b/src/Umbraco.Core/IO/IPluginConfigurationFileProviderFactory.cs new file mode 100644 index 0000000000..221ccb3a5f --- /dev/null +++ b/src/Umbraco.Core/IO/IPluginConfigurationFileProviderFactory.cs @@ -0,0 +1,10 @@ +using Microsoft.Extensions.FileProviders; + +namespace Umbraco.Cms.Core.IO; + +/// +/// Factory for creating instances for providing the umbraco-package.json file. +/// +public interface IPluginConfigurationFileProviderFactory : IFileProviderFactory +{ +} diff --git a/src/Umbraco.Core/Manifest/IExtensionManifestService.cs b/src/Umbraco.Core/Manifest/IExtensionManifestService.cs deleted file mode 100644 index d11d6fd389..0000000000 --- a/src/Umbraco.Core/Manifest/IExtensionManifestService.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Umbraco.Cms.Core.Manifest; - -public interface IExtensionManifestService -{ - Task> GetManifestsAsync(); -} diff --git a/src/Umbraco.Core/Plugin/IPluginConfigurationService.cs b/src/Umbraco.Core/Plugin/IPluginConfigurationService.cs new file mode 100644 index 0000000000..c176c1ba47 --- /dev/null +++ b/src/Umbraco.Core/Plugin/IPluginConfigurationService.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Core.Plugin; + +public interface IPluginConfigurationService +{ + Task> GetPluginConfigurationsAsync(); +} diff --git a/src/Umbraco.Core/Manifest/ExtensionManifest.cs b/src/Umbraco.Core/Plugin/PluginConfiguration.cs similarity index 73% rename from src/Umbraco.Core/Manifest/ExtensionManifest.cs rename to src/Umbraco.Core/Plugin/PluginConfiguration.cs index 3b88c8e826..f16f0243d6 100644 --- a/src/Umbraco.Core/Manifest/ExtensionManifest.cs +++ b/src/Umbraco.Core/Plugin/PluginConfiguration.cs @@ -1,6 +1,6 @@ -namespace Umbraco.Cms.Core.Manifest; +namespace Umbraco.Cms.Core.Plugin; -public class ExtensionManifest +public class PluginConfiguration { public required string Name { get; set; } diff --git a/src/Umbraco.Core/Telemetry/TelemetryService.cs b/src/Umbraco.Core/Telemetry/TelemetryService.cs index 541ee3ea85..09ab4483de 100644 --- a/src/Umbraco.Core/Telemetry/TelemetryService.cs +++ b/src/Umbraco.Core/Telemetry/TelemetryService.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.Plugin; using Umbraco.Cms.Core.Manifest; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services; @@ -19,7 +20,7 @@ internal class TelemetryService : ITelemetryService private readonly ISiteIdentifierService _siteIdentifierService; private readonly IUmbracoVersion _umbracoVersion; private readonly IUsageInformationService _usageInformationService; - private readonly IExtensionManifestService _extensionManifestService; + private readonly IPluginConfigurationService _pluginConfigurationService; [Obsolete("Please use the constructor that does not take an IManifestParser. Will be removed in V15.")] public TelemetryService( @@ -34,7 +35,7 @@ internal class TelemetryService : ITelemetryService siteIdentifierService, usageInformationService, metricsConsentService, - StaticServiceProvider.Instance.GetRequiredService()) + StaticServiceProvider.Instance.GetRequiredService()) { } @@ -45,13 +46,13 @@ internal class TelemetryService : ITelemetryService ISiteIdentifierService siteIdentifierService, IUsageInformationService usageInformationService, IMetricsConsentService metricsConsentService, - IExtensionManifestService extensionManifestService) + IPluginConfigurationService pluginConfigurationService) : this( umbracoVersion, siteIdentifierService, usageInformationService, metricsConsentService, - extensionManifestService) + pluginConfigurationService) { } @@ -63,13 +64,13 @@ internal class TelemetryService : ITelemetryService ISiteIdentifierService siteIdentifierService, IUsageInformationService usageInformationService, IMetricsConsentService metricsConsentService, - IExtensionManifestService extensionManifestService) + IPluginConfigurationService pluginConfigurationService) { _umbracoVersion = umbracoVersion; _siteIdentifierService = siteIdentifierService; _usageInformationService = usageInformationService; _metricsConsentService = metricsConsentService; - _extensionManifestService = extensionManifestService; + _pluginConfigurationService = pluginConfigurationService; } [Obsolete("Please use GetTelemetryReportDataAsync. Will be removed in V15.")] @@ -107,7 +108,7 @@ internal class TelemetryService : ITelemetryService return null; } - IEnumerable manifests = await _extensionManifestService.GetManifestsAsync(); + IEnumerable manifests = await _pluginConfigurationService.GetPluginConfigurationsAsync(); return manifests .Where(manifest => manifest.AllowTelemetry) diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs index 994dac3564..2b5a4188bf 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs @@ -10,6 +10,7 @@ using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.DistributedLocking; using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Plugin; using Umbraco.Cms.Core.Handlers; using Umbraco.Cms.Core.HealthChecks.NotificationMethods; using Umbraco.Cms.Core.Hosting; @@ -39,6 +40,7 @@ using Umbraco.Cms.Core.Trees; using Umbraco.Cms.Core.Web; using Umbraco.Cms.Infrastructure.DistributedLocking; using Umbraco.Cms.Infrastructure.Examine; +using Umbraco.Cms.Infrastructure.Plugin; using Umbraco.Cms.Infrastructure.HealthChecks; using Umbraco.Cms.Infrastructure.HostedServices; using Umbraco.Cms.Infrastructure.Install; @@ -127,8 +129,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(); + 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 deleted file mode 100644 index 6adc9cffe0..0000000000 --- a/src/Umbraco.Infrastructure/Manifest/ExtensionManifestReader.cs +++ /dev/null @@ -1,93 +0,0 @@ -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> ReadManifestsAsync() - { - IFileProvider? manifestFileProvider = _manifestFileProviderFactory.Create(); - if (manifestFileProvider is null) - { - throw new ArgumentNullException(nameof(manifestFileProvider)); - } - - IFileInfo[] manifestFiles = GetAllManifestFiles(manifestFileProvider, Constants.SystemDirectories.AppPlugins).ToArray(); - return await ParseManifests(manifestFiles); - } - - private static IEnumerable GetAllManifestFiles(IFileProvider fileProvider, string path) - { - const string extensionFileName = "umbraco-package.json"; - foreach (IFileInfo fileInfo in fileProvider.GetDirectoryContents(path)) - { - if (fileInfo.IsDirectory) - { - var virtualPath = WebPath.Combine(path, fileInfo.Name); - - // find all extension manifest files recursively - foreach (IFileInfo nested in GetAllManifestFiles(fileProvider, virtualPath)) - { - yield return nested; - } - } - else if (fileInfo.Name.InvariantEquals(extensionFileName)) - { - yield return fileInfo; - } - } - } - - private async Task> ParseManifests(IFileInfo[] manifestFiles) - { - var manifests = new List(); - 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; - } -} diff --git a/src/Umbraco.Infrastructure/Manifest/ExtensionManifestService.cs b/src/Umbraco.Infrastructure/Manifest/ExtensionManifestService.cs deleted file mode 100644 index 01b8787647..0000000000 --- a/src/Umbraco.Infrastructure/Manifest/ExtensionManifestService.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Umbraco.Cms.Core.Cache; -using Umbraco.Extensions; - -namespace Umbraco.Cms.Core.Manifest; - -internal sealed class ExtensionManifestService : IExtensionManifestService -{ - private readonly IExtensionManifestReader _extensionManifestReader; - private readonly IAppPolicyCache _cache; - - public ExtensionManifestService(IExtensionManifestReader extensionManifestReader, AppCaches appCaches) - { - _extensionManifestReader = extensionManifestReader; - _cache = appCaches.RuntimeCache; - } - - public async Task> GetManifestsAsync() - => await _cache.GetCacheItemAsync( - $"{nameof(ExtensionManifestService)}-Manifests", - async () => await _extensionManifestReader.ReadManifestsAsync(), - TimeSpan.FromMinutes(10)) - ?? Array.Empty(); -} diff --git a/src/Umbraco.Infrastructure/Manifest/IExtensionManifestReader.cs b/src/Umbraco.Infrastructure/Manifest/IExtensionManifestReader.cs deleted file mode 100644 index 4f62434506..0000000000 --- a/src/Umbraco.Infrastructure/Manifest/IExtensionManifestReader.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Umbraco.Cms.Core.Manifest; - -public interface IExtensionManifestReader -{ - Task> ReadManifestsAsync(); -} diff --git a/src/Umbraco.Infrastructure/Plugin/IPluginConfigurationReader.cs b/src/Umbraco.Infrastructure/Plugin/IPluginConfigurationReader.cs new file mode 100644 index 0000000000..f1f012dbe9 --- /dev/null +++ b/src/Umbraco.Infrastructure/Plugin/IPluginConfigurationReader.cs @@ -0,0 +1,8 @@ +using Umbraco.Cms.Core.Plugin; + +namespace Umbraco.Cms.Infrastructure.Plugin; + +public interface IPluginConfigurationReader +{ + Task> ReadPluginConfigurationsAsync(); +} diff --git a/src/Umbraco.Infrastructure/Plugin/PluginConfigurationReader.cs b/src/Umbraco.Infrastructure/Plugin/PluginConfigurationReader.cs new file mode 100644 index 0000000000..9fba461a7a --- /dev/null +++ b/src/Umbraco.Infrastructure/Plugin/PluginConfigurationReader.cs @@ -0,0 +1,98 @@ +using System.Text; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Plugin; +using Umbraco.Cms.Core.IO; +using Umbraco.Cms.Core.Routing; +using Umbraco.Cms.Core.Serialization; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Infrastructure.Plugin; + +internal sealed class PluginConfigurationReader : IPluginConfigurationReader +{ + private readonly IPluginConfigurationFileProviderFactory _pluginConfigurationFileProviderFactory; + private readonly IJsonSerializer _jsonSerializer; + private readonly ILogger _logger; + + public PluginConfigurationReader( + IPluginConfigurationFileProviderFactory pluginConfigurationFileProviderFactory, + IJsonSerializer jsonSerializer, + ILogger logger) + { + _pluginConfigurationFileProviderFactory = pluginConfigurationFileProviderFactory; + _jsonSerializer = jsonSerializer; + _logger = logger; + } + + public async Task> ReadPluginConfigurationsAsync() + { + IFileProvider? fileProvider = _pluginConfigurationFileProviderFactory.Create(); + if (fileProvider is null) + { + throw new ArgumentNullException(nameof(fileProvider)); + } + + IFileInfo[] files = GetAllPluginConfigurationFiles(fileProvider, Constants.SystemDirectories.AppPlugins).ToArray(); + return await ParsePluginConfigurationFiles(files); + } + + private static IEnumerable GetAllPluginConfigurationFiles(IFileProvider fileProvider, string path) + { + const string extensionFileName = "umbraco-package.json"; + foreach (IFileInfo fileInfo in fileProvider.GetDirectoryContents(path)) + { + if (fileInfo.IsDirectory) + { + var virtualPath = WebPath.Combine(path, fileInfo.Name); + + // find all extension package configuration files recursively + foreach (IFileInfo nested in GetAllPluginConfigurationFiles(fileProvider, virtualPath)) + { + yield return nested; + } + } + else if (fileInfo.Name.InvariantEquals(extensionFileName)) + { + yield return fileInfo; + } + } + } + + private async Task> ParsePluginConfigurationFiles(IFileInfo[] files) + { + var pluginConfigurations = new List(); + foreach (IFileInfo fileInfo in files) + { + 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 + { + PluginConfiguration? pluginConfiguration = _jsonSerializer.Deserialize(fileContent); + if (pluginConfiguration != null) + { + pluginConfigurations.Add(pluginConfiguration); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Unable to load plugin configuration file: {FileName}", fileInfo.Name); + } + } + + return pluginConfigurations; + } +} diff --git a/src/Umbraco.Infrastructure/Plugin/PluginConfigurationService.cs b/src/Umbraco.Infrastructure/Plugin/PluginConfigurationService.cs new file mode 100644 index 0000000000..06a8dd10fe --- /dev/null +++ b/src/Umbraco.Infrastructure/Plugin/PluginConfigurationService.cs @@ -0,0 +1,24 @@ +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Plugin; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Infrastructure.Plugin; + +internal sealed class PluginConfigurationService : IPluginConfigurationService +{ + private readonly IPluginConfigurationReader _pluginConfigurationReader; + private readonly IAppPolicyCache _cache; + + public PluginConfigurationService(IPluginConfigurationReader pluginConfigurationReader, AppCaches appCaches) + { + _pluginConfigurationReader = pluginConfigurationReader; + _cache = appCaches.RuntimeCache; + } + + public async Task> GetPluginConfigurationsAsync() + => await _cache.GetCacheItemAsync( + $"{nameof(PluginConfigurationService)}-PluginConfigurations", + async () => await _pluginConfigurationReader.ReadPluginConfigurationsAsync(), + TimeSpan.FromMinutes(10)) + ?? Array.Empty(); +} diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs index dc94fa3dc8..7efcb3e67d 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs @@ -151,6 +151,7 @@ public static partial class UmbracoBuilderExtensions // therefore no need to register it as singleton builder.Services.AddSingleton(); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); // Must be added here because DbProviderFactories is netstandard 2.1 so cannot exist in Infra for now builder.Services.AddSingleton(factory => new DbProviderFactoryCreator( diff --git a/src/Umbraco.Web.Common/FileProviders/ContentAndWebRootFileProviderFactory.cs b/src/Umbraco.Web.Common/FileProviders/ContentAndWebRootFileProviderFactory.cs index 7c3831b919..64263075d8 100644 --- a/src/Umbraco.Web.Common/FileProviders/ContentAndWebRootFileProviderFactory.cs +++ b/src/Umbraco.Web.Common/FileProviders/ContentAndWebRootFileProviderFactory.cs @@ -4,7 +4,7 @@ using Umbraco.Cms.Core.IO; namespace Umbraco.Cms.Web.Common.FileProviders; -public class ContentAndWebRootFileProviderFactory : IManifestFileProviderFactory +public class ContentAndWebRootFileProviderFactory : IManifestFileProviderFactory, IPluginConfigurationFileProviderFactory { private readonly IWebHostEnvironment _webHostEnvironment; diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Manifest/ExtensionManifestServiceTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Manifest/ExtensionManifestServiceTests.cs deleted file mode 100644 index 364aba4d05..0000000000 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Manifest/ExtensionManifestServiceTests.cs +++ /dev/null @@ -1,66 +0,0 @@ -using Moq; -using NUnit.Framework; -using Umbraco.Cms.Core.Cache; -using Umbraco.Cms.Core.Manifest; - -namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Manifest; - -[TestFixture] -public class ExtensionManifestServiceTests -{ - private IExtensionManifestService _service; - private Mock _readerMock; - private IAppPolicyCache _runtimeCache; - - [SetUp] - public void SetUp() - { - _readerMock = new Mock(); - _readerMock.Setup(r => r.ReadManifestsAsync()).ReturnsAsync( - new[] - { - new ExtensionManifest { Name = "Test", Extensions = Array.Empty() } - }); - - _runtimeCache = new ObjectCacheAppCache(); - AppCaches appCaches = new AppCaches( - _runtimeCache, - NoAppCache.Instance, - new IsolatedCaches(type => NoAppCache.Instance)); - - _service = new ExtensionManifestService(_readerMock.Object, appCaches); - } - - [Test] - public async Task CachesManifests() - { - var result = await _service.GetManifestsAsync(); - Assert.AreEqual(1, result.Count()); - - var result2 = await _service.GetManifestsAsync(); - Assert.AreEqual(1, result2.Count()); - - var result3 = await _service.GetManifestsAsync(); - Assert.AreEqual(1, result3.Count()); - - _readerMock.Verify(r => r.ReadManifestsAsync(), Times.Exactly(1)); - } - - [Test] - public async Task ReloadsManifestsAfterCacheClear() - { - var result = await _service.GetManifestsAsync(); - Assert.AreEqual(1, result.Count()); - _runtimeCache.Clear(); - - var result2 = await _service.GetManifestsAsync(); - Assert.AreEqual(1, result2.Count()); - _runtimeCache.Clear(); - - var result3 = await _service.GetManifestsAsync(); - Assert.AreEqual(1, result3.Count()); - _runtimeCache.Clear(); - - _readerMock.Verify(r => r.ReadManifestsAsync(), Times.Exactly(3)); - } -} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Manifest/ExtensionManifestReaderTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Plugin/PluginConfigurationReaderTests.cs similarity index 60% rename from tests/Umbraco.Tests.UnitTests/Umbraco.Core/Manifest/ExtensionManifestReaderTests.cs rename to tests/Umbraco.Tests.UnitTests/Umbraco.Core/Plugin/PluginConfigurationReaderTests.cs index 2b76421820..0b7e5c982f 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Manifest/ExtensionManifestReaderTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Plugin/PluginConfigurationReaderTests.cs @@ -6,17 +6,17 @@ using Moq; using NUnit.Framework; using Umbraco.Cms.Core; using Umbraco.Cms.Core.IO; -using Umbraco.Cms.Core.Manifest; +using Umbraco.Cms.Infrastructure.Plugin; using Umbraco.Cms.Infrastructure.Serialization; -namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Manifest; +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Plugin; [TestFixture] -public class ExtensionManifestReaderTests +public class PluginConfigurationReaderTests { - private IExtensionManifestReader _reader; + private IPluginConfigurationReader _reader; private Mock _rootDirectoryContentsMock; - private Mock> _loggerMock; + private Mock> _loggerMock; private Mock _fileProviderMock; [SetUp] @@ -27,82 +27,82 @@ public class ExtensionManifestReaderTests _fileProviderMock .Setup(m => m.GetDirectoryContents(Constants.SystemDirectories.AppPlugins)) .Returns(_rootDirectoryContentsMock.Object); - var fileProviderFactoryMock = new Mock(); + var fileProviderFactoryMock = new Mock(); fileProviderFactoryMock.Setup(m => m.Create()).Returns(_fileProviderMock.Object); - _loggerMock = new Mock>(); - _reader = new ExtensionManifestReader(fileProviderFactoryMock.Object, new SystemTextJsonSerializer(), _loggerMock.Object); + _loggerMock = new Mock>(); + _reader = new PluginConfigurationReader(fileProviderFactoryMock.Object, new SystemTextJsonSerializer(), _loggerMock.Object); } [Test] - public async Task CanReadManifestAtRoot() + public async Task Can_Read_PluginConfigurations_At_Root() { _rootDirectoryContentsMock .Setup(f => f.GetEnumerator()) - .Returns(new List { CreateExtensionManifestFile() }.GetEnumerator()); + .Returns(new List { CreatePluginConfigurationFile() }.GetEnumerator()); - var result = await _reader.ReadManifestsAsync(); + var result = await _reader.ReadPluginConfigurationsAsync(); Assert.AreEqual(1, result.Count()); var first = result.First(); - Assert.AreEqual("My Extension Manifest", first.Name); + Assert.AreEqual("My Plugin Configuration", first.Name); Assert.AreEqual("1.2.3", first.Version); Assert.AreEqual(2, first.Extensions.Count()); Assert.IsTrue(first.Extensions.All(e => e is JsonElement)); } [Test] - public async Task CanReadManifestsInRootDirectories() + public async Task Can_Read_PluginConfiguration_In_Root_Directories() { - var directory1 = CreateDirectoryMock("/my-extension", CreateExtensionManifestFile(DefaultManifestContent("Extension One"))); - var directory2 = CreateDirectoryMock("/my-other-extension", CreateExtensionManifestFile(DefaultManifestContent("Extension Two"))); + var plugin1 = CreateDirectoryMock("/my-extension", CreatePluginConfigurationFile(DefaultPluginConfigurationContent("Plugin One"))); + var plugin2 = CreateDirectoryMock("/my-other-extension", CreatePluginConfigurationFile(DefaultPluginConfigurationContent("Plugin Two"))); _rootDirectoryContentsMock .Setup(f => f.GetEnumerator()) - .Returns(new List { directory1, directory2 }.GetEnumerator()); + .Returns(new List { plugin1, plugin2 }.GetEnumerator()); - var result = await _reader.ReadManifestsAsync(); + var result = await _reader.ReadPluginConfigurationsAsync(); Assert.AreEqual(2, result.Count()); - Assert.AreEqual("Extension One", result.First().Name); - Assert.AreEqual("Extension Two", result.Last().Name); + Assert.AreEqual("Plugin One", result.First().Name); + Assert.AreEqual("Plugin Two", result.Last().Name); } [Test] - public async Task CanReadManifestsRecursively() + public async Task Can_Read_PluginConfigurations_Recursively() { - var childFolder = CreateDirectoryMock("/my-parent-folder/my-child-folder", CreateExtensionManifestFile(DefaultManifestContent("Nested Extension"))); + var childFolder = CreateDirectoryMock("/my-parent-folder/my-child-folder", CreatePluginConfigurationFile(DefaultPluginConfigurationContent("Nested Plugin"))); var parentFolder = CreateDirectoryMock("/my-parent-folder", childFolder); _rootDirectoryContentsMock .Setup(f => f.GetEnumerator()) .Returns(new List { parentFolder }.GetEnumerator()); - var result = await _reader.ReadManifestsAsync(); + var result = await _reader.ReadPluginConfigurationsAsync(); Assert.AreEqual(1, result.Count()); - Assert.AreEqual("Nested Extension", result.First().Name); + Assert.AreEqual("Nested Plugin", result.First().Name); } [Test] - public async Task CanSkipEmptyDirectories() + public async Task Can_Skip_Empty_Directories() { - var extensionFolder = CreateDirectoryMock("/my-extension-folder", CreateExtensionManifestFile(DefaultManifestContent("My Extension"))); + var pluginFolder = CreateDirectoryMock("/my-plugin-folder", CreatePluginConfigurationFile(DefaultPluginConfigurationContent("My Plugin"))); var emptyFolder = CreateDirectoryMock("/my-empty-folder"); _rootDirectoryContentsMock .Setup(f => f.GetEnumerator()) - .Returns(new List { emptyFolder, extensionFolder }.GetEnumerator()); + .Returns(new List { emptyFolder, pluginFolder }.GetEnumerator()); - var result = await _reader.ReadManifestsAsync(); + var result = await _reader.ReadPluginConfigurationsAsync(); Assert.AreEqual(1, result.Count()); - Assert.AreEqual("My Extension", result.First().Name); + Assert.AreEqual("My Plugin", result.First().Name); } [Test] - public async Task CanSkipOtherFiles() + public async Task Can_Skip_Other_Files() { - var extensionFolder = CreateDirectoryMock( - "/my-extension-folder", + var pluginFolder = CreateDirectoryMock( + "/my-plugin-folder", CreateOtherFile("my.js"), - CreateExtensionManifestFile(DefaultManifestContent("My Extension"))); + CreatePluginConfigurationFile(DefaultPluginConfigurationContent("My Plugin"))); var otherFolder = CreateDirectoryMock( "/my-empty-folder", CreateOtherFile("some.js"), @@ -110,15 +110,15 @@ public class ExtensionManifestReaderTests _rootDirectoryContentsMock .Setup(f => f.GetEnumerator()) - .Returns(new List { otherFolder, extensionFolder }.GetEnumerator()); + .Returns(new List { otherFolder, pluginFolder }.GetEnumerator()); - var result = await _reader.ReadManifestsAsync(); + var result = await _reader.ReadPluginConfigurationsAsync(); Assert.AreEqual(1, result.Count()); - Assert.AreEqual("My Extension", result.First().Name); + Assert.AreEqual("My Plugin", result.First().Name); } [Test] - public async Task CanHandleAllEmptyDirectories() + public async Task Can_Handle_All_Empty_Directories() { var folders = Enumerable.Range(1, 10).Select(i => CreateDirectoryMock($"/my-empty-folder-{i}")).ToList(); @@ -126,12 +126,12 @@ public class ExtensionManifestReaderTests .Setup(f => f.GetEnumerator()) .Returns(folders.GetEnumerator()); - var result = await _reader.ReadManifestsAsync(); + var result = await _reader.ReadPluginConfigurationsAsync(); Assert.AreEqual(0, result.Count()); } [Test] - public async Task CannotReadManifestWithoutName() + public async Task Cannot_Read_PluginConfiguration_Without_Name() { var content = @"{ ""version"": ""1.2.3"", @@ -145,16 +145,16 @@ public class ExtensionManifestReaderTests }"; _rootDirectoryContentsMock .Setup(f => f.GetEnumerator()) - .Returns(new List { CreateExtensionManifestFile(content) }.GetEnumerator()); + .Returns(new List { CreatePluginConfigurationFile(content) }.GetEnumerator()); - var result = await _reader.ReadManifestsAsync(); + var result = await _reader.ReadPluginConfigurationsAsync(); Assert.AreEqual(0, result.Count()); EnsureLogErrorWasCalled(); } [Test] - public async Task CannotReadManifestWithoutExtensions() + public async Task Cannot_Read_PluginConfiguration_Without_Extensions() { var content = @"{ ""name"": ""Something"", @@ -163,9 +163,9 @@ public class ExtensionManifestReaderTests }"; _rootDirectoryContentsMock .Setup(f => f.GetEnumerator()) - .Returns(new List { CreateExtensionManifestFile(content) }.GetEnumerator()); + .Returns(new List { CreatePluginConfigurationFile(content) }.GetEnumerator()); - var result = await _reader.ReadManifestsAsync(); + var result = await _reader.ReadPluginConfigurationsAsync(); Assert.AreEqual(0, result.Count()); EnsureLogErrorWasCalled(); @@ -173,13 +173,13 @@ public class ExtensionManifestReaderTests [TestCase("This is not JSON")] [TestCase(@"{""name"": ""invalid-json"", ""version"": ")] - public async Task CannotReadInvalidManifest(string content) + public async Task Cannot_Read_Invalid_PluginConfiguration(string content) { _rootDirectoryContentsMock .Setup(f => f.GetEnumerator()) - .Returns(new List { CreateExtensionManifestFile(content) }.GetEnumerator()); + .Returns(new List { CreatePluginConfigurationFile(content) }.GetEnumerator()); - var result = await _reader.ReadManifestsAsync(); + var result = await _reader.ReadPluginConfigurationsAsync(); Assert.AreEqual(0, result.Count()); EnsureLogErrorWasCalled(); @@ -213,9 +213,9 @@ public class ExtensionManifestReaderTests return fileInfo.Object; } - private IFileInfo CreateExtensionManifestFile(string? content = null) + private IFileInfo CreatePluginConfigurationFile(string? content = null) { - content ??= DefaultManifestContent(); + content ??= DefaultPluginConfigurationContent(); var fileInfo = new Mock(); fileInfo.SetupGet(f => f.IsDirectory).Returns(false); @@ -235,7 +235,7 @@ public class ExtensionManifestReaderTests return fileInfo.Object; } - private static string DefaultManifestContent(string name = "My Extension Manifest") + private static string DefaultPluginConfigurationContent(string name = "My Plugin Configuration") => @"{ ""name"": ""##NAME##"", ""version"": ""1.2.3"", diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Plugin/PluginConfigurationServiceTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Plugin/PluginConfigurationServiceTests.cs new file mode 100644 index 0000000000..016799f58a --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Plugin/PluginConfigurationServiceTests.cs @@ -0,0 +1,67 @@ +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Plugin; +using Umbraco.Cms.Infrastructure.Plugin; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Plugin; + +[TestFixture] +public class PluginConfigurationServiceTests +{ + private IPluginConfigurationService _service; + private Mock _readerMock; + private IAppPolicyCache _runtimeCache; + + [SetUp] + public void SetUp() + { + _readerMock = new Mock(); + _readerMock.Setup(r => r.ReadPluginConfigurationsAsync()).ReturnsAsync( + new[] + { + new PluginConfiguration { Name = "Test", Extensions = Array.Empty() } + }); + + _runtimeCache = new ObjectCacheAppCache(); + AppCaches appCaches = new AppCaches( + _runtimeCache, + NoAppCache.Instance, + new IsolatedCaches(type => NoAppCache.Instance)); + + _service = new PluginConfigurationService(_readerMock.Object, appCaches); + } + + [Test] + public async Task CachesExtensionPackageConfigurations() + { + var result = await _service.GetPluginConfigurationsAsync(); + Assert.AreEqual(1, result.Count()); + + var result2 = await _service.GetPluginConfigurationsAsync(); + Assert.AreEqual(1, result2.Count()); + + var result3 = await _service.GetPluginConfigurationsAsync(); + Assert.AreEqual(1, result3.Count()); + + _readerMock.Verify(r => r.ReadPluginConfigurationsAsync(), Times.Exactly(1)); + } + + [Test] + public async Task ReloadsExtensionPackageConfigurationsAfterCacheClear() + { + var result = await _service.GetPluginConfigurationsAsync(); + Assert.AreEqual(1, result.Count()); + _runtimeCache.Clear(); + + var result2 = await _service.GetPluginConfigurationsAsync(); + Assert.AreEqual(1, result2.Count()); + _runtimeCache.Clear(); + + var result3 = await _service.GetPluginConfigurationsAsync(); + Assert.AreEqual(1, result3.Count()); + _runtimeCache.Clear(); + + _readerMock.Verify(r => r.ReadPluginConfigurationsAsync(), Times.Exactly(3)); + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Telemetry/TelemetryServiceTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Telemetry/TelemetryServiceTests.cs index 88bcbb710c..f9a69a6aa1 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Telemetry/TelemetryServiceTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Telemetry/TelemetryServiceTests.cs @@ -1,6 +1,7 @@ using Moq; using NUnit.Framework; using Umbraco.Cms.Core.Configuration; +using Umbraco.Cms.Core.Plugin; using Umbraco.Cms.Core.Manifest; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Semver; @@ -23,7 +24,7 @@ public class TelemetryServiceTests siteIdentifierServiceMock.Object, usageInformationServiceMock.Object, Mock.Of(), - Mock.Of()); + Mock.Of()); Guid guid; await sut.GetTelemetryReportDataAsync(); @@ -39,7 +40,7 @@ public class TelemetryServiceTests CreateSiteIdentifierService(false), Mock.Of(), Mock.Of(), - Mock.Of()); + Mock.Of()); var result = await sut.GetTelemetryReportDataAsync(); Assert.IsNull(result); @@ -57,7 +58,7 @@ public class TelemetryServiceTests CreateSiteIdentifierService(), Mock.Of(), metricsConsentService.Object, - Mock.Of()); + Mock.Of()); var result = await sut.GetTelemetryReportDataAsync(); @@ -72,7 +73,7 @@ public class TelemetryServiceTests var versionPackageName = "VersionPackage"; var packageVersion = "1.0.0"; var noVersionPackageName = "NoVersionPackage"; - ExtensionManifest[] manifests = + PluginConfiguration[] manifests = { new() { Name = versionPackageName, Version = packageVersion, Extensions = Array.Empty()}, new() { Name = noVersionPackageName, Extensions = Array.Empty() }, @@ -107,7 +108,7 @@ public class TelemetryServiceTests public async Task RespectsAllowPackageTelemetry() { var version = CreateUmbracoVersion(9, 1, 1); - ExtensionManifest[] manifests = + PluginConfiguration[] manifests = { new() { Name = "DoNotTrack", AllowTelemetry = false, Extensions = Array.Empty() }, new() { Name = "TrackingAllowed", AllowTelemetry = true, Extensions = Array.Empty() }, @@ -132,10 +133,10 @@ public class TelemetryServiceTests }); } - private IExtensionManifestService CreateExtensionManifestService(IEnumerable manifests) + private IPluginConfigurationService CreateExtensionManifestService(IEnumerable manifests) { - var mock = new Mock(); - mock.Setup(x => x.GetManifestsAsync()).Returns(Task.FromResult(manifests)); + var mock = new Mock(); + mock.Setup(x => x.GetPluginConfigurationsAsync()).Returns(Task.FromResult(manifests)); return mock.Object; }