diff --git a/src/Umbraco.Web/Editors/DashboardController.cs b/src/Umbraco.Web/Editors/DashboardController.cs index 00635d852f..825ffb7314 100644 --- a/src/Umbraco.Web/Editors/DashboardController.cs +++ b/src/Umbraco.Web/Editors/DashboardController.cs @@ -1,144 +1,182 @@ -using System.Collections.Generic; -using Umbraco.Core; -using Umbraco.Core.Configuration; -using Umbraco.Web.Models.ContentEditing; -using Umbraco.Web.Mvc; -using Newtonsoft.Json.Linq; -using System.Threading.Tasks; -using System.Net.Http; -using System.Web.Http; -using System; -using System.Net; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; using System.Text; +using Newtonsoft.Json; using Umbraco.Core.Cache; -using Umbraco.Web.WebApi; -using Umbraco.Web.WebApi.Filters; +using Umbraco.Core.Exceptions; +using Umbraco.Core.IO; using Umbraco.Core.Logging; -using Umbraco.Core.Persistence; -using Umbraco.Core.Services; -using Umbraco.Core.Dashboards; -using Umbraco.Web.Services; +using Umbraco.Core.PropertyEditors; -namespace Umbraco.Web.Editors +namespace Umbraco.Core.Manifest { - //we need to fire up the controller like this to enable loading of remote css directly from this controller - [PluginController("UmbracoApi")] - [ValidationFilter] - [AngularJsonOnlyConfiguration] - [IsBackOffice] - [WebApi.UmbracoAuthorize] - [JsonCamelCaseFormatter] - - public class DashboardController : UmbracoApiController + /// + /// Parses the Main.js file and replaces all tokens accordingly. + /// + public class ManifestParser { - private readonly IDashboardService _dashboardService; + private static readonly string Utf8Preamble = Encoding.UTF8.GetString(Encoding.UTF8.GetPreamble()); + + private readonly IAppPolicyCache _cache; + private readonly ILogger _logger; + private readonly ManifestValueValidatorCollection _validators; + + private string _path; /// - /// Initializes a new instance of the with auto dependencies. + /// Initializes a new instance of the class. /// - public DashboardController() + public ManifestParser(AppCaches appCaches, ManifestValueValidatorCollection validators, ILogger logger) + : this(appCaches, validators, "~/App_Plugins", logger) { } /// - /// Initializes a new instance of the with all its dependencies. + /// Initializes a new instance of the class. /// - public DashboardController(IGlobalSettings globalSettings, UmbracoContext umbracoContext, ISqlContext sqlContext, ServiceContext services, AppCaches appCaches, IProfilingLogger logger, IRuntimeState runtimeState, IDashboardService dashboardService) - : base(globalSettings, umbracoContext, sqlContext, services, appCaches, logger, runtimeState) + private ManifestParser(AppCaches appCaches, ManifestValueValidatorCollection validators, string path, ILogger logger) { - _dashboardService = dashboardService; + if (appCaches == null) throw new ArgumentNullException(nameof(appCaches)); + _cache = appCaches.RuntimeCache; + _validators = validators ?? throw new ArgumentNullException(nameof(validators)); + if (string.IsNullOrWhiteSpace(path)) throw new ArgumentNullOrEmptyException(nameof(path)); + Path = path; + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } - //we have just one instance of HttpClient shared for the entire application - private static readonly HttpClient HttpClient = new HttpClient(); - - //we have baseurl as a param to make previewing easier, so we can test with a dev domain from client side - [ValidateAngularAntiForgeryToken] - public async Task GetRemoteDashboardContent(string section, string baseUrl = "https://dashboard.umbraco.org/") + public string Path { - var user = Security.CurrentUser; - var allowedSections = string.Join(",", user.AllowedSections); - var language = user.Language; - var version = UmbracoVersion.SemanticVersion.ToSemanticString(); - - var url = string.Format(baseUrl + "{0}?section={0}&allowed={1}&lang={2}&version={3}", section, allowedSections, language, version); - var key = "umbraco-dynamic-dashboard-" + language + allowedSections.Replace(",", "-") + section; - - var content = AppCaches.RuntimeCache.GetCacheItem(key); - var result = new JObject(); - if (content != null) - { - result = content; - } - else - { - //content is null, go get it - try - { - //fetch dashboard json and parse to JObject - var json = await HttpClient.GetStringAsync(url); - content = JObject.Parse(json); - result = content; - - AppCaches.RuntimeCache.InsertCacheItem(key, () => result, new TimeSpan(0, 30, 0)); - } - catch (HttpRequestException ex) - { - Logger.Error(ex.InnerException ?? ex, "Error getting dashboard content from {Url}", url); - - //it's still new JObject() - we return it like this to avoid error codes which triggers UI warnings - AppCaches.RuntimeCache.InsertCacheItem(key, () => result, new TimeSpan(0, 5, 0)); - } - } - - return result; + get => _path; + set => _path = value.StartsWith("~/") ? IOHelper.MapPath(value) : value; } - public async Task GetRemoteDashboardCss(string section, string baseUrl = "https://dashboard.umbraco.org/") + /// + /// Gets all manifests, merged into a single manifest object. + /// + /// + public PackageManifest Manifest + => _cache.GetCacheItem("Umbraco.Core.Manifest.ManifestParser::Manifests", () => + { + var manifests = GetManifests(); + return MergeManifests(manifests); + }, new TimeSpan(0, 4, 0)); + + /// + /// Gets all manifests. + /// + private IEnumerable GetManifests() { - var url = string.Format(baseUrl + "css/dashboard.css?section={0}", section); - var key = "umbraco-dynamic-dashboard-css-" + section; + var manifests = new List(); - var content = AppCaches.RuntimeCache.GetCacheItem(key); - var result = string.Empty; - - if (content != null) + foreach (var path in GetManifestFiles()) { - result = content; - } - else - { - //content is null, go get it try { - //fetch remote css - content = await HttpClient.GetStringAsync(url); - - //can't use content directly, modified closure problem - result = content; - - //save server content for 30 mins - AppCaches.RuntimeCache.InsertCacheItem(key, () => result, new TimeSpan(0, 30, 0)); + var text = File.ReadAllText(path); + text = TrimPreamble(text); + if (string.IsNullOrWhiteSpace(text)) + continue; + var manifest = ParseManifest(text); + manifests.Add(manifest); } - catch (HttpRequestException ex) + catch (Exception e) { - Logger.Error(ex.InnerException ?? ex, "Error getting dashboard CSS from {Url}", url); - - //it's still string.Empty - we return it like this to avoid error codes which triggers UI warnings - AppCaches.RuntimeCache.InsertCacheItem(key, () => result, new TimeSpan(0, 5, 0)); + _logger.Error(e, "Failed to parse manifest at '{Path}', ignoring.", path); } } - return new HttpResponseMessage(HttpStatusCode.OK) + return manifests; + } + + /// + /// Merges all manifests into one. + /// + private static PackageManifest MergeManifests(IEnumerable manifests) + { + var scripts = new HashSet(); + var stylesheets = new HashSet(); + var propertyEditors = new List(); + var parameterEditors = new List(); + var gridEditors = new List(); + var contentApps = new List(); + var dashboards = new List(); + var sections = new List(); + + foreach (var manifest in manifests) { - Content = new StringContent(result, Encoding.UTF8, "text/css") + if (manifest.Scripts != null) foreach (var script in manifest.Scripts) scripts.Add(script); + if (manifest.Stylesheets != null) foreach (var stylesheet in manifest.Stylesheets) stylesheets.Add(stylesheet); + if (manifest.PropertyEditors != null) propertyEditors.AddRange(manifest.PropertyEditors); + if (manifest.ParameterEditors != null) parameterEditors.AddRange(manifest.ParameterEditors); + if (manifest.GridEditors != null) gridEditors.AddRange(manifest.GridEditors); + if (manifest.ContentApps != null) contentApps.AddRange(manifest.ContentApps); + if (manifest.Dashboards != null) dashboards.AddRange(manifest.Dashboards); + if (manifest.Sections != null) sections.AddRange(manifest.Sections.DistinctBy(x => x.Alias.ToLowerInvariant())); + } + + return new PackageManifest + { + Scripts = scripts.ToArray(), + Stylesheets = stylesheets.ToArray(), + PropertyEditors = propertyEditors.ToArray(), + ParameterEditors = parameterEditors.ToArray(), + GridEditors = gridEditors.ToArray(), + ContentApps = contentApps.ToArray(), + Dashboards = dashboards.ToArray(), + Sections = sections.ToArray() }; } - [ValidateAngularAntiForgeryToken] - [OutgoingEditorModelEvent] - public IEnumerable> GetDashboard(string section) + // gets all manifest files (recursively) + private IEnumerable GetManifestFiles() { - return _dashboardService.GetDashboards(section, Security.CurrentUser); + if (Directory.Exists(_path) == false) + return new string[0]; + return Directory.GetFiles(_path, "package.manifest", SearchOption.AllDirectories); + } + + private static string TrimPreamble(string text) + { + // strangely StartsWith(preamble) would always return true + if (text.Substring(0, 1) == Utf8Preamble) + text = text.Remove(0, Utf8Preamble.Length); + + return text; + } + + /// + /// Parses a manifest. + /// + internal PackageManifest ParseManifest(string text) + { + if (string.IsNullOrWhiteSpace(text)) + throw new ArgumentNullOrEmptyException(nameof(text)); + + var manifest = JsonConvert.DeserializeObject(text, + new DataEditorConverter(_logger), + new ValueValidatorConverter(_validators), + new DashboardAccessRuleConverter()); + + // scripts and stylesheets are raw string, must process here + for (var i = 0; i < manifest.Scripts.Length; i++) + manifest.Scripts[i] = IOHelper.ResolveVirtualUrl(manifest.Scripts[i]); + for (var i = 0; i < manifest.Stylesheets.Length; i++) + manifest.Stylesheets[i] = IOHelper.ResolveVirtualUrl(manifest.Stylesheets[i]); + + // add property editors that are also parameter editors, to the parameter editors list + // (the manifest format is kinda legacy) + var ppEditors = manifest.PropertyEditors.Where(x => (x.Type & EditorType.MacroParameter) > 0).ToList(); + if (ppEditors.Count > 0) + manifest.ParameterEditors = manifest.ParameterEditors.Union(ppEditors).ToArray(); + + return manifest; + } + + // purely for tests + internal IEnumerable ParseGridEditors(string text) + { + return JsonConvert.DeserializeObject>(text); } } }