Port v7@2aa0dfb2c5 - WIP
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
Reference in New Issue
Block a user