diff --git a/.editorconfig b/.editorconfig
index e99299d699..208052cbb7 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -3,7 +3,7 @@ root=true
[*]
end_of_line = lf
indent_style = space
-indent_size = 2
+indent_size = 4
trim_trailing_whitespace = true
[*.{cs,cshtml,csx,vb,vbx,vbhtml,fs,fsx,txt,ps1,sql}]
diff --git a/build/NuSpecs/UmbracoCms.Core.nuspec b/build/NuSpecs/UmbracoCms.Core.nuspec
index eb090ab6d8..aac6f26878 100644
--- a/build/NuSpecs/UmbracoCms.Core.nuspec
+++ b/build/NuSpecs/UmbracoCms.Core.nuspec
@@ -21,9 +21,9 @@
-
-
-
+
+
+
@@ -34,13 +34,13 @@
-
+
-
+
diff --git a/build/NuSpecs/tools/ReadmeUpgrade.txt b/build/NuSpecs/tools/ReadmeUpgrade.txt
index 5c46ee20bf..e85b22a902 100644
--- a/build/NuSpecs/tools/ReadmeUpgrade.txt
+++ b/build/NuSpecs/tools/ReadmeUpgrade.txt
@@ -8,13 +8,13 @@
----------------------------------------------------
-*** IMPORTANT NOTICE FOR 7.6 UPGRADES ***
+*** IMPORTANT NOTICE FOR 7.7 UPGRADES ***
Be sure to read the version specific upgrade information before proceeding:
-https://our.umbraco.org/documentation/Getting-Started/Setup/Upgrading/version-specific#version-7-6-0
+https://our.umbraco.org/documentation/Getting-Started/Setup/Upgrading/version-specific#version-7-7-0
-You will most likely need to make some changes to your web.config and you will need to be
-aware of the breaking changes listed there to see if these affect your installation.
+Depending on the version you are upgrading from, you may need to make some changes to your web.config
+and you will need to be aware of the breaking changes listed there to see if these affect your installation.
Don't forget to build!
diff --git a/build/NuSpecs/tools/Web.config.install.xdt b/build/NuSpecs/tools/Web.config.install.xdt
index e482eb5d6e..5ee5ed32c6 100644
--- a/build/NuSpecs/tools/Web.config.install.xdt
+++ b/build/NuSpecs/tools/Web.config.install.xdt
@@ -15,6 +15,7 @@
+
@@ -23,6 +24,7 @@
+
@@ -30,6 +32,7 @@
+
@@ -368,19 +371,19 @@
-
+
-
+
-
+
-
+
diff --git a/build/NuSpecs/tools/trees.config.install.xdt b/build/NuSpecs/tools/trees.config.install.xdt
index 7d41835fb1..65aa9c2b53 100644
--- a/build/NuSpecs/tools/trees.config.install.xdt
+++ b/build/NuSpecs/tools/trees.config.install.xdt
@@ -102,15 +102,16 @@
xdt:Transform="Remove" />
-
+
+
-
+
-
+ xdt:Transform="Remove" />
-
+
-
+
-
+
-
+
diff --git a/src/SolutionInfo.cs b/src/SolutionInfo.cs
index d8d55dc8ee..34696a7a59 100644
--- a/src/SolutionInfo.cs
+++ b/src/SolutionInfo.cs
@@ -11,5 +11,5 @@ using System.Resources;
[assembly: AssemblyVersion("1.0.*")]
-[assembly: AssemblyFileVersion("7.6.8")]
-[assembly: AssemblyInformationalVersion("7.6.8")]
\ No newline at end of file
+[assembly: AssemblyFileVersion("7.7.1")]
+[assembly: AssemblyInformationalVersion("7.7.1")]
\ No newline at end of file
diff --git a/src/Umbraco.Core/ActionsResolver.cs b/src/Umbraco.Core/ActionsResolver.cs
index 206182c6f2..ff2eaa8553 100644
--- a/src/Umbraco.Core/ActionsResolver.cs
+++ b/src/Umbraco.Core/ActionsResolver.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.Linq;
using System.Reflection;
using Umbraco.Core.Logging;
@@ -34,7 +35,31 @@ namespace Umbraco.Core
{
return Values;
}
- }
+ }
+
+ ///
+ /// This method will return a list of IAction's based on a string (letter) list. Each character in the list may represent
+ /// an IAction. This will associate any found IActions based on the Letter property of the IAction with the character being referenced.
+ ///
+ ///
+ /// returns a list of actions that have an associated letter found in the action string list
+ public IEnumerable FromActionSymbols(IEnumerable actions)
+ {
+ var allActions = Actions.ToArray();
+ return actions
+ .Select(c => allActions.FirstOrDefault(a => a.Letter.ToString(CultureInfo.InvariantCulture) == c))
+ .WhereNotNull()
+ .ToArray();
+ }
+
+ ///
+ /// Returns the string (letter) representation of the actions that make up the actions collection
+ ///
+ ///
+ public IEnumerable ToActionSymbols(IEnumerable actions)
+ {
+ return actions.Select(x => x.Letter.ToString(CultureInfo.InvariantCulture)).ToArray();
+ }
///
/// Gets an Action if it exists.
diff --git a/src/Umbraco.Core/Cache/CacheKeys.cs b/src/Umbraco.Core/Cache/CacheKeys.cs
index 81fe4dc2f2..c9991ba45a 100644
--- a/src/Umbraco.Core/Cache/CacheKeys.cs
+++ b/src/Umbraco.Core/Cache/CacheKeys.cs
@@ -55,8 +55,10 @@ namespace Umbraco.Core.Cache
[Obsolete("This is no longer used and will be removed from the codebase in the future")]
[EditorBrowsable(EditorBrowsableState.Never)]
public const string UserCacheKey = "UmbracoUser";
-
- public const string UserPermissionsCacheKey = "UmbracoUserPermissions";
+
+ [Obsolete("This is no longer used and will be removed from the codebase in the future")]
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public const string UserGroupPermissionsCacheKey = "UmbracoUserGroupPermissions";
[UmbracoWillObsolete("This cache key is only used for legacy business logic caching, remove in v8")]
public const string ContentTypeCacheKey = "UmbracoContentType";
diff --git a/src/Umbraco.Core/CodeAnnotations/ActionMetadataAttribute.cs b/src/Umbraco.Core/CodeAnnotations/ActionMetadataAttribute.cs
new file mode 100644
index 0000000000..697c6b7652
--- /dev/null
+++ b/src/Umbraco.Core/CodeAnnotations/ActionMetadataAttribute.cs
@@ -0,0 +1,34 @@
+using System;
+
+namespace Umbraco.Core.CodeAnnotations
+{
+ [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
+ internal class ActionMetadataAttribute : Attribute
+ {
+ public string Category { get; private set; }
+ public string Name { get; private set; }
+
+ ///
+ /// Constructor used to assign a Category, since no name is assigned it will try to be translated from the language files based on the action's alias
+ ///
+ ///
+ public ActionMetadataAttribute(string category)
+ {
+ if (string.IsNullOrWhiteSpace(category)) throw new ArgumentException("Value cannot be null or whitespace.", "category");
+ Category = category;
+ }
+
+ ///
+ /// Constructor used to assign an explicit name and category
+ ///
+ ///
+ ///
+ public ActionMetadataAttribute(string category, string name)
+ {
+ if (string.IsNullOrWhiteSpace(category)) throw new ArgumentException("Value cannot be null or whitespace.", "category");
+ if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value cannot be null or whitespace.", "name");
+ Category = category;
+ Name = name;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Core/Configuration/FileSystemProviderElement.cs b/src/Umbraco.Core/Configuration/FileSystemProviderElement.cs
index c0773e64e3..a1221bc0d4 100644
--- a/src/Umbraco.Core/Configuration/FileSystemProviderElement.cs
+++ b/src/Umbraco.Core/Configuration/FileSystemProviderElement.cs
@@ -6,7 +6,7 @@ using System.Text;
namespace Umbraco.Core.Configuration
{
- public class FileSystemProviderElement : ConfigurationElement
+ public class FileSystemProviderElement : ConfigurationElement, IFileSystemProviderElement
{
private const string ALIAS_KEY = "alias";
private const string TYPE_KEY = "type";
@@ -38,5 +38,30 @@ namespace Umbraco.Core.Configuration
return ((KeyValueConfigurationCollection)(base[PARAMETERS_KEY]));
}
}
+
+ string IFileSystemProviderElement.Alias
+ {
+ get { return Alias; }
+ }
+
+ string IFileSystemProviderElement.Type
+ {
+ get { return Type; }
+ }
+
+ private IDictionary _params;
+ IDictionary IFileSystemProviderElement.Parameters
+ {
+ get
+ {
+ if (_params != null) return _params;
+ _params = new Dictionary();
+ foreach (KeyValueConfigurationElement element in Parameters)
+ {
+ _params.Add(element.Key, element.Value);
+ }
+ return _params;
+ }
+ }
}
}
diff --git a/src/Umbraco.Core/Configuration/FileSystemProviderElementCollection.cs b/src/Umbraco.Core/Configuration/FileSystemProviderElementCollection.cs
index 0604560939..26957dd68e 100644
--- a/src/Umbraco.Core/Configuration/FileSystemProviderElementCollection.cs
+++ b/src/Umbraco.Core/Configuration/FileSystemProviderElementCollection.cs
@@ -7,7 +7,7 @@ using System.Text;
namespace Umbraco.Core.Configuration
{
[ConfigurationCollection(typeof(FileSystemProviderElement), AddItemName = "Provider")]
- public class FileSystemProviderElementCollection : ConfigurationElementCollection
+ public class FileSystemProviderElementCollection : ConfigurationElementCollection, IEnumerable
{
protected override ConfigurationElement CreateNewElement()
{
@@ -19,12 +19,25 @@ namespace Umbraco.Core.Configuration
return ((FileSystemProviderElement)(element)).Alias;
}
- new public FileSystemProviderElement this[string key]
+ public new FileSystemProviderElement this[string key]
{
get
{
return (FileSystemProviderElement)BaseGet(key);
}
}
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ for (var i = 0; i < Count; i++)
+ {
+ yield return BaseGet(i) as IFileSystemProviderElement;
+ }
+ }
+
+ System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
}
}
diff --git a/src/Umbraco.Core/Configuration/FileSystemProvidersSection.cs b/src/Umbraco.Core/Configuration/FileSystemProvidersSection.cs
index 0da1cb802a..fa32fe0885 100644
--- a/src/Umbraco.Core/Configuration/FileSystemProvidersSection.cs
+++ b/src/Umbraco.Core/Configuration/FileSystemProvidersSection.cs
@@ -6,7 +6,7 @@ using System.Text;
namespace Umbraco.Core.Configuration
{
- public class FileSystemProvidersSection : ConfigurationSection
+ public class FileSystemProvidersSection : ConfigurationSection, IFileSystemProvidersSection
{
[ConfigurationProperty("", IsDefaultCollection = true, IsRequired = true)]
@@ -14,5 +14,17 @@ namespace Umbraco.Core.Configuration
{
get { return ((FileSystemProviderElementCollection)(base[""])); }
}
+
+ private IDictionary _providers;
+
+ IDictionary IFileSystemProvidersSection.Providers
+ {
+ get
+ {
+ if (_providers != null) return _providers;
+ _providers = Providers.ToDictionary(x => x.Alias, x => x);
+ return _providers;
+ }
+ }
}
}
diff --git a/src/Umbraco.Core/Configuration/GlobalSettings.cs b/src/Umbraco.Core/Configuration/GlobalSettings.cs
index acbf0065c0..02f3322ec9 100644
--- a/src/Umbraco.Core/Configuration/GlobalSettings.cs
+++ b/src/Umbraco.Core/Configuration/GlobalSettings.cs
@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Configuration;
using System.Linq;
+using System.Net.Configuration;
using System.Web;
using System.Web.Configuration;
using System.Web.Hosting;
@@ -42,7 +43,6 @@ namespace Umbraco.Core.Configuration
//ensure the built on (non-changeable) reserved paths are there at all times
private const string StaticReservedPaths = "~/app_plugins/,~/install/,";
private const string StaticReservedUrls = "~/config/splashes/booting.aspx,~/config/splashes/noNodes.aspx,~/VSEnterpriseHelper.axd,";
-
#endregion
///
@@ -53,6 +53,7 @@ namespace Umbraco.Core.Configuration
_reservedUrlsCache = null;
_reservedPaths = null;
_reservedUrls = null;
+ HasSmtpServer = null;
}
///
@@ -64,7 +65,26 @@ namespace Umbraco.Core.Configuration
ResetInternal();
}
- ///
+ public static bool HasSmtpServerConfigured(string appPath)
+ {
+ if (HasSmtpServer.HasValue) return HasSmtpServer.Value;
+
+ var config = WebConfigurationManager.OpenWebConfiguration(appPath);
+ var settings = (MailSettingsSectionGroup)config.GetSectionGroup("system.net/mailSettings");
+ if (settings == null || settings.Smtp == null) return false;
+ if (settings.Smtp.SpecifiedPickupDirectory != null && string.IsNullOrEmpty(settings.Smtp.SpecifiedPickupDirectory.PickupDirectoryLocation) == false)
+ return true;
+ if (settings.Smtp.Network != null && string.IsNullOrEmpty(settings.Smtp.Network.Host) == false)
+ return true;
+ return false;
+ }
+
+ ///
+ /// For testing only
+ ///
+ internal static bool? HasSmtpServer { get; set; }
+
+ ///
/// Gets the reserved urls from web.config.
///
/// The reserved urls.
diff --git a/src/Umbraco.Core/Configuration/Grid/GridEditorsConfig.cs b/src/Umbraco.Core/Configuration/Grid/GridEditorsConfig.cs
index 389c620637..3026064dec 100644
--- a/src/Umbraco.Core/Configuration/Grid/GridEditorsConfig.cs
+++ b/src/Umbraco.Core/Configuration/Grid/GridEditorsConfig.cs
@@ -9,7 +9,7 @@ using Umbraco.Core.PropertyEditors;
namespace Umbraco.Core.Configuration.Grid
{
- class GridEditorsConfig : IGridEditorsConfig
+ internal class GridEditorsConfig : IGridEditorsConfig
{
private readonly ILogger _logger;
private readonly IRuntimeCacheProvider _runtimeCache;
diff --git a/src/Umbraco.Core/Configuration/HealthChecks/DisabledHealthCheckElement.cs b/src/Umbraco.Core/Configuration/HealthChecks/DisabledHealthCheckElement.cs
new file mode 100644
index 0000000000..01392da614
--- /dev/null
+++ b/src/Umbraco.Core/Configuration/HealthChecks/DisabledHealthCheckElement.cs
@@ -0,0 +1,42 @@
+using System;
+using System.Collections.Generic;
+using System.Configuration;
+using System.Linq;
+using System.Text;
+
+namespace Umbraco.Core.Configuration.HealthChecks
+{
+ public class DisabledHealthCheckElement : ConfigurationElement, IDisabledHealthCheck
+ {
+ private const string IdKey = "id";
+ private const string DisabledOnKey = "disabledOn";
+ private const string DisabledByKey = "disabledBy";
+
+ [ConfigurationProperty(IdKey, IsKey = true, IsRequired = true)]
+ public Guid Id
+ {
+ get
+ {
+ return ((Guid)(base[IdKey]));
+ }
+ }
+
+ [ConfigurationProperty(DisabledOnKey, IsKey = false, IsRequired = false)]
+ public DateTime DisabledOn
+ {
+ get
+ {
+ return ((DateTime)(base[DisabledOnKey]));
+ }
+ }
+
+ [ConfigurationProperty(DisabledByKey, IsKey = false, IsRequired = false)]
+ public int DisabledBy
+ {
+ get
+ {
+ return ((int)(base[DisabledByKey]));
+ }
+ }
+ }
+}
diff --git a/src/Umbraco.Core/Configuration/HealthChecks/DisabledHealthChecksElementCollection.cs b/src/Umbraco.Core/Configuration/HealthChecks/DisabledHealthChecksElementCollection.cs
new file mode 100644
index 0000000000..cf57a3a91a
--- /dev/null
+++ b/src/Umbraco.Core/Configuration/HealthChecks/DisabledHealthChecksElementCollection.cs
@@ -0,0 +1,40 @@
+using System.Collections.Generic;
+using System.Configuration;
+
+namespace Umbraco.Core.Configuration.HealthChecks
+{
+ [ConfigurationCollection(typeof(DisabledHealthCheckElement), AddItemName = "check")]
+ public class DisabledHealthChecksElementCollection : ConfigurationElementCollection, IEnumerable
+ {
+ protected override ConfigurationElement CreateNewElement()
+ {
+ return new DisabledHealthCheckElement();
+ }
+
+ protected override object GetElementKey(ConfigurationElement element)
+ {
+ return ((DisabledHealthCheckElement)(element)).Id;
+ }
+
+ public new DisabledHealthCheckElement this[string key]
+ {
+ get
+ {
+ return (DisabledHealthCheckElement)BaseGet(key);
+ }
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ for (var i = 0; i < Count; i++)
+ {
+ yield return BaseGet(i) as DisabledHealthCheckElement;
+ }
+ }
+
+ System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+ }
+}
diff --git a/src/Umbraco.Core/Configuration/HealthChecks/HealthCheckNotificationSettingsElement.cs b/src/Umbraco.Core/Configuration/HealthChecks/HealthCheckNotificationSettingsElement.cs
new file mode 100644
index 0000000000..1ccf3e357b
--- /dev/null
+++ b/src/Umbraco.Core/Configuration/HealthChecks/HealthCheckNotificationSettingsElement.cs
@@ -0,0 +1,85 @@
+using System;
+using System.Collections.Generic;
+using System.Configuration;
+
+namespace Umbraco.Core.Configuration.HealthChecks
+{
+ public class HealthCheckNotificationSettingsElement : ConfigurationElement, IHealthCheckNotificationSettings
+ {
+ private const string EnabledKey = "enabled";
+ private const string FirstRunTimeKey = "firstRunTime";
+ private const string PeriodKey = "periodInHours";
+ private const string NotificationMethodsKey = "notificationMethods";
+ private const string DisabledChecksKey = "disabledChecks";
+
+ [ConfigurationProperty(EnabledKey, IsRequired = true)]
+ public bool Enabled
+ {
+ get
+ {
+ return (bool)base[EnabledKey];
+ }
+ }
+
+ [ConfigurationProperty(FirstRunTimeKey, IsRequired = false)]
+ public string FirstRunTime
+ {
+ get
+ {
+ return (string)base[FirstRunTimeKey];
+ }
+ }
+
+ [ConfigurationProperty(PeriodKey, IsRequired = true)]
+ public int PeriodInHours
+ {
+ get
+ {
+ return (int)base[PeriodKey];
+ }
+ }
+
+ [ConfigurationProperty(NotificationMethodsKey, IsDefaultCollection = true, IsRequired = false)]
+ public NotificationMethodsElementCollection NotificationMethods
+ {
+ get
+ {
+ return (NotificationMethodsElementCollection)base[NotificationMethodsKey];
+ }
+ }
+
+ [ConfigurationProperty(DisabledChecksKey, IsDefaultCollection = false, IsRequired = false)]
+ public DisabledHealthChecksElementCollection DisabledChecks
+ {
+ get
+ {
+ return (DisabledHealthChecksElementCollection)base[DisabledChecksKey];
+ }
+ }
+
+ bool IHealthCheckNotificationSettings.Enabled
+ {
+ get { return Enabled; }
+ }
+
+ string IHealthCheckNotificationSettings.FirstRunTime
+ {
+ get { return FirstRunTime; }
+ }
+
+ int IHealthCheckNotificationSettings.PeriodInHours
+ {
+ get { return PeriodInHours; }
+ }
+
+ IReadOnlyDictionary IHealthCheckNotificationSettings.NotificationMethods
+ {
+ get { return NotificationMethods; }
+ }
+
+ IEnumerable IHealthCheckNotificationSettings.DisabledChecks
+ {
+ get { return DisabledChecks; }
+ }
+ }
+}
diff --git a/src/Umbraco.Core/Configuration/HealthChecks/HealthCheckNotificationVerbosity.cs b/src/Umbraco.Core/Configuration/HealthChecks/HealthCheckNotificationVerbosity.cs
new file mode 100644
index 0000000000..6556c19c32
--- /dev/null
+++ b/src/Umbraco.Core/Configuration/HealthChecks/HealthCheckNotificationVerbosity.cs
@@ -0,0 +1,8 @@
+namespace Umbraco.Core.Configuration.HealthChecks
+{
+ public enum HealthCheckNotificationVerbosity
+ {
+ Summary,
+ Detailed
+ }
+}
diff --git a/src/Umbraco.Core/Configuration/HealthChecks/HealthChecksSection.cs b/src/Umbraco.Core/Configuration/HealthChecks/HealthChecksSection.cs
new file mode 100644
index 0000000000..90a7d8c567
--- /dev/null
+++ b/src/Umbraco.Core/Configuration/HealthChecks/HealthChecksSection.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Collections.Generic;
+using System.Configuration;
+
+namespace Umbraco.Core.Configuration.HealthChecks
+{
+ public class HealthChecksSection : ConfigurationSection, IHealthChecks
+ {
+ private const string DisabledChecksKey = "disabledChecks";
+ private const string NotificationSettingsKey = "notificationSettings";
+
+ [ConfigurationProperty(DisabledChecksKey)]
+ public DisabledHealthChecksElementCollection DisabledChecks
+ {
+ get { return ((DisabledHealthChecksElementCollection)(base[DisabledChecksKey])); }
+ }
+
+ [ConfigurationProperty(NotificationSettingsKey, IsRequired = true)]
+ public HealthCheckNotificationSettingsElement NotificationSettings
+ {
+ get { return ((HealthCheckNotificationSettingsElement)(base[NotificationSettingsKey])); }
+ }
+
+ IEnumerable IHealthChecks.DisabledChecks
+ {
+ get { return DisabledChecks; }
+ }
+
+ IHealthCheckNotificationSettings IHealthChecks.NotificationSettings
+ {
+ get { return NotificationSettings; }
+ }
+ }
+}
diff --git a/src/Umbraco.Core/Configuration/HealthChecks/IDisabledHealthCheck.cs b/src/Umbraco.Core/Configuration/HealthChecks/IDisabledHealthCheck.cs
new file mode 100644
index 0000000000..4ea63048f8
--- /dev/null
+++ b/src/Umbraco.Core/Configuration/HealthChecks/IDisabledHealthCheck.cs
@@ -0,0 +1,11 @@
+using System;
+
+namespace Umbraco.Core.Configuration.HealthChecks
+{
+ public interface IDisabledHealthCheck
+ {
+ Guid Id { get; }
+ DateTime DisabledOn { get; }
+ int DisabledBy { get; }
+ }
+}
diff --git a/src/Umbraco.Core/Configuration/HealthChecks/IHealthCheckNotificationSettings.cs b/src/Umbraco.Core/Configuration/HealthChecks/IHealthCheckNotificationSettings.cs
new file mode 100644
index 0000000000..4564e87ed6
--- /dev/null
+++ b/src/Umbraco.Core/Configuration/HealthChecks/IHealthCheckNotificationSettings.cs
@@ -0,0 +1,13 @@
+using System.Collections.Generic;
+
+namespace Umbraco.Core.Configuration.HealthChecks
+{
+ public interface IHealthCheckNotificationSettings
+ {
+ bool Enabled { get; }
+ string FirstRunTime { get; }
+ int PeriodInHours { get; }
+ IReadOnlyDictionary NotificationMethods { get; }
+ IEnumerable DisabledChecks { get; }
+ }
+}
diff --git a/src/Umbraco.Core/Configuration/HealthChecks/IHealthChecks.cs b/src/Umbraco.Core/Configuration/HealthChecks/IHealthChecks.cs
new file mode 100644
index 0000000000..7680836074
--- /dev/null
+++ b/src/Umbraco.Core/Configuration/HealthChecks/IHealthChecks.cs
@@ -0,0 +1,10 @@
+using System.Collections.Generic;
+
+namespace Umbraco.Core.Configuration.HealthChecks
+{
+ public interface IHealthChecks
+ {
+ IEnumerable DisabledChecks { get; }
+ IHealthCheckNotificationSettings NotificationSettings { get; }
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Core/Configuration/HealthChecks/INotificationMethod.cs b/src/Umbraco.Core/Configuration/HealthChecks/INotificationMethod.cs
new file mode 100644
index 0000000000..84bf55e160
--- /dev/null
+++ b/src/Umbraco.Core/Configuration/HealthChecks/INotificationMethod.cs
@@ -0,0 +1,13 @@
+using System.Collections.Generic;
+
+namespace Umbraco.Core.Configuration.HealthChecks
+{
+ public interface INotificationMethod
+ {
+ string Alias { get; }
+ bool Enabled { get; }
+ HealthCheckNotificationVerbosity Verbosity { get; }
+ bool FailureOnly { get; }
+ IReadOnlyDictionary Settings { get; }
+ }
+}
diff --git a/src/Umbraco.Core/Configuration/HealthChecks/INotificationMethodSettings.cs b/src/Umbraco.Core/Configuration/HealthChecks/INotificationMethodSettings.cs
new file mode 100644
index 0000000000..89d4799484
--- /dev/null
+++ b/src/Umbraco.Core/Configuration/HealthChecks/INotificationMethodSettings.cs
@@ -0,0 +1,8 @@
+namespace Umbraco.Core.Configuration.HealthChecks
+{
+ public interface INotificationMethodSettings
+ {
+ string Key { get; }
+ string Value { get; }
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Core/Configuration/HealthChecks/NotificationMethodElement.cs b/src/Umbraco.Core/Configuration/HealthChecks/NotificationMethodElement.cs
new file mode 100644
index 0000000000..2646c97475
--- /dev/null
+++ b/src/Umbraco.Core/Configuration/HealthChecks/NotificationMethodElement.cs
@@ -0,0 +1,85 @@
+using System;
+using System.Collections.Generic;
+using System.Configuration;
+
+namespace Umbraco.Core.Configuration.HealthChecks
+{
+ public class NotificationMethodElement : ConfigurationElement, INotificationMethod
+ {
+ private const string AliasKey = "alias";
+ private const string EnabledKey = "enabled";
+ private const string VerbosityKey = "verbosity";
+ private const string FailureonlyKey = "failureOnly";
+ private const string SettingsKey = "settings";
+
+ [ConfigurationProperty(AliasKey, IsKey = true, IsRequired = true)]
+ public string Alias
+ {
+ get
+ {
+ return (string)base[AliasKey];
+ }
+ }
+
+ [ConfigurationProperty(EnabledKey, IsKey = true, IsRequired = true)]
+ public bool Enabled
+ {
+ get
+ {
+ return (bool)base[EnabledKey];
+ }
+ }
+
+ [ConfigurationProperty(VerbosityKey, IsRequired = true)]
+ public HealthCheckNotificationVerbosity Verbosity
+ {
+ get
+ {
+ return (HealthCheckNotificationVerbosity)base[VerbosityKey];
+ }
+ }
+
+ [ConfigurationProperty(FailureonlyKey, IsRequired = false)]
+ public bool FailureOnly
+ {
+ get
+ {
+ return (bool)base[FailureonlyKey];
+ }
+ }
+
+ [ConfigurationProperty(SettingsKey, IsDefaultCollection = true, IsRequired = false)]
+ public NotificationMethodSettingsElementCollection Settings
+ {
+ get
+ {
+ return (NotificationMethodSettingsElementCollection)base[SettingsKey];
+ }
+ }
+
+ string INotificationMethod.Alias
+ {
+ get { return Alias; }
+ }
+
+ bool INotificationMethod.Enabled
+ {
+ get { return Enabled; }
+ }
+
+ HealthCheckNotificationVerbosity INotificationMethod.Verbosity
+ {
+ get { return Verbosity; }
+ }
+
+ bool INotificationMethod.FailureOnly
+ {
+ get { return FailureOnly; }
+ }
+
+ IReadOnlyDictionary INotificationMethod.Settings
+ {
+ get { return Settings; }
+ }
+ }
+}
diff --git a/src/Umbraco.Core/Configuration/HealthChecks/NotificationMethodSettingsElement.cs b/src/Umbraco.Core/Configuration/HealthChecks/NotificationMethodSettingsElement.cs
new file mode 100644
index 0000000000..a861291427
--- /dev/null
+++ b/src/Umbraco.Core/Configuration/HealthChecks/NotificationMethodSettingsElement.cs
@@ -0,0 +1,28 @@
+using System.Configuration;
+
+namespace Umbraco.Core.Configuration.HealthChecks
+{
+ public class NotificationMethodSettingsElement : ConfigurationElement, INotificationMethodSettings
+ {
+ private const string KeyKey = "key";
+ private const string ValueKey = "value";
+
+ [ConfigurationProperty(KeyKey, IsKey = true, IsRequired = true)]
+ public string Key
+ {
+ get
+ {
+ return (string)base[KeyKey];
+ }
+ }
+
+ [ConfigurationProperty(ValueKey, IsRequired = true)]
+ public string Value
+ {
+ get
+ {
+ return (string)base[ValueKey];
+ }
+ }
+ }
+}
diff --git a/src/Umbraco.Core/Configuration/HealthChecks/NotificationMethodSettingsElementCollection.cs b/src/Umbraco.Core/Configuration/HealthChecks/NotificationMethodSettingsElementCollection.cs
new file mode 100644
index 0000000000..174727f722
--- /dev/null
+++ b/src/Umbraco.Core/Configuration/HealthChecks/NotificationMethodSettingsElementCollection.cs
@@ -0,0 +1,80 @@
+using System;
+using System.Collections.Generic;
+using System.Configuration;
+using System.Linq;
+
+namespace Umbraco.Core.Configuration.HealthChecks
+{
+ [ConfigurationCollection(typeof(NotificationMethodSettingsElement), AddItemName = "add")]
+ public class NotificationMethodSettingsElementCollection : ConfigurationElementCollection, IEnumerable, IReadOnlyDictionary
+ {
+ protected override ConfigurationElement CreateNewElement()
+ {
+ return new NotificationMethodSettingsElement();
+ }
+
+ protected override object GetElementKey(ConfigurationElement element)
+ {
+ return ((NotificationMethodSettingsElement)(element)).Key;
+ }
+
+ IEnumerator> IEnumerable>.GetEnumerator()
+ {
+ for (var i = 0; i < Count; i++)
+ {
+ var val = (NotificationMethodSettingsElement)BaseGet(i);
+ var key = (string)BaseGetKey(i);
+ yield return new KeyValuePair(key, val);
+ }
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ for (var i = 0; i < Count; i++)
+ {
+ yield return (NotificationMethodSettingsElement)BaseGet(i);
+ }
+ }
+
+ bool IReadOnlyDictionary.ContainsKey(string key)
+ {
+ return ((IReadOnlyDictionary)this).Keys.Any(x => x == key);
+ }
+
+ bool IReadOnlyDictionary.TryGetValue(string key, out INotificationMethodSettings value)
+ {
+ try
+ {
+ var val = (NotificationMethodSettingsElement)BaseGet(key);
+ value = val;
+ return true;
+ }
+ catch (Exception)
+ {
+ value = null;
+ return false;
+ }
+ }
+
+ INotificationMethodSettings IReadOnlyDictionary.this[string key]
+ {
+ get { return (NotificationMethodSettingsElement)BaseGet(key); }
+ }
+
+ IEnumerable IReadOnlyDictionary.Keys
+ {
+ get { return BaseGetAllKeys().Cast(); }
+ }
+
+ IEnumerable IReadOnlyDictionary.Values
+ {
+ get
+ {
+ for (var i = 0; i < Count; i++)
+ {
+ yield return (NotificationMethodSettingsElement)BaseGet(i);
+ }
+ }
+ }
+ }
+}
diff --git a/src/Umbraco.Core/Configuration/HealthChecks/NotificationMethodsElementCollection.cs b/src/Umbraco.Core/Configuration/HealthChecks/NotificationMethodsElementCollection.cs
new file mode 100644
index 0000000000..9eebc47309
--- /dev/null
+++ b/src/Umbraco.Core/Configuration/HealthChecks/NotificationMethodsElementCollection.cs
@@ -0,0 +1,80 @@
+using System;
+using System.Collections.Generic;
+using System.Configuration;
+using System.Linq;
+
+namespace Umbraco.Core.Configuration.HealthChecks
+{
+ [ConfigurationCollection(typeof(NotificationMethodElement), AddItemName = "notificationMethod")]
+ public class NotificationMethodsElementCollection : ConfigurationElementCollection, IEnumerable, IReadOnlyDictionary
+ {
+ protected override ConfigurationElement CreateNewElement()
+ {
+ return new NotificationMethodElement();
+ }
+
+ protected override object GetElementKey(ConfigurationElement element)
+ {
+ return ((NotificationMethodElement)(element)).Alias;
+ }
+
+ IEnumerator> IEnumerable>.GetEnumerator()
+ {
+ for (var i = 0; i < Count; i++)
+ {
+ var val = (NotificationMethodElement)BaseGet(i);
+ var key = (string)BaseGetKey(i);
+ yield return new KeyValuePair(key, val);
+ }
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ for (var i = 0; i < Count; i++)
+ {
+ yield return (NotificationMethodElement)BaseGet(i);
+ }
+ }
+
+ bool IReadOnlyDictionary.ContainsKey(string key)
+ {
+ return ((IReadOnlyDictionary) this).Keys.Any(x => x == key);
+ }
+
+ bool IReadOnlyDictionary.TryGetValue(string key, out INotificationMethod value)
+ {
+ try
+ {
+ var val = (NotificationMethodElement)BaseGet(key);
+ value = val;
+ return true;
+ }
+ catch (Exception)
+ {
+ value = null;
+ return false;
+ }
+ }
+
+ INotificationMethod IReadOnlyDictionary.this[string key]
+ {
+ get { return (NotificationMethodElement)BaseGet(key); }
+ }
+
+ IEnumerable IReadOnlyDictionary.Keys
+ {
+ get { return BaseGetAllKeys().Cast(); }
+ }
+
+ IEnumerable IReadOnlyDictionary.Values
+ {
+ get
+ {
+ for (var i = 0; i < Count; i++)
+ {
+ yield return (NotificationMethodElement)BaseGet(i);
+ }
+ }
+ }
+ }
+}
diff --git a/src/Umbraco.Core/Configuration/IFileSystemProviderElement.cs b/src/Umbraco.Core/Configuration/IFileSystemProviderElement.cs
new file mode 100644
index 0000000000..9427f42b68
--- /dev/null
+++ b/src/Umbraco.Core/Configuration/IFileSystemProviderElement.cs
@@ -0,0 +1,11 @@
+using System.Collections.Generic;
+
+namespace Umbraco.Core.Configuration
+{
+ public interface IFileSystemProviderElement
+ {
+ string Alias { get; }
+ string Type { get; }
+ IDictionary Parameters { get; }
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Core/Configuration/IFileSystemProvidersSection.cs b/src/Umbraco.Core/Configuration/IFileSystemProvidersSection.cs
new file mode 100644
index 0000000000..108d5a87de
--- /dev/null
+++ b/src/Umbraco.Core/Configuration/IFileSystemProvidersSection.cs
@@ -0,0 +1,9 @@
+using System.Collections.Generic;
+
+namespace Umbraco.Core.Configuration
+{
+ public interface IFileSystemProvidersSection
+ {
+ IDictionary Providers { get; }
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Core/Configuration/UmbracoConfig.cs b/src/Umbraco.Core/Configuration/UmbracoConfig.cs
index 8881c5fb2e..82d90073e8 100644
--- a/src/Umbraco.Core/Configuration/UmbracoConfig.cs
+++ b/src/Umbraco.Core/Configuration/UmbracoConfig.cs
@@ -9,6 +9,7 @@ using Umbraco.Core.Cache;
using Umbraco.Core.Configuration.BaseRest;
using Umbraco.Core.Configuration.Dashboard;
using Umbraco.Core.Configuration.Grid;
+using Umbraco.Core.Configuration.HealthChecks;
using Umbraco.Core.Configuration.UmbracoSettings;
using Umbraco.Core.Logging;
@@ -52,6 +53,12 @@ namespace Umbraco.Core.Configuration
var dashboardConfig = ConfigurationManager.GetSection("umbracoConfiguration/dashBoard") as IDashboardSection;
SetDashboardSettings(dashboardConfig);
}
+
+ if (_healthChecks == null)
+ {
+ var healthCheckConfig = ConfigurationManager.GetSection("umbracoConfiguration/HealthChecks") as IHealthChecks;
+ SetHealthCheckSettings(healthCheckConfig);
+ }
}
///
@@ -60,18 +67,36 @@ namespace Umbraco.Core.Configuration
///
///
///
- public UmbracoConfig(IUmbracoSettingsSection umbracoSettings, IBaseRestSection baseRestSettings, IDashboardSection dashboardSettings)
+ ///
+ public UmbracoConfig(IUmbracoSettingsSection umbracoSettings, IBaseRestSection baseRestSettings, IDashboardSection dashboardSettings, IHealthChecks healthChecks)
{
+ SetHealthCheckSettings(healthChecks);
SetUmbracoSettings(umbracoSettings);
SetBaseRestExtensions(baseRestSettings);
SetDashboardSettings(dashboardSettings);
}
+ private IHealthChecks _healthChecks;
private IDashboardSection _dashboardSection;
private IUmbracoSettingsSection _umbracoSettings;
private IBaseRestSection _baseRestExtensions;
private IGridConfig _gridConfig;
+ ///
+ /// Gets the IHealthCheck config
+ ///
+ public IHealthChecks HealthCheck()
+ {
+ if (_healthChecks == null)
+ {
+ var ex = new ConfigurationErrorsException("Could not load the " + typeof(IHealthChecks) + " from config file, ensure the web.config and healthchecks.config files are formatted correctly");
+ LogHelper.Error("Config error", ex);
+ throw ex;
+ }
+
+ return _healthChecks;
+ }
+
///
/// Gets the IDashboardSection
///
@@ -86,14 +111,23 @@ namespace Umbraco.Core.Configuration
return _dashboardSection;
}
+
+ ///
+ /// Only for testing
+ ///
+ ///
+ public void SetDashboardSettings(IDashboardSection value)
+ {
+ _dashboardSection = value;
+ }
///
/// Only for testing
///
///
- internal void SetDashboardSettings(IDashboardSection value)
+ public void SetHealthCheckSettings(IHealthChecks value)
{
- _dashboardSection = value;
+ _healthChecks = value;
}
///
diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/HelpElement.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/HelpElement.cs
index cc4d459359..eb1e452100 100644
--- a/src/Umbraco.Core/Configuration/UmbracoSettings/HelpElement.cs
+++ b/src/Umbraco.Core/Configuration/UmbracoSettings/HelpElement.cs
@@ -1,8 +1,12 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
using System.Configuration;
namespace Umbraco.Core.Configuration.UmbracoSettings
{
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ [Obsolete("This is no longer used and will be removed in future versions")]
internal class HelpElement : ConfigurationElement, IHelpSection
{
[ConfigurationProperty("defaultUrl", DefaultValue = "http://our.umbraco.org/wiki/umbraco-help/{0}/{1}")]
diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/IHelpSection.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/IHelpSection.cs
index 61be2dfaf2..dc9b6af437 100644
--- a/src/Umbraco.Core/Configuration/UmbracoSettings/IHelpSection.cs
+++ b/src/Umbraco.Core/Configuration/UmbracoSettings/IHelpSection.cs
@@ -1,7 +1,11 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
namespace Umbraco.Core.Configuration.UmbracoSettings
-{
+{
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ [Obsolete("This is no longer used and will be removed in future versions")]
public interface IHelpSection : IUmbracoConfigurationSection
{
string DefaultUrl { get; }
diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/ILink.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/ILink.cs
index d2afec55f3..689878cf0d 100644
--- a/src/Umbraco.Core/Configuration/UmbracoSettings/ILink.cs
+++ b/src/Umbraco.Core/Configuration/UmbracoSettings/ILink.cs
@@ -1,5 +1,10 @@
-namespace Umbraco.Core.Configuration.UmbracoSettings
+using System;
+using System.ComponentModel;
+
+namespace Umbraco.Core.Configuration.UmbracoSettings
{
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ [Obsolete("This is no longer used and will be removed in future versions")]
public interface ILink
{
string Application { get; }
diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/ISecuritySection.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/ISecuritySection.cs
index c44c0cf0df..31ee4611d0 100644
--- a/src/Umbraco.Core/Configuration/UmbracoSettings/ISecuritySection.cs
+++ b/src/Umbraco.Core/Configuration/UmbracoSettings/ISecuritySection.cs
@@ -5,11 +5,23 @@
bool KeepUserLoggedIn { get; }
bool HideDisabledUsersInBackoffice { get; }
-
+
+ ///
+ /// Used to enable/disable the forgot password functionality on the back office login screen
+ ///
bool AllowPasswordReset { get; }
string AuthCookieName { get; }
- string AuthCookieDomain { get; }
+ string AuthCookieDomain { get; }
+
+ ///
+ /// A boolean indicating that by default the email address will be the username
+ ///
+ ///
+ /// Even if this is true and the username is different from the email in the database, the username field will still be shown.
+ /// When this is false, the username and email fields will be shown in the user section.
+ ///
+ bool UsernameIsEmail { get; }
}
}
\ No newline at end of file
diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/IUmbracoSettingsSection.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/IUmbracoSettingsSection.cs
index 899de7d1f9..cd4db216ab 100644
--- a/src/Umbraco.Core/Configuration/UmbracoSettings/IUmbracoSettingsSection.cs
+++ b/src/Umbraco.Core/Configuration/UmbracoSettings/IUmbracoSettingsSection.cs
@@ -1,4 +1,7 @@
-namespace Umbraco.Core.Configuration.UmbracoSettings
+using System;
+using System.ComponentModel;
+
+namespace Umbraco.Core.Configuration.UmbracoSettings
{
public interface IUmbracoSettingsSection : IUmbracoConfigurationSection
{
@@ -24,6 +27,8 @@
IProvidersSection Providers { get; }
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ [Obsolete("This is no longer used and will be removed in future versions")]
IHelpSection Help { get; }
IWebRoutingSection WebRouting { get; }
diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/LinkElement.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/LinkElement.cs
index 31b4aa3e93..ad1655dfb0 100644
--- a/src/Umbraco.Core/Configuration/UmbracoSettings/LinkElement.cs
+++ b/src/Umbraco.Core/Configuration/UmbracoSettings/LinkElement.cs
@@ -1,7 +1,11 @@
-using System.Configuration;
+using System;
+using System.ComponentModel;
+using System.Configuration;
namespace Umbraco.Core.Configuration.UmbracoSettings
{
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ [Obsolete("This is no longer used and will be removed in future versions")]
internal class LinkElement : ConfigurationElement, ILink
{
[ConfigurationProperty("application")]
diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/LinksCollection.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/LinksCollection.cs
index 5c317790cb..485c8e4bbd 100644
--- a/src/Umbraco.Core/Configuration/UmbracoSettings/LinksCollection.cs
+++ b/src/Umbraco.Core/Configuration/UmbracoSettings/LinksCollection.cs
@@ -1,8 +1,12 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
using System.Configuration;
namespace Umbraco.Core.Configuration.UmbracoSettings
{
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ [Obsolete("This is no longer used and will be removed in future versions")]
internal class LinksCollection : ConfigurationElementCollection, IEnumerable
{
protected override ConfigurationElement CreateNewElement()
diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/SecurityElement.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/SecurityElement.cs
index ddb168ddbd..dc2ba7e983 100644
--- a/src/Umbraco.Core/Configuration/UmbracoSettings/SecurityElement.cs
+++ b/src/Umbraco.Core/Configuration/UmbracoSettings/SecurityElement.cs
@@ -16,12 +16,28 @@ namespace Umbraco.Core.Configuration.UmbracoSettings
get { return GetOptionalTextElement("hideDisabledUsersInBackoffice", false); }
}
+ ///
+ /// Used to enable/disable the forgot password functionality on the back office login screen
+ ///
[ConfigurationProperty("allowPasswordReset")]
internal InnerTextConfigurationElement AllowPasswordReset
{
get { return GetOptionalTextElement("allowPasswordReset", true); }
}
+ ///
+ /// A boolean indicating that by default the email address will be the username
+ ///
+ ///
+ /// Even if this is true and the username is different from the email in the database, the username field will still be shown.
+ /// When this is false, the username and email fields will be shown in the user section.
+ ///
+ [ConfigurationProperty("usernameIsEmail")]
+ internal InnerTextConfigurationElement UsernameIsEmail
+ {
+ get { return GetOptionalTextElement("usernameIsEmail", true); }
+ }
+
[ConfigurationProperty("authCookieName")]
internal InnerTextConfigurationElement AuthCookieName
{
@@ -44,11 +60,26 @@ namespace Umbraco.Core.Configuration.UmbracoSettings
get { return HideDisabledUsersInBackoffice; }
}
+ ///
+ /// Used to enable/disable the forgot password functionality on the back office login screen
+ ///
bool ISecuritySection.AllowPasswordReset
{
get { return AllowPasswordReset; }
}
+ ///
+ /// A boolean indicating that by default the email address will be the username
+ ///
+ ///
+ /// Even if this is true and the username is different from the email in the database, the username field will still be shown.
+ /// When this is false, the username and email fields will be shown in the user section.
+ ///
+ bool ISecuritySection.UsernameIsEmail
+ {
+ get { return UsernameIsEmail; }
+ }
+
string ISecuritySection.AuthCookieName
{
get { return AuthCookieName; }
diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/UmbracoSettingsSection.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/UmbracoSettingsSection.cs
index dd6fed5cd5..2c4751f580 100644
--- a/src/Umbraco.Core/Configuration/UmbracoSettings/UmbracoSettingsSection.cs
+++ b/src/Umbraco.Core/Configuration/UmbracoSettings/UmbracoSettingsSection.cs
@@ -1,4 +1,5 @@
using System;
+using System.ComponentModel;
using System.Configuration;
using System.Linq;
@@ -183,6 +184,8 @@ namespace Umbraco.Core.Configuration.UmbracoSettings
get { return Providers; }
}
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ [Obsolete("This is no longer used and will be removed in future versions")]
IHelpSection IUmbracoSettingsSection.Help
{
get { return Help; }
diff --git a/src/Umbraco.Core/Configuration/UmbracoVersion.cs b/src/Umbraco.Core/Configuration/UmbracoVersion.cs
index 612afa7310..173c1ef6f6 100644
--- a/src/Umbraco.Core/Configuration/UmbracoVersion.cs
+++ b/src/Umbraco.Core/Configuration/UmbracoVersion.cs
@@ -6,7 +6,7 @@ namespace Umbraco.Core.Configuration
{
public class UmbracoVersion
{
- private static readonly Version Version = new Version("7.6.8");
+ private static readonly Version Version = new Version("7.7.1");
///
/// Gets the current version of Umbraco.
diff --git a/src/Umbraco.Core/Constants-Applications.cs b/src/Umbraco.Core/Constants-Applications.cs
index fd9476a8ab..3911a6d204 100644
--- a/src/Umbraco.Core/Constants-Applications.cs
+++ b/src/Umbraco.Core/Constants-Applications.cs
@@ -56,7 +56,12 @@
///
/// alias for the content tree.
///
- public const string Content = "content";
+ public const string Content = "content";
+
+ ///
+ /// alias for the content blueprint tree.
+ ///
+ public const string ContentBlueprints = "contentBlueprints";
///
/// alias for the member tree.
@@ -118,6 +123,8 @@
public const string Scripts = "scripts";
+ public const string Users = "users";
+
//TODO: Fill in the rest!
}
}
diff --git a/src/Umbraco.Core/Constants-Conventions.cs b/src/Umbraco.Core/Constants-Conventions.cs
index 2e3d652d7e..b59b7e487e 100644
--- a/src/Umbraco.Core/Constants-Conventions.cs
+++ b/src/Umbraco.Core/Constants-Conventions.cs
@@ -12,6 +12,14 @@ namespace Umbraco.Core
///
public static class Conventions
{
+ internal static class PermissionCategories
+ {
+ public const string ContentCategory = "content";
+ public const string AdministrationCategory = "administration";
+ public const string StructureCategory = "structure";
+ public const string OtherCategory = "other";
+ }
+
public static class PublicAccess
{
public const string MemberUsernameRuleType = "MemberUsername";
diff --git a/src/Umbraco.Core/Constants-ObjectTypes.cs b/src/Umbraco.Core/Constants-ObjectTypes.cs
index a1e6e55ee9..059b1cbd3c 100644
--- a/src/Umbraco.Core/Constants-ObjectTypes.cs
+++ b/src/Umbraco.Core/Constants-ObjectTypes.cs
@@ -68,12 +68,22 @@ namespace Umbraco.Core
/// Guid for a Document object.
///
public const string Document = "C66BA18E-EAF3-4CFF-8A22-41B16D66A972";
-
+
///
/// Guid for a Document object.
///
public static readonly Guid DocumentGuid = new Guid(Document);
+ ///
+ /// Guid for a Document Blueprint object.
+ ///
+ public const string DocumentBlueprint = "6EBEF410-03AA-48CF-A792-E1C1CB087ACA";
+
+ ///
+ /// Guid for a Document object.
+ ///
+ public static readonly Guid DocumentBlueprintGuid = new Guid(DocumentBlueprint);
+
///
/// Guid for a Document Type object.
///
diff --git a/src/Umbraco.Core/Constants-PropertyEditors.cs b/src/Umbraco.Core/Constants-PropertyEditors.cs
index eba5cef23d..1f3986eeaf 100644
--- a/src/Umbraco.Core/Constants-PropertyEditors.cs
+++ b/src/Umbraco.Core/Constants-PropertyEditors.cs
@@ -437,6 +437,11 @@ namespace Umbraco.Core
///
public const string EmailAddressAlias = "Umbraco.EmailAddress";
+ ///
+ /// Alias for the nested content property editor.
+ ///
+ public const string NestedContentAlias = "Umbraco.NestedContent";
+
public static class PreValueKeys
{
///
diff --git a/src/Umbraco.Core/Constants-Security.cs b/src/Umbraco.Core/Constants-Security.cs
index e4f2e5b4a8..0ada375163 100644
--- a/src/Umbraco.Core/Constants-Security.cs
+++ b/src/Umbraco.Core/Constants-Security.cs
@@ -8,6 +8,9 @@ namespace Umbraco.Core
public static class Security
{
+ public const string AdminGroupAlias = "admin";
+ public const string TranslatorGroupAlias = "translator";
+
public const string BackOfficeAuthenticationType = "UmbracoBackOffice";
public const string BackOfficeExternalAuthenticationType = "UmbracoExternalCookie";
public const string BackOfficeExternalCookieName = "UMB_EXTLOGIN";
@@ -15,7 +18,8 @@ namespace Umbraco.Core
public const string BackOfficeTwoFactorAuthenticationType = "UmbracoTwoFactorCookie";
internal const string EmptyPasswordPrefix = "___UIDEMPTYPWORD__";
-
+ internal const string ForceReAuthFlag = "umbraco-force-auth";
+
///
/// The prefix used for external identity providers for their authentication type
///
diff --git a/src/Umbraco.Core/DateTimeExtensions.cs b/src/Umbraco.Core/DateTimeExtensions.cs
index 1851eded9f..6e2676ac72 100644
--- a/src/Umbraco.Core/DateTimeExtensions.cs
+++ b/src/Umbraco.Core/DateTimeExtensions.cs
@@ -41,7 +41,43 @@ namespace Umbraco.Core
Hour,
Minute,
Second
+ }
+
+ ///
+ /// Calculates the number of minutes from a date time, on a rolling daily basis (so if
+ /// date time is before the time, calculate onto next day)
+ ///
+ /// Date to start from
+ /// Time to compare against (in Hmm form, e.g. 330, 2200)
+ ///
+ public static int PeriodicMinutesFrom(this DateTime fromDateTime, string scheduledTime)
+ {
+ // Ensure time provided is 4 digits long
+ if (scheduledTime.Length == 3)
+ {
+ scheduledTime = "0" + scheduledTime;
+ }
+
+ var scheduledHour = int.Parse(scheduledTime.Substring(0, 2));
+ var scheduledMinute = int.Parse(scheduledTime.Substring(2));
+
+ DateTime scheduledDateTime;
+ if (IsScheduledInRemainingDay(fromDateTime, scheduledHour, scheduledMinute))
+ {
+ scheduledDateTime = new DateTime(fromDateTime.Year, fromDateTime.Month, fromDateTime.Day, scheduledHour, scheduledMinute, 0);
+ }
+ else
+ {
+ var nextDay = fromDateTime.AddDays(1);
+ scheduledDateTime = new DateTime(nextDay.Year, nextDay.Month, nextDay.Day, scheduledHour, scheduledMinute, 0);
+ }
+
+ return (int)(scheduledDateTime - fromDateTime).TotalMinutes;
+ }
+
+ private static bool IsScheduledInRemainingDay(DateTime fromDateTime, int scheduledHour, int scheduledMinute)
+ {
+ return scheduledHour > fromDateTime.Hour || (scheduledHour == fromDateTime.Hour && scheduledMinute >= fromDateTime.Minute);
}
-
}
}
diff --git a/src/Umbraco.Core/Deploy/IUniqueIdentifyingServiceConnector.cs b/src/Umbraco.Core/Deploy/IUniqueIdentifyingServiceConnector.cs
new file mode 100644
index 0000000000..a657ba78b6
--- /dev/null
+++ b/src/Umbraco.Core/Deploy/IUniqueIdentifyingServiceConnector.cs
@@ -0,0 +1,25 @@
+namespace Umbraco.Core.Deploy
+{
+ ///
+ /// Provides a method to retrieve an artifact's unique identifier.
+ ///
+ ///
+ /// Artifacts are uniquely identified by their , however they represent
+ /// elements in Umbraco that may be uniquely identified by another value. For example,
+ /// a content type is uniquely identified by its alias. If someone creates a new content
+ /// type, and tries to deploy it to a remote environment where a content type with the
+ /// same alias already exists, both content types end up having different
+ /// but the same alias. By default, Deploy would fail and throw when trying to save the
+ /// new content type (duplicate alias). However, if the connector also implements this
+ /// interface, the situation can be detected beforehand and reported in a nicer way.
+ ///
+ public interface IUniqueIdentifyingServiceConnector
+ {
+ ///
+ /// Gets the unique identifier of the specified artifact.
+ ///
+ /// The artifact.
+ /// The unique identifier.
+ string GetUniqueIdentifier(IArtifact artifact);
+ }
+}
diff --git a/src/Umbraco.Core/EmailSender.cs b/src/Umbraco.Core/EmailSender.cs
new file mode 100644
index 0000000000..6f381f693c
--- /dev/null
+++ b/src/Umbraco.Core/EmailSender.cs
@@ -0,0 +1,114 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net.Mail;
+using System.Text;
+using System.Threading.Tasks;
+using System.Web;
+using System.Web.Routing;
+using Umbraco.Core.Configuration;
+using Umbraco.Core.Events;
+
+namespace Umbraco.Core
+{
+ ///
+ /// A utility class for sending emails
+ ///
+ public class EmailSender : IEmailSender
+ {
+ //TODO: This should encapsulate a BackgroundTaskRunner with a queue to send these emails!
+
+ private readonly bool _enableEvents;
+
+ ///
+ /// Default constructor
+ ///
+ public EmailSender() : this(false)
+ {
+ }
+
+ internal EmailSender(bool enableEvents)
+ {
+ _enableEvents = enableEvents;
+ }
+
+ private static readonly Lazy SmtpConfigured = new Lazy(() => GlobalSettings.HasSmtpServerConfigured(HttpRuntime.AppDomainAppVirtualPath));
+
+ ///
+ /// Sends the message non-async
+ ///
+ ///
+ public void Send(MailMessage message)
+ {
+ if (SmtpConfigured.Value == false && _enableEvents)
+ {
+ OnSendEmail(new SendEmailEventArgs(message));
+ }
+ else
+ {
+ using (var client = new SmtpClient())
+ {
+ client.Send(message);
+ }
+ }
+ }
+
+ ///
+ /// Sends the message async
+ ///
+ ///
+ ///
+ public async Task SendAsync(MailMessage message)
+ {
+ if (SmtpConfigured.Value == false && _enableEvents)
+ {
+ OnSendEmail(new SendEmailEventArgs(message));
+ }
+ else
+ {
+ using (var client = new SmtpClient())
+ {
+ if (client.DeliveryMethod == SmtpDeliveryMethod.Network)
+ {
+ await client.SendMailAsync(message);
+ }
+ else
+ {
+ client.Send(message);
+ }
+ }
+ }
+ }
+
+ ///
+ /// Returns true if the application should be able to send a required application email
+ ///
+ ///
+ /// We assume this is possible if either an event handler is registered or an smtp server is configured
+ ///
+ internal static bool CanSendRequiredEmail
+ {
+ get { return EventHandlerRegistered || SmtpConfigured.Value; }
+ }
+
+ ///
+ /// returns true if an event handler has been registered
+ ///
+ internal static bool EventHandlerRegistered
+ {
+ get { return SendEmail != null; }
+ }
+
+ ///
+ /// An event that is raised when no smtp server is configured if events are enabled
+ ///
+ internal static event EventHandler SendEmail;
+
+ private static void OnSendEmail(SendEmailEventArgs e)
+ {
+ var handler = SendEmail;
+ if (handler != null) handler(null, e);
+ }
+ }
+}
diff --git a/src/Umbraco.Core/EnumerableExtensions.cs b/src/Umbraco.Core/EnumerableExtensions.cs
index bb115b394d..81c0df793e 100644
--- a/src/Umbraco.Core/EnumerableExtensions.cs
+++ b/src/Umbraco.Core/EnumerableExtensions.cs
@@ -111,6 +111,9 @@ namespace Umbraco.Core
///
public static bool ContainsAll(this IEnumerable source, IEnumerable other)
{
+ if (source == null) throw new ArgumentNullException("source");
+ if (other == null) throw new ArgumentNullException("other");
+
return other.Except(source).Any() == false;
}
diff --git a/src/Umbraco.Core/Events/SendEmailEventArgs.cs b/src/Umbraco.Core/Events/SendEmailEventArgs.cs
new file mode 100644
index 0000000000..8c6a2138d5
--- /dev/null
+++ b/src/Umbraco.Core/Events/SendEmailEventArgs.cs
@@ -0,0 +1,15 @@
+using System;
+using System.Net.Mail;
+
+namespace Umbraco.Core.Events
+{
+ internal class SendEmailEventArgs : EventArgs
+ {
+ public MailMessage Message { get; private set; }
+
+ public SendEmailEventArgs(MailMessage message)
+ {
+ Message = message;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Core/ExpressionHelper.cs b/src/Umbraco.Core/ExpressionHelper.cs
index c34c4591b8..75dd63a0df 100644
--- a/src/Umbraco.Core/ExpressionHelper.cs
+++ b/src/Umbraco.Core/ExpressionHelper.cs
@@ -217,8 +217,21 @@ namespace Umbraco.Core
public static MemberInfo GetMemberInfo(Expression> fromExpression)
{
if (fromExpression == null) return null;
- var body = fromExpression.Body as MemberExpression;
- return body != null ? body.Member : null;
+
+ MemberExpression me;
+ switch (fromExpression.Body.NodeType)
+ {
+ case ExpressionType.Convert:
+ case ExpressionType.ConvertChecked:
+ var ue = fromExpression.Body as UnaryExpression;
+ me = ((ue != null) ? ue.Operand : null) as MemberExpression;
+ break;
+ default:
+ me = fromExpression.Body as MemberExpression;
+ break;
+ }
+
+ return me != null ? me.Member : null;
}
///
diff --git a/src/Umbraco.Core/IEmailSender.cs b/src/Umbraco.Core/IEmailSender.cs
new file mode 100644
index 0000000000..7e6565a53b
--- /dev/null
+++ b/src/Umbraco.Core/IEmailSender.cs
@@ -0,0 +1,13 @@
+using System.Net.Mail;
+using System.Threading.Tasks;
+
+namespace Umbraco.Core
+{
+ ///
+ /// Simple abstraction to send an email message
+ ///
+ public interface IEmailSender
+ {
+ Task SendAsync(MailMessage message);
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Core/IO/FileSystemProviderManager.cs b/src/Umbraco.Core/IO/FileSystemProviderManager.cs
index d2ae8a0612..e50322d95c 100644
--- a/src/Umbraco.Core/IO/FileSystemProviderManager.cs
+++ b/src/Umbraco.Core/IO/FileSystemProviderManager.cs
@@ -11,7 +11,7 @@ namespace Umbraco.Core.IO
{
public class FileSystemProviderManager
{
- private readonly FileSystemProvidersSection _config;
+ private readonly IFileSystemProvidersSection _config;
private readonly ConcurrentSet _wrappers = new ConcurrentSet();
private readonly ConcurrentDictionary _providerLookup = new ConcurrentDictionary();
@@ -28,16 +28,45 @@ namespace Umbraco.Core.IO
private ShadowWrapper _mvcViewsFileSystem;
#region Singleton & Constructor
-
- private static readonly FileSystemProviderManager Instance = new FileSystemProviderManager();
+
+ private static volatile FileSystemProviderManager _instance;
+ private static readonly object _instanceLocker = new object();
public static FileSystemProviderManager Current
{
- get { return Instance; }
+ get
+ {
+ if (_instance != null) return _instance;
+ lock (_instanceLocker)
+ {
+ return _instance ?? (_instance = new FileSystemProviderManager());
+ }
+ }
+ }
+
+ ///
+ /// For tests only, allows setting the value of the singleton "Current" property
+ ///
+ ///
+ public static void SetCurrent(FileSystemProviderManager instance)
+ {
+ lock (_instanceLocker)
+ {
+ _instance = instance;
+ }
}
+ internal static void ResetCurrent()
+ {
+ lock (_instanceLocker)
+ {
+ if (_instance != null)
+ _instance.Reset();
+ }
+ }
+
// for tests only, totally unsafe
- internal void Reset()
+ private void Reset()
{
_wrappers.Clear();
_providerLookup.Clear();
@@ -52,10 +81,24 @@ namespace Umbraco.Core.IO
// beware: means that we capture the "current" scope provider - take care in tests!
get { return ApplicationContext.Current == null ? null : ApplicationContext.Current.ScopeProvider as IScopeProviderInternal; }
}
-
- internal FileSystemProviderManager()
+
+ ///
+ /// Constructor that can be used for tests
+ ///
+ ///
+ public FileSystemProviderManager(IFileSystemProvidersSection configSection)
{
- _config = (FileSystemProvidersSection) ConfigurationManager.GetSection("umbracoConfiguration/FileSystemProviders");
+ if (configSection == null) throw new ArgumentNullException("configSection");
+ _config = configSection;
+ CreateWellKnownFileSystems();
+ }
+
+ ///
+ /// Default constructor that will read the config from the locally found config section
+ ///
+ public FileSystemProviderManager()
+ {
+ _config = (FileSystemProvidersSection)ConfigurationManager.GetSection("umbracoConfiguration/FileSystemProviders");
CreateWellKnownFileSystems();
}
@@ -150,8 +193,9 @@ namespace Umbraco.Core.IO
private ProviderConstructionInfo GetUnderlyingFileSystemCtor(string alias, Func fallback)
{
// get config
- var providerConfig = _config.Providers[alias];
- if (providerConfig == null)
+ IFileSystemProviderElement providerConfig;
+
+ if (_config.Providers.TryGetValue(alias, out providerConfig) == false)
{
if (fallback != null) return null;
throw new ArgumentException(string.Format("No provider found with alias {0}.", alias));
@@ -166,17 +210,21 @@ namespace Umbraco.Core.IO
if (providerType.IsAssignableFrom(typeof(IFileSystem)))
throw new InvalidOperationException(string.Format("Type {0} does not implement IFileSystem.", providerType.FullName));
- // find a ctor matching the config parameters
+ // find a ctor matching the config parameters
var paramCount = providerConfig.Parameters != null ? providerConfig.Parameters.Count : 0;
var constructor = providerType.GetConstructors().SingleOrDefault(x
- => x.GetParameters().Length == paramCount && x.GetParameters().All(y => providerConfig.Parameters.AllKeys.Contains(y.Name)));
+ => x.GetParameters().Length == paramCount && x.GetParameters().All(y => providerConfig.Parameters.Keys.Contains(y.Name)));
if (constructor == null)
throw new InvalidOperationException(string.Format("Type {0} has no ctor matching the {1} configuration parameter(s).", providerType.FullName, paramCount));
var parameters = new object[paramCount];
- if (providerConfig.Parameters != null) // keeps ReSharper happy
+
+ if (providerConfig.Parameters != null)
+ {
+ var allKeys = providerConfig.Parameters.Keys.ToArray();
for (var i = 0; i < paramCount; i++)
- parameters[i] = providerConfig.Parameters[providerConfig.Parameters.AllKeys[i]].Value;
+ parameters[i] = providerConfig.Parameters[allKeys[i]];
+ }
return new ProviderConstructionInfo
{
diff --git a/src/Umbraco.Core/Models/ApplicationTree.cs b/src/Umbraco.Core/Models/ApplicationTree.cs
index 0397b4238d..92002f1e99 100644
--- a/src/Umbraco.Core/Models/ApplicationTree.cs
+++ b/src/Umbraco.Core/Models/ApplicationTree.cs
@@ -1,7 +1,6 @@
using System;
using System.Collections.Concurrent;
using System.Diagnostics;
-using Umbraco.Core.Logging;
namespace Umbraco.Core.Models
{
diff --git a/src/Umbraco.Core/Models/Content.cs b/src/Umbraco.Core/Models/Content.cs
index fed1312ec3..ef16a78248 100644
--- a/src/Umbraco.Core/Models/Content.cs
+++ b/src/Umbraco.Core/Models/Content.cs
@@ -20,7 +20,7 @@ namespace Umbraco.Core.Models
private DateTime? _expireDate;
private int _writer;
private string _nodeName;//NOTE Once localization is introduced this will be the non-localized Node Name.
- private bool _permissionsChanged;
+
///
/// Constructor for creating a Content object
///
@@ -29,7 +29,7 @@ namespace Umbraco.Core.Models
/// ContentType for the current Content object
public Content(string name, IContent parent, IContentType contentType)
: this(name, parent, contentType, new PropertyCollection())
- {
+ {
}
///
@@ -65,7 +65,7 @@ namespace Umbraco.Core.Models
/// Id of the Parent content
/// ContentType for the current Content object
/// Collection of properties
- public Content(string name, int parentId, IContentType contentType, PropertyCollection properties)
+ public Content(string name, int parentId, IContentType contentType, PropertyCollection properties)
: base(name, parentId, contentType, properties)
{
Mandate.ParameterNotNull(contentType, "contentType");
@@ -84,7 +84,6 @@ namespace Umbraco.Core.Models
public readonly PropertyInfo ExpireDateSelector = ExpressionHelper.GetPropertyInfo(x => x.ExpireDate);
public readonly PropertyInfo WriterSelector = ExpressionHelper.GetPropertyInfo(x => x.WriterId);
public readonly PropertyInfo NodeNameSelector = ExpressionHelper.GetPropertyInfo(x => x.NodeName);
- public readonly PropertyInfo PermissionsChangedSelector = ExpressionHelper.GetPropertyInfo(x => x.PermissionsChanged);
}
///
@@ -92,7 +91,7 @@ namespace Umbraco.Core.Models
/// This is used to override the default one from the ContentType.
///
///
- /// If no template is explicitly set on the Content object,
+ /// If no template is explicitly set on the Content object,
/// the Default template from the ContentType will be returned.
///
[DataMember]
@@ -194,15 +193,6 @@ namespace Umbraco.Core.Models
set { SetPropertyValueAndDetectChanges(value, ref _nodeName, Ps.Value.NodeNameSelector); }
}
- ///
- /// Used internally to track if permissions have been changed during the saving process for this entity
- ///
- [IgnoreDataMember]
- internal bool PermissionsChanged
- {
- get { return _permissionsChanged; }
- set { SetPropertyValueAndDetectChanges(value, ref _permissionsChanged, Ps.Value.PermissionsChangedSelector); }
- }
///
/// Gets the ContentType used by this content object
@@ -274,6 +264,9 @@ namespace Umbraco.Core.Models
[IgnoreDataMember]
internal DateTime PublishedDate { get; set; }
+ [DataMember]
+ public bool IsBlueprint { get; internal set; }
+
///
/// Changes the Trashed state of the content object
///
@@ -290,7 +283,7 @@ namespace Umbraco.Core.Models
ChangePublishedState(PublishedState.Unpublished);
}
}
-
+
///
/// Method to call when Entity is being updated
///
diff --git a/src/Umbraco.Core/Models/ContentType.cs b/src/Umbraco.Core/Models/ContentType.cs
index 88c498a147..81b5272a58 100644
--- a/src/Umbraco.Core/Models/ContentType.cs
+++ b/src/Umbraco.Core/Models/ContentType.cs
@@ -54,6 +54,11 @@ namespace Umbraco.Core.Models
{
public readonly PropertyInfo DefaultTemplateSelector = ExpressionHelper.GetPropertyInfo(x => x.DefaultTemplateId);
public readonly PropertyInfo AllowedTemplatesSelector = ExpressionHelper.GetPropertyInfo>(x => x.AllowedTemplates);
+
+ //Custom comparer for enumerable
+ public readonly DelegateEqualityComparer> TemplateComparer = new DelegateEqualityComparer>(
+ (templates, enumerable) => templates.UnsortedSequenceEqual(enumerable),
+ templates => templates.GetHashCode());
}
///
@@ -91,10 +96,7 @@ namespace Umbraco.Core.Models
set
{
SetPropertyValueAndDetectChanges(value, ref _allowedTemplates, Ps.Value.AllowedTemplatesSelector,
- //Custom comparer for enumerable
- new DelegateEqualityComparer>(
- (templates, enumerable) => templates.UnsortedSequenceEqual(enumerable),
- templates => templates.GetHashCode()));
+ Ps.Value.TemplateComparer);
}
}
diff --git a/src/Umbraco.Core/Models/ContentTypeBase.cs b/src/Umbraco.Core/Models/ContentTypeBase.cs
index 88476f946d..db0bc0e900 100644
--- a/src/Umbraco.Core/Models/ContentTypeBase.cs
+++ b/src/Umbraco.Core/Models/ContentTypeBase.cs
@@ -88,6 +88,12 @@ namespace Umbraco.Core.Models
public readonly PropertyInfo PropertyGroupCollectionSelector = ExpressionHelper.GetPropertyInfo(x => x.PropertyGroups);
public readonly PropertyInfo PropertyTypeCollectionSelector = ExpressionHelper.GetPropertyInfo>(x => x.PropertyTypes);
public readonly PropertyInfo HasPropertyTypeBeenRemovedSelector = ExpressionHelper.GetPropertyInfo(x => x.HasPropertyTypeBeenRemoved);
+
+ //Custom comparer for enumerable
+ public readonly DelegateEqualityComparer> ContentTypeSortComparer =
+ new DelegateEqualityComparer>(
+ (sorts, enumerable) => sorts.UnsortedSequenceEqual(enumerable),
+ sorts => sorts.GetHashCode());
}
@@ -254,7 +260,7 @@ namespace Umbraco.Core.Models
set { SetPropertyValueAndDetectChanges(value, ref _trashed, Ps.Value.TrashedSelector); }
}
- private IDictionary _additionalData;
+ private readonly IDictionary _additionalData;
///
/// Some entities may expose additional data that other's might not, this custom data will be available in this collection
///
@@ -273,11 +279,8 @@ namespace Umbraco.Core.Models
get { return _allowedContentTypes; }
set
{
- SetPropertyValueAndDetectChanges(value, ref _allowedContentTypes, Ps.Value.AllowedContentTypesSelector,
- //Custom comparer for enumerable
- new DelegateEqualityComparer>(
- (sorts, enumerable) => sorts.UnsortedSequenceEqual(enumerable),
- sorts => sorts.GetHashCode()));
+ SetPropertyValueAndDetectChanges(value, ref _allowedContentTypes, Ps.Value.AllowedContentTypesSelector,
+ Ps.Value.ContentTypeSortComparer);
}
}
diff --git a/src/Umbraco.Core/Models/DictionaryItem.cs b/src/Umbraco.Core/Models/DictionaryItem.cs
index 42b047e35b..d5fcc89994 100644
--- a/src/Umbraco.Core/Models/DictionaryItem.cs
+++ b/src/Umbraco.Core/Models/DictionaryItem.cs
@@ -37,6 +37,12 @@ namespace Umbraco.Core.Models
public readonly PropertyInfo ParentIdSelector = ExpressionHelper.GetPropertyInfo(x => x.ParentId);
public readonly PropertyInfo ItemKeySelector = ExpressionHelper.GetPropertyInfo(x => x.ItemKey);
public readonly PropertyInfo TranslationsSelector = ExpressionHelper.GetPropertyInfo>(x => x.Translations);
+
+ //Custom comparer for enumerable
+ public readonly DelegateEqualityComparer> DictionaryTranslationComparer =
+ new DelegateEqualityComparer>(
+ (enumerable, translations) => enumerable.UnsortedSequenceEqual(translations),
+ enumerable => enumerable.GetHashCode());
}
///
@@ -79,10 +85,7 @@ namespace Umbraco.Core.Models
}
SetPropertyValueAndDetectChanges(asArray, ref _translations, Ps.Value.TranslationsSelector,
- //Custom comparer for enumerable
- new DelegateEqualityComparer>(
- (enumerable, translations) => enumerable.UnsortedSequenceEqual(translations),
- enumerable => enumerable.GetHashCode()));
+ Ps.Value.DictionaryTranslationComparer);
}
}
}
diff --git a/src/Umbraco.Core/Models/EntityBase/Entity.cs b/src/Umbraco.Core/Models/EntityBase/Entity.cs
index 6af14c830b..296780c753 100644
--- a/src/Umbraco.Core/Models/EntityBase/Entity.cs
+++ b/src/Umbraco.Core/Models/EntityBase/Entity.cs
@@ -116,7 +116,7 @@ namespace Umbraco.Core.Models.EntityBase
if (IsPropertyDirty("CreateDate") == false || _createDate == default(DateTime))
CreateDate = DateTime.Now;
if (IsPropertyDirty("UpdateDate") == false || _updateDate == default(DateTime))
- UpdateDate = CreateDate;
+ UpdateDate = DateTime.Now;
}
///
@@ -126,6 +126,10 @@ namespace Umbraco.Core.Models.EntityBase
{
if (IsPropertyDirty("UpdateDate") == false || _updateDate == default(DateTime))
UpdateDate = DateTime.Now;
+
+ //this is just in case
+ if (_createDate == default(DateTime))
+ CreateDate = DateTime.Now;
}
///
diff --git a/src/Umbraco.Core/Models/EntityBase/EntityPath.cs b/src/Umbraco.Core/Models/EntityBase/EntityPath.cs
new file mode 100644
index 0000000000..368d6bd87b
--- /dev/null
+++ b/src/Umbraco.Core/Models/EntityBase/EntityPath.cs
@@ -0,0 +1,8 @@
+namespace Umbraco.Core.Models.EntityBase
+{
+ public class EntityPath
+ {
+ public int Id { get; set; }
+ public string Path { get; set; }
+ }
+}
diff --git a/src/Umbraco.Core/Models/IContent.cs b/src/Umbraco.Core/Models/IContent.cs
index 7caefc1121..3d385f1b36 100644
--- a/src/Umbraco.Core/Models/IContent.cs
+++ b/src/Umbraco.Core/Models/IContent.cs
@@ -1,7 +1,5 @@
using System;
using System.ComponentModel;
-using System.Diagnostics;
-using Umbraco.Core.Persistence.Mappers;
namespace Umbraco.Core.Models
{
@@ -85,5 +83,10 @@ namespace Umbraco.Core.Models
/// Gets the unique identifier of the published version, if any.
///
Guid PublishedVersionGuid { get; }
+
+ ///
+ /// Gets a value indicating whether the content item is a blueprint.
+ ///
+ bool IsBlueprint { get; }
}
}
\ No newline at end of file
diff --git a/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs b/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs
index 50dc7d06f8..565aa4f4c9 100644
--- a/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs
+++ b/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs
@@ -2,23 +2,69 @@ using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
+using System.Linq;
+using System.Reflection;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNet.Identity;
+using Umbraco.Core.Models.EntityBase;
+using Umbraco.Core.Models.Membership;
using Umbraco.Core.Security;
namespace Umbraco.Core.Models.Identity
{
- public class BackOfficeIdentityUser : IdentityUser, IdentityUserClaim>
+ public class BackOfficeIdentityUser : IdentityUser, IdentityUserClaim>, IRememberBeingDirty
{
-
- public BackOfficeIdentityUser()
+ ///
+ /// Used to construct a new instance without an identity
+ ///
+ ///
+ /// This is allowed to be null (but would need to be filled in if trying to persist this instance)
+ ///
+ ///
+ public static BackOfficeIdentityUser CreateNew(string username, string email, string culture)
{
- StartMediaId = -1;
- StartContentId = -1;
- Culture = Configuration.GlobalSettings.DefaultUILanguage;
+ if (string.IsNullOrWhiteSpace(username)) throw new ArgumentException("Value cannot be null or whitespace.", "username");
+ if (string.IsNullOrWhiteSpace(culture)) throw new ArgumentException("Value cannot be null or whitespace.", "culture");
+
+ var user = new BackOfficeIdentityUser();
+ user.DisableChangeTracking();
+ user._userName = username;
+ user._email = email;
+ //we are setting minvalue here because the default is "0" which is the id of the admin user
+ //which we cannot allow because the admin user will always exist
+ user._id = int.MinValue;
+ user._hasIdentity = false;
+ user._culture = culture;
+ user.EnableChangeTracking();
+ return user;
}
+ private BackOfficeIdentityUser()
+ {
+ }
+
+ ///
+ /// Creates an existing user with the specified groups
+ ///
+ ///
+ ///
+ public BackOfficeIdentityUser(int userId, IEnumerable groups)
+ {
+ _startMediaIds = new int[] { };
+ _startContentIds = new int[] { };
+ _groups = new IReadOnlyUserGroup[] { };
+ _allowedSections = new string[] { };
+ _culture = Configuration.GlobalSettings.DefaultUILanguage;
+ _groups = groups.ToArray();
+ _roles = new ObservableCollection>(_groups.Select(x => new IdentityUserRole
+ {
+ RoleId = x.Alias,
+ UserId = userId.ToString()
+ }));
+ _roles.CollectionChanged += _roles_CollectionChanged;
+ }
+
public virtual async Task GenerateUserIdentityAsync(BackOfficeUserManager manager)
{
// NOTE the authenticationType must match the umbraco one
@@ -27,16 +73,155 @@ namespace Umbraco.Core.Models.Identity
return userIdentity;
}
+ ///
+ /// Returns true if an Id has been set on this object this will be false if the object is new and not peristed to the database
+ ///
+ public bool HasIdentity
+ {
+ get { return _hasIdentity; }
+ }
+
+ public int[] CalculatedMediaStartNodeIds { get; internal set; }
+ public int[] CalculatedContentStartNodeIds { get; internal set; }
+
+ public override int Id
+ {
+ get { return _id; }
+ set
+ {
+ _id = value;
+ _hasIdentity = true;
+ }
+ }
+
+ ///
+ /// Override Email so we can track changes to it
+ ///
+ public override string Email
+ {
+ get { return _email; }
+ set { _tracker.SetPropertyValueAndDetectChanges(value, ref _email, Ps.Value.EmailSelector); }
+ }
+
+ ///
+ /// Override UserName so we can track changes to it
+ ///
+ public override string UserName
+ {
+ get { return _userName; }
+ set { _tracker.SetPropertyValueAndDetectChanges(value, ref _userName, Ps.Value.UserNameSelector); }
+ }
+
+ ///
+ /// Override LastLoginDateUtc so we can track changes to it
+ ///
+ public override DateTime? LastLoginDateUtc
+ {
+ get { return _lastLoginDateUtc; }
+ set { _tracker.SetPropertyValueAndDetectChanges(value, ref _lastLoginDateUtc, Ps.Value.LastLoginDateUtcSelector); }
+ }
+
+ ///
+ /// Override EmailConfirmed so we can track changes to it
+ ///
+ public override bool EmailConfirmed
+ {
+ get { return _emailConfirmed; }
+ set { _tracker.SetPropertyValueAndDetectChanges(value, ref _emailConfirmed, Ps.Value.EmailConfirmedSelector); }
+ }
+
///
/// Gets/sets the user's real name
///
- public string Name { get; set; }
- public int StartContentId { get; set; }
- public int StartMediaId { get; set; }
- public string[] AllowedSections { get; set; }
- public string Culture { get; set; }
+ public string Name
+ {
+ get { return _name; }
+ set { _tracker.SetPropertyValueAndDetectChanges(value, ref _name, Ps.Value.NameSelector); }
+ }
- public string UserTypeAlias { get; set; }
+ ///
+ /// Override AccessFailedCount so we can track changes to it
+ ///
+ public override int AccessFailedCount
+ {
+ get { return _accessFailedCount; }
+ set { _tracker.SetPropertyValueAndDetectChanges(value, ref _accessFailedCount, Ps.Value.AccessFailedCountSelector); }
+ }
+
+ ///
+ /// Override PasswordHash so we can track changes to it
+ ///
+ public override string PasswordHash
+ {
+ get { return _passwordHash; }
+ set { _tracker.SetPropertyValueAndDetectChanges(value, ref _passwordHash, Ps.Value.PasswordHashSelector); }
+ }
+
+
+ ///
+ /// Content start nodes assigned to the User (not ones assigned to the user's groups)
+ ///
+ public int[] StartContentIds
+ {
+ get { return _startContentIds; }
+ set
+ {
+ if (value == null) value = new int[0];
+ _tracker.SetPropertyValueAndDetectChanges(value, ref _startContentIds, Ps.Value.StartContentIdsSelector, Ps.Value.StartIdsComparer);
+ }
+ }
+
+ ///
+ /// Media start nodes assigned to the User (not ones assigned to the user's groups)
+ ///
+ public int[] StartMediaIds
+ {
+ get { return _startMediaIds; }
+ set
+ {
+ if (value == null) value = new int[0];
+ _tracker.SetPropertyValueAndDetectChanges(value, ref _startMediaIds, Ps.Value.StartMediaIdsSelector, Ps.Value.StartIdsComparer);
+ }
+ }
+
+ ///
+ /// This is a readonly list of the user's allowed sections which are based on it's user groups
+ ///
+ public string[] AllowedSections
+ {
+ get { return _allowedSections ?? (_allowedSections = _groups.SelectMany(x => x.AllowedSections).Distinct().ToArray()); }
+ }
+
+ public string Culture
+ {
+ get { return _culture; }
+ set { _tracker.SetPropertyValueAndDetectChanges(value, ref _culture, Ps.Value.CultureSelector); }
+ }
+
+ public IReadOnlyUserGroup[] Groups
+ {
+ get { return _groups; }
+ set
+ {
+ //so they recalculate
+ _allowedSections = null;
+
+ //now clear all roles and re-add them
+ _roles.CollectionChanged -= _roles_CollectionChanged;
+ _roles.Clear();
+ foreach (var identityUserRole in _groups.Select(x => new IdentityUserRole
+ {
+ RoleId = x.Alias,
+ UserId = Id.ToString()
+ }))
+ {
+ _roles.Add(identityUserRole);
+ }
+ _roles.CollectionChanged += _roles_CollectionChanged;
+
+ _tracker.SetPropertyValueAndDetectChanges(value, ref _groups, Ps.Value.GroupsSelector, Ps.Value.GroupsComparer);
+ }
+ }
///
/// Lockout is always enabled
@@ -44,7 +229,7 @@ namespace Umbraco.Core.Models.Identity
public override bool LockoutEnabled
{
get { return true; }
- set
+ set
{
//do nothing
}
@@ -62,6 +247,11 @@ namespace Umbraco.Core.Models.Identity
}
}
+ ///
+ /// This is a 1:1 mapping with IUser.IsApproved
+ ///
+ internal bool IsApproved { get; set; }
+
///
/// Overridden to make the retrieval lazy
///
@@ -82,16 +272,42 @@ namespace Umbraco.Core.Models.Identity
return _logins;
}
}
-
- public bool LoginsChanged { get; private set; }
-
+
void Logins_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
- LoginsChanged = true;
+ _tracker.OnPropertyChanged(Ps.Value.LoginsSelector);
}
- private ObservableCollection _logins;
- private Lazy> _getLogins;
+ private void _roles_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
+ {
+ _tracker.OnPropertyChanged(Ps.Value.RolesSelector);
+ }
+
+ private readonly ObservableCollection> _roles;
+
+ ///
+ /// helper method to easily add a role without having to deal with IdentityUserRole{T}
+ ///
+ ///
+ ///
+ /// Adding a role this way will not reflect on the user's group's collection or it's allowed sections until the user is persisted
+ ///
+ public void AddRole(string role)
+ {
+ Roles.Add(new IdentityUserRole
+ {
+ UserId = this.Id.ToString(),
+ RoleId = role
+ });
+ }
+
+ ///
+ /// Override Roles because the value of these are the user's group aliases
+ ///
+ public override ICollection> Roles
+ {
+ get { return _roles; }
+ }
///
/// Used to set a lazy call back to populate the user's Login list
@@ -101,6 +317,128 @@ namespace Umbraco.Core.Models.Identity
{
if (callback == null) throw new ArgumentNullException("callback");
_getLogins = callback;
+ }
+
+ #region Change tracking
+
+ public void DisableChangeTracking()
+ {
+ _tracker.DisableChangeTracking();
}
+
+ public void EnableChangeTracking()
+ {
+ _tracker.EnableChangeTracking();
+ }
+
+ ///
+ /// Since this class only has change tracking turned on for Email/Username this will return true if either of those have changed
+ ///
+ ///
+ public bool IsDirty()
+ {
+ return _tracker.IsDirty();
+ }
+
+ ///
+ /// Returns true if the specified property is dirty
+ ///
+ ///
+ ///
+ public bool IsPropertyDirty(string propName)
+ {
+ return _tracker.IsPropertyDirty(propName);
+ }
+
+ ///
+ /// Resets dirty properties
+ ///
+ void ICanBeDirty.ResetDirtyProperties()
+ {
+ _tracker.ResetDirtyProperties();
+ }
+
+ bool IRememberBeingDirty.WasDirty()
+ {
+ return _tracker.WasDirty();
+ }
+
+ bool IRememberBeingDirty.WasPropertyDirty(string propertyName)
+ {
+ return _tracker.WasPropertyDirty(propertyName);
+ }
+
+ void IRememberBeingDirty.ForgetPreviouslyDirtyProperties()
+ {
+ _tracker.ForgetPreviouslyDirtyProperties();
+ }
+
+ public void ResetDirtyProperties(bool rememberPreviouslyChangedProperties)
+ {
+ _tracker.ResetDirtyProperties(rememberPreviouslyChangedProperties);
+ }
+
+ private static readonly Lazy Ps = new Lazy();
+ private class PropertySelectors
+ {
+ public readonly PropertyInfo EmailSelector = ExpressionHelper.GetPropertyInfo(x => x.Email);
+ public readonly PropertyInfo UserNameSelector = ExpressionHelper.GetPropertyInfo(x => x.UserName);
+ public readonly PropertyInfo LastLoginDateUtcSelector = ExpressionHelper.GetPropertyInfo(x => x.LastLoginDateUtc);
+ public readonly PropertyInfo EmailConfirmedSelector = ExpressionHelper.GetPropertyInfo(x => x.EmailConfirmed);
+ public readonly PropertyInfo NameSelector = ExpressionHelper.GetPropertyInfo(x => x.Name);
+ public readonly PropertyInfo AccessFailedCountSelector = ExpressionHelper.GetPropertyInfo(x => x.AccessFailedCount);
+ public readonly PropertyInfo PasswordHashSelector = ExpressionHelper.GetPropertyInfo(x => x.PasswordHash);
+ public readonly PropertyInfo CultureSelector = ExpressionHelper.GetPropertyInfo(x => x.Culture);
+ public readonly PropertyInfo StartMediaIdsSelector = ExpressionHelper.GetPropertyInfo(x => x.StartMediaIds);
+ public readonly PropertyInfo StartContentIdsSelector = ExpressionHelper.GetPropertyInfo(x => x.StartContentIds);
+ public readonly PropertyInfo GroupsSelector = ExpressionHelper.GetPropertyInfo(x => x.Groups);
+ public readonly PropertyInfo LoginsSelector = ExpressionHelper.GetPropertyInfo>(x => x.Logins);
+ public readonly PropertyInfo RolesSelector = ExpressionHelper.GetPropertyInfo>>(x => x.Roles);
+
+ //Custom comparer for enumerables
+ public readonly DelegateEqualityComparer GroupsComparer = new DelegateEqualityComparer(
+ (groups, enumerable) => groups.Select(x => x.Alias).UnsortedSequenceEqual(enumerable.Select(x => x.Alias)),
+ groups => groups.GetHashCode());
+ public readonly DelegateEqualityComparer StartIdsComparer = new DelegateEqualityComparer(
+ (groups, enumerable) => groups.UnsortedSequenceEqual(enumerable),
+ groups => groups.GetHashCode());
+
+ }
+
+ private readonly ChangeTracker _tracker = new ChangeTracker();
+ private string _email;
+ private string _userName;
+ private int _id;
+ private bool _hasIdentity = false;
+ private DateTime? _lastLoginDateUtc;
+ private bool _emailConfirmed;
+ private string _name;
+ private int _accessFailedCount;
+ private string _passwordHash;
+ private string _culture;
+ private ObservableCollection _logins;
+ private Lazy> _getLogins;
+ private IReadOnlyUserGroup[] _groups;
+ private string[] _allowedSections;
+ private int[] _startMediaIds;
+ private int[] _startContentIds;
+
+ ///
+ /// internal class used to track changes for properties that have it enabled
+ ///
+ private class ChangeTracker : TracksChangesEntityBase
+ {
+ ///
+ /// Make this public so that it's usable
+ ///
+ ///
+ public new void OnPropertyChanged(PropertyInfo propertyInfo)
+ {
+ base.OnPropertyChanged(propertyInfo);
+ }
+ }
+ #endregion
+
+
}
}
\ No newline at end of file
diff --git a/src/Umbraco.Core/Models/Identity/IdentityModelMappings.cs b/src/Umbraco.Core/Models/Identity/IdentityModelMappings.cs
index 78b58fc0d6..c5a2245777 100644
--- a/src/Umbraco.Core/Models/Identity/IdentityModelMappings.cs
+++ b/src/Umbraco.Core/Models/Identity/IdentityModelMappings.cs
@@ -1,7 +1,7 @@
using System;
using System.Linq;
using AutoMapper;
-
+using Umbraco.Core.Models.EntityBase;
using Umbraco.Core.Models.Mapping;
using Umbraco.Core.Models.Membership;
using Umbraco.Core.Security;
@@ -13,28 +13,43 @@ namespace Umbraco.Core.Models.Identity
public override void ConfigureMappings(IConfiguration config, ApplicationContext applicationContext)
{
config.CreateMap()
+ .BeforeMap((user, identityUser) =>
+ {
+ identityUser.DisableChangeTracking();
+ })
+ .ConstructUsing(user => new BackOfficeIdentityUser(user.Id, user.Groups))
.ForMember(user => user.LastLoginDateUtc, expression => expression.MapFrom(user => user.LastLoginDate.ToUniversalTime()))
.ForMember(user => user.Email, expression => expression.MapFrom(user => user.Email))
+ .ForMember(user => user.EmailConfirmed, expression => expression.MapFrom(user => user.EmailConfirmedDate.HasValue))
.ForMember(user => user.Id, expression => expression.MapFrom(user => user.Id))
.ForMember(user => user.LockoutEndDateUtc, expression => expression.MapFrom(user => user.IsLockedOut ? DateTime.MaxValue.ToUniversalTime() : (DateTime?)null))
+ .ForMember(user => user.IsApproved, expression => expression.MapFrom(user => user.IsApproved))
.ForMember(user => user.UserName, expression => expression.MapFrom(user => user.Username))
.ForMember(user => user.PasswordHash, expression => expression.MapFrom(user => GetPasswordHash(user.RawPasswordValue)))
.ForMember(user => user.Culture, expression => expression.MapFrom(user => user.GetUserCulture(applicationContext.Services.TextService)))
.ForMember(user => user.Name, expression => expression.MapFrom(user => user.Name))
- .ForMember(user => user.StartMediaId, expression => expression.MapFrom(user => user.StartMediaId))
- .ForMember(user => user.StartContentId, expression => expression.MapFrom(user => user.StartContentId))
- .ForMember(user => user.UserTypeAlias, expression => expression.MapFrom(user => user.UserType.Alias))
+ .ForMember(user => user.StartMediaIds, expression => expression.MapFrom(user => user.StartMediaIds))
+ .ForMember(user => user.StartContentIds, expression => expression.MapFrom(user => user.StartContentIds))
.ForMember(user => user.AccessFailedCount, expression => expression.MapFrom(user => user.FailedPasswordAttempts))
- .ForMember(user => user.AllowedSections, expression => expression.MapFrom(user => user.AllowedSections.ToArray()));
-
+ .ForMember(user => user.CalculatedContentStartNodeIds, expression => expression.MapFrom(user => user.CalculateContentStartNodeIds(applicationContext.Services.EntityService)))
+ .ForMember(user => user.CalculatedMediaStartNodeIds, expression => expression.MapFrom(user => user.CalculateMediaStartNodeIds(applicationContext.Services.EntityService)))
+ .ForMember(user => user.AllowedSections, expression => expression.MapFrom(user => user.AllowedSections.ToArray()))
+ .AfterMap((user, identityUser) =>
+ {
+ identityUser.ResetDirtyProperties(true);
+ identityUser.EnableChangeTracking();
+ });
+
config.CreateMap()
.ConstructUsing((BackOfficeIdentityUser user) => new UserData(Guid.NewGuid().ToString("N"))) //this is the 'session id'
.ForMember(detail => detail.Id, opt => opt.MapFrom(user => user.Id))
.ForMember(detail => detail.AllowedApplications, opt => opt.MapFrom(user => user.AllowedSections))
+ .ForMember(detail => detail.Roles, opt => opt.MapFrom(user => user.Roles.Select(x => x.RoleId).ToArray()))
.ForMember(detail => detail.RealName, opt => opt.MapFrom(user => user.Name))
- .ForMember(detail => detail.Roles, opt => opt.MapFrom(user => new[] { user.UserTypeAlias }))
- .ForMember(detail => detail.StartContentNode, opt => opt.MapFrom(user => user.StartContentId))
- .ForMember(detail => detail.StartMediaNode, opt => opt.MapFrom(user => user.StartMediaId))
+ //When mapping to UserData which is used in the authcookie we want ALL start nodes including ones defined on the groups
+ .ForMember(detail => detail.StartContentNodes, opt => opt.MapFrom(user => user.CalculatedContentStartNodeIds))
+ //When mapping to UserData which is used in the authcookie we want ALL start nodes including ones defined on the groups
+ .ForMember(detail => detail.StartMediaNodes, opt => opt.MapFrom(user => user.CalculatedMediaStartNodeIds))
.ForMember(detail => detail.Username, opt => opt.MapFrom(user => user.UserName))
.ForMember(detail => detail.Culture, opt => opt.MapFrom(user => user.Culture))
.ForMember(detail => detail.SessionId, opt => opt.MapFrom(user => user.SecurityStamp.IsNullOrWhiteSpace() ? Guid.NewGuid().ToString("N") : user.SecurityStamp));
diff --git a/src/Umbraco.Core/Models/Membership/ContentPermissionSet.cs b/src/Umbraco.Core/Models/Membership/ContentPermissionSet.cs
new file mode 100644
index 0000000000..ca0a910c05
--- /dev/null
+++ b/src/Umbraco.Core/Models/Membership/ContentPermissionSet.cs
@@ -0,0 +1,54 @@
+using System;
+using System.Collections.Generic;
+using Umbraco.Core.Models.EntityBase;
+
+namespace Umbraco.Core.Models.Membership
+{
+ ///
+ /// Represents an -> user group & permission key value pair collection
+ ///
+ ///
+ /// This implements purely so it can be used with the repository layer which is why it's explicitly implemented.
+ ///
+ public class ContentPermissionSet : EntityPermissionSet, IAggregateRoot
+ {
+ private readonly IContent _content;
+
+ public ContentPermissionSet(IContent content, EntityPermissionCollection permissionsSet)
+ : base(content.Id, permissionsSet)
+ {
+ _content = content;
+ }
+
+ public override int EntityId
+ {
+ get { return _content.Id; }
+ }
+
+ #region Explicit implementation of IAggregateRoot
+ int IEntity.Id
+ {
+ get { return EntityId; }
+ set { throw new NotImplementedException(); }
+ }
+
+ bool IEntity.HasIdentity
+ {
+ get { return EntityId > 0; }
+ }
+
+ Guid IEntity.Key { get; set; }
+
+ DateTime IEntity.CreateDate { get; set; }
+
+ DateTime IEntity.UpdateDate { get; set; }
+
+ DateTime? IDeletableEntity.DeletedDate { get; set; }
+
+ object IDeepCloneable.DeepClone()
+ {
+ throw new NotImplementedException();
+ }
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Core/Models/Membership/EntityPermission.cs b/src/Umbraco.Core/Models/Membership/EntityPermission.cs
index 7ab1ddc817..8f609c0e13 100644
--- a/src/Umbraco.Core/Models/Membership/EntityPermission.cs
+++ b/src/Umbraco.Core/Models/Membership/EntityPermission.cs
@@ -1,26 +1,66 @@
-using System.Collections;
+using System;
namespace Umbraco.Core.Models.Membership
{
///
- /// Represents a user -> entity permission
+ /// Represents an entity permission (defined on the user group and derived to retrieve permissions for a given user)
///
- public class EntityPermission
+ public class EntityPermission : IEquatable
{
- public EntityPermission(int userId, int entityId, string[] assignedPermissions)
+ public EntityPermission(int groupId, int entityId, string[] assignedPermissions)
{
- UserId = userId;
+ UserGroupId = groupId;
EntityId = entityId;
AssignedPermissions = assignedPermissions;
+ IsDefaultPermissions = false;
+ }
+
+ public EntityPermission(int groupId, int entityId, string[] assignedPermissions, bool isDefaultPermissions)
+ {
+ UserGroupId = groupId;
+ EntityId = entityId;
+ AssignedPermissions = assignedPermissions;
+ IsDefaultPermissions = isDefaultPermissions;
}
- public int UserId { get; private set; }
public int EntityId { get; private set; }
+ public int UserGroupId { get; private set; }
///
/// The assigned permissions for the user/entity combo
///
public string[] AssignedPermissions { get; private set; }
+
+ ///
+ /// True if the permissions assigned to this object are the group's default permissions and not explicitly defined permissions
+ ///
+ ///
+ /// This will be the case when looking up entity permissions and falling back to the default permissions
+ ///
+ public bool IsDefaultPermissions { get; private set; }
+
+ public bool Equals(EntityPermission other)
+ {
+ if (ReferenceEquals(null, other)) return false;
+ if (ReferenceEquals(this, other)) return true;
+ return EntityId == other.EntityId && UserGroupId == other.UserGroupId;
+ }
+
+ public override bool Equals(object obj)
+ {
+ if (ReferenceEquals(null, obj)) return false;
+ if (ReferenceEquals(this, obj)) return true;
+ if (obj.GetType() != this.GetType()) return false;
+ return Equals((EntityPermission) obj);
+ }
+
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ return (EntityId * 397) ^ UserGroupId;
+ }
+ }
}
}
\ No newline at end of file
diff --git a/src/Umbraco.Core/Models/Membership/EntityPermissionCollection.cs b/src/Umbraco.Core/Models/Membership/EntityPermissionCollection.cs
new file mode 100644
index 0000000000..5fca079cfc
--- /dev/null
+++ b/src/Umbraco.Core/Models/Membership/EntityPermissionCollection.cs
@@ -0,0 +1,34 @@
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Umbraco.Core.Models.Membership
+{
+ ///
+ /// A of
+ ///
+ public class EntityPermissionCollection : HashSet
+ {
+ public EntityPermissionCollection()
+ {
+ }
+
+ public EntityPermissionCollection(IEnumerable collection) : base(collection)
+ {
+ }
+
+ ///
+ /// Returns the aggregate permissions in the permission set
+ ///
+ ///
+ ///
+ /// This value is only calculated once
+ ///
+ public IEnumerable GetAllPermissions()
+ {
+ return _aggregatePermissions ?? (_aggregatePermissions =
+ this.SelectMany(x => x.AssignedPermissions).Distinct().ToArray());
+ }
+
+ private string[] _aggregatePermissions;
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Core/Models/Membership/EntityPermissionSet.cs b/src/Umbraco.Core/Models/Membership/EntityPermissionSet.cs
index c4669caf59..c33c4aa315 100644
--- a/src/Umbraco.Core/Models/Membership/EntityPermissionSet.cs
+++ b/src/Umbraco.Core/Models/Membership/EntityPermissionSet.cs
@@ -1,59 +1,55 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
+using System.Linq;
namespace Umbraco.Core.Models.Membership
{
///
- /// Represents an entity -> user & permission key value pair collection
- ///
+ /// Represents an entity -> user group & permission key value pair collection
+ ///
public class EntityPermissionSet
{
+ private static readonly Lazy EmptyInstance = new Lazy(() => new EntityPermissionSet(-1, new EntityPermissionCollection()));
+ ///
+ /// Returns an empty permission set
+ ///
+ ///
+ public static EntityPermissionSet Empty()
+ {
+ return EmptyInstance.Value;
+ }
+
+ public EntityPermissionSet(int entityId, EntityPermissionCollection permissionsSet)
+ {
+ EntityId = entityId;
+ PermissionsSet = permissionsSet;
+ }
+
///
/// The entity id with permissions assigned
///
- public int EntityId { get; private set; }
+ public virtual int EntityId { get; private set; }
///
- /// The key/value pairs of user id & single permission
+ /// The key/value pairs of user group id & single permission
///
- public IEnumerable UserPermissionsSet { get; private set; }
+ public EntityPermissionCollection PermissionsSet { get; private set; }
- public EntityPermissionSet(int entityId, IEnumerable userPermissionsSet)
+
+ ///
+ /// Returns the aggregate permissions in the permission set
+ ///
+ ///
+ ///
+ /// This value is only calculated once
+ ///
+ public IEnumerable GetAllPermissions()
{
- EntityId = entityId;
- UserPermissionsSet = userPermissionsSet;
+ return PermissionsSet.GetAllPermissions();
}
- public class UserPermission
- {
- public UserPermission(int userId, string permission)
- {
- UserId = userId;
- Permission = permission;
- }
- public int UserId { get; private set; }
- public string Permission { get; private set; }
- protected bool Equals(UserPermission other)
- {
- return UserId == other.UserId && string.Equals(Permission, other.Permission);
- }
- public override bool Equals(object obj)
- {
- if (ReferenceEquals(null, obj)) return false;
- if (ReferenceEquals(this, obj)) return true;
- if (obj.GetType() != this.GetType()) return false;
- return Equals((UserPermission) obj);
- }
-
- public override int GetHashCode()
- {
- unchecked
- {
- return (UserId*397) ^ Permission.GetHashCode();
- }
- }
- }
}
}
\ No newline at end of file
diff --git a/src/Umbraco.Core/Models/Membership/IProfile.cs b/src/Umbraco.Core/Models/Membership/IProfile.cs
index 749c3371b8..0e6267a10b 100644
--- a/src/Umbraco.Core/Models/Membership/IProfile.cs
+++ b/src/Umbraco.Core/Models/Membership/IProfile.cs
@@ -1,15 +1,11 @@
namespace Umbraco.Core.Models.Membership
{
///
- /// Defines the the Profile interface
- ///
- ///
- /// This interface is pretty useless but has been exposed publicly from 6.x so we're stuck with it. It would make more sense
- /// if the Id was an int but since it's not people have to cast it to int all of the time!
- ///
+ /// Defines the the User Profile interface
+ ///
public interface IProfile
{
- object Id { get; set; }
- string Name { get; set; }
+ int Id { get; }
+ string Name { get; }
}
}
\ No newline at end of file
diff --git a/src/Umbraco.Core/Models/Membership/IReadOnlyUserGroup.cs b/src/Umbraco.Core/Models/Membership/IReadOnlyUserGroup.cs
new file mode 100644
index 0000000000..deebc03401
--- /dev/null
+++ b/src/Umbraco.Core/Models/Membership/IReadOnlyUserGroup.cs
@@ -0,0 +1,31 @@
+using System.Collections.Generic;
+
+namespace Umbraco.Core.Models.Membership
+{
+ ///
+ /// A readonly user group providing basic information
+ ///
+ public interface IReadOnlyUserGroup
+ {
+ string Name { get; }
+ string Icon { get; }
+ int Id { get; }
+ int? StartContentId { get; }
+ int? StartMediaId { get; }
+
+ ///
+ /// The alias
+ ///
+ string Alias { get; }
+
+ ///
+ /// The set of default permissions
+ ///
+ ///
+ /// By default each permission is simply a single char but we've made this an enumerable{string} to support a more flexible permissions structure in the future.
+ ///
+ IEnumerable Permissions { get; set; }
+
+ IEnumerable AllowedSections { get; }
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Core/Models/Membership/IUser.cs b/src/Umbraco.Core/Models/Membership/IUser.cs
index f1f9c23971..b9d9447343 100644
--- a/src/Umbraco.Core/Models/Membership/IUser.cs
+++ b/src/Umbraco.Core/Models/Membership/IUser.cs
@@ -1,6 +1,7 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
using Umbraco.Core.Models.EntityBase;
-using Umbraco.Core.Persistence.Mappers;
namespace Umbraco.Core.Models.Membership
{
@@ -10,29 +11,50 @@ namespace Umbraco.Core.Models.Membership
/// Will be left internal until a proper Membership implementation is part of the roadmap
public interface IUser : IMembershipUser, IRememberBeingDirty, ICanBeDirty
{
+ UserState UserState { get; }
+
string Name { get; set; }
int SessionTimeout { get; set; }
- int StartContentId { get; set; }
- int StartMediaId { get; set; }
- string Language { get; set; }
+
+ [Obsolete("This should not be used it exists for legacy reasons only, use user groups instead, it will be removed in future versions")]
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ int StartContentId { get; set; }
+
+ int[] StartContentIds { get; set; }
+
+ [Obsolete("This should not be used it exists for legacy reasons only, use user groups instead, it will be removed in future versions")]
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ int StartMediaId { get; set; }
+
+ int[] StartMediaIds { get; set; }
+
+ string Language { get; set; }
+
+ [Obsolete("This should not be used it exists for legacy reasons only, use user groups instead, it will be removed in future versions")]
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ IUserType UserType { get; set; }
+
+ DateTime? EmailConfirmedDate { get; set; }
+ DateTime? InvitedDate { get; set; }
+
+ ///
+ /// Gets the groups that user is part of
+ ///
+ IEnumerable Groups { get; }
+
+ void RemoveGroup(string group);
+ void ClearGroups();
+ void AddGroup(IReadOnlyUserGroup group);
- ///
- /// Gets/sets the user type for the user
- ///
- IUserType UserType { get; set; }
-
- //TODO: This should be a private set
- ///
- /// The default permission set for the user
- ///
- ///
- /// Currently in umbraco each permission is a single char but with an Enumerable{string} collection this allows for flexible changes to this in the future
- ///
- IEnumerable DefaultPermissions { get; set; }
-
IEnumerable AllowedSections { get; }
+
+ [Obsolete("This should not be used it exists for legacy reasons only, use user groups instead, it will be removed in future versions")]
+ [EditorBrowsable(EditorBrowsableState.Never)]
void RemoveAllowedSection(string sectionAlias);
- void AddAllowedSection(string sectionAlias);
+
+ [Obsolete("This should not be used it exists for legacy reasons only, use user groups instead, it will be removed in future versions")]
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ void AddAllowedSection(string sectionAlias);
///
/// Exposes the basic profile data
@@ -43,5 +65,10 @@ namespace Umbraco.Core.Models.Membership
/// The security stamp used by ASP.Net identity
///
string SecurityStamp { get; set; }
+
+ ///
+ /// Will hold the media file system relative path of the users custom avatar if they uploaded one
+ ///
+ string Avatar { get; set; }
}
}
\ No newline at end of file
diff --git a/src/Umbraco.Core/Models/Membership/IUserGroup.cs b/src/Umbraco.Core/Models/Membership/IUserGroup.cs
new file mode 100644
index 0000000000..0282073a2e
--- /dev/null
+++ b/src/Umbraco.Core/Models/Membership/IUserGroup.cs
@@ -0,0 +1,44 @@
+using System.Collections.Generic;
+using Umbraco.Core.Models.EntityBase;
+
+namespace Umbraco.Core.Models.Membership
+{
+ public interface IUserGroup : IAggregateRoot
+ {
+ string Alias { get; set; }
+
+ int? StartContentId { get; set; }
+ int? StartMediaId { get; set; }
+
+ ///
+ /// The icon
+ ///
+ string Icon { get; set; }
+
+ ///
+ /// The name
+ ///
+ string Name { get; set; }
+
+ ///
+ /// The set of default permissions
+ ///
+ ///
+ /// By default each permission is simply a single char but we've made this an enumerable{string} to support a more flexible permissions structure in the future.
+ ///
+ IEnumerable Permissions { get; set; }
+
+ IEnumerable AllowedSections { get; }
+
+ void RemoveAllowedSection(string sectionAlias);
+
+ void AddAllowedSection(string sectionAlias);
+
+ void ClearAllowedSections();
+
+ ///
+ /// Specifies the number of users assigned to this group
+ ///
+ int UserCount { get; }
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Core/Models/Membership/IUserType.cs b/src/Umbraco.Core/Models/Membership/IUserType.cs
index fe678afd2b..ba004cea4a 100644
--- a/src/Umbraco.Core/Models/Membership/IUserType.cs
+++ b/src/Umbraco.Core/Models/Membership/IUserType.cs
@@ -1,28 +1,17 @@
-using System.Collections.Generic;
-using Umbraco.Core.Models.EntityBase;
-using Umbraco.Core.Persistence.Mappers;
-
-namespace Umbraco.Core.Models.Membership
-{
-
- public interface IUserType : IAggregateRoot
- {
- ///
- /// The user type alias
- ///
- string Alias { get; set; }
-
- ///
- /// The user type name
- ///
- string Name { get; set; }
-
- ///
- /// The set of default permissions for the user type
- ///
- ///
- /// By default each permission is simply a single char but we've made this an enumerable{string} to support a more flexible permissions structure in the future.
- ///
- IEnumerable Permissions { get; set; }
- }
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using Umbraco.Core.Models.EntityBase;
+
+namespace Umbraco.Core.Models.Membership
+{
+ [Obsolete("This should not be used it exists for legacy reasons only, use user groups instead, it will be removed in future versions")]
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public interface IUserType : IAggregateRoot
+ {
+ string Alias { get; set; }
+ string Name { get; set; }
+ IEnumerable Permissions { get; set; }
+
+ }
}
\ No newline at end of file
diff --git a/src/Umbraco.Core/Models/Membership/ReadOnlyUserGroup.cs b/src/Umbraco.Core/Models/Membership/ReadOnlyUserGroup.cs
new file mode 100644
index 0000000000..ceb2671067
--- /dev/null
+++ b/src/Umbraco.Core/Models/Membership/ReadOnlyUserGroup.cs
@@ -0,0 +1,70 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Umbraco.Core.Models.Membership
+{
+ public class ReadOnlyUserGroup : IReadOnlyUserGroup, IEquatable
+ {
+ public ReadOnlyUserGroup(int id, string name, string icon, int? startContentId, int? startMediaId, string @alias,
+ IEnumerable allowedSections, IEnumerable permissions)
+ {
+ Name = name;
+ Icon = icon;
+ Id = id;
+ Alias = alias;
+ AllowedSections = allowedSections.ToArray();
+ Permissions = permissions.ToArray();
+
+ //Zero is invalid and will be treated as Null
+ StartContentId = startContentId == 0 ? null : startContentId;
+ StartMediaId = startMediaId == 0 ? null : startMediaId;
+ }
+
+ public int Id { get; private set; }
+ public string Name { get; private set; }
+ public string Icon { get; private set; }
+ public int? StartContentId { get; private set; }
+ public int? StartMediaId { get; private set; }
+ public string Alias { get; private set; }
+
+ ///
+ /// The set of default permissions
+ ///
+ ///
+ /// By default each permission is simply a single char but we've made this an enumerable{string} to support a more flexible permissions structure in the future.
+ ///
+ public IEnumerable Permissions { get; set; }
+ public IEnumerable AllowedSections { get; private set; }
+
+ public bool Equals(ReadOnlyUserGroup other)
+ {
+ if (ReferenceEquals(null, other)) return false;
+ if (ReferenceEquals(this, other)) return true;
+ return string.Equals(Alias, other.Alias);
+ }
+
+ public override bool Equals(object obj)
+ {
+ if (ReferenceEquals(null, obj)) return false;
+ if (ReferenceEquals(this, obj)) return true;
+ if (obj.GetType() != this.GetType()) return false;
+ return Equals((ReadOnlyUserGroup) obj);
+ }
+
+ public override int GetHashCode()
+ {
+ return Alias.GetHashCode();
+ }
+
+ public static bool operator ==(ReadOnlyUserGroup left, ReadOnlyUserGroup right)
+ {
+ return Equals(left, right);
+ }
+
+ public static bool operator !=(ReadOnlyUserGroup left, ReadOnlyUserGroup right)
+ {
+ return !Equals(left, right);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Core/Models/Membership/UmbracoMembershipUser.cs b/src/Umbraco.Core/Models/Membership/UmbracoMembershipUser.cs
index ada2a7102d..0ada5c3d0e 100644
--- a/src/Umbraco.Core/Models/Membership/UmbracoMembershipUser.cs
+++ b/src/Umbraco.Core/Models/Membership/UmbracoMembershipUser.cs
@@ -34,12 +34,11 @@ namespace Umbraco.Core.Models.Membership
/// The last lockout date.
/// The full name.
/// The language.
- /// Type of the user.
///
public UmbracoMembershipUser(string providerName, string name, object providerUserKey, string email,
string passwordQuestion, string comment, bool isApproved, bool isLockedOut,
DateTime creationDate, DateTime lastLoginDate, DateTime lastActivityDate, DateTime lastPasswordChangedDate,
- DateTime lastLockoutDate, string fullName, string language, IUserType userType, T user)
+ DateTime lastLockoutDate, string fullName, string language, T user)
: base( providerName, name, providerUserKey, email, passwordQuestion, comment, isApproved, isLockedOut,
creationDate, lastLoginDate, lastActivityDate, lastPasswordChangedDate, lastLockoutDate)
{
diff --git a/src/Umbraco.Core/Models/Membership/User.cs b/src/Umbraco.Core/Models/Membership/User.cs
index cadda14508..e01b070d18 100644
--- a/src/Umbraco.Core/Models/Membership/User.cs
+++ b/src/Umbraco.Core/Models/Membership/User.cs
@@ -2,13 +2,14 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
+using System.ComponentModel;
using System.Linq;
using System.Reflection;
using System.Runtime.Serialization;
using Umbraco.Core.Configuration;
+using Umbraco.Core.Logging;
using Umbraco.Core.Models.EntityBase;
-
namespace Umbraco.Core.Models.Membership
{
///
@@ -16,66 +17,109 @@ namespace Umbraco.Core.Models.Membership
///
[Serializable]
[DataContract(IsReference = true)]
- public class User : Entity, IUser
+ public class User : Entity, IUser, IProfile
{
- public User(IUserType userType)
+ ///
+ /// Constructor for creating a new/empty user
+ ///
+ public User()
{
- if (userType == null) throw new ArgumentNullException("userType");
-
- _userType = userType;
- _defaultPermissions = _userType.Permissions == null ? Enumerable.Empty() : new List(_userType.Permissions);
- //Groups = new List