diff --git a/.github/workflows/codeql-config.yml b/.github/workflows/codeql-config.yml index 59b55e48ec..7bac345491 100644 --- a/.github/workflows/codeql-config.yml +++ b/.github/workflows/codeql-config.yml @@ -9,5 +9,6 @@ paths-ignore: - Umbraco.Tests.AcceptanceTest - Umbraco.Tests.Benchmarks - bin + - build.tmp paths: - - src \ No newline at end of file + - src diff --git a/build/NuSpecs/UmbracoCms.Web.nuspec b/build/NuSpecs/UmbracoCms.Web.nuspec index 48f5f32222..de634b4884 100644 --- a/build/NuSpecs/UmbracoCms.Web.nuspec +++ b/build/NuSpecs/UmbracoCms.Web.nuspec @@ -28,7 +28,7 @@ - + @@ -42,7 +42,7 @@ - + diff --git a/src/SolutionInfo.cs b/src/SolutionInfo.cs index 3ecfd20f03..c064920d34 100644 --- a/src/SolutionInfo.cs +++ b/src/SolutionInfo.cs @@ -18,5 +18,5 @@ using System.Resources; [assembly: AssemblyVersion("8.0.0")] // these are FYI and changed automatically -[assembly: AssemblyFileVersion("8.11.1")] -[assembly: AssemblyInformationalVersion("8.11.1")] +[assembly: AssemblyFileVersion("8.12.0")] +[assembly: AssemblyInformationalVersion("8.12.0")] diff --git a/src/Umbraco.Core/Cache/CacheKeys.cs b/src/Umbraco.Core/Cache/CacheKeys.cs index b8ee0e97c4..0e9a9a3862 100644 --- a/src/Umbraco.Core/Cache/CacheKeys.cs +++ b/src/Umbraco.Core/Cache/CacheKeys.cs @@ -12,5 +12,10 @@ public const string MacroContentCacheKey = "macroContent_"; // used in MacroRenderers public const string MacroFromAliasCacheKey = "macroFromAlias_"; + + public const string UserAllContentStartNodesPrefix = "AllContentStartNodes"; + public const string UserAllMediaStartNodesPrefix = "AllMediaStartNodes"; + public const string UserMediaStartNodePathsPrefix = "MediaStartNodePaths"; + public const string UserContentStartNodePathsPrefix = "ContentStartNodePaths"; } } diff --git a/src/Umbraco.Core/Configuration/GlobalSettings.cs b/src/Umbraco.Core/Configuration/GlobalSettings.cs index 1d1ccaf7b4..b7dce21285 100644 --- a/src/Umbraco.Core/Configuration/GlobalSettings.cs +++ b/src/Umbraco.Core/Configuration/GlobalSettings.cs @@ -6,7 +6,9 @@ using System.Web; using System.Web.Configuration; using System.Web.Hosting; using System.Xml.Linq; +using Umbraco.Core.Composing; using Umbraco.Core.IO; +using Umbraco.Core.Logging; namespace Umbraco.Core.Configuration { @@ -23,6 +25,7 @@ namespace Umbraco.Core.Configuration // TODO these should not be static private static string _reservedPaths; private static string _reservedUrls; + private static int _sqlWriteLockTimeOut; //ensure the built on (non-changeable) reserved paths are there at all times internal const string StaticReservedPaths = "~/app_plugins/,~/install/,~/mini-profiler-resources/,"; //must end with a comma! @@ -397,21 +400,34 @@ namespace Umbraco.Core.Configuration /// An int value representing the time in milliseconds to lock the database for a write operation /// /// - /// The default value is 1800 milliseconds + /// The default value is 5000 milliseconds /// /// The timeout in milliseconds. public int SqlWriteLockTimeOut { get { - try + if (_sqlWriteLockTimeOut != default) return _sqlWriteLockTimeOut; + + var timeOut = 5000; // 5 seconds + var appSettingSqlWriteLockTimeOut = ConfigurationManager.AppSettings[Constants.AppSettings.SqlWriteLockTimeOut]; + if(int.TryParse(appSettingSqlWriteLockTimeOut, out var configuredTimeOut)) { - return int.Parse(ConfigurationManager.AppSettings[Constants.AppSettings.SqlWriteLockTimeOut]); - } - catch - { - return 1800; + // Only apply this setting if it's not excessively high or low + const int minimumTimeOut = 100; + const int maximumTimeOut = 20000; + if (configuredTimeOut >= minimumTimeOut && configuredTimeOut <= maximumTimeOut) // between 0.1 and 20 seconds + { + timeOut = configuredTimeOut; + } + else + { + Current.Logger.Warn($"The `{Constants.AppSettings.SqlWriteLockTimeOut}` setting in web.config is not between the minimum of {minimumTimeOut} ms and maximum of {maximumTimeOut} ms, defaulting back to {timeOut}"); + } } + + _sqlWriteLockTimeOut = timeOut; + return _sqlWriteLockTimeOut; } } } diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs index 3ebb632882..7c6ff4405f 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs @@ -29,11 +29,11 @@ namespace Umbraco.Core.Configuration.UmbracoSettings internal InnerTextConfigurationElement MacroErrors => GetOptionalTextElement("MacroErrors", MacroErrorBehaviour.Inline); [ConfigurationProperty("disallowedUploadFiles")] - internal CommaDelimitedConfigurationElement DisallowedUploadFiles => GetOptionalDelimitedElement("disallowedUploadFiles", new[] {"ashx", "aspx", "ascx", "config", "cshtml", "vbhtml", "asmx", "air", "axd"}); + internal CommaDelimitedConfigurationElement DisallowedUploadFiles => GetOptionalDelimitedElement("disallowedUploadFiles", new[] {"ashx", "aspx", "ascx", "config", "cshtml", "vbhtml", "asmx", "air", "axd", "xamlx"}); [ConfigurationProperty("allowedUploadFiles")] internal CommaDelimitedConfigurationElement AllowedUploadFiles => GetOptionalDelimitedElement("allowedUploadFiles", new string[0]); - + [ConfigurationProperty("showDeprecatedPropertyEditors")] internal InnerTextConfigurationElement ShowDeprecatedPropertyEditors => GetOptionalTextElement("showDeprecatedPropertyEditors", false); diff --git a/src/Umbraco.Core/Constants-AppSettings.cs b/src/Umbraco.Core/Constants-AppSettings.cs index 0182034011..f04f0e1f5f 100644 --- a/src/Umbraco.Core/Constants-AppSettings.cs +++ b/src/Umbraco.Core/Constants-AppSettings.cs @@ -144,9 +144,6 @@ namespace Umbraco.Core /// /// An int value representing the time in milliseconds to lock the database for a write operation /// - /// - /// The default value is 1800 milliseconds - /// public const string SqlWriteLockTimeOut = "Umbraco.Core.SqlWriteLockTimeOut"; } } diff --git a/src/Umbraco.Core/Models/Identity/IdentityMapDefinition.cs b/src/Umbraco.Core/Models/Identity/IdentityMapDefinition.cs index 57e1c9ee5c..2d7eda0bc2 100644 --- a/src/Umbraco.Core/Models/Identity/IdentityMapDefinition.cs +++ b/src/Umbraco.Core/Models/Identity/IdentityMapDefinition.cs @@ -1,4 +1,6 @@ using System; +using Umbraco.Core.Cache; +using Umbraco.Core.Composing; using Umbraco.Core.Configuration; using Umbraco.Core.Mapping; using Umbraco.Core.Models.Membership; @@ -11,12 +13,27 @@ namespace Umbraco.Core.Models.Identity private readonly ILocalizedTextService _textService; private readonly IEntityService _entityService; private readonly IGlobalSettings _globalSettings; + private readonly AppCaches _appCaches; - public IdentityMapDefinition(ILocalizedTextService textService, IEntityService entityService, IGlobalSettings globalSettings) + [Obsolete("Use constructor specifying all dependencies")] + public IdentityMapDefinition( + ILocalizedTextService textService, + IEntityService entityService, + IGlobalSettings globalSettings) + : this(textService, entityService, globalSettings, Current.AppCaches) + { + } + + public IdentityMapDefinition( + ILocalizedTextService textService, + IEntityService entityService, + IGlobalSettings globalSettings, + AppCaches appCaches) { _textService = textService; _entityService = entityService; _globalSettings = globalSettings; + _appCaches = appCaches; } public void DefineMaps(UmbracoMapper mapper) @@ -46,8 +63,8 @@ namespace Umbraco.Core.Models.Identity target.Groups = source.Groups.ToArray(); */ - target.CalculatedMediaStartNodeIds = source.CalculateMediaStartNodeIds(_entityService); - target.CalculatedContentStartNodeIds = source.CalculateContentStartNodeIds(_entityService); + target.CalculatedMediaStartNodeIds = source.CalculateMediaStartNodeIds(_entityService, _appCaches); + target.CalculatedContentStartNodeIds = source.CalculateContentStartNodeIds(_entityService, _appCaches); target.Email = source.Email; target.UserName = source.Username; target.LastPasswordChangeDateUtc = source.LastPasswordChangeDate.ToUniversalTime(); diff --git a/src/Umbraco.Core/Models/Membership/User.cs b/src/Umbraco.Core/Models/Membership/User.cs index 3d071b0a18..dc39463925 100644 --- a/src/Umbraco.Core/Models/Membership/User.cs +++ b/src/Umbraco.Core/Models/Membership/User.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Linq; using System.Runtime.Serialization; using Umbraco.Core.Composing; @@ -384,11 +385,10 @@ namespace Umbraco.Core.Models.Membership #endregion - /// - /// This is used as an internal cache for this entity - specifically for calculating start nodes so we don't re-calculated all of the time - /// [IgnoreDataMember] [DoNotClone] + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("This should not be used, it's currently used for only a single edge case - should probably be removed for netcore")] internal IDictionary AdditionalData { get @@ -402,6 +402,8 @@ namespace Umbraco.Core.Models.Membership [IgnoreDataMember] [DoNotClone] + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Not used, will be removed in future versions")] internal object AdditionalDataLock => _additionalDataLock; protected override void PerformDeepClone(object clone) diff --git a/src/Umbraco.Core/Models/Membership/UserGroup.cs b/src/Umbraco.Core/Models/Membership/UserGroup.cs index 31421f990d..eb797d6621 100644 --- a/src/Umbraco.Core/Models/Membership/UserGroup.cs +++ b/src/Umbraco.Core/Models/Membership/UserGroup.cs @@ -19,7 +19,7 @@ namespace Umbraco.Core.Models.Membership private string _icon; private string _name; private IEnumerable _permissions; - private readonly List _sectionCollection; + private List _sectionCollection; //Custom comparer for enumerable private static readonly DelegateEqualityComparer> StringEnumerableComparer = @@ -101,7 +101,10 @@ namespace Umbraco.Core.Models.Membership set => SetPropertyValueAndDetectChanges(value, ref _permissions, nameof(Permissions), StringEnumerableComparer); } - public IEnumerable AllowedSections => _sectionCollection; + public IEnumerable AllowedSections + { + get => _sectionCollection; + } public void RemoveAllowedSection(string sectionAlias) { @@ -121,5 +124,16 @@ namespace Umbraco.Core.Models.Membership } public int UserCount { get; } + + protected override void PerformDeepClone(object clone) + { + + base.PerformDeepClone(clone); + + var clonedEntity = (UserGroup)clone; + + //manually clone the start node props + clonedEntity._sectionCollection = new List(_sectionCollection); + } } } diff --git a/src/Umbraco.Core/Models/UserExtensions.cs b/src/Umbraco.Core/Models/UserExtensions.cs index 5be66bac47..fdb833a821 100644 --- a/src/Umbraco.Core/Models/UserExtensions.cs +++ b/src/Umbraco.Core/Models/UserExtensions.cs @@ -150,48 +150,40 @@ namespace Umbraco.Core.Models } } - internal static bool HasContentRootAccess(this IUser user, IEntityService entityService) - { - return ContentPermissionsHelper.HasPathAccess(Constants.System.RootString, user.CalculateContentStartNodeIds(entityService), Constants.System.RecycleBinContent); - } + internal static bool HasContentRootAccess(this IUser user, IEntityService entityService, AppCaches appCaches) + => ContentPermissionsHelper.HasPathAccess(Constants.System.RootString, user.CalculateContentStartNodeIds(entityService, appCaches), Constants.System.RecycleBinContent); - internal static bool HasContentBinAccess(this IUser user, IEntityService entityService) - { - return ContentPermissionsHelper.HasPathAccess(Constants.System.RecycleBinContentString, user.CalculateContentStartNodeIds(entityService), Constants.System.RecycleBinContent); - } + internal static bool HasContentBinAccess(this IUser user, IEntityService entityService, AppCaches appCaches) + => ContentPermissionsHelper.HasPathAccess(Constants.System.RecycleBinContentString, user.CalculateContentStartNodeIds(entityService, appCaches), Constants.System.RecycleBinContent); - internal static bool HasMediaRootAccess(this IUser user, IEntityService entityService) - { - return ContentPermissionsHelper.HasPathAccess(Constants.System.RootString, user.CalculateMediaStartNodeIds(entityService), Constants.System.RecycleBinMedia); - } + internal static bool HasMediaRootAccess(this IUser user, IEntityService entityService, AppCaches appCaches) + => ContentPermissionsHelper.HasPathAccess(Constants.System.RootString, user.CalculateMediaStartNodeIds(entityService, appCaches), Constants.System.RecycleBinMedia); - internal static bool HasMediaBinAccess(this IUser user, IEntityService entityService) - { - return ContentPermissionsHelper.HasPathAccess(Constants.System.RecycleBinMediaString, user.CalculateMediaStartNodeIds(entityService), Constants.System.RecycleBinMedia); - } + internal static bool HasMediaBinAccess(this IUser user, IEntityService entityService, AppCaches appCaches) + => ContentPermissionsHelper.HasPathAccess(Constants.System.RecycleBinMediaString, user.CalculateMediaStartNodeIds(entityService, appCaches), Constants.System.RecycleBinMedia); - internal static bool HasPathAccess(this IUser user, IContent content, IEntityService entityService) + internal static bool HasPathAccess(this IUser user, IContent content, IEntityService entityService, AppCaches appCaches) { if (content == null) throw new ArgumentNullException(nameof(content)); - return ContentPermissionsHelper.HasPathAccess(content.Path, user.CalculateContentStartNodeIds(entityService), Constants.System.RecycleBinContent); + return ContentPermissionsHelper.HasPathAccess(content.Path, user.CalculateContentStartNodeIds(entityService, appCaches), Constants.System.RecycleBinContent); } - internal static bool HasPathAccess(this IUser user, IMedia media, IEntityService entityService) + internal static bool HasPathAccess(this IUser user, IMedia media, IEntityService entityService, AppCaches appCaches) { if (media == null) throw new ArgumentNullException(nameof(media)); - return ContentPermissionsHelper.HasPathAccess(media.Path, user.CalculateMediaStartNodeIds(entityService), Constants.System.RecycleBinMedia); + return ContentPermissionsHelper.HasPathAccess(media.Path, user.CalculateMediaStartNodeIds(entityService, appCaches), Constants.System.RecycleBinMedia); } - internal static bool HasContentPathAccess(this IUser user, IUmbracoEntity entity, IEntityService entityService) + internal static bool HasContentPathAccess(this IUser user, IUmbracoEntity entity, IEntityService entityService, AppCaches appCaches) { if (entity == null) throw new ArgumentNullException(nameof(entity)); - return ContentPermissionsHelper.HasPathAccess(entity.Path, user.CalculateContentStartNodeIds(entityService), Constants.System.RecycleBinContent); + return ContentPermissionsHelper.HasPathAccess(entity.Path, user.CalculateContentStartNodeIds(entityService, appCaches), Constants.System.RecycleBinContent); } - internal static bool HasMediaPathAccess(this IUser user, IUmbracoEntity entity, IEntityService entityService) + internal static bool HasMediaPathAccess(this IUser user, IUmbracoEntity entity, IEntityService entityService, AppCaches appCaches) { if (entity == null) throw new ArgumentNullException(nameof(entity)); - return ContentPermissionsHelper.HasPathAccess(entity.Path, user.CalculateMediaStartNodeIds(entityService), Constants.System.RecycleBinMedia); + return ContentPermissionsHelper.HasPathAccess(entity.Path, user.CalculateMediaStartNodeIds(entityService, appCaches), Constants.System.RecycleBinMedia); } /// @@ -204,84 +196,92 @@ namespace Umbraco.Core.Models return user.Groups != null && user.Groups.Any(x => x.Alias == Constants.Security.SensitiveDataGroupAlias); } - // calc. start nodes, combining groups' and user's, and excluding what's in the bin + [Obsolete("Use the overload specifying all parameters instead")] 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; + => CalculateContentStartNodeIds(user, entityService, Current.AppCaches); - 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; + /// + /// Calculate start nodes, combining groups' and user's, and excluding what's in the bin + /// + /// + /// + /// + /// + public static int[] CalculateContentStartNodeIds(this IUser user, IEntityService entityService, AppCaches appCaches) + { + var cacheKey = CacheKeys.UserAllContentStartNodesPrefix + user.Id; + var runtimeCache = appCaches.IsolatedCaches.GetOrCreate(); + var result = runtimeCache.GetCacheItem(cacheKey, () => + { + 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); + return vals; + }, TimeSpan.FromMinutes(2), true); + + return result; } - // calc. start nodes, combining groups' and user's, and excluding what's in the bin + [Obsolete("Use the overload specifying all parameters instead")] 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; + => CalculateMediaStartNodeIds(user, entityService, Current.AppCaches); - 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; + /// + /// Calculate start nodes, combining groups' and user's, and excluding what's in the bin + /// + /// + /// + /// + /// + public static int[] CalculateMediaStartNodeIds(this IUser user, IEntityService entityService, AppCaches appCaches) + { + var cacheKey = CacheKeys.UserAllMediaStartNodesPrefix + user.Id; + var runtimeCache = appCaches.IsolatedCaches.GetOrCreate(); + var result = runtimeCache.GetCacheItem(cacheKey, () => + { + 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); + return vals; + }, TimeSpan.FromMinutes(2), true); + + return result; } + [Obsolete("Use the overload specifying all parameters instead")] public static string[] GetMediaStartNodePaths(this IUser user, IEntityService entityService) - { - const string cacheKey = "MediaStartNodePaths"; - //try to look them up from cache so we don't recalculate - var valuesInUserCache = FromUserCache(user, cacheKey); - if (valuesInUserCache != null) return valuesInUserCache; + => GetMediaStartNodePaths(user, entityService, Current.AppCaches); - var startNodeIds = user.CalculateMediaStartNodeIds(entityService); - var vals = entityService.GetAllPaths(UmbracoObjectTypes.Media, startNodeIds).Select(x => x.Path).ToArray(); - ToUserCache(user, cacheKey, vals); - return vals; + public static string[] GetMediaStartNodePaths(this IUser user, IEntityService entityService, AppCaches appCaches) + { + var cacheKey = CacheKeys.UserMediaStartNodePathsPrefix + user.Id; + var runtimeCache = appCaches.IsolatedCaches.GetOrCreate(); + var result = runtimeCache.GetCacheItem(cacheKey, () => + { + var startNodeIds = user.CalculateMediaStartNodeIds(entityService, appCaches); + var vals = entityService.GetAllPaths(UmbracoObjectTypes.Media, startNodeIds).Select(x => x.Path).ToArray(); + return vals; + }, TimeSpan.FromMinutes(2), true); + + return result; } + [Obsolete("Use the overload specifying all parameters instead")] public static string[] GetContentStartNodePaths(this IUser user, IEntityService entityService) + => GetContentStartNodePaths(user, entityService, Current.AppCaches); + + public static string[] GetContentStartNodePaths(this IUser user, IEntityService entityService, AppCaches appCaches) { - const string cacheKey = "ContentStartNodePaths"; - //try to look them up from cache so we don't recalculate - var valuesInUserCache = FromUserCache(user, cacheKey); - if (valuesInUserCache != null) return valuesInUserCache; - - var startNodeIds = user.CalculateContentStartNodeIds(entityService); - var vals = entityService.GetAllPaths(UmbracoObjectTypes.Document, startNodeIds).Select(x => x.Path).ToArray(); - ToUserCache(user, cacheKey, vals); - return vals; - } - - private static T FromUserCache(IUser user, string cacheKey) - where T: class - { - if (!(user is User entityUser)) return null; - - lock (entityUser.AdditionalDataLock) + var cacheKey = CacheKeys.UserContentStartNodePathsPrefix + user.Id; + var runtimeCache = appCaches.IsolatedCaches.GetOrCreate(); + var result = runtimeCache.GetCacheItem(cacheKey, () => { - return entityUser.AdditionalData.TryGetValue(cacheKey, out var allContentStartNodes) - ? allContentStartNodes as T - : null; - } - } + var startNodeIds = user.CalculateContentStartNodeIds(entityService, appCaches); + var vals = entityService.GetAllPaths(UmbracoObjectTypes.Document, startNodeIds).Select(x => x.Path).ToArray(); + return vals; + }, TimeSpan.FromMinutes(2), true); - private static void ToUserCache(IUser user, string cacheKey, T vals) - where T: class - { - if (!(user is User entityUser)) return; - - lock (entityUser.AdditionalDataLock) - { - entityUser.AdditionalData[cacheKey] = vals; - } + return result; } private static bool StartsWithPath(string test, string path) diff --git a/src/Umbraco.Core/Packaging/PackageDataInstallation.cs b/src/Umbraco.Core/Packaging/PackageDataInstallation.cs index 4d1f12baaa..6262f99c56 100644 --- a/src/Umbraco.Core/Packaging/PackageDataInstallation.cs +++ b/src/Umbraco.Core/Packaging/PackageDataInstallation.cs @@ -358,12 +358,28 @@ namespace Umbraco.Core.Packaging Key = key }; + // Handle culture specific node names + const string nodeNamePrefix = "nodeName-"; + // Get the installed culture iso names, we create a localized content node with a culture that does not exist in the project + // We have to use Invariant comparisons, because when we get them from ContentBase in EntityXmlSerializer they're all lowercase. + var installedLanguages = _localizationService.GetAllLanguages().Select(l => l.IsoCode).ToArray(); + foreach (var localizedNodeName in element.Attributes().Where(a => a.Name.LocalName.InvariantStartsWith(nodeNamePrefix))) + { + var newCulture = localizedNodeName.Name.LocalName.Substring(nodeNamePrefix.Length); + // Skip the culture if it does not exist in the current project + if (installedLanguages.InvariantContains(newCulture)) + { + content.SetCultureName(localizedNodeName.Value, newCulture); + } + } + //Here we make sure that we take composition properties in account as well //otherwise we would skip them and end up losing content var propTypes = contentType.CompositionPropertyTypes.Any() ? contentType.CompositionPropertyTypes.ToDictionary(x => x.Alias, x => x) : contentType.PropertyTypes.ToDictionary(x => x.Alias, x => x); + var foundLanguages = new HashSet(); foreach (var property in properties) { string propertyTypeAlias = property.Name.LocalName; @@ -371,14 +387,30 @@ namespace Umbraco.Core.Packaging { var propertyValue = property.Value; + // Handle properties language attributes + var propertyLang = property.Attribute(XName.Get("lang"))?.Value; + foundLanguages.Add(propertyLang); if (propTypes.TryGetValue(propertyTypeAlias, out var propertyType)) { - //set property value - content.SetValue(propertyTypeAlias, propertyValue); + // set property value + // Skip unsupported language variation, otherwise we'll get a "not supported error" + // We allow null, because that's invariant + if (installedLanguages.InvariantContains(propertyLang) || propertyLang is null) + { + content.SetValue(propertyTypeAlias, propertyValue, propertyLang); + } } } } + foreach (var propertyLang in foundLanguages) + { + if (string.IsNullOrEmpty(content.GetCultureName(propertyLang)) && installedLanguages.InvariantContains(propertyLang)) + { + content.SetCultureName(nodeName, propertyLang); + } + } + return content; } diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/ISqlSyntaxProvider.cs b/src/Umbraco.Core/Persistence/SqlSyntax/ISqlSyntaxProvider.cs index 7ae001bf24..4c322f9648 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/ISqlSyntaxProvider.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/ISqlSyntaxProvider.cs @@ -9,6 +9,12 @@ using Umbraco.Core.Persistence.Querying; namespace Umbraco.Core.Persistence.SqlSyntax { + public interface ISqlSyntaxProvider2 : ISqlSyntaxProvider + { + void ReadLock(IDatabase db, TimeSpan timeout, int lockId); + void WriteLock(IDatabase db, TimeSpan timeout, int lockId); + } + /// /// Defines an SqlSyntaxProvider /// @@ -77,7 +83,7 @@ namespace Umbraco.Core.Persistence.SqlSyntax string ConvertIntegerToOrderableString { get; } string ConvertDateToOrderableString { get; } string ConvertDecimalToOrderableString { get; } - + /// /// Returns the default isolation level for the database /// diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/SqlCeSyntaxProvider.cs b/src/Umbraco.Core/Persistence/SqlSyntax/SqlCeSyntaxProvider.cs index 9c5edf595b..adbcd11aca 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/SqlCeSyntaxProvider.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/SqlCeSyntaxProvider.cs @@ -166,6 +166,16 @@ where table_name=@0 and column_name=@1", tableName, columnName).FirstOrDefault() return result > 0; } + public override void WriteLock(IDatabase db, TimeSpan timeout, int lockId) + { + // soon as we get Database, a transaction is started + + if (db.Transaction.IsolationLevel < IsolationLevel.RepeatableRead) + throw new InvalidOperationException("A transaction with minimum RepeatableRead isolation level is required."); + + ObtainWriteLock(db, timeout, lockId); + } + public override void WriteLock(IDatabase db, params int[] lockIds) { // soon as we get Database, a transaction is started @@ -173,17 +183,32 @@ where table_name=@0 and column_name=@1", tableName, columnName).FirstOrDefault() if (db.Transaction.IsolationLevel < IsolationLevel.RepeatableRead) throw new InvalidOperationException("A transaction with minimum RepeatableRead isolation level is required."); - var timeOut = Current.Configs.Global().SqlWriteLockTimeOut; - db.Execute(@"SET LOCK_TIMEOUT " + timeOut + ";"); - // *not* using a unique 'WHERE IN' query here because the *order* of lockIds is important to avoid deadlocks + var timeout = TimeSpan.FromMilliseconds(Current.Configs.Global().SqlWriteLockTimeOut); + foreach (var lockId in lockIds) { - var i = db.Execute(@"UPDATE umbracoLock SET value = (CASE WHEN (value=1) THEN -1 ELSE 1 END) WHERE id=@id", new { id = lockId }); - if (i == 0) // ensure we are actually locking! - throw new ArgumentException($"LockObject with id={lockId} does not exist."); + ObtainWriteLock(db, timeout, lockId); } } + private static void ObtainWriteLock(IDatabase db, TimeSpan timeout, int lockId) + { + db.Execute(@"SET LOCK_TIMEOUT " + timeout.TotalMilliseconds + ";"); + var i = db.Execute(@"UPDATE umbracoLock SET value = (CASE WHEN (value=1) THEN -1 ELSE 1 END) WHERE id=@id", new { id = lockId }); + if (i == 0) // ensure we are actually locking! + throw new ArgumentException($"LockObject with id={lockId} does not exist."); + } + + public override void ReadLock(IDatabase db, TimeSpan timeout, int lockId) + { + // soon as we get Database, a transaction is started + + if (db.Transaction.IsolationLevel < IsolationLevel.RepeatableRead) + throw new InvalidOperationException("A transaction with minimum RepeatableRead isolation level is required."); + + ObtainReadLock(db, timeout, lockId); + } + public override void ReadLock(IDatabase db, params int[] lockIds) { // soon as we get Database, a transaction is started @@ -191,15 +216,25 @@ where table_name=@0 and column_name=@1", tableName, columnName).FirstOrDefault() if (db.Transaction.IsolationLevel < IsolationLevel.RepeatableRead) throw new InvalidOperationException("A transaction with minimum RepeatableRead isolation level is required."); - // *not* using a unique 'WHERE IN' query here because the *order* of lockIds is important to avoid deadlocks foreach (var lockId in lockIds) { - var i = db.ExecuteScalar("SELECT value FROM umbracoLock WHERE id=@id", new { id = lockId }); - if (i == null) // ensure we are actually locking! - throw new ArgumentException($"LockObject with id={lockId} does not exist."); + ObtainReadLock(db, null, lockId); } } + private static void ObtainReadLock(IDatabase db, TimeSpan? timeout, int lockId) + { + if (timeout.HasValue) + { + db.Execute(@"SET LOCK_TIMEOUT " + timeout.Value.TotalMilliseconds + ";"); + } + + var i = db.ExecuteScalar("SELECT value FROM umbracoLock WHERE id=@id", new {id = lockId}); + + if (i == null) // ensure we are actually locking! + throw new ArgumentException($"LockObject with id={lockId} does not exist."); + } + protected override string FormatIdentity(ColumnDefinition column) { return column.IsIdentity ? GetIdentityString(column) : string.Empty; diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs b/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs index 313b2352a9..6168778f5b 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs @@ -254,30 +254,50 @@ where tbl.[name]=@0 and col.[name]=@1;", tableName, columnName) return result > 0; } - public override void WriteLock(IDatabase db, params int[] lockIds) - { - var timeOut = Current.Configs.Global().SqlWriteLockTimeOut; - WriteLock(db, TimeSpan.FromMilliseconds(timeOut), lockIds); - } - - public void WriteLock(IDatabase db, TimeSpan timeout, params int[] lockIds) + public override void WriteLock(IDatabase db, TimeSpan timeout, int lockId) { // soon as we get Database, a transaction is started if (db.Transaction.IsolationLevel < IsolationLevel.ReadCommitted) throw new InvalidOperationException("A transaction with minimum ReadCommitted isolation level is required."); + ObtainWriteLock(db, timeout, lockId); + } + + public override void WriteLock(IDatabase db, params int[] lockIds) + { + // soon as we get Database, a transaction is started + + if (db.Transaction.IsolationLevel < IsolationLevel.ReadCommitted) + throw new InvalidOperationException("A transaction with minimum ReadCommitted isolation level is required."); + + var timeout = TimeSpan.FromMilliseconds(Current.Configs.Global().SqlWriteLockTimeOut); - // *not* using a unique 'WHERE IN' query here because the *order* of lockIds is important to avoid deadlocks foreach (var lockId in lockIds) { - db.Execute($"SET LOCK_TIMEOUT {timeout.TotalMilliseconds};"); - var i = db.Execute(@"UPDATE umbracoLock WITH (REPEATABLEREAD) SET value = (CASE WHEN (value=1) THEN -1 ELSE 1 END) WHERE id=@id", new { id = lockId }); - if (i == 0) // ensure we are actually locking! - throw new ArgumentException($"LockObject with id={lockId} does not exist."); + ObtainWriteLock(db, timeout, lockId); } } + private static void ObtainWriteLock(IDatabase db, TimeSpan timeout, int lockId) + { + db.Execute("SET LOCK_TIMEOUT " + timeout.TotalMilliseconds + ";"); + var i = db.Execute( + @"UPDATE umbracoLock WITH (REPEATABLEREAD) SET value = (CASE WHEN (value=1) THEN -1 ELSE 1 END) WHERE id=@id", + new {id = lockId}); + if (i == 0) // ensure we are actually locking! + throw new ArgumentException($"LockObject with id={lockId} does not exist."); + } + + public override void ReadLock(IDatabase db, TimeSpan timeout, int lockId) + { + // soon as we get Database, a transaction is started + + if (db.Transaction.IsolationLevel < IsolationLevel.ReadCommitted) + throw new InvalidOperationException("A transaction with minimum RepeatableRead isolation level is required."); + + ObtainReadLock(db, timeout, lockId); + } public override void ReadLock(IDatabase db, params int[] lockIds) { @@ -286,15 +306,25 @@ where tbl.[name]=@0 and col.[name]=@1;", tableName, columnName) if (db.Transaction.IsolationLevel < IsolationLevel.ReadCommitted) throw new InvalidOperationException("A transaction with minimum ReadCommitted isolation level is required."); - // *not* using a unique 'WHERE IN' query here because the *order* of lockIds is important to avoid deadlocks foreach (var lockId in lockIds) { - var i = db.ExecuteScalar("SELECT value FROM umbracoLock WITH (REPEATABLEREAD) WHERE id=@id", new { id = lockId }); - if (i == null) // ensure we are actually locking! - throw new ArgumentException($"LockObject with id={lockId} does not exist.", nameof(lockIds)); + ObtainReadLock(db, null, lockId); } } + private static void ObtainReadLock(IDatabase db, TimeSpan? timeout, int lockId) + { + if (timeout.HasValue) + { + db.Execute(@"SET LOCK_TIMEOUT " + timeout.Value.TotalMilliseconds + ";"); + } + + var i = db.ExecuteScalar("SELECT value FROM umbracoLock WITH (REPEATABLEREAD) WHERE id=@id", new {id = lockId}); + + if (i == null) // ensure we are actually locking! + throw new ArgumentException($"LockObject with id={lockId} does not exist."); + } + public override string FormatColumnRename(string tableName, string oldName, string newName) { return string.Format(RenameColumn, tableName, oldName, newName); diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs b/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs index 8570c49f69..6f13afb24c 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs @@ -19,7 +19,7 @@ namespace Umbraco.Core.Persistence.SqlSyntax /// All Sql Syntax provider implementations should derive from this abstract class. /// /// - public abstract class SqlSyntaxProviderBase : ISqlSyntaxProvider + public abstract class SqlSyntaxProviderBase : ISqlSyntaxProvider2 where TSyntax : ISqlSyntaxProvider { protected SqlSyntaxProviderBase() @@ -235,6 +235,9 @@ namespace Umbraco.Core.Persistence.SqlSyntax public abstract void ReadLock(IDatabase db, params int[] lockIds); public abstract void WriteLock(IDatabase db, params int[] lockIds); + public abstract void ReadLock(IDatabase db, TimeSpan timeout, int lockId); + + public abstract void WriteLock(IDatabase db, TimeSpan timeout, int lockId); public virtual bool DoesTableExist(IDatabase db, string tableName) { diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/ImageCropperValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/ImageCropperValueConverter.cs index 8926174c03..29e501f993 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/ImageCropperValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/ImageCropperValueConverter.cs @@ -25,6 +25,12 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Element; + private static readonly JsonSerializerSettings ImageCropperValueJsonSerializerSettings = new JsonSerializerSettings + { + Culture = CultureInfo.InvariantCulture, + FloatParseHandling = FloatParseHandling.Decimal + }; + /// public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) { @@ -34,11 +40,7 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters ImageCropperValue value; try { - value = JsonConvert.DeserializeObject(sourceString, new JsonSerializerSettings - { - Culture = CultureInfo.InvariantCulture, - FloatParseHandling = FloatParseHandling.Decimal - }); + value = JsonConvert.DeserializeObject(sourceString, ImageCropperValueJsonSerializerSettings); } catch (Exception ex) { diff --git a/src/Umbraco.Core/Scoping/IScope.cs b/src/Umbraco.Core/Scoping/IScope.cs index de4eef0a08..0c38031558 100644 --- a/src/Umbraco.Core/Scoping/IScope.cs +++ b/src/Umbraco.Core/Scoping/IScope.cs @@ -5,6 +5,24 @@ using Umbraco.Core.Persistence; namespace Umbraco.Core.Scoping { + // TODO: This is for backward compat - Merge this in netcore + public interface IScope2 : IScope + { + /// + /// Write-locks some lock objects. + /// + /// The database timeout in milliseconds + /// The lock object identifier. + void WriteLock(TimeSpan timeout, int lockId); + + /// + /// Read-locks some lock objects. + /// + /// The database timeout in milliseconds + /// The lock object identifier. + void ReadLock(TimeSpan timeout, int lockId); + } + /// /// Represents a scope. /// diff --git a/src/Umbraco.Core/Scoping/Scope.cs b/src/Umbraco.Core/Scoping/Scope.cs index 84273e23da..7015cee5eb 100644 --- a/src/Umbraco.Core/Scoping/Scope.cs +++ b/src/Umbraco.Core/Scoping/Scope.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Data; using Umbraco.Core.Cache; using Umbraco.Core.Composing; @@ -6,6 +7,7 @@ using Umbraco.Core.Events; using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Scoping { @@ -13,7 +15,7 @@ namespace Umbraco.Core.Scoping /// Implements . /// /// Not thread-safe obviously. - internal class Scope : IScope + internal class Scope : IScope2 { private readonly ScopeProvider _scopeProvider; private readonly ILogger _logger; @@ -33,6 +35,13 @@ namespace Umbraco.Core.Scoping private ICompletable _fscope; private IEventDispatcher _eventDispatcher; + private object _dictionaryLocker; + + // ReadLocks and WriteLocks if we're the outer most scope it's those owned by the entire chain + // If we're a child scope it's those that we have requested. + internal readonly Dictionary ReadLocks; + internal readonly Dictionary WriteLocks; + // initializes a new scope private Scope(ScopeProvider scopeProvider, ILogger logger, FileSystems fileSystems, Scope parent, ScopeContext scopeContext, bool detachable, @@ -57,6 +66,10 @@ namespace Umbraco.Core.Scoping Detachable = detachable; + _dictionaryLocker = new object(); + ReadLocks = new Dictionary(); + WriteLocks = new Dictionary(); + #if DEBUG_SCOPES _scopeProvider.RegisterScope(this); Console.WriteLine("create " + InstanceId.ToString("N").Substring(0, 8)); @@ -347,6 +360,23 @@ namespace Umbraco.Core.Scoping #endif } + // Decrement the lock counters on the parent if any. + if (ParentScope != null) + { + lock (_dictionaryLocker) + { + foreach (var readLockPair in ReadLocks) + { + DecrementReadLock(readLockPair.Key, readLockPair.Value); + } + + foreach (var writeLockPair in WriteLocks) + { + DecrementWriteLock(writeLockPair.Key, writeLockPair.Value); + } + } + } + var parent = ParentScope; _scopeProvider.AmbientScope = parent; // might be null = this is how scopes are removed from context objects @@ -485,10 +515,260 @@ namespace Umbraco.Core.Scoping private static bool LogUncompletedScopes => (_logUncompletedScopes ?? (_logUncompletedScopes = Current.Configs.CoreDebug().LogUncompletedScopes)).Value; - /// - public void ReadLock(params int[] lockIds) => Database.SqlContext.SqlSyntax.ReadLock(Database, lockIds); + /// + /// Decrements the count of the ReadLocks with a specific lock object identifier we currently hold + /// + /// Lock object identifier to decrement + /// Amount to decrement the lock count with + public void DecrementReadLock(int lockId, int amountToDecrement) + { + // If we aren't the outermost scope, pass it on to the parent. + if (ParentScope != null) + { + ParentScope.DecrementReadLock(lockId, amountToDecrement); + return; + } + + lock (_dictionaryLocker) + { + ReadLocks[lockId] -= amountToDecrement; + } + } + + /// + /// Decrements the count of the WriteLocks with a specific lock object identifier we currently hold. + /// + /// Lock object identifier to decrement. + /// Amount to decrement the lock count with + public void DecrementWriteLock(int lockId, int amountToDecrement) + { + // If we aren't the outermost scope, pass it on to the parent. + if (ParentScope != null) + { + ParentScope.DecrementWriteLock(lockId, amountToDecrement); + return; + } + + lock (_dictionaryLocker) + { + WriteLocks[lockId] -= amountToDecrement; + } + } + + /// + /// Increment the count of the read locks we've requested + /// + /// + /// This should only be done on child scopes since it's then used to decrement the count later. + /// + /// + private void IncrementRequestedReadLock(params int[] lockIds) + { + // We need to keep track of what lockIds we have requested locks for to be able to decrement them. + if (ParentScope != null) + { + foreach (var lockId in lockIds) + { + lock (_dictionaryLocker) + { + if (ReadLocks.ContainsKey(lockId)) + { + ReadLocks[lockId] += 1; + } + else + { + ReadLocks[lockId] = 1; + } + } + } + } + } + + /// + /// Increment the count of the write locks we've requested + /// + /// + /// This should only be done on child scopes since it's then used to decrement the count later. + /// + /// + private void IncrementRequestedWriteLock(params int[] lockIds) + { + // We need to keep track of what lockIds we have requested locks for to be able to decrement them. + if (ParentScope != null) + { + foreach (var lockId in lockIds) + { + lock (_dictionaryLocker) + { + if (WriteLocks.ContainsKey(lockId)) + { + WriteLocks[lockId] += 1; + } + else + { + WriteLocks[lockId] = 1; + } + } + } + } + } /// - public void WriteLock(params int[] lockIds) => Database.SqlContext.SqlSyntax.WriteLock(Database, lockIds); + public void ReadLock(params int[] lockIds) + { + IncrementRequestedReadLock(lockIds); + ReadLockInner(null, lockIds); + } + + /// + public void ReadLock(TimeSpan timeout, int lockId) + { + IncrementRequestedReadLock(lockId); + ReadLockInner(timeout, lockId); + } + + /// + public void WriteLock(params int[] lockIds) + { + IncrementRequestedWriteLock(lockIds); + WriteLockInner(null, lockIds); + } + + /// + public void WriteLock(TimeSpan timeout, int lockId) + { + IncrementRequestedWriteLock(lockId); + WriteLockInner(timeout, lockId); + } + + /// + /// Handles acquiring a read lock, will delegate it to the parent if there are any. + /// + /// Optional database timeout in milliseconds. + /// Array of lock object identifiers. + internal void ReadLockInner(TimeSpan? timeout = null, params int[] lockIds) + { + if (ParentScope != null) + { + // Delegate acquiring the lock to the parent if any. + ParentScope.ReadLockInner(timeout, lockIds); + return; + } + + // If we are the parent, then handle the lock request. + foreach (var lockId in lockIds) + { + lock (_dictionaryLocker) + { + // Only acquire the lock if we haven't done so yet. + if (!ReadLocks.ContainsKey(lockId)) + { + if (timeout is null) + { + // We want a lock with a custom timeout + ObtainReadLock(lockId); + } + else + { + // We just want an ordinary lock. + ObtainTimoutReadLock(lockId, timeout.Value); + } + // Add the lockId as a key to the dict. + ReadLocks[lockId] = 0; + } + + ReadLocks[lockId] += 1; + } + } + } + + /// + /// Handles acquiring a write lock with a specified timeout, will delegate it to the parent if there are any. + /// + /// Optional database timeout in milliseconds. + /// Array of lock object identifiers. + internal void WriteLockInner(TimeSpan? timeout = null, params int[] lockIds) + { + if (ParentScope != null) + { + // If we have a parent we delegate lock creation to parent. + ParentScope.WriteLockInner(timeout, lockIds); + return; + } + + foreach (var lockId in lockIds) + { + lock (_dictionaryLocker) + { + // Only acquire lock if we haven't yet (WriteLocks not containing the key) + if (!WriteLocks.ContainsKey(lockId)) + { + if (timeout is null) + { + ObtainWriteLock(lockId); + } + else + { + ObtainTimeoutWriteLock(lockId, timeout.Value); + } + // Add the lockId as a key to the dict. + WriteLocks[lockId] = 0; + } + + // Increment count of the lock by 1. + WriteLocks[lockId] += 1; + } + } + } + + /// + /// Obtains an ordinary read lock. + /// + /// Lock object identifier to lock. + private void ObtainReadLock(int lockId) + { + Database.SqlContext.SqlSyntax.ReadLock(Database, lockId); + } + + /// + /// Obtains a read lock with a custom timeout. + /// + /// Lock object identifier to lock. + /// TimeSpan specifying the timout period. + private void ObtainTimoutReadLock(int lockId, TimeSpan timeout) + { + var syntax2 = Database.SqlContext.SqlSyntax as ISqlSyntaxProvider2; + if (syntax2 == null) + { + throw new InvalidOperationException($"{Database.SqlContext.SqlSyntax.GetType()} is not of type {typeof(ISqlSyntaxProvider2)}"); + } + + syntax2.ReadLock(Database, timeout, lockId); + } + + /// + /// Obtains an ordinary write lock. + /// + /// Lock object identifier to lock. + private void ObtainWriteLock(int lockId) + { + Database.SqlContext.SqlSyntax.WriteLock(Database, lockId); + } + + /// + /// Obtains a write lock with a custom timeout. + /// + /// Lock object identifier to lock. + /// TimeSpan specifying the timout period. + private void ObtainTimeoutWriteLock(int lockId, TimeSpan timeout) + { + var syntax2 = Database.SqlContext.SqlSyntax as ISqlSyntaxProvider2; + if (syntax2 == null) + { + throw new InvalidOperationException($"{Database.SqlContext.SqlSyntax.GetType()} is not of type {typeof(ISqlSyntaxProvider2)}"); + } + + syntax2.WriteLock(Database, timeout, lockId); + } } } diff --git a/src/Umbraco.Core/Security/BackOfficeUserStore.cs b/src/Umbraco.Core/Security/BackOfficeUserStore.cs index 7df328b5b7..fe1673bca6 100644 --- a/src/Umbraco.Core/Security/BackOfficeUserStore.cs +++ b/src/Umbraco.Core/Security/BackOfficeUserStore.cs @@ -5,6 +5,8 @@ using System.Linq; using System.Threading.Tasks; using System.Web.Security; using Microsoft.AspNet.Identity; +using Umbraco.Core.Cache; +using Umbraco.Core.Composing; using Umbraco.Core.Configuration; using Umbraco.Core.Mapping; using Umbraco.Core.Models; @@ -38,20 +40,26 @@ namespace Umbraco.Core.Security private readonly IExternalLoginService _externalLoginService; private readonly IGlobalSettings _globalSettings; private readonly UmbracoMapper _mapper; + private readonly AppCaches _appCaches; private bool _disposed = false; + [Obsolete("Use the constructor specifying all dependencies")] public BackOfficeUserStore(IUserService userService, IMemberTypeService memberTypeService, IEntityService entityService, IExternalLoginService externalLoginService, IGlobalSettings globalSettings, MembershipProviderBase usersMembershipProvider, UmbracoMapper mapper) + : this(userService, memberTypeService, entityService, externalLoginService, globalSettings, usersMembershipProvider, mapper, Current.AppCaches) { } + + public BackOfficeUserStore(IUserService userService, IMemberTypeService memberTypeService, IEntityService entityService, IExternalLoginService externalLoginService, IGlobalSettings globalSettings, MembershipProviderBase usersMembershipProvider, UmbracoMapper mapper, AppCaches appCaches) { + if (userService == null) throw new ArgumentNullException("userService"); + if (usersMembershipProvider == null) throw new ArgumentNullException("usersMembershipProvider"); + if (externalLoginService == null) throw new ArgumentNullException("externalLoginService"); + _userService = userService; _memberTypeService = memberTypeService; _entityService = entityService; _externalLoginService = externalLoginService; - _globalSettings = globalSettings; - if (userService == null) throw new ArgumentNullException("userService"); - if (usersMembershipProvider == null) throw new ArgumentNullException("usersMembershipProvider"); - if (externalLoginService == null) throw new ArgumentNullException("externalLoginService"); + _globalSettings = globalSettings; _mapper = mapper; - + _appCaches = appCaches; _userService = userService; _externalLoginService = externalLoginService; @@ -775,8 +783,8 @@ namespace Umbraco.Core.Security } //we should re-set the calculated start nodes - identityUser.CalculatedMediaStartNodeIds = user.CalculateMediaStartNodeIds(_entityService); - identityUser.CalculatedContentStartNodeIds = user.CalculateContentStartNodeIds(_entityService); + identityUser.CalculatedMediaStartNodeIds = user.CalculateMediaStartNodeIds(_entityService, _appCaches); + identityUser.CalculatedContentStartNodeIds = user.CalculateContentStartNodeIds(_entityService, _appCaches); //reset all changes identityUser.ResetDirtyProperties(false); diff --git a/src/Umbraco.Core/Security/ContentPermissionsHelper.cs b/src/Umbraco.Core/Security/ContentPermissionsHelper.cs index 1a329fcdcb..8c3a138f6a 100644 --- a/src/Umbraco.Core/Security/ContentPermissionsHelper.cs +++ b/src/Umbraco.Core/Security/ContentPermissionsHelper.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.Linq; using System.Text; using System.Threading.Tasks; +using Umbraco.Core.Cache; using Umbraco.Core.Models; using Umbraco.Core.Models.Entities; using Umbraco.Core.Models.Membership; @@ -25,6 +26,7 @@ namespace Umbraco.Core.Security IUser user, IUserService userService, IEntityService entityService, + AppCaches appCaches, params char[] permissionsToCheck) { if (user == null) throw new ArgumentNullException("user"); @@ -33,7 +35,7 @@ namespace Umbraco.Core.Security if (content == null) return ContentAccess.NotFound; - var hasPathAccess = user.HasPathAccess(content, entityService); + var hasPathAccess = user.HasPathAccess(content, entityService, appCaches); if (hasPathAccess == false) return ContentAccess.Denied; @@ -52,6 +54,7 @@ namespace Umbraco.Core.Security IUser user, IUserService userService, IEntityService entityService, + AppCaches appCaches, params char[] permissionsToCheck) { if (user == null) throw new ArgumentNullException("user"); @@ -60,7 +63,7 @@ namespace Umbraco.Core.Security if (entity == null) return ContentAccess.NotFound; - var hasPathAccess = user.HasContentPathAccess(entity, entityService); + var hasPathAccess = user.HasContentPathAccess(entity, entityService, appCaches); if (hasPathAccess == false) return ContentAccess.Denied; @@ -89,6 +92,7 @@ namespace Umbraco.Core.Security IUser user, IUserService userService, IEntityService entityService, + AppCaches appCaches, out IUmbracoEntity entity, params char[] permissionsToCheck) { @@ -100,16 +104,16 @@ namespace Umbraco.Core.Security entity = null; if (nodeId == Constants.System.Root) - hasPathAccess = user.HasContentRootAccess(entityService); + hasPathAccess = user.HasContentRootAccess(entityService, appCaches); else if (nodeId == Constants.System.RecycleBinContent) - hasPathAccess = user.HasContentBinAccess(entityService); + hasPathAccess = user.HasContentBinAccess(entityService, appCaches); if (hasPathAccess.HasValue) return hasPathAccess.Value ? ContentAccess.Granted : ContentAccess.Denied; entity = entityService.Get(nodeId, UmbracoObjectTypes.Document); if (entity == null) return ContentAccess.NotFound; - hasPathAccess = user.HasContentPathAccess(entity, entityService); + hasPathAccess = user.HasContentPathAccess(entity, entityService, appCaches); if (hasPathAccess == false) return ContentAccess.Denied; @@ -140,6 +144,7 @@ namespace Umbraco.Core.Security IUserService userService, IContentService contentService, IEntityService entityService, + AppCaches appCaches, out IContent contentItem, params char[] permissionsToCheck) { @@ -152,16 +157,16 @@ namespace Umbraco.Core.Security contentItem = null; if (nodeId == Constants.System.Root) - hasPathAccess = user.HasContentRootAccess(entityService); + hasPathAccess = user.HasContentRootAccess(entityService, appCaches); else if (nodeId == Constants.System.RecycleBinContent) - hasPathAccess = user.HasContentBinAccess(entityService); + hasPathAccess = user.HasContentBinAccess(entityService, appCaches); if (hasPathAccess.HasValue) return hasPathAccess.Value ? ContentAccess.Granted : ContentAccess.Denied; contentItem = contentService.GetById(nodeId); if (contentItem == null) return ContentAccess.NotFound; - hasPathAccess = user.HasPathAccess(contentItem, entityService); + hasPathAccess = user.HasPathAccess(contentItem, entityService, appCaches); if (hasPathAccess == false) return ContentAccess.Denied; diff --git a/src/Umbraco.Core/Serialization/NoTypeConverterJsonConverter.cs b/src/Umbraco.Core/Serialization/NoTypeConverterJsonConverter.cs index b06ee870de..ab64d5b368 100644 --- a/src/Umbraco.Core/Serialization/NoTypeConverterJsonConverter.cs +++ b/src/Umbraco.Core/Serialization/NoTypeConverterJsonConverter.cs @@ -19,6 +19,7 @@ namespace Umbraco.Core.Serialization internal class NoTypeConverterJsonConverter : JsonConverter { static readonly IContractResolver resolver = new NoTypeConverterContractResolver(); + private static readonly JsonSerializerSettings JsonSerializerSettings = new JsonSerializerSettings { ContractResolver = resolver }; private class NoTypeConverterContractResolver : DefaultContractResolver { @@ -41,12 +42,12 @@ namespace Umbraco.Core.Serialization public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { - return JsonSerializer.CreateDefault(new JsonSerializerSettings { ContractResolver = resolver }).Deserialize(reader, objectType); + return JsonSerializer.CreateDefault(JsonSerializerSettings).Deserialize(reader, objectType); } public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { - JsonSerializer.CreateDefault(new JsonSerializerSettings { ContractResolver = resolver }).Serialize(writer, value); + JsonSerializer.CreateDefault(JsonSerializerSettings).Serialize(writer, value); } } } diff --git a/src/Umbraco.Core/Services/Implement/EntityXmlSerializer.cs b/src/Umbraco.Core/Services/Implement/EntityXmlSerializer.cs index 4d7694d1b8..8cfcaa1182 100644 --- a/src/Umbraco.Core/Services/Implement/EntityXmlSerializer.cs +++ b/src/Umbraco.Core/Services/Implement/EntityXmlSerializer.cs @@ -77,7 +77,7 @@ namespace Umbraco.Core.Services.Implement var children = _contentService.GetPagedChildren(content.Id, page++, pageSize, out total); SerializeChildren(children, xml, published); } - + } return xml; @@ -488,7 +488,7 @@ namespace Umbraco.Core.Services.Implement new XElement("Alias", propertyType.Alias), new XElement("Key", propertyType.Key), new XElement("Type", propertyType.PropertyEditorAlias), - new XElement("Definition", definition.Key), + new XElement("Definition", definition.Key), new XElement("Tab", propertyGroup == null ? "" : propertyGroup.Name), new XElement("SortOrder", propertyType.SortOrder), new XElement("Mandatory", propertyType.Mandatory.ToString()), @@ -553,6 +553,13 @@ namespace Umbraco.Core.Services.Implement new XAttribute("path", contentBase.Path), new XAttribute("isDoc", "")); + + // Add culture specific node names + foreach (var culture in contentBase.AvailableCultures) + { + xml.Add(new XAttribute("nodeName-" + culture, contentBase.GetCultureName(culture))); + } + foreach (var property in contentBase.Properties) xml.Add(SerializeProperty(property, published)); diff --git a/src/Umbraco.Core/Services/Implement/PublicAccessService.cs b/src/Umbraco.Core/Services/Implement/PublicAccessService.cs index 4e3cd96012..fa9f2c55f1 100644 --- a/src/Umbraco.Core/Services/Implement/PublicAccessService.cs +++ b/src/Umbraco.Core/Services/Implement/PublicAccessService.cs @@ -62,12 +62,10 @@ namespace Umbraco.Core.Services.Implement //start with the deepest id ids.Reverse(); - using (var scope = ScopeProvider.CreateScope()) + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { //This will retrieve from cache! - var entries = _publicAccessRepository.GetMany().ToArray(); - - scope.Complete(); + var entries = _publicAccessRepository.GetMany().ToList(); foreach (var id in ids) { var found = entries.FirstOrDefault(x => x.ProtectedNodeId == id); diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index eb51e20f5d..84df785bba 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -132,7 +132,6 @@ - diff --git a/src/Umbraco.Examine/ContentValueSetValidator.cs b/src/Umbraco.Examine/ContentValueSetValidator.cs index 9555566c53..f702e8197d 100644 --- a/src/Umbraco.Examine/ContentValueSetValidator.cs +++ b/src/Umbraco.Examine/ContentValueSetValidator.cs @@ -4,7 +4,9 @@ using System.Linq; using Examine; using Examine.LuceneEngine.Providers; using Umbraco.Core; +using Umbraco.Core.Composing; using Umbraco.Core.Models; +using Umbraco.Core.Scoping; using Umbraco.Core.Services; namespace Umbraco.Examine @@ -15,9 +17,9 @@ namespace Umbraco.Examine public class ContentValueSetValidator : ValueSetValidator, IContentValueSetValidator { private readonly IPublicAccessService _publicAccessService; - + private readonly IScopeProvider _scopeProvider; private const string PathKey = "path"; - private static readonly IEnumerable ValidCategories = new[] {IndexTypes.Content, IndexTypes.Media}; + private static readonly IEnumerable ValidCategories = new[] { IndexTypes.Content, IndexTypes.Media }; protected override IEnumerable ValidIndexCategories => ValidCategories; public bool PublishedValuesOnly { get; } @@ -53,25 +55,38 @@ namespace Umbraco.Examine public bool ValidateProtectedContent(string path, string category) { - if (category == IndexTypes.Content - && !SupportProtectedContent - //if the service is null we can't look this up so we'll return false - && (_publicAccessService == null || _publicAccessService.IsProtected(path))) + if (category == IndexTypes.Content && !SupportProtectedContent) { - return false; + //if the service is null we can't look this up so we'll return false + if (_publicAccessService == null || _scopeProvider == null) + { + return false; + } + + // explicit scope since we may be in a background thread + using (_scopeProvider.CreateScope(autoComplete: true)) + { + if (_publicAccessService.IsProtected(path)) + { + return false; + } + } } return true; } + // used for tests public ContentValueSetValidator(bool publishedValuesOnly, int? parentId = null, IEnumerable includeItemTypes = null, IEnumerable excludeItemTypes = null) - : this(publishedValuesOnly, true, null, parentId, includeItemTypes, excludeItemTypes) + : this(publishedValuesOnly, true, null, null, parentId, includeItemTypes, excludeItemTypes) { } public ContentValueSetValidator(bool publishedValuesOnly, bool supportProtectedContent, - IPublicAccessService publicAccessService, int? parentId = null, + IPublicAccessService publicAccessService, + IScopeProvider scopeProvider, + int? parentId = null, IEnumerable includeItemTypes = null, IEnumerable excludeItemTypes = null) : base(includeItemTypes, excludeItemTypes, null, null) { @@ -79,6 +94,16 @@ namespace Umbraco.Examine SupportProtectedContent = supportProtectedContent; ParentId = parentId; _publicAccessService = publicAccessService; + _scopeProvider = scopeProvider; + } + + [Obsolete("Use the ctor with all parameters instead")] + public ContentValueSetValidator(bool publishedValuesOnly, bool supportProtectedContent, + IPublicAccessService publicAccessService, int? parentId = null, + IEnumerable includeItemTypes = null, IEnumerable excludeItemTypes = null) + : this(publishedValuesOnly, supportProtectedContent, publicAccessService, Current.ScopeProvider, + parentId, includeItemTypes, excludeItemTypes) + { } public override ValueSetValidationResult Validate(ValueSet valueSet) @@ -103,7 +128,7 @@ namespace Umbraco.Examine && variesByCulture.Count > 0 && variesByCulture[0].Equals("y")) { //so this valueset is for a content that varies by culture, now check for non-published cultures and remove those values - foreach(var publishField in valueSet.Values.Where(x => x.Key.StartsWith($"{UmbracoExamineIndex.PublishedFieldName}_")).ToList()) + foreach (var publishField in valueSet.Values.Where(x => x.Key.StartsWith($"{UmbracoExamineIndex.PublishedFieldName}_")).ToList()) { if (publishField.Value.Count <= 0 || !publishField.Value[0].Equals("y")) { @@ -134,7 +159,7 @@ namespace Umbraco.Examine || !ValidateProtectedContent(path, valueSet.Category)) return ValueSetValidationResult.Filtered; - return isFiltered ? ValueSetValidationResult.Filtered: ValueSetValidationResult.Valid; + return isFiltered ? ValueSetValidationResult.Filtered : ValueSetValidationResult.Valid; } } } diff --git a/src/Umbraco.Examine/Umbraco.Examine.csproj b/src/Umbraco.Examine/Umbraco.Examine.csproj index 0e0ee62139..517edf354c 100644 --- a/src/Umbraco.Examine/Umbraco.Examine.csproj +++ b/src/Umbraco.Examine/Umbraco.Examine.csproj @@ -49,7 +49,7 @@ - + 1.0.0-beta2-19324-01 runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Content/content.ts b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Content/content.ts index 22f1f883d0..1a40e8451f 100644 --- a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Content/content.ts +++ b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Content/content.ts @@ -574,7 +574,7 @@ context('Content', () => { // Create content with content picker cy.get('.umb-tree-root-link').rightclick(); - cy.get('.-opens-dialog > .umb-action-link').click(); + cy.get('[data-element="action-create"]').click(); cy.get('[data-element="action-create-' + pickerDocTypeAlias + '"] > .umb-action-link').click(); // Fill out content cy.umbracoEditorHeaderName('ContentPickerContent'); diff --git a/src/Umbraco.Tests.Benchmarks/JsonSerializerSettingsBenchmarks.cs b/src/Umbraco.Tests.Benchmarks/JsonSerializerSettingsBenchmarks.cs new file mode 100644 index 0000000000..7f419547bd --- /dev/null +++ b/src/Umbraco.Tests.Benchmarks/JsonSerializerSettingsBenchmarks.cs @@ -0,0 +1,69 @@ +using BenchmarkDotNet.Attributes; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Umbraco.Tests.Benchmarks.Config; + +namespace Umbraco.Tests.Benchmarks +{ + [QuickRunConfig] + [MemoryDiagnoser] + public class JsonSerializerSettingsBenchmarks + { + [Benchmark] + public void SerializerSettingsInstantiation() + { + int instances = 1000; + for (int i = 0; i < instances; i++) + { + new JsonSerializerSettings(); + } + } + + [Benchmark(Baseline =true)] + public void SerializerSettingsSingleInstantiation() + { + new JsonSerializerSettings(); + } + +// // * Summary * + +// BenchmarkDotNet=v0.11.3, OS=Windows 10.0.18362 +//Intel Core i5-8265U CPU 1.60GHz(Kaby Lake R), 1 CPU, 8 logical and 4 physical cores +// [Host] : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.8.4250.0 +// Job-JIATTD : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.8.4250.0 + +//IterationCount=3 IterationTime=100.0000 ms LaunchCount = 1 +//WarmupCount=3 + +// Method | Mean | Error | StdDev | Ratio | RatioSD | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | +//-------------------------------------- |-------------:|-------------:|------------:|-------:|--------:|------------:|------------:|------------:|--------------------:| +// SerializerSettingsInstantiation | 29,120.48 ns | 5,532.424 ns | 303.2508 ns | 997.84 | 23.66 | 73.8122 | - | - | 232346 B | +// SerializerSettingsSingleInstantiation | 29.19 ns | 8.089 ns | 0.4434 ns | 1.00 | 0.00 | 0.0738 | - | - | 232 B | + +//// * Warnings * +//MinIterationTime +// JsonSerializerSettingsBenchmarks.SerializerSettingsSingleInstantiation: IterationCount= 3, IterationTime= 100.0000 ms, LaunchCount= 1, WarmupCount= 3->MinIterationTime = 96.2493 ms which is very small. It's recommended to increase it. + +//// * Legends * +// Mean : Arithmetic mean of all measurements +// Error : Half of 99.9% confidence interval +// StdDev : Standard deviation of all measurements +// Ratio : Mean of the ratio distribution ([Current]/[Baseline]) +// RatioSD : Standard deviation of the ratio distribution([Current]/[Baseline]) +// Gen 0/1k Op : GC Generation 0 collects per 1k Operations +// Gen 1/1k Op : GC Generation 1 collects per 1k Operations +// Gen 2/1k Op : GC Generation 2 collects per 1k Operations +// Allocated Memory/Op : Allocated memory per single operation(managed only, inclusive, 1KB = 1024B) +// 1 ns : 1 Nanosecond(0.000000001 sec) + +//// * Diagnostic Output - MemoryDiagnoser * + + +// // ***** BenchmarkRunner: End ***** +// Run time: 00:00:04 (4.88 sec), executed benchmarks: 2 + } +} diff --git a/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj b/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj index df2af67573..386cf12a92 100644 --- a/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj +++ b/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj @@ -53,6 +53,7 @@ + diff --git a/src/Umbraco.Tests/Configurations/UmbracoSettings/ContentElementTests.cs b/src/Umbraco.Tests/Configurations/UmbracoSettings/ContentElementTests.cs index 33df3caaad..498c12101c 100644 --- a/src/Umbraco.Tests/Configurations/UmbracoSettings/ContentElementTests.cs +++ b/src/Umbraco.Tests/Configurations/UmbracoSettings/ContentElementTests.cs @@ -83,7 +83,7 @@ namespace Umbraco.Tests.Configurations.UmbracoSettings [Test] public void DisallowedUploadFiles() { - Assert.IsTrue(SettingsSection.Content.DisallowedUploadFiles.All(x => "ashx,aspx,ascx,config,cshtml,vbhtml,asmx,air,axd".Split(',').Contains(x))); + Assert.IsTrue(SettingsSection.Content.DisallowedUploadFiles.All(x => "ashx,aspx,ascx,config,cshtml,vbhtml,asmx,air,axd,xamlx".Split(',').Contains(x))); } [Test] diff --git a/src/Umbraco.Tests/Configurations/UmbracoSettings/umbracoSettings.config b/src/Umbraco.Tests/Configurations/UmbracoSettings/umbracoSettings.config index 8cbb799d88..d802cfc7ad 100644 --- a/src/Umbraco.Tests/Configurations/UmbracoSettings/umbracoSettings.config +++ b/src/Umbraco.Tests/Configurations/UmbracoSettings/umbracoSettings.config @@ -18,9 +18,9 @@ umbracoBytes2 umbracoExtension2 - + - + inline - + - ashx,aspx,ascx,config,cshtml,vbhtml,asmx,air,axd + ashx,aspx,ascx,config,cshtml,vbhtml,asmx,air,axd,xamlx - jpg,png,gif + jpg,png,gif @@ -108,7 +108,7 @@ 1440 - - ashx,aspx,ascx,config,cshtml,vbhtml,asmx,air,axd,swf,xml,xhtml,html,htm,php,htaccess + ashx,aspx,ascx,config,cshtml,vbhtml,asmx,air,axd,swf,xml,xhtml,html,htm,php,htaccess,xamlx assets/img/login.jpg diff --git a/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs b/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs index f360d37d03..92a9dd6e98 100644 --- a/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs +++ b/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs @@ -135,7 +135,10 @@ namespace Umbraco.Web.Cache public static void RemoveMemberCache(this DistributedCache dc, params IMember[] members) { if (members.Length == 0) return; - dc.RefreshByPayload(MemberCacheRefresher.UniqueId, members.Select(x => new MemberCacheRefresher.JsonPayload(x.Id, x.Username))); + dc.RefreshByPayload(MemberCacheRefresher.UniqueId, members.Select(x => new MemberCacheRefresher.JsonPayload(x.Id, x.Username) + { + Removed = true + })); } #endregion diff --git a/src/Umbraco.Web/Cache/MemberCacheRefresher.cs b/src/Umbraco.Web/Cache/MemberCacheRefresher.cs index 736a858af3..48ae40ce3b 100644 --- a/src/Umbraco.Web/Cache/MemberCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/MemberCacheRefresher.cs @@ -33,6 +33,10 @@ namespace Umbraco.Web.Cache public int Id { get; } public string Username { get; } + // TODO: In netcore change this to be get only and adjust the ctor. We cannot do that now since that + // is a breaking change due to only having a single jsonconstructor allowed. + public bool Removed { get; set; } + } #region Define diff --git a/src/Umbraco.Web/Cache/UserCacheRefresher.cs b/src/Umbraco.Web/Cache/UserCacheRefresher.cs index 922a9df385..ce2cbbf754 100644 --- a/src/Umbraco.Web/Cache/UserCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/UserCacheRefresher.cs @@ -42,7 +42,14 @@ namespace Umbraco.Web.Cache { var userCache = AppCaches.IsolatedCaches.Get(); if (userCache) + { userCache.Result.Clear(RepositoryCacheKeys.GetKey(id)); + userCache.Result.ClearByKey(CacheKeys.UserContentStartNodePathsPrefix + id); + userCache.Result.ClearByKey(CacheKeys.UserMediaStartNodePathsPrefix + id); + userCache.Result.ClearByKey(CacheKeys.UserAllContentStartNodesPrefix + id); + userCache.Result.ClearByKey(CacheKeys.UserAllMediaStartNodesPrefix + id); + } + base.Remove(id); } diff --git a/src/Umbraco.Web/Editors/BackOfficeController.cs b/src/Umbraco.Web/Editors/BackOfficeController.cs index 458c76b3ae..92b67cbf1b 100644 --- a/src/Umbraco.Web/Editors/BackOfficeController.cs +++ b/src/Umbraco.Web/Editors/BackOfficeController.cs @@ -226,7 +226,7 @@ namespace Umbraco.Web.Editors .ToDictionary(pv => pv.Key, pv => pv.ToDictionary(pve => pve.valueAlias, pve => pve.value)); - return new JsonNetResult { Data = nestedDictionary, Formatting = Formatting.None }; + return new JsonNetResult(JsonNetResult.DefaultJsonSerializerSettings) { Data = nestedDictionary, Formatting = Formatting.None }; } /// @@ -273,7 +273,7 @@ namespace Umbraco.Web.Editors GetAssetList, new TimeSpan(0, 2, 0)); - return new JsonNetResult { Data = result, Formatting = Formatting.None }; + return new JsonNetResult(JsonNetResult.DefaultJsonSerializerSettings) { Data = result, Formatting = Formatting.None }; } [UmbracoAuthorize(Order = 0)] @@ -281,7 +281,7 @@ namespace Umbraco.Web.Editors public JsonNetResult GetGridConfig() { var gridConfig = Current.Configs.Grids(); - return new JsonNetResult { Data = gridConfig.EditorsConfig.Editors, Formatting = Formatting.None }; + return new JsonNetResult(JsonNetResult.DefaultJsonSerializerSettings) { Data = gridConfig.EditorsConfig.Editors, Formatting = Formatting.None }; } diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index 6f85a08751..7093edf23d 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -1168,7 +1168,7 @@ namespace Umbraco.Web.Editors //if this item's path has already been denied or if the user doesn't have access to it, add to the deny list if (denied.Any(x => c.Path.StartsWith($"{x.Path},")) || (ContentPermissionsHelper.CheckPermissions(c, - Security.CurrentUser, Services.UserService, Services.EntityService, + Security.CurrentUser, Services.UserService, Services.EntityService, AppCaches, ActionPublish.ActionLetter) == ContentPermissionsHelper.ContentAccess.Denied)) { denied.Add(c); diff --git a/src/Umbraco.Web/Editors/EntityController.cs b/src/Umbraco.Web/Editors/EntityController.cs index fe78b29123..9399deb1e5 100644 --- a/src/Umbraco.Web/Editors/EntityController.cs +++ b/src/Umbraco.Web/Editors/EntityController.cs @@ -670,9 +670,9 @@ namespace Umbraco.Web.Editors switch (type) { case UmbracoEntityTypes.Document: - return Security.CurrentUser.CalculateContentStartNodeIds(Services.EntityService); + return Security.CurrentUser.CalculateContentStartNodeIds(Services.EntityService, AppCaches); case UmbracoEntityTypes.Media: - return Security.CurrentUser.CalculateMediaStartNodeIds(Services.EntityService); + return Security.CurrentUser.CalculateMediaStartNodeIds(Services.EntityService, AppCaches); default: return Array.Empty(); } @@ -811,10 +811,10 @@ namespace Umbraco.Web.Editors switch (entityType) { case UmbracoEntityTypes.Document: - aids = Security.CurrentUser.CalculateContentStartNodeIds(Services.EntityService); + aids = Security.CurrentUser.CalculateContentStartNodeIds(Services.EntityService, AppCaches); break; case UmbracoEntityTypes.Media: - aids = Security.CurrentUser.CalculateMediaStartNodeIds(Services.EntityService); + aids = Security.CurrentUser.CalculateMediaStartNodeIds(Services.EntityService, AppCaches); break; } diff --git a/src/Umbraco.Web/Editors/Filters/ContentSaveValidationAttribute.cs b/src/Umbraco.Web/Editors/Filters/ContentSaveValidationAttribute.cs index fbce2d0414..c0e190989a 100644 --- a/src/Umbraco.Web/Editors/Filters/ContentSaveValidationAttribute.cs +++ b/src/Umbraco.Web/Editors/Filters/ContentSaveValidationAttribute.cs @@ -7,6 +7,7 @@ using System.Web.Http; using System.Web.Http.Controllers; using System.Web.Http.Filters; using Umbraco.Core; +using Umbraco.Core.Cache; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Security; @@ -29,11 +30,12 @@ namespace Umbraco.Web.Editors.Filters private readonly IContentService _contentService; private readonly IUserService _userService; private readonly IEntityService _entityService; + private readonly AppCaches _appCaches; - public ContentSaveValidationAttribute(): this(Current.Logger, Current.UmbracoContextAccessor, Current.Services.TextService, Current.Services.ContentService, Current.Services.UserService, Current.Services.EntityService) + public ContentSaveValidationAttribute(): this(Current.Logger, Current.UmbracoContextAccessor, Current.Services.TextService, Current.Services.ContentService, Current.Services.UserService, Current.Services.EntityService, Current.AppCaches) { } - public ContentSaveValidationAttribute(ILogger logger, IUmbracoContextAccessor umbracoContextAccessor, ILocalizedTextService textService, IContentService contentService, IUserService userService, IEntityService entityService) + public ContentSaveValidationAttribute(ILogger logger, IUmbracoContextAccessor umbracoContextAccessor, ILocalizedTextService textService, IContentService contentService, IUserService userService, IEntityService entityService, AppCaches appCaches) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _umbracoContextAccessor = umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor)); @@ -41,6 +43,7 @@ namespace Umbraco.Web.Editors.Filters _contentService = contentService ?? throw new ArgumentNullException(nameof(contentService)); _userService = userService ?? throw new ArgumentNullException(nameof(userService)); _entityService = entityService ?? throw new ArgumentNullException(nameof(entityService)); + _appCaches = appCaches; } public override void OnActionExecuting(HttpActionContext actionContext) @@ -195,13 +198,13 @@ namespace Umbraco.Web.Editors.Filters accessResult = ContentPermissionsHelper.CheckPermissions( contentToCheck, webSecurity.CurrentUser, - _userService, _entityService, permissionToCheck.ToArray()); + _userService, _entityService, _appCaches, permissionToCheck.ToArray()); } else { accessResult = ContentPermissionsHelper.CheckPermissions( contentIdToCheck, webSecurity.CurrentUser, - _userService, _contentService, _entityService, + _userService, _contentService, _entityService, _appCaches, out contentToCheck, permissionToCheck.ToArray()); if (contentToCheck != null) diff --git a/src/Umbraco.Web/Editors/Filters/MediaItemSaveValidationAttribute.cs b/src/Umbraco.Web/Editors/Filters/MediaItemSaveValidationAttribute.cs index 449ef95675..af973d0662 100644 --- a/src/Umbraco.Web/Editors/Filters/MediaItemSaveValidationAttribute.cs +++ b/src/Umbraco.Web/Editors/Filters/MediaItemSaveValidationAttribute.cs @@ -4,6 +4,7 @@ using System.Net.Http; using System.Web.Http.Controllers; using System.Web.Http.Filters; using Umbraco.Core; +using Umbraco.Core.Cache; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Services; @@ -23,18 +24,20 @@ namespace Umbraco.Web.Editors.Filters private readonly ILocalizedTextService _textService; private readonly IMediaService _mediaService; private readonly IEntityService _entityService; + private readonly AppCaches _appCaches; - public MediaItemSaveValidationAttribute() : this(Current.Logger, Current.UmbracoContextAccessor, Current.Services.TextService, Current.Services.MediaService, Current.Services.EntityService) + public MediaItemSaveValidationAttribute() : this(Current.Logger, Current.UmbracoContextAccessor, Current.Services.TextService, Current.Services.MediaService, Current.Services.EntityService, Current.AppCaches) { } - public MediaItemSaveValidationAttribute(ILogger logger, IUmbracoContextAccessor umbracoContextAccessor, ILocalizedTextService textService, IMediaService mediaService, IEntityService entityService) + public MediaItemSaveValidationAttribute(ILogger logger, IUmbracoContextAccessor umbracoContextAccessor, ILocalizedTextService textService, IMediaService mediaService, IEntityService entityService, AppCaches appCaches) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _umbracoContextAccessor = umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor)); _textService = textService ?? throw new ArgumentNullException(nameof(textService)); _mediaService = mediaService ?? throw new ArgumentNullException(nameof(mediaService)); _entityService = entityService ?? throw new ArgumentNullException(nameof(entityService)); + _appCaches = appCaches; } public override void OnActionExecuting(HttpActionContext actionContext) @@ -91,7 +94,7 @@ namespace Umbraco.Web.Editors.Filters if (MediaController.CheckPermissions( actionContext.Request.Properties, _umbracoContextAccessor.UmbracoContext.Security.CurrentUser, - _mediaService, _entityService, + _mediaService, _entityService, _appCaches, contentIdToCheck, contentToCheck) == false) { actionContext.Response = actionContext.Request.CreateUserNoAccessResponse(); diff --git a/src/Umbraco.Web/Editors/Filters/UserGroupAuthorizationAttribute.cs b/src/Umbraco.Web/Editors/Filters/UserGroupAuthorizationAttribute.cs index e1d6626055..e94098db82 100644 --- a/src/Umbraco.Web/Editors/Filters/UserGroupAuthorizationAttribute.cs +++ b/src/Umbraco.Web/Editors/Filters/UserGroupAuthorizationAttribute.cs @@ -54,7 +54,8 @@ namespace Umbraco.Web.Editors.Filters Current.Services.UserService, Current.Services.ContentService, Current.Services.MediaService, - Current.Services.EntityService); + Current.Services.EntityService, + Current.AppCaches); return authHelper.AuthorizeGroupAccess(currentUser, intIds); } } diff --git a/src/Umbraco.Web/Editors/Filters/UserGroupEditorAuthorizationHelper.cs b/src/Umbraco.Web/Editors/Filters/UserGroupEditorAuthorizationHelper.cs index 985c42bbbf..ad3dae5e06 100644 --- a/src/Umbraco.Web/Editors/Filters/UserGroupEditorAuthorizationHelper.cs +++ b/src/Umbraco.Web/Editors/Filters/UserGroupEditorAuthorizationHelper.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using Umbraco.Core; +using Umbraco.Core.Cache; using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; using Umbraco.Core.Services; @@ -13,13 +14,15 @@ namespace Umbraco.Web.Editors.Filters private readonly IContentService _contentService; private readonly IMediaService _mediaService; private readonly IEntityService _entityService; + private readonly AppCaches _appCaches; - public UserGroupEditorAuthorizationHelper(IUserService userService, IContentService contentService, IMediaService mediaService, IEntityService entityService) + public UserGroupEditorAuthorizationHelper(IUserService userService, IContentService contentService, IMediaService mediaService, IEntityService entityService, AppCaches appCaches) { _userService = userService; _contentService = contentService; _mediaService = mediaService; _entityService = entityService; + _appCaches = appCaches; } /// @@ -73,18 +76,15 @@ namespace Umbraco.Web.Editors.Filters /// /// Authorize that the user is not adding a section to the group that they don't have access to /// - /// - /// - /// - /// - public Attempt AuthorizeSectionChanges(IUser currentUser, - IEnumerable currentAllowedSections, + public Attempt AuthorizeSectionChanges( + IUser currentUser, + IEnumerable existingSections, IEnumerable proposedAllowedSections) { if (currentUser.IsAdmin()) return Attempt.Succeed(); - var sectionsAdded = currentAllowedSections.Except(proposedAllowedSections).ToArray(); + var sectionsAdded = proposedAllowedSections.Except(existingSections).ToArray(); var sectionAccessMissing = sectionsAdded.Except(currentUser.AllowedSections).ToArray(); return sectionAccessMissing.Length > 0 ? Attempt.Fail("Current user doesn't have access to add these sections " + string.Join(", ", sectionAccessMissing)) @@ -111,7 +111,7 @@ namespace Umbraco.Web.Editors.Filters var content = _contentService.GetById(proposedContentStartId.Value); if (content != null) { - if (currentUser.HasPathAccess(content, _entityService) == false) + if (currentUser.HasPathAccess(content, _entityService, _appCaches) == false) return Attempt.Fail("Current user doesn't have access to the content path " + content.Path); } } @@ -121,7 +121,7 @@ namespace Umbraco.Web.Editors.Filters var media = _mediaService.GetById(proposedMediaStartId.Value); if (media != null) { - if (currentUser.HasPathAccess(media, _entityService) == false) + if (currentUser.HasPathAccess(media, _entityService, _appCaches) == false) return Attempt.Fail("Current user doesn't have access to the media path " + media.Path); } } diff --git a/src/Umbraco.Web/Editors/Filters/UserGroupValidateAttribute.cs b/src/Umbraco.Web/Editors/Filters/UserGroupValidateAttribute.cs index 78cd8e6a4d..a2647d2ee9 100644 --- a/src/Umbraco.Web/Editors/Filters/UserGroupValidateAttribute.cs +++ b/src/Umbraco.Web/Editors/Filters/UserGroupValidateAttribute.cs @@ -58,13 +58,9 @@ namespace Umbraco.Web.Editors.Filters return; } - //map the model to the persisted instance - Mapper.Map(userGroupSave, persisted); break; case ContentSaveAction.SaveNew: - //create the persisted model from mapping the saved model - persisted = Mapper.Map(userGroupSave); - ((UserGroup)persisted).ResetIdentity(); + persisted = new UserGroup(); break; default: actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.NotFound, new ArgumentOutOfRangeException()); diff --git a/src/Umbraco.Web/Editors/MediaController.cs b/src/Umbraco.Web/Editors/MediaController.cs index 5fd59d0c9f..8d13ccd4d7 100644 --- a/src/Umbraco.Web/Editors/MediaController.cs +++ b/src/Umbraco.Web/Editors/MediaController.cs @@ -239,7 +239,7 @@ namespace Umbraco.Web.Editors protected int[] UserStartNodes { - get { return _userStartNodes ?? (_userStartNodes = Security.CurrentUser.CalculateMediaStartNodeIds(Services.EntityService)); } + get { return _userStartNodes ?? (_userStartNodes = Security.CurrentUser.CalculateMediaStartNodeIds(Services.EntityService, AppCaches)); } } /// @@ -835,6 +835,7 @@ namespace Umbraco.Web.Editors Security.CurrentUser, Services.MediaService, Services.EntityService, + AppCaches, intParentId) == false) { throw new HttpResponseException(Request.CreateResponse( @@ -919,7 +920,7 @@ namespace Umbraco.Web.Editors /// The content to lookup, if the contentItem is not specified /// Specifies the already resolved content item to check against, setting this ignores the nodeId /// - internal static bool CheckPermissions(IDictionary storage, IUser user, IMediaService mediaService, IEntityService entityService, int nodeId, IMedia media = null) + internal static bool CheckPermissions(IDictionary storage, IUser user, IMediaService mediaService, IEntityService entityService, AppCaches appCaches, int nodeId, IMedia media = null) { if (storage == null) throw new ArgumentNullException("storage"); if (user == null) throw new ArgumentNullException("user"); @@ -940,10 +941,10 @@ namespace Umbraco.Web.Editors } var hasPathAccess = (nodeId == Constants.System.Root) - ? user.HasMediaRootAccess(entityService) + ? user.HasMediaRootAccess(entityService, appCaches) : (nodeId == Constants.System.RecycleBinMedia) - ? user.HasMediaBinAccess(entityService) - : user.HasPathAccess(media, entityService); + ? user.HasMediaBinAccess(entityService, appCaches) + : user.HasPathAccess(media, entityService, appCaches); return hasPathAccess; } diff --git a/src/Umbraco.Web/Editors/PasswordChanger.cs b/src/Umbraco.Web/Editors/PasswordChanger.cs index 2698a68b40..404ce27e39 100644 --- a/src/Umbraco.Web/Editors/PasswordChanger.cs +++ b/src/Umbraco.Web/Editors/PasswordChanger.cs @@ -1,5 +1,6 @@ using System; using System.ComponentModel.DataAnnotations; +using System.Linq; using System.Threading.Tasks; using System.Web; using System.Web.Http.ModelBinding; @@ -84,6 +85,11 @@ namespace Umbraco.Web.Editors return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("The current user is not authorized", new[] { "resetPassword" }) }); } + if (!currentUser.IsAdmin() && savingUser.IsAdmin()) + { + return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("The current user cannot change the password for the specified user", new[] { "resetPassword" }) }); + } + //ok, we should be able to reset it var resetToken = await userMgr.GeneratePasswordResetTokenAsync(savingUser.Id); var newPass = passwordModel.NewPassword.IsNullOrWhiteSpace() @@ -246,7 +252,7 @@ namespace Umbraco.Web.Editors return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Cannot set an empty password", new[] { "value" }) }); } - //without being able to retrieve the original password, + //without being able to retrieve the original password, //we cannot arbitrarily change the password without knowing the old one and no old password was supplied - need to return an error if (passwordModel.OldPassword.IsNullOrWhiteSpace() && membershipProvider.EnablePasswordRetrieval == false) { diff --git a/src/Umbraco.Web/Editors/UserEditorAuthorizationHelper.cs b/src/Umbraco.Web/Editors/UserEditorAuthorizationHelper.cs index 320580aaf9..bd031c0527 100644 --- a/src/Umbraco.Web/Editors/UserEditorAuthorizationHelper.cs +++ b/src/Umbraco.Web/Editors/UserEditorAuthorizationHelper.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using Umbraco.Core; +using Umbraco.Core.Cache; using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; using Umbraco.Core.Security; @@ -14,13 +15,15 @@ namespace Umbraco.Web.Editors private readonly IMediaService _mediaService; private readonly IUserService _userService; private readonly IEntityService _entityService; + private readonly AppCaches _appCaches; - public UserEditorAuthorizationHelper(IContentService contentService, IMediaService mediaService, IUserService userService, IEntityService entityService) + public UserEditorAuthorizationHelper(IContentService contentService, IMediaService mediaService, IUserService userService, IEntityService entityService, AppCaches appCaches) { _contentService = contentService; _mediaService = mediaService; _userService = userService; _entityService = entityService; + _appCaches = appCaches; } /// @@ -75,6 +78,18 @@ namespace Umbraco.Web.Editors if (userGroupAliases != null) { var savingGroupAliases = userGroupAliases.ToArray(); + var existingGroupAliases = savingUser == null + ? new string[0] + : savingUser.Groups.Select(x => x.Alias).ToArray(); + + var addedGroupAliases = savingGroupAliases.Except(existingGroupAliases); + + // As we know the current user is not admin, it is only allowed to use groups that the user do have themselves. + var savingGroupAliasesNotAllowed = addedGroupAliases.Except(currentUser.Groups.Select(x=>x.Alias)).ToArray(); + if (savingGroupAliasesNotAllowed.Any()) + { + return Attempt.Fail("Cannot assign the group(s) '" + string.Join(", ", savingGroupAliasesNotAllowed) + "', the current user is not part of them or admin"); + } //only validate any groups that have changed. //a non-admin user can remove groups and add groups that they have access to @@ -90,9 +105,7 @@ namespace Umbraco.Web.Editors if (userGroupsChanged) { // d) A user cannot assign a group to another user that they do not belong to - var currentUserGroups = currentUser.Groups.Select(x => x.Alias).ToArray(); - foreach (var group in newGroups) { if (currentUserGroups.Contains(group) == false) @@ -114,7 +127,7 @@ namespace Umbraco.Web.Editors { if (contentId == Constants.System.Root) { - var hasAccess = ContentPermissionsHelper.HasPathAccess("-1", currentUser.CalculateContentStartNodeIds(_entityService), Constants.System.RecycleBinContent); + var hasAccess = ContentPermissionsHelper.HasPathAccess("-1", currentUser.CalculateContentStartNodeIds(_entityService, _appCaches), Constants.System.RecycleBinContent); if (hasAccess == false) return Attempt.Fail("The current user does not have access to the content root"); } @@ -122,7 +135,7 @@ namespace Umbraco.Web.Editors { var content = _contentService.GetById(contentId); if (content == null) continue; - var hasAccess = currentUser.HasPathAccess(content, _entityService); + var hasAccess = currentUser.HasPathAccess(content, _entityService, _appCaches); if (hasAccess == false) return Attempt.Fail("The current user does not have access to the content path " + content.Path); } @@ -135,7 +148,7 @@ namespace Umbraco.Web.Editors { if (mediaId == Constants.System.Root) { - var hasAccess = ContentPermissionsHelper.HasPathAccess("-1", currentUser.CalculateMediaStartNodeIds(_entityService), Constants.System.RecycleBinMedia); + var hasAccess = ContentPermissionsHelper.HasPathAccess("-1", currentUser.CalculateMediaStartNodeIds(_entityService, _appCaches), Constants.System.RecycleBinMedia); if (hasAccess == false) return Attempt.Fail("The current user does not have access to the media root"); } @@ -143,7 +156,7 @@ namespace Umbraco.Web.Editors { var media = _mediaService.GetById(mediaId); if (media == null) continue; - var hasAccess = currentUser.HasPathAccess(media, _entityService); + var hasAccess = currentUser.HasPathAccess(media, _entityService, _appCaches); if (hasAccess == false) return Attempt.Fail("The current user does not have access to the media path " + media.Path); } diff --git a/src/Umbraco.Web/Editors/UserGroupsController.cs b/src/Umbraco.Web/Editors/UserGroupsController.cs index b081ca6137..c8620b35fe 100644 --- a/src/Umbraco.Web/Editors/UserGroupsController.cs +++ b/src/Umbraco.Web/Editors/UserGroupsController.cs @@ -28,14 +28,19 @@ namespace Umbraco.Web.Editors //authorize that the user has access to save this user group var authHelper = new UserGroupEditorAuthorizationHelper( - Services.UserService, Services.ContentService, Services.MediaService, Services.EntityService); + Services.UserService, + Services.ContentService, + Services.MediaService, + Services.EntityService, + AppCaches); var isAuthorized = authHelper.AuthorizeGroupAccess(Security.CurrentUser, userGroupSave.Alias); if (isAuthorized == false) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.Unauthorized, isAuthorized.Result)); //if sections were added we need to check that the current user has access to that section - isAuthorized = authHelper.AuthorizeSectionChanges(Security.CurrentUser, + isAuthorized = authHelper.AuthorizeSectionChanges( + Security.CurrentUser, userGroupSave.PersistedUserGroup.AllowedSections, userGroupSave.Sections); if (isAuthorized == false) @@ -53,6 +58,9 @@ namespace Umbraco.Web.Editors //need to ensure current user is in a group if not an admin to avoid a 401 EnsureNonAdminUserIsInSavedUserGroup(userGroupSave); + //map the model to the persisted instance + Mapper.Map(userGroupSave, userGroupSave.PersistedUserGroup); + //save the group Services.UserService.Save(userGroupSave.PersistedUserGroup, userGroupSave.Users.ToArray()); diff --git a/src/Umbraco.Web/Editors/UsersController.cs b/src/Umbraco.Web/Editors/UsersController.cs index 3f29203819..e8bc33cc26 100644 --- a/src/Umbraco.Web/Editors/UsersController.cs +++ b/src/Umbraco.Web/Editors/UsersController.cs @@ -304,7 +304,7 @@ namespace Umbraco.Web.Editors CheckUniqueEmail(userSave.Email, null); //Perform authorization here to see if the current user can actually save this user with the info being requested - var authHelper = new UserEditorAuthorizationHelper(Services.ContentService, Services.MediaService, Services.UserService, Services.EntityService); + var authHelper = new UserEditorAuthorizationHelper(Services.ContentService, Services.MediaService, Services.UserService, Services.EntityService, AppCaches); var canSaveUser = authHelper.IsAuthorized(Security.CurrentUser, null, null, null, userSave.UserGroups); if (canSaveUser == false) { @@ -398,7 +398,7 @@ namespace Umbraco.Web.Editors } //Perform authorization here to see if the current user can actually save this user with the info being requested - var authHelper = new UserEditorAuthorizationHelper(Services.ContentService, Services.MediaService, Services.UserService, Services.EntityService); + var authHelper = new UserEditorAuthorizationHelper(Services.ContentService, Services.MediaService, Services.UserService, Services.EntityService, AppCaches); var canSaveUser = authHelper.IsAuthorized(Security.CurrentUser, user, null, null, userSave.UserGroups); if (canSaveUser == false) { @@ -573,7 +573,7 @@ namespace Umbraco.Web.Editors throw new HttpResponseException(HttpStatusCode.NotFound); //Perform authorization here to see if the current user can actually save this user with the info being requested - var authHelper = new UserEditorAuthorizationHelper(Services.ContentService, Services.MediaService, Services.UserService, Services.EntityService); + var authHelper = new UserEditorAuthorizationHelper(Services.ContentService, Services.MediaService, Services.UserService, Services.EntityService, AppCaches); var canSaveUser = authHelper.IsAuthorized(Security.CurrentUser, found, userSave.StartContentIds, userSave.StartMediaIds, userSave.UserGroups); if (canSaveUser == false) { diff --git a/src/Umbraco.Web/ImageCropperTemplateExtensions.cs b/src/Umbraco.Web/ImageCropperTemplateExtensions.cs index 26dd2a5d36..78b55a8930 100644 --- a/src/Umbraco.Web/ImageCropperTemplateExtensions.cs +++ b/src/Umbraco.Web/ImageCropperTemplateExtensions.cs @@ -253,19 +253,20 @@ namespace Umbraco.Web ImageCropRatioMode? ratioMode = null, bool upScale = true) => ImageCropperTemplateCoreExtensions.GetCropUrl(imageUrl, Current.ImageUrlGenerator, cropDataSet, width, height, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBusterValue, furtherOptions, ratioMode, upScale); + private static readonly JsonSerializerSettings ImageCropperValueJsonSerializerSettings = new JsonSerializerSettings + { + Culture = CultureInfo.InvariantCulture, + FloatParseHandling = FloatParseHandling.Decimal + }; internal static ImageCropperValue DeserializeImageCropperValue(this string json) { - var imageCrops = new ImageCropperValue(); + ImageCropperValue imageCrops = null; if (json.DetectIsJson()) { try { - imageCrops = JsonConvert.DeserializeObject(json, new JsonSerializerSettings - { - Culture = CultureInfo.InvariantCulture, - FloatParseHandling = FloatParseHandling.Decimal - }); + imageCrops = JsonConvert.DeserializeObject(json, ImageCropperValueJsonSerializerSettings); } catch (Exception ex) { @@ -273,6 +274,7 @@ namespace Umbraco.Web } } + imageCrops = imageCrops ?? new ImageCropperValue(); return imageCrops; } } diff --git a/src/Umbraco.Web/Models/Mapping/ContentMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/ContentMapDefinition.cs index 16465914cb..0360ee568b 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentMapDefinition.cs @@ -2,6 +2,8 @@ using System.Collections.Generic; using System.Linq; using Umbraco.Core; +using Umbraco.Core.Cache; +using Umbraco.Core.Composing; using Umbraco.Core.Logging; using Umbraco.Core.Mapping; using Umbraco.Core.Models; @@ -29,14 +31,25 @@ namespace Umbraco.Web.Models.Mapping private readonly ILogger _logger; private readonly IUserService _userService; private readonly IEntityService _entityService; + private readonly AppCaches _appCaches; private readonly TabsAndPropertiesMapper _tabsAndPropertiesMapper; private readonly ContentSavedStateMapper _stateMapper; private readonly ContentBasicSavedStateMapper _basicStateMapper; private readonly ContentVariantMapper _contentVariantMapper; - public ContentMapDefinition(CommonMapper commonMapper, ILocalizedTextService localizedTextService, IContentService contentService, IContentTypeService contentTypeService, - IFileService fileService, IUmbracoContextAccessor umbracoContextAccessor, IPublishedRouter publishedRouter, ILocalizationService localizationService, ILogger logger, - IUserService userService, IEntityService entityService) + public ContentMapDefinition( + CommonMapper commonMapper, + ILocalizedTextService localizedTextService, + IContentService contentService, + IContentTypeService contentTypeService, + IFileService fileService, + IUmbracoContextAccessor umbracoContextAccessor, + IPublishedRouter publishedRouter, + ILocalizationService localizationService, + ILogger logger, + IUserService userService, + IEntityService entityService, + AppCaches appCaches) { _commonMapper = commonMapper; _localizedTextService = localizedTextService; @@ -49,6 +62,7 @@ namespace Umbraco.Web.Models.Mapping _logger = logger; _userService = userService; _entityService = entityService; + _appCaches = appCaches; _tabsAndPropertiesMapper = new TabsAndPropertiesMapper(localizedTextService); _stateMapper = new ContentSavedStateMapper(); _basicStateMapper = new ContentBasicSavedStateMapper(); @@ -238,7 +252,7 @@ namespace Umbraco.Web.Models.Mapping // false here. if (context.HasItems && context.Items.TryGetValue("CurrentUser", out var usr) && usr is IUser currentUser) { - userStartNodes = currentUser.CalculateContentStartNodeIds(_entityService); + userStartNodes = currentUser.CalculateContentStartNodeIds(_entityService, _appCaches); if (!userStartNodes.Contains(Constants.System.Root)) { // return false if this is the user's actual start node, the node will be rendered in the tree diff --git a/src/Umbraco.Web/Models/Mapping/UserMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/UserMapDefinition.cs index aa158799cb..6c58309f57 100644 --- a/src/Umbraco.Web/Models/Mapping/UserMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/UserMapDefinition.cs @@ -273,8 +273,8 @@ namespace Umbraco.Web.Models.Mapping { target.AvailableCultures = _textService.GetSupportedCultures().ToDictionary(x => x.Name, x => x.DisplayName); target.Avatars = source.GetUserAvatarUrls(_appCaches.RuntimeCache); - target.CalculatedStartContentIds = GetStartNodes(source.CalculateContentStartNodeIds(_entityService), UmbracoObjectTypes.Document, "content/contentRoot", context); - target.CalculatedStartMediaIds = GetStartNodes(source.CalculateMediaStartNodeIds(_entityService), UmbracoObjectTypes.Media, "media/mediaRoot", context); + target.CalculatedStartContentIds = GetStartNodes(source.CalculateContentStartNodeIds(_entityService, _appCaches), UmbracoObjectTypes.Document, "content/contentRoot", context); + target.CalculatedStartMediaIds = GetStartNodes(source.CalculateMediaStartNodeIds(_entityService, _appCaches), UmbracoObjectTypes.Media, "media/mediaRoot", context); target.CreateDate = source.CreateDate; target.Culture = source.GetUserCulture(_textService, _globalSettings).ToString(); target.Email = source.Email; @@ -327,8 +327,8 @@ namespace Umbraco.Web.Models.Mapping target.Email = source.Email; target.EmailHash = source.Email.ToLowerInvariant().Trim().GenerateHash(); target.Name = source.Name; - target.StartContentIds = source.CalculateContentStartNodeIds(_entityService); - target.StartMediaIds = source.CalculateMediaStartNodeIds(_entityService); + target.StartContentIds = source.CalculateContentStartNodeIds(_entityService, _appCaches); + target.StartMediaIds = source.CalculateMediaStartNodeIds(_entityService, _appCaches); target.UserId = source.Id; //we need to map the legacy UserType diff --git a/src/Umbraco.Web/Mvc/JsonNetResult.cs b/src/Umbraco.Web/Mvc/JsonNetResult.cs index da6780451e..3dd6c2f398 100644 --- a/src/Umbraco.Web/Mvc/JsonNetResult.cs +++ b/src/Umbraco.Web/Mvc/JsonNetResult.cs @@ -22,10 +22,19 @@ namespace Umbraco.Web.Mvc public JsonSerializerSettings SerializerSettings { get; set; } public Formatting Formatting { get; set; } + /// + /// Default, unchanged JsonSerializerSettings + /// + public static readonly JsonSerializerSettings DefaultJsonSerializerSettings = new JsonSerializerSettings(); + public JsonNetResult() { SerializerSettings = new JsonSerializerSettings(); } + public JsonNetResult(JsonSerializerSettings jsonSerializerSettings) + { + SerializerSettings = jsonSerializerSettings; + } public override void ExecuteResult(ControllerContext context) { diff --git a/src/Umbraco.Web/PropertyEditors/MultiUrlPickerValueEditor.cs b/src/Umbraco.Web/PropertyEditors/MultiUrlPickerValueEditor.cs index 5a84e4b20c..560275b29a 100644 --- a/src/Umbraco.Web/PropertyEditors/MultiUrlPickerValueEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MultiUrlPickerValueEditor.cs @@ -121,6 +121,10 @@ namespace Umbraco.Web.PropertyEditors return base.ToEditor(property, dataTypeService, culture, segment); } + private static readonly JsonSerializerSettings LinkDisplayJsonSerializerSettings = new JsonSerializerSettings + { + NullValueHandling = NullValueHandling.Ignore + }; public override object FromEditor(ContentPropertyData editorValue, object currentValue) { @@ -142,11 +146,8 @@ namespace Umbraco.Web.PropertyEditors Target = link.Target, Udi = link.Udi, Url = link.Udi == null ? link.Url : null, // only save the URL for external links - }, - new JsonSerializerSettings - { - NullValueHandling = NullValueHandling.Ignore - }); + }, LinkDisplayJsonSerializerSettings + ); } catch (Exception ex) { diff --git a/src/Umbraco.Web/Routing/PublishedRouter.cs b/src/Umbraco.Web/Routing/PublishedRouter.cs index fee9a62158..676b973271 100644 --- a/src/Umbraco.Web/Routing/PublishedRouter.cs +++ b/src/Umbraco.Web/Routing/PublishedRouter.cs @@ -417,8 +417,8 @@ namespace Umbraco.Web.Routing // some finders may implement caching using (_profilingLogger.DebugDuration( - $"{tracePrefix}Begin finders", - $"{tracePrefix}End finders, {(request.HasPublishedContent ? "a document was found" : "no document was found")}")) + $"{tracePrefix}Executing finders...", + $"{tracePrefix}Completed executing finders")) { //iterate but return on first one that finds it var found = _contentFinders.Any(finder => @@ -426,6 +426,16 @@ namespace Umbraco.Web.Routing _logger.Debug("Finder {ContentFinderType}", finder.GetType().FullName); return finder.TryFindContent(request); }); + + _profilingLogger.Debug( + "Found? {Found} Content: {PublishedContentId}, Template: {TemplateAlias}, Domain: {Domain}, Culture: {Culture}, Is404: {Is404}, StatusCode: {StatusCode}", + found, + request.HasPublishedContent ? request.PublishedContent.Id : "NULL", + request.HasTemplate ? request.TemplateAlias : "NULL", + request.HasDomain ? request.Domain.ToString() : "NULL", + request.Culture?.Name ?? "NULL", + request.Is404, + request.ResponseStatusCode); } // indicate that the published content (if any) we have at the moment is the diff --git a/src/Umbraco.Web/Routing/UrlProviderExtensions.cs b/src/Umbraco.Web/Routing/UrlProviderExtensions.cs index 3bd9d985a6..f135316cea 100644 --- a/src/Umbraco.Web/Routing/UrlProviderExtensions.cs +++ b/src/Umbraco.Web/Routing/UrlProviderExtensions.cs @@ -78,7 +78,7 @@ namespace Umbraco.Web.Routing if (urls.Add(otherUrl)) //avoid duplicates yield return otherUrl; } - + /// /// Tries to return a for each culture for the content while detecting collisions/errors /// @@ -131,10 +131,14 @@ namespace Umbraco.Web.Routing // got a URL, deal with collisions, add URL default: - if (DetectCollision(content, url, culture, umbracoContext, publishedRouter, textService, out var urlInfo)) // detect collisions, etc + if (DetectCollision(logger, content, url, culture, umbracoContext, publishedRouter, textService, out var urlInfo)) // detect collisions, etc + { yield return urlInfo; + } else + { yield return UrlInfo.Url(url, culture); + } break; } } @@ -152,16 +156,23 @@ namespace Umbraco.Web.Routing while (parent != null && parent.Published && (!parent.ContentType.VariesByCulture() || parent.IsCulturePublished(culture))); if (parent == null) // oops, internal error + { return UrlInfo.Message(textService.Localize("content/parentNotPublishedAnomaly"), culture); + } else if (!parent.Published) // totally not published - return UrlInfo.Message(textService.Localize("content/parentNotPublished", new[] {parent.Name}), culture); + { + return UrlInfo.Message(textService.Localize("content/parentNotPublished", new[] { parent.Name }), culture); + } - else // culture not published - return UrlInfo.Message(textService.Localize("content/parentCultureNotPublished", new[] {parent.Name}), culture); + else + { + // culture not published + return UrlInfo.Message(textService.Localize("content/parentCultureNotPublished", new[] { parent.Name }), culture); + } } - private static bool DetectCollision(IContent content, string url, string culture, UmbracoContext umbracoContext, IPublishedRouter publishedRouter, ILocalizedTextService textService, out UrlInfo urlInfo) + private static bool DetectCollision(ILogger logger, IContent content, string url, string culture, UmbracoContext umbracoContext, IPublishedRouter publishedRouter, ILocalizedTextService textService, out UrlInfo urlInfo) { // test for collisions on the 'main' URL var uri = new Uri(url.TrimEnd(Constants.CharArrays.ForwardSlash), UriKind.RelativeOrAbsolute); @@ -174,6 +185,16 @@ namespace Umbraco.Web.Routing if (pcr.HasPublishedContent == false) { + var logMsg = nameof(DetectCollision) + " did not resolve a content item for original url: {Url}, translated to {TranslatedUrl} and culture: {Culture}"; + if (pcr.IgnorePublishedContentCollisions) + { + logger.Debug(typeof(UrlProviderExtensions), logMsg, url, uri, culture); + } + else + { + logger.Warn(typeof(UrlProviderExtensions), logMsg, url, uri, culture); + } + urlInfo = UrlInfo.Message(textService.Localize("content/routeErrorCannotRoute"), culture); return true; } @@ -193,7 +214,7 @@ namespace Umbraco.Web.Routing l.Reverse(); var s = "/" + string.Join("/", l) + " (id=" + pcr.PublishedContent.Id + ")"; - urlInfo = UrlInfo.Message(textService.Localize("content/routeError", new[] { s }), culture); + urlInfo = UrlInfo.Message(textService.Localize("content/routeError", new[] { s }), culture); return true; } diff --git a/src/Umbraco.Web/Runtime/WebInitialComposer.cs b/src/Umbraco.Web/Runtime/WebInitialComposer.cs index ac7e34d5cb..b15641b503 100644 --- a/src/Umbraco.Web/Runtime/WebInitialComposer.cs +++ b/src/Umbraco.Web/Runtime/WebInitialComposer.cs @@ -40,6 +40,7 @@ using Current = Umbraco.Web.Composing.Current; using Umbraco.Web.PropertyEditors; using Umbraco.Core.Models; using Umbraco.Web.Models; +using Ganss.XSS; namespace Umbraco.Web.Runtime { @@ -139,6 +140,14 @@ namespace Umbraco.Web.Runtime composition.RegisterUnique(); composition.RegisterUnique(); composition.RegisterUnique(); + composition.Register(_ => + { + var sanitizer = new HtmlSanitizer(); + sanitizer.AllowedAttributes.UnionWith(Umbraco.Core.Constants.SvgSanitizer.Attributes); + sanitizer.AllowedCssProperties.UnionWith(Umbraco.Core.Constants.SvgSanitizer.Attributes); + sanitizer.AllowedTags.UnionWith(Umbraco.Core.Constants.SvgSanitizer.Tags); + return sanitizer; + },Lifetime.Singleton); composition.RegisterUnique(factory => ExamineManager.Instance); diff --git a/src/Umbraco.Web/Search/ExamineComponent.cs b/src/Umbraco.Web/Search/ExamineComponent.cs index c9d7b7cf56..d1b3a868d7 100644 --- a/src/Umbraco.Web/Search/ExamineComponent.cs +++ b/src/Umbraco.Web/Search/ExamineComponent.cs @@ -271,10 +271,20 @@ namespace Umbraco.Web.Search break; case MessageType.RefreshByPayload: var payload = (MemberCacheRefresher.JsonPayload[])args.MessageObject; - var members = payload.Select(x => _services.MemberService.GetById(x.Id)); - foreach(var m in members) + foreach(var p in payload) { - ReIndexForMember(m); + if (p.Removed) + { + DeleteIndexForEntity(p.Id, false); + } + else + { + var m = _services.MemberService.GetById(p.Id); + if (m != null) + { + ReIndexForMember(m); + } + } } break; case MessageType.RefreshAll: @@ -629,22 +639,27 @@ namespace Umbraco.Web.Search // perform the ValueSet lookup on a background thread examineComponent._indexItemTaskRunner.Add(new SimpleTask(() => { - // for content we have a different builder for published vs unpublished - // we don't want to build more value sets than is needed so we'll lazily build 2 one for published one for non-published - var builders = new Dictionary>> + // Background thread, wrap the whole thing in an explicit scope since we know + // DB services are used within this logic. + using (examineComponent._scopeProvider.CreateScope(autoComplete: true)) { - [true] = new Lazy>(() => examineComponent._publishedContentValueSetBuilder.GetValueSets(content).ToList()), - [false] = new Lazy>(() => examineComponent._contentValueSetBuilder.GetValueSets(content).ToList()) - }; + // for content we have a different builder for published vs unpublished + // we don't want to build more value sets than is needed so we'll lazily build 2 one for published one for non-published + var builders = new Dictionary>> + { + [true] = new Lazy>(() => examineComponent._publishedContentValueSetBuilder.GetValueSets(content).ToList()), + [false] = new Lazy>(() => examineComponent._contentValueSetBuilder.GetValueSets(content).ToList()) + }; - foreach (var index in examineComponent._examineManager.Indexes.OfType() - //filter the indexers - .Where(x => isPublished || !x.PublishedValuesOnly) - .Where(x => x.EnableDefaultEventHandler)) - { - var valueSet = builders[index.PublishedValuesOnly].Value; - index.IndexItems(valueSet); - } + foreach (var index in examineComponent._examineManager.Indexes.OfType() + //filter the indexers + .Where(x => isPublished || !x.PublishedValuesOnly) + .Where(x => x.EnableDefaultEventHandler)) + { + var valueSet = builders[index.PublishedValuesOnly].Value; + index.IndexItems(valueSet); + } + } })); } } @@ -675,14 +690,19 @@ namespace Umbraco.Web.Search // perform the ValueSet lookup on a background thread examineComponent._indexItemTaskRunner.Add(new SimpleTask(() => { - var valueSet = examineComponent._mediaValueSetBuilder.GetValueSets(media).ToList(); - - foreach (var index in examineComponent._examineManager.Indexes.OfType() - //filter the indexers - .Where(x => isPublished || !x.PublishedValuesOnly) - .Where(x => x.EnableDefaultEventHandler)) + // Background thread, wrap the whole thing in an explicit scope since we know + // DB services are used within this logic. + using (examineComponent._scopeProvider.CreateScope(autoComplete: true)) { - index.IndexItems(valueSet); + var valueSet = examineComponent._mediaValueSetBuilder.GetValueSets(media).ToList(); + + foreach (var index in examineComponent._examineManager.Indexes.OfType() + //filter the indexers + .Where(x => isPublished || !x.PublishedValuesOnly) + .Where(x => x.EnableDefaultEventHandler)) + { + index.IndexItems(valueSet); + } } })); } @@ -712,12 +732,17 @@ namespace Umbraco.Web.Search // perform the ValueSet lookup on a background thread examineComponent._indexItemTaskRunner.Add(new SimpleTask(() => { - var valueSet = examineComponent._memberValueSetBuilder.GetValueSets(member).ToList(); - foreach (var index in examineComponent._examineManager.Indexes.OfType() - //filter the indexers - .Where(x => x.EnableDefaultEventHandler)) + // Background thread, wrap the whole thing in an explicit scope since we know + // DB services are used within this logic. + using (examineComponent._scopeProvider.CreateScope(autoComplete: true)) { - index.IndexItems(valueSet); + var valueSet = examineComponent._memberValueSetBuilder.GetValueSets(member).ToList(); + foreach (var index in examineComponent._examineManager.Indexes.OfType() + //filter the indexers + .Where(x => x.EnableDefaultEventHandler)) + { + index.IndexItems(valueSet); + } } })); } diff --git a/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs b/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs index 410b654e32..134d482b40 100644 --- a/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs +++ b/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs @@ -5,6 +5,8 @@ using System.Text; using System.Text.RegularExpressions; using Examine; using Umbraco.Core; +using Umbraco.Core.Cache; +using Umbraco.Core.Composing; using Umbraco.Core.Mapping; using Umbraco.Core.Models; using Umbraco.Core.Models.Entities; @@ -30,14 +32,29 @@ namespace Umbraco.Web.Search private readonly UmbracoMapper _mapper; private readonly ISqlContext _sqlContext; private readonly IUmbracoTreeSearcherFields _umbracoTreeSearcherFields; + private readonly AppCaches _appCaches; - - public UmbracoTreeSearcher(IExamineManager examineManager, + [Obsolete("Use constructor specifying all dependencies instead")] + public UmbracoTreeSearcher( + IExamineManager examineManager, UmbracoContext umbracoContext, ILocalizationService languageService, IEntityService entityService, UmbracoMapper mapper, - ISqlContext sqlContext,IUmbracoTreeSearcherFields umbracoTreeSearcherFields) + ISqlContext sqlContext, + IUmbracoTreeSearcherFields umbracoTreeSearcherFields) + : this(examineManager, umbracoContext, languageService, entityService, mapper, sqlContext, umbracoTreeSearcherFields, Current.AppCaches) + { } + + public UmbracoTreeSearcher( + IExamineManager examineManager, + UmbracoContext umbracoContext, + ILocalizationService languageService, + IEntityService entityService, + UmbracoMapper mapper, + ISqlContext sqlContext, + IUmbracoTreeSearcherFields umbracoTreeSearcherFields, + AppCaches appCaches) { _examineManager = examineManager ?? throw new ArgumentNullException(nameof(examineManager)); _umbracoContext = umbracoContext; @@ -46,6 +63,7 @@ namespace Umbraco.Web.Search _mapper = mapper; _sqlContext = sqlContext; _umbracoTreeSearcherFields = umbracoTreeSearcherFields; + _appCaches = appCaches; } /// @@ -112,13 +130,13 @@ namespace Umbraco.Web.Search case UmbracoEntityTypes.Media: type = "media"; fields.AddRange(_umbracoTreeSearcherFields.GetBackOfficeMediaFields()); - var allMediaStartNodes = _umbracoContext.Security.CurrentUser.CalculateMediaStartNodeIds(_entityService); + var allMediaStartNodes = _umbracoContext.Security.CurrentUser.CalculateMediaStartNodeIds(_entityService, _appCaches); AppendPath(sb, UmbracoObjectTypes.Media, allMediaStartNodes, searchFrom, ignoreUserStartNodes, _entityService); break; case UmbracoEntityTypes.Document: type = "content"; fields.AddRange(_umbracoTreeSearcherFields.GetBackOfficeDocumentFields()); - var allContentStartNodes = _umbracoContext.Security.CurrentUser.CalculateContentStartNodeIds(_entityService); + var allContentStartNodes = _umbracoContext.Security.CurrentUser.CalculateContentStartNodeIds(_entityService, _appCaches); AppendPath(sb, UmbracoObjectTypes.Document, allContentStartNodes, searchFrom, ignoreUserStartNodes, _entityService); break; default: @@ -462,11 +480,13 @@ namespace Umbraco.Web.Search var defaultLang = _languageService.GetDefaultLanguageIsoCode(); foreach (var result in results) { - var entity = _mapper.Map(result, context => { - if(culture != null) { - context.SetCulture(culture); - } + var entity = _mapper.Map(result, context => + { + if (culture != null) + { + context.SetCulture(culture); } + } ); var intId = entity.Id.TryConvertTo(); diff --git a/src/Umbraco.Web/Security/AppBuilderExtensions.cs b/src/Umbraco.Web/Security/AppBuilderExtensions.cs index 8f33f10eea..23afc36f93 100644 --- a/src/Umbraco.Web/Security/AppBuilderExtensions.cs +++ b/src/Umbraco.Web/Security/AppBuilderExtensions.cs @@ -169,11 +169,31 @@ namespace Umbraco.Web.Security // Enables the application to validate the security stamp when the user // logs in. This is a security feature which is used when you // change a password or add an external login to your account. - OnValidateIdentity = SecurityStampValidator - .OnValidateIdentity( + OnValidateIdentity = context => + { + // capture the current ticket for the request + var identity = context.Identity; + + return SecurityStampValidator + .OnValidateIdentity( + // This will re-verify the security stamp at a throttled 30 mins + // (the standard/default set in aspnet identity). + // This ensures that if the security stamp has changed - i.e. passwords, + // external logins, or other security profile data changed behind the + // scenes while being logged in, that they are logged out and have + // to re-verify their identity. TimeSpan.FromMinutes(30), - (manager, user) => manager.GenerateUserIdentityAsync(user), - identity => identity.GetUserId()), + async (manager, user) => + { + var regenerated = await manager.GenerateUserIdentityAsync(user); + + // Keep any custom claims from the original identity + regenerated.MergeClaimsFromBackOfficeIdentity(identity); + + return regenerated; + }, + identity => identity.GetUserId())(context); + } }; diff --git a/src/Umbraco.Web/Security/BackOfficeClaimsIdentityFactory.cs b/src/Umbraco.Web/Security/BackOfficeClaimsIdentityFactory.cs index d61d2ea711..2883ae8faf 100644 --- a/src/Umbraco.Web/Security/BackOfficeClaimsIdentityFactory.cs +++ b/src/Umbraco.Web/Security/BackOfficeClaimsIdentityFactory.cs @@ -27,12 +27,6 @@ namespace Umbraco.Web.Security { var baseIdentity = await base.CreateAsync(manager, user, authenticationType); - // now we can flow any custom claims that the actual user has currently assigned which could be done in the OnExternalLogin callback - foreach (var claim in user.Claims) - { - baseIdentity.AddClaim(new Claim(claim.ClaimType, claim.ClaimValue)); - } - var umbracoIdentity = new UmbracoBackOfficeIdentity(baseIdentity, user.Id, user.UserName, @@ -40,12 +34,16 @@ namespace Umbraco.Web.Security user.CalculatedContentStartNodeIds, user.CalculatedMediaStartNodeIds, user.Culture, - //NOTE - there is no session id assigned here, this is just creating the identity, a session id will be generated when the cookie is written + // NOTE - there is no session id assigned here, this is just creating the identity, a session id will be generated when the cookie is written Guid.NewGuid().ToString(), user.SecurityStamp, user.AllowedSections, user.Roles.Select(x => x.RoleId).ToArray()); + // now we can flow any custom claims that the actual user has currently + // assigned which could be done in the OnExternalLogin callback + umbracoIdentity.MergeClaimsFromBackOfficeIdentity(user); + return umbracoIdentity; } } diff --git a/src/Umbraco.Web/Security/ClaimsIdentityExtensions.cs b/src/Umbraco.Web/Security/ClaimsIdentityExtensions.cs new file mode 100644 index 0000000000..aa8549462d --- /dev/null +++ b/src/Umbraco.Web/Security/ClaimsIdentityExtensions.cs @@ -0,0 +1,34 @@ +using System.Linq; +using System.Security.Claims; +using Umbraco.Core.Models.Identity; +using Constants = Umbraco.Core.Constants; + +namespace Umbraco.Web.Security +{ + internal static class ClaimsIdentityExtensions + { + // Ignore these Claims when merging, these claims are dynamically added whenever the ticket + // is re-issued and we don't want to merge old values of these. + private static readonly string[] IgnoredClaims = new[] { ClaimTypes.CookiePath, Constants.Security.SessionIdClaimType }; + + internal static void MergeClaimsFromBackOfficeIdentity(this ClaimsIdentity destination, ClaimsIdentity source) + { + foreach (var claim in source.Claims + .Where(claim => !IgnoredClaims.Contains(claim.Type)) + .Where(claim => !destination.HasClaim(claim.Type, claim.Value))) + { + destination.AddClaim(new Claim(claim.Type, claim.Value)); + } + } + + internal static void MergeClaimsFromBackOfficeIdentity(this ClaimsIdentity destination, BackOfficeIdentityUser source) + { + foreach (var claim in source.Claims + .Where(claim => !IgnoredClaims.Contains(claim.ClaimType)) + .Where(claim => !destination.HasClaim(claim.ClaimType, claim.ClaimValue))) + { + destination.AddClaim(new Claim(claim.ClaimType, claim.ClaimValue)); + } + } + } +} diff --git a/src/Umbraco.Web/Security/Providers/UmbracoMembershipProvider.cs b/src/Umbraco.Web/Security/Providers/UmbracoMembershipProvider.cs index bf9ee654c4..ad53949c28 100644 --- a/src/Umbraco.Web/Security/Providers/UmbracoMembershipProvider.cs +++ b/src/Umbraco.Web/Security/Providers/UmbracoMembershipProvider.cs @@ -3,7 +3,6 @@ using System.Collections.Specialized; using System.Configuration.Provider; using System.Linq; using System.Text; -using System.Web; using System.Web.Configuration; using System.Web.Security; using Umbraco.Core; @@ -14,7 +13,6 @@ using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Security; using Umbraco.Core.Services; using Umbraco.Web.Composing; -using Umbraco.Core.Models.Identity; namespace Umbraco.Web.Security.Providers { @@ -357,7 +355,7 @@ namespace Umbraco.Web.Security.Providers member.LastLoginDate = now; member.UpdateDate = now; } - + } return ConvertToMembershipUser(member); @@ -604,11 +602,21 @@ namespace Umbraco.Web.Security.Providers { // when upgrading from 7.2 to 7.3 trying to save will throw if (UmbracoVersion.Current >= new Version(7, 3, 0, 0)) - MemberService.Save(member, false); + { + // We need to raise event to ensure caches are updated. (e.g. the cache that uses username as key). + // Even that this is a heavy operation, because indexes are updates, we consider that okay, as it + // is still cheap to do a successful login. + MemberService.Save(member, true); + } + } else { - // set the last login date without full save (fast, no locks) + // set the last login date without full save (fast, no locks). + // We do not update caches. This is to the best of our knowledge okay, as this info are only stored + // because it is required by the membership provider. + // If we one day have to revisit this, we will most likely need to spilt the events in membership info + // saved and umbraco info saved. We don't want to update indexes etc when it is just membership info that is saved MemberService.SetLastLogin(member.Username, member.LastLoginDate); } diff --git a/src/Umbraco.Web/Services/IconService.cs b/src/Umbraco.Web/Services/IconService.cs index 206af296f9..175650fd12 100644 --- a/src/Umbraco.Web/Services/IconService.cs +++ b/src/Umbraco.Web/Services/IconService.cs @@ -13,31 +13,24 @@ namespace Umbraco.Web.Services public class IconService : IIconService { private readonly IGlobalSettings _globalSettings; + private readonly IHtmlSanitizer _htmlSanitizer; - public IconService(IGlobalSettings globalSettings) + public IconService(IGlobalSettings globalSettings, IHtmlSanitizer htmlSanitizer) { _globalSettings = globalSettings; + _htmlSanitizer = htmlSanitizer; } /// public IList GetAllIcons() { - var icons = new List(); var directory = new DirectoryInfo(IOHelper.MapPath($"{_globalSettings.IconsPath}/")); var iconNames = directory.GetFiles("*.svg"); - iconNames.OrderBy(f => f.Name).ToList().ForEach(iconInfo => - { - var icon = GetIcon(iconInfo); + return iconNames.OrderBy(f => f.Name) + .Select(iconInfo => GetIcon(iconInfo)).WhereNotNull().ToList(); - if (icon != null) - { - icons.Add(icon); - } - }); - - return icons; } /// @@ -68,15 +61,10 @@ namespace Umbraco.Web.Services /// private IconModel CreateIconModel(string iconName, string iconPath) { - var sanitizer = new HtmlSanitizer(); - sanitizer.AllowedAttributes.UnionWith(Constants.SvgSanitizer.Attributes); - sanitizer.AllowedCssProperties.UnionWith(Constants.SvgSanitizer.Attributes); - sanitizer.AllowedTags.UnionWith(Constants.SvgSanitizer.Tags); - try { var svgContent = System.IO.File.ReadAllText(iconPath); - var sanitizedString = sanitizer.Sanitize(svgContent); + var sanitizedString = _htmlSanitizer.Sanitize(svgContent); var svg = new IconModel { diff --git a/src/Umbraco.Web/Trees/ContentTreeController.cs b/src/Umbraco.Web/Trees/ContentTreeController.cs index ead265e7c5..d82166b9a3 100644 --- a/src/Umbraco.Web/Trees/ContentTreeController.cs +++ b/src/Umbraco.Web/Trees/ContentTreeController.cs @@ -47,7 +47,7 @@ namespace Umbraco.Web.Trees private int[] _userStartNodes; protected override int[] UserStartNodes - => _userStartNodes ?? (_userStartNodes = Security.CurrentUser.CalculateContentStartNodeIds(Services.EntityService)); + => _userStartNodes ?? (_userStartNodes = Security.CurrentUser.CalculateContentStartNodeIds(Services.EntityService, AppCaches)); public ContentTreeController(UmbracoTreeSearcher treeSearcher, ActionCollection actions, IGlobalSettings globalSettings, IUmbracoContextAccessor umbracoContextAccessor, ISqlContext sqlContext, ServiceContext services, AppCaches appCaches, IProfilingLogger logger, IRuntimeState runtimeState, UmbracoHelper umbracoHelper) : base(globalSettings, umbracoContextAccessor, sqlContext, services, appCaches, logger, runtimeState, umbracoHelper) { @@ -165,7 +165,7 @@ namespace Umbraco.Web.Trees } //if the user has no path access for this node, all they can do is refresh - if (!Security.CurrentUser.HasContentPathAccess(item, Services.EntityService)) + if (!Security.CurrentUser.HasContentPathAccess(item, Services.EntityService, AppCaches)) { var menu = new MenuItemCollection(); menu.Items.Add(new RefreshNode(Services.TextService, true)); diff --git a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs index e9c34608a2..57c7027598 100644 --- a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs +++ b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs @@ -126,12 +126,12 @@ namespace Umbraco.Web.Trees switch (RecycleBinId) { case Constants.System.RecycleBinMedia: - startNodeIds = Security.CurrentUser.CalculateMediaStartNodeIds(Services.EntityService); - startNodePaths = Security.CurrentUser.GetMediaStartNodePaths(Services.EntityService); + startNodeIds = Security.CurrentUser.CalculateMediaStartNodeIds(Services.EntityService, AppCaches); + startNodePaths = Security.CurrentUser.GetMediaStartNodePaths(Services.EntityService, AppCaches); break; case Constants.System.RecycleBinContent: - startNodeIds = Security.CurrentUser.CalculateContentStartNodeIds(Services.EntityService); - startNodePaths = Security.CurrentUser.GetContentStartNodePaths(Services.EntityService); + startNodeIds = Security.CurrentUser.CalculateContentStartNodeIds(Services.EntityService, AppCaches); + startNodePaths = Security.CurrentUser.GetContentStartNodePaths(Services.EntityService, AppCaches); break; default: throw new NotSupportedException("Path access is only determined on content or media"); @@ -291,8 +291,8 @@ namespace Umbraco.Web.Trees { if (entity == null) return false; return RecycleBinId == Constants.System.RecycleBinContent - ? Security.CurrentUser.HasContentPathAccess(entity, Services.EntityService) - : Security.CurrentUser.HasMediaPathAccess(entity, Services.EntityService); + ? Security.CurrentUser.HasContentPathAccess(entity, Services.EntityService, AppCaches) + : Security.CurrentUser.HasMediaPathAccess(entity, Services.EntityService, AppCaches); } /// diff --git a/src/Umbraco.Web/Trees/MediaTreeController.cs b/src/Umbraco.Web/Trees/MediaTreeController.cs index cc71b92d15..43b5a83282 100644 --- a/src/Umbraco.Web/Trees/MediaTreeController.cs +++ b/src/Umbraco.Web/Trees/MediaTreeController.cs @@ -50,7 +50,7 @@ namespace Umbraco.Web.Trees private int[] _userStartNodes; protected override int[] UserStartNodes - => _userStartNodes ?? (_userStartNodes = Security.CurrentUser.CalculateMediaStartNodeIds(Services.EntityService)); + => _userStartNodes ?? (_userStartNodes = Security.CurrentUser.CalculateMediaStartNodeIds(Services.EntityService, AppCaches)); /// /// Creates a tree node for a content item based on an UmbracoEntity @@ -117,7 +117,7 @@ namespace Umbraco.Web.Trees } //if the user has no path access for this node, all they can do is refresh - if (!Security.CurrentUser.HasMediaPathAccess(item, Services.EntityService)) + if (!Security.CurrentUser.HasMediaPathAccess(item, Services.EntityService, AppCaches)) { menu.Items.Add(new RefreshNode(Services.TextService, true)); return menu; diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index e16f836352..5d7d3f7c2c 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -63,10 +63,10 @@ - + - 4.0.217 + 5.0.376 2.7.0.100 @@ -306,6 +306,7 @@ + @@ -1315,6 +1316,7 @@ +