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 @@
+