Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- using System;
- using System.Collections.Generic;
- using System.Diagnostics;
- using System.IO;
- using System.Linq;
- using System.Reflection;
- using System.Threading;
- using Microsoft.AspNetCore.Mvc.ApplicationParts;
- using Newtonsoft.Json;
- using Nop.Core.ComponentModel;
- using Nop.Core.Configuration;
- //Contributor: Umbraco (http://www.umbraco.com). Thanks a lot!
- //SEE THIS POST for full details of what this does - http://shazwazza.com/post/Developing-a-plugin-framework-in-ASPNET-with-medium-trust.aspx
- namespace Nop.Core.Plugins
- {
- /// <summary>
- /// Sets the application up for the plugin referencing
- /// </summary>
- public class PluginManager
- {
- #region Fields
- private static readonly ReaderWriterLockSlim Locker = new ReaderWriterLockSlim();
- private static readonly List<string> BaseAppLibraries;
- #endregion
- #region Ctor
- static PluginManager()
- {
- //get all libraries from /bin/{version}/ directory
- BaseAppLibraries = new DirectoryInfo(AppDomain.CurrentDomain.BaseDirectory)
- .GetFiles("*.dll", SearchOption.TopDirectoryOnly).Select(fi => fi.Name).ToList();
- //get all libraries from base site directory
- if (!AppDomain.CurrentDomain.BaseDirectory.Equals(Environment.CurrentDirectory, StringComparison.InvariantCultureIgnoreCase))
- BaseAppLibraries.AddRange(new DirectoryInfo(Environment.CurrentDirectory).GetFiles("*.dll", SearchOption.TopDirectoryOnly).Select(fi => fi.Name));
- //get all libraries from refs directory
- var refsPathName = new DirectoryInfo(Path.Combine(Environment.CurrentDirectory, RefsPathName));
- if (refsPathName.Exists)
- BaseAppLibraries.AddRange(refsPathName.GetFiles("*.dll", SearchOption.TopDirectoryOnly).Select(fi => fi.Name));
- }
- #endregion
- #region Utilities
- /// <summary>
- /// Get description files
- /// </summary>
- /// <param name="pluginFolder">Plugin directory info</param>
- /// <returns>Original and parsed description files</returns>
- private static IEnumerable<KeyValuePair<FileInfo, PluginDescriptor>> GetDescriptionFilesAndDescriptors(DirectoryInfo pluginFolder)
- {
- if (pluginFolder == null)
- throw new ArgumentNullException(nameof(pluginFolder));
- //create list (<file info, parsed plugin descritor>)
- var result = new List<KeyValuePair<FileInfo, PluginDescriptor>>();
- //add display order and path to list
- foreach (var descriptionFile in pluginFolder.GetFiles(PluginDescriptionFileName, SearchOption.AllDirectories))
- {
- if (!IsPackagePluginFolder(descriptionFile.Directory))
- continue;
- //parse file
- var pluginDescriptor = GetPluginDescriptorFromFile(descriptionFile.FullName);
- //populate list
- result.Add(new KeyValuePair<FileInfo, PluginDescriptor>(descriptionFile, pluginDescriptor));
- }
- //sort list by display order. NOTE: Lowest DisplayOrder will be first i.e 0 , 1, 1, 1, 5, 10
- //it's required: https://www.nopcommerce.com/boards/t/17455/load-plugins-based-on-their-displayorder-on-startup.aspx
- result.Sort((firstPair, nextPair) => firstPair.Value.DisplayOrder.CompareTo(nextPair.Value.DisplayOrder));
- return result;
- }
- /// <summary>
- /// Get system names of installed plugins
- /// </summary>
- /// <param name="filePath">Path to the file</param>
- /// <returns>List of plugin system names</returns>
- private static IList<string> GetInstalledPluginNames(string filePath)
- {
- //check whether file exists
- if (!File.Exists(filePath))
- {
- //if not, try to parse the file that was used in previous nopCommerce versions
- filePath = CommonHelper.MapPath(ObsoleteInstalledPluginsFilePath);
- if (!File.Exists(filePath))
- return new List<string>();
- //get plugin system names from the old txt file
- var pluginSystemNames = new List<string>();
- using (var reader = new StringReader(File.ReadAllText(filePath)))
- {
- var pluginName = string.Empty;
- while ((pluginName = reader.ReadLine()) != null)
- {
- if (!string.IsNullOrWhiteSpace(pluginName))
- pluginSystemNames.Add(pluginName.Trim());
- }
- }
- //save system names of installed plugins to the new file
- SaveInstalledPluginNames(pluginSystemNames, CommonHelper.MapPath(InstalledPluginsFilePath));
- //and delete the old one
- File.Delete(filePath);
- return pluginSystemNames;
- }
- var text = File.ReadAllText(filePath);
- if (string.IsNullOrEmpty(text))
- return new List<string>();
- //get plugin system names from the JSON file
- return JsonConvert.DeserializeObject<IList<string>>(text);
- }
- /// <summary>
- /// Save system names of installed plugins to the file
- /// </summary>
- /// <param name="pluginSystemNames">List of plugin system names</param>
- /// <param name="filePath">Path to the file</param>
- private static void SaveInstalledPluginNames(IList<string> pluginSystemNames, string filePath)
- {
- //save the file
- var text = JsonConvert.SerializeObject(pluginSystemNames, Formatting.Indented);
- File.WriteAllText(filePath, text);
- }
- /// <summary>
- /// Indicates whether assembly file is already loaded
- /// </summary>
- /// <param name="fileInfo">File info</param>
- /// <returns>Result</returns>
- private static bool IsAlreadyLoaded(FileInfo fileInfo)
- {
- //search library file name in base directory to ignore already existing (loaded) libraries
- //(we do it because not all libraries are loaded immediately after application start)
- if (BaseAppLibraries.Any(sli => sli.Equals(fileInfo.Name, StringComparison.InvariantCultureIgnoreCase)))
- return true;
- //compare full assembly name
- //var fileAssemblyName = AssemblyName.GetAssemblyName(fileInfo.FullName);
- //foreach (var a in AppDomain.CurrentDomain.GetAssemblies())
- //{
- // if (a.FullName.Equals(fileAssemblyName.FullName, StringComparison.InvariantCultureIgnoreCase))
- // return true;
- //}
- //return false;
- //do not compare the full assembly name, just filename
- try
- {
- var fileNameWithoutExt = Path.GetFileNameWithoutExtension(fileInfo.FullName);
- if (string.IsNullOrEmpty(fileNameWithoutExt))
- throw new Exception($"Cannot get file extension for {fileInfo.Name}");
- foreach (var a in AppDomain.CurrentDomain.GetAssemblies())
- {
- var assemblyName = a.FullName.Split(',').FirstOrDefault();
- if (fileNameWithoutExt.Equals(assemblyName, StringComparison.InvariantCultureIgnoreCase))
- return true;
- }
- }
- catch (Exception exc)
- {
- Debug.WriteLine("Cannot validate whether an assembly is already loaded. " + exc);
- }
- return false;
- }
- /// <summary>
- /// Perform file deploy
- /// </summary>
- /// <param name="plug">Plugin file info</param>
- /// <param name="applicationPartManager">Application part manager</param>
- /// <param name="config">Config</param>
- /// <returns>Assembly</returns>
- private static Assembly PerformFileDeploy(FileInfo plug, ApplicationPartManager applicationPartManager, NopConfig config)
- {
- if (plug.Directory == null || plug.Directory.Parent == null)
- throw new InvalidOperationException("The plugin directory for the " + plug.Name + " file exists in a folder outside of the allowed nopCommerce folder hierarchy");
- //we can now register the plugin definition
- var assemblyName = AssemblyName.GetAssemblyName(plug.FullName);
- Assembly assembly;
- try
- {
- assembly = Assembly.Load(assemblyName);
- }
- catch (FileLoadException)
- {
- if (config.UseUnsafeLoadAssembly)
- {
- //if an application has been copied from the web, it is flagged by Windows as being a web application,
- //even if it resides on the local computer.You can change that designation by changing the file properties,
- //or you can use the<loadFromRemoteSources> element to grant the assembly full trust.As an alternative,
- //you can use the UnsafeLoadFrom method to load a local assembly that the operating system has flagged as
- //having been loaded from the web.
- //see http://go.microsoft.com/fwlink/?LinkId=155569 for more information.
- assembly = Assembly.UnsafeLoadFrom(plug.FullName);
- }
- else
- {
- throw;
- }
- }
- Debug.WriteLine("Adding to ApplicationParts: '{0}'", assembly.FullName);
- applicationPartManager.ApplicationParts.Add(new AssemblyPart(assembly));
- return assembly;
- }
- /// <summary>
- /// Determines if the folder is a bin plugin folder for a package
- /// </summary>
- /// <param name="folder"></param>
- /// <returns></returns>
- private static bool IsPackagePluginFolder(DirectoryInfo folder)
- {
- if (folder?.Parent == null) return false;
- if (!folder.Parent.Name.Equals(PluginsPathName, StringComparison.InvariantCultureIgnoreCase)) return false;
- return true;
- }
- #endregion
- #region Methods
- /// <summary>
- /// Initialize
- /// </summary>
- /// <param name="applicationPartManager">Application part manager</param>
- /// <param name="config">Config</param>
- public static void Initialize(ApplicationPartManager applicationPartManager, NopConfig config)
- {
- if (applicationPartManager == null)
- throw new ArgumentNullException(nameof(applicationPartManager));
- if (config == null)
- throw new ArgumentNullException(nameof(config));
- using (new WriteLockDisposable(Locker))
- {
- // TODO: Add verbose exception handling / raising here since this is happening on app startup and could
- // prevent app from starting altogether
- var pluginFolder = new DirectoryInfo(CommonHelper.MapPath(PluginsPath));
- var referencedPlugins = new List<PluginDescriptor>();
- var incompatiblePlugins = new List<string>();
- try
- {
- var installedPluginSystemNames = GetInstalledPluginNames(CommonHelper.MapPath(InstalledPluginsFilePath));
- //ensure folders are created
- Directory.CreateDirectory(pluginFolder.FullName);
- //load description files
- foreach (var dfd in GetDescriptionFilesAndDescriptors(pluginFolder))
- {
- var descriptionFile = dfd.Key;
- var pluginDescriptor = dfd.Value;
- //ensure that version of plugin is valid
- if (!pluginDescriptor.SupportedVersions.Contains(NopVersion.CurrentVersion, StringComparer.InvariantCultureIgnoreCase))
- {
- incompatiblePlugins.Add(pluginDescriptor.SystemName);
- continue;
- }
- //some validation
- if (string.IsNullOrWhiteSpace(pluginDescriptor.SystemName))
- throw new Exception($"A plugin '{descriptionFile.FullName}' has no system name. Try assigning the plugin a unique name and recompiling.");
- if (referencedPlugins.Contains(pluginDescriptor))
- throw new Exception($"A plugin with '{pluginDescriptor.SystemName}' system name is already defined");
- //set 'Installed' property
- pluginDescriptor.Installed = installedPluginSystemNames
- .FirstOrDefault(x => x.Equals(pluginDescriptor.SystemName, StringComparison.InvariantCultureIgnoreCase)) != null;
- try
- {
- if (descriptionFile.Directory == null)
- throw new Exception($"Directory cannot be resolved for '{descriptionFile.Name}' description file");
- //get list of all DLLs in plugins (not in bin!)
- var pluginFiles = descriptionFile.Directory.GetFiles("*.dll", SearchOption.AllDirectories)
- .Where(x => IsPackagePluginFolder(x.Directory))
- .ToList();
- //other plugin description info
- var mainPluginFile = pluginFiles
- .FirstOrDefault(x => x.Name.Equals(pluginDescriptor.AssemblyFileName, StringComparison.InvariantCultureIgnoreCase));
- //plugin have wrong directory
- if (mainPluginFile == null)
- {
- incompatiblePlugins.Add(pluginDescriptor.SystemName);
- continue;
- }
- pluginDescriptor.OriginalAssemblyFile = mainPluginFile;
- //shadow copy main plugin file
- pluginDescriptor.ReferencedAssembly = PerformFileDeploy(mainPluginFile, applicationPartManager, config);
- //load all other referenced assemblies now
- foreach (var plugin in pluginFiles
- .Where(x => !x.Name.Equals(mainPluginFile.Name, StringComparison.InvariantCultureIgnoreCase))
- .Where(x => !IsAlreadyLoaded(x)))
- PerformFileDeploy(plugin, applicationPartManager, config);
- //init plugin type (only one plugin per assembly is allowed)
- foreach (var t in pluginDescriptor.ReferencedAssembly.GetTypes())
- if (typeof(IPlugin).IsAssignableFrom(t))
- if (!t.IsInterface)
- if (t.IsClass && !t.IsAbstract)
- {
- pluginDescriptor.PluginType = t;
- break;
- }
- referencedPlugins.Add(pluginDescriptor);
- }
- catch (ReflectionTypeLoadException ex)
- {
- //add a plugin name. this way we can easily identify a problematic plugin
- var msg = $"Plugin '{pluginDescriptor.FriendlyName}'. ";
- foreach (var e in ex.LoaderExceptions)
- msg += e.Message + Environment.NewLine;
- var fail = new Exception(msg, ex);
- throw fail;
- }
- catch (Exception ex)
- {
- //add a plugin name. this way we can easily identify a problematic plugin
- var msg = $"Plugin '{pluginDescriptor.FriendlyName}'. {ex.Message}";
- var fail = new Exception(msg, ex);
- throw fail;
- }
- }
- }
- catch (Exception ex)
- {
- var msg = string.Empty;
- for (var e = ex; e != null; e = e.InnerException)
- msg += e.Message + Environment.NewLine;
- var fail = new Exception(msg, ex);
- throw fail;
- }
- ReferencedPlugins = referencedPlugins;
- IncompatiblePlugins = incompatiblePlugins;
- }
- }
- /// <summary>
- /// Mark plugin as installed
- /// </summary>
- /// <param name="systemName">Plugin system name</param>
- public static void MarkPluginAsInstalled(string systemName)
- {
- if (string.IsNullOrEmpty(systemName))
- throw new ArgumentNullException(nameof(systemName));
- var filePath = CommonHelper.MapPath(InstalledPluginsFilePath);
- //create file if not exists
- if (!File.Exists(filePath))
- {
- //we use 'using' to close the file after it's created
- using (File.Create(filePath)) { }
- }
- //get installed plugin names
- var installedPluginSystemNames = GetInstalledPluginNames(filePath);
- //add plugin system name to the list if doesn't already exist
- var alreadyMarkedAsInstalled = installedPluginSystemNames.Any(pluginName => pluginName.Equals(systemName, StringComparison.InvariantCultureIgnoreCase));
- if (!alreadyMarkedAsInstalled)
- installedPluginSystemNames.Add(systemName);
- //save installed plugin names to the file
- SaveInstalledPluginNames(installedPluginSystemNames, filePath);
- }
- /// <summary>
- /// Mark plugin as uninstalled
- /// </summary>
- /// <param name="systemName">Plugin system name</param>
- public static void MarkPluginAsUninstalled(string systemName)
- {
- if (string.IsNullOrEmpty(systemName))
- throw new ArgumentNullException(nameof(systemName));
- var filePath = CommonHelper.MapPath(InstalledPluginsFilePath);
- //create file if not exists
- if (!File.Exists(filePath))
- {
- //we use 'using' to close the file after it's created
- using (File.Create(filePath)) { }
- }
- //get installed plugin names
- var installedPluginSystemNames = GetInstalledPluginNames(filePath);
- //remove plugin system name from the list if exists
- var alreadyMarkedAsInstalled = installedPluginSystemNames.Any(pluginName => pluginName.Equals(systemName, StringComparison.InvariantCultureIgnoreCase));
- if (alreadyMarkedAsInstalled)
- installedPluginSystemNames.Remove(systemName);
- //save installed plugin names to the file
- SaveInstalledPluginNames(installedPluginSystemNames, filePath);
- }
- /// <summary>
- /// Mark plugin as uninstalled
- /// </summary>
- public static void MarkAllPluginsAsUninstalled()
- {
- var filePath = CommonHelper.MapPath(InstalledPluginsFilePath);
- if (File.Exists(filePath))
- File.Delete(filePath);
- }
- /// <summary>
- /// Find a plugin descriptor by some type which is located into the same assembly as plugin
- /// </summary>
- /// <param name="typeInAssembly">Type</param>
- /// <returns>Plugin descriptor if exists; otherwise null</returns>
- public static PluginDescriptor FindPlugin(Type typeInAssembly)
- {
- if (typeInAssembly == null)
- throw new ArgumentNullException(nameof(typeInAssembly));
- if (ReferencedPlugins == null)
- return null;
- return ReferencedPlugins.FirstOrDefault(plugin => plugin.ReferencedAssembly != null
- && plugin.ReferencedAssembly.FullName.Equals(typeInAssembly.Assembly.FullName, StringComparison.InvariantCultureIgnoreCase));
- }
- /// <summary>
- /// Get plugin descriptor from the plugin description file
- /// </summary>
- /// <param name="filePath">Path to the description file</param>
- /// <returns>Plugin descriptor</returns>
- public static PluginDescriptor GetPluginDescriptorFromFile(string filePath)
- {
- var text = File.ReadAllText(filePath);
- return GetPluginDescriptorFromText(text);
- }
- /// <summary>
- /// Get plugin descriptor from the description text
- /// </summary>
- /// <param name="text">Description text</param>
- /// <returns>Plugin descriptor</returns>
- public static PluginDescriptor GetPluginDescriptorFromText(string text)
- {
- if (string.IsNullOrEmpty(text))
- return new PluginDescriptor();
- //get plugin descriptor from the JSON file
- var descriptor = JsonConvert.DeserializeObject<PluginDescriptor>(text);
- //nopCommerce 2.00 didn't have 'SupportedVersions' parameter, so let's set it to "2.00"
- if (!descriptor.SupportedVersions.Any())
- descriptor.SupportedVersions.Add("2.00");
- return descriptor;
- }
- /// <summary>
- /// Save plugin descriptor to the plugin description file
- /// </summary>
- /// <param name="pluginDescriptor">Plugin descriptor</param>
- public static void SavePluginDescriptor(PluginDescriptor pluginDescriptor)
- {
- if (pluginDescriptor == null)
- throw new ArgumentException(nameof(pluginDescriptor));
- //get the description file path
- if (pluginDescriptor.OriginalAssemblyFile == null)
- throw new Exception($"Cannot load original assembly path for {pluginDescriptor.SystemName} plugin.");
- var filePath = Path.Combine(pluginDescriptor.OriginalAssemblyFile.Directory.FullName, PluginDescriptionFileName);
- if (!File.Exists(filePath))
- throw new Exception($"Description file for {pluginDescriptor.SystemName} plugin does not exist. {filePath}");
- //save the file
- var text = JsonConvert.SerializeObject(pluginDescriptor, Formatting.Indented);
- File.WriteAllText(filePath, text);
- }
- /// <summary>
- /// Delete plugin directory from disk storage
- /// </summary>
- /// <param name="pluginDescriptor">Plugin descriptor</param>
- /// <returns>True if plugin directory is deleted, false if not</returns>
- public static bool DeletePlugin(PluginDescriptor pluginDescriptor)
- {
- //no plugin descriptor set
- if (pluginDescriptor == null)
- return false;
- //check whether plugin is installed
- if (pluginDescriptor.Installed)
- return false;
- if (pluginDescriptor.OriginalAssemblyFile.Directory.Exists)
- CommonHelper.DeleteDirectory(pluginDescriptor.OriginalAssemblyFile.DirectoryName);
- return true;
- }
- #endregion
- #region Properties
- /// <summary>
- /// Gets the path to file that contained (in previous versions) installed plugin system names
- /// </summary>
- public static string ObsoleteInstalledPluginsFilePath => "~/App_Data/InstalledPlugins.txt";
- /// <summary>
- /// Gets the path to file that contains installed plugin system names
- /// </summary>
- public static string InstalledPluginsFilePath => "~/App_Data/installedPlugins.json";
- /// <summary>
- /// Gets the path to plugins folder
- /// </summary>
- public static string PluginsPath => "~/Plugins";
- /// <summary>
- /// Gets the plugins folder name
- /// </summary>
- public static string PluginsPathName => "Plugins";
- /// <summary>
- /// Gets the path to plugins shadow copies folder
- /// </summary>
- public static string ShadowCopyPath => "~/Plugins/bin";
- /// <summary>
- /// Gets the path to plugins refs folder
- /// </summary>
- public static string RefsPathName => "refs";
- /// <summary>
- /// Gets the name of the plugin description file
- /// </summary>
- public static string PluginDescriptionFileName => "plugin.json";
- /// <summary>
- /// Returns a collection of all referenced plugin assemblies that have been shadow copied
- /// </summary>
- public static IEnumerable<PluginDescriptor> ReferencedPlugins { get; set; }
- /// <summary>
- /// Returns a collection of all plugin which are not compatible with the current version
- /// </summary>
- public static IEnumerable<string> IncompatiblePlugins { get; set; }
- #endregion
- }
- }
RAW Paste Data