using System.Reflection; using Microsoft.AspNetCore.Http; using Nop.Core; using Nop.Core.Domain.Catalog; using Nop.Core.Domain.Customers; using Nop.Core.Domain.Media; using Nop.Core.Infrastructure; using Nop.Data.Migrations; using Nop.Services.Customers; using Nop.Services.Localization; using Nop.Services.Logging; namespace Nop.Services.Plugins; /// /// Represents the plugin service implementation /// public partial class PluginService : IPluginService { #region Fields protected readonly CatalogSettings _catalogSettings; protected readonly ICustomerService _customerService; protected readonly IHttpContextAccessor _httpContextAccessor; protected readonly Lazy _migrationManager; protected readonly ILogger _logger; protected readonly INopFileProvider _fileProvider; protected readonly IPluginsInfo _pluginsInfo; protected readonly IWebHelper _webHelper; protected readonly MediaSettings _mediaSettings; #endregion #region Ctor public PluginService(CatalogSettings catalogSettings, ICustomerService customerService, IHttpContextAccessor httpContextAccessor, Lazy migrationManager, ILogger logger, INopFileProvider fileProvider, IWebHelper webHelper, MediaSettings mediaSettings) { _catalogSettings = catalogSettings; _customerService = customerService; _httpContextAccessor = httpContextAccessor; _migrationManager = migrationManager; _logger = logger; _fileProvider = fileProvider; _pluginsInfo = Singleton.Instance; _webHelper = webHelper; _mediaSettings = mediaSettings; } #endregion #region Utilities /// /// Check whether to load the plugin based on the load mode passed /// /// Plugin descriptor to check /// Load plugins mode /// Result of check protected virtual bool FilterByLoadMode(PluginDescriptor pluginDescriptor, LoadPluginsMode loadMode) { ArgumentNullException.ThrowIfNull(pluginDescriptor); return loadMode switch { LoadPluginsMode.All => true, LoadPluginsMode.InstalledOnly => pluginDescriptor.Installed, LoadPluginsMode.NotInstalledOnly => !pluginDescriptor.Installed, _ => throw new NotSupportedException(nameof(loadMode)), }; } /// /// Check whether to load the plugin based on the plugin group passed /// /// Plugin descriptor to check /// Group name /// Result of check protected virtual bool FilterByPluginGroup(PluginDescriptor pluginDescriptor, string group) { ArgumentNullException.ThrowIfNull(pluginDescriptor); if (string.IsNullOrEmpty(group)) return true; return group.Equals(pluginDescriptor.Group, StringComparison.InvariantCultureIgnoreCase); } /// /// Check whether to load the plugin based on the customer passed /// /// Plugin descriptor to check /// Customer /// /// A task that represents the asynchronous operation /// The task result contains the result of check /// protected virtual async Task FilterByCustomerAsync(PluginDescriptor pluginDescriptor, Customer customer) { ArgumentNullException.ThrowIfNull(pluginDescriptor); if (customer == null || !pluginDescriptor.LimitedToCustomerRoles.Any()) return true; if (_catalogSettings.IgnoreAcl) return true; return pluginDescriptor.LimitedToCustomerRoles.Intersect(await _customerService.GetCustomerRoleIdsAsync(customer)).Any(); } /// /// Check whether to load the plugin based on the store identifier passed /// /// Plugin descriptor to check /// Store identifier /// Result of check protected virtual bool FilterByStore(PluginDescriptor pluginDescriptor, int storeId) { ArgumentNullException.ThrowIfNull(pluginDescriptor); //no validation required if (storeId == 0) return true; if (!pluginDescriptor.LimitedToStores.Any()) return true; return pluginDescriptor.LimitedToStores.Contains(storeId); } /// /// Check whether to load the plugin based on dependency from other plugin /// /// Plugin descriptor to check /// Other plugin system name /// Result of check protected virtual bool FilterByDependsOn(PluginDescriptor pluginDescriptor, string dependsOnSystemName) { ArgumentNullException.ThrowIfNull(pluginDescriptor); if (string.IsNullOrEmpty(dependsOnSystemName)) return true; return pluginDescriptor.DependsOn?.Contains(dependsOnSystemName) ?? false; } /// /// Check whether to load the plugin based on the plugin friendly name passed /// /// Plugin descriptor to check /// Plugin friendly name /// Result of check protected virtual bool FilterByPluginFriendlyName(PluginDescriptor pluginDescriptor, string friendlyName) { ArgumentNullException.ThrowIfNull(pluginDescriptor); if (string.IsNullOrEmpty(friendlyName)) return true; return pluginDescriptor.FriendlyName.Contains(friendlyName, StringComparison.InvariantCultureIgnoreCase); } /// /// Check whether to load the plugin based on the plugin author passed /// /// Plugin descriptor to check /// Plugin author /// Result of check protected virtual bool FilterByPluginAuthor(PluginDescriptor pluginDescriptor, string author) { ArgumentNullException.ThrowIfNull(pluginDescriptor); if (string.IsNullOrEmpty(author)) return true; return pluginDescriptor.Author.Contains(author, StringComparison.InvariantCultureIgnoreCase); } /// /// Insert plugin data /// /// Plugin type /// Migration process type protected virtual void InsertPluginData(Type pluginType, MigrationProcessType migrationProcessType = MigrationProcessType.NoMatter) { var assembly = Assembly.GetAssembly(pluginType); _migrationManager.Value.ApplyUpMigrations(assembly, migrationProcessType); //mark update migrations as applied if (migrationProcessType == MigrationProcessType.Installation) { _migrationManager.Value.ApplyUpMigrations(assembly, MigrationProcessType.Update, true); } } #endregion #region Methods /// /// Get plugin descriptors /// /// The type of plugins to get /// Filter by load plugins mode /// Filter by customer; pass null to load all records /// Filter by store; pass 0 to load all records /// Filter by plugin group; pass null to load all records /// Filter by plugin friendly name; pass null to load all records /// Filter by plugin author; pass null to load all records /// System name of the plugin to define dependencies /// /// A task that represents the asynchronous operation /// The task result contains the plugin descriptors /// public virtual async Task> GetPluginDescriptorsAsync(LoadPluginsMode loadMode = LoadPluginsMode.InstalledOnly, Customer customer = null, int storeId = 0, string group = null, string dependsOnSystemName = "", string friendlyName = null, string author = null) where TPlugin : class, IPlugin { var pluginDescriptors = _pluginsInfo.PluginDescriptors.Select(p => p.pluginDescriptor).ToList(); //filter plugins pluginDescriptors = await pluginDescriptors.WhereAwait(async descriptor => FilterByLoadMode(descriptor, loadMode) && await FilterByCustomerAsync(descriptor, customer) && FilterByStore(descriptor, storeId) && FilterByPluginGroup(descriptor, group) && FilterByDependsOn(descriptor, dependsOnSystemName) && FilterByPluginFriendlyName(descriptor, friendlyName) && FilterByPluginAuthor(descriptor, author)).ToListAsync(); //filter by the passed type if (typeof(TPlugin) != typeof(IPlugin)) pluginDescriptors = pluginDescriptors.Where(descriptor => typeof(TPlugin).IsAssignableFrom(descriptor.PluginType)).ToList(); //order by group name pluginDescriptors = pluginDescriptors.OrderBy(descriptor => descriptor.Group) .ThenBy(descriptor => descriptor.DisplayOrder).ToList(); return pluginDescriptors; } /// /// Get a plugin descriptor by the system name /// /// The type of plugin to get /// Plugin system name /// Load plugins mode /// Filter by customer; pass null to load all records /// Filter by store; pass 0 to load all records /// Filter by plugin group; pass null to load all records /// /// A task that represents the asynchronous operation /// The task result contains the >Plugin descriptor /// public virtual async Task GetPluginDescriptorBySystemNameAsync(string systemName, LoadPluginsMode loadMode = LoadPluginsMode.InstalledOnly, Customer customer = null, int storeId = 0, string @group = null) where TPlugin : class, IPlugin { return (await GetPluginDescriptorsAsync(loadMode, customer, storeId, group)) .FirstOrDefault(descriptor => descriptor.SystemName.Equals(systemName)); } /// /// Get plugins /// /// The type of plugins to get /// Filter by load plugins mode /// Filter by customer; pass null to load all records /// Filter by store; pass 0 to load all records /// Filter by plugin group; pass null to load all records /// /// A task that represents the asynchronous operation /// The task result contains the plugins /// public virtual async Task> GetPluginsAsync( LoadPluginsMode loadMode = LoadPluginsMode.InstalledOnly, Customer customer = null, int storeId = 0, string @group = null) where TPlugin : class, IPlugin { return (await GetPluginDescriptorsAsync(loadMode, customer, storeId, group)) .Select(descriptor => descriptor.Instance()).ToList(); } /// /// Find a plugin by the type which is located into the same assembly as a plugin /// /// Type /// Plugin public virtual IPlugin FindPluginByTypeInAssembly(Type typeInAssembly) { ArgumentNullException.ThrowIfNull(typeInAssembly); //try to do magic var pluginDescriptor = _pluginsInfo.PluginDescriptors.FirstOrDefault(descriptor => descriptor.pluginDescriptor?.ReferencedAssembly?.FullName?.Equals(typeInAssembly.Assembly.FullName, StringComparison.InvariantCultureIgnoreCase) ?? false); return pluginDescriptor.pluginDescriptor?.Instance(); } /// /// Get plugin logo URL /// /// Plugin descriptor /// /// A task that represents the asynchronous operation /// The task result contains the logo URL /// public virtual Task GetPluginLogoUrlAsync(PluginDescriptor pluginDescriptor) { var pluginDirectory = _fileProvider.GetDirectoryName(pluginDescriptor.OriginalAssemblyFile); if (string.IsNullOrEmpty(pluginDirectory)) return Task.FromResult(null); //check for supported extensions var logoExtension = NopPluginDefaults.SupportedLogoImageExtensions .FirstOrDefault(ext => _fileProvider.FileExists(_fileProvider.Combine(pluginDirectory, $"{NopPluginDefaults.LogoFileName}.{ext}"))); if (string.IsNullOrWhiteSpace(logoExtension)) return Task.FromResult(null); var pathBase = _httpContextAccessor.HttpContext.Request.PathBase.Value ?? string.Empty; var logoPathUrl = _mediaSettings.UseAbsoluteImagePath ? _webHelper.GetStoreLocation() : $"{pathBase}/"; var logoUrl = $"{logoPathUrl}{NopPluginDefaults.PathName}/" + $"{_fileProvider.GetDirectoryNameOnly(pluginDirectory)}/{NopPluginDefaults.LogoFileName}.{logoExtension}"; return Task.FromResult(logoUrl); } /// /// Prepare plugin to the installation /// /// Plugin system name /// Customer /// Specifies whether to check plugin dependencies /// A task that represents the asynchronous operation public virtual async Task PreparePluginToInstallAsync(string systemName, Customer customer = null, bool checkDependencies = true) { //add plugin name to the appropriate list (if not yet contained) and save changes if (_pluginsInfo.PluginNamesToInstall.Any(item => item.SystemName == systemName)) return; var pluginsAfterRestart = _pluginsInfo.InstalledPlugins.Select(pd => pd.SystemName).Where(installedSystemName => !_pluginsInfo.PluginNamesToUninstall.Contains(installedSystemName)).ToList(); pluginsAfterRestart.AddRange(_pluginsInfo.PluginNamesToInstall.Select(item => item.SystemName)); if (checkDependencies) { var descriptor = await GetPluginDescriptorBySystemNameAsync(systemName, LoadPluginsMode.NotInstalledOnly); if (descriptor.DependsOn?.Any() ?? false) { var dependsOn = descriptor.DependsOn .Where(dependsOnSystemName => !pluginsAfterRestart.Contains(dependsOnSystemName)).ToList(); if (dependsOn.Any()) { var dependsOnSystemNames = dependsOn.Aggregate((all, current) => $"{all}, {current}"); //do not inject services via constructor because it'll cause circular references var localizationService = EngineContext.Current.Resolve(); var errorMessage = string.Format(await localizationService.GetResourceAsync("Admin.Plugins.Errors.InstallDependsOn"), string.IsNullOrEmpty(descriptor.FriendlyName) ? descriptor.SystemName : descriptor.FriendlyName, dependsOnSystemNames); throw new NopException(errorMessage); } } } _pluginsInfo.PluginNamesToInstall.Add((systemName, customer?.CustomerGuid)); await _pluginsInfo.SaveAsync(); } /// /// Prepare plugin to the uninstallation /// /// Plugin system name /// A task that represents the asynchronous operation public virtual async Task PreparePluginToUninstallAsync(string systemName) { //add plugin name to the appropriate list (if not yet contained) and save changes if (_pluginsInfo.PluginNamesToUninstall.Contains(systemName)) return; var dependentPlugins = await GetPluginDescriptorsAsync(dependsOnSystemName: systemName); var descriptor = await GetPluginDescriptorBySystemNameAsync(systemName); if (dependentPlugins.Any()) { var dependsOn = new List(); foreach (var dependentPlugin in dependentPlugins) { if (!_pluginsInfo.InstalledPlugins.Select(pd => pd.SystemName).Contains(dependentPlugin.SystemName)) continue; if (_pluginsInfo.PluginNamesToUninstall.Contains(dependentPlugin.SystemName)) continue; dependsOn.Add(string.IsNullOrEmpty(dependentPlugin.FriendlyName) ? dependentPlugin.SystemName : dependentPlugin.FriendlyName); } if (dependsOn.Any()) { var dependsOnSystemNames = dependsOn.Aggregate((all, current) => $"{all}, {current}"); //do not inject services via constructor because it'll cause circular references var localizationService = EngineContext.Current.Resolve(); var errorMessage = string.Format(await localizationService.GetResourceAsync("Admin.Plugins.Errors.UninstallDependsOn"), string.IsNullOrEmpty(descriptor.FriendlyName) ? descriptor.SystemName : descriptor.FriendlyName, dependsOnSystemNames); throw new NopException(errorMessage); } } var plugin = descriptor?.Instance(); if (plugin != null) await plugin.PreparePluginToUninstallAsync(); _pluginsInfo.PluginNamesToUninstall.Add(systemName); await _pluginsInfo.SaveAsync(); } /// /// Prepare plugin to the removing /// /// Plugin system name /// A task that represents the asynchronous operation public virtual async Task PreparePluginToDeleteAsync(string systemName) { //add plugin name to the appropriate list (if not yet contained) and save changes if (_pluginsInfo.PluginNamesToDelete.Contains(systemName)) return; _pluginsInfo.PluginNamesToDelete.Add(systemName); await _pluginsInfo.SaveAsync(); } /// /// Reset changes /// public virtual void ResetChanges() { //clear lists and save changes _pluginsInfo.PluginNamesToDelete.Clear(); _pluginsInfo.PluginNamesToInstall.Clear(); _pluginsInfo.PluginNamesToUninstall.Clear(); _pluginsInfo.Save(); //display all plugins on the plugin list page var pluginDescriptors = _pluginsInfo.PluginDescriptors.ToList(); foreach (var pluginDescriptor in pluginDescriptors) pluginDescriptor.pluginDescriptor.ShowInPluginsList = true; //clear the uploaded directory foreach (var directory in _fileProvider.GetDirectories(_fileProvider.MapPath(NopPluginDefaults.UploadedPath))) _fileProvider.DeleteDirectory(directory); } /// /// Clear installed plugins list /// public virtual void ClearInstalledPluginsList() { _pluginsInfo.InstalledPlugins.Clear(); } /// /// Install plugins /// /// A task that represents the asynchronous operation public virtual async Task InstallPluginsAsync() { //get all uninstalled plugins var pluginDescriptors = _pluginsInfo.PluginDescriptors.Where(descriptor => !descriptor.pluginDescriptor.Installed).ToList(); //filter plugins need to install pluginDescriptors = pluginDescriptors.Where(descriptor => _pluginsInfo.PluginNamesToInstall .Any(item => item.SystemName.Equals(descriptor.pluginDescriptor.SystemName))).ToList(); if (!pluginDescriptors.Any()) return; //do not inject services via constructor because it'll cause circular references var localizationService = EngineContext.Current.Resolve(); var customerActivityService = EngineContext.Current.Resolve(); //install plugins foreach (var descriptor in pluginDescriptors.OrderBy(pluginDescriptor => pluginDescriptor.pluginDescriptor.DisplayOrder)) { try { InsertPluginData(descriptor.pluginDescriptor.PluginType, MigrationProcessType.Installation); //try to install an instance await descriptor.pluginDescriptor.Instance().InstallAsync(); //remove and add plugin system name to appropriate lists var pluginToInstall = _pluginsInfo.PluginNamesToInstall .FirstOrDefault(plugin => plugin.SystemName.Equals(descriptor.pluginDescriptor.SystemName)); _pluginsInfo.InstalledPlugins.Add(descriptor.pluginDescriptor.GetBaseInfoCopy); _pluginsInfo.PluginNamesToInstall.Remove(pluginToInstall); //activity log var customer = await _customerService.GetCustomerByGuidAsync(pluginToInstall.CustomerGuid ?? Guid.Empty); await customerActivityService.InsertActivityAsync(customer, "InstallNewPlugin", string.Format(await localizationService.GetResourceAsync("ActivityLog.InstallNewPlugin"), descriptor.pluginDescriptor.SystemName, descriptor.pluginDescriptor.Version)); //mark the plugin as installed descriptor.pluginDescriptor.Installed = true; descriptor.pluginDescriptor.ShowInPluginsList = true; } catch (Exception exception) { //log error var message = string.Format(await localizationService.GetResourceAsync("Admin.Plugins.Errors.NotInstalled"), descriptor.pluginDescriptor.SystemName); await _logger.ErrorAsync(message, exception); } } //save changes await _pluginsInfo.SaveAsync(); } /// /// Uninstall plugins /// /// A task that represents the asynchronous operation public virtual async Task UninstallPluginsAsync() { //get all installed plugins var pluginDescriptors = _pluginsInfo.PluginDescriptors.Where(descriptor => descriptor.pluginDescriptor.Installed).ToList(); //filter plugins need to uninstall pluginDescriptors = pluginDescriptors .Where(descriptor => _pluginsInfo.PluginNamesToUninstall.Contains(descriptor.pluginDescriptor.SystemName)).ToList(); if (!pluginDescriptors.Any()) return; //do not inject services via constructor because it'll cause circular references var localizationService = EngineContext.Current.Resolve(); var customerActivityService = EngineContext.Current.Resolve(); //uninstall plugins foreach (var descriptor in pluginDescriptors.OrderByDescending(pluginDescriptor => pluginDescriptor.pluginDescriptor.DisplayOrder)) { try { var plugin = descriptor.pluginDescriptor.Instance(); //try to uninstall an instance await plugin.UninstallAsync(); //clear plugin data on the database var assembly = Assembly.GetAssembly(descriptor.pluginDescriptor.PluginType); _migrationManager.Value.ApplyDownMigrations(assembly); //remove plugin system name from appropriate lists _pluginsInfo.InstalledPlugins.Remove(descriptor.pluginDescriptor); _pluginsInfo.PluginNamesToUninstall.Remove(descriptor.pluginDescriptor.SystemName); //activity log await customerActivityService.InsertActivityAsync("UninstallPlugin", string.Format(await localizationService.GetResourceAsync("ActivityLog.UninstallPlugin"), descriptor.pluginDescriptor.SystemName, descriptor.pluginDescriptor.Version)); //mark the plugin as uninstalled descriptor.pluginDescriptor.Installed = false; descriptor.pluginDescriptor.ShowInPluginsList = true; } catch (Exception exception) { //log error var message = string.Format(await localizationService.GetResourceAsync("Admin.Plugins.Errors.NotUninstalled"), descriptor.pluginDescriptor.SystemName); await _logger.ErrorAsync(message, exception); } } //save changes await _pluginsInfo.SaveAsync(); } /// /// Delete plugins /// /// A task that represents the asynchronous operation public virtual async Task DeletePluginsAsync() { //get all uninstalled plugins (delete plugin only previously uninstalled) var pluginDescriptors = _pluginsInfo.PluginDescriptors.Where(descriptor => !descriptor.pluginDescriptor.Installed).ToList(); //filter plugins need to delete pluginDescriptors = pluginDescriptors .Where(descriptor => _pluginsInfo.PluginNamesToDelete.Contains(descriptor.pluginDescriptor.SystemName)).ToList(); if (!pluginDescriptors.Any()) return; //do not inject services via constructor because it'll cause circular references var localizationService = EngineContext.Current.Resolve(); var customerActivityService = EngineContext.Current.Resolve(); //delete plugins foreach (var descriptor in pluginDescriptors) { try { //try to delete a plugin directory from disk storage var pluginDirectory = _fileProvider.GetDirectoryName(descriptor.pluginDescriptor.OriginalAssemblyFile); if (_fileProvider.DirectoryExists(pluginDirectory)) _fileProvider.DeleteDirectory(pluginDirectory); //remove plugin system name from the appropriate list _pluginsInfo.PluginNamesToDelete.Remove(descriptor.pluginDescriptor.SystemName); //activity log await customerActivityService.InsertActivityAsync("DeletePlugin", string.Format(await localizationService.GetResourceAsync("ActivityLog.DeletePlugin"), descriptor.pluginDescriptor.SystemName, descriptor.pluginDescriptor.Version)); } catch (Exception exception) { //log error var message = string.Format(await localizationService.GetResourceAsync("Admin.Plugins.Errors.NotDeleted"), descriptor.pluginDescriptor.SystemName); await _logger.ErrorAsync(message, exception); } } //save changes await _pluginsInfo.SaveAsync(); } /// /// Check whether application restart is required to apply changes to plugins /// /// Result of check public virtual bool IsRestartRequired() { //return true if any of lists contains items or some plugins were uploaded return _pluginsInfo.PluginNamesToInstall.Any() || _pluginsInfo.PluginNamesToUninstall.Any() || _pluginsInfo.PluginNamesToDelete.Any() || IsPluginsUploaded; } /// /// Update plugins /// /// A task that represents the asynchronous operation public virtual async Task UpdatePluginsAsync() { //do not inject services via constructor because it'll cause circular references var localizationService = EngineContext.Current.Resolve(); var customerActivityService = EngineContext.Current.Resolve(); foreach (var installedPlugin in _pluginsInfo.InstalledPlugins) { var newVersion = _pluginsInfo.PluginDescriptors.FirstOrDefault(pd => pd.pluginDescriptor.SystemName.Equals(installedPlugin.SystemName, StringComparison.InvariantCultureIgnoreCase)); if (newVersion.pluginDescriptor == null) continue; if (installedPlugin.Version == newVersion.pluginDescriptor.Version) continue; //run new migrations from the plugin if there are exists InsertPluginData(newVersion.pluginDescriptor.PluginType, MigrationProcessType.Update); //run the plugin update logic await newVersion.pluginDescriptor.Instance().UpdateAsync(installedPlugin.Version, newVersion.pluginDescriptor.Version); //activity log await customerActivityService.InsertActivityAsync("UpdatePlugin", string.Format(await localizationService.GetResourceAsync("ActivityLog.UpdatePlugin"), newVersion.pluginDescriptor.SystemName, installedPlugin.Version, newVersion.pluginDescriptor.Version)); //update installed plugin info installedPlugin.Version = newVersion.pluginDescriptor.Version; } await _pluginsInfo.SaveAsync(); } /// /// Get names of incompatible plugins /// /// List of plugin names public virtual IDictionary GetIncompatiblePlugins() { return _pluginsInfo.IncompatiblePlugins; } /// /// Get all assembly loaded collisions /// /// List of plugin loaded assembly info public virtual IList GetAssemblyCollisions() { return _pluginsInfo.AssemblyLoadedCollision; } #endregion #region Properties /// /// Indicates whether new or updated plugins have been uploaded. /// True - if the plugins were loaded, false otherwise /// protected virtual bool IsPluginsUploaded { get { var pluginsDirectories = _fileProvider.GetDirectories(_fileProvider.MapPath(NopPluginDefaults.UploadedPath)); if (!pluginsDirectories.Any()) return false; return pluginsDirectories.Any(d => _fileProvider.GetFiles(d, "*.dll").Any() || _fileProvider.GetFiles(d, "plugin.json").Any()); } } #endregion }