From 934d03e63f1d9c7ffb0f73a900c733a2b72fdd76 Mon Sep 17 00:00:00 2001 From: Stephan Date: Thu, 24 Aug 2017 21:24:14 +0200 Subject: [PATCH] Port 7.7 - WIP --- src/Umbraco.Core/Composing/TypeFinder.cs | 5 +- src/Umbraco.Core/Composing/TypeLoader.cs | 29 +- src/Umbraco.Core/Constants-Applications.cs | 7 + src/Umbraco.Core/Constants-Conventions.cs | 8 + src/Umbraco.Core/Constants-ObjectTypes.cs | 24 +- src/Umbraco.Core/Constants-PropertyEditors.cs | 5 + src/Umbraco.Core/Constants-Security.cs | 5 + src/Umbraco.Core/DateTimeExtensions.cs | 38 ++- src/Umbraco.Core/EnumerableExtensions.cs | 3 + src/Umbraco.Core/ExpressionHelper.cs | 17 +- src/Umbraco.Core/Models/Content.cs | 15 +- src/Umbraco.Core/Models/ContentType.cs | 10 +- src/Umbraco.Core/Models/ContentTypeBase.cs | 15 +- src/Umbraco.Core/Models/DictionaryItem.cs | 11 +- src/Umbraco.Core/Models/IContent.cs | 9 +- src/Umbraco.Core/Models/Member.cs | 24 ++ .../Models/Packaging/InstallationSummary.cs | 2 +- src/Umbraco.Core/Models/Packaging/MetaData.cs | 2 +- .../Models/Packaging/PackageAction.cs | 4 +- src/Umbraco.Core/Models/Property.cs | 54 ++-- .../PublishedContent/PropertySetWrapped.cs | 14 +- .../PublishedContentWrapped.cs | 58 ++-- .../Models/Rdbms/DictionaryDto.cs | 3 +- src/Umbraco.Core/Models/Rdbms/User2AppDto.cs | 21 -- .../Models/Rdbms/User2UserGroupDto.cs | 19 ++ src/Umbraco.Core/Models/Rdbms/UserDto.cs | 59 +++- .../Models/Rdbms/UserGroup2AppDto.cs | 19 ++ ...nDto.cs => UserGroup2NodePermissionDto.cs} | 18 +- src/Umbraco.Core/Models/Rdbms/UserGroupDto.cs | 69 +++++ .../Models/Rdbms/UserStartNodeDto.cs | 67 +++++ src/Umbraco.Core/Models/Rdbms/UserTypeDto.cs | 4 +- src/Umbraco.Core/Models/UmbracoObjectTypes.cs | 19 +- .../Models/UmbracoObjectTypesExtensions.cs | 9 +- src/Umbraco.Core/Models/UserExtensions.cs | 274 ++++++++++++++++-- .../Packaging/Models/UninstallationSummary.cs | 2 +- .../ValueConverters/DecimalValueConverter.cs | 2 +- src/Umbraco.Core/StringExtensions.cs | 193 ++++++++---- src/Umbraco.Core/StringUdi.cs | 16 +- src/Umbraco.Core/Strings/CleanStringType.cs | 6 +- .../Strings/DefaultShortStringHelper.cs | 17 +- .../Strings/DefaultShortStringHelperConfig.cs | 19 +- .../Strings/Utf8ToAsciiConverter.cs | 22 +- src/Umbraco.Core/UdiEntityType.cs | 15 +- src/Umbraco.Core/UdiGetterExtensions.cs | 31 +- 44 files changed, 998 insertions(+), 265 deletions(-) delete mode 100644 src/Umbraco.Core/Models/Rdbms/User2AppDto.cs create mode 100644 src/Umbraco.Core/Models/Rdbms/User2UserGroupDto.cs create mode 100644 src/Umbraco.Core/Models/Rdbms/UserGroup2AppDto.cs rename src/Umbraco.Core/Models/Rdbms/{User2NodePermissionDto.cs => UserGroup2NodePermissionDto.cs} (54%) create mode 100644 src/Umbraco.Core/Models/Rdbms/UserGroupDto.cs create mode 100644 src/Umbraco.Core/Models/Rdbms/UserStartNodeDto.cs diff --git a/src/Umbraco.Core/Composing/TypeFinder.cs b/src/Umbraco.Core/Composing/TypeFinder.cs index dc0f3eb7ea..07e088eaed 100644 --- a/src/Umbraco.Core/Composing/TypeFinder.cs +++ b/src/Umbraco.Core/Composing/TypeFinder.cs @@ -7,6 +7,7 @@ using System.Security; using System.Text; using System.Web; using System.Web.Compilation; +using System.Web.Hosting; using Umbraco.Core.Composing; using Umbraco.Core.IO; @@ -44,7 +45,7 @@ namespace Umbraco.Core.Composing HashSet assemblies = null; try { - var isHosted = HttpContext.Current != null; + var isHosted = HttpContext.Current != null || HostingEnvironment.IsHosted; try { @@ -55,7 +56,7 @@ namespace Umbraco.Core.Composing } catch (InvalidOperationException e) { - if ((e.InnerException is SecurityException) == false) + if (e.InnerException is SecurityException == false) throw; } diff --git a/src/Umbraco.Core/Composing/TypeLoader.cs b/src/Umbraco.Core/Composing/TypeLoader.cs index 58d8d80432..83efcc89f0 100644 --- a/src/Umbraco.Core/Composing/TypeLoader.cs +++ b/src/Umbraco.Core/Composing/TypeLoader.cs @@ -5,6 +5,7 @@ using System.IO; using System.Linq; using System.Reflection; using System.Text; +using System.Threading; using System.Web.Compilation; using Umbraco.Core.Cache; using Umbraco.Core.IO; @@ -285,6 +286,9 @@ namespace Umbraco.Core.Composing #region Cache + private const int ListFileOpenReadTimeout = 4000; // milliseconds + private const int ListFileOpenWriteTimeout = 2000; // milliseconds + // internal for tests internal Attempt> TryGetCached(Type baseType, Type attributeType) { @@ -327,7 +331,7 @@ namespace Umbraco.Core.Composing if (File.Exists(filePath) == false) return cache; - using (var stream = File.OpenRead(filePath)) + using (var stream = GetFileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read, ListFileOpenReadTimeout)) using (var reader = new StreamReader(stream)) { while (true) @@ -382,7 +386,7 @@ namespace Umbraco.Core.Composing { var filePath = GeTypesListFilePath(); - using (var stream = File.Open(filePath, FileMode.Create, FileAccess.ReadWrite)) + using (var stream = GetFileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None, ListFileOpenWriteTimeout)) using (var writer = new StreamWriter(stream)) { foreach (var typeList in _types.Values) @@ -423,6 +427,27 @@ namespace Umbraco.Core.Composing _runtimeCache.ClearCacheItem(CacheKey); } + private Stream GetFileStream(string path, FileMode fileMode, FileAccess fileAccess, FileShare fileShare, int timeoutMilliseconds) + { + const int pauseMilliseconds = 250; + var attempts = timeoutMilliseconds / pauseMilliseconds; + while (true) + { + try + { + return new FileStream(path, fileMode, fileAccess, fileShare); + } + catch + { + if (--attempts == 0) + throw; + + _logger.Logger.Debug($"Attempted to get filestream for file {path} failed, {attempts} attempts left, pausing for {pauseMilliseconds} milliseconds"); + Thread.Sleep(pauseMilliseconds); + } + } + } + #endregion #region Get Types diff --git a/src/Umbraco.Core/Constants-Applications.cs b/src/Umbraco.Core/Constants-Applications.cs index c917d62154..3f5d659f39 100644 --- a/src/Umbraco.Core/Constants-Applications.cs +++ b/src/Umbraco.Core/Constants-Applications.cs @@ -58,6 +58,11 @@ /// public const string Content = "content"; + /// + /// alias for the content blueprint tree. + /// + public const string ContentBlueprints = "contentBlueprints"; + /// /// alias for the member tree. /// @@ -139,6 +144,8 @@ public const string PartialViewMacros = "partialViewMacros"; + 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 0d2c8b9887..9674a86fd8 100644 --- a/src/Umbraco.Core/Constants-Conventions.cs +++ b/src/Umbraco.Core/Constants-Conventions.cs @@ -11,6 +11,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 41e6a0bc4a..ccaacf6d5b 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. /// @@ -212,6 +222,16 @@ namespace Umbraco.Core /// Guid for a Forms DataSource. /// public static readonly Guid FormsDataSourceGuid = new Guid(FormsDataSource); + + /// + /// Guid for a Language. + /// + public const string Language = "6B05D05B-EC78-49BE-A4E4-79E274F07A77"; + + /// + /// Guid for a Forms DataSource. + /// + public static readonly Guid LanguageGuid = new Guid(Language); } } -} +} \ No newline at end of file diff --git a/src/Umbraco.Core/Constants-PropertyEditors.cs b/src/Umbraco.Core/Constants-PropertyEditors.cs index 0837a9bcb9..c31fdf1544 100644 --- a/src/Umbraco.Core/Constants-PropertyEditors.cs +++ b/src/Umbraco.Core/Constants-PropertyEditors.cs @@ -440,6 +440,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 e02e2cb988..6db909ebe6 100644 --- a/src/Umbraco.Core/Constants-Security.cs +++ b/src/Umbraco.Core/Constants-Security.cs @@ -8,12 +8,17 @@ namespace Umbraco.Core public static class Security { + public const string AdminGroupAlias = "admin"; + public const string BackOfficeAuthenticationType = "UmbracoBackOffice"; public const string BackOfficeExternalAuthenticationType = "UmbracoExternalCookie"; public const string BackOfficeExternalCookieName = "UMB_EXTLOGIN"; public const string BackOfficeTokenAuthenticationType = "UmbracoBackOfficeToken"; 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/EnumerableExtensions.cs b/src/Umbraco.Core/EnumerableExtensions.cs index 9b03a3a901..e5eb7f2d74 100644 --- a/src/Umbraco.Core/EnumerableExtensions.cs +++ b/src/Umbraco.Core/EnumerableExtensions.cs @@ -135,6 +135,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/ExpressionHelper.cs b/src/Umbraco.Core/ExpressionHelper.cs index 10b0eda8e6..0225dfcf2e 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/Models/Content.cs b/src/Umbraco.Core/Models/Content.cs index 94b102287e..8b3c72a0ac 100644 --- a/src/Umbraco.Core/Models/Content.cs +++ b/src/Umbraco.Core/Models/Content.cs @@ -21,7 +21,6 @@ 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 @@ -82,7 +81,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); } /// @@ -203,16 +201,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 /// @@ -285,6 +273,9 @@ namespace Umbraco.Core.Models [IgnoreDataMember] internal DateTime PublishedDate { get; set; } + [DataMember] + public bool IsBlueprint { get; internal set; } + public override void ResetDirtyProperties(bool rememberPreviouslyChangedProperties) { base.ResetDirtyProperties(rememberPreviouslyChangedProperties); diff --git a/src/Umbraco.Core/Models/ContentType.cs b/src/Umbraco.Core/Models/ContentType.cs index c831236fde..882ce5cc25 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 e3d66e72ad..772be82769 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 9e2b20eede..ff1e26165f 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/IContent.cs b/src/Umbraco.Core/Models/IContent.cs index b33aa072bf..c879455ec4 100644 --- a/src/Umbraco.Core/Models/IContent.cs +++ b/src/Umbraco.Core/Models/IContent.cs @@ -84,6 +84,11 @@ namespace Umbraco.Core.Models /// /// Gets the unique identifier of the published version, if any. /// - Guid PublishedVersionGuid { get; } + 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/Member.cs b/src/Umbraco.Core/Models/Member.cs index 589457f251..0b4435585d 100644 --- a/src/Umbraco.Core/Models/Member.cs +++ b/src/Umbraco.Core/Models/Member.cs @@ -106,6 +106,30 @@ namespace Umbraco.Core.Models IsApproved = true; } + /// + /// Constructor for creating a Member object + /// + /// + /// + /// + /// + /// The password value passed in to this parameter should be the encoded/encrypted/hashed format of the member's password + /// + /// + /// + public Member(string name, string email, string username, string rawPasswordValue, IMemberType contentType, bool isApproved) + : base(name, -1, contentType, new PropertyCollection()) + { + Mandate.ParameterNotNull(contentType, "contentType"); + + _contentTypeAlias = contentType.Alias; + _contentType = contentType; + _email = email; + _username = username; + _rawPasswordValue = rawPasswordValue; + IsApproved = isApproved; + } + private static readonly Lazy Ps = new Lazy(); private class PropertySelectors diff --git a/src/Umbraco.Core/Models/Packaging/InstallationSummary.cs b/src/Umbraco.Core/Models/Packaging/InstallationSummary.cs index ca05b4533f..4ba6a7a1c7 100644 --- a/src/Umbraco.Core/Models/Packaging/InstallationSummary.cs +++ b/src/Umbraco.Core/Models/Packaging/InstallationSummary.cs @@ -6,7 +6,7 @@ namespace Umbraco.Core.Models.Packaging { [Serializable] [DataContract(IsReference = true)] - internal class InstallationSummary + public class InstallationSummary { public MetaData MetaData { get; set; } public IEnumerable DataTypesInstalled { get; set; } diff --git a/src/Umbraco.Core/Models/Packaging/MetaData.cs b/src/Umbraco.Core/Models/Packaging/MetaData.cs index 32e3996794..a1cb450c5b 100644 --- a/src/Umbraco.Core/Models/Packaging/MetaData.cs +++ b/src/Umbraco.Core/Models/Packaging/MetaData.cs @@ -5,7 +5,7 @@ namespace Umbraco.Core.Models.Packaging { [Serializable] [DataContract(IsReference = true)] - internal class MetaData + public class MetaData { public string Name { get; set; } public string Version { get; set; } diff --git a/src/Umbraco.Core/Models/Packaging/PackageAction.cs b/src/Umbraco.Core/Models/Packaging/PackageAction.cs index 115899be8a..7ad86977a6 100644 --- a/src/Umbraco.Core/Models/Packaging/PackageAction.cs +++ b/src/Umbraco.Core/Models/Packaging/PackageAction.cs @@ -4,7 +4,7 @@ using System.Xml.Linq; namespace Umbraco.Core.Models.Packaging { - internal enum ActionRunAt + public enum ActionRunAt { Undefined = 0, Install, @@ -13,7 +13,7 @@ namespace Umbraco.Core.Models.Packaging [Serializable] [DataContract(IsReference = true)] - internal class PackageAction + public class PackageAction { private ActionRunAt _runAt; private bool? _undo; diff --git a/src/Umbraco.Core/Models/Property.cs b/src/Umbraco.Core/Models/Property.cs index 5104f912ce..697241d715 100644 --- a/src/Umbraco.Core/Models/Property.cs +++ b/src/Umbraco.Core/Models/Property.cs @@ -20,9 +20,7 @@ namespace Umbraco.Core.Models private readonly PropertyTags _tagSupport = new PropertyTags(); protected Property() - { - - } + { } public Property(PropertyType propertyType) { @@ -49,36 +47,36 @@ namespace Umbraco.Core.Models { public readonly PropertyInfo ValueSelector = ExpressionHelper.GetPropertyInfo(x => x.Value); public readonly PropertyInfo VersionSelector = ExpressionHelper.GetPropertyInfo(x => x.Version); - } - private static readonly DelegateEqualityComparer ValueComparer = new DelegateEqualityComparer( - (o, o1) => - { - if (o == null && o1 == null) return true; - - //custom comparer for strings. - if (o is string || o1 is string) + public readonly DelegateEqualityComparer PropertyValueComparer = new DelegateEqualityComparer( + (o, o1) => { - //if one is null and another is empty then they are the same - if ((o as string).IsNullOrWhiteSpace() && (o1 as string).IsNullOrWhiteSpace()) + if (o == null && o1 == null) return true; + + //custom comparer for strings. + if (o is string || o1 is string) { - return true; + //if one is null and another is empty then they are the same + if ((o as string).IsNullOrWhiteSpace() && (o1 as string).IsNullOrWhiteSpace()) + { + return true; + } + if (o == null || o1 == null) return false; + return o.Equals(o1); } + if (o == null || o1 == null) return false; + + //Custom comparer for enumerable if it is enumerable + var enum1 = o as IEnumerable; + var enum2 = o1 as IEnumerable; + if (enum1 != null && enum2 != null) + { + return enum1.Cast().UnsortedSequenceEqual(enum2.Cast()); + } return o.Equals(o1); - } - - if (o == null || o1 == null) return false; - - //Custom comparer for enumerable if it is enumerable - var enum1 = o as IEnumerable; - var enum2 = o1 as IEnumerable; - if (enum1 != null && enum2 != null) - { - return enum1.Cast().UnsortedSequenceEqual(enum2.Cast()); - } - return o.Equals(o1); - }, o => o.GetHashCode()); + }, o => o.GetHashCode()); + } /// /// Returns the instance of the tag support, by default tags are not enabled @@ -200,7 +198,7 @@ namespace Umbraco.Core.Models } } - SetPropertyValueAndDetectChanges(value, ref _value, Ps.Value.ValueSelector, ValueComparer); + SetPropertyValueAndDetectChanges(value, ref _value, Ps.Value.ValueSelector, Ps.Value.PropertyValueComparer); } } diff --git a/src/Umbraco.Core/Models/PublishedContent/PropertySetWrapped.cs b/src/Umbraco.Core/Models/PublishedContent/PropertySetWrapped.cs index 6330093e72..4041d43c8c 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PropertySetWrapped.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PropertySetWrapped.cs @@ -9,7 +9,7 @@ namespace Umbraco.Core.Models.PublishedContent /// public abstract class PropertySetWrapped : IPropertySet { - protected readonly IPropertySet Content; + private readonly IPropertySet _content; /// /// Initializes a new instance of the class @@ -18,25 +18,25 @@ namespace Umbraco.Core.Models.PublishedContent /// The content to wrap. protected PropertySetWrapped(IPropertySet content) { - Content = content; + _content = content; } /// /// Gets the wrapped content. /// /// The wrapped content, that was passed as an argument to the constructor. - public IPropertySet Unwrap() => Content; + public IPropertySet Unwrap() => _content; /// - public PublishedContentType ContentType => Content.ContentType; + public PublishedContentType ContentType => _content.ContentType; /// - public Guid Key => Content.Key; + public Guid Key => _content.Key; /// - public IEnumerable Properties => Content.Properties; + public IEnumerable Properties => _content.Properties; /// - public IPublishedProperty GetProperty(string alias) => Content.GetProperty(alias); + public IPublishedProperty GetProperty(string alias) => _content.GetProperty(alias); } } diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentWrapped.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentWrapped.cs index e8d879769f..da3add40e5 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentWrapped.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentWrapped.cs @@ -20,7 +20,7 @@ namespace Umbraco.Core.Models.PublishedContent /// public abstract class PublishedContentWrapped : IPublishedContent { - protected readonly IPublishedContent Content; + private readonly IPublishedContent _content; /// /// Initialize a new instance of the class @@ -29,85 +29,85 @@ namespace Umbraco.Core.Models.PublishedContent /// The content to wrap. protected PublishedContentWrapped(IPublishedContent content) { - Content = content; + _content = content; } /// /// Gets the wrapped content. /// /// The wrapped content, that was passed as an argument to the constructor. - public IPublishedContent Unwrap() => Content; + public IPublishedContent Unwrap() => _content; #region ContentType - public virtual PublishedContentType ContentType => Content.ContentType; + public virtual PublishedContentType ContentType => _content.ContentType; #endregion #region Content - public virtual int Id => Content.Id; + public virtual int Id => _content.Id; - public Guid Key => Content.Key; + public Guid Key => _content.Key; - public virtual int TemplateId => Content.TemplateId; + public virtual int TemplateId => _content.TemplateId; - public virtual int SortOrder => Content.SortOrder; + public virtual int SortOrder => _content.SortOrder; - public virtual string Name => Content.Name; + public virtual string Name => _content.Name; - public virtual string UrlName => Content.UrlName; + public virtual string UrlName => _content.UrlName; - public virtual string DocumentTypeAlias => Content.DocumentTypeAlias; + public virtual string DocumentTypeAlias => _content.DocumentTypeAlias; - public virtual int DocumentTypeId => Content.DocumentTypeId; + public virtual int DocumentTypeId => _content.DocumentTypeId; - public virtual string WriterName => Content.WriterName; + public virtual string WriterName => _content.WriterName; - public virtual string CreatorName => Content.CreatorName; + public virtual string CreatorName => _content.CreatorName; - public virtual int WriterId => Content.WriterId; + public virtual int WriterId => _content.WriterId; - public virtual int CreatorId => Content.CreatorId; + public virtual int CreatorId => _content.CreatorId; - public virtual string Path => Content.Path; + public virtual string Path => _content.Path; - public virtual DateTime CreateDate => Content.CreateDate; + public virtual DateTime CreateDate => _content.CreateDate; - public virtual DateTime UpdateDate => Content.UpdateDate; + public virtual DateTime UpdateDate => _content.UpdateDate; - public virtual Guid Version => Content.Version; + public virtual Guid Version => _content.Version; - public virtual int Level => Content.Level; + public virtual int Level => _content.Level; - public virtual string Url => Content.Url; + public virtual string Url => _content.Url; - public virtual PublishedItemType ItemType => Content.ItemType; + public virtual PublishedItemType ItemType => _content.ItemType; - public virtual bool IsDraft => Content.IsDraft; + public virtual bool IsDraft => _content.IsDraft; #endregion #region Tree - public virtual IPublishedContent Parent => Content.Parent; + public virtual IPublishedContent Parent => _content.Parent; - public virtual IEnumerable Children => Content.Children; + public virtual IEnumerable Children => _content.Children; #endregion #region Properties - public virtual IEnumerable Properties => Content.Properties; + public virtual IEnumerable Properties => _content.Properties; public virtual IPublishedProperty GetProperty(string alias) { - return Content.GetProperty(alias); + return _content.GetProperty(alias); } public virtual IPublishedProperty GetProperty(string alias, bool recurse) { - return Content.GetProperty(alias, recurse); + return _content.GetProperty(alias, recurse); } #endregion diff --git a/src/Umbraco.Core/Models/Rdbms/DictionaryDto.cs b/src/Umbraco.Core/Models/Rdbms/DictionaryDto.cs index be5f2a9495..8b4a078f35 100644 --- a/src/Umbraco.Core/Models/Rdbms/DictionaryDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/DictionaryDto.cs @@ -25,7 +25,8 @@ namespace Umbraco.Core.Models.Rdbms public Guid? Parent { get; set; } [Column("key")] - [Length(1000)] + [Length(450)] + [Index(IndexTypes.NonClustered, Name = "IX_cmsDictionary_key")] public string Key { get; set; } [ResultColumn] diff --git a/src/Umbraco.Core/Models/Rdbms/User2AppDto.cs b/src/Umbraco.Core/Models/Rdbms/User2AppDto.cs deleted file mode 100644 index eba72562ed..0000000000 --- a/src/Umbraco.Core/Models/Rdbms/User2AppDto.cs +++ /dev/null @@ -1,21 +0,0 @@ -using NPoco; -using Umbraco.Core.Persistence; -using Umbraco.Core.Persistence.DatabaseAnnotations; - -namespace Umbraco.Core.Models.Rdbms -{ - [TableName("umbracoUser2app")] - [PrimaryKey("user", AutoIncrement = false)] - [ExplicitColumns] - internal class User2AppDto - { - [Column("user")] - [PrimaryKeyColumn(AutoIncrement = false, Name = "PK_user2app", OnColumns = "user, app")] - [ForeignKey(typeof(UserDto))] - public int UserId { get; set; } - - [Column("app")] - [Length(50)] - public string AppAlias { get; set; } - } -} diff --git a/src/Umbraco.Core/Models/Rdbms/User2UserGroupDto.cs b/src/Umbraco.Core/Models/Rdbms/User2UserGroupDto.cs new file mode 100644 index 0000000000..afdbf661b5 --- /dev/null +++ b/src/Umbraco.Core/Models/Rdbms/User2UserGroupDto.cs @@ -0,0 +1,19 @@ +using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.DatabaseAnnotations; + +namespace Umbraco.Core.Models.Rdbms +{ + [TableName("umbracoUser2UserGroup")] + [ExplicitColumns] + internal class User2UserGroupDto + { + [Column("userId")] + [PrimaryKeyColumn(AutoIncrement = false, Name = "PK_user2userGroup", OnColumns = "userId, userGroupId")] + [ForeignKey(typeof(UserDto))] + public int UserId { get; set; } + + [Column("userGroupId")] + [ForeignKey(typeof(UserGroupDto))] + public int UserGroupId { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Rdbms/UserDto.cs b/src/Umbraco.Core/Models/Rdbms/UserDto.cs index 6214b0670b..38e4344f1e 100644 --- a/src/Umbraco.Core/Models/Rdbms/UserDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/UserDto.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using NPoco; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.DatabaseAnnotations; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; namespace Umbraco.Core.Models.Rdbms { @@ -11,6 +12,12 @@ namespace Umbraco.Core.Models.Rdbms [ExplicitColumns] internal class UserDto { + public UserDto() + { + UserGroupDtos = new List(); + UserStartNodeDtos = new HashSet(); + } + [Column("id")] [PrimaryKeyColumn(Name = "PK_user")] public int Id { get; set; } @@ -22,18 +29,7 @@ namespace Umbraco.Core.Models.Rdbms [Column("userNoConsole")] [Constraint(Default = "0")] public bool NoConsole { get; set; } - - [Column("userType")] - [ForeignKey(typeof(UserTypeDto))] - public short Type { get; set; } - - [Column("startStructureID")] - public int ContentStartId { get; set; } - - [Column("startMediaID")] - [NullSetting(NullSetting = NullSettings.Null)] - public int? MediaStartId { get; set; } - + [Column("userName")] public string UserName { get; set; } @@ -46,6 +42,14 @@ namespace Umbraco.Core.Models.Rdbms [Length(500)] public string Password { get; set; } + /// + /// This will represent a JSON structure of how the password has been created (i.e hash algorithm, iterations) + /// + [Column("passwordConfig")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(500)] + public string PasswordConfig { get; set; } + [Column("userEmail")] public string Email { get; set; } @@ -75,8 +79,37 @@ namespace Umbraco.Core.Models.Rdbms [NullSetting(NullSetting = NullSettings.Null)] public DateTime? LastLoginDate { get; set; } + [Column("emailConfirmedDate")] + [NullSetting(NullSetting = NullSettings.Null)] + public DateTime? EmailConfirmedDate { get; set; } + + [Column("invitedDate")] + [NullSetting(NullSetting = NullSettings.Null)] + public DateTime? InvitedDate { get; set; } + + [Column("createDate")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Constraint(Default = SystemMethods.CurrentDateTime)] + public DateTime CreateDate { get; set; } + + [Column("updateDate")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Constraint(Default = SystemMethods.CurrentDateTime)] + public DateTime UpdateDate { get; set; } + + /// + /// Will hold the media file system relative path of the users custom avatar if they uploaded one + /// + [Column("avatar")] + [NullSetting(NullSetting = NullSettings.Null)] + public string Avatar { get; set; } + [ResultColumn] [Reference(ReferenceType.Many, ReferenceMemberName = "UserId")] - public List User2AppDtos { get; set; } + public List UserGroupDtos { get; set; } + + [ResultColumn] + // fixme - reference? + public HashSet UserStartNodeDtos { get; set; } } } diff --git a/src/Umbraco.Core/Models/Rdbms/UserGroup2AppDto.cs b/src/Umbraco.Core/Models/Rdbms/UserGroup2AppDto.cs new file mode 100644 index 0000000000..14fc60e197 --- /dev/null +++ b/src/Umbraco.Core/Models/Rdbms/UserGroup2AppDto.cs @@ -0,0 +1,19 @@ +using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.DatabaseAnnotations; + +namespace Umbraco.Core.Models.Rdbms +{ + [TableName("umbracoUserGroup2App")] + [ExplicitColumns] + internal class UserGroup2AppDto + { + [Column("userGroupId")] + [PrimaryKeyColumn(AutoIncrement = false, Name = "PK_userGroup2App", OnColumns = "userGroupId, app")] + [ForeignKey(typeof(UserGroupDto))] + public int UserGroupId { get; set; } + + [Column("app")] + [Length(50)] + public string AppAlias { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Rdbms/User2NodePermissionDto.cs b/src/Umbraco.Core/Models/Rdbms/UserGroup2NodePermissionDto.cs similarity index 54% rename from src/Umbraco.Core/Models/Rdbms/User2NodePermissionDto.cs rename to src/Umbraco.Core/Models/Rdbms/UserGroup2NodePermissionDto.cs index 981481a7fe..8c75dcf4f7 100644 --- a/src/Umbraco.Core/Models/Rdbms/User2NodePermissionDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/UserGroup2NodePermissionDto.cs @@ -1,18 +1,16 @@ -using NPoco; -using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.DatabaseAnnotations; namespace Umbraco.Core.Models.Rdbms { - [TableName("umbracoUser2NodePermission")] - [PrimaryKey("userId", AutoIncrement = false)] + [TableName("umbracoUserGroup2NodePermission")] [ExplicitColumns] - internal class User2NodePermissionDto + internal class UserGroup2NodePermissionDto { - [Column("userId")] - [PrimaryKeyColumn(AutoIncrement = false, Name = "PK_umbracoUser2NodePermission", OnColumns = "userId, nodeId, permission")] - [ForeignKey(typeof(UserDto))] - public int UserId { get; set; } + [Column("userGroupId")] + [PrimaryKeyColumn(AutoIncrement = false, Name = "PK_umbracoUserGroup2NodePermission", OnColumns = "userGroupId, nodeId, permission")] + [ForeignKey(typeof(UserGroupDto))] + public int UserGroupId { get; set; } [Column("nodeId")] [ForeignKey(typeof(NodeDto))] @@ -22,4 +20,4 @@ namespace Umbraco.Core.Models.Rdbms [Column("permission")] public string Permission { get; set; } } -} +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Rdbms/UserGroupDto.cs b/src/Umbraco.Core/Models/Rdbms/UserGroupDto.cs new file mode 100644 index 0000000000..fdad78bfcd --- /dev/null +++ b/src/Umbraco.Core/Models/Rdbms/UserGroupDto.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.DatabaseAnnotations; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; + +namespace Umbraco.Core.Models.Rdbms +{ + [TableName("umbracoUserGroup")] + [PrimaryKey("id")] + [ExplicitColumns] + internal class UserGroupDto + { + public UserGroupDto() + { + UserGroup2AppDtos = new List(); + } + + [Column("id")] + [PrimaryKeyColumn(IdentitySeed = 5)] + public int Id { get; set; } + + [Column("userGroupAlias")] + [Length(200)] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoUserGroup_userGroupAlias")] + public string Alias { get; set; } + + [Column("userGroupName")] + [Length(200)] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoUserGroup_userGroupName")] + public string Name { get; set; } + + [Column("userGroupDefaultPermissions")] + [Length(50)] + [NullSetting(NullSetting = NullSettings.Null)] + public string DefaultPermissions { get; set; } + + [Column("createDate")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Constraint(Default = SystemMethods.CurrentDateTime)] + public DateTime CreateDate { get; set; } + + [Column("updateDate")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Constraint(Default = SystemMethods.CurrentDateTime)] + public DateTime UpdateDate { get; set; } + + [Column("icon")] + [NullSetting(NullSetting = NullSettings.Null)] + public string Icon { get; set; } + + [Column("startContentId")] + [NullSetting(NullSetting = NullSettings.Null)] + public int? StartContentId { get; set; } + + [Column("startMediaId")] + [NullSetting(NullSetting = NullSettings.Null)] + public int? StartMediaId { get; set; } + + [ResultColumn] + public List UserGroup2AppDtos { get; set; } + + /// + /// This is only relevant when this column is included in the results (i.e. GetUserGroupsWithUserCounts) + /// + [ResultColumn] + public int UserCount { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Rdbms/UserStartNodeDto.cs b/src/Umbraco.Core/Models/Rdbms/UserStartNodeDto.cs new file mode 100644 index 0000000000..fa8af80759 --- /dev/null +++ b/src/Umbraco.Core/Models/Rdbms/UserStartNodeDto.cs @@ -0,0 +1,67 @@ +using System; +using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.DatabaseAnnotations; + +namespace Umbraco.Core.Models.Rdbms +{ + [TableName("umbracoUserStartNode")] + [PrimaryKey("id", autoIncrement = true)] + [ExplicitColumns] + internal class UserStartNodeDto : IEquatable + { + [Column("id")] + [PrimaryKeyColumn(Name = "PK_userStartNode")] + public int Id { get; set; } + + [Column("userId")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [ForeignKey(typeof(UserDto))] + public int UserId { get; set; } + + [Column("startNode")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [ForeignKey(typeof(NodeDto))] + public int StartNode { get; set; } + + [Column("startNodeType")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Index(IndexTypes.UniqueNonClustered, ForColumns = "startNodeType, startNode, userId", Name = "IX_umbracoUserStartNode_startNodeType")] + public int StartNodeType { get; set; } + + public enum StartNodeTypeValue + { + Content = 1, + Media = 2 + } + + public bool Equals(UserStartNodeDto other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Id == other.Id; + } + + 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((UserStartNodeDto) obj); + } + + public override int GetHashCode() + { + return Id; + } + + public static bool operator ==(UserStartNodeDto left, UserStartNodeDto right) + { + return Equals(left, right); + } + + public static bool operator !=(UserStartNodeDto left, UserStartNodeDto right) + { + return !Equals(left, right); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Rdbms/UserTypeDto.cs b/src/Umbraco.Core/Models/Rdbms/UserTypeDto.cs index 80b588fec4..1114ead34f 100644 --- a/src/Umbraco.Core/Models/Rdbms/UserTypeDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/UserTypeDto.cs @@ -1,9 +1,11 @@ -using NPoco; +using System; +using NPoco; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.DatabaseAnnotations; namespace Umbraco.Core.Models.Rdbms { + [Obsolete("Table no longer exists as of 7.6 - retained only to support migrations from previous versions")] [TableName("umbracoUserType")] [PrimaryKey("id")] [ExplicitColumns] diff --git a/src/Umbraco.Core/Models/UmbracoObjectTypes.cs b/src/Umbraco.Core/Models/UmbracoObjectTypes.cs index c1e9ba47b8..c4513d8d94 100644 --- a/src/Umbraco.Core/Models/UmbracoObjectTypes.cs +++ b/src/Umbraco.Core/Models/UmbracoObjectTypes.cs @@ -178,6 +178,21 @@ namespace Umbraco.Core.Models /// [UmbracoObjectType(Constants.ObjectTypes.FormsDataSource)] [FriendlyName("DataSource")] - FormsDataSource + FormsDataSource, + + /// + /// Language + /// + [UmbracoObjectType(Constants.ObjectTypes.Language)] + [FriendlyName("Language")] + Language, + + /// + /// Document + /// + [UmbracoObjectType(Constants.ObjectTypes.DocumentBlueprint, typeof(IContent))] + [FriendlyName("DocumentBlueprint")] + [UmbracoUdiType(Constants.UdiEntityType.DocumentBluePrint)] + DocumentBlueprint } -} +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/UmbracoObjectTypesExtensions.cs b/src/Umbraco.Core/Models/UmbracoObjectTypesExtensions.cs index e19f2efb09..9582af36d6 100644 --- a/src/Umbraco.Core/Models/UmbracoObjectTypesExtensions.cs +++ b/src/Umbraco.Core/Models/UmbracoObjectTypesExtensions.cs @@ -68,15 +68,14 @@ namespace Umbraco.Core.Models { return UmbracoObjectTypeCache.GetOrAdd(umbracoObjectType, types => { - var type = typeof(UmbracoObjectTypes); - var memInfo = type.GetMember(umbracoObjectType.ToString()); - var attributes = memInfo[0].GetCustomAttributes(typeof(UmbracoObjectTypeAttribute), - false); + var type = typeof (UmbracoObjectTypes); + var memberInfo = type.GetMember(umbracoObjectType.ToString()); + var attributes = memberInfo[0].GetCustomAttributes(typeof (UmbracoObjectTypeAttribute), false); if (attributes.Length == 0) return Guid.Empty; - var attribute = ((UmbracoObjectTypeAttribute)attributes[0]); + var attribute = (UmbracoObjectTypeAttribute) attributes[0]; if (attribute == null) return Guid.Empty; diff --git a/src/Umbraco.Core/Models/UserExtensions.cs b/src/Umbraco.Core/Models/UserExtensions.cs index 43a4477894..740fab9345 100644 --- a/src/Umbraco.Core/Models/UserExtensions.cs +++ b/src/Umbraco.Core/Models/UserExtensions.cs @@ -1,7 +1,12 @@ using System; +using System.Collections.Generic; using System.Globalization; using System.Linq; +using System.Net; using Umbraco.Core.Exceptions; +using Umbraco.Core.Cache; +using Umbraco.Core.IO; +using Umbraco.Core.Models.Identity; using Umbraco.Core.Models.Membership; using Umbraco.Core.Services; @@ -18,7 +23,76 @@ namespace Umbraco.Core.Models public static bool IsAdmin(this IUser user) { if (user == null) throw new ArgumentNullException(nameof(user)); - return user.UserType.Alias == "admin"; + return user.Groups != null && user.Groups.Any(x => x.Alias == Constants.Security.AdminGroupAlias); + } + + /// + /// Tries to lookup the user's gravatar to see if the endpoint can be reached, if so it returns the valid URL + /// + /// + /// + /// + /// + /// A list of 5 different sized avatar URLs + /// + internal static string[] GetCurrentUserAvatarUrls(this IUser user, IUserService userService, ICacheProvider staticCache) + { + //check if the user has explicitly removed all avatars including a gravatar, this will be possible and the value will be "none" + if (user.Avatar == "none") + { + return new string[0]; + } + + if (user.Avatar.IsNullOrWhiteSpace()) + { + var gravatarHash = user.Email.ToMd5(); + var gravatarUrl = "https://www.gravatar.com/avatar/" + gravatarHash + "?d=404"; + + //try gravatar + var gravatarAccess = staticCache.GetCacheItem("UserAvatar" + user.Id, () => + { + // Test if we can reach this URL, will fail when there's network or firewall errors + var request = (HttpWebRequest)WebRequest.Create(gravatarUrl); + // Require response within 10 seconds + request.Timeout = 10000; + try + { + using ((HttpWebResponse)request.GetResponse()) { } + } + catch (Exception) + { + // There was an HTTP or other error, return an null instead + return false; + } + return true; + }); + + if (gravatarAccess) + { + return new[] + { + gravatarUrl + "&s=30", + gravatarUrl + "&s=60", + gravatarUrl + "&s=90", + gravatarUrl + "&s=150", + gravatarUrl + "&s=300" + }; + } + + return new string[0]; + } + + //use the custom avatar + var avatarUrl = FileSystemProviderManager.Current.MediaFileSystem.GetUrl(user.Avatar); + return new[] + { + avatarUrl + "?width=30&height=30&mode=crop", + avatarUrl + "?width=60&height=60&mode=crop", + avatarUrl + "?width=90&height=90&mode=crop", + avatarUrl + "?width=150&height=150&mode=crop", + avatarUrl + "?width=300&height=300&mode=crop" + }; + } public static void ClearAllowedSections(this IUser user) @@ -61,47 +135,183 @@ namespace Umbraco.Core.Models } } - /// - /// Checks if the user has access to the content item based on their start noe - /// - /// - /// - /// - internal static bool HasPathAccess(this IUser user, IContent content) + internal static bool HasContentRootAccess(this IUser user, IEntityService entityService) { - if (user == null) throw new ArgumentNullException(nameof(user)); - if (content == null) throw new ArgumentNullException(nameof(content)); - return HasPathAccess(content.Path, user.StartContentId, Constants.System.RecycleBinContent); + return HasPathAccess(Constants.System.Root.ToInvariantString(), user.CalculateContentStartNodeIds(entityService), Constants.System.RecycleBinContent); } - internal static bool HasPathAccess(string path, int startNodeId, int recycleBinId) + internal static bool HasContentBinAccess(this IUser user, IEntityService entityService) { - if (string.IsNullOrWhiteSpace(path)) throw new ArgumentNullOrEmptyException(nameof(path)); + return HasPathAccess(Constants.System.RecycleBinContent.ToInvariantString(), user.CalculateContentStartNodeIds(entityService), Constants.System.RecycleBinContent); + } + + internal static bool HasMediaRootAccess(this IUser user, IEntityService entityService) + { + return HasPathAccess(Constants.System.Root.ToInvariantString(), user.CalculateMediaStartNodeIds(entityService), Constants.System.RecycleBinMedia); + } + + internal static bool HasMediaBinAccess(this IUser user, IEntityService entityService) + { + return HasPathAccess(Constants.System.RecycleBinMedia.ToInvariantString(), user.CalculateMediaStartNodeIds(entityService), Constants.System.RecycleBinMedia); + } + + internal static bool HasPathAccess(this IUser user, IContent content, IEntityService entityService) + { + return HasPathAccess(content.Path, user.CalculateContentStartNodeIds(entityService), Constants.System.RecycleBinContent); + } + + internal static bool HasPathAccess(this IUser user, IMedia media, IEntityService entityService) + { + return HasPathAccess(media.Path, user.CalculateMediaStartNodeIds(entityService), Constants.System.RecycleBinMedia); + } + + internal static bool HasPathAccess(string path, int[] startNodeIds, int recycleBinId) + { + if (string.IsNullOrWhiteSpace(path)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(path)); + + // check for no access + if (startNodeIds.Length == 0) + return false; + + // check for root access + if (startNodeIds.Contains(Constants.System.Root)) + return true; var formattedPath = "," + path + ","; - var formattedStartNodeId = "," + startNodeId.ToInvariantString() + ","; - var formattedRecycleBinId = "," + recycleBinId.ToInvariantString() + ","; - //only users with root access have access to the recycle bin - if (formattedPath.Contains(formattedRecycleBinId)) + // only users with root access have access to the recycle bin, + // if the above check didn't pass then access is denied + if (formattedPath.Contains("," + recycleBinId + ",")) + return false; + + // check for a start node in the path + return startNodeIds.Any(x => formattedPath.Contains("," + x + ",")); + } + + // calc. start nodes, combining groups' and user's, and excluding what's in the bin + public static int[] CalculateContentStartNodeIds(this IUser user, IEntityService entityService) + { + const string cacheKey = "AllContentStartNodes"; + //try to look them up from cache so we don't recalculate + var valuesInUserCache = FromUserCache(user, cacheKey); + if (valuesInUserCache != null) return valuesInUserCache; + + var gsn = user.Groups.Where(x => x.StartContentId.HasValue).Select(x => x.StartContentId.Value).Distinct().ToArray(); + var usn = user.StartContentIds; + var vals = CombineStartNodes(UmbracoObjectTypes.Document, gsn, usn, entityService); + ToUserCache(user, cacheKey, vals); + return vals; + } + + // calc. start nodes, combining groups' and user's, and excluding what's in the bin + public static int[] CalculateMediaStartNodeIds(this IUser user, IEntityService entityService) + { + const string cacheKey = "AllMediaStartNodes"; + //try to look them up from cache so we don't recalculate + var valuesInUserCache = FromUserCache(user, cacheKey); + if (valuesInUserCache != null) return valuesInUserCache; + + var gsn = user.Groups.Where(x => x.StartMediaId.HasValue).Select(x => x.StartMediaId.Value).Distinct().ToArray(); + var usn = user.StartMediaIds; + var vals = CombineStartNodes(UmbracoObjectTypes.Media, gsn, usn, entityService); + ToUserCache(user, cacheKey, vals); + return vals; + } + + private static int[] FromUserCache(IUser user, string cacheKey) + { + var entityUser = user as User; + if (entityUser == null) return null; + + lock (entityUser.AdditionalDataLock) { - return startNodeId == Constants.System.Root; + object allContentStartNodes; + return entityUser.AdditionalData.TryGetValue(cacheKey, out allContentStartNodes) + ? allContentStartNodes as int[] + : null; + } + } + + private static void ToUserCache(IUser user, string cacheKey, int[] vals) + { + var entityUser = user as User; + if (entityUser == null) return; + + lock (entityUser.AdditionalDataLock) + { + entityUser.AdditionalData[cacheKey] = vals; + } + } + + private static bool StartsWithPath(string test, string path) + { + return test.StartsWith(path) && test.Length > path.Length && test[path.Length] == ','; + } + + private static string GetBinPath(UmbracoObjectTypes objectType) + { + var binPath = Constants.System.Root + ","; + switch (objectType) + { + case UmbracoObjectTypes.Document: + binPath += Constants.System.RecycleBinContent; + break; + case UmbracoObjectTypes.Media: + binPath += Constants.System.RecycleBinMedia; + break; + default: + throw new ArgumentOutOfRangeException("objectType"); + } + return binPath; + } + + internal static int[] CombineStartNodes(UmbracoObjectTypes objectType, int[] groupSn, int[] userSn, IEntityService entityService) + { + // assume groupSn and userSn each don't contain duplicates + + var asn = groupSn.Concat(userSn).Distinct().ToArray(); + var paths = entityService.GetAllPaths(objectType, asn).ToDictionary(x => x.Id, x => x.Path); + + paths[Constants.System.Root] = Constants.System.Root.ToString(); // entityService does not get that one + + var binPath = GetBinPath(objectType); + + var lsn = new List(); + foreach (var sn in groupSn) + { + string snp; + if (paths.TryGetValue(sn, out snp) == false) continue; // ignore rogue node (no path) + + if (StartsWithPath(snp, binPath)) continue; // ignore bin + + if (lsn.Any(x => StartsWithPath(snp, paths[x]))) continue; // skip if something above this sn + lsn.RemoveAll(x => StartsWithPath(paths[x], snp)); // remove anything below this sn + lsn.Add(sn); } - return formattedPath.Contains(formattedStartNodeId); - } + var usn = new List(); + foreach (var sn in userSn) + { + if (groupSn.Contains(sn)) continue; // ignore, already there - /// - /// Checks if the user has access to the media item based on their start noe - /// - /// - /// - /// - internal static bool HasPathAccess(this IUser user, IMedia media) - { - if (user == null) throw new ArgumentNullException(nameof(user)); - if (media == null) throw new ArgumentNullException(nameof(media)); - return HasPathAccess(media.Path, user.StartMediaId, Constants.System.RecycleBinMedia); + string snp; + if (paths.TryGetValue(sn, out snp) == false) continue; // ignore rogue node (no path) + + if (StartsWithPath(snp, binPath)) continue; // ignore bin + + if (usn.Any(x => StartsWithPath(paths[x], snp))) continue; // skip if something below this sn + usn.RemoveAll(x => StartsWithPath(snp, paths[x])); // remove anything above this sn + usn.Add(sn); + } + + foreach (var sn in usn) + { + var snp = paths[sn]; // has to be here now + lsn.RemoveAll(x => StartsWithPath(snp, paths[x]) || StartsWithPath(paths[x], snp)); // remove anything above or below this sn + lsn.Add(sn); + } + + return lsn.ToArray(); } - } + } } diff --git a/src/Umbraco.Core/Packaging/Models/UninstallationSummary.cs b/src/Umbraco.Core/Packaging/Models/UninstallationSummary.cs index 9ec8f773db..6f28ba567e 100644 --- a/src/Umbraco.Core/Packaging/Models/UninstallationSummary.cs +++ b/src/Umbraco.Core/Packaging/Models/UninstallationSummary.cs @@ -8,7 +8,7 @@ namespace Umbraco.Core.Packaging.Models { [Serializable] [DataContract(IsReference = true)] - internal class UninstallationSummary + public class UninstallationSummary { public MetaData MetaData { get; set; } public IEnumerable DataTypesUninstalled { get; set; } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/DecimalValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/DecimalValueConverter.cs index cb4a34cd1a..2e1c527caf 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/DecimalValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/DecimalValueConverter.cs @@ -23,7 +23,7 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters // in XML a decimal is a string if (source is string sourceString) { - return decimal.TryParse(sourceString, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out decimal d) ? d : 0M; + return decimal.TryParse(sourceString, NumberStyles.AllowDecimalPoint | NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out decimal d) ? d : 0M; } // in the database an a decimal is an a decimal diff --git a/src/Umbraco.Core/StringExtensions.cs b/src/Umbraco.Core/StringExtensions.cs index 97be4958a9..571a279afa 100644 --- a/src/Umbraco.Core/StringExtensions.cs +++ b/src/Umbraco.Core/StringExtensions.cs @@ -38,6 +38,22 @@ namespace Umbraco.Core ToCSharpEscapeChars[escape[0]] = escape[1]; } + /// + /// Convert a path to node ids in the order from right to left (deepest to shallowest) + /// + /// + /// + internal static int[] GetIdsFromPathReversed(this string path) + { + var nodeIds = path.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + .Select(x => x.TryConvertTo()) + .Where(x => x.Success) + .Select(x => x.Result) + .Reverse() + .ToArray(); + return nodeIds; + } + /// /// Removes new lines and tabs /// @@ -713,67 +729,72 @@ namespace Umbraco.Core return val; } + /// + /// Generates a hash of a string based on the FIPS compliance setting. + /// + /// Referrs to itself + /// The hashed string + public static string GenerateHash(this string str) + { + return CryptoConfig.AllowOnlyFipsAlgorithms + ? str.ToSHA1() + : str.ToMd5(); + } + /// /// Converts the string to MD5 /// - /// referrs to itself - /// the md5 hashed string + /// Referrs to itself + /// The MD5 hashed string public static string ToMd5(this string stringToConvert) { - //create an instance of the MD5CryptoServiceProvider - var md5Provider = new MD5CryptoServiceProvider(); - - //convert our string into byte array - var byteArray = Encoding.UTF8.GetBytes(stringToConvert); - - //get the hashed values created by our MD5CryptoServiceProvider - var hashedByteArray = md5Provider.ComputeHash(byteArray); - - //create a StringBuilder object - var stringBuilder = new StringBuilder(); - - //loop to each each byte - foreach (var b in hashedByteArray) - { - //append it to our StringBuilder - stringBuilder.Append(b.ToString("x2").ToLower()); - } - - //return the hashed value - return stringBuilder.ToString(); + return stringToConvert.GenerateHash("MD5"); } /// /// Converts the string to SHA1 /// /// referrs to itself - /// the md5 hashed string + /// The SHA1 hashed string public static string ToSHA1(this string stringToConvert) { - //create an instance of the SHA1CryptoServiceProvider - var md5Provider = new SHA1CryptoServiceProvider(); - - //convert our string into byte array - var byteArray = Encoding.UTF8.GetBytes(stringToConvert); - - //get the hashed values created by our SHA1CryptoServiceProvider - var hashedByteArray = md5Provider.ComputeHash(byteArray); - - //create a StringBuilder object - var stringBuilder = new StringBuilder(); - - //loop to each each byte - foreach (var b in hashedByteArray) - { - //append it to our StringBuilder - stringBuilder.Append(b.ToString("x2").ToLower()); - } - - //return the hashed value - return stringBuilder.ToString(); + return stringToConvert.GenerateHash("SHA1"); } + /// Generate a hash of a string based on the hashType passed in + /// + /// Referrs to itself + /// String with the hash type. See remarks section of the CryptoConfig Class in MSDN docs for a list of possible values. + /// The hashed string + private static string GenerateHash(this string str, string hashType) + { + //create an instance of the correct hashing provider based on the type passed in + var hasher = HashAlgorithm.Create(hashType); + if (hasher == null) throw new InvalidOperationException("No hashing type found by name " + hashType); + using (hasher) + { + //convert our string into byte array + var byteArray = Encoding.UTF8.GetBytes(str); + + //get the hashed values created by our selected provider + var hashedByteArray = hasher.ComputeHash(byteArray); + + //create a StringBuilder object + var stringBuilder = new StringBuilder(); + + //loop to each each byte + foreach (var b in hashedByteArray) + { + //append it to our StringBuilder + stringBuilder.Append(b.ToString("x2").ToLower()); + } + + //return the hashed value + return stringBuilder.ToString(); + } + } + /// /// Decodes a string that was encoded with UrlTokenEncode /// @@ -1361,10 +1382,84 @@ namespace Umbraco.Core /// internal static Guid ToGuid(this string text) { - var md5 = MD5.Create(); - byte[] myStringBytes = Encoding.ASCII.GetBytes(text); - byte[] hash = md5.ComputeHash(myStringBytes); - return new Guid(hash); + return CreateGuidFromHash(UrlNamespace, + text, + CryptoConfig.AllowOnlyFipsAlgorithms + ? 5 // SHA1 + : 3); // MD5 + } + + /// + /// The namespace for URLs (from RFC 4122, Appendix C). + /// + /// See RFC 4122 + /// + internal static readonly Guid UrlNamespace = new Guid("6ba7b811-9dad-11d1-80b4-00c04fd430c8"); + + /// + /// Creates a name-based UUID using the algorithm from RFC 4122 §4.3. + /// + /// See GuidUtility.cs for original implementation. + /// + /// The ID of the namespace. + /// The name (within that namespace). + /// The version number of the UUID to create; this value must be either + /// 3 (for MD5 hashing) or 5 (for SHA-1 hashing). + /// A UUID derived from the namespace and name. + /// See Generating a deterministic GUID. + internal static Guid CreateGuidFromHash(Guid namespaceId, string name, int version) + { + if (name == null) + throw new ArgumentNullException("name"); + if (version != 3 && version != 5) + throw new ArgumentOutOfRangeException("version", "version must be either 3 or 5."); + + // convert the name to a sequence of octets (as defined by the standard or conventions of its namespace) (step 3) + // ASSUME: UTF-8 encoding is always appropriate + byte[] nameBytes = Encoding.UTF8.GetBytes(name); + + // convert the namespace UUID to network order (step 3) + byte[] namespaceBytes = namespaceId.ToByteArray(); + SwapByteOrder(namespaceBytes); + + // comput the hash of the name space ID concatenated with the name (step 4) + byte[] hash; + using (HashAlgorithm algorithm = version == 3 ? (HashAlgorithm)MD5.Create() : SHA1.Create()) + { + algorithm.TransformBlock(namespaceBytes, 0, namespaceBytes.Length, null, 0); + algorithm.TransformFinalBlock(nameBytes, 0, nameBytes.Length); + hash = algorithm.Hash; + } + + // most bytes from the hash are copied straight to the bytes of the new GUID (steps 5-7, 9, 11-12) + byte[] newGuid = new byte[16]; + Array.Copy(hash, 0, newGuid, 0, 16); + + // set the four most significant bits (bits 12 through 15) of the time_hi_and_version field to the appropriate 4-bit version number from Section 4.1.3 (step 8) + newGuid[6] = (byte)((newGuid[6] & 0x0F) | (version << 4)); + + // set the two most significant bits (bits 6 and 7) of the clock_seq_hi_and_reserved to zero and one, respectively (step 10) + newGuid[8] = (byte)((newGuid[8] & 0x3F) | 0x80); + + // convert the resulting UUID to local byte order (step 13) + SwapByteOrder(newGuid); + return new Guid(newGuid); + } + + // Converts a GUID (expressed as a byte array) to/from network order (MSB-first). + internal static void SwapByteOrder(byte[] guid) + { + SwapBytes(guid, 0, 3); + SwapBytes(guid, 1, 2); + SwapBytes(guid, 4, 5); + SwapBytes(guid, 6, 7); + } + + private static void SwapBytes(byte[] guid, int left, int right) + { + byte temp = guid[left]; + guid[left] = guid[right]; + guid[right] = temp; } } } diff --git a/src/Umbraco.Core/StringUdi.cs b/src/Umbraco.Core/StringUdi.cs index d172d634b5..0e1201665e 100644 --- a/src/Umbraco.Core/StringUdi.cs +++ b/src/Umbraco.Core/StringUdi.cs @@ -1,5 +1,6 @@ using System; using System.ComponentModel; +using System.Linq; namespace Umbraco.Core { @@ -20,7 +21,7 @@ namespace Umbraco.Core /// The entity type part of the udi. /// The string id part of the udi. public StringUdi(string entityType, string id) - : base(entityType, "umb://" + entityType + "/" + Uri.EscapeUriString(id)) + : base(entityType, "umb://" + entityType + "/" + EscapeUriString(id)) { Id = id; } @@ -35,6 +36,19 @@ namespace Umbraco.Core Id = Uri.UnescapeDataString(uriValue.AbsolutePath.TrimStart('/')); } + private static string EscapeUriString(string s) + { + // Uri.EscapeUriString preserves / but also [ and ] which is bad + // Uri.EscapeDataString does not preserve / which is bad + + // reserved = : / ? # [ ] @ ! $ & ' ( ) * + , ; = + // unreserved = alpha digit - . _ ~ + + // we want to preserve the / and the unreserved + // so... + return string.Join("/", s.Split('/').Select(Uri.EscapeDataString)); + } + /// /// Converts the string representation of an entity identifier into the equivalent StringUdi instance. /// diff --git a/src/Umbraco.Core/Strings/CleanStringType.cs b/src/Umbraco.Core/Strings/CleanStringType.cs index 0e0a7c9908..c8c99be869 100644 --- a/src/Umbraco.Core/Strings/CleanStringType.cs +++ b/src/Umbraco.Core/Strings/CleanStringType.cs @@ -68,7 +68,7 @@ namespace Umbraco.Core.Strings /// /// Flag mask for encoding. /// - CodeMask = Unicode | Utf8 | Ascii, + CodeMask = Unicode | Utf8 | Ascii | TryAscii, /// /// Unicode encoding. @@ -86,6 +86,10 @@ namespace Umbraco.Core.Strings /// Ascii = 0x0400, + /// + /// Ascii encoding, if possible. + /// + TryAscii = 0x0800, // role values diff --git a/src/Umbraco.Core/Strings/DefaultShortStringHelper.cs b/src/Umbraco.Core/Strings/DefaultShortStringHelper.cs index 71c9a76331..5ff4052a45 100644 --- a/src/Umbraco.Core/Strings/DefaultShortStringHelper.cs +++ b/src/Umbraco.Core/Strings/DefaultShortStringHelper.cs @@ -314,9 +314,20 @@ function validateSafeAlias(input, value, immediate, callback) {{ // recode var codeType = stringType & CleanStringType.CodeMask; - text = codeType == CleanStringType.Ascii - ? Utf8ToAsciiConverter.ToAsciiString(text) - : RemoveSurrogatePairs(text); + switch (codeType) + { + case CleanStringType.Ascii: + text = Utf8ToAsciiConverter.ToAsciiString(text); + break; + case CleanStringType.TryAscii: + const char ESC = (char) 27; + var ctext = Utf8ToAsciiConverter.ToAsciiString(text, ESC); + if (ctext.Contains(ESC) == false) text = ctext; + break; + default: + text = RemoveSurrogatePairs(text); + break; + } // clean text = CleanCodeString(text, stringType, separator.Value, culture, config); diff --git a/src/Umbraco.Core/Strings/DefaultShortStringHelperConfig.cs b/src/Umbraco.Core/Strings/DefaultShortStringHelperConfig.cs index ec98eb17c0..8a6aca632b 100644 --- a/src/Umbraco.Core/Strings/DefaultShortStringHelperConfig.cs +++ b/src/Umbraco.Core/Strings/DefaultShortStringHelperConfig.cs @@ -12,10 +12,12 @@ namespace Umbraco.Core.Strings public DefaultShortStringHelperConfig Clone() { - var config = new DefaultShortStringHelperConfig(); - config.DefaultCulture = DefaultCulture; - config.ForceSafeAliases = ForceSafeAliases; - config.UrlReplaceCharacters = UrlReplaceCharacters; + var config = new DefaultShortStringHelperConfig + { + DefaultCulture = DefaultCulture, + ForceSafeAliases = ForceSafeAliases, + UrlReplaceCharacters = UrlReplaceCharacters + }; foreach (var kvp1 in _configs) { @@ -63,14 +65,19 @@ namespace Umbraco.Core.Strings UrlReplaceCharacters = umbracoSettings.RequestHandler.CharCollection .Where(x => string.IsNullOrEmpty(x.Char) == false) .ToDictionary(x => x.Char, x => x.Replacement); - var convertUrlsToAscii = umbracoSettings.RequestHandler.ConvertUrlsToAscii; + + var urlSegmentConvertTo = CleanStringType.Utf8; + if (umbracoSettings.RequestHandler.ConvertUrlsToAscii) + urlSegmentConvertTo = CleanStringType.Ascii; + if (umbracoSettings.RequestHandler.TryConvertUrlsToAscii) + urlSegmentConvertTo = CleanStringType.TryAscii; return WithConfig(CleanStringType.UrlSegment, new Config { PreFilter = ApplyUrlReplaceCharacters, PostFilter = x => CutMaxLength(x, 240), IsTerm = (c, leading) => char.IsLetterOrDigit(c) || c == '_', // letter, digit or underscore - StringType = (convertUrlsToAscii ? CleanStringType.Ascii : CleanStringType.Utf8) | CleanStringType.LowerCase, + StringType = urlSegmentConvertTo | CleanStringType.LowerCase, BreakTermsOnUpper = false, Separator = '-' }).WithConfig(CleanStringType.FileName, new Config diff --git a/src/Umbraco.Core/Strings/Utf8ToAsciiConverter.cs b/src/Umbraco.Core/Strings/Utf8ToAsciiConverter.cs index 318fe2b5df..4656c6bc62 100644 --- a/src/Umbraco.Core/Strings/Utf8ToAsciiConverter.cs +++ b/src/Umbraco.Core/Strings/Utf8ToAsciiConverter.cs @@ -17,8 +17,9 @@ namespace Umbraco.Core.Strings /// Converts an Utf8 string into an Ascii string. /// /// The text to convert. + /// The character to used to replace characters that cannot properly be converted. /// The converted text. - public static string ToAsciiString(string text) + public static string ToAsciiString(string text, char fail = '?') { var input = text.ToCharArray(); @@ -26,7 +27,7 @@ namespace Umbraco.Core.Strings // but... we should be filtering short strings only... var output = new char[input.Length * 3]; // *3 because of things such as OE - var len = ToAscii(input, output); + var len = ToAscii(input, output, fail); return new string(output, 0, len); //var output = new StringBuilder(input.Length + 16); // default is 16, start with at least input length + little extra @@ -38,8 +39,9 @@ namespace Umbraco.Core.Strings /// Converts an Utf8 string into an array of Ascii characters. /// /// The text to convert. + /// The character to used to replace characters that cannot properly be converted. /// The converted text. - public static char[] ToAsciiCharArray(string text) + public static char[] ToAsciiCharArray(string text, char fail = '?') { var input = text.ToCharArray(); @@ -47,7 +49,7 @@ namespace Umbraco.Core.Strings // but... we should be filtering short strings only... var output = new char[input.Length * 3]; // *3 because of things such as OE - var len = ToAscii(input, output); + var len = ToAscii(input, output, fail); var array = new char[len]; Array.Copy(output, array, len); return array; @@ -64,10 +66,11 @@ namespace Umbraco.Core.Strings /// /// The input array. /// The output array. + /// The character to used to replace characters that cannot properly be converted. /// The number of characters in the output array. /// The caller must ensure that the output array is big enough. /// The output array is not big enough. - private static int ToAscii(char[] input, char[] output) + private static int ToAscii(char[] input, char[] output, char fail = '?') { var opos = 0; @@ -75,10 +78,10 @@ namespace Umbraco.Core.Strings if (char.IsSurrogate(input[ipos])) // ignore high surrogate { ipos++; // and skip low surrogate - output[opos++] = '?'; + output[opos++] = fail; } else - ToAscii(input, ipos, output, ref opos); + ToAscii(input, ipos, output, ref opos, fail); return opos; } @@ -109,12 +112,13 @@ namespace Umbraco.Core.Strings /// The input position. /// The output array. /// The output position. + /// The character to used to replace characters that cannot properly be converted. /// /// Adapted from various sources on the 'net including Lucene.Net.Analysis.ASCIIFoldingFilter. /// Input should contain Utf8 characters exclusively and NOT Unicode. /// Removes controls, normalizes whitespaces, replaces symbols by '?'. /// - private static void ToAscii(char[] input, int ipos, char[] output, ref int opos) + private static void ToAscii(char[] input, int ipos, char[] output, ref int opos, char fail = '?') { var c = input[ipos]; @@ -3583,7 +3587,7 @@ namespace Umbraco.Core.Strings // output[opos++] = c; // strict ASCII - output[opos++] = '?'; + output[opos++] = fail; break; } diff --git a/src/Umbraco.Core/UdiEntityType.cs b/src/Umbraco.Core/UdiEntityType.cs index bfbdefd87a..e786ac3146 100644 --- a/src/Umbraco.Core/UdiEntityType.cs +++ b/src/Umbraco.Core/UdiEntityType.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using Umbraco.Core.Models; namespace Umbraco.Core @@ -24,6 +23,10 @@ namespace Umbraco.Core [UdiType(UdiType.GuidUdi)] public const string Document = "document"; + + [UdiType(UdiType.GuidUdi)] + public const string DocumentBluePrint = "document-blueprint"; + [UdiType(UdiType.GuidUdi)] public const string Media = "media"; [UdiType(UdiType.GuidUdi)] @@ -70,6 +73,10 @@ namespace Umbraco.Core [UdiType(UdiType.StringUdi)] public const string AnyString = "any-string"; // that one is for tests + [UdiType(UdiType.StringUdi)] + public const string Language = "language"; + [UdiType(UdiType.StringUdi)] + public const string MacroScript = "macroscript"; [UdiType(UdiType.StringUdi)] public const string MediaFile = "media-file"; [UdiType(UdiType.StringUdi)] @@ -83,6 +90,8 @@ namespace Umbraco.Core [UdiType(UdiType.StringUdi)] public const string PartialViewMacro = "partial-view-macro"; [UdiType(UdiType.StringUdi)] + public const string UserControl = "usercontrol"; + [UdiType(UdiType.StringUdi)] public const string Xslt = "xslt"; public static string FromUmbracoObjectType(UmbracoObjectTypes umbracoObjectType) @@ -123,6 +132,8 @@ namespace Umbraco.Core return FormsPreValue; case UmbracoObjectTypes.FormsDataSource: return FormsDataSource; + case UmbracoObjectTypes.Language: + return Language; } throw new NotSupportedException(string.Format("UmbracoObjectType \"{0}\" does not have a matching EntityType.", umbracoObjectType)); } @@ -165,6 +176,8 @@ namespace Umbraco.Core return UmbracoObjectTypes.FormsPreValue; case FormsDataSource: return UmbracoObjectTypes.FormsDataSource; + case Language: + return UmbracoObjectTypes.Language; } throw new NotSupportedException( string.Format("EntityType \"{0}\" does not have a matching UmbracoObjectType.", entityType)); diff --git a/src/Umbraco.Core/UdiGetterExtensions.cs b/src/Umbraco.Core/UdiGetterExtensions.cs index 631c5b454d..3e4d2ea51a 100644 --- a/src/Umbraco.Core/UdiGetterExtensions.cs +++ b/src/Umbraco.Core/UdiGetterExtensions.cs @@ -97,7 +97,7 @@ namespace Umbraco.Core /// /// The entity. /// The entity identifier of the entity. - public static GuidUdi GetUdi(this Umbraco.Core.Models.EntityContainer entity) + public static GuidUdi GetUdi(this EntityContainer entity) { if (entity == null) throw new ArgumentNullException("entity"); @@ -132,7 +132,7 @@ namespace Umbraco.Core public static GuidUdi GetUdi(this IContent entity) { if (entity == null) throw new ArgumentNullException("entity"); - return new GuidUdi(Constants.UdiEntityType.Document, entity.Key).EnsureClosed(); + return new GuidUdi(entity.IsBlueprint ? Constants.UdiEntityType.DocumentBluePrint : Constants.UdiEntityType.Document, entity.Key).EnsureClosed(); } /// @@ -190,6 +190,17 @@ namespace Umbraco.Core return new GuidUdi(Constants.UdiEntityType.Macro, entity.Key).EnsureClosed(); } + /// + /// Gets the entity identifier of the entity. + /// + /// The entity. + /// The entity identifier of the entity. + public static StringUdi GetUdi(this IUserControl entity) + { + if (entity == null) throw new ArgumentNullException("entity"); + return new StringUdi(Constants.UdiEntityType.UserControl, entity.Path.TrimStart('/')).EnsureClosed(); + } + /// /// Gets the entity identifier of the entity. /// @@ -246,6 +257,17 @@ namespace Umbraco.Core return new GuidUdi(Constants.UdiEntityType.RelationType, entity.Key).EnsureClosed(); } + /// + /// Gets the entity identifier of the entity. + /// + /// The entity. + /// The entity identifier of the entity. + public static StringUdi GetUdi(this ILanguage entity) + { + if (entity == null) throw new ArgumentNullException("entity"); + return new StringUdi(Constants.UdiEntityType.Language, entity.IsoCode).EnsureClosed(); + } + /// /// Gets the entity identifier of the entity. /// @@ -279,7 +301,7 @@ namespace Umbraco.Core var dataTypeComposition = entity as IDataTypeDefinition; if (dataTypeComposition != null) return dataTypeComposition.GetUdi(); - var container = entity as Umbraco.Core.Models.EntityContainer; + var container = entity as EntityContainer; if (container != null) return container.GetUdi(); var media = entity as IMedia; @@ -315,6 +337,9 @@ namespace Umbraco.Core var relationType = entity as IRelationType; if (relationType != null) return relationType.GetUdi(); + var language = entity as ILanguage; + if (language != null) return language.GetUdi(); + throw new NotSupportedException(string.Format("Entity type {0} is not supported.", entity.GetType().FullName)); } }