Port v7@2aa0dfb2c5 - WIP

This commit is contained in:
Stephan
2018-03-21 09:06:32 +01:00
parent a9147f1473
commit 41948607d0
48 changed files with 1537 additions and 514 deletions
@@ -10,7 +10,8 @@ namespace Umbraco.Core.Collections
/// <summary>
/// Initializes a new instance of the <see cref="CompositeTypeTypeKey"/> struct.
/// </summary>
public CompositeTypeTypeKey(Type type1, Type type2) : this()
public CompositeTypeTypeKey(Type type1, Type type2)
: this()
{
Type1 = type1;
Type2 = type2;
@@ -19,12 +20,12 @@ namespace Umbraco.Core.Collections
/// <summary>
/// Gets the first type.
/// </summary>
public Type Type1 { get; private set; }
public Type Type1 { get; }
/// <summary>
/// Gets the second type.
/// </summary>
public Type Type2 { get; private set; }
public Type Type2 { get; }
/// <inheritdoc/>
public bool Equals(CompositeTypeTypeKey other)
@@ -35,7 +36,7 @@ namespace Umbraco.Core.Collections
/// <inheritdoc/>
public override bool Equals(object obj)
{
var other = obj is CompositeTypeTypeKey ? (CompositeTypeTypeKey)obj : default(CompositeTypeTypeKey);
var other = obj is CompositeTypeTypeKey key ? key : default;
return Type1 == other.Type1 && Type2 == other.Type2;
}
+1 -1
View File
@@ -5,7 +5,7 @@ namespace Umbraco.Core.Events
/// <summary>
/// Event messages collection
/// </summary>
public sealed class EventMessages : DisposableObject
public sealed class EventMessages : DisposableObjectSlim
{
private readonly List<EventMessage> _msgs = new List<EventMessage>();
@@ -0,0 +1,18 @@
using System;
using Umbraco.Core.Models;
using Umbraco.Core.Models.Membership;
namespace Umbraco.Core.Events
{
internal class ExportedMemberEventArgs : EventArgs
{
public IMember Member { get; }
public MemberExportModel Exported { get; }
public ExportedMemberEventArgs(IMember member, MemberExportModel exported)
{
Member = member;
Exported = exported;
}
}
}
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using Umbraco.Core.Models.Packaging;
namespace Umbraco.Core.Events
@@ -8,17 +9,26 @@ namespace Umbraco.Core.Events
{
private readonly MetaData _packageMetaData;
[EditorBrowsable(EditorBrowsableState.Never)]
[Obsolete("Use the overload specifying packageMetaData instead")]
public ImportPackageEventArgs(TEntity eventObject, bool canCancel)
: base(new[] { eventObject }, canCancel)
{
}
public ImportPackageEventArgs(TEntity eventObject, MetaData packageMetaData)
: base(new[] { eventObject })
public ImportPackageEventArgs(TEntity eventObject, MetaData packageMetaData, bool canCancel)
: base(new[] { eventObject }, canCancel)
{
if (packageMetaData == null) throw new ArgumentNullException("packageMetaData");
_packageMetaData = packageMetaData;
}
public ImportPackageEventArgs(TEntity eventObject, MetaData packageMetaData)
: this(eventObject, packageMetaData, true)
{
}
public MetaData PackageMetaData
{
get { return _packageMetaData; }
@@ -78,255 +78,256 @@ namespace Umbraco.Core.Events
if (_events == null)
return Enumerable.Empty<IEventDefinition>();
IReadOnlyList<IEventDefinition> events;
switch (filter)
{
case EventDefinitionFilter.All:
return FilterSupersededAndUpdateToLatestEntity(_events);
events = _events;
break;
case EventDefinitionFilter.FirstIn:
var l1 = new OrderedHashSet<IEventDefinition>();
foreach (var e in _events)
l1.Add(e);
return FilterSupersededAndUpdateToLatestEntity(l1);
events = l1;
break;
case EventDefinitionFilter.LastIn:
var l2 = new OrderedHashSet<IEventDefinition>(keepOldest: false);
foreach (var e in _events)
l2.Add(e);
return FilterSupersededAndUpdateToLatestEntity(l2);
events = l2;
break;
default:
throw new ArgumentOutOfRangeException(nameof(filter), filter, null);
throw new ArgumentOutOfRangeException("filter", filter, null);
}
return FilterSupersededAndUpdateToLatestEntity(events);
}
private class EventDefinitionTypeData
private class EventDefinitionInfos
{
public IEventDefinition EventDefinition { get; set; }
public Type EventArgType { get; set; }
public SupersedeEventAttribute[] SupersedeAttributes { get; set; }
public Type[] SupersedeTypes { get; set; }
}
/// <summary>
/// This will iterate over the events (latest first) and filter out any events or entities in event args that are included
/// in more recent events that Supersede previous ones. For example, If an Entity has been Saved and then Deleted, we don't want
/// to raise the Saved event (well actually we just don't want to include it in the args for that saved event)
/// </summary>
/// <param name="events"></param>
/// <returns></returns>
private static IEnumerable<IEventDefinition> FilterSupersededAndUpdateToLatestEntity(IReadOnlyList<IEventDefinition> events)
// fixme
// this is way too convoluted, the superceede attribute is used only on DeleteEventargs to specify
// that it superceeds save, publish, move and copy - BUT - publish event args is also used for
// unpublishing and should NOT be superceeded - so really it should not be managed at event args
// level but at event level
//
// what we want is:
// if an entity is deleted, then all Saved, Moved, Copied, Published events prior to this should
// not trigger for the entity - and even though, does it make any sense? making a copy of an entity
// should ... trigger?
//
// not going to refactor it all - we probably want to *always* trigger event but tell people that
// due to scopes, they should not expected eg a saved entity to still be around - however, now,
// going to write a ugly condition to deal with U4-10764
// iterates over the events (latest first) and filter out any events or entities in event args that are included
// in more recent events that Supersede previous ones. For example, If an Entity has been Saved and then Deleted, we don't want
// to raise the Saved event (well actually we just don't want to include it in the args for that saved event)
internal static IEnumerable<IEventDefinition> FilterSupersededAndUpdateToLatestEntity(IReadOnlyList<IEventDefinition> events)
{
//used to keep the 'latest' entity and associated event definition data
var allEntities = new List<Tuple<IEntity, EventDefinitionTypeData>>();
//tracks all CancellableObjectEventArgs instances in the events which is the only type of args we can work with
var cancelableArgs = new List<CancellableObjectEventArgs>();
// keeps the 'latest' entity and associated event data
var entities = new List<Tuple<IEntity, EventDefinitionInfos>>();
// collects the event definitions
// collects the arguments in result, that require their entities to be updated
var result = new List<IEventDefinition>();
var resultArgs = new List<CancellableObjectEventArgs>();
//This will eagerly load all of the event arg types and their attributes so we don't have to continuously look this data up
var allArgTypesWithAttributes = events.Select(x => x.Args.GetType())
// eagerly fetch superceeded arg types for each arg type
var argTypeSuperceeding = events.Select(x => x.Args.GetType())
.Distinct()
.ToDictionary(x => x, x => x.GetCustomAttributes<SupersedeEventAttribute>(false).ToArray());
.ToDictionary(x => x, x => x.GetCustomAttributes<SupersedeEventAttribute>(false).Select(y => y.SupersededEventArgsType).ToArray());
//Iterate all events and collect the actual entities in them and relates them to their corresponding EventDefinitionTypeData
//we'll process the list in reverse because events are added in the order they are raised and we want to filter out
//any entities from event args that are not longer relevant
//(i.e. if an item is Deleted after it's Saved, we won't include the item in the Saved args)
// iterate over all events and filter
//
// process the list in reverse, because events are added in the order they are raised and we want to keep
// the latest (most recent) entities and filter out what is not relevant anymore (too old), eg if an entity
// is Deleted after being Saved, we want to filter out the Saved event
for (var index = events.Count - 1; index >= 0; index--)
{
var eventDefinition = events[index];
var def = events[index];
var argType = eventDefinition.Args.GetType();
var attributes = allArgTypesWithAttributes[eventDefinition.Args.GetType()];
var meta = new EventDefinitionTypeData
var infos = new EventDefinitionInfos
{
EventDefinition = eventDefinition,
EventArgType = argType,
SupersedeAttributes = attributes
EventDefinition = def,
SupersedeTypes = argTypeSuperceeding[def.Args.GetType()]
};
var args = eventDefinition.Args as CancellableObjectEventArgs;
if (args != null)
var args = def.Args as CancellableObjectEventArgs;
if (args == null)
{
var list = TypeHelper.CreateGenericEnumerableFromObject(args.EventObject);
if (list == null)
// not a cancellable event arg, include event definition in result
result.Add(def);
}
else
{
// event object can either be a single object or an enumerable of objects
// try to get as an enumerable, get null if it's not
var eventObjects = TypeHelper.CreateGenericEnumerableFromObject(args.EventObject);
if (eventObjects == null)
{
//extract the event object
var obj = args.EventObject as IEntity;
if (obj != null)
// single object, cast as an IEntity
// if cannot cast, cannot filter, nothing - just include event definition in result
var eventEntity = args.EventObject as IEntity;
if (eventEntity == null)
{
//Now check if this entity already exists in other event args that supersede this current event arg type
if (IsFiltered(obj, meta, allEntities) == false)
{
//if it's not filtered we can adde these args to the response
cancelableArgs.Add(args);
result.Add(eventDefinition);
//track the entity
allEntities.Add(Tuple.Create(obj, meta));
}
result.Add(def);
continue;
}
else
// look for this entity in superceding event args
// found = must be removed (ie not added), else track
if (IsSuperceeded(eventEntity, infos, entities) == false)
{
//Can't retrieve the entity so cant' filter or inspect, just add to the output
result.Add(eventDefinition);
// track
entities.Add(Tuple.Create(eventEntity, infos));
// track result arguments
// include event definition in result
resultArgs.Add(args);
result.Add(def);
}
}
else
{
// enumerable of objects
var toRemove = new List<IEntity>();
foreach (var entity in list)
foreach (var eventObject in eventObjects)
{
//extract the event object
var obj = entity as IEntity;
if (obj != null)
{
//Now check if this entity already exists in other event args that supersede this current event arg type
if (IsFiltered(obj, meta, allEntities))
{
//track it to be removed
toRemove.Add(obj);
}
else
{
//track the entity, it's not filtered
allEntities.Add(Tuple.Create(obj, meta));
}
}
// extract the event object, cast as an IEntity
// if cannot cast, cannot filter, nothing to do - just leave it in the list & continue
var eventEntity = eventObject as IEntity;
if (eventEntity == null)
continue;
// look for this entity in superceding event args
// found = must be removed, else track
if (IsSuperceeded(eventEntity, infos, entities))
toRemove.Add(eventEntity);
else
{
//we don't need to do anything here, we can't cast to IEntity so we cannot filter, so it will just remain in the list
}
entities.Add(Tuple.Create(eventEntity, infos));
}
//remove anything that has been filtered
// remove superceded entities
foreach (var entity in toRemove)
{
list.Remove(entity);
}
eventObjects.Remove(entity);
//track the event and include in the response if there's still entities remaining in the list
if (list.Count > 0)
// if there are still entities in the list, keep the event definition
if (eventObjects.Count > 0)
{
if (toRemove.Count > 0)
{
//re-assign if the items have changed
args.EventObject = list;
// re-assign if changed
args.EventObject = eventObjects;
}
cancelableArgs.Add(args);
result.Add(eventDefinition);
// track result arguments
// include event definition in result
resultArgs.Add(args);
result.Add(def);
}
}
}
else
{
//it's not a cancelable event arg so we just include it in the result
result.Add(eventDefinition);
}
}
//Now we'll deal with ensuring that only the latest(non stale) entities are used throughout all event args
UpdateToLatestEntities(allEntities, cancelableArgs);
// go over all args in result, and update them with the latest instanceof each entity
UpdateToLatestEntities(entities, resultArgs);
//we need to reverse the result since we've been adding by latest added events first!
// reverse, since we processed the list in reverse
result.Reverse();
return result;
}
private static void UpdateToLatestEntities(IEnumerable<Tuple<IEntity, EventDefinitionTypeData>> allEntities, IEnumerable<CancellableObjectEventArgs> cancelableArgs)
// edits event args to use the latest instance of each entity
private static void UpdateToLatestEntities(IEnumerable<Tuple<IEntity, EventDefinitionInfos>> entities, IEnumerable<CancellableObjectEventArgs> args)
{
//Now we'll deal with ensuring that only the latest(non stale) entities are used throughout all event args
// get the latest entities
// ordered hash set + keepOldest will keep the latest inserted entity (in case of duplicates)
var latestEntities = new OrderedHashSet<IEntity>(keepOldest: true);
foreach (var entity in allEntities.OrderByDescending(entity => entity.Item1.UpdateDate))
{
foreach (var entity in entities.OrderByDescending(entity => entity.Item1.UpdateDate))
latestEntities.Add(entity.Item1);
}
foreach (var args in cancelableArgs)
foreach (var arg in args)
{
var list = TypeHelper.CreateGenericEnumerableFromObject(args.EventObject);
if (list == null)
// event object can either be a single object or an enumerable of objects
// try to get as an enumerable, get null if it's not
var eventObjects = TypeHelper.CreateGenericEnumerableFromObject(arg.EventObject);
if (eventObjects == null)
{
//try to find the args entity in the latest entity - based on the equality operators, this will
//match by Id since that is the default equality checker for IEntity. If one is found, than it is
//the most recent entity instance so update the args with that instance so we don't emit a stale instance.
var foundEntity = latestEntities.FirstOrDefault(x => Equals(x, args.EventObject));
// single object
// look for a more recent entity for that object, and replace if any
// works by "equalling" entities ie the more recent one "equals" this one (though different object)
var foundEntity = latestEntities.FirstOrDefault(x => Equals(x, arg.EventObject));
if (foundEntity != null)
{
args.EventObject = foundEntity;
}
arg.EventObject = foundEntity;
}
else
{
// enumerable of objects
// same as above but for each object
var updated = false;
for (int i = 0; i < list.Count; i++)
for (var i = 0; i < eventObjects.Count; i++)
{
//try to find the args entity in the latest entity - based on the equality operators, this will
//match by Id since that is the default equality checker for IEntity. If one is found, than it is
//the most recent entity instance so update the args with that instance so we don't emit a stale instance.
var foundEntity = latestEntities.FirstOrDefault(x => Equals(x, list[i]));
if (foundEntity != null)
{
list[i] = foundEntity;
updated = true;
}
var foundEntity = latestEntities.FirstOrDefault(x => Equals(x, eventObjects[i]));
if (foundEntity == null) continue;
eventObjects[i] = foundEntity;
updated = true;
}
if (updated)
{
args.EventObject = list;
}
arg.EventObject = eventObjects;
}
}
}
/// <summary>
/// This will check against all of the processed entity/events (allEntities) to see if this entity already exists in
/// event args that supersede the event args being passed in and if so returns true.
/// </summary>
/// <param name="entity"></param>
/// <param name="eventDef"></param>
/// <param name="allEntities"></param>
/// <returns></returns>
private static bool IsFiltered(
IEntity entity,
EventDefinitionTypeData eventDef,
List<Tuple<IEntity, EventDefinitionTypeData>> allEntities)
// determines if a given entity, appearing in a given event definition, should be filtered out,
// considering the entities that have already been visited - an entity is filtered out if it
// appears in another even definition, which superceedes this event definition.
private static bool IsSuperceeded(IEntity entity, EventDefinitionInfos infos, List<Tuple<IEntity, EventDefinitionInfos>> entities)
{
var argType = eventDef.EventDefinition.Args.GetType();
//var argType = meta.EventArgsType;
var argType = infos.EventDefinition.Args.GetType();
//check if the entity is found in any processed event data that could possible supersede this one
var foundByEntity = allEntities
.Where(x => x.Item2.SupersedeAttributes.Length > 0
//if it's the same arg type than it cannot supersede
&& x.Item2.EventArgType != argType
&& Equals(x.Item1, entity))
// look for other instances of the same entity, coming from an event args that supercedes other event args,
// ie is marked with the attribute, and is not this event args (cannot supersede itself)
var superceeding = entities
.Where(x => x.Item2.SupersedeTypes.Length > 0 // has the attribute
&& x.Item2.EventDefinition.Args.GetType() != argType // is not the same
&& Equals(x.Item1, entity)) // same entity
.ToArray();
//no args have been processed with this entity so it should not be filtered
if (foundByEntity.Length == 0)
// first time we see this entity = not filtered
if (superceeding.Length == 0)
return false;
// fixme see notes above
// delete event args does NOT superceedes 'unpublished' event
if (argType.IsGenericType && argType.GetGenericTypeDefinition() == typeof(PublishEventArgs<>) && infos.EventDefinition.EventName == "UnPublished")
return false;
// found occurences, need to determine if this event args is superceded
if (argType.IsGenericType)
{
var supercededBy = foundByEntity
.FirstOrDefault(x =>
x.Item2.SupersedeAttributes.Any(y =>
//if the attribute type is a generic type def then compare with the generic type def of the event arg
(y.SupersededEventArgsType.IsGenericTypeDefinition && y.SupersededEventArgsType == argType.GetGenericTypeDefinition())
//if the attribute type is not a generic type def then compare with the normal type of the event arg
|| (y.SupersededEventArgsType.IsGenericTypeDefinition == false && y.SupersededEventArgsType == argType)));
// generic, must compare type arguments
var supercededBy = superceeding.FirstOrDefault(x =>
x.Item2.SupersedeTypes.Any(y =>
// superceeding a generic type which has the same generic type definition
// fixme no matter the generic type parameters? could be different?
y.IsGenericTypeDefinition && y == argType.GetGenericTypeDefinition()
// or superceeding a non-generic type which is ... fixme how is this ever possible? argType *is* generic?
|| y.IsGenericTypeDefinition == false && y == argType));
return supercededBy != null;
}
else
{
var supercededBy = foundByEntity
.FirstOrDefault(x =>
x.Item2.SupersedeAttributes.Any(y =>
//since the event arg type is not a generic type, then we just compare type 1:1
y.SupersededEventArgsType == argType));
// non-generic, can compare types 1:1
var supercededBy = superceeding.FirstOrDefault(x =>
x.Item2.SupersedeTypes.Any(y => y == argType));
return supercededBy != null;
}
}
+16
View File
@@ -0,0 +1,16 @@
using System;
namespace Umbraco.Core.Events
{
public class RolesEventArgs : EventArgs
{
public RolesEventArgs(int[] memberIds, string[] roles)
{
MemberIds = memberIds;
Roles = roles;
}
public int[] MemberIds { get; set; }
public string[] Roles { get; set; }
}
}
@@ -0,0 +1,19 @@
using System;
using Umbraco.Core.Models.Membership;
namespace Umbraco.Core.Events
{
internal class UserGroupWithUsers
{
public UserGroupWithUsers(IUserGroup userGroup, IUser[] addedUsers, IUser[] removedUsers)
{
UserGroup = userGroup;
AddedUsers = addedUsers;
RemovedUsers = removedUsers;
}
public IUserGroup UserGroup { get; }
public IUser[] AddedUsers { get; }
public IUser[] RemovedUsers { get; }
}
}
@@ -0,0 +1,95 @@
using System;
using System.IO;
using log4net.Appender;
using log4net.Util;
namespace Umbraco.Core.Logging
{
/// <summary>
/// This class will do the exact same thing as the RollingFileAppender that comes from log4net
/// With the extension, that it is able to do automatic cleanup of the logfiles in the directory where logging happens
///
/// By specifying the properties MaxLogFileDays and BaseFilePattern, the files will automaticly get deleted when
/// the logger is configured(typically when the app starts). To utilize this appender swap out the type of the rollingFile appender
/// that ships with Umbraco, to be Umbraco.Core.Logging.RollingFileCleanupAppender, and add the maxLogFileDays and baseFilePattern elements
/// to the configuration i.e.:
///
/// <example>
/// <appender name="rollingFile" type="Log4netAwesomeness.CustomRollingFileAppender, Log4netAwesomeness">
/// <file type="log4net.Util.PatternString" value="App_Data\Logs\UmbracoTraceLog.%property{log4net:HostName}.txt" />
/// <lockingModel type="log4net.Appender.FileAppender+MinimalLock" />
/// <appendToFile value="true" />
/// <rollingStyle value="Date" />
/// <maximumFileSize value="5MB" />
/// <maxLogFileDays value="5"/>
/// <basefilePattern value="UmbracoTraceLog.*.txt.*"/>
/// <layout type="log4net.Layout.PatternLayout">
/// <conversionPattern value=" %date [P%property{processId}/D%property{appDomainId}/T%thread] %-5level %logger - %message%newline" />
/// </layout>
/// <layout type="log4net.Layout.PatternLayout">
/// <conversionPattern value=" %date [P%property{processId}/D%property{appDomainId}/T%thread] %-5level %logger - %message%newline" />
/// </layout>
/// <encoding value="utf-8" />
/// </appender>
/// </example>
/// </summary>
public class RollingFileCleanupAppender : RollingFileAppender
{
public int MaxLogFileDays { get; set; }
public string BaseFilePattern { get; set; }
/// <summary>
/// This override will delete logs older than the specified amount of days
/// </summary>
/// <param name="fileName"></param>
/// <param name="append"></param>
protected override void OpenFile(string fileName, bool append)
{
bool cleanup = true;
// Validate settings and input
if (MaxLogFileDays <= 0)
{
LogLog.Warn(typeof(RollingFileCleanupAppender), "Parameter 'MaxLogFileDays' needs to be a positive integer, aborting cleanup");
cleanup = false;
}
if (string.IsNullOrWhiteSpace(BaseFilePattern))
{
LogLog.Warn(typeof(RollingFileCleanupAppender), "Parameter 'BaseFilePattern' is empty, aborting cleanup");
cleanup = false;
}
// grab the directory we are logging to, as this is were we will search for older logfiles
var logFolder = Path.GetDirectoryName(fileName);
if (Directory.Exists(logFolder) == false)
{
LogLog.Warn(typeof(RollingFileCleanupAppender), string.Format("Directory '{0}' for logfiles does not exist, aborting cleanup", logFolder));
cleanup = false;
}
// If everything is validated, we can do the actual cleanup
if (cleanup)
{
Cleanup(logFolder);
}
base.OpenFile(fileName, append);
}
private void Cleanup(string directoryPath)
{
// only take files that matches the pattern we are using i.e. UmbracoTraceLog.*.txt.*
string[] logFiles = Directory.GetFiles(directoryPath, BaseFilePattern);
LogLog.Debug(typeof(RollingFileCleanupAppender), string.Format("Found {0} files that matches the baseFilePattern: '{1}'", logFiles.Length, BaseFilePattern));
foreach (var logFile in logFiles)
{
DateTime lastAccessTime = System.IO.File.GetLastWriteTimeUtc(logFile);
// take the value from the config file
if (lastAccessTime < DateTime.Now.AddDays(-MaxLogFileDays))
{
LogLog.Debug(typeof(RollingFileCleanupAppender), string.Format("Deleting file {0} as its lastAccessTime is older than {1} days speficied by MaxLogFileDays", logFile, MaxLogFileDays));
base.DeleteFile(logFile);
}
}
}
}
}
@@ -87,6 +87,15 @@ namespace Umbraco.Core.Models.Entities
_currentChanges = null;
}
/// <inheritdoc />
public virtual IEnumerable<string> GetWereDirtyProperties()
{
// ReSharper disable once MergeConditionalExpression
return _savedChanges == null
? Enumerable.Empty<string>()
: _savedChanges.Where(x => x.Value).Select(x => x.Key);
}
#endregion
#region Change Tracking
@@ -1,4 +1,6 @@
namespace Umbraco.Core.Models.Entities
using System.Collections.Generic;
namespace Umbraco.Core.Models.Entities
{
/// <summary>
/// Defines an entity that tracks property changes and can be dirty, and remembers
@@ -29,5 +31,10 @@
/// <param name="rememberDirty">A value indicating whether to remember dirty properties.</param>
/// <remarks>When <paramref name="rememberDirty"/> is true, dirty properties are saved so they can be checked with WasDirty.</remarks>
void ResetDirtyProperties(bool rememberDirty);
/// <summary>
/// Gets properties that were dirty.
/// </summary>
IEnumerable<string> GetWereDirtyProperties();
}
}
@@ -60,6 +60,14 @@ namespace Umbraco.Core.Models.Identity
private BackOfficeIdentityUser()
{
_startMediaIds = new int[] { };
_startContentIds = new int[] { };
_groups = new IReadOnlyUserGroup[] { };
_allowedSections = new string[] { };
_culture = Configuration.GlobalSettings.DefaultUILanguage;
_groups = new IReadOnlyUserGroup[0];
_roles = new ObservableCollection<IdentityUserRole<string>>();
_roles.CollectionChanged += _roles_CollectionChanged;
}
/// <summary>
@@ -46,20 +46,6 @@ namespace Umbraco.Core.Models.Identity
dest.ResetDirtyProperties(true);
dest.EnableChangeTracking();
});
CreateMap<BackOfficeIdentityUser, UserData>()
.ConstructUsing(source => new UserData(Guid.NewGuid().ToString("N"))) //this is the 'session id'
.ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.Id))
.ForMember(dest => dest.AllowedApplications, opt => opt.MapFrom(src => src.AllowedSections))
.ForMember(dest => dest.Roles, opt => opt.MapFrom(src => new[] { src.Roles.Select(x => x.RoleId).ToArray()}))
.ForMember(dest => dest.RealName, opt => opt.MapFrom(src => src.Name))
//When mapping to UserData which is used in the authcookie we want ALL start nodes including ones defined on the groups
.ForMember(dest => dest.StartContentNodes, opt => opt.MapFrom(src => src.CalculatedContentStartNodeIds))
//When mapping to UserData which is used in the authcookie we want ALL start nodes including ones defined on the groups
.ForMember(dest => dest.StartMediaNodes, opt => opt.MapFrom(src => src.CalculatedMediaStartNodeIds))
.ForMember(dest => dest.Username, opt => opt.MapFrom(src => src.UserName))
.ForMember(dest => dest.Culture, opt => opt.MapFrom(src => src.Culture))
.ForMember(dest => dest.SessionId, opt => opt.MapFrom(src => src.SecurityStamp.IsNullOrWhiteSpace() ? Guid.NewGuid().ToString("N") : src.SecurityStamp));
}
private static string GetPasswordHash(string storedPass)
@@ -17,7 +17,30 @@ namespace Umbraco.Core.Models.Membership
}
/// <summary>
/// Returns the aggregate permissions in the permission set
/// Returns the aggregate permissions in the permission set for a single node
/// </summary>
/// <returns></returns>
/// <remarks>
/// This value is only calculated once per node
/// </remarks>
public IEnumerable<string> GetAllPermissions(int entityId)
{
if (_aggregateNodePermissions == null)
_aggregateNodePermissions = new Dictionary<int, string[]>();
string[] entityPermissions;
if (_aggregateNodePermissions.TryGetValue(entityId, out entityPermissions) == false)
{
entityPermissions = this.Where(x => x.EntityId == entityId).SelectMany(x => x.AssignedPermissions).Distinct().ToArray();
_aggregateNodePermissions[entityId] = entityPermissions;
}
return entityPermissions;
}
private Dictionary<int, string[]> _aggregateNodePermissions;
/// <summary>
/// Returns the aggregate permissions in the permission set for all nodes
/// </summary>
/// <returns></returns>
/// <remarks>
+7 -2
View File
@@ -58,6 +58,11 @@ namespace Umbraco.Core.Models.Membership
/// <summary>
/// Will hold the media file system relative path of the users custom avatar if they uploaded one
/// </summary>
string Avatar { get; set; }
string Avatar { get; set; }
/// <summary>
/// A Json blob stored for recording tour data for a user
/// </summary>
string TourData { get; set; }
}
}
}
@@ -3,7 +3,7 @@ using Umbraco.Core.Models.Entities;
namespace Umbraco.Core.Models.Membership
{
public interface IUserGroup : IEntity
public interface IUserGroup : IEntity, IRememberBeingDirty
{
string Alias { get; set; }
@@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
namespace Umbraco.Core.Models.Membership
{
internal class MemberExportModel
{
public int Id { get; set; }
public Guid Key { get; set; }
public string Name { get; set; }
public string Username { get; set; }
public string Email { get; set; }
public List<string> Groups { get; set; }
public string ContentTypeAlias { get; set; }
public DateTime CreateDate { get; set; }
public DateTime UpdateDate { get; set; }
public List<MemberExportProperty> Properties { get; set; }
}
}
@@ -0,0 +1,14 @@
using System;
namespace Umbraco.Core.Models.Membership
{
internal class MemberExportProperty
{
public int Id { get; set; }
public string Alias { get; set; }
public string Name { get; set; }
public object Value { get; set; }
public DateTime? CreateDate { get; set; }
public DateTime? UpdateDate { get; set; }
}
}
@@ -100,6 +100,7 @@ namespace Umbraco.Core.Models.Membership
private string _name;
private string _securityStamp;
private string _avatar;
private string _tourData;
private int _sessionTimeout;
private int[] _startContentIds;
private int[] _startMediaIds;
@@ -133,6 +134,7 @@ namespace Umbraco.Core.Models.Membership
public readonly PropertyInfo SecurityStampSelector = ExpressionHelper.GetPropertyInfo<User, string>(x => x.SecurityStamp);
public readonly PropertyInfo AvatarSelector = ExpressionHelper.GetPropertyInfo<User, string>(x => x.Avatar);
public readonly PropertyInfo TourDataSelector = ExpressionHelper.GetPropertyInfo<User, string>(x => x.TourData);
public readonly PropertyInfo SessionTimeoutSelector = ExpressionHelper.GetPropertyInfo<User, int>(x => x.SessionTimeout);
public readonly PropertyInfo StartContentIdSelector = ExpressionHelper.GetPropertyInfo<User, int[]>(x => x.StartContentIds);
public readonly PropertyInfo StartMediaIdSelector = ExpressionHelper.GetPropertyInfo<User, int[]>(x => x.StartMediaIds);
@@ -467,6 +469,16 @@ namespace Umbraco.Core.Models.Membership
set { SetPropertyValueAndDetectChanges(value, ref _avatar, Ps.Value.AvatarSelector); }
}
/// <summary>
/// A Json blob stored for recording tour data for a user
/// </summary>
[DataMember]
public string TourData
{
get { return _tourData; }
set { SetPropertyValueAndDetectChanges(value, ref _tourData, Ps.Value.TourDataSelector); }
}
/// <summary>
/// Gets or sets the session timeout.
/// </summary>
@@ -671,5 +683,6 @@ namespace Umbraco.Core.Models.Membership
return _user.GetHashCode();
}
}
}
}
@@ -12,7 +12,7 @@ namespace Umbraco.Core.Models.Membership
/// </summary>
[Serializable]
[DataContract(IsReference = true)]
internal class UserGroup : EntityBase, IUserGroup, IReadOnlyUserGroup
public class UserGroup : EntityBase, IUserGroup, IReadOnlyUserGroup
{
private int? _startContentId;
private int? _startMediaId;
+342 -246
View File
@@ -6,10 +6,10 @@ using System.ComponentModel;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Xml;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using Umbraco.Core.Composing;
using Umbraco.Core.Collections;
namespace Umbraco.Core
{
@@ -18,8 +18,15 @@ namespace Umbraco.Core
/// </summary>
public static class ObjectExtensions
{
private static readonly ConcurrentDictionary<Type, Dictionary<string, object>> ToObjectTypes
= new ConcurrentDictionary<Type, Dictionary<string, object>>();
private static readonly ConcurrentDictionary<Type, Dictionary<string, object>> ToObjectTypes = new ConcurrentDictionary<Type, Dictionary<string, object>>();
private static readonly ConcurrentDictionary<Type, Type> NullableGenericCache = new ConcurrentDictionary<Type, Type>();
private static readonly ConcurrentDictionary<CompositeTypeTypeKey, TypeConverter> InputTypeConverterCache = new ConcurrentDictionary<CompositeTypeTypeKey, TypeConverter>();
private static readonly ConcurrentDictionary<CompositeTypeTypeKey, TypeConverter> DestinationTypeConverterCache = new ConcurrentDictionary<CompositeTypeTypeKey, TypeConverter>();
private static readonly ConcurrentDictionary<CompositeTypeTypeKey, bool> AssignableTypeCache = new ConcurrentDictionary<CompositeTypeTypeKey, bool>();
private static readonly ConcurrentDictionary<Type, bool> BoolConvertCache = new ConcurrentDictionary<Type, bool>();
private static readonly char[] NumberDecimalSeparatorsToNormalize = { '.', ',' };
private static readonly CustomBooleanTypeConverter CustomBooleanTypeConverter = new CustomBooleanTypeConverter();
//private static readonly ConcurrentDictionary<Type, Func<object>> ObjectFactoryCache = new ConcurrentDictionary<Type, Func<object>>();
@@ -40,8 +47,8 @@ namespace Umbraco.Core
/// <param name="input"></param>
public static void DisposeIfDisposable(this object input)
{
var disposable = input as IDisposable;
if (disposable != null) disposable.Dispose();
if (input is IDisposable disposable)
disposable.Dispose();
}
/// <summary>
@@ -53,347 +60,335 @@ namespace Umbraco.Core
/// <returns></returns>
internal static T SafeCast<T>(this object input)
{
if (ReferenceEquals(null, input) || ReferenceEquals(default(T), input)) return default(T);
if (input is T) return (T)input;
return default(T);
if (ReferenceEquals(null, input) || ReferenceEquals(default(T), input)) return default;
if (input is T variable) return variable;
return default;
}
/// <summary>
/// Tries to convert the input object to the output type using TypeConverters
/// Attempts to convert the input object to the output type.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="input"></param>
/// <returns></returns>
/// <remarks>This code is an optimized version of the original Umbraco method</remarks>
/// <typeparam name="T">The type to convert to</typeparam>
/// <param name="input">The input.</param>
/// <returns>The <see cref="Attempt{T}"/></returns>
public static Attempt<T> TryConvertTo<T>(this object input)
{
var result = TryConvertTo(input, typeof(T));
if (result.Success == false)
if (result.Success)
return Attempt<T>.Succeed((T)result.Result);
// just try to cast
try
{
//just try a straight up conversion
try
{
var converted = (T) input;
return Attempt<T>.Succeed(converted);
}
catch (Exception e)
{
return Attempt<T>.Fail(e);
}
return Attempt<T>.Succeed((T)input);
}
catch (Exception e)
{
return Attempt<T>.Fail(e);
}
return result.Success == false ? Attempt<T>.Fail() : Attempt<T>.Succeed((T)result.Result);
}
/// <summary>
/// Tries to convert the input object to the output type using TypeConverters. If the destination
/// type is a superclass of the input type, if will use <see cref="Convert.ChangeType(object,System.Type)"/>.
/// Attempts to convert the input object to the output type.
/// </summary>
/// <remarks>This code is an optimized version of the original Umbraco method</remarks>
/// <param name="input">The input.</param>
/// <param name="destinationType">Type of the destination.</param>
/// <returns></returns>
public static Attempt<object> TryConvertTo(this object input, Type destinationType)
/// <param name="target">The type to convert to</param>
/// <returns>The <see cref="Attempt{Object}"/></returns>
public static Attempt<object> TryConvertTo(this object input, Type target)
{
// if null...
if (input == null)
if (target == null)
{
// nullable is ok
if (destinationType.IsGenericType && destinationType.GetGenericTypeDefinition() == typeof(Nullable<>))
return Attempt<object>.Succeed(null);
// value type is nok, else can be null, so is ok
return Attempt<object>.If(destinationType.IsValueType == false, null);
return Attempt<object>.Fail();
}
// easy
if (destinationType == typeof(object)) return Attempt.Succeed(input);
if (input.GetType() == destinationType) return Attempt.Succeed(input);
// check for string so that overloaders of ToString() can take advantage of the conversion.
if (destinationType == typeof(string)) return Attempt<object>.Succeed(input.ToString());
// if we've got a nullable of something, we try to convert directly to that thing.
if (destinationType.IsGenericType && destinationType.GetGenericTypeDefinition() == typeof(Nullable<>))
try
{
var underlyingType = Nullable.GetUnderlyingType(destinationType);
//special case for empty strings for bools/dates which should return null if an empty string
var asString = input as string;
if (asString != null && string.IsNullOrEmpty(asString) && (underlyingType == typeof(DateTime) || underlyingType == typeof(bool)))
if (input == null)
{
return Attempt<object>.Succeed(null);
// Nullable is ok
if (target.IsGenericType && GetCachedGenericNullableType(target) != null)
{
return Attempt<object>.Succeed(null);
}
// Reference types are ok
return Attempt<object>.If(target.IsValueType == false, null);
}
// recursively call into myself with the inner (not-nullable) type and handle the outcome
var nonNullable = input.TryConvertTo(underlyingType);
var inputType = input.GetType();
// and if sucessful, fall on through to rewrap in a nullable; if failed, pass on the exception
if (nonNullable.Success)
input = nonNullable.Result; // now fall on through...
// Easy
if (target == typeof(object) || inputType == target)
{
return Attempt.Succeed(input);
}
// Check for string so that overloaders of ToString() can take advantage of the conversion.
if (target == typeof(string))
{
return Attempt<object>.Succeed(input.ToString());
}
// If we've got a nullable of something, we try to convert directly to that thing.
// We cache the destination type and underlying nullable types
// Any other generic types need to fall through
if (target.IsGenericType)
{
var underlying = GetCachedGenericNullableType(target);
if (underlying != null)
{
// Special case for empty strings for bools/dates which should return null if an empty string.
if (input is string inputString)
{
//TODO: Why the check against only bool/date when a string is null/empty? In what scenario can we convert to another type when the string is null or empty other than just being null?
if (string.IsNullOrEmpty(inputString) && (underlying == typeof(DateTime) || underlying == typeof(bool)))
{
return Attempt<object>.Succeed(null);
}
}
// Recursively call into this method with the inner (not-nullable) type and handle the outcome
var inner = input.TryConvertTo(underlying);
// And if sucessful, fall on through to rewrap in a nullable; if failed, pass on the exception
if (inner.Success)
{
input = inner.Result; // Now fall on through...
}
else
{
return Attempt<object>.Fail(inner.Exception);
}
}
}
else
return Attempt<object>.Fail(nonNullable.Exception);
}
// we've already dealed with nullables, so any other generic types need to fall through
if (destinationType.IsGenericType == false)
{
if (input is string)
{
// try convert from string, returns an Attempt if the string could be
// processed (either succeeded or failed), else null if we need to try
// other methods
var result = TryConvertToFromString(input as string, destinationType);
if (result.HasValue) return result.Value;
}
// target is not a generic type
//TODO: Do a check for destination type being IEnumerable<T> and source type implementing IEnumerable<T> with
// the same 'T', then we'd have to find the extension method for the type AsEnumerable() and execute it.
if (TypeHelper.IsTypeAssignableFrom(destinationType, input.GetType())
&& TypeHelper.IsTypeAssignableFrom<IConvertible>(input))
{
try
if (input is string inputString)
{
var casted = Convert.ChangeType(input, destinationType);
return Attempt.Succeed(casted);
// Try convert from string, returns an Attempt if the string could be
// processed (either succeeded or failed), else null if we need to try
// other methods
var result = TryConvertToFromString(inputString, target);
if (result.HasValue)
{
return result.Value;
}
}
catch (Exception e)
// TODO: Do a check for destination type being IEnumerable<T> and source type implementing IEnumerable<T> with
// the same 'T', then we'd have to find the extension method for the type AsEnumerable() and execute it.
if (GetCachedCanAssign(input, inputType, target))
{
return Attempt<object>.Fail(e);
return Attempt.Succeed(Convert.ChangeType(input, target));
}
}
}
var inputConverter = TypeDescriptor.GetConverter(input);
if (inputConverter.CanConvertTo(destinationType))
{
try
if (target == typeof(bool))
{
var converted = inputConverter.ConvertTo(input, destinationType);
return Attempt.Succeed(converted);
}
catch (Exception e)
{
return Attempt<object>.Fail(e);
}
}
if (destinationType == typeof(bool))
{
var boolConverter = new CustomBooleanTypeConverter();
if (boolConverter.CanConvertFrom(input.GetType()))
{
try
if (GetCachedCanConvertToBoolean(inputType))
{
var converted = boolConverter.ConvertFrom(input);
return Attempt.Succeed(converted);
}
catch (Exception e)
{
return Attempt<object>.Fail(e);
return Attempt.Succeed(CustomBooleanTypeConverter.ConvertFrom(input));
}
}
}
var outputConverter = TypeDescriptor.GetConverter(destinationType);
if (outputConverter.CanConvertFrom(input.GetType()))
{
try
var inputConverter = GetCachedSourceTypeConverter(inputType, target);
if (inputConverter != null)
{
var converted = outputConverter.ConvertFrom(input);
return Attempt.Succeed(converted);
return Attempt.Succeed(inputConverter.ConvertTo(input, target));
}
catch (Exception e)
var outputConverter = GetCachedTargetTypeConverter(inputType, target);
if (outputConverter != null)
{
return Attempt<object>.Fail(e);
return Attempt.Succeed(outputConverter.ConvertFrom(input));
}
if (target.IsGenericType && GetCachedGenericNullableType(target) != null)
{
// cannot Convert.ChangeType as that does not work with nullable
// input has already been converted to the underlying type - just
// return input, there's an implicit conversion from T to T? anyways
return Attempt.Succeed(input);
}
// Re-check convertables since we altered the input through recursion
if (input is IConvertible convertible2)
{
return Attempt.Succeed(Convert.ChangeType(convertible2, target));
}
}
if (TypeHelper.IsTypeAssignableFrom<IConvertible>(input))
catch (Exception e)
{
try
{
var casted = Convert.ChangeType(input, destinationType);
return Attempt.Succeed(casted);
}
catch (Exception e)
{
return Attempt<object>.Fail(e);
}
return Attempt<object>.Fail(e);
}
return Attempt<object>.Fail();
}
// returns an attempt if the string has been processed (either succeeded or failed)
// returns null if we need to try other methods
private static Attempt<object>? TryConvertToFromString(this string input, Type destinationType)
/// <summary>
/// Attempts to convert the input string to the output type.
/// </summary>
/// <remarks>This code is an optimized version of the original Umbraco method</remarks>
/// <param name="input">The input.</param>
/// <param name="target">The type to convert to</param>
/// <returns>The <see cref="Nullable{Attempt}"/></returns>
private static Attempt<object>? TryConvertToFromString(this string input, Type target)
{
// easy
if (destinationType == typeof(string))
return Attempt<object>.Succeed(input);
// null, empty, whitespaces
if (string.IsNullOrWhiteSpace(input))
// Easy
if (target == typeof(string))
{
if (destinationType == typeof(bool)) // null/empty = bool false
return Attempt<object>.Succeed(false);
if (destinationType == typeof(DateTime)) // null/empty = min DateTime value
return Attempt<object>.Succeed(DateTime.MinValue);
// cannot decide here,
// any of the types below will fail parsing and will return a failed attempt
// but anything else will not be processed and will return null
// so even though the string is null/empty we have to proceed
return Attempt<object>.Succeed(input);
}
// look for type conversions in the expected order of frequency of use...
if (destinationType.IsPrimitive)
// Null, empty, whitespaces
if (string.IsNullOrWhiteSpace(input))
{
if (destinationType == typeof(int)) // aka Int32
if (target == typeof(bool))
{
int value;
if (int.TryParse(input, out value)) return Attempt<object>.Succeed(value);
// null/empty = bool false
return Attempt<object>.Succeed(false);
}
// because decimal 100.01m will happily convert to integer 100, it
if (target == typeof(DateTime))
{
// null/empty = min DateTime value
return Attempt<object>.Succeed(DateTime.MinValue);
}
// Cannot decide here,
// Any of the types below will fail parsing and will return a failed attempt
// but anything else will not be processed and will return null
// so even though the string is null/empty we have to proceed.
}
// Look for type conversions in the expected order of frequency of use.
//
// By using a mixture of ordered if statements and switches we can optimize both for
// fast conditional checking for most frequently used types and the branching
// that does not depend on previous values available to switch statements.
if (target.IsPrimitive)
{
if (target == typeof(int))
{
if (int.TryParse(input, out var value))
{
return Attempt<object>.Succeed(value);
}
// Because decimal 100.01m will happily convert to integer 100, it
// makes sense that string "100.01" *also* converts to integer 100.
decimal value2;
var input2 = NormalizeNumberDecimalSeparator(input);
return Attempt<object>.If(decimal.TryParse(input2, out value2), Convert.ToInt32(value2));
return Attempt<object>.If(decimal.TryParse(input2, out var value2), Convert.ToInt32(value2));
}
if (destinationType == typeof(long)) // aka Int64
if (target == typeof(long))
{
long value;
if (long.TryParse(input, out value)) return Attempt<object>.Succeed(value);
if (long.TryParse(input, out var value))
{
return Attempt<object>.Succeed(value);
}
// same as int
decimal value2;
// Same as int
var input2 = NormalizeNumberDecimalSeparator(input);
return Attempt<object>.If(decimal.TryParse(input2, out value2), Convert.ToInt64(value2));
return Attempt<object>.If(decimal.TryParse(input2, out var value2), Convert.ToInt64(value2));
}
// fixme - should we do the decimal trick for short, byte, unsigned?
// TODO: Should we do the decimal trick for short, byte, unsigned?
if (destinationType == typeof(bool)) // aka Boolean
if (target == typeof(bool))
{
bool value;
if (bool.TryParse(input, out value)) return Attempt<object>.Succeed(value);
// don't declare failure so the CustomBooleanTypeConverter can try
if (bool.TryParse(input, out var value))
{
return Attempt<object>.Succeed(value);
}
// Don't declare failure so the CustomBooleanTypeConverter can try
return null;
}
if (destinationType == typeof(short)) // aka Int16
// Calling this method directly is faster than any attempt to cache it.
switch (Type.GetTypeCode(target))
{
short value;
return Attempt<object>.If(short.TryParse(input, out value), value);
}
case TypeCode.Int16:
return Attempt<object>.If(short.TryParse(input, out var value), value);
if (destinationType == typeof(double)) // aka Double
{
double value;
var input2 = NormalizeNumberDecimalSeparator(input);
return Attempt<object>.If(double.TryParse(input2, out value), value);
}
case TypeCode.Double:
var input2 = NormalizeNumberDecimalSeparator(input);
return Attempt<object>.If(double.TryParse(input2, out var valueD), valueD);
if (destinationType == typeof(float)) // aka Single
{
float value;
var input2 = NormalizeNumberDecimalSeparator(input);
return Attempt<object>.If(float.TryParse(input2, out value), value);
}
case TypeCode.Single:
var input3 = NormalizeNumberDecimalSeparator(input);
return Attempt<object>.If(float.TryParse(input3, out var valueF), valueF);
if (destinationType == typeof(char)) // aka Char
{
char value;
return Attempt<object>.If(char.TryParse(input, out value), value);
}
case TypeCode.Char:
return Attempt<object>.If(char.TryParse(input, out var valueC), valueC);
if (destinationType == typeof(byte)) // aka Byte
{
byte value;
return Attempt<object>.If(byte.TryParse(input, out value), value);
}
case TypeCode.Byte:
return Attempt<object>.If(byte.TryParse(input, out var valueB), valueB);
if (destinationType == typeof(sbyte)) // aka SByte
{
sbyte value;
return Attempt<object>.If(sbyte.TryParse(input, out value), value);
}
case TypeCode.SByte:
return Attempt<object>.If(sbyte.TryParse(input, out var valueSb), valueSb);
if (destinationType == typeof(uint)) // aka UInt32
{
uint value;
return Attempt<object>.If(uint.TryParse(input, out value), value);
}
case TypeCode.UInt32:
return Attempt<object>.If(uint.TryParse(input, out var valueU), valueU);
if (destinationType == typeof(ushort)) // aka UInt16
{
ushort value;
return Attempt<object>.If(ushort.TryParse(input, out value), value);
}
case TypeCode.UInt16:
return Attempt<object>.If(ushort.TryParse(input, out var valueUs), valueUs);
if (destinationType == typeof(ulong)) // aka UInt64
{
ulong value;
return Attempt<object>.If(ulong.TryParse(input, out value), value);
case TypeCode.UInt64:
return Attempt<object>.If(ulong.TryParse(input, out var valueUl), valueUl);
}
}
else if (destinationType == typeof(Guid))
else if (target == typeof(Guid))
{
Guid value;
return Attempt<object>.If(Guid.TryParse(input, out value), value);
return Attempt<object>.If(Guid.TryParse(input, out var value), value);
}
else if (destinationType == typeof(DateTime))
else if (target == typeof(DateTime))
{
DateTime value;
if (DateTime.TryParse(input, out value))
if (DateTime.TryParse(input, out var value))
{
switch (value.Kind)
{
case DateTimeKind.Unspecified:
case DateTimeKind.Utc:
return Attempt<object>.Succeed(value);
case DateTimeKind.Local:
return Attempt<object>.Succeed(value.ToUniversalTime());
default:
throw new ArgumentOutOfRangeException();
}
}
return Attempt<object>.Fail();
}
else if (destinationType == typeof(DateTimeOffset))
else if (target == typeof(DateTimeOffset))
{
DateTimeOffset value;
return Attempt<object>.If(DateTimeOffset.TryParse(input, out value), value);
return Attempt<object>.If(DateTimeOffset.TryParse(input, out var value), value);
}
else if (destinationType == typeof(TimeSpan))
else if (target == typeof(TimeSpan))
{
TimeSpan value;
return Attempt<object>.If(TimeSpan.TryParse(input, out value), value);
return Attempt<object>.If(TimeSpan.TryParse(input, out var value), value);
}
else if (destinationType == typeof(decimal)) // aka Decimal
else if (target == typeof(decimal))
{
decimal value;
var input2 = NormalizeNumberDecimalSeparator(input);
return Attempt<object>.If(decimal.TryParse(input2, out value), value);
return Attempt<object>.If(decimal.TryParse(input2, out var value), value);
}
else if (destinationType == typeof(Version))
else if (input != null && target == typeof(Version))
{
Version value;
return Attempt<object>.If(Version.TryParse(input, out value), value);
return Attempt<object>.If(Version.TryParse(input, out var value), value);
}
// E_NOTIMPL IPAddress, BigInteger
return null; // we can't decide...
return null; // we can't decide...
}
private static readonly char[] NumberDecimalSeparatorsToNormalize = {'.', ','};
private static string NormalizeNumberDecimalSeparator(string s)
{
var normalized = System.Threading.Thread.CurrentThread.CurrentCulture.NumberFormat.NumberDecimalSeparator[0];
return s.ReplaceMany(NumberDecimalSeparatorsToNormalize, normalized);
}
internal static void CheckThrowObjectDisposed(this IDisposable disposable, bool isDisposed, string objectname)
{
//TODO: Localise this exception
@@ -475,8 +470,7 @@ namespace Umbraco.Core
/// <param name="o"></param>
/// <param name="ignoreProperties"></param>
/// <returns></returns>
public static IDictionary<string, TVal> ToDictionary<T, TProperty, TVal>(this T o,
params Expression<Func<T, TProperty>>[] ignoreProperties)
public static IDictionary<string, TVal> ToDictionary<T, TProperty, TVal>(this T o, params Expression<Func<T, TProperty>>[] ignoreProperties)
{
return o.ToDictionary<TVal>(ignoreProperties.Select(e => o.GetPropertyInfo(e)).Select(propInfo => propInfo.Name).ToArray());
}
@@ -563,7 +557,7 @@ namespace Umbraco.Core
var items = (from object enumItem in enumerable let value = GetEnumPropertyDebugString(enumItem, levels) where value != null select value).Take(10).ToList();
return items.Any()
? "{{ {0} }}".InvariantFormat(String.Join(", ", items))
? "{{ {0} }}".InvariantFormat(string.Join(", ", items))
: null;
}
@@ -585,9 +579,9 @@ namespace Umbraco.Core
{
var items =
(from propertyInfo in props
let value = GetPropertyDebugString(propertyInfo, obj, levels)
where value != null
select "{0}={1}".InvariantFormat(propertyInfo.Name, value)).ToArray();
let value = GetPropertyDebugString(propertyInfo, obj, levels)
where value != null
select "{0}={1}".InvariantFormat(propertyInfo.Name, value)).ToArray();
return items.Any()
? "[{0}]:{{ {1} }}".InvariantFormat(obj.GetType().Name, String.Join(", ", items))
@@ -690,7 +684,109 @@ namespace Umbraco.Core
internal static Guid AsGuid(this object value)
{
return value is Guid ? (Guid) value : Guid.Empty;
return value is Guid guid ? guid : Guid.Empty;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static string NormalizeNumberDecimalSeparator(string s)
{
var normalized = System.Threading.Thread.CurrentThread.CurrentCulture.NumberFormat.NumberDecimalSeparator[0];
return s.ReplaceMany(NumberDecimalSeparatorsToNormalize, normalized);
}
// gets a converter for source, that can convert to target, or null if none exists
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static TypeConverter GetCachedSourceTypeConverter(Type source, Type target)
{
var key = new CompositeTypeTypeKey(source, target);
if (InputTypeConverterCache.TryGetValue(key, out var typeConverter))
{
return typeConverter;
}
var converter = TypeDescriptor.GetConverter(source);
if (converter.CanConvertTo(target))
{
return InputTypeConverterCache[key] = converter;
}
return InputTypeConverterCache[key] = null;
}
// gets a converter for target, that can convert from source, or null if none exists
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static TypeConverter GetCachedTargetTypeConverter(Type source, Type target)
{
var key = new CompositeTypeTypeKey(source, target);
if (DestinationTypeConverterCache.TryGetValue(key, out var typeConverter))
{
return typeConverter;
}
TypeConverter converter = TypeDescriptor.GetConverter(target);
if (converter.CanConvertFrom(source))
{
return DestinationTypeConverterCache[key] = converter;
}
return DestinationTypeConverterCache[key] = null;
}
// gets the underlying type of a nullable type, or null if the type is not nullable
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Type GetCachedGenericNullableType(Type type)
{
if (NullableGenericCache.TryGetValue(type, out var underlyingType))
{
return underlyingType;
}
if (type.GetGenericTypeDefinition() == typeof(Nullable<>))
{
Type underlying = Nullable.GetUnderlyingType(type);
return NullableGenericCache[type] = underlying;
}
return NullableGenericCache[type] = null;
}
// gets an IConvertible from source to target type, or null if none exists
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool GetCachedCanAssign(object input, Type source, Type target)
{
var key = new CompositeTypeTypeKey(source, target);
if (AssignableTypeCache.TryGetValue(key, out var canConvert))
{
return canConvert;
}
// "object is" is faster than "Type.IsAssignableFrom.
// We can use it to very quickly determine whether true/false
if (input is IConvertible && target.IsAssignableFrom(source))
{
return AssignableTypeCache[key] = true;
}
return AssignableTypeCache[key] = false;
}
// determines whether a type can be converted to boolean
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool GetCachedCanConvertToBoolean(Type type)
{
if (BoolConvertCache.TryGetValue(type, out var result))
{
return result;
}
if (CustomBooleanTypeConverter.CanConvertFrom(type))
{
return BoolConvertCache[type] = true;
}
return BoolConvertCache[type] = false;
}
}
}
@@ -71,6 +71,10 @@ namespace Umbraco.Core
public const string TaskType = /*TableNamePrefix*/ "cms" + "TaskType";
public const string KeyValue = TableNamePrefix + "KeyValue";
public const string AuditEntry = /*TableNamePrefix*/ "umbraco" + "Audit";
public const string Consent = /*TableNamePrefix*/ "umbraco" + "Consent";
public const string UserLogin = /*TableNamePrefix*/ "umbraco" + "UserLogin";
}
}
}
@@ -0,0 +1,59 @@
using System;
using NPoco;
using Umbraco.Core.Persistence.DatabaseAnnotations;
using Umbraco.Core.Persistence.DatabaseModelDefinitions;
namespace Umbraco.Core.Persistence.Dtos
{
[TableName(Constants.DatabaseSchema.Tables.AuditEntry)]
[PrimaryKey("id")]
[ExplicitColumns]
internal class AuditEntryDto
{
public const int IpLength = 64;
public const int EventTypeLength = 256;
public const int DetailsLength = 1024;
[Column("id")]
[PrimaryKeyColumn]
public int Id { get; set; }
// there is NO foreign key to the users table here, neither for performing user nor for
// affected user, so we can delete users and NOT delete the associated audit trails, and
// users can still be identified via the details free-form text fields.
[Column("performingUserId")]
public int PerformingUserId { get; set; }
[Column("performingDetails")]
[NullSetting(NullSetting = NullSettings.Null)]
[Length(DetailsLength)]
public string PerformingDetails { get; set; }
[Column("performingIp")]
[NullSetting(NullSetting = NullSettings.Null)]
[Length(IpLength)]
public string PerformingIp { get; set; }
[Column("eventDateUtc")]
[Constraint(Default = SystemMethods.CurrentDateTime)]
public DateTime EventDateUtc { get; set; }
[Column("affectedUserId")]
public int AffectedUserId { get; set; }
[Column("affectedDetails")]
[NullSetting(NullSetting = NullSettings.Null)]
[Length(DetailsLength)]
public string AffectedDetails { get; set; }
[Column("eventType")]
[Length(EventTypeLength)]
public string EventType { get; set; }
[Column("eventDetails")]
[NullSetting(NullSetting = NullSettings.Null)]
[Length(DetailsLength)]
public string EventDetails { get; set; }
}
}
@@ -27,5 +27,10 @@ namespace Umbraco.Core.Persistence.Dtos
[NullSetting(NullSetting = NullSettings.NotNull)]
[Length(500)]
public string OriginIdentity { get; set; }
[Column("instructionCount")]
[NullSetting(NullSetting = NullSettings.NotNull)]
[Constraint(Default = 1)]
public int InstructionCount { get; set; }
}
}
@@ -0,0 +1,42 @@
using System;
using NPoco;
using Umbraco.Core.Persistence.DatabaseAnnotations;
using Umbraco.Core.Persistence.DatabaseModelDefinitions;
namespace Umbraco.Core.Persistence.Dtos
{
[TableName(Constants.DatabaseSchema.Tables.Consent)]
[PrimaryKey("id")]
[ExplicitColumns]
public class ConsentDto
{
[Column("id")]
[PrimaryKeyColumn]
public int Id { get; set; }
[Column("current")]
public bool Current { get; set; }
[Column("source")]
[Length(512)]
public string Source { get; set; }
[Column("context")]
[Length(128)]
public string Context { get; set; }
[Column("action")]
[Length(512)]
public string Action { get; set; }
[Column("createDate")]
[Constraint(Default = SystemMethods.CurrentDateTime)]
public DateTime CreateDate { get; set; }
[Column("state")]
public int State { get; set; }
[Column("comment")]
public string Comment { get; set; }
}
}
@@ -27,5 +27,9 @@ namespace Umbraco.Core.Persistence.Dtos
[Column("viewOnProfile")]
[Constraint(Default = "0")]
public bool ViewOnProfile { get; set; }
[Column("isSensitive")]
[Constraint(Default = "0")]
public bool IsSensitive { get; set; }
}
}
@@ -27,6 +27,7 @@ namespace Umbraco.Core.Persistence.Dtos
[ForeignKey(typeof(PropertyTypeGroupDto))]
public int? PropertyTypeGroupId { get; set; }
[Index(IndexTypes.NonClustered, Name = "IX_cmsPropertyTypeAlias")]
[Column("Alias")]
public string Alias { get; set; }
@@ -45,6 +45,9 @@ namespace Umbraco.Core.Persistence.Dtos
[Column("viewOnProfile")]
public bool ViewOnProfile { get; set; }
[Column("isSensitive")]
public bool IsSensitive { get; set; }
/* DataType */
[Column("propertyEditorAlias")]
public string PropertyEditorAlias { get; set; }
@@ -104,6 +104,14 @@ namespace Umbraco.Core.Persistence.Dtos
[Length(500)]
public string Avatar { get; set; }
/// <summary>
/// A Json blob stored for recording tour data for a user
/// </summary>
[Column("tourData")]
[NullSetting(NullSetting = NullSettings.Null)]
[SpecialDbType(SpecialDbTypes.NTEXT)]
public string TourData { get; set; }
[ResultColumn]
[Reference(ReferenceType.Many, ReferenceMemberName = "UserId")]
public List<UserGroupDto> UserGroupDtos { get; set; }
@@ -17,7 +17,7 @@ namespace Umbraco.Core.Persistence.Dtos
}
[Column("id")]
[PrimaryKeyColumn(IdentitySeed = 5)]
[PrimaryKeyColumn(IdentitySeed = 6)]
public int Id { get; set; }
[Column("userGroupAlias")]
@@ -0,0 +1,52 @@
using System;
using NPoco;
using Umbraco.Core.Persistence.DatabaseAnnotations;
namespace Umbraco.Core.Persistence.Dtos
{
[TableName(Constants.DatabaseSchema.Tables.UserLogin)]
[PrimaryKey("sessionId", AutoIncrement = false)]
[ExplicitColumns]
internal class UserLoginDto
{
[Column("sessionId")]
[PrimaryKeyColumn(AutoIncrement = false)]
public Guid SessionId { get; set; }
[Column("userId")]
[ForeignKey(typeof(UserDto), Name = "FK_" + Constants.DatabaseSchema.Tables.UserLogin + "_umbracoUser_id")]
public int UserId { get; set; }
/// <summary>
/// Tracks when the session is created
/// </summary>
[Column("loggedInUtc")]
[NullSetting(NullSetting = NullSettings.NotNull)]
public DateTime LoggedInUtc { get; set; }
/// <summary>
/// Updated every time a user's session is validated
/// </summary>
/// <remarks>
/// This allows us to guess if a session is timed out if a user doesn't actively log out
/// and also allows us to trim the data in the table
/// </remarks>
[Column("lastValidatedUtc")]
[NullSetting(NullSetting = NullSettings.NotNull)]
public DateTime LastValidatedUtc { get; set; }
/// <summary>
/// Tracks when the session is removed when the user's account is logged out
/// </summary>
[Column("loggedOutUtc")]
[NullSetting(NullSetting = NullSettings.Null)]
public DateTime? LoggedOutUtc { get; set; }
/// <summary>
/// Logs the IP address of the session if available
/// </summary>
[Column("ipAddress")]
[NullSetting(NullSetting = NullSettings.Null)]
public string IpAddress { get; set; }
}
}
@@ -33,5 +33,8 @@ using System.Runtime.InteropServices;
[assembly: InternalsVisibleTo("Umbraco.Forms.Core.Providers")]
[assembly: InternalsVisibleTo("Umbraco.Forms.Web")]
// Umbraco Headless
[assembly: InternalsVisibleTo("Umbraco.Headless")]
// v8
[assembly: InternalsVisibleTo("Umbraco.Compat7")]
+14 -1
View File
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Web;
using Semver;
@@ -18,6 +19,7 @@ namespace Umbraco.Core
private readonly ILogger _logger;
private readonly Lazy<IServerRegistrar> _serverRegistrar;
private readonly Lazy<MainDom> _mainDom;
private readonly HashSet<string> _applicationUrls = new HashSet<string>();
private RuntimeLevel _level;
/// <summary>
@@ -99,7 +101,18 @@ namespace Umbraco.Core
/// <param name="settings"></param>
internal void EnsureApplicationUrl(HttpRequestBase request = null, IUmbracoSettingsSection settings = null)
{
if (ApplicationUrl != null) return;
// see U4-10626 - in some cases we want to reset the application url
// (this is a simplified version of what was in 7.x)
// note: should this be optional? is it expensive?
var url = request == null ? null : ApplicationUrlHelper.GetApplicationUrlFromCurrentRequest(request);
var change = url != null && !_applicationUrls.Contains(url);
if (change)
{
_logger.Info(typeof(ApplicationUrlHelper), $"New url \"{url}\" detected, re-discovering application url.");
_applicationUrls.Add(url);
}
if (ApplicationUrl != null && !change) return;
ApplicationUrl = new Uri(ApplicationUrlHelper.GetApplicationUrl(_logger, request, settings));
}
@@ -11,6 +11,7 @@ using System.Security.Principal;
using System.Threading;
using System.Web;
using System.Web.Security;
using Microsoft.AspNet.Identity;
using AutoMapper;
using Microsoft.Owin;
using Newtonsoft.Json;
@@ -18,6 +19,7 @@ using Umbraco.Core.Configuration;
using Umbraco.Core.Composing;
using Umbraco.Core.Models.Membership;
using Umbraco.Core.Logging;
using IUser = Umbraco.Core.Models.Membership.IUser;
namespace Umbraco.Core.Security
{
@@ -224,8 +226,11 @@ namespace Umbraco.Core.Security
/// <param name="userdata"></param>
public static FormsAuthenticationTicket CreateUmbracoAuthTicket(this HttpContextBase http, UserData userdata)
{
//ONLY used by BasePage.doLogin!
if (http == null) throw new ArgumentNullException("http");
if (userdata == null) throw new ArgumentNullException("userdata");
if (userdata == null) throw new ArgumentNullException("userdata");
var userDataString = JsonConvert.SerializeObject(userdata);
return CreateAuthTicketAndCookie(
http,
@@ -237,14 +242,7 @@ namespace Umbraco.Core.Security
1440,
UmbracoConfig.For.UmbracoSettings().Security.AuthCookieName,
UmbracoConfig.For.UmbracoSettings().Security.AuthCookieDomain);
}
internal static FormsAuthenticationTicket CreateUmbracoAuthTicket(this HttpContext http, UserData userdata)
{
if (http == null) throw new ArgumentNullException("http");
if (userdata == null) throw new ArgumentNullException("userdata");
return new HttpContextWrapper(http).CreateUmbracoAuthTicket(userdata);
}
}
/// <summary>
/// returns the number of seconds the user has until their auth session times out
@@ -314,7 +312,23 @@ namespace Umbraco.Core.Security
/// <param name="http"></param>
/// <param name="cookieName"></param>
private static void Logout(this HttpContextBase http, string cookieName)
{
{
//We need to clear the sessionId from the database. This is legacy code to do any logging out and shouldn't really be used at all but in any case
//we need to make sure the session is cleared. Due to the legacy nature of this it means we need to use singletons
if (http.User != null)
{
var claimsIdentity = http.User.Identity as ClaimsIdentity;
if (claimsIdentity != null)
{
var sessionId = claimsIdentity.FindFirstValue(Constants.Security.SessionIdClaimType);
Guid guidSession;
if (sessionId.IsNullOrWhiteSpace() == false && Guid.TryParse(sessionId, out guidSession))
{
ApplicationContext.Current.Services.UserService.ClearLoginSession(guidSession);
}
}
}
if (http == null) throw new ArgumentNullException("http");
//clear the preview cookie and external login
var cookies = new[] { cookieName, Constants.Web.PreviewCookieName, Constants.Security.BackOfficeExternalCookieName };
@@ -2,16 +2,29 @@
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using System.Web;
using Microsoft.AspNet.Identity;
using Umbraco.Core.Models.Identity;
using Umbraco.Core.Services;
namespace Umbraco.Core.Security
{
public class BackOfficeClaimsIdentityFactory<T> : ClaimsIdentityFactory<T, int>
where T: BackOfficeIdentityUser
{
private readonly ApplicationContext _appCtx;
[Obsolete("Use the overload specifying all dependencies instead")]
public BackOfficeClaimsIdentityFactory()
:this(ApplicationContext.Current)
{
}
public BackOfficeClaimsIdentityFactory(ApplicationContext appCtx)
{
if (appCtx == null) throw new ArgumentNullException("appCtx");
_appCtx = appCtx;
SecurityStampClaimType = Constants.Security.SessionIdClaimType;
UserNameClaimType = ClaimTypes.Name;
}
@@ -24,9 +37,9 @@ namespace Umbraco.Core.Security
public override async Task<ClaimsIdentity> CreateAsync(UserManager<T, int> manager, T user, string authenticationType)
{
var baseIdentity = await base.CreateAsync(manager, user, authenticationType);
var umbracoIdentity = new UmbracoBackOfficeIdentity(baseIdentity,
//set a new session id
//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
new UserData
{
Id = user.Id,
@@ -37,15 +50,22 @@ namespace Umbraco.Core.Security
Roles = user.Roles.Select(x => x.RoleId).ToArray(),
StartContentNodes = user.CalculatedContentStartNodeIds,
StartMediaNodes = user.CalculatedMediaStartNodeIds,
SessionId = user.SecurityStamp
SecurityStamp = user.SecurityStamp
});
return umbracoIdentity;
}
}
}
public class BackOfficeClaimsIdentityFactory : BackOfficeClaimsIdentityFactory<BackOfficeIdentityUser>
{
[Obsolete("Use the overload specifying all dependencies instead")]
public BackOfficeClaimsIdentityFactory()
{
}
public BackOfficeClaimsIdentityFactory(ApplicationContext appCtx) : base(appCtx)
{
}
}
}
@@ -1,21 +1,83 @@
using System;
using System.Collections.Concurrent;
using System.ComponentModel;
using System.Globalization;
using System.Net.Http.Headers;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNet.Identity;
using Microsoft.Owin;
using Microsoft.Owin.Security.Cookies;
using Semver;
using Umbraco.Core.Configuration;
using Umbraco.Core.Models.Identity;
namespace Umbraco.Core.Security
{
public class BackOfficeCookieAuthenticationProvider : CookieAuthenticationProvider
{
private readonly ApplicationContext _appCtx;
[Obsolete("Use the ctor specifying all dependencies")]
[EditorBrowsable(EditorBrowsableState.Never)]
public BackOfficeCookieAuthenticationProvider()
: this(ApplicationContext.Current)
{
}
public BackOfficeCookieAuthenticationProvider(ApplicationContext appCtx)
{
if (appCtx == null) throw new ArgumentNullException("appCtx");
_appCtx = appCtx;
}
private static readonly SemVersion MinUmbracoVersionSupportingLoginSessions = new SemVersion(7, 8);
public override void ResponseSignIn(CookieResponseSignInContext context)
{
var backOfficeIdentity = context.Identity as UmbracoBackOfficeIdentity;
if (backOfficeIdentity != null)
{
//generate a session id and assign it
//create a session token - if we are configured and not in an upgrade state then use the db, otherwise just generate one
//NOTE - special check because when we are upgrading to 7.8 we cannot create a session since the db isn't ready and we'll get exceptions
var canAcquireSession = _appCtx.IsUpgrading == false || _appCtx.CurrentVersion() >= MinUmbracoVersionSupportingLoginSessions;
var session = canAcquireSession
? _appCtx.Services.UserService.CreateLoginSession((int)backOfficeIdentity.Id, context.OwinContext.GetCurrentRequestIpAddress())
: Guid.NewGuid();
backOfficeIdentity.UserData.SessionId = session.ToString();
}
base.ResponseSignIn(context);
}
public override void ResponseSignOut(CookieResponseSignOutContext context)
{
//Clear the user's session on sign out
if (context != null && context.OwinContext != null && context.OwinContext.Authentication != null
&& context.OwinContext.Authentication.User != null && context.OwinContext.Authentication.User.Identity != null)
{
var claimsIdentity = context.OwinContext.Authentication.User.Identity as ClaimsIdentity;
var sessionId = claimsIdentity.FindFirstValue(Constants.Security.SessionIdClaimType);
Guid guidSession;
if (sessionId.IsNullOrWhiteSpace() == false && Guid.TryParse(sessionId, out guidSession))
{
_appCtx.Services.UserService.ClearLoginSession(guidSession);
}
}
base.ResponseSignOut(context);
//Make sure the definitely all of these cookies are cleared when signing out with cookies
context.Response.Cookies.Append(SessionIdValidator.CookieName, "", new CookieOptions
{
Expires = DateTime.Now.AddYears(-1),
Path = "/"
});
context.Response.Cookies.Append(UmbracoConfig.For.UmbracoSettings().Security.AuthCookieName, "", new CookieOptions
{
Expires = DateTime.Now.AddYears(-1),
@@ -34,21 +96,45 @@ namespace Umbraco.Core.Security
}
/// <summary>
/// Ensures that the culture is set correctly for the current back office user
/// Ensures that the culture is set correctly for the current back office user and that the user's session token is valid
/// </summary>
/// <param name="context"/>
/// <returns/>
public override Task ValidateIdentity(CookieValidateIdentityContext context)
public override async Task ValidateIdentity(CookieValidateIdentityContext context)
{
EnsureCulture(context);
await EnsureValidSessionId(context);
await base.ValidateIdentity(context);
}
/// <summary>
/// Ensures that the user has a valid session id
/// </summary>
/// <remarks>
/// So that we are not overloading the database this throttles it's check to every minute
/// </remarks>
protected virtual async Task EnsureValidSessionId(CookieValidateIdentityContext context)
{
if (_appCtx.IsConfigured && _appCtx.IsUpgrading == false)
await SessionIdValidator.ValidateSessionAsync(TimeSpan.FromMinutes(1), context);
}
private void EnsureCulture(CookieValidateIdentityContext context)
{
var umbIdentity = context.Identity as UmbracoBackOfficeIdentity;
if (umbIdentity != null && umbIdentity.IsAuthenticated)
{
Thread.CurrentThread.CurrentCulture =
Thread.CurrentThread.CurrentUICulture =
new CultureInfo(umbIdentity.Culture);
UserCultures.GetOrAdd(umbIdentity.Culture, s => new CultureInfo(s));
}
return base.ValidateIdentity(context);
}
/// <summary>
/// Used so that we aren't creating a new CultureInfo object for every single request
/// </summary>
private static readonly ConcurrentDictionary<string, CultureInfo> UserCultures = new ConcurrentDictionary<string, CultureInfo>();
}
}
@@ -12,6 +12,7 @@ using Umbraco.Core.Models.Identity;
namespace Umbraco.Core.Security
{
//TODO: In v8 we need to change this to use an int? nullable TKey instead, see notes against overridden TwoFactorSignInAsync
public class BackOfficeSignInManager : SignInManager<BackOfficeIdentityUser, int>
{
private readonly ILogger _logger;
@@ -222,6 +223,9 @@ namespace Umbraco.Core.Security
user.AccessFailedCount = 0;
await UserManager.UpdateAsync(user);
//set the current request's principal to the identity just signed in!
_request.User = new ClaimsPrincipal(userIdentity);
_logger.WriteCore(TraceEventType.Information, 0,
string.Format(
"Login attempt succeeded for username {0} from IP address {1}",
@@ -259,5 +263,67 @@ namespace Umbraco.Core.Security
}
return null;
}
/// <summary>
/// Two factor verification step
/// </summary>
/// <param name="provider"></param>
/// <param name="code"></param>
/// <param name="isPersistent"></param>
/// <param name="rememberBrowser"></param>
/// <returns></returns>
/// <remarks>
/// This is implemented because we cannot override GetVerifiedUserIdAsync and instead we have to shadow it
/// so due to this and because we are using an INT as the TKey and not an object, it can never be null. Adding to that
/// the default(int) value returned by the base class is always a valid user (i.e. the admin) so we just have to duplicate
/// all of this code to check for -1 instead.
/// </remarks>
public override async Task<SignInStatus> TwoFactorSignInAsync(string provider, string code, bool isPersistent, bool rememberBrowser)
{
var userId = await GetVerifiedUserIdAsync();
if (userId == -1)
{
return SignInStatus.Failure;
}
var user = await UserManager.FindByIdAsync(userId);
if (user == null)
{
return SignInStatus.Failure;
}
if (await UserManager.IsLockedOutAsync(user.Id))
{
return SignInStatus.LockedOut;
}
if (await UserManager.VerifyTwoFactorTokenAsync(user.Id, provider, code))
{
// When token is verified correctly, clear the access failed count used for lockout
await UserManager.ResetAccessFailedCountAsync(user.Id);
await SignInAsync(user, isPersistent, rememberBrowser);
return SignInStatus.Success;
}
// If the token is incorrect, record the failure which also may cause the user to be locked out
await UserManager.AccessFailedAsync(user.Id);
return SignInStatus.Failure;
}
/// <summary>Send a two factor code to a user</summary>
/// <param name="provider"></param>
/// <returns></returns>
/// <remarks>
/// This is implemented because we cannot override GetVerifiedUserIdAsync and instead we have to shadow it
/// so due to this and because we are using an INT as the TKey and not an object, it can never be null. Adding to that
/// the default(int) value returned by the base class is always a valid user (i.e. the admin) so we just have to duplicate
/// all of this code to check for -1 instead.
/// </remarks>
public override async Task<bool> SendTwoFactorCodeAsync(string provider)
{
var userId = await GetVerifiedUserIdAsync();
if (userId == -1)
return false;
var token = await UserManager.GenerateTwoFactorTokenAsync(userId, provider);
var identityResult = await UserManager.NotifyTwoFactorTokenAsync(userId, provider, token);
return identityResult.Succeeded;
}
}
}
@@ -157,7 +157,7 @@ namespace Umbraco.Core.Security
#region What we support do not currently
//NOTE: Not sure if we really want/need to ever support this
//TODO: We could support this - but a user claims will mostly just be what is in the auth cookie
public override bool SupportsUserClaim
{
get { return false; }
@@ -259,6 +259,22 @@ namespace Umbraco.Core.Security
//manager.SmsService = new SmsService();
}
/// <summary>
/// Used to validate a user's session
/// </summary>
/// <param name="userId"></param>
/// <param name="sessionId"></param>
/// <returns></returns>
public virtual async Task<bool> ValidateSessionIdAsync(int userId, string sessionId)
{
var userSessionStore = Store as IUserSessionStore<BackOfficeIdentityUser, int>;
//if this is not set, for backwards compat (which would be super rare), we'll just approve it
if (userSessionStore == null)
return true;
return await userSessionStore.ValidateSessionIdAsync(userId, sessionId);
}
/// <summary>
/// This will determine which password hasher to use based on what is defined in config
/// </summary>
@@ -393,6 +409,33 @@ namespace Umbraco.Core.Security
return await base.CheckPasswordAsync(user, password);
}
public override Task<IdentityResult> ResetPasswordAsync(int userId, string token, string newPassword)
{
var result = base.ResetPasswordAsync(userId, token, newPassword);
if (result.Result.Succeeded)
RaisePasswordResetEvent(userId);
return result;
}
/// <summary>
/// This is a special method that will reset the password but will raise the Password Changed event instead of the reset event
/// </summary>
/// <param name="userId"></param>
/// <param name="token"></param>
/// <param name="newPassword"></param>
/// <returns></returns>
/// <remarks>
/// We use this because in the back office the only way an admin can change another user's password without first knowing their password
/// is to generate a token and reset it, however, when we do this we want to track a password change, not a password reset
/// </remarks>
public Task<IdentityResult> ChangePasswordWithResetAsync(int userId, string token, string newPassword)
{
var result = base.ResetPasswordAsync(userId, token, newPassword);
if (result.Result.Succeeded)
RaisePasswordChangedEvent(userId);
return result;
}
public override Task<IdentityResult> ChangePasswordAsync(int userId, string currentPassword, string newPassword)
{
var result = base.ChangePasswordAsync(userId, currentPassword, newPassword);
@@ -527,27 +570,27 @@ namespace Umbraco.Core.Security
internal void RaiseAccountLockedEvent(int userId)
{
OnAccountLocked(new IdentityAuditEventArgs(AuditEvent.AccountLocked, GetCurrentRequestIpAddress(), userId));
OnAccountLocked(new IdentityAuditEventArgs(AuditEvent.AccountLocked, GetCurrentRequestIpAddress(), affectedUser: userId));
}
internal void RaiseAccountUnlockedEvent(int userId)
{
OnAccountUnlocked(new IdentityAuditEventArgs(AuditEvent.AccountUnlocked, GetCurrentRequestIpAddress(), userId));
OnAccountUnlocked(new IdentityAuditEventArgs(AuditEvent.AccountUnlocked, GetCurrentRequestIpAddress(), affectedUser: userId));
}
internal void RaiseForgotPasswordRequestedEvent(int userId)
{
OnForgotPasswordRequested(new IdentityAuditEventArgs(AuditEvent.ForgotPasswordRequested, GetCurrentRequestIpAddress(), userId));
OnForgotPasswordRequested(new IdentityAuditEventArgs(AuditEvent.ForgotPasswordRequested, GetCurrentRequestIpAddress(), affectedUser: userId));
}
internal void RaiseForgotPasswordChangedSuccessEvent(int userId)
{
OnForgotPasswordChangedSuccess(new IdentityAuditEventArgs(AuditEvent.ForgotPasswordChangedSuccess, GetCurrentRequestIpAddress(), userId));
OnForgotPasswordChangedSuccess(new IdentityAuditEventArgs(AuditEvent.ForgotPasswordChangedSuccess, GetCurrentRequestIpAddress(), affectedUser: userId));
}
internal void RaiseLoginFailedEvent(int userId)
{
OnLoginFailed(new IdentityAuditEventArgs(AuditEvent.LoginFailed, GetCurrentRequestIpAddress(), userId));
OnLoginFailed(new IdentityAuditEventArgs(AuditEvent.LoginFailed, GetCurrentRequestIpAddress(), affectedUser: userId));
}
internal void RaiseInvalidLoginAttemptEvent(string username)
@@ -557,31 +600,33 @@ namespace Umbraco.Core.Security
internal void RaiseLoginRequiresVerificationEvent(int userId)
{
OnLoginRequiresVerification(new IdentityAuditEventArgs(AuditEvent.LoginRequiresVerification, GetCurrentRequestIpAddress(), userId));
OnLoginRequiresVerification(new IdentityAuditEventArgs(AuditEvent.LoginRequiresVerification, GetCurrentRequestIpAddress(), affectedUser: userId));
}
internal void RaiseLoginSuccessEvent(int userId)
{
OnLoginSuccess(new IdentityAuditEventArgs(AuditEvent.LoginSucces, GetCurrentRequestIpAddress(), userId));
OnLoginSuccess(new IdentityAuditEventArgs(AuditEvent.LoginSucces, GetCurrentRequestIpAddress(), affectedUser: userId));
}
internal void RaiseLogoutSuccessEvent(int userId)
{
OnLogoutSuccess(new IdentityAuditEventArgs(AuditEvent.LogoutSuccess, GetCurrentRequestIpAddress(), userId));
OnLogoutSuccess(new IdentityAuditEventArgs(AuditEvent.LogoutSuccess, GetCurrentRequestIpAddress(), affectedUser: userId));
}
internal void RaisePasswordChangedEvent(int userId)
{
OnPasswordChanged(new IdentityAuditEventArgs(AuditEvent.PasswordChanged, GetCurrentRequestIpAddress(), userId));
OnPasswordChanged(new IdentityAuditEventArgs(AuditEvent.PasswordChanged, GetCurrentRequestIpAddress(), affectedUser: userId));
}
//TODO: I don't think this is required anymore since from 7.7 we no longer display the reset password checkbox since that didn't make sense.
internal void RaisePasswordResetEvent(int userId)
{
OnPasswordReset(new IdentityAuditEventArgs(AuditEvent.PasswordReset, GetCurrentRequestIpAddress(), userId));
OnPasswordReset(new IdentityAuditEventArgs(AuditEvent.PasswordReset, GetCurrentRequestIpAddress(), affectedUser: userId));
}
internal void RaiseResetAccessFailedCountEvent(int userId)
{
OnResetAccessFailedCount(new IdentityAuditEventArgs(AuditEvent.ResetAccessFailedCount, GetCurrentRequestIpAddress(), userId));
OnResetAccessFailedCount(new IdentityAuditEventArgs(AuditEvent.ResetAccessFailedCount, GetCurrentRequestIpAddress(), affectedUser: userId));
}
public static event EventHandler AccountLocked;
@@ -662,4 +707,5 @@ namespace Umbraco.Core.Security
return httpContext.GetCurrentRequestIpAddress();
}
}
}
@@ -18,7 +18,7 @@ using Task = System.Threading.Tasks.Task;
namespace Umbraco.Core.Security
{
public class BackOfficeUserStore : DisposableObject,
public class BackOfficeUserStore : DisposableObjectSlim,
IUserStore<BackOfficeIdentityUser, int>,
IUserPasswordStore<BackOfficeIdentityUser, int>,
IUserEmailStore<BackOfficeIdentityUser, int>,
@@ -26,12 +26,13 @@ namespace Umbraco.Core.Security
IUserRoleStore<BackOfficeIdentityUser, int>,
IUserSecurityStampStore<BackOfficeIdentityUser, int>,
IUserLockoutStore<BackOfficeIdentityUser, int>,
IUserTwoFactorStore<BackOfficeIdentityUser, int>
IUserTwoFactorStore<BackOfficeIdentityUser, int>,
IUserSessionStore<BackOfficeIdentityUser, int>
//TODO: This would require additional columns/tables for now people will need to implement this on their own
//IUserPhoneNumberStore<BackOfficeIdentityUser, int>,
//TODO: To do this we need to implement IQueryable - we'll have an IQuerable implementation soon with the UmbracoLinqPadDriver implementation
//IQueryableUserStore<BackOfficeIdentityUser, int>
//TODO: This would require additional columns/tables for now people will need to implement this on their own
//IUserPhoneNumberStore<BackOfficeIdentityUser, int>,
//TODO: To do this we need to implement IQueryable - we'll have an IQuerable implementation soon with the UmbracoLinqPadDriver implementation
//IQueryableUserStore<BackOfficeIdentityUser, int>
{
private readonly IUserService _userService;
private readonly IMemberTypeService _memberTypeService;
@@ -59,7 +60,7 @@ namespace Umbraco.Core.Security
}
/// <summary>
/// Handles the disposal of resources. Derived from abstract class <see cref="DisposableObject"/> which handles common required locking logic.
/// Handles the disposal of resources. Derived from abstract class <see cref="DisposableObjectSlim"/> which handles common required locking logic.
/// </summary>
protected override void DisposeResources()
{
@@ -126,12 +127,15 @@ namespace Umbraco.Core.Security
var found = _userService.GetUserById(asInt.Result);
if (found != null)
{
// we have to remember whether Logins property is dirty, since the UpdateMemberProperties will reset it.
var isLoginsPropertyDirty = user.IsPropertyDirty("Logins");
if (UpdateMemberProperties(found, user))
{
_userService.Save(found);
}
if (user.IsPropertyDirty("Logins"))
if (isLoginsPropertyDirty)
{
var logins = await GetLoginsAsync(user);
_externalLoginService.SaveUserLogins(found.Id, logins);
@@ -752,5 +756,15 @@ namespace Umbraco.Core.Security
if (_disposed)
throw new ObjectDisposedException(GetType().Name);
}
public Task<bool> ValidateSessionIdAsync(int userId, string sessionId)
{
Guid guidSessionId;
if (Guid.TryParse(sessionId, out guidSessionId))
{
return Task.FromResult(_userService.ValidateLoginSession(userId, guidSessionId));
}
return Task.FromResult(false);
}
}
}
@@ -0,0 +1,17 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNet.Identity;
namespace Umbraco.Core.Security
{
/// <summary>
/// An IUserStore interface part to implement if the store supports validating user session Ids
/// </summary>
/// <typeparam name="TUser"></typeparam>
/// <typeparam name="TKey"></typeparam>
public interface IUserSessionStore<TUser, in TKey> : IUserStore<TUser, TKey>, IDisposable
where TUser : class, IUser<TKey>
{
Task<bool> ValidateSessionIdAsync(int userId, string sessionId);
}
}
@@ -1,5 +1,6 @@
using System;
using System.Collections.Specialized;
using System.ComponentModel.DataAnnotations;
using System.Configuration.Provider;
using System.Security.Cryptography;
using System.Text;
@@ -9,6 +10,7 @@ using System.Web.Configuration;
using System.Web.Hosting;
using System.Web.Security;
using Umbraco.Core.Composing;
using Umbraco.Core.Configuration;
using Umbraco.Core.Logging;
using Umbraco.Core.Models;
@@ -677,11 +679,7 @@ namespace Umbraco.Core.Security
internal static bool IsEmailValid(string email)
{
const string pattern = @"^(?!\.)(""([^""\r\\]|\\[""\r\\])*""|"
+ @"([-a-z0-9!#$%&'*+/=?^_`{|}~]|(?<!\.)\.)*)(?<!\.)"
+ @"@[a-z0-9][\w\.-]*[a-z0-9]\.[a-z][a-z\.]*[a-z]$";
return Regex.IsMatch(email, pattern, RegexOptions.IgnoreCase | RegexOptions.Compiled);
return new EmailAddressAttribute().IsValid(email);
}
protected internal string EncryptOrHashPassword(string pass, string salt)
@@ -1,4 +1,5 @@
using System;
using System.Web;
using Microsoft.AspNet.Identity.Owin;
using Microsoft.Owin;
using Umbraco.Core.Models.Identity;
@@ -7,6 +8,37 @@ namespace Umbraco.Core.Security
{
public static class OwinExtensions
{
public static string GetCurrentRequestIpAddress(this IOwinContext owinContext)
{
if (owinContext == null)
{
return "Unknown, owinContext is null";
}
if (owinContext.Request == null)
{
return "Unknown, owinContext.Request is null";
}
var httpContext = owinContext.TryGetHttpContext();
if (httpContext == false)
{
return "Unknown, cannot resolve HttpContext from owinContext";
}
return httpContext.Result.GetCurrentRequestIpAddress();
}
/// <summary>
/// Nasty little hack to get httpcontextbase from an owin context
/// </summary>
/// <param name="owinContext"></param>
/// <returns></returns>
internal static Attempt<HttpContextBase> TryGetHttpContext(this IOwinContext owinContext)
{
var ctx = owinContext.Get<HttpContextBase>(typeof(HttpContextBase).FullName);
return ctx == null ? Attempt<HttpContextBase>.Fail() : Attempt.Succeed(ctx);
}
/// <summary>
/// Gets the back office sign in manager out of OWIN
/// </summary>
@@ -0,0 +1,118 @@
using System;
using System.Collections.Concurrent;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.Owin;
using Microsoft.Owin;
using Microsoft.Owin.Infrastructure;
using Microsoft.Owin.Security.Cookies;
using Umbraco.Core.Configuration;
using Umbraco.Core.Models.Identity;
namespace Umbraco.Core.Security
{
/// <summary>
/// Static helper class used to configure a CookieAuthenticationProvider to validate a cookie against a user's session id
/// </summary>
/// <remarks>
/// This uses another cookie to track the last checked time which is done for a few reasons:
/// * We can't use the user's auth ticket to do thsi because we'd be re-issuing the auth ticket all of the time and it would never expire
/// plus the auth ticket size is much larger than this small value
/// * This will execute quite often (every minute per user) and in some cases there might be several requests that end up re-issuing the cookie so the cookie value should be small
/// * We want to avoid the user lookup if it's not required so that will only happen when the time diff is great enough in the cookie
/// </remarks>
internal static class SessionIdValidator
{
public const string CookieName = "UMB_UCONTEXT_C";
public static async Task ValidateSessionAsync(TimeSpan validateInterval, CookieValidateIdentityContext context)
{
if (context.Request.Uri.IsBackOfficeRequest(HttpRuntime.AppDomainAppVirtualPath) == false)
return;
var valid = await ValidateSessionAsync(validateInterval, context.OwinContext, context.Options.CookieManager, context.Options.SystemClock, context.Properties.IssuedUtc, context.Identity);
if (valid == false)
{
context.RejectIdentity();
context.OwinContext.Authentication.SignOut(context.Options.AuthenticationType);
}
}
public static async Task<bool> ValidateSessionAsync(
TimeSpan validateInterval,
IOwinContext owinCtx,
ICookieManager cookieManager,
ISystemClock systemClock,
DateTimeOffset? authTicketIssueDate,
ClaimsIdentity currentIdentity)
{
if (owinCtx == null) throw new ArgumentNullException("owinCtx");
if (cookieManager == null) throw new ArgumentNullException("cookieManager");
if (systemClock == null) throw new ArgumentNullException("systemClock");
DateTimeOffset? issuedUtc = null;
var currentUtc = systemClock.UtcNow;
//read the last checked time from a custom cookie
var lastCheckedCookie = cookieManager.GetRequestCookie(owinCtx, CookieName);
if (lastCheckedCookie.IsNullOrWhiteSpace() == false)
{
DateTimeOffset parsed;
if (DateTimeOffset.TryParse(lastCheckedCookie, out parsed))
{
issuedUtc = parsed;
}
}
//no cookie, use the issue time of the auth ticket
if (issuedUtc.HasValue == false)
{
issuedUtc = authTicketIssueDate;
}
// Only validate if enough time has elapsed
var validate = issuedUtc.HasValue == false;
if (issuedUtc.HasValue)
{
var timeElapsed = currentUtc.Subtract(issuedUtc.Value);
validate = timeElapsed > validateInterval;
}
if (validate == false)
return true;
var manager = owinCtx.GetUserManager<BackOfficeUserManager>();
if (manager == null)
return false;
var userId = currentIdentity.GetUserId<int>();
var user = await manager.FindByIdAsync(userId);
if (user == null)
return false;
var sessionId = currentIdentity.FindFirstValue(Constants.Security.SessionIdClaimType);
if (await manager.ValidateSessionIdAsync(userId, sessionId) == false)
return false;
//we will re-issue the cookie last checked cookie
cookieManager.AppendResponseCookie(
owinCtx,
CookieName,
DateTimeOffset.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffffffzzz"),
new CookieOptions
{
HttpOnly = true,
Secure = GlobalSettings.UseSSL || owinCtx.Request.IsSecure,
Path = "/"
});
return true;
}
}
}
@@ -36,6 +36,7 @@ namespace Umbraco.Core.Security
var username = identity.GetUserName();
var session = identity.FindFirstValue(Constants.Security.SessionIdClaimType);
var securityStamp = identity.FindFirstValue(Microsoft.AspNet.Identity.Constants.DefaultSecurityStampClaimType);
var startContentId = identity.FindFirstValue(Constants.Security.StartContentNodeIdClaimType);
var startMediaId = identity.FindFirstValue(Constants.Security.StartMediaNodeIdClaimType);
@@ -66,8 +67,9 @@ namespace Umbraco.Core.Security
var roles = identity.FindAll(x => x.Type == DefaultRoleClaimType).Select(role => role.Value).ToList();
var allowedApps = identity.FindAll(x => x.Type == Constants.Security.AllowedApplicationsClaimType).Select(app => app.Value).ToList();
var userData = new UserData(session)
var userData = new UserData
{
SecurityStamp = securityStamp,
SessionId = session,
AllowedApplications = allowedApps.ToArray(),
Culture = culture,
@@ -189,7 +191,8 @@ namespace Umbraco.Core.Security
Constants.Security.StartContentNodeIdClaimType,
Constants.Security.StartMediaNodeIdClaimType,
ClaimTypes.Locality,
Constants.Security.SessionIdClaimType
Constants.Security.SessionIdClaimType,
Microsoft.AspNet.Identity.Constants.DefaultSecurityStampClaimType
};
}
}
@@ -219,16 +222,12 @@ namespace Umbraco.Core.Security
AddClaim(new Claim(ClaimTypes.Locality, Culture, ClaimValueTypes.String, Issuer, Issuer, this));
if (HasClaim(x => x.Type == Constants.Security.SessionIdClaimType) == false && SessionId.IsNullOrWhiteSpace() == false)
{
AddClaim(new Claim(Constants.Security.SessionIdClaimType, SessionId, ClaimValueTypes.String, Issuer, Issuer, this));
//The security stamp claim is also required... this is because this claim type is hard coded
// by the SecurityStampValidator, see: https://katanaproject.codeplex.com/workitem/444
if (HasClaim(x => x.Type == Microsoft.AspNet.Identity.Constants.DefaultSecurityStampClaimType) == false)
{
AddClaim(new Claim(Microsoft.AspNet.Identity.Constants.DefaultSecurityStampClaimType, SessionId, ClaimValueTypes.String, Issuer, Issuer, this));
}
}
//The security stamp claim is also required... this is because this claim type is hard coded
// by the SecurityStampValidator, see: https://katanaproject.codeplex.com/workitem/444
if (HasClaim(x => x.Type == Microsoft.AspNet.Identity.Constants.DefaultSecurityStampClaimType) == false)
AddClaim(new Claim(Microsoft.AspNet.Identity.Constants.DefaultSecurityStampClaimType, SecurityStamp, ClaimValueTypes.String, Issuer, Issuer, this));
//Add each app as a separate claim
if (HasClaim(x => x.Type == Constants.Security.AllowedApplicationsClaimType) == false)
@@ -307,6 +306,11 @@ namespace Umbraco.Core.Security
get { return UserData.SessionId; }
}
public string SecurityStamp
{
get { return UserData.SecurityStamp; }
}
public string[] Roles
{
get { return UserData.Roles; }
+8 -2
View File
@@ -20,7 +20,7 @@ namespace Umbraco.Core.Security
/// Use this constructor to create/assign new UserData to the ticket
/// </summary>
/// <param name="sessionId">
/// The security stamp for the user
/// The current sessionId for the user
/// </param>
public UserData(string sessionId)
{
@@ -30,11 +30,17 @@ namespace Umbraco.Core.Security
}
/// <summary>
/// This is the 'security stamp' for validation
/// Gets or sets the session identifier.
/// </summary>
[DataMember(Name = "sessionId")]
public string SessionId { get; set; }
/// <summary>
/// Gets or sets the security stamp.
/// </summary>
[DataMember(Name = "securityStamp")]
public string SecurityStamp { get; set; }
[DataMember(Name = "id")]
public object Id { get; set; }
@@ -100,7 +100,7 @@ namespace Umbraco.Core.Sync
return null;
}
private static string GetApplicationUrlFromCurrentRequest(HttpRequestBase request)
public static string GetApplicationUrlFromCurrentRequest(HttpRequestBase request)
{
// if (HTTP and SSL not required) or (HTTPS and SSL required),
// use ports from request
@@ -15,6 +15,7 @@ using Umbraco.Core.IO;
using Umbraco.Core.Logging;
using Umbraco.Core.Persistence;
using Umbraco.Core.Persistence.Dtos;
using Umbraco.Core.Configuration;
using Umbraco.Core.Scoping;
namespace Umbraco.Core.Sync
@@ -34,6 +35,7 @@ namespace Umbraco.Core.Sync
private readonly object _locko = new object();
private readonly ProfilingLogger _profilingLogger;
private readonly ISqlContext _sqlContext;
private readonly Lazy<string> _distCacheFilePath = new Lazy<string>(GetDistCacheFilePath);
private int _lastId = -1;
private DateTime _lastSync;
private DateTime _lastPruned;
@@ -63,6 +65,8 @@ namespace Umbraco.Core.Sync
protected IScopeProvider ScopeProvider { get; }
protected Sql<ISqlContext> Sql() => _sqlContext.Sql();
private string DistCacheFilePath => _distCacheFilePath.Value;
#region Messenger
@@ -92,7 +96,8 @@ namespace Umbraco.Core.Sync
{
UtcStamp = DateTime.UtcNow,
Instructions = JsonConvert.SerializeObject(instructions, Formatting.None),
OriginIdentity = LocalIdentity
OriginIdentity = LocalIdentity,
InstructionCount = instructions.Sum(x => x.JsonIdCount)
};
using (var scope = ScopeProvider.CreateScope())
@@ -182,10 +187,9 @@ namespace Umbraco.Core.Sync
}
else
{
//check for how many instructions there are to process
//TODO: In 7.6 we need to store the count of instructions per row since this is not affective because there can be far more than one (if not thousands)
// of instructions in a single row.
var count = database.ExecuteScalar<int>("SELECT COUNT(*) FROM umbracoCacheInstruction WHERE id > @lastId", new {lastId = _lastId});
//check for how many instructions there are to process, each row contains a count of the number of instructions contained in each
//row so we will sum these numbers to get the actual count.
var count = database.ExecuteScalar<int>("SELECT SUM(instructionCount) FROM umbracoCacheInstruction WHERE id > @lastId", new {lastId = _lastId});
if (count > Options.MaxProcessingInstructionCount)
{
//too many instructions, proceed to cold boot
@@ -222,7 +226,7 @@ namespace Umbraco.Core.Sync
/// <summary>
/// Synchronize the server (throttled).
/// </summary>
protected void Sync()
protected internal void Sync()
{
lock (_locko)
{
@@ -484,11 +488,10 @@ namespace Umbraco.Core.Sync
/// </remarks>
private void ReadLastSynced()
{
var path = SyncFilePath;
if (File.Exists(path) == false) return;
if (File.Exists(DistCacheFilePath) == false) return;
var content = File.ReadAllText(path);
if (int.TryParse(content, out int last))
var content = File.ReadAllText(DistCacheFilePath);
if (int.TryParse(content, out var last))
_lastId = last;
}
@@ -501,7 +504,7 @@ namespace Umbraco.Core.Sync
/// </remarks>
private void SaveLastSynced(int id)
{
File.WriteAllText(SyncFilePath, id.ToString(CultureInfo.InvariantCulture));
File.WriteAllText(DistCacheFilePath, id.ToString(CultureInfo.InvariantCulture));
_lastId = id;
}
@@ -521,20 +524,40 @@ namespace Umbraco.Core.Sync
+ "/D" + AppDomain.CurrentDomain.Id // eg 22
+ "] " + Guid.NewGuid().ToString("N").ToUpper(); // make it truly unique
/// <summary>
/// Gets the sync file path for the local server.
/// </summary>
/// <returns>The sync file path for the local server.</returns>
private static string SyncFilePath
private static string GetDistCacheFilePath()
{
get
{
var tempFolder = IOHelper.MapPath("~/App_Data/TEMP/DistCache/" + NetworkHelper.FileSafeMachineName);
if (Directory.Exists(tempFolder) == false)
Directory.CreateDirectory(tempFolder);
var fileName = HttpRuntime.AppDomainAppId.ReplaceNonAlphanumericChars(string.Empty) + "-lastsynced.txt";
return Path.Combine(tempFolder, HttpRuntime.AppDomainAppId.ReplaceNonAlphanumericChars(string.Empty) + "-lastsynced.txt");
string distCacheFilePath;
switch (GlobalSettings.LocalTempStorageLocation)
{
case LocalTempStorage.AspNetTemp:
distCacheFilePath = Path.Combine(HttpRuntime.CodegenDir, @"UmbracoData", fileName);
break;
case LocalTempStorage.EnvironmentTemp:
var appDomainHash = HttpRuntime.AppDomainAppId.ToSHA1();
var cachePath = Path.Combine(Environment.ExpandEnvironmentVariables("%temp%"), "UmbracoData",
//include the appdomain hash is just a safety check, for example if a website is moved from worker A to worker B and then back
// to worker A again, in theory the %temp% folder should already be empty but we really want to make sure that its not
// utilizing an old path
appDomainHash);
distCacheFilePath = Path.Combine(cachePath, fileName);
break;
case LocalTempStorage.Default:
default:
var tempFolder = IOHelper.MapPath("~/App_Data/TEMP/DistCache");
distCacheFilePath = Path.Combine(tempFolder, fileName);
break;
}
//ensure the folder exists
var folder = Path.GetDirectoryName(distCacheFilePath);
if (folder == null)
throw new InvalidOperationException("The folder could not be determined for the file " + distCacheFilePath);
if (Directory.Exists(folder) == false)
Directory.CreateDirectory(folder);
return distCacheFilePath;
}
#endregion
+35 -4
View File
@@ -17,7 +17,10 @@ namespace Umbraco.Core.Sync
// need this public, parameter-less constructor so the web service messenger
// can de-serialize the instructions it receives
public RefreshInstruction()
{ }
{
//set default - this value is not used for reading after it's been deserialized, it's only used for persisting the instruction to the db
JsonIdCount = 1;
}
// need this public one so it can be de-serialized - used by the Json thing
// otherwise, should use GetInstructions(...)
@@ -29,12 +32,16 @@ namespace Umbraco.Core.Sync
IntId = intId;
JsonIds = jsonIds;
JsonPayload = jsonPayload;
//set default - this value is not used for reading after it's been deserialized, it's only used for persisting the instruction to the db
JsonIdCount = 1;
}
private RefreshInstruction(ICacheRefresher refresher, RefreshMethodType refreshType)
{
RefresherId = refresher.RefresherUniqueId;
RefreshType = refreshType;
//set default - this value is not used for reading after it's been deserialized, it's only used for persisting the instruction to the db
JsonIdCount = 1;
}
private RefreshInstruction(ICacheRefresher refresher, RefreshMethodType refreshType, Guid guidId)
@@ -49,9 +56,21 @@ namespace Umbraco.Core.Sync
IntId = intId;
}
private RefreshInstruction(ICacheRefresher refresher, RefreshMethodType refreshType, string json)
/// <summary>
/// A private constructor to create a new instance
/// </summary>
/// <param name="refresher"></param>
/// <param name="refreshType"></param>
/// <param name="json"></param>
/// <param name="idCount">
/// When the refresh method is <see cref="RefreshMethodType.RefreshByIds"/> we know how many Ids are being refreshed so we know the instruction
/// count which will be taken into account when we store this count in the database.
/// </param>
private RefreshInstruction(ICacheRefresher refresher, RefreshMethodType refreshType, string json, int idCount = 1)
: this(refresher, refreshType)
{
JsonIdCount = idCount;
if (refreshType == RefreshMethodType.RefreshByJson)
JsonPayload = json;
else
@@ -76,8 +95,12 @@ namespace Umbraco.Core.Sync
case MessageType.RefreshById:
if (idType == null)
throw new InvalidOperationException("Cannot refresh by id if idType is null.");
if (idType == typeof (int)) // bulk of ints is supported
return new[] { new RefreshInstruction(refresher, RefreshMethodType.RefreshByIds, JsonConvert.SerializeObject(ids.Cast<int>().ToArray())) };
if (idType == typeof(int))
{
// bulk of ints is supported
var intIds = ids.Cast<int>().ToArray();
return new[] { new RefreshInstruction(refresher, RefreshMethodType.RefreshByIds, JsonConvert.SerializeObject(intIds), intIds.Length) };
}
// else must be guids, bulk of guids is not supported, iterate
return ids.Select(x => new RefreshInstruction(refresher, RefreshMethodType.RefreshByGuid, (Guid) x));
@@ -120,6 +143,14 @@ namespace Umbraco.Core.Sync
/// </summary>
public string JsonIds { get; set; }
/// <summary>
/// Gets or sets the number of Ids contained in the JsonIds json value
/// </summary>
/// <remarks>
/// This is used to determine the instruction count per row
/// </remarks>
public int JsonIdCount { get; set; }
/// <summary>
/// Gets or sets the payload data value.
/// </summary>
+14
View File
@@ -98,6 +98,7 @@
<Compile Include="Attempt.cs" />
<Compile Include="AttemptOfTResult.cs" />
<Compile Include="AttemptOfTResultTStatus.cs" />
<Compile Include="Auditing\AuditEventHandler.cs" />
<Compile Include="Auditing\IdentityAuditEventArgs.cs" />
<Compile Include="BindingRedirects.cs" />
<Compile Include="ByteArrayExtensions.cs" />
@@ -140,10 +141,12 @@
<Compile Include="CodeAnnotations\UmbracoUdiTypeAttribute.cs" />
<Compile Include="CodeAnnotations\UmbracoWillObsoleteAttribute.cs" />
<Compile Include="Collections\CompositeIntStringKey.cs" />
<Compile Include="Collections\CompositeTypeTypeKey.cs" />
<Compile Include="Collections\ConcurrentHashSet.cs" />
<Compile Include="Collections\DeepCloneableList.cs" />
<Compile Include="Collections\ListCloneBehavior.cs" />
<Compile Include="Collections\ObservableDictionary.cs" />
<Compile Include="Collections\TypeList.cs" />
<Compile Include="Components\BootLoader.cs" />
<Compile Include="Components\Composition.cs" />
<Compile Include="Components\CompositionExtensions.cs" />
@@ -317,7 +320,11 @@
<Compile Include="Constants-Web.cs" />
<Compile Include="Constants.cs" />
<Compile Include="DisposableObjectSlim.cs" />
<Compile Include="Events\ExportedMemberEventArgs.cs" />
<Compile Include="Events\RolesEventArgs.cs" />
<Compile Include="Events\UserGroupWithUsers.cs" />
<Compile Include="KeyValuePairExtensions.cs" />
<Compile Include="Logging\RollingFileCleanupAppender.cs" />
<Compile Include="Migrations\MigrationBase_Extra.cs" />
<Compile Include="Migrations\Upgrade\V_8_0_0\DataTypeMigration.cs" />
<Compile Include="Migrations\Upgrade\V_8_0_0\PropertyEditorsMigration.cs" />
@@ -335,9 +342,14 @@
<Compile Include="Models\Entities\IDocumentEntitySlim.cs" />
<Compile Include="Models\Entities\ContentEntitySlim.cs" />
<Compile Include="Models\Entities\DocumentEntitySlim.cs" />
<Compile Include="Models\Membership\MemberExportModel.cs" />
<Compile Include="Models\Membership\MemberExportProperty.cs" />
<Compile Include="Models\PathValidationExtensions.cs" />
<Compile Include="Models\Entities\TreeEntityBase.cs" />
<Compile Include="Models\PropertyTagsExtensions.cs" />
<Compile Include="Persistence\Dtos\AuditEntryDto.cs" />
<Compile Include="Persistence\Dtos\ConsentDto.cs" />
<Compile Include="Persistence\Dtos\UserLoginDto.cs" />
<Compile Include="PropertyEditors\ConfigurationEditorOfTConfiguration.cs" />
<Compile Include="PropertyEditors\DataEditorAttribute.cs" />
<Compile Include="PropertyEditors\ConfigurationEditor.cs" />
@@ -1276,6 +1288,7 @@
<Compile Include="Security\IMembershipProviderPasswordHasher.cs" />
<Compile Include="Security\IUmbracoMemberTypeMembershipProvider.cs" />
<Compile Include="Security\IUserAwarePasswordHasher.cs" />
<Compile Include="Security\IUserSessionStore.cs" />
<Compile Include="Security\IUsersMembershipProvider.cs" />
<Compile Include="Security\MachineKeyGenerator.cs" />
<Compile Include="Security\MembershipProviderBase.cs" />
@@ -1283,6 +1296,7 @@
<Compile Include="Security\MembershipProviderPasswordHasher.cs" />
<Compile Include="Security\MembershipProviderPasswordValidator.cs" />
<Compile Include="Security\OwinExtensions.cs" />
<Compile Include="Security\SessionIdValidator.cs" />
<Compile Include="Security\UmbracoBackOfficeIdentity.cs" />
<Compile Include="Security\UmbracoEmailMessage.cs" />
<Compile Include="Security\UmbracoMembershipProviderBase.cs" />