diff --git a/common/ASC.Core.Common/Data/DbWebPluginService.cs b/common/ASC.Core.Common/Data/DbWebPluginService.cs index a0ed169e9d..f3326cb1b6 100644 --- a/common/ASC.Core.Common/Data/DbWebPluginService.cs +++ b/common/ASC.Core.Common/Data/DbWebPluginService.cs @@ -62,13 +62,11 @@ public class DbWebPluginService return await Queries.WebPluginByNameAsync(dbContext, tenantId, name); } - public async Task> GetAsync(int tenantId, bool? enabled = null) + public async Task> GetAsync(int tenantId) { await using var dbContext = _dbContextFactory.CreateDbContext(); - return enabled.HasValue - ? await Queries.WebPluginsByStatusAsync(dbContext, tenantId, enabled.Value).ToListAsync() - : await Queries.WebPluginsAsync(dbContext, tenantId).ToListAsync(); + return await Queries.WebPluginsAsync(dbContext, tenantId).ToListAsync(); } public async Task UpdateAsync(int tenantId, int id, bool enabled) @@ -95,14 +93,6 @@ static file class Queries .AsNoTracking() .Where(r => r.TenantId == tenantId)); - public static readonly Func> - WebPluginsByStatusAsync = EF.CompileAsyncQuery( - (WebPluginDbContext ctx, int tenantId, bool enabled) => - ctx.WebPlugins - .AsNoTracking() - .Where(r => r.TenantId == tenantId) - .Where(r => r.Enabled == enabled)); - public static readonly Func> WebPluginByIdAsync = EF.CompileAsyncQuery( (WebPluginDbContext ctx, int tenantId, int id) => diff --git a/config/appsettings.json b/config/appsettings.json index 1b0ab1e87d..72a80256ec 100644 --- a/config/appsettings.json +++ b/config/appsettings.json @@ -451,8 +451,9 @@ "plugins": { "enabled": "true", "extension": ".zip", - "max-size": 5242880, - "allow": ["upload", "delete"] + "maxSize": 5242880, + "allow": ["upload", "delete"], + "assetExtensions": [".jpg", ".jpeg", ".png", ".svg"] }, "aws": { "cloudWatch": { diff --git a/config/storage.json b/config/storage.json index 38f0b6eb54..6b9faa8ba6 100644 --- a/config/storage.json +++ b/config/storage.json @@ -134,6 +134,16 @@ "type": "disc", "path": "$STORAGE_ROOT\\Studio\\{0}\\webplugins", "virtualpath": "~/studio/{0}/webplugins" + }, + { + "name": "systemwebplugins", + "data": "00000000-0000-0000-0000-000000000000", + "type": "disc", + "path": "$STORAGE_ROOT\\Studio\\webplugins", + "virtualpath": "~/studio/webplugins", + "appendTenantId": false, + "disableMigrate": true, + "disableEncryption": true } ] } diff --git a/packages/client/src/store/PluginStore.js b/packages/client/src/store/PluginStore.js index cda33b6d5f..6f9f9eec05 100644 --- a/packages/client/src/store/PluginStore.js +++ b/packages/client/src/store/PluginStore.js @@ -186,7 +186,7 @@ class PluginStore { ...this.pluginFrame.contentWindow.Plugins[plugin.pluginName], }); - newPlugin.createBy = newPlugin.createBy.displayName; + // newPlugin.createBy = newPlugin.createBy.displayName; newPlugin.scopes = newPlugin.scopes.split(","); newPlugin.iconUrl = getPluginUrl(newPlugin.url, ""); @@ -226,7 +226,7 @@ class PluginStore { if (!plugin || !plugin.enabled) return; if (plugin.scopes.includes(PluginScopes.API)) { - plugin.setAPI(origin, proxy, prefix); + plugin.setAPI && plugin.setAPI(origin, proxy, prefix); } if (plugin.onLoadCallback) { diff --git a/web/ASC.Web.Api/Api/Settings/WebPluginsController.cs b/web/ASC.Web.Api/Api/Settings/WebPluginsController.cs index 88c5f02b1d..449ca8aa62 100644 --- a/web/ASC.Web.Api/Api/Settings/WebPluginsController.cs +++ b/web/ASC.Web.Api/Api/Settings/WebPluginsController.cs @@ -49,8 +49,10 @@ public class WebPluginsController : BaseSettingsController } [HttpPost("webplugins")] - public async Task AddWebPluginFromFile() + public async Task AddWebPluginFromFile(bool system) { + var tenantId = system ? Tenant.DefaultTenant : Tenant.Id; + await _permissionContext.DemandPermissionsAsync(SecutiryConstants.EditPortalSettings); if (HttpContext.Request.Form?.Files == null || HttpContext.Request.Form.Files.Count == 0) @@ -65,11 +67,11 @@ public class WebPluginsController : BaseSettingsController var file = HttpContext.Request.Form.Files[0] ?? throw new ArgumentException("Input file is null"); - var plugin = await _webPluginManager.AddWebPluginFromFile(Tenant.Id, file); + var plugin = await _webPluginManager.AddWebPluginFromFileAsync(tenantId, file); var outDto = _mapper.Map(plugin); - var urlTemplate = await _webPluginManager.GetPluginUrlTemplate(Tenant.Id); + var urlTemplate = await _webPluginManager.GetPluginUrlTemplateAsync(tenantId); outDto.Url = string.Format(urlTemplate, outDto.Name); @@ -79,17 +81,37 @@ public class WebPluginsController : BaseSettingsController [HttpGet("webplugins")] public async Task> GetWebPluginsAsync(bool? enabled = null) { - var plugins = await _webPluginManager.GetWebPluginsAsync(Tenant.Id, enabled); + var plugins = new List(); - var outDto = _mapper.Map, IEnumerable>(plugins); + plugins.AddRange(await _webPluginManager.GetSystemWebPluginsAsync()); - if (outDto != null && outDto.Any()) + plugins.AddRange(await _webPluginManager.GetWebPluginsAsync(Tenant.Id)); + + var outDto = _mapper.Map, List>(plugins); + + if (enabled.HasValue) { - var urlTemplate = await _webPluginManager.GetPluginUrlTemplate(Tenant.Id); + outDto = outDto.Where(i => i.Enabled == enabled).ToList(); + } + + if (outDto.Any()) + { + string urlTemplate = null; + string systemUrlTemplate = null; foreach (var dto in outDto) { - dto.Url = string.Format(urlTemplate, dto.Name); + if (dto.System && systemUrlTemplate == null) + { + systemUrlTemplate = await _webPluginManager.GetPluginUrlTemplateAsync(Tenant.DefaultTenant); + } + + if (!dto.System && urlTemplate == null) + { + urlTemplate = await _webPluginManager.GetPluginUrlTemplateAsync(Tenant.Id); + } + + dto.Url = string.Format(dto.System ? systemUrlTemplate : urlTemplate, dto.Name); } } @@ -105,7 +127,7 @@ public class WebPluginsController : BaseSettingsController if (outDto != null) { - var urlTemplate = await _webPluginManager.GetPluginUrlTemplate(Tenant.Id); + var urlTemplate = await _webPluginManager.GetPluginUrlTemplateAsync(Tenant.Id); outDto.Url = string.Format(urlTemplate, outDto.Name); } @@ -128,4 +150,39 @@ public class WebPluginsController : BaseSettingsController await _webPluginManager.DeleteWebPluginAsync(Tenant.Id, id); } + + + + [HttpGet("webplugins/system/{name}")] + public async Task GetSystemWebPluginByNameAsync(string name) + { + var plugin = await _webPluginManager.GetSystemWebPluginAsync(name); + + var outDto = _mapper.Map(plugin); + + if (outDto != null) + { + var urlTemplate = await _webPluginManager.GetPluginUrlTemplateAsync(Tenant.DefaultTenant); + + outDto.Url = string.Format(urlTemplate, outDto.Name); + } + + return outDto; + } + + [HttpPut("webplugins/system/{name}")] + public async Task UpdateSystemWebPluginAsync(string name, WebPluginRequestsDto inDto) + { + await _permissionContext.DemandPermissionsAsync(SecutiryConstants.EditPortalSettings); + + await _webPluginManager.UpdateSystemWebPluginAsync(name, inDto.Enabled); + } + + [HttpDelete("webplugins/system/{name}")] + public async Task DeleteSystemWebPluginAsync(string name) + { + await _permissionContext.DemandPermissionsAsync(SecutiryConstants.EditPortalSettings); + + await _webPluginManager.DeleteSystemWebPluginAsync(name); + } } diff --git a/web/ASC.Web.Core/ASC.Web.Core.csproj b/web/ASC.Web.Core/ASC.Web.Core.csproj index eded733c59..0d34b4a5b9 100644 --- a/web/ASC.Web.Core/ASC.Web.Core.csproj +++ b/web/ASC.Web.Core/ASC.Web.Core.csproj @@ -416,6 +416,7 @@ + diff --git a/web/ASC.Web.Core/WebPluginManager.cs b/web/ASC.Web.Core/WebPluginManager.cs index 999859eec4..5e9fa843e3 100644 --- a/web/ASC.Web.Core/WebPluginManager.cs +++ b/web/ASC.Web.Core/WebPluginManager.cs @@ -26,29 +26,77 @@ namespace ASC.Web.Core; +[Singletone] +public class WebPluginCache +{ + private readonly ICache _сache; + private readonly ICacheNotify _notify; + private readonly TimeSpan _cacheExpiration = TimeSpan.FromDays(1); + + public WebPluginCache(ICacheNotify notify, ICache cache) + { + _сache = cache; + _notify = notify; + + _notify.Subscribe((i) => _сache.Remove(i.Key), CacheNotifyAction.Remove); + } + + public List Get(string key) + { + return _сache.Get>(key); + } + + public void Insert(string key, object value) + { + _notify.Publish(new WebPluginCacheItem { Key = key }, CacheNotifyAction.Remove); + + _сache.Insert(key, value, _cacheExpiration); + } + + public void Remove(string key) + { + _notify.Publish(new WebPluginCacheItem { Key = key }, CacheNotifyAction.Remove); + + _сache.Remove(key); + } +} + [Scope] public class WebPluginManager { + private const string StorageSystemModuleName = "systemwebplugins"; private const string StorageModuleName = "webplugins"; private const string ConfigFileName = "config.json"; private const string PluginFileName = "plugin.js"; private const string AssetsFolderName = "assets"; + private readonly CoreBaseSettings _coreBaseSettings; + private readonly SettingsManager _settingsManager; private readonly DbWebPluginService _webPluginService; private readonly WebPluginSettings _webPluginSettings; + private readonly WebPluginCache _webPluginCache; private readonly StorageFactory _storageFactory; private readonly AuthContext _authContext; + private readonly ILogger _log; public WebPluginManager( + CoreBaseSettings coreBaseSettings, + SettingsManager settingsManager, DbWebPluginService webPluginService, WebPluginSettings webPluginSettings, + WebPluginCache webPluginCache, StorageFactory storageFactory, - AuthContext authContext) + AuthContext authContext, + ILogger log) { + _coreBaseSettings = coreBaseSettings; + _settingsManager = settingsManager; _webPluginService = webPluginService; _webPluginSettings = webPluginSettings; + _webPluginCache = webPluginCache; _storageFactory = storageFactory; _authContext = authContext; + _log = log; } private void DemandWebPlugins(string action = null) @@ -58,25 +106,76 @@ public class WebPluginManager throw new SecurityException("Plugins disabled"); } - if (!string.IsNullOrWhiteSpace(action) && !_webPluginSettings.Allow.Contains(action)) + if (!string.IsNullOrWhiteSpace(action) && _webPluginSettings.Allow.Any() && !_webPluginSettings.Allow.Contains(action)) { throw new SecurityException("Forbidden action"); } } - public async Task GetPluginUrlTemplate(int tenantId) + private async Task GetPluginStorageAsync(int tenantId) { - var storage = await _storageFactory.GetStorageAsync(tenantId, StorageModuleName); + var module = tenantId == Tenant.DefaultTenant ? StorageSystemModuleName : StorageModuleName; + + var storage = await _storageFactory.GetStorageAsync(tenantId, module); + + return storage; + } + + private static string GetCacheKey(int tenantId) + { + return $"{StorageModuleName}:{tenantId}"; + } + + public async Task GetPluginUrlTemplateAsync(int tenantId) + { + var storage = await GetPluginStorageAsync(tenantId); var uri = await storage.GetUriAsync(Path.Combine("{0}", PluginFileName)); return uri?.ToString() ?? string.Empty; } - public async Task AddWebPluginFromFile(int tenantId, IFormFile file) + public async Task AddWebPluginFromFileAsync(int tenantId, IFormFile file) { DemandWebPlugins("upload"); + var system = tenantId == Tenant.DefaultTenant; + + if (system && !_coreBaseSettings.Standalone) + { + throw new SecurityException("System plugin"); + } + + var webPlugin = await SaveWebPluginToStorageAsync(tenantId, file); + + webPlugin.TenantId = tenantId; + webPlugin.Enabled = true; + webPlugin.System = system; + + if (!system) + { + var existingPlugin = await _webPluginService.GetByNameAsync(tenantId, webPlugin.Name); + + if (existingPlugin != null) + { + webPlugin.Id = existingPlugin.Id; + } + + webPlugin.CreateBy = _authContext.CurrentAccount.ID; + webPlugin.CreateOn = DateTime.UtcNow; + + webPlugin = await _webPluginService.SaveAsync(webPlugin); + } + + var key = GetCacheKey(tenantId); + + _webPluginCache.Remove(key); + + return webPlugin; + } + + private async Task SaveWebPluginToStorageAsync(int tenantId, IFormFile file) + { if (Path.GetExtension(file.FileName)?.ToLowerInvariant() != _webPluginSettings.Extension) { throw new ArgumentException("Wrong file extension"); @@ -87,7 +186,7 @@ public class WebPluginManager throw new ArgumentException("File size exceeds limit"); } - var storage = await _storageFactory.GetStorageAsync(tenantId, StorageModuleName); + var storage = await GetPluginStorageAsync(tenantId); DbWebPlugin webPlugin = null; Uri uri = null; @@ -114,34 +213,24 @@ public class WebPluginManager webPlugin = System.Text.Json.JsonSerializer.Deserialize(configContent, options); - if (webPlugin == null || string.IsNullOrEmpty(webPlugin.Name)) + if (webPlugin == null) { throw new ArgumentException("Wrong plugin archive"); } - //TODO: think about special characters - webPlugin.Name = webPlugin.Name.Replace(" ", string.Empty).ToLowerInvariant(); + var nameRegex = new Regex(@"^[a-z0-9_.-]+$"); - var existingPlugin = await _webPluginService.GetByNameAsync(tenantId, webPlugin.Name); - if (existingPlugin != null) + if (string.IsNullOrEmpty(webPlugin.Name) || !nameRegex.IsMatch(webPlugin.Name) || webPlugin.Name.StartsWith('.')) { - if (webPlugin.Version == existingPlugin.Version) - { - throw new ArgumentException("Plugin already exist"); - } - - webPlugin.Id = existingPlugin.Id; - - await storage.DeleteDirectoryAsync(string.Empty, webPlugin.Name); + throw new ArgumentException("Wrong plugin name"); } - webPlugin.TenantId = tenantId; - webPlugin.CreateBy = _authContext.CurrentAccount.ID; - webPlugin.CreateOn = DateTime.UtcNow; - webPlugin.Enabled = true; - webPlugin.System = false; + if (await storage.IsDirectoryAsync(webPlugin.Name)) + { + await storage.DeleteDirectoryAsync(webPlugin.Name); + } - webPlugin = await _webPluginService.SaveAsync(webPlugin); + uri = await storage.SaveAsync(Path.Combine(webPlugin.Name, ConfigFileName), stream); } using (var stream = zipFile.GetInputStream(pluginFile)) @@ -153,6 +242,13 @@ public class WebPluginManager { if (zipEntry.IsFile && zipEntry.Name.StartsWith(AssetsFolderName)) { + var ext = Path.GetExtension(zipEntry.Name); + + if (_webPluginSettings.AssetExtensions.Any() && !_webPluginSettings.AssetExtensions.Contains(ext)) + { + continue; + } + using (var stream = zipFile.GetInputStream(zipEntry)) { uri = await storage.SaveAsync(Path.Combine(webPlugin.Name, zipEntry.Name), stream); @@ -164,11 +260,20 @@ public class WebPluginManager return webPlugin; } - public async Task> GetWebPluginsAsync(int tenantId, bool? enabled = null) + public async Task> GetWebPluginsAsync(int tenantId) { DemandWebPlugins(); - var plugins = await _webPluginService.GetAsync(tenantId, enabled); + var key = GetCacheKey(tenantId); + + var plugins = _webPluginCache.Get(key); + + if (plugins == null) + { + plugins = await _webPluginService.GetAsync(tenantId); + + _webPluginCache.Insert(key, plugins); + } return plugins; } @@ -188,12 +293,11 @@ public class WebPluginManager var plugin = await _webPluginService.GetByIdAsync(tenantId, id) ?? throw new ItemNotFoundException("Plugin not found"); - if (plugin.System) - { - throw new SecurityException("System plugin"); - } - await _webPluginService.UpdateAsync(tenantId, plugin.Id, enabled); + + var key = GetCacheKey(tenantId); + + _webPluginCache.Remove(key); } public async Task DeleteWebPluginAsync(int tenantId, int id) @@ -202,15 +306,158 @@ public class WebPluginManager var plugin = await _webPluginService.GetByIdAsync(tenantId, id) ?? throw new ItemNotFoundException("Plugin not found"); - if (plugin.System) + await _webPluginService.DeleteAsync(tenantId, plugin.Id); + + var storage = await GetPluginStorageAsync(tenantId); + + await storage.DeleteDirectoryAsync(plugin.Name); + + var key = GetCacheKey(tenantId); + + _webPluginCache.Remove(key); + } + + + + public async Task> GetSystemWebPluginsAsync() + { + var key = GetCacheKey(Tenant.DefaultTenant); + + var systemPlugins = _webPluginCache.Get(key); + + if (systemPlugins == null) + { + systemPlugins = await GetSystemWebPluginsFromStorageAsync(); + + _webPluginCache.Insert(key, systemPlugins); + } + + return systemPlugins; + } + + private async Task> GetSystemWebPluginsFromStorageAsync() + { + var systemPlugins = new List(); + + var systemWebPluginSettings = await _settingsManager.LoadForDefaultTenantAsync(); + + var disabledPlugins = systemWebPluginSettings?.DisabledPlugins ?? new List(); + + var storage = await GetPluginStorageAsync(Tenant.DefaultTenant); + + var configFiles = await storage.ListFilesRelativeAsync(string.Empty, string.Empty, ConfigFileName, true).ToArrayAsync(); + + foreach (var path in configFiles) + { + try + { + using var readStream = await storage.GetReadStreamAsync(path); + + using var reader = new StreamReader(readStream); + + var configContent = reader.ReadToEnd(); + + var options = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }; + + var webPlugin = System.Text.Json.JsonSerializer.Deserialize(configContent, options); + + webPlugin.TenantId = Tenant.DefaultTenant; + webPlugin.System = true; + webPlugin.Enabled = !disabledPlugins.Contains(webPlugin.Name); + + systemPlugins.Add(webPlugin); + } + catch (Exception e) + { + _log.ErrorWithException(e); + } + } + + return systemPlugins; + } + + public async Task GetSystemWebPluginAsync(string name) + { + var systemWebPluginSettings = await _settingsManager.LoadForDefaultTenantAsync(); + + var disabledPlugins = systemWebPluginSettings?.DisabledPlugins ?? new List(); + + var storage = await GetPluginStorageAsync(Tenant.DefaultTenant); + + var path = Path.Combine(name, ConfigFileName); + + if (!await storage.IsFileAsync(path)) + { + throw new ItemNotFoundException("Plugin not found"); + } + + using var readStream = await storage.GetReadStreamAsync(path); + + using var reader = new StreamReader(readStream); + + var configContent = reader.ReadToEnd(); + + var options = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }; + + var webPlugin = System.Text.Json.JsonSerializer.Deserialize(configContent, options); + + webPlugin.TenantId = Tenant.DefaultTenant; + webPlugin.System = true; + webPlugin.Enabled = !disabledPlugins.Contains(webPlugin.Name); + + return webPlugin; + } + + public async Task UpdateSystemWebPluginAsync(string name, bool enabled) + { + DemandWebPlugins(); + + var systemWebPluginSettings = await _settingsManager.LoadForDefaultTenantAsync(); + + var disabledPlugins = systemWebPluginSettings?.DisabledPlugins ?? new List(); + + if (enabled) + { + disabledPlugins.Remove(name); + } + else + { + disabledPlugins.Add(name); + } + + systemWebPluginSettings.DisabledPlugins = disabledPlugins.Any() ? disabledPlugins : null; + + await _settingsManager.SaveForDefaultTenantAsync(systemWebPluginSettings); + + var key = GetCacheKey(Tenant.DefaultTenant); + + _webPluginCache.Remove(key); + } + + public async Task DeleteSystemWebPluginAsync(string name) + { + DemandWebPlugins("delete"); + + if (!_coreBaseSettings.Standalone) { throw new SecurityException("System plugin"); } - await _webPluginService.DeleteAsync(tenantId, plugin.Id); + var storage = await GetPluginStorageAsync(Tenant.DefaultTenant); - var storage = await _storageFactory.GetStorageAsync(tenantId, StorageModuleName); + if (!await storage.IsDirectoryAsync(name)) + { + throw new ItemNotFoundException("Plugin not found"); + } - await storage.DeleteDirectoryAsync(string.Empty, plugin.Name); + await UpdateSystemWebPluginAsync(name, true); + + await storage.DeleteDirectoryAsync(name); } } diff --git a/web/ASC.Web.Core/WebPluginSettings.cs b/web/ASC.Web.Core/WebPluginSettings.cs index c294247cba..695ea0cd41 100644 --- a/web/ASC.Web.Core/WebPluginSettings.cs +++ b/web/ASC.Web.Core/WebPluginSettings.cs @@ -38,6 +38,8 @@ public class WebPluginSettings private long _maxSize; private string _extension; private string[] _allow; + private string[] _assetExtensions; + private string _systemUrl; public bool Enabled { @@ -61,4 +63,26 @@ public class WebPluginSettings get => _allow ?? Array.Empty(); set => _allow = value; } + + public string[] AssetExtensions + { + get => _assetExtensions ?? Array.Empty(); + set => _assetExtensions = value; + } } + +public class SystemWebPluginSettings : ISettings +{ + public List DisabledPlugins { get; set; } + + [JsonIgnore] + public Guid ID + { + get { return new Guid("{33039FD8-CF74-46B5-9AF2-2B3D4B651F31}"); } + } + + public SystemWebPluginSettings GetDefault() + { + return new SystemWebPluginSettings(); + } +} \ No newline at end of file diff --git a/web/ASC.Web.Core/protos/web_plugin_cache_item.proto b/web/ASC.Web.Core/protos/web_plugin_cache_item.proto new file mode 100644 index 0000000000..abb3ffdb74 --- /dev/null +++ b/web/ASC.Web.Core/protos/web_plugin_cache_item.proto @@ -0,0 +1,8 @@ + +syntax = "proto3"; + +package ASC.Web.Core; + +message WebPluginCacheItem { + string key = 1; +} \ No newline at end of file