Rename ExtensionManifest to PluginConfiguration
This commit is contained in:
@@ -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<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,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<PluginConfigurationViewModel>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<PagedViewModel<PluginConfigurationViewModel>>> AllPlugins(int skip = 0, int take = 100)
|
||||
{
|
||||
PluginConfiguration[] pluginConfigurations = (await _pluginConfigurationService.GetPluginConfigurationsAsync()).ToArray();
|
||||
return Ok(
|
||||
new PagedViewModel<PluginConfigurationViewModel>
|
||||
{
|
||||
Items = _umbracoMapper.MapEnumerable<PluginConfiguration, PluginConfigurationViewModel>(pluginConfigurations.Skip(skip).Take(take)),
|
||||
Total = pluginConfigurations.Length
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ internal static class PackageBuilderExtensions
|
||||
{
|
||||
internal static IUmbracoBuilder AddPackages(this IUmbracoBuilder builder)
|
||||
{
|
||||
builder.WithCollectionBuilder<MapDefinitionCollectionBuilder>().Add<ExtensionManifestViewModelMapDefinition>();
|
||||
builder.WithCollectionBuilder<MapDefinitionCollectionBuilder>().Add<PluginConfigurationViewModelMapDefinition>();
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
@@ -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<ExtensionManifest, ExtensionManifestViewModel>((_, _) => new ExtensionManifestViewModel(), Map);
|
||||
=> mapper.Define<PluginConfiguration, PluginConfigurationViewModel>((_, _) => 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;
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace Umbraco.Cms.Api.Management.ViewModels.Package;
|
||||
|
||||
public class ExtensionManifestViewModel
|
||||
public class PluginConfigurationViewModel
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
|
||||
namespace Umbraco.Cms.Core.IO;
|
||||
|
||||
/// <summary>
|
||||
/// Factory for creating <see cref="IFileProvider" /> instances for providing the umbraco-package.json file.
|
||||
/// </summary>
|
||||
public interface IPluginConfigurationFileProviderFactory : IFileProviderFactory
|
||||
{
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
namespace Umbraco.Cms.Core.Manifest;
|
||||
|
||||
public interface IExtensionManifestService
|
||||
{
|
||||
Task<IEnumerable<ExtensionManifest>> GetManifestsAsync();
|
||||
}
|
||||
6
src/Umbraco.Core/Plugin/IPluginConfigurationService.cs
Normal file
6
src/Umbraco.Core/Plugin/IPluginConfigurationService.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace Umbraco.Cms.Core.Plugin;
|
||||
|
||||
public interface IPluginConfigurationService
|
||||
{
|
||||
Task<IEnumerable<PluginConfiguration>> GetPluginConfigurationsAsync();
|
||||
}
|
||||
@@ -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; }
|
||||
|
||||
@@ -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<IExtensionManifestService>())
|
||||
StaticServiceProvider.Instance.GetRequiredService<IPluginConfigurationService>())
|
||||
{
|
||||
}
|
||||
|
||||
@@ -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<ExtensionManifest> manifests = await _extensionManifestService.GetManifestsAsync();
|
||||
IEnumerable<PluginConfiguration> manifests = await _pluginConfigurationService.GetPluginConfigurationsAsync();
|
||||
|
||||
return manifests
|
||||
.Where(manifest => manifest.AllowTelemetry)
|
||||
|
||||
@@ -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<IManifestParser, ManifestParser>();
|
||||
builder.Services.AddSingleton<IExtensionManifestReader, ExtensionManifestReader>();
|
||||
builder.Services.AddSingleton<IExtensionManifestService, ExtensionManifestService>();
|
||||
builder.Services.AddSingleton<IPluginConfigurationReader, PluginConfigurationReader>();
|
||||
builder.Services.AddSingleton<IPluginConfigurationService, PluginConfigurationService>();
|
||||
|
||||
// register the manifest filter collection builder (collection is empty by default)
|
||||
builder.ManifestFilters();
|
||||
|
||||
@@ -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<ExtensionManifestReader> _logger;
|
||||
|
||||
public ExtensionManifestReader(IManifestFileProviderFactory manifestFileProviderFactory, IJsonSerializer jsonSerializer, ILogger<ExtensionManifestReader> logger)
|
||||
{
|
||||
_manifestFileProviderFactory = manifestFileProviderFactory;
|
||||
_jsonSerializer = jsonSerializer;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<ExtensionManifest>> 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<IFileInfo> 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<IEnumerable<ExtensionManifest>> ParseManifests(IFileInfo[] manifestFiles)
|
||||
{
|
||||
var manifests = new List<ExtensionManifest>();
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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<IEnumerable<ExtensionManifest>> GetManifestsAsync()
|
||||
=> await _cache.GetCacheItemAsync(
|
||||
$"{nameof(ExtensionManifestService)}-Manifests",
|
||||
async () => await _extensionManifestReader.ReadManifestsAsync(),
|
||||
TimeSpan.FromMinutes(10))
|
||||
?? Array.Empty<ExtensionManifest>();
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
namespace Umbraco.Cms.Core.Manifest;
|
||||
|
||||
public interface IExtensionManifestReader
|
||||
{
|
||||
Task<IEnumerable<ExtensionManifest>> ReadManifestsAsync();
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using Umbraco.Cms.Core.Plugin;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Plugin;
|
||||
|
||||
public interface IPluginConfigurationReader
|
||||
{
|
||||
Task<IEnumerable<PluginConfiguration>> ReadPluginConfigurationsAsync();
|
||||
}
|
||||
@@ -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<PluginConfigurationReader> _logger;
|
||||
|
||||
public PluginConfigurationReader(
|
||||
IPluginConfigurationFileProviderFactory pluginConfigurationFileProviderFactory,
|
||||
IJsonSerializer jsonSerializer,
|
||||
ILogger<PluginConfigurationReader> logger)
|
||||
{
|
||||
_pluginConfigurationFileProviderFactory = pluginConfigurationFileProviderFactory;
|
||||
_jsonSerializer = jsonSerializer;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<PluginConfiguration>> 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<IFileInfo> 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<IEnumerable<PluginConfiguration>> ParsePluginConfigurationFiles(IFileInfo[] files)
|
||||
{
|
||||
var pluginConfigurations = new List<PluginConfiguration>();
|
||||
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<PluginConfiguration>(fileContent);
|
||||
if (pluginConfiguration != null)
|
||||
{
|
||||
pluginConfigurations.Add(pluginConfiguration);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Unable to load plugin configuration file: {FileName}", fileInfo.Name);
|
||||
}
|
||||
}
|
||||
|
||||
return pluginConfigurations;
|
||||
}
|
||||
}
|
||||
@@ -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<IEnumerable<PluginConfiguration>> GetPluginConfigurationsAsync()
|
||||
=> await _cache.GetCacheItemAsync(
|
||||
$"{nameof(PluginConfigurationService)}-PluginConfigurations",
|
||||
async () => await _pluginConfigurationReader.ReadPluginConfigurationsAsync(),
|
||||
TimeSpan.FromMinutes(10))
|
||||
?? Array.Empty<PluginConfiguration>();
|
||||
}
|
||||
@@ -151,6 +151,7 @@ public static partial class UmbracoBuilderExtensions
|
||||
// therefore no need to register it as singleton
|
||||
builder.Services.AddSingleton<IManifestFileProviderFactory, ContentAndWebRootFileProviderFactory>();
|
||||
builder.Services.AddSingleton<IGridEditorsConfigFileProviderFactory, WebRootFileProviderFactory>();
|
||||
builder.Services.AddSingleton<IPluginConfigurationFileProviderFactory, ContentAndWebRootFileProviderFactory>();
|
||||
|
||||
// Must be added here because DbProviderFactories is netstandard 2.1 so cannot exist in Infra for now
|
||||
builder.Services.AddSingleton<IDbProviderFactoryCreator>(factory => new DbProviderFactoryCreator(
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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<IExtensionManifestReader> _readerMock;
|
||||
private IAppPolicyCache _runtimeCache;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
_readerMock = new Mock<IExtensionManifestReader>();
|
||||
_readerMock.Setup(r => r.ReadManifestsAsync()).ReturnsAsync(
|
||||
new[]
|
||||
{
|
||||
new ExtensionManifest { Name = "Test", Extensions = Array.Empty<object>() }
|
||||
});
|
||||
|
||||
_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));
|
||||
}
|
||||
}
|
||||
@@ -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<IDirectoryContents> _rootDirectoryContentsMock;
|
||||
private Mock<ILogger<ExtensionManifestReader>> _loggerMock;
|
||||
private Mock<ILogger<PluginConfigurationReader>> _loggerMock;
|
||||
private Mock<IFileProvider> _fileProviderMock;
|
||||
|
||||
[SetUp]
|
||||
@@ -27,82 +27,82 @@ public class ExtensionManifestReaderTests
|
||||
_fileProviderMock
|
||||
.Setup(m => m.GetDirectoryContents(Constants.SystemDirectories.AppPlugins))
|
||||
.Returns(_rootDirectoryContentsMock.Object);
|
||||
var fileProviderFactoryMock = new Mock<IManifestFileProviderFactory>();
|
||||
var fileProviderFactoryMock = new Mock<IPluginConfigurationFileProviderFactory>();
|
||||
fileProviderFactoryMock.Setup(m => m.Create()).Returns(_fileProviderMock.Object);
|
||||
|
||||
_loggerMock = new Mock<ILogger<ExtensionManifestReader>>();
|
||||
_reader = new ExtensionManifestReader(fileProviderFactoryMock.Object, new SystemTextJsonSerializer(), _loggerMock.Object);
|
||||
_loggerMock = new Mock<ILogger<PluginConfigurationReader>>();
|
||||
_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<IFileInfo> { CreateExtensionManifestFile() }.GetEnumerator());
|
||||
.Returns(new List<IFileInfo> { 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<IFileInfo> { directory1, directory2 }.GetEnumerator());
|
||||
.Returns(new List<IFileInfo> { 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<IFileInfo> { 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<IFileInfo> { emptyFolder, extensionFolder }.GetEnumerator());
|
||||
.Returns(new List<IFileInfo> { 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<IFileInfo> { otherFolder, extensionFolder }.GetEnumerator());
|
||||
.Returns(new List<IFileInfo> { 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<IFileInfo> { CreateExtensionManifestFile(content) }.GetEnumerator());
|
||||
.Returns(new List<IFileInfo> { 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<IFileInfo> { CreateExtensionManifestFile(content) }.GetEnumerator());
|
||||
.Returns(new List<IFileInfo> { 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<IFileInfo> { CreateExtensionManifestFile(content) }.GetEnumerator());
|
||||
.Returns(new List<IFileInfo> { 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<IFileInfo>();
|
||||
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"",
|
||||
@@ -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<IPluginConfigurationReader> _readerMock;
|
||||
private IAppPolicyCache _runtimeCache;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
_readerMock = new Mock<IPluginConfigurationReader>();
|
||||
_readerMock.Setup(r => r.ReadPluginConfigurationsAsync()).ReturnsAsync(
|
||||
new[]
|
||||
{
|
||||
new PluginConfiguration { Name = "Test", Extensions = Array.Empty<object>() }
|
||||
});
|
||||
|
||||
_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));
|
||||
}
|
||||
}
|
||||
@@ -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<IMetricsConsentService>(),
|
||||
Mock.Of<IExtensionManifestService>());
|
||||
Mock.Of<IPluginConfigurationService>());
|
||||
Guid guid;
|
||||
|
||||
await sut.GetTelemetryReportDataAsync();
|
||||
@@ -39,7 +40,7 @@ public class TelemetryServiceTests
|
||||
CreateSiteIdentifierService(false),
|
||||
Mock.Of<IUsageInformationService>(),
|
||||
Mock.Of<IMetricsConsentService>(),
|
||||
Mock.Of<IExtensionManifestService>());
|
||||
Mock.Of<IPluginConfigurationService>());
|
||||
|
||||
var result = await sut.GetTelemetryReportDataAsync();
|
||||
Assert.IsNull(result);
|
||||
@@ -57,7 +58,7 @@ public class TelemetryServiceTests
|
||||
CreateSiteIdentifierService(),
|
||||
Mock.Of<IUsageInformationService>(),
|
||||
metricsConsentService.Object,
|
||||
Mock.Of<IExtensionManifestService>());
|
||||
Mock.Of<IPluginConfigurationService>());
|
||||
|
||||
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<object>()},
|
||||
new() { Name = noVersionPackageName, Extensions = Array.Empty<object>() },
|
||||
@@ -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<object>() },
|
||||
new() { Name = "TrackingAllowed", AllowTelemetry = true, Extensions = Array.Empty<object>() },
|
||||
@@ -132,10 +133,10 @@ public class TelemetryServiceTests
|
||||
});
|
||||
}
|
||||
|
||||
private IExtensionManifestService CreateExtensionManifestService(IEnumerable<ExtensionManifest> manifests)
|
||||
private IPluginConfigurationService CreateExtensionManifestService(IEnumerable<PluginConfiguration> manifests)
|
||||
{
|
||||
var mock = new Mock<IExtensionManifestService>();
|
||||
mock.Setup(x => x.GetManifestsAsync()).Returns(Task.FromResult(manifests));
|
||||
var mock = new Mock<IPluginConfigurationService>();
|
||||
mock.Setup(x => x.GetPluginConfigurationsAsync()).Returns(Task.FromResult(manifests));
|
||||
return mock.Object;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user