diff --git a/build/NuSpecs/tools/Dashboard.config.install.xdt b/build/NuSpecs/tools/Dashboard.config.install.xdt index 036beeba29..a81af8c365 100644 --- a/build/NuSpecs/tools/Dashboard.config.install.xdt +++ b/build/NuSpecs/tools/Dashboard.config.install.xdt @@ -3,7 +3,7 @@
- + views/dashboard/settings/settingsdashboardintro.html @@ -14,7 +14,7 @@ forms - + views/dashboard/forms/formsdashboardintro.html @@ -28,7 +28,7 @@
- + views/dashboard/developer/developerdashboardvideos.html @@ -47,7 +47,7 @@ - + views/dashboard/media/mediafolderbrowser.html @@ -56,7 +56,7 @@
- + views/dashboard/members/membersdashboardvideos.html @@ -92,4 +92,4 @@
- \ No newline at end of file + diff --git a/src/Umbraco.Core/Composing/Composers/ConfigurationComposer.cs b/src/Umbraco.Core/Composing/Composers/ConfigurationComposer.cs index 8e81256e3b..7032bbe88e 100644 --- a/src/Umbraco.Core/Composing/Composers/ConfigurationComposer.cs +++ b/src/Umbraco.Core/Composing/Composers/ConfigurationComposer.cs @@ -16,6 +16,7 @@ namespace Umbraco.Core.Composing.Composers composition.Register(factory => factory.GetInstance().Templates); composition.Register(factory => factory.GetInstance().RequestHandler); composition.Register(factory => UmbracoConfig.For.GlobalSettings()); + composition.Register(factory => UmbracoConfig.For.DashboardSettings()); // fixme - other sections we need to add? diff --git a/src/Umbraco.Core/Configuration/Dashboard/AccessElement.cs b/src/Umbraco.Core/Configuration/Dashboard/AccessElement.cs index 1642f23fc5..01538c8e0b 100644 --- a/src/Umbraco.Core/Configuration/Dashboard/AccessElement.cs +++ b/src/Umbraco.Core/Configuration/Dashboard/AccessElement.cs @@ -7,26 +7,22 @@ namespace Umbraco.Core.Configuration.Dashboard internal class AccessElement : RawXmlConfigurationElement, IAccess { public AccessElement() - { - - } + { } public AccessElement(XElement rawXml) - :base(rawXml) - { - } + : base(rawXml) + { } - public IEnumerable Rules + public IEnumerable Rules { get { - var result = new List(); - if (RawXml != null) - { - result.AddRange(RawXml.Elements("deny").Select(x => new AccessItem {Action = AccessType.Deny, Value = x.Value })); - result.AddRange(RawXml.Elements("grant").Select(x => new AccessItem { Action = AccessType.Grant, Value = x.Value })); - result.AddRange(RawXml.Elements("grantBySection").Select(x => new AccessItem { Action = AccessType.GrantBySection, Value = x.Value })); - } + var result = new List(); + if (RawXml == null) return result; + + result.AddRange(RawXml.Elements("deny").Select(x => new AccessRule {Type = AccessRuleType.Deny, Value = x.Value })); + result.AddRange(RawXml.Elements("grant").Select(x => new AccessRule { Type = AccessRuleType.Grant, Value = x.Value })); + result.AddRange(RawXml.Elements("grantBySection").Select(x => new AccessRule { Type = AccessRuleType.GrantBySection, Value = x.Value })); return result; } } diff --git a/src/Umbraco.Core/Configuration/Dashboard/AccessItem.cs b/src/Umbraco.Core/Configuration/Dashboard/AccessItem.cs deleted file mode 100644 index 37cf491536..0000000000 --- a/src/Umbraco.Core/Configuration/Dashboard/AccessItem.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Umbraco.Core.Configuration.Dashboard -{ - internal class AccessItem : IAccessItem - { - /// - /// This can be grant, deny or grantBySection - /// - public AccessType Action { get; set; } - - /// - /// The value of the action - /// - public string Value { get; set; } - } -} diff --git a/src/Umbraco.Core/Configuration/Dashboard/AccessRule.cs b/src/Umbraco.Core/Configuration/Dashboard/AccessRule.cs new file mode 100644 index 0000000000..fe6840ff64 --- /dev/null +++ b/src/Umbraco.Core/Configuration/Dashboard/AccessRule.cs @@ -0,0 +1,14 @@ +namespace Umbraco.Core.Configuration.Dashboard +{ + /// + /// Implements . + /// + internal class AccessRule : IAccessRule + { + /// + public AccessRuleType Type { get; set; } + + /// + public string Value { get; set; } + } +} diff --git a/src/Umbraco.Core/Configuration/Dashboard/AccessRuleType.cs b/src/Umbraco.Core/Configuration/Dashboard/AccessRuleType.cs new file mode 100644 index 0000000000..cb9ce983fe --- /dev/null +++ b/src/Umbraco.Core/Configuration/Dashboard/AccessRuleType.cs @@ -0,0 +1,28 @@ +namespace Umbraco.Core.Configuration.Dashboard +{ + /// + /// Defines dashboard access rules type. + /// + public enum AccessRuleType + { + /// + /// Unknown (default value). + /// + Unknown = 0, + + /// + /// Grant access to the dashboard if user belongs to the specified user group. + /// + Grant, + + /// + /// Deny access to the dashboard if user belongs to the specified user group. + /// + Deny, + + /// + /// Grant access to the dashboard if user has access to the specified section. + /// + GrantBySection + } +} diff --git a/src/Umbraco.Core/Configuration/Dashboard/AccessType.cs b/src/Umbraco.Core/Configuration/Dashboard/AccessType.cs deleted file mode 100644 index d72cac15d0..0000000000 --- a/src/Umbraco.Core/Configuration/Dashboard/AccessType.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Umbraco.Core.Configuration.Dashboard -{ - public enum AccessType - { - Grant, - Deny, - GrantBySection - } -} diff --git a/src/Umbraco.Core/Configuration/Dashboard/ControlElement.cs b/src/Umbraco.Core/Configuration/Dashboard/ControlElement.cs index 0434eea47e..20dac7460e 100644 --- a/src/Umbraco.Core/Configuration/Dashboard/ControlElement.cs +++ b/src/Umbraco.Core/Configuration/Dashboard/ControlElement.cs @@ -1,5 +1,4 @@ -using System; -using System.Configuration; +using System.Configuration; using System.Linq; using System.Xml.Linq; @@ -8,33 +7,12 @@ namespace Umbraco.Core.Configuration.Dashboard internal class ControlElement : RawXmlConfigurationElement, IDashboardControl { - public bool ShowOnce - { - get - { - return RawXml.Attribute("showOnce") == null - ? false - : bool.Parse(RawXml.Attribute("showOnce").Value); - } - } - - public bool AddPanel - { - get - { - return RawXml.Attribute("addPanel") == null - ? true - : bool.Parse(RawXml.Attribute("addPanel").Value); - } - } - public string PanelCaption { get { - return RawXml.Attribute("panelCaption") == null - ? "" - : RawXml.Attribute("panelCaption").Value; + var panelCaption = RawXml.Attribute("panelCaption"); + return panelCaption == null ? "" : panelCaption.Value; } } @@ -43,11 +21,7 @@ namespace Umbraco.Core.Configuration.Dashboard get { var access = RawXml.Element("access"); - if (access == null) - { - return new AccessElement(); - } - return new AccessElement(access); + return access == null ? new AccessElement() : new AccessElement(access); } } @@ -65,10 +39,6 @@ namespace Umbraco.Core.Configuration.Dashboard } } - - IAccess IDashboardControl.AccessRights - { - get { return Access; } - } + IAccess IDashboardControl.AccessRights => Access; } } diff --git a/src/Umbraco.Core/Configuration/Dashboard/IAccess.cs b/src/Umbraco.Core/Configuration/Dashboard/IAccess.cs index b7d8540a79..8ac1b18cca 100644 --- a/src/Umbraco.Core/Configuration/Dashboard/IAccess.cs +++ b/src/Umbraco.Core/Configuration/Dashboard/IAccess.cs @@ -4,6 +4,6 @@ namespace Umbraco.Core.Configuration.Dashboard { public interface IAccess { - IEnumerable Rules { get; } + IEnumerable Rules { get; } } } diff --git a/src/Umbraco.Core/Configuration/Dashboard/IAccessItem.cs b/src/Umbraco.Core/Configuration/Dashboard/IAccessItem.cs deleted file mode 100644 index 8b18d50bb3..0000000000 --- a/src/Umbraco.Core/Configuration/Dashboard/IAccessItem.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Umbraco.Core.Configuration.Dashboard -{ - public interface IAccessItem - { - /// - /// This can be grant, deny or grantBySection - /// - AccessType Action { get; set; } - - /// - /// The value of the action - /// - string Value { get; set; } - } -} diff --git a/src/Umbraco.Core/Configuration/Dashboard/IAccessRule.cs b/src/Umbraco.Core/Configuration/Dashboard/IAccessRule.cs new file mode 100644 index 0000000000..8b51b1b73a --- /dev/null +++ b/src/Umbraco.Core/Configuration/Dashboard/IAccessRule.cs @@ -0,0 +1,18 @@ +namespace Umbraco.Core.Configuration.Dashboard +{ + /// + /// Represents an access rule. + /// + public interface IAccessRule + { + /// + /// Gets or sets the rule type. + /// + AccessRuleType Type { get; set; } + + /// + /// Gets or sets the value for the rule. + /// + string Value { get; set; } + } +} diff --git a/src/Umbraco.Core/Configuration/Dashboard/IDashboardControl.cs b/src/Umbraco.Core/Configuration/Dashboard/IDashboardControl.cs index 7dab542258..cdf05af1ec 100644 --- a/src/Umbraco.Core/Configuration/Dashboard/IDashboardControl.cs +++ b/src/Umbraco.Core/Configuration/Dashboard/IDashboardControl.cs @@ -2,10 +2,6 @@ { public interface IDashboardControl { - bool ShowOnce { get; } - - bool AddPanel { get; } - string PanelCaption { get; } string ControlPath { get; } diff --git a/src/Umbraco.Core/Manifest/DashboardAccessRuleConverter.cs b/src/Umbraco.Core/Manifest/DashboardAccessRuleConverter.cs new file mode 100644 index 0000000000..c627728a32 --- /dev/null +++ b/src/Umbraco.Core/Manifest/DashboardAccessRuleConverter.cs @@ -0,0 +1,45 @@ +using System; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Umbraco.Core.Configuration.Dashboard; +using Umbraco.Core.Serialization; + +namespace Umbraco.Core.Manifest +{ + /// + /// Implements a json read converter for . + /// + internal class DashboardAccessRuleConverter : JsonReadConverter + { + /// + protected override IAccessRule Create(Type objectType, string path, JObject jObject) + { + return new AccessRule(); + } + + /// + protected override void Deserialize(JObject jobject, IAccessRule target, JsonSerializer serializer) + { + // see Create above, target is either DataEditor (parameter) or ConfiguredDataEditor (property) + + if (!(target is AccessRule accessRule)) + throw new Exception("panic."); + + GetRule(accessRule, jobject, "grant", AccessRuleType.Grant); + GetRule(accessRule, jobject, "deny", AccessRuleType.Deny); + GetRule(accessRule, jobject, "grantBySection", AccessRuleType.GrantBySection); + + if (accessRule.Type == AccessRuleType.Unknown) throw new InvalidOperationException("Rule is not defined."); + } + + private void GetRule(AccessRule rule, JObject jobject, string name, AccessRuleType type) + { + var token = jobject[name]; + if (token == null) return; + if (rule.Type != AccessRuleType.Unknown) throw new InvalidOperationException("Multiple definition of a rule."); + if (token.Type != JTokenType.String) throw new InvalidOperationException("Rule value is not a string."); + rule.Type = type; + rule.Value = token.Value(); + } + } +} diff --git a/src/Umbraco.Core/Manifest/ManifestDashboardDefinition.cs b/src/Umbraco.Core/Manifest/ManifestDashboardDefinition.cs new file mode 100644 index 0000000000..83f047b264 --- /dev/null +++ b/src/Umbraco.Core/Manifest/ManifestDashboardDefinition.cs @@ -0,0 +1,36 @@ +using System; +using System.ComponentModel; +using Newtonsoft.Json; +using Umbraco.Core.Configuration.Dashboard; +using Umbraco.Core.IO; + +namespace Umbraco.Core.Manifest +{ + public class ManifestDashboardDefinition + { + private string _view; + + [JsonProperty("name", Required = Required.Always)] + public string Name { get; set; } + + [JsonProperty("alias", Required = Required.Always)] + public string Alias { get; set; } + + [JsonProperty("weight", DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] + [DefaultValue(100)] + public int Weight { get; set; } + + [JsonProperty("view", Required = Required.Always)] + public string View + { + get => _view; + set => _view = IOHelper.ResolveVirtualUrl(value); + } + + [JsonProperty("sections")] + public string[] Sections { get; set; } = Array.Empty(); + + [JsonProperty("access")] + public IAccessRule[] AccessRules { get; set; } = Array.Empty(); + } +} diff --git a/src/Umbraco.Core/Manifest/ManifestParser.cs b/src/Umbraco.Core/Manifest/ManifestParser.cs index 125dee5c05..fe021fae5b 100644 --- a/src/Umbraco.Core/Manifest/ManifestParser.cs +++ b/src/Umbraco.Core/Manifest/ManifestParser.cs @@ -1,177 +1,180 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using Newtonsoft.Json; -using Umbraco.Core.Cache; -using Umbraco.Core.Exceptions; -using Umbraco.Core.IO; -using Umbraco.Core.Logging; -using Umbraco.Core.Models.ContentEditing; -using Umbraco.Core.PropertyEditors; - -namespace Umbraco.Core.Manifest -{ - /// - /// Parses the Main.js file and replaces all tokens accordingly. - /// - public class ManifestParser - { - private static readonly string Utf8Preamble = Encoding.UTF8.GetString(Encoding.UTF8.GetPreamble()); - - private readonly IRuntimeCacheProvider _cache; - private readonly ILogger _logger; - private readonly ManifestValueValidatorCollection _validators; - - private string _path; - - /// - /// Initializes a new instance of the class. - /// - public ManifestParser(IRuntimeCacheProvider cache, ManifestValueValidatorCollection validators, ILogger logger) - : this(cache, validators, "~/App_Plugins", logger) - { } - - /// - /// Initializes a new instance of the class. - /// - private ManifestParser(IRuntimeCacheProvider cache, ManifestValueValidatorCollection validators, string path, ILogger logger) - { - _cache = cache ?? throw new ArgumentNullException(nameof(cache)); - _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)); - } - - public string Path - { - get => _path; - set => _path = value.StartsWith("~/") ? IOHelper.MapPath(value) : value; - } - - /// - /// 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 manifests = new List(); - - foreach (var path in GetManifestFiles()) - { - try - { - var text = File.ReadAllText(path); - text = TrimPreamble(text); - if (string.IsNullOrWhiteSpace(text)) - continue; - var manifest = ParseManifest(text); - manifests.Add(manifest); - } - catch (Exception e) - { - _logger.Error(e, "Failed to parse manifest at '{Path}', ignoring.", path); - } - } - - 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(); - - foreach (var manifest in manifests) - { - 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); - } - - return new PackageManifest - { - Scripts = scripts.ToArray(), - Stylesheets = stylesheets.ToArray(), - PropertyEditors = propertyEditors.ToArray(), - ParameterEditors = parameterEditors.ToArray(), - GridEditors = gridEditors.ToArray(), - ContentApps = contentApps.ToArray() - }; - } - - // gets all manifest files (recursively) - private IEnumerable GetManifestFiles() - { - 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 ContentAppDefinitionConverter()); - - // 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); - } - } -} +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using Newtonsoft.Json; +using Umbraco.Core.Cache; +using Umbraco.Core.Exceptions; +using Umbraco.Core.IO; +using Umbraco.Core.Logging; +using Umbraco.Core.Models.ContentEditing; +using Umbraco.Core.PropertyEditors; + +namespace Umbraco.Core.Manifest +{ + /// + /// Parses the Main.js file and replaces all tokens accordingly. + /// + public class ManifestParser + { + private static readonly string Utf8Preamble = Encoding.UTF8.GetString(Encoding.UTF8.GetPreamble()); + + private readonly IRuntimeCacheProvider _cache; + private readonly ILogger _logger; + private readonly ManifestValueValidatorCollection _validators; + + private string _path; + + /// + /// Initializes a new instance of the class. + /// + public ManifestParser(IRuntimeCacheProvider cache, ManifestValueValidatorCollection validators, ILogger logger) + : this(cache, validators, "~/App_Plugins", logger) + { } + + /// + /// Initializes a new instance of the class. + /// + private ManifestParser(IRuntimeCacheProvider cache, ManifestValueValidatorCollection validators, string path, ILogger logger) + { + _cache = cache ?? throw new ArgumentNullException(nameof(cache)); + _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)); + } + + public string Path + { + get => _path; + set => _path = value.StartsWith("~/") ? IOHelper.MapPath(value) : value; + } + + /// + /// 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 manifests = new List(); + + foreach (var path in GetManifestFiles()) + { + try + { + var text = File.ReadAllText(path); + text = TrimPreamble(text); + if (string.IsNullOrWhiteSpace(text)) + continue; + var manifest = ParseManifest(text); + manifests.Add(manifest); + } + catch (Exception e) + { + _logger.Error(e, "Failed to parse manifest at '{Path}', ignoring.", path); + } + } + + 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(); + + foreach (var manifest in manifests) + { + 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); + } + + return new PackageManifest + { + Scripts = scripts.ToArray(), + Stylesheets = stylesheets.ToArray(), + PropertyEditors = propertyEditors.ToArray(), + ParameterEditors = parameterEditors.ToArray(), + GridEditors = gridEditors.ToArray(), + ContentApps = contentApps.ToArray(), + Dashboards = dashboards.ToArray() + }; + } + + // gets all manifest files (recursively) + private IEnumerable GetManifestFiles() + { + 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 ContentAppDefinitionConverter(), + 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); + } + } +} diff --git a/src/Umbraco.Core/Manifest/PackageManifest.cs b/src/Umbraco.Core/Manifest/PackageManifest.cs index 32dae46a9a..95a5c01b6a 100644 --- a/src/Umbraco.Core/Manifest/PackageManifest.cs +++ b/src/Umbraco.Core/Manifest/PackageManifest.cs @@ -1,31 +1,34 @@ -using System; -using Newtonsoft.Json; -using Umbraco.Core.Models.ContentEditing; -using Umbraco.Core.PropertyEditors; - -namespace Umbraco.Core.Manifest -{ - /// - /// Represents the content of a package manifest. - /// - public class PackageManifest - { - [JsonProperty("javascript")] - public string[] Scripts { get; set; } = Array.Empty(); - - [JsonProperty("css")] - public string[] Stylesheets { get; set; }= Array.Empty(); - - [JsonProperty("propertyEditors")] - public IDataEditor[] PropertyEditors { get; set; } = Array.Empty(); - - [JsonProperty("parameterEditors")] - public IDataEditor[] ParameterEditors { get; set; } = Array.Empty(); - - [JsonProperty("gridEditors")] - public GridEditor[] GridEditors { get; set; } = Array.Empty(); - - [JsonProperty("contentApps")] - public IContentAppDefinition[] ContentApps { get; set; } = Array.Empty(); - } -} +using System; +using Newtonsoft.Json; +using Umbraco.Core.Models.ContentEditing; +using Umbraco.Core.PropertyEditors; + +namespace Umbraco.Core.Manifest +{ + /// + /// Represents the content of a package manifest. + /// + public class PackageManifest + { + [JsonProperty("javascript")] + public string[] Scripts { get; set; } = Array.Empty(); + + [JsonProperty("css")] + public string[] Stylesheets { get; set; }= Array.Empty(); + + [JsonProperty("propertyEditors")] + public IDataEditor[] PropertyEditors { get; set; } = Array.Empty(); + + [JsonProperty("parameterEditors")] + public IDataEditor[] ParameterEditors { get; set; } = Array.Empty(); + + [JsonProperty("gridEditors")] + public GridEditor[] GridEditors { get; set; } = Array.Empty(); + + [JsonProperty("contentApps")] + public IContentAppDefinition[] ContentApps { get; set; } = Array.Empty(); + + [JsonProperty("dashboards")] + public ManifestDashboardDefinition[] Dashboards { get; set; } = Array.Empty(); + } +} diff --git a/src/Umbraco.Core/Models/ContentEditing/IContentAppDefinition.cs b/src/Umbraco.Core/Models/ContentEditing/IContentAppDefinition.cs index 2d30fc6ba9..af83c5a2f5 100644 --- a/src/Umbraco.Core/Models/ContentEditing/IContentAppDefinition.cs +++ b/src/Umbraco.Core/Models/ContentEditing/IContentAppDefinition.cs @@ -3,6 +3,7 @@ using Umbraco.Core.Models.Membership; namespace Umbraco.Core.Models.ContentEditing { + /// /// Represents a content app definition. /// diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index ecc8e719b4..06d410e39f 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -202,8 +202,8 @@ - - + + @@ -211,7 +211,7 @@ - + @@ -346,7 +346,9 @@ + + diff --git a/src/Umbraco.Tests/Configurations/DashboardSettings/Dashboard.config b/src/Umbraco.Tests/Configurations/DashboardSettings/Dashboard.config index 4040412603..4c86355a1b 100644 --- a/src/Umbraco.Tests/Configurations/DashboardSettings/Dashboard.config +++ b/src/Umbraco.Tests/Configurations/DashboardSettings/Dashboard.config @@ -6,10 +6,10 @@ settings - + views/dashboard/settings/settingsdashboardintro.html - + views/dashboard/settings/settingsdashboardvideos.html @@ -23,10 +23,10 @@ developer - + views/dashboard/developer/developerdashboardintro.html - + views/dashboard/developer/developerdashboardvideos.html @@ -37,7 +37,7 @@ media - + views/dashboard/media/mediafolderbrowser.html @@ -45,13 +45,13 @@ admin - + views/dashboard/media/mediadashboardintro.html - + views/dashboard/media/desktopmediauploader.html - + views/dashboard/media/mediadashboardvideos.html
@@ -70,25 +70,25 @@ admin - + views/dashboard/default/startupdashboardintro.html - + views/dashboard/default/startupdashboardkits.html editor writer - + views/dashboard/default/startupdashboardvideos.html
- dashboard/latestEdits.ascx + dashboard/latestEdits.ascx - + views/dashboard/changepassword.html @@ -100,13 +100,13 @@ member - + views/dashboard/members/membersdashboardintro.html - + members/membersearch.ascx - + views/dashboard/members/membersdashboardvideos.html diff --git a/src/Umbraco.Tests/Configurations/DashboardSettings/DashboardSettingsTests.cs b/src/Umbraco.Tests/Configurations/DashboardSettings/DashboardSettingsTests.cs index 862dfb3dc2..920de683b4 100644 --- a/src/Umbraco.Tests/Configurations/DashboardSettings/DashboardSettingsTests.cs +++ b/src/Umbraco.Tests/Configurations/DashboardSettings/DashboardSettingsTests.cs @@ -56,11 +56,11 @@ namespace Umbraco.Tests.Configurations.DashboardSettings Assert.AreEqual(3, SettingsSection.Sections.ElementAt(3).AccessRights.Rules.Count()); Assert.AreEqual("translator", SettingsSection.Sections.ElementAt(3).AccessRights.Rules.ElementAt(0).Value); - Assert.AreEqual(AccessType.Deny, SettingsSection.Sections.ElementAt(3).AccessRights.Rules.ElementAt(0).Action); + Assert.AreEqual(AccessRuleType.Deny, SettingsSection.Sections.ElementAt(3).AccessRights.Rules.ElementAt(0).Type); Assert.AreEqual("hello", SettingsSection.Sections.ElementAt(3).AccessRights.Rules.ElementAt(1).Value); - Assert.AreEqual(AccessType.Grant, SettingsSection.Sections.ElementAt(3).AccessRights.Rules.ElementAt(1).Action); + Assert.AreEqual(AccessRuleType.Grant, SettingsSection.Sections.ElementAt(3).AccessRights.Rules.ElementAt(1).Type); Assert.AreEqual("world", SettingsSection.Sections.ElementAt(3).AccessRights.Rules.ElementAt(2).Value); - Assert.AreEqual(AccessType.GrantBySection, SettingsSection.Sections.ElementAt(3).AccessRights.Rules.ElementAt(2).Action); + Assert.AreEqual(AccessRuleType.GrantBySection, SettingsSection.Sections.ElementAt(3).AccessRights.Rules.ElementAt(2).Type); } [Test] @@ -94,21 +94,17 @@ namespace Umbraco.Tests.Configurations.DashboardSettings public void Test_Tab_Access() { Assert.AreEqual(1, SettingsSection.Sections.ElementAt(2).Tabs.ElementAt(1).AccessRights.Rules.Count()); - Assert.AreEqual(AccessType.Grant, SettingsSection.Sections.ElementAt(2).Tabs.ElementAt(1).AccessRights.Rules.ElementAt(0).Action); + Assert.AreEqual(AccessRuleType.Grant, SettingsSection.Sections.ElementAt(2).Tabs.ElementAt(1).AccessRights.Rules.ElementAt(0).Type); Assert.AreEqual("admin", SettingsSection.Sections.ElementAt(2).Tabs.ElementAt(1).AccessRights.Rules.ElementAt(0).Value); } [Test] public void Test_Control() { - Assert.AreEqual(true, SettingsSection.Sections.ElementAt(0).Tabs.ElementAt(0).Controls.ElementAt(0).ShowOnce); - Assert.AreEqual(true, SettingsSection.Sections.ElementAt(0).Tabs.ElementAt(0).Controls.ElementAt(0).AddPanel); Assert.AreEqual("hello", SettingsSection.Sections.ElementAt(0).Tabs.ElementAt(0).Controls.ElementAt(0).PanelCaption); Assert.AreEqual("views/dashboard/settings/settingsdashboardintro.html", SettingsSection.Sections.ElementAt(0).Tabs.ElementAt(0).Controls.ElementAt(0).ControlPath); - Assert.AreEqual(false, SettingsSection.Sections.ElementAt(0).Tabs.ElementAt(0).Controls.ElementAt(1).ShowOnce); - Assert.AreEqual(false, SettingsSection.Sections.ElementAt(0).Tabs.ElementAt(0).Controls.ElementAt(1).AddPanel); Assert.AreEqual("", SettingsSection.Sections.ElementAt(0).Tabs.ElementAt(0).Controls.ElementAt(1).PanelCaption); Assert.AreEqual("views/dashboard/settings/settingsdashboardvideos.html", SettingsSection.Sections.ElementAt(0).Tabs.ElementAt(0).Controls.ElementAt(1).ControlPath); @@ -118,9 +114,9 @@ namespace Umbraco.Tests.Configurations.DashboardSettings public void Test_Control_Access() { Assert.AreEqual(2, SettingsSection.Sections.ElementAt(3).Tabs.ElementAt(0).Controls.ElementAt(1).AccessRights.Rules.Count()); - Assert.AreEqual(AccessType.Deny, SettingsSection.Sections.ElementAt(3).Tabs.ElementAt(0).Controls.ElementAt(1).AccessRights.Rules.ElementAt(0).Action); + Assert.AreEqual(AccessRuleType.Deny, SettingsSection.Sections.ElementAt(3).Tabs.ElementAt(0).Controls.ElementAt(1).AccessRights.Rules.ElementAt(0).Type); Assert.AreEqual("editor", SettingsSection.Sections.ElementAt(3).Tabs.ElementAt(0).Controls.ElementAt(1).AccessRights.Rules.ElementAt(0).Value); - Assert.AreEqual(AccessType.Deny, SettingsSection.Sections.ElementAt(3).Tabs.ElementAt(0).Controls.ElementAt(1).AccessRights.Rules.ElementAt(1).Action); + Assert.AreEqual(AccessRuleType.Deny, SettingsSection.Sections.ElementAt(3).Tabs.ElementAt(0).Controls.ElementAt(1).AccessRights.Rules.ElementAt(1).Type); Assert.AreEqual("writer", SettingsSection.Sections.ElementAt(3).Tabs.ElementAt(0).Controls.ElementAt(1).AccessRights.Rules.ElementAt(1).Value); } } diff --git a/src/Umbraco.Tests/Manifest/ManifestParserTests.cs b/src/Umbraco.Tests/Manifest/ManifestParserTests.cs index a0cdb2b1bf..7acc9298a5 100644 --- a/src/Umbraco.Tests/Manifest/ManifestParserTests.cs +++ b/src/Umbraco.Tests/Manifest/ManifestParserTests.cs @@ -7,6 +7,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Umbraco.Core.Cache; using Umbraco.Core.Composing; +using Umbraco.Core.Configuration.Dashboard; using Umbraco.Core.Logging; using Umbraco.Core.Manifest; using Umbraco.Core.PropertyEditors; @@ -380,5 +381,53 @@ javascript: ['~/test.js',/*** some note about stuff asd09823-4**09234*/ '~/test2 Assert.AreEqual("icon-bar", app1.Icon); Assert.AreEqual("/App_Plugins/MyPackage/ContentApps/MyApp2.html", app1.View); } + + [Test] + public void CanParseManifest_Dashboards() + { + const string json = @"{'dashboards': [ + { + 'name': 'First One', + 'alias': 'something', + 'view': '~/App_Plugins/MyPackage/Dashboards/one.html', + 'sections': [ 'content' ], + 'access': [ {'grant':'user'}, {'deny':'foo'} ] + + }, + { + 'name': 'Second-One', + 'alias': 'something.else', + 'weight': -1, + 'view': '~/App_Plugins/MyPackage/Dashboards/two.html', + 'sections': [ 'forms' ], + } +]}"; + + var manifest = _parser.ParseManifest(json); + Assert.AreEqual(2, manifest.Dashboards.Length); + + Assert.IsInstanceOf(manifest.Dashboards[0]); + var db0 = manifest.Dashboards[0]; + Assert.AreEqual("something", db0.Alias); + Assert.AreEqual("First One", db0.Name); + Assert.AreEqual(100, db0.Weight); + Assert.AreEqual("/App_Plugins/MyPackage/Dashboards/one.html", db0.View); + Assert.AreEqual(1, db0.Sections.Length); + Assert.AreEqual("content", db0.Sections[0]); + Assert.AreEqual(2, db0.AccessRules.Length); + Assert.AreEqual(AccessRuleType.Grant, db0.AccessRules[0].Type); + Assert.AreEqual("user", db0.AccessRules[0].Value); + Assert.AreEqual(AccessRuleType.Deny, db0.AccessRules[1].Type); + Assert.AreEqual("foo", db0.AccessRules[1].Value); + + Assert.IsInstanceOf(manifest.Dashboards[1]); + var db1 = manifest.Dashboards[1]; + Assert.AreEqual("something.else", db1.Alias); + Assert.AreEqual("Second-One", db1.Name); + Assert.AreEqual(-1, db1.Weight); + Assert.AreEqual("/App_Plugins/MyPackage/Dashboards/two.html", db1.View); + Assert.AreEqual(1, db1.Sections.Length); + Assert.AreEqual("forms", db1.Sections[0]); + } } } diff --git a/src/Umbraco.Web.UI.Client/gulpfile.js b/src/Umbraco.Web.UI.Client/gulpfile.js index 580acae6ca..0af327d148 100644 --- a/src/Umbraco.Web.UI.Client/gulpfile.js +++ b/src/Umbraco.Web.UI.Client/gulpfile.js @@ -282,48 +282,29 @@ gulp.task('dependencies', function () { ], "base": "./node_modules/jquery/dist" }, - { - "name": "jquery-migrate", - "src": ["./node_modules/jquery-migrate/dist/jquery-migrate.min.js"], - "base": "./node_modules/jquery-migrate/dist" - }, { "name": "jquery-ui", "src": ["./node_modules/jquery-ui-dist/jquery-ui.min.js"], "base": "./node_modules/jquery-ui-dist" }, { - "name": "jquery-validate", - "src": ["./node_modules/jquery-validation/dist/jquery.validate.min.js"], - "base": "./node_modules/jquery-validation/dist" - }, - { - "name": "jquery-validation-unobtrusive", - "src": ["./node_modules/jquery-validation-unobtrusive/dist/jquery.validate.unobtrusive.min.js"], - "base": "./node_modules/jquery-validation-unobtrusive/dist" + "name": "jquery-ui-touch-punch", + "src": ["./node_modules/jquery-ui-touch-punch/jquery.ui.touch-punch.min.js"], + "base": "./node_modules/jquery-ui-touch-punch" }, { "name": "lazyload-js", "src": ["./node_modules/lazyload-js/lazyload.min.js"], "base": "./node_modules/lazyload-js" }, - // TODO: We can optimize here: - // we don't have to ship with the moment-with-locales libraries - // we lazyload the user locale { "name": "moment", - "src": [ - "./node_modules/moment/min/moment.min.js", - "./node_modules/moment/min/moment-with-locales.js", - "./node_modules/moment/min/moment-with-locales.min.js" - ], + "src": ["./node_modules/moment/min/moment.min.js"], "base": "./node_modules/moment/min" }, { "name": "moment", - "src": [ - "./node_modules/moment/locale/*.js" - ], + "src": ["./node_modules/moment/locale/*.js"], "base": "./node_modules/moment/locale" }, { @@ -344,6 +325,14 @@ gulp.task('dependencies', function () { "src": ["./node_modules/signalr/jquery.signalR.js"], "base": "./node_modules/signalr" }, + { + "name": "spectrum", + "src": [ + "./node_modules/spectrum-colorpicker/spectrum.js", + "./node_modules/spectrum-colorpicker/spectrum.css" + ], + "base": "./node_modules/spectrum-colorpicker" + }, { "name": "tinymce", "src": [ diff --git a/src/Umbraco.Web.UI.Client/lib/bootstrap/js/bootstrap.2.3.2.js b/src/Umbraco.Web.UI.Client/lib/bootstrap/js/bootstrap.2.3.2.js deleted file mode 100644 index 31701ad5df..0000000000 --- a/src/Umbraco.Web.UI.Client/lib/bootstrap/js/bootstrap.2.3.2.js +++ /dev/null @@ -1,2284 +0,0 @@ -/* =================================================== - * bootstrap-transition.js v2.3.2 - * http://getbootstrap.com/2.3.2/javascript.html#transitions - * =================================================== - * Copyright 2013 Twitter, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ========================================================== */ - - -!function ($) { - - "use strict"; // jshint ;_; - - - /* CSS TRANSITION SUPPORT (http://www.modernizr.com/) - * ======================================================= */ - - $(function () { - - $.support.transition = (function () { - - var transitionEnd = (function () { - - var el = document.createElement('bootstrap') - , transEndEventNames = { - 'WebkitTransition' : 'webkitTransitionEnd' - , 'MozTransition' : 'transitionend' - , 'OTransition' : 'oTransitionEnd otransitionend' - , 'transition' : 'transitionend' - } - , name - - for (name in transEndEventNames){ - if (el.style[name] !== undefined) { - return transEndEventNames[name] - } - } - - }()) - - return transitionEnd && { - end: transitionEnd - } - - })() - - }) - -}(window.jQuery);/* ========================================================== - * bootstrap-alert.js v2.3.2 - * http://getbootstrap.com/2.3.2/javascript.html#alerts - * ========================================================== - * Copyright 2013 Twitter, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ========================================================== */ - - -!function ($) { - - "use strict"; // jshint ;_; - - - /* ALERT CLASS DEFINITION - * ====================== */ - - var dismiss = '[data-dismiss="alert"]' - , Alert = function (el) { - $(el).on('click', dismiss, this.close) - } - - Alert.prototype.close = function (e) { - var $this = $(this) - , selector = $this.attr('data-target') - , $parent - - if (!selector) { - selector = $this.attr('href') - selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 - } - - $parent = $(selector) - - e && e.preventDefault() - - $parent.length || ($parent = $this.hasClass('alert') ? $this : $this.parent()) - - $parent.trigger(e = $.Event('close')) - - if (e.isDefaultPrevented()) return - - $parent.removeClass('in') - - function removeElement() { - $parent - .trigger('closed') - .remove() - } - - $.support.transition && $parent.hasClass('fade') ? - $parent.on($.support.transition.end, removeElement) : - removeElement() - } - - - /* ALERT PLUGIN DEFINITION - * ======================= */ - - var old = $.fn.alert - - $.fn.alert = function (option) { - return this.each(function () { - var $this = $(this) - , data = $this.data('alert') - if (!data) $this.data('alert', (data = new Alert(this))) - if (typeof option == 'string') data[option].call($this) - }) - } - - $.fn.alert.Constructor = Alert - - - /* ALERT NO CONFLICT - * ================= */ - - $.fn.alert.noConflict = function () { - $.fn.alert = old - return this - } - - - /* ALERT DATA-API - * ============== */ - - $(document).on('click.alert.data-api', dismiss, Alert.prototype.close) - -}(window.jQuery);/* ============================================================ - * bootstrap-button.js v2.3.2 - * http://getbootstrap.com/2.3.2/javascript.html#buttons - * ============================================================ - * Copyright 2013 Twitter, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ============================================================ */ - - -!function ($) { - - "use strict"; // jshint ;_; - - - /* BUTTON PUBLIC CLASS DEFINITION - * ============================== */ - - var Button = function (element, options) { - this.$element = $(element) - this.options = $.extend({}, $.fn.button.defaults, options) - } - - Button.prototype.setState = function (state) { - var d = 'disabled' - , $el = this.$element - , data = $el.data() - , val = $el.is('input') ? 'val' : 'html' - - state = state + 'Text' - data.resetText || $el.data('resetText', $el[val]()) - - $el[val](data[state] || this.options[state]) - - // push to event loop to allow forms to submit - setTimeout(function () { - state == 'loadingText' ? - $el.addClass(d).attr(d, d) : - $el.removeClass(d).removeAttr(d) - }, 0) - } - - Button.prototype.toggle = function () { - var $parent = this.$element.closest('[data-toggle="buttons-radio"]') - - $parent && $parent - .find('.active') - .removeClass('active') - - this.$element.toggleClass('active') - } - - - /* BUTTON PLUGIN DEFINITION - * ======================== */ - - var old = $.fn.button - - $.fn.button = function (option) { - return this.each(function () { - var $this = $(this) - , data = $this.data('button') - , options = typeof option == 'object' && option - if (!data) $this.data('button', (data = new Button(this, options))) - if (option == 'toggle') data.toggle() - else if (option) data.setState(option) - }) - } - - $.fn.button.defaults = { - loadingText: 'loading...' - } - - $.fn.button.Constructor = Button - - - /* BUTTON NO CONFLICT - * ================== */ - - $.fn.button.noConflict = function () { - $.fn.button = old - return this - } - - - /* BUTTON DATA-API - * =============== */ - - $(document).on('click.button.data-api', '[data-toggle^=button]', function (e) { - var $btn = $(e.target) - if (!$btn.hasClass('btn')) $btn = $btn.closest('.btn') - $btn.button('toggle') - }) - -}(window.jQuery);/* ========================================================== - * bootstrap-carousel.js v2.3.2 - * http://getbootstrap.com/2.3.2/javascript.html#carousel - * ========================================================== - * Copyright 2013 Twitter, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ========================================================== */ - - -!function ($) { - - "use strict"; // jshint ;_; - - - /* CAROUSEL CLASS DEFINITION - * ========================= */ - - var Carousel = function (element, options) { - this.$element = $(element) - this.$indicators = this.$element.find('.carousel-indicators') - this.options = options - this.options.pause == 'hover' && this.$element - .on('mouseenter', $.proxy(this.pause, this)) - .on('mouseleave', $.proxy(this.cycle, this)) - } - - Carousel.prototype = { - - cycle: function (e) { - if (!e) this.paused = false - if (this.interval) clearInterval(this.interval); - this.options.interval - && !this.paused - && (this.interval = setInterval($.proxy(this.next, this), this.options.interval)) - return this - } - - , getActiveIndex: function () { - this.$active = this.$element.find('.item.active') - this.$items = this.$active.parent().children() - return this.$items.index(this.$active) - } - - , to: function (pos) { - var activeIndex = this.getActiveIndex() - , that = this - - if (pos > (this.$items.length - 1) || pos < 0) return - - if (this.sliding) { - return this.$element.one('slid', function () { - that.to(pos) - }) - } - - if (activeIndex == pos) { - return this.pause().cycle() - } - - return this.slide(pos > activeIndex ? 'next' : 'prev', $(this.$items[pos])) - } - - , pause: function (e) { - if (!e) this.paused = true - if (this.$element.find('.next, .prev').length && $.support.transition.end) { - this.$element.trigger($.support.transition.end) - this.cycle(true) - } - clearInterval(this.interval) - this.interval = null - return this - } - - , next: function () { - if (this.sliding) return - return this.slide('next') - } - - , prev: function () { - if (this.sliding) return - return this.slide('prev') - } - - , slide: function (type, next) { - var $active = this.$element.find('.item.active') - , $next = next || $active[type]() - , isCycling = this.interval - , direction = type == 'next' ? 'left' : 'right' - , fallback = type == 'next' ? 'first' : 'last' - , that = this - , e - - this.sliding = true - - isCycling && this.pause() - - $next = $next.length ? $next : this.$element.find('.item')[fallback]() - - e = $.Event('slide', { - relatedTarget: $next[0] - , direction: direction - }) - - if ($next.hasClass('active')) return - - if (this.$indicators.length) { - this.$indicators.find('.active').removeClass('active') - this.$element.one('slid', function () { - var $nextIndicator = $(that.$indicators.children()[that.getActiveIndex()]) - $nextIndicator && $nextIndicator.addClass('active') - }) - } - - if ($.support.transition && this.$element.hasClass('slide')) { - this.$element.trigger(e) - if (e.isDefaultPrevented()) return - $next.addClass(type) - $next[0].offsetWidth // force reflow - $active.addClass(direction) - $next.addClass(direction) - this.$element.one($.support.transition.end, function () { - $next.removeClass([type, direction].join(' ')).addClass('active') - $active.removeClass(['active', direction].join(' ')) - that.sliding = false - setTimeout(function () { that.$element.trigger('slid') }, 0) - }) - } else { - this.$element.trigger(e) - if (e.isDefaultPrevented()) return - $active.removeClass('active') - $next.addClass('active') - this.sliding = false - this.$element.trigger('slid') - } - - isCycling && this.cycle() - - return this - } - - } - - - /* CAROUSEL PLUGIN DEFINITION - * ========================== */ - - var old = $.fn.carousel - - $.fn.carousel = function (option) { - return this.each(function () { - var $this = $(this) - , data = $this.data('carousel') - , options = $.extend({}, $.fn.carousel.defaults, typeof option == 'object' && option) - , action = typeof option == 'string' ? option : options.slide - if (!data) $this.data('carousel', (data = new Carousel(this, options))) - if (typeof option == 'number') data.to(option) - else if (action) data[action]() - else if (options.interval) data.pause().cycle() - }) - } - - $.fn.carousel.defaults = { - interval: 5000 - , pause: 'hover' - } - - $.fn.carousel.Constructor = Carousel - - - /* CAROUSEL NO CONFLICT - * ==================== */ - - $.fn.carousel.noConflict = function () { - $.fn.carousel = old - return this - } - - /* CAROUSEL DATA-API - * ================= */ - - $(document).on('click.carousel.data-api', '[data-slide], [data-slide-to]', function (e) { - var $this = $(this), href - , $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) //strip for ie7 - , options = $.extend({}, $target.data(), $this.data()) - , slideIndex - - $target.carousel(options) - - if (slideIndex = $this.attr('data-slide-to')) { - $target.data('carousel').pause().to(slideIndex).cycle() - } - - e.preventDefault() - }) - -}(window.jQuery);/* ============================================================= - * bootstrap-collapse.js v2.3.2 - * http://getbootstrap.com/2.3.2/javascript.html#collapse - * ============================================================= - * Copyright 2013 Twitter, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ============================================================ */ - - -!function ($) { - - "use strict"; // jshint ;_; - - - /* COLLAPSE PUBLIC CLASS DEFINITION - * ================================ */ - - var Collapse = function (element, options) { - this.$element = $(element) - this.options = $.extend({}, $.fn.collapse.defaults, options) - - if (this.options.parent) { - this.$parent = $(this.options.parent) - } - - this.options.toggle && this.toggle() - } - - Collapse.prototype = { - - constructor: Collapse - - , dimension: function () { - var hasWidth = this.$element.hasClass('width') - return hasWidth ? 'width' : 'height' - } - - , show: function () { - var dimension - , scroll - , actives - , hasData - - if (this.transitioning || this.$element.hasClass('in')) return - - dimension = this.dimension() - scroll = $.camelCase(['scroll', dimension].join('-')) - actives = this.$parent && this.$parent.find('> .accordion-group > .in') - - if (actives && actives.length) { - hasData = actives.data('collapse') - if (hasData && hasData.transitioning) return - actives.collapse('hide') - hasData || actives.data('collapse', null) - } - - this.$element[dimension](0) - this.transition('addClass', $.Event('show'), 'shown') - $.support.transition && this.$element[dimension](this.$element[0][scroll]) - } - - , hide: function () { - var dimension - if (this.transitioning || !this.$element.hasClass('in')) return - dimension = this.dimension() - this.reset(this.$element[dimension]()) - this.transition('removeClass', $.Event('hide'), 'hidden') - this.$element[dimension](0) - } - - , reset: function (size) { - var dimension = this.dimension() - - this.$element - .removeClass('collapse') - [dimension](size || 'auto') - [0].offsetWidth - - this.$element[size !== null ? 'addClass' : 'removeClass']('collapse') - - return this - } - - , transition: function (method, startEvent, completeEvent) { - var that = this - , complete = function () { - if (startEvent.type == 'show') that.reset() - that.transitioning = 0 - that.$element.trigger(completeEvent) - } - - this.$element.trigger(startEvent) - - if (startEvent.isDefaultPrevented()) return - - this.transitioning = 1 - - this.$element[method]('in') - - $.support.transition && this.$element.hasClass('collapse') ? - this.$element.one($.support.transition.end, complete) : - complete() - } - - , toggle: function () { - this[this.$element.hasClass('in') ? 'hide' : 'show']() - } - - } - - - /* COLLAPSE PLUGIN DEFINITION - * ========================== */ - - var old = $.fn.collapse - - $.fn.collapse = function (option) { - return this.each(function () { - var $this = $(this) - , data = $this.data('collapse') - , options = $.extend({}, $.fn.collapse.defaults, $this.data(), typeof option == 'object' && option) - if (!data) $this.data('collapse', (data = new Collapse(this, options))) - if (typeof option == 'string') data[option]() - }) - } - - $.fn.collapse.defaults = { - toggle: true - } - - $.fn.collapse.Constructor = Collapse - - - /* COLLAPSE NO CONFLICT - * ==================== */ - - $.fn.collapse.noConflict = function () { - $.fn.collapse = old - return this - } - - - /* COLLAPSE DATA-API - * ================= */ - - $(document).on('click.collapse.data-api', '[data-toggle=collapse]', function (e) { - var $this = $(this), href - , target = $this.attr('data-target') - || e.preventDefault() - || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') //strip for ie7 - , option = $(target).data('collapse') ? 'toggle' : $this.data() - $this[$(target).hasClass('in') ? 'addClass' : 'removeClass']('collapsed') - $(target).collapse(option) - }) - -}(window.jQuery);/* ============================================================ - * bootstrap-dropdown.js v2.3.2 - * http://getbootstrap.com/2.3.2/javascript.html#dropdowns - * ============================================================ - * Copyright 2013 Twitter, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ============================================================ */ - - -!function ($) { - - "use strict"; // jshint ;_; - - - /* DROPDOWN CLASS DEFINITION - * ========================= */ - - var toggle = '[data-toggle=dropdown]' - , Dropdown = function (element) { - var $el = $(element).on('click.dropdown.data-api', this.toggle) - $('html').on('click.dropdown.data-api', function () { - $el.parent().removeClass('open') - }) - } - - Dropdown.prototype = { - - constructor: Dropdown - - , toggle: function (e) { - var $this = $(this) - , $parent - , isActive - - if ($this.is('.disabled, :disabled')) return - - $parent = getParent($this) - - isActive = $parent.hasClass('open') - - clearMenus() - - if (!isActive) { - if ('ontouchstart' in document.documentElement) { - // if mobile we we use a backdrop because click events don't delegate - $('
+
media - + views/dashboard/media/mediafolderbrowser.html
-
- - forms - - - - views/dashboard/forms/formsdashboardintro.html - - -
+
translator @@ -61,29 +55,35 @@ admin - + + views/dashboard/default/startupdashboardintro.html
+
member - + views/dashboard/members/membersdashboardvideos.html
-
+ +
- contour + settings - - plugins/umbracocontour/formsdashboard.ascx + + + /App_Plugins/ModelsBuilder/modelsbuilder.htm +
+
settings @@ -104,14 +104,4 @@
-
- - settings - - - - /App_Plugins/ModelsBuilder/modelsbuilder.htm - - -
diff --git a/src/Umbraco.Web/Editors/DashboardController.cs b/src/Umbraco.Web/Editors/DashboardController.cs index b8a961ee5a..829501b46e 100644 --- a/src/Umbraco.Web/Editors/DashboardController.cs +++ b/src/Umbraco.Web/Editors/DashboardController.cs @@ -27,6 +27,8 @@ namespace Umbraco.Web.Editors [WebApi.UmbracoAuthorize] public class DashboardController : UmbracoApiController { + private readonly Dashboards _dashboards; + /// /// Initializes a new instance of the with auto dependencies. /// @@ -36,9 +38,11 @@ namespace Umbraco.Web.Editors /// /// Initializes a new instance of the with all its dependencies. /// - public DashboardController(IGlobalSettings globalSettings, IUmbracoContextAccessor umbracoContextAccessor, ISqlContext sqlContext, ServiceContext services, CacheHelper applicationCache, IProfilingLogger logger, IRuntimeState runtimeState) + public DashboardController(IGlobalSettings globalSettings, IUmbracoContextAccessor umbracoContextAccessor, ISqlContext sqlContext, ServiceContext services, CacheHelper applicationCache, IProfilingLogger logger, IRuntimeState runtimeState, Dashboards dashboards) : base(globalSettings, umbracoContextAccessor, sqlContext, services, applicationCache, logger, runtimeState) - { } + { + _dashboards = dashboards; + } //we have just one instance of HttpClient shared for the entire application private static readonly HttpClient HttpClient = new HttpClient(); @@ -47,10 +51,6 @@ namespace Umbraco.Web.Editors [ValidateAngularAntiForgeryToken] public async Task GetRemoteDashboardContent(string section, string baseUrl = "https://dashboard.umbraco.org/") { - var context = UmbracoContext.Current; - if (context == null) - throw new HttpResponseException(HttpStatusCode.InternalServerError); - var user = Security.CurrentUser; var allowedSections = string.Join(",", user.AllowedSections); var language = user.Language; @@ -133,8 +133,7 @@ namespace Umbraco.Web.Editors [ValidateAngularAntiForgeryToken] public IEnumerable> GetDashboard(string section) { - var dashboardHelper = new DashboardHelper(Services.SectionService); - return dashboardHelper.GetDashboard(section, Security.CurrentUser); + return _dashboards.GetDashboards(section, Security.CurrentUser); } } } diff --git a/src/Umbraco.Web/Editors/DashboardHelper.cs b/src/Umbraco.Web/Editors/DashboardHelper.cs deleted file mode 100644 index 75ccda1a9b..0000000000 --- a/src/Umbraco.Web/Editors/DashboardHelper.cs +++ /dev/null @@ -1,99 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Umbraco.Core; -using Umbraco.Core.Configuration; -using Umbraco.Core.IO; -using Umbraco.Core.Models.Membership; -using Umbraco.Core.Services; -using Umbraco.Web.Models.ContentEditing; - -namespace Umbraco.Web.Editors -{ - internal class DashboardHelper - { - private readonly ISectionService _sectionService; - - public DashboardHelper(ISectionService sectionService) - { - if (sectionService == null) throw new ArgumentNullException("sectionService"); - _sectionService = sectionService; - } - - /// - /// Returns the dashboard models per section for the current user and it's access - /// - /// - /// - public IDictionary>> GetDashboards(IUser currentUser) - { - var result = new Dictionary>>(); - foreach (var section in _sectionService.GetSections()) - { - result[section.Alias] = GetDashboard(section.Alias, currentUser); - } - return result; - } - - /// - /// Returns the dashboard model for the given section based on the current user and it's access - /// - /// - /// - /// - public IEnumerable> GetDashboard(string section, IUser currentUser) - { - var tabs = new List>(); - var i = 1; - - //disable packages section dashboard - if (section == "packages") return tabs; - - foreach (var dashboardSection in UmbracoConfig.For.DashboardSettings().Sections.Where(x => x.Areas.Contains(section))) - { - //we need to validate access to this section - if (DashboardSecurity.AuthorizeAccess(dashboardSection, currentUser, _sectionService) == false) - continue; - - //User is authorized - foreach (var tab in dashboardSection.Tabs) - { - //we need to validate access to this tab - if (DashboardSecurity.AuthorizeAccess(tab, currentUser, _sectionService) == false) - continue; - - var dashboardControls = new List(); - - foreach (var control in tab.Controls) - { - if (DashboardSecurity.AuthorizeAccess(control, currentUser, _sectionService) == false) - continue; - - var dashboardControl = new DashboardControl(); - var controlPath = control.ControlPath.Trim(); - dashboardControl.Caption = control.PanelCaption; - dashboardControl.Path = IOHelper.FindFile(controlPath); - if (controlPath.ToLowerInvariant().EndsWith(".ascx".ToLowerInvariant())) - dashboardControl.ServerSide = true; - - dashboardControls.Add(dashboardControl); - } - - tabs.Add(new Tab - { - Id = i, - Alias = tab.Caption.ToSafeAlias(), - IsActive = i == 1, - Label = tab.Caption, - Properties = dashboardControls - }); - - i++; - } - } - - //In case there are no tabs or a user doesn't have access the empty tabs list is returned - return tabs; - } - } -} diff --git a/src/Umbraco.Web/Editors/DashboardSecurity.cs b/src/Umbraco.Web/Editors/DashboardSecurity.cs index 87ed5af196..1481606c7e 100644 --- a/src/Umbraco.Web/Editors/DashboardSecurity.cs +++ b/src/Umbraco.Web/Editors/DashboardSecurity.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Globalization; using System.Linq; using Umbraco.Core; @@ -17,105 +18,97 @@ namespace Umbraco.Web.Editors public static bool AuthorizeAccess(ISection dashboardSection, IUser user, ISectionService sectionService) { - if (user.Id.ToString(CultureInfo.InvariantCulture) == 0.ToInvariantString()) - { - return true; - } - - var denyTypes = dashboardSection.AccessRights.Rules.Where(x => x.Action == AccessType.Deny).ToArray(); - var grantedTypes = dashboardSection.AccessRights.Rules.Where(x => x.Action == AccessType.Grant).ToArray(); - var grantedBySectionTypes = dashboardSection.AccessRights.Rules.Where(x => x.Action == AccessType.GrantBySection).ToArray(); - - return CheckUserAccessByRules(user, sectionService, denyTypes, grantedTypes, grantedBySectionTypes); + return CheckUserAccessByRules(user, sectionService, dashboardSection.AccessRights.Rules); } public static bool AuthorizeAccess(IDashboardTab dashboardTab, IUser user, ISectionService sectionService) { - if (user.Id.ToString(CultureInfo.InvariantCulture) == Constants.System.Root.ToInvariantString()) - { - return true; - } - - var denyTypes = dashboardTab.AccessRights.Rules.Where(x => x.Action == AccessType.Deny).ToArray(); - var grantedTypes = dashboardTab.AccessRights.Rules.Where(x => x.Action == AccessType.Grant).ToArray(); - var grantedBySectionTypes = dashboardTab.AccessRights.Rules.Where(x => x.Action == AccessType.GrantBySection).ToArray(); - - return CheckUserAccessByRules(user, sectionService, denyTypes, grantedTypes, grantedBySectionTypes); + return CheckUserAccessByRules(user, sectionService, dashboardTab.AccessRights.Rules); } - public static bool AuthorizeAccess(IDashboardControl dashboardTab, IUser user, ISectionService sectionService) + public static bool AuthorizeAccess(IDashboardControl dashboardControl, IUser user, ISectionService sectionService) { - if (user.Id.ToString(CultureInfo.InvariantCulture) == Constants.System.Root.ToInvariantString()) - { - return true; - } - - var denyTypes = dashboardTab.AccessRights.Rules.Where(x => x.Action == AccessType.Deny).ToArray(); - var grantedTypes = dashboardTab.AccessRights.Rules.Where(x => x.Action == AccessType.Grant).ToArray(); - var grantedBySectionTypes = dashboardTab.AccessRights.Rules.Where(x => x.Action == AccessType.GrantBySection).ToArray(); - - return CheckUserAccessByRules(user, sectionService, denyTypes, grantedTypes, grantedBySectionTypes); + return CheckUserAccessByRules(user, sectionService, dashboardControl.AccessRights.Rules); } - public static bool CheckUserAccessByRules(IUser user, ISectionService sectionService, IAccessItem[] denyTypes, IAccessItem[] grantedTypes, IAccessItem[] grantedBySectionTypes) + private static (IAccessRule[], IAccessRule[], IAccessRule[]) GroupRules(IEnumerable rules) { - var allowedSoFar = false; + IAccessRule[] denyRules = null, grantRules = null, grantBySectionRules = null; - // if there's no grantBySection or grant rules defined - we allow access so far and skip to checking deny rules - if (grantedBySectionTypes.Any() == false && grantedTypes.Any() == false) + var groupedRules = rules.GroupBy(x => x.Type); + foreach (var group in groupedRules) { - allowedSoFar = true; + var a = group.ToArray(); + switch (group.Key) + { + case AccessRuleType.Deny: + denyRules = a; + break; + case AccessRuleType.Grant: + grantRules = a; + break; + case AccessRuleType.GrantBySection: + grantBySectionRules = a; + break; + default: + throw new Exception("panic"); + } } - // else we check the rules and only allow if one matches - else + + return (denyRules ?? Array.Empty(), grantRules ?? Array.Empty(), grantBySectionRules ?? Array.Empty()); + } + + public static bool CheckUserAccessByRules(IUser user, ISectionService sectionService, IEnumerable rules) + { + if (user.Id == Constants.Security.SuperUserId) + return true; + + var (denyRules, grantRules, grantBySectionRules) = GroupRules(rules); + + var hasAccess = true; + string[] assignedUserGroups = null; + + // if there are no grant rules, then access is granted by default, unless denied + // otherwise, grant rules determine if access can be granted at all + if (grantBySectionRules.Length > 0 || grantRules.Length > 0) { + hasAccess = false; + // check if this item has any grant-by-section arguments. // if so check if the user has access to any of the sections approved, if so they will be allowed to see it (so far) - if (grantedBySectionTypes.Any()) + if (grantBySectionRules.Length > 0) { - var allowedApps = sectionService.GetAllowedSections(Convert.ToInt32(user.Id)) - .Select(x => x.Alias) - .ToArray(); + var allowedSections = sectionService.GetAllowedSections(user.Id).Select(x => x.Alias).ToArray(); + var wantedSections = grantBySectionRules.SelectMany(g => g.Value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)).ToArray(); - var allApprovedSections = grantedBySectionTypes.SelectMany(g => g.Value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)).ToArray(); - if (allApprovedSections.Any(allowedApps.Contains)) - { - allowedSoFar = true; - } + if (wantedSections.Intersect(allowedSections).Any()) + hasAccess = true; } // if not already granted access, check if this item as any grant arguments. // if so check if the user is in one of the user groups approved, if so they will be allowed to see it (so far) - if (allowedSoFar == false && grantedTypes.Any()) + if (hasAccess == false && grantRules.Any()) { - var allApprovedUserTypes = grantedTypes.SelectMany(g => g.Value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)).ToArray(); - foreach (var userGroup in user.Groups) - { - if (allApprovedUserTypes.InvariantContains(userGroup.Alias)) - { - allowedSoFar = true; - break; - } - } + assignedUserGroups = user.Groups.Select(x => x.Alias).ToArray(); + var wantedUserGroups = grantRules.SelectMany(g => g.Value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)).ToArray(); + + if (wantedUserGroups.Intersect(assignedUserGroups).Any()) + hasAccess = true; } } + if (!hasAccess || denyRules.Length == 0) + return false; + // check if this item has any deny arguments, if so check if the user is in one of the denied user groups, if so they will // be denied to see it no matter what - if (denyTypes.Any()) - { - var allDeniedUserTypes = denyTypes.SelectMany(g => g.Value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)).ToArray(); - foreach (var userGroup in user.Groups) - { - if (allDeniedUserTypes.InvariantContains(userGroup.Alias)) - { - allowedSoFar = false; - break; - } - } - } + assignedUserGroups = assignedUserGroups ?? user.Groups.Select(x => x.Alias).ToArray(); + var deniedUserGroups = denyRules.SelectMany(g => g.Value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)).ToArray(); - return allowedSoFar; + if (deniedUserGroups.Intersect(assignedUserGroups).Any()) + hasAccess = false; + + return hasAccess; } } } diff --git a/src/Umbraco.Web/Editors/Dashboards.cs b/src/Umbraco.Web/Editors/Dashboards.cs new file mode 100644 index 0000000000..4bf2d81045 --- /dev/null +++ b/src/Umbraco.Web/Editors/Dashboards.cs @@ -0,0 +1,159 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core; +using Umbraco.Core.Configuration.Dashboard; +using Umbraco.Core.IO; +using Umbraco.Core.Manifest; +using Umbraco.Core.Models.Membership; +using Umbraco.Core.Services; +using Umbraco.Web.Models.ContentEditing; + +namespace Umbraco.Web.Editors +{ + public class Dashboards + { + private readonly ISectionService _sectionService; + private readonly IDashboardSection _dashboardSection; + private readonly ManifestParser _manifestParser; + + public Dashboards(ISectionService sectionService, IDashboardSection dashboardSection, ManifestParser manifestParser) + { + _sectionService = sectionService ?? throw new ArgumentNullException(nameof(sectionService)); + _dashboardSection = dashboardSection; + _manifestParser = manifestParser; + } + + /// + /// Gets all dashboards, organized by section, for a user. + /// + public IDictionary>> GetDashboards(IUser currentUser) + { + return _sectionService.GetSections().ToDictionary(x => x.Alias, x => GetDashboards(x.Alias, currentUser)); + } + + /// + /// Returns dashboards for a specific section, for a user. + /// + public IEnumerable> GetDashboards(string section, IUser currentUser) + { + var tabId = 1; + var configDashboards = GetDashboardsFromConfig(ref tabId, section, currentUser); + var pluginDashboards = GetDashboardsFromPlugins(ref tabId, section, currentUser); + + // merge dashboards + // both collections contain tab.alias -> controls + var dashboards = configDashboards; + + // until now, it was fine to have duplicate tab.aliases in configDashboard + // so... the rule should be - just merge whatever we get, don't be clever + dashboards.AddRange(pluginDashboards); + + // re-sort by id + dashboards.Sort((tab1, tab2) => tab1.Id > tab2.Id ? 1 : 0); + + // re-assign ids (why?) + var i = 1; + foreach (var tab in dashboards) + { + tab.Id = i++; + tab.IsActive = tab.Id == 1; + } + + return configDashboards; + } + + // note: + // in dashboard.config we have 'sections' which define 'tabs' for 'areas' + // and 'areas' are the true UI sections - and each tab can have more than + // one control + // in a manifest, we directly have 'dashboards' which map to a unique + // control in a tab + + // gets all tabs & controls from the config file + private List> GetDashboardsFromConfig(ref int tabId, string section, IUser currentUser) + { + var tabs = new List>(); + + // disable packages section dashboard + if (section == "packages") return tabs; + + foreach (var dashboardSection in _dashboardSection.Sections.Where(x => x.Areas.InvariantContains(section))) + { + // validate access to this section + if (!DashboardSecurity.AuthorizeAccess(dashboardSection, currentUser, _sectionService)) + continue; + + foreach (var tab in dashboardSection.Tabs) + { + // validate access to this tab + if (!DashboardSecurity.AuthorizeAccess(tab, currentUser, _sectionService)) + continue; + + var dashboardControls = new List(); + + foreach (var control in tab.Controls) + { + // validate access to this control + if (!DashboardSecurity.AuthorizeAccess(control, currentUser, _sectionService)) + continue; + + // create and add control + var dashboardControl = new DashboardControl + { + Caption = control.PanelCaption, + Path = IOHelper.FindFile(control.ControlPath.Trim()) + }; + + if (dashboardControl.Path.InvariantEndsWith(".ascx")) + throw new NotSupportedException("Legacy UserControl (.ascx) dashboards are no longer supported."); + + dashboardControls.Add(dashboardControl); + } + + // create and add tab + tabs.Add(new Tab + { + Id = tabId++, + Alias = tab.Caption.ToSafeAlias(), + Label = tab.Caption, + Properties = dashboardControls + }); + } + } + + return tabs; + } + + private List> GetDashboardsFromPlugins(ref int tabId, string section, IUser currentUser) + { + var tabs = new List>(); + + foreach (var dashboard in _manifestParser.Manifest.Dashboards.Where(x => x.Sections.InvariantContains(section)).OrderBy(x => x.Weight)) + { + // validate access + if (!DashboardSecurity.CheckUserAccessByRules(currentUser, _sectionService, dashboard.AccessRules)) + continue; + + var dashboardControl = new DashboardControl + { + Caption = "", + Path = IOHelper.FindFile(dashboard.View.Trim()) + }; + + if (dashboardControl.Path.InvariantEndsWith(".ascx")) + throw new NotSupportedException("Legacy UserControl (.ascx) dashboards are no longer supported."); + + tabs.Add(new Tab + { + Id = tabId++, + Alias = dashboard.Alias.ToSafeAlias(), + Label = dashboard.Name, + Properties = new[] { dashboardControl } + }); + } + + return tabs; + } + } +} diff --git a/src/Umbraco.Web/Editors/SectionController.cs b/src/Umbraco.Web/Editors/SectionController.cs index 16af20132a..f650c18fb3 100644 --- a/src/Umbraco.Web/Editors/SectionController.cs +++ b/src/Umbraco.Web/Editors/SectionController.cs @@ -15,38 +15,35 @@ namespace Umbraco.Web.Editors [PluginController("UmbracoApi")] public class SectionController : UmbracoAuthorizedJsonController { + private readonly Dashboards _dashboards; + + public SectionController(Dashboards dashboards) + { + _dashboards = dashboards; + } + public IEnumerable
GetSections() { var sections = Services.SectionService.GetAllowedSections(Security.GetUserId().ResultOr(0)); var sectionModels = sections.Select(Mapper.Map).ToArray(); - - //Check if there are empty dashboards or dashboards that will end up empty based on the current user's access - //and add the meta data about them - var dashboardHelper = new DashboardHelper(Services.SectionService); - + // this is a bit nasty since we'll be proxying via the app tree controller but we sort of have to do that // since tree's by nature are controllers and require request contextual data var appTreeController = new ApplicationTreeController { ControllerContext = ControllerContext }; - var dashboards = dashboardHelper.GetDashboards(Security.CurrentUser); + var dashboards = _dashboards.GetDashboards(Security.CurrentUser); + //now we can add metadata for each section so that the UI knows if there's actually anything at all to render for //a dashboard for a given section, then the UI can deal with it accordingly (i.e. redirect to the first tree) foreach (var section in sectionModels) { - var hasDashboards = false; - if (dashboards.TryGetValue(section.Alias, out var dashboardsForSection)) - { - if (dashboardsForSection.Any()) - hasDashboards = true; - } + var hasDashboards = dashboards.TryGetValue(section.Alias, out var dashboardsForSection) && dashboardsForSection.Any(); + if (hasDashboards) continue; - if (hasDashboards == false) - { - //get the first tree in the section and get it's root node route path - var sectionRoot = appTreeController.GetApplicationTrees(section.Alias, null, null).Result; - section.RoutePath = GetRoutePathForFirstTree(sectionRoot); - } + // get the first tree in the section and get its root node route path + var sectionRoot = appTreeController.GetApplicationTrees(section.Alias, null, null).Result; + section.RoutePath = GetRoutePathForFirstTree(sectionRoot); } return sectionModels; diff --git a/src/Umbraco.Web/Models/ContentEditing/DashboardControl.cs b/src/Umbraco.Web/Models/ContentEditing/DashboardControl.cs index d51084fb16..aad6bf2d64 100644 --- a/src/Umbraco.Web/Models/ContentEditing/DashboardControl.cs +++ b/src/Umbraco.Web/Models/ContentEditing/DashboardControl.cs @@ -10,15 +10,6 @@ namespace Umbraco.Web.Models.ContentEditing [DataContract(Name = "control", Namespace = "")] public class DashboardControl { - [DataMember(Name = "showOnce")] - public bool ShowOnce { get; set; } - - [DataMember(Name = "addPanel")] - public bool AddPanel { get; set; } - - [DataMember(Name = "serverSide")] - public bool ServerSide { get; set; } - [DataMember(Name = "path")] public string Path { get; set; } diff --git a/src/Umbraco.Web/Runtime/WebRuntimeComponent.cs b/src/Umbraco.Web/Runtime/WebRuntimeComponent.cs index 53ae6112af..1e54afd90c 100644 --- a/src/Umbraco.Web/Runtime/WebRuntimeComponent.cs +++ b/src/Umbraco.Web/Runtime/WebRuntimeComponent.cs @@ -120,6 +120,9 @@ namespace Umbraco.Web.Runtime // configure the container for web composition.ConfigureForWeb(); + + composition.RegisterUnique(); + composition .ComposeUmbracoControllers(GetType().Assembly) .SetDefaultRenderMvcController(); // default controller for template views diff --git a/src/Umbraco.Web/UI/JavaScript/JsInitialize.js b/src/Umbraco.Web/UI/JavaScript/JsInitialize.js index eef1d25909..75ca46437a 100644 --- a/src/Umbraco.Web/UI/JavaScript/JsInitialize.js +++ b/src/Umbraco.Web/UI/JavaScript/JsInitialize.js @@ -1,7 +1,7 @@ [ 'lib/jquery/jquery.min.js', 'lib/jquery-ui/jquery-ui.min.js', - 'lib/jquery-ui-touch-punch/jquery.ui.touch-punch.js', + 'lib/jquery-ui-touch-punch/jquery.ui.touch-punch.min.js', 'lib/angular/angular.js', 'lib/underscore/underscore-min.js', @@ -23,7 +23,6 @@ 'lib/ng-file-upload/ng-file-upload.min.js', 'lib/angular-local-storage/angular-local-storage.min.js', - 'lib/bootstrap/js/bootstrap.2.3.2.min.js', 'lib/umbraco/Extensions.js', 'lib/umbraco/NamespaceManager.js', diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 8952cb78c6..212dcdf89d 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -193,7 +193,7 @@ - +