Merge remote-tracking branch 'origin/temp8' into temp8-3527-indexes-via-code

This commit is contained in:
Shannon
2018-12-05 17:36:50 +11:00
126 changed files with 8055 additions and 14467 deletions

View File

@@ -1,5 +1,4 @@
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Umbraco.Core;
using Umbraco.Core.Composing;
@@ -22,19 +21,21 @@ namespace Umbraco.Web.Actions
internal IEnumerable<IAction> GetByLetters(IEnumerable<string> letters)
{
var all = this.ToArray();
return letters.Select(x => all.FirstOrDefault(y => y.Letter.ToString(CultureInfo.InvariantCulture) == x))
var actions = this.ToArray(); // no worry: internally, it's already an array
return letters
.Where(x => x.Length == 1)
.Select(x => actions.FirstOrDefault(y => y.Letter == x[0]))
.WhereNotNull()
.ToArray();
.ToList();
}
internal IReadOnlyList<IAction> FromEntityPermission(EntityPermission entityPermission)
{
var actions = this.ToArray(); // no worry: internally, it's already an array
return entityPermission.AssignedPermissions
.Where(x => x.Length == 1)
.Select(x => x.ToCharArray()[0])
.SelectMany(c => this.Where(x => x.Letter == c))
.Where(action => action != null)
.SelectMany(x => actions.Where(y => y.Letter == x[0]))
.WhereNotNull()
.ToList();
}
}

View File

@@ -1,11 +1,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using LightInject;
using Umbraco.Core.Composing;
namespace Umbraco.Web.Actions
{
internal class ActionCollectionBuilder : LazyCollectionBuilderBase<ActionCollectionBuilder, ActionCollection, IAction>
@@ -19,13 +17,13 @@ namespace Umbraco.Web.Actions
protected override IEnumerable<IAction> CreateItems(params object[] args)
{
var items = base.CreateItems(args).ToList();
//validate the items, no actions should exist that do not either expose notifications or permissions
var invalid = items.Where(x => !x.CanBePermissionAssigned && !x.ShowInNotifier).ToList();
if (invalid.Count > 0)
{
throw new InvalidOperationException($"Invalid actions '{string.Join(", ", invalid.Select(x => x.Alias))}'. All {typeof(IAction)} implementations must be true for either {nameof(IAction.CanBePermissionAssigned)} or {nameof(IAction.ShowInNotifier)}");
}
return items;
var invalidItems = items.Where(x => !x.CanBePermissionAssigned && !x.ShowInNotifier).ToList();
if (invalidItems.Count == 0) return items;
var invalidActions = string.Join(", ", invalidItems.Select(x => "'" + x.Alias + "'"));
throw new InvalidOperationException($"Invalid actions {invalidActions}'. All {typeof(IAction)} implementations must be true for either {nameof(IAction.CanBePermissionAssigned)} or {nameof(IAction.ShowInNotifier)}.");
}
}
}

View File

@@ -1,126 +1,126 @@
using System.Collections.Generic;
using Umbraco.Core;
using Umbraco.Core.Configuration;
using Umbraco.Web.Models.ContentEditing;
using Umbraco.Web.Mvc;
using System.Linq;
using Umbraco.Core.IO;
using Newtonsoft.Json.Linq;
using System.Threading.Tasks;
using System.Net.Http;
using System.Web.Http;
using System;
using System.Net;
using System.Text;
using Umbraco.Core.Cache;
using Umbraco.Web.WebApi;
using Umbraco.Web.WebApi.Filters;
using Umbraco.Core.Logging;
namespace Umbraco.Web.Editors
{
//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]
public class DashboardController : UmbracoApiController
{
//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<JObject> 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;
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 = ApplicationCache.RuntimeCache.GetCacheItem<JObject>(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;
ApplicationCache.RuntimeCache.InsertCacheItem<JObject>(key, () => result, new TimeSpan(0, 30, 0));
}
catch (HttpRequestException ex)
{
Logger.Error<DashboardController>(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
ApplicationCache.RuntimeCache.InsertCacheItem<JObject>(key, () => result, new TimeSpan(0, 5, 0));
}
}
return result;
}
public async Task<HttpResponseMessage> GetRemoteDashboardCss(string section, string baseUrl = "https://dashboard.umbraco.org/")
{
var url = string.Format(baseUrl + "css/dashboard.css?section={0}", section);
var key = "umbraco-dynamic-dashboard-css-" + section;
var content = ApplicationCache.RuntimeCache.GetCacheItem<string>(key);
var result = string.Empty;
if (content != null)
{
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
ApplicationCache.RuntimeCache.InsertCacheItem<string>(key, () => result, new TimeSpan(0, 30, 0));
}
catch (HttpRequestException ex)
{
Logger.Error<DashboardController>(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
ApplicationCache.RuntimeCache.InsertCacheItem<string>(key, () => result, new TimeSpan(0, 5, 0));
}
}
return new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(result, Encoding.UTF8, "text/css")
};
}
[ValidateAngularAntiForgeryToken]
public IEnumerable<Tab<DashboardControl>> GetDashboard(string section)
{
var dashboardHelper = new DashboardHelper(Services.SectionService);
return dashboardHelper.GetDashboard(section, Security.CurrentUser);
}
}
}
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.Text;
using Umbraco.Core.Cache;
using Umbraco.Web.WebApi;
using Umbraco.Web.WebApi.Filters;
using Umbraco.Core.Logging;
namespace Umbraco.Web.Editors
{
//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]
public class DashboardController : UmbracoApiController
{
private readonly Dashboards _dashboards;
public DashboardController(Dashboards dashboards)
{
_dashboards = dashboards;
}
//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<JObject> GetRemoteDashboardContent(string section, string baseUrl = "https://dashboard.umbraco.org/")
{
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 = ApplicationCache.RuntimeCache.GetCacheItem<JObject>(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;
ApplicationCache.RuntimeCache.InsertCacheItem<JObject>(key, () => result, new TimeSpan(0, 30, 0));
}
catch (HttpRequestException ex)
{
Logger.Error<DashboardController>(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
ApplicationCache.RuntimeCache.InsertCacheItem<JObject>(key, () => result, new TimeSpan(0, 5, 0));
}
}
return result;
}
public async Task<HttpResponseMessage> GetRemoteDashboardCss(string section, string baseUrl = "https://dashboard.umbraco.org/")
{
var url = string.Format(baseUrl + "css/dashboard.css?section={0}", section);
var key = "umbraco-dynamic-dashboard-css-" + section;
var content = ApplicationCache.RuntimeCache.GetCacheItem<string>(key);
var result = string.Empty;
if (content != null)
{
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
ApplicationCache.RuntimeCache.InsertCacheItem<string>(key, () => result, new TimeSpan(0, 30, 0));
}
catch (HttpRequestException ex)
{
Logger.Error<DashboardController>(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
ApplicationCache.RuntimeCache.InsertCacheItem<string>(key, () => result, new TimeSpan(0, 5, 0));
}
}
return new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(result, Encoding.UTF8, "text/css")
};
}
[ValidateAngularAntiForgeryToken]
public IEnumerable<Tab<DashboardControl>> GetDashboard(string section)
{
return _dashboards.GetDashboards(section, Security.CurrentUser);
}
}
}

View File

@@ -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;
}
/// <summary>
/// Returns the dashboard models per section for the current user and it's access
/// </summary>
/// <param name="currentUser"></param>
/// <returns></returns>
public IDictionary<string, IEnumerable<Tab<DashboardControl>>> GetDashboards(IUser currentUser)
{
var result = new Dictionary<string, IEnumerable<Tab<DashboardControl>>>();
foreach (var section in _sectionService.GetSections())
{
result[section.Alias] = GetDashboard(section.Alias, currentUser);
}
return result;
}
/// <summary>
/// Returns the dashboard model for the given section based on the current user and it's access
/// </summary>
/// <param name="section"></param>
/// <param name="currentUser"></param>
/// <returns></returns>
public IEnumerable<Tab<DashboardControl>> GetDashboard(string section, IUser currentUser)
{
var tabs = new List<Tab<DashboardControl>>();
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<DashboardControl>();
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<DashboardControl>
{
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;
}
}
}

View File

@@ -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<IAccessRule> 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<IAccessRule>(), grantRules ?? Array.Empty<IAccessRule>(), grantBySectionRules ?? Array.Empty<IAccessRule>());
}
public static bool CheckUserAccessByRules(IUser user, ISectionService sectionService, IEnumerable<IAccessRule> 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;
}
}
}

View File

@@ -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;
}
/// <summary>
/// Gets all dashboards, organized by section, for a user.
/// </summary>
public IDictionary<string, IEnumerable<Tab<DashboardControl>>> GetDashboards(IUser currentUser)
{
return _sectionService.GetSections().ToDictionary(x => x.Alias, x => GetDashboards(x.Alias, currentUser));
}
/// <summary>
/// Returns dashboards for a specific section, for a user.
/// </summary>
public IEnumerable<Tab<DashboardControl>> 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<Tab<DashboardControl>> GetDashboardsFromConfig(ref int tabId, string section, IUser currentUser)
{
var tabs = new List<Tab<DashboardControl>>();
// 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<DashboardControl>();
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<DashboardControl>
{
Id = tabId++,
Alias = tab.Caption.ToSafeAlias(),
Label = tab.Caption,
Properties = dashboardControls
});
}
}
return tabs;
}
private List<Tab<DashboardControl>> GetDashboardsFromPlugins(ref int tabId, string section, IUser currentUser)
{
var tabs = new List<Tab<DashboardControl>>();
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<DashboardControl>
{
Id = tabId++,
Alias = dashboard.Alias.ToSafeAlias(),
Label = dashboard.Name,
Properties = new[] { dashboardControl }
});
}
return tabs;
}
}
}

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http;
@@ -194,7 +195,7 @@ namespace Umbraco.Web.Editors
const int level = 0;
foreach (var dictionaryItem in Services.LocalizationService.GetRootDictionaryItems())
foreach (var dictionaryItem in Services.LocalizationService.GetRootDictionaryItems().OrderBy(ItemSort()))
{
var item = Mapper.Map<IDictionaryItem, DictionaryOverviewDisplay>(dictionaryItem);
item.Level = 0;
@@ -220,8 +221,7 @@ namespace Umbraco.Web.Editors
/// </param>
private void GetChildItemsForList(IDictionaryItem dictionaryItem, int level, List<DictionaryOverviewDisplay> list)
{
foreach (var childItem in Services.LocalizationService.GetDictionaryItemChildren(
dictionaryItem.Key))
foreach (var childItem in Services.LocalizationService.GetDictionaryItemChildren(dictionaryItem.Key).OrderBy(ItemSort()))
{
var item = Mapper.Map<IDictionaryItem, DictionaryOverviewDisplay>(childItem);
item.Level = level;
@@ -230,5 +230,7 @@ namespace Umbraco.Web.Editors
GetChildItemsForList(childItem, level + 1, list);
}
}
private Func<IDictionaryItem, string> ItemSort() => item => item.ItemKey;
}
}

View File

@@ -1,98 +1,94 @@
using System.Collections.Generic;
using AutoMapper;
using Umbraco.Web.Models.ContentEditing;
using Umbraco.Web.Mvc;
using System.Linq;
using Umbraco.Core.Composing;
using Umbraco.Core.Models;
using Umbraco.Web.Trees;
using Section = Umbraco.Web.Models.ContentEditing.Section;
using Umbraco.Web.Models.Trees;
namespace Umbraco.Web.Editors
{
/// <summary>
/// The API controller used for using the list of sections
/// </summary>
[PluginController("UmbracoApi")]
public class SectionController : UmbracoAuthorizedJsonController
{
public IEnumerable<Section> GetSections()
{
var sections = Services.SectionService.GetAllowedSections(Security.GetUserId().ResultOr(0));
var sectionModels = sections.Select(Mapper.Map<Core.Models.Section, Section>).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 - and then we have to
// remember to inject properties - nasty indeed
// fixme - this controller could/should be able to be created from the container and/or from webapi's IHttpControllerTypeResolver
var appTreeController = new ApplicationTreeController();
Current.Container.InjectProperties(appTreeController);
appTreeController.ControllerContext = ControllerContext;
var dashboards = dashboardHelper.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;
}
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);
}
}
return sectionModels;
}
/// <summary>
/// Returns the first non root/group node's route path
/// </summary>
/// <param name="rootNode"></param>
/// <returns></returns>
private string GetRoutePathForFirstTree(TreeRootNode rootNode)
{
if (!rootNode.IsContainer || !rootNode.ContainsTrees)
return rootNode.RoutePath;
foreach(var node in rootNode.Children)
{
if (node is TreeRootNode groupRoot)
return GetRoutePathForFirstTree(groupRoot);//recurse to get the first tree in the group
else
return node.RoutePath;
}
return string.Empty;
}
/// <summary>
/// Returns all the sections that the user has access to
/// </summary>
/// <returns></returns>
public IEnumerable<Section> GetAllSections()
{
var sections = Services.SectionService.GetSections();
var mapped = sections.Select(Mapper.Map<Core.Models.Section, Section>);
if (Security.CurrentUser.IsAdmin())
return mapped;
return mapped.Where(x => Security.CurrentUser.AllowedSections.Contains(x.Alias)).ToArray();
}
}
}
using System.Collections.Generic;
using AutoMapper;
using Umbraco.Web.Models.ContentEditing;
using Umbraco.Web.Mvc;
using System.Linq;
using Umbraco.Core.Composing;
using Umbraco.Core.Models;
using Umbraco.Web.Trees;
using Section = Umbraco.Web.Models.ContentEditing.Section;
using Umbraco.Web.Models.Trees;
namespace Umbraco.Web.Editors
{
/// <summary>
/// The API controller used for using the list of sections
/// </summary>
[PluginController("UmbracoApi")]
public class SectionController : UmbracoAuthorizedJsonController
{
private readonly Dashboards _dashboards;
public SectionController(Dashboards dashboards)
{
_dashboards = dashboards;
}
public IEnumerable<Section> GetSections()
{
var sections = Services.SectionService.GetAllowedSections(Security.GetUserId().ResultOr(0));
var sectionModels = sections.Select(Mapper.Map<Core.Models.Section, Section>).ToArray();
// 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 - and then we have to
// remember to inject properties - nasty indeed
// fixme - this controller could/should be able to be created from the container and/or from webapi's IHttpControllerTypeResolver
var appTreeController = new ApplicationTreeController();
Current.Container.InjectProperties(appTreeController);
appTreeController.ControllerContext = ControllerContext;
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 = dashboards.TryGetValue(section.Alias, out var dashboardsForSection) && dashboardsForSection.Any();
if (hasDashboards) continue;
// 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;
}
/// <summary>
/// Returns the first non root/group node's route path
/// </summary>
/// <param name="rootNode"></param>
/// <returns></returns>
private string GetRoutePathForFirstTree(TreeRootNode rootNode)
{
if (!rootNode.IsContainer || !rootNode.ContainsTrees)
return rootNode.RoutePath;
foreach(var node in rootNode.Children)
{
if (node is TreeRootNode groupRoot)
return GetRoutePathForFirstTree(groupRoot);//recurse to get the first tree in the group
else
return node.RoutePath;
}
return string.Empty;
}
/// <summary>
/// Returns all the sections that the user has access to
/// </summary>
/// <returns></returns>
public IEnumerable<Section> GetAllSections()
{
var sections = Services.SectionService.GetSections();
var mapped = sections.Select(Mapper.Map<Core.Models.Section, Section>);
if (Security.CurrentUser.IsAdmin())
return mapped;
return mapped.Where(x => Security.CurrentUser.AllowedSections.Contains(x.Alias)).ToArray();
}
}
}

View File

@@ -1,49 +1,82 @@
using System.Reflection;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Web.Http.Controllers;
using System.Web.Mvc;
using LightInject;
using Umbraco.Core;
using Umbraco.Core.Composing;
using Umbraco.Web.Mvc;
using Umbraco.Web.WebApi;
namespace Umbraco.Web
{
internal static class LightInjectExtensions
{
/// <summary>
/// Registers all IControllers using the TypeLoader for scanning and caching found instances for the calling assembly
/// Registers Umbraco controllers.
/// </summary>
/// <param name="container"></param>
/// <param name="typeLoader"></param>
/// <param name="assembly"></param>
public static void RegisterMvcControllers(this IServiceRegistry container, TypeLoader typeLoader, Assembly assembly)
public static void RegisterUmbracoControllers(this IServiceRegistry container, TypeLoader typeLoader, Assembly umbracoWebAssembly)
{
//TODO: We've already scanned for UmbracoApiControllers and SurfaceControllers - should we scan again
// for all controllers? Seems like we should just do this once and then filter. That said here we are
// only scanning our own single assembly. Hrm.
// notes
//
// We scan and auto-registers:
// - every IController and IHttpController that *we* have in Umbraco.Web
// - PluginController and UmbracoApiController in every assembly
//
// We do NOT scan:
// - any IController or IHttpController (anything not PluginController nor UmbracoApiController), outside of Umbraco.Web
// which means that users HAVE to explicitly register their own non-Umbraco controllers
//
// This is because we try to achieve a balance between "simple" and "fast. Scanning for PluginController or
// UmbracoApiController is fast-ish because they both are IDiscoverable. Scanning for IController or IHttpController
// is a full, non-cached scan = expensive, we do it only for 1 assembly.
//
// TODO
// find a way to scan for IController *and* IHttpController in one single pass
// or, actually register them manually so don't require a full scan for these
// 5 are IController but not PluginController
// Umbraco.Web.Mvc.RenderMvcController
// Umbraco.Web.Install.Controllers.InstallController
// Umbraco.Web.Macros.PartialViewMacroController
// Umbraco.Web.Editors.PreviewController
// Umbraco.Web.Editors.BackOfficeController
// 9 are IHttpController but not UmbracoApiController
// Umbraco.Web.Controllers.UmbProfileController
// Umbraco.Web.Controllers.UmbLoginStatusController
// Umbraco.Web.Controllers.UmbRegisterController
// Umbraco.Web.Controllers.UmbLoginController
// Umbraco.Web.Mvc.RenderMvcController
// Umbraco.Web.Install.Controllers.InstallController
// Umbraco.Web.Macros.PartialViewMacroController
// Umbraco.Web.Editors.PreviewController
// Umbraco.Web.Editors.BackOfficeController
container.RegisterControllers<IController>(typeLoader, assembly);
// scan and register every IController in Umbraco.Web
var umbracoWebControllers = typeLoader.GetTypes<IController>(specificAssemblies: new[] { umbracoWebAssembly });
//foreach (var controller in umbracoWebControllers.Where(x => !typeof(PluginController).IsAssignableFrom(x)))
// Current.Logger.Debug(typeof(LightInjectExtensions), "IController NOT PluginController: " + controller.FullName);
container.RegisterControllers(umbracoWebControllers);
// scan and register every PluginController in everything (PluginController is IDiscoverable and IController)
var nonUmbracoWebPluginController = typeLoader.GetTypes<PluginController>().Where(x => x.Assembly != umbracoWebAssembly);
container.RegisterControllers(nonUmbracoWebPluginController);
// scan and register every IHttpController in Umbraco.Web
var umbracoWebHttpControllers = typeLoader.GetTypes<IHttpController>(specificAssemblies: new[] { umbracoWebAssembly });
//foreach (var controller in umbracoWebControllers.Where(x => !typeof(UmbracoApiController).IsAssignableFrom(x)))
// Current.Logger.Debug(typeof(LightInjectExtensions), "IHttpController NOT UmbracoApiController: " + controller.FullName);
container.RegisterControllers(umbracoWebHttpControllers);
// scan and register every UmbracoApiController in everything (UmbracoApiController is IDiscoverable and IHttpController)
var nonUmbracoWebApiControllers = typeLoader.GetTypes<UmbracoApiController>().Where(x => x.Assembly != umbracoWebAssembly);
container.RegisterControllers(nonUmbracoWebApiControllers);
}
/// <summary>
/// Registers all IHttpController using the TypeLoader for scanning and caching found instances for the calling assembly
/// </summary>
/// <param name="container"></param>
/// <param name="typeLoader"></param>
/// <param name="assembly"></param>
public static void RegisterApiControllers(this IServiceRegistry container, TypeLoader typeLoader, Assembly assembly)
private static void RegisterControllers(this IServiceRegistry container, IEnumerable<Type> controllerTypes)
{
//TODO: We've already scanned for UmbracoApiControllers and SurfaceControllers - should we scan again
// for all controllers? Seems like we should just do this once and then filter. That said here we are
// only scanning our own single assembly. Hrm.
container.RegisterControllers<IHttpController>(typeLoader, assembly);
}
private static void RegisterControllers<TController>(this IServiceRegistry container, TypeLoader typeLoader, Assembly assembly)
{
var types = typeLoader.GetTypes<TController>(specificAssemblies: new[] { assembly });
foreach (var type in types)
container.Register(type, new PerRequestLifeTime());
foreach (var controllerType in controllerTypes)
container.Register(controllerType, new PerRequestLifeTime());
}
}
}

View File

@@ -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; }

View File

@@ -122,9 +122,10 @@ namespace Umbraco.Web.Runtime
composition.Container.EnableMvc(); // does container.EnablePerWebRequestScope()
composition.Container.ScopeManagerProvider = smp; // reverts - we will do it last (in WebRuntime)
composition.Container.RegisterMvcControllers(typeLoader, GetType().Assembly);
composition.Container.RegisterSingleton<Dashboards>();
composition.Container.RegisterUmbracoControllers(typeLoader, GetType().Assembly);
composition.Container.EnableWebApi(GlobalConfiguration.Configuration);
composition.Container.RegisterApiControllers(typeLoader, GetType().Assembly);
composition.Container.RegisterCollectionBuilder<SearchableTreeCollectionBuilder>()
.Add(() => typeLoader.GetTypes<ISearchableTree>()); // fixme which searchable trees?!
@@ -183,7 +184,7 @@ namespace Umbraco.Web.Runtime
.Append<ContentFinderByRedirectUrl>();
composition.Container.RegisterSingleton<ISiteDomainHelper, SiteDomainHelper>();
composition.Container.RegisterSingleton<ICultureDictionaryFactory, DefaultCultureDictionaryFactory>();
// register *all* checks, except those marked [HideFromTypeFinder] of course

View File

@@ -2,6 +2,7 @@
using System.Linq;
using System.Net.Http.Formatting;
using Umbraco.Core;
using Umbraco.Core.Models;
using Umbraco.Core.Services;
using Umbraco.Web.Actions;
@@ -52,10 +53,12 @@ namespace Umbraco.Web.Trees
var nodes = new TreeNodeCollection();
Func<IDictionaryItem, string> ItemSort() => item => item.ItemKey;
if (id == Constants.System.Root.ToInvariantString())
{
nodes.AddRange(
Services.LocalizationService.GetRootDictionaryItems().Select(
Services.LocalizationService.GetRootDictionaryItems().OrderBy(ItemSort()).Select(
x => CreateTreeNode(
x.Id.ToInvariantString(),
id,
@@ -71,7 +74,7 @@ namespace Umbraco.Web.Trees
if (parentDictionary == null)
return nodes;
nodes.AddRange(Services.LocalizationService.GetDictionaryItemChildren(parentDictionary.Key).ToList().OrderByDescending(item => item.Key).Select(
nodes.AddRange(Services.LocalizationService.GetDictionaryItemChildren(parentDictionary.Key).ToList().OrderBy(ItemSort()).Select(
x => CreateTreeNode(
x.Id.ToInvariantString(),
id,

View File

@@ -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',

View File

@@ -196,7 +196,7 @@
<Compile Include="Models\ContentEditing\IContentSave.cs" />
<Compile Include="WebApi\TrimModelBinder.cs" />
<Compile Include="Editors\CodeFileController.cs" />
<Compile Include="Editors\DashboardHelper.cs" />
<Compile Include="Editors\Dashboards.cs" />
<Compile Include="Editors\DictionaryController.cs" />
<Compile Include="Editors\EditorModelEventArgs.cs" />
<Compile Include="Editors\EditorValidatorCollection.cs" />