using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Options; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Extensions; using File = System.IO.File; using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment; namespace Umbraco.Cms.Web.BackOffice.Services; public class IconService : IIconService { private readonly IAppPolicyCache _cache; private readonly IHostingEnvironment _hostingEnvironment; private readonly IWebHostEnvironment _webHostEnvironment; private GlobalSettings _globalSettings; [Obsolete("Use other ctor - Will be removed in Umbraco 12")] public IconService( IOptionsMonitor globalSettings, IHostingEnvironment hostingEnvironment, AppCaches appCaches) : this( globalSettings, hostingEnvironment, appCaches, StaticServiceProvider.Instance.GetRequiredService()) { } public IconService( IOptionsMonitor globalSettings, IHostingEnvironment hostingEnvironment, AppCaches appCaches, IWebHostEnvironment webHostEnvironment) { _globalSettings = globalSettings.CurrentValue; _hostingEnvironment = hostingEnvironment; _webHostEnvironment = webHostEnvironment; _cache = appCaches.RuntimeCache; globalSettings.OnChange(x => _globalSettings = x); } /// public IReadOnlyDictionary? GetIcons() => GetIconDictionary(); /// public IconModel? GetIcon(string iconName) { if (iconName.IsNullOrWhiteSpace()) { return null; } IReadOnlyDictionary? allIconModels = GetIconDictionary(); if (allIconModels?.ContainsKey(iconName) ?? false) { return new IconModel { Name = iconName, SvgString = allIconModels[iconName] }; } return null; } /// /// Gets an IconModel using values from a FileInfo model /// /// /// private IconModel? GetIcon(FileInfo fileInfo) => fileInfo == null || string.IsNullOrWhiteSpace(fileInfo.Name) ? null : CreateIconModel(fileInfo.Name.StripFileExtension(), fileInfo.FullName); /// /// Gets an IconModel containing the icon name and SvgString /// /// /// /// private IconModel? CreateIconModel(string iconName, string iconPath) { try { var svgContent = File.ReadAllText(iconPath); var svg = new IconModel { Name = iconName, SvgString = svgContent }; return svg; } catch { return null; } } // TODO: Refactor to return IEnumerable private IEnumerable GetAllIconsFiles() { var icons = new HashSet(new CaseInsensitiveFileInfoComparer()); // add icons from plugins var appPluginsDirectoryPath = _hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.AppPlugins); if (Directory.Exists(appPluginsDirectoryPath)) { var appPlugins = new DirectoryInfo(appPluginsDirectoryPath); // iterate sub directories of app plugins foreach (DirectoryInfo dir in appPlugins.EnumerateDirectories()) { // AppPluginIcons path was previoulsy the wrong case, so we first check for the prefered directory // and then check the legacy directory. var iconPath = _hostingEnvironment.MapPathContentRoot( $"{Constants.SystemDirectories.AppPlugins}/{dir.Name}{Constants.SystemDirectories.PluginIcons}"); var iconPathExists = Directory.Exists(iconPath); if (!iconPathExists) { iconPath = _hostingEnvironment.MapPathContentRoot( $"{Constants.SystemDirectories.AppPlugins}/{dir.Name}{Constants.SystemDirectories.AppPluginIcons}"); iconPathExists = Directory.Exists(iconPath); } if (iconPathExists) { IEnumerable dirIcons = new DirectoryInfo(iconPath).EnumerateFiles("*.svg", SearchOption.TopDirectoryOnly); icons.UnionWith(dirIcons); } } } // Get icons from the web root file provider (both physical and virtual) icons.UnionWith(GetIconsFiles(_webHostEnvironment.WebRootFileProvider, Constants.SystemDirectories.AppPlugins)); IDirectoryContents? iconFolder = _webHostEnvironment.WebRootFileProvider.GetDirectoryContents(_globalSettings.IconsPath); IEnumerable coreIcons = iconFolder .Where(x => !x.IsDirectory && x.Name.EndsWith(".svg")) .Select(x => new FileInfo(x.PhysicalPath!)); icons.UnionWith(coreIcons); return icons; } /// /// Finds all SVG icon files based on the specified and . /// The method will find both physical and virtual (eg. from a Razor Class Library) icons. /// /// The file provider to be used. /// The sub path to start from - should probably always be . /// A collection of representing the found SVG icon files. private static IEnumerable GetIconsFiles(IFileProvider fileProvider, string path) { // Iterate through all plugin folders and their subfolders, this is necessary because on Linux we'll get casing issues when // we directly try to access {path}/{pluginDirectory.Name}/{Constants.SystemDirectories.PluginIcons} foreach (IFileInfo pluginDirectory in fileProvider.GetDirectoryContents(path)) { if (!pluginDirectory.IsDirectory) { continue; } // Iterate through the sub directories of each plugin folder in order to support case insensitive paths (for Linux) foreach (IFileInfo subDir1 in fileProvider.GetDirectoryContents($"{path}/{pluginDirectory.Name}")) { // Hard-coding the "backoffice" directory name to gain a better performance when traversing the pluginDirectory directories if (subDir1.IsDirectory && subDir1.Name.InvariantEquals("backoffice")) { // Iterate through second level sub directories in order to support case insensitive paths (for Linux) foreach (IFileInfo subDir2 in fileProvider.GetDirectoryContents($"{path}/{pluginDirectory.Name}/{subDir1.Name}")) { if (!subDir2.IsDirectory) { continue; } // Does the directory match the plugin icons folder? (case insensitive for legacy support) if (!$"/{subDir1.Name}/{subDir2.Name}".InvariantEquals(Constants.SystemDirectories.PluginIcons)) { continue; } // Iterate though the files of the second level sub directory. This should be where the SVG files are located :D foreach (IFileInfo file in fileProvider.GetDirectoryContents($"{path}/{pluginDirectory.Name}/{subDir1.Name}/{subDir2.Name}")) { // TODO: Refactor to work with IFileInfo, then we can also remove the .PhysicalPath check // as this won't work for files that aren't located on a physical file system // (e.g. embedded resource, Azure Blob Storage, etc.) if (file.Name.InvariantEndsWith(".svg") && !string.IsNullOrEmpty(file.PhysicalPath)) { yield return new FileInfo(file.PhysicalPath); } } } } } } } private IReadOnlyDictionary? GetIconDictionary() => _cache.GetCacheItem( $"{typeof(IconService).FullName}.{nameof(GetIconDictionary)}", () => GetAllIconsFiles() .Select(GetIcon) .WhereNotNull() .GroupBy(i => i.Name, StringComparer.OrdinalIgnoreCase) .ToDictionary(g => g.Key, g => g.First().SvgString, StringComparer.OrdinalIgnoreCase)); private class CaseInsensitiveFileInfoComparer : IEqualityComparer { public bool Equals(FileInfo? one, FileInfo? two) => StringComparer.InvariantCultureIgnoreCase.Equals(one?.Name, two?.Name); public int GetHashCode(FileInfo item) => StringComparer.InvariantCultureIgnoreCase.GetHashCode(item.Name); } }