diff --git a/src/Umbraco.Core/Collections/CompositeTypeTypeKey.cs b/src/Umbraco.Core/Collections/CompositeTypeTypeKey.cs
index 1a4e7ae1a9..cc555afe55 100644
--- a/src/Umbraco.Core/Collections/CompositeTypeTypeKey.cs
+++ b/src/Umbraco.Core/Collections/CompositeTypeTypeKey.cs
@@ -10,7 +10,8 @@ namespace Umbraco.Core.Collections
///
/// Initializes a new instance of the struct.
///
- 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
///
/// Gets the first type.
///
- public Type Type1 { get; private set; }
+ public Type Type1 { get; }
///
/// Gets the second type.
///
- public Type Type2 { get; private set; }
+ public Type Type2 { get; }
///
public bool Equals(CompositeTypeTypeKey other)
@@ -35,7 +36,7 @@ namespace Umbraco.Core.Collections
///
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;
}
diff --git a/src/Umbraco.Core/Events/EventMessages.cs b/src/Umbraco.Core/Events/EventMessages.cs
index aac0d07e85..2df1911249 100644
--- a/src/Umbraco.Core/Events/EventMessages.cs
+++ b/src/Umbraco.Core/Events/EventMessages.cs
@@ -5,7 +5,7 @@ namespace Umbraco.Core.Events
///
/// Event messages collection
///
- public sealed class EventMessages : DisposableObject
+ public sealed class EventMessages : DisposableObjectSlim
{
private readonly List _msgs = new List();
diff --git a/src/Umbraco.Core/Events/ExportedMemberEventArgs.cs b/src/Umbraco.Core/Events/ExportedMemberEventArgs.cs
new file mode 100644
index 0000000000..9c91f3e5bd
--- /dev/null
+++ b/src/Umbraco.Core/Events/ExportedMemberEventArgs.cs
@@ -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;
+ }
+ }
+}
diff --git a/src/Umbraco.Core/Events/ImportPackageEventArgs.cs b/src/Umbraco.Core/Events/ImportPackageEventArgs.cs
index 6ad7b52806..7536b43e93 100644
--- a/src/Umbraco.Core/Events/ImportPackageEventArgs.cs
+++ b/src/Umbraco.Core/Events/ImportPackageEventArgs.cs
@@ -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; }
diff --git a/src/Umbraco.Core/Events/QueuingEventDispatcherBase.cs b/src/Umbraco.Core/Events/QueuingEventDispatcherBase.cs
index 2cc7046078..0283ac372e 100644
--- a/src/Umbraco.Core/Events/QueuingEventDispatcherBase.cs
+++ b/src/Umbraco.Core/Events/QueuingEventDispatcherBase.cs
@@ -78,255 +78,256 @@ namespace Umbraco.Core.Events
if (_events == null)
return Enumerable.Empty();
+ IReadOnlyList events;
switch (filter)
{
case EventDefinitionFilter.All:
- return FilterSupersededAndUpdateToLatestEntity(_events);
+ events = _events;
+ break;
case EventDefinitionFilter.FirstIn:
var l1 = new OrderedHashSet();
foreach (var e in _events)
l1.Add(e);
- return FilterSupersededAndUpdateToLatestEntity(l1);
+ events = l1;
+ break;
case EventDefinitionFilter.LastIn:
var l2 = new OrderedHashSet(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; }
}
- ///
- /// 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)
- ///
- ///
- ///
- private static IEnumerable FilterSupersededAndUpdateToLatestEntity(IReadOnlyList 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 FilterSupersededAndUpdateToLatestEntity(IReadOnlyList events)
{
- //used to keep the 'latest' entity and associated event definition data
- var allEntities = new List>();
-
- //tracks all CancellableObjectEventArgs instances in the events which is the only type of args we can work with
- var cancelableArgs = new List();
+ // keeps the 'latest' entity and associated event data
+ var entities = new List>();
+ // collects the event definitions
+ // collects the arguments in result, that require their entities to be updated
var result = new List();
+ var resultArgs = new List();
- //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(false).ToArray());
+ .ToDictionary(x => x, x => x.GetCustomAttributes(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();
- 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> allEntities, IEnumerable cancelableArgs)
+ // edits event args to use the latest instance of each entity
+ private static void UpdateToLatestEntities(IEnumerable> entities, IEnumerable 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(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;
}
}
}
- ///
- /// 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.
- ///
- ///
- ///
- ///
- ///
- private static bool IsFiltered(
- IEntity entity,
- EventDefinitionTypeData eventDef,
- List> 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> 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;
}
}
diff --git a/src/Umbraco.Core/Events/RolesEventArgs.cs b/src/Umbraco.Core/Events/RolesEventArgs.cs
new file mode 100644
index 0000000000..3104412f99
--- /dev/null
+++ b/src/Umbraco.Core/Events/RolesEventArgs.cs
@@ -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; }
+ }
+}
diff --git a/src/Umbraco.Core/Events/UserGroupWithUsers.cs b/src/Umbraco.Core/Events/UserGroupWithUsers.cs
new file mode 100644
index 0000000000..b69650d33f
--- /dev/null
+++ b/src/Umbraco.Core/Events/UserGroupWithUsers.cs
@@ -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; }
+ }
+}
diff --git a/src/Umbraco.Core/Logging/RollingFileCleanupAppender.cs b/src/Umbraco.Core/Logging/RollingFileCleanupAppender.cs
new file mode 100644
index 0000000000..6be2552296
--- /dev/null
+++ b/src/Umbraco.Core/Logging/RollingFileCleanupAppender.cs
@@ -0,0 +1,95 @@
+using System;
+using System.IO;
+using log4net.Appender;
+using log4net.Util;
+
+namespace Umbraco.Core.Logging
+{
+ ///
+ /// 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.:
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public class RollingFileCleanupAppender : RollingFileAppender
+ {
+ public int MaxLogFileDays { get; set; }
+ public string BaseFilePattern { get; set; }
+
+ ///
+ /// This override will delete logs older than the specified amount of days
+ ///
+ ///
+ ///
+ 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);
+ }
+ }
+ }
+ }
+}
diff --git a/src/Umbraco.Core/Models/Entities/BeingDirtyBase.cs b/src/Umbraco.Core/Models/Entities/BeingDirtyBase.cs
index 4e2279caaf..711b7c9b9f 100644
--- a/src/Umbraco.Core/Models/Entities/BeingDirtyBase.cs
+++ b/src/Umbraco.Core/Models/Entities/BeingDirtyBase.cs
@@ -87,6 +87,15 @@ namespace Umbraco.Core.Models.Entities
_currentChanges = null;
}
+ ///
+ public virtual IEnumerable GetWereDirtyProperties()
+ {
+ // ReSharper disable once MergeConditionalExpression
+ return _savedChanges == null
+ ? Enumerable.Empty()
+ : _savedChanges.Where(x => x.Value).Select(x => x.Key);
+ }
+
#endregion
#region Change Tracking
diff --git a/src/Umbraco.Core/Models/Entities/IRememberBeingDirty.cs b/src/Umbraco.Core/Models/Entities/IRememberBeingDirty.cs
index 75faba729b..e679b98b93 100644
--- a/src/Umbraco.Core/Models/Entities/IRememberBeingDirty.cs
+++ b/src/Umbraco.Core/Models/Entities/IRememberBeingDirty.cs
@@ -1,4 +1,6 @@
-namespace Umbraco.Core.Models.Entities
+using System.Collections.Generic;
+
+namespace Umbraco.Core.Models.Entities
{
///
/// Defines an entity that tracks property changes and can be dirty, and remembers
@@ -29,5 +31,10 @@
/// A value indicating whether to remember dirty properties.
/// When is true, dirty properties are saved so they can be checked with WasDirty.
void ResetDirtyProperties(bool rememberDirty);
+
+ ///
+ /// Gets properties that were dirty.
+ ///
+ IEnumerable GetWereDirtyProperties();
}
}
diff --git a/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs b/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs
index 3de0d11de1..eab9e21013 100644
--- a/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs
+++ b/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs
@@ -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>();
+ _roles.CollectionChanged += _roles_CollectionChanged;
}
///
diff --git a/src/Umbraco.Core/Models/Identity/IdentityProfile.cs b/src/Umbraco.Core/Models/Identity/IdentityProfile.cs
index 46baad29b7..eef2a17aa5 100644
--- a/src/Umbraco.Core/Models/Identity/IdentityProfile.cs
+++ b/src/Umbraco.Core/Models/Identity/IdentityProfile.cs
@@ -46,20 +46,6 @@ namespace Umbraco.Core.Models.Identity
dest.ResetDirtyProperties(true);
dest.EnableChangeTracking();
});
-
- CreateMap()
- .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)
diff --git a/src/Umbraco.Core/Models/Membership/EntityPermissionCollection.cs b/src/Umbraco.Core/Models/Membership/EntityPermissionCollection.cs
index a2f70ef5ef..12e874d5d7 100644
--- a/src/Umbraco.Core/Models/Membership/EntityPermissionCollection.cs
+++ b/src/Umbraco.Core/Models/Membership/EntityPermissionCollection.cs
@@ -17,7 +17,30 @@ namespace Umbraco.Core.Models.Membership
}
///
- /// Returns the aggregate permissions in the permission set
+ /// Returns the aggregate permissions in the permission set for a single node
+ ///
+ ///
+ ///
+ /// This value is only calculated once per node
+ ///
+ public IEnumerable GetAllPermissions(int entityId)
+ {
+ if (_aggregateNodePermissions == null)
+ _aggregateNodePermissions = new Dictionary();
+
+ 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 _aggregateNodePermissions;
+
+ ///
+ /// Returns the aggregate permissions in the permission set for all nodes
///
///
///
diff --git a/src/Umbraco.Core/Models/Membership/IUser.cs b/src/Umbraco.Core/Models/Membership/IUser.cs
index dec0095243..8219af17b9 100644
--- a/src/Umbraco.Core/Models/Membership/IUser.cs
+++ b/src/Umbraco.Core/Models/Membership/IUser.cs
@@ -58,6 +58,11 @@ namespace Umbraco.Core.Models.Membership
///
/// Will hold the media file system relative path of the users custom avatar if they uploaded one
///
- string Avatar { get; set; }
+ string Avatar { get; set; }
+
+ ///
+ /// A Json blob stored for recording tour data for a user
+ ///
+ string TourData { get; set; }
}
-}
+}
\ No newline at end of file
diff --git a/src/Umbraco.Core/Models/Membership/IUserGroup.cs b/src/Umbraco.Core/Models/Membership/IUserGroup.cs
index de5842df61..508eb015ed 100644
--- a/src/Umbraco.Core/Models/Membership/IUserGroup.cs
+++ b/src/Umbraco.Core/Models/Membership/IUserGroup.cs
@@ -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; }
diff --git a/src/Umbraco.Core/Models/Membership/MemberExportModel.cs b/src/Umbraco.Core/Models/Membership/MemberExportModel.cs
new file mode 100644
index 0000000000..7153d380b4
--- /dev/null
+++ b/src/Umbraco.Core/Models/Membership/MemberExportModel.cs
@@ -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 Groups { get; set; }
+ public string ContentTypeAlias { get; set; }
+ public DateTime CreateDate { get; set; }
+ public DateTime UpdateDate { get; set; }
+ public List Properties { get; set; }
+ }
+}
diff --git a/src/Umbraco.Core/Models/Membership/MemberExportProperty.cs b/src/Umbraco.Core/Models/Membership/MemberExportProperty.cs
new file mode 100644
index 0000000000..546d9255ea
--- /dev/null
+++ b/src/Umbraco.Core/Models/Membership/MemberExportProperty.cs
@@ -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; }
+ }
+}
diff --git a/src/Umbraco.Core/Models/Membership/User.cs b/src/Umbraco.Core/Models/Membership/User.cs
index de410ffb9a..2dd750a353 100644
--- a/src/Umbraco.Core/Models/Membership/User.cs
+++ b/src/Umbraco.Core/Models/Membership/User.cs
@@ -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(x => x.SecurityStamp);
public readonly PropertyInfo AvatarSelector = ExpressionHelper.GetPropertyInfo(x => x.Avatar);
+ public readonly PropertyInfo TourDataSelector = ExpressionHelper.GetPropertyInfo(x => x.TourData);
public readonly PropertyInfo SessionTimeoutSelector = ExpressionHelper.GetPropertyInfo(x => x.SessionTimeout);
public readonly PropertyInfo StartContentIdSelector = ExpressionHelper.GetPropertyInfo(x => x.StartContentIds);
public readonly PropertyInfo StartMediaIdSelector = ExpressionHelper.GetPropertyInfo(x => x.StartMediaIds);
@@ -467,6 +469,16 @@ namespace Umbraco.Core.Models.Membership
set { SetPropertyValueAndDetectChanges(value, ref _avatar, Ps.Value.AvatarSelector); }
}
+ ///
+ /// A Json blob stored for recording tour data for a user
+ ///
+ [DataMember]
+ public string TourData
+ {
+ get { return _tourData; }
+ set { SetPropertyValueAndDetectChanges(value, ref _tourData, Ps.Value.TourDataSelector); }
+ }
+
///
/// Gets or sets the session timeout.
///
@@ -671,5 +683,6 @@ namespace Umbraco.Core.Models.Membership
return _user.GetHashCode();
}
}
+
}
}
diff --git a/src/Umbraco.Core/Models/Membership/UserGroup.cs b/src/Umbraco.Core/Models/Membership/UserGroup.cs
index ccd60f5861..db21c78438 100644
--- a/src/Umbraco.Core/Models/Membership/UserGroup.cs
+++ b/src/Umbraco.Core/Models/Membership/UserGroup.cs
@@ -12,7 +12,7 @@ namespace Umbraco.Core.Models.Membership
///
[Serializable]
[DataContract(IsReference = true)]
- internal class UserGroup : EntityBase, IUserGroup, IReadOnlyUserGroup
+ public class UserGroup : EntityBase, IUserGroup, IReadOnlyUserGroup
{
private int? _startContentId;
private int? _startMediaId;
diff --git a/src/Umbraco.Core/ObjectExtensions.cs b/src/Umbraco.Core/ObjectExtensions.cs
index 1c928730e2..4f48322a8f 100644
--- a/src/Umbraco.Core/ObjectExtensions.cs
+++ b/src/Umbraco.Core/ObjectExtensions.cs
@@ -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
///
public static class ObjectExtensions
{
- private static readonly ConcurrentDictionary> ToObjectTypes
- = new ConcurrentDictionary>();
+ private static readonly ConcurrentDictionary> ToObjectTypes = new ConcurrentDictionary>();
+ private static readonly ConcurrentDictionary NullableGenericCache = new ConcurrentDictionary();
+ private static readonly ConcurrentDictionary InputTypeConverterCache = new ConcurrentDictionary();
+ private static readonly ConcurrentDictionary DestinationTypeConverterCache = new ConcurrentDictionary();
+ private static readonly ConcurrentDictionary AssignableTypeCache = new ConcurrentDictionary();
+ private static readonly ConcurrentDictionary BoolConvertCache = new ConcurrentDictionary();
+
+ private static readonly char[] NumberDecimalSeparatorsToNormalize = { '.', ',' };
+ private static readonly CustomBooleanTypeConverter CustomBooleanTypeConverter = new CustomBooleanTypeConverter();
//private static readonly ConcurrentDictionary> ObjectFactoryCache = new ConcurrentDictionary>();
@@ -40,8 +47,8 @@ namespace Umbraco.Core
///
public static void DisposeIfDisposable(this object input)
{
- var disposable = input as IDisposable;
- if (disposable != null) disposable.Dispose();
+ if (input is IDisposable disposable)
+ disposable.Dispose();
}
///
@@ -53,347 +60,335 @@ namespace Umbraco.Core
///
internal static T SafeCast(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;
}
///
- /// Tries to convert the input object to the output type using TypeConverters
+ /// Attempts to convert the input object to the output type.
///
- ///
- ///
- ///
+ /// This code is an optimized version of the original Umbraco method
+ /// The type to convert to
+ /// The input.
+ /// The
public static Attempt TryConvertTo(this object input)
{
var result = TryConvertTo(input, typeof(T));
- if (result.Success == false)
+
+ if (result.Success)
+ return Attempt.Succeed((T)result.Result);
+
+ // just try to cast
+ try
{
- //just try a straight up conversion
- try
- {
- var converted = (T) input;
- return Attempt.Succeed(converted);
- }
- catch (Exception e)
- {
- return Attempt.Fail(e);
- }
+ return Attempt.Succeed((T)input);
+ }
+ catch (Exception e)
+ {
+ return Attempt.Fail(e);
}
- return result.Success == false ? Attempt.Fail() : Attempt.Succeed((T)result.Result);
}
///
- /// 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 .
+ /// Attempts to convert the input object to the output type.
///
+ /// This code is an optimized version of the original Umbraco method
/// The input.
- /// Type of the destination.
- ///
- public static Attempt
public string JsonIds { get; set; }
+ ///
+ /// Gets or sets the number of Ids contained in the JsonIds json value
+ ///
+ ///
+ /// This is used to determine the instruction count per row
+ ///
+ public int JsonIdCount { get; set; }
+
///
/// Gets or sets the payload data value.
///
diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj
index 434f25638e..7ffdaa909c 100644
--- a/src/Umbraco.Core/Umbraco.Core.csproj
+++ b/src/Umbraco.Core/Umbraco.Core.csproj
@@ -98,6 +98,7 @@
+
@@ -140,10 +141,12 @@
+
+
@@ -317,7 +320,11 @@
+
+
+
+
@@ -335,9 +342,14 @@
+
+
+
+
+
@@ -1276,6 +1288,7 @@
+
@@ -1283,6 +1296,7 @@
+